| // Copyright 2013 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #import <UIKit/UIKit.h> |
| |
| #import <memory> |
| #import <vector> |
| |
| #import "base/apple/foundation_util.h" |
| #import "base/ios/ios_util.h" |
| #import "base/memory/ptr_util.h" |
| #import "base/strings/utf_string_conversions.h" |
| #import "base/task/thread_pool/thread_pool_instance.h" |
| #import "base/test/ios/wait_util.h" |
| #import "base/test/metrics/histogram_tester.h" |
| #import "base/test/scoped_feature_list.h" |
| #import "base/test/test_future.h" |
| #import "base/types/id_type.h" |
| #import "base/uuid.h" |
| #import "components/autofill/core/browser/data_manager/addresses/address_data_manager.h" |
| #import "components/autofill/core/browser/data_manager/addresses/address_data_manager_test_api.h" |
| #import "components/autofill/core/browser/data_manager/payments/payments_data_manager.h" |
| #import "components/autofill/core/browser/data_manager/personal_data_manager.h" |
| #import "components/autofill/core/browser/data_manager/personal_data_manager_test_utils.h" |
| #import "components/autofill/core/browser/form_structure.h" |
| #import "components/autofill/core/browser/foundations/browser_autofill_manager.h" |
| #import "components/autofill/core/browser/foundations/test_autofill_manager_waiter.h" |
| #import "components/autofill/core/browser/geo/alternative_state_name_map_updater.h" |
| #import "components/autofill/core/browser/metrics/autofill_metrics.h" |
| #import "components/autofill/core/browser/webdata/autocomplete/autocomplete_entry.h" |
| #import "components/autofill/core/common/autofill_clock.h" |
| #import "components/autofill/core/common/autofill_features.h" |
| #import "components/autofill/core/common/autofill_test_utils.h" |
| #import "components/autofill/core/common/field_data_manager.h" |
| #import "components/autofill/core/common/form_data.h" |
| #import "components/autofill/core/common/mojom/autofill_types.mojom-shared.h" |
| #import "components/autofill/core/common/unique_ids.h" |
| #import "components/autofill/ios/browser/autofill_agent.h" |
| #import "components/autofill/ios/browser/autofill_driver_ios.h" |
| #import "components/autofill/ios/browser/autofill_driver_ios_factory.h" |
| #import "components/autofill/ios/browser/autofill_java_script_feature.h" |
| #import "components/autofill/ios/browser/form_suggestion.h" |
| #import "components/autofill/ios/browser/test_autofill_client_ios.h" |
| #import "components/autofill/ios/browser/test_autofill_manager_injector.h" |
| #import "components/autofill/ios/common/field_data_manager_factory_ios.h" |
| #import "components/infobars/core/confirm_infobar_delegate.h" |
| #import "components/infobars/core/infobar.h" |
| #import "components/infobars/core/infobar_manager.h" |
| #import "components/keyed_service/core/service_access_type.h" |
| #import "components/password_manager/core/browser/password_manager_test_utils.h" |
| #import "components/password_manager/core/browser/password_store/mock_password_store_interface.h" |
| #import "ios/chrome/browser/autofill/model/bottom_sheet/autofill_bottom_sheet_tab_helper.h" |
| #import "ios/chrome/browser/autofill/model/bottom_sheet/save_card_bottom_sheet_model.h" |
| #import "ios/chrome/browser/autofill/model/form_suggestion_controller.h" |
| #import "ios/chrome/browser/autofill/model/personal_data_manager_factory.h" |
| #import "ios/chrome/browser/autofill/ui_bundled/chrome_autofill_client_ios.h" |
| #import "ios/chrome/browser/autofill/ui_bundled/form_input_accessory/form_input_accessory_mediator.h" |
| #import "ios/chrome/browser/infobars/model/infobar_manager_impl.h" |
| #import "ios/chrome/browser/passwords/model/ios_chrome_profile_password_store_factory.h" |
| #import "ios/chrome/browser/passwords/model/password_controller.h" |
| #import "ios/chrome/browser/shared/model/application_context/application_context.h" |
| #import "ios/chrome/browser/shared/model/profile/test/test_profile_ios.h" |
| #import "ios/chrome/browser/shared/public/commands/autofill_commands.h" |
| #import "ios/chrome/browser/web/model/chrome_web_client.h" |
| #import "ios/chrome/browser/webdata_services/model/web_data_service_factory.h" |
| #import "ios/chrome/test/ios_chrome_scoped_testing_local_state.h" |
| #import "ios/web/public/js_messaging/web_frame.h" |
| #import "ios/web/public/js_messaging/web_frames_manager.h" |
| #import "ios/web/public/navigation/navigation_item.h" |
| #import "ios/web/public/navigation/navigation_manager.h" |
| #import "ios/web/public/test/scoped_testing_web_client.h" |
| #import "ios/web/public/test/task_observer_util.h" |
| #import "ios/web/public/test/web_state_test_util.h" |
| #import "ios/web/public/test/web_task_environment.h" |
| #import "ios/web/public/web_state.h" |
| #import "testing/gtest_mac.h" |
| #import "testing/platform_test.h" |
| #import "third_party/ocmock/OCMock/OCMock.h" |
| #import "third_party/ocmock/OCMock/OCMockMacros.h" |
| |
| using base::test::ScopedFeatureList; |
| |
| // Real FormSuggestionController is wrapped to register the addition of |
| // suggestions. |
| @interface TestSuggestionController : FormSuggestionController |
| |
| @property(nonatomic, copy) NSArray* suggestions; |
| @property(nonatomic, assign) BOOL suggestionRetrievalComplete; |
| @property(nonatomic, assign) BOOL suggestionRetrievalStarted; |
| |
| @end |
| |
| @implementation TestSuggestionController |
| |
| @synthesize suggestions = _suggestions; |
| @synthesize suggestionRetrievalComplete = _suggestionRetrievalComplete; |
| |
| - (void)retrieveSuggestionsForForm:(const autofill::FormActivityParams&)params |
| webState:(web::WebState*)webState |
| accessoryViewUpdateBlock: |
| (FormSuggestionsReadyCompletion)accessoryViewUpdateBlock { |
| self.suggestionRetrievalStarted = YES; |
| [super retrieveSuggestionsForForm:params |
| webState:webState |
| accessoryViewUpdateBlock:accessoryViewUpdateBlock]; |
| } |
| |
| - (void)updateKeyboardWithSuggestions:(NSArray*)suggestions { |
| self.suggestions = suggestions; |
| self.suggestionRetrievalComplete = YES; |
| } |
| |
| - (void)onNoSuggestionsAvailable { |
| self.suggestionRetrievalComplete = YES; |
| } |
| |
| - (void)resetSuggestionAvailable { |
| self.suggestionRetrievalComplete = NO; |
| self.suggestionRetrievalStarted = NO; |
| self.suggestions = nil; |
| } |
| |
| @end |
| |
| namespace autofill { |
| |
| namespace { |
| |
| // The profile-type form used by tests. |
| NSString* const kProfileFormHtml = |
| @"<form action='/submit' method='post'>" |
| "Name <input type='text' name='name'>" |
| "Address <input type='text' name='address'>" |
| "City <input type='text' name='city'>" |
| "State <input type='text' name='state'>" |
| "Zip <input type='text' name='zip'>" |
| "<input type='submit' id='submit' value='Submit'>" |
| "</form>"; |
| |
| // A minimal form with a name. |
| NSString* const kMinimalFormWithNameHtml = @"<form id='form1'>" |
| "<input name='name'>" |
| "<input name='address'>" |
| "<input name='city'>" |
| "</form>"; |
| |
| // The key/value-type form used by tests. |
| NSString* const kKeyValueFormHtml = |
| @"<form action='/submit' method='post'>" |
| "Greeting <input id='greeting' type='text' name='greeting'>" |
| "Dummy field <input id='dummy' type='text' name='dummy'>" |
| "<input type='submit' id='submit' value='Submit'>" |
| "</form>"; |
| |
| // The credit card-type form used by tests. |
| NSString* const kCreditCardFormHtml = |
| @"<form id='form' action='/submit' method='post'>" |
| "Name on card: <input id='name' type='text' name='name'>" |
| "Credit card number: <input id='CCNo' type='text' name='CCNo'>" |
| "Expiry Month: <input id='CCExpiresMonth' type='text' " |
| "name='CCExpiresMonth'>" |
| "Expiry Year: <input id='CCExpiresYear' type='text' name='CCExpiresYear'>" |
| "<input type='submit' id='submit' value='Submit'>" |
| "</form>"; |
| |
| // An HTML page without a card-type form. |
| static NSString* kNoCreditCardFormHtml = |
| @"<form><input type=\"text\" autofocus autocomplete=\"username\"></form>"; |
| |
| // A credit card-type form with the autofocus attribute (which is detected at |
| // page load). |
| NSString* const kCreditCardAutofocusFormHtml = |
| @"<form><input type=\"text\" autofocus autocomplete=\"cc-number\"></form>"; |
| |
| // A profile-type formless form. The fields are not inside a form element. |
| NSString* const kProfileFormlessHtml = |
| @"<div id='div'>" |
| "Name <input id='name' type='text' name='name'>" |
| "Address <input id='address' type='text' name='address'>" |
| "City <input id='city' type='text' name='city'>" |
| "State <input id='state' type='text' name='state'>" |
| "Zip <input id='zip' type='text' name='zip'>" |
| "<input type='submit' id='submit' value='Submit'>" |
| "</div>"; |
| |
| using ::testing::AllOf; |
| using ::testing::AssertionFailure; |
| using ::testing::AssertionResult; |
| using ::testing::AssertionSuccess; |
| using ::testing::ElementsAre; |
| using ::testing::Eq; |
| using ::testing::IsEmpty; |
| using ::testing::IsTrue; |
| using ::testing::Property; |
| using ::testing::SizeIs; |
| using ::testing::UnorderedElementsAre; |
| |
| // FAIL if a field with the supplied `name` and `fieldType` is not present on |
| // the `form`. |
| void CheckField(const FormStructure& form, |
| FieldType fieldType, |
| const char* name) { |
| for (const auto& field : form) { |
| if (field->heuristic_type() == fieldType) { |
| EXPECT_EQ(base::UTF8ToUTF16(name), field->name()); |
| return; |
| } |
| } |
| FAIL() << "Missing field " << name; |
| } |
| |
| // Forces rendering of a UIView. This is used in tests to make sure that UIKit |
| // optimizations don't have the views return the previous values (such as |
| // zoomScale). |
| void ForceViewRendering(UIView* view) { |
| EXPECT_TRUE(view); |
| CALayer* layer = view.layer; |
| EXPECT_TRUE(layer); |
| const CGFloat kArbitraryNonZeroPositiveValue = 19; |
| const CGSize arbitraryNonEmptyArea = CGSizeMake( |
| kArbitraryNonZeroPositiveValue, kArbitraryNonZeroPositiveValue); |
| UIGraphicsBeginImageContext(arbitraryNonEmptyArea); |
| CGContext* context = UIGraphicsGetCurrentContext(); |
| EXPECT_TRUE(context); |
| [layer renderInContext:context]; |
| UIGraphicsEndImageContext(); |
| } |
| |
| // Returns a matcher to verify a child frame in the FormData. |
| auto ChildFrameMatcher(int expected_predecessor) { |
| const auto valid_token_matcher = ::testing::Field( |
| &FrameTokenWithPredecessor::token, |
| ::testing::VariantWith<RemoteFrameToken>(::testing::IsTrue())); |
| const auto predecessor_matcher = |
| ::testing::Field(&FrameTokenWithPredecessor::predecessor, |
| testing::Eq(expected_predecessor)); |
| return AllOf(valid_token_matcher, predecessor_matcher); |
| } |
| |
| // Text fixture to test autofill. |
| class AutofillControllerTest : public PlatformTest { |
| public: |
| AutofillControllerTest() : web_client_(std::make_unique<ChromeWebClient>()) { |
| TestProfileIOS::Builder builder; |
| builder.AddTestingFactory( |
| IOSChromeProfilePasswordStoreFactory::GetInstance(), |
| base::BindRepeating(&password_manager::BuildPasswordStoreInterface< |
| web::BrowserState, |
| password_manager::MockPasswordStoreInterface>)); |
| // Profile import requires a PersonalDataManager which itself needs the |
| // WebDataService; this is not initialized on a TestProfileIOS by |
| // default. |
| builder.AddTestingFactory(ios::WebDataServiceFactory::GetInstance(), |
| ios::WebDataServiceFactory::GetDefaultFactory()); |
| profile_ = std::move(builder).Build(); |
| |
| web::WebState::CreateParams params(profile_.get()); |
| web_state_ = web::WebState::Create(params); |
| web_state_->GetView(); |
| web_state_->SetKeepRenderProcessAlive(true); |
| } |
| |
| AutofillControllerTest(const AutofillControllerTest&) = delete; |
| AutofillControllerTest& operator=(const AutofillControllerTest&) = delete; |
| |
| ~AutofillControllerTest() override {} |
| |
| protected: |
| class TestAutofillManager : public BrowserAutofillManager { |
| public: |
| explicit TestAutofillManager(AutofillDriverIOS* driver) |
| : BrowserAutofillManager(driver) {} |
| |
| TestAutofillManagerWaiter& waiter() { return waiter_; } |
| |
| private: |
| TestAutofillManagerWaiter waiter_{*this, |
| {AutofillManagerEvent::kFormsSeen}}; |
| }; |
| |
| void SetUp() override; |
| void TearDown() override; |
| |
| void SetUpForSuggestions(NSString* data, size_t expected_number_of_forms); |
| |
| // Adds key value data to the Personal Data Manager and loads test page. |
| void SetUpKeyValueData(); |
| |
| // Blocks until suggestion retrieval has completed. |
| // If `wait_for_trigger` is yes, wait for the call to |
| // `retrieveSuggestionsForForm` to avoid considering a former call. |
| void WaitForSuggestionRetrieval(BOOL wait_for_trigger); |
| void ResetWaitForSuggestionRetrieval(); |
| |
| // Loads the page and wait until the initial form processing has been done. |
| // This processing must find `expected_size` forms. |
| [[nodiscard]] bool LoadHtmlAndWaitForFormFetched( |
| NSString* html, |
| size_t expected_number_of_forms, |
| size_t expected_number_of_calls = 1); |
| |
| void LoadHtmlAndInitRendererIds(NSString* html); |
| |
| // Fails if the specified metric was not registered the given number of times. |
| void ExpectMetric(const std::string& histogram_name, int sum); |
| |
| TestSuggestionController* suggestion_controller() { |
| return suggestion_controller_; |
| } |
| |
| void WaitForCondition(ConditionBlock condition); |
| |
| // Simulates a text input event by focusing the field with 'field_id' and |
| // dispatching a TextEvent with value 'field_value'. |
| void SimulateTextInputEvent(NSString* field_id, NSString* field_value); |
| |
| void LoadAndFillCreditCardForm(); |
| |
| // Returns the AutofillManager for the main frame. |
| BrowserAutofillManager* autofill_manager_for_main_frame() { |
| web::WebFramesManager* frames_manager = |
| AutofillJavaScriptFeature::GetInstance()->GetWebFramesManager( |
| web_state()); |
| web::WebFrame* main_frame = frames_manager->GetMainWebFrame(); |
| return &AutofillDriverIOS::FromWebStateAndWebFrame(web_state(), main_frame) |
| ->GetAutofillManager(); |
| } |
| |
| PrefService* local_state() { |
| return GetApplicationContext()->GetLocalState(); |
| } |
| |
| protected: |
| web::WebState* web_state() { return web_state_.get(); } |
| |
| web::ScopedTestingWebClient web_client_; |
| web::WebTaskEnvironment task_environment_; |
| autofill::test::AutofillUnitTestEnvironment autofill_test_environment_{ |
| {.disable_server_communication = true}}; |
| IOSChromeScopedTestingLocalState scoped_testing_local_state_; |
| std::unique_ptr<TestProfileIOS> profile_; |
| std::unique_ptr<web::WebState> web_state_; |
| bool processed_a_task_ = false; |
| // Histogram tester for these tests. |
| std::unique_ptr<base::HistogramTester> histogram_tester_; |
| raw_ptr<AutofillBottomSheetTabHelper> bottomsheet_tab_helper_; |
| id<AutofillCommands> autofill_commands_handler_; |
| ScopedFeatureList scoped_feature_list_{ |
| features::kAutofillLocalSaveCardBottomSheet}; |
| |
| private: |
| std::unique_ptr<autofill::AutofillClient> autofill_client_; |
| |
| AutofillAgent* autofill_agent_; |
| |
| std::unique_ptr<TestAutofillManagerInjector<TestAutofillManager>> |
| autofill_manager_injector_; |
| |
| // Retrieves suggestions according to form events. |
| TestSuggestionController* suggestion_controller_; |
| |
| // Retrieves accessory views according to form events. |
| FormInputAccessoryMediator* accessory_mediator_; |
| |
| PasswordController* passwordController_; |
| }; |
| |
| void AutofillControllerTest::SetUp() { |
| PlatformTest::SetUp(); |
| |
| // Create a PasswordController instance that will handle set up for renderer |
| // ids. |
| passwordController_ = |
| [[PasswordController alloc] initWithWebState:web_state()]; |
| |
| autofill_agent_ = |
| [[AutofillAgent alloc] initWithPrefService:profile_->GetPrefs() |
| webState:web_state()]; |
| suggestion_controller_ = |
| [[TestSuggestionController alloc] initWithWebState:web_state() |
| providers:@[ autofill_agent_ ]]; |
| |
| InfoBarManagerImpl::CreateForWebState(web_state()); |
| infobars::InfoBarManager* infobar_manager = |
| InfoBarManagerImpl::FromWebState(web_state()); |
| autofill_client_ = |
| std::make_unique<WithFakedFromWebState<ChromeAutofillClientIOS>>( |
| profile_.get(), web_state(), infobar_manager, autofill_agent_); |
| |
| autofill_client_->GetPersonalDataManager() |
| .address_data_manager() |
| .get_alternative_state_name_map_updater_for_testing() |
| ->set_local_state_for_testing(local_state()); |
| |
| autofill_manager_injector_ = |
| std::make_unique<TestAutofillManagerInjector<TestAutofillManager>>( |
| web_state()); |
| |
| accessory_mediator_ = |
| [[FormInputAccessoryMediator alloc] initWithConsumer:nil |
| handler:nil |
| webStateList:nullptr |
| personalDataManager:nullptr |
| profilePasswordStore:nullptr |
| accountPasswordStore:nullptr |
| securityAlertHandler:nil |
| reauthenticationModule:nil |
| engagementTracker:nil]; |
| |
| [accessory_mediator_ injectWebState:web_state()]; |
| [accessory_mediator_ injectProvider:suggestion_controller_]; |
| |
| AutofillBottomSheetTabHelper::CreateForWebState(web_state()); |
| bottomsheet_tab_helper_ = |
| AutofillBottomSheetTabHelper::FromWebState(web_state_.get()); |
| autofill_commands_handler_ = OCMProtocolMock(@protocol(AutofillCommands)); |
| bottomsheet_tab_helper_->SetAutofillBottomSheetHandler( |
| autofill_commands_handler_); |
| |
| histogram_tester_ = std::make_unique<base::HistogramTester>(); |
| } |
| |
| void AutofillControllerTest::TearDown() { |
| [accessory_mediator_ disconnect]; |
| [suggestion_controller_ detachFromWebState]; |
| |
| web::test::WaitForBackgroundTasks(); |
| web_state_.reset(); |
| } |
| |
| void AutofillControllerTest::ResetWaitForSuggestionRetrieval() { |
| [suggestion_controller() resetSuggestionAvailable]; |
| } |
| |
| void AutofillControllerTest::WaitForSuggestionRetrieval(BOOL wait_for_trigger) { |
| // Wait for the message queue to ensure that JS events fired in the tests |
| // trigger TestSuggestionController's retrieveSuggestionsForFormNamed: method |
| // and set suggestionRetrievalComplete to NO. |
| if (wait_for_trigger) { |
| WaitForCondition(^bool { |
| return suggestion_controller().suggestionRetrievalStarted; |
| }); |
| } |
| // Now we can wait for suggestionRetrievalComplete to be set to YES. |
| WaitForCondition(^bool { |
| return suggestion_controller().suggestionRetrievalComplete; |
| }); |
| } |
| |
| bool AutofillControllerTest::LoadHtmlAndWaitForFormFetched( |
| NSString* html, |
| size_t expected_number_of_forms, |
| size_t expected_number_of_calls) { |
| web::test::LoadHtml(html, web_state()); |
| TestAutofillManager* autofill_manager = |
| autofill_manager_injector_->GetForMainFrame(); |
| return autofill_manager->waiter().Wait(expected_number_of_calls) && |
| autofill_manager->form_structures().size() == expected_number_of_forms; |
| } |
| |
| void AutofillControllerTest::ExpectMetric(const std::string& histogram_name, |
| int sum) { |
| histogram_tester_->ExpectBucketCount(histogram_name, sum, 1); |
| } |
| |
| void AutofillControllerTest::WaitForCondition(ConditionBlock condition) { |
| ASSERT_TRUE(base::test::ios::WaitUntilConditionOrTimeout(base::Seconds(1000), |
| true, condition)); |
| } |
| |
| void AutofillControllerTest::SimulateTextInputEvent(NSString* field_id, |
| NSString* field_value) { |
| // First focus the field, otherwise the input event does not get delivered to |
| // the browser process. |
| // Then create and dispatch a TextEvent from the field with the given id. |
| web::test::ExecuteJavaScript( |
| [NSString |
| stringWithFormat: |
| @"document.getElementById('%@').focus();" |
| @"var event = document.createEvent('TextEvent');" |
| @"event.initTextEvent('textInput', true, true, window, '%@');" |
| @"document.getElementById('%@').dispatchEvent(event);", |
| field_id, field_value, field_id], |
| web_state()); |
| } |
| |
| void AutofillControllerTest::LoadAndFillCreditCardForm() { |
| ASSERT_TRUE(LoadHtmlAndWaitForFormFetched(kCreditCardFormHtml, 1)); |
| |
| // Simulate entering a credit card in the form. |
| SimulateTextInputEvent(/*field_id=*/@"name", /*field_value=*/@"Superman"); |
| SimulateTextInputEvent(/*field_id=*/@"CCNo", |
| /*field_value=*/@"4000-4444-4444-4444"); |
| SimulateTextInputEvent(/*field_id=*/@"CCExpiresMonth", /*field_value=*/@"11"); |
| SimulateTextInputEvent(/*field_id=*/@"CCExpiresYear", |
| /*field_value=*/@"2999"); |
| } |
| |
| // Checks that viewing an HTML page containing a form results in the form being |
| // registered as a FormStructure by the BrowserAutofillManager. |
| TEST_F(AutofillControllerTest, ReadForm) { |
| ASSERT_TRUE(LoadHtmlAndWaitForFormFetched(kProfileFormHtml, 1)); |
| web::WebFramesManager* frames_manager = |
| AutofillJavaScriptFeature::GetInstance()->GetWebFramesManager( |
| web_state()); |
| web::WebFrame* main_frame = frames_manager->GetMainWebFrame(); |
| BrowserAutofillManager& autofill_manager = |
| AutofillDriverIOS::FromWebStateAndWebFrame(web_state(), main_frame) |
| ->GetAutofillManager(); |
| const auto& forms = autofill_manager.form_structures(); |
| const auto& form = *(forms.begin()->second); |
| CheckField(form, NAME_FULL, "name"); |
| CheckField(form, ADDRESS_HOME_LINE1, "address"); |
| CheckField(form, ADDRESS_HOME_CITY, "city"); |
| CheckField(form, ADDRESS_HOME_STATE, "state"); |
| CheckField(form, ADDRESS_HOME_ZIP, "zip"); |
| ExpectMetric("Autofill.IsEnabled.PageLoad", 1); |
| } |
| |
| // Checks that when autofill across iframes is enabled the child frames are |
| // carried over for their parent form. |
| TEST_F(AutofillControllerTest, ReadForm_WithChildFrames) { |
| // A form with iframes and inputs where some of the iframes have predecessors. |
| NSString* const test_page = |
| @"<form id='form1'>" |
| "<iframe></iframe>" |
| "Name <input id='name' type='text' name='name' />" |
| "<iframe></iframe>" |
| "<iframe></iframe>" |
| "Address <input type='text' name='address'>" |
| "City <input type='text' name='city'>" |
| "<iframe></iframe>" |
| "State <input type='text' name='state'>" |
| "</form>"; |
| |
| ASSERT_TRUE(LoadHtmlAndWaitForFormFetched(test_page, |
| /*expected_number_of_forms=*/1, |
| /*expected_number_of_calls=*/5)); |
| |
| // Verify that the child frames are present in the form data. |
| std::vector<FormData> form_data; |
| for (const auto& [_, form] : |
| autofill_manager_for_main_frame()->form_structures()) { |
| form_data.push_back(form->ToFormData()); |
| } |
| EXPECT_THAT( |
| form_data, |
| ElementsAre(AllOf( |
| Property(&FormData::renderer_id, IsTrue()), |
| Property(&FormData::child_frames, |
| ElementsAre(ChildFrameMatcher(-1), ChildFrameMatcher(0), |
| ChildFrameMatcher(0), ChildFrameMatcher(2)))))); |
| } |
| |
| // Checks that when autofill across iframes is enabled the child frames are |
| // carried over for their synthetic form. |
| TEST_F(AutofillControllerTest, ReadForm_WithChildFrames_Synthetic) { |
| // A syntethic form with iframes and inputs where some of the iframes have |
| // predecessors. |
| NSString* const test_page = |
| @"<html><body><div id='div'>" |
| "<iframe></iframe>" |
| "Name <input id='name' type='text' name='name' />" |
| "<iframe></iframe>" |
| "<iframe></iframe>" |
| "Address <input type='text' name='address'>" |
| "City <input type='text' name='city'>" |
| "<iframe></iframe>" |
| "State <input type='text' name='state'>" |
| "</div></html></body>"; |
| |
| ASSERT_TRUE(LoadHtmlAndWaitForFormFetched(test_page, |
| /*expected_number_of_forms=*/1, |
| /*expected_number_of_calls=*/3)); |
| |
| // Verify that the child frames are present in the form data. |
| std::vector<FormData> form_data; |
| for (const auto& [_, form] : |
| autofill_manager_for_main_frame()->form_structures()) { |
| form_data.push_back(form->ToFormData()); |
| } |
| EXPECT_THAT( |
| form_data, |
| ElementsAre(AllOf( |
| Property(&FormData::renderer_id, ::testing::IsFalse()), |
| Property(&FormData::child_frames, |
| ElementsAre(ChildFrameMatcher(-1), ChildFrameMatcher(0), |
| ChildFrameMatcher(0), ChildFrameMatcher(2)))))); |
| } |
| |
| // Checks that with autofill across iframes and throttling enabled, the child |
| // frames will stop being extracted for forms once the limit of frames is |
| // reached. |
| TEST_F(AutofillControllerTest, |
| ReadForm_WithChildFrames_Throttling_AcrossForms) { |
| // A form with iframes and inputs where some of the iframes have predecessors. |
| NSString* const test_page = |
| @"<form id='form1'>" |
| "<!-- 20 frames, just a the limit -->" |
| "<iframe></iframe>" |
| "<iframe></iframe>" |
| "<iframe></iframe>" |
| "<iframe></iframe>" |
| "<iframe></iframe>" |
| "<iframe></iframe>" |
| "<iframe></iframe>" |
| "<iframe></iframe>" |
| "<iframe></iframe>" |
| "<iframe></iframe>" |
| "<iframe></iframe>" |
| "<iframe></iframe>" |
| "<iframe></iframe>" |
| "<iframe></iframe>" |
| "<iframe></iframe>" |
| "<iframe></iframe>" |
| "<iframe></iframe>" |
| "<iframe></iframe>" |
| "<iframe></iframe>" |
| "<iframe></iframe>" |
| "Name <input id='name' type='text' name='name' />" |
| "Address <input type='text' name='address'>" |
| "City <input type='text' name='city'>" |
| "State <input type='text' name='state'>" |
| "</form>" |
| "<form id='form2'>" |
| "<!-- Frame limit busted -->" |
| "<iframe></iframe>" |
| "Name <input id='name' type='text' name='name' />" |
| "Address <input type='text' name='address'>" |
| "City <input type='text' name='city'>" |
| "State <input type='text' name='state'>" |
| "</form>" |
| "<form id='form3'>" |
| "<!-- Frame limit busted -->" |
| "<iframe></iframe>" |
| "Name <input id='name' type='text' name='name' />" |
| "Address <input type='text' name='address'>" |
| "City <input type='text' name='city'>" |
| "State <input type='text' name='state'>" |
| "</form>"; |
| |
| ASSERT_TRUE(LoadHtmlAndWaitForFormFetched(test_page, |
| /*expected_number_of_forms=*/3)); |
| |
| // Verify that the form data is correctly filled with the child frames data |
| // by respecting the child frames limit, where the first form has its 20 child |
| // frames then the follow up forms don't have any child frames. |
| std::vector<FormData> form_data; |
| for (const auto& [_, form] : |
| autofill_manager_for_main_frame()->form_structures()) { |
| form_data.push_back(form->ToFormData()); |
| } |
| auto form1_matcher = AllOf(Property(&FormData::renderer_id, IsTrue()), |
| Property(&FormData::child_frames, SizeIs(20))); |
| auto following_forms_matcher = |
| AllOf(Property(&FormData::renderer_id, IsTrue()), |
| Property(&FormData::child_frames, IsEmpty())); |
| EXPECT_THAT(form_data, ElementsAre(form1_matcher, following_forms_matcher, |
| following_forms_matcher)); |
| } |
| |
| // Checks that with autofill across iframes and throttling enabled, the child |
| // frames won't be extracted for the syntethic forms once the limit of frames is |
| // reached. |
| TEST_F(AutofillControllerTest, |
| ReadForm_WithChildFrames_Throttling_AcrossForms_Synthetic) { |
| // A form with iframes and inputs where some of the iframes have predecessors. |
| NSString* const test_page = |
| @"<form id='form1'>" |
| "<!-- 4 iframes, below the per-form limit -->" |
| "<iframe></iframe>" |
| "<iframe></iframe>" |
| "<iframe></iframe>" |
| "<iframe></iframe>" |
| "Name <input id='name' type='text' name='name' />" |
| "Address <input type='text' name='address'>" |
| "City <input type='text' name='city'>" |
| "State <input type='text' name='state'>" |
| "</form>" |
| "<!-- 17 frames in the synthetic form, just above the xform limit -->" |
| "<iframe></iframe>" |
| "<iframe></iframe>" |
| "<iframe></iframe>" |
| "<iframe></iframe>" |
| "<iframe></iframe>" |
| "<iframe></iframe>" |
| "<iframe></iframe>" |
| "<iframe></iframe>" |
| "<iframe></iframe>" |
| "<iframe></iframe>" |
| "<iframe></iframe>" |
| "<iframe></iframe>" |
| "<iframe></iframe>" |
| "<iframe></iframe>" |
| "<iframe></iframe>" |
| "<iframe></iframe>" |
| "<iframe></iframe>" |
| "Name <input id='name' type='text' name='name' />" |
| "Address <input type='text' name='address'>" |
| "City <input type='text' name='city'>" |
| "State <input type='text' name='state'>"; |
| |
| ASSERT_TRUE(LoadHtmlAndWaitForFormFetched(test_page, |
| /*expected_number_of_forms=*/2)); |
| |
| // Verify that the form data is correctly filled with the child frames data |
| // by respecting the child frames limit, where the first form has its 4 child |
| // frames then the follow up synthetic form hasn't any child frame because it |
| // busted the xform limit. |
| std::vector<FormData> form_data; |
| for (const auto& [_, form] : |
| autofill_manager_for_main_frame()->form_structures()) { |
| form_data.push_back(form->ToFormData()); |
| } |
| auto form1_matcher = AllOf(Property(&FormData::renderer_id, IsTrue()), |
| Property(&FormData::child_frames, SizeIs(4))); |
| auto synthetic_form_matcher = |
| AllOf(Property(&FormData::renderer_id, testing::Eq(FormRendererId(0))), |
| Property(&FormData::child_frames, IsEmpty())); |
| EXPECT_THAT(form_data, ElementsAre(synthetic_form_matcher, form1_matcher)); |
| } |
| |
| // Checks that with autofill across iframes and throttling enabled, the child |
| // frames will not be extracted on a form that exceeds the limit of child |
| // frames. |
| TEST_F(AutofillControllerTest, ReadForm_WithChildFrames_Throttling_SingleForm) { |
| // A form with iframes and inputs where some of the iframes have predecessors. |
| NSString* const test_page = |
| @"<form id='form1'>" |
| "<!-- 21 frames, just above the limit -->" |
| "<iframe></iframe>" |
| "<iframe></iframe>" |
| "<iframe></iframe>" |
| "<iframe></iframe>" |
| "<iframe></iframe>" |
| "<iframe></iframe>" |
| "<iframe></iframe>" |
| "<iframe></iframe>" |
| "<iframe></iframe>" |
| "<iframe></iframe>" |
| "<iframe></iframe>" |
| "<iframe></iframe>" |
| "<iframe></iframe>" |
| "<iframe></iframe>" |
| "<iframe></iframe>" |
| "<iframe></iframe>" |
| "<iframe></iframe>" |
| "<iframe></iframe>" |
| "<iframe></iframe>" |
| "<iframe></iframe>" |
| "<iframe></iframe>" |
| "Name <input id='name' type='text' name='name' />" |
| "Address <input type='text' name='address'>" |
| "City <input type='text' name='city'>" |
| "State <input type='text' name='state'>" |
| "</form>"; |
| |
| ASSERT_TRUE(LoadHtmlAndWaitForFormFetched(test_page, |
| /*expected_number_of_forms=*/1)); |
| |
| // Verify that the form data doesn't have child frames when the form exceeds |
| // the child frame limit. |
| std::vector<FormData> form_data; |
| for (const auto& [_, form] : |
| autofill_manager_for_main_frame()->form_structures()) { |
| form_data.push_back(form->ToFormData()); |
| } |
| auto form_matcher = AllOf(Property(&FormData::renderer_id, IsTrue()), |
| Property(&FormData::child_frames, IsEmpty())); |
| EXPECT_THAT(form_data, ElementsAre(form_matcher)); |
| } |
| |
| // Checks that with autofill across iframes and throttling enabled, the child |
| // frames will not be extracted on a synthetic form that exceeds the limit of |
| // child frames. |
| TEST_F(AutofillControllerTest, |
| ReadForm_WithChildFrames_Throttling_SingleForm_Synthetic) { |
| // A synthetic form with too many child frames exceeding the limit. |
| NSString* const test_page = |
| @"<html><body><div id='div'>" |
| "<!-- 21 frames in synthetic form, just above the limit -->" |
| "<iframe></iframe>" |
| "<iframe></iframe>" |
| "<iframe></iframe>" |
| "<iframe></iframe>" |
| "<iframe></iframe>" |
| "<iframe></iframe>" |
| "<iframe></iframe>" |
| "<iframe></iframe>" |
| "<iframe></iframe>" |
| "<iframe></iframe>" |
| "<iframe></iframe>" |
| "<iframe></iframe>" |
| "<iframe></iframe>" |
| "<iframe></iframe>" |
| "<iframe></iframe>" |
| "<iframe></iframe>" |
| "<iframe></iframe>" |
| "<iframe></iframe>" |
| "<iframe></iframe>" |
| "<iframe></iframe>" |
| "<iframe></iframe>" |
| "Name <input id='name' type='text' name='name' />" |
| "Address <input type='text' name='address'>" |
| "City <input type='text' name='city'>" |
| "State <input type='text' name='state'>" |
| "</div></html></body>"; |
| |
| ASSERT_TRUE(LoadHtmlAndWaitForFormFetched(test_page, |
| /*expected_number_of_forms=*/1)); |
| |
| // Verify that the synthetic form data doesn't have child frames when the form |
| // exceeds the child frame limit. |
| std::vector<FormData> form_data; |
| for (const auto& [_, form] : |
| autofill_manager_for_main_frame()->form_structures()) { |
| form_data.push_back(form->ToFormData()); |
| } |
| auto form_matcher = |
| AllOf(Property(&FormData::renderer_id, Eq(FormRendererId(0))), |
| Property(&FormData::child_frames, IsEmpty())); |
| EXPECT_THAT(form_data, UnorderedElementsAre(form_matcher)); |
| } |
| |
| // Checks that viewing an HTML page containing a form with an 'id' results in |
| // the form being registered as a FormStructure by the BrowserAutofillManager, |
| // and the name is correctly set. |
| TEST_F(AutofillControllerTest, ReadFormName) { |
| ASSERT_TRUE(LoadHtmlAndWaitForFormFetched(kMinimalFormWithNameHtml, 1)); |
| web::WebFramesManager* frames_manager = |
| AutofillJavaScriptFeature::GetInstance()->GetWebFramesManager( |
| web_state()); |
| web::WebFrame* main_frame = frames_manager->GetMainWebFrame(); |
| BrowserAutofillManager& autofill_manager = |
| AutofillDriverIOS::FromWebStateAndWebFrame(web_state(), main_frame) |
| ->GetAutofillManager(); |
| const auto& forms = autofill_manager.form_structures(); |
| const auto& form = *(forms.begin()->second); |
| EXPECT_EQ(u"form1", form.ToFormData().name()); |
| } |
| |
| // Checks that an HTML page containing a profile-type form which is submitted |
| // with scripts (simulating user form submission) results in a profile being |
| // successfully imported into the PersonalDataManager. |
| TEST_F(AutofillControllerTest, ProfileImport) { |
| PersonalDataManager* personal_data_manager = |
| PersonalDataManagerFactory::GetForProfile(profile_.get()); |
| test_api(personal_data_manager->address_data_manager()) |
| .set_auto_accept_address_imports(true); |
| // Check there are no registered profiles already. |
| EXPECT_EQ(0U, |
| personal_data_manager->address_data_manager().GetProfiles().size()); |
| ASSERT_TRUE(LoadHtmlAndWaitForFormFetched(kProfileFormHtml, 1)); |
| web::test::ExecuteJavaScript( |
| @"document.forms[0].name.value = 'Homer Simpson'", web_state()); |
| web::test::ExecuteJavaScript( |
| @"document.forms[0].address.value = '123 Main Street'", web_state()); |
| web::test::ExecuteJavaScript(@"document.forms[0].city.value = 'Springfield'", |
| web_state()); |
| web::test::ExecuteJavaScript(@"document.forms[0].state.value = 'IL'", |
| web_state()); |
| web::test::ExecuteJavaScript(@"document.forms[0].zip.value = '55123'", |
| web_state()); |
| web::test::ExecuteJavaScript(@"submit.click()", web_state()); |
| WaitForCondition(^bool { |
| return personal_data_manager->address_data_manager().GetProfiles().size(); |
| }); |
| const std::vector<const AutofillProfile*>& profiles = |
| personal_data_manager->address_data_manager().GetProfiles(); |
| if (profiles.size() != 1) { |
| FAIL() << "Not exactly one profile found after attempted import"; |
| } |
| const AutofillProfile& profile = *profiles[0]; |
| EXPECT_EQ(u"Homer Simpson", profile.GetInfo(NAME_FULL, "en-US")); |
| EXPECT_EQ(u"123 Main Street", profile.GetInfo(ADDRESS_HOME_LINE1, "en-US")); |
| EXPECT_EQ(u"Springfield", profile.GetInfo(ADDRESS_HOME_CITY, "en-US")); |
| EXPECT_EQ(u"IL", profile.GetInfo(ADDRESS_HOME_STATE, "en-US")); |
| EXPECT_EQ(u"55123", profile.GetInfo(ADDRESS_HOME_ZIP, "en-US")); |
| } |
| |
| void AutofillControllerTest::SetUpForSuggestions( |
| NSString* data, |
| size_t expected_number_of_forms) { |
| PersonalDataManager* personal_data_manager = |
| PersonalDataManagerFactory::GetForProfile(profile_.get()); |
| AutofillProfile profile( |
| autofill::i18n_model_definition::kLegacyHierarchyCountryCode); |
| profile.SetRawInfo(NAME_FULL, u"Homer Simpson"); |
| profile.SetRawInfo(ADDRESS_HOME_LINE1, u"123 Main Street"); |
| profile.SetRawInfo(ADDRESS_HOME_CITY, u"Springfield"); |
| profile.SetRawInfo(ADDRESS_HOME_STATE, u"IL"); |
| profile.SetRawInfo(ADDRESS_HOME_ZIP, u"55123"); |
| EXPECT_EQ(0U, |
| personal_data_manager->address_data_manager().GetProfiles().size()); |
| PersonalDataChangedWaiter waiter(*personal_data_manager); |
| personal_data_manager->address_data_manager().AddProfile(profile); |
| std::move(waiter).Wait(); |
| EXPECT_EQ(1U, |
| personal_data_manager->address_data_manager().GetProfiles().size()); |
| |
| ASSERT_TRUE(LoadHtmlAndWaitForFormFetched(data, expected_number_of_forms)); |
| web::test::WaitForBackgroundTasks(); |
| } |
| |
| // Checks that focusing on a text element of a profile-type form will result in |
| // suggestions being sent to the AutofillAgent, once data has been loaded into a |
| // test data manager. |
| TEST_F(AutofillControllerTest, ProfileSuggestions) { |
| if (@available(iOS 16.3, *)) { |
| // TODO(crbug.com/40064372): Re-enable when fixed on iOS16.3+. |
| return; |
| } |
| |
| SetUpForSuggestions(kProfileFormHtml, 1); |
| ForceViewRendering(web_state()->GetView()); |
| ResetWaitForSuggestionRetrieval(); |
| web::test::ExecuteJavaScript(@"document.forms[0].name.focus()", web_state()); |
| WaitForSuggestionRetrieval(/*wait_for_trigger=*/YES); |
| ExpectMetric("Autofill.SuggestionsCount.Address", 1); |
| EXPECT_EQ(1U, [suggestion_controller() suggestions].count); |
| FormSuggestion* suggestion = [suggestion_controller() suggestions][0]; |
| EXPECT_NSEQ(@"Homer Simpson", suggestion.value); |
| } |
| |
| // Tests that the system is able to offer suggestions for an anonymous form when |
| // there is another anonymous form on the page. |
| TEST_F(AutofillControllerTest, ProfileSuggestionsTwoAnonymousForms) { |
| if (@available(iOS 16.3, *)) { |
| // TODO(crbug.com/40064372): Re-enable when fixed on iOS16.3+. |
| return; |
| } |
| |
| SetUpForSuggestions( |
| [NSString stringWithFormat:@"%@%@", kProfileFormHtml, kProfileFormHtml], |
| 2); |
| ForceViewRendering(web_state()->GetView()); |
| ResetWaitForSuggestionRetrieval(); |
| web::test::ExecuteJavaScript(@"document.forms[0].name.focus()", web_state()); |
| WaitForSuggestionRetrieval(/*wait_for_trigger=*/YES); |
| ExpectMetric("Autofill.SuggestionsCount.Address", 1); |
| EXPECT_EQ(1U, [suggestion_controller() suggestions].count); |
| FormSuggestion* suggestion = [suggestion_controller() suggestions][0]; |
| EXPECT_NSEQ(@"Homer Simpson", suggestion.value); |
| } |
| |
| // Checks that focusing on a select element in a profile-type form will result |
| // in suggestions being sent to the AutofillAgent, once data has been loaded |
| // into a test data manager. |
| TEST_F(AutofillControllerTest, ProfileSuggestionsFromSelectField) { |
| if (@available(iOS 16.3, *)) { |
| // TODO(crbug.com/40064372): Re-enable when fixed on iOS16.3+. |
| return; |
| } |
| |
| SetUpForSuggestions(kProfileFormHtml, 1); |
| ForceViewRendering(web_state()->GetView()); |
| ResetWaitForSuggestionRetrieval(); |
| web::test::ExecuteJavaScript(@"document.forms[0].state.focus()", web_state()); |
| WaitForSuggestionRetrieval(/*wait_for_trigger=*/YES); |
| ExpectMetric("Autofill.SuggestionsCount.Address", 1); |
| EXPECT_EQ(1U, [suggestion_controller() suggestions].count); |
| FormSuggestion* suggestion = [suggestion_controller() suggestions][0]; |
| EXPECT_NSEQ(@"IL", suggestion.value); |
| } |
| |
| // Checks that multiple profiles will offer a matching number of suggestions. |
| TEST_F(AutofillControllerTest, MultipleProfileSuggestions) { |
| if (@available(iOS 16.3, *)) { |
| // TODO(crbug.com/40064372): Re-enable when fixed on iOS16.3+. |
| return; |
| } |
| |
| PersonalDataManager* personal_data_manager = |
| PersonalDataManagerFactory::GetForProfile(profile_.get()); |
| personal_data_manager->SetSyncServiceForTest(nullptr); |
| |
| AutofillProfile profile( |
| autofill::i18n_model_definition::kLegacyHierarchyCountryCode); |
| profile.SetRawInfo(NAME_FULL, u"Homer Simpson"); |
| profile.SetRawInfo(ADDRESS_HOME_LINE1, u"123 Main Street"); |
| profile.SetRawInfo(ADDRESS_HOME_CITY, u"Springfield"); |
| profile.SetRawInfo(ADDRESS_HOME_STATE, u"IL"); |
| profile.SetRawInfo(ADDRESS_HOME_ZIP, u"55123"); |
| |
| AutofillProfile profile2( |
| autofill::i18n_model_definition::kLegacyHierarchyCountryCode); |
| profile2.SetRawInfo(NAME_FULL, u"Larry Page"); |
| profile2.SetRawInfo(ADDRESS_HOME_LINE1, u"1600 Amphitheatre Parkway"); |
| profile2.SetRawInfo(ADDRESS_HOME_CITY, u"Mountain View"); |
| profile2.SetRawInfo(ADDRESS_HOME_STATE, u"CA"); |
| profile2.SetRawInfo(ADDRESS_HOME_ZIP, u"94043"); |
| |
| EXPECT_EQ(0U, |
| personal_data_manager->address_data_manager().GetProfiles().size()); |
| PersonalDataChangedWaiter waiter(*personal_data_manager); |
| personal_data_manager->address_data_manager().AddProfile(profile); |
| personal_data_manager->address_data_manager().AddProfile(profile2); |
| std::move(waiter).Wait(); |
| EXPECT_EQ(2U, |
| personal_data_manager->address_data_manager().GetProfiles().size()); |
| |
| EXPECT_TRUE(LoadHtmlAndWaitForFormFetched(kProfileFormHtml, 1)); |
| ForceViewRendering(web_state()->GetView()); |
| ResetWaitForSuggestionRetrieval(); |
| web::test::ExecuteJavaScript(@"document.forms[0].name.focus()", web_state()); |
| WaitForSuggestionRetrieval(/*wait_for_trigger=*/YES); |
| ExpectMetric("Autofill.SuggestionsCount.Address", 2); |
| EXPECT_EQ(2U, [suggestion_controller() suggestions].count); |
| } |
| |
| // Check that an HTML page containing a key/value type form which is submitted |
| // with scripts (simulating user form submission) results in data being |
| // successfully registered. |
| TEST_F(AutofillControllerTest, KeyValueImport) { |
| ASSERT_TRUE(LoadHtmlAndWaitForFormFetched(kKeyValueFormHtml, 1)); |
| web::test::ExecuteJavaScript(@"document.forms[0].greeting.value = 'Hello'", |
| web_state()); |
| scoped_refptr<AutofillWebDataService> web_data_service = |
| ios::WebDataServiceFactory::GetAutofillWebDataForProfile( |
| profile_.get(), ServiceAccessType::EXPLICIT_ACCESS); |
| |
| { |
| base::test::TestFuture<WebDataServiceBase::Handle, |
| std::unique_ptr<WDTypedResult>> |
| future; |
| web_data_service->GetFormValuesForElementName(u"greeting", std::u16string(), |
| /*limit=*/1, |
| future.GetCallback()); |
| base::ThreadPoolInstance::Get()->FlushForTesting(); |
| web::test::WaitForBackgroundTasks(); |
| std::vector<AutocompleteEntry> result = |
| static_cast<WDResult<std::vector<AutocompleteEntry>>&>(*future.Get<1>()) |
| .GetValue(); |
| // No value should be returned before anything is loaded via form |
| // submission. |
| EXPECT_THAT(result, IsEmpty()); |
| } |
| |
| web::test::ExecuteJavaScript(@"submit.click()", web_state()); |
| |
| { |
| base::test::TestFuture<WebDataServiceBase::Handle, |
| std::unique_ptr<WDTypedResult>> |
| future; |
| web_data_service->GetFormValuesForElementName(u"greeting", std::u16string(), |
| /*limit=*/1, |
| future.GetCallback()); |
| base::ThreadPoolInstance::Get()->FlushForTesting(); |
| web::test::WaitForBackgroundTasks(); |
| std::vector<AutocompleteEntry> result = |
| static_cast<WDResult<std::vector<AutocompleteEntry>>&>(*future.Get<1>()) |
| .GetValue(); |
| // One result should be returned, matching the filled value. |
| EXPECT_THAT(result, ElementsAre(Property( |
| &AutocompleteEntry::key, |
| Property(&AutocompleteKey::value, u"Hello")))); |
| } |
| } |
| |
| void AutofillControllerTest::SetUpKeyValueData() { |
| scoped_refptr<AutofillWebDataService> web_data_service = |
| ios::WebDataServiceFactory::GetAutofillWebDataForProfile( |
| profile_.get(), ServiceAccessType::EXPLICIT_ACCESS); |
| // Load value into database. |
| std::vector<FormFieldData> values; |
| FormFieldData fieldData; |
| fieldData.set_name(u"greeting"); |
| fieldData.set_value(u"Bonjour"); |
| values.push_back(fieldData); |
| web_data_service->AddFormFields(values); |
| |
| // Load test page. |
| ASSERT_TRUE(LoadHtmlAndWaitForFormFetched(kKeyValueFormHtml, 1)); |
| web::test::WaitForBackgroundTasks(); |
| } |
| |
| // Checks that focusing on an element of a key/value type form then typing the |
| // first letter of a suggestion will result in suggestions being sent to the |
| // AutofillAgent, once data has been loaded into a test data manager. |
| TEST_F(AutofillControllerTest, KeyValueSuggestions) { |
| SetUpKeyValueData(); |
| ResetWaitForSuggestionRetrieval(); |
| // Focus element. |
| web::test::ExecuteJavaScript(@"document.forms[0].greeting.value='B'", |
| web_state()); |
| web::test::ExecuteJavaScript(@"document.forms[0].greeting.focus()", |
| web_state()); |
| WaitForSuggestionRetrieval(/*wait_for_trigger=*/YES); |
| EXPECT_EQ(1U, [suggestion_controller() suggestions].count); |
| FormSuggestion* suggestion = [suggestion_controller() suggestions][0]; |
| EXPECT_NSEQ(@"Bonjour", suggestion.value); |
| } |
| |
| // Checks that typing events (simulated in script) result in suggestions. Note |
| // that the field is not explicitly focused before typing starts; this can |
| // happen in practice and should not result in a crash or incorrect behavior. |
| TEST_F(AutofillControllerTest, KeyValueTypedSuggestions) { |
| SetUpKeyValueData(); |
| ResetWaitForSuggestionRetrieval(); |
| web::test::ExecuteJavaScript(@"document.forms[0].greeting.focus()", |
| web_state()); |
| WaitForSuggestionRetrieval(/*wait_for_trigger=*/YES); |
| ResetWaitForSuggestionRetrieval(); |
| SimulateTextInputEvent(/*field_id=*/@"greeting", /*field_value=*/@"B"); |
| |
| WaitForSuggestionRetrieval(/*wait_for_trigger=*/YES); |
| EXPECT_EQ(1U, [suggestion_controller() suggestions].count); |
| FormSuggestion* suggestion = [suggestion_controller() suggestions][0]; |
| EXPECT_NSEQ(@"Bonjour", suggestion.value); |
| } |
| |
| // Checks that focusing on and typing on one field, then changing focus before |
| // typing again, result in suggestions. |
| TEST_F(AutofillControllerTest, KeyValueFocusChange) { |
| SetUpKeyValueData(); |
| |
| // Focus the dummy field and confirm no suggestions are presented. |
| ResetWaitForSuggestionRetrieval(); |
| web::test::ExecuteJavaScript(@"document.forms[0].dummy.focus()", web_state()); |
| WaitForSuggestionRetrieval(/*wait_for_trigger=*/YES); |
| ASSERT_EQ(0U, [suggestion_controller() suggestions].count); |
| ResetWaitForSuggestionRetrieval(); |
| |
| // Enter 'B' in the dummy field and confirm no suggestions are presented. |
| SimulateTextInputEvent(/*field_id=*/@"dummy", /*field_value=*/@"B"); |
| |
| WaitForSuggestionRetrieval(/*wait_for_trigger=*/YES); |
| ASSERT_EQ(0U, [suggestion_controller() suggestions].count); |
| ResetWaitForSuggestionRetrieval(); |
| |
| // Enter 'B' in the greeting field and confirm that one suggestion ("Bonjour") |
| // is presented. |
| web::test::ExecuteJavaScript(@"document.forms[0].greeting.focus()", |
| web_state()); |
| WaitForSuggestionRetrieval(/*wait_for_trigger=*/YES); |
| ResetWaitForSuggestionRetrieval(); |
| web::test::ExecuteJavaScript( |
| @"var event = document.createEvent('TextEvent');", web_state()); |
| web::test::ExecuteJavaScript( |
| @"event.initTextEvent('textInput', true, true, window, 'B');", |
| web_state()); |
| web::test::ExecuteJavaScript( |
| @"document.forms[0].greeting.dispatchEvent(event);", web_state()); |
| WaitForSuggestionRetrieval(/*wait_for_trigger=*/YES); |
| |
| ASSERT_EQ(1U, [suggestion_controller() suggestions].count); |
| FormSuggestion* suggestion = [suggestion_controller() suggestions][0]; |
| EXPECT_NSEQ(@"Bonjour", suggestion.value); |
| } |
| |
| // Checks that focusing on an element of a key/value type form without typing |
| // won't result in suggestions being sent to the AutofillAgent, once data has |
| // been loaded into a test data manager. |
| TEST_F(AutofillControllerTest, NoKeyValueSuggestionsWithoutTyping) { |
| SetUpKeyValueData(); |
| ResetWaitForSuggestionRetrieval(); |
| // Focus element. |
| web::test::ExecuteJavaScript(@"document.forms[0].greeting.focus()", |
| web_state()); |
| WaitForSuggestionRetrieval(/*wait_for_trigger=*/YES); |
| EXPECT_EQ(0U, [suggestion_controller() suggestions].count); |
| } |
| |
| // Checks that an HTML page containing a credit card-type form which is |
| // submitted with scripts (simulating user form submission) results in a credit |
| // card being successfully imported into the PersonalDataManager. |
| TEST_F(AutofillControllerTest, CreditCardImport) { |
| PersonalDataManager* personal_data_manager = |
| PersonalDataManagerFactory::GetForProfile(profile_.get()); |
| personal_data_manager->SetSyncServiceForTest(nullptr); |
| |
| // Check there are no registered profiles already. |
| EXPECT_EQ( |
| 0U, |
| personal_data_manager->payments_data_manager().GetCreditCards().size()); |
| |
| LoadAndFillCreditCardForm(); |
| |
| __block bool save_card_bottomsheet_shown = false; |
| OCMStub([autofill_commands_handler_ |
| showSaveCardBottomSheetOnOriginWebState:web_state()]) |
| .andDo(^(NSInvocation* invocation) { |
| save_card_bottomsheet_shown = true; |
| }); |
| web::test::ExecuteJavaScript(@"submit.click()", web_state()); |
| WaitForCondition(^bool() { |
| return save_card_bottomsheet_shown; |
| }); |
| ASSERT_TRUE(save_card_bottomsheet_shown); |
| |
| { |
| // This call cause a modification of the PersonalDataManager, so wait until |
| // the asynchronous task completes in addition to waiting for the UI update. |
| PersonalDataChangedWaiter waiter(*personal_data_manager); |
| bottomsheet_tab_helper_->GetSaveCardBottomSheetModel()->OnAccepted(); |
| std::move(waiter).Wait(); |
| } |
| |
| const std::vector<const CreditCard*>& credit_cards = |
| personal_data_manager->payments_data_manager().GetCreditCards(); |
| ASSERT_EQ(1U, credit_cards.size()); |
| const CreditCard& credit_card = *credit_cards[0]; |
| EXPECT_EQ(u"Superman", credit_card.GetInfo(CREDIT_CARD_NAME_FULL, "en-US")); |
| EXPECT_EQ(u"4000444444444444", |
| credit_card.GetInfo(CREDIT_CARD_NUMBER, "en-US")); |
| EXPECT_EQ(u"11", credit_card.GetInfo(CREDIT_CARD_EXP_MONTH, "en-US")); |
| EXPECT_EQ(u"2999", |
| credit_card.GetInfo(CREDIT_CARD_EXP_4_DIGIT_YEAR, "en-US")); |
| |
| histogram_tester_->ExpectUniqueSample( |
| /*name=*/kAutofillSubmissionDetectionSourceHistogram, |
| /*sample=*/mojom::SubmissionSource::FORM_SUBMISSION, |
| /*expected_count=*/1); |
| } |
| |
| // Checks that an HTML page containing a credit card-type form which is |
| // submitted with scripts (simulating form removal) results in a credit |
| // card being successfully imported into the PersonalDataManager. |
| TEST_F(AutofillControllerTest, CreditCardImportAfterFormRemoval) { |
| PersonalDataManager* personal_data_manager = |
| PersonalDataManagerFactory::GetForProfile(profile_.get()); |
| personal_data_manager->SetSyncServiceForTest(nullptr); |
| |
| // Check there are no registered profiles already. |
| EXPECT_EQ( |
| 0U, |
| personal_data_manager->payments_data_manager().GetCreditCards().size()); |
| |
| LoadAndFillCreditCardForm(); |
| |
| __block bool save_card_bottomsheet_shown = false; |
| OCMStub([autofill_commands_handler_ |
| showSaveCardBottomSheetOnOriginWebState:web_state()]) |
| .andDo(^(NSInvocation* invocation) { |
| save_card_bottomsheet_shown = true; |
| }); |
| |
| // Deleting the form should be detected as a submission because it had user |
| // input. Adding a delay is necessary or the event above might not be |
| // dispatched. |
| web::test::ExecuteJavaScript(@"setTimeout(function(){" |
| @" document.forms[0].remove();" |
| @"}, 30);", |
| web_state()); |
| WaitForCondition(^bool() { |
| return save_card_bottomsheet_shown; |
| }); |
| ASSERT_TRUE(save_card_bottomsheet_shown); |
| |
| { |
| // This call cause a modification of the PersonalDataManager, so wait until |
| // the asynchronous task completes in addition to waiting for the UI update. |
| PersonalDataChangedWaiter waiter(*personal_data_manager); |
| bottomsheet_tab_helper_->GetSaveCardBottomSheetModel()->OnAccepted(); |
| std::move(waiter).Wait(); |
| } |
| |
| const std::vector<const CreditCard*>& credit_cards = |
| personal_data_manager->payments_data_manager().GetCreditCards(); |
| ASSERT_EQ(1U, credit_cards.size()); |
| const CreditCard& credit_card = *credit_cards[0]; |
| EXPECT_EQ(u"Superman", credit_card.GetInfo(CREDIT_CARD_NAME_FULL, "en-US")); |
| EXPECT_EQ(u"4000444444444444", |
| credit_card.GetInfo(CREDIT_CARD_NUMBER, "en-US")); |
| EXPECT_EQ(u"11", credit_card.GetInfo(CREDIT_CARD_EXP_MONTH, "en-US")); |
| EXPECT_EQ(u"2999", |
| credit_card.GetInfo(CREDIT_CARD_EXP_4_DIGIT_YEAR, "en-US")); |
| |
| histogram_tester_->ExpectUniqueSample( |
| /*name=*/kAutofillSubmissionDetectionSourceHistogram, |
| /*sample=*/mojom::SubmissionSource::XHR_SUCCEEDED, |
| /*expected_count=*/1); |
| } |
| |
| // Checks that an HTML page containing a credit card-type form which is |
| // submitted with scripts (simulating form removal) results in a credit |
| // card being successfully imported into the PersonalDataManager. The test |
| // verifies that the imported card includes the lastest known field values for |
| // the submitted form. |
| TEST_F(AutofillControllerTest, |
| CreditCardImportWithFieldDataManagerValuesAfterFormRemoval) { |
| PersonalDataManager* personal_data_manager = |
| PersonalDataManagerFactory::GetForProfile(profile_.get()); |
| personal_data_manager->SetSyncServiceForTest(nullptr); |
| |
| // Check there are no registered profiles already. |
| EXPECT_EQ( |
| 0U, |
| personal_data_manager->payments_data_manager().GetCreditCards().size()); |
| |
| LoadAndFillCreditCardForm(); |
| |
| // Update the form fields in `FieldDataManager`. |
| // When detecting a submission, the imported credit card should include the |
| // latest values in `FieldDataManager`. |
| auto* frames_manager = |
| AutofillJavaScriptFeature::GetInstance()->GetWebFramesManager( |
| web_state()); |
| auto* main_frame = frames_manager->GetMainWebFrame(); |
| auto* fieldDataManager = |
| autofill::FieldDataManagerFactoryIOS::FromWebFrame(main_frame); |
| // Name. |
| fieldDataManager->UpdateFieldDataMap(FieldRendererId(2), u"Chuck", |
| FieldPropertiesFlags::kAutofilled); |
| // CCNo. |
| fieldDataManager->UpdateFieldDataMap(FieldRendererId(3), |
| u"5425-2334-3010-9903", |
| FieldPropertiesFlags::kAutofilled); |
| // CCExpiresMonth. |
| fieldDataManager->UpdateFieldDataMap(FieldRendererId(4), u"12", |
| FieldPropertiesFlags::kAutofilled); |
| // CCExpiresYear. |
| fieldDataManager->UpdateFieldDataMap(FieldRendererId(5), u"2998", |
| FieldPropertiesFlags::kAutofilled); |
| |
| __block bool save_card_bottomsheet_shown = false; |
| OCMStub([autofill_commands_handler_ |
| showSaveCardBottomSheetOnOriginWebState:web_state()]) |
| .andDo(^(NSInvocation* invocation) { |
| save_card_bottomsheet_shown = true; |
| }); |
| |
| // Deleting the form should be detected as a submission because it had user |
| // input. Adding a delay is necessary or the event above might not be |
| // dispatched. |
| web::test::ExecuteJavaScript(@"setTimeout(function(){" |
| @" document.forms[0].remove();" |
| @"}, 30);", |
| web_state()); |
| |
| WaitForCondition(^bool() { |
| return save_card_bottomsheet_shown; |
| }); |
| ASSERT_TRUE(save_card_bottomsheet_shown); |
| |
| { |
| // This call cause a modification of the PersonalDataManager, so wait until |
| // the asynchronous task completes in addition to waiting for the UI update. |
| PersonalDataChangedWaiter waiter(*personal_data_manager); |
| bottomsheet_tab_helper_->GetSaveCardBottomSheetModel()->OnAccepted(); |
| std::move(waiter).Wait(); |
| } |
| |
| const std::vector<const CreditCard*>& credit_cards = |
| personal_data_manager->payments_data_manager().GetCreditCards(); |
| ASSERT_EQ(1U, credit_cards.size()); |
| const CreditCard& credit_card = *credit_cards[0]; |
| |
| EXPECT_EQ(u"Chuck", credit_card.GetInfo(CREDIT_CARD_NAME_FULL, "en-US")); |
| EXPECT_EQ(u"5425233430109903", |
| credit_card.GetInfo(CREDIT_CARD_NUMBER, "en-US")); |
| EXPECT_EQ(u"12", credit_card.GetInfo(CREDIT_CARD_EXP_MONTH, "en-US")); |
| EXPECT_EQ(u"2998", |
| credit_card.GetInfo(CREDIT_CARD_EXP_4_DIGIT_YEAR, "en-US")); |
| } |
| |
| // Checks that an HTML page containing a profile-type formless form which is |
| // submitted with scripts (simulating form removal) results in a profile being |
| // successfully imported into the PersonalDataManager. |
| TEST_F(AutofillControllerTest, ProfileImportAfterFormlessFormRemoval) { |
| PersonalDataManager* personal_data_manager = |
| PersonalDataManagerFactory::GetForProfile(profile_.get()); |
| test_api(personal_data_manager->address_data_manager()) |
| .set_auto_accept_address_imports(true); |
| // Check there are no registered profiles already. |
| EXPECT_EQ(0U, |
| personal_data_manager->address_data_manager().GetProfiles().size()); |
| ASSERT_TRUE(LoadHtmlAndWaitForFormFetched(kProfileFormlessHtml, 1)); |
| |
| // Simulate entering a profile in the fields. |
| SimulateTextInputEvent(/*field_id=*/@"name", |
| /*field_value=*/@"Homer Simpson"); |
| SimulateTextInputEvent(/*field_id=*/@"address", |
| /*field_value=*/@"123 Main Street"); |
| SimulateTextInputEvent(/*field_id=*/@"city", /*field_value=*/@"Springfield"); |
| SimulateTextInputEvent(/*field_id=*/@"state", /*field_value=*/@"IL"); |
| SimulateTextInputEvent(/*field_id=*/@"zip", /*field_value=*/@"55123"); |
| |
| // Deleting the form should be detected as a submission because it had user |
| // input. Adding a delay is necessary or the event above might not be |
| // dispatched. |
| web::test::ExecuteJavaScript(@"setTimeout(function(){" |
| @" document.getElementById('div').remove();" |
| @"}, 30);", |
| web_state()); |
| |
| WaitForCondition(^bool { |
| return personal_data_manager->address_data_manager().GetProfiles().size(); |
| }); |
| const std::vector<const AutofillProfile*>& profiles = |
| personal_data_manager->address_data_manager().GetProfiles(); |
| if (profiles.size() != 1) { |
| FAIL() << "Not exactly one profile found after attempted import"; |
| } |
| const AutofillProfile& profile = *profiles[0]; |
| EXPECT_EQ(u"Homer Simpson", profile.GetInfo(NAME_FULL, "en-US")); |
| EXPECT_EQ(u"123 Main Street", profile.GetInfo(ADDRESS_HOME_LINE1, "en-US")); |
| EXPECT_EQ(u"Springfield", profile.GetInfo(ADDRESS_HOME_CITY, "en-US")); |
| EXPECT_EQ(u"IL", profile.GetInfo(ADDRESS_HOME_STATE, "en-US")); |
| EXPECT_EQ(u"55123", profile.GetInfo(ADDRESS_HOME_ZIP, "en-US")); |
| |
| histogram_tester_->ExpectUniqueSample( |
| /*name=*/kAutofillSubmissionDetectionSourceHistogram, |
| /*sample=*/mojom::SubmissionSource::XHR_SUCCEEDED, |
| /*expected_count=*/1); |
| } |
| |
| class AutofillControllerWithoutLocalSaveCardBottomSheetTest |
| : public AutofillControllerTest { |
| protected: |
| AutofillControllerWithoutLocalSaveCardBottomSheetTest() { |
| scoped_feature_list_.InitAndDisableFeature( |
| features::kAutofillLocalSaveCardBottomSheet); |
| } |
| ScopedFeatureList scoped_feature_list_; |
| }; |
| |
| // Checks that an HTML page containing a credit card-type form which is |
| // submitted with scripts (simulating user form submission) results in a credit |
| // card being successfully imported into the PersonalDataManager. |
| // TODO(crbug.com/422148854): Remove this test post local save card bottomsheet |
| // is launched. |
| TEST_F(AutofillControllerWithoutLocalSaveCardBottomSheetTest, |
| CreditCardImport) { |
| InfoBarManagerImpl::CreateForWebState(web_state()); |
| PersonalDataManager* personal_data_manager = |
| PersonalDataManagerFactory::GetForProfile(profile_.get()); |
| personal_data_manager->SetSyncServiceForTest(nullptr); |
| |
| // Check there are no registered profiles already. |
| EXPECT_EQ( |
| 0U, |
| personal_data_manager->payments_data_manager().GetCreditCards().size()); |
| |
| LoadAndFillCreditCardForm(); |
| web::test::ExecuteJavaScript(@"submit.click()", web_state()); |
| infobars::InfoBarManager* infobar_manager = |
| InfoBarManagerImpl::FromWebState(web_state()); |
| WaitForCondition(^bool() { |
| return infobar_manager->infobars().size(); |
| }); |
| ExpectMetric("Autofill.CreditCardInfoBar.Local", |
| AutofillMetrics::INFOBAR_SHOWN); |
| ExpectMetric("Autofill.SaveCreditCardPromptResult.IOS.Local.Banner." |
| "NumStrikes.0.NoFixFlow", |
| static_cast<int>( |
| autofill_metrics::SaveCreditCardPromptResultIOS::kShown)); |
| ASSERT_EQ(1U, infobar_manager->infobars().size()); |
| infobars::InfoBarDelegate* infobar = |
| infobar_manager->infobars()[0]->delegate(); |
| ConfirmInfoBarDelegate* confirm_infobar = infobar->AsConfirmInfoBarDelegate(); |
| |
| { |
| // This call cause a modification of the PersonalDataManager, so wait until |
| // the asynchronous task completes in addition to waiting for the UI update. |
| PersonalDataChangedWaiter waiter(*personal_data_manager); |
| confirm_infobar->Accept(); |
| std::move(waiter).Wait(); |
| } |
| |
| const std::vector<const CreditCard*>& credit_cards = |
| personal_data_manager->payments_data_manager().GetCreditCards(); |
| ASSERT_EQ(1U, credit_cards.size()); |
| const CreditCard& credit_card = *credit_cards[0]; |
| EXPECT_EQ(u"Superman", credit_card.GetInfo(CREDIT_CARD_NAME_FULL, "en-US")); |
| EXPECT_EQ(u"4000444444444444", |
| credit_card.GetInfo(CREDIT_CARD_NUMBER, "en-US")); |
| EXPECT_EQ(u"11", credit_card.GetInfo(CREDIT_CARD_EXP_MONTH, "en-US")); |
| EXPECT_EQ(u"2999", |
| credit_card.GetInfo(CREDIT_CARD_EXP_4_DIGIT_YEAR, "en-US")); |
| |
| histogram_tester_->ExpectUniqueSample( |
| /*name=*/kAutofillSubmissionDetectionSourceHistogram, |
| /*sample=*/mojom::SubmissionSource::FORM_SUBMISSION, |
| /*expected_count=*/1); |
| } |
| |
| } // namespace |
| |
| } // namespace autofill |