Commit 57f8507b authored by sandromaggi's avatar sandromaggi Committed by Commit Bot

[Autofill Assistant] GetElementStatus: Add innerText and TextFilter

This CL uses the new |GetStringAttribute| web-action to get the required
attribute for matching. It extends the previously |value| only action
to also support |innerText|.

This CL adds the support for |SelectorProto.TextFilter| as the RE2
enabled filter criterium.

Bug: b/169924567
Change-Id: I182504434c7769674f6b9ddecc98d74cb57a4f4e
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2464265
Commit-Queue: Sandro Maggi <sandromaggi@google.com>
Reviewed-by: default avatarStephane Zermatten <szermatt@chromium.org>
Reviewed-by: default avatarLuca Hunkeler <hluca@google.com>
Cr-Commit-Position: refs/heads/master@{#816563}
parent 6d65b916
...@@ -4,53 +4,75 @@ ...@@ -4,53 +4,75 @@
#include "components/autofill_assistant/browser/actions/get_element_status_action.h" #include "components/autofill_assistant/browser/actions/get_element_status_action.h"
#include "base/i18n/case_conversion.h"
#include "base/stl_util.h" #include "base/stl_util.h"
#include "base/strings/string_util.h" #include "base/strings/string_util.h"
#include "base/strings/utf_string_conversions.h" #include "base/strings/utf_string_conversions.h"
#include "components/autofill/core/browser/data_model/autofill_profile.h" #include "components/autofill/core/browser/data_model/autofill_profile.h"
#include "components/autofill_assistant/browser/actions/action_delegate.h" #include "components/autofill_assistant/browser/actions/action_delegate.h"
#include "components/autofill_assistant/browser/actions/action_delegate_util.h"
#include "components/autofill_assistant/browser/client_status.h" #include "components/autofill_assistant/browser/client_status.h"
#include "components/autofill_assistant/browser/service.pb.h" #include "components/autofill_assistant/browser/service.pb.h"
#include "components/autofill_assistant/browser/user_data_util.h" #include "components/autofill_assistant/browser/user_data_util.h"
#include "third_party/re2/src/re2/re2.h"
namespace autofill_assistant { namespace autofill_assistant {
namespace { namespace {
std::string PrepareStringForMatching(const std::string& value, struct MaybeRe2 {
bool case_sensitive, std::string value;
bool remove_space) { bool is_re2 = false;
};
std::string RemoveWhitespace(const std::string& value) {
std::string copy = value; std::string copy = value;
if (!case_sensitive) { base::EraseIf(copy, base::IsUnicodeWhitespace);
copy = base::UTF16ToUTF8(base::i18n::FoldCase(base::UTF8ToUTF16(copy)));
}
if (remove_space) {
base::EraseIf(copy, base::IsUnicodeWhitespace);
}
return copy; return copy;
} }
GetElementStatusProto::ComparisonReport CreateComparisonReport( GetElementStatusProto::ComparisonReport CreateComparisonReport(
const std::string& actual, const std::string& actual,
const std::string& expected, const MaybeRe2& re2,
bool case_sensitive, bool case_sensitive,
bool remove_space) { bool remove_space) {
std::string actual_for_match =
PrepareStringForMatching(actual, case_sensitive, remove_space);
std::string expected_for_match =
PrepareStringForMatching(expected, case_sensitive, remove_space);
GetElementStatusProto::ComparisonReport report; GetElementStatusProto::ComparisonReport report;
report.mutable_match_options()->set_case_sensitive(case_sensitive); report.mutable_match_options()->set_case_sensitive(case_sensitive);
report.mutable_match_options()->set_remove_space(remove_space); report.mutable_match_options()->set_remove_space(remove_space);
report.set_full_match(actual_for_match == expected_for_match); std::string actual_for_match =
size_t pos = actual_for_match.find(expected_for_match); remove_space ? RemoveWhitespace(actual) : actual;
report.set_empty(actual_for_match.empty());
if (!re2.is_re2 && re2.value.empty()) {
if (actual_for_match.empty()) {
report.set_full_match(true);
report.set_contains(true);
report.set_starts_with(true);
report.set_ends_with(true);
}
return report;
}
std::string re2_for_match =
re2.is_re2 ? re2.value
: re2::RE2::QuoteMeta(
remove_space ? RemoveWhitespace(re2.value) : re2.value);
re2::RE2::Options options;
options.set_case_sensitive(case_sensitive);
re2::RE2 regexp(re2_for_match, options);
std::string match;
bool found_match = RE2::Extract(actual_for_match, regexp, "\\0", &match);
if (!found_match) {
return report;
}
report.set_full_match(actual_for_match == match);
size_t pos = actual_for_match.find(match);
report.set_contains(pos != std::string::npos); report.set_contains(pos != std::string::npos);
report.set_starts_with(pos != std::string::npos && pos == 0); report.set_starts_with(pos != std::string::npos && pos == 0);
report.set_ends_with(pos != std::string::npos && report.set_ends_with(pos != std::string::npos &&
pos == actual.size() - expected.size()); pos == actual_for_match.size() - match.size());
return report; return report;
} }
...@@ -87,51 +109,55 @@ void GetElementStatusAction::OnWaitForElement( ...@@ -87,51 +109,55 @@ void GetElementStatusAction::OnWaitForElement(
return; return;
} }
const auto& get_element_status = proto_.get_element_status(); std::vector<std::string> attribute_list;
if (get_element_status.has_expected_value_match()) { switch (proto_.get_element_status().value_source()) {
CheckValue(); case GetElementStatusProto::VALUE:
return; attribute_list.emplace_back("value");
break;
case GetElementStatusProto::INNER_TEXT:
attribute_list.emplace_back("innerText");
break;
case GetElementStatusProto::NOT_SET:
EndAction(ClientStatus(INVALID_ACTION));
return;
} }
// TODO(b/169924567): Add option to check inner text. action_delegate_util::FindElementAndGetProperty(
delegate_, selector_,
EndAction(ClientStatus(INVALID_ACTION)); base::BindOnce(&ActionDelegate::GetStringAttribute,
} delegate_->GetWeakPtr(), attribute_list),
base::BindOnce(&GetElementStatusAction::OnGetStringAttribute,
void GetElementStatusAction::CheckValue() { weak_ptr_factory_.GetWeakPtr()));
// TODO(b/169924567): Add TextFilter option.
delegate_->GetFieldValue(
selector_,
base::BindOnce(
&GetElementStatusAction::OnGetContentForTextMatch,
weak_ptr_factory_.GetWeakPtr(),
proto_.get_element_status().expected_value_match().text_match()));
} }
void GetElementStatusAction::OnGetContentForTextMatch( void GetElementStatusAction::OnGetStringAttribute(const ClientStatus& status,
const GetElementStatusProto::TextMatch& expected_match, const std::string& text) {
const ClientStatus& status,
const std::string& text) {
if (!status.ok()) { if (!status.ok()) {
EndAction(status); EndAction(status);
return; return;
} }
std::string expected_text; const auto& expected_match =
proto_.get_element_status().expected_value_match().text_match();
MaybeRe2 expected_re2;
switch (expected_match.value_source_case()) { switch (expected_match.value_source_case()) {
case GetElementStatusProto::TextMatch::kValue: case GetElementStatusProto::TextMatch::kValue:
expected_text = expected_match.value(); expected_re2.value = expected_match.value();
break; break;
case GetElementStatusProto::TextMatch::kAutofillValue: { case GetElementStatusProto::TextMatch::kAutofillValue: {
ClientStatus autofill_status = ClientStatus autofill_status = GetFormattedAutofillValue(
GetFormattedAutofillValue(expected_match.autofill_value(), expected_match.autofill_value(), delegate_->GetUserData(),
delegate_->GetUserData(), &expected_text); &expected_re2.value);
if (!autofill_status.ok()) { if (!autofill_status.ok()) {
EndAction(autofill_status); EndAction(autofill_status);
return; return;
} }
break; break;
} }
case GetElementStatusProto::TextMatch::kRe2:
expected_re2.value = expected_match.re2();
expected_re2.is_re2 = true;
break;
case GetElementStatusProto::TextMatch::VALUE_SOURCE_NOT_SET: case GetElementStatusProto::TextMatch::VALUE_SOURCE_NOT_SET:
EndAction(ClientStatus(INVALID_ACTION)); EndAction(ClientStatus(INVALID_ACTION));
return; return;
...@@ -141,41 +167,38 @@ void GetElementStatusAction::OnGetContentForTextMatch( ...@@ -141,41 +167,38 @@ void GetElementStatusAction::OnGetContentForTextMatch(
result->set_not_empty(!text.empty()); result->set_not_empty(!text.empty());
bool success = true; bool success = true;
if (expected_text.empty()) { *result->add_reports() = CreateComparisonReport(
success = text.empty(); text, expected_re2, /* case_sensitive= */ true, /* remove_space= */ true);
} else if (text.empty()) { *result->add_reports() =
success = false; CreateComparisonReport(text, expected_re2, /* case_sensitive= */ true,
} else { /* remove_space= */ false);
*result->add_reports() = *result->add_reports() =
CreateComparisonReport(text, expected_text, true, true); CreateComparisonReport(text, expected_re2, /* case_sensitive= */ false,
*result->add_reports() = /* remove_space= */ true);
CreateComparisonReport(text, expected_text, true, false); *result->add_reports() =
*result->add_reports() = CreateComparisonReport(text, expected_re2, /* case_sensitive= */ false,
CreateComparisonReport(text, expected_text, false, true); /* remove_space= */ false);
*result->add_reports() =
CreateComparisonReport(text, expected_text, false, false); if (expected_match.has_match_expectation()) {
const auto& expectation = expected_match.match_expectation();
if (expected_match.has_match_expectation()) { auto report = CreateComparisonReport(
const auto& expectation = expected_match.match_expectation(); text, expected_re2, expectation.match_options().case_sensitive(),
auto report = CreateComparisonReport( expectation.match_options().remove_space());
text, expected_text, expectation.match_options().case_sensitive(),
expectation.match_options().remove_space()); switch (expectation.match_level_case()) {
case GetElementStatusProto::MatchExpectation::MATCH_LEVEL_NOT_SET:
switch (expectation.match_level_case()) { case GetElementStatusProto::MatchExpectation::kFullMatch:
case GetElementStatusProto::MatchExpectation::MATCH_LEVEL_NOT_SET: success = report.full_match();
case GetElementStatusProto::MatchExpectation::kFullMatch: break;
success = report.full_match(); case GetElementStatusProto::MatchExpectation::kContains:
break; success = report.contains();
case GetElementStatusProto::MatchExpectation::kContains: break;
success = report.contains(); case GetElementStatusProto::MatchExpectation::kStartsWith:
break; success = report.starts_with();
case GetElementStatusProto::MatchExpectation::kStartsWith: break;
success = report.starts_with(); case GetElementStatusProto::MatchExpectation::kEndsWith:
break; success = report.ends_with();
case GetElementStatusProto::MatchExpectation::kEndsWith: break;
success = report.ends_with();
break;
}
} }
} }
......
...@@ -28,11 +28,8 @@ class GetElementStatusAction : public Action { ...@@ -28,11 +28,8 @@ class GetElementStatusAction : public Action {
void InternalProcessAction(ProcessActionCallback callback) override; void InternalProcessAction(ProcessActionCallback callback) override;
void OnWaitForElement(const ClientStatus& element_status); void OnWaitForElement(const ClientStatus& element_status);
void CheckValue(); void OnGetStringAttribute(const ClientStatus& status,
void OnGetContentForTextMatch( const std::string& text);
const GetElementStatusProto::TextMatch& expected_match,
const ClientStatus& status,
const std::string& text);
void EndAction(const ClientStatus& status); void EndAction(const ClientStatus& status);
......
...@@ -23,6 +23,7 @@ namespace { ...@@ -23,6 +23,7 @@ namespace {
using ::base::test::RunOnceCallback; using ::base::test::RunOnceCallback;
using ::testing::_; using ::testing::_;
using ::testing::ElementsAre;
using ::testing::Pointee; using ::testing::Pointee;
using ::testing::Property; using ::testing::Property;
using ::testing::Return; using ::testing::Return;
...@@ -40,8 +41,11 @@ class GetElementStatusActionTest : public testing::Test { ...@@ -40,8 +41,11 @@ class GetElementStatusActionTest : public testing::Test {
.WillByDefault(Return(&user_data_)); .WillByDefault(Return(&user_data_));
ON_CALL(mock_action_delegate_, OnShortWaitForElement(_, _)) ON_CALL(mock_action_delegate_, OnShortWaitForElement(_, _))
.WillByDefault(RunOnceCallback<1>(OkClientStatus())); .WillByDefault(RunOnceCallback<1>(OkClientStatus()));
ON_CALL(mock_action_delegate_, OnGetFieldValue(_, _)) test_util::MockFindAnyElement(mock_action_delegate_);
.WillByDefault(RunOnceCallback<1>(OkClientStatus(), kValue)); ON_CALL(mock_action_delegate_, GetStringAttribute(_, _, _))
.WillByDefault(RunOnceCallback<2>(OkClientStatus(), kValue));
proto_.set_value_source(GetElementStatusProto::VALUE);
} }
protected: protected:
...@@ -179,6 +183,13 @@ TEST_F(GetElementStatusActionTest, ActionSucceedsForCaseSensitiveFullMatch) { ...@@ -179,6 +183,13 @@ TEST_F(GetElementStatusActionTest, ActionSucceedsForCaseSensitiveFullMatch) {
->set_full_match(true); ->set_full_match(true);
proto_.set_mismatch_should_fail(true); proto_.set_mismatch_should_fail(true);
auto expected_element =
test_util::MockFindElement(mock_action_delegate_, selector);
EXPECT_CALL(mock_action_delegate_,
GetStringAttribute(ElementsAre("value"),
EqualsElement(expected_element), _))
.WillOnce(RunOnceCallback<2>(OkClientStatus(), kValue));
EXPECT_CALL( EXPECT_CALL(
callback_, callback_,
Run(Pointee(AllOf( Run(Pointee(AllOf(
...@@ -336,9 +347,9 @@ TEST_F(GetElementStatusActionTest, ActionSucceedsForFullMatchWithoutSpaces) { ...@@ -336,9 +347,9 @@ TEST_F(GetElementStatusActionTest, ActionSucceedsForFullMatchWithoutSpaces) {
Run(); Run();
} }
TEST_F(GetElementStatusActionTest, EmptyTextForEmptyFieldIsSuccess) { TEST_F(GetElementStatusActionTest, EmptyTextForEmptyValueIsSuccess) {
ON_CALL(mock_action_delegate_, OnGetFieldValue(_, _)) ON_CALL(mock_action_delegate_, GetStringAttribute(_, _, _))
.WillByDefault(RunOnceCallback<1>(OkClientStatus(), "")); .WillByDefault(RunOnceCallback<2>(OkClientStatus(), ""));
Selector selector({"#element"}); Selector selector({"#element"});
*proto_.mutable_element() = selector.proto; *proto_.mutable_element() = selector.proto;
...@@ -357,5 +368,129 @@ TEST_F(GetElementStatusActionTest, EmptyTextForEmptyFieldIsSuccess) { ...@@ -357,5 +368,129 @@ TEST_F(GetElementStatusActionTest, EmptyTextForEmptyFieldIsSuccess) {
Run(); Run();
} }
TEST_F(GetElementStatusActionTest, InnerTextLookupSuccess) {
Selector selector({"#element"});
*proto_.mutable_element() = selector.proto;
proto_.mutable_expected_value_match()->mutable_text_match()->set_value(
kValue);
proto_.set_value_source(GetElementStatusProto::INNER_TEXT);
proto_.set_mismatch_should_fail(true);
auto expected_element =
test_util::MockFindElement(mock_action_delegate_, selector);
EXPECT_CALL(mock_action_delegate_,
GetStringAttribute(ElementsAre("innerText"),
EqualsElement(expected_element), _))
.WillOnce(RunOnceCallback<2>(OkClientStatus(), kValue));
EXPECT_CALL(
callback_,
Run(Pointee(AllOf(
Property(&ProcessedActionProto::status, ACTION_APPLIED),
Property(
&ProcessedActionProto::get_element_status_result,
AllOf(Property(&GetElementStatusProto::Result::not_empty, true),
Property(&GetElementStatusProto::Result::match_success,
true)))))));
Run();
}
TEST_F(GetElementStatusActionTest, MatchingValueWithRegexpCaseSensitive) {
Selector selector({"#element"});
*proto_.mutable_element() = selector.proto;
proto_.mutable_expected_value_match()->mutable_text_match()->set_re2("Valu.");
proto_.mutable_expected_value_match()
->mutable_text_match()
->mutable_match_expectation()
->mutable_match_options()
->set_case_sensitive(true);
proto_.mutable_expected_value_match()
->mutable_text_match()
->mutable_match_expectation()
->set_ends_with(true);
proto_.set_mismatch_should_fail(true);
EXPECT_CALL(
callback_,
Run(Pointee(AllOf(
Property(&ProcessedActionProto::status, ACTION_APPLIED),
Property(
&ProcessedActionProto::get_element_status_result,
AllOf(Property(&GetElementStatusProto::Result::not_empty, true),
Property(&GetElementStatusProto::Result::match_success,
true)))))));
Run();
}
TEST_F(GetElementStatusActionTest, MatchingValueWithRegexpCaseInsensitive) {
Selector selector({"#element"});
*proto_.mutable_element() = selector.proto;
proto_.mutable_expected_value_match()->mutable_text_match()->set_re2("vAlU.");
proto_.mutable_expected_value_match()
->mutable_text_match()
->mutable_match_expectation()
->mutable_match_options()
->set_case_sensitive(false);
proto_.mutable_expected_value_match()
->mutable_text_match()
->mutable_match_expectation()
->set_ends_with(true);
proto_.set_mismatch_should_fail(true);
EXPECT_CALL(
callback_,
Run(Pointee(AllOf(
Property(&ProcessedActionProto::status, ACTION_APPLIED),
Property(
&ProcessedActionProto::get_element_status_result,
AllOf(Property(&GetElementStatusProto::Result::not_empty, true),
Property(&GetElementStatusProto::Result::match_success,
true)))))));
Run();
}
TEST_F(GetElementStatusActionTest, ActionFailsForRegexMismatchIfRequired) {
Selector selector({"#element"});
*proto_.mutable_element() = selector.proto;
proto_.mutable_expected_value_match()->mutable_text_match()->set_re2("none");
proto_.mutable_expected_value_match()
->mutable_text_match()
->mutable_match_expectation()
->set_full_match(true);
proto_.set_mismatch_should_fail(true);
EXPECT_CALL(
callback_,
Run(Pointee(AllOf(
Property(&ProcessedActionProto::status, ELEMENT_MISMATCH),
Property(
&ProcessedActionProto::get_element_status_result,
AllOf(Property(&GetElementStatusProto::Result::not_empty, true),
Property(&GetElementStatusProto::Result::match_success,
false)))))));
Run();
}
TEST_F(GetElementStatusActionTest, EmptyRegexpForEmptyValueIsSuccess) {
ON_CALL(mock_action_delegate_, GetStringAttribute(_, _, _))
.WillByDefault(RunOnceCallback<2>(OkClientStatus(), ""));
Selector selector({"#element"});
*proto_.mutable_element() = selector.proto;
proto_.mutable_expected_value_match()->mutable_text_match()->set_re2("^$");
proto_.set_mismatch_should_fail(true);
EXPECT_CALL(
callback_,
Run(Pointee(AllOf(
Property(&ProcessedActionProto::status, ACTION_APPLIED),
Property(
&ProcessedActionProto::get_element_status_result,
AllOf(Property(&GetElementStatusProto::Result::not_empty, false),
Property(&GetElementStatusProto::Result::match_success,
true)))))));
Run();
}
} // namespace } // namespace
} // namespace autofill_assistant } // namespace autofill_assistant
...@@ -2366,9 +2366,8 @@ message PopupMessageProto { ...@@ -2366,9 +2366,8 @@ message PopupMessageProto {
message GetElementStatusProto { message GetElementStatusProto {
optional SelectorProto element = 1; optional SelectorProto element = 1;
// Compare the element's |value| attribute against this expected match.
optional ValueMatch expected_value_match = 2; optional ValueMatch expected_value_match = 2;
// TODO(b/169924567): Add expected_inner_text_match optional ValueSource value_source = 4;
// If set and a mismatch happens, the action will report an failure status // If set and a mismatch happens, the action will report an failure status
// with |ELEMENT_MISMATCH|. If this flag is set to false, the action will not // with |ELEMENT_MISMATCH|. If this flag is set to false, the action will not
...@@ -2386,6 +2385,14 @@ message GetElementStatusProto { ...@@ -2386,6 +2385,14 @@ message GetElementStatusProto {
repeated ComparisonReport reports = 3; repeated ComparisonReport reports = 3;
} }
enum ValueSource {
NOT_SET = 0;
// Compare the element's |value| attribute against this expected match.
VALUE = 1;
// Compare the element's |innerText| attribute against this expected match.
INNER_TEXT = 2;
}
message MatchOptions { message MatchOptions {
optional bool case_sensitive = 1; optional bool case_sensitive = 1;
optional bool remove_space = 2; optional bool remove_space = 2;
...@@ -2409,21 +2416,19 @@ message GetElementStatusProto { ...@@ -2409,21 +2416,19 @@ message GetElementStatusProto {
// A value from an Autofill source. Note that this must be proceeded by a // A value from an Autofill source. Note that this must be proceeded by a
// |CollectUserDataAction|. // |CollectUserDataAction|.
AutofillValue autofill_value = 2; AutofillValue autofill_value = 2;
// A regular expression.
string re2 = 4;
} }
// Optional. The expectations to declare this as a matching success. If // Optional. The expectations to declare this as a matching success. If
// left empty, the action will always be treated as successful. This field // left empty, the action will always be treated as successful.
// is not necessary if the expected value is empty.
optional MatchExpectation match_expectation = 3; optional MatchExpectation match_expectation = 3;
} }
message ValueMatch { message ValueMatch {
optional TextMatch text_match = 1; optional TextMatch text_match = 1;
// TODO(b/169924567): Add text_filter
} }
// TODO(b/169924567): Add InnerTextMatch
message ComparisonReport { message ComparisonReport {
optional MatchOptions match_options = 1; optional MatchOptions match_options = 1;
...@@ -2431,5 +2436,7 @@ message GetElementStatusProto { ...@@ -2431,5 +2436,7 @@ message GetElementStatusProto {
optional bool contains = 3; optional bool contains = 3;
optional bool starts_with = 4; optional bool starts_with = 4;
optional bool ends_with = 5; optional bool ends_with = 5;
optional bool empty = 6;
} }
} }
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment