summaryrefslogtreecommitdiff
path: root/chromium/components/autofill_assistant/browser/web
diff options
context:
space:
mode:
authorAllan Sandfeld Jensen <allan.jensen@qt.io>2020-10-12 14:27:29 +0200
committerAllan Sandfeld Jensen <allan.jensen@qt.io>2020-10-13 09:35:20 +0000
commitc30a6232df03e1efbd9f3b226777b07e087a1122 (patch)
treee992f45784689f373bcc38d1b79a239ebe17ee23 /chromium/components/autofill_assistant/browser/web
parent7b5b123ac58f58ffde0f4f6e488bcd09aa4decd3 (diff)
downloadqtwebengine-chromium-85-based.tar.gz
BASELINE: Update Chromium to 85.0.4183.14085-based
Change-Id: Iaa42f4680837c57725b1344f108c0196741f6057 Reviewed-by: Allan Sandfeld Jensen <allan.jensen@qt.io>
Diffstat (limited to 'chromium/components/autofill_assistant/browser/web')
-rw-r--r--chromium/components/autofill_assistant/browser/web/element_finder.cc929
-rw-r--r--chromium/components/autofill_assistant/browser/web/element_finder.h286
-rw-r--r--chromium/components/autofill_assistant/browser/web/element_position_getter.cc9
-rw-r--r--chromium/components/autofill_assistant/browser/web/element_rect_getter.cc1
-rw-r--r--chromium/components/autofill_assistant/browser/web/web_controller.cc38
-rw-r--r--chromium/components/autofill_assistant/browser/web/web_controller_browsertest.cc759
6 files changed, 1576 insertions, 446 deletions
diff --git a/chromium/components/autofill_assistant/browser/web/element_finder.cc b/chromium/components/autofill_assistant/browser/web/element_finder.cc
index 3cff6bc1b18..add03320a31 100644
--- a/chromium/components/autofill_assistant/browser/web/element_finder.cc
+++ b/chromium/components/autofill_assistant/browser/web/element_finder.cc
@@ -14,52 +14,10 @@ namespace autofill_assistant {
namespace {
// Javascript code to get document root element.
-const char* const kGetDocumentElement =
- R"(
- (function() {
- return document.documentElement;
- }())
- )";
-
-// Javascript code to query an elements for a selector, either the first
-// (non-strict) or a single (strict) element.
-//
-// Returns undefined if no elements are found, TOO_MANY_ELEMENTS (18) if too
-// many elements were found and strict mode is enabled.
-const char* const kQuerySelector =
- R"(function (selector, strictMode) {
- var found = this.querySelectorAll(selector);
- if(found.length == 0) return undefined;
- if(found.length > 1 && strictMode) return 18;
- return found[0];
- })";
-
-// Javascript code to query a visible elements for a selector, either the first
-// (non-strict) or a single (strict) visible element.q
-//
-// Returns undefined if no elements are found, TOO_MANY_ELEMENTS (18) if too
-// many elements were found and strict mode is enabled.
-const char* const kQuerySelectorWithConditions =
- R"(function (selector, strict, visible, inner_text_str, value_str) {
- var found = this.querySelectorAll(selector);
- var found_index = -1;
- var inner_text_re = inner_text_str ? RegExp(inner_text_str) : undefined;
- var value_re = value_str ? RegExp(value_str) : undefined;
- var match = function(e) {
- if (visible && e.getClientRects().length == 0) return false;
- if (inner_text_re && !inner_text_re.test(e.innerText)) return false;
- if (value_re && !value_re.test(e.value)) return false;
- return true;
- };
- for (let i = 0; i < found.length; i++) {
- if (match(found[i])) {
- if (found_index != -1) return 18;
- found_index = i;
- if (!strict) break;
- }
- }
- return found_index == -1 ? undefined : found[found_index];
- })";
+const char kGetDocumentElement[] =
+ "(function() { return document.documentElement; }())";
+
+const char kGetArrayElement[] = "function(index) { return this[index]; }";
bool ConvertPseudoType(const PseudoType pseudo_type,
dom::PseudoType* pseudo_type_output) {
@@ -116,25 +74,175 @@ bool ConvertPseudoType(const PseudoType pseudo_type,
}
} // namespace
+ElementFinder::JsFilterBuilder::JsFilterBuilder() = default;
+ElementFinder::JsFilterBuilder::~JsFilterBuilder() = default;
+
+std::vector<std::unique_ptr<runtime::CallArgument>>
+ElementFinder::JsFilterBuilder::BuildArgumentList() const {
+ auto str_array_arg = std::make_unique<base::Value>(base::Value::Type::LIST);
+ for (const std::string& str : arguments_) {
+ str_array_arg->Append(str);
+ }
+ std::vector<std::unique_ptr<runtime::CallArgument>> arguments;
+ arguments.emplace_back(runtime::CallArgument::Builder()
+ .SetValue(std::move(str_array_arg))
+ .Build());
+ return arguments;
+}
+
+// clang-format off
+std::string ElementFinder::JsFilterBuilder::BuildFunction() const {
+ return base::StrCat({
+ R"(
+ function(args) {
+ let elements = [this];
+ )",
+ base::JoinString(lines_, "\n"),
+ R"(
+ if (elements.length == 0) return null;
+ if (elements.length == 1) { return elements[0] }
+ return elements;
+ })"
+ });
+}
+// clang-format on
+
+bool ElementFinder::JsFilterBuilder::AddFilter(
+ const SelectorProto::Filter& filter) {
+ switch (filter.filter_case()) {
+ case SelectorProto::Filter::kCssSelector:
+ // clang-format off
+ AddLine({
+ "elements = elements.flatMap((e) => Array.from(e.querySelectorAll(",
+ AddArgument(filter.css_selector()),
+ ")));"
+ });
+
+ // Elements are temporarily put into a set to get rid of duplicates, which
+ // are likely when using inner text before CSS selector filters. We must
+ // not return duplicates as they cause incorrect TOO_MANY_ELEMENTS errors.
+ AddLine(R"(if (elements.length > 1) {
+ elements = Array.from(new Set(elements));
+ })");
+ // clang-format on
+ return true;
+
+ case SelectorProto::Filter::kInnerText:
+ AddRegexpFilter(filter.inner_text(), "innerText");
+ return true;
+
+ case SelectorProto::Filter::kValue:
+ AddRegexpFilter(filter.value(), "value");
+ return true;
+
+ case SelectorProto::Filter::kBoundingBox:
+ AddLine(
+ "elements = elements.filter((e) => e.getClientRects().length > 0);");
+ return true;
+
+ case SelectorProto::Filter::kPseudoElementContent: {
+ // When a content is set, window.getComputedStyle().content contains a
+ // double-quoted string with the content, unquoted here by JSON.parse().
+ std::string re_var =
+ AddRegexpInstance(filter.pseudo_element_content().content());
+ std::string pseudo_type =
+ PseudoTypeName(filter.pseudo_element_content().pseudo_type());
+
+ AddLine("elements = elements.filter((e) => {");
+ AddLine({" const s = window.getComputedStyle(e, '", pseudo_type, "');"});
+ AddLine(" if (!s || !s.content || !s.content.startsWith('\"')) {");
+ AddLine(" return false;");
+ AddLine(" }");
+ AddLine({" return ", re_var, ".test(JSON.parse(s.content));"});
+ AddLine("});");
+ return true;
+ }
+
+ case SelectorProto::Filter::kLabelled:
+ AddLine(R"(elements = elements.flatMap((e) => {
+ if (e.tagName != 'LABEL') return [];
+ let element = null;
+ const id = e.getAttribute('for');
+ if (id) {
+ element = document.getElementById(id)
+ }
+ if (!element) {
+ element = e.querySelector(
+ 'button,input,keygen,meter,output,progress,select,textarea');
+ }
+ if (element) return [element];
+ return [];
+});
+)");
+ // The selector above for the case where there's no "for" corresponds to
+ // the list of labelable elements listed on "W3C's HTML5: Edition for Web
+ // Authors":
+ // https://www.w3.org/TR/2011/WD-html5-author-20110809/forms.html#category-label
+ return true;
+
+ case SelectorProto::Filter::kEnterFrame:
+ case SelectorProto::Filter::kPseudoType:
+ case SelectorProto::Filter::kPickOne:
+ case SelectorProto::Filter::kClosest:
+ case SelectorProto::Filter::FILTER_NOT_SET:
+ return false;
+ }
+}
+
+std::string ElementFinder::JsFilterBuilder::AddRegexpInstance(
+ const SelectorProto::TextFilter& filter) {
+ std::string re_flags = filter.case_sensitive() ? "" : "i";
+ std::string re_var = DeclareVariable();
+ AddLine({"const ", re_var, " = RegExp(", AddArgument(filter.re2()), ", '",
+ re_flags, "');"});
+ return re_var;
+}
+
+void ElementFinder::JsFilterBuilder::AddRegexpFilter(
+ const SelectorProto::TextFilter& filter,
+ const std::string& property) {
+ std::string re_var = AddRegexpInstance(filter);
+ AddLine({"elements = elements.filter((e) => ", re_var, ".test(e.", property,
+ "));"});
+}
+
+std::string ElementFinder::JsFilterBuilder::DeclareVariable() {
+ return base::StrCat({"v", base::NumberToString(variable_counter_++)});
+}
+
+std::string ElementFinder::JsFilterBuilder::AddArgument(
+ const std::string& value) {
+ int index = arguments_.size();
+ arguments_.emplace_back(value);
+ return base::StrCat({"args[", base::NumberToString(index), "]"});
+}
+
ElementFinder::Result::Result() = default;
ElementFinder::Result::~Result() = default;
-ElementFinder::Result::Result(const Result& to_copy) = default;
+ElementFinder::Result::Result(const Result&) = default;
ElementFinder::ElementFinder(content::WebContents* web_contents,
DevtoolsClient* devtools_client,
const Selector& selector,
- bool strict)
+ ResultType result_type)
: web_contents_(web_contents),
devtools_client_(devtools_client),
selector_(selector),
- strict_(strict),
- element_result_(std::make_unique<Result>()) {}
+ result_type_(result_type) {}
ElementFinder::~ElementFinder() = default;
void ElementFinder::Start(Callback callback) {
+ StartInternal(std::move(callback), web_contents_->GetMainFrame(),
+ /* frame_id= */ "", /* document_object_id= */ "");
+}
+
+void ElementFinder::StartInternal(Callback callback,
+ content::RenderFrameHost* frame,
+ const std::string& frame_id,
+ const std::string& document_object_id) {
callback_ = std::move(callback);
if (selector_.empty()) {
@@ -142,22 +250,287 @@ void ElementFinder::Start(Callback callback) {
return;
}
- element_result_->container_frame_selector_index = 0;
- element_result_->container_frame_host = web_contents_->GetMainFrame();
- devtools_client_->GetRuntime()->Evaluate(
- std::string(kGetDocumentElement), /* node_frame_id= */ std::string(),
- base::BindOnce(&ElementFinder::OnGetDocumentElement,
- weak_ptr_factory_.GetWeakPtr(), 0));
+ current_frame_ = frame;
+ current_frame_id_ = frame_id;
+ current_frame_root_ = document_object_id;
+ if (current_frame_root_.empty()) {
+ GetDocumentElement();
+ } else {
+ current_matches_.emplace_back(current_frame_root_);
+ ExecuteNextTask();
+ }
}
void ElementFinder::SendResult(const ClientStatus& status) {
- DCHECK(callback_);
- DCHECK(element_result_);
- std::move(callback_).Run(status, std::move(element_result_));
+ if (!callback_)
+ return;
+
+ std::move(callback_).Run(status, std::make_unique<Result>());
+}
+
+void ElementFinder::SendSuccessResult(const std::string& object_id) {
+ if (!callback_)
+ return;
+
+ // Fill in result and return
+ std::unique_ptr<Result> result =
+ std::make_unique<Result>(BuildResult(object_id));
+ result->frame_stack = frame_stack_;
+ std::move(callback_).Run(OkClientStatus(), std::move(result));
+}
+
+ElementFinder::Result ElementFinder::BuildResult(const std::string& object_id) {
+ Result result;
+ result.container_frame_host = current_frame_;
+ result.object_id = object_id;
+ result.node_frame_id = current_frame_id_;
+ return result;
+}
+
+void ElementFinder::ExecuteNextTask() {
+ const auto& filters = selector_.proto.filters();
+
+ if (next_filter_index_ >= filters.size()) {
+ std::string object_id;
+ switch (result_type_) {
+ case ResultType::kExactlyOneMatch:
+ if (!ConsumeOneMatchOrFail(object_id)) {
+ return;
+ }
+ break;
+
+ case ResultType::kAnyMatch:
+ if (!ConsumeAnyMatchOrFail(object_id)) {
+ return;
+ }
+ break;
+
+ case ResultType::kMatchArray:
+ if (!ConsumeMatchArrayOrFail(object_id)) {
+ return;
+ }
+ break;
+ }
+ SendSuccessResult(object_id);
+ return;
+ }
+
+ const auto& filter = filters.Get(next_filter_index_);
+ switch (filter.filter_case()) {
+ case SelectorProto::Filter::kEnterFrame: {
+ std::string object_id;
+ if (!ConsumeOneMatchOrFail(object_id))
+ return;
+
+ // The above fails if there is more than one frame. To preserve
+ // backward-compatibility with the previous, lax behavior, callers must
+ // add pick_one before enter_frame. TODO(b/155264465): allow searching in
+ // more than one frame.
+ next_filter_index_++;
+ EnterFrame(object_id);
+ return;
+ }
+
+ case SelectorProto::Filter::kPseudoType: {
+ std::vector<std::string> matches;
+ if (!ConsumeAllMatchesOrFail(matches))
+ return;
+
+ next_filter_index_++;
+ matching_pseudo_elements_ = true;
+ ResolvePseudoElement(filter.pseudo_type(), matches);
+ return;
+ }
+
+ case SelectorProto::Filter::kPickOne: {
+ std::string object_id;
+ if (!ConsumeAnyMatchOrFail(object_id))
+ return;
+
+ next_filter_index_++;
+ current_matches_ = {object_id};
+ ExecuteNextTask();
+ return;
+ }
+
+ case SelectorProto::Filter::kCssSelector:
+ case SelectorProto::Filter::kInnerText:
+ case SelectorProto::Filter::kValue:
+ case SelectorProto::Filter::kBoundingBox:
+ case SelectorProto::Filter::kPseudoElementContent:
+ case SelectorProto::Filter::kLabelled: {
+ std::vector<std::string> matches;
+ if (!ConsumeAllMatchesOrFail(matches))
+ return;
+
+ JsFilterBuilder js_filter;
+ for (int i = next_filter_index_; i < filters.size(); i++) {
+ if (!js_filter.AddFilter(filters.Get(i))) {
+ break;
+ }
+ next_filter_index_++;
+ }
+ ApplyJsFilters(js_filter, matches);
+ return;
+ }
+
+ case SelectorProto::Filter::kClosest: {
+ std::string array_object_id;
+ if (!ConsumeMatchArrayOrFail(array_object_id))
+ return;
+
+ ApplyProximityFilter(next_filter_index_++, array_object_id);
+ return;
+ }
+
+ case SelectorProto::Filter::FILTER_NOT_SET:
+ VLOG(1) << __func__ << " Unset or unknown filter in " << filter << " in "
+ << selector_;
+ SendResult(ClientStatus(INVALID_SELECTOR));
+ return;
+ }
+}
+
+bool ElementFinder::ConsumeOneMatchOrFail(std::string& object_id_out) {
+ // This logic relies on JsFilterBuilder::BuildFunction guaranteeing that
+ // arrays contain at least 2 elements to avoid having to fetch all matching
+ // elements in the common case where we just want to know whether there is at
+ // least one match.
+
+ if (!current_match_arrays_.empty()) {
+ VLOG(1) << __func__ << " Got " << current_match_arrays_.size()
+ << " arrays of 2 or more matches for " << selector_
+ << ", when only 1 match was expected.";
+ SendResult(ClientStatus(TOO_MANY_ELEMENTS));
+ return false;
+ }
+ if (current_matches_.size() > 1) {
+ VLOG(1) << __func__ << " Got " << current_matches_.size() << " matches for "
+ << selector_ << ", when only 1 was expected.";
+ SendResult(ClientStatus(TOO_MANY_ELEMENTS));
+ return false;
+ }
+ if (current_matches_.empty()) {
+ SendResult(ClientStatus(ELEMENT_RESOLUTION_FAILED));
+ return false;
+ }
+
+ object_id_out = current_matches_[0];
+ current_matches_.clear();
+ return true;
+}
+
+bool ElementFinder::ConsumeAnyMatchOrFail(std::string& object_id_out) {
+ // This logic relies on ApplyJsFilters guaranteeing that arrays contain at
+ // least 2 elements to avoid having to fetch all matching elements in the
+ // common case where we just want one match.
+
+ if (current_matches_.size() > 0) {
+ object_id_out = current_matches_[0];
+ current_matches_.clear();
+ current_match_arrays_.clear();
+ return true;
+ }
+ if (!current_match_arrays_.empty()) {
+ std::string array_object_id = current_match_arrays_[0];
+ current_match_arrays_.clear();
+ ResolveMatchArrays({array_object_id}, /* max_count= */ 1);
+ return false; // Caller should call again to check
+ }
+ SendResult(ClientStatus(ELEMENT_RESOLUTION_FAILED));
+ return false;
+}
+
+bool ElementFinder::ConsumeAllMatchesOrFail(
+ std::vector<std::string>& matches_out) {
+ if (!current_match_arrays_.empty()) {
+ std::vector<std::string> array_object_ids =
+ std::move(current_match_arrays_);
+ ResolveMatchArrays(array_object_ids, /* max_count= */ -1);
+ return false; // Caller should call again to check
+ }
+ if (!current_matches_.empty()) {
+ matches_out = std::move(current_matches_);
+ current_matches_.clear();
+ return true;
+ }
+ SendResult(ClientStatus(ELEMENT_RESOLUTION_FAILED));
+ return false;
+}
+
+bool ElementFinder::ConsumeMatchArrayOrFail(std::string& array_object_id) {
+ if (current_matches_.empty() && current_match_arrays_.empty()) {
+ SendResult(ClientStatus(ELEMENT_RESOLUTION_FAILED));
+ return false;
+ }
+
+ if (current_matches_.empty() && current_match_arrays_.size() == 1) {
+ array_object_id = current_match_arrays_[0];
+ current_match_arrays_.clear();
+ return true;
+ }
+
+ std::vector<std::unique_ptr<runtime::CallArgument>> arguments;
+ std::string object_id; // Will be "this" in Javascript.
+ std::string function;
+ if (current_match_arrays_.size() > 1) {
+ object_id = current_match_arrays_.back();
+ current_match_arrays_.pop_back();
+ // Merge both arrays into current_match_arrays_[0]
+ function = "function(dest) { dest.push(...this); }";
+ AddRuntimeCallArgumentObjectId(current_match_arrays_[0], &arguments);
+ } else if (!current_matches_.empty()) {
+ object_id = current_matches_.back();
+ current_matches_.pop_back();
+ if (current_match_arrays_.empty()) {
+ // Create an array containing a single element.
+ function = "function() { return [this]; }";
+ } else {
+ // Add an element to an existing array.
+ function = "function(dest) { dest.push(this); }";
+ AddRuntimeCallArgumentObjectId(current_match_arrays_[0], &arguments);
+ }
+ }
+ devtools_client_->GetRuntime()->CallFunctionOn(
+ runtime::CallFunctionOnParams::Builder()
+ .SetObjectId(object_id)
+ .SetArguments(std::move(arguments))
+ .SetFunctionDeclaration(function)
+ .Build(),
+ current_frame_id_,
+ base::BindOnce(&ElementFinder::OnConsumeMatchArray,
+ weak_ptr_factory_.GetWeakPtr()));
+ return false;
+}
+
+void ElementFinder::OnConsumeMatchArray(
+ const DevtoolsClient::ReplyStatus& reply_status,
+ std::unique_ptr<runtime::CallFunctionOnResult> result) {
+ ClientStatus status =
+ CheckJavaScriptResult(reply_status, result.get(), __FILE__, __LINE__);
+ if (!status.ok()) {
+ VLOG(1) << __func__ << ": Failed to get element from array for "
+ << selector_;
+ SendResult(status);
+ return;
+ }
+ if (current_match_arrays_.empty()) {
+ std::string returned_object_id;
+ if (SafeGetObjectId(result->GetResult(), &returned_object_id)) {
+ current_match_arrays_.push_back(returned_object_id);
+ }
+ }
+ ExecuteNextTask();
+}
+
+void ElementFinder::GetDocumentElement() {
+ devtools_client_->GetRuntime()->Evaluate(
+ std::string(kGetDocumentElement), current_frame_id_,
+ base::BindOnce(&ElementFinder::OnGetDocumentElement,
+ weak_ptr_factory_.GetWeakPtr()));
}
void ElementFinder::OnGetDocumentElement(
- size_t index,
const DevtoolsClient::ReplyStatus& reply_status,
std::unique_ptr<runtime::EvaluateResult> result) {
ClientStatus status =
@@ -174,46 +547,32 @@ void ElementFinder::OnGetDocumentElement(
return;
}
- RecursiveFindElement(object_id, index);
+ current_frame_root_ = object_id;
+ // Use the node as root for the rest of the evaluation.
+ current_matches_.emplace_back(object_id);
+
+ DecrementResponseCountAndContinue();
}
-void ElementFinder::RecursiveFindElement(const std::string& object_id,
- size_t index) {
- std::vector<std::unique_ptr<runtime::CallArgument>> arguments;
- AddRuntimeCallArgument(selector_.selectors[index], &arguments);
- // For finding intermediate elements, strict mode would be more appropriate,
- // as long as the logic does not support more than one intermediate match.
- //
- // TODO(b/129387787): first, add logging to figure out whether it matters and
- // decide between strict mode and full support for multiple matching
- // intermeditate elements.
- AddRuntimeCallArgument(strict_, &arguments);
- std::string function;
- if (index == (selector_.selectors.size() - 1)) {
- if (selector_.must_be_visible || !selector_.inner_text_pattern.empty() ||
- !selector_.value_pattern.empty()) {
- function.assign(kQuerySelectorWithConditions);
- AddRuntimeCallArgument(selector_.must_be_visible, &arguments);
- AddRuntimeCallArgument(selector_.inner_text_pattern, &arguments);
- AddRuntimeCallArgument(selector_.value_pattern, &arguments);
- }
- }
- if (function.empty()) {
- function.assign(kQuerySelector);
+void ElementFinder::ApplyJsFilters(const JsFilterBuilder& builder,
+ const std::vector<std::string>& object_ids) {
+ DCHECK(!object_ids.empty()); // Guaranteed by ExecuteNextTask()
+ pending_response_count_ = object_ids.size();
+ std::string function = builder.BuildFunction();
+ for (const std::string& object_id : object_ids) {
+ devtools_client_->GetRuntime()->CallFunctionOn(
+ runtime::CallFunctionOnParams::Builder()
+ .SetObjectId(object_id)
+ .SetArguments(builder.BuildArgumentList())
+ .SetFunctionDeclaration(function)
+ .Build(),
+ current_frame_id_,
+ base::BindOnce(&ElementFinder::OnApplyJsFilters,
+ weak_ptr_factory_.GetWeakPtr()));
}
- devtools_client_->GetRuntime()->CallFunctionOn(
- runtime::CallFunctionOnParams::Builder()
- .SetObjectId(object_id)
- .SetArguments(std::move(arguments))
- .SetFunctionDeclaration(function)
- .Build(),
- element_result_->node_frame_id,
- base::BindOnce(&ElementFinder::OnQuerySelectorAll,
- weak_ptr_factory_.GetWeakPtr(), index));
}
-void ElementFinder::OnQuerySelectorAll(
- size_t index,
+void ElementFinder::OnApplyJsFilters(
const DevtoolsClient::ReplyStatus& reply_status,
std::unique_ptr<runtime::CallFunctionOnResult> result) {
if (!result) {
@@ -221,63 +580,55 @@ void ElementFinder::OnQuerySelectorAll(
// available yet to query because the document hasn't been loaded. This
// results in OnQuerySelectorAll getting a nullptr result. For this specific
// call, it is expected.
- VLOG(1) << __func__ << ": Context doesn't exist yet to query selector "
- << index << " of " << selector_;
+ VLOG(1) << __func__ << ": Context doesn't exist yet to query frame "
+ << frame_stack_.size() << " of " << selector_;
SendResult(ClientStatus(ELEMENT_RESOLUTION_FAILED));
return;
}
ClientStatus status =
CheckJavaScriptResult(reply_status, result.get(), __FILE__, __LINE__);
if (!status.ok()) {
- VLOG(1) << __func__ << ": Failed to query selector " << index << " of "
- << selector_;
+ VLOG(1) << __func__ << ": Failed to query selector for frame "
+ << frame_stack_.size() << " of " << selector_ << ": " << status;
SendResult(status);
return;
}
- int int_result;
- if (SafeGetIntValue(result->GetResult(), &int_result)) {
- DCHECK(int_result == TOO_MANY_ELEMENTS);
- SendResult(ClientStatus(TOO_MANY_ELEMENTS));
- return;
- }
- std::string object_id;
- if (!SafeGetObjectId(result->GetResult(), &object_id)) {
- SendResult(ClientStatus(ELEMENT_RESOLUTION_FAILED));
- return;
- }
- if (selector_.selectors.size() == index + 1) {
- // The pseudo type is associated to the final element matched by
- // |selector_|, which means that we currently don't handle matching an
- // element inside a pseudo element.
- if (selector_.pseudo_type == PseudoType::UNDEFINED) {
- // Return object id of the element.
- element_result_->object_id = object_id;
- SendResult(OkClientStatus());
- return;
+ // The result can be empty (nothing found), a an array (multiple matches
+ // found) or a single node.
+ std::string object_id;
+ if (SafeGetObjectId(result->GetResult(), &object_id)) {
+ if (result->GetResult()->HasSubtype() &&
+ result->GetResult()->GetSubtype() ==
+ runtime::RemoteObjectSubtype::ARRAY) {
+ current_match_arrays_.emplace_back(object_id);
+ } else {
+ current_matches_.emplace_back(object_id);
}
+ }
+ DecrementResponseCountAndContinue();
+}
- // We are looking for a pseudo element associated with this element.
- dom::PseudoType pseudo_type;
- if (!ConvertPseudoType(selector_.pseudo_type, &pseudo_type)) {
- // Return empty result.
- SendResult(ClientStatus(INVALID_ACTION));
- return;
- }
+void ElementFinder::ResolvePseudoElement(
+ PseudoType proto_pseudo_type,
+ const std::vector<std::string>& object_ids) {
+ dom::PseudoType pseudo_type;
+ if (!ConvertPseudoType(proto_pseudo_type, &pseudo_type)) {
+ VLOG(1) << __func__ << ": Unsupported pseudo-type "
+ << PseudoTypeName(proto_pseudo_type);
+ SendResult(ClientStatus(INVALID_ACTION));
+ return;
+ }
+ DCHECK(!object_ids.empty()); // Guaranteed by ExecuteNextTask()
+ pending_response_count_ = object_ids.size();
+ for (const std::string& object_id : object_ids) {
devtools_client_->GetDOM()->DescribeNode(
dom::DescribeNodeParams::Builder().SetObjectId(object_id).Build(),
- element_result_->node_frame_id,
+ current_frame_id_,
base::BindOnce(&ElementFinder::OnDescribeNodeForPseudoElement,
weak_ptr_factory_.GetWeakPtr(), pseudo_type));
- return;
}
-
- devtools_client_->GetDOM()->DescribeNode(
- dom::DescribeNodeParams::Builder().SetObjectId(object_id).Build(),
- element_result_->node_frame_id,
- base::BindOnce(&ElementFinder::OnDescribeNode,
- weak_ptr_factory_.GetWeakPtr(), object_id, index));
}
void ElementFinder::OnDescribeNodeForPseudoElement(
@@ -299,30 +650,35 @@ void ElementFinder::OnDescribeNodeForPseudoElement(
dom::ResolveNodeParams::Builder()
.SetBackendNodeId(pseudo_element->GetBackendNodeId())
.Build(),
- element_result_->node_frame_id,
+ current_frame_id_,
base::BindOnce(&ElementFinder::OnResolveNodeForPseudoElement,
weak_ptr_factory_.GetWeakPtr()));
return;
}
}
}
-
- // Failed to find the pseudo element: run the callback with empty result.
- SendResult(ClientStatus(ELEMENT_RESOLUTION_FAILED));
+ DecrementResponseCountAndContinue();
}
void ElementFinder::OnResolveNodeForPseudoElement(
const DevtoolsClient::ReplyStatus& reply_status,
std::unique_ptr<dom::ResolveNodeResult> result) {
if (result && result->GetObject() && result->GetObject()->HasObjectId()) {
- element_result_->object_id = result->GetObject()->GetObjectId();
+ current_matches_.emplace_back(result->GetObject()->GetObjectId());
}
- SendResult(OkClientStatus());
+ DecrementResponseCountAndContinue();
+}
+
+void ElementFinder::EnterFrame(const std::string& object_id) {
+ devtools_client_->GetDOM()->DescribeNode(
+ dom::DescribeNodeParams::Builder().SetObjectId(object_id).Build(),
+ current_frame_id_,
+ base::BindOnce(&ElementFinder::OnDescribeNodeForFrame,
+ weak_ptr_factory_.GetWeakPtr(), object_id));
}
-void ElementFinder::OnDescribeNode(
+void ElementFinder::OnDescribeNodeForFrame(
const std::string& object_id,
- size_t index,
const DevtoolsClient::ReplyStatus& reply_status,
std::unique_ptr<dom::DescribeNodeResult> result) {
if (!result || !result->GetNode()) {
@@ -337,44 +693,26 @@ void ElementFinder::OnDescribeNode(
if (node->GetNodeName() == "IFRAME") {
DCHECK(node->HasFrameId()); // Ensure all frames have an id.
- element_result_->container_frame_selector_index = index;
- element_result_->container_frame_host =
- FindCorrespondingRenderFrameHost(node->GetFrameId());
-
- Result result_frame;
- result_frame.container_frame_selector_index =
- element_result_->container_frame_selector_index;
- result_frame.container_frame_host = element_result_->container_frame_host;
- result_frame.object_id = object_id;
- element_result_->frame_stack.emplace_back(result_frame);
+ frame_stack_.push_back(BuildResult(object_id));
- if (!element_result_->container_frame_host) {
+ auto* frame = FindCorrespondingRenderFrameHost(node->GetFrameId());
+ if (!frame) {
VLOG(1) << __func__ << " Failed to find corresponding owner frame.";
SendResult(ClientStatus(FRAME_HOST_NOT_FOUND));
return;
}
+ current_frame_ = frame;
+ current_frame_root_.clear();
if (node->HasContentDocument()) {
- // If the frame has a ContentDocument it's considered a local frame. We
- // don't need to assign the frame id, since devtools can just send the
- // commands to the main session.
-
+ // If the frame has a ContentDocument it's considered a local frame. In
+ // this case, current_frame_ doesn't change and can directly use the
+ // content document as root for the evaluation.
backend_ids.emplace_back(node->GetContentDocument()->GetBackendNodeId());
} else {
- // If the frame has no ContentDocument, it's considered an
- // OutOfProcessIFrame.
- // See https://www.chromium.org/developers/design-documents/oop-iframes
- // for full documentation.
- // With the iFrame running in a different process it is necessary to pass
- // the correct session id from devtools. We need to set the frame id,
- // such that devtools can resolve the corresponding session id.
- element_result_->node_frame_id = node->GetFrameId();
-
+ current_frame_id_ = node->GetFrameId();
// Kick off another find element chain to walk down the OOP iFrame.
- devtools_client_->GetRuntime()->Evaluate(
- std::string(kGetDocumentElement), element_result_->node_frame_id,
- base::BindOnce(&ElementFinder::OnGetDocumentElement,
- weak_ptr_factory_.GetWeakPtr(), index + 1));
+ GetDocumentElement();
return;
}
}
@@ -390,17 +728,20 @@ void ElementFinder::OnDescribeNode(
dom::ResolveNodeParams::Builder()
.SetBackendNodeId(backend_ids[0])
.Build(),
- element_result_->node_frame_id,
+ current_frame_id_,
base::BindOnce(&ElementFinder::OnResolveNode,
- weak_ptr_factory_.GetWeakPtr(), index));
+ weak_ptr_factory_.GetWeakPtr()));
return;
}
- RecursiveFindElement(object_id, index + 1);
+ // Element was not a frame and didn't have shadow dom. This is unexpected, but
+ // to remain backward compatible, don't complain and just continue filtering
+ // with the current element as root.
+ current_matches_.emplace_back(object_id);
+ DecrementResponseCountAndContinue();
}
void ElementFinder::OnResolveNode(
- size_t index,
const DevtoolsClient::ReplyStatus& reply_status,
std::unique_ptr<dom::ResolveNodeResult> result) {
if (!result || !result->GetObject() || !result->GetObject()->HasObjectId()) {
@@ -409,7 +750,13 @@ void ElementFinder::OnResolveNode(
return;
}
- RecursiveFindElement(result->GetObject()->GetObjectId(), ++index);
+ std::string object_id = result->GetObject()->GetObjectId();
+ if (current_frame_root_.empty()) {
+ current_frame_root_ = object_id;
+ }
+ // Use the node as root for the rest of the evaluation.
+ current_matches_.emplace_back(object_id);
+ DecrementResponseCountAndContinue();
}
content::RenderFrameHost* ElementFinder::FindCorrespondingRenderFrameHost(
@@ -423,4 +770,236 @@ content::RenderFrameHost* ElementFinder::FindCorrespondingRenderFrameHost(
return nullptr;
}
+void ElementFinder::ApplyProximityFilter(int filter_index,
+ const std::string& array_object_id) {
+ Selector target_selector;
+ target_selector.proto.mutable_filters()->MergeFrom(
+ selector_.proto.filters(filter_index).closest().target());
+ proximity_target_filter_ =
+ std::make_unique<ElementFinder>(web_contents_, devtools_client_,
+ target_selector, ResultType::kMatchArray);
+ proximity_target_filter_->StartInternal(
+ base::BindOnce(&ElementFinder::OnProximityFilterTarget,
+ weak_ptr_factory_.GetWeakPtr(), filter_index,
+ array_object_id),
+ current_frame_, current_frame_id_, current_frame_root_);
+}
+
+void ElementFinder::OnProximityFilterTarget(int filter_index,
+ const std::string& array_object_id,
+ const ClientStatus& status,
+ std::unique_ptr<Result> result) {
+ if (!status.ok()) {
+ VLOG(1) << __func__
+ << " Could not find proximity filter target for resolving "
+ << selector_.proto.filters(filter_index);
+ SendResult(status);
+ return;
+ }
+ if (result->container_frame_host != current_frame_) {
+ VLOG(1) << __func__ << " Cannot compare elements on different frames.";
+ SendResult(ClientStatus(INVALID_SELECTOR));
+ return;
+ }
+
+ const auto& filter = selector_.proto.filters(filter_index).closest();
+
+ std::string function = R"(function(targets, maxPairs) {
+ const candidates = this;
+ const pairs = candidates.length * targets.length;
+ if (pairs > maxPairs) {
+ return pairs;
+ }
+ const candidateBoxes = candidates.map((e) => e.getBoundingClientRect());
+ let closest = null;
+ let shortestDistance = Number.POSITIVE_INFINITY;
+ for (target of targets) {
+ const targetBox = target.getBoundingClientRect();
+ for (let i = 0; i < candidates.length; i++) {
+ const box = candidateBoxes[i];
+)";
+
+ if (filter.in_alignment()) {
+ // Rejects candidates that are not on the same row or or the same column as
+ // the target.
+ function.append("if ((box.bottom <= targetBox.top || ");
+ function.append(" box.top >= targetBox.bottom) && ");
+ function.append(" (box.right <= targetBox.left || ");
+ function.append(" box.left >= targetBox.right)) continue;");
+ }
+ switch (filter.relative_position()) {
+ case SelectorProto::ProximityFilter::UNSPECIFIED_POSITION:
+ // No constraints.
+ break;
+
+ case SelectorProto::ProximityFilter::ABOVE:
+ // Candidate must be above target
+ function.append("if (box.bottom > targetBox.top) continue;");
+ break;
+
+ case SelectorProto::ProximityFilter::BELOW:
+ // Candidate must be below target
+ function.append("if (box.top < targetBox.bottom) continue;");
+ break;
+
+ case SelectorProto::ProximityFilter::LEFT:
+ // Candidate must be left of target
+ function.append("if (box.right > targetBox.left) continue;");
+ break;
+
+ case SelectorProto::ProximityFilter::RIGHT:
+ // Candidate must be right of target
+ function.append("if (box.left < targetBox.right) continue;");
+ break;
+ }
+
+ // The algorithm below computes distance to the closest border. If the
+ // distance is 0, then we have got our closest element and can stop there.
+ function.append(R"(
+ let w = 0;
+ if (targetBox.right < box.left) {
+ w = box.left - targetBox.right;
+ } else if (box.right < targetBox.left) {
+ w = targetBox.left - box.right;
+ }
+ let h = 0;
+ if (targetBox.bottom < box.top) {
+ h = box.top - targetBox.bottom;
+ } else if (box.bottom < targetBox.top) {
+ h = targetBox.top - box.bottom;
+ }
+ const dist = Math.sqrt(h * h + w * w);
+ if (dist == 0) return candidates[i];
+ if (dist < shortestDistance) {
+ closest = candidates[i];
+ shortestDistance = dist;
+ }
+ }
+ }
+ return closest;
+})");
+
+ std::vector<std::unique_ptr<runtime::CallArgument>> arguments;
+ AddRuntimeCallArgumentObjectId(result->object_id, &arguments);
+ AddRuntimeCallArgument(filter.max_pairs(), &arguments);
+
+ devtools_client_->GetRuntime()->CallFunctionOn(
+ runtime::CallFunctionOnParams::Builder()
+ .SetObjectId(array_object_id)
+ .SetArguments(std::move(arguments))
+ .SetFunctionDeclaration(function)
+ .Build(),
+ current_frame_id_,
+ base::BindOnce(&ElementFinder::OnProximityFilterJs,
+ weak_ptr_factory_.GetWeakPtr()));
+}
+
+void ElementFinder::OnProximityFilterJs(
+ const DevtoolsClient::ReplyStatus& reply_status,
+ std::unique_ptr<runtime::CallFunctionOnResult> result) {
+ ClientStatus status =
+ CheckJavaScriptResult(reply_status, result.get(), __FILE__, __LINE__);
+ if (!status.ok()) {
+ VLOG(1) << __func__ << ": Failed to execute proximity filter " << status;
+ SendResult(status);
+ return;
+ }
+
+ std::string object_id;
+ if (SafeGetObjectId(result->GetResult(), &object_id)) {
+ // Function found a match.
+ current_matches_.push_back(object_id);
+ ExecuteNextTask();
+ return;
+ }
+
+ int pair_count = 0;
+ if (SafeGetIntValue(result->GetResult(), &pair_count)) {
+ // Function got too many pairs to check.
+ VLOG(1) << __func__ << ": Too many pairs to consider for proximity checks: "
+ << pair_count;
+ SendResult(ClientStatus(TOO_MANY_CANDIDATES));
+ return;
+ }
+
+ // Function found nothing, which is possible if the relative position
+ // constraints forced the algorithm to discard all candidates.
+ ExecuteNextTask();
+}
+
+void ElementFinder::ResolveMatchArrays(
+ const std::vector<std::string>& array_object_ids,
+ int max_count) {
+ if (array_object_ids.empty()) {
+ // Nothing to do
+ ExecuteNextTask();
+ return;
+ }
+ pending_response_count_ = array_object_ids.size();
+ for (const std::string& array_object_id : array_object_ids) {
+ ResolveMatchArrayRecursive(array_object_id, 0, max_count);
+ }
+}
+
+void ElementFinder::ResolveMatchArrayRecursive(
+ const std::string& array_object_id,
+ int index,
+ int max_count) {
+ std::vector<std::unique_ptr<runtime::CallArgument>> arguments;
+ AddRuntimeCallArgument(index, &arguments);
+ devtools_client_->GetRuntime()->CallFunctionOn(
+ runtime::CallFunctionOnParams::Builder()
+ .SetObjectId(array_object_id)
+ .SetArguments(std::move(arguments))
+ .SetFunctionDeclaration(std::string(kGetArrayElement))
+ .Build(),
+ current_frame_id_,
+ base::BindOnce(&ElementFinder::OnResolveMatchArray,
+ weak_ptr_factory_.GetWeakPtr(), array_object_id, index,
+ max_count));
+}
+
+void ElementFinder::OnResolveMatchArray(
+ const std::string& array_object_id,
+ int index,
+ int max_count,
+ const DevtoolsClient::ReplyStatus& reply_status,
+ std::unique_ptr<runtime::CallFunctionOnResult> result) {
+ ClientStatus status =
+ CheckJavaScriptResult(reply_status, result.get(), __FILE__, __LINE__);
+ if (!status.ok()) {
+ VLOG(1) << __func__ << ": Failed to get element from array for "
+ << selector_;
+ SendResult(status);
+ return;
+ }
+ std::string object_id;
+ if (!SafeGetObjectId(result->GetResult(), &object_id)) {
+ // We've reached the end of the array
+ DecrementResponseCountAndContinue();
+ return;
+ }
+
+ current_matches_.emplace_back(object_id);
+ int next_index = index + 1;
+ if (max_count != -1 && next_index >= max_count) {
+ DecrementResponseCountAndContinue();
+ return;
+ }
+
+ // Fetch the next element.
+ ResolveMatchArrayRecursive(array_object_id, next_index, max_count);
+}
+
+void ElementFinder::DecrementResponseCountAndContinue() {
+ if (pending_response_count_ > 1) {
+ pending_response_count_--;
+ return;
+ }
+
+ pending_response_count_ = 0;
+ ExecuteNextTask();
+ return;
+}
+
} // namespace autofill_assistant
diff --git a/chromium/components/autofill_assistant/browser/web/element_finder.h b/chromium/components/autofill_assistant/browser/web/element_finder.h
index d868e274d12..a783f77945d 100644
--- a/chromium/components/autofill_assistant/browser/web/element_finder.h
+++ b/chromium/components/autofill_assistant/browser/web/element_finder.h
@@ -11,6 +11,7 @@
#include "base/callback.h"
#include "base/macros.h"
#include "base/memory/weak_ptr.h"
+#include "base/strings/strcat.h"
#include "components/autofill_assistant/browser/client_status.h"
#include "components/autofill_assistant/browser/devtools/devtools/domains/types_dom.h"
#include "components/autofill_assistant/browser/devtools/devtools/domains/types_runtime.h"
@@ -29,24 +30,37 @@ class DevtoolsClient;
// Worker class to find element(s) matching a selector.
class ElementFinder : public WebControllerWorker {
public:
+ enum ResultType {
+ // Result.object_id contains the object ID of the single node that matched.
+ // If there are no matches, status is ELEMENT_RESOLUTION_FAILED. If there
+ // are more than one matches, status is TOO_MANY_ELEMENTS.
+ kExactlyOneMatch = 0,
+
+ // Result.object_id contains the object ID of one of the nodes that matched.
+ // If there are no matches, status is ELEMENT_RESOLUTION_FAILED.
+ kAnyMatch,
+
+ // Result.object_id contains the object ID of an array containing all the
+ // nodes
+ // that matched. If there are no matches, status is
+ // ELEMENT_RESOLUTION_FAILED.
+ kMatchArray,
+ };
+
struct Result {
Result();
~Result();
- Result(const Result& to_copy);
+ Result(const Result&);
// The render frame host contains the element.
- content::RenderFrameHost* container_frame_host;
-
- // The selector index in the given selectors corresponding to the container
- // frame. Zero indicates the element is in main frame or the first element
- // is the container frame selector. Compare main frame with the above
- // |container_frame_host| to distinguish them.
- size_t container_frame_selector_index;
+ content::RenderFrameHost* container_frame_host = nullptr;
// The object id of the element.
std::string object_id;
- // The id of the frame the element's node is in.
+ // The frame id to use to execute devtools Javascript calls within the
+ // context of the frame. Might be empty if no frame id needs to be
+ // specified.
std::string node_frame_id;
std::vector<Result> frame_stack;
@@ -54,10 +68,10 @@ class ElementFinder : public WebControllerWorker {
// |web_contents| and |devtools_client| must be valid for the lifetime of the
// instance.
- ElementFinder(content::WebContents* web_contents_,
+ ElementFinder(content::WebContents* web_contents,
DevtoolsClient* devtools_client,
const Selector& selector,
- bool strict);
+ ResultType result_type);
~ElementFinder() override;
using Callback =
@@ -67,15 +81,150 @@ class ElementFinder : public WebControllerWorker {
void Start(Callback callback_);
private:
+ // Helper for building JavaScript functions.
+ //
+ // TODO(b/155264465): extract this into a top-level class in its own file, so
+ // it can be tested.
+ class JsFilterBuilder {
+ public:
+ JsFilterBuilder();
+ ~JsFilterBuilder();
+
+ // Builds the argument list for the function.
+ std::vector<std::unique_ptr<runtime::CallArgument>> BuildArgumentList()
+ const;
+
+ // Return the JavaScript function.
+ std::string BuildFunction() const;
+
+ // Adds a filter, if possible.
+ bool AddFilter(const SelectorProto::Filter& filter);
+
+ private:
+ std::vector<std::string> arguments_;
+ std::vector<std::string> lines_;
+
+ // A number that's increased by each call to DeclareVariable() to make sure
+ // we generate unique variables.
+ int variable_counter_ = 0;
+
+ // Adds a regexp filter.
+ void AddRegexpFilter(const SelectorProto::TextFilter& filter,
+ const std::string& property);
+
+ // Declares and initializes a variable containing a RegExp object that
+ // correspond to |filter| and returns the variable name.
+ std::string AddRegexpInstance(const SelectorProto::TextFilter& filter);
+
+ // Returns the name of a new unique variable.
+ std::string DeclareVariable();
+
+ // Adds an argument to the argument list and returns its JavaScript
+ // representation.
+ //
+ // This allows passing strings to the JavaScript code without having to
+ // hardcode and escape them - this helps avoid XSS issues.
+ std::string AddArgument(const std::string& value);
+
+ // Adds a line of JavaScript code to the function, between the header and
+ // footer. At that point, the variable "elements" contains the current set
+ // of matches, as an array of nodes. It should be updated to contain the new
+ // set of matches.
+ void AddLine(const std::string& line) { lines_.emplace_back(line); }
+
+ void AddLine(const std::vector<std::string>& line) {
+ lines_.emplace_back(base::StrCat(line));
+ }
+ };
+
+ // Finds the element, starting at |frame| and calls |callback|.
+ //
+ // |document_object_id| might be empty, in which case we first look for the
+ // frame's document.
+ void StartInternal(Callback callback,
+ content::RenderFrameHost* frame,
+ const std::string& frame_id,
+ const std::string& document_object_id);
+
+ // Sends a result with the given status and no data.
void SendResult(const ClientStatus& status);
- void OnGetDocumentElement(size_t index,
- const DevtoolsClient::ReplyStatus& reply_status,
- std::unique_ptr<runtime::EvaluateResult> result);
- void RecursiveFindElement(const std::string& object_id, size_t index);
- void OnQuerySelectorAll(
- size_t index,
+
+ // Builds a result from the current state of the finder and returns it.
+ void SendSuccessResult(const std::string& object_id);
+
+ // Report |object_id| as result in |result| and initialize the frame-related
+ // fields of |result| from the current state. Leaves the frame stack empty.
+ Result BuildResult(const std::string& object_id);
+
+ // Figures out what to do next given the current state.
+ //
+ // Most background operations in this worker end by updating the state and
+ // calling ExecuteNextTask() again either directly or through
+ // DecrementResponseCountAndContinue().
+ void ExecuteNextTask();
+
+ // Make sure there's exactly one match, set it |object_id_out| then return
+ // true.
+ //
+ // If there are too many or too few matches, this function sends an error and
+ // returns false.
+ //
+ // If this returns true, continue processing. If this returns false, return
+ // from ExecuteNextTask(). ExecuteNextTask() will be called again once the
+ // required data is available.
+ bool ConsumeOneMatchOrFail(std::string& object_id_out);
+
+ // Make sure there's at least one match, take one and put it in
+ // |object_id_out|, then return true.
+ //
+ // If there are no matches, send an error response and return false.
+ // If there are not enough matches yet, fetch them in the background and
+ // return false. This calls ExecuteNextTask() once matches have been fetched.
+ //
+ // If this returns true, continue processing. If this returns false, return
+ // from ExecuteNextTask(). ExecuteNextTask() will be called again once the
+ // required data is available.
+ bool ConsumeAnyMatchOrFail(std::string& object_id_out);
+
+ // Make sure there's at least one match and move them all into
+ // |matches_out|.
+ //
+ // If there are no matches, send an error response and return false.
+ // If there are not enough matches yet, fetch them in the background and
+ // return false. This calls ExecuteNextTask() once matches have been fetched.
+ //
+ // If this returns true, continue processing. If this returns false, return
+ // from ExecuteNextTask(). ExecuteNextTask() will be called again once the
+ // required data is available.
+ bool ConsumeAllMatchesOrFail(std::vector<std::string>& matches_out);
+
+ // Make sure there's at least one match and move them all into a single array.
+ //
+ // If there are no matches, call SendResult() return false. If there are
+ // matches, but they're not in a single array, move the element into the array
+ // in the background and return false. ExecuteNextTask() is called again once
+ // the background tasks have executed.
+ bool ConsumeMatchArrayOrFail(std::string& array_object_id_out);
+
+ void OnConsumeMatchArray(
const DevtoolsClient::ReplyStatus& reply_status,
std::unique_ptr<runtime::CallFunctionOnResult> result);
+
+ // Gets a document element from the current frame and us it as root for the
+ // rest of the tasks.
+ void GetDocumentElement();
+ void OnGetDocumentElement(const DevtoolsClient::ReplyStatus& reply_status,
+ std::unique_ptr<runtime::EvaluateResult> result);
+
+ // Handle Javascript filters
+ void ApplyJsFilters(const JsFilterBuilder& builder,
+ const std::vector<std::string>& object_ids);
+ void OnApplyJsFilters(const DevtoolsClient::ReplyStatus& reply_status,
+ std::unique_ptr<runtime::CallFunctionOnResult> result);
+
+ // Handle PSEUDO_TYPE
+ void ResolvePseudoElement(PseudoType pseudo_type,
+ const std::vector<std::string>& object_ids);
void OnDescribeNodeForPseudoElement(
dom::PseudoType pseudo_type,
const DevtoolsClient::ReplyStatus& reply_status,
@@ -83,24 +232,107 @@ class ElementFinder : public WebControllerWorker {
void OnResolveNodeForPseudoElement(
const DevtoolsClient::ReplyStatus& reply_status,
std::unique_ptr<dom::ResolveNodeResult> result);
- void OnDescribeNode(const std::string& object_id,
- size_t index,
- const DevtoolsClient::ReplyStatus& reply_status,
- std::unique_ptr<dom::DescribeNodeResult> result);
- void OnResolveNode(size_t index,
- const DevtoolsClient::ReplyStatus& reply_status,
- std::unique_ptr<dom::ResolveNodeResult> result);
+ // Handle ENTER_FRAME
+ void EnterFrame(const std::string& object_id);
+ void OnDescribeNodeForFrame(const std::string& object_id,
+ const DevtoolsClient::ReplyStatus& reply_status,
+ std::unique_ptr<dom::DescribeNodeResult> result);
+ void OnResolveNode(const DevtoolsClient::ReplyStatus& reply_status,
+ std::unique_ptr<dom::ResolveNodeResult> result);
content::RenderFrameHost* FindCorrespondingRenderFrameHost(
std::string frame_id);
+ // Handle TaskType::PROXIMITY
+ void ApplyProximityFilter(int filter_index,
+ const std::string& array_object_id);
+ void OnProximityFilterTarget(int filter_index,
+ const std::string& array_object_id,
+ const ClientStatus& status,
+ std::unique_ptr<Result> result);
+ void OnProximityFilterJs(
+ const DevtoolsClient::ReplyStatus& reply_status,
+ std::unique_ptr<runtime::CallFunctionOnResult> result);
+
+ // Get elements from |array_object_ids|, and put the result into
+ // |element_matches_|.
+ //
+ // This calls ExecuteNextTask() once all the elements of all the arrays are in
+ // |element_matches_|. If |max_count| is -1, fetch until the end of the array,
+ // otherwise fetch |max_count| elements at most in each array.
+ void ResolveMatchArrays(const std::vector<std::string>& array_object_ids,
+ int max_count);
+
+ // ResolveMatchArrayRecursive calls itself recursively, incrementing |index|,
+ // as long as there are elements. The chain of calls end with
+ // DecrementResponseCountAndContinue() as there can be more than one such
+ // chains executing at a time.
+ void ResolveMatchArrayRecursive(const std::string& array_object_ids,
+ int index,
+ int max_count);
+
+ void OnResolveMatchArray(
+ const std::string& array_object_id,
+ int index,
+ int max_count,
+ const DevtoolsClient::ReplyStatus& reply_status,
+ std::unique_ptr<runtime::CallFunctionOnResult> result);
+
+ // Tracks pending_response_count_ and call ExecuteNextTask() once the count
+ // has reached 0.
+ void DecrementResponseCountAndContinue();
+
content::WebContents* const web_contents_;
DevtoolsClient* const devtools_client_;
const Selector selector_;
-
- const bool strict_;
+ const ResultType result_type_;
Callback callback_;
- std::unique_ptr<Result> element_result_;
+
+ // The index of the next filter to process, in selector_.proto.filters.
+ int next_filter_index_ = 0;
+
+ // Pointer to the current frame
+ content::RenderFrameHost* current_frame_ = nullptr;
+
+ // The frame id to use to execute devtools Javascript calls within the
+ // context of the frame. Might be empty if no frame id needs to be
+ // specified.
+ std::string current_frame_id_;
+
+ // Object ID of the root of |current_frame_|.
+ std::string current_frame_root_;
+
+ // Object IDs of the current set matching elements. Cleared once it's used to
+ // query or filter.
+ //
+ // More matches can be found in |current_match_arrays_|. Use one of the
+ // Consume*Match() function to current matches.
+ std::vector<std::string> current_matches_;
+
+ // Object ID of arrays of at least 2 matching elements.
+ //
+ // More matches can be found in |current_matches_|. Use one of the
+ // Consume*Match() function to current matches.
+ std::vector<std::string> current_match_arrays_;
+
+ // True if current_matches are pseudo-elements.
+ bool matching_pseudo_elements_ = false;
+
+ // Number of responses still pending.
+ //
+ // Before starting several background operations in parallel, set this counter
+ // to the number of operations and make sure that
+ // DecrementResponseCountAndContinue() is called once the result of the
+ // operation has been processed and the state of ElementFinder updated.
+ // DecrementResponseCountAndContinue() will then make sure to call
+ // ExecuteNextTask() again once this counter has reached 0 to continue the
+ // work.
+ size_t pending_response_count_ = 0;
+
+ std::vector<Result> frame_stack_;
+
+ // Finder for the target of the current proximity filter.
+ std::unique_ptr<ElementFinder> proximity_target_filter_;
base::WeakPtrFactory<ElementFinder> weak_ptr_factory_{this};
};
diff --git a/chromium/components/autofill_assistant/browser/web/element_position_getter.cc b/chromium/components/autofill_assistant/browser/web/element_position_getter.cc
index b154eb3292f..b1321964135 100644
--- a/chromium/components/autofill_assistant/browser/web/element_position_getter.cc
+++ b/chromium/components/autofill_assistant/browser/web/element_position_getter.cc
@@ -4,7 +4,6 @@
#include "components/autofill_assistant/browser/web/element_position_getter.h"
-#include "base/task/post_task.h"
#include "components/autofill_assistant/browser/devtools/devtools_client.h"
#include "components/autofill_assistant/browser/service.pb.h"
#include "components/autofill_assistant/browser/web/web_controller_util.h"
@@ -134,8 +133,8 @@ void ElementPositionGetter::OnGetBoxModelForStableCheck(
}
--remaining_rounds_;
- base::PostDelayedTask(
- FROM_HERE, {content::BrowserThread::UI},
+ content::GetUIThreadTaskRunner({})->PostDelayedTask(
+ FROM_HERE,
base::BindOnce(&ElementPositionGetter::GetAndWaitBoxModelStable,
weak_ptr_factory_.GetWeakPtr()),
check_interval_);
@@ -153,8 +152,8 @@ void ElementPositionGetter::OnScrollIntoView(
}
--remaining_rounds_;
- base::PostDelayedTask(
- FROM_HERE, {content::BrowserThread::UI},
+ content::GetUIThreadTaskRunner({})->PostDelayedTask(
+ FROM_HERE,
base::BindOnce(&ElementPositionGetter::GetAndWaitBoxModelStable,
weak_ptr_factory_.GetWeakPtr()),
check_interval_);
diff --git a/chromium/components/autofill_assistant/browser/web/element_rect_getter.cc b/chromium/components/autofill_assistant/browser/web/element_rect_getter.cc
index 544e949a802..e9154125be4 100644
--- a/chromium/components/autofill_assistant/browser/web/element_rect_getter.cc
+++ b/chromium/components/autofill_assistant/browser/web/element_rect_getter.cc
@@ -5,6 +5,7 @@
#include "components/autofill_assistant/browser/web/element_rect_getter.h"
#include "base/callback.h"
+#include "base/logging.h"
#include "base/values.h"
#include "components/autofill_assistant/browser/devtools/devtools/domains/types_runtime.h"
#include "components/autofill_assistant/browser/devtools/devtools_client.h"
diff --git a/chromium/components/autofill_assistant/browser/web/web_controller.cc b/chromium/components/autofill_assistant/browser/web/web_controller.cc
index 28ca0272cce..a6b1d74a4ab 100644
--- a/chromium/components/autofill_assistant/browser/web/web_controller.cc
+++ b/chromium/components/autofill_assistant/browser/web/web_controller.cc
@@ -17,7 +17,6 @@
#include "base/strings/strcat.h"
#include "base/strings/string_util.h"
#include "base/strings/stringprintf.h"
-#include "base/task/post_task.h"
#include "build/build_config.h"
#include "components/autofill/content/browser/content_autofill_driver.h"
#include "components/autofill/core/browser/autofill_manager.h"
@@ -645,7 +644,9 @@ void WebController::FindElement(const Selector& selector,
bool strict_mode,
ElementFinder::Callback callback) {
auto finder = std::make_unique<ElementFinder>(
- web_contents_, devtools_client_.get(), selector, strict_mode);
+ web_contents_, devtools_client_.get(), selector,
+ strict_mode ? ElementFinder::ResultType::kExactlyOneMatch
+ : ElementFinder::ResultType::kAnyMatch);
auto* ptr = finder.get();
pending_workers_.emplace_back(std::move(finder));
ptr->Start(base::BindOnce(&WebController::OnFindElementResult,
@@ -774,12 +775,16 @@ void WebController::OnFindElementForFillingForm(
FillAutofillErrorStatus(UnexpectedErrorStatus(__FILE__, __LINE__)));
return;
}
- DCHECK(!selector.empty());
- // TODO(crbug.com/806868): Figure out whether there are cases where we need
- // more than one selector, and come up with a solution that can figure out the
- // right number of selectors to include.
+
+ base::Optional<std::string> css_selector =
+ selector.ExtractSingleCssSelectorForAutofill();
+ if (!css_selector) {
+ std::move(callback).Run(ClientStatus(INVALID_SELECTOR));
+ return;
+ }
+
driver->GetAutofillAgent()->GetElementFormAndFieldData(
- std::vector<std::string>(1, selector.selectors.back()),
+ {*css_selector},
base::BindOnce(&WebController::OnGetFormAndFieldDataForFillingForm,
weak_ptr_factory_.GetWeakPtr(),
std::move(data_to_autofill), std::move(callback),
@@ -858,9 +863,16 @@ void WebController::OnFindElementToRetrieveFormAndFieldData(
autofill::FormData(), autofill::FormFieldData());
return;
}
- DCHECK(!selector.empty());
+ base::Optional<std::string> css_selector =
+ selector.ExtractSingleCssSelectorForAutofill();
+ if (!css_selector) {
+ std::move(callback).Run(ClientStatus(INVALID_SELECTOR),
+ autofill::FormData(), autofill::FormFieldData());
+ return;
+ }
+
driver->GetAutofillAgent()->GetElementFormAndFieldData(
- std::vector<std::string>(1, selector.selectors.back()),
+ {*css_selector},
base::BindOnce(&WebController::OnGetFormAndFieldDataForRetrieving,
weak_ptr_factory_.GetWeakPtr(), std::move(callback)));
}
@@ -1189,8 +1201,8 @@ void WebController::DispatchKeyboardTextDownEvent(
}
if (delay && delay_in_millisecond > 0) {
- base::PostDelayedTask(
- FROM_HERE, {content::BrowserThread::UI},
+ content::GetUIThreadTaskRunner({})->PostDelayedTask(
+ FROM_HERE,
base::BindOnce(
&WebController::DispatchKeyboardTextDownEvent,
weak_ptr_factory_.GetWeakPtr(), node_frame_id, codepoints, index,
@@ -1642,8 +1654,8 @@ void WebController::OnWaitForDocumentToBecomeInteractive(
return;
}
- base::PostDelayedTask(
- FROM_HERE, {content::BrowserThread::UI},
+ content::GetUIThreadTaskRunner({})->PostDelayedTask(
+ FROM_HERE,
base::BindOnce(&WebController::WaitForDocumentToBecomeInteractive,
weak_ptr_factory_.GetWeakPtr(), --remaining_rounds,
object_id, node_frame_id, std::move(callback)),
diff --git a/chromium/components/autofill_assistant/browser/web/web_controller_browsertest.cc b/chromium/components/autofill_assistant/browser/web/web_controller_browsertest.cc
index c03df182c13..d132e4b4d21 100644
--- a/chromium/components/autofill_assistant/browser/web/web_controller_browsertest.cc
+++ b/chromium/components/autofill_assistant/browser/web/web_controller_browsertest.cc
@@ -129,7 +129,8 @@ class WebControllerBrowserTest : public content::ContentBrowserTest,
size_t* pending_number_of_checks_output,
bool expected_result,
const ClientStatus& result) {
- EXPECT_EQ(expected_result, result.ok()) << "selector: " << selector;
+ EXPECT_EQ(expected_result, result.ok())
+ << "selector: " << selector << " status: " << result;
*pending_number_of_checks_output -= 1;
if (*pending_number_of_checks_output == 0) {
std::move(done_callback).Run();
@@ -301,14 +302,13 @@ class WebControllerBrowserTest : public content::ContentBrowserTest,
}
void FindElementAndCheck(const Selector& selector,
- size_t expected_index,
bool is_main_frame) {
SCOPED_TRACE(::testing::Message() << selector << " strict");
ClientStatus status;
ElementFinder::Result result;
FindElement(selector, &status, &result);
EXPECT_EQ(ACTION_APPLIED, status.proto_status());
- CheckFindElementResult(result, expected_index, is_main_frame);
+ CheckFindElementResult(result, is_main_frame);
}
void FindElementExpectEmptyResult(const Selector& selector) {
@@ -321,16 +321,16 @@ class WebControllerBrowserTest : public content::ContentBrowserTest,
}
void CheckFindElementResult(const ElementFinder::Result& result,
- size_t expected_index,
bool is_main_frame) {
if (is_main_frame) {
EXPECT_EQ(shell()->web_contents()->GetMainFrame(),
result.container_frame_host);
+ EXPECT_EQ(result.frame_stack.size(), 0u);
} else {
EXPECT_NE(shell()->web_contents()->GetMainFrame(),
result.container_frame_host);
+ EXPECT_GE(result.frame_stack.size(), 1u);
}
- EXPECT_EQ(result.container_frame_selector_index, expected_index);
EXPECT_FALSE(result.object_id.empty());
}
@@ -486,8 +486,7 @@ class WebControllerBrowserTest : public content::ContentBrowserTest,
// the desired y position.
void TestScrollIntoView(int initial_window_scroll_y,
int initial_container_scroll_y) {
- Selector selector;
- selector.selectors.emplace_back("#scroll_item_5");
+ Selector selector({"#scroll_item_5"});
SetupScrollContainerHeights();
ScrollWindowTo(initial_window_scroll_y);
@@ -528,11 +527,11 @@ class WebControllerBrowserTest : public content::ContentBrowserTest,
protected:
std::unique_ptr<WebController> web_controller_;
+ ClientSettings settings_;
private:
std::unique_ptr<net::EmbeddedTestServer> http_server_;
std::unique_ptr<net::EmbeddedTestServer> http_server_iframe_;
- ClientSettings settings_;
DISALLOW_COPY_AND_ASSIGN(WebControllerBrowserTest);
};
@@ -546,29 +545,42 @@ IN_PROC_BROWSER_TEST_F(WebControllerBrowserTest, ElementExistenceCheck) {
// A nonexistent element.
RunLaxElementCheck(Selector({"#doesnotexist"}), false);
+}
+IN_PROC_BROWSER_TEST_F(WebControllerBrowserTest, PseudoElementChecks) {
// A pseudo-element
- RunLaxElementCheck(Selector({"#terms-and-conditions"}, BEFORE), true);
+ RunLaxElementCheck(Selector({"#terms-and-conditions"}).SetPseudoType(BEFORE),
+ true);
// An invisible pseudo-element
//
// TODO(b/129461999): This is wrong; it should exist. Fix it.
- RunLaxElementCheck(Selector({"#button"}, BEFORE), false);
+ RunLaxElementCheck(Selector({"#button"}).SetPseudoType(BEFORE), false);
// A non-existent pseudo-element
- RunLaxElementCheck(Selector({"#button"}, AFTER), false);
+ RunLaxElementCheck(Selector({"#button"}).SetPseudoType(AFTER), false);
+}
+IN_PROC_BROWSER_TEST_F(WebControllerBrowserTest, ElementInFrameChecks) {
// An iFrame.
RunLaxElementCheck(Selector({"#iframe"}), true);
// An element in a same-origin iFrame.
RunLaxElementCheck(Selector({"#iframe", "#button"}), true);
+ // An element in a same-origin iFrame.
+ RunLaxElementCheck(Selector({"#iframe", "#doesnotexist"}), false);
+}
+
+IN_PROC_BROWSER_TEST_F(WebControllerBrowserTest, ElementInExternalFrameChecks) {
// An OOPIF.
RunLaxElementCheck(Selector({"#iframeExternal"}), true);
// An element in an OOPIF.
RunLaxElementCheck(Selector({"#iframeExternal", "#button"}), true);
+
+ // An element in an OOPIF.
+ RunLaxElementCheck(Selector({"#iframeExternal", "#doesnotexist"}), false);
}
IN_PROC_BROWSER_TEST_F(WebControllerBrowserTest, VisibilityRequirementCheck) {
@@ -583,13 +595,16 @@ IN_PROC_BROWSER_TEST_F(WebControllerBrowserTest, VisibilityRequirementCheck) {
// A pseudo-element
RunLaxElementCheck(
- Selector({"#terms-and-conditions"}, BEFORE).MustBeVisible(), true);
+ Selector({"#terms-and-conditions"}).MustBeVisible().SetPseudoType(BEFORE),
+ true);
// An invisible pseudo-element
- RunLaxElementCheck(Selector({"#button"}, BEFORE).MustBeVisible(), false);
+ RunLaxElementCheck(
+ Selector({"#button"}).MustBeVisible().SetPseudoType(BEFORE), false);
// A non-existent pseudo-element
- RunLaxElementCheck(Selector({"#button"}, AFTER).MustBeVisible(), false);
+ RunLaxElementCheck(Selector({"#button"}).MustBeVisible().SetPseudoType(AFTER),
+ false);
// An iFrame.
RunLaxElementCheck(Selector({"#iframe"}).MustBeVisible(), true);
@@ -625,61 +640,221 @@ IN_PROC_BROWSER_TEST_F(WebControllerBrowserTest, MultipleVisibleElementCheck) {
false);
}
+IN_PROC_BROWSER_TEST_F(WebControllerBrowserTest, SearchMultipleIframes) {
+ // There are two "iframe" elements in the document so the selector would need
+ // to search in both iframes, which isn't supported.
+ SelectorProto proto;
+ proto.add_filters()->set_css_selector("iframe");
+ proto.add_filters()->mutable_enter_frame();
+ proto.add_filters()->set_css_selector("#element_in_iframe_two");
+
+ ClientStatus status;
+ FindElement(Selector(proto), &status, nullptr);
+ EXPECT_EQ(TOO_MANY_ELEMENTS, status.proto_status());
+}
+
IN_PROC_BROWSER_TEST_F(WebControllerBrowserTest, InnerTextCondition) {
- Selector selector({"#with_inner_text span"});
- selector.must_be_visible = true;
+ const Selector base_selector({"#with_inner_text span"});
+ Selector selector = base_selector;
+ selector.MustBeVisible();
RunLaxElementCheck(selector, true);
RunStrictElementCheck(selector.MustBeVisible(), false);
// No matches
- selector.inner_text_pattern = "no match";
- selector.must_be_visible = false;
+ selector = base_selector;
+ selector.MatchingInnerText("no match");
RunLaxElementCheck(selector, false);
- selector.must_be_visible = true;
+ selector.MustBeVisible();
RunLaxElementCheck(selector, false);
// Matches exactly one visible element.
- selector.inner_text_pattern = "hello, world";
- selector.must_be_visible = false;
+ selector = base_selector;
+ selector.MatchingInnerText("hello, world");
RunLaxElementCheck(selector, true);
RunStrictElementCheck(selector, true);
- selector.must_be_visible = true;
+ selector.MustBeVisible();
RunLaxElementCheck(selector, true);
RunStrictElementCheck(selector, true);
+ // Matches case (in)sensitive.
+ selector = base_selector;
+ selector.MatchingInnerText("HELLO, WORLD", /* case_sensitive=*/false);
+ RunLaxElementCheck(selector, true);
+ RunStrictElementCheck(selector, true);
+ selector = base_selector;
+ selector.MatchingInnerText("HELLO, WORLD", /* case_sensitive=*/true);
+ RunLaxElementCheck(selector, false);
+ RunStrictElementCheck(selector, false);
+
// Matches two visible elements
- selector.inner_text_pattern = "^hello";
- selector.must_be_visible = false;
+ selector = base_selector;
+ selector.MatchingInnerText("^hello");
RunLaxElementCheck(selector, true);
RunStrictElementCheck(selector, false);
- selector.must_be_visible = true;
+ selector.MustBeVisible();
RunLaxElementCheck(selector, true);
RunStrictElementCheck(selector, false);
// Matches one visible, one invisible element
- selector.inner_text_pattern = "world$";
- selector.must_be_visible = false;
+ selector = base_selector;
+ selector.MatchingInnerText("world$");
+ RunLaxElementCheck(selector, true);
+ selector.MustBeVisible();
+ RunLaxElementCheck(selector, true);
+ RunStrictElementCheck(selector, true);
+}
+
+IN_PROC_BROWSER_TEST_F(WebControllerBrowserTest, PseudoTypeAndInnerText) {
+ // Inner text conditions then pseudo type vs pseudo type then inner text
+ // condition.
+ Selector selector({"#with_inner_text span"});
+ selector.MatchingInnerText("world");
+ selector.SetPseudoType(PseudoType::BEFORE);
+ RunLaxElementCheck(selector, true);
+
+ // "before" is the content of the :before, checking the text of pseudo-types
+ // doesn't work.
+ selector = Selector({"#with_inner_text span"});
+ selector.SetPseudoType(PseudoType::BEFORE);
+ selector.MatchingInnerText("before");
+ RunLaxElementCheck(selector, false);
+}
+
+IN_PROC_BROWSER_TEST_F(WebControllerBrowserTest, MultipleBefore) {
+ Selector selector({"span"});
+ selector.SetPseudoType(PseudoType::BEFORE);
+
+ // There's more than one "span" with a before, so only a lax check can
+ // succeed.
RunLaxElementCheck(selector, true);
RunStrictElementCheck(selector, false);
- selector.must_be_visible = true;
+}
+
+IN_PROC_BROWSER_TEST_F(WebControllerBrowserTest, PseudoTypeThenBoundingBox) {
+ Selector selector({"span"});
+ selector.SetPseudoType(PseudoType::BEFORE);
+ selector.proto.add_filters()->mutable_bounding_box();
+
RunLaxElementCheck(selector, true);
+}
+
+IN_PROC_BROWSER_TEST_F(WebControllerBrowserTest, PseudoTypeThenPickOne) {
+ Selector selector({"span"});
+ selector.SetPseudoType(PseudoType::BEFORE);
+ selector.proto.add_filters()->mutable_pick_one();
+
RunStrictElementCheck(selector, true);
+}
+
+IN_PROC_BROWSER_TEST_F(WebControllerBrowserTest, PseudoTypeThenCss) {
+ Selector selector({"span"});
+ selector.SetPseudoType(PseudoType::BEFORE);
+ selector.proto.add_filters()->set_css_selector("div");
+
+ // This makes no sense, but shouldn't return an unexpected error.
+ ClientStatus status;
+ ElementFinder::Result result;
+ FindElement(selector, &status, &result);
+ EXPECT_EQ(ELEMENT_RESOLUTION_FAILED, status.proto_status());
+}
+
+IN_PROC_BROWSER_TEST_F(WebControllerBrowserTest, PseudoTypeThenInnerText) {
+ Selector selector({"span"});
+ selector.SetPseudoType(PseudoType::BEFORE);
+ selector.proto.add_filters()->mutable_inner_text()->set_re2("before");
+
+ // This isn't supported yet.
+ RunLaxElementCheck(selector, false);
+}
- // Inner text conditions are applied before looking for the pseudo-type.
- selector.pseudo_type = PseudoType::BEFORE;
- selector.inner_text_pattern = "world";
- selector.must_be_visible = false;
+IN_PROC_BROWSER_TEST_F(WebControllerBrowserTest, PseudoTypeContent) {
+ Selector selector({"#with_inner_text span"});
+ auto* content =
+ selector.proto.add_filters()->mutable_pseudo_element_content();
+ content->set_pseudo_type(PseudoType::BEFORE);
+ content->mutable_content()->set_re2("before");
RunLaxElementCheck(selector, true);
- selector.inner_text_pattern = "before"; // matches :before content
+
+ content->mutable_content()->set_re2("nomatch");
RunLaxElementCheck(selector, false);
}
+IN_PROC_BROWSER_TEST_F(WebControllerBrowserTest, InnerTextThenCss) {
+ // There are two divs containing "Section with text", but only one has a
+ // button, which removes #button.
+ SelectorProto proto;
+ proto.add_filters()->set_css_selector("div");
+ proto.add_filters()->mutable_inner_text()->set_re2("Section with text");
+ proto.add_filters()->set_css_selector("button");
+
+ ClickOrTapElement(Selector(proto), ClickType::CLICK);
+ WaitForElementRemove(Selector({"#button"}));
+}
+
+IN_PROC_BROWSER_TEST_F(WebControllerBrowserTest, FindFormInputByLabel) {
+ // #option1_label refers to the labelled control by id.
+ Selector option1;
+ option1.proto.add_filters()->set_css_selector("#option1_label");
+ option1.proto.add_filters()->mutable_labelled();
+
+ const std::string option1_checked = R"(
+ document.querySelector("#option1").checked;
+ )";
+ EXPECT_FALSE(content::EvalJs(shell(), option1_checked).ExtractBool());
+ ClickOrTapElement(option1, ClickType::CLICK);
+ EXPECT_TRUE(content::EvalJs(shell(), option1_checked).ExtractBool());
+
+ // #option2 contains the labelled control.
+ Selector option2;
+ option2.proto.add_filters()->set_css_selector("#option2_label");
+ option2.proto.add_filters()->mutable_labelled();
+
+ const std::string option2_checked = R"(
+ document.querySelector("#option2").checked;
+ )";
+ EXPECT_FALSE(content::EvalJs(shell(), option2_checked).ExtractBool());
+ ClickOrTapElement(option2, ClickType::CLICK);
+ EXPECT_TRUE(content::EvalJs(shell(), option2_checked).ExtractBool());
+
+ // #button is not a label.
+ Selector not_a_label;
+ not_a_label.proto.add_filters()->set_css_selector("#button");
+ not_a_label.proto.add_filters()->mutable_labelled();
+
+ // #bad_label1 and #bad_label2 are labels that don't reference a valid
+ // element. They must not cause JavaScript errors.
+ Selector bad_label1;
+ bad_label1.proto.add_filters()->set_css_selector("#bad_label1");
+ bad_label1.proto.add_filters()->mutable_labelled();
+
+ ClientStatus status;
+ FindElement(bad_label1, &status, nullptr);
+ EXPECT_EQ(ELEMENT_RESOLUTION_FAILED, status.proto_status());
+
+ Selector bad_label2;
+ bad_label2.proto.add_filters()->set_css_selector("#bad_label2");
+ bad_label2.proto.add_filters()->mutable_labelled();
+
+ FindElement(bad_label2, &status, nullptr);
+ EXPECT_EQ(ELEMENT_RESOLUTION_FAILED, status.proto_status());
+}
+
IN_PROC_BROWSER_TEST_F(WebControllerBrowserTest, ValueCondition) {
// One match
RunLaxElementCheck(Selector({"#input1"}).MatchingValue("helloworld1"), true);
RunStrictElementCheck(Selector({"#input1"}).MatchingValue("helloworld1"),
true);
+ // Case (in)sensitive match
+ RunLaxElementCheck(Selector({"#input1"}).MatchingValue("HELLOWORLD1", false),
+ true);
+ RunLaxElementCheck(Selector({"#input1"}).MatchingValue("HELLOWORLD1", true),
+ false);
+ RunStrictElementCheck(
+ Selector({"#input1"}).MatchingValue("HELLOWORLD1", false), true);
+ RunStrictElementCheck(
+ Selector({"#input1"}).MatchingValue("HELLOWORLD1", true), false);
+
// No matches
RunLaxElementCheck(Selector({"#input2"}).MatchingValue("doesnotmatch"),
false);
@@ -704,104 +879,72 @@ IN_PROC_BROWSER_TEST_F(WebControllerBrowserTest,
std::vector<Selector> selectors;
std::vector<bool> results;
- Selector a_selector;
- a_selector.must_be_visible = true;
- a_selector.selectors.emplace_back("#button");
- selectors.emplace_back(a_selector);
+ Selector visible_button({"#button"});
+ visible_button.MustBeVisible();
+ selectors.emplace_back(visible_button);
results.emplace_back(true);
- a_selector.selectors.emplace_back("#whatever");
- selectors.emplace_back(a_selector);
+ Selector visible_with_iframe({"#button", "#watever"});
+ visible_with_iframe.MustBeVisible();
+ selectors.emplace_back(visible_with_iframe);
results.emplace_back(false);
// IFrame.
- a_selector.selectors.clear();
- a_selector.selectors.emplace_back("#iframe");
- a_selector.selectors.emplace_back("#button");
- selectors.emplace_back(a_selector);
+ selectors.emplace_back(Selector({"#iframe", "#button"}));
results.emplace_back(true);
- a_selector.selectors.emplace_back("#whatever");
- selectors.emplace_back(a_selector);
+ selectors.emplace_back(Selector({"#iframe", "#button", "#whatever"}));
results.emplace_back(false);
- a_selector.selectors.clear();
- a_selector.selectors.emplace_back("#iframe");
- a_selector.selectors.emplace_back("[name=name]");
- selectors.emplace_back(a_selector);
+ selectors.emplace_back(Selector({"#iframe", "[name=name]"}));
results.emplace_back(true);
// OOPIF.
- a_selector.selectors.clear();
- a_selector.selectors.emplace_back("#iframeExternal");
- a_selector.selectors.emplace_back("#button");
- selectors.emplace_back(a_selector);
+ selectors.emplace_back(Selector({"#iframeExternal", "#button"}));
results.emplace_back(true);
// Shadow DOM.
- a_selector.selectors.clear();
- a_selector.selectors.emplace_back("#iframe");
- a_selector.selectors.emplace_back("#shadowsection");
- a_selector.selectors.emplace_back("#shadowbutton");
- selectors.emplace_back(a_selector);
+ selectors.emplace_back(
+ Selector({"#iframe", "#shadowsection", "#shadowbutton"}));
results.emplace_back(true);
- a_selector.selectors.emplace_back("#whatever");
- selectors.emplace_back(a_selector);
+ selectors.emplace_back(
+ Selector({"#iframe", "#shadowsection", "#shadowbutton", "#whatever"}));
results.emplace_back(false);
// IFrame inside IFrame.
- a_selector.selectors.clear();
- a_selector.selectors.emplace_back("#iframe");
- a_selector.selectors.emplace_back("#iframe");
- a_selector.selectors.emplace_back("#button");
- selectors.emplace_back(a_selector);
+ selectors.emplace_back(Selector({"#iframe", "#iframe", "#button"}));
results.emplace_back(true);
- a_selector.selectors.emplace_back("#whatever");
- selectors.emplace_back(a_selector);
+ selectors.emplace_back(
+ Selector({"#iframe", "#iframe", "#button", "#whatever"}));
results.emplace_back(false);
// Hidden element.
- a_selector.selectors.clear();
- a_selector.selectors.emplace_back("#hidden");
- selectors.emplace_back(a_selector);
+ selectors.emplace_back(Selector({"#hidden"}).MustBeVisible());
results.emplace_back(false);
RunElementChecks(/* strict= */ false, selectors, results);
}
IN_PROC_BROWSER_TEST_F(WebControllerBrowserTest, ClickElement) {
- Selector selector;
- selector.selectors.emplace_back("#button");
+ Selector selector({"#button"});
ClickOrTapElement(selector, ClickType::CLICK);
WaitForElementRemove(selector);
}
IN_PROC_BROWSER_TEST_F(WebControllerBrowserTest, ClickElementInIFrame) {
- Selector selector;
- selector.selectors.emplace_back("#iframe");
- selector.selectors.emplace_back("#shadowsection");
- selector.selectors.emplace_back("#shadowbutton");
- ClickOrTapElement(selector, ClickType::CLICK);
+ ClickOrTapElement(Selector({"#iframe", "#shadowsection", "#shadowbutton"}),
+ ClickType::CLICK);
- selector.selectors.clear();
- selector.selectors.emplace_back("#iframe");
- selector.selectors.emplace_back("#button");
- WaitForElementRemove(selector);
+ WaitForElementRemove(Selector({"#iframe", "#button"}));
}
IN_PROC_BROWSER_TEST_F(WebControllerBrowserTest, ClickElementInOOPIF) {
- Selector selector;
- selector.selectors.emplace_back("#iframeExternal");
- selector.selectors.emplace_back("#button");
- ClickOrTapElement(selector, ClickType::CLICK);
+ ClickOrTapElement(Selector({"#iframeExternal", "#button"}), ClickType::CLICK);
- selector.selectors.clear();
- selector.selectors.emplace_back("#iframeExternal");
- selector.selectors.emplace_back("#div");
- WaitForElementRemove(selector);
+ WaitForElementRemove(Selector({"#iframeExternal", "#div"}));
}
IN_PROC_BROWSER_TEST_F(WebControllerBrowserTest,
@@ -820,8 +963,7 @@ IN_PROC_BROWSER_TEST_F(WebControllerBrowserTest,
scrollItem3WasClicked = true;
});)"));
- Selector selector;
- selector.selectors.emplace_back("#scroll_item_3");
+ Selector selector({"#scroll_item_3"});
ClickOrTapElement(selector, ClickType::CLICK);
EXPECT_TRUE(content::EvalJs(shell(), "scrollItem3WasClicked").ExtractBool());
@@ -831,31 +973,29 @@ IN_PROC_BROWSER_TEST_F(WebControllerBrowserTest,
}
IN_PROC_BROWSER_TEST_F(WebControllerBrowserTest, TapElement) {
- Selector selector;
- selector.selectors.emplace_back("#touch_area_two");
- ClickOrTapElement(selector, ClickType::TAP);
- WaitForElementRemove(selector);
+ Selector area_two({"#touch_area_two"});
+ ClickOrTapElement(area_two, ClickType::TAP);
+ WaitForElementRemove(area_two);
- selector.selectors.clear();
- selector.selectors.emplace_back("#touch_area_one");
- ClickOrTapElement(selector, ClickType::TAP);
- WaitForElementRemove(selector);
+ Selector area_one({"#touch_area_one"});
+ ClickOrTapElement(area_one, ClickType::TAP);
+ WaitForElementRemove(area_one);
}
-IN_PROC_BROWSER_TEST_F(WebControllerBrowserTest, TapElementMovingOutOfView) {
- Selector selector;
- selector.selectors.emplace_back("#touch_area_three");
+IN_PROC_BROWSER_TEST_F(WebControllerBrowserTest,
+ DISABLED_TapElementMovingOutOfView) {
+ Selector selector({"#touch_area_three"});
ClickOrTapElement(selector, ClickType::TAP);
WaitForElementRemove(selector);
}
-IN_PROC_BROWSER_TEST_F(WebControllerBrowserTest, TapElementAfterPageIsIdle) {
+IN_PROC_BROWSER_TEST_F(WebControllerBrowserTest,
+ DISABLED_TapElementAfterPageIsIdle) {
// Set a very long timeout to make sure either the page is idle or the test
// timeout.
WaitTillPageIsIdle(base::TimeDelta::FromHours(1));
- Selector selector;
- selector.selectors.emplace_back("#touch_area_one");
+ Selector selector({"#touch_area_one"});
ClickOrTapElement(selector, ClickType::TAP);
WaitForElementRemove(selector);
@@ -863,16 +1003,14 @@ IN_PROC_BROWSER_TEST_F(WebControllerBrowserTest, TapElementAfterPageIsIdle) {
// TODO(crbug.com/920948) Disabled for strong flakiness.
IN_PROC_BROWSER_TEST_F(WebControllerBrowserTest, DISABLED_TapElementInIFrame) {
- Selector selector;
- selector.selectors.emplace_back("#iframe");
- selector.selectors.emplace_back("#touch_area");
+ Selector selector({"#iframe", "#touch_area"});
ClickOrTapElement(selector, ClickType::TAP);
WaitForElementRemove(selector);
}
IN_PROC_BROWSER_TEST_F(WebControllerBrowserTest,
- TapRandomMovingElementRepeatedly) {
+ DISABLED_TapRandomMovingElementRepeatedly) {
Selector button_selector({"#random_moving_button"});
int num_clicks = 100;
for (int i = 0; i < num_clicks; ++i) {
@@ -903,8 +1041,7 @@ IN_PROC_BROWSER_TEST_F(WebControllerBrowserTest, TapMovingElementRepeatedly) {
}
IN_PROC_BROWSER_TEST_F(WebControllerBrowserTest, TapStaticElementRepeatedly) {
- Selector button_selector;
- button_selector.selectors.emplace_back("#static_button");
+ Selector button_selector({"#static_button"});
int num_clicks = 100;
for (int i = 0; i < num_clicks; ++i) {
@@ -914,8 +1051,7 @@ IN_PROC_BROWSER_TEST_F(WebControllerBrowserTest, TapStaticElementRepeatedly) {
std::vector<Selector> click_counter_selectors;
std::vector<std::string> expected_values;
expected_values.emplace_back(base::NumberToString(num_clicks));
- Selector click_counter_selector;
- click_counter_selector.selectors.emplace_back("#static_click_counter");
+ Selector click_counter_selector({"#static_click_counter"});
click_counter_selectors.emplace_back(click_counter_selector);
GetFieldsValue(click_counter_selectors, expected_values);
}
@@ -925,54 +1061,40 @@ IN_PROC_BROWSER_TEST_F(WebControllerBrowserTest, ClickPseudoElement) {
document.querySelector("#terms-and-conditions").checked;
)";
EXPECT_FALSE(content::EvalJs(shell(), javascript).ExtractBool());
- Selector selector({R"(label[for="terms-and-conditions"])"},
- PseudoType::BEFORE);
+ Selector selector({R"(label[for="terms-and-conditions"])"});
+ selector.SetPseudoType(PseudoType::BEFORE);
ClickOrTapElement(selector, ClickType::CLICK);
EXPECT_TRUE(content::EvalJs(shell(), javascript).ExtractBool());
}
IN_PROC_BROWSER_TEST_F(WebControllerBrowserTest, FindElement) {
- Selector selector;
- selector.selectors.emplace_back("#button");
- FindElementAndCheck(selector, 0, true);
- selector.must_be_visible = true;
- FindElementAndCheck(selector, 0, true);
+ Selector selector({"#button"});
+ FindElementAndCheck(selector, true);
+ selector.MustBeVisible();
+ FindElementAndCheck(selector, true);
// IFrame.
- selector.selectors.clear();
- selector.selectors.emplace_back("#iframe");
- selector.selectors.emplace_back("#button");
- selector.must_be_visible = false;
- FindElementAndCheck(selector, 0, false);
- selector.must_be_visible = true;
- FindElementAndCheck(selector, 0, false);
-
- selector.selectors.clear();
- selector.selectors.emplace_back("#iframe");
- selector.selectors.emplace_back("[name=name]");
- selector.must_be_visible = false;
- FindElementAndCheck(selector, 0, false);
- selector.must_be_visible = true;
- FindElementAndCheck(selector, 0, false);
+ selector = Selector({"#iframe", "#button"});
+ FindElementAndCheck(selector, false);
+ selector.MustBeVisible();
+ FindElementAndCheck(selector, false);
+
+ selector = Selector({"#iframe", "[name=name]"});
+ FindElementAndCheck(selector, false);
+ selector.MustBeVisible();
+ FindElementAndCheck(selector, false);
// IFrame inside IFrame.
- selector.selectors.clear();
- selector.selectors.emplace_back("#iframe");
- selector.selectors.emplace_back("#iframe");
- selector.selectors.emplace_back("#button");
- selector.must_be_visible = false;
- FindElementAndCheck(selector, 1, false);
- selector.must_be_visible = true;
- FindElementAndCheck(selector, 1, false);
+ selector = Selector({"#iframe", "#iframe", "#button"});
+ FindElementAndCheck(selector, false);
+ selector.MustBeVisible();
+ FindElementAndCheck(selector, false);
// OutOfProcessIFrame.
- selector.selectors.clear();
- selector.selectors.emplace_back("#iframeExternal");
- selector.selectors.emplace_back("#button");
- selector.must_be_visible = false;
- FindElementAndCheck(selector, 0, false);
- selector.must_be_visible = true;
- FindElementAndCheck(selector, 0, false);
+ selector = Selector({"#iframeExternal", "#button"});
+ FindElementAndCheck(selector, false);
+ selector.MustBeVisible();
+ FindElementAndCheck(selector, false);
}
IN_PROC_BROWSER_TEST_F(WebControllerBrowserTest, FindElementNotFound) {
@@ -986,25 +1108,18 @@ IN_PROC_BROWSER_TEST_F(WebControllerBrowserTest, FindElementNotFound) {
IN_PROC_BROWSER_TEST_F(WebControllerBrowserTest, FindElementErrorStatus) {
ClientStatus status;
- FindElement(
- Selector(ElementReferenceProto::default_instance()).MustBeVisible(),
- &status, nullptr);
+ FindElement(Selector(SelectorProto::default_instance()), &status, nullptr);
EXPECT_EQ(INVALID_SELECTOR, status.proto_status());
- FindElement(Selector({"#doesnotexist"}).MustBeVisible(), &status, nullptr);
+ FindElement(Selector({"#doesnotexist"}), &status, nullptr);
EXPECT_EQ(ELEMENT_RESOLUTION_FAILED, status.proto_status());
FindElement(Selector({"div"}), &status, nullptr);
EXPECT_EQ(TOO_MANY_ELEMENTS, status.proto_status());
-
- FindElement(Selector({"div"}).MustBeVisible(), &status, nullptr);
- EXPECT_EQ(TOO_MANY_ELEMENTS, status.proto_status());
}
IN_PROC_BROWSER_TEST_F(WebControllerBrowserTest, FocusElement) {
- Selector selector;
- selector.selectors.emplace_back("#iframe");
- selector.selectors.emplace_back("#focus");
+ Selector selector({"#iframe", "#focus"});
const std::string checkVisibleScript = R"(
let iframe = document.querySelector("#iframe");
@@ -1033,8 +1148,7 @@ IN_PROC_BROWSER_TEST_F(WebControllerBrowserTest,
IN_PROC_BROWSER_TEST_F(WebControllerBrowserTest,
FocusElement_WithPaddingInPixels) {
- Selector selector;
- selector.selectors.emplace_back("#scroll-me");
+ Selector selector({"#scroll-me"});
const std::string checkScrollDifferentThanTargetScript = R"(
window.scrollTo(0, 0);
@@ -1062,8 +1176,7 @@ IN_PROC_BROWSER_TEST_F(WebControllerBrowserTest,
IN_PROC_BROWSER_TEST_F(WebControllerBrowserTest,
FocusElement_WithPaddingInRatio) {
- Selector selector;
- selector.selectors.emplace_back("#scroll-me");
+ Selector selector({"#scroll-me"});
const std::string checkScrollDifferentThanTargetScript = R"(
window.scrollTo(0, 0);
@@ -1094,8 +1207,7 @@ IN_PROC_BROWSER_TEST_F(WebControllerBrowserTest,
}
IN_PROC_BROWSER_TEST_F(WebControllerBrowserTest, SelectOption) {
- Selector selector;
- selector.selectors.emplace_back("#select");
+ Selector selector({"#select"});
const std::string javascript = R"(
let select = document.querySelector("#select");
@@ -1122,20 +1234,16 @@ IN_PROC_BROWSER_TEST_F(WebControllerBrowserTest, SelectOption) {
SelectOption(selector, "Aü万𠜎", VALUE_MATCH).proto_status());
EXPECT_EQ("Character Test Entry", content::EvalJs(shell(), javascript));
- selector.selectors.clear();
- selector.selectors.emplace_back("#incorrect_selector");
EXPECT_EQ(ELEMENT_RESOLUTION_FAILED,
- SelectOption(selector, "not important", LABEL_STARTS_WITH)
+ SelectOption(Selector({"#incorrect_selector"}), "not important",
+ LABEL_STARTS_WITH)
.proto_status());
}
IN_PROC_BROWSER_TEST_F(WebControllerBrowserTest, SelectOptionInIFrame) {
- Selector select_selector;
// IFrame.
- select_selector.selectors.clear();
- select_selector.selectors.emplace_back("#iframe");
- select_selector.selectors.emplace_back("select[name=state]");
+ Selector select_selector({"#iframe", "select[name=state]"});
EXPECT_EQ(
ACTION_APPLIED,
SelectOption(select_selector, "NY", LABEL_STARTS_WITH).proto_status());
@@ -1149,16 +1257,12 @@ IN_PROC_BROWSER_TEST_F(WebControllerBrowserTest, SelectOptionInIFrame) {
// OOPIF.
// Checking elements through EvalJs in OOPIF is blocked by cross-site.
- select_selector.selectors.clear();
- select_selector.selectors.emplace_back("#iframeExternal");
- select_selector.selectors.emplace_back("select[name=pet]");
+ select_selector = Selector({"#iframeExternal", "select[name=pet]"});
EXPECT_EQ(
ACTION_APPLIED,
SelectOption(select_selector, "Cat", LABEL_STARTS_WITH).proto_status());
- Selector result_selector;
- result_selector.selectors.emplace_back("#iframeExternal");
- result_selector.selectors.emplace_back("#myPet");
+ Selector result_selector({"#iframeExternal", "#myPet"});
GetFieldsValue({result_selector}, {"Cat"});
}
@@ -1166,25 +1270,20 @@ IN_PROC_BROWSER_TEST_F(WebControllerBrowserTest, GetOuterHtml) {
std::string html;
// Div.
- Selector div_selector;
- div_selector.selectors.emplace_back("#testOuterHtml");
+ Selector div_selector({"#testOuterHtml"});
ASSERT_EQ(ACTION_APPLIED, GetOuterHtml(div_selector, &html).proto_status());
EXPECT_EQ(
R"(<div id="testOuterHtml"><span>Span</span><p>Paragraph</p></div>)",
html);
// IFrame.
- Selector iframe_selector;
- iframe_selector.selectors.emplace_back("#iframe");
- iframe_selector.selectors.emplace_back("#input");
+ Selector iframe_selector({"#iframe", "#input"});
ASSERT_EQ(ACTION_APPLIED,
GetOuterHtml(iframe_selector, &html).proto_status());
EXPECT_EQ(R"(<input id="input" type="text">)", html);
// OOPIF.
- Selector oopif_selector;
- oopif_selector.selectors.emplace_back("#iframeExternal");
- oopif_selector.selectors.emplace_back("#divToRemove");
+ Selector oopif_selector({"#iframeExternal", "#divToRemove"});
ASSERT_EQ(ACTION_APPLIED, GetOuterHtml(oopif_selector, &html).proto_status());
EXPECT_EQ(R"(<div id="divToRemove">Text</div>)", html);
}
@@ -1213,15 +1312,13 @@ IN_PROC_BROWSER_TEST_F(WebControllerBrowserTest, GetAndSetFieldValue) {
std::vector<Selector> selectors;
std::vector<std::string> expected_values;
- Selector a_selector;
- a_selector.selectors.emplace_back("body"); // Body has 'undefined' value
+ Selector a_selector({"body"}); // Body has 'undefined' value
selectors.emplace_back(a_selector);
expected_values.emplace_back("");
GetFieldsValue(selectors, expected_values);
selectors.clear();
- a_selector.selectors.clear();
- a_selector.selectors.emplace_back("#input1");
+ a_selector = Selector({"#input1"});
selectors.emplace_back(a_selector);
expected_values.clear();
expected_values.emplace_back("helloworld1");
@@ -1234,8 +1331,7 @@ IN_PROC_BROWSER_TEST_F(WebControllerBrowserTest, GetAndSetFieldValue) {
GetFieldsValue(selectors, expected_values);
selectors.clear();
- a_selector.selectors.clear();
- a_selector.selectors.emplace_back("#uppercase_input");
+ a_selector = Selector({"#uppercase_input"});
selectors.emplace_back(a_selector);
EXPECT_EQ(ACTION_APPLIED,
SetFieldValue(a_selector, /* Zürich */ "Z\xc3\xbcrich",
@@ -1246,8 +1342,7 @@ IN_PROC_BROWSER_TEST_F(WebControllerBrowserTest, GetAndSetFieldValue) {
GetFieldsValue(selectors, expected_values);
selectors.clear();
- a_selector.selectors.clear();
- a_selector.selectors.emplace_back("#input2");
+ a_selector = Selector({"#input2"});
selectors.emplace_back(a_selector);
expected_values.clear();
expected_values.emplace_back("helloworld2");
@@ -1260,8 +1355,7 @@ IN_PROC_BROWSER_TEST_F(WebControllerBrowserTest, GetAndSetFieldValue) {
GetFieldsValue(selectors, expected_values);
selectors.clear();
- a_selector.selectors.clear();
- a_selector.selectors.emplace_back("#input3");
+ a_selector = Selector({"#input3"});
selectors.emplace_back(a_selector);
expected_values.clear();
expected_values.emplace_back("helloworld3");
@@ -1274,8 +1368,7 @@ IN_PROC_BROWSER_TEST_F(WebControllerBrowserTest, GetAndSetFieldValue) {
GetFieldsValue(selectors, expected_values);
selectors.clear();
- a_selector.selectors.clear();
- a_selector.selectors.emplace_back("#invalid_selector");
+ a_selector = Selector({"#invalid_selector"});
selectors.emplace_back(a_selector);
expected_values.clear();
expected_values.emplace_back("");
@@ -1286,20 +1379,15 @@ IN_PROC_BROWSER_TEST_F(WebControllerBrowserTest, GetAndSetFieldValue) {
}
IN_PROC_BROWSER_TEST_F(WebControllerBrowserTest, GetAndSetFieldValueInIFrame) {
- Selector a_selector;
// IFrame.
- a_selector.selectors.clear();
- a_selector.selectors.emplace_back("#iframe");
- a_selector.selectors.emplace_back("#input");
+ Selector a_selector({"#iframe", "#input"});
EXPECT_EQ(ACTION_APPLIED,
SetFieldValue(a_selector, "text", SET_VALUE).proto_status());
GetFieldsValue({a_selector}, {"text"});
// OOPIF.
- a_selector.selectors.clear();
- a_selector.selectors.emplace_back("#iframeExternal");
- a_selector.selectors.emplace_back("#input");
+ a_selector = Selector({"#iframeExternal", "#input"});
EXPECT_EQ(ACTION_APPLIED,
SetFieldValue(a_selector, "text", SET_VALUE).proto_status());
GetFieldsValue({a_selector}, {"text"});
@@ -1310,8 +1398,7 @@ IN_PROC_BROWSER_TEST_F(WebControllerBrowserTest, SendKeyboardInput) {
std::string expected_output = "Zürich";
std::vector<Selector> selectors;
- Selector a_selector;
- a_selector.selectors.emplace_back("#input6");
+ Selector a_selector({"#input6"});
selectors.emplace_back(a_selector);
EXPECT_EQ(ACTION_APPLIED,
SendKeyboardInput(a_selector, input).proto_status());
@@ -1324,8 +1411,7 @@ IN_PROC_BROWSER_TEST_F(WebControllerBrowserTest,
std::string expected_output = "ZürichEnter";
std::vector<Selector> selectors;
- Selector a_selector;
- a_selector.selectors.emplace_back("#input_js_event_listener");
+ Selector a_selector({"#input_js_event_listener"});
selectors.emplace_back(a_selector);
EXPECT_EQ(ACTION_APPLIED,
SendKeyboardInput(a_selector, input).proto_status());
@@ -1342,8 +1428,7 @@ IN_PROC_BROWSER_TEST_F(WebControllerBrowserTest,
std::string expected_output = "012 345";
std::vector<Selector> selectors;
- Selector a_selector;
- a_selector.selectors.emplace_back("#input_js_event_with_timeout");
+ Selector a_selector({"#input_js_event_with_timeout"});
selectors.emplace_back(a_selector);
EXPECT_EQ(ACTION_APPLIED,
SendKeyboardInput(a_selector, input, /*delay_in_milli*/ 100)
@@ -1352,10 +1437,9 @@ IN_PROC_BROWSER_TEST_F(WebControllerBrowserTest,
}
IN_PROC_BROWSER_TEST_F(WebControllerBrowserTest, SetAttribute) {
- Selector selector;
std::vector<std::string> attribute;
- selector.selectors.emplace_back("#full_height_section");
+ Selector selector({"#full_height_section"});
attribute.emplace_back("style");
attribute.emplace_back("backgroundColor");
std::string value = "red";
@@ -1372,28 +1456,23 @@ IN_PROC_BROWSER_TEST_F(WebControllerBrowserTest, ConcurrentGetFieldsValue) {
std::vector<Selector> selectors;
std::vector<std::string> expected_values;
- Selector a_selector;
- a_selector.selectors.emplace_back("#input1");
+ Selector a_selector({"#input1"});
selectors.emplace_back(a_selector);
expected_values.emplace_back("helloworld1");
- a_selector.selectors.clear();
- a_selector.selectors.emplace_back("#input2");
+ a_selector = Selector({"#input2"});
selectors.emplace_back(a_selector);
expected_values.emplace_back("helloworld2");
- a_selector.selectors.clear();
- a_selector.selectors.emplace_back("#input3");
+ a_selector = Selector({"#input3"});
selectors.emplace_back(a_selector);
expected_values.emplace_back("helloworld3");
- a_selector.selectors.clear();
- a_selector.selectors.emplace_back("#input4");
+ a_selector = Selector({"#input4"});
selectors.emplace_back(a_selector);
expected_values.emplace_back("helloworld4");
- a_selector.selectors.clear();
- a_selector.selectors.emplace_back("#input5");
+ a_selector = Selector({"#input5"});
selectors.emplace_back(a_selector);
expected_values.emplace_back("helloworld5");
@@ -1410,8 +1489,7 @@ IN_PROC_BROWSER_TEST_F(WebControllerBrowserTest, NavigateToUrl) {
}
IN_PROC_BROWSER_TEST_F(WebControllerBrowserTest, HighlightElement) {
- Selector selector;
- selector.selectors.emplace_back("#select");
+ Selector selector({"#select"});
const std::string javascript = R"(
let select = document.querySelector("#select");
@@ -1509,4 +1587,233 @@ IN_PROC_BROWSER_TEST_F(WebControllerBrowserTest, GetElementPosition) {
EXPECT_LT(iframe_element_rect.bottom, iframe_rect.bottom);
}
+IN_PROC_BROWSER_TEST_F(WebControllerBrowserTest, GetElementByProximity) {
+ Selector input1_selector({"input"});
+ auto* input1_closest = input1_selector.proto.add_filters()->mutable_closest();
+ input1_closest->add_target()->set_css_selector("label");
+ input1_closest->add_target()->mutable_inner_text()->set_re2("Input1");
+
+ GetFieldsValue({input1_selector}, {"helloworld1"});
+}
+
+IN_PROC_BROWSER_TEST_F(WebControllerBrowserTest,
+ GetElementByProximityWithTooManyCandidates) {
+ Selector selector({"input.pairs"});
+ auto* closest = selector.proto.add_filters()->mutable_closest();
+ closest->add_target()->set_css_selector("label.pairs");
+ closest->set_max_pairs(24);
+
+ ClientStatus status;
+ ElementFinder::Result result;
+ FindElement(selector, &status, &result);
+ EXPECT_EQ(TOO_MANY_CANDIDATES, status.proto_status());
+
+ closest->set_max_pairs(25);
+ FindElement(selector, &status, &result);
+ EXPECT_EQ(ACTION_APPLIED, status.proto_status());
+}
+
+IN_PROC_BROWSER_TEST_F(WebControllerBrowserTest, ProximityRelative_Position) {
+ Selector selector({"#at_center"});
+ auto* closest = selector.proto.add_filters()->mutable_closest();
+ closest->add_target()->set_css_selector("table.proximity td");
+ auto* inner_text = closest->add_target()->mutable_inner_text();
+
+ // The cells of the table look like the following:
+ //
+ // One Two Three
+ // Four Center Five
+ // Six Seven Eight
+ //
+ // The element is "Center", the target is "One" to "Eight". The
+ // relative_position specify that the element should be below|above|... the
+ // target.
+
+ closest->set_relative_position(SelectorProto::ProximityFilter::BELOW);
+ inner_text->set_re2("One");
+ RunStrictElementCheck(selector, true);
+ inner_text->set_re2("Two");
+ RunStrictElementCheck(selector, true);
+ inner_text->set_re2("Three");
+ RunStrictElementCheck(selector, true);
+ inner_text->set_re2("Four");
+ RunStrictElementCheck(selector, false);
+ inner_text->set_re2("Five");
+ RunStrictElementCheck(selector, false);
+ inner_text->set_re2("Six");
+ RunStrictElementCheck(selector, false);
+ inner_text->set_re2("Seven");
+ RunStrictElementCheck(selector, false);
+ inner_text->set_re2("Eight");
+ RunStrictElementCheck(selector, false);
+ inner_text->set_re2("Center");
+ RunStrictElementCheck(selector, false);
+
+ closest->set_relative_position(SelectorProto::ProximityFilter::ABOVE);
+ inner_text->set_re2("One");
+ RunStrictElementCheck(selector, false);
+ inner_text->set_re2("Two");
+ RunStrictElementCheck(selector, false);
+ inner_text->set_re2("Three");
+ RunStrictElementCheck(selector, false);
+ inner_text->set_re2("Four");
+ RunStrictElementCheck(selector, false);
+ inner_text->set_re2("Five");
+ RunStrictElementCheck(selector, false);
+ inner_text->set_re2("Six");
+ RunStrictElementCheck(selector, true);
+ inner_text->set_re2("Seven");
+ RunStrictElementCheck(selector, true);
+ inner_text->set_re2("Eight");
+ RunStrictElementCheck(selector, true);
+ inner_text->set_re2("Center");
+ RunStrictElementCheck(selector, false);
+
+ closest->set_relative_position(SelectorProto::ProximityFilter::LEFT);
+ inner_text->set_re2("One");
+ RunStrictElementCheck(selector, false);
+ inner_text->set_re2("Two");
+ RunStrictElementCheck(selector, false);
+ inner_text->set_re2("Three");
+ RunStrictElementCheck(selector, true);
+ inner_text->set_re2("Four");
+ RunStrictElementCheck(selector, false);
+ inner_text->set_re2("Five");
+ RunStrictElementCheck(selector, true);
+ inner_text->set_re2("Six");
+ RunStrictElementCheck(selector, false);
+ inner_text->set_re2("Seven");
+ RunStrictElementCheck(selector, false);
+ inner_text->set_re2("Eight");
+ RunStrictElementCheck(selector, true);
+ inner_text->set_re2("Center");
+ RunStrictElementCheck(selector, false);
+
+ closest->set_relative_position(SelectorProto::ProximityFilter::RIGHT);
+ inner_text->set_re2("One");
+ RunStrictElementCheck(selector, true);
+ inner_text->set_re2("Two");
+ RunStrictElementCheck(selector, false);
+ inner_text->set_re2("Three");
+ RunStrictElementCheck(selector, false);
+ inner_text->set_re2("Four");
+ RunStrictElementCheck(selector, true);
+ inner_text->set_re2("Five");
+ RunStrictElementCheck(selector, false);
+ inner_text->set_re2("Six");
+ RunStrictElementCheck(selector, true);
+ inner_text->set_re2("Seven");
+ RunStrictElementCheck(selector, false);
+ inner_text->set_re2("Eight");
+ RunStrictElementCheck(selector, false);
+ inner_text->set_re2("Center");
+ RunStrictElementCheck(selector, false);
+}
+
+IN_PROC_BROWSER_TEST_F(WebControllerBrowserTest, ProximityAlignment) {
+ Selector selector({"#at_center"});
+ auto* closest = selector.proto.add_filters()->mutable_closest();
+ closest->add_target()->set_css_selector("table.proximity td");
+ auto* inner_text = closest->add_target()->mutable_inner_text();
+
+ closest->set_in_alignment(true);
+ inner_text->set_re2("One");
+ RunStrictElementCheck(selector, false);
+ inner_text->set_re2("Two");
+ RunStrictElementCheck(selector, true);
+ inner_text->set_re2("Three");
+ RunStrictElementCheck(selector, false);
+ inner_text->set_re2("Four");
+ RunStrictElementCheck(selector, true);
+ inner_text->set_re2("Five");
+ RunStrictElementCheck(selector, true);
+ inner_text->set_re2("Six");
+ RunStrictElementCheck(selector, false);
+ inner_text->set_re2("Seven");
+ RunStrictElementCheck(selector, true);
+ inner_text->set_re2("Eight");
+ RunStrictElementCheck(selector, false);
+ inner_text->set_re2("Center");
+ RunStrictElementCheck(selector, true);
+}
+
+IN_PROC_BROWSER_TEST_F(WebControllerBrowserTest,
+ ProximityAlignmentWithPosition) {
+ Selector selector({"#at_center"});
+ auto* closest = selector.proto.add_filters()->mutable_closest();
+ closest->add_target()->set_css_selector("table.proximity td");
+ auto* inner_text = closest->add_target()->mutable_inner_text();
+
+ closest->set_in_alignment(true);
+ closest->set_relative_position(SelectorProto::ProximityFilter::LEFT);
+
+ inner_text->set_re2("One");
+ RunStrictElementCheck(selector, false);
+ inner_text->set_re2("Two");
+ RunStrictElementCheck(selector, false);
+ inner_text->set_re2("Three");
+ RunStrictElementCheck(selector, false);
+ inner_text->set_re2("Four");
+ RunStrictElementCheck(selector, false);
+ inner_text->set_re2("Five");
+ RunStrictElementCheck(selector, true);
+ inner_text->set_re2("Six");
+ RunStrictElementCheck(selector, false);
+ inner_text->set_re2("Seven");
+ RunStrictElementCheck(selector, false);
+ inner_text->set_re2("Eight");
+ RunStrictElementCheck(selector, false);
+ inner_text->set_re2("Center");
+ RunStrictElementCheck(selector, false);
+}
+
+IN_PROC_BROWSER_TEST_F(WebControllerBrowserTest,
+ FindPseudoElementToClickByProximity) {
+ const std::string javascript = R"(
+ document.querySelector("#terms-and-conditions").checked;
+ )";
+ EXPECT_FALSE(content::EvalJs(shell(), javascript).ExtractBool());
+
+ // This test clicks on the before pseudo-element that's closest to
+ // #terms-and-conditions - this has the same effect as clicking on
+ // #terms-and-conditions. This checks that pseudo-elements have positions and
+ // that we can go through an array of pseudo-elements and choose the closest
+ // one.
+ Selector selector({"label, span"});
+ selector.SetPseudoType(PseudoType::BEFORE);
+ auto* closest = selector.proto.add_filters()->mutable_closest();
+ closest->add_target()->set_css_selector("#terms-and-conditions");
+
+ ClickOrTapElement(selector, ClickType::CLICK);
+ EXPECT_TRUE(content::EvalJs(shell(), javascript).ExtractBool());
+}
+
+IN_PROC_BROWSER_TEST_F(WebControllerBrowserTest,
+ GetElementByProximityDifferentFrames) {
+ Selector selector({"input"});
+ auto* closest = selector.proto.add_filters()->mutable_closest();
+ closest->add_target()->set_css_selector("#iframe");
+ closest->add_target()->mutable_pick_one();
+ closest->add_target()->mutable_enter_frame();
+ closest->add_target()->set_css_selector("div");
+
+ // Cannot compare position of elements on different frames.
+ ClientStatus status;
+ FindElement(Selector(SelectorProto::default_instance()), &status, nullptr);
+ EXPECT_EQ(INVALID_SELECTOR, status.proto_status());
+}
+
+IN_PROC_BROWSER_TEST_F(WebControllerBrowserTest,
+ GetElementByProximitySameFrame) {
+ Selector selector({"#iframe", "input[name='email']"});
+
+ // The target is searched within #iframe.
+ auto* closest = selector.proto.add_filters()->mutable_closest();
+ closest->add_target()->set_css_selector("span");
+ closest->add_target()->mutable_inner_text()->set_re2("Email");
+
+ RunLaxElementCheck(selector, true);
+ GetFieldsValue({selector}, {"email@example.com"});
+}
+
} // namespace autofill_assistant