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.
5 #include "chrome/test/chromedriver/chrome/navigation_tracker.h"
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"
13 NavigationTracker::NavigationTracker(DevToolsClient* client,
14 const BrowserInfo* browser_info)
16 loading_state_(kUnknown),
17 browser_info_(browser_info) {
18 client_->AddListener(this);
21 NavigationTracker::NavigationTracker(DevToolsClient* client,
22 LoadingState known_state,
23 const BrowserInfo* browser_info)
25 loading_state_(known_state),
26 browser_info_(browser_info) {
27 client_->AddListener(this);
30 NavigationTracker::~NavigationTracker() {}
32 Status NavigationTracker::IsPendingNavigation(const std::string& frame_id,
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
42 const char kStartLoadingIfMainFrameNotLoading[] =
43 "var isLoaded = document.readyState == 'complete' ||"
44 " document.readyState == 'interactive';"
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);"
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);
59 return Status(kUnknownError, "cannot determine loading status", status);
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;
68 *is_pending = loading_state_ == kLoading;
70 *is_pending |= scheduled_frame_set_.size() > 0;
72 *is_pending |= scheduled_frame_set_.count(frame_id) > 0;
76 Status NavigationTracker::OnConnected(DevToolsClient* client) {
77 ResetLoadingState(kUnknown);
79 // Enable page domain notifications to allow tracking navigation state.
80 base::DictionaryValue empty_params;
81 return client_->SendCommand("Page.enable", empty_params);
84 Status NavigationTracker::OnEvent(DevToolsClient* client,
85 const std::string& method,
86 const base::DictionaryValue& params) {
87 if (method == "Page.frameStartedLoading") {
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.
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
101 bool expecting_single_stop_event = false;
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;
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
112 expecting_single_stop_event = browser_info_->blink_revision < 170248;
115 std::string frame_id;
116 if (!params.GetString("frameId", &frame_id))
117 return Status(kUnknownError, "missing or invalid 'frameId'");
119 pending_frame_set_.erase(frame_id);
121 if (pending_frame_set_.empty() || expecting_single_stop_event) {
122 pending_frame_set_.clear();
123 loading_state_ = kNotLoading;
125 } else if (method == "Page.frameScheduledNavigation") {
127 if (!params.GetDouble("delay", &delay))
128 return Status(kUnknownError, "missing or invalid 'delay'");
130 std::string frame_id;
131 if (!params.GetString("frameId", &frame_id))
132 return Status(kUnknownError, "missing or invalid 'frameId'");
134 // WebDriver spec says to ignore redirects over 1s.
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'");
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).
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();
157 } else if (method == "Inspector.targetCrashed") {
158 ResetLoadingState(kNotLoading);
163 Status 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.
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.
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
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);
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;
206 void NavigationTracker::ResetLoadingState(LoadingState loading_state) {
207 loading_state_ = loading_state;
208 pending_frame_set_.clear();
209 scheduled_frame_set_.clear();