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