1// Copyright (c) 2013 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5#include "chrome/test/chromedriver/chrome/navigation_tracker.h"
6
7#include "base/strings/stringprintf.h"
8#include "base/values.h"
9#include "chrome/test/chromedriver/chrome/browser_info.h"
10#include "chrome/test/chromedriver/chrome/devtools_client.h"
11#include "chrome/test/chromedriver/chrome/status.h"
12
13NavigationTracker::NavigationTracker(DevToolsClient* client,
14                                     const BrowserInfo* browser_info)
15    : client_(client),
16      loading_state_(kUnknown),
17      browser_info_(browser_info) {
18  client_->AddListener(this);
19}
20
21NavigationTracker::NavigationTracker(DevToolsClient* client,
22                                     LoadingState known_state,
23                                     const BrowserInfo* browser_info)
24    : client_(client),
25      loading_state_(known_state),
26      browser_info_(browser_info) {
27  client_->AddListener(this);
28}
29
30NavigationTracker::~NavigationTracker() {}
31
32Status NavigationTracker::IsPendingNavigation(const std::string& frame_id,
33                                              bool* is_pending) {
34  if (loading_state_ == kUnknown) {
35    // If the loading state is unknown (which happens after first connecting),
36    // force loading to start and set the state to loading. This will
37    // cause a frame start event to be received, and the frame stop event
38    // will not be received until all frames are loaded.
39    // Loading is forced to start by attaching a temporary iframe.
40    // Forcing loading to start is not necessary if the main frame is not yet
41    // loaded.
42    const char kStartLoadingIfMainFrameNotLoading[] =
43       "var isLoaded = document.readyState == 'complete' ||"
44       "    document.readyState == 'interactive';"
45       "if (isLoaded) {"
46       "  var frame = document.createElement('iframe');"
47       "  frame.src = 'about:blank';"
48       "  document.body.appendChild(frame);"
49       "  window.setTimeout(function() {"
50       "    document.body.removeChild(frame);"
51       "  }, 0);"
52       "}";
53    base::DictionaryValue params;
54    params.SetString("expression", kStartLoadingIfMainFrameNotLoading);
55    scoped_ptr<base::DictionaryValue> result;
56    Status status = client_->SendCommandAndGetResult(
57        "Runtime.evaluate", params, &result);
58    if (status.IsError())
59      return Status(kUnknownError, "cannot determine loading status", status);
60
61    // Between the time the JavaScript is evaluated and SendCommandAndGetResult
62    // returns, OnEvent may have received info about the loading state.
63    // This is only possible during a nested command. Only set the loading state
64    // if the loading state is still unknown.
65    if (loading_state_ == kUnknown)
66      loading_state_ = kLoading;
67  }
68  *is_pending = loading_state_ == kLoading;
69  if (frame_id.empty())
70    *is_pending |= scheduled_frame_set_.size() > 0;
71  else
72    *is_pending |= scheduled_frame_set_.count(frame_id) > 0;
73  return Status(kOk);
74}
75
76Status NavigationTracker::OnConnected(DevToolsClient* client) {
77  ResetLoadingState(kUnknown);
78
79  // Enable page domain notifications to allow tracking navigation state.
80  base::DictionaryValue empty_params;
81  return client_->SendCommand("Page.enable", empty_params);
82}
83
84Status NavigationTracker::OnEvent(DevToolsClient* client,
85                                  const std::string& method,
86                                  const base::DictionaryValue& params) {
87  if (method == "Page.frameStartedLoading") {
88    std::string frame_id;
89    if (!params.GetString("frameId", &frame_id))
90      return Status(kUnknownError, "missing or invalid 'frameId'");
91    pending_frame_set_.insert(frame_id);
92    loading_state_ = kLoading;
93  } else if (method == "Page.frameStoppedLoading") {
94    // Versions of Blink before revision 170248 sent a single
95    // Page.frameStoppedLoading event per page, but 170248 and newer revisions
96    // only send one event for each frame on the page.
97    //
98    // This change was rolled into the Chromium tree in revision 260203.
99    // Versions of Chrome with build number 1916 and earlier do not contain this
100    // change.
101    bool expecting_single_stop_event = false;
102
103    if (browser_info_->browser_name == "chrome") {
104      // If we're talking to a version of Chrome with an old build number, we
105      // are using a branched version of Blink which does not contain 170248
106      // (even if blink_revision > 170248).
107      expecting_single_stop_event = browser_info_->build_no <= 1916;
108    } else {
109      // If we're talking to a non-Chrome embedder (e.g. Content Shell, Android
110      // WebView), assume that the browser does not use a branched version of
111      // Blink.
112      expecting_single_stop_event = browser_info_->blink_revision < 170248;
113    }
114
115    std::string frame_id;
116    if (!params.GetString("frameId", &frame_id))
117      return Status(kUnknownError, "missing or invalid 'frameId'");
118
119    pending_frame_set_.erase(frame_id);
120
121    if (pending_frame_set_.empty() || expecting_single_stop_event) {
122      pending_frame_set_.clear();
123      loading_state_ = kNotLoading;
124    }
125  } else if (method == "Page.frameScheduledNavigation") {
126    double delay;
127    if (!params.GetDouble("delay", &delay))
128      return Status(kUnknownError, "missing or invalid 'delay'");
129
130    std::string frame_id;
131    if (!params.GetString("frameId", &frame_id))
132      return Status(kUnknownError, "missing or invalid 'frameId'");
133
134    // WebDriver spec says to ignore redirects over 1s.
135    if (delay > 1)
136      return Status(kOk);
137    scheduled_frame_set_.insert(frame_id);
138  } else if (method == "Page.frameClearedScheduledNavigation") {
139    std::string frame_id;
140    if (!params.GetString("frameId", &frame_id))
141      return Status(kUnknownError, "missing or invalid 'frameId'");
142
143    scheduled_frame_set_.erase(frame_id);
144  } else if (method == "Page.frameNavigated") {
145    // Note: in some cases Page.frameNavigated may be received for subframes
146    // without a frameStoppedLoading (for example cnn.com).
147
148    // If the main frame just navigated, discard any pending scheduled
149    // navigations. For some reasons at times the cleared event is not
150    // received when navigating.
151    // See crbug.com/180742.
152    const base::Value* unused_value;
153    if (!params.Get("frame.parentId", &unused_value)) {
154      pending_frame_set_.clear();
155      scheduled_frame_set_.clear();
156    }
157  } else if (method == "Inspector.targetCrashed") {
158    ResetLoadingState(kNotLoading);
159  }
160  return Status(kOk);
161}
162
163Status NavigationTracker::OnCommandSuccess(DevToolsClient* client,
164                                           const std::string& method) {
165  if (method == "Page.navigate" && loading_state_ != kLoading) {
166    // At this point the browser has initiated the navigation, but besides that,
167    // it is unknown what will happen.
168    //
169    // There are a few cases (perhaps more):
170    // 1 The RenderFrameHost has already queued FrameMsg_Navigate and loading
171    //   will start shortly.
172    // 2 The RenderFrameHost has already queued FrameMsg_Navigate and loading
173    //   will never start because it is just an in-page fragment navigation.
174    // 3 The RenderFrameHost is suspended and hasn't queued FrameMsg_Navigate
175    //   yet. This happens for cross-site navigations. The RenderFrameHost
176    //   will not queue FrameMsg_Navigate until it is ready to unload the
177    //   previous page (after running unload handlers and such).
178    // TODO(nasko): Revisit case 3, since now unload handlers are run in the
179    // background. http://crbug.com/323528.
180    //
181    // To determine whether a load is expected, do a round trip to the
182    // renderer to ask what the URL is.
183    // If case #1, by the time the command returns, the frame started to load
184    // event will also have been received, since the DevTools command will
185    // be queued behind FrameMsg_Navigate.
186    // If case #2, by the time the command returns, the navigation will
187    // have already happened, although no frame start/stop events will have
188    // been received.
189    // If case #3, the URL will be blank if the navigation hasn't been started
190    // yet. In that case, expect a load to happen in the future.
191    loading_state_ = kUnknown;
192    base::DictionaryValue params;
193    params.SetString("expression", "document.URL");
194    scoped_ptr<base::DictionaryValue> result;
195    Status status = client_->SendCommandAndGetResult(
196        "Runtime.evaluate", params, &result);
197    std::string url;
198    if (status.IsError() || !result->GetString("result.value", &url))
199      return Status(kUnknownError, "cannot determine loading status", status);
200    if (loading_state_ == kUnknown && url.empty())
201      loading_state_ = kLoading;
202  }
203  return Status(kOk);
204}
205
206void NavigationTracker::ResetLoadingState(LoadingState loading_state) {
207  loading_state_ = loading_state;
208  pending_frame_set_.clear();
209  scheduled_frame_set_.clear();
210}
211