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 | |
parent | 7b5b123ac58f58ffde0f4f6e488bcd09aa4decd3 (diff) | |
download | qtwebengine-chromium-c30a6232df03e1efbd9f3b226777b07e087a1122.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')
101 files changed, 5968 insertions, 2309 deletions
diff --git a/chromium/components/autofill_assistant/browser/BUILD.gn b/chromium/components/autofill_assistant/browser/BUILD.gn index 5d059ccea71..803c0e3f8bc 100644 --- a/chromium/components/autofill_assistant/browser/BUILD.gn +++ b/chromium/components/autofill_assistant/browser/BUILD.gn @@ -32,8 +32,6 @@ jumbo_static_library("browser") { "actions/configure_bottom_sheet_action.h", "actions/expect_navigation_action.cc", "actions/expect_navigation_action.h", - "actions/fallback_handler/fallback_data.cc", - "actions/fallback_handler/fallback_data.h", "actions/fallback_handler/required_field.cc", "actions/fallback_handler/required_field.h", "actions/fallback_handler/required_fields_fallback_handler.cc", @@ -115,6 +113,8 @@ jumbo_static_library("browser") { "event_handler.h", "features.cc", "features.h", + "field_formatter.cc", + "field_formatter.h", "generic_ui_java_generated_enums.h", "info_box.cc", "info_box.h", @@ -123,6 +123,8 @@ jumbo_static_library("browser") { "overlay_state.h", "protocol_utils.cc", "protocol_utils.h", + "radio_button_controller.cc", + "radio_button_controller.h", "rectf.cc", "rectf.h", "retry_timer.cc", @@ -190,6 +192,7 @@ jumbo_static_library("browser") { "//components/autofill_assistant/browser/devtools", "//components/google/core/common:common", "//components/password_manager/core/browser:browser", + "//components/password_manager/core/browser/form_parsing:form_parsing", "//components/signin/public/identity_manager", "//components/strings:components_strings_grit", "//components/version_info", @@ -248,6 +251,7 @@ source_set("unit_tests") { "actions/set_form_field_value_action_unittest.cc", "actions/show_details_action_unittest.cc", "actions/show_generic_ui_action_unittest.cc", + "actions/show_progress_bar_action_unittest.cc", "actions/tell_action_unittest.cc", "actions/use_address_action_unittest.cc", "actions/use_credit_card_action_unittest.cc", @@ -260,7 +264,9 @@ source_set("unit_tests") { "element_area_unittest.cc", "element_precondition_unittest.cc", "event_handler_unittest.cc", + "field_formatter_unittest.cc", "protocol_utils_unittest.cc", + "radio_button_controller_unittest.cc", "retry_timer_unittest.cc", "script_executor_unittest.cc", "script_precondition_unittest.cc", diff --git a/chromium/components/autofill_assistant/browser/actions/action_delegate.h b/chromium/components/autofill_assistant/browser/actions/action_delegate.h index 2998d21d193..2bb9e13af86 100644 --- a/chromium/components/autofill_assistant/browser/actions/action_delegate.h +++ b/chromium/components/autofill_assistant/browser/actions/action_delegate.h @@ -134,17 +134,14 @@ class ActionDelegate { base::OnceCallback<void(UserData*, UserData::FieldChange*)> write_callback) = 0; - // Executes |write_callback| on the currently stored user_model. - virtual void WriteUserModel( - base::OnceCallback<void(UserModel*)> write_callback) = 0; - using GetFullCardCallback = base::OnceCallback<void(std::unique_ptr<autofill::CreditCard> card, const base::string16& cvc)>; - // Asks for the full card information for the selected card. Might require the + // Asks for the full card information for |credit_card|. Might require the // user entering CVC. - virtual void GetFullCard(GetFullCardCallback callback) = 0; + virtual void GetFullCard(const autofill::CreditCard* credit_card, + GetFullCardCallback callback) = 0; // Fill the address form given by |selector| with the given address // |profile|. |profile| cannot be nullptr. @@ -316,9 +313,17 @@ class ActionDelegate { // Show the progress bar and set it at |progress|%. virtual void SetProgress(int progress) = 0; + // Show the progress bar and set the |active_step| to active. + virtual void SetProgressActiveStep(int active_step) = 0; + // Shows the progress bar when |visible| is true. Hides it when false. virtual void SetProgressVisible(bool visible) = 0; + // Sets a new step progress bar configuration. + virtual void SetStepProgressBarConfiguration( + const ShowProgressBarProto::StepProgressBarConfiguration& + configuration) = 0; + // Set the viewport mode. virtual void SetViewportMode(ViewportMode mode) = 0; @@ -363,14 +368,19 @@ class ActionDelegate { // Gets the user data. virtual const UserData* GetUserData() const = 0; + // Access to the user model. + virtual UserModel* GetUserModel() = 0; + // Show |generic_ui| to the user and call |end_action_callback| when done. // Note that this callback needs to be tied to one or multiple interactions // specified in |generic_ui|, as otherwise it will never be called. + // |view_inflation_finished_callback| should be called immediately after + // view inflation, with a status indicating whether view inflation succeeded. virtual void SetGenericUi( std::unique_ptr<GenericUserInterfaceProto> generic_ui, - base::OnceCallback<void(bool, - ProcessedActionStatusProto, - const UserModel*)> end_action_callback) = 0; + base::OnceCallback<void(const ClientStatus&)> end_action_callback, + base::OnceCallback<void(const ClientStatus&)> + view_inflation_finished_callback) = 0; // Clears the generic UI. This will remove all corresponding views from the // view hierarchy and remove all corresponding interactions. Note that diff --git a/chromium/components/autofill_assistant/browser/actions/collect_user_data_action.cc b/chromium/components/autofill_assistant/browser/actions/collect_user_data_action.cc index f8b2add2361..43dc6724748 100644 --- a/chromium/components/autofill_assistant/browser/actions/collect_user_data_action.cc +++ b/chromium/components/autofill_assistant/browser/actions/collect_user_data_action.cc @@ -85,8 +85,6 @@ bool OnlyLoginRequested( collect_user_data_options.additional_appended_sections.end(), find_additional_input_sections) != collect_user_data_options.additional_appended_sections.end(); - LOG(ERROR) << "HAS_INPUT_SECTIONS: " << has_input_sections; - return !has_input_sections && !collect_user_data_options.request_payer_name && !collect_user_data_options.request_payer_email && !collect_user_data_options.request_payer_phone && @@ -98,107 +96,6 @@ bool OnlyLoginRequested( .has_value(); } -bool IsCompleteContact( - const autofill::AutofillProfile* profile, - const CollectUserDataOptions& collect_user_data_options) { - if (!collect_user_data_options.request_payer_name && - !collect_user_data_options.request_payer_email && - !collect_user_data_options.request_payer_phone) { - return true; - } - - if (!profile) { - return false; - } - - if (collect_user_data_options.request_payer_name && - !profile->HasInfo(autofill::NAME_FULL)) { - return false; - } - - if (collect_user_data_options.request_payer_email && - !profile->HasInfo(autofill::EMAIL_ADDRESS)) { - return false; - } - - if (collect_user_data_options.request_payer_phone && - !profile->HasInfo(autofill::PHONE_HOME_WHOLE_NUMBER)) { - return false; - } - return true; -} - -bool IsCompleteAddress(const autofill::AutofillProfile* profile, - bool require_postal_code) { - if (!profile) { - return false; - } - // We use a hard coded locale here since it's not used in the autofill:: code - // anyway. I.e. creating this profile ends up in FormGroup::GetInfoImpl, which - // simply ignores the app_locale. - auto address_data = - autofill::i18n::CreateAddressDataFromAutofillProfile(*profile, "en-US"); - if (!autofill::addressinput::HasAllRequiredFields(*address_data)) { - return false; - } - - if (require_postal_code && address_data->postal_code.empty()) { - return false; - } - - return true; -} - -bool IsCompleteShippingAddress( - const autofill::AutofillProfile* profile, - const CollectUserDataOptions& collect_user_data_options) { - return !collect_user_data_options.request_shipping || - IsCompleteAddress(profile, /* require_postal_code = */ false); -} - -bool IsCompleteCreditCard( - const autofill::CreditCard* credit_card, - const autofill::AutofillProfile* billing_profile, - const CollectUserDataOptions& collect_user_data_options) { - if (!collect_user_data_options.request_payment_method) { - return true; - } - - if (!credit_card || !billing_profile) { - return false; - } - - if (!IsCompleteAddress( - billing_profile, - collect_user_data_options.require_billing_postal_code)) { - return false; - } - - if (credit_card->record_type() != autofill::CreditCard::MASKED_SERVER_CARD && - !credit_card->HasValidCardNumber()) { - // Can't check validity of masked server card numbers because they are - // incomplete until decrypted. - return false; - } - - if (!credit_card->HasValidExpirationDate() || - credit_card->billing_address_id().empty()) { - return false; - } - - std::string basic_card_network = - autofill::data_util::GetPaymentRequestData(credit_card->network()) - .basic_card_issuer_network; - if (!collect_user_data_options.supported_basic_card_networks.empty() && - std::find(collect_user_data_options.supported_basic_card_networks.begin(), - collect_user_data_options.supported_basic_card_networks.end(), - basic_card_network) == - collect_user_data_options.supported_basic_card_networks.end()) { - return false; - } - return true; -} - bool IsValidLoginChoice( const std::string& choice_identifier, const CollectUserDataOptions& collect_user_data_options) { @@ -535,7 +432,6 @@ void CollectUserDataAction::InternalProcessAction( ProcessActionCallback callback) { callback_ = std::move(callback); if (!CreateOptionsFromProto()) { - LOG(ERROR) << "INVALID"; EndAction(ClientStatus(INVALID_ACTION)); return; } @@ -975,17 +871,16 @@ bool CollectUserDataAction::CreateOptionsFromProto() { collect_user_data.additional_model_identifier_to_check(); } - // TODO(crbug.com/806868): Maybe we could refactor this to make the confirm - // chip and direct_action part of the additional_actions. - std::string confirm_text = collect_user_data.confirm_button_text(); - if (confirm_text.empty()) { - confirm_text = - l10n_util::GetStringUTF8(IDS_AUTOFILL_ASSISTANT_PAYMENT_INFO_CONFIRM); + auto* confirm_chip = + collect_user_data_options_->confirm_action.mutable_chip(); + if (collect_user_data.has_confirm_chip()) { + *confirm_chip = collect_user_data.confirm_chip(); + } else { + confirm_chip->set_text( + l10n_util::GetStringUTF8(IDS_AUTOFILL_ASSISTANT_PAYMENT_INFO_CONFIRM)); + confirm_chip->set_type(HIGHLIGHTED_ACTION); } - collect_user_data_options_->confirm_action.mutable_chip()->set_text( - confirm_text); - collect_user_data_options_->confirm_action.mutable_chip()->set_type( - HIGHLIGHTED_ACTION); + *collect_user_data_options_->confirm_action.mutable_direct_action() = collect_user_data.confirm_direct_action(); diff --git a/chromium/components/autofill_assistant/browser/actions/collect_user_data_action_unittest.cc b/chromium/components/autofill_assistant/browser/actions/collect_user_data_action_unittest.cc index 42f3aec6dd2..894d0f751fe 100644 --- a/chromium/components/autofill_assistant/browser/actions/collect_user_data_action_unittest.cc +++ b/chromium/components/autofill_assistant/browser/actions/collect_user_data_action_unittest.cc @@ -2286,5 +2286,60 @@ TEST_F(CollectUserDataActionTest, LinkClickWritesPartialUserData) { action.ProcessAction(callback_.Get()); } +TEST_F(CollectUserDataActionTest, ConfirmButtonChip) { + ActionProto action_proto; + auto* collect_user_data_proto = action_proto.mutable_collect_user_data(); + collect_user_data_proto->set_request_terms_and_conditions(false); + auto* confirm_chip = collect_user_data_proto->mutable_confirm_chip(); + confirm_chip->set_text("Custom text"); + confirm_chip->set_icon(ICON_CLEAR); + confirm_chip->set_sticky(true); + + ON_CALL(mock_action_delegate_, CollectUserData(_)) + .WillByDefault( + Invoke([this](CollectUserDataOptions* collect_user_data_options) { + EXPECT_EQ(collect_user_data_options->confirm_action.chip().text(), + "Custom text"); + EXPECT_EQ(collect_user_data_options->confirm_action.chip().icon(), + ICON_CLEAR); + EXPECT_EQ(collect_user_data_options->confirm_action.chip().sticky(), + true); + user_data_.succeed_ = true; + std::move(collect_user_data_options->confirm_callback) + .Run(&user_data_, &user_model_); + })); + + EXPECT_CALL( + callback_, + Run(Pointee(Property(&ProcessedActionProto::status, ACTION_APPLIED)))); + + CollectUserDataAction action(&mock_action_delegate_, action_proto); + action.ProcessAction(callback_.Get()); +} + +TEST_F(CollectUserDataActionTest, ConfirmButtonFallbackText) { + ActionProto action_proto; + auto* collect_user_data_proto = action_proto.mutable_collect_user_data(); + collect_user_data_proto->set_request_terms_and_conditions(false); + + ON_CALL(mock_action_delegate_, CollectUserData(_)) + .WillByDefault( + Invoke([this](CollectUserDataOptions* collect_user_data_options) { + EXPECT_EQ(collect_user_data_options->confirm_action.chip().text(), + l10n_util::GetStringUTF8( + IDS_AUTOFILL_ASSISTANT_PAYMENT_INFO_CONFIRM)); + user_data_.succeed_ = true; + std::move(collect_user_data_options->confirm_callback) + .Run(&user_data_, &user_model_); + })); + + EXPECT_CALL( + callback_, + Run(Pointee(Property(&ProcessedActionProto::status, ACTION_APPLIED)))); + + CollectUserDataAction action(&mock_action_delegate_, action_proto); + action.ProcessAction(callback_.Get()); +} + } // namespace } // namespace autofill_assistant diff --git a/chromium/components/autofill_assistant/browser/actions/fallback_handler/fallback_data.cc b/chromium/components/autofill_assistant/browser/actions/fallback_handler/fallback_data.cc deleted file mode 100644 index f0e08b1e03d..00000000000 --- a/chromium/components/autofill_assistant/browser/actions/fallback_handler/fallback_data.cc +++ /dev/null @@ -1,73 +0,0 @@ -// 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/fallback_handler/fallback_data.h" - -#include "base/optional.h" -#include "base/strings/string_number_conversions.h" -#include "base/strings/utf_string_conversions.h" -#include "components/autofill/core/browser/autofill_type.h" -#include "components/autofill/core/browser/data_model/form_group.h" -#include "third_party/re2/src/re2/re2.h" -#include "third_party/re2/src/re2/stringpiece.h" - -namespace autofill_assistant { -namespace { - -// Matches a single $ followed by an opening {, then an inner key and a closing -// }. -const char kKeyPattern[] = R"re(\$\{([^}]+)\})re"; - -} // namespace - -FallbackData::FallbackData() = default; - -FallbackData::~FallbackData() = default; - -void FallbackData::AddFormGroup(const autofill::FormGroup& form_group) { - autofill::ServerFieldTypeSet available_fields; - form_group.GetNonEmptyTypes("en-US", &available_fields); - for (const auto& field : available_fields) { - field_values.emplace(static_cast<int>(field), - base::UTF16ToUTF8(form_group.GetInfo( - autofill::AutofillType(field), "en-US"))); - } -} - -base::Optional<std::string> FallbackData::GetValue(int key) { - auto it = field_values.find(key); - if (it != field_values.end() && !it->second.empty()) { - return it->second; - } - return base::nullopt; -} - -base::Optional<std::string> FallbackData::GetValueByExpression( - const std::string& value_expression) { - int key; - if (base::StringToInt(value_expression, &key)) { - return GetValue(key); - } - - std::string out = value_expression; - - re2::StringPiece input(value_expression); - while (re2::RE2::FindAndConsume(&input, kKeyPattern, &key)) { - auto rewrite_value = GetValue(key); - if (!rewrite_value.has_value()) { - DVLOG(3) << "No value for " << key << " in " << value_expression; - return base::nullopt; - } - - re2::RE2::Replace(&out, kKeyPattern, - re2::StringPiece(rewrite_value.value())); - } - - if (out.empty()) { - return base::nullopt; - } - return out; -} - -} // namespace autofill_assistant diff --git a/chromium/components/autofill_assistant/browser/actions/fallback_handler/fallback_data.h b/chromium/components/autofill_assistant/browser/actions/fallback_handler/fallback_data.h deleted file mode 100644 index 5deb5111876..00000000000 --- a/chromium/components/autofill_assistant/browser/actions/fallback_handler/fallback_data.h +++ /dev/null @@ -1,40 +0,0 @@ -// 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_FALLBACK_HANDLER_FALLBACK_DATA_H_ -#define COMPONENTS_AUTOFILL_ASSISTANT_BROWSER_ACTIONS_FALLBACK_HANDLER_FALLBACK_DATA_H_ - -#include <map> - -#include "base/optional.h" -#include "components/autofill/core/browser/data_model/form_group.h" - -namespace autofill_assistant { - -// Data necessary for filling in the fallback fields. This is kept in a -// separate struct to make sure we don't keep it for longer than strictly -// necessary. -struct FallbackData { - public: - FallbackData(); - ~FallbackData(); - - FallbackData(const FallbackData&) = delete; - FallbackData& operator=(const FallbackData&) = delete; - - // The key of the map. Should be either an entry of field_types.h or an - // enum of Use*Action::AutofillAssistantCustomField. - std::map<int, std::string> field_values; - - void AddFormGroup(const autofill::FormGroup& form_group); - - base::Optional<std::string> GetValueByExpression( - const std::string& value_expression); - - private: - base::Optional<std::string> GetValue(int key); -}; - -} // namespace autofill_assistant -#endif // COMPONENTS_AUTOFILL_ASSISTANT_BROWSER_ACTIONS_FALLBACK_HANDLER_FALLBACK_DATA_H_ diff --git a/chromium/components/autofill_assistant/browser/actions/fallback_handler/required_field.cc b/chromium/components/autofill_assistant/browser/actions/fallback_handler/required_field.cc index d22104117e8..1a12d5b8696 100644 --- a/chromium/components/autofill_assistant/browser/actions/fallback_handler/required_field.cc +++ b/chromium/components/autofill_assistant/browser/actions/fallback_handler/required_field.cc @@ -12,40 +12,13 @@ RequiredField::~RequiredField() = default; RequiredField::RequiredField(const RequiredField& copy) = default; -void RequiredField::FromProto( - const UseAddressProto::RequiredField& required_field_proto) { - value_expression = required_field_proto.value_expression(); - selector = Selector(required_field_proto.element()); - fill_strategy = required_field_proto.fill_strategy(); - select_strategy = required_field_proto.select_strategy(); - delay_in_millisecond = required_field_proto.delay_in_millisecond(); - if (required_field_proto.has_option_element_to_click()) { - fallback_click_element = - Selector(required_field_proto.option_element_to_click()); - click_type = required_field_proto.click_type(); - } - forced = required_field_proto.forced(); -} - -void RequiredField::FromProto( - const UseCreditCardProto::RequiredField& required_field_proto) { - value_expression = required_field_proto.value_expression(); - selector = Selector(required_field_proto.element()); - fill_strategy = required_field_proto.fill_strategy(); - select_strategy = required_field_proto.select_strategy(); - delay_in_millisecond = required_field_proto.delay_in_millisecond(); - if (required_field_proto.has_option_element_to_click()) { - fallback_click_element = - Selector(required_field_proto.option_element_to_click()); - click_type = required_field_proto.click_type(); - } - forced = required_field_proto.forced(); -} - -bool RequiredField::ShouldFallback(bool has_fallback_data) const { - return (status == EMPTY && !fallback_click_element.has_value()) || - (forced && has_fallback_data) || - (fallback_click_element.has_value() && has_fallback_data); +bool RequiredField::ShouldFallback(bool apply_fallback) const { + return (status == EMPTY && !value_expression.empty() && + !fallback_click_element.has_value()) || + (status != EMPTY && value_expression.empty() && + !fallback_click_element.has_value()) || + (forced && apply_fallback) || + (fallback_click_element.has_value() && apply_fallback); } } // namespace autofill_assistant diff --git a/chromium/components/autofill_assistant/browser/actions/fallback_handler/required_field.h b/chromium/components/autofill_assistant/browser/actions/fallback_handler/required_field.h index 97026813ae4..f5713c2ff03 100644 --- a/chromium/components/autofill_assistant/browser/actions/fallback_handler/required_field.h +++ b/chromium/components/autofill_assistant/browser/actions/fallback_handler/required_field.h @@ -16,12 +16,25 @@ struct RequiredField { public: enum FieldValueStatus { UNKNOWN, EMPTY, NOT_EMPTY }; - RequiredField(); ~RequiredField(); + RequiredField(); RequiredField(const RequiredField& copy); - void FromProto(const UseAddressProto::RequiredField& proto); - void FromProto(const UseCreditCardProto::RequiredField& proto); + template <typename T> + void FromProto(const T& required_field_proto) { + selector = Selector(required_field_proto.element()); + value_expression = required_field_proto.value_expression(); + forced = required_field_proto.forced(); + fill_strategy = required_field_proto.fill_strategy(); + delay_in_millisecond = required_field_proto.delay_in_millisecond(); + select_strategy = required_field_proto.select_strategy(); + + if (required_field_proto.has_option_element_to_click()) { + fallback_click_element = + Selector(required_field_proto.option_element_to_click()); + click_type = required_field_proto.click_type(); + } + } // The selector of the field that must be filled. Selector selector; @@ -60,7 +73,7 @@ struct RequiredField { ClickType click_type = ClickType::NOT_SET; // Returns true if fallback is required for this field. - bool ShouldFallback(bool has_fallback_data) const; + bool ShouldFallback(bool apply_fallback) const; }; } // namespace autofill_assistant diff --git a/chromium/components/autofill_assistant/browser/actions/fallback_handler/required_fields_fallback_handler.cc b/chromium/components/autofill_assistant/browser/actions/fallback_handler/required_fields_fallback_handler.cc index bf8eeaab150..1b127e12dd1 100644 --- a/chromium/components/autofill_assistant/browser/actions/fallback_handler/required_fields_fallback_handler.cc +++ b/chromium/components/autofill_assistant/browser/actions/fallback_handler/required_fields_fallback_handler.cc @@ -13,6 +13,7 @@ #include "components/autofill_assistant/browser/actions/action_delegate.h" #include "components/autofill_assistant/browser/batch_element_checker.h" #include "components/autofill_assistant/browser/client_status.h" +#include "components/autofill_assistant/browser/field_formatter.h" namespace autofill_assistant { namespace { @@ -22,11 +23,12 @@ const char kSelectElementTag[] = "SELECT"; AutofillErrorInfoProto::AutofillFieldError* AddAutofillError( const RequiredField& required_field, ClientStatus* client_status) { + client_status->set_proto_status( + ProcessedActionStatusProto::AUTOFILL_INCOMPLETE); auto* field_error = client_status->mutable_details() ->mutable_autofill_error_info() ->add_autofill_field_error(); - *field_error->mutable_field() = - required_field.selector.ToElementReferenceProto(); + *field_error->mutable_field() = required_field.selector.proto; field_error->set_value_expression(required_field.value_expression); return field_error; } @@ -45,25 +47,35 @@ void FillStatusDetailsWithError(const RequiredField& required_field, field_error->set_status(error_status); } -} // namespace +void FillStatusDetailsWithEmptyField(const RequiredField& required_field, + ClientStatus* client_status) { + auto* field_error = AddAutofillError(required_field, client_status); + field_error->set_empty_after_fallback(true); +} -RequiredFieldsFallbackHandler::RequiredFieldsFallbackHandler( - const std::vector<RequiredField>& required_fields, - ActionDelegate* action_delegate) { - required_fields_.assign(required_fields.begin(), required_fields.end()); - action_delegate_ = action_delegate; +void FillStatusDetailsWithNotClearedField(const RequiredField& required_field, + ClientStatus* client_status) { + auto* field_error = AddAutofillError(required_field, client_status); + field_error->set_filled_after_clear(true); } +} // namespace + RequiredFieldsFallbackHandler::~RequiredFieldsFallbackHandler() = default; +RequiredFieldsFallbackHandler::RequiredFieldsFallbackHandler( + const std::vector<RequiredField>& required_fields, + const std::map<std::string, std::string>& fallback_values, + ActionDelegate* delegate) + : required_fields_(required_fields), + fallback_values_(fallback_values), + action_delegate_(delegate) {} + void RequiredFieldsFallbackHandler::CheckAndFallbackRequiredFields( const ClientStatus& initial_autofill_status, - std::unique_ptr<FallbackData> fallback_data, base::OnceCallback<void(const ClientStatus&, const base::Optional<ClientStatus>&)> status_update_callback) { - DCHECK(fallback_data != nullptr); - client_status_ = initial_autofill_status; status_update_callback_ = std::move(status_update_callback); @@ -78,18 +90,18 @@ void RequiredFieldsFallbackHandler::CheckAndFallbackRequiredFields( return; } - CheckAllRequiredFields(std::move(fallback_data)); + CheckAllRequiredFields(/* apply_fallback = */ true); } void RequiredFieldsFallbackHandler::CheckAllRequiredFields( - std::unique_ptr<FallbackData> fallback_data) { + bool apply_fallback) { DCHECK(!batch_element_checker_); batch_element_checker_ = std::make_unique<BatchElementChecker>(); for (size_t i = 0; i < required_fields_.size(); i++) { - // First run (with fallback data) we skip checking forced fields, since - // we overwrite them anyway. Second run (without fallback data) forced - // fields should be checked. - if (required_fields_[i].forced && fallback_data != nullptr) { + // First run (with fallback) we skip checking forced fields, since we + // overwrite them anyway. Second run (without fallback) forced fields should + // be checked. + if (required_fields_[i].forced && apply_fallback) { continue; } @@ -109,7 +121,7 @@ void RequiredFieldsFallbackHandler::CheckAllRequiredFields( batch_element_checker_->AddAllDoneCallback( base::BindOnce(&RequiredFieldsFallbackHandler::OnCheckRequiredFieldsDone, - weak_ptr_factory_.GetWeakPtr(), std::move(fallback_data))); + weak_ptr_factory_.GetWeakPtr(), apply_fallback)); action_delegate_->RunElementChecks(batch_element_checker_.get()); } @@ -122,15 +134,26 @@ void RequiredFieldsFallbackHandler::OnGetRequiredFieldValue( } void RequiredFieldsFallbackHandler::OnCheckRequiredFieldsDone( - std::unique_ptr<FallbackData> fallback_data) { + bool apply_fallback) { batch_element_checker_.reset(); // We process all fields with an empty value in order to perform the fallback // on all those fields, if any. bool should_fallback = false; for (const RequiredField& required_field : required_fields_) { - if (required_field.ShouldFallback(fallback_data != nullptr)) { + if (required_field.ShouldFallback(apply_fallback)) { should_fallback = true; + if (!apply_fallback) { + if (required_field.value_expression.empty()) { + VLOG(1) << "Field was filled after attempting to clear it: " + << required_field.selector; + FillStatusDetailsWithNotClearedField(required_field, &client_status_); + } else { + VLOG(1) << "Field was empty after applying fallback: " + << required_field.selector; + FillStatusDetailsWithEmptyField(required_field, &client_status_); + } + } break; } } @@ -141,7 +164,7 @@ void RequiredFieldsFallbackHandler::OnCheckRequiredFieldsDone( return; } - if (fallback_data == nullptr) { + if (!apply_fallback) { // Validation failed and we don't want to try the fallback. std::move(status_update_callback_) .Run(ClientStatus(AUTOFILL_INCOMPLETE), client_status_); @@ -152,14 +175,19 @@ void RequiredFieldsFallbackHandler::OnCheckRequiredFieldsDone( // immediately. bool has_fallbacks = false; for (const RequiredField& required_field : required_fields_) { - if (!required_field.ShouldFallback(/* has_fallback_data= */ true)) { + if (!required_field.ShouldFallback(/* apply_fallback= */ true)) { continue; } - if (fallback_data->GetValueByExpression(required_field.value_expression) - .has_value()) { + if (required_field.value_expression.empty()) { + has_fallbacks = true; + } else if (field_formatter::FormatString(required_field.value_expression, + fallback_values_) + .has_value()) { has_fallbacks = true; } else { + VLOG(3) << "Field has no fallback data: " << required_field.selector + << " " << required_field.value_expression; FillStatusDetailsWithMissingFallbackData(required_field, &client_status_); } } @@ -170,16 +198,15 @@ void RequiredFieldsFallbackHandler::OnCheckRequiredFieldsDone( } // Set the fallback values and check again. - SetFallbackFieldValuesSequentially(0, std::move(fallback_data)); + SetFallbackFieldValuesSequentially(0); } void RequiredFieldsFallbackHandler::SetFallbackFieldValuesSequentially( - size_t required_fields_index, - std::unique_ptr<FallbackData> fallback_data) { + size_t required_fields_index) { // Skip non-empty fields. while (required_fields_index < required_fields_.size() && !required_fields_[required_fields_index].ShouldFallback( - fallback_data != nullptr)) { + /* apply_fallback= */ true)) { required_fields_index++; } @@ -188,18 +215,29 @@ void RequiredFieldsFallbackHandler::SetFallbackFieldValuesSequentially( if (required_fields_index >= required_fields_.size()) { DCHECK_EQ(required_fields_index, required_fields_.size()); - return CheckAllRequiredFields(/* fallback_data= */ nullptr); + CheckAllRequiredFields(/* apply_fallback= */ false); + return; } - // Set the next field to its fallback value. + // Treat the next field. const RequiredField& required_field = required_fields_[required_fields_index]; - auto fallback_value = - fallback_data->GetValueByExpression(required_field.value_expression); + + if (required_field.value_expression.empty()) { + action_delegate_->SetFieldValue( + required_field.selector, "", required_field.fill_strategy, + required_field.delay_in_millisecond, + base::BindOnce(&RequiredFieldsFallbackHandler::OnSetFallbackFieldValue, + weak_ptr_factory_.GetWeakPtr(), required_fields_index)); + return; + } + + auto fallback_value = field_formatter::FormatString( + required_field.value_expression, fallback_values_); if (!fallback_value.has_value()) { VLOG(3) << "No fallback for " << required_field.selector; // If there is no fallback value, we skip this failed field. - return SetFallbackFieldValuesSequentially(++required_fields_index, - std::move(fallback_data)); + SetFallbackFieldValuesSequentially(++required_fields_index); + return; } if (required_field.fallback_click_element.has_value()) { @@ -214,7 +252,7 @@ void RequiredFieldsFallbackHandler::SetFallbackFieldValuesSequentially( base::BindOnce( &RequiredFieldsFallbackHandler::OnClickOrTapFallbackElement, weak_ptr_factory_.GetWeakPtr(), fallback_value.value(), - required_fields_index, std::move(fallback_data))); + required_fields_index)); return; } @@ -223,13 +261,12 @@ void RequiredFieldsFallbackHandler::SetFallbackFieldValuesSequentially( required_field.selector, base::BindOnce(&RequiredFieldsFallbackHandler::OnGetFallbackFieldTag, weak_ptr_factory_.GetWeakPtr(), fallback_value.value(), - required_fields_index, std::move(fallback_data))); + required_fields_index)); } void RequiredFieldsFallbackHandler::OnGetFallbackFieldTag( const std::string& value, size_t required_fields_index, - std::unique_ptr<FallbackData> fallback_data, const ClientStatus& element_tag_status, const std::string& element_tag) { if (!element_tag_status.ok()) { @@ -252,8 +289,7 @@ void RequiredFieldsFallbackHandler::OnGetFallbackFieldTag( action_delegate_->SelectOption( required_field.selector, value, select_strategy, base::BindOnce(&RequiredFieldsFallbackHandler::OnSetFallbackFieldValue, - weak_ptr_factory_.GetWeakPtr(), required_fields_index, - std::move(fallback_data))); + weak_ptr_factory_.GetWeakPtr(), required_fields_index)); return; } @@ -261,14 +297,12 @@ void RequiredFieldsFallbackHandler::OnGetFallbackFieldTag( required_field.selector, value, required_field.fill_strategy, required_field.delay_in_millisecond, base::BindOnce(&RequiredFieldsFallbackHandler::OnSetFallbackFieldValue, - weak_ptr_factory_.GetWeakPtr(), required_fields_index, - std::move(fallback_data))); + weak_ptr_factory_.GetWeakPtr(), required_fields_index)); } void RequiredFieldsFallbackHandler::OnClickOrTapFallbackElement( const std::string& value, size_t required_fields_index, - std::unique_ptr<FallbackData> fallback_data, const ClientStatus& element_click_status) { const RequiredField& required_field = required_fields_[required_fields_index]; if (!element_click_status.ok()) { @@ -283,21 +317,20 @@ void RequiredFieldsFallbackHandler::OnClickOrTapFallbackElement( DCHECK(required_field.fallback_click_element.has_value()); Selector value_selector = required_field.fallback_click_element.value(); - value_selector.inner_text_pattern = value; - value_selector.must_be_visible = true; + value_selector.MatchingInnerText(value).MustBeVisible(); DVLOG(3) << "Finding option for " << required_field.selector; action_delegate_->ShortWaitForElement( value_selector, base::BindOnce(&RequiredFieldsFallbackHandler::OnShortWaitForElement, weak_ptr_factory_.GetWeakPtr(), value_selector, - required_fields_index, std::move(fallback_data))); + required_fields_index)); } void RequiredFieldsFallbackHandler::OnShortWaitForElement( const Selector& selector_to_click, size_t required_fields_index, - std::unique_ptr<FallbackData> fallback_data, + const ClientStatus& find_element_status) { const RequiredField& required_field = required_fields_[required_fields_index]; if (!find_element_status.ok()) { @@ -319,15 +352,16 @@ void RequiredFieldsFallbackHandler::OnShortWaitForElement( action_delegate_->ClickOrTapElement( selector_to_click, click_type, base::BindOnce(&RequiredFieldsFallbackHandler::OnSetFallbackFieldValue, - weak_ptr_factory_.GetWeakPtr(), required_fields_index, - std::move(fallback_data))); + weak_ptr_factory_.GetWeakPtr(), required_fields_index)); } void RequiredFieldsFallbackHandler::OnSetFallbackFieldValue( size_t required_fields_index, - std::unique_ptr<FallbackData> fallback_data, const ClientStatus& set_field_status) { if (!set_field_status.ok()) { + VLOG(1) << "Error setting value for required_field: " + << required_fields_[required_fields_index].selector << " " + << set_field_status; FillStatusDetailsWithError(required_fields_[required_fields_index], set_field_status.proto_status(), &client_status_); @@ -338,8 +372,7 @@ void RequiredFieldsFallbackHandler::OnSetFallbackFieldValue( return; } - SetFallbackFieldValuesSequentially(++required_fields_index, - std::move(fallback_data)); + SetFallbackFieldValuesSequentially(++required_fields_index); } } // namespace autofill_assistant diff --git a/chromium/components/autofill_assistant/browser/actions/fallback_handler/required_fields_fallback_handler.h b/chromium/components/autofill_assistant/browser/actions/fallback_handler/required_fields_fallback_handler.h index 9707612510d..23637adc736 100644 --- a/chromium/components/autofill_assistant/browser/actions/fallback_handler/required_fields_fallback_handler.h +++ b/chromium/components/autofill_assistant/browser/actions/fallback_handler/required_fields_fallback_handler.h @@ -16,20 +16,21 @@ #include "base/optional.h" #include "components/autofill/core/browser/data_model/autofill_profile.h" #include "components/autofill_assistant/browser/actions/action.h" -#include "components/autofill_assistant/browser/actions/fallback_handler/fallback_data.h" #include "components/autofill_assistant/browser/actions/fallback_handler/required_field.h" #include "components/autofill_assistant/browser/batch_element_checker.h" namespace autofill_assistant { class ClientStatus; -// A handler for required fields and fallback values, used for example in -// UseAddressAction. +// A handler for required fields and fallback values, used by UseAddressAction +// and UseCreditCardAction. class RequiredFieldsFallbackHandler { public: explicit RequiredFieldsFallbackHandler( const std::vector<RequiredField>& required_fields, + const std::map<std::string, std::string>& fallback_values, ActionDelegate* delegate); + ~RequiredFieldsFallbackHandler(); // Check if there are required fields. If so, verify them and fallback if @@ -37,23 +38,20 @@ class RequiredFieldsFallbackHandler { // action. void CheckAndFallbackRequiredFields( const ClientStatus& initial_autofill_status, - std::unique_ptr<FallbackData> fallback_data, base::OnceCallback<void(const ClientStatus&, const base::Optional<ClientStatus>&)> status_update_callback); private: // Check whether all required fields have a non-empty value. If it is the - // case, update the status to success. If it's not and |fallback_data| - // is null, update the status to failure. If |fallback_data| is non-null, use - // it to attempt to fill the failed fields without Autofill. - void CheckAllRequiredFields(std::unique_ptr<FallbackData> fallback_data); + // case, update the status to success. If it's not and |apply_fallback| + // is false, update the status to failure. If |apply_fallback| is true, + // attempt to fill the failed fields without Autofill using fallback values. + void CheckAllRequiredFields(bool apply_fallback); // Triggers the check for a specific field. - void CheckRequiredFieldsSequentially( - bool allow_fallback, - size_t required_fields_index, - std::unique_ptr<FallbackData> fallback_data); + void CheckRequiredFieldsSequentially(bool allow_fallback, + size_t required_fields_index); // Updates the status of the required field. void OnGetRequiredFieldValue(size_t required_fields_index, @@ -61,40 +59,35 @@ class RequiredFieldsFallbackHandler { const std::string& value); // Called when all required fields have been checked. - void OnCheckRequiredFieldsDone(std::unique_ptr<FallbackData> fallback_data); + void OnCheckRequiredFieldsDone(bool apply_fallback); // Sets fallback field values for empty fields. - void SetFallbackFieldValuesSequentially( - size_t required_fields_index, - std::unique_ptr<FallbackData> fallback_data); + void SetFallbackFieldValuesSequentially(size_t required_fields_index); // Called after retrieving tag name from a field. void OnGetFallbackFieldTag(const std::string& value, size_t required_fields_index, - std::unique_ptr<FallbackData> fallback_data, const ClientStatus& element_tag_status, const std::string& element_tag); // Called after clicking a fallback element. void OnClickOrTapFallbackElement(const std::string& value, size_t required_fields_index, - std::unique_ptr<FallbackData> fallback_data, const ClientStatus& element_click_status); // Called after waiting for option element to appear before clicking it. void OnShortWaitForElement(const Selector& selector_to_click, size_t required_fields_index, - std::unique_ptr<FallbackData> fallback_data, const ClientStatus& find_element_status); // Called after trying to set form values without Autofill in case of // fallback after failed validation. void OnSetFallbackFieldValue(size_t required_fields_index, - std::unique_ptr<FallbackData> fallback_data, const ClientStatus& status); ClientStatus client_status_; std::vector<RequiredField> required_fields_; + std::map<std::string, std::string> fallback_values_; base::OnceCallback<void(const ClientStatus&, const base::Optional<ClientStatus>&)> status_update_callback_; diff --git a/chromium/components/autofill_assistant/browser/actions/fallback_handler/required_fields_fallback_handler_unittest.cc b/chromium/components/autofill_assistant/browser/actions/fallback_handler/required_fields_fallback_handler_unittest.cc index 62c9c65a8e4..863135de8b9 100644 --- a/chromium/components/autofill_assistant/browser/actions/fallback_handler/required_fields_fallback_handler_unittest.cc +++ b/chromium/components/autofill_assistant/browser/actions/fallback_handler/required_fields_fallback_handler_unittest.cc @@ -12,10 +12,10 @@ #include "base/test/mock_callback.h" #include "components/autofill/core/browser/autofill_test_utils.h" #include "components/autofill/core/browser/data_model/autofill_profile.h" -#include "components/autofill_assistant/browser/actions/fallback_handler/fallback_data.h" #include "components/autofill_assistant/browser/actions/fallback_handler/required_field.h" #include "components/autofill_assistant/browser/actions/mock_action_delegate.h" #include "components/autofill_assistant/browser/client_status.h" +#include "components/autofill_assistant/browser/service.pb.h" #include "components/autofill_assistant/browser/web/mock_web_controller.h" #include "testing/gmock/include/gmock/gmock.h" @@ -57,12 +57,9 @@ class RequiredFieldsFallbackHandlerTest : public testing::Test { TEST_F(RequiredFieldsFallbackHandlerTest, AutofillFailureExitsEarlyForEmptyRequiredFields) { - std::vector<RequiredField> fields; - - auto fallback_data = std::make_unique<FallbackData>(); - - RequiredFieldsFallbackHandler fallback_handler(fields, - &mock_action_delegate_); + RequiredFieldsFallbackHandler fallback_handler( + /* required_fields = */ {}, + /* fallback_values = */ {}, &mock_action_delegate_); base::OnceCallback<void(const ClientStatus&, const base::Optional<ClientStatus>&)> @@ -74,30 +71,30 @@ TEST_F(RequiredFieldsFallbackHandlerTest, }); fallback_handler.CheckAndFallbackRequiredFields( - ClientStatus(OTHER_ACTION_STATUS), std::move(fallback_data), - std::move(callback)); + ClientStatus(OTHER_ACTION_STATUS), std::move(callback)); } TEST_F(RequiredFieldsFallbackHandlerTest, - AddsMissingOrEmptyFallbackFieldToError) { + AddsMissingOrEmptyFallbackValuesToError) { ON_CALL(mock_web_controller_, OnGetFieldValue(_, _)) .WillByDefault(RunOnceCallback<1>(OkClientStatus(), "")); - std::vector<RequiredField> fields; - fields.emplace_back(CreateRequiredField("51", {"#card_name"})); - fields.emplace_back(CreateRequiredField("52", {"#card_number"})); - fields.emplace_back(CreateRequiredField("-3", {"#card_network"})); + std::vector<RequiredField> required_fields = { + CreateRequiredField("${51}", {"#card_name"}), + CreateRequiredField("${52}", {"#card_number"}), + CreateRequiredField("${-3}", {"#card_network"})}; - auto fallback_data = std::make_unique<FallbackData>(); - fallback_data->field_values.emplace( - static_cast<int>(autofill::ServerFieldType::CREDIT_CARD_NAME_FULL), + std::map<std::string, std::string> fallback_values; + fallback_values.emplace( + base::NumberToString( + static_cast<int>(autofill::ServerFieldType::CREDIT_CARD_NAME_FULL)), "John Doe"); - fallback_data->field_values.emplace( - static_cast<int>(UseCreditCardProto::RequiredField::CREDIT_CARD_NETWORK), - ""); + fallback_values.emplace(base::NumberToString(static_cast<int>( + AutofillFormatProto::CREDIT_CARD_NETWORK)), + ""); - RequiredFieldsFallbackHandler fallback_handler(fields, - &mock_action_delegate_); + RequiredFieldsFallbackHandler fallback_handler( + required_fields, fallback_values, &mock_action_delegate_); base::OnceCallback<void(const ClientStatus&, const base::Optional<ClientStatus>&)> @@ -106,17 +103,19 @@ TEST_F(RequiredFieldsFallbackHandlerTest, const base::Optional<ClientStatus>& detail_status) { EXPECT_EQ(status.proto_status(), AUTOFILL_INCOMPLETE); ASSERT_TRUE(detail_status.has_value()); + ASSERT_EQ(detail_status.value().proto_status(), + AUTOFILL_INCOMPLETE); ASSERT_EQ(detail_status.value() .details() .autofill_error_info() .autofill_field_error_size(), - 2); + 3); EXPECT_EQ(detail_status.value() .details() .autofill_error_info() .autofill_field_error(0) .value_expression(), - "52"); + "${52}"); EXPECT_TRUE(detail_status.value() .details() .autofill_error_info() @@ -127,16 +126,27 @@ TEST_F(RequiredFieldsFallbackHandlerTest, .autofill_error_info() .autofill_field_error(1) .value_expression(), - "-3"); + "${-3}"); EXPECT_TRUE(detail_status.value() .details() .autofill_error_info() .autofill_field_error(1) .no_fallback_value()); + EXPECT_EQ(detail_status.value() + .details() + .autofill_error_info() + .autofill_field_error(2) + .value_expression(), + "${51}"); + EXPECT_TRUE(detail_status.value() + .details() + .autofill_error_info() + .autofill_field_error(2) + .empty_after_fallback()); }); - fallback_handler.CheckAndFallbackRequiredFields( - OkClientStatus(), std::move(fallback_data), std::move(callback)); + fallback_handler.CheckAndFallbackRequiredFields(OkClientStatus(), + std::move(callback)); } TEST_F(RequiredFieldsFallbackHandlerTest, AddsFirstFieldFillingError) { @@ -145,20 +155,21 @@ TEST_F(RequiredFieldsFallbackHandlerTest, AddsFirstFieldFillingError) { ON_CALL(mock_action_delegate_, OnSetFieldValue(_, _, _)) .WillByDefault(RunOnceCallback<2>(ClientStatus(OTHER_ACTION_STATUS))); - std::vector<RequiredField> fields; - fields.emplace_back(CreateRequiredField("51", {"#card_name"})); - fields.emplace_back(CreateRequiredField("52", {"#card_number"})); + std::vector<RequiredField> required_fields = { + CreateRequiredField("${51}", {"#card_name"}), + CreateRequiredField("${52}", {"#card_number"})}; - auto fallback_data = std::make_unique<FallbackData>(); - fallback_data->field_values.emplace( - static_cast<int>(autofill::ServerFieldType::CREDIT_CARD_NAME_FULL), + std::map<std::string, std::string> fallback_values; + fallback_values.emplace( + base::NumberToString( + static_cast<int>(autofill::ServerFieldType::CREDIT_CARD_NAME_FULL)), "John Doe"); - fallback_data->field_values.emplace( - static_cast<int>(autofill::ServerFieldType::CREDIT_CARD_NUMBER), - "4111111111111111"); + fallback_values.emplace(base::NumberToString(static_cast<int>( + autofill::ServerFieldType::CREDIT_CARD_NUMBER)), + "4111111111111111"); - RequiredFieldsFallbackHandler fallback_handler(fields, - &mock_action_delegate_); + RequiredFieldsFallbackHandler fallback_handler( + required_fields, fallback_values, &mock_action_delegate_); base::OnceCallback<void(const ClientStatus&, const base::Optional<ClientStatus>&)> @@ -167,6 +178,8 @@ TEST_F(RequiredFieldsFallbackHandlerTest, AddsFirstFieldFillingError) { const base::Optional<ClientStatus>& detail_status) { EXPECT_EQ(status.proto_status(), AUTOFILL_INCOMPLETE); ASSERT_TRUE(detail_status.has_value()); + ASSERT_EQ(detail_status.value().proto_status(), + AUTOFILL_INCOMPLETE); ASSERT_EQ(detail_status.value() .details() .autofill_error_info() @@ -177,7 +190,7 @@ TEST_F(RequiredFieldsFallbackHandlerTest, AddsFirstFieldFillingError) { .autofill_error_info() .autofill_field_error(0) .value_expression(), - "51"); + "${51}"); EXPECT_EQ(detail_status.value() .details() .autofill_error_info() @@ -186,8 +199,60 @@ TEST_F(RequiredFieldsFallbackHandlerTest, AddsFirstFieldFillingError) { OTHER_ACTION_STATUS); }); - fallback_handler.CheckAndFallbackRequiredFields( - OkClientStatus(), std::move(fallback_data), std::move(callback)); + fallback_handler.CheckAndFallbackRequiredFields(OkClientStatus(), + std::move(callback)); +} + +TEST_F(RequiredFieldsFallbackHandlerTest, + AddsFirstEmptyFieldAfterFillingToError) { + ON_CALL(mock_web_controller_, OnGetFieldValue(_, _)) + .WillByDefault(RunOnceCallback<1>(OkClientStatus(), "")); + + std::vector<RequiredField> required_fields = { + CreateRequiredField("${51}", {"#card_name"}), + CreateRequiredField("${52}", {"#card_number"})}; + + std::map<std::string, std::string> fallback_values; + fallback_values.emplace( + base::NumberToString( + static_cast<int>(autofill::ServerFieldType::CREDIT_CARD_NAME_FULL)), + "John Doe"); + fallback_values.emplace(base::NumberToString(static_cast<int>( + autofill::ServerFieldType::CREDIT_CARD_NUMBER)), + "4111111111111111"); + + RequiredFieldsFallbackHandler fallback_handler( + required_fields, fallback_values, &mock_action_delegate_); + + base::OnceCallback<void(const ClientStatus&, + const base::Optional<ClientStatus>&)> + callback = + base::BindOnce([](const ClientStatus& status, + const base::Optional<ClientStatus>& detail_status) { + EXPECT_EQ(status.proto_status(), AUTOFILL_INCOMPLETE); + ASSERT_TRUE(detail_status.has_value()); + ASSERT_EQ(detail_status.value().proto_status(), + AUTOFILL_INCOMPLETE); + ASSERT_EQ(detail_status.value() + .details() + .autofill_error_info() + .autofill_field_error_size(), + 1); + EXPECT_EQ(detail_status.value() + .details() + .autofill_error_info() + .autofill_field_error(0) + .value_expression(), + "${51}"); + EXPECT_TRUE(detail_status.value() + .details() + .autofill_error_info() + .autofill_field_error(0) + .empty_after_fallback()); + }); + + fallback_handler.CheckAndFallbackRequiredFields(OkClientStatus(), + std::move(callback)); } TEST_F(RequiredFieldsFallbackHandlerTest, DoesNotFallbackIfFieldsAreFilled) { @@ -195,12 +260,10 @@ TEST_F(RequiredFieldsFallbackHandlerTest, DoesNotFallbackIfFieldsAreFilled) { .WillByDefault(RunOnceCallback<1>(OkClientStatus(), "value")); EXPECT_CALL(mock_action_delegate_, OnSetFieldValue(_, _, _)).Times(0); - std::vector<RequiredField> fields; - fields.emplace_back(CreateRequiredField("51", {"#card_name"})); - - auto fallback_data = std::make_unique<FallbackData>(); + std::vector<RequiredField> required_fields = { + CreateRequiredField("${51}", {"#card_name"})}; - RequiredFieldsFallbackHandler fallback_handler(fields, + RequiredFieldsFallbackHandler fallback_handler(required_fields, {}, &mock_action_delegate_); base::OnceCallback<void(const ClientStatus&, @@ -211,8 +274,8 @@ TEST_F(RequiredFieldsFallbackHandlerTest, DoesNotFallbackIfFieldsAreFilled) { EXPECT_EQ(status.proto_status(), ACTION_APPLIED); }); - fallback_handler.CheckAndFallbackRequiredFields( - OkClientStatus(), std::move(fallback_data), std::move(callback)); + fallback_handler.CheckAndFallbackRequiredFields(OkClientStatus(), + std::move(callback)); } TEST_F(RequiredFieldsFallbackHandlerTest, FillsEmptyRequiredField) { @@ -226,16 +289,17 @@ TEST_F(RequiredFieldsFallbackHandlerTest, FillsEmptyRequiredField) { .After(set_value) .WillOnce(RunOnceCallback<1>(OkClientStatus(), "John Doe")); - std::vector<RequiredField> fields; - fields.emplace_back(CreateRequiredField("51", {"#card_name"})); + std::vector<RequiredField> required_fields = { + CreateRequiredField("${51}", {"#card_name"})}; - auto fallback_data = std::make_unique<FallbackData>(); - fallback_data->field_values.emplace( - static_cast<int>(autofill::ServerFieldType::CREDIT_CARD_NAME_FULL), + std::map<std::string, std::string> fallback_values; + fallback_values.emplace( + base::NumberToString( + static_cast<int>(autofill::ServerFieldType::CREDIT_CARD_NAME_FULL)), "John Doe"); - RequiredFieldsFallbackHandler fallback_handler(fields, - &mock_action_delegate_); + RequiredFieldsFallbackHandler fallback_handler( + required_fields, fallback_values, &mock_action_delegate_); base::OnceCallback<void(const ClientStatus&, const base::Optional<ClientStatus>&)> @@ -245,8 +309,8 @@ TEST_F(RequiredFieldsFallbackHandlerTest, FillsEmptyRequiredField) { EXPECT_EQ(status.proto_status(), ACTION_APPLIED); }); - fallback_handler.CheckAndFallbackRequiredFields( - OkClientStatus(), std::move(fallback_data), std::move(callback)); + fallback_handler.CheckAndFallbackRequiredFields(OkClientStatus(), + std::move(callback)); } TEST_F(RequiredFieldsFallbackHandlerTest, FallsBackForForcedFilledField) { @@ -256,18 +320,18 @@ TEST_F(RequiredFieldsFallbackHandlerTest, FallsBackForForcedFilledField) { OnSetFieldValue(Eq(Selector({"#card_name"})), "John Doe", _)) .WillOnce(RunOnceCallback<2>(OkClientStatus())); - std::vector<RequiredField> fields; - auto field = CreateRequiredField("51", {"#card_name"}); - field.forced = true; - fields.emplace_back(field); + std::vector<RequiredField> required_fields = { + CreateRequiredField("${51}", {"#card_name"})}; + required_fields[0].forced = true; - auto fallback_data = std::make_unique<FallbackData>(); - fallback_data->field_values.emplace( - static_cast<int>(autofill::ServerFieldType::CREDIT_CARD_NAME_FULL), + std::map<std::string, std::string> fallback_values; + fallback_values.emplace( + base::NumberToString( + static_cast<int>(autofill::ServerFieldType::CREDIT_CARD_NAME_FULL)), "John Doe"); - RequiredFieldsFallbackHandler fallback_handler(fields, - &mock_action_delegate_); + RequiredFieldsFallbackHandler fallback_handler( + required_fields, fallback_values, &mock_action_delegate_); base::OnceCallback<void(const ClientStatus&, const base::Optional<ClientStatus>&)> @@ -277,8 +341,8 @@ TEST_F(RequiredFieldsFallbackHandlerTest, FallsBackForForcedFilledField) { EXPECT_EQ(status.proto_status(), ACTION_APPLIED); }); - fallback_handler.CheckAndFallbackRequiredFields( - OkClientStatus(), std::move(fallback_data), std::move(callback)); + fallback_handler.CheckAndFallbackRequiredFields(OkClientStatus(), + std::move(callback)); } TEST_F(RequiredFieldsFallbackHandlerTest, FailsIfForcedFieldDidNotGetFilled) { @@ -286,14 +350,11 @@ TEST_F(RequiredFieldsFallbackHandlerTest, FailsIfForcedFieldDidNotGetFilled) { .WillByDefault(RunOnceCallback<1>(OkClientStatus(), "value")); EXPECT_CALL(mock_action_delegate_, OnSetFieldValue(_, _, _)).Times(0); - std::vector<RequiredField> fields; - auto field = CreateRequiredField("51", {"#card_name"}); - field.forced = true; - fields.emplace_back(field); + std::vector<RequiredField> required_fields = { + CreateRequiredField("${51}", {"#card_name"})}; + required_fields[0].forced = true; - auto fallback_data = std::make_unique<FallbackData>(); - - RequiredFieldsFallbackHandler fallback_handler(fields, + RequiredFieldsFallbackHandler fallback_handler(required_fields, {}, &mock_action_delegate_); base::OnceCallback<void(const ClientStatus&, @@ -303,6 +364,8 @@ TEST_F(RequiredFieldsFallbackHandlerTest, FailsIfForcedFieldDidNotGetFilled) { const base::Optional<ClientStatus>& detail_status) { EXPECT_EQ(status.proto_status(), AUTOFILL_INCOMPLETE); ASSERT_TRUE(detail_status.has_value()); + ASSERT_EQ(detail_status.value().proto_status(), + AUTOFILL_INCOMPLETE); ASSERT_EQ(detail_status.value() .details() .autofill_error_info() @@ -313,7 +376,7 @@ TEST_F(RequiredFieldsFallbackHandlerTest, FailsIfForcedFieldDidNotGetFilled) { .autofill_error_info() .autofill_field_error(0) .value_expression(), - "51"); + "${51}"); EXPECT_TRUE(detail_status.value() .details() .autofill_error_info() @@ -321,8 +384,8 @@ TEST_F(RequiredFieldsFallbackHandlerTest, FailsIfForcedFieldDidNotGetFilled) { .no_fallback_value()); }); - fallback_handler.CheckAndFallbackRequiredFields( - OkClientStatus(), std::move(fallback_data), std::move(callback)); + fallback_handler.CheckAndFallbackRequiredFields(OkClientStatus(), + std::move(callback)); } TEST_F(RequiredFieldsFallbackHandlerTest, FillsFieldWithPattern) { @@ -336,18 +399,21 @@ TEST_F(RequiredFieldsFallbackHandlerTest, FillsFieldWithPattern) { .After(set_value) .WillOnce(RunOnceCallback<1>(OkClientStatus(), "not empty")); - std::vector<RequiredField> fields; - fields.emplace_back(CreateRequiredField("${53}/${55}", {"#card_expiry"})); - - auto fallback_data = std::make_unique<FallbackData>(); - fallback_data->field_values.emplace( - static_cast<int>(autofill::ServerFieldType::CREDIT_CARD_EXP_MONTH), "08"); - fallback_data->field_values.emplace( - static_cast<int>(autofill::ServerFieldType::CREDIT_CARD_EXP_4_DIGIT_YEAR), + std::vector<RequiredField> required_fields = { + CreateRequiredField("${53}/${55}", {"#card_expiry"})}; + + std::map<std::string, std::string> fallback_values; + fallback_values.emplace( + base::NumberToString( + static_cast<int>(autofill::ServerFieldType::CREDIT_CARD_EXP_MONTH)), + "08"); + fallback_values.emplace( + base::NumberToString(static_cast<int>( + autofill::ServerFieldType::CREDIT_CARD_EXP_4_DIGIT_YEAR)), "2050"); - RequiredFieldsFallbackHandler fallback_handler(fields, - &mock_action_delegate_); + RequiredFieldsFallbackHandler fallback_handler( + required_fields, fallback_values, &mock_action_delegate_); base::OnceCallback<void(const ClientStatus&, const base::Optional<ClientStatus>&)> @@ -357,8 +423,8 @@ TEST_F(RequiredFieldsFallbackHandlerTest, FillsFieldWithPattern) { EXPECT_EQ(status.proto_status(), ACTION_APPLIED); }); - fallback_handler.CheckAndFallbackRequiredFields( - OkClientStatus(), std::move(fallback_data), std::move(callback)); + fallback_handler.CheckAndFallbackRequiredFields(OkClientStatus(), + std::move(callback)); } TEST_F(RequiredFieldsFallbackHandlerTest, @@ -368,17 +434,17 @@ TEST_F(RequiredFieldsFallbackHandlerTest, .WillRepeatedly(RunOnceCallback<1>(OkClientStatus(), "")); EXPECT_CALL(mock_action_delegate_, OnSetFieldValue(_, _, _)).Times(0); - std::vector<RequiredField> fields; - fields.emplace_back(CreateRequiredField("53", {"#card_expiry"})); - fields.emplace_back(CreateRequiredField("-3", {"#card_network"})); + std::vector<RequiredField> required_fields = { + CreateRequiredField("${53}", {"#card_expiry"}), + CreateRequiredField("${-3}", {"#card_network"})}; - auto fallback_data = std::make_unique<FallbackData>(); - fallback_data->field_values.emplace( - static_cast<int>(UseCreditCardProto::RequiredField::CREDIT_CARD_NETWORK), - ""); + std::map<std::string, std::string> fallback_values; + fallback_values.emplace(base::NumberToString(static_cast<int>( + AutofillFormatProto::CREDIT_CARD_NETWORK)), + ""); - RequiredFieldsFallbackHandler fallback_handler(fields, - &mock_action_delegate_); + RequiredFieldsFallbackHandler fallback_handler( + required_fields, fallback_values, &mock_action_delegate_); base::OnceCallback<void(const ClientStatus&, const base::Optional<ClientStatus>&)> @@ -387,6 +453,8 @@ TEST_F(RequiredFieldsFallbackHandlerTest, const base::Optional<ClientStatus>& detail_status) { EXPECT_EQ(status.proto_status(), AUTOFILL_INCOMPLETE); ASSERT_TRUE(detail_status.has_value()); + ASSERT_EQ(detail_status.value().proto_status(), + AUTOFILL_INCOMPLETE); ASSERT_EQ(detail_status.value() .details() .autofill_error_info() @@ -397,7 +465,7 @@ TEST_F(RequiredFieldsFallbackHandlerTest, .autofill_error_info() .autofill_field_error(0) .value_expression(), - "53"); + "${53}"); EXPECT_TRUE(detail_status.value() .details() .autofill_error_info() @@ -408,7 +476,7 @@ TEST_F(RequiredFieldsFallbackHandlerTest, .autofill_error_info() .autofill_field_error(1) .value_expression(), - "-3"); + "${-3}"); EXPECT_TRUE(detail_status.value() .details() .autofill_error_info() @@ -416,114 +484,8 @@ TEST_F(RequiredFieldsFallbackHandlerTest, .no_fallback_value()); }); - fallback_handler.CheckAndFallbackRequiredFields( - OkClientStatus(), std::move(fallback_data), std::move(callback)); -} - -TEST_F(RequiredFieldsFallbackHandlerTest, IgnoresNonIntegerKeys) { - EXPECT_CALL(mock_web_controller_, OnGetFieldValue(_, _)) - .WillOnce(RunOnceCallback<1>(OkClientStatus(), "")); - Expectation set_value = - EXPECT_CALL(mock_action_delegate_, - OnSetFieldValue(Eq(Selector({"#card_expiry"})), "${KEY}", _)) - .WillOnce(RunOnceCallback<2>(OkClientStatus())); - EXPECT_CALL(mock_web_controller_, OnGetFieldValue(_, _)) - .After(set_value) - .WillOnce(RunOnceCallback<1>(OkClientStatus(), "not empty")); - - std::vector<RequiredField> fields; - fields.emplace_back(CreateRequiredField("${KEY}", {"#card_expiry"})); - - auto fallback_data = std::make_unique<FallbackData>(); - - RequiredFieldsFallbackHandler fallback_handler(fields, - &mock_action_delegate_); - - base::OnceCallback<void(const ClientStatus&, - const base::Optional<ClientStatus>&)> - callback = - base::BindOnce([](const ClientStatus& status, - const base::Optional<ClientStatus>& detail_status) { - EXPECT_EQ(status.proto_status(), ACTION_APPLIED); - }); - - fallback_handler.CheckAndFallbackRequiredFields( - OkClientStatus(), std::move(fallback_data), std::move(callback)); -} - -TEST_F(RequiredFieldsFallbackHandlerTest, AddsAllProfileFields) { - std::unordered_map<int, std::string> expected_values = { - {3, "Alpha"}, - {4, "Beta"}, - {5, "Gamma"}, - {6, "B"}, - {7, "Alpha Beta Gamma"}, - {9, "alpha@google.com"}, - {10, "1234567"}, - {11, "79"}, - {12, "41"}, - {13, "0791234567"}, - {14, "+41791234567"}, - {30, "Brandschenkestrasse 110"}, - {31, "Google Building 110"}, - {33, "Zurich"}, - {34, "Canton Zurich"}, - {35, "8002"}, - {36, "Switzerland"}, - {60, "Google"}, - {77, "Brandschenkestrasse 110\nGoogle Building 110"}}; - - auto profile = std::make_unique<autofill::AutofillProfile>( - base::GenerateGUID(), autofill::test::kEmptyOrigin); - autofill::test::SetProfileInfo( - profile.get(), "Alpha", "Beta", "Gamma", "alpha@google.com", "Google", - "Brandschenkestrasse 110", "Google Building 110", "Zurich", - "Canton Zurich", "8002", "CH", "+41791234567"); - FallbackData fallback_data; - fallback_data.AddFormGroup(*profile); - - for (auto iter = expected_values.begin(); iter != expected_values.end(); - ++iter) { - ASSERT_TRUE( - fallback_data.GetValueByExpression(base::NumberToString(iter->first)) - .has_value()); - EXPECT_EQ( - fallback_data.GetValueByExpression(base::NumberToString(iter->first)) - .value(), - iter->second); - } -} - -TEST_F(RequiredFieldsFallbackHandlerTest, AddsAllCreditCardFields) { - std::unordered_map<int, std::string> expected_values = { - {51, "Alpha Beta Gamma"}, - {52, "4111111111111111"}, - {53, "08"}, - {54, "50"}, - {55, "2050"}, - {56, "08/50"}, - {57, "08/2050"}, - {58, "Visa"}, - {91, "Alpha"}, - {92, "Gamma"}}; - - auto card = std::make_unique<autofill::CreditCard>( - base::GenerateGUID(), autofill::test::kEmptyOrigin); - autofill::test::SetCreditCardInfo(card.get(), "Alpha Beta Gamma", - "4111111111111111", "8", "2050", ""); - FallbackData fallback_data; - fallback_data.AddFormGroup(*card); - - for (auto iter = expected_values.begin(); iter != expected_values.end(); - ++iter) { - ASSERT_TRUE( - fallback_data.GetValueByExpression(base::NumberToString(iter->first)) - .has_value()); - EXPECT_EQ( - fallback_data.GetValueByExpression(base::NumberToString(iter->first)) - .value(), - iter->second); - } + fallback_handler.CheckAndFallbackRequiredFields(OkClientStatus(), + std::move(callback)); } TEST_F(RequiredFieldsFallbackHandlerTest, ClicksOnCustomDropdown) { @@ -534,8 +496,8 @@ TEST_F(RequiredFieldsFallbackHandlerTest, ClicksOnCustomDropdown) { ClickOrTapElement(Eq(Selector({"#card_expiry"})), ClickType::TAP, _)) .WillOnce(RunOnceCallback<2>(OkClientStatus())); Selector expected_selector({".option"}); - expected_selector.must_be_visible = true; - expected_selector.inner_text_pattern = "08"; + expected_selector.MatchingInnerText("08"); + expected_selector.MustBeVisible(); EXPECT_CALL(mock_action_delegate_, OnShortWaitForElement(Eq(expected_selector), _)) .WillOnce(RunOnceCallback<1>(OkClientStatus())); @@ -543,17 +505,18 @@ TEST_F(RequiredFieldsFallbackHandlerTest, ClicksOnCustomDropdown) { ClickOrTapElement(Eq(expected_selector), ClickType::TAP, _)) .WillOnce(RunOnceCallback<2>(OkClientStatus())); - std::vector<RequiredField> fields; - RequiredField required_field = CreateRequiredField("53", {"#card_expiry"}); - required_field.fallback_click_element = Selector({".option"}); - fields.emplace_back(required_field); + std::vector<RequiredField> required_fields = { + CreateRequiredField("${53}", {"#card_expiry"})}; + required_fields[0].fallback_click_element = Selector({".option"}); - auto fallback_data = std::make_unique<FallbackData>(); - fallback_data->field_values.emplace( - static_cast<int>(autofill::ServerFieldType::CREDIT_CARD_EXP_MONTH), "08"); + std::map<std::string, std::string> fallback_values; + fallback_values.emplace( + base::NumberToString( + static_cast<int>(autofill::ServerFieldType::CREDIT_CARD_EXP_MONTH)), + "08"); - RequiredFieldsFallbackHandler fallback_handler(fields, - &mock_action_delegate_); + RequiredFieldsFallbackHandler fallback_handler( + required_fields, fallback_values, &mock_action_delegate_); base::OnceCallback<void(const ClientStatus&, const base::Optional<ClientStatus>&)> @@ -563,8 +526,8 @@ TEST_F(RequiredFieldsFallbackHandlerTest, ClicksOnCustomDropdown) { EXPECT_EQ(status.proto_status(), ACTION_APPLIED); }); - fallback_handler.CheckAndFallbackRequiredFields( - OkClientStatus(), std::move(fallback_data), std::move(callback)); + fallback_handler.CheckAndFallbackRequiredFields(OkClientStatus(), + std::move(callback)); } TEST_F(RequiredFieldsFallbackHandlerTest, CustomDropdownClicksStopOnError) { @@ -575,8 +538,8 @@ TEST_F(RequiredFieldsFallbackHandlerTest, CustomDropdownClicksStopOnError) { ClickOrTapElement(Eq(Selector({"#card_expiry"})), ClickType::TAP, _)) .WillOnce(RunOnceCallback<2>(OkClientStatus())); Selector expected_selector({".option"}); - expected_selector.must_be_visible = true; - expected_selector.inner_text_pattern = "08"; + expected_selector.MatchingInnerText("08"); + expected_selector.MustBeVisible(); EXPECT_CALL(mock_action_delegate_, OnShortWaitForElement(Eq(expected_selector), _)) .WillOnce(RunOnceCallback<1>(ClientStatus(ELEMENT_RESOLUTION_FAILED))); @@ -584,17 +547,18 @@ TEST_F(RequiredFieldsFallbackHandlerTest, CustomDropdownClicksStopOnError) { ClickOrTapElement(Eq(expected_selector), _, _)) .Times(0); - std::vector<RequiredField> fields; - RequiredField required_field = CreateRequiredField("53", {"#card_expiry"}); - required_field.fallback_click_element = Selector({".option"}); - fields.emplace_back(required_field); + std::vector<RequiredField> required_fields = { + CreateRequiredField("${53}", {"#card_expiry"})}; + required_fields[0].fallback_click_element = Selector({".option"}); - auto fallback_data = std::make_unique<FallbackData>(); - fallback_data->field_values.emplace( - static_cast<int>(autofill::ServerFieldType::CREDIT_CARD_EXP_MONTH), "08"); + std::map<std::string, std::string> fallback_values; + fallback_values.emplace( + base::NumberToString( + static_cast<int>(autofill::ServerFieldType::CREDIT_CARD_EXP_MONTH)), + "08"); - RequiredFieldsFallbackHandler fallback_handler(fields, - &mock_action_delegate_); + RequiredFieldsFallbackHandler fallback_handler( + required_fields, fallback_values, &mock_action_delegate_); base::OnceCallback<void(const ClientStatus&, const base::Optional<ClientStatus>&)> @@ -604,8 +568,53 @@ TEST_F(RequiredFieldsFallbackHandlerTest, CustomDropdownClicksStopOnError) { EXPECT_EQ(status.proto_status(), AUTOFILL_INCOMPLETE); }); - fallback_handler.CheckAndFallbackRequiredFields( - OkClientStatus(), std::move(fallback_data), std::move(callback)); + fallback_handler.CheckAndFallbackRequiredFields(OkClientStatus(), + std::move(callback)); +} + +TEST_F(RequiredFieldsFallbackHandlerTest, ClearsFilledFields) { + Selector full_field_selector({"#full_field"}); + Selector empty_field_selector({"#empty_field"}); + EXPECT_CALL(mock_web_controller_, OnGetFieldValue(full_field_selector, _)) + .WillOnce(RunOnceCallback<1>(OkClientStatus(), "value")); + EXPECT_CALL(mock_web_controller_, OnGetFieldValue(empty_field_selector, _)) + .Times(0); + + Expectation clear_full_value = + EXPECT_CALL(mock_action_delegate_, + OnSetFieldValue(full_field_selector, "", _)) + .WillOnce(RunOnceCallback<2>(OkClientStatus())); + EXPECT_CALL(mock_web_controller_, OnGetFieldValue(full_field_selector, _)) + .After(clear_full_value) + .WillOnce(RunOnceCallback<1>(OkClientStatus(), "")); + Expectation clear_empty_value = + EXPECT_CALL(mock_action_delegate_, + OnSetFieldValue(empty_field_selector, "", _)) + .WillOnce(RunOnceCallback<2>(OkClientStatus())); + EXPECT_CALL(mock_web_controller_, OnGetFieldValue(empty_field_selector, _)) + .After(clear_empty_value) + .WillOnce(RunOnceCallback<1>(OkClientStatus(), "")); + + auto non_forced_field = CreateRequiredField("", {"#full_field"}); + auto forced_field = CreateRequiredField("", {"#empty_field"}); + forced_field.forced = true; + std::vector<RequiredField> required_fields = {non_forced_field, forced_field}; + + std::map<std::string, std::string> fallback_values; + + RequiredFieldsFallbackHandler fallback_handler( + required_fields, fallback_values, &mock_action_delegate_); + + base::OnceCallback<void(const ClientStatus&, + const base::Optional<ClientStatus>&)> + callback = + base::BindOnce([](const ClientStatus& status, + const base::Optional<ClientStatus>& detail_status) { + EXPECT_EQ(status.proto_status(), ACTION_APPLIED); + }); + + fallback_handler.CheckAndFallbackRequiredFields(OkClientStatus(), + std::move(callback)); } } // namespace diff --git a/chromium/components/autofill_assistant/browser/actions/generate_password_for_form_field_action_unittest.cc b/chromium/components/autofill_assistant/browser/actions/generate_password_for_form_field_action_unittest.cc index dacb0c3fc80..df974c8433e 100644 --- a/chromium/components/autofill_assistant/browser/actions/generate_password_for_form_field_action_unittest.cc +++ b/chromium/components/autofill_assistant/browser/actions/generate_password_for_form_field_action_unittest.cc @@ -62,9 +62,8 @@ class GeneratePasswordForFormFieldActionTest : public testing::Test { TEST_F(GeneratePasswordForFormFieldActionTest, GeneratedPassword) { GeneratePasswordForFormFieldProto* generate_password_proto = proto_.mutable_generate_password_for_form_field(); - generate_password_proto->mutable_element()->add_selectors(kFakeSelector); - generate_password_proto->mutable_element()->set_visibility_requirement( - MUST_BE_VISIBLE); + *generate_password_proto->mutable_element() = + Selector({kFakeSelector}).MustBeVisible().proto; generate_password_proto->set_memory_key(kMemoryKeyForGeneratedPassword); Selector fake_selector = Selector({kFakeSelector}).MustBeVisible(); diff --git a/chromium/components/autofill_assistant/browser/actions/mock_action_delegate.h b/chromium/components/autofill_assistant/browser/actions/mock_action_delegate.h index 22d50591d0d..808d29d18a7 100644 --- a/chromium/components/autofill_assistant/browser/actions/mock_action_delegate.h +++ b/chromium/components/autofill_assistant/browser/actions/mock_action_delegate.h @@ -135,26 +135,18 @@ class MockActionDelegate : public ActionDelegate { MOCK_METHOD1( WriteUserData, void(base::OnceCallback<void(UserData*, UserData::FieldChange*)>)); - MOCK_METHOD1(WriteUserModel, void(base::OnceCallback<void(UserModel*)>)); - - MOCK_METHOD1(OnGetFullCard, - void(base::OnceCallback<void(const autofill::CreditCard& card, - const base::string16& cvc)>&)); - - void GetFullCard(GetFullCardCallback callback) override { - // A local variable is necessary to allow OnGetFullCard to get a reference. - base::OnceCallback<void(const autofill::CreditCard& card, - const base::string16& cvc)> - transformed_callback = base::BindOnce( - [](GetFullCardCallback real_callback, - const autofill::CreditCard& card, const base::string16& cvc) { - std::move(real_callback) - .Run(std::make_unique<autofill::CreditCard>(card), cvc); - }, - std::move(callback)); - OnGetFullCard(transformed_callback); + + void GetFullCard(const autofill::CreditCard* credit_card, + ActionDelegate::GetFullCardCallback callback) override { + OnGetFullCard(credit_card, callback); } + MOCK_METHOD2( + OnGetFullCard, + void(const autofill::CreditCard* credit_card, + base::OnceCallback<void(std::unique_ptr<autofill::CreditCard> card, + const base::string16& cvc)>& callback)); + void GetFieldValue(const Selector& selector, base::OnceCallback<void(const ClientStatus&, const std::string&)> callback) { @@ -238,7 +230,11 @@ class MockActionDelegate : public ActionDelegate { MOCK_METHOD1(SetInfoBox, void(const InfoBox& info_box)); MOCK_METHOD0(ClearInfoBox, void()); MOCK_METHOD1(SetProgress, void(int progress)); + MOCK_METHOD1(SetProgressActiveStep, void(int active_step)); MOCK_METHOD1(SetProgressVisible, void(bool visible)); + MOCK_METHOD1(SetStepProgressBarConfiguration, + void(const ShowProgressBarProto::StepProgressBarConfiguration& + configuration)); MOCK_METHOD1(SetUserActions, void(std::unique_ptr<std::vector<UserAction>> user_action)); MOCK_METHOD1(SetViewportMode, void(ViewportMode mode)); @@ -294,19 +290,20 @@ class MockActionDelegate : public ActionDelegate { MOCK_METHOD0(RequireUI, void()); MOCK_METHOD0(SetExpandSheetForPromptAction, bool()); - MOCK_METHOD2( + MOCK_METHOD3( OnSetGenericUi, void(std::unique_ptr<GenericUserInterfaceProto> generic_ui, - base::OnceCallback<void(bool, - ProcessedActionStatusProto, - const UserModel*)>& end_action_callback)); + base::OnceCallback<void(const ClientStatus&)>& end_action_callback, + base::OnceCallback<void(const ClientStatus&)>& + view_inflation_finished_callback)); void SetGenericUi( std::unique_ptr<GenericUserInterfaceProto> generic_ui, - base::OnceCallback<void(bool, - ProcessedActionStatusProto, - const UserModel*)> end_action_callback) override { - OnSetGenericUi(std::move(generic_ui), end_action_callback); + base::OnceCallback<void(const ClientStatus&)> end_action_callback, + base::OnceCallback<void(const ClientStatus&)> + view_inflation_finished_callback) override { + OnSetGenericUi(std::move(generic_ui), end_action_callback, + view_inflation_finished_callback); } MOCK_METHOD0(ClearGenericUi, void()); diff --git a/chromium/components/autofill_assistant/browser/actions/navigate_action.cc b/chromium/components/autofill_assistant/browser/actions/navigate_action.cc index 684c694dd34..21773c5dca2 100644 --- a/chromium/components/autofill_assistant/browser/actions/navigate_action.cc +++ b/chromium/components/autofill_assistant/browser/actions/navigate_action.cc @@ -26,16 +26,20 @@ void NavigateAction::InternalProcessAction(ProcessActionCallback callback) { // We know to expect navigation to happen, since we're about to cause it. This // allows scripts to put wait_for_navigation just after navigate, if needed, // without having to add an expect_navigation first. - delegate_->ExpectNavigation(); + // The navigations are not declared as renderer initiated, they can cause + // failures. Setting the navigation to expected will prevent this from + // happening. auto& proto = proto_.navigate(); if (!proto.url().empty()) { + delegate_->ExpectNavigation(); GURL url(proto_.navigate().url()); delegate_->LoadURL(url); UpdateProcessedAction(ACTION_APPLIED); } else if (proto.go_backward()) { auto& controller = delegate_->GetWebContents()->GetController(); if (controller.CanGoBack()) { + delegate_->ExpectNavigation(); controller.GoBack(); UpdateProcessedAction(ACTION_APPLIED); } else { @@ -44,6 +48,7 @@ void NavigateAction::InternalProcessAction(ProcessActionCallback callback) { } else if (proto.go_forward()) { auto& controller = delegate_->GetWebContents()->GetController(); if (controller.CanGoForward()) { + delegate_->ExpectNavigation(); controller.GoForward(); UpdateProcessedAction(ACTION_APPLIED); } else { diff --git a/chromium/components/autofill_assistant/browser/actions/prompt_action.cc b/chromium/components/autofill_assistant/browser/actions/prompt_action.cc index c6f3fde6429..9f1cd9cd746 100644 --- a/chromium/components/autofill_assistant/browser/actions/prompt_action.cc +++ b/chromium/components/autofill_assistant/browser/actions/prompt_action.cc @@ -221,8 +221,8 @@ void PromptAction::OnSuggestionChosen(int choice_index) { } DCHECK(choice_index >= 0 && choice_index <= proto_.prompt().choices_size()); - *processed_action_proto_->mutable_prompt_choice() = - proto_.prompt().choices(choice_index); + processed_action_proto_->mutable_prompt_choice()->set_server_payload( + proto_.prompt().choices(choice_index).server_payload()); EndAction(ClientStatus(ACTION_APPLIED)); } diff --git a/chromium/components/autofill_assistant/browser/actions/prompt_action_unittest.cc b/chromium/components/autofill_assistant/browser/actions/prompt_action_unittest.cc index d5455c5bdce..afb7d7a6336 100644 --- a/chromium/components/autofill_assistant/browser/actions/prompt_action_unittest.cc +++ b/chromium/components/autofill_assistant/browser/actions/prompt_action_unittest.cc @@ -172,9 +172,9 @@ TEST_F(PromptActionTest, SelectButtons) { Run(Pointee(AllOf( Property(&ProcessedActionProto::status, ACTION_APPLIED), Property(&ProcessedActionProto::prompt_choice, - Property(&PromptProto::Choice::navigation_ended, false)), + Property(&PromptProto::Result::navigation_ended, false)), Property(&ProcessedActionProto::prompt_choice, - Property(&PromptProto::Choice::server_payload, "ok")))))); + Property(&PromptProto::Result::server_payload, "ok")))))); EXPECT_TRUE((*user_actions_)[0].HasCallback()); (*user_actions_)[0].Call(TriggerContext::CreateEmpty()); } @@ -209,7 +209,8 @@ TEST_F(PromptActionTest, ShowOnlyIfElementExists) { ok_proto->mutable_chip()->set_text("Ok"); ok_proto->mutable_chip()->set_type(HIGHLIGHTED_ACTION); ok_proto->set_server_payload("ok"); - ok_proto->mutable_show_only_when()->mutable_match()->add_selectors("element"); + *ok_proto->mutable_show_only_when()->mutable_match() = + ToSelectorProto("element"); PromptAction action(&mock_action_delegate_, proto_); action.ProcessAction(callback_.Get()); @@ -235,7 +236,8 @@ TEST_F(PromptActionTest, DisabledUnlessElementExists) { ok_proto->mutable_chip()->set_type(HIGHLIGHTED_ACTION); ok_proto->set_server_payload("ok"); ok_proto->set_allow_disabling(true); - ok_proto->mutable_show_only_when()->mutable_match()->add_selectors("element"); + *ok_proto->mutable_show_only_when()->mutable_match() = + ToSelectorProto("element"); PromptAction action(&mock_action_delegate_, proto_); action.ProcessAction(callback_.Get()); @@ -259,8 +261,8 @@ TEST_F(PromptActionTest, DisabledUnlessElementExists) { TEST_F(PromptActionTest, AutoSelectWhenElementExists) { auto* choice_proto = prompt_proto_->add_choices(); choice_proto->set_server_payload("auto-select"); - choice_proto->mutable_auto_select_when()->mutable_match()->add_selectors( - "element"); + *choice_proto->mutable_auto_select_when()->mutable_match() = + ToSelectorProto("element"); PromptAction action(&mock_action_delegate_, proto_); action.ProcessAction(callback_.Get()); @@ -275,7 +277,7 @@ TEST_F(PromptActionTest, AutoSelectWhenElementExists) { callback_, Run(Pointee(AllOf(Property(&ProcessedActionProto::status, ACTION_APPLIED), Property(&ProcessedActionProto::prompt_choice, - Property(&PromptProto::Choice::server_payload, + Property(&PromptProto::Result::server_payload, "auto-select")))))); task_env_.FastForwardBy(base::TimeDelta::FromSeconds(1)); } @@ -288,8 +290,8 @@ TEST_F(PromptActionTest, AutoSelectWithButton) { auto* choice_proto = prompt_proto_->add_choices(); choice_proto->set_server_payload("auto-select"); - choice_proto->mutable_auto_select_when()->mutable_match()->add_selectors( - "element"); + *choice_proto->mutable_auto_select_when()->mutable_match() = + ToSelectorProto("element"); PromptAction action(&mock_action_delegate_, proto_); action.ProcessAction(callback_.Get()); @@ -303,7 +305,7 @@ TEST_F(PromptActionTest, AutoSelectWithButton) { callback_, Run(Pointee(AllOf(Property(&ProcessedActionProto::status, ACTION_APPLIED), Property(&ProcessedActionProto::prompt_choice, - Property(&PromptProto::Choice::server_payload, + Property(&PromptProto::Result::server_payload, "auto-select")))))); task_env_.FastForwardBy(base::TimeDelta::FromSeconds(1)); } @@ -401,8 +403,8 @@ TEST_F(PromptActionTest, ForwardInterruptFailure) { prompt_proto_->set_allow_interrupt(true); auto* choice_proto = prompt_proto_->add_choices(); choice_proto->set_server_payload("auto-select"); - choice_proto->mutable_auto_select_when()->mutable_match()->add_selectors( - "element"); + *choice_proto->mutable_auto_select_when()->mutable_match() = + ToSelectorProto("element"); PromptAction action(&mock_action_delegate_, proto_); action.ProcessAction(callback_.Get()); @@ -419,7 +421,7 @@ TEST_F(PromptActionTest, ForwardInterruptFailure) { Pointee(Property(&ProcessedActionProto::status, INTERRUPT_FAILED)), Pointee( Property(&ProcessedActionProto::prompt_choice, - Property(&PromptProto::Choice::server_payload, "")))))); + Property(&PromptProto::Result::server_payload, "")))))); ASSERT_TRUE(fake_wait_for_dom_done_); std::move(fake_wait_for_dom_done_).Run(ClientStatus(INTERRUPT_FAILED)); } @@ -446,7 +448,7 @@ TEST_F(PromptActionTest, EndActionOnNavigation) { Run(Pointee(AllOf( Property(&ProcessedActionProto::status, ACTION_APPLIED), Property(&ProcessedActionProto::prompt_choice, - Property(&PromptProto::Choice::navigation_ended, true)))))); + Property(&PromptProto::Result::navigation_ended, true)))))); action.ProcessAction(callback_.Get()); } diff --git a/chromium/components/autofill_assistant/browser/actions/set_attribute_action.cc b/chromium/components/autofill_assistant/browser/actions/set_attribute_action.cc index a1a8ceb4088..b49213d0b5e 100644 --- a/chromium/components/autofill_assistant/browser/actions/set_attribute_action.cc +++ b/chromium/components/autofill_assistant/browser/actions/set_attribute_action.cc @@ -16,7 +16,6 @@ namespace autofill_assistant { SetAttributeAction::SetAttributeAction(ActionDelegate* delegate, const ActionProto& proto) : Action(delegate, proto) { - DCHECK_GT(proto_.set_attribute().element().selectors_size(), 0); DCHECK_GT(proto_.set_attribute().attribute_size(), 0); } diff --git a/chromium/components/autofill_assistant/browser/actions/set_form_field_value_action.cc b/chromium/components/autofill_assistant/browser/actions/set_form_field_value_action.cc index c628e837b8f..4e2464531d5 100644 --- a/chromium/components/autofill_assistant/browser/actions/set_form_field_value_action.cc +++ b/chromium/components/autofill_assistant/browser/actions/set_form_field_value_action.cc @@ -40,7 +40,6 @@ SetFormFieldValueAction::SetFormFieldValueAction(ActionDelegate* delegate, const ActionProto& proto) : Action(delegate, proto) { DCHECK(proto_.has_set_form_value()); - DCHECK_GT(proto_.set_form_value().element().selectors_size(), 0); DCHECK_GT(proto_.set_form_value().value_size(), 0); } diff --git a/chromium/components/autofill_assistant/browser/actions/set_form_field_value_action_unittest.cc b/chromium/components/autofill_assistant/browser/actions/set_form_field_value_action_unittest.cc index a397b7d5182..d90f43e9cfc 100644 --- a/chromium/components/autofill_assistant/browser/actions/set_form_field_value_action_unittest.cc +++ b/chromium/components/autofill_assistant/browser/actions/set_form_field_value_action_unittest.cc @@ -36,9 +36,8 @@ class SetFormFieldValueActionTest : public testing::Test { public: void SetUp() override { set_form_field_proto_ = proto_.mutable_set_form_value(); - set_form_field_proto_->mutable_element()->add_selectors(kFakeSelector); - set_form_field_proto_->mutable_element()->set_visibility_requirement( - MUST_BE_VISIBLE); + *set_form_field_proto_->mutable_element() = + Selector({kFakeSelector}).MustBeVisible().proto; ON_CALL(mock_action_delegate_, GetUserData) .WillByDefault(Return(&user_data_)); ON_CALL(mock_action_delegate_, WriteUserData) diff --git a/chromium/components/autofill_assistant/browser/actions/show_generic_ui_action.cc b/chromium/components/autofill_assistant/browser/actions/show_generic_ui_action.cc index 1d239707e18..6f66a26ded7 100644 --- a/chromium/components/autofill_assistant/browser/actions/show_generic_ui_action.cc +++ b/chromium/components/autofill_assistant/browser/actions/show_generic_ui_action.cc @@ -53,8 +53,8 @@ void WriteProfilesToUserModel( void WriteLoginOptionsToUserModel( const ShowGenericUiProto::RequestLoginOptions& proto, - std::vector<WebsiteLoginManager::Login> logins, - UserModel* user_model) { + UserModel* user_model, + std::vector<WebsiteLoginManager::Login> logins) { DCHECK(user_model); ValueProto model_value; model_value.set_is_client_side_only(true); @@ -83,7 +83,6 @@ void WriteLoginOptionsToUserModel( } user_model->SetValue(proto.model_identifier(), model_value); } - } // namespace ShowGenericUiAction::ShowGenericUiAction(ActionDelegate* delegate, @@ -107,17 +106,34 @@ void ShowGenericUiAction::InternalProcessAction( /* force_notifications = */ false); if (!temp_model.GetValues(proto_.show_generic_ui().output_model_identifiers()) .has_value()) { - EndAction(false, INVALID_ACTION, nullptr); + EndAction(ClientStatus(INVALID_ACTION)); return; } + for (const auto& element_check : + proto_.show_generic_ui().periodic_element_checks().element_checks()) { + if (element_check.model_identifier().empty()) { + VLOG(1) << "Invalid action: ElementCheck with empty model_identifier"; + EndAction(ClientStatus(INVALID_ACTION)); + return; + } + } delegate_->Prompt(/* user_actions = */ nullptr, /* disable_force_expand_sheet = */ false); delegate_->SetGenericUi( std::make_unique<GenericUserInterfaceProto>( proto_.show_generic_ui().generic_user_interface()), - base::BindOnce(&ShowGenericUiAction::EndAction, + base::BindOnce(&ShowGenericUiAction::OnEndActionInteraction, + weak_ptr_factory_.GetWeakPtr()), + base::BindOnce(&ShowGenericUiAction::OnViewInflationFinished, weak_ptr_factory_.GetWeakPtr())); +} + +void ShowGenericUiAction::OnViewInflationFinished(const ClientStatus& status) { + if (!status.ok()) { + EndAction(status); + return; + } // Note: it is important to write autofill profiles etc. to the model AFTER // the UI has been inflated, otherwise the UI won't get change notifications @@ -133,31 +149,108 @@ void ShowGenericUiAction::InternalProcessAction( }) != login_options.end()) { delegate_->GetWebsiteLoginManager()->GetLoginsForUrl( delegate_->GetWebContents()->GetLastCommittedURL(), - base::BindOnce(&ShowGenericUiAction::OnGetLogins, - weak_ptr_factory_.GetWeakPtr(), - proto_.show_generic_ui().request_login_options())); + base::BindOnce(&WriteLoginOptionsToUserModel, + proto_.show_generic_ui().request_login_options(), + delegate_->GetUserModel())); } else { - delegate_->WriteUserModel(base::BindOnce( - &WriteLoginOptionsToUserModel, + WriteLoginOptionsToUserModel( proto_.show_generic_ui().request_login_options(), - /* logins = */ std::vector<WebsiteLoginManager::Login>())); + delegate_->GetUserModel(), + /* logins = */ std::vector<WebsiteLoginManager::Login>()); } } delegate_->GetPersonalDataManager()->AddObserver(this); OnPersonalDataChanged(); + for (const auto& element_check : + proto_.show_generic_ui().periodic_element_checks().element_checks()) { + preconditions_.emplace_back(std::make_unique<ElementPrecondition>( + element_check.element_condition())); + } + if (std::any_of( + preconditions_.begin(), preconditions_.end(), + [&](const auto& precondition) { return !precondition->empty(); })) { + has_pending_wait_for_dom_ = true; + delegate_->WaitForDom( + base::TimeDelta::Max(), false, + base::BindRepeating(&ShowGenericUiAction::RegisterChecks, + weak_ptr_factory_.GetWeakPtr()), + base::BindOnce(&ShowGenericUiAction::OnDoneWaitForDom, + weak_ptr_factory_.GetWeakPtr())); + } } -void ShowGenericUiAction::EndAction(bool view_inflation_successful, - ProcessedActionStatusProto status, - const UserModel* user_model) { +void ShowGenericUiAction::RegisterChecks( + BatchElementChecker* checker, + base::OnceCallback<void(const ClientStatus&)> wait_for_dom_callback) { + if (!callback_) { + // Action is done; checks aren't necessary anymore. + std::move(wait_for_dom_callback).Run(OkClientStatus()); + return; + } + + for (size_t i = 0; i < preconditions_.size(); i++) { + preconditions_[i]->Check( + checker, base::BindOnce(&ShowGenericUiAction::OnPreconditionResult, + weak_ptr_factory_.GetWeakPtr(), i)); + } + // Let WaitForDom know we're still waiting for elements. + checker->AddAllDoneCallback(base::BindOnce( + &ShowGenericUiAction::OnElementChecksDone, weak_ptr_factory_.GetWeakPtr(), + std::move(wait_for_dom_callback))); +} + +void ShowGenericUiAction::OnPreconditionResult( + size_t precondition_index, + const ClientStatus& status, + const std::vector<std::string>& ignored_payloads) { + delegate_->GetUserModel()->SetValue(proto_.show_generic_ui() + .periodic_element_checks() + .element_checks(precondition_index) + .model_identifier(), + SimpleValue(status.ok())); +} + +void ShowGenericUiAction::OnElementChecksDone( + base::OnceCallback<void(const ClientStatus&)> wait_for_dom_callback) { + // Calling wait_for_dom_callback with successful status is a way of asking the + // WaitForDom to end gracefully and call OnDoneWaitForDom with the status. + // Note that it is possible for WaitForDom to decide not to call + // OnDoneWaitForDom, if an interrupt triggers at the same time, so we cannot + // cancel the prompt and choose the suggestion just yet. + if (should_end_action_) { + std::move(wait_for_dom_callback).Run(OkClientStatus()); + return; + } + // Let WaitForDom know we're still waiting for an element. + std::move(wait_for_dom_callback).Run(ClientStatus(ELEMENT_RESOLUTION_FAILED)); +} + +void ShowGenericUiAction::OnDoneWaitForDom(const ClientStatus& status) { + if (!callback_) { + return; + } + EndAction(status); +} + +void ShowGenericUiAction::OnEndActionInteraction(const ClientStatus& status) { + // If WaitForDom was called, we end the action the next time the callback + // is called in order to end WaitForDom gracefully. + if (has_pending_wait_for_dom_) { + should_end_action_ = true; + return; + } + EndAction(status); +} + +void ShowGenericUiAction::EndAction(const ClientStatus& status) { delegate_->ClearGenericUi(); delegate_->CleanUpAfterPrompt(); UpdateProcessedAction(status); - if (view_inflation_successful) { - DCHECK(user_model); + if (status.ok()) { const auto& output_model_identifiers = proto_.show_generic_ui().output_model_identifiers(); - auto values = user_model->GetValues(output_model_identifiers); + auto values = + delegate_->GetUserModel()->GetValues(output_model_identifiers); // This should always be the case since there is no way to erase a value // from the model. DCHECK(values.has_value()); @@ -185,9 +278,9 @@ void ShowGenericUiAction::OnPersonalDataChanged() { profiles->emplace_back( std::make_unique<autofill::AutofillProfile>(*profile)); } - delegate_->WriteUserModel( - base::BindOnce(&WriteProfilesToUserModel, std::move(profiles), - proto_.show_generic_ui().request_profiles())); + WriteProfilesToUserModel(std::move(profiles), + proto_.show_generic_ui().request_profiles(), + delegate_->GetUserModel()); } if (proto_.show_generic_ui().has_request_credit_cards()) { @@ -198,18 +291,10 @@ void ShowGenericUiAction::OnPersonalDataChanged() { credit_cards->emplace_back( std::make_unique<autofill::CreditCard>(*credit_card)); } - delegate_->WriteUserModel( - base::BindOnce(&WriteCreditCardsToUserModel, std::move(credit_cards), - proto_.show_generic_ui().request_credit_cards())); + WriteCreditCardsToUserModel(std::move(credit_cards), + proto_.show_generic_ui().request_credit_cards(), + delegate_->GetUserModel()); } } -void ShowGenericUiAction::OnGetLogins( - const ShowGenericUiProto::RequestLoginOptions& proto, - std::vector<WebsiteLoginManager::Login> logins) { - LOG(ERROR) << "Retrieved " << logins.size() << " logins"; - delegate_->WriteUserModel( - base::BindOnce(&WriteLoginOptionsToUserModel, proto, logins)); -} - } // namespace autofill_assistant diff --git a/chromium/components/autofill_assistant/browser/actions/show_generic_ui_action.h b/chromium/components/autofill_assistant/browser/actions/show_generic_ui_action.h index 091191ade32..5cdca28c679 100644 --- a/chromium/components/autofill_assistant/browser/actions/show_generic_ui_action.h +++ b/chromium/components/autofill_assistant/browser/actions/show_generic_ui_action.h @@ -11,10 +11,10 @@ #include "components/autofill/core/browser/personal_data_manager.h" #include "components/autofill/core/browser/personal_data_manager_observer.h" #include "components/autofill_assistant/browser/actions/action.h" +#include "components/autofill_assistant/browser/element_precondition.h" #include "components/autofill_assistant/browser/website_login_manager.h" namespace autofill_assistant { -class UserModel; // Action to show generic UI in the sheet. class ShowGenericUiAction : public Action, @@ -31,16 +31,28 @@ class ShowGenericUiAction : public Action, // Overrides Action: void InternalProcessAction(ProcessActionCallback callback) override; - void EndAction(bool view_inflation_successful, - ProcessedActionStatusProto status, - const UserModel* user_model); + void RegisterChecks( + BatchElementChecker* checker, + base::OnceCallback<void(const ClientStatus&)> wait_for_dom_callback); + void OnPreconditionResult(size_t choice_index, + const ClientStatus& status, + const std::vector<std::string>& ignored_payloads); + void OnElementChecksDone( + base::OnceCallback<void(const ClientStatus&)> wait_for_dom_callback); + void OnDoneWaitForDom(const ClientStatus& status); + // If there is an active WaitForDom this method ends it before calling + // EndAction, otherwise it just calls EndAction. + void OnEndActionInteraction(const ClientStatus& status); + void EndAction(const ClientStatus& status); + + void OnViewInflationFinished(const ClientStatus& status); // From autofill::PersonalDataManagerObserver. void OnPersonalDataChanged() override; - void OnGetLogins(const ShowGenericUiProto::RequestLoginOptions& proto, - std::vector<WebsiteLoginManager::Login> logins); - + bool has_pending_wait_for_dom_ = false; + bool should_end_action_ = false; + std::vector<std::unique_ptr<ElementPrecondition>> preconditions_; ProcessActionCallback callback_; base::WeakPtrFactory<ShowGenericUiAction> weak_ptr_factory_{this}; }; diff --git a/chromium/components/autofill_assistant/browser/actions/show_generic_ui_action_unittest.cc b/chromium/components/autofill_assistant/browser/actions/show_generic_ui_action_unittest.cc index 8464d22ef0b..d0405c87cff 100644 --- a/chromium/components/autofill_assistant/browser/actions/show_generic_ui_action_unittest.cc +++ b/chromium/components/autofill_assistant/browser/actions/show_generic_ui_action_unittest.cc @@ -43,21 +43,20 @@ class ShowGenericUiActionTest : public content::RenderViewHostTestHarness { void SetUp() override { RenderViewHostTestHarness::SetUp(); - ON_CALL(mock_action_delegate_, OnSetGenericUi(_, _)) - .WillByDefault(Invoke( - [this](std::unique_ptr<GenericUserInterfaceProto> generic_ui, - base::OnceCallback<void(bool, ProcessedActionStatusProto, - const UserModel*)>& - end_action_callback) { - std::move(end_action_callback) - .Run(true, UNKNOWN_ACTION_STATUS, &user_model_); - })); - ON_CALL(mock_action_delegate_, ClearGenericUi()).WillByDefault(Return()); - ON_CALL(mock_action_delegate_, WriteUserModel(_)) + ON_CALL(mock_action_delegate_, OnSetGenericUi(_, _, _)) .WillByDefault( - Invoke([this](base::OnceCallback<void(UserModel*)> write_callback) { - std::move(write_callback).Run(&user_model_); + Invoke([&](std::unique_ptr<GenericUserInterfaceProto> generic_ui, + base::OnceCallback<void(const ClientStatus&)>& + end_action_callback, + base::OnceCallback<void(const ClientStatus&)>& + view_inflation_finished_callback) { + std::move(view_inflation_finished_callback) + .Run(ClientStatus(ACTION_APPLIED)); + std::move(end_action_callback).Run(ClientStatus(ACTION_APPLIED)); })); + ON_CALL(mock_action_delegate_, ClearGenericUi()).WillByDefault(Return()); + ON_CALL(mock_action_delegate_, GetUserModel()) + .WillByDefault(Return(&user_model_)); ON_CALL(mock_action_delegate_, GetPersonalDataManager) .WillByDefault(Return(&mock_personal_data_manager_)); ON_CALL(mock_action_delegate_, GetWebsiteLoginManager) @@ -92,15 +91,16 @@ class ShowGenericUiActionTest : public content::RenderViewHostTestHarness { ShowGenericUiProto proto_; }; -TEST_F(ShowGenericUiActionTest, SmokeTest) { - ON_CALL(mock_action_delegate_, OnSetGenericUi(_, _)) - .WillByDefault(Invoke( - [this]( - std::unique_ptr<GenericUserInterfaceProto> generic_ui, - base::OnceCallback<void(bool, ProcessedActionStatusProto, - const UserModel*)>& end_action_callback) { - std::move(end_action_callback) - .Run(false, INVALID_ACTION, &user_model_); +TEST_F(ShowGenericUiActionTest, FailedViewInflationEndsAction) { + ON_CALL(mock_action_delegate_, OnSetGenericUi(_, _, _)) + .WillByDefault( + Invoke([&](std::unique_ptr<GenericUserInterfaceProto> generic_ui, + base::OnceCallback<void(const ClientStatus&)>& + end_action_callback, + base::OnceCallback<void(const ClientStatus&)>& + view_inflation_finished_callback) { + std::move(view_inflation_finished_callback) + .Run(ClientStatus(INVALID_ACTION)); })); EXPECT_CALL(mock_action_delegate_, ClearGenericUi()).Times(1); @@ -112,19 +112,9 @@ TEST_F(ShowGenericUiActionTest, SmokeTest) { } TEST_F(ShowGenericUiActionTest, GoesIntoPromptState) { - ON_CALL(mock_action_delegate_, OnSetGenericUi(_, _)) - .WillByDefault(Invoke( - [this]( - std::unique_ptr<GenericUserInterfaceProto> generic_ui, - base::OnceCallback<void(bool, ProcessedActionStatusProto, - const UserModel*)>& end_action_callback) { - std::move(end_action_callback) - .Run(true, ACTION_APPLIED, &user_model_); - })); - InSequence seq; EXPECT_CALL(mock_action_delegate_, Prompt(_, _, _, _)).Times(1); - EXPECT_CALL(mock_action_delegate_, OnSetGenericUi(_, _)).Times(1); + EXPECT_CALL(mock_action_delegate_, OnSetGenericUi(_, _, _)).Times(1); EXPECT_CALL(mock_action_delegate_, ClearGenericUi()).Times(1); EXPECT_CALL(mock_action_delegate_, CleanUpAfterPrompt()).Times(1); EXPECT_CALL( @@ -140,16 +130,6 @@ TEST_F(ShowGenericUiActionTest, EmptyOutputModel) { input_value->set_identifier("input"); *input_value->mutable_value() = SimpleValue(std::string("InputValue")); - ON_CALL(mock_action_delegate_, OnSetGenericUi(_, _)) - .WillByDefault(Invoke( - [this]( - std::unique_ptr<GenericUserInterfaceProto> generic_ui, - base::OnceCallback<void(bool, ProcessedActionStatusProto, - const UserModel*)>& end_action_callback) { - std::move(end_action_callback) - .Run(true, ACTION_APPLIED, &user_model_); - })); - EXPECT_CALL(mock_action_delegate_, ClearGenericUi()).Times(1); EXPECT_CALL( callback_, @@ -174,15 +154,17 @@ TEST_F(ShowGenericUiActionTest, NonEmptyOutputModel) { proto_.add_output_model_identifiers("value_2"); - ON_CALL(mock_action_delegate_, OnSetGenericUi(_, _)) - .WillByDefault(Invoke( - [this]( - std::unique_ptr<GenericUserInterfaceProto> generic_ui, - base::OnceCallback<void(bool, ProcessedActionStatusProto, - const UserModel*)>& end_action_callback) { + ON_CALL(mock_action_delegate_, OnSetGenericUi(_, _, _)) + .WillByDefault( + Invoke([this](std::unique_ptr<GenericUserInterfaceProto> generic_ui, + base::OnceCallback<void(const ClientStatus&)>& + end_action_callback, + base::OnceCallback<void(const ClientStatus&)>& + view_inflation_finished_callback) { + std::move(view_inflation_finished_callback) + .Run(ClientStatus(ACTION_APPLIED)); user_model_.SetValue("value_2", SimpleValue(std::string("change"))); - std::move(end_action_callback) - .Run(true, ACTION_APPLIED, &user_model_); + std::move(end_action_callback).Run(ClientStatus(ACTION_APPLIED)); })); EXPECT_CALL(mock_action_delegate_, ClearGenericUi()).Times(1); @@ -215,7 +197,7 @@ TEST_F(ShowGenericUiActionTest, OutputModelNotSubsetOfInputModel) { proto_.add_output_model_identifiers("value_2"); proto_.add_output_model_identifiers("value_3"); - EXPECT_CALL(mock_action_delegate_, OnSetGenericUi(_, _)).Times(0); + EXPECT_CALL(mock_action_delegate_, OnSetGenericUi(_, _, _)).Times(0); EXPECT_CALL(mock_action_delegate_, ClearGenericUi()).Times(1); EXPECT_CALL( callback_, @@ -242,16 +224,6 @@ TEST_F(ShowGenericUiActionTest, ClientOnlyValuesDoNotLeaveDevice) { proto_.add_output_model_identifiers("regular_value"); proto_.add_output_model_identifiers("sensitive_value"); - ON_CALL(mock_action_delegate_, OnSetGenericUi(_, _)) - .WillByDefault(Invoke( - [this]( - std::unique_ptr<GenericUserInterfaceProto> generic_ui, - base::OnceCallback<void(bool, ProcessedActionStatusProto, - const UserModel*)>& end_action_callback) { - std::move(end_action_callback) - .Run(true, ACTION_APPLIED, &user_model_); - })); - EXPECT_CALL( callback_, Run(Pointee(AllOf( @@ -462,5 +434,26 @@ TEST_F(ShowGenericUiActionTest, RequestLogins) { EXPECT_EQ(*user_model_.GetValue("login_options"), expected_value); } +TEST_F(ShowGenericUiActionTest, ElementPreconditionMissesIdentifier) { + auto* element_check = + proto_.mutable_periodic_element_checks()->add_element_checks(); + element_check->mutable_element_condition() + ->mutable_match() + ->add_filters() + ->set_css_selector("selector"); + + EXPECT_CALL(mock_action_delegate_, OnSetGenericUi(_, _, _)).Times(0); + EXPECT_CALL(mock_action_delegate_, ClearGenericUi()).Times(1); + EXPECT_CALL( + callback_, + Run(Pointee(AllOf( + Property(&ProcessedActionProto::status, INVALID_ACTION), + Property(&ProcessedActionProto::show_generic_ui_result, + Property(&ShowGenericUiProto::Result::model, + Property(&ModelProto::values, SizeIs(0)))))))); + + Run(); +} + } // namespace } // namespace autofill_assistant diff --git a/chromium/components/autofill_assistant/browser/actions/show_progress_bar_action.cc b/chromium/components/autofill_assistant/browser/actions/show_progress_bar_action.cc index e23d511cc44..d99c9298f61 100644 --- a/chromium/components/autofill_assistant/browser/actions/show_progress_bar_action.cc +++ b/chromium/components/autofill_assistant/browser/actions/show_progress_bar_action.cc @@ -20,7 +20,7 @@ ShowProgressBarAction::ShowProgressBarAction(ActionDelegate* delegate, DCHECK(proto_.has_show_progress_bar()); } -ShowProgressBarAction::~ShowProgressBarAction() {} +ShowProgressBarAction::~ShowProgressBarAction() = default; void ShowProgressBarAction::InternalProcessAction( ProcessActionCallback callback) { @@ -29,14 +29,49 @@ void ShowProgressBarAction::InternalProcessAction( // use tell instead. delegate_->SetStatusMessage(proto_.show_progress_bar().message()); } - int progress = - base::ClampToRange(proto_.show_progress_bar().progress(), 0, 100); - delegate_->SetProgress(progress); + if (proto_.show_progress_bar().has_hide()) { delegate_->SetProgressVisible(!proto_.show_progress_bar().hide()); } + if (proto_.show_progress_bar().has_step_progress_bar_configuration()) { + const auto& configuration = + proto_.show_progress_bar().step_progress_bar_configuration(); + if (!configuration.step_icons().empty() && + configuration.step_icons().size() < 2) { + EndAction(std::move(callback), INVALID_ACTION); + return; + } + delegate_->SetStepProgressBarConfiguration(configuration); + } + + switch (proto_.show_progress_bar().progress_indicator_case()) { + case ShowProgressBarProto::ProgressIndicatorCase::kProgress: { + int progress = + base::ClampToRange(proto_.show_progress_bar().progress(), 0, 100); + delegate_->SetProgress(progress); + break; + } + case ShowProgressBarProto::ProgressIndicatorCase::kActiveStep: { + int active_step = proto_.show_progress_bar().active_step(); + if (active_step < 0) { + EndAction(std::move(callback), INVALID_ACTION); + return; + } + + delegate_->SetProgressActiveStep(active_step); + break; + } + default: + // Ignore. + break; + } + + EndAction(std::move(callback), ACTION_APPLIED); +} - UpdateProcessedAction(ACTION_APPLIED); +void ShowProgressBarAction::EndAction(ProcessActionCallback callback, + ProcessedActionStatusProto status) { + UpdateProcessedAction(status); std::move(callback).Run(std::move(processed_action_proto_)); } diff --git a/chromium/components/autofill_assistant/browser/actions/show_progress_bar_action.h b/chromium/components/autofill_assistant/browser/actions/show_progress_bar_action.h index e6832adfa31..395fca951b1 100644 --- a/chromium/components/autofill_assistant/browser/actions/show_progress_bar_action.h +++ b/chromium/components/autofill_assistant/browser/actions/show_progress_bar_action.h @@ -8,7 +8,7 @@ #include "components/autofill_assistant/browser/actions/action.h" #include "base/macros.h" -#include "base/memory/weak_ptr.h" +#include "components/autofill_assistant/browser/service.pb.h" namespace autofill_assistant { // An action to show the current progress. @@ -22,6 +22,9 @@ class ShowProgressBarAction : public Action { // Overrides Action: void InternalProcessAction(ProcessActionCallback callback) override; + void EndAction(ProcessActionCallback callback, + ProcessedActionStatusProto status); + DISALLOW_COPY_AND_ASSIGN(ShowProgressBarAction); }; diff --git a/chromium/components/autofill_assistant/browser/actions/show_progress_bar_action_unittest.cc b/chromium/components/autofill_assistant/browser/actions/show_progress_bar_action_unittest.cc new file mode 100644 index 00000000000..571a9c99577 --- /dev/null +++ b/chromium/components/autofill_assistant/browser/actions/show_progress_bar_action_unittest.cc @@ -0,0 +1,166 @@ +// 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/show_progress_bar_action.h" + +#include "base/test/gmock_callback_support.h" +#include "base/test/mock_callback.h" +#include "components/autofill_assistant/browser/actions/mock_action_delegate.h" +#include "components/autofill_assistant/browser/service.pb.h" +#include "components/autofill_assistant/browser/view_layout.pb.h" +#include "testing/gmock/include/gmock/gmock.h" + +namespace autofill_assistant { +namespace { + +using ::testing::_; +using ::testing::Invoke; +using ::testing::Property; +using ::testing::Return; +using ::testing::StrEq; + +class ShowProgressBarActionTest : public testing::Test { + public: + ShowProgressBarActionTest() {} + + void SetUp() override {} + + protected: + void Run() { + ActionProto action_proto; + *action_proto.mutable_show_progress_bar() = proto_; + ShowProgressBarAction action(&mock_action_delegate_, action_proto); + action.ProcessAction(callback_.Get()); + } + + MockActionDelegate mock_action_delegate_; + base::MockCallback<Action::ProcessActionCallback> callback_; + ShowProgressBarProto proto_; +}; + +TEST_F(ShowProgressBarActionTest, EmptyProgressBarDoesNothing) { + EXPECT_CALL(mock_action_delegate_, SetStatusMessage(_)).Times(0); + EXPECT_CALL(mock_action_delegate_, SetProgressVisible(_)).Times(0); + EXPECT_CALL(mock_action_delegate_, SetStepProgressBarConfiguration(_)) + .Times(0); + EXPECT_CALL(mock_action_delegate_, SetProgress(_)).Times(0); + EXPECT_CALL(mock_action_delegate_, SetProgressActiveStep(_)).Times(0); + EXPECT_CALL( + callback_, + Run(Pointee(Property(&ProcessedActionProto::status, ACTION_APPLIED)))); + Run(); +} + +TEST_F(ShowProgressBarActionTest, SpecifiedMessageGetsSet) { + proto_.set_message("Message"); + + EXPECT_CALL(mock_action_delegate_, SetStatusMessage("Message")); + EXPECT_CALL( + callback_, + Run(Pointee(Property(&ProcessedActionProto::status, ACTION_APPLIED)))); + Run(); +} + +TEST_F(ShowProgressBarActionTest, HidesProgressBar) { + proto_.set_hide(true); + + EXPECT_CALL(mock_action_delegate_, SetProgressVisible(false)); + EXPECT_CALL( + callback_, + Run(Pointee(Property(&ProcessedActionProto::status, ACTION_APPLIED)))); + Run(); +} + +TEST_F(ShowProgressBarActionTest, ShowsProgressBar) { + proto_.set_hide(false); + + EXPECT_CALL(mock_action_delegate_, SetProgressVisible(true)); + EXPECT_CALL( + callback_, + Run(Pointee(Property(&ProcessedActionProto::status, ACTION_APPLIED)))); + Run(); +} + +TEST_F(ShowProgressBarActionTest, FewerThanTwoStepsProgressBarFailsAction) { + auto* config = proto_.mutable_step_progress_bar_configuration(); + config->set_use_step_progress_bar(true); + config->add_step_icons()->set_icon( + DrawableProto::PROGRESSBAR_DEFAULT_INITIAL_STEP); + + EXPECT_CALL(mock_action_delegate_, SetStepProgressBarConfiguration(_)) + .Times(0); + EXPECT_CALL( + callback_, + Run(Pointee(Property(&ProcessedActionProto::status, INVALID_ACTION)))); + Run(); +} + +TEST_F(ShowProgressBarActionTest, UpdateStepProgressBarConfiguration) { + auto* config = proto_.mutable_step_progress_bar_configuration(); + config->set_use_step_progress_bar(true); + config->add_step_icons()->set_icon( + DrawableProto::PROGRESSBAR_DEFAULT_INITIAL_STEP); + config->add_step_icons()->set_icon( + DrawableProto::PROGRESSBAR_DEFAULT_FINAL_STEP); + + EXPECT_CALL(mock_action_delegate_, SetStepProgressBarConfiguration(_)); + EXPECT_CALL( + callback_, + Run(Pointee(Property(&ProcessedActionProto::status, ACTION_APPLIED)))); + Run(); +} + +TEST_F(ShowProgressBarActionTest, DeactivateStepProgressBar) { + auto* config = proto_.mutable_step_progress_bar_configuration(); + config->set_use_step_progress_bar(false); + + EXPECT_CALL(mock_action_delegate_, SetStepProgressBarConfiguration(_)); + EXPECT_CALL( + callback_, + Run(Pointee(Property(&ProcessedActionProto::status, ACTION_APPLIED)))); + Run(); +} + +TEST_F(ShowProgressBarActionTest, SetProgress) { + proto_.set_progress(50); + + EXPECT_CALL(mock_action_delegate_, SetProgress(50)); + EXPECT_CALL( + callback_, + Run(Pointee(Property(&ProcessedActionProto::status, ACTION_APPLIED)))); + Run(); +} + +TEST_F(ShowProgressBarActionTest, ClampsProgress) { + proto_.set_progress(150); + + EXPECT_CALL(mock_action_delegate_, SetProgress(100)); + EXPECT_CALL( + callback_, + Run(Pointee(Property(&ProcessedActionProto::status, ACTION_APPLIED)))); + Run(); +} + +TEST_F(ShowProgressBarActionTest, SetActiveStep) { + proto_.set_active_step(2); + + EXPECT_CALL(mock_action_delegate_, SetProgressActiveStep(2)); + EXPECT_CALL( + callback_, + Run(Pointee(Property(&ProcessedActionProto::status, ACTION_APPLIED)))); + Run(); +} + +TEST_F(ShowProgressBarActionTest, OutOfBoundsActiveStepFailsAction) { + proto_.set_active_step(-1); + + EXPECT_CALL(mock_action_delegate_, SetProgressActiveStep(_)).Times(0); + EXPECT_CALL( + callback_, + Run(Pointee(Property(&ProcessedActionProto::status, INVALID_ACTION)))); + Run(); +} + +} // namespace +} // namespace autofill_assistant diff --git a/chromium/components/autofill_assistant/browser/actions/use_address_action.cc b/chromium/components/autofill_assistant/browser/actions/use_address_action.cc index ad2c1078bf2..938cab87309 100644 --- a/chromium/components/autofill_assistant/browser/actions/use_address_action.cc +++ b/chromium/components/autofill_assistant/browser/actions/use_address_action.cc @@ -12,12 +12,13 @@ #include "base/optional.h" #include "base/time/time.h" #include "components/autofill/core/browser/autofill_data_util.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/fallback_handler/fallback_data.h" #include "components/autofill_assistant/browser/actions/fallback_handler/required_field.h" #include "components/autofill_assistant/browser/actions/fallback_handler/required_fields_fallback_handler.h" #include "components/autofill_assistant/browser/client_status.h" +#include "components/autofill_assistant/browser/field_formatter.h" +#include "components/autofill_assistant/browser/user_model.h" +#include "components/autofill_assistant/browser/value_util.h" namespace autofill_assistant { @@ -25,23 +26,6 @@ UseAddressAction::UseAddressAction(ActionDelegate* delegate, const ActionProto& proto) : Action(delegate, proto) { DCHECK(proto.has_use_address()); - prompt_ = proto.use_address().prompt(); - name_ = proto.use_address().name(); - std::vector<RequiredField> required_fields; - for (const auto& required_field_proto : - proto_.use_address().required_fields()) { - if (required_field_proto.value_expression().empty()) { - DVLOG(3) << "No fallback filling information provided, skipping field"; - continue; - } - - required_fields.emplace_back(); - required_fields.back().FromProto(required_field_proto); - } - - required_fields_fallback_handler_ = - std::make_unique<RequiredFieldsFallbackHandler>(required_fields, - delegate); selector_ = Selector(proto.use_address().form_field_element()); selector_.MustBeVisible(); } @@ -65,19 +49,68 @@ void UseAddressAction::InternalProcessAction( } // Ensure data already selected in a previous action. - auto* user_data = delegate_->GetUserData(); - if (!user_data->has_selected_address(name_)) { - auto* error_info = processed_action_proto_->mutable_status_details() - ->mutable_autofill_error_info(); - error_info->set_address_key_requested(name_); - error_info->set_client_memory_address_key_names( - user_data->GetAllAddressKeyNames()); - error_info->set_address_pointee_was_null( - !user_data->has_selected_address(name_) || - !user_data->selected_address(name_)); - EndAction(ClientStatus(PRECONDITION_FAILED)); - return; + switch (proto_.use_address().address_source_case()) { + case UseAddressProto::kName: { + if (proto_.use_address().name().empty()) { + VLOG(1) << "UseAddress failed: |name| specified but empty"; + EndAction(ClientStatus(INVALID_ACTION)); + return; + } + auto* profile = delegate_->GetUserData()->selected_address( + proto_.use_address().name()); + if (profile == nullptr) { + auto* error_info = processed_action_proto_->mutable_status_details() + ->mutable_autofill_error_info(); + error_info->set_address_key_requested(proto_.use_address().name()); + error_info->set_client_memory_address_key_names( + delegate_->GetUserData()->GetAllAddressKeyNames()); + error_info->set_address_pointee_was_null(true); + VLOG(1) << "UseAddress failed: no profile found under " + << proto_.use_address().name(); + EndAction(ClientStatus(PRECONDITION_FAILED)); + return; + } + profile_ = std::make_unique<autofill::AutofillProfile>(*profile); + break; + } + case UseAddressProto::kModelIdentifier: { + if (proto_.use_address().model_identifier().empty()) { + VLOG(1) << "UseAddress failed: |model_identifier| set but empty"; + EndAction(ClientStatus(INVALID_ACTION)); + return; + } + auto profile_value = delegate_->GetUserModel()->GetValue( + proto_.use_address().model_identifier()); + if (!profile_value.has_value()) { + VLOG(1) << "UseAddress failed: " + << proto_.use_address().model_identifier() + << " not found in user model"; + EndAction(ClientStatus(PRECONDITION_FAILED)); + return; + } + if (profile_value->profiles().values().size() != 1) { + VLOG(1) << "UseAddress failed: expected single profile for " + << proto_.use_address().model_identifier() << ", but got " + << *profile_value; + EndAction(ClientStatus(PRECONDITION_FAILED)); + return; + } + auto* profile = delegate_->GetUserModel()->GetProfile( + profile_value->profiles().values(0).guid()); + if (profile == nullptr) { + VLOG(1) << "UseAddress failed: profile not found for guid " + << *profile_value; + EndAction(ClientStatus(PRECONDITION_FAILED)); + return; + } + profile_ = std::make_unique<autofill::AutofillProfile>(*profile); + break; + } + case UseAddressProto::ADDRESS_SOURCE_NOT_SET: + EndAction(ClientStatus(INVALID_ACTION)); + return; } + DCHECK(profile_ != nullptr); FillFormWithData(); } @@ -94,21 +127,12 @@ void UseAddressAction::EndAction( } void UseAddressAction::FillFormWithData() { - if (proto_.use_address().skip_autofill()) { - VLOG(3) << "Retrieving address from client memory under '" << name_ << "'."; - const autofill::AutofillProfile* profile = - delegate_->GetUserData()->selected_address(name_); - DCHECK(profile); - auto fallback_data = CreateFallbackData(*profile); - - required_fields_fallback_handler_->CheckAndFallbackRequiredFields( - OkClientStatus(), std::move(fallback_data), - base::BindOnce(&UseAddressAction::EndAction, - weak_ptr_factory_.GetWeakPtr())); + if (selector_.empty()) { + DCHECK(proto_.use_address().skip_autofill()); + OnWaitForElement(OkClientStatus()); return; } - DCHECK(!selector_.empty()); delegate_->ShortWaitForElement( selector_, base::BindOnce(&UseAddressAction::OnWaitForElement, weak_ptr_factory_.GetWeakPtr())); @@ -116,36 +140,46 @@ void UseAddressAction::FillFormWithData() { void UseAddressAction::OnWaitForElement(const ClientStatus& element_status) { if (!element_status.ok()) { - EndAction(ClientStatus(element_status.proto_status())); + EndAction(element_status); return; } - VLOG(3) << "Retrieving address from client memory under '" << name_ << "'."; - const autofill::AutofillProfile* profile = - delegate_->GetUserData()->selected_address(name_); - DCHECK(profile); - auto fallback_data = CreateFallbackData(*profile); + if (proto_.use_address().skip_autofill()) { + ExecuteFallback(OkClientStatus()); + return; + } - delegate_->FillAddressForm( - profile, selector_, - base::BindOnce(&UseAddressAction::OnFormFilled, - weak_ptr_factory_.GetWeakPtr(), std::move(fallback_data))); + DCHECK(!selector_.empty()); + DCHECK(profile_ != nullptr); + delegate_->FillAddressForm(profile_.get(), selector_, + base::BindOnce(&UseAddressAction::ExecuteFallback, + weak_ptr_factory_.GetWeakPtr())); } -void UseAddressAction::OnFormFilled(std::unique_ptr<FallbackData> fallback_data, - const ClientStatus& status) { - required_fields_fallback_handler_->CheckAndFallbackRequiredFields( - status, std::move(fallback_data), - base::BindOnce(&UseAddressAction::EndAction, - weak_ptr_factory_.GetWeakPtr())); -} +void UseAddressAction::ExecuteFallback(const ClientStatus& status) { + DCHECK(profile_ != nullptr); + std::vector<RequiredField> required_fields; + for (const auto& required_field_proto : + proto_.use_address().required_fields()) { + if (!required_field_proto.has_value_expression()) { + continue; + } + + RequiredField required_field; + required_field.FromProto(required_field_proto); + required_fields.emplace_back(required_field); + } -std::unique_ptr<FallbackData> UseAddressAction::CreateFallbackData( - const autofill::AutofillProfile& profile) { - auto fallback_data = std::make_unique<FallbackData>(); + DCHECK(fallback_handler_ == nullptr); + fallback_handler_ = std::make_unique<RequiredFieldsFallbackHandler>( + required_fields, + field_formatter::CreateAutofillMappings(*profile_, + /* locale = */ "en-US"), + delegate_); - fallback_data->AddFormGroup(profile); - return fallback_data; + fallback_handler_->CheckAndFallbackRequiredFields( + status, base::BindOnce(&UseAddressAction::EndAction, + weak_ptr_factory_.GetWeakPtr())); } } // namespace autofill_assistant diff --git a/chromium/components/autofill_assistant/browser/actions/use_address_action.h b/chromium/components/autofill_assistant/browser/actions/use_address_action.h index 6366e7420fb..01c5c53c705 100644 --- a/chromium/components/autofill_assistant/browser/actions/use_address_action.h +++ b/chromium/components/autofill_assistant/browser/actions/use_address_action.h @@ -13,8 +13,8 @@ #include "base/macros.h" #include "base/memory/weak_ptr.h" #include "base/optional.h" +#include "components/autofill/core/browser/data_model/autofill_profile.h" #include "components/autofill_assistant/browser/actions/action.h" -#include "components/autofill_assistant/browser/actions/fallback_handler/fallback_data.h" #include "components/autofill_assistant/browser/actions/fallback_handler/required_fields_fallback_handler.h" namespace autofill { @@ -38,27 +38,20 @@ class UseAddressAction : public Action { const base::Optional<ClientStatus>& optional_details_status = base::nullopt); - // Fill the form using data in client memory. Return whether filling succeeded - // or not through OnFormFilled. + // Fill the form using |profile_|. Return whether filling succeeded or not + // through OnFormFilled. void FillFormWithData(); void OnWaitForElement(const ClientStatus& element_status); // Called when the address has been filled. - void OnFormFilled(std::unique_ptr<FallbackData> fallback_data, - const ClientStatus& status); + void ExecuteFallback(const ClientStatus& status); - // Create fallback data. - std::unique_ptr<FallbackData> CreateFallbackData( - const autofill::AutofillProfile& profile); - - // Usage of the autofilled address. - std::string name_; - std::string prompt_; + // Note: |fallback_handler_| must be a member, because checking for fallbacks + // is asynchronous and the existence of the handler must be ensured. + std::unique_ptr<RequiredFieldsFallbackHandler> fallback_handler_; + std::unique_ptr<autofill::AutofillProfile> profile_; Selector selector_; - std::unique_ptr<RequiredFieldsFallbackHandler> - required_fields_fallback_handler_; - ProcessActionCallback process_action_callback_; base::WeakPtrFactory<UseAddressAction> weak_ptr_factory_{this}; diff --git a/chromium/components/autofill_assistant/browser/actions/use_address_action_unittest.cc b/chromium/components/autofill_assistant/browser/actions/use_address_action_unittest.cc index 5017c774a1d..3827d03443c 100644 --- a/chromium/components/autofill_assistant/browser/actions/use_address_action_unittest.cc +++ b/chromium/components/autofill_assistant/browser/actions/use_address_action_unittest.cc @@ -7,6 +7,7 @@ #include <utility> #include "base/guid.h" +#include "base/strings/strcat.h" #include "base/strings/string_number_conversions.h" #include "base/strings/utf_string_conversions.h" #include "base/test/gmock_callback_support.h" @@ -16,12 +17,20 @@ #include "components/autofill/core/browser/data_model/autofill_profile.h" #include "components/autofill_assistant/browser/actions/mock_action_delegate.h" #include "components/autofill_assistant/browser/mock_personal_data_manager.h" +#include "components/autofill_assistant/browser/user_model.h" #include "components/autofill_assistant/browser/web/mock_web_controller.h" #include "components/autofill_assistant/browser/web/web_controller_util.h" #include "testing/gmock/include/gmock/gmock.h" namespace autofill_assistant { namespace { +const char kAddressName[] = "billing"; +const char kFakeSelector[] = "#selector"; +const char kFirstName[] = "FirstName"; +const char kLastName[] = "LastName"; +const char kEmail[] = "foobar@gmail.com"; +const char kPhoneNumber[] = "+41791234567"; +const char kModelIdentifier[] = "identifier"; using ::base::test::RunOnceCallback; using ::testing::_; @@ -30,25 +39,31 @@ using ::testing::Expectation; using ::testing::InSequence; using ::testing::Invoke; using ::testing::NotNull; +using ::testing::Pointee; using ::testing::Return; using ::testing::SaveArgPointee; class UseAddressActionTest : public testing::Test { public: void SetUp() override { - // Build two identical autofill profiles. One for the memory, one for the - // mock. - auto autofill_profile = std::make_unique<autofill::AutofillProfile>( - base::GenerateGUID(), autofill::test::kEmptyOrigin); - autofill::test::SetProfileInfo(autofill_profile.get(), kFirstName, "", - kLastName, kEmail, "", "", "", "", "", "", - "", kPhoneNumber); - user_data_.selected_addresses_[kAddressName] = std::move(autofill_profile); - - ON_CALL(mock_personal_data_manager_, GetProfileByGUID) - .WillByDefault(Return(&autofill_profile_)); + autofill::test::SetProfileInfo(&profile_, kFirstName, "", kLastName, kEmail, + "", "", "", "", "", "", "", kPhoneNumber); + // Store copies of |profile_| in |user_data_| and |user_model_|. + user_data_.selected_addresses_[kAddressName] = + std::make_unique<autofill::AutofillProfile>(profile_); + auto profiles = std::make_unique< + std::vector<std::unique_ptr<autofill::AutofillProfile>>>(); + profiles->emplace_back( + std::make_unique<autofill::AutofillProfile>(profile_)); + user_model_.SetAutofillProfiles(std::move(profiles)); + ValueProto profile_value; + profile_value.mutable_profiles()->add_values()->set_guid(profile_.guid()); + user_model_.SetValue(kModelIdentifier, profile_value); + ON_CALL(mock_action_delegate_, GetUserData) .WillByDefault(Return(&user_data_)); + ON_CALL(mock_action_delegate_, GetUserModel) + .WillByDefault(Return(&user_model_)); ON_CALL(mock_action_delegate_, GetPersonalDataManager) .WillByDefault(Return(&mock_personal_data_manager_)); ON_CALL(mock_action_delegate_, RunElementChecks) @@ -60,19 +75,11 @@ class UseAddressActionTest : public testing::Test { } protected: - const char* const kAddressName = "billing"; - const char* const kFakeSelector = "#selector"; - const char* const kSelectionPrompt = "prompt"; - const char* const kFirstName = "FirstName"; - const char* const kLastName = "LastName"; - const char* const kEmail = "foobar@gmail.com"; - const char* const kPhoneNumber = "+41791234567"; - ActionProto CreateUseAddressAction() { ActionProto action; UseAddressProto* use_address = action.mutable_use_address(); use_address->set_name(kAddressName); - use_address->mutable_form_field_element()->add_selectors(kFakeSelector); + *use_address->mutable_form_field_element() = ToSelectorProto(kFakeSelector); return action; } @@ -81,7 +88,7 @@ class UseAddressActionTest : public testing::Test { std::string selector) { auto* required_field = action->mutable_use_address()->add_required_fields(); required_field->set_value_expression(value_expression); - required_field->mutable_element()->add_selectors(selector); + *required_field->mutable_element() = ToSelectorProto(selector); return required_field; } @@ -98,8 +105,9 @@ class UseAddressActionTest : public testing::Test { MockActionDelegate mock_action_delegate_; MockWebController mock_web_controller_; UserData user_data_; - - autofill::AutofillProfile autofill_profile_; + UserModel user_model_; + autofill::AutofillProfile profile_ = {base::GenerateGUID(), + autofill::test::kEmptyOrigin}; }; #if !defined(OS_ANDROID) @@ -111,37 +119,97 @@ TEST_F(UseAddressActionTest, MAYBE_FillManually) { InSequence seq; ActionProto action_proto = CreateUseAddressAction(); - action_proto.mutable_use_address()->set_prompt(kSelectionPrompt); EXPECT_EQ(ProcessedActionStatusProto::MANUAL_FALLBACK, ProcessAction(action_proto)); } -TEST_F(UseAddressActionTest, NoSelectedAddress) { - InSequence seq; +TEST_F(UseAddressActionTest, InvalidActionNoSelectorSet) { + ActionProto action; + action.mutable_use_address(); + EXPECT_EQ(ProcessedActionStatusProto::INVALID_ACTION, ProcessAction(action)); +} - ActionProto action_proto = CreateUseAddressAction(); - action_proto.mutable_use_address()->set_prompt(kSelectionPrompt); +TEST_F(UseAddressActionTest, InvalidActionNameSetButEmpty) { + ActionProto action; + UseAddressProto* use_address = action.mutable_use_address(); + *use_address->mutable_form_field_element() = ToSelectorProto(kFakeSelector); + use_address->set_name(""); + EXPECT_EQ(ProcessedActionStatusProto::INVALID_ACTION, ProcessAction(action)); +} - user_data_.selected_addresses_[kAddressName] = nullptr; +TEST_F(UseAddressActionTest, InvalidActionSkipAutofillWithoutRequiredFields) { + ActionProto action; + UseAddressProto* use_address = action.mutable_use_address(); + use_address->set_name(kAddressName); + use_address->set_skip_autofill(true); + EXPECT_EQ(ProcessedActionStatusProto::INVALID_ACTION, ProcessAction(action)); +} +TEST_F(UseAddressActionTest, PreconditionFailedNoProfileForName) { + ActionProto action; + UseAddressProto* use_address = action.mutable_use_address(); + *use_address->mutable_form_field_element() = ToSelectorProto(kFakeSelector); + use_address->set_name("invalid"); EXPECT_EQ(ProcessedActionStatusProto::PRECONDITION_FAILED, - ProcessAction(action_proto)); + ProcessAction(action)); } -TEST_F(UseAddressActionTest, InvalidActionSkipAutofillWithoutRequiredFields) { +TEST_F(UseAddressActionTest, ResolveProfileByNameSucceeds) { + ON_CALL(mock_action_delegate_, + OnShortWaitForElement(Selector({kFakeSelector}).MustBeVisible(), _)) + .WillByDefault(RunOnceCallback<1>(OkClientStatus())); + ON_CALL(mock_web_controller_, OnGetFieldValue(_, _)) + .WillByDefault(RunOnceCallback<1>(OkClientStatus(), "not empty")); + ActionProto action; UseAddressProto* use_address = action.mutable_use_address(); + *use_address->mutable_form_field_element() = ToSelectorProto(kFakeSelector); use_address->set_name(kAddressName); - use_address->set_skip_autofill(true); + EXPECT_CALL(mock_action_delegate_, + OnFillAddressForm(Pointee(Eq(profile_)), _, _)) + .WillOnce(RunOnceCallback<2>(OkClientStatus())); + EXPECT_EQ(ProcessedActionStatusProto::ACTION_APPLIED, ProcessAction(action)); +} + +TEST_F(UseAddressActionTest, InvalidActionModelIdentifierSetButEmpty) { + ActionProto action; + UseAddressProto* use_address = action.mutable_use_address(); + *use_address->mutable_form_field_element() = ToSelectorProto(kFakeSelector); + use_address->set_model_identifier(""); EXPECT_EQ(ProcessedActionStatusProto::INVALID_ACTION, ProcessAction(action)); } +TEST_F(UseAddressActionTest, PreconditionFailedNoProfileForModelIdentifier) { + ActionProto action; + UseAddressProto* use_address = action.mutable_use_address(); + *use_address->mutable_form_field_element() = ToSelectorProto(kFakeSelector); + use_address->set_model_identifier("invalid"); + EXPECT_EQ(ProcessedActionStatusProto::PRECONDITION_FAILED, + ProcessAction(action)); +} + +TEST_F(UseAddressActionTest, ResolveProfileByModelIdentifierSucceeds) { + ON_CALL(mock_action_delegate_, + OnShortWaitForElement(Selector({kFakeSelector}).MustBeVisible(), _)) + .WillByDefault(RunOnceCallback<1>(OkClientStatus())); + ON_CALL(mock_web_controller_, OnGetFieldValue(_, _)) + .WillByDefault(RunOnceCallback<1>(OkClientStatus(), "not empty")); + + ActionProto action; + UseAddressProto* use_address = action.mutable_use_address(); + *use_address->mutable_form_field_element() = ToSelectorProto(kFakeSelector); + use_address->set_model_identifier(kModelIdentifier); + EXPECT_CALL(mock_action_delegate_, + OnFillAddressForm(Pointee(Eq(profile_)), _, _)) + .WillOnce(RunOnceCallback<2>(OkClientStatus())); + EXPECT_EQ(ProcessedActionStatusProto::ACTION_APPLIED, ProcessAction(action)); +} + TEST_F(UseAddressActionTest, PreconditionFailedPopulatesUnexpectedErrorInfo) { InSequence seq; ActionProto action_proto = CreateUseAddressAction(); - action_proto.mutable_use_address()->set_prompt(kSelectionPrompt); user_data_.selected_addresses_[kAddressName] = nullptr; user_data_.selected_addresses_["one_more"] = nullptr; @@ -185,16 +253,22 @@ TEST_F(UseAddressActionTest, ValidationSucceeds) { ActionProto action_proto = CreateUseAddressAction(); AddRequiredField(&action_proto, - base::NumberToString( - static_cast<int>(autofill::ServerFieldType::NAME_FIRST)), + base::StrCat({"${", + base::NumberToString(static_cast<int>( + autofill::ServerFieldType::NAME_FIRST)), + "}"}), "#first_name"); AddRequiredField(&action_proto, - base::NumberToString( - static_cast<int>(autofill::ServerFieldType::NAME_LAST)), + base::StrCat({"${", + base::NumberToString(static_cast<int>( + autofill::ServerFieldType::NAME_LAST)), + "}"}), "#last_name"); AddRequiredField(&action_proto, - base::NumberToString(static_cast<int>( - autofill::ServerFieldType::EMAIL_ADDRESS)), + base::StrCat({"${", + base::NumberToString(static_cast<int>( + autofill::ServerFieldType::EMAIL_ADDRESS)), + "}"}), "#email"); // Autofill succeeds. @@ -217,16 +291,22 @@ TEST_F(UseAddressActionTest, FallbackFails) { ActionProto action_proto = CreateUseAddressAction(); AddRequiredField(&action_proto, - base::NumberToString( - static_cast<int>(autofill::ServerFieldType::NAME_FIRST)), + base::StrCat({"${", + base::NumberToString(static_cast<int>( + autofill::ServerFieldType::NAME_FIRST)), + "}"}), "#first_name"); AddRequiredField(&action_proto, - base::NumberToString( - static_cast<int>(autofill::ServerFieldType::NAME_LAST)), + base::StrCat({"${", + base::NumberToString(static_cast<int>( + autofill::ServerFieldType::NAME_LAST)), + "}"}), "#last_name"); AddRequiredField(&action_proto, - base::NumberToString(static_cast<int>( - autofill::ServerFieldType::EMAIL_ADDRESS)), + base::StrCat({"${", + base::NumberToString(static_cast<int>( + autofill::ServerFieldType::EMAIL_ADDRESS)), + "}"}), "#email"); // Autofill succeeds. @@ -251,8 +331,23 @@ TEST_F(UseAddressActionTest, FallbackFails) { OnSetFieldValue(Eq(Selector({"#first_name"})), kFirstName, _)) .WillOnce(RunOnceCallback<2>(ClientStatus(OTHER_ACTION_STATUS))); - EXPECT_EQ(ProcessedActionStatusProto::AUTOFILL_INCOMPLETE, - ProcessAction(action_proto)); + ProcessedActionProto processed_action; + EXPECT_CALL(callback_, Run(_)).WillOnce(SaveArgPointee<0>(&processed_action)); + + UseAddressAction action(&mock_action_delegate_, action_proto); + action.ProcessAction(callback_.Get()); + + EXPECT_EQ(processed_action.status(), + ProcessedActionStatusProto::AUTOFILL_INCOMPLETE); + EXPECT_TRUE(processed_action.has_status_details()); + EXPECT_EQ(processed_action.status_details() + .autofill_error_info() + .autofill_field_error_size(), + 1); + EXPECT_EQ(OTHER_ACTION_STATUS, processed_action.status_details() + .autofill_error_info() + .autofill_field_error(0) + .status()); } TEST_F(UseAddressActionTest, FallbackSucceeds) { @@ -261,16 +356,22 @@ TEST_F(UseAddressActionTest, FallbackSucceeds) { ActionProto action_proto = CreateUseAddressAction(); AddRequiredField(&action_proto, - base::NumberToString( - static_cast<int>(autofill::ServerFieldType::NAME_FIRST)), + base::StrCat({"${", + base::NumberToString(static_cast<int>( + autofill::ServerFieldType::NAME_FIRST)), + "}"}), "#first_name"); AddRequiredField(&action_proto, - base::NumberToString( - static_cast<int>(autofill::ServerFieldType::NAME_LAST)), + base::StrCat({"${", + base::NumberToString(static_cast<int>( + autofill::ServerFieldType::NAME_LAST)), + "}"}), "#last_name"); AddRequiredField(&action_proto, - base::NumberToString(static_cast<int>( - autofill::ServerFieldType::EMAIL_ADDRESS)), + base::StrCat({"${", + base::NumberToString(static_cast<int>( + autofill::ServerFieldType::EMAIL_ADDRESS)), + "}"}), "#email"); // Autofill succeeds. @@ -331,8 +432,10 @@ TEST_F(UseAddressActionTest, ActionProto action_proto = CreateUseAddressAction(); AddRequiredField(&action_proto, - base::NumberToString( - static_cast<int>(autofill::ServerFieldType::NAME_FIRST)), + base::StrCat({"${", + base::NumberToString(static_cast<int>( + autofill::ServerFieldType::NAME_FIRST)), + "}"}), "#first_name"); EXPECT_CALL(mock_action_delegate_, @@ -411,8 +514,10 @@ TEST_F(UseAddressActionTest, ForcedFallbackWithKeystrokes) { ActionProto action_proto = CreateUseAddressAction(); auto* name_required = AddRequiredField( &action_proto, - base::NumberToString( - static_cast<int>(autofill::ServerFieldType::NAME_FIRST)), + base::StrCat({"${", + base::NumberToString(static_cast<int>( + autofill::ServerFieldType::NAME_FIRST)), + "}"}), "#first_name"); name_required->set_forced(true); name_required->set_fill_strategy(SIMULATE_KEY_PRESSES); @@ -428,7 +533,8 @@ TEST_F(UseAddressActionTest, ForcedFallbackWithKeystrokes) { ON_CALL(mock_web_controller_, OnGetFieldValue(_, _)) .WillByDefault(RunOnceCallback<1>(OkClientStatus(), "not empty")); - // But we still want the first name filled, with simulated keypresses. + // But we still want the first name filled, with + // simulated keypresses. EXPECT_CALL(mock_action_delegate_, OnSetFieldValue(Selector({"#first_name"}), kFirstName, true, 1000, _)) .WillOnce(RunOnceCallback<4>(OkClientStatus())); @@ -444,11 +550,14 @@ TEST_F(UseAddressActionTest, SkippingAutofill) { ActionProto action_proto; action_proto.mutable_use_address()->set_name(kAddressName); AddRequiredField(&action_proto, - base::NumberToString( - static_cast<int>(autofill::ServerFieldType::NAME_FIRST)), + base::StrCat({"${", + base::NumberToString(static_cast<int>( + autofill::ServerFieldType::NAME_FIRST)), + "}"}), "#first_name"); action_proto.mutable_use_address()->set_skip_autofill(true); + EXPECT_CALL(mock_action_delegate_, OnShortWaitForElement(_, _)).Times(0); EXPECT_CALL(mock_action_delegate_, OnFillAddressForm(_, _, _)).Times(0); // First validation fails. diff --git a/chromium/components/autofill_assistant/browser/actions/use_credit_card_action.cc b/chromium/components/autofill_assistant/browser/actions/use_credit_card_action.cc index 655533eddeb..717f02e68a2 100644 --- a/chromium/components/autofill_assistant/browser/actions/use_credit_card_action.cc +++ b/chromium/components/autofill_assistant/browser/actions/use_credit_card_action.cc @@ -4,12 +4,14 @@ #include "components/autofill_assistant/browser/actions/use_credit_card_action.h" +#include <map> #include <utility> #include <vector> #include "base/bind.h" #include "base/callback.h" #include "base/optional.h" +#include "base/strings/string_number_conversions.h" #include "base/strings/stringprintf.h" #include "base/strings/utf_string_conversions.h" #include "base/time/time.h" @@ -17,10 +19,11 @@ #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/actions/action_delegate.h" -#include "components/autofill_assistant/browser/actions/fallback_handler/fallback_data.h" #include "components/autofill_assistant/browser/actions/fallback_handler/required_field.h" #include "components/autofill_assistant/browser/actions/fallback_handler/required_fields_fallback_handler.h" #include "components/autofill_assistant/browser/client_status.h" +#include "components/autofill_assistant/browser/field_formatter.h" +#include "components/autofill_assistant/browser/user_model.h" namespace autofill_assistant { @@ -28,21 +31,6 @@ UseCreditCardAction::UseCreditCardAction(ActionDelegate* delegate, const ActionProto& proto) : Action(delegate, proto) { DCHECK(proto.has_use_card()); - prompt_ = proto.use_card().prompt(); - std::vector<RequiredField> required_fields; - for (const auto& required_field_proto : proto_.use_card().required_fields()) { - if (required_field_proto.value_expression().empty()) { - DVLOG(3) << "No fallback filling information provided, skipping field"; - continue; - } - - required_fields.emplace_back(); - required_fields.back().FromProto(required_field_proto); - } - - required_fields_fallback_handler_ = - std::make_unique<RequiredFieldsFallbackHandler>(required_fields, - delegate); selector_ = Selector(proto.use_card().form_field_element()); selector_.MustBeVisible(); } @@ -66,11 +54,45 @@ void UseCreditCardAction::InternalProcessAction( } // Ensure data already selected in a previous action. - auto* user_data = delegate_->GetUserData(); - if (user_data->selected_card_.get() == nullptr) { - EndAction(ClientStatus(PRECONDITION_FAILED)); - return; + if (proto_.use_card().has_model_identifier()) { + if (proto_.use_card().model_identifier().empty()) { + VLOG(1) << "UseCreditCard failed: |model_identifier| set but empty"; + EndAction(ClientStatus(INVALID_ACTION)); + return; + } + auto credit_card_value = delegate_->GetUserModel()->GetValue( + proto_.use_card().model_identifier()); + if (!credit_card_value.has_value()) { + VLOG(1) << "UseCreditCard failed: " + << proto_.use_card().model_identifier() + << " not found in user model"; + EndAction(ClientStatus(PRECONDITION_FAILED)); + return; + } + if (credit_card_value->credit_cards().values().size() != 1) { + VLOG(1) << "UseCreditCard failed: expected single card for " + << proto_.use_card().model_identifier() << ", but got " + << *credit_card_value; + } + auto* credit_card = delegate_->GetUserModel()->GetCreditCard( + credit_card_value->credit_cards().values(0).guid()); + if (credit_card == nullptr) { + VLOG(1) << "UseCreditCard failed: card not found for guid " + << *credit_card_value; + EndAction(ClientStatus(PRECONDITION_FAILED)); + return; + } + credit_card_ = std::make_unique<autofill::CreditCard>(*credit_card); + } else { + auto* credit_card = delegate_->GetUserData()->selected_card_.get(); + if (credit_card == nullptr) { + VLOG(1) << "UseCreditCard failed: card not found in user_data"; + EndAction(ClientStatus(PRECONDITION_FAILED)); + return; + } + credit_card_ = std::make_unique<autofill::CreditCard>(*credit_card); } + DCHECK(credit_card_ != nullptr); FillFormWithData(); } @@ -87,13 +109,12 @@ void UseCreditCardAction::EndAction( } void UseCreditCardAction::FillFormWithData() { - if (proto_.use_card().skip_autofill()) { - delegate_->GetFullCard(base::BindOnce(&UseCreditCardAction::OnGetFullCard, - weak_ptr_factory_.GetWeakPtr())); + if (selector_.empty()) { + DCHECK(proto_.use_card().skip_autofill()); + OnWaitForElement(OkClientStatus()); return; } - DCHECK(!selector_.empty()); delegate_->ShortWaitForElement( selector_, base::BindOnce(&UseCreditCardAction::OnWaitForElement, weak_ptr_factory_.GetWeakPtr())); @@ -105,7 +126,9 @@ void UseCreditCardAction::OnWaitForElement(const ClientStatus& element_status) { return; } - delegate_->GetFullCard(base::BindOnce(&UseCreditCardAction::OnGetFullCard, + DCHECK(credit_card_ != nullptr); + delegate_->GetFullCard(credit_card_.get(), + base::BindOnce(&UseCreditCardAction::OnGetFullCard, weak_ptr_factory_.GetWeakPtr())); } @@ -117,49 +140,49 @@ void UseCreditCardAction::OnGetFullCard( return; } - auto fallback_data = CreateFallbackData(cvc, *card); + std::vector<RequiredField> required_fields; + for (const auto& required_field_proto : proto_.use_card().required_fields()) { + if (!required_field_proto.has_value_expression()) { + continue; + } + + RequiredField required_field; + required_field.FromProto(required_field_proto); + required_fields.emplace_back(required_field); + } + + std::map<std::string, std::string> fallback_values = + field_formatter::CreateAutofillMappings(*card, + /* locale = */ "en-US"); + fallback_values.emplace( + base::NumberToString( + static_cast<int>(AutofillFormatProto::CREDIT_CARD_VERIFICATION_CODE)), + base::UTF16ToUTF8(cvc)); + fallback_values.emplace( + base::NumberToString( + static_cast<int>(AutofillFormatProto::CREDIT_CARD_RAW_NUMBER)), + base::UTF16ToUTF8(card->GetRawInfo(autofill::CREDIT_CARD_NUMBER))); + + DCHECK(fallback_handler_ == nullptr); + fallback_handler_ = std::make_unique<RequiredFieldsFallbackHandler>( + required_fields, fallback_values, delegate_); if (proto_.use_card().skip_autofill()) { - required_fields_fallback_handler_->CheckAndFallbackRequiredFields( - OkClientStatus(), std::move(fallback_data), - base::BindOnce(&UseCreditCardAction::EndAction, - weak_ptr_factory_.GetWeakPtr())); + ExecuteFallback(OkClientStatus()); return; } - delegate_->FillCardForm( - std::move(card), cvc, selector_, - base::BindOnce(&UseCreditCardAction::OnFormFilled, - weak_ptr_factory_.GetWeakPtr(), std::move(fallback_data))); + DCHECK(!selector_.empty()); + delegate_->FillCardForm(std::move(card), cvc, selector_, + base::BindOnce(&UseCreditCardAction::ExecuteFallback, + weak_ptr_factory_.GetWeakPtr())); } -void UseCreditCardAction::OnFormFilled( - std::unique_ptr<FallbackData> fallback_data, - const ClientStatus& status) { - required_fields_fallback_handler_->CheckAndFallbackRequiredFields( - status, std::move(fallback_data), - base::BindOnce(&UseCreditCardAction::EndAction, - weak_ptr_factory_.GetWeakPtr())); +void UseCreditCardAction::ExecuteFallback(const ClientStatus& status) { + DCHECK(fallback_handler_ != nullptr); + fallback_handler_->CheckAndFallbackRequiredFields( + status, base::BindOnce(&UseCreditCardAction::EndAction, + weak_ptr_factory_.GetWeakPtr())); } -std::unique_ptr<FallbackData> UseCreditCardAction::CreateFallbackData( - const base::string16& cvc, - const autofill::CreditCard& card) { - auto fallback_data = std::make_unique<FallbackData>(); - - fallback_data->field_values.emplace( - static_cast<int>( - UseCreditCardProto::RequiredField::CREDIT_CARD_VERIFICATION_CODE), - base::UTF16ToUTF8(cvc)); - fallback_data->field_values.emplace( - (int)UseCreditCardProto::RequiredField::CREDIT_CARD_RAW_NUMBER, - base::UTF16ToUTF8(card.GetRawInfo(autofill::CREDIT_CARD_NUMBER))); - fallback_data->field_values.emplace( - static_cast<int>(UseCreditCardProto::RequiredField::CREDIT_CARD_NETWORK), - autofill::data_util::GetPaymentRequestData(card.network()) - .basic_card_issuer_network); - - fallback_data->AddFormGroup(card); - return fallback_data; -} } // namespace autofill_assistant diff --git a/chromium/components/autofill_assistant/browser/actions/use_credit_card_action.h b/chromium/components/autofill_assistant/browser/actions/use_credit_card_action.h index 99191230fec..7bf36ff9eda 100644 --- a/chromium/components/autofill_assistant/browser/actions/use_credit_card_action.h +++ b/chromium/components/autofill_assistant/browser/actions/use_credit_card_action.h @@ -14,7 +14,6 @@ #include "base/memory/weak_ptr.h" #include "base/optional.h" #include "components/autofill_assistant/browser/actions/action.h" -#include "components/autofill_assistant/browser/actions/fallback_handler/fallback_data.h" #include "components/autofill_assistant/browser/actions/fallback_handler/required_fields_fallback_handler.h" namespace autofill { @@ -49,20 +48,14 @@ class UseCreditCardAction : public Action { const base::string16& cvc); // Called when the form credit card has been filled. - void OnFormFilled(std::unique_ptr<FallbackData> fallback_data, - const ClientStatus& status); + void ExecuteFallback(const ClientStatus& status); - // Create fallback data. - std::unique_ptr<FallbackData> CreateFallbackData( - const base::string16& cvc, - const autofill::CreditCard& card); - - std::string prompt_; + // Note: |fallback_handler_| must be a member, because checking for fallbacks + // is asynchronous and the existence of the handler must be ensured. + std::unique_ptr<RequiredFieldsFallbackHandler> fallback_handler_; + std::unique_ptr<autofill::CreditCard> credit_card_; Selector selector_; - std::unique_ptr<RequiredFieldsFallbackHandler> - required_fields_fallback_handler_; - ProcessActionCallback process_action_callback_; base::WeakPtrFactory<UseCreditCardAction> weak_ptr_factory_{this}; diff --git a/chromium/components/autofill_assistant/browser/actions/use_credit_card_action_unittest.cc b/chromium/components/autofill_assistant/browser/actions/use_credit_card_action_unittest.cc index 7d737b8b98b..a81d311d961 100644 --- a/chromium/components/autofill_assistant/browser/actions/use_credit_card_action_unittest.cc +++ b/chromium/components/autofill_assistant/browser/actions/use_credit_card_action_unittest.cc @@ -7,45 +7,63 @@ #include <utility> #include "base/guid.h" +#include "base/strings/strcat.h" #include "base/strings/string_number_conversions.h" #include "base/strings/utf_string_conversions.h" #include "base/test/gmock_callback_support.h" #include "base/test/mock_callback.h" #include "components/autofill/core/browser/autofill_test_utils.h" -#include "components/autofill/core/browser/data_model/autofill_profile.h" #include "components/autofill_assistant/browser/actions/mock_action_delegate.h" #include "components/autofill_assistant/browser/mock_personal_data_manager.h" +#include "components/autofill_assistant/browser/user_model.h" #include "components/autofill_assistant/browser/web/mock_web_controller.h" #include "components/autofill_assistant/browser/web/web_controller_util.h" #include "testing/gmock/include/gmock/gmock.h" namespace autofill_assistant { namespace { +const char kFakeSelector[] = "#selector"; +const char kFakeCvc[] = "123"; +const char kModelIdentifier[] = "identifier"; +const char kCardName[] = "Adam West"; +const char kCardNumber[] = "4111111111111111"; +const char kExpirationMonth[] = "9"; +const char kExpirationYear[] = "2050"; using ::base::test::RunOnceCallback; using ::testing::_; using ::testing::Eq; using ::testing::Expectation; +using ::testing::InSequence; using ::testing::Invoke; using ::testing::NotNull; +using ::testing::Pointee; using ::testing::Return; using ::testing::SaveArgPointee; class UseCreditCardActionTest : public testing::Test { public: void SetUp() override { - // Build two identical autofill profiles. One for the memory, one for the - // mock. - auto autofill_profile = std::make_unique<autofill::AutofillProfile>( - base::GenerateGUID(), autofill::test::kEmptyOrigin); - autofill::test::SetProfileInfo(autofill_profile.get(), kFirstName, "", - kLastName, kEmail, "", "", "", "", "", "", - "", ""); - user_data_.selected_addresses_[kAddressName] = std::move(autofill_profile); - ON_CALL(mock_personal_data_manager_, GetProfileByGUID) - .WillByDefault(Return(&autofill_profile_)); + autofill::test::SetCreditCardInfo(&credit_card_, kCardName, kCardNumber, + kExpirationMonth, kExpirationYear, + /* billing_address_id= */ ""); + + // Store copies of |credit_card_| in |user_data_| and |user_model_|. + user_data_.selected_card_ = + std::make_unique<autofill::CreditCard>(credit_card_); + auto cards = + std::make_unique<std::vector<std::unique_ptr<autofill::CreditCard>>>(); + cards->emplace_back(std::make_unique<autofill::CreditCard>(credit_card_)); + user_model_.SetAutofillCreditCards(std::move(cards)); + ValueProto card_value; + card_value.mutable_credit_cards()->add_values()->set_guid( + credit_card_.guid()); + user_model_.SetValue(kModelIdentifier, card_value); + ON_CALL(mock_action_delegate_, GetUserData) .WillByDefault(Return(&user_data_)); + ON_CALL(mock_action_delegate_, GetUserModel) + .WillByDefault(Return(&user_model_)); ON_CALL(mock_action_delegate_, GetPersonalDataManager) .WillByDefault(Return(&mock_personal_data_manager_)); ON_CALL(mock_action_delegate_, RunElementChecks) @@ -54,20 +72,23 @@ class UseCreditCardActionTest : public testing::Test { })); ON_CALL(mock_action_delegate_, OnShortWaitForElement(_, _)) .WillByDefault(RunOnceCallback<1>(OkClientStatus())); + ON_CALL(mock_action_delegate_, OnGetFullCard) + .WillByDefault(Invoke([](const autofill::CreditCard* credit_card, + base::OnceCallback<void( + std::unique_ptr<autofill::CreditCard> card, + const base::string16& cvc)>& callback) { + std::move(callback).Run( + credit_card ? std::make_unique<autofill::CreditCard>(*credit_card) + : nullptr, + base::UTF8ToUTF16(kFakeCvc)); + })); } protected: - const char* const kAddressName = "billing"; - const char* const kFakeSelector = "#selector"; - const char* const kSelectionPrompt = "prompt"; - const char* const kFirstName = "FirstName"; - const char* const kLastName = "LastName"; - const char* const kEmail = "foobar@gmail.com"; - ActionProto CreateUseCreditCardAction() { ActionProto action; - action.mutable_use_card()->mutable_form_field_element()->add_selectors( - kFakeSelector); + *action.mutable_use_card()->mutable_form_field_element() = + ToSelectorProto(kFakeSelector); return action; } @@ -77,14 +98,14 @@ class UseCreditCardActionTest : public testing::Test { std::string selector) { auto* required_field = action->mutable_use_card()->add_required_fields(); required_field->set_value_expression(value_expression); - required_field->mutable_element()->add_selectors(selector); + *required_field->mutable_element() = ToSelectorProto(selector); return required_field; } ActionProto CreateUseCardAction() { ActionProto action; UseCreditCardProto* use_card = action.mutable_use_card(); - use_card->mutable_form_field_element()->add_selectors(kFakeSelector); + *use_card->mutable_form_field_element() = ToSelectorProto(kFakeSelector); return action; } @@ -101,14 +122,15 @@ class UseCreditCardActionTest : public testing::Test { MockActionDelegate mock_action_delegate_; MockWebController mock_web_controller_; UserData user_data_; - - autofill::AutofillProfile autofill_profile_; + UserModel user_model_; + autofill::CreditCard credit_card_ = {base::GenerateGUID(), + autofill::test::kEmptyOrigin}; }; -TEST_F(UseCreditCardActionTest, FillCreditCardNoCardSelected) { - ActionProto action = CreateUseCreditCardAction(); - EXPECT_EQ(ProcessedActionStatusProto::PRECONDITION_FAILED, - ProcessAction(action)); +TEST_F(UseCreditCardActionTest, InvalidActionNoSelectorSet) { + ActionProto action; + action.mutable_use_card(); + EXPECT_EQ(ProcessedActionStatusProto::INVALID_ACTION, ProcessAction(action)); } TEST_F(UseCreditCardActionTest, @@ -119,16 +141,74 @@ TEST_F(UseCreditCardActionTest, EXPECT_EQ(ProcessedActionStatusProto::INVALID_ACTION, ProcessAction(action)); } +TEST_F(UseCreditCardActionTest, PreconditionFailedNoCreditCardInUserData) { + ActionProto action; + auto* use_card = action.mutable_use_card(); + *use_card->mutable_form_field_element() = ToSelectorProto(kFakeSelector); + user_data_.selected_card_.reset(); + EXPECT_EQ(ProcessedActionStatusProto::PRECONDITION_FAILED, + ProcessAction(action)); +} + +TEST_F(UseCreditCardActionTest, CreditCardInUserDataSucceeds) { + ON_CALL(mock_action_delegate_, + OnShortWaitForElement(Selector({kFakeSelector}).MustBeVisible(), _)) + .WillByDefault(RunOnceCallback<1>(OkClientStatus())); + ON_CALL(mock_web_controller_, OnGetFieldValue(_, _)) + .WillByDefault(RunOnceCallback<1>(OkClientStatus(), "not empty")); + ActionProto action; + auto* use_card = action.mutable_use_card(); + *use_card->mutable_form_field_element() = ToSelectorProto(kFakeSelector); + EXPECT_CALL( + mock_action_delegate_, + OnFillCardForm(Pointee(Eq(credit_card_)), base::UTF8ToUTF16(kFakeCvc), + Selector({kFakeSelector}).MustBeVisible(), _)) + .WillOnce(RunOnceCallback<3>(OkClientStatus())); + EXPECT_EQ(ProcessedActionStatusProto::ACTION_APPLIED, ProcessAction(action)); +} + +TEST_F(UseCreditCardActionTest, InvalidActionModelIdentifierSetButEmpty) { + ActionProto action; + auto* use_card = action.mutable_use_card(); + *use_card->mutable_form_field_element() = ToSelectorProto(kFakeSelector); + use_card->set_model_identifier(""); + EXPECT_EQ(ProcessedActionStatusProto::INVALID_ACTION, ProcessAction(action)); +} + +TEST_F(UseCreditCardActionTest, + PreconditionFailedNoCreditCardForModelIdentifier) { + ActionProto action; + auto* use_card = action.mutable_use_card(); + *use_card->mutable_form_field_element() = ToSelectorProto(kFakeSelector); + use_card->set_model_identifier("invalid"); + EXPECT_EQ(ProcessedActionStatusProto::PRECONDITION_FAILED, + ProcessAction(action)); +} + +TEST_F(UseCreditCardActionTest, CreditCardInUserModelSucceeds) { + ON_CALL(mock_action_delegate_, + OnShortWaitForElement(Selector({kFakeSelector}).MustBeVisible(), _)) + .WillByDefault(RunOnceCallback<1>(OkClientStatus())); + ON_CALL(mock_web_controller_, OnGetFieldValue(_, _)) + .WillByDefault(RunOnceCallback<1>(OkClientStatus(), "not empty")); + ActionProto action; + auto* use_card = action.mutable_use_card(); + *use_card->mutable_form_field_element() = ToSelectorProto(kFakeSelector); + use_card->set_model_identifier(kModelIdentifier); + EXPECT_CALL( + mock_action_delegate_, + OnFillCardForm(Pointee(Eq(credit_card_)), base::UTF8ToUTF16(kFakeCvc), + Selector({kFakeSelector}).MustBeVisible(), _)) + .WillOnce(RunOnceCallback<3>(OkClientStatus())); + EXPECT_EQ(ProcessedActionStatusProto::ACTION_APPLIED, ProcessAction(action)); +} + TEST_F(UseCreditCardActionTest, FillCreditCard) { ActionProto action = CreateUseCreditCardAction(); - autofill::CreditCard credit_card; - user_data_.selected_card_ = - std::make_unique<autofill::CreditCard>(credit_card); - EXPECT_CALL(mock_action_delegate_, OnGetFullCard(_)) - .WillOnce(RunOnceCallback<0>(credit_card, base::UTF8ToUTF16("123"))); + user_data_.selected_card_ = std::make_unique<autofill::CreditCard>(); EXPECT_CALL(mock_action_delegate_, - OnFillCardForm(_, base::UTF8ToUTF16("123"), + OnFillCardForm(_, base::UTF8ToUTF16(kFakeCvc), Selector({kFakeSelector}).MustBeVisible(), _)) .WillOnce(RunOnceCallback<3>(OkClientStatus())); @@ -141,23 +221,18 @@ TEST_F(UseCreditCardActionTest, FillCreditCardRequiredFieldsFilled) { .WillByDefault(RunOnceCallback<1>(OkClientStatus(), "not empty")); ActionProto action = CreateUseCreditCardAction(); - AddRequiredField( - &action, - base::NumberToString(static_cast<int>( - UseCreditCardProto::RequiredField::CREDIT_CARD_VERIFICATION_CODE)), - "#cvc"); + AddRequiredField(&action, + base::NumberToString(static_cast<int>( + AutofillFormatProto::CREDIT_CARD_VERIFICATION_CODE)), + "#cvc"); AddRequiredField(&action, base::NumberToString(static_cast<int>( autofill::ServerFieldType::CREDIT_CARD_EXP_MONTH)), "#expmonth"); - autofill::CreditCard credit_card; - user_data_.selected_card_ = - std::make_unique<autofill::CreditCard>(credit_card); - EXPECT_CALL(mock_action_delegate_, OnGetFullCard(_)) - .WillOnce(RunOnceCallback<0>(credit_card, base::UTF8ToUTF16("123"))); + user_data_.selected_card_ = std::make_unique<autofill::CreditCard>(); EXPECT_CALL(mock_action_delegate_, - OnFillCardForm(_, base::UTF8ToUTF16("123"), + OnFillCardForm(_, base::UTF8ToUTF16(kFakeCvc), Selector({kFakeSelector}).MustBeVisible(), _)) .WillOnce(RunOnceCallback<3>(OkClientStatus())); @@ -171,34 +246,53 @@ TEST_F(UseCreditCardActionTest, FillCreditCardWithFallback) { ActionProto action = CreateUseCreditCardAction(); AddRequiredField( &action, - base::NumberToString(static_cast<int>( - UseCreditCardProto::RequiredField::CREDIT_CARD_VERIFICATION_CODE)), + base::StrCat({"${", + base::NumberToString(static_cast<int>( + AutofillFormatProto::CREDIT_CARD_VERIFICATION_CODE)), + "}"}), "#cvc"); - AddRequiredField(&action, - base::NumberToString(static_cast<int>( - autofill::ServerFieldType::CREDIT_CARD_EXP_MONTH)), - "#expmonth"); AddRequiredField( &action, - base::NumberToString(static_cast<int>( - autofill::ServerFieldType::CREDIT_CARD_EXP_2_DIGIT_YEAR)), + base::StrCat({"${", + base::NumberToString(static_cast<int>( + autofill::ServerFieldType::CREDIT_CARD_EXP_MONTH)), + "}"}), + "#expmonth"); + AddRequiredField( + &action, + base::StrCat( + {"${", + base::NumberToString(static_cast<int>( + autofill::ServerFieldType::CREDIT_CARD_EXP_2_DIGIT_YEAR)), + "}"}), "#expyear2"); AddRequiredField( &action, - base::NumberToString(static_cast<int>( - autofill::ServerFieldType::CREDIT_CARD_EXP_4_DIGIT_YEAR)), + base::StrCat( + {"${", + base::NumberToString(static_cast<int>( + autofill::ServerFieldType::CREDIT_CARD_EXP_4_DIGIT_YEAR)), + "}"}), "#expyear4"); + AddRequiredField( + &action, + base::StrCat({"${", + base::NumberToString(static_cast<int>( + autofill::ServerFieldType::CREDIT_CARD_NAME_FULL)), + "}"}), + "#card_name"); + AddRequiredField( + &action, + base::StrCat({"${", + base::NumberToString(static_cast<int>( + autofill::ServerFieldType::CREDIT_CARD_NUMBER)), + "}"}), + "#card_number"); AddRequiredField(&action, - base::NumberToString(static_cast<int>( - autofill::ServerFieldType::CREDIT_CARD_NAME_FULL)), - "#card_name"); - AddRequiredField(&action, - base::NumberToString(static_cast<int>( - autofill::ServerFieldType::CREDIT_CARD_NUMBER)), - "#card_number"); - AddRequiredField(&action, - base::NumberToString(static_cast<int>( - UseCreditCardProto::RequiredField::CREDIT_CARD_NETWORK)), + base::StrCat({"${", + base::NumberToString(static_cast<int>( + AutofillFormatProto::CREDIT_CARD_NETWORK)), + "}"}), "#network"); // First validation fails. @@ -222,7 +316,7 @@ TEST_F(UseCreditCardActionTest, FillCreditCardWithFallback) { // Expect fields to be filled Expectation set_cvc = EXPECT_CALL(mock_action_delegate_, - OnSetFieldValue(Selector({"#cvc"}), "123", _)) + OnSetFieldValue(Selector({"#cvc"}), kFakeCvc, _)) .WillOnce(RunOnceCallback<2>(OkClientStatus())); Expectation set_expmonth = EXPECT_CALL(mock_action_delegate_, @@ -230,15 +324,15 @@ TEST_F(UseCreditCardActionTest, FillCreditCardWithFallback) { .WillOnce(RunOnceCallback<2>(OkClientStatus())); Expectation set_expyear2 = EXPECT_CALL(mock_action_delegate_, - OnSetFieldValue(Selector({"#expyear2"}), "24", _)) + OnSetFieldValue(Selector({"#expyear2"}), "50", _)) .WillOnce(RunOnceCallback<2>(OkClientStatus())); Expectation set_expyear4 = EXPECT_CALL(mock_action_delegate_, - OnSetFieldValue(Selector({"#expyear4"}), "2024", _)) + OnSetFieldValue(Selector({"#expyear4"}), "2050", _)) .WillOnce(RunOnceCallback<2>(OkClientStatus())); Expectation set_cardholder_name = EXPECT_CALL(mock_action_delegate_, - OnSetFieldValue(Selector({"#card_name"}), "Jon Doe", _)) + OnSetFieldValue(Selector({"#card_name"}), "Adam West", _)) .WillOnce(RunOnceCallback<2>(OkClientStatus())); Expectation set_card_number = EXPECT_CALL( @@ -275,19 +369,8 @@ TEST_F(UseCreditCardActionTest, FillCreditCardWithFallback) { .After(set_card_network) .WillOnce(RunOnceCallback<1>(OkClientStatus(), "not empty")); - autofill::CreditCard credit_card; - credit_card.SetExpirationMonth(9); - credit_card.SetExpirationYear(2024); - credit_card.SetRawInfo(autofill::CREDIT_CARD_NAME_FULL, - base::UTF8ToUTF16("Jon Doe")); - credit_card.SetRawInfo(autofill::CREDIT_CARD_NUMBER, - base::UTF8ToUTF16("4111111111111111")); - user_data_.selected_card_ = - std::make_unique<autofill::CreditCard>(credit_card); - EXPECT_CALL(mock_action_delegate_, OnGetFullCard(_)) - .WillOnce(RunOnceCallback<0>(credit_card, base::UTF8ToUTF16("123"))); EXPECT_CALL(mock_action_delegate_, - OnFillCardForm(_, base::UTF8ToUTF16("123"), + OnFillCardForm(_, base::UTF8ToUTF16(kFakeCvc), Selector({kFakeSelector}).MustBeVisible(), _)) .WillOnce(RunOnceCallback<3>(OkClientStatus())); @@ -301,8 +384,10 @@ TEST_F(UseCreditCardActionTest, ForcedFallbackWithKeystrokes) { ActionProto action = CreateUseCreditCardAction(); auto* cvc_required = AddRequiredField( &action, - base::NumberToString(static_cast<int>( - UseCreditCardProto::RequiredField::CREDIT_CARD_VERIFICATION_CODE)), + base::StrCat({"${", + base::NumberToString(static_cast<int>( + AutofillFormatProto::CREDIT_CARD_VERIFICATION_CODE)), + "}"}), "#cvc"); cvc_required->set_forced(true); cvc_required->set_fill_strategy(SIMULATE_KEY_PRESSES); @@ -314,16 +399,12 @@ TEST_F(UseCreditCardActionTest, ForcedFallbackWithKeystrokes) { // But we still want the CVC filled, with simulated keypresses. EXPECT_CALL(mock_action_delegate_, - OnSetFieldValue(Selector({"#cvc"}), "123", true, 1000, _)) + OnSetFieldValue(Selector({"#cvc"}), kFakeCvc, true, 1000, _)) .WillOnce(RunOnceCallback<4>(OkClientStatus())); - autofill::CreditCard credit_card; - user_data_.selected_card_ = - std::make_unique<autofill::CreditCard>(credit_card); - EXPECT_CALL(mock_action_delegate_, OnGetFullCard(_)) - .WillOnce(RunOnceCallback<0>(credit_card, base::UTF8ToUTF16("123"))); + user_data_.selected_card_ = std::make_unique<autofill::CreditCard>(); EXPECT_CALL(mock_action_delegate_, - OnFillCardForm(_, base::UTF8ToUTF16("123"), + OnFillCardForm(_, base::UTF8ToUTF16(kFakeCvc), Selector({kFakeSelector}).MustBeVisible(), _)) .WillOnce(RunOnceCallback<3>(OkClientStatus())); @@ -334,19 +415,17 @@ TEST_F(UseCreditCardActionTest, SkippingAutofill) { ON_CALL(mock_action_delegate_, GetElementTag(_, _)) .WillByDefault(RunOnceCallback<1>(OkClientStatus(), "INPUT")); - ActionProto action = CreateUseCreditCardAction(); + ActionProto action; AddRequiredField( &action, - base::NumberToString(static_cast<int>( - UseCreditCardProto::RequiredField::CREDIT_CARD_VERIFICATION_CODE)), + base::StrCat({"${", + base::NumberToString(static_cast<int>( + AutofillFormatProto::CREDIT_CARD_VERIFICATION_CODE)), + "}"}), "#cvc"); action.mutable_use_card()->set_skip_autofill(true); - autofill::CreditCard credit_card; - user_data_.selected_card_ = - std::make_unique<autofill::CreditCard>(credit_card); - EXPECT_CALL(mock_action_delegate_, OnGetFullCard(_)) - .WillOnce(RunOnceCallback<0>(credit_card, base::UTF8ToUTF16("123"))); + EXPECT_CALL(mock_action_delegate_, OnShortWaitForElement(_, _)).Times(0); EXPECT_CALL(mock_action_delegate_, OnFillCardForm(_, _, _, _)).Times(0); // First validation fails. @@ -355,7 +434,7 @@ TEST_F(UseCreditCardActionTest, SkippingAutofill) { // Fill cvc. Expectation set_cvc = EXPECT_CALL(mock_action_delegate_, - OnSetFieldValue(Selector({"#cvc"}), "123", _)) + OnSetFieldValue(Selector({"#cvc"}), kFakeCvc, _)) .WillOnce(RunOnceCallback<2>(OkClientStatus())); // Second validation succeeds. EXPECT_CALL(mock_web_controller_, OnGetFieldValue(Selector({"#cvc"}), _)) @@ -368,13 +447,9 @@ TEST_F(UseCreditCardActionTest, SkippingAutofill) { TEST_F(UseCreditCardActionTest, AutofillFailureWithoutRequiredFieldsIsFatal) { ActionProto action_proto = CreateUseCreditCardAction(); - autofill::CreditCard credit_card; - user_data_.selected_card_ = - std::make_unique<autofill::CreditCard>(credit_card); - EXPECT_CALL(mock_action_delegate_, OnGetFullCard(_)) - .WillOnce(RunOnceCallback<0>(credit_card, base::UTF8ToUTF16("123"))); + user_data_.selected_card_ = std::make_unique<autofill::CreditCard>(); EXPECT_CALL(mock_action_delegate_, - OnFillCardForm(_, base::UTF8ToUTF16("123"), + OnFillCardForm(_, base::UTF8ToUTF16(kFakeCvc), Selector({kFakeSelector}).MustBeVisible(), _)) .WillOnce(RunOnceCallback<3>(ClientStatus(OTHER_ACTION_STATUS))); @@ -397,17 +472,15 @@ TEST_F(UseCreditCardActionTest, ActionProto action_proto = CreateUseCreditCardAction(); AddRequiredField( &action_proto, - base::NumberToString(static_cast<int>( - UseCreditCardProto::RequiredField::CREDIT_CARD_VERIFICATION_CODE)), + base::StrCat({"${", + base::NumberToString(static_cast<int>( + AutofillFormatProto::CREDIT_CARD_VERIFICATION_CODE)), + "}"}), "#cvc"); - autofill::CreditCard credit_card; - user_data_.selected_card_ = - std::make_unique<autofill::CreditCard>(credit_card); - EXPECT_CALL(mock_action_delegate_, OnGetFullCard(_)) - .WillOnce(RunOnceCallback<0>(credit_card, base::UTF8ToUTF16("123"))); + user_data_.selected_card_ = std::make_unique<autofill::CreditCard>(); EXPECT_CALL(mock_action_delegate_, - OnFillCardForm(_, base::UTF8ToUTF16("123"), + OnFillCardForm(_, base::UTF8ToUTF16(kFakeCvc), Selector({kFakeSelector}).MustBeVisible(), _)) .WillOnce(RunOnceCallback<3>( FillAutofillErrorStatus(ClientStatus(OTHER_ACTION_STATUS)))); @@ -418,7 +491,7 @@ TEST_F(UseCreditCardActionTest, // Fill CVC. Expectation set_cvc = EXPECT_CALL(mock_action_delegate_, - OnSetFieldValue(Selector({"#cvc"}), "123", _)) + OnSetFieldValue(Selector({"#cvc"}), kFakeCvc, _)) .WillOnce(RunOnceCallback<2>(OkClientStatus())); // Second validation succeeds. EXPECT_CALL(mock_web_controller_, OnGetFieldValue(Selector({"#cvc"}), _)) @@ -447,19 +520,8 @@ TEST_F(UseCreditCardActionTest, FallbackForCardExpirationSucceeds) { AddRequiredField(&action_proto, "${53} - ${55}", "#expiration_date"); // Autofill succeeds. - autofill::CreditCard credit_card; - credit_card.SetExpirationMonth(9); - credit_card.SetExpirationYear(2050); - credit_card.SetRawInfo(autofill::CREDIT_CARD_NAME_FULL, - base::UTF8ToUTF16("Jon Doe")); - credit_card.SetRawInfo(autofill::CREDIT_CARD_NUMBER, - base::UTF8ToUTF16("4111111111111111")); - user_data_.selected_card_ = - std::make_unique<autofill::CreditCard>(credit_card); - EXPECT_CALL(mock_action_delegate_, OnGetFullCard(_)) - .WillOnce(RunOnceCallback<0>(credit_card, base::UTF8ToUTF16("123"))); EXPECT_CALL(mock_action_delegate_, - OnFillCardForm(_, base::UTF8ToUTF16("123"), + OnFillCardForm(_, base::UTF8ToUTF16(kFakeCvc), Selector({kFakeSelector}).MustBeVisible(), _)) .WillOnce(RunOnceCallback<3>(OkClientStatus())); @@ -489,22 +551,11 @@ TEST_F(UseCreditCardActionTest, FallbackFails) { .WillByDefault(RunOnceCallback<1>(OkClientStatus(), "INPUT")); ActionProto action_proto = CreateUseCreditCardAction(); - AddRequiredField(&action_proto, "57", "#expiration_date"); + AddRequiredField(&action_proto, "${57}", "#expiration_date"); // Autofill succeeds. - autofill::CreditCard credit_card; - credit_card.SetExpirationMonth(9); - credit_card.SetExpirationYear(2050); - credit_card.SetRawInfo(autofill::CREDIT_CARD_NAME_FULL, - base::UTF8ToUTF16("Jon Doe")); - credit_card.SetRawInfo(autofill::CREDIT_CARD_NUMBER, - base::UTF8ToUTF16("4111111111111111")); - user_data_.selected_card_ = - std::make_unique<autofill::CreditCard>(credit_card); - EXPECT_CALL(mock_action_delegate_, OnGetFullCard(_)) - .WillOnce(RunOnceCallback<0>(credit_card, base::UTF8ToUTF16("123"))); EXPECT_CALL(mock_action_delegate_, - OnFillCardForm(_, base::UTF8ToUTF16("123"), + OnFillCardForm(_, base::UTF8ToUTF16(kFakeCvc), Selector({kFakeSelector}).MustBeVisible(), _)) .WillOnce(RunOnceCallback<3>(OkClientStatus())); @@ -518,8 +569,23 @@ TEST_F(UseCreditCardActionTest, FallbackFails) { OnSetFieldValue(Eq(Selector({"#expiration_date"})), "09/2050", _)) .WillOnce(RunOnceCallback<2>(ClientStatus(OTHER_ACTION_STATUS))); - EXPECT_EQ(ProcessedActionStatusProto::AUTOFILL_INCOMPLETE, - ProcessAction(action_proto)); + ProcessedActionProto processed_action; + EXPECT_CALL(callback_, Run(_)).WillOnce(SaveArgPointee<0>(&processed_action)); + + UseCreditCardAction action(&mock_action_delegate_, action_proto); + action.ProcessAction(callback_.Get()); + + EXPECT_EQ(processed_action.status(), + ProcessedActionStatusProto::AUTOFILL_INCOMPLETE); + EXPECT_TRUE(processed_action.has_status_details()); + EXPECT_EQ(processed_action.status_details() + .autofill_error_info() + .autofill_field_error_size(), + 1); + EXPECT_EQ(OTHER_ACTION_STATUS, processed_action.status_details() + .autofill_error_info() + .autofill_field_error(0) + .status()); } } // namespace diff --git a/chromium/components/autofill_assistant/browser/actions/wait_for_document_action_unittest.cc b/chromium/components/autofill_assistant/browser/actions/wait_for_document_action_unittest.cc index 7cf07a0faad..3ae75c0a587 100644 --- a/chromium/components/autofill_assistant/browser/actions/wait_for_document_action_unittest.cc +++ b/chromium/components/autofill_assistant/browser/actions/wait_for_document_action_unittest.cc @@ -192,7 +192,7 @@ TEST_F(WaitForDocumentActionTest, CheckDocumentInFrame) { .WillOnce(RunOnceCallback<1>(OkClientStatus(), DOCUMENT_COMPLETE)); proto_.set_timeout_ms(0); - proto_.mutable_frame()->add_selectors("#frame"); + *proto_.mutable_frame() = ToSelectorProto("#frame"); Run(); EXPECT_EQ(ACTION_APPLIED, processed_action_.status()); } @@ -203,7 +203,7 @@ TEST_F(WaitForDocumentActionTest, CheckFrameElementNotFound) { RunOnceCallback<1>(ClientStatus(ELEMENT_RESOLUTION_FAILED))); proto_.set_timeout_ms(0); - proto_.mutable_frame()->add_selectors("#frame"); + *proto_.mutable_frame() = ToSelectorProto("#frame"); Run(); EXPECT_EQ(ELEMENT_RESOLUTION_FAILED, processed_action_.status()); } diff --git a/chromium/components/autofill_assistant/browser/actions/wait_for_dom_action_unittest.cc b/chromium/components/autofill_assistant/browser/actions/wait_for_dom_action_unittest.cc index ecedb4531a9..4f496d18a68 100644 --- a/chromium/components/autofill_assistant/browser/actions/wait_for_dom_action_unittest.cc +++ b/chromium/components/autofill_assistant/browser/actions/wait_for_dom_action_unittest.cc @@ -102,7 +102,8 @@ TEST_F(WaitForDomActionTest, ConditionMet) { EXPECT_CALL(mock_web_controller_, OnElementCheck(Selector({"#element"}), _)) .WillRepeatedly(RunOnceCallback<1>(OkClientStatus())); - proto_.mutable_wait_condition()->mutable_match()->add_selectors("#element"); + *proto_.mutable_wait_condition()->mutable_match() = + ToSelectorProto("#element"); EXPECT_CALL( callback_, Run(Pointee(Property(&ProcessedActionProto::status, ACTION_APPLIED)))); @@ -110,7 +111,8 @@ TEST_F(WaitForDomActionTest, ConditionMet) { } TEST_F(WaitForDomActionTest, ConditionNotMet) { - proto_.mutable_wait_condition()->mutable_match()->add_selectors("#element"); + *proto_.mutable_wait_condition()->mutable_match() = + ToSelectorProto("#element"); EXPECT_CALL(callback_, Run(Pointee(Property(&ProcessedActionProto::status, ELEMENT_RESOLUTION_FAILED)))); Run(); @@ -128,25 +130,21 @@ TEST_F(WaitForDomActionTest, ReportMatchesToServer) { auto* any_of = proto_.mutable_wait_condition()->mutable_any_of(); auto* condition1 = any_of->add_conditions(); - condition1->mutable_match()->add_selectors("#element1"); + *condition1->mutable_match() = ToSelectorProto("#element1"); condition1->set_payload("1"); auto* condition2 = any_of->add_conditions(); - condition2->mutable_none_of() - ->add_conditions() - ->mutable_match() - ->add_selectors("#element2"); + *condition2->mutable_none_of()->add_conditions()->mutable_match() = + ToSelectorProto("#element2"); condition2->set_payload("2"); auto* condition3 = any_of->add_conditions(); - condition3->mutable_match()->add_selectors("#element3"); + *condition3->mutable_match() = ToSelectorProto("#element3"); condition3->set_payload("3"); auto* condition4 = any_of->add_conditions(); - condition4->mutable_none_of() - ->add_conditions() - ->mutable_match() - ->add_selectors("#element4"); + *condition4->mutable_none_of()->add_conditions()->mutable_match() = + ToSelectorProto("#element4"); condition4->set_payload("4"); // Condition 1 and 2 are met, conditions 3 and 4 are not. diff --git a/chromium/components/autofill_assistant/browser/basic_interactions.cc b/chromium/components/autofill_assistant/browser/basic_interactions.cc index 149429ede1d..e11808288fb 100644 --- a/chromium/components/autofill_assistant/browser/basic_interactions.cc +++ b/chromium/components/autofill_assistant/browser/basic_interactions.cc @@ -9,6 +9,7 @@ #include "base/strings/string_number_conversions.h" #include "base/strings/utf_string_conversions.h" #include "components/autofill/core/browser/autofill_data_util.h" +#include "components/autofill_assistant/browser/field_formatter.h" #include "components/autofill_assistant/browser/script_executor_delegate.h" #include "components/autofill_assistant/browser/trigger_context.h" #include "components/autofill_assistant/browser/user_model.h" @@ -103,10 +104,8 @@ bool ValueToString(UserModel* user_model, switch (value->kind_case()) { case ValueProto::kUserActions: case ValueProto::kLoginOptions: - case ValueProto::kCreditCards: - case ValueProto::kProfiles: case ValueProto::kCreditCardResponse: - case ValueProto::kLoginOptionResponse: + case ValueProto::kServerPayload: DVLOG(2) << "Error evaluating " << __func__ << ": does not support values of type " << value->kind_case(); return false; @@ -156,12 +155,59 @@ bool ValueToString(UserModel* user_model, time, proto.date_format().date_format().c_str()))); break; } + case ValueProto::kCreditCards: { + if (proto.autofill_format().pattern().empty()) { + DVLOG(2) << "Error evaluating " << __func__ << ": pattern not set"; + return false; + } + auto* credit_card = + user_model->GetCreditCard(value->credit_cards().values(i).guid()); + if (!credit_card) { + DVLOG(2) << "Error evaluating " << __func__ + << ": credit card not found"; + return false; + } + auto formatted_string = field_formatter::FormatString( + proto.autofill_format().pattern(), + field_formatter::CreateAutofillMappings( + *credit_card, proto.autofill_format().locale())); + if (!formatted_string.has_value()) { + DVLOG(2) << "Error evaluating " << __func__ + << ": error formatting pattern '" + << proto.autofill_format().pattern() << "'"; + return false; + } + result.mutable_strings()->add_values(*formatted_string); + break; + } + case ValueProto::kProfiles: { + if (proto.autofill_format().pattern().empty()) { + DVLOG(2) << "Error evaluating " << __func__ << ": pattern not set"; + return false; + } + auto* profile = + user_model->GetProfile(value->profiles().values(i).guid()); + if (!profile) { + DVLOG(2) << "Error evaluating " << __func__ << ": profile not found"; + return false; + } + auto formatted_string = field_formatter::FormatString( + proto.autofill_format().pattern(), + field_formatter::CreateAutofillMappings( + *profile, proto.autofill_format().locale())); + if (!formatted_string.has_value()) { + DVLOG(2) << "Error evaluating " << __func__ + << ": error formatting pattern '" + << proto.autofill_format().pattern() << "'"; + return false; + } + result.mutable_strings()->add_values(*formatted_string); + break; + } case ValueProto::kUserActions: case ValueProto::kLoginOptions: - case ValueProto::kCreditCards: - case ValueProto::kProfiles: case ValueProto::kCreditCardResponse: - case ValueProto::kLoginOptionResponse: + case ValueProto::kServerPayload: case ValueProto::KIND_NOT_SET: NOTREACHED(); return false; @@ -326,8 +372,7 @@ bool CreateLoginOptionResponse(UserModel* user_model, // The result is intentionally not client_side_only, irrespective of input. ValueProto result; - result.mutable_login_option_response()->set_payload( - value->login_options().values(0).payload()); + result.set_server_payload(value->login_options().values(0).payload()); user_model->SetValue(result_model_identifier, result); return true; } @@ -515,28 +560,46 @@ bool BasicInteractions::ToggleUserAction(const ToggleUserActionProto& proto) { return true; } -bool BasicInteractions::EndAction(bool view_inflation_successful, - const EndActionProto& proto) { +bool BasicInteractions::EndAction(const ClientStatus& status) { if (!end_action_callback_) { DVLOG(2) << "Failed to EndAction: no callback set"; return false; } - std::move(end_action_callback_) - .Run(view_inflation_successful, proto.status(), - delegate_->GetUserModel()); + + // It is possible for the action to end before view inflation was finished. + // In that case, the action can end directly and does not need to receive this + // callback. + view_inflation_finished_callback_.Reset(); + std::move(end_action_callback_).Run(status); return true; } -void BasicInteractions::ClearEndActionCallback() { +bool BasicInteractions::NotifyViewInflationFinished( + const ClientStatus& status) { + if (!view_inflation_finished_callback_) { + return false; + } + std::move(view_inflation_finished_callback_).Run(status); + return true; +} + +void BasicInteractions::ClearCallbacks() { end_action_callback_.Reset(); + view_inflation_finished_callback_.Reset(); } void BasicInteractions::SetEndActionCallback( - base::OnceCallback<void(bool, ProcessedActionStatusProto, const UserModel*)> - end_action_callback) { + base::OnceCallback<void(const ClientStatus&)> end_action_callback) { end_action_callback_ = std::move(end_action_callback); } +void BasicInteractions::SetViewInflationFinishedCallback( + base::OnceCallback<void(const ClientStatus&)> + view_inflation_finished_callback) { + view_inflation_finished_callback_ = + std::move(view_inflation_finished_callback); +} + bool BasicInteractions::RunConditionalCallback( const std::string& condition_identifier, base::RepeatingCallback<void()> callback) { @@ -559,36 +622,4 @@ bool BasicInteractions::RunConditionalCallback( return true; } -bool BasicInteractions::UpdateRadioButtonGroup( - const std::vector<std::string>& model_identifiers, - const std::string& selected_model_identifier) { - auto selected_iterator = - std::find(model_identifiers.begin(), model_identifiers.end(), - selected_model_identifier); - if (selected_iterator == model_identifiers.end()) { - return false; - } - - auto values = delegate_->GetUserModel()->GetValues(model_identifiers); - if (!values.has_value()) { - return false; - } - - if (!AreAllValuesOfType(*values, ValueProto::kBooleans)) { - return false; - } - - if (!AreAllValuesOfSize(*values, 1)) { - return false; - } - - for (const auto& model_identifier : model_identifiers) { - if (model_identifier == selected_model_identifier) { - continue; - } - delegate_->GetUserModel()->SetValue(model_identifier, SimpleValue(false)); - } - return true; -} - } // namespace autofill_assistant diff --git a/chromium/components/autofill_assistant/browser/basic_interactions.h b/chromium/components/autofill_assistant/browser/basic_interactions.h index 0a265fad541..69e256e15db 100644 --- a/chromium/components/autofill_assistant/browser/basic_interactions.h +++ b/chromium/components/autofill_assistant/browser/basic_interactions.h @@ -7,11 +7,11 @@ #include "base/bind_helpers.h" #include "base/memory/weak_ptr.h" +#include "components/autofill_assistant/browser/client_status.h" #include "components/autofill_assistant/browser/generic_ui.pb.h" namespace autofill_assistant { class ScriptExecutorDelegate; -class UserModel; // Provides basic interactions for use by the generic UI framework. These // methods are intended to be bound to by the corresponding interaction @@ -43,16 +43,23 @@ class BasicInteractions { bool ToggleUserAction(const ToggleUserActionProto& proto); // Ends the current action. Can only be called during a ShowGenericUiAction. - bool EndAction(bool view_inflation_successful, const EndActionProto& proto); + bool EndAction(const ClientStatus& status); + + // Runs |view_inflation_finished_callback_| to notify its owner that view + // inflation has finished. Can only be called during a ShowGenericUiAction. + bool NotifyViewInflationFinished(const ClientStatus& status); // Sets the callback to end the current ShowGenericUiAction. void SetEndActionCallback( - base::OnceCallback<void(bool, - ProcessedActionStatusProto, - const UserModel*)> end_action_callback); + base::OnceCallback<void(const ClientStatus&)> end_action_callback); + + // Sets the callback to indicate whether view inflation was successful or not. + void SetViewInflationFinishedCallback( + base::OnceCallback<void(const ClientStatus&)> + view_inflation_finished_callback); - // Clears the |end_action_callback_|. - void ClearEndActionCallback(); + // Clears all callbacks associated with the current ShowGenericUi action. + void ClearCallbacks(); // Runs |callback| if |condition_identifier| points to a single boolean set to // 'true'. Returns true on success (i.e., condition was evaluated @@ -60,17 +67,12 @@ class BasicInteractions { bool RunConditionalCallback(const std::string& condition_identifier, base::RepeatingCallback<void()> callback); - // Disables all radio buttons in |model_identifiers| except - // |selected_model_identifier|. Fails if one or more model identifiers are not - // found. - bool UpdateRadioButtonGroup(const std::vector<std::string>& model_identifiers, - const std::string& selected_model_identifier); - private: ScriptExecutorDelegate* delegate_; // Only valid during a ShowGenericUiAction. - base::OnceCallback<void(bool, ProcessedActionStatusProto, const UserModel*)> - end_action_callback_; + base::OnceCallback<void(const ClientStatus&)> end_action_callback_; + base::OnceCallback<void(const ClientStatus&)> + view_inflation_finished_callback_; base::WeakPtrFactory<BasicInteractions> weak_ptr_factory_{this}; }; diff --git a/chromium/components/autofill_assistant/browser/basic_interactions_unittest.cc b/chromium/components/autofill_assistant/browser/basic_interactions_unittest.cc index fb1316eba9e..1eb98bdb3cc 100644 --- a/chromium/components/autofill_assistant/browser/basic_interactions_unittest.cc +++ b/chromium/components/autofill_assistant/browser/basic_interactions_unittest.cc @@ -16,6 +16,7 @@ namespace autofill_assistant { +using ::testing::_; using ::testing::ElementsAre; using ::testing::Eq; using ::testing::InSequence; @@ -219,6 +220,91 @@ TEST_F(BasicInteractionsTest, ComputeValueToString) { /* is_client_side_only = */ true)); } + // Credit cards + autofill::CreditCard credit_card_a(base::GenerateGUID(), + "https://www.example.com"); + autofill::test::SetCreditCardInfo(&credit_card_a, "Marion Mitchell", + "4111 1111 1111 1111", "01", "2050", ""); + autofill::CreditCard credit_card_b(base::GenerateGUID(), + "https://www.example.com"); + autofill::test::SetCreditCardInfo(&credit_card_b, "John Doe", + "4111 1111 1111 2222", "02", "2051", ""); + auto credit_cards = + std::make_unique<std::vector<std::unique_ptr<autofill::CreditCard>>>(); + credit_cards->emplace_back( + std::make_unique<autofill::CreditCard>(credit_card_a)); + credit_cards->emplace_back( + std::make_unique<autofill::CreditCard>(credit_card_b)); + user_model_.SetAutofillCreditCards(std::move(credit_cards)); + ValueProto credit_cards_value; + credit_cards_value.mutable_credit_cards()->add_values()->set_guid( + credit_card_a.guid()); + credit_cards_value.mutable_credit_cards()->add_values()->set_guid( + credit_card_b.guid()); + user_model_.SetValue("value", credit_cards_value); + // Formatting credit cards fails if pattern or locale are not set. + proto.mutable_to_string()->mutable_autofill_format()->set_locale("en-US"); + EXPECT_FALSE(basic_interactions_.ComputeValue(proto)); + // {name} {network} **** {last-4-digits} ({month/year}) + proto.mutable_to_string()->mutable_autofill_format()->set_pattern( + "${51}. ${-5} **** ${-4} (${53}/${54})"); + EXPECT_TRUE(basic_interactions_.ComputeValue(proto)); + ValueProto expected_result; + expected_result.mutable_strings()->add_values( + "Marion Mitchell. Visa **** 1111 (01/50)"); + expected_result.mutable_strings()->add_values( + "John Doe. Visa **** 2222 (02/51)"); + EXPECT_EQ(*user_model_.GetValue("output"), expected_result); + + // Profiles + autofill::AutofillProfile profile_a(base::GenerateGUID(), + "https://www.example.com"); + autofill::test::SetProfileInfo( + &profile_a, "Marion", "Mitchell", "Morrison", "marion@me.xyz", "Fox", + "123 Zoo St.", "unit 5", "Hollywood", "CA", "91601", "US", "16505678910"); + autofill::AutofillProfile profile_b(base::GenerateGUID(), + "https://www.example.com"); + autofill::test::SetProfileInfo(&profile_b, "John", "", "Doe", + "editor@gmail.com", "", "203 Barfield Lane", + "apt A", "Mountain View", "CA", "94043", "US", + "+12345678901"); + auto profiles = std::make_unique< + std::vector<std::unique_ptr<autofill::AutofillProfile>>>(); + profiles->emplace_back( + std::make_unique<autofill::AutofillProfile>(profile_a)); + profiles->emplace_back( + std::make_unique<autofill::AutofillProfile>(profile_b)); + user_model_.SetAutofillProfiles(std::move(profiles)); + ValueProto profiles_value; + profiles_value.mutable_profiles()->add_values()->set_guid(profile_a.guid()); + profiles_value.mutable_profiles()->add_values()->set_guid(profile_b.guid()); + user_model_.SetValue("value", profiles_value); + // Formatting profiles fails if pattern is not set. + EXPECT_FALSE(basic_interactions_.ComputeValue(proto)); + // {name_full}, {address_line_1} {address_line_2} {zip code} {city} {country} + proto.mutable_to_string()->mutable_autofill_format()->set_pattern( + "${7} ${30} ${31} ${35} ${33} ${36}"); + EXPECT_TRUE(basic_interactions_.ComputeValue(proto)); + expected_result.Clear(); + expected_result.mutable_strings()->add_values( + "Marion Mitchell Morrison 123 Zoo St. unit 5 91601 Hollywood United " + "States"); + expected_result.mutable_strings()->add_values( + "John Doe 203 Barfield Lane apt A 94043 Mountain View United States"); + EXPECT_EQ(*user_model_.GetValue("output"), expected_result); + + // Different locale. + proto.mutable_to_string()->mutable_autofill_format()->set_locale("de-DE"); + EXPECT_TRUE(basic_interactions_.ComputeValue(proto)); + expected_result.Clear(); + expected_result.mutable_strings()->add_values( + "Marion Mitchell Morrison 123 Zoo St. unit 5 91601 Hollywood Vereinigte " + "Staaten"); + expected_result.mutable_strings()->add_values( + "John Doe 203 Barfield Lane apt A 94043 Mountain View Vereinigte " + "Staaten"); + EXPECT_EQ(*user_model_.GetValue("output"), expected_result); + // Empty value fails. user_model_.SetValue("value", ValueProto()); EXPECT_FALSE(basic_interactions_.ComputeValue(proto)); @@ -229,7 +315,7 @@ TEST_F(BasicInteractionsTest, ComputeValueToString) { multi_value.mutable_booleans()->add_values(false); user_model_.SetValue("value", multi_value); EXPECT_TRUE(basic_interactions_.ComputeValue(proto)); - ValueProto expected_result; + expected_result.Clear(); expected_result.mutable_strings()->add_values("true"); expected_result.mutable_strings()->add_values("false"); EXPECT_EQ(*user_model_.GetValue("output"), expected_result); @@ -294,20 +380,42 @@ TEST_F(BasicInteractionsTest, ComputeValueIntegerSum) { TEST_F(BasicInteractionsTest, EndActionWithoutCallbackFails) { EndActionProto proto; - EXPECT_FALSE(basic_interactions_.EndAction(true, proto)); + EXPECT_FALSE(basic_interactions_.EndAction(ClientStatus(INVALID_ACTION))); } TEST_F(BasicInteractionsTest, EndActionWithCallbackSucceeds) { - base::MockCallback<base::OnceCallback<void(bool, ProcessedActionStatusProto, - const UserModel*)>> - callback; + base::MockCallback<base::OnceCallback<void(const ClientStatus&)>> callback; basic_interactions_.SetEndActionCallback(callback.Get()); - EndActionProto proto; - proto.set_status(ACTION_APPLIED); + EXPECT_CALL(callback, + Run(Property(&ClientStatus::proto_status, ACTION_APPLIED))); + EXPECT_TRUE(basic_interactions_.EndAction(ClientStatus(ACTION_APPLIED))); +} + +TEST_F(BasicInteractionsTest, NotifyViewInflationFinishedRunsCallback) { + base::MockCallback<base::OnceCallback<void(const ClientStatus&)>> callback; + basic_interactions_.SetViewInflationFinishedCallback(callback.Get()); + + EXPECT_CALL(callback, + Run(Property(&ClientStatus::proto_status, ACTION_APPLIED))); + basic_interactions_.NotifyViewInflationFinished(ClientStatus(ACTION_APPLIED)); +} - EXPECT_CALL(callback, Run(true, ACTION_APPLIED, &user_model_)); - EXPECT_TRUE(basic_interactions_.EndAction(true, proto)); +TEST_F(BasicInteractionsTest, EndActionResetsViewInflationCallback) { + base::MockCallback<base::OnceCallback<void(const ClientStatus&)>> + view_inflation_finished_callback; + base::MockCallback<base::OnceCallback<void(const ClientStatus&)>> + end_action_callback; + basic_interactions_.SetViewInflationFinishedCallback( + view_inflation_finished_callback.Get()); + basic_interactions_.SetEndActionCallback(end_action_callback.Get()); + + EXPECT_CALL(end_action_callback, + Run(Property(&ClientStatus::proto_status, INVALID_ACTION))); + EXPECT_CALL(view_inflation_finished_callback, Run(_)).Times(0); + EXPECT_TRUE(basic_interactions_.EndAction(ClientStatus(INVALID_ACTION))); + EXPECT_FALSE(basic_interactions_.NotifyViewInflationFinished( + ClientStatus(ACTION_APPLIED))); } TEST_F(BasicInteractionsTest, ComputeValueCompare) { @@ -493,8 +601,7 @@ TEST_F(BasicInteractionsTest, ComputeValueCreateLoginOptionResponse) { // LoginOptionResponseProto is allowed to extract the payload from // client-only values. ValueProto expected_response_value; - expected_response_value.mutable_login_option_response()->set_payload( - "payload"); + expected_response_value.set_server_payload("payload"); expected_response_value.set_is_client_side_only(false); EXPECT_EQ(user_model_.GetValue("result"), expected_response_value); } diff --git a/chromium/components/autofill_assistant/browser/client.h b/chromium/components/autofill_assistant/browser/client.h index 2b44e689581..bbcba879b07 100644 --- a/chromium/components/autofill_assistant/browser/client.h +++ b/chromium/components/autofill_assistant/browser/client.h @@ -76,6 +76,9 @@ class Client { // Returns details about the device. virtual DeviceContext GetDeviceContext() const = 0; + // Returns whether a11y (talkback and touch exploration) is enabled or not. + virtual bool IsAccessibilityEnabled() const = 0; + // Returns current WebContents. virtual content::WebContents* GetWebContents() const = 0; diff --git a/chromium/components/autofill_assistant/browser/client_settings.h b/chromium/components/autofill_assistant/browser/client_settings.h index 94674066739..330aac1a2d8 100644 --- a/chromium/components/autofill_assistant/browser/client_settings.h +++ b/chromium/components/autofill_assistant/browser/client_settings.h @@ -82,7 +82,7 @@ struct ClientSettings { base::Optional<OverlayImageProto> overlay_image; // Optional settings intended for integration tests. - base::Optional<ClientSettingsProto_IntegrationTestSettings> + base::Optional<ClientSettingsProto::IntegrationTestSettings> integration_test_settings; void UpdateFromProto(const ClientSettingsProto& proto); diff --git a/chromium/components/autofill_assistant/browser/client_status.cc b/chromium/components/autofill_assistant/browser/client_status.cc index d99fa0dfb91..c3b4ad9cd54 100644 --- a/chromium/components/autofill_assistant/browser/client_status.cc +++ b/chromium/components/autofill_assistant/browser/client_status.cc @@ -119,6 +119,9 @@ std::ostream& operator<<(std::ostream& out, case ProcessedActionStatusProto::AUTOFILL_INCOMPLETE: out << "AUTOFILL_INCOMPLETE"; break; + case ProcessedActionStatusProto::TOO_MANY_CANDIDATES: + out << "TOO_MANY_CANDIDATES"; + break; // Intentionally no default case to make compilation fail if a new value // was added to the enum but not to this list. diff --git a/chromium/components/autofill_assistant/browser/controller.cc b/chromium/components/autofill_assistant/browser/controller.cc index f980f810704..db966cfd225 100644 --- a/chromium/components/autofill_assistant/browser/controller.cc +++ b/chromium/components/autofill_assistant/browser/controller.cc @@ -12,7 +12,6 @@ #include "base/no_destructor.h" #include "base/strings/string_util.h" #include "base/strings/utf_string_conversions.h" -#include "base/task/post_task.h" #include "base/time/tick_clock.h" #include "base/values.h" #include "components/autofill_assistant/browser/actions/collect_user_data_action.h" @@ -23,6 +22,7 @@ #include "components/autofill_assistant/browser/service_impl.h" #include "components/autofill_assistant/browser/trigger_context.h" #include "components/autofill_assistant/browser/user_data.h" +#include "components/autofill_assistant/browser/view_layout.pb.h" #include "components/google/core/common/google_util.h" #include "components/password_manager/core/browser/password_manager_client.h" #include "components/strings/grit/components_strings.h" @@ -43,14 +43,16 @@ namespace { static constexpr int kAutostartInitialProgress = 5; // Parameter that allows setting the color of the overlay. -static const char* const kOverlayColorParameterName = "OVERLAY_COLORS"; +const char kOverlayColorParameterName[] = "OVERLAY_COLORS"; // Parameter that contains the current session username. Should be synced with // |SESSION_USERNAME_PARAMETER| from // .../password_manager/PasswordChangeLauncher.java // TODO(b/151401974): Eliminate duplicate parameter definitions. -static const char* const kPasswordChangeUsernameParameterName = - "PASSWORD_CHANGE_USERNAME"; +const char kPasswordChangeUsernameParameterName[] = "PASSWORD_CHANGE_USERNAME"; + +// Experiment for toggling the new progress bar. +const char kProgressBarExperiment[] = "4400697"; // Returns true if the state requires a UI to be shown. // @@ -241,6 +243,15 @@ int Controller::GetProgress() const { return progress_; } +base::Optional<int> Controller::GetProgressActiveStep() const { + return progress_active_step_; +} + +base::Optional<ShowProgressBarProto::StepProgressBarConfiguration> +Controller::GetStepProgressBarConfiguration() const { + return step_progress_bar_configuration_; +} + void Controller::SetInfoBox(const InfoBox& info_box) { if (!info_box_) { info_box_ = std::make_unique<InfoBox>(); @@ -273,6 +284,18 @@ void Controller::SetProgress(int progress) { } } +void Controller::SetProgressActiveStep(int active_step) { + // Step can only increase. + if (progress_active_step_ >= active_step) { + return; + } + + progress_active_step_ = active_step; + for (ControllerObserver& observer : observers_) { + observer.OnProgressActiveStepChanged(active_step); + } +} + void Controller::SetProgressVisible(bool visible) { if (progress_visible_ == visible) return; @@ -287,6 +310,38 @@ bool Controller::GetProgressVisible() const { return progress_visible_; } +void Controller::SetStepProgressBarConfiguration( + const ShowProgressBarProto::StepProgressBarConfiguration& configuration) { + step_progress_bar_configuration_ = configuration; + if (!configuration.step_icons().empty() && + progress_active_step_.has_value() && + configuration.step_icons().size() < *progress_active_step_) { + progress_active_step_ = configuration.step_icons().size(); + } + for (ControllerObserver& observer : observers_) { + observer.OnStepProgressBarConfigurationChanged(configuration); + if (progress_active_step_.has_value()) { + observer.OnProgressActiveStepChanged(*progress_active_step_); + } + observer.OnProgressBarErrorStateChanged(progress_bar_error_state_); + } +} + +void Controller::SetProgressBarErrorState(bool error) { + if (progress_bar_error_state_ == error) { + return; + } + + progress_bar_error_state_ = error; + for (ControllerObserver& observer : observers_) { + observer.OnProgressBarErrorStateChanged(error); + } +} + +bool Controller::GetProgressBarErrorState() const { + return progress_bar_error_state_; +} + const std::vector<UserAction>& Controller::GetUserActions() const { static const base::NoDestructor<std::vector<UserAction>> no_user_actions_; return user_actions_ ? *user_actions_ : *no_user_actions_; @@ -321,10 +376,13 @@ void Controller::RequireUI() { void Controller::SetGenericUi( std::unique_ptr<GenericUserInterfaceProto> generic_ui, - base::OnceCallback<void(bool, ProcessedActionStatusProto, const UserModel*)> - end_action_callback) { + base::OnceCallback<void(const ClientStatus&)> end_action_callback, + base::OnceCallback<void(const ClientStatus&)> + view_inflation_finished_callback) { generic_user_interface_ = std::move(generic_ui); basic_interactions_.SetEndActionCallback(std::move(end_action_callback)); + basic_interactions_.SetViewInflationFinishedCallback( + std::move(view_inflation_finished_callback)); for (ControllerObserver& observer : observers_) { observer.OnGenericUserInterfaceChanged(generic_user_interface_.get()); } @@ -332,7 +390,7 @@ void Controller::SetGenericUi( void Controller::ClearGenericUi() { generic_user_interface_.reset(); - basic_interactions_.ClearEndActionCallback(); + basic_interactions_.ClearCallbacks(); for (ControllerObserver& observer : observers_) { observer.OnGenericUserInterfaceChanged(nullptr); } @@ -699,10 +757,11 @@ void Controller::StartPeriodicScriptChecks() { if (periodic_script_check_scheduled_) return; periodic_script_check_scheduled_ = true; - base::PostDelayedTask(FROM_HERE, {content::BrowserThread::UI}, - base::BindOnce(&Controller::OnPeriodicScriptCheck, - weak_ptr_factory_.GetWeakPtr()), - settings_.periodic_script_check_interval); + content::GetUIThreadTaskRunner({})->PostDelayedTask( + FROM_HERE, + base::BindOnce(&Controller::OnPeriodicScriptCheck, + weak_ptr_factory_.GetWeakPtr()), + settings_.periodic_script_check_interval); } void Controller::StopPeriodicScriptChecks() { @@ -732,10 +791,11 @@ void Controller::OnPeriodicScriptCheck() { } script_tracker()->CheckScripts(); - base::PostDelayedTask(FROM_HERE, {content::BrowserThread::UI}, - base::BindOnce(&Controller::OnPeriodicScriptCheck, - weak_ptr_factory_.GetWeakPtr()), - settings_.periodic_script_check_interval); + content::GetUIThreadTaskRunner({})->PostDelayedTask( + FROM_HERE, + base::BindOnce(&Controller::OnPeriodicScriptCheck, + weak_ptr_factory_.GetWeakPtr()), + settings_.periodic_script_check_interval); } void Controller::OnGetScripts(const GURL& url, @@ -980,6 +1040,12 @@ void Controller::InitFromParameters() { user_data_->selected_login_.emplace(web_contents()->GetLastCommittedURL(), *password_change_username); } + + if (trigger_context_->HasExperimentId(kProgressBarExperiment)) { + ShowProgressBarProto::StepProgressBarConfiguration mock_configuration; + mock_configuration.set_use_step_progress_bar(true); + SetStepProgressBarConfiguration(mock_configuration); + } } void Controller::Track(std::unique_ptr<TriggerContext> trigger_context, @@ -1043,7 +1109,12 @@ void Controller::ShowFirstMessageAndStart() { SetStatusMessage( l10n_util::GetStringFUTF8(IDS_AUTOFILL_ASSISTANT_LOADING, base::UTF8ToUTF16(GetCurrentURL().host()))); - SetProgress(kAutostartInitialProgress); + if (step_progress_bar_configuration_.has_value() && + step_progress_bar_configuration_->use_step_progress_bar()) { + SetProgressActiveStep(0); + } else { + SetProgress(kAutostartInitialProgress); + } EnterState(AutofillAssistantState::STARTING); } @@ -1051,6 +1122,10 @@ AutofillAssistantState Controller::GetState() { return state_; } +int64_t Controller::GetErrorCausingNavigationId() const { + return error_causing_navigation_id_; +} + void Controller::OnScriptSelected(const ScriptHandle& handle, std::unique_ptr<TriggerContext> context) { ExecuteScript(handle.path, handle.start_message, handle.needs_ui, @@ -1435,6 +1510,7 @@ void Controller::OnScriptError(const std::string& error_message, RequireUI(); SetStatusMessage(error_message); + SetProgressBarErrorState(true); EnterStoppedState(); if (tracking_) { @@ -1454,6 +1530,7 @@ void Controller::OnFatalError(const std::string& error_message, return; SetStatusMessage(error_message); + SetProgressBarErrorState(true); EnterStoppedState(); // If we haven't managed to check the set of scripts yet at this point, we @@ -1588,10 +1665,6 @@ void Controller::OnRunnableScriptsChanged( SetUserActions(std::move(user_actions)); } -void Controller::DidAttachInterstitialPage() { - client_->Shutdown(Metrics::DropOutReason::INTERSTITIAL_PAGE); -} - void Controller::DidFinishLoad(content::RenderFrameHost* render_frame_host, const GURL& validated_url) { // validated_url might not be the page URL. Ignore it and always check the @@ -1599,6 +1672,10 @@ void Controller::DidFinishLoad(content::RenderFrameHost* render_frame_host, OnUrlChange(); } +void Controller::ExpectNavigation() { + expect_navigation_ = true; +} + void Controller::DidStartNavigation( content::NavigationHandle* navigation_handle) { if (!navigation_handle->IsInMainFrame() || @@ -1611,6 +1688,12 @@ void Controller::DidStartNavigation( ReportNavigationStateChanged(); } + // The navigation is expected, do not check for errors below. + if (expect_navigation_) { + expect_navigation_ = false; + return; + } + // The following types of navigations are allowed for the main frame, when // in PROMPT state: // - first-time URL load @@ -1627,19 +1710,38 @@ void Controller::DidStartNavigation( // Everything else, such as going back to a previous page, or refreshing the // page is considered an end condition. If going back to a previous page is // required, consider using the BROWSE state instead. - // Note that BROWSE state end conditions are in DidFinishNavigation, in order - // to be able to properly evaluate the committed url. if (state_ == AutofillAssistantState::PROMPT && web_contents()->GetLastCommittedURL().is_valid() && !navigation_handle->WasServerRedirect() && !navigation_handle->IsRendererInitiated()) { + error_causing_navigation_id_ = navigation_handle->GetNavigationId(); OnScriptError(l10n_util::GetStringUTF8(IDS_AUTOFILL_ASSISTANT_GIVE_UP), Metrics::DropOutReason::NAVIGATION); + return; } + + if (base::FeatureList::IsEnabled( + features::kAutofillAssistantBreakOnRunningNavigation)) { + // When in RUNNING state, all renderer initiated navigation is allowed, + // user initiated navigation will cause an error. + if (state_ == AutofillAssistantState::RUNNING && + !navigation_handle->WasServerRedirect() && + !navigation_handle->IsRendererInitiated()) { + error_causing_navigation_id_ = navigation_handle->GetNavigationId(); + OnScriptError(l10n_util::GetStringUTF8(IDS_AUTOFILL_ASSISTANT_GIVE_UP), + Metrics::DropOutReason::NAVIGATION_WHILE_RUNNING); + return; + } + } + + // Note that BROWSE state end conditions are in DidFinishNavigation, in order + // to be able to properly evaluate the committed url. } void Controller::DidFinishNavigation( content::NavigationHandle* navigation_handle) { + // TODO(b/159871774): Rethink how we handle navigation events. The early + // return here may prevent us from updating |navigating_to_new_document_|. if (!navigation_handle->IsInMainFrame() || navigation_handle->IsSameDocument() || !navigation_handle->HasCommitted() || !IsNavigatingToNewDocument()) { @@ -1758,11 +1860,6 @@ void Controller::WriteUserData( } } -void Controller::WriteUserModel( - base::OnceCallback<void(UserModel*)> write_callback) { - std::move(write_callback).Run(&user_model_); -} - ElementArea* Controller::touchable_element_area() { if (!touchable_element_area_) { touchable_element_area_ = std::make_unique<ElementArea>(this); diff --git a/chromium/components/autofill_assistant/browser/controller.h b/chromium/components/autofill_assistant/browser/controller.h index 998f8f84e7d..e35235d7bd4 100644 --- a/chromium/components/autofill_assistant/browser/controller.h +++ b/chromium/components/autofill_assistant/browser/controller.h @@ -119,7 +119,11 @@ class Controller : public ScriptExecutorDelegate, void SetInfoBox(const InfoBox& info_box) override; void ClearInfoBox() override; void SetProgress(int progress) override; + void SetProgressActiveStep(int active_step) override; void SetProgressVisible(bool visible) override; + void SetStepProgressBarConfiguration( + const ShowProgressBarProto::StepProgressBarConfiguration& configuration) + override; void SetUserActions( std::unique_ptr<std::vector<UserAction>> user_actions) override; void SetViewportMode(ViewportMode mode) override; @@ -130,13 +134,14 @@ class Controller : public ScriptExecutorDelegate, std::unique_ptr<FormProto> form, base::RepeatingCallback<void(const FormProto::Result*)> changed_callback, base::OnceCallback<void(const ClientStatus&)> cancel_callback) override; + void ExpectNavigation() override; bool IsNavigatingToNewDocument() override; bool HasNavigationError() override; void SetGenericUi( std::unique_ptr<GenericUserInterfaceProto> generic_ui, - base::OnceCallback<void(bool, - ProcessedActionStatusProto, - const UserModel*)> end_action_callback) override; + base::OnceCallback<void(const ClientStatus&)> end_action_callback, + base::OnceCallback<void(const ClientStatus&)> + view_inflation_finished_callback) override; void ClearGenericUi() override; // Show the UI if it's not already shown. This is only meaningful while in @@ -153,18 +158,21 @@ class Controller : public ScriptExecutorDelegate, void SetCollectUserDataOptions(CollectUserDataOptions* options) override; void WriteUserData( base::OnceCallback<void(UserData*, UserData::FieldChange*)>) override; - void WriteUserModel( - base::OnceCallback<void(UserModel*)> write_callback) override; void OnScriptError(const std::string& error_message, Metrics::DropOutReason reason); // Overrides autofill_assistant::UiDelegate: AutofillAssistantState GetState() override; + int64_t GetErrorCausingNavigationId() const override; void OnUserInteractionInsideTouchableArea() override; const Details* GetDetails() const override; const InfoBox* GetInfoBox() const override; int GetProgress() const override; + base::Optional<int> GetProgressActiveStep() const override; bool GetProgressVisible() const override; + bool GetProgressBarErrorState() const override; + base::Optional<ShowProgressBarProto::StepProgressBarConfiguration> + GetStepProgressBarConfiguration() const override; const std::vector<UserAction>& GetUserActions() const override; bool PerformUserActionWithContext( int index, @@ -279,7 +287,6 @@ class Controller : public ScriptExecutorDelegate, const std::vector<ScriptHandle>& runnable_scripts) override; // Overrides content::WebContentsObserver: - void DidAttachInterstitialPage() override; void DidFinishLoad(content::RenderFrameHost* render_frame_host, const GURL& validated_url) override; void DidStartNavigation( @@ -311,6 +318,8 @@ class Controller : public ScriptExecutorDelegate, // Clear out visible state and enter the stopped state. void EnterStoppedState(); + void SetProgressBarErrorState(bool error); + ElementArea* touchable_element_area(); ScriptTracker* script_tracker(); bool allow_autostart() { return state_ == AutofillAssistantState::STARTING; } @@ -327,6 +336,7 @@ class Controller : public ScriptExecutorDelegate, std::unique_ptr<TriggerContext> trigger_context_; AutofillAssistantState state_ = AutofillAssistantState::INACTIVE; + int64_t error_causing_navigation_id_ = -1; // The URL passed to Start(). Used only as long as there's no committed URL. // Note that this is the deeplink passed by a caller. @@ -373,9 +383,13 @@ class Controller : public ScriptExecutorDelegate, // Current progress. int progress_ = 0; + base::Optional<int> progress_active_step_; // Current visibility of the progress bar. It is initially visible. bool progress_visible_ = true; + bool progress_bar_error_state_ = false; + base::Optional<ShowProgressBarProto::StepProgressBarConfiguration> + step_progress_bar_configuration_; // Current set of user actions. May be null, but never empty. std::unique_ptr<std::vector<UserAction>> user_actions_; @@ -411,6 +425,9 @@ class Controller : public ScriptExecutorDelegate, bool navigation_error_ = false; base::ObserverList<NavigationListener> navigation_listeners_; + // The next DidStartNavigation will not cause an error. + bool expect_navigation_ = false; + // Tracks scripts and script execution. It's kept at the end, as it tend to // depend on everything the controller support, through script and script // actions. diff --git a/chromium/components/autofill_assistant/browser/controller_observer.h b/chromium/components/autofill_assistant/browser/controller_observer.h index 6cc7d6a6cd4..eeb525f47fe 100644 --- a/chromium/components/autofill_assistant/browser/controller_observer.h +++ b/chromium/components/autofill_assistant/browser/controller_observer.h @@ -67,10 +67,20 @@ class ControllerObserver : public base::CheckedObserver { // percentage. virtual void OnProgressChanged(int progress) = 0; + // Called when the currently active progress step has changed. + virtual void OnProgressActiveStepChanged(int active_step) = 0; + // Called when the current progress bar visibility has changed. If |visible| // is true, then the bar is now shown. virtual void OnProgressVisibilityChanged(bool visible) = 0; + virtual void OnStepProgressBarConfigurationChanged( + const ShowProgressBarProto::StepProgressBarConfiguration& + configuration) = 0; + + // Called when the progress bar error state changes. + virtual void OnProgressBarErrorStateChanged(bool error) = 0; + // Updates the area of the visible viewport that is accessible when the // overlay state is OverlayState::PARTIAL. // diff --git a/chromium/components/autofill_assistant/browser/controller_unittest.cc b/chromium/components/autofill_assistant/browser/controller_unittest.cc index eb8dbeed3c6..9b16df336cf 100644 --- a/chromium/components/autofill_assistant/browser/controller_unittest.cc +++ b/chromium/components/autofill_assistant/browser/controller_unittest.cc @@ -416,12 +416,11 @@ TEST_F(ControllerTest, NoRelevantScripts) { TEST_F(ControllerTest, NoRelevantScriptYet) { SupportsScriptResponseProto script_response; - AddRunnableScript(&script_response, "no_match_yet") - ->mutable_presentation() - ->mutable_precondition() - ->mutable_element_condition() - ->mutable_match() - ->add_selectors("#element"); + *AddRunnableScript(&script_response, "no_match_yet") + ->mutable_presentation() + ->mutable_precondition() + ->mutable_element_condition() + ->mutable_match() = ToSelectorProto("#element"); SetNextScriptResponse(script_response); Start("http://a.example.com/path"); @@ -714,12 +713,11 @@ TEST_F(ControllerTest, AttachUIWhenContentsFocused) { TEST_F(ControllerTest, KeepCheckingForElement) { SupportsScriptResponseProto script_response; - AddRunnableScript(&script_response, "no_match_yet") - ->mutable_presentation() - ->mutable_precondition() - ->mutable_element_condition() - ->mutable_match() - ->add_selectors("#element"); + *AddRunnableScript(&script_response, "no_match_yet") + ->mutable_presentation() + ->mutable_precondition() + ->mutable_element_condition() + ->mutable_match() = ToSelectorProto("#element"); SetNextScriptResponse(script_response); Start("http://a.example.com/path"); @@ -743,12 +741,11 @@ TEST_F(ControllerTest, ScriptTimeoutError) { // Wait for #element to show up for will_never_match. After 25s, execute the // script on_timeout_error. SupportsScriptResponseProto script_response; - AddRunnableScript(&script_response, "will_never_match") - ->mutable_presentation() - ->mutable_precondition() - ->mutable_element_condition() - ->mutable_match() - ->add_selectors("#element"); + *AddRunnableScript(&script_response, "will_never_match") + ->mutable_presentation() + ->mutable_precondition() + ->mutable_element_condition() + ->mutable_match() = ToSelectorProto("#element"); script_response.mutable_script_timeout_error()->set_timeout_ms(30000); script_response.mutable_script_timeout_error()->set_script_path( "on_timeout_error"); @@ -777,12 +774,11 @@ TEST_F(ControllerTest, ScriptTimeoutWarning) { // Wait for #element to show up for will_never_match. After 10s, execute the // script on_timeout_error. SupportsScriptResponseProto script_response; - AddRunnableScript(&script_response, "will_never_match") - ->mutable_presentation() - ->mutable_precondition() - ->mutable_element_condition() - ->mutable_match() - ->add_selectors("#element"); + *AddRunnableScript(&script_response, "will_never_match") + ->mutable_presentation() + ->mutable_precondition() + ->mutable_element_condition() + ->mutable_match() = ToSelectorProto("#element"); script_response.mutable_script_timeout_error()->set_timeout_ms(4000); script_response.mutable_script_timeout_error()->set_script_path( "on_timeout_error"); @@ -1249,12 +1245,11 @@ TEST_F(ControllerTest, TrackReportsNoScripts) { TEST_F(ControllerTest, TrackReportsNoScriptsForNow) { SupportsScriptResponseProto script_response; - AddRunnableScript(&script_response, "no_match_yet") - ->mutable_presentation() - ->mutable_precondition() - ->mutable_element_condition() - ->mutable_match() - ->add_selectors("#element"); + *AddRunnableScript(&script_response, "no_match_yet") + ->mutable_presentation() + ->mutable_precondition() + ->mutable_element_condition() + ->mutable_match() = ToSelectorProto("#element"); SetNextScriptResponse(script_response); SetLastCommittedUrl(GURL("http://example.com/")); @@ -1574,10 +1569,23 @@ TEST_F(ControllerTest, UnexpectedNavigationDuringPromptAction) { EXPECT_CALL(mock_observer_, OnStatusMessageChanged(testing::Not(never_shown))) .Times(testing::AnyNumber()); - EXPECT_CALL(mock_client_, Shutdown(Metrics::DropOutReason::NAVIGATION)); + // Renderer (Document) initiated navigation is allowed. + EXPECT_CALL(mock_client_, Shutdown(_)).Times(0); + content::NavigationSimulator::NavigateAndCommitFromDocument( + GURL("http://a.example.com/page"), web_contents()->GetMainFrame()); + EXPECT_EQ(AutofillAssistantState::PROMPT, controller_->GetState()); + + // Expected browser initiated navigation is allowed. + EXPECT_CALL(mock_client_, Shutdown(_)).Times(0); + controller_->ExpectNavigation(); content::NavigationSimulator::NavigateAndCommitFromBrowser( - web_contents(), GURL("http://example.com/otherpage")); + web_contents(), GURL("http://b.example.com/page")); + EXPECT_EQ(AutofillAssistantState::PROMPT, controller_->GetState()); + // Unexpected browser initiated navigation will cause an error. + EXPECT_CALL(mock_client_, Shutdown(Metrics::DropOutReason::NAVIGATION)); + content::NavigationSimulator::NavigateAndCommitFromBrowser( + web_contents(), GURL("http://c.example.com/page")); EXPECT_EQ(AutofillAssistantState::STOPPED, controller_->GetState()); // Full history of state transitions. @@ -1587,6 +1595,55 @@ TEST_F(ControllerTest, UnexpectedNavigationDuringPromptAction) { AutofillAssistantState::STOPPED)); } +TEST_F(ControllerTest, UnexpectedNavigationInRunningState) { + SupportsScriptResponseProto script_response; + AddRunnableScript(&script_response, "autostart") + ->mutable_presentation() + ->set_autostart(true); + SetNextScriptResponse(script_response); + + ActionsResponseProto autostart_script; + auto* wait_for_dom = autostart_script.add_actions()->mutable_wait_for_dom(); + wait_for_dom->set_timeout_ms(10000); + wait_for_dom->mutable_wait_condition() + ->mutable_match() + ->add_filters() + ->set_css_selector("#some-element"); + SetupActionsForScript("autostart", autostart_script); + + Start(); + EXPECT_EQ(AutofillAssistantState::RUNNING, controller_->GetState()); + + // Document (not user) initiated navigation while in RUNNING state: + // The controller keeps going. + EXPECT_CALL(mock_client_, Shutdown(_)).Times(0); + content::NavigationSimulator::NavigateAndCommitFromDocument( + GURL("http://a.example.com/page"), web_contents()->GetMainFrame()); + EXPECT_EQ(AutofillAssistantState::RUNNING, controller_->GetState()); + + // Expected browser initiated navigation while in RUNNING state: + // The controller keeps going. + EXPECT_CALL(mock_client_, Shutdown(_)).Times(0); + controller_->ExpectNavigation(); + content::NavigationSimulator::NavigateAndCommitFromBrowser( + web_contents(), GURL("http://b.example.com/page")); + EXPECT_EQ(AutofillAssistantState::RUNNING, controller_->GetState()); + + // Unexpected browser initiated navigation while in RUNNING state: + // The controller stops the scripts, shows an error and shuts down. + EXPECT_CALL(mock_client_, + Shutdown(Metrics::DropOutReason::NAVIGATION_WHILE_RUNNING)); + EXPECT_CALL(mock_observer_, OnStatusMessageChanged(_)); + content::NavigationSimulator::NavigateAndCommitFromBrowser( + web_contents(), GURL("http://c.example.com/page")); + EXPECT_EQ(AutofillAssistantState::STOPPED, controller_->GetState()); + + // Full history of state transitions. + EXPECT_THAT(states_, ElementsAre(AutofillAssistantState::STARTING, + AutofillAssistantState::RUNNING, + AutofillAssistantState::STOPPED)); +} + TEST_F(ControllerTest, NavigationToGooglePropertyDestroysUI) { SupportsScriptResponseProto script_response; AddRunnableScript(&script_response, "autostart") @@ -2271,7 +2328,7 @@ TEST_F(ControllerTest, SetGenericUi) { } controller_->SetGenericUi( std::make_unique<GenericUserInterfaceProto>(GenericUserInterfaceProto()), - base::DoNothing()); + base::DoNothing(), base::DoNothing()); controller_->ClearGenericUi(); } diff --git a/chromium/components/autofill_assistant/browser/devtools/devtools_api/domain_type_conversions_h.template b/chromium/components/autofill_assistant/browser/devtools/devtools_api/domain_type_conversions_h.template index bbc506c3ed8..681e555e16d 100644 --- a/chromium/components/autofill_assistant/browser/devtools/devtools_api/domain_type_conversions_h.template +++ b/chromium/components/autofill_assistant/browser/devtools/devtools_api/domain_type_conversions_h.template @@ -9,7 +9,7 @@ #ifndef COMPONENTS_AUTOFILL_ASSISTANT_BROWSER_DEVTOOLS_DEVTOOLS_INTERNAL_TYPE_CONVERSIONS_{{domain.domain | camelcase_to_hacker_style | upper}}_H_ #define COMPONENTS_AUTOFILL_ASSISTANT_BROWSER_DEVTOOLS_DEVTOOLS_INTERNAL_TYPE_CONVERSIONS_{{domain.domain | camelcase_to_hacker_style | upper}}_H_ -#include "base/logging.h" +#include "base/notreached.h" #include "components/autofill_assistant/browser/devtools/devtools/domains/types_{{domain.domain | camelcase_to_hacker_style}}.h" #include "components/autofill_assistant/browser/devtools/value_conversions.h" diff --git a/chromium/components/autofill_assistant/browser/devtools/devtools_client.cc b/chromium/components/autofill_assistant/browser/devtools/devtools_client.cc index 3c213373bba..8f7af508b55 100644 --- a/chromium/components/autofill_assistant/browser/devtools/devtools_client.cc +++ b/chromium/components/autofill_assistant/browser/devtools/devtools_client.cc @@ -13,7 +13,6 @@ #include "base/callback_forward.h" #include "base/json/json_reader.h" #include "base/json/json_writer.h" -#include "base/task/post_task.h" #include "content/public/browser/browser_task_traits.h" #include "content/public/browser/browser_thread.h" @@ -30,8 +29,7 @@ DevtoolsClient::DevtoolsClient( renderer_crashed_(false), next_message_id_(0), frame_tracker_(this) { - browser_main_thread_ = - base::CreateSingleThreadTaskRunner({content::BrowserThread::UI}); + browser_main_thread_ = content::GetUIThreadTaskRunner({}); agent_host_->AttachClient(this); frame_tracker_.Start(); } diff --git a/chromium/components/autofill_assistant/browser/element_area.cc b/chromium/components/autofill_assistant/browser/element_area.cc index 6136a6e62e7..0b513492506 100644 --- a/chromium/components/autofill_assistant/browser/element_area.cc +++ b/chromium/components/autofill_assistant/browser/element_area.cc @@ -50,6 +50,9 @@ void ElementArea::Clear() { void ElementArea::SetFromProto(const ElementAreaProto& proto) { rectangles_.clear(); + last_visual_viewport_ = RectF(); + last_rectangles_.clear(); + AddRectangles(proto.touchable(), /* restricted= */ false); AddRectangles(proto.restricted(), /* restricted= */ true); diff --git a/chromium/components/autofill_assistant/browser/element_area_unittest.cc b/chromium/components/autofill_assistant/browser/element_area_unittest.cc index 5da55e62e68..3ccde28d42f 100644 --- a/chromium/components/autofill_assistant/browser/element_area_unittest.cc +++ b/chromium/components/autofill_assistant/browser/element_area_unittest.cc @@ -86,7 +86,7 @@ class ElementAreaTest : public testing::Test { void SetElement(const std::string& selector, bool restricted) { ElementAreaProto area; auto* rectangle = restricted ? area.add_restricted() : area.add_touchable(); - rectangle->add_elements()->add_selectors(selector); + *rectangle->add_elements() = ToSelectorProto(selector); element_area_.SetFromProto(area); } @@ -164,6 +164,17 @@ TEST_F(ElementAreaTest, CallOnUpdate) { EXPECT_THAT(reported_area_, ElementsAre(MatchingRectF(25, 25, 75, 75))); } +TEST_F(ElementAreaTest, CallOnUpdateAfterSetFromProto) { + EXPECT_CALL(mock_web_controller_, + OnGetElementPosition(Eq(Selector({"#found"}).MustBeVisible()), _)) + .WillRepeatedly(RunOnceCallback<1>(true, RectF(25, 25, 75, 75))); + + SetElement("#found"); + EXPECT_EQ(on_update_call_count_, 1); + SetElement("#found"); + EXPECT_EQ(on_update_call_count_, 2); +} + TEST_F(ElementAreaTest, DontCallOnUpdateWhenViewportMissing) { // Swallowing calls to OnGetVisualViewport guarantees that the viewport // position will never be known. @@ -203,8 +214,9 @@ TEST_F(ElementAreaTest, TwoRectangles) { .WillOnce(RunOnceCallback<1>(true, RectF(25, 25, 100, 100))); ElementAreaProto area_proto; - area_proto.add_touchable()->add_elements()->add_selectors("#top_left"); - area_proto.add_touchable()->add_elements()->add_selectors("#bottom_right"); + *area_proto.add_touchable()->add_elements() = ToSelectorProto("#top_left"); + *area_proto.add_touchable()->add_elements() = + ToSelectorProto("#bottom_right"); element_area_.SetFromProto(area_proto); std::vector<RectF> rectangles; @@ -225,8 +237,8 @@ TEST_F(ElementAreaTest, OneRectangleTwoElements) { ElementAreaProto area_proto; auto* rectangle_proto = area_proto.add_touchable(); - rectangle_proto->add_elements()->add_selectors("#element1"); - rectangle_proto->add_elements()->add_selectors("#element2"); + *rectangle_proto->add_elements() = ToSelectorProto("#element1"); + *rectangle_proto->add_elements() = ToSelectorProto("#element2"); element_area_.SetFromProto(area_proto); std::vector<RectF> rectangles; @@ -249,8 +261,8 @@ TEST_F(ElementAreaTest, DoNotReportIncompleteRectangles) { ElementAreaProto area_proto; auto* rectangle_proto = area_proto.add_touchable(); - rectangle_proto->add_elements()->add_selectors("#element1"); - rectangle_proto->add_elements()->add_selectors("#element2"); + *rectangle_proto->add_elements() = ToSelectorProto("#element1"); + *rectangle_proto->add_elements() = ToSelectorProto("#element2"); element_area_.SetFromProto(area_proto); EXPECT_THAT(reported_area_, IsEmpty()); @@ -280,10 +292,10 @@ TEST_F(ElementAreaTest, OneRectangleFourElements) { ElementAreaProto area_proto; auto* rectangle_proto = area_proto.add_touchable(); - rectangle_proto->add_elements()->add_selectors("#element1"); - rectangle_proto->add_elements()->add_selectors("#element2"); - rectangle_proto->add_elements()->add_selectors("#element3"); - rectangle_proto->add_elements()->add_selectors("#element4"); + *rectangle_proto->add_elements() = ToSelectorProto("#element1"); + *rectangle_proto->add_elements() = ToSelectorProto("#element2"); + *rectangle_proto->add_elements() = ToSelectorProto("#element3"); + *rectangle_proto->add_elements() = ToSelectorProto("#element4"); element_area_.SetFromProto(area_proto); std::vector<RectF> rectangles; @@ -303,8 +315,8 @@ TEST_F(ElementAreaTest, OneRectangleMissingElementsReported) { ElementAreaProto area_proto; auto* rectangle_proto = area_proto.add_touchable(); - rectangle_proto->add_elements()->add_selectors("#element1"); - rectangle_proto->add_elements()->add_selectors("#element2"); + *rectangle_proto->add_elements() = ToSelectorProto("#element1"); + *rectangle_proto->add_elements() = ToSelectorProto("#element2"); element_area_.SetFromProto(area_proto); std::vector<RectF> rectangles; @@ -328,8 +340,8 @@ TEST_F(ElementAreaTest, FullWidthRectangle) { ElementAreaProto area_proto; auto* rectangle_proto = area_proto.add_touchable(); - rectangle_proto->add_elements()->add_selectors("#element1"); - rectangle_proto->add_elements()->add_selectors("#element2"); + *rectangle_proto->add_elements() = ToSelectorProto("#element1"); + *rectangle_proto->add_elements() = ToSelectorProto("#element2"); rectangle_proto->set_full_width(true); element_area_.SetFromProto(area_proto); diff --git a/chromium/components/autofill_assistant/browser/element_precondition_unittest.cc b/chromium/components/autofill_assistant/browser/element_precondition_unittest.cc index 7756d8ccdb1..b9f1058c92a 100644 --- a/chromium/components/autofill_assistant/browser/element_precondition_unittest.cc +++ b/chromium/components/autofill_assistant/browser/element_precondition_unittest.cc @@ -67,7 +67,7 @@ TEST_F(ElementPreconditionTest, Empty) { } TEST_F(ElementPreconditionTest, NonEmpty) { - condition_.mutable_match()->add_selectors("exists"); + *condition_.mutable_match() = ToSelectorProto("exists"); EXPECT_FALSE(ElementPrecondition(condition_).empty()); } @@ -87,15 +87,15 @@ TEST_F(ElementPreconditionTest, EmptySelector) { } TEST_F(ElementPreconditionTest, ElementExists) { - condition_.mutable_match()->add_selectors("exists"); + *condition_.mutable_match() = ToSelectorProto("exists"); EXPECT_CALL(mock_callback_, Run(Property(&ClientStatus::proto_status, ACTION_APPLIED), _)); Check(mock_callback_.Get()); } -TEST_F(ElementPreconditionTest, ElementDoesNotExist) { - condition_.mutable_match()->add_selectors("does_not_exist"); +TEST_F(ElementPreconditionTest, ElementDoes_Not_Exist) { + *condition_.mutable_match() = ToSelectorProto("does_not_exist"); EXPECT_CALL( mock_callback_, @@ -113,10 +113,10 @@ TEST_F(ElementPreconditionTest, AnyOf_Empty) { } TEST_F(ElementPreconditionTest, AnyOf_NoneMatch) { - condition_.mutable_any_of()->add_conditions()->mutable_match()->add_selectors( - "does_not_exist"); - condition_.mutable_any_of()->add_conditions()->mutable_match()->add_selectors( - "does_not_exist_either"); + *condition_.mutable_any_of()->add_conditions()->mutable_match() = + ToSelectorProto("does_not_exist"); + *condition_.mutable_any_of()->add_conditions()->mutable_match() = + ToSelectorProto("does_not_exist_either"); EXPECT_CALL( mock_callback_, @@ -125,10 +125,10 @@ TEST_F(ElementPreconditionTest, AnyOf_NoneMatch) { } TEST_F(ElementPreconditionTest, AnyOf_SomeMatch) { - condition_.mutable_any_of()->add_conditions()->mutable_match()->add_selectors( - "exists"); - condition_.mutable_any_of()->add_conditions()->mutable_match()->add_selectors( - "does_not_exist"); + *condition_.mutable_any_of()->add_conditions()->mutable_match() = + ToSelectorProto("exists"); + *condition_.mutable_any_of()->add_conditions()->mutable_match() = + ToSelectorProto("does_not_exist"); EXPECT_CALL(mock_callback_, Run(Property(&ClientStatus::proto_status, ACTION_APPLIED), _)); @@ -136,10 +136,10 @@ TEST_F(ElementPreconditionTest, AnyOf_SomeMatch) { } TEST_F(ElementPreconditionTest, AnyOf_AllMatch) { - condition_.mutable_any_of()->add_conditions()->mutable_match()->add_selectors( - "exists"); - condition_.mutable_any_of()->add_conditions()->mutable_match()->add_selectors( - "exists_too"); + *condition_.mutable_any_of()->add_conditions()->mutable_match() = + ToSelectorProto("exists"); + *condition_.mutable_any_of()->add_conditions()->mutable_match() = + ToSelectorProto("exists_too"); EXPECT_CALL(mock_callback_, Run(Property(&ClientStatus::proto_status, ACTION_APPLIED), _)); @@ -155,10 +155,10 @@ TEST_F(ElementPreconditionTest, AllOf_Empty) { } TEST_F(ElementPreconditionTest, AllOf_NoneMatch) { - condition_.mutable_all_of()->add_conditions()->mutable_match()->add_selectors( - "does_not_exist"); - condition_.mutable_all_of()->add_conditions()->mutable_match()->add_selectors( - "does_not_exist_either"); + *condition_.mutable_all_of()->add_conditions()->mutable_match() = + ToSelectorProto("does_not_exist"); + *condition_.mutable_all_of()->add_conditions()->mutable_match() = + ToSelectorProto("does_not_exist_either"); EXPECT_CALL( mock_callback_, @@ -167,10 +167,10 @@ TEST_F(ElementPreconditionTest, AllOf_NoneMatch) { } TEST_F(ElementPreconditionTest, AllOf_SomeMatch) { - condition_.mutable_all_of()->add_conditions()->mutable_match()->add_selectors( - "exists"); - condition_.mutable_all_of()->add_conditions()->mutable_match()->add_selectors( - "does_not_exist"); + *condition_.mutable_all_of()->add_conditions()->mutable_match() = + ToSelectorProto("exists"); + *condition_.mutable_all_of()->add_conditions()->mutable_match() = + ToSelectorProto("does_not_exist"); EXPECT_CALL( mock_callback_, @@ -179,10 +179,10 @@ TEST_F(ElementPreconditionTest, AllOf_SomeMatch) { } TEST_F(ElementPreconditionTest, AllOf_AllMatch) { - condition_.mutable_all_of()->add_conditions()->mutable_match()->add_selectors( - "exists"); - condition_.mutable_all_of()->add_conditions()->mutable_match()->add_selectors( - "exists_too"); + *condition_.mutable_all_of()->add_conditions()->mutable_match() = + ToSelectorProto("exists"); + *condition_.mutable_all_of()->add_conditions()->mutable_match() = + ToSelectorProto("exists_too"); EXPECT_CALL(mock_callback_, Run(Property(&ClientStatus::proto_status, ACTION_APPLIED), _)); @@ -198,14 +198,10 @@ TEST_F(ElementPreconditionTest, NoneOf_Empty) { } TEST_F(ElementPreconditionTest, NoneOf_NoneMatch) { - condition_.mutable_none_of() - ->add_conditions() - ->mutable_match() - ->add_selectors("does_not_exist"); - condition_.mutable_none_of() - ->add_conditions() - ->mutable_match() - ->add_selectors("does_not_exist_either"); + *condition_.mutable_none_of()->add_conditions()->mutable_match() = + ToSelectorProto("does_not_exist"); + *condition_.mutable_none_of()->add_conditions()->mutable_match() = + ToSelectorProto("does_not_exist_either"); EXPECT_CALL(mock_callback_, Run(Property(&ClientStatus::proto_status, ACTION_APPLIED), _)); @@ -213,14 +209,10 @@ TEST_F(ElementPreconditionTest, NoneOf_NoneMatch) { } TEST_F(ElementPreconditionTest, NoneOf_SomeMatch) { - condition_.mutable_none_of() - ->add_conditions() - ->mutable_match() - ->add_selectors("exists"); - condition_.mutable_none_of() - ->add_conditions() - ->mutable_match() - ->add_selectors("does_not_exist"); + *condition_.mutable_none_of()->add_conditions()->mutable_match() = + ToSelectorProto("exists"); + *condition_.mutable_none_of()->add_conditions()->mutable_match() = + ToSelectorProto("does_not_exist"); EXPECT_CALL( mock_callback_, @@ -229,14 +221,10 @@ TEST_F(ElementPreconditionTest, NoneOf_SomeMatch) { } TEST_F(ElementPreconditionTest, NoneOf_AllMatch) { - condition_.mutable_none_of() - ->add_conditions() - ->mutable_match() - ->add_selectors("exists"); - condition_.mutable_none_of() - ->add_conditions() - ->mutable_match() - ->add_selectors("exists_too"); + *condition_.mutable_none_of()->add_conditions()->mutable_match() = + ToSelectorProto("exists"); + *condition_.mutable_none_of()->add_conditions()->mutable_match() = + ToSelectorProto("exists_too"); EXPECT_CALL( mock_callback_, @@ -246,11 +234,11 @@ TEST_F(ElementPreconditionTest, NoneOf_AllMatch) { TEST_F(ElementPreconditionTest, Payload_ConditionMet) { auto* exists = condition_.mutable_any_of()->add_conditions(); - exists->mutable_match()->add_selectors("exists"); + *exists->mutable_match() = ToSelectorProto("exists"); exists->set_payload("exists"); auto* exists_too = condition_.mutable_any_of()->add_conditions(); - exists_too->mutable_match()->add_selectors("exists_too"); + *exists_too->mutable_match() = ToSelectorProto("exists_too"); exists_too->set_payload("exists_too"); condition_.set_payload("any_of"); @@ -263,11 +251,11 @@ TEST_F(ElementPreconditionTest, Payload_ConditionMet) { TEST_F(ElementPreconditionTest, Payload_ConditionNotMet) { auto* exists = condition_.mutable_none_of()->add_conditions(); - exists->mutable_match()->add_selectors("exists"); + *exists->mutable_match() = ToSelectorProto("exists"); exists->set_payload("exists"); auto* exists_too = condition_.mutable_none_of()->add_conditions(); - exists_too->mutable_match()->add_selectors("exists_too"); + *exists_too->mutable_match() = ToSelectorProto("exists_too"); exists_too->set_payload("exists_too"); condition_.set_payload("none_of"); @@ -279,27 +267,28 @@ TEST_F(ElementPreconditionTest, Payload_ConditionNotMet) { } TEST_F(ElementPreconditionTest, Complex) { - condition_.mutable_all_of()->add_conditions()->mutable_match()->add_selectors( - "exists"); - condition_.mutable_all_of()->add_conditions()->mutable_match()->add_selectors( - "exists_too"); + *condition_.mutable_all_of()->add_conditions()->mutable_match() = + ToSelectorProto("exists"); + *condition_.mutable_all_of()->add_conditions()->mutable_match() = + ToSelectorProto("exists_too"); auto* none_of = condition_.mutable_all_of()->add_conditions(); none_of->set_payload("none_of"); auto* does_not_exist_in_none_of = none_of->mutable_none_of()->add_conditions(); - does_not_exist_in_none_of->mutable_match()->add_selectors("does_not_exist"); + *does_not_exist_in_none_of->mutable_match() = + ToSelectorProto("does_not_exist"); does_not_exist_in_none_of->set_payload("does_not_exist in none_of"); - none_of->mutable_none_of()->add_conditions()->mutable_match()->add_selectors( - "does_not_exist_either"); + *none_of->mutable_none_of()->add_conditions()->mutable_match() = + ToSelectorProto("does_not_exist_either"); auto* any_of = condition_.mutable_all_of()->add_conditions(); any_of->set_payload("any_of"); auto* exists_in_any_of = any_of->mutable_any_of()->add_conditions(); - exists_in_any_of->mutable_match()->add_selectors("exists"); + *exists_in_any_of->mutable_match() = ToSelectorProto("exists"); exists_in_any_of->set_payload("exists in any_of"); - any_of->mutable_any_of()->add_conditions()->mutable_match()->add_selectors( - "does_not_exist"); + *any_of->mutable_any_of()->add_conditions()->mutable_match() = + ToSelectorProto("does_not_exist"); EXPECT_CALL(mock_callback_, Run(Property(&ClientStatus::proto_status, ACTION_APPLIED), diff --git a/chromium/components/autofill_assistant/browser/event_handler.cc b/chromium/components/autofill_assistant/browser/event_handler.cc index 886f8f96c4c..543d22d176b 100644 --- a/chromium/components/autofill_assistant/browser/event_handler.cc +++ b/chromium/components/autofill_assistant/browser/event_handler.cc @@ -3,6 +3,9 @@ // found in the LICENSE file. #include "components/autofill_assistant/browser/event_handler.h" +#include "base/strings/string_number_conversions.h" + +#include "base/logging.h" namespace autofill_assistant { @@ -24,6 +27,66 @@ void EventHandler::RemoveObserver(const Observer* observer) { observers_.RemoveObserver(observer); } +// static +base::Optional<EventHandler::EventKey> EventHandler::CreateEventKeyFromProto( + const EventProto& proto) { + switch (proto.kind_case()) { + case EventProto::kOnValueChanged: + if (proto.on_value_changed().model_identifier().empty()) { + VLOG(1) << "Invalid OnValueChangedEventProto: no model_identifier " + "specified"; + return base::nullopt; + } + return base::Optional<EventHandler::EventKey>( + {proto.kind_case(), proto.on_value_changed().model_identifier()}); + case EventProto::kOnViewClicked: + if (proto.on_view_clicked().view_identifier().empty()) { + VLOG(1) << "Invalid OnViewClickedEventProto: no view_identifier " + "specified"; + return base::nullopt; + } + return base::Optional<EventHandler::EventKey>( + {proto.kind_case(), proto.on_view_clicked().view_identifier()}); + case EventProto::kOnUserActionCalled: + if (proto.on_user_action_called().user_action_identifier().empty()) { + VLOG(1) << "Invalid OnUserActionCalled: no user_action_identifier " + "specified"; + return base::nullopt; + } + return base::Optional<EventHandler::EventKey>( + {proto.kind_case(), + proto.on_user_action_called().user_action_identifier()}); + case EventProto::kOnTextLinkClicked: + if (!proto.on_text_link_clicked().has_text_link()) { + VLOG(1) << "Invalid OnTextLinkClickedProto: no text_link specified"; + return base::nullopt; + } + return base::Optional<EventHandler::EventKey>( + {proto.kind_case(), + base::NumberToString(proto.on_text_link_clicked().text_link())}); + case EventProto::kOnPopupDismissed: + if (proto.on_popup_dismissed().popup_identifier().empty()) { + VLOG(1) + << "Invalid OnPopupDismissedProto: no popup_identifier specified"; + return base::nullopt; + } + return base::Optional<EventHandler::EventKey>( + {proto.kind_case(), proto.on_popup_dismissed().popup_identifier()}); + case EventProto::kOnViewContainerCleared: + if (proto.on_view_container_cleared().view_identifier().empty()) { + VLOG(1) << "Invalid OnViewContainerClearedProto: no view_identifier " + "specified"; + return base::nullopt; + } + return base::Optional<EventHandler::EventKey>( + {proto.kind_case(), + proto.on_view_container_cleared().view_identifier()}); + case EventProto::KIND_NOT_SET: + VLOG(1) << "Error creating event: kind not set"; + return base::nullopt; + } +} + std::ostream& operator<<(std::ostream& out, const EventProto::KindCase& event_case) { #ifdef NDEBUG @@ -46,6 +109,9 @@ std::ostream& operator<<(std::ostream& out, case EventProto::kOnPopupDismissed: out << "kOnPopupDismissed"; break; + case EventProto::kOnViewContainerCleared: + out << "kOnViewContainerCleared"; + break; case EventProto::KIND_NOT_SET: break; } diff --git a/chromium/components/autofill_assistant/browser/event_handler.h b/chromium/components/autofill_assistant/browser/event_handler.h index e4718433008..92ac4b96344 100644 --- a/chromium/components/autofill_assistant/browser/event_handler.h +++ b/chromium/components/autofill_assistant/browser/event_handler.h @@ -10,6 +10,7 @@ #include "base/macros.h" #include "base/memory/weak_ptr.h" #include "base/observer_list.h" +#include "base/optional.h" #include "components/autofill_assistant/browser/service.pb.h" #include "components/autofill_assistant/browser/user_model.h" @@ -37,6 +38,9 @@ class EventHandler { void DispatchEvent(const EventKey& key); + static base::Optional<EventKey> CreateEventKeyFromProto( + const EventProto& proto); + void AddObserver(Observer* observer); void RemoveObserver(const Observer* observer); diff --git a/chromium/components/autofill_assistant/browser/fake_script_executor_delegate.cc b/chromium/components/autofill_assistant/browser/fake_script_executor_delegate.cc index 3c8b0febdc0..616f918d7e9 100644 --- a/chromium/components/autofill_assistant/browser/fake_script_executor_delegate.cc +++ b/chromium/components/autofill_assistant/browser/fake_script_executor_delegate.cc @@ -103,8 +103,13 @@ void FakeScriptExecutorDelegate::ClearInfoBox() { void FakeScriptExecutorDelegate::SetProgress(int progress) {} +void FakeScriptExecutorDelegate::SetProgressActiveStep(int active_step) {} + void FakeScriptExecutorDelegate::SetProgressVisible(bool visible) {} +void FakeScriptExecutorDelegate::SetStepProgressBarConfiguration( + const ShowProgressBarProto::StepProgressBarConfiguration& configuration) {} + void FakeScriptExecutorDelegate::SetUserActions( std::unique_ptr<std::vector<UserAction>> user_actions) { user_actions_ = std::move(user_actions); @@ -126,11 +131,6 @@ void FakeScriptExecutorDelegate::WriteUserData( std::move(write_callback).Run(payment_request_info_.get(), &field_change); } -void FakeScriptExecutorDelegate::WriteUserModel( - base::OnceCallback<void(UserModel*)> write_callback) { - std::move(write_callback).Run(user_model_); -} - void FakeScriptExecutorDelegate::SetViewportMode(ViewportMode mode) { viewport_mode_ = mode; } @@ -158,6 +158,8 @@ void FakeScriptExecutorDelegate::CollapseBottomSheet() { expand_or_collapse_value_ = false; } +void FakeScriptExecutorDelegate::ExpectNavigation() {} + bool FakeScriptExecutorDelegate::HasNavigationError() { return navigation_error_; } @@ -204,8 +206,9 @@ EventHandler* FakeScriptExecutorDelegate::GetEventHandler() { void FakeScriptExecutorDelegate::SetGenericUi( std::unique_ptr<GenericUserInterfaceProto> generic_ui, - base::OnceCallback<void(bool, ProcessedActionStatusProto, const UserModel*)> - end_action_callback) {} + base::OnceCallback<void(const ClientStatus&)> end_action_callback, + base::OnceCallback<void(const ClientStatus&)> + view_inflation_finished_callback) {} void FakeScriptExecutorDelegate::ClearGenericUi() {} diff --git a/chromium/components/autofill_assistant/browser/fake_script_executor_delegate.h b/chromium/components/autofill_assistant/browser/fake_script_executor_delegate.h index 4594334faac..3948c00cc2a 100644 --- a/chromium/components/autofill_assistant/browser/fake_script_executor_delegate.h +++ b/chromium/components/autofill_assistant/browser/fake_script_executor_delegate.h @@ -46,14 +46,16 @@ class FakeScriptExecutorDelegate : public ScriptExecutorDelegate { void SetInfoBox(const InfoBox& info_box) override; void ClearInfoBox() override; void SetProgress(int progress) override; + void SetProgressActiveStep(int active_step) override; void SetProgressVisible(bool visible) override; + void SetStepProgressBarConfiguration( + const ShowProgressBarProto::StepProgressBarConfiguration& configuration) + override; void SetUserActions( std::unique_ptr<std::vector<UserAction>> user_actions) override; void SetCollectUserDataOptions(CollectUserDataOptions* options) override; void WriteUserData( base::OnceCallback<void(UserData*, UserData::FieldChange*)>) override; - void WriteUserModel( - base::OnceCallback<void(UserModel*)> write_callback) override; void SetViewportMode(ViewportMode mode) override; ViewportMode GetViewportMode() override; void SetPeekMode(ConfigureBottomSheetProto::PeekMode peek_mode) override; @@ -66,6 +68,7 @@ class FakeScriptExecutorDelegate : public ScriptExecutorDelegate { base::OnceCallback<void(const ClientStatus&)> cancel_callback) override; UserModel* GetUserModel() override; EventHandler* GetEventHandler() override; + void ExpectNavigation() override; bool HasNavigationError() override; bool IsNavigatingToNewDocument() override; void RequireUI() override; @@ -76,9 +79,9 @@ class FakeScriptExecutorDelegate : public ScriptExecutorDelegate { void SetGenericUi( std::unique_ptr<GenericUserInterfaceProto> generic_ui, - base::OnceCallback<void(bool, - ProcessedActionStatusProto, - const UserModel*)> end_action_callback) override; + base::OnceCallback<void(const ClientStatus&)> end_action_callback, + base::OnceCallback<void(const ClientStatus&)> + view_inflation_finished_callback) override; void ClearGenericUi() override; ClientSettings* GetMutableSettings() { return &client_settings_; } diff --git a/chromium/components/autofill_assistant/browser/features.cc b/chromium/components/autofill_assistant/browser/features.cc index 943ab632ad6..6f42c54b476 100644 --- a/chromium/components/autofill_assistant/browser/features.cc +++ b/chromium/components/autofill_assistant/browser/features.cc @@ -12,6 +12,14 @@ namespace features { const base::Feature kAutofillAssistant{"AutofillAssistant", base::FEATURE_ENABLED_BY_DEFAULT}; +// Guard for the end condition when a non-renderer-initiated navigation occurs +// while the AutofillAssistant is in RUNNING state. +// TODO(b/159309621): Remove this if the end condition shows no unwanted side +// effects. +const base::Feature kAutofillAssistantBreakOnRunningNavigation{ + "AutofillAssistantBreakOnRunningNavigation", + base::FEATURE_ENABLED_BY_DEFAULT}; + // Controls whether to enable Assistant Autofill in a normal Chrome tab. const base::Feature kAutofillAssistantChromeEntry{ "AutofillAssistantChromeEntry", base::FEATURE_ENABLED_BY_DEFAULT}; diff --git a/chromium/components/autofill_assistant/browser/features.h b/chromium/components/autofill_assistant/browser/features.h index 689c466b23c..0478342a905 100644 --- a/chromium/components/autofill_assistant/browser/features.h +++ b/chromium/components/autofill_assistant/browser/features.h @@ -11,8 +11,10 @@ struct Feature; namespace autofill_assistant { namespace features { + // All features in alphabetical order. extern const base::Feature kAutofillAssistant; +extern const base::Feature kAutofillAssistantBreakOnRunningNavigation; extern const base::Feature kAutofillAssistantChromeEntry; extern const base::Feature kAutofillAssistantDirectActions; diff --git a/chromium/components/autofill_assistant/browser/field_formatter.cc b/chromium/components/autofill_assistant/browser/field_formatter.cc new file mode 100644 index 00000000000..8045c89530e --- /dev/null +++ b/chromium/components/autofill_assistant/browser/field_formatter.cc @@ -0,0 +1,128 @@ +// 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/field_formatter.h" + +#include "base/logging.h" +#include "base/strings/string_number_conversions.h" +#include "base/strings/utf_string_conversions.h" +#include "components/autofill/core/browser/autofill_data_util.h" +#include "components/autofill/core/browser/autofill_type.h" +#include "components/autofill/core/browser/field_types.h" +#include "components/autofill/core/browser/geo/state_names.h" +#include "components/autofill_assistant/browser/generic_ui.pb.h" +#include "third_party/re2/src/re2/re2.h" +#include "third_party/re2/src/re2/stringpiece.h" + +namespace { +// Regex to find placeholders of the form ${key}, where key is an arbitrary +// string that does not contain curly braces. +const char kPlaceholderExtractor[] = R"re(\$\{([^{}]+)\})re"; + +base::Optional<std::string> GetFieldValue( + const std::map<std::string, std::string>& mappings, + const std::string& key) { + auto it = mappings.find(key); + if (it == mappings.end()) { + return base::nullopt; + } + return it->second; +} + +std::map<std::string, std::string> CreateFormGroupMappings( + const autofill::FormGroup& form_group, + const std::string& locale) { + std::map<std::string, std::string> mappings; + autofill::ServerFieldTypeSet available_fields; + form_group.GetNonEmptyTypes(locale, &available_fields); + for (const auto& field : available_fields) { + mappings.emplace(base::NumberToString(static_cast<int>(field)), + base::UTF16ToUTF8(form_group.GetInfo( + autofill::AutofillType(field), locale))); + } + return mappings; +} + +} // namespace + +namespace autofill_assistant { +namespace field_formatter { + +base::Optional<std::string> FormatString( + const std::string& pattern, + const std::map<std::string, std::string>& mappings) { + if (pattern.empty()) { + return std::string(); + } + + std::string key; + std::string out = pattern; + re2::StringPiece input(pattern); + while (re2::RE2::FindAndConsume(&input, kPlaceholderExtractor, &key)) { + auto rewrite_value = GetFieldValue(mappings, key); + if (!rewrite_value.has_value()) { + VLOG(2) << "No value for " << key << " in " << pattern; + return base::nullopt; + } + + re2::RE2::Replace(&out, kPlaceholderExtractor, + re2::StringPiece(rewrite_value.value())); + } + + return out; +} + +template <> +std::map<std::string, std::string> +CreateAutofillMappings<autofill::AutofillProfile>( + const autofill::AutofillProfile& profile, + const std::string& locale) { + auto mappings = CreateFormGroupMappings(profile, locale); + + auto state = profile.GetInfo( + autofill::AutofillType(autofill::ADDRESS_HOME_STATE), locale); + if (!state.empty()) { + // TODO(b/159309560): Capitalize first letter of the state name. + auto state_name = + base::UTF16ToUTF8(autofill::state_names::GetNameForAbbreviation(state)); + if (!state_name.empty()) { + mappings[base::NumberToString(static_cast<int>( + AutofillFormatProto::ADDRESS_HOME_STATE_NAME))] = state_name; + } + } + + return mappings; +} + +template <> +std::map<std::string, std::string> CreateAutofillMappings<autofill::CreditCard>( + const autofill::CreditCard& credit_card, + const std::string& locale) { + auto mappings = CreateFormGroupMappings(credit_card, locale); + + auto network = std::string( + autofill::data_util::GetPaymentRequestData(credit_card.network()) + .basic_card_issuer_network); + if (!network.empty()) { + mappings[base::NumberToString( + static_cast<int>(AutofillFormatProto::CREDIT_CARD_NETWORK))] = network; + } + auto network_for_display = base::UTF16ToUTF8(credit_card.NetworkForDisplay()); + if (!network_for_display.empty()) { + mappings[base::NumberToString(static_cast<int>( + AutofillFormatProto::CREDIT_CARD_NETWORK_FOR_DISPLAY))] = + network_for_display; + } + auto last_four_digits = base::UTF16ToUTF8(credit_card.LastFourDigits()); + if (!last_four_digits.empty()) { + mappings[base::NumberToString(static_cast<int>( + AutofillFormatProto::CREDIT_CARD_NUMBER_LAST_FOUR_DIGITS))] = + last_four_digits; + } + + return mappings; +} + +} // namespace field_formatter +} // namespace autofill_assistant diff --git a/chromium/components/autofill_assistant/browser/field_formatter.h b/chromium/components/autofill_assistant/browser/field_formatter.h new file mode 100644 index 00000000000..ce934e9132a --- /dev/null +++ b/chromium/components/autofill_assistant/browser/field_formatter.h @@ -0,0 +1,37 @@ +// 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_FIELD_FORMATTER_H_ +#define COMPONENTS_AUTOFILL_ASSISTANT_BROWSER_FIELD_FORMATTER_H_ + +#include <map> +#include <string> +#include "base/optional.h" +#include "components/autofill/core/browser/data_model/autofill_profile.h" +#include "components/autofill/core/browser/data_model/credit_card.h" + +namespace autofill_assistant { +namespace field_formatter { + +// Replaces all placeholder occurrences of the form ${key} in |input| with the +// corresponding value in |mappings|, where |key| is an arbitrary string that +// does not contain curly braces. Fails if any of the found placeholders is not +// in |mappings|. +base::Optional<std::string> FormatString( + const std::string& input, + const std::map<std::string, std::string>& mappings); + +// Creates a lookup map for all non-empty autofill and custom +// AutofillFormatProto::AutofillAssistantCustomField field types in +// |autofill_data_model|. +// |locale| should be a locale string such as "en-US". +template <typename T> +std::map<std::string, std::string> CreateAutofillMappings( + const T& autofill_data_model, + const std::string& locale); + +} // namespace field_formatter +} // namespace autofill_assistant + +#endif // COMPONENTS_AUTOFILL_ASSISTANT_BROWSER_FIELD_FORMATTER_H_ diff --git a/chromium/components/autofill_assistant/browser/field_formatter_unittest.cc b/chromium/components/autofill_assistant/browser/field_formatter_unittest.cc new file mode 100644 index 00000000000..f7bda3bdfbf --- /dev/null +++ b/chromium/components/autofill_assistant/browser/field_formatter_unittest.cc @@ -0,0 +1,209 @@ +// 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/field_formatter.h" + +#include "base/guid.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 "testing/gmock/include/gmock/gmock.h" + +namespace autofill_assistant { +namespace field_formatter { +namespace { +const char kFakeUrl[] = "https://www.example.com"; + +using ::testing::_; +using ::testing::Eq; +using ::testing::UnorderedElementsAreArray; + +TEST(FieldFormatterTest, FormatString) { + std::map<std::string, std::string> mappings = { + {"keyA", "valueA"}, {"keyB", "valueB"}, {"keyC", "valueC"}}; + + EXPECT_EQ(*FormatString("", mappings), ""); + EXPECT_EQ(*FormatString("input", mappings), "input"); + EXPECT_EQ(*FormatString("prefix ${keyA}", mappings), "prefix valueA"); + EXPECT_EQ(*FormatString("prefix ${keyA}${keyB}${keyC} suffix", mappings), + "prefix valueAvalueBvalueC suffix"); + EXPECT_EQ(*FormatString("keyA = ${keyA}", mappings), "keyA = valueA"); + EXPECT_EQ(FormatString("${keyD}", mappings), base::nullopt); + EXPECT_EQ(FormatString("${keyA}${keyD}", mappings), base::nullopt); +} + +TEST(FieldFormatterTest, AutofillProfile) { + autofill::AutofillProfile profile(base::GenerateGUID(), kFakeUrl); + autofill::test::SetProfileInfo( + &profile, "John", "", "Doe", "editor@gmail.com", "", "203 Barfield Lane", + "", "Mountain View", "CA", "94043", "US", "+12345678901"); + + // NAME_FIRST NAME_LAST + EXPECT_EQ( + *FormatString("${3} ${5}", CreateAutofillMappings(profile, "en-US")), + "John Doe"); + + // PHONE_HOME_COUNTRY_CODE, PHONE_HOME_CITY_CODE, PHONE_HOME_NUMBER + EXPECT_EQ(*FormatString("(+${12}) (${11}) ${10}", + CreateAutofillMappings(profile, "en-US")), + "(+1) (234) 5678901"); + + // ADDRESS_HOME_STATE, ADDRESS_HOME_STATE_NAME + EXPECT_EQ( + *FormatString("${34} - ${-6}", CreateAutofillMappings(profile, "en-US")), + "CA - california"); + + // Unknown state. + autofill::AutofillProfile unknown_state_profile(base::GenerateGUID(), + kFakeUrl); + autofill::test::SetProfileInfo(&unknown_state_profile, "John", "", "Doe", "", + "", "", "", "", "XY", "", "US", ""); + EXPECT_EQ(FormatString("${34}", CreateAutofillMappings(unknown_state_profile, + "en-US")), + "XY"); + EXPECT_EQ(FormatString("${-6}", CreateAutofillMappings(unknown_state_profile, + "en-US")), + base::nullopt); + + // UNKNOWN_TYPE + EXPECT_EQ(FormatString("${1}", CreateAutofillMappings(profile, "en-US")), + base::nullopt); +} + +TEST(FieldFormatterTest, CreditCard) { + autofill::CreditCard credit_card(base::GenerateGUID(), kFakeUrl); + autofill::test::SetCreditCardInfo(&credit_card, "John Doe", + "4111 1111 1111 1111", "01", "2050", ""); + + // CREDIT_CARD_NAME_FULL + EXPECT_EQ( + *FormatString("${51}", CreateAutofillMappings(credit_card, "en-US")), + "John Doe"); + + // CREDIT_CARD_NUMBER + EXPECT_EQ( + *FormatString("${52}", CreateAutofillMappings(credit_card, "en-US")), + "4111111111111111"); + + // CREDIT_CARD_NUMBER_LAST_FOUR_DIGITS + EXPECT_EQ( + *FormatString("**** ${-4}", CreateAutofillMappings(credit_card, "en-US")), + "**** 1111"); + + // CREDIT_CARD_EXP_MONTH, CREDIT_CARD_EXP_2_DIGIT_YEAR + EXPECT_EQ(*FormatString("${53}/${54}", + CreateAutofillMappings(credit_card, "en-US")), + "01/50"); + + // CREDIT_CARD_NETWORK, CREDIT_CARD_NETWORK_FOR_DISPLAY + EXPECT_EQ(*FormatString("${-2} ${-5}", + CreateAutofillMappings(credit_card, "en-US")), + "visa Visa"); +} + +TEST(FieldFormatterTest, SpecialCases) { + autofill::AutofillProfile profile(base::GenerateGUID(), kFakeUrl); + autofill::test::SetProfileInfo( + &profile, "John", "", "Doe", "editor@gmail.com", "", "203 Barfield Lane", + "", "Mountain View", "CA", "94043", "US", "+12345678901"); + + EXPECT_EQ(*FormatString("", CreateAutofillMappings(profile, "en-US")), + std::string()); + EXPECT_EQ(*FormatString("${3}", CreateAutofillMappings(profile, "en-US")), + "John"); + EXPECT_EQ(FormatString("${-1}", CreateAutofillMappings(profile, "en-US")), + base::nullopt); + EXPECT_EQ( + FormatString( + "${" + base::NumberToString(autofill::MAX_VALID_FIELD_TYPE) + "}", + CreateAutofillMappings(profile, "en-US")), + base::nullopt); + + // Second {} is not prefixed with $. + EXPECT_EQ( + *FormatString("${3} {10}", CreateAutofillMappings(profile, "en-US")), + "John {10}"); +} + +TEST(FieldFormatterTest, DifferentLocales) { + autofill::AutofillProfile profile(base::GenerateGUID(), kFakeUrl); + autofill::test::SetProfileInfo( + &profile, "John", "", "Doe", "editor@gmail.com", "", "203 Barfield Lane", + "", "Mountain View", "CA", "94043", "US", "+12345678901"); + auto mappings = CreateAutofillMappings(profile, "de-DE"); + + // 36 == ADDRESS_HOME_COUNTRY + EXPECT_EQ(*FormatString("${36}", CreateAutofillMappings(profile, "en-US")), + "United States"); + EXPECT_EQ(*FormatString("${36}", CreateAutofillMappings(profile, "de-DE")), + "Vereinigte Staaten"); + + // Invalid locales default to "en-US". + EXPECT_EQ(*FormatString("${36}", CreateAutofillMappings(profile, "")), + "United States"); + EXPECT_EQ(*FormatString("${36}", CreateAutofillMappings(profile, "invalid")), + "United States"); +} + +TEST(FieldFormatterTest, AddsAllProfileFields) { + std::map<std::string, std::string> expected_values = { + {"3", "Alpha"}, + {"4", "Beta"}, + {"5", "Gamma"}, + {"6", "B"}, + {"7", "Alpha Beta Gamma"}, + {"9", "alpha@google.com"}, + {"10", "1234567"}, + {"11", "79"}, + {"12", "41"}, + {"13", "0791234567"}, + {"14", "+41791234567"}, + {"30", "Brandschenkestrasse 110"}, + {"31", "Google Building 110"}, + {"33", "Zurich"}, + {"34", "Canton Zurich"}, + {"35", "8002"}, + {"36", "Switzerland"}, + {"60", "Google"}, + {"77", "Brandschenkestrasse 110\nGoogle Building 110"}}; + + autofill::AutofillProfile profile(base::GenerateGUID(), kFakeUrl); + autofill::test::SetProfileInfo( + &profile, "Alpha", "Beta", "Gamma", "alpha@google.com", "Google", + "Brandschenkestrasse 110", "Google Building 110", "Zurich", + "Canton Zurich", "8002", "CH", "+41791234567"); + + EXPECT_THAT(CreateAutofillMappings(profile, "en-US"), + UnorderedElementsAreArray(expected_values)); +} + +TEST(FieldFormatterTest, AddsAllCreditCardFields) { + std::map<std::string, std::string> expected_values = { + {"-5", "Visa"}, + {"-4", "1111"}, + {"-2", "visa"}, + {"51", "Alpha Beta Gamma"}, + {"52", "4111111111111111"}, + {"53", "08"}, + {"54", "50"}, + {"55", "2050"}, + {"56", "08/50"}, + {"57", "08/2050"}, + {"58", "Visa"}, + {"91", "Alpha"}, + {"92", "Gamma"}}; + + autofill::CreditCard credit_card(base::GenerateGUID(), kFakeUrl); + autofill::test::SetCreditCardInfo(&credit_card, "Alpha Beta Gamma", + "4111111111111111", "8", "2050", ""); + + EXPECT_THAT(CreateAutofillMappings(credit_card, "en-US"), + UnorderedElementsAreArray(expected_values)); +} + +} // namespace +} // namespace field_formatter +} // namespace autofill_assistant diff --git a/chromium/components/autofill_assistant/browser/generic_ui.proto b/chromium/components/autofill_assistant/browser/generic_ui.proto index d87228400a7..e4031ba99c7 100644 --- a/chromium/components/autofill_assistant/browser/generic_ui.proto +++ b/chromium/components/autofill_assistant/browser/generic_ui.proto @@ -49,6 +49,9 @@ message CallbackProto { SetViewVisibilityProto set_view_visibility = 11; SetViewEnabledProto set_view_enabled = 12; ShowGenericUiPopupProto show_generic_popup = 13; + CreateNestedGenericUiProto create_nested_ui = 14; + ClearViewContainerProto clear_view_container = 15; + ForEachProto for_each = 16; } // Optional model identifier pointing to a single boolean. If set, the // callback will only be invoked if the condition is true. @@ -62,6 +65,7 @@ message EventProto { OnUserActionCalled on_user_action_called = 3; OnTextLinkClickedProto on_text_link_clicked = 4; OnPopupDismissedProto on_popup_dismissed = 5; + OnViewContainerClearedProto on_view_container_cleared = 6; } } @@ -96,6 +100,13 @@ message OnPopupDismissedProto { optional string popup_identifier = 1; } +// Event which is called whenever |view_identifier| was cleared with a +// |ClearViewContainer| interaction. +message OnViewContainerClearedProto { + // The view container that was cleared. + optional string view_identifier = 1; +} + // Callback that writes the specified value to |model_identifier|. message SetModelValueProto { // The model identifier to write to. @@ -158,7 +169,10 @@ message ToStringProto { optional ValueReferenceProto value = 3; // Optional format options. - oneof format_options { DateFormatProto date_format = 2; } + oneof format_options { + DateFormatProto date_format = 2; + AutofillFormatProto autofill_format = 4; + } reserved 1; } @@ -168,6 +182,32 @@ message DateFormatProto { optional string date_format = 1; } +// Format options for autofill profiles and credit cards. +message AutofillFormatProto { + // Fields we add that are not mapped to field_types.h. The values must be + // negative to avoid overlap with field_types.h. + // Note: CREDIT_CARD_VERIFICATION_CODE and CREDIT_CARD_RAW_NUMBER are only + // available in UseCreditCardAction, as they require the CVC dialog to be + // shown. + enum AutofillAssistantCustomField { + UNDEFINED = 0; + CREDIT_CARD_VERIFICATION_CODE = -1; + CREDIT_CARD_NETWORK = -2; + CREDIT_CARD_RAW_NUMBER = -3; + CREDIT_CARD_NUMBER_LAST_FOUR_DIGITS = -4; + CREDIT_CARD_NETWORK_FOR_DISPLAY = -5; + // Currently only US states are supported. The state name is in lower case. + ADDRESS_HOME_STATE_NAME = -6; + } + // The format string to use. May contain one or multiple "${key}" + // placeholders, where the key is an integer corresponding to + // entries from field_types.h or AutofillAssistantCustomField. + optional string pattern = 1; + // The locale to use when stringifying data. Invalid locales will + // automatically default to "en-US". + optional string locale = 2; +} + // A comparison of two values in the form |value_a| <mode> |value_b|. EQUAL is // supported for all values. All other comparison modes are only supported for // single integers, strings, and dates. @@ -331,3 +371,42 @@ message SetViewEnabledProto { // Whether the view should be enabled or not. Must point to a single boolean. optional ValueReferenceProto enabled = 2; } + +// Creates new UI and interactions, i.e., a nested instance and attaches it to +// |parent_view_identifier|. Nested UI instances have their own view layout and +// interactions, but share the same model with their parent. All interactions +// between ancestor and descendant UIs must pass through the user model, as +// neither can directly reference views or interactions of the other. +// +// Use |ClearViewContainerProto| on a nested UI's view parent to detach and +// destroy the nested UI. +message CreateNestedGenericUiProto { + // Identifier for the newly created instance. + optional string generic_ui_identifier = 1; + // The UI to create. + optional GenericUserInterfaceProto generic_ui = 2; + // The view identifier of the parent to attach (=append) this UI to. + optional string parent_view_identifier = 3; +} + +// Removes all views from the target view container. Nested UI instances +// attached to |view_identifier| will be destroyed. +message ClearViewContainerProto { + // The view container to clear. + optional string view_identifier = 1; +} + +// Invokes |callbacks| for each item in the loop value. Automatically replaces +// instances of "${i}" in model and view identifiers with the loop counter. +message ForEachProto { + // The loop counter, usually "i", "j", etc. Callbacks may use this counter + // in view and model identifiers by using "${i}"" placeholders, e.g., + // "profiles[${i}]" or "my_view_${i}". + optional string loop_counter = 1; + + // The value list to loop over. + optional string loop_value_model_identifier = 2; + + // The callbacks to invoke for every iteration of the loop. + repeated CallbackProto callbacks = 3; +} diff --git a/chromium/components/autofill_assistant/browser/generic_ui_java_generated_enums.h b/chromium/components/autofill_assistant/browser/generic_ui_java_generated_enums.h index 29539e5368a..23d29a1a570 100644 --- a/chromium/components/autofill_assistant/browser/generic_ui_java_generated_enums.h +++ b/chromium/components/autofill_assistant/browser/generic_ui_java_generated_enums.h @@ -19,6 +19,26 @@ enum class VerticalExpanderChevronStyle { NEVER = 2, }; +// GENERATED_JAVA_ENUM_PACKAGE: ( +// org.chromium.chrome.browser.autofill_assistant.drawable) +// GENERATED_JAVA_CLASS_NAME_OVERRIDE: AssistantDrawableIcon +enum class DrawableIcon { + DRAWABLE_ICON_UNDEFINED = 0, + PROGRESSBAR_DEFAULT_INITIAL_STEP = 1, + PROGRESSBAR_DEFAULT_DATA_COLLECTION = 2, + PROGRESSBAR_DEFAULT_PAYMENT = 3, + PROGRESSBAR_DEFAULT_FINAL_STEP = 4, + SITTING_PERSON = 5, + TICKET_STUB = 6, + SHOPPING_BASKET = 7, + FAST_FOOD = 8, + LOCAL_DINING = 9, + COGWHEEL = 10, + KEY = 11, + CAR = 12, + GROCERY = 13, +}; + } // namespace autofill_assistant #endif // COMPONENTS_AUTOFILL_ASSISTANT_BROWSER_GENERIC_UI_JAVA_GENERATED_ENUMS_H_ diff --git a/chromium/components/autofill_assistant/browser/metrics.h b/chromium/components/autofill_assistant/browser/metrics.h index 8eabecca360..ff8a84c225a 100644 --- a/chromium/components/autofill_assistant/browser/metrics.h +++ b/chromium/components/autofill_assistant/browser/metrics.h @@ -45,8 +45,11 @@ class Metrics { NO_INITIAL_SCRIPTS = 19, DFM_INSTALL_FAILED = 20, DOMAIN_CHANGE_DURING_BROWSE_MODE = 21, + BACK_BUTTON_CLICKED = 22, + ONBOARDING_BACK_BUTTON_CLICKED = 23, + NAVIGATION_WHILE_RUNNING = 24, - kMaxValue = DOMAIN_CHANGE_DURING_BROWSE_MODE + kMaxValue = NAVIGATION_WHILE_RUNNING }; // The different ways that autofill assistant can stop. @@ -64,8 +67,9 @@ class Metrics { OB_NOT_SHOWN = 1, OB_ACCEPTED = 2, OB_CANCELLED = 3, + OB_NO_ANSWER = 4, - kMaxValue = OB_CANCELLED + kMaxValue = OB_NO_ANSWER }; // The different ways for payment request to succeed or fail, broken down by @@ -222,6 +226,15 @@ class Metrics { case DropOutReason::DOMAIN_CHANGE_DURING_BROWSE_MODE: out << "DOMAIN_CHANGE_DURING_BROWSE_MODE"; break; + case DropOutReason::BACK_BUTTON_CLICKED: + out << "BACK_BUTTON_CLICKED"; + break; + case DropOutReason::ONBOARDING_BACK_BUTTON_CLICKED: + out << "ONBOARDING_BACK_BUTTON_CLICKED"; + break; + case DropOutReason::NAVIGATION_WHILE_RUNNING: + out << "NAVIGATION_WHILE_RUNNING"; + break; // Do not add default case to force compilation error for new values. } return out; @@ -249,6 +262,9 @@ class Metrics { case OnBoarding::OB_CANCELLED: out << "OB_CANCELLED"; break; + case OnBoarding::OB_NO_ANSWER: + out << "OB_NO_ANSWER"; + break; // Do not add default case to force compilation error for new values. } return out; diff --git a/chromium/components/autofill_assistant/browser/mock_client.h b/chromium/components/autofill_assistant/browser/mock_client.h index b96f0c72eb9..a83fe7ab273 100644 --- a/chromium/components/autofill_assistant/browser/mock_client.h +++ b/chromium/components/autofill_assistant/browser/mock_client.h @@ -25,6 +25,7 @@ class MockClient : public Client { MOCK_CONST_METHOD0(GetLocale, std::string()); MOCK_CONST_METHOD0(GetCountryCode, std::string()); MOCK_CONST_METHOD0(GetDeviceContext, DeviceContext()); + MOCK_CONST_METHOD0(IsAccessibilityEnabled, bool()); MOCK_CONST_METHOD0(GetEmailAddressForAccessTokenAccount, std::string()); MOCK_CONST_METHOD0(GetChromeSignedInEmailAddress, std::string()); MOCK_CONST_METHOD0(GetWebContents, content::WebContents*()); diff --git a/chromium/components/autofill_assistant/browser/mock_controller_observer.h b/chromium/components/autofill_assistant/browser/mock_controller_observer.h index be6ae4d2d51..8cd6fc4f8d7 100644 --- a/chromium/components/autofill_assistant/browser/mock_controller_observer.h +++ b/chromium/components/autofill_assistant/browser/mock_controller_observer.h @@ -37,7 +37,12 @@ class MockControllerObserver : public ControllerObserver { MOCK_METHOD1(OnDetailsChanged, void(const Details* details)); MOCK_METHOD1(OnInfoBoxChanged, void(const InfoBox* info_box)); MOCK_METHOD1(OnProgressChanged, void(int progress)); + MOCK_METHOD1(OnProgressActiveStepChanged, void(int active_step)); MOCK_METHOD1(OnProgressVisibilityChanged, void(bool visible)); + MOCK_METHOD1(OnStepProgressBarConfigurationChanged, + void(const ShowProgressBarProto::StepProgressBarConfiguration& + configuration)); + MOCK_METHOD1(OnProgressBarErrorStateChanged, void(bool error)); MOCK_METHOD3(OnTouchableAreaChanged, void(const RectF&, const std::vector<RectF>& touchable_areas, diff --git a/chromium/components/autofill_assistant/browser/model.proto b/chromium/components/autofill_assistant/browser/model.proto index 607825be75d..7e3547ed105 100644 --- a/chromium/components/autofill_assistant/browser/model.proto +++ b/chromium/components/autofill_assistant/browser/model.proto @@ -33,13 +33,15 @@ message ValueProto { ProfileList profiles = 8; LoginOptionList login_options = 9; CreditCardResponseProto credit_card_response = 10; - LoginOptionResponseProto login_option_response = 11; + bytes server_payload = 12; } // If set to true, this value will not be sent to the backend. This flag is // mostly used internally, to prevent certain values from leaving the device. // Note that values derived from client_side_only values will inherit this // property. optional bool is_client_side_only = 6; + + reserved 11; } message ValueReferenceProto { @@ -192,6 +194,10 @@ enum ProcessedActionStatusProto { // ProcessedActionStatusProto.AutofillErrorInfoProto contains more details. AUTOFILL_INCOMPLETE = 24; + // Evaluating a selector required considering too many candidates. Please + // generate more specific selectors. See SelectorProto::ProximityFilter. + TOO_MANY_CANDIDATES = 25; + reserved 15, 23; } @@ -337,7 +343,3 @@ message LoginOptionProto { optional string sublabel = 2; optional bytes payload = 3; } - -message LoginOptionResponseProto { - optional bytes payload = 1; -} diff --git a/chromium/components/autofill_assistant/browser/radio_button_controller.cc b/chromium/components/autofill_assistant/browser/radio_button_controller.cc new file mode 100644 index 00000000000..b905bb39e7a --- /dev/null +++ b/chromium/components/autofill_assistant/browser/radio_button_controller.cc @@ -0,0 +1,44 @@ +// 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/radio_button_controller.h" + +namespace autofill_assistant { + +RadioButtonController::RadioButtonController(UserModel* user_model) + : user_model_(user_model) {} +RadioButtonController::~RadioButtonController() = default; + +base::WeakPtr<RadioButtonController> RadioButtonController::GetWeakPtr() { + return weak_ptr_factory_.GetWeakPtr(); +} + +void RadioButtonController::AddRadioButtonToGroup( + const std::string& radio_group, + const std::string& model_identifier) { + radio_groups_[radio_group].insert(model_identifier); +} + +bool RadioButtonController::UpdateRadioButtonGroup( + const std::string& radio_group, + const std::string& selected_model_identifier) { + auto radio_group_it = radio_groups_.find(radio_group); + if (radio_group_it == radio_groups_.end()) { + return false; + } + + if (radio_group_it->second.find(selected_model_identifier) == + radio_group_it->second.end()) { + return false; + } + + for (const auto& model_identifier : radio_group_it->second) { + user_model_->SetValue( + model_identifier, + SimpleValue(model_identifier == selected_model_identifier)); + } + return true; +} + +} // namespace autofill_assistant diff --git a/chromium/components/autofill_assistant/browser/radio_button_controller.h b/chromium/components/autofill_assistant/browser/radio_button_controller.h new file mode 100644 index 00000000000..b9287ea91dd --- /dev/null +++ b/chromium/components/autofill_assistant/browser/radio_button_controller.h @@ -0,0 +1,49 @@ +// 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_RADIO_BUTTON_CONTROLLER_H_ +#define COMPONENTS_AUTOFILL_ASSISTANT_BROWSER_RADIO_BUTTON_CONTROLLER_H_ + +#include <map> +#include <set> +#include <string> + +#include "base/memory/weak_ptr.h" +#include "components/autofill_assistant/browser/user_model.h" + +namespace autofill_assistant { + +class RadioButtonController { + public: + RadioButtonController(UserModel* user_model); + ~RadioButtonController(); + RadioButtonController(const RadioButtonController&) = delete; + RadioButtonController& operator=(const RadioButtonController&) = delete; + + base::WeakPtr<RadioButtonController> GetWeakPtr(); + + // Adds |model_identifier| to the list of model identifiers belonging to + // |radio_group|. + void AddRadioButtonToGroup(const std::string& radio_group, + const std::string& model_identifier); + + // Ensures that only |selected_model_identifier| is set to true in + // |radio_group|. + bool UpdateRadioButtonGroup(const std::string& radio_group, + const std::string& selected_model_identifier); + + private: + // Maps radiogroup identifiers to the list of corresponding model identifiers. + std::map<std::string, std::set<std::string>> radio_groups_; + + private: + friend class RadioButtonControllerTest; + + UserModel* user_model_; + base::WeakPtrFactory<RadioButtonController> weak_ptr_factory_{this}; +}; + +} // namespace autofill_assistant + +#endif // COMPONENTS_AUTOFILL_ASSISTANT_BROWSER_RADIO_BUTTON_CONTROLLER_H_ diff --git a/chromium/components/autofill_assistant/browser/radio_button_controller_unittest.cc b/chromium/components/autofill_assistant/browser/radio_button_controller_unittest.cc new file mode 100644 index 00000000000..5de656a82c4 --- /dev/null +++ b/chromium/components/autofill_assistant/browser/radio_button_controller_unittest.cc @@ -0,0 +1,77 @@ +// 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/radio_button_controller.h" +#include "components/autofill_assistant/browser/user_model.h" +#include "testing/gmock/include/gmock/gmock.h" + +namespace autofill_assistant { + +using ::testing::Pair; +using ::testing::SizeIs; +using ::testing::UnorderedElementsAre; + +class RadioButtonControllerTest : public testing::Test { + protected: + RadioButtonControllerTest() : controller_(&user_model_) {} + ~RadioButtonControllerTest() override {} + + std::map<std::string, std::set<std::string>> GetRadioGroups() { + return controller_.radio_groups_; + } + + UserModel user_model_; + RadioButtonController controller_; +}; + +TEST_F(RadioButtonControllerTest, AddRadioButtonToGroup) { + EXPECT_THAT(GetRadioGroups(), SizeIs(0)); + controller_.AddRadioButtonToGroup("group_1", "id_1"); + EXPECT_THAT(GetRadioGroups(), UnorderedElementsAre(Pair( + "group_1", std::set<std::string>{"id_1"}))); + + controller_.AddRadioButtonToGroup("group_1", "id_1"); + EXPECT_THAT(GetRadioGroups(), UnorderedElementsAre(Pair( + "group_1", std::set<std::string>{"id_1"}))); + + controller_.AddRadioButtonToGroup("group_1", "id_2"); + EXPECT_THAT(GetRadioGroups(), + UnorderedElementsAre( + Pair("group_1", std::set<std::string>{"id_1", "id_2"}))); + + controller_.AddRadioButtonToGroup("group_2", "id_3"); + EXPECT_THAT(GetRadioGroups(), + UnorderedElementsAre( + Pair("group_1", std::set<std::string>{"id_1", "id_2"}), + Pair("group_2", std::set<std::string>{"id_3"}))); +} + +TEST_F(RadioButtonControllerTest, UpdateRadioButtonGroup) { + controller_.AddRadioButtonToGroup("group_1", "id_1"); + controller_.AddRadioButtonToGroup("group_1", "id_2"); + controller_.AddRadioButtonToGroup("group_1", "id_3"); + controller_.AddRadioButtonToGroup("group_2", "id_4"); + controller_.AddRadioButtonToGroup("group_2", "id_5"); + + EXPECT_FALSE(controller_.UpdateRadioButtonGroup("does_not_exist", "id_1")); + + EXPECT_TRUE(controller_.UpdateRadioButtonGroup("group_1", "id_1")); + EXPECT_EQ(user_model_.GetValue("id_1"), SimpleValue(true)); + EXPECT_EQ(user_model_.GetValue("id_2"), SimpleValue(false)); + EXPECT_EQ(user_model_.GetValue("id_3"), SimpleValue(false)); + + EXPECT_FALSE(controller_.UpdateRadioButtonGroup("group_1", "does_not_exist")); + EXPECT_EQ(user_model_.GetValue("id_1"), SimpleValue(true)); + EXPECT_EQ(user_model_.GetValue("id_2"), SimpleValue(false)); + EXPECT_EQ(user_model_.GetValue("id_3"), SimpleValue(false)); + + EXPECT_TRUE(controller_.UpdateRadioButtonGroup("group_2", "id_5")); + EXPECT_EQ(user_model_.GetValue("id_1"), SimpleValue(true)); + EXPECT_EQ(user_model_.GetValue("id_2"), SimpleValue(false)); + EXPECT_EQ(user_model_.GetValue("id_3"), SimpleValue(false)); + EXPECT_EQ(user_model_.GetValue("id_4"), SimpleValue(false)); + EXPECT_EQ(user_model_.GetValue("id_5"), SimpleValue(true)); +} + +} // namespace autofill_assistant diff --git a/chromium/components/autofill_assistant/browser/script_executor.cc b/chromium/components/autofill_assistant/browser/script_executor.cc index a0c76ad340e..5125c48d81b 100644 --- a/chromium/components/autofill_assistant/browser/script_executor.cc +++ b/chromium/components/autofill_assistant/browser/script_executor.cc @@ -104,7 +104,12 @@ void ScriptExecutor::Run(const UserData* user_data, callback_ = std::move(callback); DCHECK(delegate_->GetService()); +#ifdef NDEBUG + VLOG(2) << "GetActions for (redacted)"; +#else VLOG(2) << "GetActions for " << delegate_->GetCurrentURL().host(); +#endif + delegate_->GetService()->GetActions( script_path_, delegate_->GetScriptURL(), MergedTriggerContext( @@ -119,6 +124,10 @@ const UserData* ScriptExecutor::GetUserData() const { return user_data_; } +UserModel* ScriptExecutor::GetUserModel() { + return delegate_->GetUserModel(); +} + void ScriptExecutor::OnNavigationStateChanged() { NavigationInfoProto& navigation_info = current_action_data_.navigation_info; if (delegate_->IsNavigatingToNewDocument()) { @@ -150,7 +159,9 @@ void ScriptExecutor::OnNavigationStateChanged() { std::move(on_expected_navigation_done_) .Run(!delegate_->HasNavigationError()); } - break; + // Early return since current_action_data_ is no longer valid at this + // point. + return; case ExpectedNavigationStep::DONE: // nothing to do @@ -242,11 +253,6 @@ void ScriptExecutor::WriteUserData( delegate_->WriteUserData(std::move(write_callback)); } -void ScriptExecutor::WriteUserModel( - base::OnceCallback<void(UserModel*)> write_callback) { - delegate_->WriteUserModel(std::move(write_callback)); -} - void ScriptExecutor::OnGetUserData( base::OnceCallback<void(UserData*, const UserModel*)> callback, UserData* user_data, @@ -274,8 +280,11 @@ void ScriptExecutor::OnTermsAndConditionsLinkClicked( std::move(callback).Run(link, user_data, user_model); } -void ScriptExecutor::GetFullCard(GetFullCardCallback callback) { - DCHECK(GetUserData()->selected_card_.get()); +void ScriptExecutor::GetFullCard( + const autofill::CreditCard* credit_card, + base::OnceCallback<void(std::unique_ptr<autofill::CreditCard> card, + const base::string16& cvc)> callback) { + DCHECK(credit_card); // User might be asked to provide the cvc. delegate_->EnterState(AutofillAssistantState::MODAL_DIALOG); @@ -284,7 +293,7 @@ void ScriptExecutor::GetFullCard(GetFullCardCallback callback) { // so as to unit test it. (new SelfDeleteFullCardRequester()) ->GetFullCard( - GetWebContents(), GetUserData()->selected_card_.get(), + GetWebContents(), credit_card, base::BindOnce(&ScriptExecutor::OnGetFullCard, weak_ptr_factory_.GetWeakPtr(), std::move(callback))); } @@ -422,10 +431,19 @@ void ScriptExecutor::SetProgress(int progress) { delegate_->SetProgress(progress); } +void ScriptExecutor::SetProgressActiveStep(int active_step) { + delegate_->SetProgressActiveStep(active_step); +} + void ScriptExecutor::SetProgressVisible(bool visible) { delegate_->SetProgressVisible(visible); } +void ScriptExecutor::SetStepProgressBarConfiguration( + const ShowProgressBarProto::StepProgressBarConfiguration& configuration) { + delegate_->SetStepProgressBarConfiguration(configuration); +} + void ScriptExecutor::GetFieldValue( const Selector& selector, base::OnceCallback<void(const ClientStatus&, const std::string&)> @@ -478,6 +496,9 @@ void ScriptExecutor::GetElementTag( } void ScriptExecutor::ExpectNavigation() { + // TODO(b/160948417): Clean this up such that the logic is not required in + // both |ScriptExecutor| and |Controller|. + delegate_->ExpectNavigation(); expected_navigation_step_ = ExpectedNavigationStep::EXPECTED; } @@ -622,10 +643,11 @@ void ScriptExecutor::RequireUI() { void ScriptExecutor::SetGenericUi( std::unique_ptr<GenericUserInterfaceProto> generic_ui, - base::OnceCallback<void(bool, ProcessedActionStatusProto, const UserModel*)> - end_action_callback) { - delegate_->SetGenericUi(std::move(generic_ui), - std::move(end_action_callback)); + base::OnceCallback<void(const ClientStatus&)> end_action_callback, + base::OnceCallback<void(const ClientStatus&)> + view_inflation_finished_callback) { + delegate_->SetGenericUi(std::move(generic_ui), std::move(end_action_callback), + std::move(view_inflation_finished_callback)); } void ScriptExecutor::ClearGenericUi() { @@ -771,6 +793,7 @@ void ScriptExecutor::OnProcessedAction( processed_action.set_direct_action(current_action_data_.direct_action); *processed_action.mutable_navigation_info() = current_action_data_.navigation_info; + if (processed_action.status() != ProcessedActionStatusProto::ACTION_APPLIED) { if (delegate_->HasNavigationError()) { // Overwrite the original error, as the root cause is most likely a @@ -848,9 +871,6 @@ ScriptExecutor::WaitForDomOperation::~WaitForDomOperation() { void ScriptExecutor::WaitForDomOperation::Run() { delegate_->AddListener(this); - if (delegate_->IsNavigatingToNewDocument()) - return; // start paused - Start(); } diff --git a/chromium/components/autofill_assistant/browser/script_executor.h b/chromium/components/autofill_assistant/browser/script_executor.h index ea4c6f4b6a7..42fe159a5fc 100644 --- a/chromium/components/autofill_assistant/browser/script_executor.h +++ b/chromium/components/autofill_assistant/browser/script_executor.h @@ -95,6 +95,7 @@ class ScriptExecutor : public ActionDelegate, void Run(const UserData* user_data, RunScriptCallback callback); const UserData* GetUserData() const override; + UserModel* GetUserModel() override; // Override ScriptExecutorDelegate::Listener void OnNavigationStateChanged() override; @@ -123,9 +124,8 @@ class ScriptExecutor : public ActionDelegate, CollectUserDataOptions* collect_user_data_options) override; void WriteUserData( base::OnceCallback<void(UserData*, UserData::FieldChange*)>) override; - void WriteUserModel( - base::OnceCallback<void(UserModel*)> write_callback) override; - void GetFullCard(GetFullCardCallback callback) override; + void GetFullCard(const autofill::CreditCard* credit_card, + GetFullCardCallback callback) override; void Prompt(std::unique_ptr<std::vector<UserAction>> user_actions, bool disable_force_expand_sheet, base::OnceCallback<void()> end_on_navigation_callback, @@ -214,7 +214,11 @@ class ScriptExecutor : public ActionDelegate, void ClearInfoBox() override; void SetInfoBox(const InfoBox& info_box) override; void SetProgress(int progress) override; + void SetProgressActiveStep(int active_step) override; void SetProgressVisible(bool visible) override; + void SetStepProgressBarConfiguration( + const ShowProgressBarProto::StepProgressBarConfiguration& configuration) + override; void SetViewportMode(ViewportMode mode) override; ViewportMode GetViewportMode() override; void SetPeekMode(ConfigureBottomSheetProto::PeekMode peek_mode) override; @@ -231,9 +235,9 @@ class ScriptExecutor : public ActionDelegate, void RequireUI() override; void SetGenericUi( std::unique_ptr<GenericUserInterfaceProto> generic_ui, - base::OnceCallback<void(bool, - ProcessedActionStatusProto, - const UserModel*)> end_action_callback) override; + base::OnceCallback<void(const ClientStatus&)> end_action_callback, + base::OnceCallback<void(const ClientStatus&)> + view_inflation_finished_callback) override; void ClearGenericUi() override; private: diff --git a/chromium/components/autofill_assistant/browser/script_executor_delegate.h b/chromium/components/autofill_assistant/browser/script_executor_delegate.h index 0cf42b6253b..7b646ed3b3b 100644 --- a/chromium/components/autofill_assistant/browser/script_executor_delegate.h +++ b/chromium/components/autofill_assistant/browser/script_executor_delegate.h @@ -78,10 +78,12 @@ class ScriptExecutorDelegate { virtual void WriteUserData( base::OnceCallback<void(UserData*, UserData::FieldChange*)> write_callback) = 0; - virtual void WriteUserModel( - base::OnceCallback<void(UserModel*)> write_callback) = 0; virtual void SetProgress(int progress) = 0; + virtual void SetProgressActiveStep(int active_step) = 0; virtual void SetProgressVisible(bool visible) = 0; + virtual void SetStepProgressBarConfiguration( + const ShowProgressBarProto::StepProgressBarConfiguration& + configuration) = 0; virtual void SetUserActions( std::unique_ptr<std::vector<UserAction>> user_action) = 0; virtual ViewportMode GetViewportMode() = 0; @@ -102,6 +104,9 @@ class ScriptExecutorDelegate { SetTouchableElementArea(ElementAreaProto::default_instance()); } + // The next navigation is expected and will not cause an error. + virtual void ExpectNavigation() = 0; + // Returns true if a new document is being fetched for the main frame. // // Navigation ends once a response, with its associated URL has been @@ -147,9 +152,9 @@ class ScriptExecutorDelegate { // Sets the generic UI to show to the user. virtual void SetGenericUi( std::unique_ptr<GenericUserInterfaceProto> generic_ui, - base::OnceCallback<void(bool, - ProcessedActionStatusProto, - const UserModel*)> end_action_callback) = 0; + base::OnceCallback<void(const ClientStatus&)> end_action_callback, + base::OnceCallback<void(const ClientStatus&)> + view_inflation_finished_callback) = 0; // Clears the generic UI. virtual void ClearGenericUi() = 0; diff --git a/chromium/components/autofill_assistant/browser/script_executor_unittest.cc b/chromium/components/autofill_assistant/browser/script_executor_unittest.cc index 6a7eca439e7..571d73aadc9 100644 --- a/chromium/components/autofill_assistant/browser/script_executor_unittest.cc +++ b/chromium/components/autofill_assistant/browser/script_executor_unittest.cc @@ -105,8 +105,8 @@ class ScriptExecutorTest : public testing::Test, interruptible.set_global_payload("main script global payload"); interruptible.set_script_payload("main script payload"); auto* wait_action = interruptible.add_actions()->mutable_wait_for_dom(); - wait_action->mutable_wait_condition()->mutable_match()->add_selectors( - element); + *wait_action->mutable_wait_condition()->mutable_match() = + ToSelectorProto(element); wait_action->set_allow_interrupt(true); interruptible.add_actions()->mutable_tell()->set_message(path); EXPECT_CALL(mock_service_, OnGetActions(StrEq(path), _, _, _, _, _)) @@ -136,9 +136,8 @@ class ScriptExecutorTest : public testing::Test, auto interrupt = std::make_unique<Script>(); interrupt->handle.path = path; ScriptPreconditionProto interrupt_preconditions; - interrupt_preconditions.mutable_element_condition() - ->mutable_match() - ->add_selectors(trigger); + *interrupt_preconditions.mutable_element_condition()->mutable_match() = + ToSelectorProto(trigger); interrupt->precondition = ScriptPrecondition::FromProto(path, interrupt_preconditions); @@ -207,10 +206,8 @@ TEST_F(ScriptExecutorTest, ForwardParameters) { TEST_F(ScriptExecutorTest, RunOneActionReportAndReturn) { ActionsResponseProto actions_response; - actions_response.add_actions() - ->mutable_click() - ->mutable_element_to_click() - ->add_selectors("will fail"); + *actions_response.add_actions()->mutable_click()->mutable_element_to_click() = + ToSelectorProto("will fail"); EXPECT_CALL(mock_service_, OnGetActions(_, _, _, _, _, _)) .WillOnce(RunOnceCallback<5>(true, Serialize(actions_response))); @@ -295,10 +292,9 @@ TEST_F(ScriptExecutorTest, InterruptActionListOnError) { ActionsResponseProto initial_actions_response; initial_actions_response.add_actions()->mutable_tell()->set_message( "will pass"); - initial_actions_response.add_actions() - ->mutable_click() - ->mutable_element_to_click() - ->add_selectors("will fail"); + *initial_actions_response.add_actions() + ->mutable_click() + ->mutable_element_to_click() = ToSelectorProto("will fail"); initial_actions_response.add_actions()->mutable_tell()->set_message( "never run"); @@ -496,8 +492,8 @@ TEST_F(ScriptExecutorTest, ForwardLastPayloadOnError) { TEST_F(ScriptExecutorTest, WaitForDomWaitUntil) { ActionsResponseProto actions_response; auto* wait_for_dom = actions_response.add_actions()->mutable_wait_for_dom(); - wait_for_dom->mutable_wait_condition()->mutable_match()->add_selectors( - "element"); + *wait_for_dom->mutable_wait_condition()->mutable_match() = + ToSelectorProto("element"); EXPECT_CALL(mock_service_, OnGetActions(_, _, _, _, _, _)) .WillOnce(RunOnceCallback<5>(true, Serialize(actions_response))); @@ -599,8 +595,8 @@ TEST_F(ScriptExecutorTest, RunSameInterruptMultipleTimes) { ActionsResponseProto interruptible; for (int i = 0; i < 3; i++) { auto* wait_action = interruptible.add_actions()->mutable_wait_for_dom(); - wait_action->mutable_wait_condition()->mutable_match()->add_selectors( - "element"); + *wait_action->mutable_wait_condition()->mutable_match() = + ToSelectorProto("element"); wait_action->set_allow_interrupt(true); } EXPECT_CALL(mock_service_, OnGetActions(StrEq("script_path"), _, _, _, _, _)) @@ -703,8 +699,8 @@ TEST_F(ScriptExecutorTest, DoNotRunInterruptIfNotInterruptible) { // The main script has a wait_for_dom, but it is not interruptible. ActionsResponseProto interruptible; auto* wait_action = interruptible.add_actions()->mutable_wait_for_dom(); - wait_action->mutable_wait_condition()->mutable_match()->add_selectors( - "element"); + *wait_action->mutable_wait_condition()->mutable_match() = + ToSelectorProto("element"); // allow_interrupt is not set EXPECT_CALL(mock_service_, OnGetActions(StrEq(kScriptPath), _, _, _, _, _)) .WillOnce(RunOnceCallback<5>(true, Serialize(interruptible))); @@ -796,10 +792,8 @@ TEST_F(ScriptExecutorTest, RunInterruptDuringPrompt) { ActionsResponseProto interruptible; auto* prompt_action = interruptible.add_actions()->mutable_prompt(); prompt_action->set_allow_interrupt(true); - prompt_action->add_choices() - ->mutable_auto_select_when() - ->mutable_match() - ->add_selectors("end_prompt"); + *prompt_action->add_choices()->mutable_auto_select_when()->mutable_match() = + ToSelectorProto("end_prompt"); interruptible.add_actions()->mutable_tell()->set_message("done"); EXPECT_CALL(mock_service_, OnGetActions(kScriptPath, _, _, _, _, _)) .WillRepeatedly(RunOnceCallback<5>(true, Serialize(interruptible))); @@ -876,10 +870,8 @@ TEST_F(ScriptExecutorTest, RunInterruptMultipleTimesDuringPrompt) { ActionsResponseProto interruptible; auto* prompt_action = interruptible.add_actions()->mutable_prompt(); prompt_action->set_allow_interrupt(true); - prompt_action->add_choices() - ->mutable_auto_select_when() - ->mutable_match() - ->add_selectors("end_prompt"); + *prompt_action->add_choices()->mutable_auto_select_when()->mutable_match() = + ToSelectorProto("end_prompt"); EXPECT_CALL(mock_service_, OnGetActions(kScriptPath, _, _, _, _, _)) .WillRepeatedly(RunOnceCallback<5>(true, Serialize(interruptible))); @@ -1041,8 +1033,8 @@ TEST_F(ScriptExecutorTest, RestorePreInterruptStatusMessage) { interruptible.add_actions()->mutable_tell()->set_message( "pre-interrupt status"); auto* wait_action = interruptible.add_actions()->mutable_wait_for_dom(); - wait_action->mutable_wait_condition()->mutable_match()->add_selectors( - "element"); + *wait_action->mutable_wait_condition()->mutable_match() = + ToSelectorProto("element"); wait_action->set_allow_interrupt(true); EXPECT_CALL(mock_service_, OnGetActions(kScriptPath, _, _, _, _, _)) .WillRepeatedly(RunOnceCallback<5>(true, Serialize(interruptible))); @@ -1070,8 +1062,8 @@ TEST_F(ScriptExecutorTest, KeepStatusMessageWhenNotInterrupted) { interruptible.add_actions()->mutable_tell()->set_message( "pre-interrupt status"); auto* wait_action = interruptible.add_actions()->mutable_wait_for_dom(); - wait_action->mutable_wait_condition()->mutable_match()->add_selectors( - "element"); + *wait_action->mutable_wait_condition()->mutable_match() = + ToSelectorProto("element"); wait_action->set_allow_interrupt(true); EXPECT_CALL(mock_service_, OnGetActions(kScriptPath, _, _, _, _, _)) .WillRepeatedly(RunOnceCallback<5>(true, Serialize(interruptible))); @@ -1091,8 +1083,8 @@ TEST_F(ScriptExecutorTest, PauseWaitForDomWhileNavigating) { ActionsResponseProto actions_response; auto* wait_for_dom = actions_response.add_actions()->mutable_wait_for_dom(); wait_for_dom->set_timeout_ms(2000); - wait_for_dom->mutable_wait_condition()->mutable_match()->add_selectors( - "element"); + *wait_for_dom->mutable_wait_condition()->mutable_match() = + ToSelectorProto("element"); EXPECT_CALL(mock_service_, OnGetActions(_, _, _, _, _, _)) .WillOnce(RunOnceCallback<5>(true, Serialize(actions_response))); @@ -1130,8 +1122,8 @@ TEST_F(ScriptExecutorTest, StartWaitForDomWhileNavigating) { ActionsResponseProto actions_response; auto* wait_for_dom = actions_response.add_actions()->mutable_wait_for_dom(); wait_for_dom->set_timeout_ms(2000); - wait_for_dom->mutable_wait_condition()->mutable_match()->add_selectors( - "element"); + *wait_for_dom->mutable_wait_condition()->mutable_match() = + ToSelectorProto("element"); EXPECT_CALL(mock_service_, OnGetActions(_, _, _, _, _, _)) .WillOnce(RunOnceCallback<5>(true, Serialize(actions_response))); @@ -1140,27 +1132,25 @@ TEST_F(ScriptExecutorTest, StartWaitForDomWhileNavigating) { .WillOnce(DoAll(SaveArg<3>(&processed_actions_capture), RunOnceCallback<4>(true, ""))); - // Navigation starts before WaitForDom even starts, so the operation starts in - // a paused state. + // Navigation starts before WaitForDom starts. WaitForDom does not wait and + // completes successfully. delegate_.UpdateNavigationState(/* navigating= */ true, /* error= */ false); - + EXPECT_CALL(executor_callback_, Run(_)); executor_->Run(&user_data_, executor_callback_.Get()); - // The end of navigation un-pauses WaitForDom, which then succeeds - // immediately. - EXPECT_CALL(executor_callback_, Run(_)); + // Navigation finishes after the WaitForDom has finished. delegate_.UpdateNavigationState(/* navigating= */ false, /* error= */ false); ASSERT_EQ(1u, processed_actions_capture.size()); EXPECT_EQ(ACTION_APPLIED, processed_actions_capture[0].status()); + EXPECT_FALSE(processed_actions_capture[0].navigation_info().started()); + EXPECT_FALSE(processed_actions_capture[0].navigation_info().ended()); } TEST_F(ScriptExecutorTest, ReportErrorAsNavigationError) { ActionsResponseProto actions_response; - actions_response.add_actions() - ->mutable_click() - ->mutable_element_to_click() - ->add_selectors("will fail"); + *actions_response.add_actions()->mutable_click()->mutable_element_to_click() = + ToSelectorProto("will fail"); EXPECT_CALL(mock_service_, OnGetActions(_, _, _, _, _, _)) .WillOnce(RunOnceCallback<5>(true, Serialize(actions_response))); @@ -1241,8 +1231,8 @@ TEST_F(ScriptExecutorTest, ReportNavigationErrors) { TEST_F(ScriptExecutorTest, ReportNavigationEnd) { ActionsResponseProto actions_response; auto* wait_for_dom = actions_response.add_actions()->mutable_wait_for_dom(); - wait_for_dom->mutable_wait_condition()->mutable_match()->add_selectors( - "element"); + *wait_for_dom->mutable_wait_condition()->mutable_match() = + ToSelectorProto("element"); EXPECT_CALL(mock_service_, OnGetActions(_, _, _, _, _, _)) .WillOnce(RunOnceCallback<5>(true, Serialize(actions_response))); @@ -1251,20 +1241,19 @@ TEST_F(ScriptExecutorTest, ReportNavigationEnd) { .WillOnce(DoAll(SaveArg<3>(&processed_actions_capture), RunOnceCallback<4>(true, ""))); - // Navigation starts, before the script is run. - delegate_.UpdateNavigationState(/* navigating= */ true, /* error= */ false); - EXPECT_CALL(executor_callback_, Run(_)); - executor_->Run(&user_data_, executor_callback_.Get()); - - // WaitForDom waits for navigation to end, then checks for the element, which - // fails. + // WaitForDom does NOT wait for navigation to end, it immediately checks for + // the element, which fails. EXPECT_CALL(mock_web_controller_, OnElementCheck(Eq(Selector({"element"})), _)) .WillOnce(RunOnceCallback<1>(ClientStatus())); + + // Navigation starts before the script is run. + delegate_.UpdateNavigationState(/* navigating= */ true, /* error= */ false); + EXPECT_CALL(executor_callback_, Run(_)); + executor_->Run(&user_data_, executor_callback_.Get()); delegate_.UpdateNavigationState(/* navigating= */ false, /* error= */ false); - // Checking for the element succeeds on the second try. Waiting avoids - // depending on the order at which the listeners are called. + // Checking for the element succeeds on the second try. EXPECT_CALL(mock_web_controller_, OnElementCheck(Eq(Selector({"element"})), _)) .WillOnce(RunOnceCallback<1>(OkClientStatus())); @@ -1279,8 +1268,8 @@ TEST_F(ScriptExecutorTest, ReportNavigationEnd) { TEST_F(ScriptExecutorTest, ReportUnexpectedNavigationStart) { ActionsResponseProto actions_response; auto* wait_for_dom = actions_response.add_actions()->mutable_wait_for_dom(); - wait_for_dom->mutable_wait_condition()->mutable_match()->add_selectors( - "element"); + *wait_for_dom->mutable_wait_condition()->mutable_match() = + ToSelectorProto("element"); EXPECT_CALL(mock_service_, OnGetActions(_, _, _, _, _, _)) .WillOnce(RunOnceCallback<5>(true, Serialize(actions_response))); @@ -1314,8 +1303,8 @@ TEST_F(ScriptExecutorTest, ReportExpectedNavigationStart) { ActionsResponseProto actions_response; actions_response.add_actions()->mutable_expect_navigation(); auto* wait_for_dom = actions_response.add_actions()->mutable_wait_for_dom(); - wait_for_dom->mutable_wait_condition()->mutable_match()->add_selectors( - "element"); + *wait_for_dom->mutable_wait_condition()->mutable_match() = + ToSelectorProto("element"); EXPECT_CALL(mock_service_, OnGetActions(_, _, _, _, _, _)) .WillOnce(RunOnceCallback<5>(true, Serialize(actions_response))); diff --git a/chromium/components/autofill_assistant/browser/script_precondition.cc b/chromium/components/autofill_assistant/browser/script_precondition.cc index f12db0b8da6..8dcab16380b 100644 --- a/chromium/components/autofill_assistant/browser/script_precondition.cc +++ b/chromium/components/autofill_assistant/browser/script_precondition.cc @@ -7,6 +7,7 @@ #include <utility> #include "base/bind.h" +#include "base/logging.h" #include "base/strings/strcat.h" #include "base/threading/thread_task_runner_handle.h" #include "components/autofill_assistant/browser/batch_element_checker.h" diff --git a/chromium/components/autofill_assistant/browser/script_precondition_unittest.cc b/chromium/components/autofill_assistant/browser/script_precondition_unittest.cc index 00f6e8c890b..2bfd4611c38 100644 --- a/chromium/components/autofill_assistant/browser/script_precondition_unittest.cc +++ b/chromium/components/autofill_assistant/browser/script_precondition_unittest.cc @@ -295,7 +295,8 @@ TEST_F(ScriptPreconditionTest, MultipleConditions) { ScriptPreconditionProto proto; proto.add_domain("http://match.example.com"); proto.add_path_pattern("/path"); - proto.mutable_element_condition()->mutable_match()->add_selectors("exists"); + *proto.mutable_element_condition()->mutable_match() = + ToSelectorProto("exists"); // Domain and path don't match. EXPECT_FALSE(Check(proto)); @@ -303,8 +304,8 @@ TEST_F(ScriptPreconditionTest, MultipleConditions) { SetUrl("http://match.example.com/path"); EXPECT_TRUE(Check(proto)) << "Domain, path and selector must match."; - proto.mutable_element_condition()->mutable_match()->set_selectors( - 0, "does_not_exist"); + *proto.mutable_element_condition()->mutable_match() = + ToSelectorProto("does_not_exist"); EXPECT_FALSE(Check(proto)) << "Element can not match."; } diff --git a/chromium/components/autofill_assistant/browser/script_tracker_unittest.cc b/chromium/components/autofill_assistant/browser/script_tracker_unittest.cc index 8b745a8eff8..90d8c7afd61 100644 --- a/chromium/components/autofill_assistant/browser/script_tracker_unittest.cc +++ b/chromium/components/autofill_assistant/browser/script_tracker_unittest.cc @@ -89,11 +89,10 @@ class ScriptTrackerTest : public testing::Test, public ScriptTracker::Listener { script->set_path(path); script->mutable_presentation()->mutable_chip()->set_text(name); if (!selector.empty()) { - script->mutable_presentation() - ->mutable_precondition() - ->mutable_element_condition() - ->mutable_match() - ->add_selectors(selector); + *script->mutable_presentation() + ->mutable_precondition() + ->mutable_element_condition() + ->mutable_match() = ToSelectorProto(selector); } ScriptStatusMatchProto dont_run_twice_precondition; dont_run_twice_precondition.set_script(path); @@ -375,8 +374,8 @@ TEST_F(ScriptTrackerTest, UpdateInterruptList) { ActionsResponseProto actions_response; auto* wait_for_dom = actions_response.add_actions()->mutable_wait_for_dom(); - wait_for_dom->mutable_wait_condition()->mutable_match()->add_selectors( - "exists"); + *wait_for_dom->mutable_wait_condition()->mutable_match() = + ToSelectorProto("exists"); wait_for_dom->set_allow_interrupt(true); SupportedScriptProto* interrupt_proto = diff --git a/chromium/components/autofill_assistant/browser/selector.cc b/chromium/components/autofill_assistant/browser/selector.cc index 99bfce41813..e460622cc42 100644 --- a/chromium/components/autofill_assistant/browser/selector.cc +++ b/chromium/components/autofill_assistant/browser/selector.cc @@ -4,150 +4,361 @@ #include "components/autofill_assistant/browser/selector.h" +#include "base/logging.h" +#include "base/notreached.h" #include "base/strings/string_util.h" namespace autofill_assistant { -Selector::Selector() {} +// Comparison operations are in the autofill_assistant scope, even though +// they're not shared outside of this module, for them to be visible to +// std::make_tuple and std::lexicographical_compare. -Selector::Selector(const ElementReferenceProto& proto) { - for (const auto& selector : proto.selectors()) { - selectors.emplace_back(selector); - } - must_be_visible = proto.visibility_requirement() == MUST_BE_VISIBLE; - inner_text_pattern = proto.inner_text_pattern(); - value_pattern = proto.value_pattern(); - pseudo_type = proto.pseudo_type(); +bool operator<(const SelectorProto::TextFilter& a, + const SelectorProto::TextFilter& b) { + return std::make_tuple(a.re2(), a.case_sensitive()) < + std::make_tuple(b.re2(), b.case_sensitive()); } -ElementReferenceProto Selector::ToElementReferenceProto() const { - ElementReferenceProto proto; - for (const auto& selector : selectors) { - proto.add_selectors(selector); +// Used by operator<(RepeatedPtrField<Filter>, RepeatedPtrField<Filter>) +bool operator<(const SelectorProto::Filter& a, const SelectorProto::Filter& b); + +bool operator<( + const google::protobuf::RepeatedPtrField<SelectorProto::Filter>& a, + const google::protobuf::RepeatedPtrField<SelectorProto::Filter>& b) { + return std::lexicographical_compare(a.begin(), a.end(), b.begin(), b.end()); +} + +bool operator<(const SelectorProto::Filter& a, const SelectorProto::Filter& b) { + if (a.filter_case() < b.filter_case()) { + return true; } - if (must_be_visible) { - proto.set_visibility_requirement(MUST_BE_VISIBLE); + if (a.filter_case() != b.filter_case()) { + return false; } - proto.set_inner_text_pattern(inner_text_pattern); - proto.set_value_pattern(value_pattern); - proto.set_pseudo_type(pseudo_type); - return proto; -} + switch (a.filter_case()) { + case SelectorProto::Filter::kCssSelector: + return a.css_selector() < b.css_selector(); -Selector::Selector(std::vector<std::string> s) - : selectors(s), pseudo_type(PseudoType::UNDEFINED) {} -Selector::Selector(std::vector<std::string> s, PseudoType p) - : selectors(s), pseudo_type(p) {} -Selector::~Selector() = default; + case SelectorProto::Filter::kInnerText: + return a.inner_text() < b.inner_text(); -Selector::Selector(Selector&& other) = default; -Selector::Selector(const Selector& other) = default; -Selector& Selector::operator=(const Selector& other) = default; -Selector& Selector::operator=(Selector&& other) = default; + case SelectorProto::Filter::kValue: + return a.value() < b.value(); -bool Selector::operator<(const Selector& other) const { - return std::tie(selectors, inner_text_pattern, value_pattern, must_be_visible, - pseudo_type) < - std::tie(other.selectors, other.inner_text_pattern, - other.value_pattern, other.must_be_visible, - other.pseudo_type); + case SelectorProto::Filter::kPseudoType: + return a.pseudo_type() < b.pseudo_type(); + + case SelectorProto::Filter::kPseudoElementContent: + return std::make_tuple(a.pseudo_element_content().pseudo_type(), + a.pseudo_element_content().content()) < + std::make_tuple(b.pseudo_element_content().pseudo_type(), + b.pseudo_element_content().content()); + + case SelectorProto::Filter::kBoundingBox: + case SelectorProto::Filter::kEnterFrame: + case SelectorProto::Filter::kPickOne: + case SelectorProto::Filter::kLabelled: + return false; + + case SelectorProto::Filter::kClosest: { + return std::make_tuple(a.closest().target(), a.closest().in_alignment(), + a.closest().relative_position()) < + std::make_tuple(b.closest().target(), b.closest().in_alignment(), + b.closest().relative_position()); + } + + case SelectorProto::Filter::FILTER_NOT_SET: + return false; + } + return false; } -bool Selector::operator==(const Selector& other) const { - return selectors == other.selectors && - inner_text_pattern == other.inner_text_pattern && - value_pattern == other.value_pattern && - must_be_visible == other.must_be_visible && - pseudo_type == other.pseudo_type; +SelectorProto ToSelectorProto(const std::string& s) { + return ToSelectorProto(std::vector<std::string>{s}); } -bool Selector::empty() const { - return this->selectors.empty(); +SelectorProto ToSelectorProto(const std::vector<std::string>& s) { + SelectorProto proto; + if (!s.empty()) { + for (size_t i = 0; i < s.size(); i++) { + if (i > 0) { + proto.add_filters()->mutable_pick_one(); + proto.add_filters()->mutable_enter_frame(); + } + proto.add_filters()->set_css_selector(s[i]); + } + } + return proto; } -std::ostream& operator<<(std::ostream& out, PseudoType pseudo_type) { -#ifdef NDEBUG - return out << static_cast<int>(pseudo_type); -#else +std::string PseudoTypeName(PseudoType pseudo_type) { switch (pseudo_type) { case UNDEFINED: - out << "UNDEFINED"; - break; + return "undefined"; case FIRST_LINE: - out << "FIRST_LINE"; - break; + return "first-line"; case FIRST_LETTER: - out << "FIRST_LETTER"; - break; + return "first-letter"; case BEFORE: - out << "BEFORE"; - break; + return "before"; case AFTER: - out << "AFTER"; - break; + return "after"; case BACKDROP: - out << "BACKDROP"; - break; + return "backdrop"; case SELECTION: - out << "SELECTION"; - break; + return "selection"; case FIRST_LINE_INHERITED: - out << "FIRST_LINE_INHERITED"; - break; + return "first-line-inherited"; case SCROLLBAR: - out << "SCROLLBAR"; - break; + return "scrollbar"; case SCROLLBAR_THUMB: - out << "SCROLLBAR_THUMB"; - break; + return "scrollbar-thumb"; case SCROLLBAR_BUTTON: - out << "SCROLLBAR_BUTTON"; - break; + return "scrollbar-button"; case SCROLLBAR_TRACK: - out << "SCROLLBAR_TRACK"; - break; + return "scrollbar-track"; case SCROLLBAR_TRACK_PIECE: - out << "SCROLLBAR_TRACK_PIECE"; - break; + return "scrollbar-track-piece"; case SCROLLBAR_CORNER: - out << "SCROLLBAR_CORNER"; - break; + return "scrollbar-corner"; case RESIZER: - out << "RESIZER"; - break; + return "resizer"; case INPUT_LIST_BUTTON: - out << "INPUT_LIST_BUTTON"; - break; + return "input-list-button"; // Intentionally no default case to make compilation fail if a new value // was added to the enum but not to this list. } - return out; -#endif } -std::ostream& operator<<(std::ostream& out, const Selector& selector) { -#ifdef NDEBUG - out << selector.selectors.size() << " element(s)"; - return out; -#else - out << "elements=[" << base::JoinString(selector.selectors, ",") << "]"; - if (!selector.inner_text_pattern.empty()) { - out << " innerText =~ /"; - out << selector.inner_text_pattern; - out << "/"; + +Selector::Selector() {} + +Selector::Selector(const SelectorProto& selector_proto) + : proto(selector_proto) {} + +Selector::~Selector() = default; + +Selector::Selector(Selector&& other) = default; +Selector::Selector(const Selector& other) = default; +Selector& Selector::operator=(const Selector& other) = default; +Selector& Selector::operator=(Selector&& other) = default; + +bool Selector::operator<(const Selector& other) const { + return proto.filters() < other.proto.filters(); +} + +bool Selector::operator==(const Selector& other) const { + return !(*this < other) && !(other < *this); +} + +Selector& Selector::MustBeVisible() { + int filter_count = proto.filters().size(); + if (filter_count == 0 || proto.filters(filter_count - 1).has_bounding_box()) { + // Avoids adding duplicate visibility requirements in the common case. + return *this; + } + proto.add_filters()->mutable_bounding_box(); + return *this; +} + +bool Selector::empty() const { + bool has_css_selector = false; + for (const SelectorProto::Filter& filter : proto.filters()) { + switch (filter.filter_case()) { + case SelectorProto::Filter::FILTER_NOT_SET: + // There must not be any unknown or invalid filters. + return true; + + case SelectorProto::Filter::kCssSelector: + // There must be at least one CSS selector, since it's the only + // way we have of expanding the set of matches. + has_css_selector = true; + break; + + default: + break; + } + } + return !has_css_selector; +} + +base::Optional<std::string> Selector::ExtractSingleCssSelectorForAutofill() + const { + int last_enter_frame_index = -1; + for (int i = proto.filters().size() - 1; i >= 0; i--) { + if (proto.filters(i).filter_case() == SelectorProto::Filter::kEnterFrame) { + last_enter_frame_index = i; + break; + } + } + std::string css_selector; + for (int i = last_enter_frame_index + 1; i < proto.filters().size(); i++) { + const SelectorProto::Filter& filter = proto.filters(i); + switch (filter.filter_case()) { + case SelectorProto::Filter::kCssSelector: + if (css_selector.empty()) { + css_selector = filter.css_selector(); + } else { + VLOG(1) << __func__ + << " Selector with multiple CSS selectors not supported for " + "autofill: " + << *this; + return base::nullopt; + } + break; + + case SelectorProto::Filter::kBoundingBox: + case SelectorProto::Filter::kPickOne: + // Ignore these; they're not relevant for the autofill use-case + break; + + case SelectorProto::Filter::kInnerText: + case SelectorProto::Filter::kValue: + case SelectorProto::Filter::kPseudoType: + case SelectorProto::Filter::kPseudoElementContent: + case SelectorProto::Filter::kLabelled: + case SelectorProto::Filter::kClosest: + VLOG(1) << __func__ + << " Selector feature not supported by autofill: " << *this; + return base::nullopt; + + case SelectorProto::Filter::FILTER_NOT_SET: + VLOG(1) << __func__ << " Unknown filter type in: " << *this; + return base::nullopt; + + case SelectorProto::Filter::kEnterFrame: + // This cannot possibly happen, since the iteration started after the + // last enter_frame. + NOTREACHED(); + break; + } } - if (!selector.value_pattern.empty()) { - out << " value =~ /"; - out << selector.value_pattern; - out << "/"; + if (css_selector.empty()) { + VLOG(1) << __func__ + << " Selector without CSS selector not supported by autofill: " + << *this; + return base::nullopt; } - if (selector.must_be_visible) { - out << " must_be_visible"; + return css_selector; +} + +std::ostream& operator<<(std::ostream& out, const Selector& selector) { + return out << selector.proto; +} + +#ifndef NDEBUG +namespace { + +// Debug output for pseudo types. +std::ostream& operator<<(std::ostream& out, PseudoType pseudo_type) { + return out << PseudoTypeName(pseudo_type); +} + +std::ostream& operator<<(std::ostream& out, + const SelectorProto::TextFilter& c) { + out << "/" << c.re2() << "/"; + if (c.case_sensitive()) { + out << "i"; } - if (selector.pseudo_type != PseudoType::UNDEFINED) { - out << "::" << selector.pseudo_type; + return out; +} + +std::ostream& operator<<( + std::ostream& out, + const google::protobuf::RepeatedPtrField<SelectorProto::Filter>& filters) { + out << "["; + std::string separator = ""; + for (const SelectorProto::Filter& filter : filters) { + out << separator << filter; + separator = " "; } + out << "]"; + return out; +} +} // namespace +#endif // NDEBUG + +std::ostream& operator<<(std::ostream& out, const SelectorProto& selector) { +#ifdef NDEBUG + out << selector.filters().size() << " filter(s)"; +#else + out << selector.filters(); +#endif // NDEBUG return out; +} + +std::ostream& operator<<(std::ostream& out, const SelectorProto::Filter& f) { +#ifdef NDEBUG + // DEBUG output not available. + return out << "filter case=" << f.filter_case(); +#else + switch (f.filter_case()) { + case SelectorProto::Filter::kEnterFrame: + out << "/"; + return out; + + case SelectorProto::Filter::kCssSelector: + out << f.css_selector(); + return out; + + case SelectorProto::Filter::kInnerText: + out << "innerText~=" << f.inner_text(); + return out; + + case SelectorProto::Filter::kValue: + out << "value~=" << f.value(); + return out; + + case SelectorProto::Filter::kPseudoType: + out << "::" << f.pseudo_type(); + return out; + + case SelectorProto::Filter::kPseudoElementContent: + out << "::" << f.pseudo_element_content().pseudo_type() + << "~=" << f.pseudo_element_content().content(); + return out; + + case SelectorProto::Filter::kBoundingBox: + out << "bounding_box"; + return out; + + case SelectorProto::Filter::kPickOne: + out << "pick_one"; + return out; + + case SelectorProto::Filter::kLabelled: + out << "labelled"; + return out; + + case SelectorProto::Filter::kClosest: + out << "closest to " << f.closest().target(); + switch (f.closest().relative_position()) { + case SelectorProto::ProximityFilter::UNSPECIFIED_POSITION: + break; + case SelectorProto::ProximityFilter::ABOVE: + out << " above"; + break; + case SelectorProto::ProximityFilter::BELOW: + out << " below"; + break; + case SelectorProto::ProximityFilter::RIGHT: + out << " right"; + break; + case SelectorProto::ProximityFilter::LEFT: + out << " left"; + break; + } + if (f.closest().in_alignment()) { + out << " in alignment"; + } + return out; + + case SelectorProto::Filter::FILTER_NOT_SET: + // Either unset or set to an unsupported value. Let's assume the worse. + out << "INVALID"; + return out; + } #endif // NDEBUG } diff --git a/chromium/components/autofill_assistant/browser/selector.h b/chromium/components/autofill_assistant/browser/selector.h index 1a174e0c166..c79849f3b71 100644 --- a/chromium/components/autofill_assistant/browser/selector.h +++ b/chromium/components/autofill_assistant/browser/selector.h @@ -10,42 +10,33 @@ #include <vector> #include "base/macros.h" +#include "base/optional.h" +#include "components/autofill_assistant/browser/client_status.h" #include "components/autofill_assistant/browser/service.pb.h" namespace autofill_assistant { -// A structure to represent a CSS selector. -struct Selector { - // A sequence of CSS selectors. Any non-final CSS selector is expected to - // arrive at a frame or an iframe, i.e. an element that contains another - // document. - std::vector<std::string> selectors; - - // If true, only match visible elements. Visible elements are elements that - // have a box model. The box model is not checked at all, so an element with a - // zero size bounding box is considered visible. - bool must_be_visible = false; - - // If non-empty, this must be a regular expression that matches the inner text - // of the element(s) matching selectors. - std::string inner_text_pattern; +// Convenience functions for creating SelectorProtos. +SelectorProto ToSelectorProto(const std::string& s); +SelectorProto ToSelectorProto(const std::vector<std::string>& s); - // If non-empty, this must be a regular expression that matches the value - // of the element(s) matching selectors. - std::string value_pattern; +// Returns the CSS name of a pseudo-type, without "::" prefix. +std::string PseudoTypeName(PseudoType pseudoType); - // An optional pseudo type. This pseudo type is associated to the final - // element matched by |selectors|, which means that we currently don't handle - // matching an element inside a pseudo element. - PseudoType pseudo_type = PseudoType::UNDEFINED; +// Convenience wrapper around a SelectorProto that makes it simpler to work with +// selectors. +// +// Selectors are comparables, can be used as std::map key or std::set elements +// and converted to string with operator<<. +struct Selector { + SelectorProto proto; Selector(); - explicit Selector(const ElementReferenceProto& element); - explicit Selector(std::vector<std::string> s); - Selector(std::vector<std::string> s, PseudoType p); ~Selector(); - ElementReferenceProto ToElementReferenceProto() const; + explicit Selector(const SelectorProto& proto); + explicit Selector(const std::vector<std::string>& s) + : Selector(ToSelectorProto(s)) {} Selector(Selector&& other); Selector(const Selector& other); @@ -56,30 +47,70 @@ struct Selector { bool operator==(const Selector& other) const; // Convenience function to update the visible field in a fluent style. - Selector& MustBeVisible() { - must_be_visible = true; - return *this; - } - - // The output operator. The actual selectors are only available in debug - // builds. - friend std::ostream& operator<<(std::ostream& out, const Selector& selector); + Selector& MustBeVisible(); - // Checks whether this selector is empty. + // Checks whether this selector is empty or invalid. bool empty() const; // Convenience function to set inner_text_pattern in a fluent style. Selector& MatchingInnerText(const std::string& pattern) { - inner_text_pattern = pattern; + return MatchingInnerText(pattern, false); + } + + // Convenience function to set inner_text_pattern matching with case + // sensitivity. + Selector& MatchingInnerText(const std::string& pattern, bool case_sensitive) { + auto* text_filter = proto.add_filters()->mutable_inner_text(); + text_filter->set_re2(pattern); + text_filter->set_case_sensitive(case_sensitive); return *this; } // Convenience function to set inner_text_pattern in a fluent style. Selector& MatchingValue(const std::string& pattern) { - value_pattern = pattern; + return MatchingValue(pattern, false); + } + + // Convenience function to set value_pattern matchinng with case sensitivity. + Selector& MatchingValue(const std::string& pattern, bool case_sensitive) { + auto* text_filter = proto.add_filters()->mutable_value(); + text_filter->set_re2(pattern); + text_filter->set_case_sensitive(case_sensitive); return *this; } + + Selector& SetPseudoType(PseudoType pseudo_type) { + proto.add_filters()->set_pseudo_type(pseudo_type); + return *this; + } + + // Returns a single CSS selector pointing to the element from the last frame, + // to pass to autofill. + // + // This call returns nothing if the selector contains unsupported filters, + // such as innerText or pseudo-element filters. + // + // AutofillAgent::GetElementFormAndFieldData takes a single CSS selector that + // identifies the form. This means that form elements for autofill are limited + // to one single CSS selector and no further filtering. TODO(b/155264465): + // have ElementFinder specify the element it has found in a format that + // Autofill can recognise. + base::Optional<std::string> ExtractSingleCssSelectorForAutofill() const; }; + +// Debug output operator for selectors. The output is only useful in +// debug builds. +std::ostream& operator<<(std::ostream& out, const Selector& selector); + +// Debug output for selector protos. The output is only useful in +// debug builds. +std::ostream& operator<<(std::ostream& out, const SelectorProto& proto); + +// Debug output for selector filter protos. The output is only useful in +// debug builds. +std::ostream& operator<<(std::ostream& out, + const SelectorProto::Filter& filter); + } // namespace autofill_assistant #endif // COMPONENTS_AUTOFILL_ASSISTANT_BROWSER_SELECTOR_H_ diff --git a/chromium/components/autofill_assistant/browser/selector_unittest.cc b/chromium/components/autofill_assistant/browser/selector_unittest.cc index 462bcc3db29..08639296f6b 100644 --- a/chromium/components/autofill_assistant/browser/selector_unittest.cc +++ b/chromium/components/autofill_assistant/browser/selector_unittest.cc @@ -8,74 +8,169 @@ #include "components/autofill_assistant/browser/service.pb.h" #include "testing/gmock/include/gmock/gmock.h" +using ::testing::UnorderedElementsAre; + namespace autofill_assistant { namespace { -TEST(SelectorTest, FromProto) { - ElementReferenceProto proto; - proto.add_selectors("a"); - proto.add_selectors("b"); - proto.set_inner_text_pattern("c"); - proto.set_value_pattern("d"); - proto.set_visibility_requirement(MUST_BE_VISIBLE); - proto.set_pseudo_type(PseudoType::BEFORE); - - Selector selector(proto); - EXPECT_THAT(selector.selectors, testing::ElementsAre("a", "b")); - EXPECT_TRUE(selector.must_be_visible); - EXPECT_EQ("c", selector.inner_text_pattern); - EXPECT_EQ("d", selector.value_pattern); - EXPECT_EQ(PseudoType::BEFORE, selector.pseudo_type); +TEST(SelectorTest, Constructor_Simple) { + Selector selector({"#test"}); + ASSERT_EQ(1, selector.proto.filters().size()); + EXPECT_EQ("#test", selector.proto.filters(0).css_selector()); } -TEST(SelectorTest, ToProto) { - Selector selector; - selector.selectors.emplace_back("a"); - selector.selectors.emplace_back("b"); - selector.inner_text_pattern = "c"; - selector.value_pattern = "d"; - selector.must_be_visible = true; - selector.pseudo_type = PseudoType::BEFORE; - - ElementReferenceProto proto = selector.ToElementReferenceProto(); - EXPECT_THAT(proto.selectors(), testing::ElementsAre("a", "b")); - EXPECT_EQ("c", proto.inner_text_pattern()); - EXPECT_EQ("d", proto.value_pattern()); - EXPECT_EQ(MUST_BE_VISIBLE, proto.visibility_requirement()); - EXPECT_EQ(PseudoType::BEFORE, proto.pseudo_type()); +TEST(SelectorTest, Constructor_WithIframe) { + Selector selector({"#frame", "#test"}); + ASSERT_EQ(4, selector.proto.filters().size()); + EXPECT_EQ("#frame", selector.proto.filters(0).css_selector()); + EXPECT_EQ(selector.proto.filters(1).filter_case(), + SelectorProto::Filter::kPickOne); + EXPECT_EQ(selector.proto.filters(2).filter_case(), + SelectorProto::Filter::kEnterFrame); + EXPECT_EQ("#test", selector.proto.filters(3).css_selector()); +} + +TEST(SelectorTest, FromProto) { + SelectorProto proto; + proto.add_filters()->set_css_selector("#test"); + + EXPECT_EQ(Selector({"#test"}), Selector(proto)); } TEST(SelectorTest, Comparison) { + // Note that comparison tests cover < indirectly through ==, since a == b is + // defined as !(a < b) && !(b < a). This makes sense, as what matters is that + // there is an order but what the order is doesn't matter. + EXPECT_FALSE(Selector({"a"}) == Selector({"b"})); - EXPECT_LT(Selector({"a"}), Selector({"b"})); EXPECT_TRUE(Selector({"a"}) == Selector({"a"})); +} - EXPECT_FALSE(Selector({"a"}, PseudoType::BEFORE) == - Selector({"a"}, PseudoType::AFTER)); - EXPECT_LT(Selector({"a"}, PseudoType::BEFORE), - Selector({"a"}, PseudoType::AFTER)); - EXPECT_LT(Selector({"a"}, PseudoType::BEFORE), Selector({"b"})); - EXPECT_TRUE(Selector({"a"}, PseudoType::BEFORE) == - Selector({"a"}, PseudoType::BEFORE)); +TEST(SelectorTest, SelectorInSet) { + std::set<Selector> selectors; + selectors.insert(Selector({"a"})); + selectors.insert(Selector({"a"})); + selectors.insert(Selector({"b"})); + selectors.insert(Selector({"c"})); + EXPECT_THAT(selectors, UnorderedElementsAre(Selector({"a"}), Selector({"b"}), + Selector({"c"}))); +} +TEST(SelectorTest, Comparison_PseudoType) { + EXPECT_FALSE(Selector({"a"}).SetPseudoType(PseudoType::BEFORE) == + Selector({"a"}).SetPseudoType(PseudoType::AFTER)); + EXPECT_FALSE(Selector({"a"}).SetPseudoType(PseudoType::BEFORE) == + Selector({"a"}).SetPseudoType(PseudoType::AFTER)); + EXPECT_FALSE(Selector({"b"}) == + Selector({"a"}).SetPseudoType(PseudoType::BEFORE)); + EXPECT_FALSE(Selector({"b"}) == + Selector({"a"}).SetPseudoType(PseudoType::BEFORE)); + EXPECT_TRUE(Selector({"a"}).SetPseudoType(PseudoType::BEFORE) == + Selector({"a"}).SetPseudoType(PseudoType::BEFORE)); +} + +TEST(SelectorTest, Comparison_Visibility) { EXPECT_FALSE(Selector({"a"}) == Selector({"a"}).MustBeVisible()); - EXPECT_LT(Selector({"a"}), Selector({"a"}).MustBeVisible()); EXPECT_TRUE(Selector({"a"}).MustBeVisible() == Selector({"a"}).MustBeVisible()); +} +TEST(SelectorTest, Comparison_InnerText) { EXPECT_FALSE(Selector({"a"}).MatchingInnerText("a") == Selector({"a"}).MatchingInnerText("b")); - EXPECT_LT(Selector({"a"}).MatchingInnerText("a"), - Selector({"a"}).MatchingInnerText("b")); EXPECT_TRUE(Selector({"a"}).MatchingInnerText("a") == Selector({"a"}).MatchingInnerText("a")); + EXPECT_FALSE(Selector({"a"}).MatchingInnerText("a", false) == + Selector({"a"}).MatchingInnerText("a", true)); + EXPECT_TRUE(Selector({"a"}).MatchingInnerText("a", true) == + Selector({"a"}).MatchingInnerText("a", true)); +} + +TEST(SelectorTest, Comparison_Value) { EXPECT_FALSE(Selector({"a"}).MatchingValue("a") == Selector({"a"}).MatchingValue("b")); - EXPECT_LT(Selector({"a"}).MatchingValue("a"), - Selector({"a"}).MatchingValue("b")); EXPECT_TRUE(Selector({"a"}).MatchingValue("a") == Selector({"a"}).MatchingValue("a")); + + EXPECT_FALSE(Selector({"a"}).MatchingValue("a", false) == + Selector({"a"}).MatchingValue("a", true)); + EXPECT_TRUE(Selector({"a"}).MatchingValue("a", true) == + Selector({"a"}).MatchingValue("a", true)); +} + +TEST(SelectorTest, Comparison_Proximity) { + SelectorProto selector; + selector.add_filters()->set_css_selector("button"); + auto* closest_to_button = selector.add_filters()->mutable_closest(); + closest_to_button->mutable_target()->Add()->set_css_selector("#label1"); + + EXPECT_TRUE(Selector(selector) == Selector(selector)); + + // Different relative positions + SelectorProto left = selector; + left.mutable_filters(0)->mutable_closest()->set_relative_position( + SelectorProto::ProximityFilter::LEFT); + + SelectorProto right = selector; + right.mutable_filters(0)->mutable_closest()->set_relative_position( + SelectorProto::ProximityFilter::RIGHT); + + EXPECT_TRUE(Selector(right) == Selector(right)); + EXPECT_TRUE(Selector(left) == Selector(left)); + EXPECT_FALSE(Selector(left) == Selector(right)); + + // Different alignment + SelectorProto aligned = selector; + selector.mutable_filters(0)->mutable_closest()->set_in_alignment(true); + EXPECT_TRUE(Selector(aligned) == Selector(aligned)); + EXPECT_FALSE(Selector(selector) == Selector(aligned)); + + // Different targets + SelectorProto label2 = selector; + label2.mutable_filters(0) + ->mutable_closest() + ->mutable_target() + ->Add() + ->set_css_selector("#label2"); + + EXPECT_TRUE(Selector(label2) == Selector(label2)); + EXPECT_FALSE(Selector(selector) == Selector(label2)); +} + +TEST(SelectorTest, Comparison_Frames) { + Selector ab({"a", "b"}); + EXPECT_EQ(ab, ab); + + Selector cb({"c", "b"}); + EXPECT_TRUE(cb == cb); + EXPECT_FALSE(ab == cb); + + Selector b({"b"}); + EXPECT_TRUE(b == b); + EXPECT_FALSE(ab == b); +} + +TEST(SelectorTest, Comparison_MultipleFilters) { + Selector abcdef; + abcdef.proto.add_filters()->set_css_selector("abc"); + abcdef.proto.add_filters()->set_css_selector("def"); + + Selector abcdef2; + abcdef2.proto.add_filters()->set_css_selector("abc"); + abcdef2.proto.add_filters()->set_css_selector("def"); + EXPECT_EQ(abcdef, abcdef2); + + Selector defabc; + defabc.proto.add_filters()->set_css_selector("def"); + defabc.proto.add_filters()->set_css_selector("abc"); + EXPECT_TRUE(defabc == defabc); + EXPECT_FALSE(abcdef == defabc); + + Selector abc; + abc.proto.add_filters()->set_css_selector("abc"); + EXPECT_TRUE(abc == abc); + EXPECT_FALSE(abcdef == abc); } } // namespace diff --git a/chromium/components/autofill_assistant/browser/service.proto b/chromium/components/autofill_assistant/browser/service.proto index de7306a466b..61d392f405c 100644 --- a/chromium/components/autofill_assistant/browser/service.proto +++ b/chromium/components/autofill_assistant/browser/service.proto @@ -79,6 +79,20 @@ message ClientContextProto { } // Whether the account from the intent and the chrome account match. optional AccountsMatchingStatus accounts_matching_status = 12; + + // Whether a11y (talkback and touch exploration) is enabled or not. + optional bool accessibility_enabled = 13; + + enum SignedIntoChromeStatus { + UNDEFINED = 0; + + // There is no account signed into Chrome. + NOT_SIGNED_IN = 1; + + // An account is signed into Chrome. + SIGNED_IN = 2; + } + optional SignedIntoChromeStatus signed_into_chrome_status = 14; } // Get the list of scripts that can potentially be run on a url. @@ -528,7 +542,7 @@ message ProcessedActionProto { optional ProcessedActionStatusDetailsProto status_details = 19; oneof result_data { - PromptProto.Choice prompt_choice = 5; + PromptProto.Result prompt_choice = 5; string html_source = 12; // Should be set as a result of CollectUserDataAction. CollectUserDataResultProto collect_user_data_result = 15; @@ -638,7 +652,7 @@ message UnexpectedErrorInfoProto { message AutofillErrorInfoProto { message AutofillFieldError { // The field the error occurred for. - optional ElementReferenceProto field = 1; + optional SelectorProto field = 1; // The value expression associated with the field that caused the error. optional string value_expression = 5; @@ -649,6 +663,14 @@ message AutofillErrorInfoProto { // The status of the action. ProcessedActionStatusProto status = 4; + + // The field was required and expected to be filled during the fallback + // flow but was empty in the end. + bool empty_after_fallback = 6; + + // The field was expected to be cleared during the fallback flow but + // still had a value in the end. + bool filled_after_clear = 7; } reserved 2; @@ -694,65 +716,187 @@ enum PseudoType { INPUT_LIST_BUTTON = 15; } -// A reference to an unique element on the page, possibly nested in frames. -message ElementReferenceProto { - // A sequence of CSS selectors. Any non-final CSS selector is expected to - // arrive at a frame or an iframe, i.e. an element that contains another - // document. - // APIs are free to reject element references that do not refer to unique - // elements (i.e. resolve to more than one element on the page). - repeated string selectors = 2; +// A reference to one or more elements on the page, possibly nested in frames. +message SelectorProto { + // Filters for the element on the page. Filter are applied sequentially, using + // the output of the previous filter as input. The root of these filters is + // the main frame's document. + repeated Filter filters = 9; + + // A filter that starts with one or more elements and returns one on more + // elements. Filters are meant to be applied sequentially. + message Filter { + oneof filter { + // Enter the document of an iframe or shadow root. The next filters apply + // to the document inside of the iframe(s) or on the shadow element. + // + // Fails if there are more than one match. + EmptyFilter enter_frame = 1; + + // Evaluate the given CSS selector on all start elements and use + // the result as end elements. + string css_selector = 2; + + // Check the inner text of all start elements, using the Javascript + // innerText property. Keep only the element whose innerText match the + // given regular expression. + TextFilter inner_text = 3; + + // Check the value of all start elements, using the Javascript value + // property. Keep only the element whose value property match the given + // regular expression. + TextFilter value = 4; + + // Select the pseudo-element of the given type associated with the current + // elements. + PseudoType pseudo_type = 5; + + // Only keep elements that have a box model, even if it is empty. + // + // This is the equivalent of the old MUST_BE_VISIBLE flag. It's been + // renamed as having a bounding box is not enough to imply visibility. + EmptyFilter bounding_box = 6; + + // Pick just one match and continue. Ignore all other matches. + // + // For backward-compatibility with older element references, pick_one can + // be put before enter_frame. + EmptyFilter pick_one = 7; + + // Only keep elements that have a pseudo-element with the given content. + // + // This only works with BEFORE and AFTER. + // + // Note that this just filters out elements. It doesn't select the + // pseudo-element; use pseudo_type for that. + PseudoElementContent pseudo_element_content = 8; + + // Go from label to the labelled control. Only works starting with current + // elements that are LABEL. + // + // For example if we have: + // <label for="someid">First Name</label>...<input id="someid" ...> + // then labelled, goes from the label to the form element. + // + // So, the form element can be accessed as "label~=/FirstName/ labelled". + // This is especially useful in situations where someid can change. + // + // The same selector also works in the case where the element is inside of + // the label, so we don't need to worry which implementation is used when + // building the selector: + // <label>First Name <input ...></label> + EmptyFilter labelled = 9; + + // Filter elements by their position on the page, relative to a given + // target element. + // + // The distance between two elements is the shortest euclidean distance + // between their borders. The distance between two overlapping elements is + // always 0. If there are multiple elements at exactly the same distance, + // an arbitrary one is returned. + ProximityFilter closest = 10; + } + } - // If non-empty, only take into accounts the elements matched by selector - // whose inner text matches the given JavaScript regex. This does a search, - // not a full match. Add ^ and $ as necessary. - // - // This is used to filter the elements matching the last selector, before - // trying to get the pseudo_type, if any was set. - optional string inner_text_pattern = 4; + // A way of filtering elements by their text. + message TextFilter { + // Javascript RE2 regular expression to apply to the text. This is evaluated + // with Regexp.test, so it's a "find" and will be satisfied whenever the + // text contains at least one substring that matches the given regular + // expression. + optional string re2 = 1; - // If non-empty, only take into accounts the elements matched by selector - // whose value matches the given JavaScript regex. This does a search, - // not a full match. Add ^ and $ as necessary. - // - // This is used to filter the elements matching the last selector, before - // trying to get the pseudo_type, if any was set. - optional string value_pattern = 6; + // If true, the regular expression is case-sensitive. + optional bool case_sensitive = 2; + } - // Specifies whether the matched element(s) must be visible. - // - // Visibility applies to the element matched in selectors; the pseudo type is - // checked afterwards. - optional VisibilityRequirement visibility_requirement = 5; + // A way of filtering elements by their pseudo-element content. + message PseudoElementContent { + optional PseudoType pseudo_type = 1; + optional TextFilter content = 2; + } - // An optional pseudo type. This pseudo type is associated to the final - // element matched, which means that we currently don't handle matching an - // element inside a pseudo element. - optional PseudoType pseudo_type = 3; -} + // Filter elements by their position on the page, relative to a given target + // element. + message ProximityFilter { + // From the set of potential matches, choose the one closest to the given + // target. The target filters are evaluated relative to the current frame + // and must select an element in the current frame. + // + // If there is no target the whole selector matches nothing. + // + // This element cannot include enter_frame filters. + repeated Filter target = 1; + + // If true, the element and targets must be aligned either + // horizontally or vertically. + // + // This is usually what we want, as elements close, but in diagonal position + // relative to each other are usually not considered part of the same group. + optional bool in_alignment = 3; + + // Require the target and element to have a specific relative position. + // + // If unspecified, the target and element be in any position relative to + // each other. + // + // If necessary, this can be combined with in_alignment, so in_aligment=true + // relative_position=LEFT requires the element to be strictly to the left or + // target. + optional RelativePosition relative_position = 4; + + enum RelativePosition { + // Unspecified relative position. + UNSPECIFIED_POSITION = 0; -enum VisibilityRequirement { - UNSPECIFIED_VISIBILITY = 0; + // Element is above target. + ABOVE = 1; + + // Element is below target. + BELOW = 2; + + // Element is left of target. + LEFT = 3; + + // Element is right of target. + RIGHT = 4; + } + + // Maximum number of pairs the client is allowed to compare. + // + // If there are too many pairs to compare, the client bails out and returns + // the status TOO_MANY_CANDIDATES to the server. + // + // The maximum number of pairs is limited, to avoid clients being slowed + // down by overly expensive selectors, as the current algorithm is not + // optimized for large number of pairs. Authors of selectors must take care + // to keep the number of pairs reasonable. + // + // For example, avoid looking for "a div near label X". This will be too + // slow to process. Look instead for "a button near label X" or "a clickable + // div near label X". + // + // This setting must not be exposed to scripts. It must not be increased + // just to allow that one slow selector. This is a value that must be + // maintained by the team responsible for keeping clients running properly. + optional int32 max_pairs = 5 [default = 50]; + } - // Element must be visible. Visible elements are elements that have a box - // model. The box model is not checked at all, so an element with a zero size - // bounding box is considered visible. - MUST_BE_VISIBLE = 1; + message EmptyFilter {} - // It's enough for the element to exist in the DOM tree for it to match. - MUST_EXIST = 2; + reserved 1 to 8; } // Contain all arguments to perform a click. message ClickProto { - optional ElementReferenceProto element_to_click = 1; + optional SelectorProto element_to_click = 1; optional ClickType click_type = 2; } // Contain all arguments to perform a select option action. message SelectOptionProto { // The drop down element on which to select an option. - optional ElementReferenceProto element = 2; + optional SelectorProto element = 2; // Value of the option to use. optional string selected_option = 3; @@ -788,7 +932,7 @@ message FocusElementProto { } } // Element to focus on. - optional ElementReferenceProto element = 1; + optional SelectorProto element = 1; // Optional title to show in the status bar. optional string title = 2; @@ -797,7 +941,7 @@ message FocusElementProto { // // Deprecated: use touchable_element_area instead. Ignored if // touchable_element_area is non-empty. - repeated ElementReferenceProto deprecated_touchable_elements = 5; + repeated SelectorProto deprecated_touchable_elements = 5; // Restrict interaction to a series of rectangular areas. optional ElementAreaProto touchable_element_area = 6; @@ -813,7 +957,7 @@ message ElementAreaProto { // // The rectangle is the smallest rectangle that includes all listed elements. message Rectangle { - repeated ElementReferenceProto elements = 1; + repeated SelectorProto elements = 1; // If true, the width of the rectangle always corresponds to the width of // the screen. @@ -833,13 +977,9 @@ message UseAddressProto { // Message used to indicate what form fields should be filled with what // information coming from the address. message RequiredField { - // Fields we add that are not mapped to field_types.h. The values must be - // negative not to overlap with the source. - enum AutofillAssistantCustomField { UNDEFINED = 0; } - // A string containing either a single integer key or multiple "${key}" // placeholders, where the key is an integer corresponding to entries from - // field_types.h or RequiredField::AutofillAssistantCustomField. + // field_types.h or AutofillFormatProto::AutofillAssistantCustomField. // Example: // * "3" -> First name. // * "${3}" -> First name. @@ -847,9 +987,10 @@ message UseAddressProto { // e.g., (+41) (79) (1234567) // Note that the set of actually available fields are outside of our // control and are retrieved automatically from the provided profile. + // An value expression set to an empty string will clear the field. optional string value_expression = 6; - optional ElementReferenceProto element = 2; + optional SelectorProto element = 2; // The strategy used to execute filling the value. optional KeyboardValueFillStrategy fill_strategy = 7; @@ -870,22 +1011,21 @@ message UseAddressProto { // selector must match a generic option, an |inner_text_pattern| will be // added to this element reference to match a single option. // Both clicks use the same |click_type|. - optional ElementReferenceProto option_element_to_click = 9; + optional SelectorProto option_element_to_click = 9; optional ClickType click_type = 10; reserved 1, 3; } - // An optional name to allow to handle multiple addresses selection (for - // instance a billing and a delivery address). - optional string name = 1; - - // An optional message to show to the user when asking to select an address. - // TODO(crbug.com/806868): Make the prompt a required field. - optional string prompt = 2; + oneof address_source { + // The client memory key from which to retrieve the address. + string name = 1; + // The client model identifier from which to retrieve the address. + string model_identifier = 9; + } // Reference to an element in the form that should be filled. - optional ElementReferenceProto form_field_element = 4; + optional SelectorProto form_field_element = 4; // An optional list of fields that should be filled by this action. repeated RequiredField required_fields = 6; @@ -894,7 +1034,7 @@ message UseAddressProto { // |required_fields|. optional bool skip_autofill = 10; - reserved 7, 8, 9; + reserved 2, 7, 8; } // Fill a form with a credit card if there is one stored in client memory, @@ -903,27 +1043,19 @@ message UseCreditCardProto { // Message used to indicate what form fields should be filled with what // information. message RequiredField { - // Fields we add that are not mapped to field_types.h. The values must be - // negative not to overlap with the source. - enum AutofillAssistantCustomField { - UNDEFINED = 0; - CREDIT_CARD_VERIFICATION_CODE = -1; - CREDIT_CARD_NETWORK = -2; - CREDIT_CARD_RAW_NUMBER = -3; - } - // A string containing either a single integer key or multiple "${key}" // placeholders, where the key is an integer corresponding to entries from - // field_types.h or RequiredField::AutofillAssistantCustomField. + // field_types.h or AutofillFormatProto::AutofillAssistantCustomField. // Example: // * "51" -> Full name. // * "${51}" -> Full Name. // * "${53}/${55}" -> expiration month / expiration year // Note that the set of actually available fields are outside of our // control and are retrieved automatically from the provided credit card. + // An value expression set to an empty string will clear the field. optional string value_expression = 6; - optional ElementReferenceProto element = 2; + optional SelectorProto element = 2; // The strategy used to execute filling the value. optional KeyboardValueFillStrategy fill_strategy = 7; @@ -944,18 +1076,18 @@ message UseCreditCardProto { // selector must match a generic option, an |inner_text_pattern| will be // added to this element reference to match a single option. // Both clicks use the same |click_type|. - optional ElementReferenceProto option_element_to_click = 9; + optional SelectorProto option_element_to_click = 9; optional ClickType click_type = 10; reserved 1, 3; } - // An optional message to show to the user when asking to select a card. - // TODO(crbug.com/806868): Make the prompt a required field. - optional string prompt = 1; + // The client model identifier from which to retrieve the credit card. + // If not specified, will use the card stored in client memory instead. + optional string model_identifier = 4; // A reference to the card number field in the form that should be filled. - optional ElementReferenceProto form_field_element = 3; + optional SelectorProto form_field_element = 3; repeated RequiredField required_fields = 7; @@ -963,7 +1095,7 @@ message UseCreditCardProto { // |required_fields|. optional bool skip_autofill = 8; - reserved 4, 5, 6; + reserved 1, 5, 6; } // Ask Chrome to wait for an element in the DOM. This can be used to only @@ -1004,7 +1136,7 @@ message ElementConditionProto { // The condition matches if the given element exists. An empty // ElementReference never match. - ElementReferenceProto match = 4; + SelectorProto match = 4; } // A payload that identifies this condition. WaitForDom puts this payload @@ -1021,7 +1153,7 @@ message ElementConditionsProto { message UploadDomProto { // The element that should be a root of uploaded DOM. If empty then the whole // page is returned. - optional ElementReferenceProto tree_root = 1; + optional SelectorProto tree_root = 1; } // Shows the progress bar. @@ -1029,19 +1161,47 @@ message ShowProgressBarProto { // Message to show on the progress bar while loading. optional string message = 3; - // Value between 0 and 100 indicating the current progress. Values above 100 - // will be capped to 100, values below 0 will be capped to 0 by the client. - // NOTE: Setting |progress| to 100 is an equivalent of setting |done| to true. - optional int32 progress = 6; + oneof progress_indicator { + // Value between 0 and 100 indicating the current progress. Values above 100 + // will be capped to 100, values below 0 will be capped to 0 by the client. + int32 progress = 6; + + // Value between 0 and |N - 1| (where N is the size of the initial + // |step_icons|) indicating the current state the flow is in, + // marking all previous steps as active. Setting the value to 0 will mark + // only the first step as active, up to |N - 1|, which marks everything up + // to the final step as active. + // If this is used over |progress| the step progress bar will be used. + int32 active_step = 8; + } // Hide the progress bar in the UI. optional bool hide = 7; + + // The configuration of the step progress bar. Only has an impact if the new + // progress bar is being used. This configuration should only be sent once in + // the initial call of the progress bar, as each appearance may cause the + // progress bar being rerendered. The previous configuration will be carried + // over in each new progress |ShowProgressBarAction|. + message StepProgressBarConfiguration { + // This boolean should always be true, unless you wish to switch to the + // old progress bar during the flow. This is not recommended! + optional bool use_step_progress_bar = 1; + + // Set the icons for the new progress bar. The size of the |step_icons| + // gives the length of the progress bar. As such an empty list is not + // supported. The list needs to be at least 2 items long. + repeated DrawableProto step_icons = 2; + } + optional StepProgressBarConfiguration step_progress_bar_configuration = 9; + + reserved 1, 2, 4, 5; } // Contain all arguments to perform a highlight element action. message HighlightElementProto { // The element to highlight. - optional ElementReferenceProto element = 1; + optional SelectorProto element = 1; } // Controls the browser navigation. @@ -1114,7 +1274,7 @@ message WaitForDocumentProto { // If specified, check the document in the given frame, instead // of the main document. - optional ElementReferenceProto frame = 2; + optional SelectorProto frame = 2; // The minimum ready state needed to satisfy the requirement. optional DocumentReadyState min_ready_state = 3 @@ -1185,6 +1345,17 @@ message ShowGenericUiProto { // manager) will be provided and auto-updated in the specified // |model_identifier|. optional RequestLoginOptions request_login_options = 5; + + message PeriodicElementChecks { + message ElementCheck { + // The element condition to be checked during the action. + optional ElementConditionProto element_condition = 1; + // The model identifier to write the result to. + optional string model_identifier = 2; + } + repeated ElementCheck element_checks = 1; + } + optional PeriodicElementChecks periodic_element_checks = 6; } // Allow choosing one or more possibility. If FocusElement was called just @@ -1222,12 +1393,7 @@ message PromptProto { // be transmitted as-is by the client without interpreting. optional bytes server_payload = 5; - // This field is only used when crafting a response Choice for the server - // when the |end_on_navigation| option is set. It means there was a - // navigation event while in prompt mode that ended the action. - optional bool navigation_ended = 17; - - reserved 4, 6, 8, 13, 14; + reserved 4, 6, 8, 13, 14, 17; } repeated Choice choices = 4; @@ -1245,13 +1411,28 @@ message PromptProto { optional bool browse_mode = 7; // When set to true, end prompt on navigation events happening during a prompt - // action. The result sent back to the server will contain a Choice marked as - // |navigation_ended|. + // action. The result sent back to the server in + // ProcessedActionProto.prompt_choice will have |navigation_ended| set to + // true. optional bool end_on_navigation = 8; // A list of domains and subdomains to allow users to navigate to when in // browse mode. repeated string browse_domains_whitelist = 9; + + // Result to pass to ProcessedActionProto. + message Result { + // This field is only used when crafting a response Choice for the server + // when the |end_on_navigation| option is set. It means there was a + // navigation event while in prompt mode that ended the action. + optional bool navigation_ended = 2; + + // Server payload originally found in Choice.server_payload. This should be + // transmitted as-is by the client without interpreting. + optional bytes server_payload = 5; + + reserved 1; + } } message ContactDetailsProto { @@ -1436,7 +1617,7 @@ message PopupListSectionProto { // Asks to provide the data used by UseAddressAction and // UseCreditCardAction. -// Next: 33 +// Next: 34 message CollectUserDataProto { enum TermsAndConditionsState { // No choice has been made yet. @@ -1465,8 +1646,11 @@ message CollectUserDataProto { repeated string supported_basic_card_networks = 6; // Contact details that should be gathered. optional ContactDetailsProto contact_details = 5; - // Override for the text of the confirm button. - optional string confirm_button_text = 7; + // Optional specification for the confirm button (defaults to standard confirm + // chip if not specified). + optional ChipProto confirm_chip = 33; + // Optionally allows confirming through the given direct actions. + optional DirectActionProto confirm_direct_action = 10; // The initial state of the terms & conditions choice. optional TermsAndConditionsState terms_and_conditions_state = 8; // When 'false', hide the terms and conditions box in the UI. @@ -1488,8 +1672,6 @@ message CollectUserDataProto { // Privacy notice telling users that autofill assistant will send personal // data to a third party’s website. optional string privacy_notice_text = 21; - // Optionally allows confiriming through the given direct actions. - optional DirectActionProto confirm_direct_action = 10; // Additional actions available to the user. This can be used for instance to // display a "Show terms" button that will navigate the user to the terms and // conditions page when clicked. @@ -1536,7 +1718,7 @@ message CollectUserDataProto { // address, the selected credit card should also be cleared! repeated string clear_previous_profile_selection = 30; - reserved 26; + reserved 7, 26; } // Stop Autofill Assistant. @@ -1668,7 +1850,7 @@ message SetFormFieldValueProto { } // A reference to the form element whose value should be set. - optional ElementReferenceProto element = 1; + optional SelectorProto element = 1; // The value to set. repeated KeyPress value = 2; @@ -1688,7 +1870,7 @@ message SetFormFieldValueProto { // login details need to be available in the client memory for this action. message GeneratePasswordForFormFieldProto { // A reference to the form element for which to generate a password. - optional ElementReferenceProto element = 1; + optional SelectorProto element = 1; // The client memory key to store the generated password. optional string memory_key = 2; } @@ -1703,7 +1885,7 @@ message SaveGeneratedPasswordProto { // Set an element attribute to a specific value. message SetAttributeProto { // A reference to the form element whose attribute should be set. - optional ElementReferenceProto element = 1; + optional SelectorProto element = 1; // The attribute to set, e.g. ["style", "position"] to change // element.style.position. diff --git a/chromium/components/autofill_assistant/browser/service_impl.cc b/chromium/components/autofill_assistant/browser/service_impl.cc index 0298031062c..45364efb3c7 100644 --- a/chromium/components/autofill_assistant/browser/service_impl.cc +++ b/chromium/components/autofill_assistant/browser/service_impl.cc @@ -133,6 +133,7 @@ void ServiceImpl::GetScriptsForUrl(const GURL& url, ResponseCallback callback) { DCHECK(url.is_valid()); + UpdateMutableClientContextFields(); SendRequest(AddLoader( script_server_url_, ProtocolUtils::CreateGetScriptsRequest( @@ -148,6 +149,7 @@ void ServiceImpl::GetActions(const std::string& script_path, ResponseCallback callback) { DCHECK(!script_path.empty()); + UpdateMutableClientContextFields(); SendRequest( AddLoader(script_action_server_url_, ProtocolUtils::CreateInitialScriptActionsRequest( @@ -162,6 +164,7 @@ void ServiceImpl::GetNextActions( const std::string& previous_script_payload, const std::vector<ProcessedActionProto>& processed_actions, ResponseCallback callback) { + UpdateMutableClientContextFields(); SendRequest(AddLoader( script_action_server_url_, ProtocolUtils::CreateNextScriptActionsRequest( @@ -335,4 +338,12 @@ ClientContextProto ServiceImpl::CreateClientContext( return context; } +void ServiceImpl::UpdateMutableClientContextFields() { + client_context_.set_accessibility_enabled(client_->IsAccessibilityEnabled()); + client_context_.set_signed_into_chrome_status( + client_->GetChromeSignedInEmailAddress().empty() + ? ClientContextProto::NOT_SIGNED_IN + : ClientContextProto::SIGNED_IN); +} + } // namespace autofill_assistant diff --git a/chromium/components/autofill_assistant/browser/service_impl.h b/chromium/components/autofill_assistant/browser/service_impl.h index 57aa4b2c5d1..df95f2f6c3f 100644 --- a/chromium/components/autofill_assistant/browser/service_impl.h +++ b/chromium/components/autofill_assistant/browser/service_impl.h @@ -30,6 +30,7 @@ class Client; // Native autofill assistant service which communicates with the server to get // scripts and client actions. +// TODO(b/158998456): Add unit tests. class ServiceImpl : public Service { public: // Convenience method for creating a service from a client. @@ -110,6 +111,9 @@ class ServiceImpl : public Service { const std::string& locale, const std::string& country_code, const DeviceContext& device_context); + // Updates the subset of |client_context_| fields that may change during a + // flow. + void UpdateMutableClientContextFields(); content::BrowserContext* context_; GURL script_server_url_; @@ -136,12 +140,14 @@ class ServiceImpl : public Service { // The client context is cached here to avoid having to recreate it for // every message. - const ClientContextProto client_context_; + ClientContextProto client_context_; const Client* client_; base::WeakPtrFactory<ServiceImpl> weak_ptr_factory_; + FRIEND_TEST_ALL_PREFIXES(ServiceImplTestSignedInStatus, SetsSignedInStatus); + DISALLOW_COPY_AND_ASSIGN(ServiceImpl); }; diff --git a/chromium/components/autofill_assistant/browser/service_impl_unittest.cc b/chromium/components/autofill_assistant/browser/service_impl_unittest.cc index 6def5e906eb..ce97f931f7f 100644 --- a/chromium/components/autofill_assistant/browser/service_impl_unittest.cc +++ b/chromium/components/autofill_assistant/browser/service_impl_unittest.cc @@ -38,4 +38,25 @@ TEST_F(ServiceImplTest, CreatesValidHashFromEmail) { "2c8fa87717fab622bb5cc4d18135fe30dae339efd274b450022d361be92b48c3"); } +class ServiceImplTestSignedInStatus + : public ServiceImplTest, + public testing::WithParamInterface< + std::pair<std::string, ClientContextProto::SignedIntoChromeStatus>> { +}; + +INSTANTIATE_TEST_SUITE_P( + ParametrizedTests, + ServiceImplTestSignedInStatus, + testing::Values(std::make_pair("", ClientContextProto::NOT_SIGNED_IN), + std::make_pair("bob@email.com", + ClientContextProto::SIGNED_IN))); + +TEST_P(ServiceImplTestSignedInStatus, SetsSignedInStatus) { + ON_CALL(mock_client_, GetChromeSignedInEmailAddress) + .WillByDefault(Return(GetParam().first)); + service_impl_->UpdateMutableClientContextFields(); + EXPECT_EQ(service_impl_->client_context_.signed_into_chrome_status(), + GetParam().second); +} + } // namespace autofill_assistant diff --git a/chromium/components/autofill_assistant/browser/trigger_context.cc b/chromium/components/autofill_assistant/browser/trigger_context.cc index f3241f2e0c7..ab59be54a4c 100644 --- a/chromium/components/autofill_assistant/browser/trigger_context.cc +++ b/chromium/components/autofill_assistant/browser/trigger_context.cc @@ -3,6 +3,7 @@ // found in the LICENSE file. #include "components/autofill_assistant/browser/trigger_context.h" +#include "base/strings/string_split.h" namespace autofill_assistant { @@ -59,6 +60,15 @@ std::string TriggerContextImpl::experiment_ids() const { return experiment_ids_; } +bool TriggerContextImpl::HasExperimentId( + const std::string& experiment_id) const { + std::vector<std::string> experiments = base::SplitString( + experiment_ids_, ",", base::WhitespaceHandling::TRIM_WHITESPACE, + base::SplitResult::SPLIT_WANT_NONEMPTY); + return std::find(experiments.begin(), experiments.end(), experiment_id) != + experiments.end(); +} + bool TriggerContextImpl::is_cct() const { return cct_; } @@ -113,6 +123,16 @@ std::string MergedTriggerContext::experiment_ids() const { return experiment_ids; } +bool MergedTriggerContext::HasExperimentId( + const std::string& experiment_id) const { + for (const TriggerContext* context : contexts_) { + if (context->HasExperimentId(experiment_id)) { + return true; + } + } + return false; +} + bool MergedTriggerContext::is_cct() const { for (const TriggerContext* context : contexts_) { if (context->is_cct()) diff --git a/chromium/components/autofill_assistant/browser/trigger_context.h b/chromium/components/autofill_assistant/browser/trigger_context.h index 2b62cd3e75b..735a1b5c782 100644 --- a/chromium/components/autofill_assistant/browser/trigger_context.h +++ b/chromium/components/autofill_assistant/browser/trigger_context.h @@ -48,6 +48,9 @@ class TriggerContext { // Returns a comma-separated set of experiment ids. virtual std::string experiment_ids() const = 0; + // Returns whether an experiment is contained in |experiment_ids|. + virtual bool HasExperimentId(const std::string& experiment_id) const = 0; + // Returns true if we're in a Chrome Custom Tab created for Autofill // Assistant, originally created through AutofillAssistantFacade.start(), in // Java. @@ -75,12 +78,6 @@ class TriggerContextImpl : public TriggerContext { ~TriggerContextImpl() override; - // Implements TriggerContext: - void AddParameters(google::protobuf::RepeatedPtrField<ScriptParameterProto>* - dest) const override; - base::Optional<std::string> GetParameter( - const std::string& name) const override; - void SetCCT(bool value) { cct_ = value; } void SetOnboardingShown(bool value) { onboarding_shown_ = value; } void SetDirectAction(bool value) { direct_action_ = value; } @@ -88,7 +85,13 @@ class TriggerContextImpl : public TriggerContext { caller_account_hash_ = value; } + // Implements TriggerContext: + void AddParameters(google::protobuf::RepeatedPtrField<ScriptParameterProto>* + dest) const override; + base::Optional<std::string> GetParameter( + const std::string& name) const override; std::string experiment_ids() const override; + bool HasExperimentId(const std::string& experiment_id) const override; bool is_cct() const override; bool is_onboarding_shown() const override; bool is_direct_action() const override; @@ -125,6 +128,7 @@ class MergedTriggerContext : public TriggerContext { base::Optional<std::string> GetParameter( const std::string& name) const override; std::string experiment_ids() const override; + bool HasExperimentId(const std::string& experiment_id) const override; bool is_cct() const override; bool is_onboarding_shown() const override; bool is_direct_action() const override; diff --git a/chromium/components/autofill_assistant/browser/trigger_context_unittest.cc b/chromium/components/autofill_assistant/browser/trigger_context_unittest.cc index 38b36831e5d..2268ae16dfe 100644 --- a/chromium/components/autofill_assistant/browser/trigger_context_unittest.cc +++ b/chromium/components/autofill_assistant/browser/trigger_context_unittest.cc @@ -80,7 +80,7 @@ TEST(TriggerContextTest, Merge) { EXPECT_EQ("exp1,exp2", merged->experiment_ids()); } -TEST(TriggerContextText, CCT) { +TEST(TriggerContextTest, CCT) { TriggerContextImpl context; EXPECT_FALSE(context.is_cct()); @@ -88,7 +88,7 @@ TEST(TriggerContextText, CCT) { EXPECT_TRUE(context.is_cct()); } -TEST(TriggerContextText, MergeCCT) { +TEST(TriggerContextTest, MergeCCT) { auto empty = TriggerContext::CreateEmpty(); auto all_empty = TriggerContext::Merge({empty.get(), empty.get()}); @@ -124,7 +124,7 @@ TEST(TriggerContextTest, MergeOnboardingShown) { EXPECT_TRUE(one_with_onboarding->is_onboarding_shown()); } -TEST(TriggerContextText, DirectAction) { +TEST(TriggerContextTest, DirectAction) { TriggerContextImpl context; EXPECT_FALSE(context.is_direct_action()); @@ -132,7 +132,7 @@ TEST(TriggerContextText, DirectAction) { EXPECT_TRUE(context.is_direct_action()); } -TEST(TriggerContextText, MergeDirectAction) { +TEST(TriggerContextTest, MergeDirectAction) { auto empty = TriggerContext::CreateEmpty(); auto all_empty = TriggerContext::Merge({empty.get(), empty.get()}); @@ -146,7 +146,7 @@ TEST(TriggerContextText, MergeDirectAction) { EXPECT_TRUE(one_direct_action->is_direct_action()); } -TEST(TriggerContextText, MergeAccountsMatchingStatusTest) { +TEST(TriggerContextTest, MergeAccountsMatchingStatusTest) { auto empty = TriggerContext::CreateEmpty(); auto all_empty = TriggerContext::Merge({empty.get(), empty.get()}); @@ -161,5 +161,45 @@ TEST(TriggerContextText, MergeAccountsMatchingStatusTest) { "accountsha"); } +TEST(TriggerContextTest, HasExperimentId) { + std::map<std::string, std::string> parameters; + + auto context = TriggerContext::Create(parameters, "1,2,3"); + ASSERT_TRUE(context); + + EXPECT_TRUE(context->HasExperimentId("2")); + EXPECT_FALSE(context->HasExperimentId("4")); + + auto other_context = TriggerContext::Create(parameters, "4,5,6"); + ASSERT_TRUE(other_context); + + EXPECT_TRUE(other_context->HasExperimentId("4")); + EXPECT_FALSE(other_context->HasExperimentId("2")); + + auto merged = TriggerContext::Merge({context.get(), other_context.get()}); + ASSERT_TRUE(merged); + + EXPECT_TRUE(merged->HasExperimentId("2")); + EXPECT_TRUE(merged->HasExperimentId("4")); + EXPECT_FALSE(merged->HasExperimentId("7")); + + // Double commas should not allow empty element to match. + auto double_comma = TriggerContext::Create(parameters, "1,,2"); + EXPECT_TRUE(double_comma->HasExperimentId("2")); + EXPECT_FALSE(double_comma->HasExperimentId("")); + + // Empty context should not aloow empty element to match. + auto empty = TriggerContext::Create(parameters, ""); + EXPECT_FALSE(empty->HasExperimentId("")); + + // Lone comma does not create empty elements. + auto lone_comma = TriggerContext::Create(parameters, ","); + EXPECT_FALSE(lone_comma->HasExperimentId("")); + + // Single element should match. + auto single_element = TriggerContext::Create(parameters, "1"); + EXPECT_TRUE(single_element->HasExperimentId("1")); +} + } // namespace } // namespace autofill_assistant diff --git a/chromium/components/autofill_assistant/browser/ui_delegate.h b/chromium/components/autofill_assistant/browser/ui_delegate.h index afb7ea6c1ee..6c12d9a38e0 100644 --- a/chromium/components/autofill_assistant/browser/ui_delegate.h +++ b/chromium/components/autofill_assistant/browser/ui_delegate.h @@ -42,6 +42,9 @@ class UiDelegate { // Returns the current state of the controller. virtual AutofillAssistantState GetState() = 0; + // Returns the last navigation id that caused an error. + virtual int64_t GetErrorCausingNavigationId() const = 0; + // Called when user interaction within the allowed touchable area was // detected. This should cause rerun of preconditions check. virtual void OnUserInteractionInsideTouchableArea() = 0; @@ -65,9 +68,19 @@ class UiDelegate { // Returns the current progress; a percentage. virtual int GetProgress() const = 0; + // Returns the currently active progress step. + virtual base::Optional<int> GetProgressActiveStep() const = 0; + // Returns whether the progress bar is visible. virtual bool GetProgressVisible() const = 0; + // Returns the current configuration of the step progress bar. + virtual base::Optional<ShowProgressBarProto::StepProgressBarConfiguration> + GetStepProgressBarConfiguration() const = 0; + + // Returns whether the progress bar should show an error state. + virtual bool GetProgressBarErrorState() const = 0; + // Returns the current set of user actions. virtual const std::vector<UserAction>& GetUserActions() const = 0; diff --git a/chromium/components/autofill_assistant/browser/user_data_util.cc b/chromium/components/autofill_assistant/browser/user_data_util.cc index dbaabf36417..1b7da34007f 100644 --- a/chromium/components/autofill_assistant/browser/user_data_util.cc +++ b/chromium/components/autofill_assistant/browser/user_data_util.cc @@ -133,6 +133,26 @@ bool CompletenessComparePaymentInstruments( return complete_fields_a > complete_fields_b; } +bool IsCompleteAddress(const autofill::AutofillProfile* profile, + bool require_postal_code) { + if (!profile) { + return false; + } + // We use a hard coded locale here since we are only interested in whether + // fields are empty or not. + auto address_data = + autofill::i18n::CreateAddressDataFromAutofillProfile(*profile, "en-US"); + if (!autofill::addressinput::HasAllRequiredFields(*address_data)) { + return false; + } + + if (require_postal_code && address_data->postal_code.empty()) { + return false; + } + + return true; +} + } // namespace std::vector<int> SortContactsByCompleteness( @@ -252,4 +272,85 @@ bool CompareContactDetails( return true; } +bool IsCompleteContact( + const autofill::AutofillProfile* profile, + const CollectUserDataOptions& collect_user_data_options) { + if (!collect_user_data_options.request_payer_name && + !collect_user_data_options.request_payer_email && + !collect_user_data_options.request_payer_phone) { + return true; + } + + if (!profile) { + return false; + } + + if (collect_user_data_options.request_payer_name && + !profile->HasInfo(autofill::NAME_FULL)) { + return false; + } + + if (collect_user_data_options.request_payer_email && + !profile->HasInfo(autofill::EMAIL_ADDRESS)) { + return false; + } + + if (collect_user_data_options.request_payer_phone && + !profile->HasInfo(autofill::PHONE_HOME_WHOLE_NUMBER)) { + return false; + } + return true; +} + +bool IsCompleteShippingAddress( + const autofill::AutofillProfile* profile, + const CollectUserDataOptions& collect_user_data_options) { + return !collect_user_data_options.request_shipping || + IsCompleteAddress(profile, /* require_postal_code = */ false); +} + +bool IsCompleteCreditCard( + const autofill::CreditCard* credit_card, + const autofill::AutofillProfile* billing_profile, + const CollectUserDataOptions& collect_user_data_options) { + if (!collect_user_data_options.request_payment_method) { + return true; + } + + if (!credit_card || !billing_profile || + credit_card->billing_address_id().empty()) { + return false; + } + + if (!IsCompleteAddress( + billing_profile, + collect_user_data_options.require_billing_postal_code)) { + return false; + } + + if (credit_card->record_type() != autofill::CreditCard::MASKED_SERVER_CARD && + !credit_card->HasValidCardNumber()) { + // Can't check validity of masked server card numbers because they are + // incomplete until decrypted. + return false; + } + + if (!credit_card->HasValidExpirationDate()) { + return false; + } + + std::string basic_card_network = + autofill::data_util::GetPaymentRequestData(credit_card->network()) + .basic_card_issuer_network; + if (!collect_user_data_options.supported_basic_card_networks.empty() && + std::find(collect_user_data_options.supported_basic_card_networks.begin(), + collect_user_data_options.supported_basic_card_networks.end(), + basic_card_network) == + collect_user_data_options.supported_basic_card_networks.end()) { + return false; + } + + return true; +} + } // namespace autofill_assistant diff --git a/chromium/components/autofill_assistant/browser/user_data_util.h b/chromium/components/autofill_assistant/browser/user_data_util.h index 58ac0df9715..e5fa363aa72 100644 --- a/chromium/components/autofill_assistant/browser/user_data_util.h +++ b/chromium/components/autofill_assistant/browser/user_data_util.h @@ -62,6 +62,18 @@ bool CompareContactDetails( const autofill::AutofillProfile* a, const autofill::AutofillProfile* b); +bool IsCompleteContact(const autofill::AutofillProfile* profile, + const CollectUserDataOptions& collect_user_data_options); + +bool IsCompleteShippingAddress( + const autofill::AutofillProfile* profile, + const CollectUserDataOptions& collect_user_data_options); + +bool IsCompleteCreditCard( + const autofill::CreditCard* credit_card, + const autofill::AutofillProfile* billing_profile, + const CollectUserDataOptions& collect_user_data_options); + } // namespace autofill_assistant #endif // COMPONENTS_AUTOFILL_ASSISTANT_BROWSER_USER_DATA_UTIL_H_ diff --git a/chromium/components/autofill_assistant/browser/user_data_util_unittest.cc b/chromium/components/autofill_assistant/browser/user_data_util_unittest.cc index ef4fb966ca3..0cf04fd4560 100644 --- a/chromium/components/autofill_assistant/browser/user_data_util_unittest.cc +++ b/chromium/components/autofill_assistant/browser/user_data_util_unittest.cc @@ -33,7 +33,7 @@ TEST(UserDataUtilTest, SortsCompleteContactsAlphabetically) { autofill::test::SetProfileInfo(profile_unicode.get(), "\xC3\x85" "dam", - "", "West", "aedam.west@gmail.com", "", "", "", + "", "West", "adam.west@gmail.com", "", "", "", "", "", "", "", ""); // Specify profiles in reverse order to force sorting. @@ -375,13 +375,13 @@ TEST(UserDataUtilTest, GetDefaultSelectionForCompletePaymentInstruments) { } TEST(UserDataUtilTest, CompareContactDetailsMatch) { - auto profile_a = std::make_unique<autofill::AutofillProfile>(); - autofill::test::SetProfileInfo(profile_a.get(), "Adam", "", "West", + autofill::AutofillProfile profile_a; + autofill::test::SetProfileInfo(&profile_a, "Adam", "", "West", "adam.west@gmail.com", "", "", "", "", "", "", "", "+41"); - auto profile_b = std::make_unique<autofill::AutofillProfile>(); - autofill::test::SetProfileInfo(profile_b.get(), "Adam", "", "West", + autofill::AutofillProfile profile_b; + autofill::test::SetProfileInfo(&profile_b, "Adam", "", "West", "adam.west@gmail.com", "", "", "", "", "", "", "", "+41"); @@ -390,84 +390,79 @@ TEST(UserDataUtilTest, CompareContactDetailsMatch) { options.request_payer_email = true; options.request_payer_phone = true; - EXPECT_TRUE(CompareContactDetails(options, profile_a.get(), profile_b.get())); + EXPECT_TRUE(CompareContactDetails(options, &profile_a, &profile_b)); } TEST(UserDataUtilTest, CompareContactDetailsMismatchForNoChecks) { - auto profile_a = std::make_unique<autofill::AutofillProfile>(); - autofill::test::SetProfileInfo(profile_a.get(), "Adam", "", "West", + autofill::AutofillProfile profile_a; + autofill::test::SetProfileInfo(&profile_a, "Adam", "", "West", "adam.west@gmail.com", "", "", "", "", "", "", "", "+41"); - auto profile_b = std::make_unique<autofill::AutofillProfile>(); - autofill::test::SetProfileInfo(profile_b.get(), "Adam", "", "West", + autofill::AutofillProfile profile_b; + autofill::test::SetProfileInfo(&profile_b, "Adam", "", "West", "adam.west@gmail.com", "", "", "", "", "", "", "", "+41"); CollectUserDataOptions options; - EXPECT_FALSE( - CompareContactDetails(options, profile_a.get(), profile_b.get())); + EXPECT_FALSE(CompareContactDetails(options, &profile_a, &profile_b)); } TEST(UserDataUtilTest, CompareContactDetailsMismatches) { - auto profile_truth = std::make_unique<autofill::AutofillProfile>(); - autofill::test::SetProfileInfo(profile_truth.get(), "Adam", "", "West", + autofill::AutofillProfile profile_truth; + autofill::test::SetProfileInfo(&profile_truth, "Adam", "", "West", "adam.west@gmail.com", "", "", "", "", "", "", "", "+41"); - auto profile_mismatching_name = std::make_unique<autofill::AutofillProfile>(); - autofill::test::SetProfileInfo(profile_mismatching_name.get(), "Berta", "", - "West", "adam.west@gmail.com", "", "", "", "", - "", "", "", "+41"); + autofill::AutofillProfile profile_mismatching_name; + autofill::test::SetProfileInfo(&profile_mismatching_name, "Berta", "", "West", + "adam.west@gmail.com", "", "", "", "", "", "", + "", "+41"); - auto profile_mismatching_email = - std::make_unique<autofill::AutofillProfile>(); - autofill::test::SetProfileInfo(profile_mismatching_email.get(), "Adam", "", - "West", "berta.west@gmail.com", "", "", "", "", - "", "", "", "+41"); + autofill::AutofillProfile profile_mismatching_email; + autofill::test::SetProfileInfo(&profile_mismatching_email, "Adam", "", "West", + "berta.west@gmail.com", "", "", "", "", "", "", + "", "+41"); - auto profile_mismatching_phone = - std::make_unique<autofill::AutofillProfile>(); - autofill::test::SetProfileInfo(profile_mismatching_name.get(), "Adam", "", - "West", "adam.west@gmail.com", "", "", "", "", - "", "", "", "+44"); + autofill::AutofillProfile profile_mismatching_phone; + autofill::test::SetProfileInfo(&profile_mismatching_name, "Adam", "", "West", + "adam.west@gmail.com", "", "", "", "", "", "", + "", "+44"); CollectUserDataOptions options; options.request_payer_name = true; options.request_payer_email = true; options.request_payer_phone = true; - EXPECT_FALSE(CompareContactDetails(options, profile_truth.get(), - profile_mismatching_name.get())); - EXPECT_FALSE(CompareContactDetails(options, profile_truth.get(), - profile_mismatching_email.get())); - EXPECT_FALSE(CompareContactDetails(options, profile_truth.get(), - profile_mismatching_phone.get())); + EXPECT_FALSE(CompareContactDetails(options, &profile_truth, + &profile_mismatching_name)); + EXPECT_FALSE(CompareContactDetails(options, &profile_truth, + &profile_mismatching_email)); + EXPECT_FALSE(CompareContactDetails(options, &profile_truth, + &profile_mismatching_phone)); } TEST(UserDataUtilTest, CompareContactDetailsMatchesForUnqueriedFields) { - auto profile_truth = std::make_unique<autofill::AutofillProfile>(); - autofill::test::SetProfileInfo(profile_truth.get(), "Adam", "", "West", + autofill::AutofillProfile profile_truth; + autofill::test::SetProfileInfo(&profile_truth, "Adam", "", "West", "adam.west@gmail.com", "", "", "", "", "", "", "", "+41"); - auto profile_mismatching_name = std::make_unique<autofill::AutofillProfile>(); - autofill::test::SetProfileInfo(profile_mismatching_name.get(), "Berta", "", - "West", "adam.west@gmail.com", "", "", "", "", - "", "", "", "+41"); + autofill::AutofillProfile profile_mismatching_name; + autofill::test::SetProfileInfo(&profile_mismatching_name, "Berta", "", "West", + "adam.west@gmail.com", "", "", "", "", "", "", + "", "+41"); - auto profile_mismatching_email = - std::make_unique<autofill::AutofillProfile>(); - autofill::test::SetProfileInfo(profile_mismatching_email.get(), "Adam", "", - "West", "berta.west@gmail.com", "", "", "", "", - "", "", "", "+41"); + autofill::AutofillProfile profile_mismatching_email; + autofill::test::SetProfileInfo(&profile_mismatching_email, "Adam", "", "West", + "berta.west@gmail.com", "", "", "", "", "", "", + "", "+41"); - auto profile_mismatching_phone = - std::make_unique<autofill::AutofillProfile>(); - autofill::test::SetProfileInfo(profile_mismatching_phone.get(), "Adam", "", - "West", "adam.west@gmail.com", "", "", "", "", - "", "", "", "+44"); + autofill::AutofillProfile profile_mismatching_phone; + autofill::test::SetProfileInfo(&profile_mismatching_phone, "Adam", "", "West", + "adam.west@gmail.com", "", "", "", "", "", "", + "", "+44"); CollectUserDataOptions options_no_check_name; options_no_check_name.request_payer_email = true; @@ -481,12 +476,199 @@ TEST(UserDataUtilTest, CompareContactDetailsMatchesForUnqueriedFields) { options_no_check_phone.request_payer_name = true; options_no_check_phone.request_payer_email = true; - EXPECT_TRUE(CompareContactDetails(options_no_check_name, profile_truth.get(), - profile_mismatching_name.get())); - EXPECT_TRUE(CompareContactDetails(options_no_check_email, profile_truth.get(), - profile_mismatching_email.get())); - EXPECT_TRUE(CompareContactDetails(options_no_check_phone, profile_truth.get(), - profile_mismatching_phone.get())); + EXPECT_TRUE(CompareContactDetails(options_no_check_name, &profile_truth, + &profile_mismatching_name)); + EXPECT_TRUE(CompareContactDetails(options_no_check_email, &profile_truth, + &profile_mismatching_email)); + EXPECT_TRUE(CompareContactDetails(options_no_check_phone, &profile_truth, + &profile_mismatching_phone)); +} + +TEST(UserDataUtilTest, ContactCompletenessNotRequired) { + CollectUserDataOptions not_required_options; + not_required_options.request_payer_name = false; + not_required_options.request_payer_email = false; + not_required_options.request_payer_phone = false; + + EXPECT_TRUE(IsCompleteContact(nullptr, not_required_options)); +} + +TEST(UserDataUtilTest, ContactCompletenessRequireName) { + autofill::AutofillProfile contact; + CollectUserDataOptions require_name_options; + require_name_options.request_payer_name = true; + + EXPECT_FALSE(IsCompleteContact(nullptr, require_name_options)); + autofill::test::SetProfileInfo(&contact, /* first_name= */ "", + /* middle_name= */ "", + /* last_name= */ "", "adam.west@gmail.com", "", + "", "", "", "", "", "", "+41"); + EXPECT_FALSE(IsCompleteContact(&contact, require_name_options)); + autofill::test::SetProfileInfo(&contact, "John", /* middle_name= */ "", "Doe", + "", "", "", "", "", "", "", "", ""); + EXPECT_TRUE(IsCompleteContact(&contact, require_name_options)); +} + +TEST(UserDataUtilTest, ContactCompletenessRequireEmail) { + autofill::AutofillProfile contact; + CollectUserDataOptions require_email_options; + require_email_options.request_payer_email = true; + + EXPECT_FALSE(IsCompleteContact(nullptr, require_email_options)); + autofill::test::SetProfileInfo(&contact, "John", "", "Doe", + /* email= */ "", "", "", "", "", "", "", "", + "+41"); + EXPECT_FALSE(IsCompleteContact(&contact, require_email_options)); + autofill::test::SetProfileInfo(&contact, "John", "", "Doe", + "john.doe@gmail.com", "", "", "", "", "", "", + "", "+41"); + EXPECT_TRUE(IsCompleteContact(&contact, require_email_options)); +} + +TEST(UserDataUtilTest, ContactCompletenessRequirePhone) { + autofill::AutofillProfile contact; + CollectUserDataOptions require_phone_options; + require_phone_options.request_payer_phone = true; + + EXPECT_FALSE(IsCompleteContact(nullptr, require_phone_options)); + autofill::test::SetProfileInfo(&contact, "John", "", "Doe", + "john.doe@gmail.com", "", "", "", "", "", "", + "", + /* phone= */ ""); + EXPECT_FALSE(IsCompleteContact(&contact, require_phone_options)); + autofill::test::SetProfileInfo(&contact, "", "", "", "", "", "", "", "", "", + "", "", "+41"); + EXPECT_TRUE(IsCompleteContact(&contact, require_phone_options)); +} + +TEST(UserDataUtilTest, CompleteShippingAddressNotRequired) { + CollectUserDataOptions not_required_options; + not_required_options.request_shipping = false; + + EXPECT_TRUE(IsCompleteShippingAddress(nullptr, not_required_options)); +} + +TEST(UserDataUtilTest, CompleteShippingAddressRequired) { + autofill::AutofillProfile address; + CollectUserDataOptions require_shipping_options; + require_shipping_options.request_shipping = true; + + autofill::test::SetProfileInfo(&address, "John", "", "Doe", + "john.doe@gmail.com", "", /* address1= */ "", + /* address2= */ "", /* city= */ "", + /* state= */ "", /* zip_code= */ "", + /* country= */ "", "+41"); + EXPECT_FALSE(IsCompleteShippingAddress(&address, require_shipping_options)); + autofill::test::SetProfileInfo(&address, "John", "", "Doe", + /* email= */ "", "", "Brandschenkestrasse 110", + "", "Zurich", "Zurich", "8002", "CH", + /* phone= */ ""); + EXPECT_TRUE(IsCompleteShippingAddress(&address, require_shipping_options)); +} + +TEST(UserDataUtilTest, CompleteCreditCardNotRequired) { + CollectUserDataOptions not_required_options; + not_required_options.request_payment_method = false; + + EXPECT_TRUE(IsCompleteCreditCard(nullptr, nullptr, not_required_options)); +} + +TEST(UserDataUtilTest, CompleteCreditCardZipNotRequired) { + CollectUserDataOptions payment_options; + payment_options.request_payment_method = true; + payment_options.require_billing_postal_code = false; + + autofill::AutofillProfile address; + autofill::CreditCard card; + autofill::test::SetCreditCardInfo(&card, "Adam West", "4111111111111111", "1", + "2050", + /* billing_address_id= */ "id"); + + EXPECT_FALSE(IsCompleteCreditCard(nullptr, nullptr, payment_options)); + EXPECT_FALSE(IsCompleteCreditCard(&card, nullptr, payment_options)); + EXPECT_FALSE(IsCompleteCreditCard(&card, &address, payment_options)); + // UK addresses do not require a zip code, they are complete without it. + autofill::test::SetProfileInfo(&address, "John", "", "Doe", + /* email= */ "", "", "Baker Street 221b", "", + "London", /* state= */ "", + /* zipcode= */ "", "UK", /* phone= */ ""); + EXPECT_TRUE(IsCompleteCreditCard(&card, &address, payment_options)); + // CH addresses require a zip code to be complete. This check outranks the + // |require_billing_postal_code| flag. + autofill::test::SetProfileInfo(&address, "John", "", "Doe", + /* email= */ "", "", "Brandschenkestrasse 110", + "", "Zurich", "Zurich", + /* zipcode= */ "", "CH", /* phone= */ ""); + EXPECT_FALSE(IsCompleteCreditCard(&card, &address, payment_options)); +} + +TEST(UserDataUtilTest, CompleteCreditCardZipRequired) { + CollectUserDataOptions payment_options; + payment_options.request_payment_method = true; + payment_options.require_billing_postal_code = true; + + autofill::AutofillProfile address; + autofill::CreditCard card; + autofill::test::SetCreditCardInfo(&card, "Adam West", "4111111111111111", "1", + "2050", + /* billing_address_id= */ "id"); + + EXPECT_FALSE(IsCompleteCreditCard(nullptr, nullptr, payment_options)); + EXPECT_FALSE(IsCompleteCreditCard(&card, nullptr, payment_options)); + EXPECT_FALSE(IsCompleteCreditCard(&card, &address, payment_options)); + autofill::test::SetProfileInfo(&address, "John", "", "Doe", + /* email= */ "", "", "Baker Street 221b", "", + "London", /* state= */ "", + /* zipcode= */ "", "UK", /* phone= */ ""); + EXPECT_FALSE(IsCompleteCreditCard(&card, &address, payment_options)); + autofill::test::SetProfileInfo(&address, "John", "", "Doe", + /* email= */ "", "", "Baker Street 221b", "", + "London", /* state= */ "", "WC2N 5DU", "UK", + /* phone= */ ""); + EXPECT_TRUE(IsCompleteCreditCard(&card, &address, payment_options)); +} + +TEST(UserDataUtilTest, CompleteExpiredCreditCard) { + CollectUserDataOptions payment_options; + payment_options.request_payment_method = true; + + autofill::AutofillProfile address; + autofill::test::SetProfileInfo( + &address, "John", "", "Doe", "john.doe@gmail.com", "", + "Brandschenkestrasse 110", "", "Zurich", "Zurich", "8002", "CH", "+41"); + autofill::CreditCard card; + + autofill::test::SetCreditCardInfo(&card, "Adam West", "4111111111111111", "1", + "2000", + /* billing_address_id= */ "id"); + EXPECT_FALSE(IsCompleteCreditCard(&card, &address, payment_options)); + autofill::test::SetCreditCardInfo(&card, "Adam West", "4111111111111111", "1", + "2050", + /* billing_address_id= */ "id"); + EXPECT_TRUE(IsCompleteCreditCard(&card, &address, payment_options)); +} + +TEST(UserDataUtilTest, CompleteCreditCardWithBadNetwork) { + autofill::AutofillProfile address; + autofill::test::SetProfileInfo( + &address, "John", "", "Doe", "john.doe@gmail.com", "", + "Brandschenkestrasse 110", "", "Zurich", "Zurich", "8002", "CH", "+41"); + autofill::CreditCard card; + autofill::test::SetCreditCardInfo(&card, "Adam West", "4111111111111111", "1", + "2050", + /* billing_address_id= */ "id"); + + CollectUserDataOptions payment_options_mastercard; + payment_options_mastercard.request_payment_method = true; + payment_options_mastercard.supported_basic_card_networks.emplace_back( + "mastercard"); + EXPECT_FALSE( + IsCompleteCreditCard(&card, &address, payment_options_mastercard)); + + CollectUserDataOptions payment_options_visa; + payment_options_visa.request_payment_method = true; + payment_options_visa.supported_basic_card_networks.emplace_back("visa"); + EXPECT_TRUE(IsCompleteCreditCard(&card, &address, payment_options_visa)); } } // namespace diff --git a/chromium/components/autofill_assistant/browser/user_model.cc b/chromium/components/autofill_assistant/browser/user_model.cc index 2130602b4e1..020f1b27a7b 100644 --- a/chromium/components/autofill_assistant/browser/user_model.cc +++ b/chromium/components/autofill_assistant/browser/user_model.cc @@ -3,7 +3,9 @@ // found in the LICENSE file. #include "components/autofill_assistant/browser/user_model.h" +#include "components/autofill_assistant/browser/field_formatter.h" +#include "base/logging.h" #include "base/strings/string_number_conversions.h" #include "base/strings/string_util.h" #include "third_party/re2/src/re2/re2.h" @@ -55,9 +57,17 @@ base::WeakPtr<UserModel> UserModel::GetWeakPtr() { return weak_ptr_factory_.GetWeakPtr(); } -void UserModel::SetValue(const std::string& identifier, +void UserModel::SetValue(const std::string& input, const ValueProto& value, bool force_notification) { + // Replace all placeholders in the input. + auto formatted_identifier = + field_formatter::FormatString(input, identifier_placeholders_); + if (!formatted_identifier.has_value()) { + VLOG(2) << "Error setting value: placeholder not found for " << input; + return; + } + std::string identifier = *formatted_identifier; auto result = values_.emplace(identifier, value); if (!force_notification && !result.second && result.first->second == value && value.is_client_side_only() == @@ -72,8 +82,14 @@ void UserModel::SetValue(const std::string& identifier, } } -base::Optional<ValueProto> UserModel::GetValue( - const std::string& identifier) const { +base::Optional<ValueProto> UserModel::GetValue(const std::string& input) const { + // Replace all placeholders in the input. + auto formatted_identifier = + field_formatter::FormatString(input, identifier_placeholders_); + if (!formatted_identifier.has_value()) { + return base::nullopt; + } + std::string identifier = *formatted_identifier; auto it = values_.find(identifier); if (it != values_.end()) { return it->second; @@ -145,6 +161,20 @@ void UserModel::RemoveObserver(Observer* observer) { observers_.RemoveObserver(observer); } +void UserModel::AddIdentifierPlaceholders( + const std::map<std::string, std::string> placeholders) { + for (const auto& placeholder : placeholders) { + identifier_placeholders_[placeholder.first] = placeholder.second; + } +} + +void UserModel::RemoveIdentifierPlaceholders( + const std::map<std::string, std::string> placeholders) { + for (const auto& placeholder : placeholders) { + identifier_placeholders_.erase(placeholder.first); + } +} + void UserModel::SetAutofillCreditCards( std::unique_ptr<std::vector<std::unique_ptr<autofill::CreditCard>>> credit_cards) { diff --git a/chromium/components/autofill_assistant/browser/user_model.h b/chromium/components/autofill_assistant/browser/user_model.h index e0b4189a42e..266dee6b7d7 100644 --- a/chromium/components/autofill_assistant/browser/user_model.h +++ b/chromium/components/autofill_assistant/browser/user_model.h @@ -48,8 +48,10 @@ class UserModel { bool force_notification = false); // Returns the value for |identifier| or nullopt if there is no such value. - // Also supports the array operator to retrieve a specific element of a list, - // e.g., "identifier[0]" to get the first item. + // - Placeholders in |identifier| of the form ${key} are automatically + // replaced (see |AddIdentifierPlaceholders|). + // - Also supports the array operator to retrieve + // a specific element of a list, e.g., "identifier[0]" to get the first item. base::Optional<ValueProto> GetValue(const std::string& identifier) const; // Returns the value for |reference| or nullopt if there is no such value. @@ -72,6 +74,17 @@ class UserModel { return values; } + // Adds a set of placeholders (overwrite if necessary). When looking up values + // by identifier, all occurrences of ${key} are automatically replaced by + // their value. Example: the current set of placeholders contains "i" -> "1". + // Looking up the value "value[${i}]" will now actually lookup "value[1]". + void AddIdentifierPlaceholders( + const std::map<std::string, std::string> placeholders); + + // Removes a set of placeholders. + void RemoveIdentifierPlaceholders( + const std::map<std::string, std::string> placeholders); + // Replaces the set of available autofill credit cards. void SetAutofillCreditCards( std::unique_ptr<std::vector<std::unique_ptr<autofill::CreditCard>>> @@ -105,6 +118,7 @@ class UserModel { friend class UserModelTest; std::map<std::string, ValueProto> values_; + std::map<std::string, std::string> identifier_placeholders_; std::map<std::string, std::unique_ptr<autofill::CreditCard>> credit_cards_; std::map<std::string, std::unique_ptr<autofill::AutofillProfile>> profiles_; base::ObserverList<Observer> observers_; diff --git a/chromium/components/autofill_assistant/browser/user_model_unittest.cc b/chromium/components/autofill_assistant/browser/user_model_unittest.cc index 799d606e023..a1d772965bc 100644 --- a/chromium/components/autofill_assistant/browser/user_model_unittest.cc +++ b/chromium/components/autofill_assistant/browser/user_model_unittest.cc @@ -366,4 +366,75 @@ TEST_F(UserModelTest, ClientSideOnlyNotifications) { EXPECT_TRUE(GetValues().at("identifier").is_client_side_only()); } +TEST_F(UserModelTest, GetValueWithPlaceholders) { + ValueProto value; + value.mutable_strings()->add_values("a"); + value.mutable_strings()->add_values("b"); + value.mutable_strings()->add_values("c"); + model_.SetValue("multi_value", value); + model_.SetValue("single_value_0", SimpleValue(std::string("d"))); + model_.SetValue("single_value_1", SimpleValue(std::string("e"))); + model_.SetValue("single_value_2", SimpleValue(std::string("f"))); + + EXPECT_EQ(model_.GetValue("multi_value[${i}]"), base::nullopt); + EXPECT_EQ(model_.GetValue("single_value_i"), base::nullopt); + model_.AddIdentifierPlaceholders({{"i", "0"}}); + EXPECT_EQ(model_.GetValue("multi_value[${i}]"), + SimpleValue(std::string("a"))); + EXPECT_EQ(model_.GetValue("single_value_${i}"), + SimpleValue(std::string("d"))); + + // Add placeholder. + model_.AddIdentifierPlaceholders({{"j", "1"}}); + EXPECT_EQ(model_.GetValue("multi_value[${j}]"), + SimpleValue(std::string("b"))); + EXPECT_EQ(model_.GetValue("single_value_${j}"), + SimpleValue(std::string("e"))); + EXPECT_EQ(model_.GetValue("single_value_${j}[${i}]"), + SimpleValue(std::string("e"))); + + // Overwrite placeholder. + model_.AddIdentifierPlaceholders({{"i", "2"}}); + EXPECT_EQ(model_.GetValue("multi_value[${i}]"), + SimpleValue(std::string("c"))); + EXPECT_EQ(model_.GetValue("single_value_${i}"), + SimpleValue(std::string("f"))); + EXPECT_EQ(model_.GetValue("single_value_${j}[${i}]"), base::nullopt); + // Remove placeholder (the value does not matter, it's just about the key). + model_.RemoveIdentifierPlaceholders({{"i", "123"}}); + EXPECT_EQ(model_.GetValue("multi_value[${i}]"), base::nullopt); + EXPECT_EQ(model_.GetValue("single_value_${i}"), base::nullopt); + EXPECT_EQ(model_.GetValue("single_value_${j}"), + SimpleValue(std::string("e"))); +} + +TEST_F(UserModelTest, SetValueWithPlaceholders) { + ValueProto value; + value.mutable_strings()->add_values("a"); + value.mutable_strings()->add_values("b"); + value.mutable_strings()->add_values("c"); + model_.SetValue("value_${i}", value); + EXPECT_EQ(model_.GetValue("value_${i}"), base::nullopt); + + model_.AddIdentifierPlaceholders({{"i", "0"}}); + model_.SetValue("value_${i}", value); + EXPECT_EQ(model_.GetValue("value_0"), value); + EXPECT_EQ(model_.GetValue("value_${i}"), value); + + model_.RemoveIdentifierPlaceholders({{"i", "0"}}); + EXPECT_EQ(model_.GetValue("value_0"), value); + EXPECT_EQ(model_.GetValue("value_${i}"), base::nullopt); + + model_.AddIdentifierPlaceholders({{"i", "0"}}); + model_.AddIdentifierPlaceholders({{"j", "1"}}); + model_.SetValue("value_${i}_${j}", value); + EXPECT_EQ(model_.GetValue("value_0_1"), value); + EXPECT_EQ(model_.GetValue("value_${i}_${j}"), value); + + model_.RemoveIdentifierPlaceholders({{"j", "1"}}); + EXPECT_EQ(model_.GetValue("value_${i}_${j}"), base::nullopt); + model_.SetValue("value_${i}", value); + EXPECT_EQ(model_.GetValue("value_${i}"), value); +} + } // namespace autofill_assistant diff --git a/chromium/components/autofill_assistant/browser/value_util.cc b/chromium/components/autofill_assistant/browser/value_util.cc index 24b0175e166..453d6da6918 100644 --- a/chromium/components/autofill_assistant/browser/value_util.cc +++ b/chromium/components/autofill_assistant/browser/value_util.cc @@ -65,8 +65,8 @@ bool operator==(const ValueProto& value_a, const ValueProto& value_b) { value_b.login_options().values(); case ValueProto::kCreditCardResponse: return value_a.credit_card_response() == value_b.credit_card_response(); - case ValueProto::kLoginOptionResponse: - return value_a.login_option_response() == value_b.login_option_response(); + case ValueProto::kServerPayload: + return value_a.server_payload() == value_b.server_payload(); case ValueProto::KIND_NOT_SET: return true; } @@ -100,7 +100,7 @@ bool operator<(const ValueProto& value_a, const ValueProto& value_b) { case ValueProto::kProfiles: case ValueProto::kLoginOptions: case ValueProto::kCreditCardResponse: - case ValueProto::kLoginOptionResponse: + case ValueProto::kServerPayload: case ValueProto::KIND_NOT_SET: NOTREACHED(); return false; @@ -183,11 +183,6 @@ bool operator==(const CreditCardResponseProto& value_a, return value_a.network() == value_b.network(); } -bool operator==(const LoginOptionResponseProto& value_a, - const LoginOptionResponseProto& value_b) { - return value_a.payload() == value_b.payload(); -} - // Intended for debugging. Writes a string representation of |values| to |out|. template <typename T> std::ostream& WriteRepeatedField(std::ostream& out, const T& values) { @@ -247,12 +242,6 @@ std::ostream& operator<<(std::ostream& out, return out; } -std::ostream& operator<<(std::ostream& out, - const LoginOptionResponseProto& value) { - out << value.payload(); - return out; -} - // Intended for debugging. Writes a string representation of |value| to |out|. std::ostream& operator<<(std::ostream& out, const ValueProto& value) { switch (value.kind_case()) { @@ -283,8 +272,8 @@ std::ostream& operator<<(std::ostream& out, const ValueProto& value) { case ValueProto::kCreditCardResponse: out << value.credit_card_response(); break; - case ValueProto::kLoginOptionResponse: - out << value.login_option_response(); + case ValueProto::kServerPayload: + out << value.server_payload(); break; case ValueProto::KIND_NOT_SET: break; @@ -413,7 +402,7 @@ int GetValueSize(const ValueProto& value) { return value.login_options().values().size(); case ValueProto::kCreditCardResponse: return 1; - case ValueProto::kLoginOptionResponse: + case ValueProto::kServerPayload: return 1; case ValueProto::KIND_NOT_SET: return 0; @@ -464,12 +453,10 @@ base::Optional<ValueProto> GetNthValue(const ValueProto& value, int index) { return nth_value; case ValueProto::kCreditCardResponse: DCHECK(index == 0); - nth_value = value; - return nth_value; - case ValueProto::kLoginOptionResponse: + return value; + case ValueProto::kServerPayload: DCHECK(index == 0); - nth_value = value; - return nth_value; + return value; case ValueProto::KIND_NOT_SET: return base::nullopt; } diff --git a/chromium/components/autofill_assistant/browser/value_util.h b/chromium/components/autofill_assistant/browser/value_util.h index 9d44a6c86a1..dae11fcae48 100644 --- a/chromium/components/autofill_assistant/browser/value_util.h +++ b/chromium/components/autofill_assistant/browser/value_util.h @@ -54,10 +54,6 @@ bool operator==(const LoginOptionProto& value_a, bool operator==(const CreditCardResponseProto& value_a, const CreditCardResponseProto& value_b); -// Custom comparison operator for |LoginOptionResponseProto|. -bool operator==(const LoginOptionResponseProto& value_a, - const LoginOptionResponseProto& value_b); - // Intended for debugging. std::ostream& operator<<(std::ostream& out, const ValueProto& value); std::ostream& operator<<(std::ostream& out, @@ -72,8 +68,6 @@ std::ostream& operator<<(std::ostream& out, const AutofillProfileProto& value); std::ostream& operator<<(std::ostream& out, const LoginOptionProto& value); std::ostream& operator<<(std::ostream& out, const CreditCardResponseProto& value); -std::ostream& operator<<(std::ostream& out, - const LoginOptionResponseProto& value); // Convenience constructors. ValueProto SimpleValue(bool value, bool is_client_side_only = false); diff --git a/chromium/components/autofill_assistant/browser/value_util_unittest.cc b/chromium/components/autofill_assistant/browser/value_util_unittest.cc index b33a2d23fd6..996b2561743 100644 --- a/chromium/components/autofill_assistant/browser/value_util_unittest.cc +++ b/chromium/components/autofill_assistant/browser/value_util_unittest.cc @@ -59,23 +59,20 @@ TEST_F(ValueUtilTest, DifferentTypesComparison) { ValueProto value_c = CreateIntValue(); ValueProto value_d = CreateBoolValue(); ValueProto value_e = SimpleValue(CreateDateProto(2020, 8, 30)); - - EXPECT_FALSE(value_a == value_b); - EXPECT_FALSE(value_a == value_c); - EXPECT_FALSE(value_a == value_d); - EXPECT_FALSE(value_a == value_e); - EXPECT_FALSE(value_b == value_c); - EXPECT_FALSE(value_b == value_d); - EXPECT_FALSE(value_b == value_e); - EXPECT_FALSE(value_c == value_d); - EXPECT_FALSE(value_c == value_e); - EXPECT_FALSE(value_d == value_e); - - EXPECT_TRUE(value_a == value_a); - EXPECT_TRUE(value_b == value_b); - EXPECT_TRUE(value_c == value_c); - EXPECT_TRUE(value_d == value_d); - EXPECT_TRUE(value_e == value_e); + ValueProto value_f; + value_f.set_server_payload("payload"); + + std::vector<ValueProto> values = {value_a, value_b, value_c, + value_d, value_e, value_f}; + for (size_t i = 0; i < values.size(); ++i) { + for (size_t j = 0; j < values.size(); ++j) { + if (j == i) { + EXPECT_TRUE(values[i] == values[j]); + continue; + } + EXPECT_FALSE(values[i] == values[j]); + } + } } TEST_F(ValueUtilTest, EmptyValueComparison) { @@ -222,13 +219,13 @@ TEST_F(ValueUtilTest, CreditCardResponseComparison) { EXPECT_FALSE(value_a == value_b); } -TEST_F(ValueUtilTest, LoginOptionResponseComparison) { +TEST_F(ValueUtilTest, ServerPayloadComparison) { ValueProto value_a; - value_a.mutable_login_option_response()->set_payload("payload"); + value_a.set_server_payload("payload"); ValueProto value_b = value_a; EXPECT_TRUE(value_a == value_b); - value_b.mutable_login_option_response()->set_payload("different"); + value_b.set_server_payload("different"); EXPECT_FALSE(value_a == value_b); } diff --git a/chromium/components/autofill_assistant/browser/view_layout.proto b/chromium/components/autofill_assistant/browser/view_layout.proto index 28ac4d5090e..fd0f6a10f8b 100644 --- a/chromium/components/autofill_assistant/browser/view_layout.proto +++ b/chromium/components/autofill_assistant/browser/view_layout.proto @@ -74,6 +74,23 @@ message BitmapDrawableProto { // A drawable for use in backgrounds or in image views. message DrawableProto { + enum Icon { + DRAWABLE_ICON_UNDEFINED = 0; + PROGRESSBAR_DEFAULT_INITIAL_STEP = 1; + PROGRESSBAR_DEFAULT_DATA_COLLECTION = 2; + PROGRESSBAR_DEFAULT_PAYMENT = 3; + PROGRESSBAR_DEFAULT_FINAL_STEP = 4; + SITTING_PERSON = 5; + TICKET_STUB = 6; + SHOPPING_BASKET = 7; + FAST_FOOD = 8; + LOCAL_DINING = 9; + COGWHEEL = 10; + KEY = 11; + CAR = 12; + GROCERY = 13; + } + oneof drawable { // The resource identifier of a drawable. string resource_identifier = 1; @@ -81,6 +98,10 @@ message DrawableProto { BitmapDrawableProto bitmap = 2; // A shape, e.g., a rounded rectangle. ShapeDrawableProto shape = 3; + // An icon from a predefined set of known icons. + Icon icon = 4; + // A Base64 encoded image string. + bytes base64 = 5; } } 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 diff --git a/chromium/components/autofill_assistant/browser/website_login_manager_impl.cc b/chromium/components/autofill_assistant/browser/website_login_manager_impl.cc index 039a5c04273..32c1ea674e8 100644 --- a/chromium/components/autofill_assistant/browser/website_login_manager_impl.cc +++ b/chromium/components/autofill_assistant/browser/website_login_manager_impl.cc @@ -6,8 +6,8 @@ #include "base/stl_util.h" #include "base/strings/utf_string_conversions.h" -#include "base/task/post_task.h" #include "components/password_manager/core/browser/form_fetcher_impl.h" +#include "components/password_manager/core/browser/form_parsing/form_parser.h" #include "components/password_manager/core/browser/password_form_metrics_recorder.h" #include "components/password_manager/core/browser/password_generation_frame_helper.h" #include "components/password_manager/core/browser/password_manager_client.h" @@ -27,8 +27,8 @@ autofill::PasswordForm CreatePasswordForm( const WebsiteLoginManager::Login& login, const std::string& password) { autofill::PasswordForm form; - form.signon_realm = login.origin.spec(); - form.origin = login.origin.GetOrigin(); + form.url = login.origin.GetOrigin(); + form.signon_realm = password_manager::GetSignonRealm(form.url); form.username_value = base::UTF8ToUTF16(login.username); form.password_value = base::UTF8ToUTF16(password); @@ -71,8 +71,8 @@ class WebsiteLoginManagerImpl::PendingRequest // destruction of |this|, which needs to happen *after* this call has // returned. if (notify_finished_callback_) { - base::PostTask(FROM_HERE, {content::BrowserThread::UI}, - base::BindOnce(&PendingRequest::NotifyFinished, + content::GetUIThreadTaskRunner({})->PostTask( + FROM_HERE, base::BindOnce(&PendingRequest::NotifyFinished, weak_ptr_factory_.GetWeakPtr(), std::move(notify_finished_callback_))); } @@ -110,7 +110,7 @@ class WebsiteLoginManagerImpl::PendingFetchLoginsRequest void OnFetchCompleted() override { std::vector<Login> logins; for (const auto* match : form_fetcher_->GetBestMatches()) { - logins.emplace_back(match->origin.GetOrigin(), + logins.emplace_back(match->url.GetOrigin(), base::UTF16ToUTF8(match->username_value)); } std::move(callback_).Run(logins); @@ -176,8 +176,9 @@ class WebsiteLoginManagerImpl::UpdatePasswordRequest CreatePasswordSaveManagerImpl(client)), metrics_recorder_( base::MakeRefCounted<password_manager::PasswordFormMetricsRecorder>( - client->IsMainFrameSecure(), - client->GetUkmSourceId())), + client->IsCommittedMainFrameSecure(), + client->GetUkmSourceId(), + client->GetPrefs())), votes_uploader_(client, true /* is_possible_change_password_form */) { DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |