• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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