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 @@
#include "components/autofill_assistant/browser/actions/get_element_status_action.h"
#include "base/i18n/case_conversion.h"
#include "base/stl_util.h"
#include "base/strings/string_util.h"
#include "base/strings/utf_string_conversions.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_util.h"
#include "components/autofill_assistant/browser/client_status.h"
#include "components/autofill_assistant/browser/service.pb.h"
#include "components/autofill_assistant/browser/user_data_util.h"
#include "third_party/re2/src/re2/re2.h"
namespace autofill_assistant {
namespace {
std::string PrepareStringForMatching(const std::string& value,
bool case_sensitive,
bool remove_space) {
struct MaybeRe2 {
std::string value;
bool is_re2 = false;
};
std::string RemoveWhitespace(const std::string& value) {
std::string copy = value;
if (!case_sensitive) {
copy = base::UTF16ToUTF8(base::i18n::FoldCase(base::UTF8ToUTF16(copy)));
}
if (remove_space) {
base::EraseIf(copy, base::IsUnicodeWhitespace);
}
return copy;
}
GetElementStatusProto::ComparisonReport CreateComparisonReport(
const std::string& actual,
const std::string& expected,
const MaybeRe2& re2,
bool case_sensitive,
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;
report.mutable_match_options()->set_case_sensitive(case_sensitive);
report.mutable_match_options()->set_remove_space(remove_space);
report.set_full_match(actual_for_match == expected_for_match);
size_t pos = actual_for_match.find(expected_for_match);
std::string actual_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_starts_with(pos != std::string::npos && pos == 0);
report.set_ends_with(pos != std::string::npos &&
pos == actual.size() - expected.size());
pos == actual_for_match.size() - match.size());
return report;
}
......@@ -87,51 +109,55 @@ void GetElementStatusAction::OnWaitForElement(
return;
}
const auto& get_element_status = proto_.get_element_status();
if (get_element_status.has_expected_value_match()) {
CheckValue();
std::vector<std::string> attribute_list;
switch (proto_.get_element_status().value_source()) {
case GetElementStatusProto::VALUE:
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.
EndAction(ClientStatus(INVALID_ACTION));
}
void GetElementStatusAction::CheckValue() {
// 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()));
action_delegate_util::FindElementAndGetProperty(
delegate_, selector_,
base::BindOnce(&ActionDelegate::GetStringAttribute,
delegate_->GetWeakPtr(), attribute_list),
base::BindOnce(&GetElementStatusAction::OnGetStringAttribute,
weak_ptr_factory_.GetWeakPtr()));
}
void GetElementStatusAction::OnGetContentForTextMatch(
const GetElementStatusProto::TextMatch& expected_match,
const ClientStatus& status,
void GetElementStatusAction::OnGetStringAttribute(const ClientStatus& status,
const std::string& text) {
if (!status.ok()) {
EndAction(status);
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()) {
case GetElementStatusProto::TextMatch::kValue:
expected_text = expected_match.value();
expected_re2.value = expected_match.value();
break;
case GetElementStatusProto::TextMatch::kAutofillValue: {
ClientStatus autofill_status =
GetFormattedAutofillValue(expected_match.autofill_value(),
delegate_->GetUserData(), &expected_text);
ClientStatus autofill_status = GetFormattedAutofillValue(
expected_match.autofill_value(), delegate_->GetUserData(),
&expected_re2.value);
if (!autofill_status.ok()) {
EndAction(autofill_status);
return;
}
break;
}
case GetElementStatusProto::TextMatch::kRe2:
expected_re2.value = expected_match.re2();
expected_re2.is_re2 = true;
break;
case GetElementStatusProto::TextMatch::VALUE_SOURCE_NOT_SET:
EndAction(ClientStatus(INVALID_ACTION));
return;
......@@ -141,24 +167,22 @@ void GetElementStatusAction::OnGetContentForTextMatch(
result->set_not_empty(!text.empty());
bool success = true;
if (expected_text.empty()) {
success = text.empty();
} else if (text.empty()) {
success = false;
} else {
*result->add_reports() =
CreateComparisonReport(text, expected_text, true, true);
*result->add_reports() = CreateComparisonReport(
text, expected_re2, /* case_sensitive= */ true, /* remove_space= */ true);
*result->add_reports() =
CreateComparisonReport(text, expected_text, true, false);
CreateComparisonReport(text, expected_re2, /* case_sensitive= */ true,
/* remove_space= */ false);
*result->add_reports() =
CreateComparisonReport(text, expected_text, false, true);
CreateComparisonReport(text, expected_re2, /* case_sensitive= */ false,
/* remove_space= */ true);
*result->add_reports() =
CreateComparisonReport(text, expected_text, false, false);
CreateComparisonReport(text, expected_re2, /* case_sensitive= */ false,
/* remove_space= */ false);
if (expected_match.has_match_expectation()) {
const auto& expectation = expected_match.match_expectation();
auto report = CreateComparisonReport(
text, expected_text, expectation.match_options().case_sensitive(),
text, expected_re2, expectation.match_options().case_sensitive(),
expectation.match_options().remove_space());
switch (expectation.match_level_case()) {
......@@ -177,7 +201,6 @@ void GetElementStatusAction::OnGetContentForTextMatch(
break;
}
}
}
result->set_match_success(success);
EndAction(!success && proto_.get_element_status().mismatch_should_fail()
......
......@@ -28,10 +28,7 @@ class GetElementStatusAction : public Action {
void InternalProcessAction(ProcessActionCallback callback) override;
void OnWaitForElement(const ClientStatus& element_status);
void CheckValue();
void OnGetContentForTextMatch(
const GetElementStatusProto::TextMatch& expected_match,
const ClientStatus& status,
void OnGetStringAttribute(const ClientStatus& status,
const std::string& text);
void EndAction(const ClientStatus& status);
......
......@@ -23,6 +23,7 @@ namespace {
using ::base::test::RunOnceCallback;
using ::testing::_;
using ::testing::ElementsAre;
using ::testing::Pointee;
using ::testing::Property;
using ::testing::Return;
......@@ -40,8 +41,11 @@ class GetElementStatusActionTest : public testing::Test {
.WillByDefault(Return(&user_data_));
ON_CALL(mock_action_delegate_, OnShortWaitForElement(_, _))
.WillByDefault(RunOnceCallback<1>(OkClientStatus()));
ON_CALL(mock_action_delegate_, OnGetFieldValue(_, _))
.WillByDefault(RunOnceCallback<1>(OkClientStatus(), kValue));
test_util::MockFindAnyElement(mock_action_delegate_);
ON_CALL(mock_action_delegate_, GetStringAttribute(_, _, _))
.WillByDefault(RunOnceCallback<2>(OkClientStatus(), kValue));
proto_.set_value_source(GetElementStatusProto::VALUE);
}
protected:
......@@ -179,6 +183,13 @@ TEST_F(GetElementStatusActionTest, ActionSucceedsForCaseSensitiveFullMatch) {
->set_full_match(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(
callback_,
Run(Pointee(AllOf(
......@@ -336,9 +347,9 @@ TEST_F(GetElementStatusActionTest, ActionSucceedsForFullMatchWithoutSpaces) {
Run();
}
TEST_F(GetElementStatusActionTest, EmptyTextForEmptyFieldIsSuccess) {
ON_CALL(mock_action_delegate_, OnGetFieldValue(_, _))
.WillByDefault(RunOnceCallback<1>(OkClientStatus(), ""));
TEST_F(GetElementStatusActionTest, EmptyTextForEmptyValueIsSuccess) {
ON_CALL(mock_action_delegate_, GetStringAttribute(_, _, _))
.WillByDefault(RunOnceCallback<2>(OkClientStatus(), ""));
Selector selector({"#element"});
*proto_.mutable_element() = selector.proto;
......@@ -357,5 +368,129 @@ TEST_F(GetElementStatusActionTest, EmptyTextForEmptyFieldIsSuccess) {
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 autofill_assistant
......@@ -2366,9 +2366,8 @@ message PopupMessageProto {
message GetElementStatusProto {
optional SelectorProto element = 1;
// Compare the element's |value| attribute against this expected match.
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
// with |ELEMENT_MISMATCH|. If this flag is set to false, the action will not
......@@ -2386,6 +2385,14 @@ message GetElementStatusProto {
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 {
optional bool case_sensitive = 1;
optional bool remove_space = 2;
......@@ -2409,21 +2416,19 @@ message GetElementStatusProto {
// A value from an Autofill source. Note that this must be proceeded by a
// |CollectUserDataAction|.
AutofillValue autofill_value = 2;
// A regular expression.
string re2 = 4;
}
// Optional. The expectations to declare this as a matching success. If
// left empty, the action will always be treated as successful. This field
// is not necessary if the expected value is empty.
// left empty, the action will always be treated as successful.
optional MatchExpectation match_expectation = 3;
}
message ValueMatch {
optional TextMatch text_match = 1;
// TODO(b/169924567): Add text_filter
}
// TODO(b/169924567): Add InnerTextMatch
message ComparisonReport {
optional MatchOptions match_options = 1;
......@@ -2431,5 +2436,7 @@ message GetElementStatusProto {
optional bool contains = 3;
optional bool starts_with = 4;
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