Commit df211c9c authored by sandromaggi's avatar sandromaggi Committed by Commit Bot

[Autofill Assistant] Implement GetElementStatusAction

Adding an action to check the value of an element. This action can be
used for controlling flow (e.g. checking if a field is filled and
offering a FocusElementAction if not) - or for debugging (e.g. logging
if a field is filled after a SetFormFieldValueAction).

Bug: b/169924567
Change-Id: I08888de3e5702a04ae72f6fd36faff198c3f409f
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2450089
Commit-Queue: Sandro Maggi <sandromaggi@google.com>
Reviewed-by: default avatarStephane Zermatten <szermatt@chromium.org>
Cr-Commit-Position: refs/heads/master@{#815129}
parent 252f02df
......@@ -42,6 +42,8 @@ static_library("browser") {
"actions/focus_element_action.h",
"actions/generate_password_for_form_field_action.cc",
"actions/generate_password_for_form_field_action.h",
"actions/get_element_status_action.cc",
"actions/get_element_status_action.h",
"actions/highlight_element_action.cc",
"actions/highlight_element_action.h",
"actions/navigate_action.cc",
......@@ -271,6 +273,7 @@ source_set("unit_tests") {
"actions/fallback_handler/required_fields_fallback_handler_unittest.cc",
"actions/focus_element_action_unittest.cc",
"actions/generate_password_for_form_field_action_unittest.cc",
"actions/get_element_status_action_unittest.cc",
"actions/highlight_element_action_unittest.cc",
"actions/popup_message_action_unittest.cc",
"actions/presave_generated_password_action_unittest.cc",
......
......@@ -140,6 +140,9 @@ std::ostream& operator<<(std::ostream& out,
case ActionProto::ActionInfoCase::kPresaveGeneratedPassword:
out << "PresaveGeneratedPassword";
break;
case ActionProto::ActionInfoCase::kGetElementStatus:
out << "GetElementStatus";
break;
case ActionProto::ActionInfoCase::ACTION_INFO_NOT_SET:
out << "ACTION_INFO_NOT_SET";
break;
......
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#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/client_status.h"
#include "components/autofill_assistant/browser/service.pb.h"
#include "components/autofill_assistant/browser/user_data_util.h"
namespace autofill_assistant {
namespace {
std::string PrepareStringForMatching(const std::string& value,
bool case_sensitive,
bool remove_space) {
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,
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);
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());
return report;
}
} // namespace
GetElementStatusAction::GetElementStatusAction(ActionDelegate* delegate,
const ActionProto& proto)
: Action(delegate, proto) {
DCHECK(proto_.has_get_element_status());
}
GetElementStatusAction::~GetElementStatusAction() = default;
void GetElementStatusAction::InternalProcessAction(
ProcessActionCallback callback) {
callback_ = std::move(callback);
selector_ = Selector(proto_.get_element_status().element());
if (selector_.empty()) {
VLOG(1) << __func__ << ": empty selector";
EndAction(ClientStatus(INVALID_SELECTOR));
return;
}
delegate_->ShortWaitForElement(
selector_, base::BindOnce(&GetElementStatusAction::OnWaitForElement,
weak_ptr_factory_.GetWeakPtr()));
}
void GetElementStatusAction::OnWaitForElement(
const ClientStatus& element_status) {
if (!element_status.ok()) {
EndAction(element_status);
return;
}
const auto& get_element_status = proto_.get_element_status();
if (get_element_status.has_expected_value_match()) {
CheckValue();
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()));
}
void GetElementStatusAction::OnGetContentForTextMatch(
const GetElementStatusProto::TextMatch& expected_match,
const ClientStatus& status,
const std::string& text) {
if (!status.ok()) {
EndAction(status);
return;
}
std::string expected_text;
switch (expected_match.value_source_case()) {
case GetElementStatusProto::TextMatch::kValue:
expected_text = expected_match.value();
break;
case GetElementStatusProto::TextMatch::kAutofillValue: {
ClientStatus autofill_status =
GetFormattedAutofillValue(expected_match.autofill_value(),
delegate_->GetUserData(), &expected_text);
if (!autofill_status.ok()) {
EndAction(autofill_status);
return;
}
break;
}
case GetElementStatusProto::TextMatch::VALUE_SOURCE_NOT_SET:
EndAction(ClientStatus(INVALID_ACTION));
return;
}
auto* result = processed_action_proto_->mutable_get_element_status_result();
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_text, true, false);
*result->add_reports() =
CreateComparisonReport(text, expected_text, false, true);
*result->add_reports() =
CreateComparisonReport(text, expected_text, false, 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(),
expectation.match_options().remove_space());
switch (expectation.match_level_case()) {
case GetElementStatusProto::MatchExpectation::MATCH_LEVEL_NOT_SET:
case GetElementStatusProto::MatchExpectation::kFullMatch:
success = report.full_match();
break;
case GetElementStatusProto::MatchExpectation::kContains:
success = report.contains();
break;
case GetElementStatusProto::MatchExpectation::kStartsWith:
success = report.starts_with();
break;
case GetElementStatusProto::MatchExpectation::kEndsWith:
success = report.ends_with();
break;
}
}
}
result->set_match_success(success);
EndAction(!success && proto_.get_element_status().mismatch_should_fail()
? ClientStatus(ELEMENT_MISMATCH)
: OkClientStatus());
}
void GetElementStatusAction::EndAction(const ClientStatus& status) {
UpdateProcessedAction(status);
std::move(callback_).Run(std::move(processed_action_proto_));
}
} // namespace autofill_assistant
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef COMPONENTS_AUTOFILL_ASSISTANT_BROWSER_ACTIONS_GET_ELEMENT_STATUS_ACTION_H_
#define COMPONENTS_AUTOFILL_ASSISTANT_BROWSER_ACTIONS_GET_ELEMENT_STATUS_ACTION_H_
#include "base/callback.h"
#include "base/memory/weak_ptr.h"
#include "components/autofill_assistant/browser/actions/action.h"
#include "components/autofill_assistant/browser/client_status.h"
#include "components/autofill_assistant/browser/service.pb.h"
namespace autofill_assistant {
// Action to get an element's status.
class GetElementStatusAction : public Action {
public:
explicit GetElementStatusAction(ActionDelegate* delegate,
const ActionProto& proto);
~GetElementStatusAction() override;
GetElementStatusAction(const GetElementStatusAction&) = delete;
GetElementStatusAction& operator=(const GetElementStatusAction&) = delete;
private:
// Overrides Action:
void InternalProcessAction(ProcessActionCallback callback) override;
void OnWaitForElement(const ClientStatus& element_status);
void CheckValue();
void OnGetContentForTextMatch(
const GetElementStatusProto::TextMatch& expected_match,
const ClientStatus& status,
const std::string& text);
void EndAction(const ClientStatus& status);
Selector selector_;
ProcessActionCallback callback_;
base::WeakPtrFactory<GetElementStatusAction> weak_ptr_factory_{this};
};
} // namespace autofill_assistant
#endif // COMPONENTS_AUTOFILL_ASSISTANT_BROWSER_ACTIONS_GET_ELEMENT_STATUS_ACTION_H_
......@@ -12,7 +12,7 @@
#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/field_formatter.h"
#include "components/autofill_assistant/browser/user_data_util.h"
namespace autofill_assistant {
......@@ -46,34 +46,12 @@ void SelectOptionAction::InternalProcessAction(ProcessActionCallback callback) {
value_ = select_option.selected_option();
break;
case SelectOptionProto::kAutofillValue: {
if (select_option.autofill_value().profile().identifier().empty() ||
select_option.autofill_value().value_expression().empty()) {
VLOG(1) << "SelectOptionAction: |autofill_value| with empty "
"|profile.identifier| or |value_expression|";
EndAction(ClientStatus(INVALID_ACTION));
return;
}
const autofill::AutofillProfile* address =
delegate_->GetUserData()->selected_address(
select_option.autofill_value().profile().identifier());
if (address == nullptr) {
VLOG(1) << "SelectOptionAction: requested unknown address '"
<< select_option.autofill_value().profile().identifier() << "'";
EndAction(ClientStatus(PRECONDITION_FAILED));
ClientStatus autofill_status = GetFormattedAutofillValue(
select_option.autofill_value(), delegate_->GetUserData(), &value_);
if (!autofill_status.ok()) {
EndAction(autofill_status);
return;
}
auto value = field_formatter::FormatString(
select_option.autofill_value().value_expression(),
field_formatter::CreateAutofillMappings(*address,
/* locale= */ "en-US"));
if (!value.has_value()) {
EndAction(ClientStatus(AUTOFILL_INFO_NOT_AVAILABLE));
return;
}
value_ = *value;
break;
}
default:
......
......@@ -12,7 +12,7 @@
#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/field_formatter.h"
#include "components/autofill_assistant/browser/user_data_util.h"
namespace autofill_assistant {
namespace {
......@@ -138,34 +138,15 @@ void SetFormFieldValueAction::InternalProcessAction(
.values(0));
break;
case SetFormFieldValueProto_KeyPress::kAutofillValue: {
if (keypress.autofill_value().profile().identifier().empty() ||
keypress.autofill_value().value_expression().empty()) {
VLOG(1) << "SetFormFieldValueAction: |autofill_value| with empty "
"|profile.identifier| or |value_expression|";
FailAction(ClientStatus(INVALID_ACTION), keypress_index);
return;
}
const autofill::AutofillProfile* address =
delegate_->GetUserData()->selected_address(
keypress.autofill_value().profile().identifier());
if (address == nullptr) {
VLOG(1) << "SetFormFieldValueAction: requested unknown address '"
<< keypress.autofill_value().profile().identifier() << "'";
FailAction(ClientStatus(PRECONDITION_FAILED), keypress_index);
return;
}
auto value = field_formatter::FormatString(
keypress.autofill_value().value_expression(),
field_formatter::CreateAutofillMappings(*address,
/* locale= */ "en-US"));
if (!value.has_value()) {
FailAction(ClientStatus(AUTOFILL_INFO_NOT_AVAILABLE), keypress_index);
std::string value;
ClientStatus autofill_status = GetFormattedAutofillValue(
keypress.autofill_value(), delegate_->GetUserData(), &value);
if (!autofill_status.ok()) {
FailAction(autofill_status, keypress_index);
return;
}
field_inputs_.emplace_back(*value);
field_inputs_.emplace_back(value);
break;
}
default:
......
......@@ -122,6 +122,9 @@ std::ostream& operator<<(std::ostream& out,
case ProcessedActionStatusProto::TOO_MANY_CANDIDATES:
out << "TOO_MANY_CANDIDATES";
break;
case ProcessedActionStatusProto::ELEMENT_MISMATCH:
out << "ELEMENT_MISMATCH";
break;
// Intentionally no default case to make compilation fail if a new value
// was added to the enum but not to this list.
......
......@@ -198,6 +198,10 @@ enum ProcessedActionStatusProto {
// generate more specific selectors. See SelectorProto::ProximityFilter.
TOO_MANY_CANDIDATES = 25;
// Evaluating the element did report an unexpected mismatch in either value
// or text.
ELEMENT_MISMATCH = 26;
reserved 15, 23;
}
......
......@@ -15,6 +15,7 @@
#include "components/autofill_assistant/browser/actions/expect_navigation_action.h"
#include "components/autofill_assistant/browser/actions/focus_element_action.h"
#include "components/autofill_assistant/browser/actions/generate_password_for_form_field_action.h"
#include "components/autofill_assistant/browser/actions/get_element_status_action.h"
#include "components/autofill_assistant/browser/actions/highlight_element_action.h"
#include "components/autofill_assistant/browser/actions/navigate_action.h"
#include "components/autofill_assistant/browser/actions/popup_message_action.h"
......@@ -228,6 +229,8 @@ std::unique_ptr<Action> ProtocolUtils::CreateAction(ActionDelegate* delegate,
return std::make_unique<ConfigureUiStateAction>(delegate, action);
case ActionProto::ActionInfoCase::kPresaveGeneratedPassword:
return std::make_unique<PresaveGeneratedPasswordAction>(delegate, action);
case ActionProto::ActionInfoCase::kGetElementStatus:
return std::make_unique<GetElementStatusAction>(delegate, action);
case ActionProto::ActionInfoCase::ACTION_INFO_NOT_SET: {
VLOG(1) << "Encountered action with ACTION_INFO_NOT_SET";
return std::make_unique<UnsupportedAction>(delegate, action);
......
......@@ -211,6 +211,7 @@ bool ScriptExecutor::ShouldInterruptOnPause(const ActionProto& proto) {
case ActionProto::ActionInfoCase::kSaveGeneratedPassword:
case ActionProto::ActionInfoCase::kConfigureUiState:
case ActionProto::ActionInfoCase::kPresaveGeneratedPassword:
case ActionProto::ActionInfoCase::kGetElementStatus:
case ActionProto::ActionInfoCase::ACTION_INFO_NOT_SET:
return false;
}
......
......@@ -513,6 +513,7 @@ message ActionProto {
SaveGeneratedPasswordProto save_generated_password = 53;
ConfigureUiStateProto configure_ui_state = 54;
PresaveGeneratedPasswordProto presave_generated_password = 55;
GetElementStatusProto get_element_status = 56;
}
// Set to true to make the client remove any contextual information if the
......@@ -589,10 +590,11 @@ message ProcessedActionProto {
WaitForDomProto.Result wait_for_dom_result = 22;
// Should be set as a result of FormAction.
FormProto.Result form_result = 21;
WaitForDocumentProto.Result wait_for_document_result = 25;
// Should be set as a result of ShowGenericUiProto.
ShowGenericUiProto.Result show_generic_ui_result = 28;
// Should be set as a result of GetElementStatusProto.
GetElementStatusProto.Result get_element_status_result = 31;
}
// Reports information about navigation that happened while
......@@ -608,7 +610,7 @@ message ProcessedActionProto {
// selecting a chip on the UI. This is meant for monitoring and debugging.
optional bool direct_action = 24;
reserved 26, 27;
reserved 3, 4, 6 to 11, 13, 14, 16, 18, 26, 27, 29, 30;
}
// Extended information about the action status, which provides more details
......@@ -2352,3 +2354,75 @@ message PopupMessageProto {
// Message to show in the popup.
optional string message = 1;
}
// Action to get an element's status.
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
// 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
// fail and simply report the result.
optional bool mismatch_should_fail = 3;
// The result that gets sent as part of |ProcessedActionProto.result_data|.
message Result {
// The field is considered not empty.
optional bool not_empty = 1;
// The field matches the expected input.
optional bool match_success = 2;
// For logging and debugging.
repeated ComparisonReport reports = 3;
}
message MatchOptions {
optional bool case_sensitive = 1;
optional bool remove_space = 2;
}
message MatchExpectation {
optional MatchOptions match_options = 1;
oneof match_level {
bool full_match = 2;
bool contains = 3;
bool starts_with = 4;
bool ends_with = 5;
}
}
message TextMatch {
oneof value_source {
// The value to check against.
string value = 1;
// A value from an Autofill source. Note that this must be proceeded by a
// |CollectUserDataAction|.
AutofillValue autofill_value = 2;
}
// 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.
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;
optional bool full_match = 2;
optional bool contains = 3;
optional bool starts_with = 4;
optional bool ends_with = 5;
}
}
......@@ -9,6 +9,7 @@
#include "base/strings/utf_string_conversions.h"
#include "components/autofill/core/browser/autofill_data_util.h"
#include "components/autofill/core/browser/geo/address_i18n.h"
#include "components/autofill_assistant/browser/field_formatter.h"
#include "third_party/libaddressinput/chromium/addressinput_util.h"
#include "third_party/libaddressinput/src/cpp/include/libaddressinput/address_data.h"
......@@ -353,4 +354,34 @@ bool IsCompleteCreditCard(
return true;
}
ClientStatus GetFormattedAutofillValue(const AutofillValue& autofill_value,
const UserData* user_data,
std::string* out_value) {
if (autofill_value.profile().identifier().empty() ||
autofill_value.value_expression().empty()) {
VLOG(1) << "|autofill_value| with empty "
"|profile.identifier| or |value_expression|";
return ClientStatus(INVALID_ACTION);
}
const autofill::AutofillProfile* address =
user_data->selected_address(autofill_value.profile().identifier());
if (address == nullptr) {
VLOG(1) << "Requested unknown address '"
<< autofill_value.profile().identifier() << "'";
return ClientStatus(PRECONDITION_FAILED);
}
auto value = field_formatter::FormatString(
autofill_value.value_expression(),
field_formatter::CreateAutofillMappings(*address,
/* locale= */ "en-US"));
if (!value.has_value()) {
return ClientStatus(AUTOFILL_INFO_NOT_AVAILABLE);
}
out_value->assign(*value);
return OkClientStatus();
}
} // namespace autofill_assistant
......@@ -8,6 +8,8 @@
#include <vector>
#include "components/autofill/core/browser/data_model/autofill_profile.h"
#include "components/autofill/core/browser/data_model/credit_card.h"
#include "components/autofill_assistant/browser/client_status.h"
#include "components/autofill_assistant/browser/service.pb.h"
#include "components/autofill_assistant/browser/user_data.h"
namespace autofill_assistant {
......@@ -74,6 +76,10 @@ bool IsCompleteCreditCard(
const autofill::AutofillProfile* billing_profile,
const CollectUserDataOptions& collect_user_data_options);
ClientStatus GetFormattedAutofillValue(const AutofillValue& autofill_value,
const UserData* user_data,
std::string* out_value);
} // namespace autofill_assistant
#endif // COMPONENTS_AUTOFILL_ASSISTANT_BROWSER_USER_DATA_UTIL_H_
......@@ -4,10 +4,16 @@
#include "components/autofill_assistant/browser/user_data_util.h"
#include "base/guid.h"
#include "base/strings/strcat.h"
#include "base/strings/string16.h"
#include "base/strings/string_number_conversions.h"
#include "components/autofill/core/browser/autofill_test_utils.h"
#include "components/autofill/core/browser/data_model/autofill_profile.h"
#include "components/autofill/core/browser/data_model/credit_card.h"
#include "components/autofill_assistant/browser/client_status.h"
#include "components/autofill_assistant/browser/service.pb.h"
#include "components/autofill_assistant/browser/user_data.h"
#include "testing/gmock/include/gmock/gmock.h"
namespace autofill_assistant {
......@@ -671,5 +677,79 @@ TEST(UserDataUtilTest, CompleteCreditCardWithBadNetwork) {
EXPECT_TRUE(IsCompleteCreditCard(&card, &address, payment_options_visa));
}
TEST(UserDataUtilTest, RequestEmptyAutofillValue) {
UserData user_data;
AutofillValue autofill_value;
std::string result;
EXPECT_EQ(GetFormattedAutofillValue(autofill_value, &user_data, &result)
.proto_status(),
INVALID_ACTION);
EXPECT_EQ(result, "");
}
TEST(UserDataUtilTest, RequestDataFromUnknownProfile) {
UserData user_data;
AutofillValue autofill_value;
autofill_value.mutable_profile()->set_identifier("none");
autofill_value.set_value_expression("value");
std::string result;
EXPECT_EQ(GetFormattedAutofillValue(autofill_value, &user_data, &result)
.proto_status(),
PRECONDITION_FAILED);
EXPECT_EQ(result, "");
}
TEST(UserDataUtilTest, RequestUnknownDataFromKnownProfile) {
UserData user_data;
autofill::AutofillProfile contact(base::GenerateGUID(),
autofill::test::kEmptyOrigin);
// Middle name is expected to be empty.
autofill::test::SetProfileInfo(&contact, "John", /* middle name */ "", "Doe",
"", "", "", "", "", "", "", "", "");
user_data.selected_addresses_["contact"] =
std::make_unique<autofill::AutofillProfile>(contact);
AutofillValue autofill_value;
autofill_value.mutable_profile()->set_identifier("contact");
autofill_value.set_value_expression(
base::StrCat({"${",
base::NumberToString(static_cast<int>(
autofill::ServerFieldType::NAME_MIDDLE)),
"}"}));
std::string result;
EXPECT_EQ(GetFormattedAutofillValue(autofill_value, &user_data, &result)
.proto_status(),
AUTOFILL_INFO_NOT_AVAILABLE);
EXPECT_EQ(result, "");
}
TEST(UserDataUtilTest, RequestKnownDataFromKnownProfile) {
UserData user_data;
autofill::AutofillProfile contact(base::GenerateGUID(),
autofill::test::kEmptyOrigin);
autofill::test::SetProfileInfo(&contact, "John", /* middle name */ "", "Doe",
"", "", "", "", "", "", "", "", "");
user_data.selected_addresses_["contact"] =
std::make_unique<autofill::AutofillProfile>(contact);
AutofillValue autofill_value;
autofill_value.mutable_profile()->set_identifier("contact");
autofill_value.set_value_expression(
base::StrCat({"${",
base::NumberToString(static_cast<int>(
autofill::ServerFieldType::NAME_FIRST)),
"}"}));
std::string result;
EXPECT_TRUE(
GetFormattedAutofillValue(autofill_value, &user_data, &result).ok());
EXPECT_EQ(result, "John");
}
} // namespace
} // namespace autofill_assistant
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