blob: 46eca00e59bbe51a579265a2883df2ab978817c2 [file] [log] [blame]
// Copyright 2013 The Chromium Authors
// 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/web_view_impl.h"
#include <stddef.h>
#include <algorithm>
#include <cstring>
#include <memory>
#include <queue>
#include <utility>
#include <vector>
#include "base/check.h"
#include "base/files/file_path.h"
#include "base/functional/bind.h"
#include "base/json/json_reader.h"
#include "base/json/json_writer.h"
#include "base/logging.h"
#include "base/memory/raw_ptr.h"
#include "base/notreached.h"
#include "base/strings/pattern.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_split.h"
#include "base/strings/string_util.h"
#include "base/strings/stringprintf.h"
#include "base/threading/platform_thread.h"
#include "base/time/time.h"
#include "base/uuid.h"
#include "base/values.h"
#include "build/build_config.h"
#include "chrome/test/chromedriver/chrome/bidi_tracker.h"
#include "chrome/test/chromedriver/chrome/browser_info.h"
#include "chrome/test/chromedriver/chrome/cast_tracker.h"
#include "chrome/test/chromedriver/chrome/devtools_client.h"
#include "chrome/test/chromedriver/chrome/devtools_client_impl.h"
#include "chrome/test/chromedriver/chrome/download_directory_override_manager.h"
#include "chrome/test/chromedriver/chrome/fedcm_tracker.h"
#include "chrome/test/chromedriver/chrome/frame_tracker.h"
#include "chrome/test/chromedriver/chrome/geolocation_override_manager.h"
#include "chrome/test/chromedriver/chrome/heap_snapshot_taker.h"
#include "chrome/test/chromedriver/chrome/js.h"
#include "chrome/test/chromedriver/chrome/mobile_emulation_override_manager.h"
#include "chrome/test/chromedriver/chrome/navigation_tracker.h"
#include "chrome/test/chromedriver/chrome/network_conditions_override_manager.h"
#include "chrome/test/chromedriver/chrome/non_blocking_navigation_tracker.h"
#include "chrome/test/chromedriver/chrome/page_load_strategy.h"
#include "chrome/test/chromedriver/chrome/status.h"
#include "chrome/test/chromedriver/chrome/ui_events.h"
#include "chrome/test/chromedriver/net/timeout.h"
#include "ui/events/keycodes/dom/keycode_converter.h"
#include "ui/events/keycodes/keyboard_code_conversion.h"
namespace {
const int kWaitForNavigationStopSeconds = 10;
const char kElementKey[] = "ELEMENT";
const char kElementKeyW3C[] = "element-6066-11e4-a52e-4f735466cecf";
const char kShadowRootKey[] = "shadow-6066-11e4-a52e-4f735466cecf";
struct ElementId {
std::string frame_id;
std::string loader_id;
int backend_node_id = 0;
bool IsValid() const { return !frame_id.empty() && !loader_id.empty(); }
explicit operator bool() const { return IsValid(); }
};
std::optional<std::string> GetBackendNodeIdKey(const base::Value::Dict& element,
bool w3c_compliant) {
if (element.contains(kShadowRootKey)) {
return kShadowRootKey;
}
if (w3c_compliant && element.contains(kElementKeyW3C)) {
return kElementKeyW3C;
}
if (!w3c_compliant && element.contains(kElementKey)) {
return kElementKey;
}
return std::nullopt;
}
ElementId GetElementId(const base::Value::Dict& element, std::string key) {
const std::string* element_id = element.FindString(std::move(key));
if (element_id == nullptr) {
return ElementId{};
}
if (!base::MatchPattern(*element_id, "f.*.d.*.e.*")) {
return ElementId{};
}
std::vector<std::string> components = base::SplitString(
*element_id, ".", base::KEEP_WHITESPACE, base::SPLIT_WANT_ALL);
if (components.size() != 6) {
return ElementId{};
}
std::string frame_id = components[1];
std::string loader_id = components[3];
std::string backend_node_id_str = components[5];
int backend_node_id;
if (!base::StringToInt(backend_node_id_str, &backend_node_id)) {
return ElementId{};
}
return ElementId{frame_id, loader_id, backend_node_id};
}
ElementId GetElementId(const base::Value::Dict& element, bool w3c_compliant) {
std::optional<std::string> key = GetBackendNodeIdKey(element, w3c_compliant);
if (!key) {
return ElementId{};
}
return GetElementId(element, std::move(*key));
}
Status GetContextIdForFrame(WebViewImpl* web_view,
const std::string& frame,
std::string* context_id) {
DCHECK(context_id);
if (frame.empty()) {
context_id->clear();
return Status(kOk);
}
Status status =
web_view->GetFrameTracker()->GetContextIdForFrame(frame, context_id);
if (status.IsError())
return status;
return Status(kOk);
}
const char* GetAsString(MouseEventType type) {
switch (type) {
case kPressedMouseEventType:
return "mousePressed";
case kReleasedMouseEventType:
return "mouseReleased";
case kMovedMouseEventType:
return "mouseMoved";
case kWheelMouseEventType:
return "mouseWheel";
default:
return "";
}
}
const char* GetAsString(TouchEventType type) {
switch (type) {
case kTouchStart:
return "touchStart";
case kTouchEnd:
return "touchEnd";
case kTouchMove:
return "touchMove";
case kTouchCancel:
return "touchCancel";
default:
return "";
}
}
const char* GetAsString(MouseButton button) {
switch (button) {
case kLeftMouseButton:
return "left";
case kMiddleMouseButton:
return "middle";
case kRightMouseButton:
return "right";
case kBackMouseButton:
return "back";
case kForwardMouseButton:
return "forward";
case kNoneMouseButton:
return "none";
default:
return "";
}
}
const char* GetAsString(KeyEventType type) {
switch (type) {
case kKeyDownEventType:
return "keyDown";
case kKeyUpEventType:
return "keyUp";
case kRawKeyDownEventType:
return "rawKeyDown";
case kCharEventType:
return "char";
default:
return "";
}
}
const char* GetAsString(PointerType type) {
switch (type) {
case kMouse:
return "mouse";
case kPen:
return "pen";
default:
NOTREACHED_IN_MIGRATION();
return "";
}
}
base::Value::Dict GenerateTouchPoint(const TouchEvent& event) {
base::Value::Dict point;
point.Set("x", event.x);
point.Set("y", event.y);
point.Set("radiusX", event.radiusX);
point.Set("radiusY", event.radiusY);
point.Set("rotationAngle", event.rotationAngle);
point.Set("force", event.force);
point.Set("tangentialPressure", event.tangentialPressure);
point.Set("tiltX", event.tiltX);
point.Set("tiltY", event.tiltY);
point.Set("twist", event.twist);
point.Set("id", event.id);
return point;
}
class ObjectGroup {
public:
explicit ObjectGroup(DevToolsClient* client)
: client_(client),
object_group_name_(base::Uuid::GenerateRandomV4().AsLowercaseString()) {
}
~ObjectGroup() {
if (is_empty_) {
return;
}
base::Value::Dict params;
params.Set("objectGroup", object_group_name_);
client_->SendCommandAndIgnoreResponse("Runtime.releaseObjectGroup", params);
}
bool IsEmpty() const { return is_empty_; }
void SetEmpty(bool value) { is_empty_ = value; }
const std::string& name() const { return object_group_name_; }
private:
raw_ptr<DevToolsClient> client_;
std::string object_group_name_;
bool is_empty_ = false;
};
Status DescribeNode(DevToolsClient* client,
int backend_node_id,
int depth,
bool pierce,
base::Value* result_node) {
DCHECK(result_node);
base::Value::Dict params;
base::Value::Dict cmd_result;
params.Set("backendNodeId", backend_node_id);
params.Set("depth", depth);
params.Set("pierce", pierce);
Status status =
client->SendCommandAndGetResult("DOM.describeNode", params, &cmd_result);
if (status.IsError()) {
return status;
}
base::Value* node = cmd_result.Find("node");
if (!node || !node->is_dict()) {
return Status(kUnknownError, "DOM.describeNode missing dictionary 'node'");
}
*result_node = std::move(*node);
return status;
}
Status GetFrameIdForBackendNodeId(DevToolsClient* client,
int backend_node_id,
bool* found_node,
std::string* frame_id) {
DCHECK(frame_id);
DCHECK(found_node);
Status status{kOk};
base::Value node;
status = DescribeNode(client, backend_node_id, 0, false, &node);
if (status.IsError()) {
return status;
}
std::string* maybe_frame_id = node.GetIfDict()->FindString("frameId");
if (maybe_frame_id) {
*frame_id = *maybe_frame_id;
*found_node = true;
return Status(kOk);
}
return Status(kOk);
}
Status ResolveWeakReferences(base::Value::List& nodes) {
Status status{kOk};
std::map<int, int> ref_to_idx;
// Mapping
for (int k = 0; static_cast<size_t>(k) < nodes.size(); ++k) {
if (!nodes[k].is_dict()) {
continue;
}
const base::Value::Dict& node = nodes[k].GetDict();
std::optional<int> weak_node_ref =
node.FindIntByDottedPath("weakLocalObjectReference");
if (!weak_node_ref) {
continue;
}
std::optional<int> maybe_backend_node_id =
node.FindIntByDottedPath("value.backendNodeId");
if (!maybe_backend_node_id) {
continue;
}
ref_to_idx[weak_node_ref.value()] = k;
}
// Resolving
for (int k = 0; static_cast<size_t>(k) < nodes.size(); ++k) {
if (!nodes[k].is_dict()) {
continue;
}
const base::Value::Dict& node = nodes[k].GetDict();
std::optional<int> weak_node_ref =
node.FindIntByDottedPath("weakLocalObjectReference");
if (!weak_node_ref) {
continue;
}
std::optional<int> maybe_backend_node_id =
node.FindIntByDottedPath("value.backendNodeId");
if (maybe_backend_node_id) {
continue;
}
auto it = ref_to_idx.find(*weak_node_ref);
if (it == ref_to_idx.end()) {
return Status{
kUnknownError,
base::StringPrintf("Unable to resolve weakLocalObjectReference=%d",
*weak_node_ref)};
}
nodes[k] = nodes[it->second].Clone();
}
return status;
}
class BidiTrackerGuard {
public:
explicit BidiTrackerGuard(DevToolsClient& client) : client_(client) {
client_->AddListener(&bidi_tracker_);
}
BidiTracker& Tracker() { return bidi_tracker_; }
const BidiTracker& Tracker() const { return bidi_tracker_; }
~BidiTrackerGuard() { client_->RemoveListener(&bidi_tracker_); }
private:
base::raw_ref<DevToolsClient> client_;
BidiTracker bidi_tracker_;
};
} // namespace
std::unique_ptr<WebViewImpl> WebViewImpl::CreateServiceWorkerWebView(
const std::string& id,
const bool w3c_compliant,
const BrowserInfo* browser_info,
std::unique_ptr<DevToolsClient> client) {
return std::unique_ptr<WebViewImpl>(new WebViewImpl(
id, w3c_compliant, nullptr, browser_info, std::move(client)));
}
std::unique_ptr<WebViewImpl> WebViewImpl::CreateTopLevelWebView(
const std::string& id,
const bool w3c_compliant,
const BrowserInfo* browser_info,
std::unique_ptr<DevToolsClient> client,
std::optional<MobileDevice> mobile_device,
std::string page_load_strategy,
bool autoaccept_beforeunload) {
return std::make_unique<WebViewImpl>(
id, w3c_compliant, nullptr, browser_info, std::move(client),
std::move(mobile_device), page_load_strategy, autoaccept_beforeunload);
}
WebViewImpl::WebViewImpl(const std::string& id,
const bool w3c_compliant,
const WebViewImpl* parent,
const BrowserInfo* browser_info,
std::unique_ptr<DevToolsClient> client)
: id_(id),
w3c_compliant_(w3c_compliant),
browser_info_(browser_info),
is_locked_(false),
is_detached_(false),
parent_(parent),
client_(std::move(client)),
frame_tracker_(nullptr),
mobile_emulation_override_manager_(nullptr),
geolocation_override_manager_(nullptr),
network_conditions_override_manager_(nullptr),
heap_snapshot_taker_(nullptr),
is_service_worker_(true) {
client_->SetOwner(this);
}
WebViewImpl::WebViewImpl(const std::string& id,
const bool w3c_compliant,
const WebViewImpl* parent,
const BrowserInfo* browser_info,
std::unique_ptr<DevToolsClient> client,
std::optional<MobileDevice> mobile_device,
std::string page_load_strategy,
bool autoaccept_beforeunload)
: id_(id),
w3c_compliant_(w3c_compliant),
browser_info_(browser_info),
is_locked_(false),
is_detached_(false),
parent_(parent),
client_(std::move(client)),
frame_tracker_(new FrameTracker(client_.get(), this)),
mobile_emulation_override_manager_(
new MobileEmulationOverrideManager(client_.get(),
std::move(mobile_device),
browser_info->major_version)),
geolocation_override_manager_(
new GeolocationOverrideManager(client_.get())),
network_conditions_override_manager_(
new NetworkConditionsOverrideManager(client_.get())),
heap_snapshot_taker_(new HeapSnapshotTaker(client_.get())),
is_service_worker_(false),
autoaccept_beforeunload_(autoaccept_beforeunload) {
client_->SetAutoAcceptBeforeunload(autoaccept_beforeunload_);
// Downloading in headless mode requires the setting of
// Browser.setDownloadBehavior. This is handled by the
// DownloadDirectoryOverrideManager, which is only instantiated
// in headless chrome.
if (browser_info->is_headless_shell) {
download_directory_override_manager_ =
std::make_unique<DownloadDirectoryOverrideManager>(client_.get());
}
// Child WebViews should not have their own navigation_tracker, but defer
// all related calls to their parent. All WebViews must have either parent_
// or navigation_tracker_
if (parent_ == nullptr) {
navigation_tracker_ = CreatePageLoadStrategy(page_load_strategy);
}
client_->SetOwner(this);
}
WebViewImpl::~WebViewImpl() = default;
std::unique_ptr<PageLoadStrategy> WebViewImpl::CreatePageLoadStrategy(
const std::string& strategy) {
if (strategy == PageLoadStrategy::kNone) {
return std::make_unique<NonBlockingNavigationTracker>();
} else if (strategy == PageLoadStrategy::kNormal) {
return std::make_unique<NavigationTracker>(client_.get(), this, false);
} else if (strategy == PageLoadStrategy::kEager) {
return std::make_unique<NavigationTracker>(client_.get(), this, true);
} else {
NOTREACHED_IN_MIGRATION() << "invalid strategy '" << strategy << "'";
return nullptr;
}
}
WebView* WebViewImpl::GetTargetForFrame(const std::string& frame) {
return frame.empty() ? this : GetFrameTracker()->GetTargetForFrame(frame);
}
bool WebViewImpl::IsServiceWorker() const {
return is_service_worker_;
}
std::unique_ptr<WebViewImpl> WebViewImpl::CreateChild(
const std::string& session_id,
const std::string& target_id) const {
// While there may be a deep hierarchy of WebViewImpl instances, the
// hierarchy for DevToolsClientImpl is flat - there's a root which
// sends/receives over the socket, and all child sessions are considered
// its children (one level deep at most).
std::unique_ptr<DevToolsClientImpl> child_client =
std::make_unique<DevToolsClientImpl>(session_id, session_id);
std::unique_ptr<WebViewImpl> child = std::make_unique<WebViewImpl>(
target_id, w3c_compliant_, this, browser_info_, std::move(child_client),
std::nullopt, "", autoaccept_beforeunload_);
const WebViewImpl* root_view = this;
while (root_view->parent_ != nullptr) {
root_view = root_view->parent_;
}
PageLoadStrategy* navigation_tracker = root_view->navigation_tracker_.get();
if (navigation_tracker && !navigation_tracker->IsNonBlocking()) {
// Find Navigation Tracker for the top of the WebViewImpl hierarchy
child->client_->AddListener(navigation_tracker);
}
return child;
}
std::string WebViewImpl::GetId() {
return id_;
}
bool WebViewImpl::WasCrashed() {
return client_->WasCrashed();
}
Status WebViewImpl::AttachTo(DevToolsClient* root_client) {
// Add this target holder to extend the lifetime of webview object.
WebViewImplHolder target_holder(this);
return client_->AttachTo(root_client);
}
Status WebViewImpl::AttachChildView(WebViewImpl* child) {
DevToolsClient* root_client = client_.get();
while (root_client->GetParentClient() != nullptr) {
root_client = root_client->GetParentClient();
}
return child->AttachTo(root_client);
}
Status WebViewImpl::HandleEventsUntil(const ConditionalFunc& conditional_func,
const Timeout& timeout) {
return client_->HandleEventsUntil(conditional_func, timeout);
}
Status WebViewImpl::HandleReceivedEvents() {
return client_->HandleReceivedEvents();
}
Status WebViewImpl::GetUrl(std::string* url) {
base::Value::Dict params;
base::Value::Dict result;
Status status = client_->SendCommandAndGetResult("Page.getNavigationHistory",
params, &result);
if (status.IsError())
return status;
std::optional<int> current_index = result.FindInt("currentIndex");
if (!current_index)
return Status(kUnknownError, "navigation history missing currentIndex");
base::Value::List* entries = result.FindList("entries");
if (!entries)
return Status(kUnknownError, "navigation history missing entries");
if (static_cast<int>(entries->size()) <= *current_index ||
!(*entries)[*current_index].is_dict()) {
return Status(kUnknownError, "navigation history missing entry");
}
base::Value& entry = (*entries)[*current_index];
if (!entry.GetDict().FindString("url"))
return Status(kUnknownError, "navigation history entry is missing url");
*url = *entry.GetDict().FindString("url");
return Status(kOk);
}
Status WebViewImpl::Load(const std::string& url, const Timeout* timeout) {
// Javascript URLs will cause a hang while waiting for the page to stop
// loading, so just disallow.
if (base::StartsWith(url,
"javascript:", base::CompareCase::INSENSITIVE_ASCII)) {
return Status(kUnknownError, "unsupported protocol");
}
base::Value::Dict params;
params.Set("url", url);
if (IsNonBlocking()) {
// With non-blocking navigation tracker, the previous navigation might
// still be in progress, and this can cause the new navigate command to be
// ignored on Chrome v63 and above. Stop previous navigation first.
client_->SendCommand("Page.stopLoading", base::Value::Dict());
// Use SendCommandAndIgnoreResponse to ensure no blocking occurs.
return client_->SendCommandAndIgnoreResponse("Page.navigate", params);
}
return client_->SendCommandWithTimeout("Page.navigate", params, timeout);
}
Status WebViewImpl::Reload(const Timeout* timeout) {
base::Value::Dict params;
params.Set("ignoreCache", false);
return client_->SendCommandWithTimeout("Page.reload", params, timeout);
}
Status WebViewImpl::Freeze(const Timeout* timeout) {
base::Value::Dict params;
params.Set("state", "frozen");
return client_->SendCommandWithTimeout("Page.setWebLifecycleState", params,
timeout);
}
Status WebViewImpl::Resume(const Timeout* timeout) {
base::Value::Dict params;
params.Set("state", "active");
return client_->SendCommandWithTimeout("Page.setWebLifecycleState", params,
timeout);
}
Status WebViewImpl::StartBidiServer(std::string bidi_mapper_script,
const base::Value::Dict& mapper_options) {
return client_->StartBidiServer(std::move(bidi_mapper_script),
mapper_options);
}
Status WebViewImpl::PostBidiCommand(base::Value::Dict command) {
return client_->PostBidiCommand(std::move(command));
}
Status WebViewImpl::SendBidiCommand(base::Value::Dict command,
const Timeout& timeout,
base::Value::Dict& response) {
WebViewImplHolder target_holder(this);
Status status{kOk};
BidiTrackerGuard bidi_tracker_guard(*client_);
base::Value* maybe_cmd_id = command.Find("id");
if (maybe_cmd_id == nullptr) {
return Status{kUnknownError, "BiDi command has no 'id' of type integer"};
}
base::Value expected_id = maybe_cmd_id->Clone();
std::string* maybe_channel = command.FindString("channel");
if (maybe_channel == nullptr) {
return Status{kUnknownError,
"BiDi command has no 'channel' of type string"};
}
bidi_tracker_guard.Tracker().SetChannelSuffix(*maybe_channel);
base::Value::Dict tmp;
auto on_bidi_message = [](base::Value::Dict& destination,
base::Value::Dict payload) {
destination = std::move(payload);
return Status{kOk};
};
bidi_tracker_guard.Tracker().SetBidiCallback(
base::BindRepeating(on_bidi_message, std::ref(tmp)));
status = client_->PostBidiCommand(std::move(command));
if (status.IsError()) {
return status;
}
auto response_is_received = [](const base::Value& expected_id,
const base::Value::Dict& destionation,
bool* is_condition_met) {
const base::Value* maybe_response_id = destionation.Find("id");
*is_condition_met =
(maybe_response_id != nullptr) && (expected_id == *maybe_response_id);
return Status{kOk};
};
status = client_->HandleEventsUntil(
base::BindRepeating(response_is_received, std::cref(expected_id),
std::cref(tmp)),
timeout);
if (status.IsError()) {
return status;
}
response = std::move(tmp);
return status;
}
Status WebViewImpl::SendCommand(const std::string& cmd,
const base::Value::Dict& params) {
return client_->SendCommand(cmd, params);
}
Status WebViewImpl::SendCommandFromWebSocket(const std::string& cmd,
const base::Value::Dict& params,
const int client_cmd_id) {
return client_->SendCommandFromWebSocket(cmd, params, client_cmd_id);
}
Status WebViewImpl::SendCommandAndGetResult(
const std::string& cmd,
const base::Value::Dict& params,
std::unique_ptr<base::Value>* value) {
base::Value::Dict result;
Status status = client_->SendCommandAndGetResult(cmd, params, &result);
if (status.IsError())
return status;
*value = std::make_unique<base::Value>(std::move(result));
return Status(kOk);
}
Status WebViewImpl::TraverseHistory(int delta, const Timeout* timeout) {
base::Value::Dict params;
base::Value::Dict result;
Status status = client_->SendCommandAndGetResult("Page.getNavigationHistory",
params, &result);
if (status.IsError())
return status;
std::optional<int> current_index = result.FindInt("currentIndex");
if (!current_index)
return Status(kUnknownError, "DevTools didn't return currentIndex");
base::Value::List* entries = result.FindList("entries");
if (!entries)
return Status(kUnknownError, "DevTools didn't return entries");
if ((*current_index + delta) < 0 ||
(static_cast<int>(entries->size()) <= *current_index + delta) ||
!(*entries)[*current_index + delta].is_dict()) {
// The WebDriver spec says that if there are no pages left in the browser's
// history (i.e. |current_index + delta| is out of range), then we must not
// navigate anywhere.
return Status(kOk);
}
base::Value& entry = (*entries)[*current_index + delta];
std::optional<int> entry_id = entry.GetDict().FindInt("id");
if (!entry_id)
return Status(kUnknownError, "history entry does not have an id");
params.Set("entryId", *entry_id);
return client_->SendCommandWithTimeout("Page.navigateToHistoryEntry", params,
timeout);
}
Status WebViewImpl::GetLoaderId(const std::string& frame_id,
const Timeout& timeout,
std::string& loader_id) {
Status status{kOk};
base::Value::Dict frame_tree_result;
status = client_->SendCommandAndGetResultWithTimeout(
"Page.getFrameTree", base::Value::Dict(), &timeout, &frame_tree_result);
if (status.IsError()) {
return status;
}
base::Value::Dict* maybe_frame_tree = frame_tree_result.FindDict("frameTree");
if (!maybe_frame_tree) {
return Status{kUnknownError,
"no frameTree in the response to Page.getFrameTree"};
}
std::queue<base::Value::Dict*> q;
for (q.push(maybe_frame_tree); !q.empty(); q.pop()) {
base::Value::Dict* frame_tree = q.front();
std::string* current_frame_id =
frame_tree->FindStringByDottedPath("frame.id");
if (!current_frame_id) {
return Status{
kUnknownError,
"no frame.id in one of the nodes of the Page.getFrameTree response"};
}
std::string* current_loader_id =
frame_tree->FindStringByDottedPath("frame.loaderId");
if (!current_loader_id) {
return Status{kUnknownError,
"no frame.loaderId in one of the nodes of the "
"Page.getFrameTree response"};
}
if (current_loader_id->empty()) {
// There is probably an ongoing navigation. Giving up.
return Status{kNoSuchExecutionContext,
"no loaderId found for the current frame"};
}
if (frame_id == *current_frame_id) {
loader_id = std::move(*current_loader_id);
break;
}
base::Value::List* child_frames = frame_tree->FindList("childFrames");
if (!child_frames) {
continue;
}
for (base::Value& item : (*child_frames)) {
if (!item.is_dict()) {
return Status{kUnknownError,
"child frame is not a dictionary in one of the nodes of "
"the Page.getFrameTree response"};
}
q.push(item.GetIfDict());
}
}
return status;
}
Status WebViewImpl::CallFunctionWithTimeoutInternal(
std::string frame,
std::string function,
base::Value::List args,
const base::TimeDelta& timeout,
std::unique_ptr<base::Value>* result) {
Status status{kOk};
std::string frame_id = frame.empty() ? id_ : frame;
Timeout local_timeout(timeout);
std::string loader_id;
// The code below tries to detect if any navigation has happened during its
// execution. The navigation is detected if either loaderId or
// context_id has changed.
status = GetLoaderId(frame_id, local_timeout, loader_id);
if (status.IsError()) {
return status;
}
std::string context_id;
status = GetFrameTracker()->GetContextIdForFrame(frame_id, &context_id);
if (status.IsError()) {
return status;
}
ObjectGroup object_group(client_.get());
base::Value::List nodes;
// Resolving the references in the execution context obtained earlier.
status = ResolveElementReferencesInPlace(
frame_id, context_id, object_group.name(), loader_id, w3c_compliant_,
local_timeout, args, nodes);
object_group.SetEmpty(nodes.empty());
// kNoSuchElement is handled in special way:
// If loader id has changed then the node was not resolved due to the
// navigation.
// Otherwise the user has sent us a node id that refers a non-existent node.
if (status.IsError() && status.code() != kNoSuchElement) {
return status;
}
std::string new_loader_id;
Status new_status = GetLoaderId(frame_id, local_timeout, new_loader_id);
if (new_status.IsError()) {
return new_status;
}
if (new_loader_id != loader_id) {
// A navigation has happened while resolving references. Giving up.
return Status{kNoSuchExecutionContext,
"loader has changed while resolving nodes"};
}
// ResolveElementReferences returned kNoSuchElement.
// The loader did not change therefore the node indeed does not exist.
if (status.IsError()) {
return status;
}
std::string new_context_id;
new_status =
GetFrameTracker()->GetContextIdForFrame(frame_id, &new_context_id);
if (new_status.IsError()) {
return new_status;
}
if (context_id != new_context_id) {
return Status{kNoSuchExecutionContext,
"context has changed while resolving nodes"};
}
// All BackendNodeId's have been resolved in the same context and using the
// same loader. The remote call will succeed if the execution context does not
// change in the mean time. This is detected by the remote code implementing
// Runtime.callFunctionOn.
std::string json;
base::JSONWriter::Write(args, &json);
std::string w3c = w3c_compliant_ ? "true" : "false";
// TODO(zachconrad): Second null should be array of shadow host ids.
std::string wrapper_function = base::StringPrintf(
"function(){ return (%s).apply(null, [%s, %s, %s, arguments]); }",
kCallFunctionScript, function.c_str(), json.c_str(), w3c.c_str());
base::Value::Dict params;
params.Set("functionDeclaration", wrapper_function);
if (!context_id.empty()) {
params.Set("uniqueContextId", context_id);
}
params.Set("arguments", std::move(nodes));
params.Set("awaitPromise", true);
if (!object_group.IsEmpty()) {
params.Set("objectGroup", object_group.name());
}
base::Value::Dict serialization_options;
serialization_options.Set("serialization", "deep");
params.Set("serializationOptions", std::move(serialization_options));
base::Value::Dict cmd_result;
status = client_->SendCommandAndGetResultWithTimeout(
"Runtime.callFunctionOn", params, &local_timeout, &cmd_result);
if (status.IsError()) {
return status;
}
if (cmd_result.contains("exceptionDetails")) {
std::string description = "unknown";
if (const std::string* maybe_description =
cmd_result.FindStringByDottedPath("result.description")) {
description = *maybe_description;
}
return Status(kUnknownError,
"Runtime.callFunctionOn threw exception: " + description);
}
base::Value::List* maybe_received_list =
cmd_result.FindListByDottedPath("result.deepSerializedValue.value");
if (!maybe_received_list || maybe_received_list->empty()) {
return Status(kUnknownError,
"result.deepSerializedValue.value list is missing or empty "
"in Runtime.callFunctionOn response");
}
base::Value::List& received_list = *maybe_received_list;
if (!received_list[0].is_dict()) {
return Status(kUnknownError,
"first element in result.deepSerializedValue.value list must "
"be a dictionary");
}
std::string* serialized_value =
received_list[0].GetDict().FindString("value");
if (!serialized_value) {
return Status(kUnknownError,
"first element in result.deepSerializedValue.value list must "
"contain a string");
}
std::optional<base::Value> maybe_call_result =
base::JSONReader::Read(*serialized_value, base::JSON_PARSE_RFC);
if (!maybe_call_result) {
return Status{kUnknownError,
"cannot deserialize the result value received from "
"Runtime.callFunctionOn"};
}
received_list.erase(received_list.begin());
if (!maybe_call_result->is_dict()) {
return Status{
kUnknownError,
"deserialized Runtime.callFunctionOn result is not a dictionary"};
}
base::Value::Dict& call_result = maybe_call_result->GetDict();
std::optional<int> status_code = call_result.FindInt("status");
if (!status_code) {
return Status(kUnknownError, "call function result missing int 'status'");
}
if (*status_code != kOk) {
const std::string* message = call_result.FindString("value");
return Status(static_cast<StatusCode>(*status_code),
message ? *message : "");
}
base::Value* call_result_value = call_result.Find("value");
if (call_result_value == nullptr) {
// Missing 'value' indicates the JavaScript code didn't return a value.
return Status(kOk);
}
status = ResolveWeakReferences(received_list);
if (!status.IsOk()) {
return status;
}
status = CreateElementReferences(frame_id, loader_id, received_list,
*call_result_value);
if (!status.IsOk()) {
return status;
}
*result = std::make_unique<base::Value>(std::move(*call_result_value));
return status;
}
Status WebViewImpl::EvaluateScript(const std::string& frame,
const std::string& expression,
const bool await_promise,
std::unique_ptr<base::Value>* result) {
WebViewImplHolder target_holder(this);
Status status{kOk};
WebView* target = GetTargetForFrame(frame);
if (target != nullptr && target != this) {
if (target->IsDetached())
return Status(kTargetDetached);
return target->EvaluateScript(frame, expression, await_promise, result);
}
std::string context_id;
status = GetContextIdForFrame(this, frame, &context_id);
if (status.IsError())
return status;
// If the target associated with the current view or its ancestor is detached
// during the script execution we don't want deleting the current WebView
// because we are executing the code in its method.
// Instead we lock the WebView with target holder and only label the view as
// detached.
const base::TimeDelta& timeout = base::TimeDelta::Max();
return internal::EvaluateScriptAndGetValue(
client_.get(), context_id, expression, timeout, await_promise, result);
}
Status WebViewImpl::CallFunctionWithTimeout(
const std::string& frame,
const std::string& function,
const base::Value::List& args,
const base::TimeDelta& timeout,
std::unique_ptr<base::Value>* result) {
WebViewImplHolder target_holder(this);
Status status{kOk};
WebView* target = GetTargetForFrame(frame);
if (target != nullptr && target != this) {
if (target->IsDetached()) {
return Status(kTargetDetached);
}
return target->CallFunctionWithTimeout(frame, function, args, timeout,
result);
}
return CallFunctionWithTimeoutInternal(frame, std::move(function),
args.Clone(), timeout, result);
}
Status WebViewImpl::CallFunction(const std::string& frame,
const std::string& function,
const base::Value::List& args,
std::unique_ptr<base::Value>* result) {
// Timeout set to Max is treated as no timeout.
return CallFunctionWithTimeout(frame, function, args, base::TimeDelta::Max(),
result);
}
Status WebViewImpl::CallUserSyncScript(const std::string& frame,
const std::string& script,
const base::Value::List& args,
const base::TimeDelta& timeout,
std::unique_ptr<base::Value>* result) {
WebViewImplHolder target_holder(this);
Status status{kOk};
WebView* target = GetTargetForFrame(frame);
if (target != nullptr && target != this) {
if (target->IsDetached()) {
return Status(kTargetDetached);
}
return target->CallUserSyncScript(frame, script, args, timeout, result);
}
base::Value::List sync_args;
sync_args.Append(script);
sync_args.Append(args.Clone());
return CallFunctionWithTimeoutInternal(frame, kExecuteScriptScript,
sync_args.Clone(), timeout, result);
}
Status WebViewImpl::CallUserAsyncFunction(
const std::string& frame,
const std::string& function,
const base::Value::List& args,
const base::TimeDelta& timeout,
std::unique_ptr<base::Value>* result) {
return CallAsyncFunctionInternal(frame, function, args, timeout, result);
}
// TODO (crbug.com/chromedriver/4364): Simplify this function
Status WebViewImpl::GetFrameByFunction(const std::string& frame,
const std::string& function,
const base::Value::List& args,
std::string* out_frame) {
WebViewImplHolder target_holder(this);
Status status{kOk};
WebView* target = GetTargetForFrame(frame);
if (target != nullptr && target != this) {
if (target->IsDetached())
return Status(kTargetDetached);
return target->GetFrameByFunction(frame, function, args, out_frame);
}
std::unique_ptr<base::Value> result;
status = CallFunctionWithTimeoutInternal(frame, function, args.Clone(),
base::TimeDelta::Max(), &result);
if (status.IsError()) {
return status;
}
if (!result->is_dict()) {
return Status{kNoSuchFrame};
}
ElementId maybe_element_id = GetElementId(result->GetDict(), w3c_compliant_);
if (!maybe_element_id) {
return Status{kNoSuchFrame, "invalid element id"};
}
bool found_node = false;
status = GetFrameIdForBackendNodeId(
client_.get(), maybe_element_id.backend_node_id, &found_node, out_frame);
if (status.IsError()) {
return status;
}
if (!found_node) {
return Status(kNoSuchFrame);
}
return status;
}
Status WebViewImpl::DispatchTouchEventsForMouseEvents(
const std::vector<MouseEvent>& events,
const std::string& frame) {
// Touch events are filtered by the compositor if there are no touch listeners
// on the page. Wait two frames for the compositor to sync with the main
// thread to get consistent behavior.
base::Value::Dict promise_params;
promise_params.Set(
"expression",
"new Promise(x => setTimeout(() => setTimeout(x, 20), 20))");
promise_params.Set("awaitPromise", true);
client_->SendCommand("Runtime.evaluate", promise_params);
for (const MouseEvent& event : events) {
base::Value::Dict params;
switch (event.type) {
case kPressedMouseEventType:
params.Set("type", "touchStart");
break;
case kReleasedMouseEventType:
params.Set("type", "touchEnd");
break;
case kMovedMouseEventType:
if (event.button == kNoneMouseButton) {
continue;
}
params.Set("type", "touchMove");
break;
default:
break;
}
base::Value::List touch_points;
if (event.type != kReleasedMouseEventType) {
base::Value::Dict touch_point;
touch_point.Set("x", event.x);
touch_point.Set("y", event.y);
touch_points.Append(std::move(touch_point));
}
params.Set("touchPoints", std::move(touch_points));
params.Set("modifiers", event.modifiers);
Status status = client_->SendCommand("Input.dispatchTouchEvent", params);
if (status.IsError())
return status;
}
return Status(kOk);
}
Status WebViewImpl::DispatchMouseEvents(const std::vector<MouseEvent>& events,
const std::string& frame,
bool async_dispatch_events) {
if (mobile_emulation_override_manager_->IsEmulatingTouch())
return DispatchTouchEventsForMouseEvents(events, frame);
Status status(kOk);
for (auto it = events.begin(); it != events.end(); ++it) {
base::Value::Dict params;
std::string type = GetAsString(it->type);
params.Set("type", type);
params.Set("x", it->x);
params.Set("y", it->y);
params.Set("modifiers", it->modifiers);
params.Set("button", GetAsString(it->button));
params.Set("buttons", it->buttons);
params.Set("clickCount", it->click_count);
params.Set("force", it->force);
params.Set("tangentialPressure", it->tangentialPressure);
params.Set("tiltX", it->tiltX);
params.Set("tiltY", it->tiltY);
params.Set("twist", it->twist);
params.Set("pointerType", GetAsString(it->pointer_type));
if (type == "mouseWheel") {
params.Set("deltaX", it->delta_x);
params.Set("deltaY", it->delta_y);
}
const bool last_event = (it == events.end() - 1);
if (async_dispatch_events || !last_event) {
status = client_->SendCommandAndIgnoreResponse("Input.dispatchMouseEvent",
params);
} else {
status = client_->SendCommand("Input.dispatchMouseEvent", params);
}
if (status.IsError())
return status;
}
return status;
}
Status WebViewImpl::DispatchTouchEvent(const TouchEvent& event,
bool async_dispatch_events) {
base::Value::Dict params;
std::string type = GetAsString(event.type);
params.Set("type", type);
base::Value::List point_list;
Status status(kOk);
if (type == "touchStart" || type == "touchMove") {
base::Value::Dict point = GenerateTouchPoint(event);
point_list.Append(std::move(point));
}
params.Set("touchPoints", std::move(point_list));
if (async_dispatch_events) {
status = client_->SendCommandAndIgnoreResponse("Input.dispatchTouchEvent",
params);
} else {
status = client_->SendCommand("Input.dispatchTouchEvent", params);
}
return status;
}
Status WebViewImpl::DispatchTouchEvents(const std::vector<TouchEvent>& events,
bool async_dispatch_events) {
for (auto it = events.begin(); it != events.end(); ++it) {
const bool last_event = (it == events.end() - 1);
Status status =
DispatchTouchEvent(*it, async_dispatch_events || !last_event);
if (status.IsError())
return status;
}
return Status(kOk);
}
Status WebViewImpl::DispatchTouchEventWithMultiPoints(
const std::vector<TouchEvent>& events,
bool async_dispatch_events) {
if (events.size() == 0)
return Status(kOk);
base::Value::Dict params;
Status status(kOk);
size_t touch_count = 1;
for (const TouchEvent& event : events) {
base::Value::List point_list;
int32_t current_time =
(base::Time::Now() - base::Time::UnixEpoch()).InMilliseconds();
params.Set("timestamp", current_time);
std::string type = GetAsString(event.type);
params.Set("type", type);
if (type == "touchCancel")
continue;
point_list.Append(GenerateTouchPoint(event));
params.Set("touchPoints", std::move(point_list));
if (async_dispatch_events || touch_count < events.size()) {
status = client_->SendCommandAndIgnoreResponse("Input.dispatchTouchEvent",
params);
} else {
status = client_->SendCommand("Input.dispatchTouchEvent", params);
}
if (status.IsError())
return status;
touch_count++;
}
return Status(kOk);
}
Status WebViewImpl::DispatchKeyEvents(const std::vector<KeyEvent>& events,
bool async_dispatch_events) {
Status status(kOk);
for (auto it = events.begin(); it != events.end(); ++it) {
base::Value::Dict params;
params.Set("type", GetAsString(it->type));
if (it->modifiers & kNumLockKeyModifierMask) {
params.Set("isKeypad", true);
params.Set("modifiers", it->modifiers & ~kNumLockKeyModifierMask);
} else {
params.Set("modifiers", it->modifiers);
}
params.Set("text", it->modified_text);
params.Set("unmodifiedText", it->unmodified_text);
params.Set("windowsVirtualKeyCode", it->key_code);
std::string code;
if (it->is_from_action) {
code = it->code;
} else {
ui::DomCode dom_code = ui::UsLayoutKeyboardCodeToDomCode(it->key_code);
code = ui::KeycodeConverter::DomCodeToCodeString(dom_code);
}
bool is_ctrl_cmd_key_down = false;
#if BUILDFLAG(IS_MAC)
if (it->modifiers & kMetaKeyModifierMask)
is_ctrl_cmd_key_down = true;
#else
if (it->modifiers & kControlKeyModifierMask)
is_ctrl_cmd_key_down = true;
#endif
if (!code.empty())
params.Set("code", code);
if (!it->key.empty())
params.Set("key", it->key);
else if (it->is_from_action)
params.Set("key", it->modified_text);
if (is_ctrl_cmd_key_down) {
std::string command;
if (code == "KeyA") {
command = "SelectAll";
} else if (code == "KeyC") {
command = "Copy";
} else if (code == "KeyX") {
command = "Cut";
} else if (code == "KeyY") {
command = "Redo";
} else if (code == "KeyV") {
if (it->modifiers & kShiftKeyModifierMask)
command = "PasteAndMatchStyle";
else
command = "Paste";
} else if (code == "KeyZ") {
if (it->modifiers & kShiftKeyModifierMask)
command = "Redo";
else
command = "Undo";
}
base::Value::List command_list;
command_list.Append(command);
params.Set("commands", std::move(command_list));
}
if (it->location != 0) {
// The |location| parameter in DevTools protocol only accepts 1 (left
// modifiers) and 2 (right modifiers). For location 3 (numeric keypad),
// it is necessary to set the |isKeypad| parameter.
if (it->location == 3)
params.Set("isKeypad", true);
else
params.Set("location", it->location);
}
const bool last_event = (it == events.end() - 1);
if (async_dispatch_events || !last_event) {
status = client_->SendCommandAndIgnoreResponse("Input.dispatchKeyEvent",
params);
} else {
status = client_->SendCommand("Input.dispatchKeyEvent", params);
}
if (status.IsError())
return status;
}
return status;
}
Status WebViewImpl::GetCookies(base::Value* cookies,
const std::string& current_page_url) {
base::Value::Dict params;
base::Value::Dict result;
if (browser_info_->browser_name != "webview") {
base::Value::List url_list;
url_list.Append(current_page_url);
params.Set("urls", std::move(url_list));
Status status =
client_->SendCommandAndGetResult("Network.getCookies", params, &result);
if (status.IsError())
return status;
} else {
Status status =
client_->SendCommandAndGetResult("Page.getCookies", params, &result);
if (status.IsError())
return status;
}
base::Value::List* const cookies_tmp = result.FindList("cookies");
if (!cookies_tmp)
return Status(kUnknownError, "DevTools didn't return cookies");
*cookies = base::Value(std::move(*cookies_tmp));
return Status(kOk);
}
Status WebViewImpl::DeleteCookie(const std::string& name,
const std::string& url,
const std::string& domain,
const std::string& path) {
base::Value::Dict params;
params.Set("url", url);
std::string command;
params.Set("name", name);
params.Set("domain", domain);
params.Set("path", path);
command = "Network.deleteCookies";
return client_->SendCommand(command, params);
}
Status WebViewImpl::AddCookie(const std::string& name,
const std::string& url,
const std::string& value,
const std::string& domain,
const std::string& path,
const std::string& same_site,
bool secure,
bool http_only,
double expiry) {
base::Value::Dict params;
params.Set("name", name);
params.Set("url", url);
params.Set("value", value);
params.Set("domain", domain);
params.Set("path", path);
params.Set("secure", secure);
params.Set("httpOnly", http_only);
if (!same_site.empty())
params.Set("sameSite", same_site);
if (expiry >= 0)
params.Set("expires", expiry);
base::Value::Dict result;
Status status =
client_->SendCommandAndGetResult("Network.setCookie", params, &result);
if (status.IsError())
return Status(kUnableToSetCookie);
if (!result.FindBool("success").value_or(false))
return Status(kUnableToSetCookie);
return Status(kOk);
}
Status WebViewImpl::WaitForPendingNavigations(const std::string& frame_id,
const Timeout& timeout,
bool stop_load_on_timeout) {
// This function should not be called for child WebViews
if (parent_ != nullptr)
return Status(kUnknownError,
"Call WaitForPendingNavigations only on the parent WebView");
VLOG(0) << "Waiting for pending navigations...";
const auto not_pending_navigation = base::BindRepeating(
&WebViewImpl::IsNotPendingNavigation, base::Unretained(this),
frame_id.empty() ? id_ : frame_id, base::Unretained(&timeout));
// If the target associated with the current view or its ancestor is detached
// while we are waiting for the pending navigation we don't want deleting the
// current WebView because we are executing the code in its method. Instead we
// lock the WebView with target holder and only label the view as detached.
WebViewImplHolder target_holder(this);
bool keep_waiting = true;
Status status{kOk};
while (keep_waiting) {
status = client_->HandleEventsUntil(not_pending_navigation, timeout);
keep_waiting = status.code() == kNoSuchExecutionContext ||
status.code() == kNavigationDetectedByRemoteEnd;
}
if (status.code() == kTimeout && stop_load_on_timeout) {
VLOG(0) << "Timed out. Stopping navigation...";
navigation_tracker_->set_timed_out(true);
client_->SendCommand("Page.stopLoading", base::Value::Dict());
// We don't consider |timeout| here to make sure the navigation actually
// stops and we cleanup properly after a command that caused a navigation
// that timed out. Otherwise we might have to wait for that before
// executing the next command, and it will be counted towards its timeout.
Status new_status{kOk};
keep_waiting = true;
while (keep_waiting) {
new_status = client_->HandleEventsUntil(
not_pending_navigation,
Timeout(base::Seconds(kWaitForNavigationStopSeconds)));
keep_waiting = status.code() == kNoSuchExecutionContext ||
status.code() == kNavigationDetectedByRemoteEnd;
}
navigation_tracker_->set_timed_out(false);
if (new_status.IsError())
status = new_status;
}
VLOG(0) << "Done waiting for pending navigations. Status: "
<< status.message();
return status;
}
Status WebViewImpl::IsPendingNavigation(const Timeout* timeout,
bool* is_pending) const {
if (navigation_tracker_)
return navigation_tracker_->IsPendingNavigation(timeout, is_pending);
return parent_->IsPendingNavigation(timeout, is_pending);
}
MobileEmulationOverrideManager* WebViewImpl::GetMobileEmulationOverrideManager()
const {
return mobile_emulation_override_manager_.get();
}
Status WebViewImpl::OverrideGeolocation(const Geoposition& geoposition) {
return geolocation_override_manager_->OverrideGeolocation(geoposition);
}
Status WebViewImpl::OverrideNetworkConditions(
const NetworkConditions& network_conditions) {
return network_conditions_override_manager_->OverrideNetworkConditions(
network_conditions);
}
Status WebViewImpl::OverrideDownloadDirectoryIfNeeded(
const std::string& download_directory) {
if (download_directory_override_manager_) {
return download_directory_override_manager_
->OverrideDownloadDirectoryWhenConnected(download_directory);
}
return Status(kOk);
}
Status WebViewImpl::CaptureScreenshot(std::string* screenshot,
const base::Value::Dict& params) {
base::Value::Dict result;
Timeout timeout(base::Seconds(10));
Status status = client_->SendCommandAndGetResultWithTimeout(
"Page.captureScreenshot", params, &timeout, &result);
if (status.IsError())
return status;
std::string* data = result.FindString("data");
if (!data)
return Status(kUnknownError, "expected string 'data' in response");
*screenshot = std::move(*data);
return Status(kOk);
}
Status WebViewImpl::PrintToPDF(const base::Value::Dict& params,
std::string* pdf) {
base::Value::Dict result;
Timeout timeout(base::Seconds(10));
Status status = client_->SendCommandAndGetResultWithTimeout(
"Page.printToPDF", params, &timeout, &result);
if (status.IsError()) {
if (status.code() == kUnknownError) {
return Status(kInvalidArgument, status);
}
return status;
}
std::string* data = result.FindString("data");
if (!data)
return Status(kUnknownError, "expected string 'data' in response");
*pdf = std::move(*data);
return Status(kOk);
}
Status WebViewImpl::GetBackendNodeIdByElement(const std::string& frame,
const base::Value& element,
int* backend_node_id) {
Status status{kOk};
if (!element.is_dict())
return Status(kUnknownError, "'element' is not a dictionary");
std::optional<std::string> maybe_key =
GetBackendNodeIdKey(element.GetDict(), w3c_compliant_);
if (!maybe_key) {
return Status{kNoSuchElement, "invalid element id"};
}
// From this point 'key' can have either of the following two values:
// * ELEMENT_KEY ("ELEMENT" or "element-6066-11e4-a52e-4f735466cecf")
// * SHADOW_ROOT_KEY ("shadow-6066-11e4-a52e-4f735466cecf")
std::string key = *maybe_key;
ElementId element_id = GetElementId(element.GetDict(), key);
if (!element_id) {
return Status{kNoSuchElement, "invalid element id"};
}
std::string frame_id = frame.empty() ? id_ : frame;
if (frame_id != element_id.frame_id) {
if (key == kShadowRootKey) {
return Status{kNoSuchShadowRoot, "shadow root not found"};
} else {
return Status{kNoSuchElement, "element not found"};
}
}
Timeout local_timeout(base::TimeDelta::Max());
std::string loader_id;
status = GetLoaderId(frame_id, local_timeout, loader_id);
if (status.IsError()) {
return status;
}
if (loader_id != element_id.loader_id) {
if (key == kShadowRootKey) {
return Status{kDetachedShadowRoot, "detached shadow root not found"};
} else {
return Status{kStaleElementReference, "stale element not found"};
}
}
*backend_node_id = element_id.backend_node_id;
return status;
}
Status WebViewImpl::SetFileInputFiles(const std::string& frame,
const base::Value& element,
const std::vector<base::FilePath>& files,
const bool append) {
WebViewImplHolder target_holder(this);
Status status{kOk};
if (!element.is_dict())
return Status(kUnknownError, "'element' is not a dictionary");
WebView* target = GetTargetForFrame(frame);
if (target != nullptr && target != this) {
if (target->IsDetached())
return Status(kTargetDetached);
return target->SetFileInputFiles(frame, element, files, append);
}
int backend_node_id;
status = GetBackendNodeIdByElement(frame, element, &backend_node_id);
if (status.IsError())
return status;
base::Value::List file_list;
// if the append flag is true, we need to retrieve the files that
// already exist in the element and add them too.
// Additionally, we need to add the old files first so that it looks
// like we're appending files.
if (append) {
// Convert the node_id to a Runtime.RemoteObject
std::string inner_remote_object_id;
{
base::Value::Dict cmd_result;
base::Value::Dict params;
params.Set("backendNodeId", backend_node_id);
status = client_->SendCommandAndGetResult("DOM.resolveNode", params,
&cmd_result);
if (status.IsError())
return status;
std::string* object_id =
cmd_result.FindStringByDottedPath("object.objectId");
if (!object_id)
return Status(kUnknownError, "DevTools didn't return objectId");
inner_remote_object_id = std::move(*object_id);
}
// figure out how many files there are
std::optional<int> number_of_files;
{
base::Value::Dict cmd_result;
base::Value::Dict params;
params.Set("functionDeclaration",
"function() { return this.files.length }");
params.Set("objectId", inner_remote_object_id);
status = client_->SendCommandAndGetResult("Runtime.callFunctionOn",
params, &cmd_result);
if (status.IsError())
return status;
number_of_files = cmd_result.FindIntByDottedPath("result.value");
if (!number_of_files)
return Status(kUnknownError, "DevTools didn't return value");
}
// Ask for each Runtime.RemoteObject and add them to the list
for (int i = 0; i < *number_of_files; i++) {
std::string file_object_id;
{
base::Value::Dict cmd_result;
base::Value::Dict params;
params.Set("functionDeclaration", "function() { return this.files[" +
base::NumberToString(i) + "] }");
params.Set("objectId", inner_remote_object_id);
status = client_->SendCommandAndGetResult("Runtime.callFunctionOn",
params, &cmd_result);
if (status.IsError())
return status;
std::string* object_id =
cmd_result.FindStringByDottedPath("result.objectId");
if (!object_id)
return Status(kUnknownError, "DevTools didn't return objectId");
file_object_id = std::move(*object_id);
}
// Now convert each RemoteObject into the full path
{
base::Value::Dict params;
params.Set("objectId", file_object_id);
base::Value::Dict get_file_info_result;
status = client_->SendCommandAndGetResult("DOM.getFileInfo", params,
&get_file_info_result);
if (status.IsError())
return status;
// Add the full path to the file_list
std::string* full_path = get_file_info_result.FindString("path");
if (!full_path)
return Status(kUnknownError, "DevTools didn't return path");
file_list.Append(std::move(*full_path));
}
}
}
// Now add the new files
for (const base::FilePath& file_path : files) {
if (!file_path.IsAbsolute()) {
return Status(kUnknownError,
"path is not absolute: " + file_path.AsUTF8Unsafe());
}
if (file_path.ReferencesParent()) {
return Status(kUnknownError,
"path is not canonical: " + file_path.AsUTF8Unsafe());
}
file_list.Append(file_path.AsUTF8Unsafe());
}
base::Value::Dict set_files_params;
set_files_params.Set("backendNodeId", backend_node_id);
set_files_params.Set("files", std::move(file_list));
return client_->SendCommand("DOM.setFileInputFiles", set_files_params);
}
Status WebViewImpl::TakeHeapSnapshot(std::unique_ptr<base::Value>* snapshot) {
return heap_snapshot_taker_->TakeSnapshot(snapshot);
}
Status WebViewImpl::InitProfileInternal() {
base::Value::Dict params;
return client_->SendCommand("Profiler.enable", params);
}
Status WebViewImpl::StopProfileInternal() {
base::Value::Dict params;
Status status_debug = client_->SendCommand("Debugger.disable", params);
Status status_profiler = client_->SendCommand("Profiler.disable", params);
if (status_debug.IsError())
return status_debug;
if (status_profiler.IsError())
return status_profiler;
return Status(kOk);
}
Status WebViewImpl::StartProfile() {
Status status_init = InitProfileInternal();
if (status_init.IsError())
return status_init;
base::Value::Dict params;
return client_->SendCommand("Profiler.start", params);
}
Status WebViewImpl::EndProfile(std::unique_ptr<base::Value>* profile_data) {
base::Value::Dict params;
base::Value::Dict profile_result;
Status status = client_->SendCommandAndGetResult("Profiler.stop", params,
&profile_result);
if (status.IsError()) {
Status disable_profile_status = StopProfileInternal();
if (disable_profile_status.IsError())
return disable_profile_status;
return status;
}
*profile_data = std::make_unique<base::Value>(std::move(profile_result));
return status;
}
Status WebViewImpl::SynthesizeTapGesture(int x,
int y,
int tap_count,
bool is_long_press) {
base::Value::Dict params;
params.Set("x", x);
params.Set("y", y);
params.Set("tapCount", tap_count);
if (is_long_press)
params.Set("duration", 1500);
return client_->SendCommand("Input.synthesizeTapGesture", params);
}
Status WebViewImpl::SynthesizeScrollGesture(int x,
int y,
int xoffset,
int yoffset) {
base::Value::Dict params;
params.Set("x", x);
params.Set("y", y);
// Chrome's synthetic scroll gesture is actually a "swipe" gesture, so the
// direction of the swipe is opposite to the scroll (i.e. a swipe up scrolls
// down, and a swipe left scrolls right).
params.Set("xDistance", -xoffset);
params.Set("yDistance", -yoffset);
return client_->SendCommand("Input.synthesizeScrollGesture", params);
}
Status WebViewImpl::CallAsyncFunctionInternal(
const std::string& frame,
const std::string& function,
const base::Value::List& args,
const base::TimeDelta& timeout,
std::unique_ptr<base::Value>* result) {
base::Value::List async_args;
async_args.Append("return (" + function + ").apply(null, arguments);");
async_args.Append(args.Clone());
/*is_user_supplied=*/
async_args.Append(true);
/*timeout=*/
async_args.Append(timeout.InMicrosecondsF());
std::unique_ptr<base::Value> tmp;
Timeout local_timeout(timeout);
std::unique_ptr<base::Value> query_value;
Status status = CallFunctionWithTimeout(frame, kExecuteAsyncScriptScript,
async_args, timeout, &query_value);
if (status.IsError()) {
return status;
}
base::Value::Dict* result_info = query_value->GetIfDict();
if (!result_info) {
return Status(kUnknownError, "async result info is not a dictionary");
}
std::optional<int> status_code = result_info->FindInt("status");
if (!status_code) {
return Status(kUnknownError, "async result info has no int 'status'");
}
if (*status_code != kOk) {
const std::string* message = result_info->FindString("value");
return Status(static_cast<StatusCode>(*status_code),
message ? *message : "");
}
base::Value* value = result_info->Find("value");
if (!value) {
return Status{kJavaScriptError,
"no value field in Reuntime.callFunctionOn result"};
}
*result = base::Value::ToUniquePtrValue(value->Clone());
return Status(kOk);
}
void WebViewImpl::SetFrame(const std::string& new_frame_id) {
if (!is_service_worker_)
navigation_tracker_->SetFrame(new_frame_id);
}
Status WebViewImpl::IsNotPendingNavigation(const std::string& frame_id,
const Timeout* timeout,
bool* is_not_pending) {
if (!frame_id.empty() && !frame_tracker_->IsKnownFrame(frame_id)) {
// Frame has already been destroyed.
*is_not_pending = true;
return Status(kOk);
}
bool is_pending = false;
Status status =
navigation_tracker_->IsPendingNavigation(timeout, &is_pending);
if (status.IsError())
return status;
// An alert may block the pending navigation.
if (client_->IsDialogOpen()) {
std::string alert_text;
status = client_->GetDialogMessage(alert_text);
if (status.IsError())
return Status(kUnexpectedAlertOpen);
return Status(kUnexpectedAlertOpen, "{Alert text : " + alert_text + "}");
}
*is_not_pending = !is_pending;
return Status(kOk);
}
bool WebViewImpl::IsNonBlocking() const {
if (navigation_tracker_)
return navigation_tracker_->IsNonBlocking();
return parent_->IsNonBlocking();
}
Status WebViewImpl::GetFedCmTracker(FedCmTracker** out_tracker) {
if (!fedcm_tracker_) {
fedcm_tracker_ = std::make_unique<FedCmTracker>(client_.get());
Status status = fedcm_tracker_->Enable(client_.get());
if (!status.IsOk()) {
fedcm_tracker_.reset();
return status;
}
}
*out_tracker = fedcm_tracker_.get();
return Status(kOk);
}
FrameTracker* WebViewImpl::GetFrameTracker() const {
return frame_tracker_.get();
}
const WebViewImpl* WebViewImpl::GetParent() const {
return parent_;
}
bool WebViewImpl::Lock() {
bool was_locked = is_locked_;
is_locked_ = true;
return was_locked;
}
void WebViewImpl::Unlock() {
is_locked_ = false;
}
bool WebViewImpl::IsLocked() const {
return is_locked_;
}
void WebViewImpl::SetDetached() {
is_detached_ = true;
client_->SetDetached();
}
bool WebViewImpl::IsDetached() const {
return is_detached_;
}
std::unique_ptr<base::Value> WebViewImpl::GetCastSinks() {
if (!cast_tracker_)
cast_tracker_ = std::make_unique<CastTracker>(client_.get());
HandleReceivedEvents();
return base::Value::ToUniquePtrValue(cast_tracker_->sinks().Clone());
}
std::unique_ptr<base::Value> WebViewImpl::GetCastIssueMessage() {
if (!cast_tracker_)
cast_tracker_ = std::make_unique<CastTracker>(client_.get());
HandleReceivedEvents();
return base::Value::ToUniquePtrValue(cast_tracker_->issue().Clone());
}
Status WebViewImpl::ResolveElementReferencesInPlace(
const std::string& expected_frame_id,
const std::string& context_id,
const std::string& object_group_name,
const std::string& expected_loader_id,
bool w3c_compliant,
const Timeout& timeout,
base::Value::Dict& arg_dict,
base::Value::List& nodes) {
Status status{kOk};
std::optional<std::string> maybe_key =
GetBackendNodeIdKey(arg_dict, w3c_compliant);
if (!maybe_key) {
for (auto it = arg_dict.begin(); status.IsOk() && it != arg_dict.end();
++it) {
status = ResolveElementReferencesInPlace(
expected_frame_id, context_id, object_group_name, expected_loader_id,
w3c_compliant, timeout, it->second, nodes);
}
return status;
}
// From this point 'key' can have either of the following two values:
// * ELEMENT_KEY ("ELEMENT" or "element-6066-11e4-a52e-4f735466cecf")
// * SHADOW_ROOT_KEY ("shadow-6066-11e4-a52e-4f735466cecf")
std::string key = *maybe_key;
ElementId maybe_element_id = GetElementId(arg_dict, key);
if (!maybe_element_id) {
return Status{kNoSuchElement, "invalid element id"};
}
const std::string& frame_id = maybe_element_id.frame_id;
const std::string& loader_id = maybe_element_id.loader_id;
int backend_node_id = maybe_element_id.backend_node_id;
// The following two conditionals mimic a weak map without storing any
// returned references. If the reference was indeed returned in this or in a
// previous navigation of the current frame then its frame id must coincide
// with the current frame id. Otherwise the reference is unknown for this
// frame.
if (frame_id != expected_frame_id) {
if (key == kShadowRootKey) {
// TODO (crbug.com/chromedriver/4379): solve the ambiguity.
// The following is not mentioned exactly by the standard as the
// definition "deserialize a shadow root" is not used anywhere.
// Still some WPT rely on this:
// * webdriver/tests/classic/execute_async_script/arguments.py
// :test_no_such_shadow_root_from_other_window_handle
// * webdriver/tests/classic/execute_script/arguments.py
// :test_no_such_shadow_root_from_other_window_handle
return Status{kNoSuchShadowRoot, "shadow root not found"};
} else {
return Status{kNoSuchElement, "element not found"};
}
}
// Any reference returned in the current navigation must have a matching
// loader id. Otherwise the reference is stale.
if (loader_id != expected_loader_id) {
if (key == kShadowRootKey) {
// TODO (crbug.com/chromedriver/4379): solve the ambiguity.
// This is also not stated in the standard however some WPT rely on this.
// We either need to fix the tests or the standard. Probably the later.
return Status{kDetachedShadowRoot, "detached shadow root not found"};
} else {
return Status{kStaleElementReference, "stale element not found"};
}
}
base::Value::Dict params;
base::Value::Dict resolve_result;
params.Set("backendNodeId", backend_node_id);
// TODO(crbug.com/chromedriver:4381): add support of uniqueContextId to
// DOM.resolveNode params.Set("uniqueContextId", context_id);
params.Set("objectGroup", object_group_name);
status = client_->SendCommandAndGetResultWithTimeout(
"DOM.resolveNode", params, &timeout, &resolve_result);
if (status.code() == kNoSuchElement) {
// If the node with given backend node id is not found then it was removed
// and therefore the reference is stale.
if (key == kShadowRootKey) {
return Status{kDetachedShadowRoot, "detached shadow root not found"};
} else {
return Status{kStaleElementReference, "stale element not found"};
}
}
if (status.IsError()) {
return status;
}
std::string* object_id =
resolve_result.FindStringByDottedPath("object.objectId");
if (!object_id) {
return Status{
kUnknownError,
"object.objectId is missing in the response to DOM.resolveNode"};
}
arg_dict.Set(std::move(key), static_cast<int>(nodes.size()));
base::Value::Dict node;
node.Set("objectId", std::move(*object_id));
nodes.Append(std::move(node));
return status;
}
Status WebViewImpl::ResolveElementReferencesInPlace(
const std::string& expected_frame_id,
const std::string& context_id,
const std::string& object_group_name,
const std::string& expected_loader_id,
bool w3c_compliant,
const Timeout& timeout,
base::Value::List& arg_list,
base::Value::List& nodes) {
Status status{kOk};
for (auto it = arg_list.begin(); status.IsOk() && it != arg_list.end();
++it) {
status = ResolveElementReferencesInPlace(
expected_frame_id, context_id, object_group_name, expected_loader_id,
w3c_compliant, timeout, *it, nodes);
}
return status;
}
Status WebViewImpl::ResolveElementReferencesInPlace(
const std::string& expected_frame_id,
const std::string& context_id,
const std::string& object_group_name,
const std::string& expected_loader_id,
bool w3c_compliant,
const Timeout& timeout,
base::Value& arg,
base::Value::List& nodes) {
if (arg.is_list()) {
return ResolveElementReferencesInPlace(
expected_frame_id, context_id, object_group_name, expected_loader_id,
w3c_compliant, timeout, arg.GetList(), nodes);
}
if (arg.is_dict()) {
return ResolveElementReferencesInPlace(
expected_frame_id, context_id, object_group_name, expected_loader_id,
w3c_compliant, timeout, arg.GetDict(), nodes);
}
return Status{kOk};
}
Status WebViewImpl::CreateElementReferences(const std::string& frame_id,
const std::string& loader_id,
const base::Value::List& nodes,
base::Value& res) {
Status status{kOk};
if (res.is_list()) {
base::Value::List& list = res.GetList();
for (base::Value& elem : list) {
status = CreateElementReferences(frame_id, loader_id, nodes, elem);
if (status.IsError()) {
return status;
}
}
return status;
}
if (res.is_dict()) {
base::Value::Dict& dict = res.GetDict();
std::optional<std::string> maybe_key =
GetBackendNodeIdKey(dict, w3c_compliant_);
if (maybe_key) {
std::optional<int> maybe_node_idx = dict.FindInt(*maybe_key);
if (!maybe_node_idx) {
return Status{kUnknownError, "node index is missing"};
}
if (*maybe_node_idx < 0 ||
static_cast<size_t>(*maybe_node_idx) >= nodes.size()) {
return Status{kUnknownError, "node index is out of range"};
}
if (!nodes[*maybe_node_idx].is_dict()) {
return Status{kUnknownError, "serialized node is not a dictionary"};
}
const base::Value::Dict& node = nodes[*maybe_node_idx].GetDict();
std::optional<int> maybe_backend_node_id =
node.FindIntByDottedPath("value.backendNodeId");
if (!maybe_backend_node_id) {
return Status{kUnknownError, "backendNodeId is missing in a node"};
}
std::string shared_id =
base::StringPrintf("f.%s.d.%s.e.%d", frame_id.c_str(),
loader_id.c_str(), *maybe_backend_node_id);
dict.Set(std::move(*maybe_key), std::move(shared_id));
return status;
}
for (auto p : dict) {
status = CreateElementReferences(frame_id, loader_id, nodes, p.second);
if (status.IsError()) {
return status;
}
}
}
return status;
}
bool WebViewImpl::IsDialogOpen() const {
return client_->IsDialogOpen();
}
Status WebViewImpl::GetDialogMessage(std::string& message) const {
return client_->GetDialogMessage(message);
}
Status WebViewImpl::GetTypeOfDialog(std::string& type) const {
return client_->GetTypeOfDialog(type);
}
Status WebViewImpl::HandleDialog(bool accept,
const std::optional<std::string>& text) {
return client_->HandleDialog(accept, text);
}
WebViewImplHolder::WebViewImplHolder(WebViewImpl* web_view) {
// Lock input web view and all its parents, to prevent them from being
// deleted while still in use. Inside |items_|, each web view must appear
// before its parent. This ensures the destructor unlocks the web views in
// the right order.
while (web_view != nullptr) {
Item item;
item.web_view = web_view;
item.was_locked = web_view->Lock();
items_.push_back(item);
web_view = const_cast<WebViewImpl*>(web_view->GetParent());
}
}
WebViewImplHolder::~WebViewImplHolder() {
for (Item& item : items_) {
// Once we find a web view that is still locked, then all its parents must
// also be locked.
if (item.was_locked)
break;
WebViewImpl* web_view = item.web_view;
if (!web_view->IsDetached()) {
web_view->Unlock();
} else if (web_view->GetParent() != nullptr) {
web_view->GetParent()->GetFrameTracker()->DeleteTargetForFrame(
web_view->GetId());
}
}
}
namespace internal {
Status EvaluateScript(DevToolsClient* client,
const std::string& context_id,
const std::string& expression,
const base::TimeDelta& timeout,
const bool await_promise,
base::Value::Dict& result) {
Status status{kOk};
base::Value::Dict params;
params.Set("expression", expression);
if (!context_id.empty()) {
params.Set("uniqueContextId", context_id);
}
params.Set("returnByValue", true);
params.Set("awaitPromise", await_promise);
base::Value::Dict cmd_result;
Timeout local_timeout(timeout);
status = client->SendCommandAndGetResultWithTimeout(
"Runtime.evaluate", params, &local_timeout, &cmd_result);
if (status.IsError())
return status;
if (cmd_result.contains("exceptionDetails")) {
std::string description = "unknown";
if (const std::string* maybe_description =
cmd_result.FindStringByDottedPath("result.description")) {
description = *maybe_description;
}
return Status(kUnknownError,
"Runtime.evaluate threw exception: " + description);
}
base::Value::Dict* unscoped_result = cmd_result.FindDict("result");
if (!unscoped_result)
return Status(kUnknownError, "evaluate missing dictionary 'result'");
result = std::move(*unscoped_result);
return status;
}
Status EvaluateScriptAndGetValue(DevToolsClient* client,
const std::string& context_id,
const std::string& expression,
const base::TimeDelta& timeout,
const bool await_promise,
std::unique_ptr<base::Value>* result) {
base::Value::Dict temp_result;
Status status = EvaluateScript(client, context_id, expression, timeout,
await_promise, temp_result);
if (status.IsError())
return status;
std::string* type = temp_result.FindString("type");
if (!type)
return Status(kUnknownError, "Runtime.evaluate missing string 'type'");
if (*type == "undefined") {
*result = std::make_unique<base::Value>();
} else {
std::optional<base::Value> value = temp_result.Extract("value");
if (!value)
return Status(kUnknownError, "Runtime.evaluate missing 'value'");
*result = base::Value::ToUniquePtrValue(std::move(*value));
}
return Status(kOk);
}
Status ParseCallFunctionResult(const base::Value& temp_result,
std::unique_ptr<base::Value>* result) {
const base::Value::Dict* dict = temp_result.GetIfDict();
if (!dict)
return Status(kUnknownError, "call function result must be a dictionary");
std::optional<int> status_code = dict->FindInt("status");
if (!status_code) {
return Status(kUnknownError,
"call function result missing int 'status'");
}
if (*status_code != kOk) {
const std::string* message = dict->FindString("value");
return Status(static_cast<StatusCode>(*status_code),
message ? *message : "");
}
const base::Value* unscoped_value = dict->Find("value");
if (unscoped_value == nullptr) {
// Missing 'value' indicates the JavaScript code didn't return a value.
return Status(kOk);
}
*result = base::Value::ToUniquePtrValue(unscoped_value->Clone());
return Status(kOk);
}
} // namespace internal