blob: 9aba316a10cb4f2962e20963f69d7ed7d0fa65c6 [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/element_commands.h"
#include <stddef.h>
#include <cmath>
#include <memory>
#include <set>
#include <string>
#include <unordered_set>
#include <vector>
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/functional/callback.h"
#include "base/logging.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/values.h"
#include "chrome/test/chromedriver/basic_types.h"
#include "chrome/test/chromedriver/chrome/chrome.h"
#include "chrome/test/chromedriver/chrome/js.h"
#include "chrome/test/chromedriver/chrome/status.h"
#include "chrome/test/chromedriver/chrome/ui_events.h"
#include "chrome/test/chromedriver/chrome/web_view.h"
#include "chrome/test/chromedriver/constants/version.h"
#include "chrome/test/chromedriver/element_util.h"
#include "chrome/test/chromedriver/session.h"
#include "chrome/test/chromedriver/util.h"
#include "third_party/selenium-atoms/atoms.h"
const int kFlickTouchEventsPerSecond = 30;
const std::set<std::string> kTextControlTypes = {"text", "search", "tel", "url",
"password"};
const std::set<std::string> kInputControlTypes = {
"text", "search", "url", "tel", "email",
"password", "date", "month", "week", "time",
"datetime-local", "number", "range", "color", "file"};
const std::set<std::string> kNontypeableControlTypes = {"color"};
const std::unordered_set<std::string> kBooleanAttributes = {
"allowfullscreen",
"allowpaymentrequest",
"allowusermedia",
"async",
"autofocus",
"autoplay",
"checked",
"compact",
"complete",
"controls",
"declare",
"default",
"defaultchecked",
"defaultselected",
"defer",
"disabled",
"ended",
"formnovalidate",
"hidden",
"indeterminate",
"iscontenteditable",
"ismap",
"itemscope",
"loop",
"multiple",
"muted",
"nohref",
"nomodule",
"noresize",
"noshade",
"novalidate",
"nowrap",
"open",
"paused",
"playsinline",
"pubdate",
"readonly",
"required",
"reversed",
"scoped",
"seamless",
"seeking",
"selected",
"truespeed",
"typemustmatch",
"willvalidate"};
namespace {
Status FocusToElement(
Session* session,
WebView* web_view,
const std::string& element_id) {
Status status{kOk};
bool is_displayed = false;
bool is_focused = false;
base::TimeTicks start_time = base::TimeTicks::Now();
while (true) {
status = IsElementDisplayed(
session, web_view, element_id, true, &is_displayed);
if (status.IsError())
return status;
if (is_displayed)
break;
status = IsElementFocused(session, web_view, element_id, &is_focused);
if (status.IsError())
return status;
if (is_focused)
break;
if (base::TimeTicks::Now() - start_time >= session->implicit_wait) {
return Status(kElementNotVisible);
}
base::PlatformThread::Sleep(base::Milliseconds(100));
}
bool is_enabled = false;
status = IsElementEnabled(session, web_view, element_id, &is_enabled);
if (status.IsError())
return status;
if (!is_enabled)
return Status(kInvalidElementState);
if (!is_focused) {
base::Value::List args;
args.Append(CreateElement(element_id));
std::unique_ptr<base::Value> result;
status = web_view->CallFunction(
session->GetCurrentFrameId(), kFocusScript, args, &result);
if (status.IsError())
return status;
}
return Status(kOk);
}
Status SendKeysToElement(Session* session,
WebView* web_view,
const std::string& element_id,
const bool is_text,
const base::Value::List* key_list) {
// If we were previously focused, we don't need to focus again.
// But also, later we don't move the carat if we were already in focus.
// However, non-text elements such as contenteditable elements needs to be
// focused to ensure the keys will end up being sent to the correct place.
// So in the case of non-text elements, we still focusToElement.
bool was_previously_focused = false;
IsElementFocused(session, web_view, element_id, &was_previously_focused);
if (!was_previously_focused || !is_text) {
Status status = FocusToElement(session, web_view, element_id);
if (status.IsError())
return Status(kElementNotInteractable);
}
// Move cursor/caret to append the input if we only just focused this
// element. keys if element's type is text-related
if (is_text && !was_previously_focused) {
base::Value::List args;
args.Append(CreateElement(element_id));
std::unique_ptr<base::Value> result;
Status status = web_view->CallFunction(
session->GetCurrentFrameId(),
"elem => elem.setSelectionRange(elem.value.length, elem.value.length)",
args, &result);
if (status.IsError())
return status;
}
return SendKeysOnWindow(web_view, key_list, true, &session->sticky_modifiers);
}
} // namespace
Status ExecuteElementCommand(const ElementCommand& command,
Session* session,
WebView* web_view,
const base::Value::Dict& params,
std::unique_ptr<base::Value>* value,
Timeout* timeout) {
const std::string* id = params.FindString("id");
if (!id)
id = params.FindString("element");
if (id) {
return command.Run(session, web_view, *id, params, value);
}
return Status(kInvalidArgument, "element identifier must be a string");
}
Status ExecuteFindChildElement(int interval_ms,
Session* session,
WebView* web_view,
const std::string& element_id,
const base::Value::Dict& params,
std::unique_ptr<base::Value>* value) {
return FindElement(
interval_ms, true, &element_id, session, web_view, params, value);
}
Status ExecuteFindChildElementFromShadowRoot(
int interval_ms,
Session* session,
WebView* web_view,
const std::string& shadow_root_id,
const base::Value::Dict& params,
std::unique_ptr<base::Value>* value) {
return FindShadowElement(interval_ms, true, &shadow_root_id, session,
web_view, params, value);
}
Status ExecuteFindChildElementsFromShadowRoot(
int interval_ms,
Session* session,
WebView* web_view,
const std::string& shadow_root_id,
const base::Value::Dict& params,
std::unique_ptr<base::Value>* value) {
return FindShadowElement(interval_ms, false, &shadow_root_id, session,
web_view, params, value);
}
Status ExecuteGetElementShadowRoot(Session* session,
WebView* web_view,
const std::string& element_id,
const base::Value::Dict& params,
std::unique_ptr<base::Value>* value) {
base::Value::List args;
args.Append(CreateElement(element_id));
Status status = web_view->CallFunction(
session->GetCurrentFrameId(),
"function(elem) { return elem.shadowRoot; }", args, value);
if (status.IsError()) {
if (status.message().find("no such shadow root") != std::string::npos) {
return Status(kNoSuchShadowRoot);
}
return status;
}
if (value->get()->is_none()) {
return Status(kNoSuchShadowRoot);
}
return status;
}
Status ExecuteFindChildElements(int interval_ms,
Session* session,
WebView* web_view,
const std::string& element_id,
const base::Value::Dict& params,
std::unique_ptr<base::Value>* value) {
return FindElement(
interval_ms, false, &element_id, session, web_view, params, value);
}
Status ExecuteClickElement(Session* session,
WebView* web_view,
const std::string& element_id,
const base::Value::Dict& params,
std::unique_ptr<base::Value>* value) {
std::string tag_name;
Status status = GetElementTagName(session, web_view, element_id, &tag_name);
if (status.IsError())
return status;
if (tag_name == "option") {
bool is_toggleable;
status = IsOptionElementTogglable(
session, web_view, element_id, &is_toggleable);
if (status.IsError())
return status;
if (is_toggleable)
return ToggleOptionElement(session, web_view, element_id);
return SetOptionElementSelected(session, web_view, element_id, true);
}
if (tag_name == "input") {
std::unique_ptr<base::Value> get_element_type;
status = GetElementAttribute(session, web_view, element_id, "type",
&get_element_type);
if (status.IsError())
return status;
std::string element_type;
if (get_element_type->is_string())
element_type = base::ToLowerASCII(get_element_type->GetString());
if (element_type == "file")
return Status(kInvalidArgument);
}
WebPoint location;
status =
GetElementClickableLocation(session, web_view, element_id, &location);
if (status.IsError())
return status;
std::vector<MouseEvent> events;
events.push_back(MouseEvent(kMovedMouseEventType, kNoneMouseButton,
location.x, location.y, session->sticky_modifiers,
0, 0));
events.push_back(MouseEvent(kPressedMouseEventType, kLeftMouseButton,
location.x, location.y, session->sticky_modifiers,
0, 1));
events.push_back(MouseEvent(kReleasedMouseEventType, kLeftMouseButton,
location.x, location.y, session->sticky_modifiers,
1, 1));
status = web_view->DispatchMouseEvents(events, session->GetCurrentFrameId(),
false);
if (status.IsOk())
session->mouse_position = location;
return status;
}
Status ExecuteTouchSingleTap(Session* session,
WebView* web_view,
const std::string& element_id,
const base::Value::Dict& params,
std::unique_ptr<base::Value>* value) {
WebPoint location;
Status status = GetElementClickableLocation(
session, web_view, element_id, &location);
if (status.IsError())
return status;
if (!session->chrome->HasTouchScreen()) {
// TODO(samuong): remove this once we stop supporting M44.
std::vector<TouchEvent> events;
events.push_back(
TouchEvent(kTouchStart, location.x, location.y));
events.push_back(
TouchEvent(kTouchEnd, location.x, location.y));
return web_view->DispatchTouchEvents(events, false);
}
return web_view->SynthesizeTapGesture(location.x, location.y, 1, false);
}
Status ExecuteTouchDoubleTap(Session* session,
WebView* web_view,
const std::string& element_id,
const base::Value::Dict& params,
std::unique_ptr<base::Value>* value) {
if (!session->chrome->HasTouchScreen()) {
// TODO(samuong): remove this once we stop supporting M44.
return Status(kUnknownCommand, "Double tap command requires Chrome 44+");
}
WebPoint location;
Status status = GetElementClickableLocation(
session, web_view, element_id, &location);
if (status.IsError())
return status;
return web_view->SynthesizeTapGesture(location.x, location.y, 2, false);
}
Status ExecuteTouchLongPress(Session* session,
WebView* web_view,
const std::string& element_id,
const base::Value::Dict& params,
std::unique_ptr<base::Value>* value) {
if (!session->chrome->HasTouchScreen()) {
// TODO(samuong): remove this once we stop supporting M44.
return Status(kUnknownCommand, "Long press command requires Chrome 44+");
}
WebPoint location;
Status status = GetElementClickableLocation(
session, web_view, element_id, &location);
if (status.IsError())
return status;
return web_view->SynthesizeTapGesture(location.x, location.y, 1, true);
}
Status ExecuteFlick(Session* session,
WebView* web_view,
const std::string& element_id,
const base::Value::Dict& params,
std::unique_ptr<base::Value>* value) {
WebPoint location;
Status status = GetElementClickableLocation(
session, web_view, element_id, &location);
if (status.IsError())
return status;
int xoffset, yoffset, speed;
std::optional<int> maybe_xoffset = params.FindInt("xoffset");
if (!maybe_xoffset)
return Status(kInvalidArgument, "'xoffset' must be an integer");
xoffset = *maybe_xoffset;
std::optional<int> maybe_yoffset = params.FindInt("yoffset");
if (!maybe_yoffset)
return Status(kInvalidArgument, "'yoffset' must be an integer");
yoffset = *maybe_yoffset;
speed = params.FindInt("speed").value_or(-1);
if (speed < 1)
return Status(kInvalidArgument, "'speed' must be a positive integer");
status = web_view->DispatchTouchEvent(
TouchEvent(kTouchStart, location.x, location.y), false);
if (status.IsError())
return status;
const double offset =
std::sqrt(static_cast<double>(xoffset * xoffset + yoffset * yoffset));
const double xoffset_per_event =
(speed * xoffset) / (kFlickTouchEventsPerSecond * offset);
const double yoffset_per_event =
(speed * yoffset) / (kFlickTouchEventsPerSecond * offset);
const int total_events =
(offset * kFlickTouchEventsPerSecond) / speed;
for (int i = 0; i < total_events; i++) {
status = web_view->DispatchTouchEvent(
TouchEvent(kTouchMove, location.x + xoffset_per_event * i,
location.y + yoffset_per_event * i),
false);
if (status.IsError())
return status;
base::PlatformThread::Sleep(
base::Milliseconds(1000 / kFlickTouchEventsPerSecond));
}
return web_view->DispatchTouchEvent(
TouchEvent(kTouchEnd, location.x + xoffset, location.y + yoffset), false);
}
Status ExecuteClearElement(Session* session,
WebView* web_view,
const std::string& element_id,
const base::Value::Dict& params,
std::unique_ptr<base::Value>* value) {
std::string tag_name;
Status status = GetElementTagName(session, web_view, element_id, &tag_name);
if (status.IsError())
return status;
bool is_input_control = false;
if (tag_name == "input") {
std::unique_ptr<base::Value> get_element_type;
status = GetElementAttribute(session, web_view, element_id, "type",
&get_element_type);
if (status.IsError())
return status;
std::string element_type;
if (get_element_type->is_string())
element_type = base::ToLowerASCII(get_element_type->GetString());
is_input_control =
kInputControlTypes.find(element_type) != kInputControlTypes.end();
}
bool is_text = tag_name == "textarea";
bool is_content_editable = false;
if (!is_text && !is_input_control) {
std::unique_ptr<base::Value> get_content_editable;
base::Value::List args;
args.Append(CreateElement(element_id));
status = web_view->CallFunction(session->GetCurrentFrameId(),
"element => element.isContentEditable",
args, &get_content_editable);
if (status.IsError())
return status;
is_content_editable = get_content_editable->GetIfBool().value_or(false);
}
std::unique_ptr<base::Value> get_readonly;
bool is_readonly = false;
base::Value::Dict params_readOnly;
if (!is_content_editable) {
params_readOnly.Set("name", "readOnly");
status = ExecuteGetElementProperty(session, web_view, element_id,
params_readOnly, &get_readonly);
if (status.IsError())
return status;
is_readonly = get_readonly->GetIfBool().value_or(false);
}
bool is_editable =
(is_input_control || is_text || is_content_editable) && !is_readonly;
if (!is_editable)
return Status(kInvalidElementState);
// Scrolling to element is done by webdriver::atoms::CLEAR
bool is_displayed = false;
base::TimeTicks start_time = base::TimeTicks::Now();
while (true) {
status = IsElementDisplayed(
session, web_view, element_id, true, &is_displayed);
if (status.IsError())
return status;
if (is_displayed)
break;
if (base::TimeTicks::Now() - start_time >= session->implicit_wait) {
return Status(kElementNotVisible);
}
base::PlatformThread::Sleep(base::Milliseconds(50));
}
static bool is_clear_warning_notified = false;
if (!is_clear_warning_notified) {
VLOG(0) << "\n\t=== NOTE: ===\n"
<< "\tThe Clear command in " << kChromeDriverProductShortName
<< " 2.43 and above\n"
<< "\thas been updated to conform to the current standard,\n"
<< "\tincluding raising blur event after clearing.\n";
is_clear_warning_notified = true;
}
base::Value::List args;
args.Append(CreateElement(element_id));
std::unique_ptr<base::Value> result;
return web_view->CallFunction(
session->GetCurrentFrameId(),
webdriver::atoms::asString(webdriver::atoms::CLEAR),
args, &result);
}
Status ExecuteSendKeysToElement(Session* session,
WebView* web_view,
const std::string& element_id,
const base::Value::Dict& params,
std::unique_ptr<base::Value>* value) {
const base::Value::List* key_list;
base::Value::List key_list_local;
const base::Value* text = nullptr;
if (session->w3c_compliant) {
text = params.Find("text");
if (text == nullptr || !text->is_string())
return Status(kInvalidArgument, "'text' must be a string");
key_list_local.Append(text->Clone());
key_list = &key_list_local;
} else {
key_list = params.FindList("value");
if (key_list == nullptr) {
return Status(kInvalidArgument, "'value' must be a list");
}
}
bool is_input = false;
Status status = IsElementAttributeEqualToIgnoreCase(
session, web_view, element_id, "tagName", "input", &is_input);
if (status.IsError())
return status;
std::unique_ptr<base::Value> get_element_type;
status = GetElementAttribute(session, web_view, element_id, "type",
&get_element_type);
if (status.IsError())
return status;
std::string element_type;
if (get_element_type->is_string())
element_type = base::ToLowerASCII(get_element_type->GetString());
bool is_file = element_type == "file";
bool is_nontypeable = kNontypeableControlTypes.find(element_type) !=
kNontypeableControlTypes.end();
if (is_input && is_file) {
if (session->strict_file_interactability) {
status = FocusToElement(session, web_view,element_id);
if (status.IsError())
return status;
}
// Compress array into a single string.
std::string paths_string;
for (const base::Value& i : *key_list) {
const std::string* path_part = i.GetIfString();
if (!path_part)
return Status(kInvalidArgument, "'value' is invalid");
paths_string.append(*path_part);
}
// w3c spec specifies empty path_part should throw invalidArgument error
if (paths_string.empty())
return Status(kInvalidArgument, "'text' is empty");
ChromeDesktopImpl* chrome_desktop = nullptr;
bool is_desktop = session->chrome->GetAsDesktop(&chrome_desktop).IsOk();
// Separate the string into separate paths, delimited by '\n'.
std::vector<base::FilePath> paths;
for (const auto& path_piece : base::SplitStringPiece(
paths_string, "\n", base::TRIM_WHITESPACE, base::SPLIT_WANT_ALL)) {
// For local desktop browser, verify that the file exists.
// No easy way to do that for remote or mobile browser.
if (is_desktop &&
!base::PathExists(base::FilePath::FromUTF8Unsafe(path_piece))) {
return Status(
kInvalidArgument,
base::StringPrintf(
"File not found : %" PRFilePath,
base::FilePath::FromUTF8Unsafe(path_piece).value().c_str()));
}
paths.push_back(base::FilePath::FromUTF8Unsafe(path_piece));
}
bool multiple = false;
status = IsElementAttributeEqualToIgnoreCase(
session, web_view, element_id, "multiple", "true", &multiple);
if (status.IsError())
return status;
if (!multiple && paths.size() > 1) {
return Status(kInvalidArgument,
"the element can not hold multiple files");
}
base::Value element = CreateElement(element_id);
return web_view->SetFileInputFiles(session->GetCurrentFrameId(), element,
paths, multiple);
}
if (session->w3c_compliant && is_input && is_nontypeable) {
// Special handling for non-typeable inputs is only included in W3C Spec
// The Spec calls for returning element not interactable if the element
// has no value property, but this is included for all input elements, so
// no check is needed here.
// text is set only when session.w3c_compliant, so confirm here
DCHECK(text != nullptr);
base::Value::List args;
args.Append(CreateElement(element_id));
args.Append(text->GetString());
std::unique_ptr<base::Value> result;
// Set value to text as given by user; if this does not match the defined
// format for the input type, results are not defined
return web_view->CallFunction(session->GetCurrentFrameId(),
"(element, text) => element.value = text",
args, &result);
}
std::unique_ptr<base::Value> get_content_editable;
base::Value::List args;
args.Append(CreateElement(element_id));
status = web_view->CallFunction(session->GetCurrentFrameId(),
"element => element.isContentEditable", args,
&get_content_editable);
if (status.IsError())
return status;
// If element_type is in kTextControlTypes, sendKeys should append
bool is_text_control_type =
is_input &&
kTextControlTypes.find(element_type) != kTextControlTypes.end();
// If the element is a textarea, sendKeys should also append
bool is_textarea = false;
status = IsElementAttributeEqualToIgnoreCase(
session, web_view, element_id, "tagName", "textarea", &is_textarea);
if (status.IsError())
return status;
bool is_text = is_text_control_type || is_textarea;
if (get_content_editable->is_bool() && get_content_editable->GetBool()) {
// If element is contentEditable
// check if element is focused
bool is_focused = false;
status = IsElementFocused(session, web_view, element_id, &is_focused);
if (status.IsError())
return status;
// Get top level contentEditable element
std::unique_ptr<base::Value> result;
status = web_view->CallFunction(session->GetCurrentFrameId(),
"function(element) {"
"while (element.parentElement && "
"element.parentElement.isContentEditable) {"
" element = element.parentElement;"
" }"
"return element;"
"}",
args, &result);
if (status.IsError())
return status;
const base::Value::Dict* element_dict = result->GetIfDict();
const std::string* top_element_id =
element_dict ? element_dict->FindString(GetElementKey()) : nullptr;
if (!top_element_id)
return Status(kUnknownError, "no element reference returned by script");
// check if top level contentEditable element is focused
bool is_top_focused = false;
status =
IsElementFocused(session, web_view, *top_element_id, &is_top_focused);
if (status.IsError())
return status;
// If is_text we want to send keys to the element
// Otherwise, send keys to the top element
if ((is_text && !is_focused) || (!is_text && !is_top_focused)) {
// If element does not currentley have focus
// will move caret
// at end of element text. W3C mandates that the
// caret be moved "after any child content"
// Set selection using the element itself
std::unique_ptr<base::Value> unused;
status = web_view->CallFunction(session->GetCurrentFrameId(),
"function(element) {"
"var range = document.createRange();"
"range.selectNodeContents(element);"
"range.collapse();"
"var sel = window.getSelection();"
"sel.removeAllRanges();"
"sel.addRange(range);"
"}",
args, &unused);
if (status.IsError())
return status;
}
// Use top level element id for the purpose of focusing
if (!is_text) {
return SendKeysToElement(session, web_view, *top_element_id, is_text,
key_list);
}
}
return SendKeysToElement(session, web_view, element_id, is_text, key_list);
}
Status ExecuteSubmitElement(Session* session,
WebView* web_view,
const std::string& element_id,
const base::Value::Dict& params,
std::unique_ptr<base::Value>* value) {
base::Value::List args;
args.Append(CreateElement(element_id));
return web_view->CallFunction(
session->GetCurrentFrameId(),
webdriver::atoms::asString(webdriver::atoms::SUBMIT),
args,
value);
}
Status ExecuteGetElementText(Session* session,
WebView* web_view,
const std::string& element_id,
const base::Value::Dict& params,
std::unique_ptr<base::Value>* value) {
base::Value::List args;
args.Append(CreateElement(element_id));
return web_view->CallFunction(
session->GetCurrentFrameId(),
webdriver::atoms::asString(webdriver::atoms::GET_TEXT),
args,
value);
}
Status ExecuteGetElementValue(Session* session,
WebView* web_view,
const std::string& element_id,
const base::Value::Dict& params,
std::unique_ptr<base::Value>* value) {
base::Value::List args;
args.Append(CreateElement(element_id));
return web_view->CallFunction(
session->GetCurrentFrameId(),
"function(elem) { return elem['value'] }",
args,
value);
}
Status ExecuteGetElementProperty(Session* session,
WebView* web_view,
const std::string& element_id,
const base::Value::Dict& params,
std::unique_ptr<base::Value>* value) {
base::Value::List args;
args.Append(CreateElement(element_id));
const std::string* name = params.FindString("name");
if (!name)
return Status(kInvalidArgument, "missing 'name'");
args.Append(*name);
return web_view->CallFunction(
session->GetCurrentFrameId(),
"function(elem, name) { return elem[name] }",
args,
value);
}
Status ExecuteGetElementTagName(Session* session,
WebView* web_view,
const std::string& element_id,
const base::Value::Dict& params,
std::unique_ptr<base::Value>* value) {
base::Value::List args;
args.Append(CreateElement(element_id));
return web_view->CallFunction(
session->GetCurrentFrameId(),
"function(elem) { return elem.tagName.toLowerCase() }",
args,
value);
}
Status ExecuteIsElementSelected(Session* session,
WebView* web_view,
const std::string& element_id,
const base::Value::Dict& params,
std::unique_ptr<base::Value>* value) {
base::Value::List args;
args.Append(CreateElement(element_id));
return web_view->CallFunction(
session->GetCurrentFrameId(),
webdriver::atoms::asString(webdriver::atoms::IS_SELECTED),
args,
value);
}
Status ExecuteIsElementEnabled(Session* session,
WebView* web_view,
const std::string& element_id,
const base::Value::Dict& params,
std::unique_ptr<base::Value>* value) {
base::Value::List args;
args.Append(CreateElement(element_id));
bool is_xml = false;
Status status = IsDocumentTypeXml(session, web_view, &is_xml);
if (status.IsError())
return status;
if (is_xml) {
*value = std::make_unique<base::Value>(false);
return Status(kOk);
}
return web_view->CallFunction(
session->GetCurrentFrameId(),
webdriver::atoms::asString(webdriver::atoms::IS_ENABLED), args, value);
}
Status ExecuteGetComputedLabel(Session* session,
WebView* web_view,
const std::string& element_id,
const base::Value::Dict& params,
std::unique_ptr<base::Value>* value) {
std::unique_ptr<base::Value> ax_node;
Status status = GetAXNodeByElementId(session, web_view, element_id, &ax_node);
if (status.IsError())
return status;
// Computed label stores as `name` in the AXTree.
base::Value::Dict* name_node = ax_node->GetDict().FindDict("name");
if (!name_node) {
// No computed label found. Return empty string.
*value = std::make_unique<base::Value>("");
return Status(kOk);
}
std::optional<base::Value> name_val = name_node->Extract("value");
if (!name_val)
return Status(kUnknownError,
"No name value found in the node in CDP response");
*value = std::make_unique<base::Value>(std::move(*name_val));
return Status(kOk);
}
Status ExecuteGetComputedRole(Session* session,
WebView* web_view,
const std::string& element_id,
const base::Value::Dict& params,
std::unique_ptr<base::Value>* value) {
std::unique_ptr<base::Value> ax_node;
Status status = GetAXNodeByElementId(session, web_view, element_id, &ax_node);
if (status.IsError())
return status;
base::Value::Dict* role_node = ax_node->GetDict().FindDict("role");
if (!role_node) {
// No computed role found. Return empty string.
*value = std::make_unique<base::Value>("");
return Status(kOk);
}
std::optional<base::Value> role_val = role_node->Extract("value");
if (!role_val) {
return Status(kUnknownError,
"No role value found in the node in CDP response");
}
*value = std::make_unique<base::Value>(std::move(*role_val));
return Status(kOk);
}
Status ExecuteIsElementDisplayed(Session* session,
WebView* web_view,
const std::string& element_id,
const base::Value::Dict& params,
std::unique_ptr<base::Value>* value) {
base::Value::List args;
args.Append(CreateElement(element_id));
return web_view->CallFunction(
session->GetCurrentFrameId(),
webdriver::atoms::asString(webdriver::atoms::IS_DISPLAYED),
args,
value);
}
Status ExecuteGetElementLocation(Session* session,
WebView* web_view,
const std::string& element_id,
const base::Value::Dict& params,
std::unique_ptr<base::Value>* value) {
base::Value::List args;
args.Append(CreateElement(element_id));
return web_view->CallFunction(
session->GetCurrentFrameId(),
webdriver::atoms::asString(webdriver::atoms::GET_LOCATION),
args,
value);
}
Status ExecuteGetElementRect(Session* session,
WebView* web_view,
const std::string& element_id,
const base::Value::Dict& params,
std::unique_ptr<base::Value>* value) {
base::Value::List args;
args.Append(CreateElement(element_id));
std::unique_ptr<base::Value> location;
Status status = web_view->CallFunction(
session->GetCurrentFrameId(),
webdriver::atoms::asString(webdriver::atoms::GET_LOCATION), args,
&location);
if (status.IsError())
return status;
std::unique_ptr<base::Value> size;
status = web_view->CallFunction(
session->GetCurrentFrameId(),
webdriver::atoms::asString(webdriver::atoms::GET_SIZE), args, &size);
if (status.IsError())
return status;
// do type conversions
base::Value::Dict* size_dict = size->GetIfDict();
if (!size_dict)
return Status(kUnknownError, "could not convert to Value::Dict");
base::Value::Dict* location_dict = location->GetIfDict();
if (!location_dict)
return Status(kUnknownError, "could not convert to Value::Dict");
// grab values
std::optional<double> maybe_x = location_dict->FindDouble("x");
if (!maybe_x.has_value())
return Status(kUnknownError, "x coordinate is missing in element location");
std::optional<double> maybe_y = location_dict->FindDouble("y");
if (!maybe_y.has_value())
return Status(kUnknownError, "y coordinate is missing in element location");
std::optional<double> maybe_height = size_dict->FindDouble("height");
if (!maybe_height.has_value())
return Status(kUnknownError, "height is missing in element size");
std::optional<double> maybe_width = size_dict->FindDouble("width");
if (!maybe_width.has_value())
return Status(kUnknownError, "width is missing in element size");
base::Value::Dict ret;
ret.Set("x", maybe_x.value());
ret.Set("y", maybe_y.value());
ret.Set("width", maybe_width.value());
ret.Set("height", maybe_height.value());
*value = std::make_unique<base::Value>(std::move(ret));
return Status(kOk);
}
Status ExecuteGetElementLocationOnceScrolledIntoView(
Session* session,
WebView* web_view,
const std::string& element_id,
const base::Value::Dict& params,
std::unique_ptr<base::Value>* value) {
WebPoint offset(0, 0);
WebPoint location;
Status status = ScrollElementIntoView(
session, web_view, element_id, &offset, &location);
if (status.IsError())
return status;
*value = std::make_unique<base::Value>(CreateValueFrom(location));
return Status(kOk);
}
Status ExecuteGetElementSize(Session* session,
WebView* web_view,
const std::string& element_id,
const base::Value::Dict& params,
std::unique_ptr<base::Value>* value) {
base::Value::List args;
args.Append(CreateElement(element_id));
return web_view->CallFunction(
session->GetCurrentFrameId(),
webdriver::atoms::asString(webdriver::atoms::GET_SIZE),
args,
value);
}
Status ExecuteGetElementAttribute(Session* session,
WebView* web_view,
const std::string& element_id,
const base::Value::Dict& params,
std::unique_ptr<base::Value>* value) {
const std::string* attribute_name = params.FindString("name");
if (!attribute_name)
return Status(kInvalidArgument, "missing 'name'");
// In legacy mode, use old behavior for backward compatibility.
if (!session->w3c_compliant) {
return GetElementAttribute(session, web_view, element_id, *attribute_name,
value);
}
base::Value::List args;
args.Append(CreateElement(element_id));
args.Append(*attribute_name);
return web_view->CallFunction(
session->GetCurrentFrameId(),
kBooleanAttributes.count(base::ToLowerASCII(*attribute_name))
? "(elem, attribute) => elem.hasAttribute(attribute) ? 'true' : null"
: "(elem, attribute) => elem.getAttribute(attribute)",
args, value);
}
Status ExecuteGetElementValueOfCSSProperty(
Session* session,
WebView* web_view,
const std::string& element_id,
const base::Value::Dict& params,
std::unique_ptr<base::Value>* value) {
bool is_xml = false;
Status status = IsDocumentTypeXml(session, web_view, &is_xml);
if (status.IsError())
return status;
if (is_xml) {
*value = std::make_unique<base::Value>("");
} else {
const std::string* property_name = params.FindString("propertyName");
if (!property_name)
return Status(kInvalidArgument, "missing 'propertyName'");
std::string property_value;
status = GetElementEffectiveStyle(session, web_view, element_id,
*property_name, &property_value);
if (status.IsError())
return status;
*value = std::make_unique<base::Value>(property_value);
}
return Status(kOk);
}
Status ExecuteElementEquals(Session* session,
WebView* web_view,
const std::string& element_id,
const base::Value::Dict& params,
std::unique_ptr<base::Value>* value) {
const std::string* other_element_id = params.FindString("other");
if (!other_element_id)
return Status(kInvalidArgument, "'other' must be a string");
*value = std::make_unique<base::Value>(element_id == *other_element_id);
return Status(kOk);
}
Status ExecuteElementScreenshot(Session* session,
WebView* web_view,
const std::string& element_id,
const base::Value::Dict& params,
std::unique_ptr<base::Value>* value) {
Status status = session->chrome->ActivateWebView(web_view->GetId());
if (status.IsError())
return status;
WebPoint offset(0, 0);
WebPoint location;
status =
ScrollElementIntoView(session, web_view, element_id, &offset, &location);
if (status.IsError())
return status;
std::unique_ptr<base::Value> clip;
status = ExecuteGetElementRect(session, web_view, element_id, params, &clip);
if (status.IsError())
return status;
// |location| returned by ScrollElementIntoView is relative to the current
// view port. However, CaptureScreenshot expects a location relative to the
// document origin. We make the adjustment using the scroll amount of the top
// level window. Scrolling of frames has already been included in |location|.
// Use window.pageXOffset and widnow.pageYOffset for scroll information,
// should always return scroll amount regardless of doctype. The parentheses
// around the JavaScript code below is needed because JavaScript syntax
// doesn't allow a statement to start with an object literal.
// document.documentElement.clientHeight and Width provide viewport height
// and width to crop screenshot if necessary.
std::unique_ptr<base::Value> browser_info;
status = web_view->EvaluateScript(
std::string(),
"({x: window.pageXOffset,"
" y: window.pageYOffset,"
" height: document.documentElement.clientHeight,"
" width: document.documentElement.clientWidth,"
" device_pixel_ratio: window.devicePixelRatio})",
false, &browser_info);
if (status.IsError())
return status;
const base::Value::Dict& browser_info_dict = browser_info->GetDict();
double scroll_left = browser_info_dict.FindDouble("x").value();
double scroll_top = browser_info_dict.FindDouble("y").value();
double viewport_height = browser_info_dict.FindDouble("height").value();
double viewport_width = browser_info_dict.FindDouble("width").value();
double device_pixel_ratio =
browser_info_dict.FindDouble("device_pixel_ratio").value();
if (!clip->is_dict())
return Status(kUnknownError, "Element Rect is not a dictionary");
base::Value::Dict screenshot_params;
base::Value::Dict& clip_dict =
screenshot_params
.Set("clip", base::Value::FromUniquePtrValue(std::move(clip)))
->GetDict();
// |clip_dict| already contains the right width and height of the target
// element, but its x and y are relative to containing frame. We replace them
// with the x and y relative to top-level document origin, as expected by
// CaptureScreenshot.
clip_dict.Set("x", location.x + scroll_left);
clip_dict.Set("y", location.y + scroll_top);
clip_dict.Set("scale", 1 / device_pixel_ratio);
// Crop screenshot by viewport if element is larger than viewport
clip_dict.Set("height", std::min(viewport_height - location.y,
clip_dict.FindDouble("height").value()));
clip_dict.Set("width", std::min(viewport_width - location.x,
clip_dict.FindDouble("width").value()));
std::string screenshot;
status = web_view->CaptureScreenshot(&screenshot, screenshot_params);
if (status.IsError())
return status;
*value = std::make_unique<base::Value>(screenshot);
return Status(kOk);
}