diff options
author | Allan Sandfeld Jensen <allan.jensen@qt.io> | 2020-10-12 14:27:29 +0200 |
---|---|---|
committer | Allan Sandfeld Jensen <allan.jensen@qt.io> | 2020-10-13 09:35:20 +0000 |
commit | c30a6232df03e1efbd9f3b226777b07e087a1122 (patch) | |
tree | e992f45784689f373bcc38d1b79a239ebe17ee23 /chromium/components/autofill_assistant/browser/web | |
parent | 7b5b123ac58f58ffde0f4f6e488bcd09aa4decd3 (diff) | |
download | qtwebengine-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')
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 |