blob: 220fc2d9cd86ea6e9bd4a21e43bc9904fe61a18b [file] [log] [blame] [edit]
// 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_util.h"
#include <memory>
#include <optional>
#include <utility>
#include "base/containers/adapters.h"
#include "base/containers/flat_set.h"
#include "base/logging.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_util.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/browser_info.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/web_view.h"
#include "chrome/test/chromedriver/net/timeout.h"
#include "chrome/test/chromedriver/session.h"
#include "third_party/selenium-atoms/atoms.h"
namespace {
const char kElementKey[] = "ELEMENT";
const char kElementKeyW3C[] = "element-6066-11e4-a52e-4f735466cecf";
const char kShadowRootKey[] = "shadow-6066-11e4-a52e-4f735466cecf";
const char kFindSubFrameScript[] =
"function findSubFrame(frame_id) {"
" const findSubFrameDeep = function(nodes, id) {"
" let r = null;"
" for (let i = 0, el; (el = nodes[i]) && !r; ++i) {"
" if ((el.tagName === 'IFRAME') "
" && el.getAttribute('cd_frame_id_') === id) {"
" r = el;"
" } else if (el.shadowRoot) {"
" r = findSubFrameDeep(el.shadowRoot.querySelectorAll('*'), id);"
" }"
" }"
" return r;"
" };"
" const xpath = \"//*[@cd_frame_id_ ='\" + frame_id + \"']\";"
" const r = document.evaluate(xpath, document, null,"
" XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;"
" return r || findSubFrameDeep(document.querySelectorAll('*'), "
"frame_id);"
"}";
bool ParseFromValue(base::Value* value, WebPoint* point) {
if (!value->is_dict())
return false;
base::Value::Dict& dict = value->GetDict();
auto x = dict.FindDouble("x");
auto y = dict.FindDouble("y");
if (!x.has_value() || !y.has_value())
return false;
point->x = x.value();
point->y = y.value();
return true;
}
bool ParseFromValue(base::Value* value, WebSize* size) {
if (!value->is_dict())
return false;
base::Value::Dict& dict = value->GetDict();
auto width = dict.FindDouble("width");
auto height = dict.FindDouble("height");
if (!width.has_value() || !height.has_value())
return false;
size->width = width.value();
size->height = height.value();
return true;
}
bool ParseFromValue(base::Value* value, WebRect* rect) {
if (!value->is_dict())
return false;
base::Value::Dict& dict = value->GetDict();
auto x = dict.FindDouble("left");
auto y = dict.FindDouble("top");
auto width = dict.FindDouble("width");
auto height = dict.FindDouble("height");
if (!x.has_value() || !y.has_value() || !width.has_value() ||
!height.has_value())
return false;
rect->origin.x = x.value();
rect->origin.y = y.value();
rect->size.width = width.value();
rect->size.height = height.value();
return true;
}
base::Value::Dict CreateValueFrom(const WebRect& rect) {
base::Value::Dict dict;
dict.Set("left", static_cast<int>(rect.X()));
dict.Set("top", static_cast<int>(rect.Y()));
dict.Set("width", static_cast<int>(rect.Width()));
dict.Set("height", static_cast<int>(rect.Height()));
return dict;
}
Status CallAtomsJs(const std::string& frame,
WebView* web_view,
const char* const* atom_function,
const base::Value::List& args,
std::unique_ptr<base::Value>* result) {
return web_view->CallFunction(
frame, webdriver::atoms::asString(atom_function), args, result);
}
Status VerifyElementClickable(const Session* session,
const std::string& frame,
WebView* web_view,
const std::string& element_id,
const WebPoint& location) {
base::Value::List args;
args.Append(CreateElement(element_id, session->w3c_compliant));
args.Append(CreateValueFrom(location));
std::unique_ptr<base::Value> result;
Status status = CallAtomsJs(
frame, web_view, webdriver::atoms::IS_ELEMENT_CLICKABLE, args, &result);
if (status.IsError())
return status;
std::optional<bool> is_clickable = std::nullopt;
if (result->is_dict())
is_clickable = result->GetDict().FindBool("clickable");
if (!is_clickable.has_value()) {
return Status(kUnknownError,
"failed to parse value of IS_ELEMENT_CLICKABLE");
}
if (!is_clickable.value()) {
std::string message;
const std::string* maybe_message = result->GetDict().FindString("message");
if (!maybe_message)
message = "element click intercepted";
else
message = *maybe_message;
return Status(kElementClickIntercepted, message);
}
return Status(kOk);
}
Status ScrollElementRegionIntoViewHelper(
const Session* session,
const std::string& frame,
WebView* web_view,
const std::string& element_id,
const WebRect& region,
bool center,
const std::string& clickable_element_id,
WebPoint* location) {
WebPoint tmp_location = *location;
base::Value::List args;
args.Append(CreateElement(element_id, session->w3c_compliant));
args.Append(center);
args.Append(CreateValueFrom(region));
std::unique_ptr<base::Value> result;
Status status = web_view->CallFunction(
frame, webdriver::atoms::asString(webdriver::atoms::GET_LOCATION_IN_VIEW),
args, &result);
if (status.IsError())
return status;
if (!ParseFromValue(result.get(), &tmp_location)) {
return Status(kUnknownError,
"failed to parse value of GET_LOCATION_IN_VIEW");
}
if (!clickable_element_id.empty()) {
WebPoint middle = tmp_location;
middle.Offset(region.Width() / 2, region.Height() / 2);
status = VerifyElementClickable(session, frame, web_view,
clickable_element_id, middle);
if (status.code() == kElementClickIntercepted) {
// Clicking at the target location isn't reaching the target element.
// One possible cause is a scroll event handler has shifted the element.
// Try again to get the updated location of the target element.
status = web_view->CallFunction(
frame,
webdriver::atoms::asString(webdriver::atoms::GET_LOCATION_IN_VIEW),
args, &result);
if (status.IsError())
return status;
if (!ParseFromValue(result.get(), &tmp_location)) {
return Status(kUnknownError,
"failed to parse value of GET_LOCATION_IN_VIEW");
}
middle = tmp_location;
middle.Offset(region.Width() / 2, region.Height() / 2);
Timeout response_timeout(base::Seconds(1));
do {
status = VerifyElementClickable(session, frame, web_view,
clickable_element_id, middle);
if (status.code() == kElementClickIntercepted)
base::PlatformThread::Sleep(base::Milliseconds(50));
else
break;
} while (!response_timeout.IsExpired());
}
if (status.IsError())
return status;
}
*location = tmp_location;
return Status(kOk);
}
Status GetElementEffectiveStyle(const Session* session,
const std::string& frame,
WebView* web_view,
const std::string& element_id,
const std::string& property,
std::string* value) {
base::Value::List args;
args.Append(CreateElement(element_id, session->w3c_compliant));
args.Append(property);
std::unique_ptr<base::Value> result;
Status status = web_view->CallFunction(
frame, webdriver::atoms::asString(webdriver::atoms::GET_EFFECTIVE_STYLE),
args, &result);
if (status.IsError())
return status;
if (!result->is_string()) {
return Status(kUnknownError,
"failed to parse value of GET_EFFECTIVE_STYLE");
}
*value = result->GetString();
return Status(kOk);
}
Status GetElementBorder(const Session* session,
const std::string& frame,
WebView* web_view,
const std::string& element_id,
int* border_left,
int* border_top) {
std::string border_left_str;
Status status =
GetElementEffectiveStyle(session, frame, web_view, element_id,
"border-left-width", &border_left_str);
if (status.IsError())
return status;
std::string border_top_str;
status = GetElementEffectiveStyle(session, frame, web_view, element_id,
"border-top-width", &border_top_str);
if (status.IsError())
return status;
int border_left_tmp = -1;
int border_top_tmp = -1;
base::StringToInt(border_left_str, &border_left_tmp);
base::StringToInt(border_top_str, &border_top_tmp);
if (border_left_tmp == -1 || border_top_tmp == -1)
return Status(kUnknownError, "failed to get border width of element");
std::string padding_left_str;
status = GetElementEffectiveStyle(session, frame, web_view, element_id,
"padding-left", &padding_left_str);
int padding_left = 0;
if (status.IsOk())
base::StringToInt(padding_left_str, &padding_left);
std::string padding_top_str;
status = GetElementEffectiveStyle(session, frame, web_view, element_id,
"padding-top", &padding_top_str);
int padding_top = 0;
if (status.IsOk())
base::StringToInt(padding_top_str, &padding_top);
*border_left = border_left_tmp + padding_left;
*border_top = border_top_tmp + padding_top;
return Status(kOk);
}
Status GetElementLocationInViewCenterHelper(Session* session,
const std::string& frame,
WebView* web_view,
const std::string& element_id,
bool center,
WebPoint* location) {
base::Value::List args;
args.Append(CreateElement(element_id, session->w3c_compliant));
args.Append(center);
std::unique_ptr<base::Value> result;
Status status =
web_view->CallFunction(frame, kGetElementLocationScript, args, &result);
if (status.IsError())
return status;
if (!ParseFromValue(result.get(), location)) {
return Status(kUnknownError,
"failed to parse value of getElementLocationInViewCenter");
}
return Status(kOk);
}
} // namespace
std::string GetElementKey(bool w3c_compliant) {
if (w3c_compliant) {
return kElementKeyW3C;
} else {
return kElementKey;
}
}
base::Value CreateElementCommon(const std::string& key,
const std::string& value) {
base::Value::Dict element;
element.SetByDottedPath(key, value);
return base::Value(std::move(element));
}
base::Value CreateElement(const std::string& element_id, bool w3c_compliant) {
return CreateElementCommon(GetElementKey(w3c_compliant), element_id);
}
base::Value CreateShadowRoot(const std::string& shadow_root_id) {
return CreateElementCommon(kShadowRootKey, shadow_root_id);
}
base::Value::Dict CreateValueFrom(const WebPoint& point) {
base::Value::Dict dict;
dict.Set("x", static_cast<int>(point.x));
dict.Set("y", static_cast<int>(point.y));
return dict;
}
Status FindElementCommon(int interval_ms,
bool only_one,
const std::string* root_element_id,
Session* session,
WebView* web_view,
const base::Value::Dict& params,
std::unique_ptr<base::Value>* value,
bool is_shadow_root) {
const std::string* strategy = params.FindString("using");
if (!strategy)
return Status(kInvalidArgument, "'using' must be a string");
if (session->w3c_compliant && *strategy != "css selector" &&
*strategy != "link text" && *strategy != "partial link text" &&
*strategy != "tag name" && *strategy != "xpath")
return Status(kInvalidArgument, "invalid locator");
/*
* Currently there is an opened discussion about if the
* following values has to be supported for a Shadow Root
* because the current implementation doesn't support them.
* We have them disabled for now.
* https://github.com/w3c/webdriver/issues/1610
*/
if (is_shadow_root && (*strategy == "tag name" || *strategy == "xpath")) {
return Status(kInvalidArgument, "invalid locator");
}
const std::string* target = params.FindString("value");
if (!target)
return Status(kInvalidArgument, "'value' must be a string");
std::string script;
if (only_one)
script = webdriver::atoms::asString(webdriver::atoms::FIND_ELEMENT);
else
script = webdriver::atoms::asString(webdriver::atoms::FIND_ELEMENTS);
base::Value::Dict locator;
locator.Set(*strategy, *target);
base::Value::List arguments;
arguments.Append(std::move(locator));
if (root_element_id) {
if (is_shadow_root)
arguments.Append(CreateShadowRoot(*root_element_id));
else
arguments.Append(CreateElement(*root_element_id, session->w3c_compliant));
}
Timeout timeout(session->implicit_wait);
while (true) {
std::unique_ptr<base::Value> temp;
Status status = web_view->CallFunction(
session->GetCurrentFrameId(), script, arguments, &temp);
// If navigation is detected during the WebView::CallFunction call the error
// code will be kNoSuchExecutionContext or kAbortedByNavigation.
// We will wait and retry again until the timeout.
static const base::flat_set<StatusCode> kNavigationHints = {
kNoSuchExecutionContext,
kAbortedByNavigation,
};
if (status.IsError() && !kNavigationHints.contains(status.code())) {
if (status.code() == kJavaScriptError) {
status = Status{kInvalidSelector, status};
}
if (status.code() == kTargetDetached) {
return Status{kAbortedByNavigation, status};
}
return status;
}
if (status.IsOk() && temp && !temp->is_none()) {
if (only_one) {
*value = std::move(temp);
return Status(kOk);
}
if (!temp->is_list())
return Status(kUnknownError, "script returns unexpected result");
if (temp->GetList().size() > 0U) {
*value = std::move(temp);
return Status(kOk);
}
}
if (timeout.IsExpired()) {
if (only_one) {
return Status(kNoSuchElement,
"Unable to locate element: {\"method\":\"" + *strategy +
"\",\"selector\":\"" + *target + "\"}");
}
*value =
base::Value::ToUniquePtrValue(base::Value(base::Value::Type::LIST));
return Status(kOk);
}
base::PlatformThread::Sleep(base::Milliseconds(interval_ms));
}
}
Status FindElement(int interval_ms,
bool only_one,
const std::string* root_element_id,
Session* session,
WebView* web_view,
const base::Value::Dict& params,
std::unique_ptr<base::Value>* value) {
return FindElementCommon(interval_ms, only_one, root_element_id, session,
web_view, params, value, false);
}
Status FindShadowElement(int interval_ms,
bool only_one,
const std::string* shadow_root_id,
Session* session,
WebView* web_view,
const base::Value::Dict& params,
std::unique_ptr<base::Value>* value) {
return FindElementCommon(interval_ms, only_one, shadow_root_id, session,
web_view, params, value, true);
}
Status GetActiveElement(Session* session,
WebView* web_view,
std::unique_ptr<base::Value>* value) {
base::Value::List args;
Status status = web_view->CallFunction(
session->GetCurrentFrameId(),
"function() { return document.activeElement || document.body }", args,
value);
if (status.IsError()) {
return status;
}
if (value->get()->is_none()) {
return Status(kNoSuchElement);
}
return status;
}
Status HasFocus(Session* session, WebView* web_view, bool* has_focus) {
std::unique_ptr<base::Value> value;
Status status = web_view->EvaluateScript(
session->GetCurrentFrameId(), "document.hasFocus()", false, &value);
if (status.IsError())
return status;
if (!value->is_bool())
return Status(kUnknownError, "document.hasFocus() returns non-boolean");
*has_focus = value->GetBool();
return Status(kOk);
}
Status IsElementFocused(
Session* session,
WebView* web_view,
const std::string& element_id,
bool* is_focused) {
std::unique_ptr<base::Value> result;
Status status = HasFocus(session, web_view, is_focused);
if (status.IsError())
return status;
if (!(*is_focused))
return status;
status = GetActiveElement(session, web_view, &result);
if (status.IsError())
return status;
base::Value element_dict = CreateElement(element_id, session->w3c_compliant);
*is_focused = *result == element_dict;
return Status(kOk);
}
Status IsDocumentTypeXml(
Session* session,
WebView* web_view,
bool* is_xml_document) {
std::unique_ptr<base::Value> contentType;
Status status =
web_view->EvaluateScript(session->GetCurrentFrameId(),
"document.contentType", false, &contentType);
if (status.IsError())
return status;
if (base::EqualsCaseInsensitiveASCII(contentType->GetString(), "text/xml"))
*is_xml_document = true;
else
*is_xml_document = false;
return Status(kOk);
}
Status GetElementAttribute(Session* session,
WebView* web_view,
const std::string& element_id,
const std::string& attribute_name,
std::unique_ptr<base::Value>* value) {
base::Value::List args;
args.Append(CreateElement(element_id, session->w3c_compliant));
args.Append(attribute_name);
return CallAtomsJs(
session->GetCurrentFrameId(), web_view, webdriver::atoms::GET_ATTRIBUTE,
args, value);
}
Status IsElementAttributeEqualToIgnoreCase(
Session* session,
WebView* web_view,
const std::string& element_id,
const std::string& attribute_name,
const std::string& attribute_value,
bool* is_equal) {
std::unique_ptr<base::Value> result;
Status status = GetElementAttribute(
session, web_view, element_id, attribute_name, &result);
if (status.IsError())
return status;
if (result->is_string()) {
*is_equal =
base::EqualsCaseInsensitiveASCII(result->GetString(), attribute_value);
} else {
*is_equal = false;
}
return status;
}
namespace {
Status WaitElementIsDisplayed(Session* session,
WebView* web_view,
const std::string& element_id,
base::TimeDelta timeout) {
bool is_displayed = false;
base::TimeTicks start_time = base::TimeTicks::Now();
while (true) {
Status 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 >= timeout) {
return Status(kElementNotVisible);
}
base::PlatformThread::Sleep(base::Milliseconds(50));
}
return Status{kOk};
}
} // namespace
Status GetElementClickableLocation(
Session* session,
WebView* web_view,
const std::string& element_id,
WebPoint* location) {
std::string tag_name;
Status status = GetElementTagName(session, web_view, element_id, &tag_name);
if (status.IsError())
return status;
std::string target_element_id = element_id;
if (tag_name == "area") {
// Scroll the image into view instead of the area.
const char kGetImageElementForArea[] =
"function (element) {"
" var map = element.parentElement;"
" if (map.tagName.toLowerCase() != 'map')"
" throw new Error('the area is not within a map');"
" var mapName = map.getAttribute('name');"
" if (mapName == null)"
" throw new Error ('area\\'s parent map must have a name');"
" mapName = '#' + mapName.toLowerCase();"
" var images = document.getElementsByTagName('img');"
" for (var i = 0; i < images.length; i++) {"
" if (images[i].useMap.toLowerCase() == mapName)"
" return images[i];"
" }"
" throw new Error('no img is found for the area');"
"}";
base::Value::List args;
args.Append(CreateElement(element_id, session->w3c_compliant));
std::unique_ptr<base::Value> result;
status = web_view->CallFunction(
session->GetCurrentFrameId(), kGetImageElementForArea, args, &result);
if (status.IsError())
return status;
std::string* maybe_target_element_id = nullptr;
if (result->is_dict())
maybe_target_element_id =
result->GetDict().FindString(GetElementKey(session->w3c_compliant));
if (!maybe_target_element_id)
return Status(kUnknownError, "no element reference returned by script");
target_element_id = *maybe_target_element_id;
}
status = WaitElementIsDisplayed(session, web_view, target_element_id,
session->implicit_wait);
if (status.IsError()) {
return status;
}
WebRect rect;
status = GetElementRegion(session, web_view, element_id, &rect);
if (status.IsError())
return status;
if (rect.Width() == 0 || rect.Height() == 0)
return Status(kElementNotInteractable, "element has zero size");
status = ScrollElementRegionIntoView(
session, web_view, target_element_id, rect,
true /* center */, element_id, location);
if (status.IsError()) {
return status;
}
location->Offset(rect.Width() / 2, rect.Height() / 2);
return Status(kOk);
}
Status GetElementEffectiveStyle(
Session* session,
WebView* web_view,
const std::string& element_id,
const std::string& property_name,
std::string* property_value) {
return GetElementEffectiveStyle(session, session->GetCurrentFrameId(),
web_view, element_id, property_name,
property_value);
}
// Wrapper to JavaScript code in js/get_element_region.js. See comments near the
// beginning of that file for what is returned.
Status GetElementRegion(Session* session,
WebView* web_view,
const std::string& element_id,
WebRect* rect) {
base::Value::List args;
args.Append(CreateElement(element_id, session->w3c_compliant));
std::unique_ptr<base::Value> result;
Status status = web_view->CallFunction(
session->GetCurrentFrameId(), kGetElementRegionScript, args, &result);
if (status.IsError())
return status;
if (!ParseFromValue(result.get(), rect)) {
return Status(kUnknownError,
"failed to parse value of getElementRegion");
}
return Status(kOk);
}
Status GetElementTagName(Session* session,
WebView* web_view,
const std::string& element_id,
std::string* name) {
base::Value::List args;
args.Append(CreateElement(element_id, session->w3c_compliant));
std::unique_ptr<base::Value> result;
Status status = web_view->CallFunction(
session->GetCurrentFrameId(), "function(elem) { return elem.tagName; }",
args, &result);
if (status.IsError())
return status;
if (!result->is_string())
return Status(kNoSuchElement, "failed to get element tag name");
*name = base::ToLowerASCII(result->GetString());
return Status(kOk);
}
Status GetElementSize(Session* session,
WebView* web_view,
const std::string& element_id,
WebSize* size) {
base::Value::List args;
args.Append(CreateElement(element_id, session->w3c_compliant));
std::unique_ptr<base::Value> result;
Status status = CallAtomsJs(session->GetCurrentFrameId(), web_view,
webdriver::atoms::GET_SIZE, args, &result);
if (status.IsError())
return status;
if (!ParseFromValue(result.get(), size))
return Status(kUnknownError, "failed to parse value of GET_SIZE");
return Status(kOk);
}
Status IsElementDisplayed(Session* session,
WebView* web_view,
const std::string& element_id,
bool ignore_opacity,
bool* is_displayed) {
base::Value::List args;
args.Append(CreateElement(element_id, session->w3c_compliant));
args.Append(ignore_opacity);
std::unique_ptr<base::Value> result;
Status status = CallAtomsJs(session->GetCurrentFrameId(), web_view,
webdriver::atoms::IS_DISPLAYED, args, &result);
if (status.IsError())
return status;
if (!result->is_bool())
return Status(kUnknownError, "IS_DISPLAYED should return a boolean value");
*is_displayed = result->GetBool();
return Status(kOk);
}
Status IsElementEnabled(Session* session,
WebView* web_view,
const std::string& element_id,
bool* is_enabled) {
base::Value::List args;
args.Append(CreateElement(element_id, session->w3c_compliant));
std::unique_ptr<base::Value> result;
Status status = CallAtomsJs(session->GetCurrentFrameId(), web_view,
webdriver::atoms::IS_ENABLED, args, &result);
if (status.IsError())
return status;
if (!result->is_bool())
return Status(kUnknownError, "IS_ENABLED should return a boolean value");
*is_enabled = result->GetBool();
return Status(kOk);
}
Status IsOptionElementSelected(Session* session,
WebView* web_view,
const std::string& element_id,
bool* is_selected) {
base::Value::List args;
args.Append(CreateElement(element_id, session->w3c_compliant));
std::unique_ptr<base::Value> result;
Status status = CallAtomsJs(session->GetCurrentFrameId(), web_view,
webdriver::atoms::IS_SELECTED, args, &result);
if (status.IsError())
return status;
if (!result->is_bool())
return Status(kUnknownError, "IS_SELECTED should return a boolean value");
*is_selected = result->GetBool();
return Status(kOk);
}
Status IsOptionElementTogglable(Session* session,
WebView* web_view,
const std::string& element_id,
bool* is_togglable) {
base::Value::List args;
args.Append(CreateElement(element_id, session->w3c_compliant));
std::unique_ptr<base::Value> result;
Status status =
web_view->CallFunction(session->GetCurrentFrameId(),
kIsOptionElementToggleableScript, args, &result);
if (status.IsError())
return status;
if (!result->is_bool())
return Status(kUnknownError, "failed check if option togglable or not");
*is_togglable = result->GetBool();
return Status(kOk);
}
Status SetOptionElementSelected(Session* session,
WebView* web_view,
const std::string& element_id,
bool selected) {
// TODO(crbug.com/40299291): need to fix throwing error if an alert is
// triggered.
base::Value::List args;
args.Append(CreateElement(element_id, session->w3c_compliant));
args.Append(selected);
std::unique_ptr<base::Value> result;
return CallAtomsJs(
session->GetCurrentFrameId(), web_view, webdriver::atoms::CLICK,
args, &result);
}
Status ToggleOptionElement(
Session* session,
WebView* web_view,
const std::string& element_id) {
bool is_selected;
Status status = IsOptionElementSelected(
session, web_view, element_id, &is_selected);
if (status.IsError())
return status;
return SetOptionElementSelected(session, web_view, element_id, !is_selected);
}
Status ScrollElementIntoView(
Session* session,
WebView* web_view,
const std::string& id,
const WebPoint* offset,
WebPoint* location) {
WebRect region;
Status status = GetElementRegion(session, web_view, id, &region);
if (status.IsError())
return status;
status = ScrollElementRegionIntoView(session, web_view, id, region,
false /* center */, std::string(), location);
if (status.IsError())
return status;
if (offset)
location->Offset(offset->x, offset->y);
else
location->Offset(region.size.width / 2, region.size.height / 2);
return Status(kOk);
}
// Scroll a region of an element (identified by |element_id|) into view.
// It first scrolls the element region relative to its enclosing viewport,
// so that the region becomes visible in that viewport.
// If that viewport is a frame, it then makes necessary scroll to make the
// region of the frame visible in its enclosing viewport. It repeats this up
// the frame chain until it reaches the top-level viewport.
//
// Upon return, |location| gives the location of the region relative to the
// top-level viewport. If |center| is true, the location is for the center of
// the region, otherwise it is for the upper-left corner of the region.
Status ScrollElementRegionIntoView(
Session* session,
WebView* web_view,
const std::string& element_id,
const WebRect& region,
bool center,
const std::string& clickable_element_id,
WebPoint* location) {
WebPoint region_offset = region.origin;
WebSize region_size = region.size;
// Scroll the element region in its enclosing viewport.
Status status = ScrollElementRegionIntoViewHelper(
session, session->GetCurrentFrameId(), web_view, element_id, region,
center, clickable_element_id, &region_offset);
if (status.IsError())
return status;
// If the element is in a frame, go up the frame chain (from the innermost
// frame up to the web_view frame) and scroll each frame relative to its
// parent frame, so that the region becomes visible in the parent frame.
auto frames = base::Reversed(session->frames);
auto end = std::ranges::find(frames, web_view->GetId(), &FrameInfo::frame_id);
for (auto it = frames.begin(); it != end; ++it) {
const FrameInfo& frame = *it;
base::Value::List args;
args.Append(frame.chromedriver_frame_id.c_str());
std::unique_ptr<base::Value> result;
status = web_view->CallFunction(frame.parent_frame_id, kFindSubFrameScript,
args, &result);
if (status.IsError())
return status;
if (!result->is_dict())
return Status(kUnknownError, "no element reference returned by script");
std::string* maybe_frame_element_id =
result->GetDict().FindString(GetElementKey(session->w3c_compliant));
if (!maybe_frame_element_id)
return Status(kUnknownError, "failed to locate a sub frame");
std::string frame_element_id = *maybe_frame_element_id;
// Modify |region_offset| by the frame's border.
int border_left = -1;
int border_top = -1;
status = GetElementBorder(session, frame.parent_frame_id, web_view,
frame_element_id, &border_left, &border_top);
if (status.IsError())
return status;
region_offset.Offset(border_left, border_top);
status = ScrollElementRegionIntoViewHelper(
session, frame.parent_frame_id, web_view, frame_element_id,
WebRect(region_offset, region_size), center, frame_element_id,
&region_offset);
if (status.IsError())
return status;
}
*location = region_offset;
return Status(kOk);
}
Status GetElementLocationInViewCenter(Session* session,
WebView* web_view,
const std::string& element_id,
WebPoint* location) {
WebPoint center_location;
Status status = GetElementLocationInViewCenterHelper(
session, session->GetCurrentFrameId(), web_view, element_id, true,
&center_location);
if (status.IsError())
return status;
for (const FrameInfo& frame : base::Reversed(session->frames)) {
base::Value::List args;
args.Append(frame.chromedriver_frame_id.c_str());
std::unique_ptr<base::Value> result;
status = web_view->CallFunction(frame.parent_frame_id, kFindSubFrameScript,
args, &result);
if (status.IsError())
return status;
if (!result->is_dict())
return Status(kUnknownError, "no element reference returned by script");
std::string* maybe_frame_element_id =
result->GetDict().FindString(GetElementKey(session->w3c_compliant));
if (!maybe_frame_element_id)
return Status(kUnknownError, "failed to locate a sub frame");
std::string frame_element_id = *maybe_frame_element_id;
// Modify |center_location| by the frame's border.
int border_left = -1;
int border_top = -1;
status = GetElementBorder(session, frame.parent_frame_id, web_view,
frame_element_id, &border_left, &border_top);
if (status.IsError())
return status;
center_location.Offset(border_left, border_top);
WebPoint frame_offset;
status = GetElementLocationInViewCenterHelper(
session, frame.parent_frame_id, web_view, frame_element_id, false,
&frame_offset);
if (status.IsError())
return status;
center_location.Offset(frame_offset.x, frame_offset.y);
}
*location = center_location;
return Status(kOk);
}
Status GetAXNodeByElementId(Session* session,
WebView* web_view,
const std::string& element_id,
std::unique_ptr<base::Value>* axNode) {
base::Value element(CreateElement(element_id, session->w3c_compliant));
int backend_node_id;
Status status = web_view->GetBackendNodeIdByElement(
session->GetCurrentFrameId(), element, &backend_node_id);
if (status.IsError())
return status;
base::Value::Dict body;
body.Set("backendNodeId", backend_node_id);
body.Set("fetchRelatives", false);
std::unique_ptr<base::Value> result;
status = web_view->SendCommandAndGetResult("Accessibility.getPartialAXTree",
body, &result);
if (status.IsError())
return status;
std::optional<base::Value> nodes = result->GetDict().Extract("nodes");
if (!nodes)
return Status(kUnknownError, "No `nodes` found in CDP response");
base::Value::List& nodes_list = nodes->GetList();
if (nodes_list.size() < 1)
return Status(kUnknownError, "Empty nodes list in CDP response");
if (nodes_list.size() > 1)
return Status(kUnknownError, "Non-unique node in CDP response");
*axNode = std::make_unique<base::Value>(std::move(nodes_list[0]));
return Status(kOk);
}