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