blob: aad70e5db6e56938c2371514a75e622820cd9090 [file] [log] [blame]
// Copyright (c) 2013 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/test/chromedriver/chrome/navigation_tracker.h"
#include "base/strings/stringprintf.h"
#include "base/values.h"
#include "chrome/test/chromedriver/chrome/devtools_client.h"
#include "chrome/test/chromedriver/chrome/status.h"
#include "chrome/test/chromedriver/chrome/version.h"
NavigationTracker::NavigationTracker(DevToolsClient* client,
const BrowserInfo* browser_info)
: client_(client),
loading_state_(kUnknown),
browser_info_(browser_info),
num_frames_pending_(0) {
client_->AddListener(this);
}
NavigationTracker::NavigationTracker(DevToolsClient* client,
LoadingState known_state,
const BrowserInfo* browser_info)
: client_(client),
loading_state_(known_state),
browser_info_(browser_info),
num_frames_pending_(0) {
client_->AddListener(this);
}
NavigationTracker::~NavigationTracker() {}
Status NavigationTracker::IsPendingNavigation(const std::string& frame_id,
bool* is_pending) {
if (loading_state_ == kUnknown) {
// If the loading state is unknown (which happens after first connecting),
// force loading to start and set the state to loading. This will
// cause a frame start event to be received, and the frame stop event
// will not be received until all frames are loaded.
// Loading is forced to start by attaching a temporary iframe.
// Forcing loading to start is not necessary if the main frame is not yet
// loaded.
const char kStartLoadingIfMainFrameNotLoading[] =
"var isLoaded = document.readyState == 'complete' ||"
" document.readyState == 'interactive';"
"if (isLoaded) {"
" var frame = document.createElement('iframe');"
" frame.src = 'about:blank';"
" document.body.appendChild(frame);"
" window.setTimeout(function() {"
" document.body.removeChild(frame);"
" }, 0);"
"}";
base::DictionaryValue params;
params.SetString("expression", kStartLoadingIfMainFrameNotLoading);
scoped_ptr<base::DictionaryValue> result;
Status status = client_->SendCommandAndGetResult(
"Runtime.evaluate", params, &result);
if (status.IsError())
return Status(kUnknownError, "cannot determine loading status", status);
// Between the time the JavaScript is evaluated and SendCommandAndGetResult
// returns, OnEvent may have received info about the loading state.
// This is only possible during a nested command. Only set the loading state
// if the loading state is still unknown.
if (loading_state_ == kUnknown)
loading_state_ = kLoading;
}
*is_pending = loading_state_ == kLoading;
if (frame_id.empty())
*is_pending |= scheduled_frame_set_.size() > 0;
else
*is_pending |= scheduled_frame_set_.count(frame_id) > 0;
return Status(kOk);
}
Status NavigationTracker::OnConnected(DevToolsClient* client) {
ResetLoadingState(kUnknown);
// Enable page domain notifications to allow tracking navigation state.
base::DictionaryValue empty_params;
return client_->SendCommand("Page.enable", empty_params);
}
Status NavigationTracker::OnEvent(DevToolsClient* client,
const std::string& method,
const base::DictionaryValue& params) {
if (method == "Page.frameStartedLoading") {
loading_state_ = kLoading;
num_frames_pending_++;
} else if (method == "Page.frameStoppedLoading") {
// Versions of Blink before revision 170248 sent a single
// Page.frameStoppedLoading event per page, but 170248 and newer revisions
// only send one event for each frame on the page.
//
// This change was rolled into the Chromium tree in revision 260203.
// Versions of Chrome with build number 1916 and earlier do not contain this
// change.
bool expecting_single_stop_event = false;
if (browser_info_->browser_name == "chrome") {
// If we're talking to a version of Chrome with an old build number, we
// are using a branched version of Blink which does not contain 170248
// (even if blink_revision > 170248).
expecting_single_stop_event = browser_info_->build_no <= 1916;
} else {
// If we're talking to a non-Chrome embedder (e.g. Content Shell, Android
// WebView), assume that the browser does not use a branched version of
// Blink.
expecting_single_stop_event = browser_info_->blink_revision < 170248;
}
num_frames_pending_--;
if (num_frames_pending_ <= 0 || expecting_single_stop_event) {
num_frames_pending_ = 0;
loading_state_ = kNotLoading;
}
} else if (method == "Page.frameScheduledNavigation") {
double delay;
if (!params.GetDouble("delay", &delay))
return Status(kUnknownError, "missing or invalid 'delay'");
std::string frame_id;
if (!params.GetString("frameId", &frame_id))
return Status(kUnknownError, "missing or invalid 'frameId'");
// WebDriver spec says to ignore redirects over 1s.
if (delay > 1)
return Status(kOk);
scheduled_frame_set_.insert(frame_id);
} else if (method == "Page.frameClearedScheduledNavigation") {
std::string frame_id;
if (!params.GetString("frameId", &frame_id))
return Status(kUnknownError, "missing or invalid 'frameId'");
scheduled_frame_set_.erase(frame_id);
} else if (method == "Page.frameNavigated") {
// Note: in some cases Page.frameNavigated may be received for subframes
// without a frameStoppedLoading (for example cnn.com).
// If the main frame just navigated, discard any pending scheduled
// navigations. For some reasons at times the cleared event is not
// received when navigating.
// See crbug.com/180742.
const base::Value* unused_value;
if (!params.Get("frame.parentId", &unused_value)) {
num_frames_pending_ = 0;
scheduled_frame_set_.clear();
}
} else if (method == "Inspector.targetCrashed") {
ResetLoadingState(kNotLoading);
}
return Status(kOk);
}
Status NavigationTracker::OnCommandSuccess(DevToolsClient* client,
const std::string& method) {
if (method == "Page.navigate" && loading_state_ != kLoading) {
// At this point the browser has initiated the navigation, but besides that,
// it is unknown what will happen.
//
// There are a few cases (perhaps more):
// 1 The RenderFrameHost has already queued FrameMsg_Navigate and loading
// will start shortly.
// 2 The RenderFrameHost has already queued FrameMsg_Navigate and loading
// will never start because it is just an in-page fragment navigation.
// 3 The RenderFrameHost is suspended and hasn't queued FrameMsg_Navigate
// yet. This happens for cross-site navigations. The RenderFrameHost
// will not queue FrameMsg_Navigate until it is ready to unload the
// previous page (after running unload handlers and such).
// TODO(nasko): Revisit case 3, since now unload handlers are run in the
// background. http://crbug.com/323528.
//
// To determine whether a load is expected, do a round trip to the
// renderer to ask what the URL is.
// If case #1, by the time the command returns, the frame started to load
// event will also have been received, since the DevTools command will
// be queued behind FrameMsg_Navigate.
// If case #2, by the time the command returns, the navigation will
// have already happened, although no frame start/stop events will have
// been received.
// If case #3, the URL will be blank if the navigation hasn't been started
// yet. In that case, expect a load to happen in the future.
loading_state_ = kUnknown;
base::DictionaryValue params;
params.SetString("expression", "document.URL");
scoped_ptr<base::DictionaryValue> result;
Status status = client_->SendCommandAndGetResult(
"Runtime.evaluate", params, &result);
std::string url;
if (status.IsError() || !result->GetString("result.value", &url))
return Status(kUnknownError, "cannot determine loading status", status);
if (loading_state_ == kUnknown && url.empty())
loading_state_ = kLoading;
}
return Status(kOk);
}
void NavigationTracker::ResetLoadingState(LoadingState loading_state) {
loading_state_ = loading_state;
num_frames_pending_ = 0;
scheduled_frame_set_.clear();
}