blob: 230423d0fd66b1c94816b8673c07acbbc69331d6 [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/browser_info.h"
#include "chrome/test/chromedriver/chrome/devtools_client.h"
#include "chrome/test/chromedriver/chrome/javascript_dialog_manager.h"
#include "chrome/test/chromedriver/chrome/status.h"
#include "chrome/test/chromedriver/net/timeout.h"
namespace {
const char kDummyFrameName[] = "chromedriver dummy frame";
const char kDummyFrameUrl[] = "about:blank";
const char kUnreachableWebDataURL[] = "chrome-error://chromewebdata/";
const char kAutomationExtensionBackgroundPage[] =
"chrome-extension://aapnijgdinlhnhlmodcfapnahmbfebeb/"
"_generated_background_page.html";
Status MakeNavigationCheckFailedStatus(Status command_status) {
if (command_status.code() == kUnexpectedAlertOpen)
return Status(kUnexpectedAlertOpen);
else if (command_status.code() == kTimeout)
return Status(kTimeout);
else
return Status(kUnknownError, "cannot determine loading status",
command_status);
}
} // namespace
NavigationTracker::NavigationTracker(
DevToolsClient* client,
const BrowserInfo* browser_info,
const JavaScriptDialogManager* dialog_manager)
: client_(client),
loading_state_(kUnknown),
dialog_manager_(dialog_manager),
dummy_execution_context_id_(0),
load_event_fired_(true),
timed_out_(false) {
client_->AddListener(this);
}
NavigationTracker::NavigationTracker(
DevToolsClient* client,
LoadingState known_state,
const BrowserInfo* browser_info,
const JavaScriptDialogManager* dialog_manager)
: client_(client),
loading_state_(known_state),
dialog_manager_(dialog_manager),
dummy_execution_context_id_(0),
load_event_fired_(true),
timed_out_(false) {
client_->AddListener(this);
}
NavigationTracker::~NavigationTracker() {}
Status NavigationTracker::IsPendingNavigation(const std::string& frame_id,
const Timeout* timeout,
bool* is_pending) {
if (dialog_manager_->IsDialogOpen()) {
// The render process is paused while modal dialogs are open, so
// Runtime.evaluate will block and time out if we attempt to call it. In
// this case we can consider the page to have loaded, so that we return
// control back to the test and let it dismiss the dialog.
*is_pending = false;
return Status(kOk);
}
// Some DevTools commands (e.g. Input.dispatchMouseEvent) are handled in the
// browser process, and may cause the renderer process to start a new
// navigation. We need to call Runtime.evaluate to force a roundtrip to the
// renderer process, and make sure that we notice any pending navigations
// (see crbug.com/524079).
base::DictionaryValue params;
params.SetString("expression", "1");
std::unique_ptr<base::DictionaryValue> result;
Status status = client_->SendCommandAndGetResultWithTimeout(
"Runtime.evaluate", params, timeout, &result);
int value = 0;
if (status.code() == kDisconnected) {
// If we receive a kDisconnected status code from Runtime.evaluate, don't
// wait for pending navigations to complete, since we won't see any more
// events from it until we reconnect.
*is_pending = false;
return Status(kOk);
} else if (status.code() == kUnexpectedAlertOpen) {
// The JS event loop is paused while modal dialogs are open, so return
// control to the test so that it can dismiss the dialog.
*is_pending = false;
return Status(kOk);
} else if (status.IsError() ||
!result->GetInteger("result.value", &value) ||
value != 1) {
return MakeNavigationCheckFailedStatus(status);
}
if (loading_state_ == kUnknown) {
// In the case that a http request is sent to server to fetch the page
// content and the server hasn't responded at all, a dummy page is created
// for the new window. In such case, the baseURL will be 'about:blank'.
base::DictionaryValue empty_params;
std::unique_ptr<base::DictionaryValue> result;
Status status = client_->SendCommandAndGetResultWithTimeout(
"DOM.getDocument", empty_params, timeout, &result);
std::string base_url;
std::string doc_url;
if (status.IsError() || !result->GetString("root.baseURL", &base_url) ||
!result->GetString("root.documentURL", &doc_url))
return MakeNavigationCheckFailedStatus(status);
if (doc_url != "about:blank" && base_url == "about:blank") {
*is_pending = true;
loading_state_ = kLoading;
return Status(kOk);
}
// If we're loading the ChromeDriver automation extension background page,
// look for a known function to determine the loading status.
if (base_url == kAutomationExtensionBackgroundPage) {
bool function_exists = false;
status = CheckFunctionExists(timeout, &function_exists);
if (status.IsError())
return MakeNavigationCheckFailedStatus(status);
loading_state_ = function_exists ? kNotLoading : kLoading;
}
// 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.
const std::string kStartLoadingIfMainFrameNotLoading = base::StringPrintf(
"var frame = document.createElement('iframe');"
"frame.name = '%s';"
"frame.src = '%s';"
"document.body.appendChild(frame);"
"window.setTimeout(function() {"
" document.body.removeChild(frame);"
"}, 0);",
kDummyFrameName, kDummyFrameUrl);
base::DictionaryValue params;
params.SetString("expression", kStartLoadingIfMainFrameNotLoading);
status = client_->SendCommandAndGetResultWithTimeout(
"Runtime.evaluate", params, timeout, &result);
if (status.IsError())
return MakeNavigationCheckFailedStatus(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;
*is_pending |= pending_frame_set_.size() > 0;
} else {
*is_pending |= scheduled_frame_set_.count(frame_id) > 0;
*is_pending |= pending_frame_set_.count(frame_id) > 0;
}
return Status(kOk);
}
Status NavigationTracker::CheckFunctionExists(const Timeout* timeout,
bool* exists) {
base::DictionaryValue params;
params.SetString("expression", "typeof(getWindowInfo)");
std::unique_ptr<base::DictionaryValue> result;
Status status = client_->SendCommandAndGetResultWithTimeout(
"Runtime.evaluate", params, timeout, &result);
std::string type;
if (status.IsError() || !result->GetString("result.value", &type))
return MakeNavigationCheckFailedStatus(status);
*exists = type == "function";
return Status(kOk);
}
void NavigationTracker::set_timed_out(bool timed_out) {
timed_out_ = timed_out;
}
bool NavigationTracker::IsNonBlocking() const {
return false;
}
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") {
std::string frame_id;
if (!params.GetString("frameId", &frame_id))
return Status(kUnknownError, "missing or invalid 'frameId'");
pending_frame_set_.insert(frame_id);
loading_state_ = kLoading;
} else if (method == "Page.frameStoppedLoading") {
std::string frame_id;
if (!params.GetString("frameId", &frame_id))
return Status(kUnknownError, "missing or invalid 'frameId'");
scheduled_frame_set_.erase(frame_id);
pending_frame_set_.erase(frame_id);
if (pending_frame_set_.empty() &&
(load_event_fired_ || timed_out_ || execution_context_set_.empty()))
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);
// A normal Page.loadEventFired event isn't expected after a scheduled
// navigation, so set load_event_fired_ flag.
load_event_fired_ = true;
} 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).
const base::Value* unused_value;
if (!params.Get("frame.parentId", &unused_value)) {
// Discard pending and scheduled frames, except for the root frame,
// which just navigated (and which we should consider pending until we
// receive a Page.frameStoppedLoading event for it).
std::string frame_id;
if (!params.GetString("frame.id", &frame_id))
return Status(kUnknownError, "missing or invalid 'frame.id'");
bool frame_was_pending = pending_frame_set_.count(frame_id) > 0;
pending_frame_set_.clear();
scheduled_frame_set_.clear();
if (frame_was_pending)
pending_frame_set_.insert(frame_id);
// If the URL indicates that the web page is unreachable (the sad tab
// page) then discard all pending navigations.
std::string frame_url;
if (!params.GetString("frame.url", &frame_url))
return Status(kUnknownError, "missing or invalid 'frame.url'");
if (frame_url == kUnreachableWebDataURL)
pending_frame_set_.clear();
} else {
// If a child frame just navigated, check if it is the dummy frame that
// was attached by IsPendingNavigation(). We don't want to track execution
// contexts created and destroyed for this dummy frame.
std::string name;
if (!params.GetString("frame.name", &name))
// https://bugs.chromium.org/p/chromium/issues/detail?id=823579
// OOPIF frames might not have names. Ignore them.
return Status(kOk);
std::string url;
if (!params.GetString("frame.url", &url))
return Status(kUnknownError, "missing or invalid 'frame.url'");
if (name == kDummyFrameName && url == kDummyFrameUrl)
params.GetString("frame.id", &dummy_frame_id_);
}
} else if (method == "Runtime.executionContextsCleared") {
execution_context_set_.clear();
load_event_fired_ = false;
// As of crrev.com/382211, DevTools sends an executionContextsCleared
// event right before the first execution context is created, but after
// Page.loadEventFired. Set the loading state to loading, but do not
// clear the pending and scheduled frame sets, since they may contain
// frames that we're still waiting for.
loading_state_ = kLoading;
} else if (method == "Runtime.executionContextCreated") {
int execution_context_id;
if (!params.GetInteger("context.id", &execution_context_id))
return Status(kUnknownError, "missing or invalid 'context.id'");
std::string frame_id;
if (!params.GetString("context.auxData.frameId", &frame_id)) {
return Status(kUnknownError,
"missing or invalid 'context.auxData.frameId'");
}
if (frame_id == dummy_frame_id_)
dummy_execution_context_id_ = execution_context_id;
else
execution_context_set_.insert(execution_context_id);
} else if (method == "Runtime.executionContextDestroyed") {
int execution_context_id;
if (!params.GetInteger("executionContextId", &execution_context_id))
return Status(kUnknownError, "missing or invalid 'context.id'");
execution_context_set_.erase(execution_context_id);
if (execution_context_id != dummy_execution_context_id_) {
if (execution_context_set_.empty()) {
loading_state_ = kLoading;
load_event_fired_ = false;
dummy_frame_id_ = std::string();
dummy_execution_context_id_ = 0;
}
}
} else if (method == "Page.loadEventFired") {
load_event_fired_ = true;
} else if (method == "Inspector.targetCrashed") {
ResetLoadingState(kNotLoading);
}
return Status(kOk);
}
Status NavigationTracker::OnCommandSuccess(
DevToolsClient* client,
const std::string& method,
const base::DictionaryValue& result,
const Timeout& command_timeout) {
// Check for start of navigation. In some case response to navigate is delayed
// until after the command has already timed out, in which case it has already
// been cancelled or will be cancelled soon, and should be ignored.
if ((method == "Page.navigate" || method == "Page.navigateToHistoryEntry") &&
loading_state_ != kLoading && !command_timeout.IsExpired()) {
// 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");
std::unique_ptr<base::DictionaryValue> result;
Status status = client_->SendCommandAndGetResultWithTimeout(
"Runtime.evaluate", params, &command_timeout, &result);
std::string url;
if (status.IsError() || !result->GetString("result.value", &url))
return MakeNavigationCheckFailedStatus(status);
if (loading_state_ == kUnknown && url.empty())
loading_state_ = kLoading;
}
return Status(kOk);
}
void NavigationTracker::ResetLoadingState(LoadingState loading_state) {
loading_state_ = loading_state;
pending_frame_set_.clear();
scheduled_frame_set_.clear();
}