| // 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 <string_view> |
| #import <vector> |
| |
| #import "base/apple/foundation_util.h" |
| #import "base/files/file_enumerator.h" |
| #import "base/files/file_path.h" |
| #import "base/files/file_util.h" |
| #import "base/memory/ptr_util.h" |
| #import "base/path_service.h" |
| #import "base/strings/string_util.h" |
| #import "base/strings/stringprintf.h" |
| #import "base/strings/sys_string_conversions.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/scoped_feature_list.h" |
| #import "components/autofill/core/browser/browser_autofill_manager.h" |
| #import "components/autofill/core/browser/form_structure.h" |
| #import "components/autofill/core/browser/test_autofill_manager_waiter.h" |
| #import "components/autofill/core/common/autofill_features.h" |
| #import "components/autofill/core/common/autofill_payments_features.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/test_autofill_manager_injector.h" |
| #import "components/autofill/ios/form_util/form_util_java_script_feature.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 "components/sync_user_events/fake_user_event_service.h" |
| #import "ios/chrome/browser/autofill/model/address_normalizer_factory.h" |
| #import "ios/chrome/browser/autofill/model/form_suggestion_controller.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/browser_state/test_chrome_browser_state.h" |
| #import "ios/chrome/browser/shared/model/paths/paths.h" |
| #import "ios/chrome/browser/sync/model/ios_user_event_service_factory.h" |
| #import "ios/chrome/browser/ui/autofill/chrome_autofill_client_ios.h" |
| #import "ios/chrome/browser/web/model/chrome_web_client.h" |
| #import "ios/web/public/js_messaging/web_frame.h" |
| #import "ios/web/public/js_messaging/web_frames_manager.h" |
| #import "ios/web/public/test/js_test_util.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/test/web_view_interaction_test_util.h" |
| #import "ios/web/public/web_state.h" |
| #import "testing/data_driven_testing/data_driven_test.h" |
| #import "testing/platform_test.h" |
| |
| using base::test::ios::kWaitForJSCompletionTimeout; |
| using base::test::ios::WaitUntilConditionOrTimeout; |
| |
| namespace autofill { |
| |
| namespace { |
| |
| const base::FilePath::CharType kFeatureName[] = FILE_PATH_LITERAL("autofill"); |
| const base::FilePath::CharType kTestName[] = FILE_PATH_LITERAL("heuristics"); |
| |
| base::FilePath GetTestDataDir() { |
| base::FilePath dir; |
| base::PathService::Get(ios::DIR_TEST_DATA, &dir); |
| return dir; |
| } |
| |
| base::FilePath GetIOSInputDirectory() { |
| base::FilePath dir; |
| CHECK(base::PathService::Get(base::DIR_SRC_TEST_DATA_ROOT, &dir)); |
| |
| return dir.AppendASCII("components") |
| .AppendASCII("test") |
| .AppendASCII("data") |
| .Append(kFeatureName) |
| .Append(kTestName) |
| .AppendASCII("input"); |
| } |
| |
| base::FilePath GetIOSOutputDirectory() { |
| base::FilePath dir; |
| CHECK(base::PathService::Get(base::DIR_SRC_TEST_DATA_ROOT, &dir)); |
| |
| return dir.AppendASCII("components") |
| .AppendASCII("test") |
| .AppendASCII("data") |
| .Append(kFeatureName) |
| .Append(kTestName) |
| .AppendASCII("output"); |
| } |
| |
| const std::vector<base::FilePath> GetTestFiles() { |
| base::FilePath dir(GetIOSInputDirectory()); |
| std::string input_list_string; |
| if (!base::ReadFileToString(dir.AppendASCII("autofill_test_files"), |
| &input_list_string)) { |
| return {}; |
| } |
| std::vector<base::FilePath> result; |
| for (std::string_view piece : |
| base::SplitStringPiece(input_list_string, "\n", base::TRIM_WHITESPACE, |
| base::SPLIT_WANT_NONEMPTY)) { |
| result.push_back(dir.AppendASCII(piece)); |
| } |
| return result; |
| } |
| |
| } // namespace |
| |
| // Test fixture for verifying Autofill heuristics. Each input is an HTML |
| // file that contains one or more forms. The corresponding output file lists the |
| // heuristically detected type for each field. |
| // This is based on FormStructureBrowserTest from the Chromium Project. |
| // TODO(crbug.com/41015125): Unify the tests. |
| class FormStructureBrowserTest |
| : public PlatformTest, |
| public testing::DataDrivenTest, |
| public testing::WithParamInterface<base::FilePath> { |
| public: |
| FormStructureBrowserTest(const FormStructureBrowserTest&) = delete; |
| FormStructureBrowserTest& operator=(const FormStructureBrowserTest&) = delete; |
| |
| protected: |
| class TestAutofillClient : public ChromeAutofillClientIOS { |
| public: |
| using ChromeAutofillClientIOS::ChromeAutofillClientIOS; |
| AutofillCrowdsourcingManager* GetCrowdsourcingManager() override { |
| return nullptr; |
| } |
| }; |
| |
| class TestAutofillManager : public BrowserAutofillManager { |
| public: |
| explicit TestAutofillManager(AutofillDriverIOS* driver) |
| : BrowserAutofillManager(driver, "en-US") {} |
| |
| TestAutofillManagerWaiter& waiter() { return waiter_; } |
| |
| private: |
| TestAutofillManagerWaiter waiter_{*this, |
| {AutofillManagerEvent::kFormsSeen}}; |
| }; |
| |
| FormStructureBrowserTest(); |
| ~FormStructureBrowserTest() override {} |
| |
| void SetUp() override; |
| void TearDown() override; |
| |
| bool LoadHtmlWithoutSubresourcesAndInitRendererIds(const std::string& html); |
| |
| // DataDrivenTest: |
| void GenerateResults(const std::string& input, std::string* output) override; |
| |
| // Serializes the given `forms` into a string. |
| std::string FormStructuresToString( |
| const std::map<FormGlobalId, std::unique_ptr<FormStructure>>& forms); |
| |
| web::WebState* web_state() const { return web_state_.get(); } |
| |
| web::ScopedTestingWebClient web_client_; |
| web::WebTaskEnvironment task_environment_; |
| std::unique_ptr<TestChromeBrowserState> browser_state_; |
| std::unique_ptr<web::WebState> web_state_; |
| std::unique_ptr<TestAutofillClient> autofill_client_; |
| AutofillAgent* autofill_agent_; |
| std::unique_ptr<TestAutofillManagerInjector<TestAutofillManager>> |
| autofill_manager_injector_; |
| FormSuggestionController* suggestion_controller_; |
| |
| private: |
| base::test::ScopedFeatureList feature_list_; |
| PasswordController* password_controller_; |
| }; |
| |
| FormStructureBrowserTest::FormStructureBrowserTest() |
| : DataDrivenTest(GetTestDataDir(), kFeatureName, kTestName), |
| web_client_(std::make_unique<ChromeWebClient>()) { |
| TestChromeBrowserState::Builder builder; |
| builder.AddTestingFactory( |
| IOSChromeProfilePasswordStoreFactory::GetInstance(), |
| base::BindRepeating(&password_manager::BuildPasswordStoreInterface< |
| web::BrowserState, |
| password_manager::MockPasswordStoreInterface>)); |
| builder.AddTestingFactory( |
| IOSUserEventServiceFactory::GetInstance(), |
| base::BindRepeating( |
| [](web::BrowserState*) -> std::unique_ptr<KeyedService> { |
| return std::make_unique<syncer::FakeUserEventService>(); |
| })); |
| browser_state_ = builder.Build(); |
| |
| web::WebState::CreateParams params(browser_state_.get()); |
| web_state_ = web::WebState::Create(params); |
| feature_list_.InitWithFeatures( |
| // Enabled |
| { |
| // TODO(crbug.com/40128551) Remove once launched. |
| autofill::features::kAutofillUseNewSectioningMethod, |
| // TODO(crbug.com/40160818) Remove once launched. |
| features::kAutofillEnableDependentLocalityParsing, |
| // TODO(crbug.com/40741721): Remove once shared labels are launched. |
| features::kAutofillEnableSupportForParsingWithSharedLabels, |
| // TODO(crbug.com/40158074) Remove once launched. |
| features::kAutofillParsingPatternProvider, |
| features::kAutofillPageLanguageDetection, |
| // TODO(crbug.com/40220393): Remove once launched. |
| features::kAutofillEnableSupportForPhoneNumberTrunkTypes, |
| features::kAutofillInferCountryCallingCode, |
| // TODO(crbug.com/40266396): Remove once launched. |
| features::kAutofillEnableExpirationDateImprovements, |
| // TODO(crbug.com/40279279): Clean up when launched. |
| features::kAutofillDefaultToCityAndNumber, |
| }, |
| // Disabled |
| { |
| // TODO(crbug.com/40220393): Remove once launched. |
| // This feature is part of the AutofillRefinedPhoneNumberTypes |
| // rollout. As it is not supported on iOS yet, it is disabled. |
| features::kAutofillConsiderPhoneNumberSeparatorsValidLabels, |
| // TODO(crbug.com/40222716): Remove once launched. This feature is |
| // disabled since it is not supported on iOS. |
| features::kAutofillAlwaysParsePlaceholders, |
| // TODO(crbug.com/40285735): Remove when/if launched. This feature |
| // changes default parsing behavior, so must be disabled to avoid |
| // fieldtrial_testing_config interference. |
| features::kAutofillEnableEmailHeuristicOnlyAddressForms, |
| }); |
| } |
| |
| void FormStructureBrowserTest::SetUp() { |
| PlatformTest::SetUp(); |
| |
| // Create a PasswordController instance that will handle set up for renderer |
| // ids. |
| password_controller_ = |
| [[PasswordController alloc] initWithWebState:web_state()]; |
| |
| // AddressNormalizerFactory must be initialized in a blocking allowed scoped. |
| // Initialize it now as it may DCHECK if it is initialized during the test. |
| AddressNormalizerFactory::GetInstance(); |
| |
| autofill_agent_ = |
| [[AutofillAgent alloc] initWithPrefService:browser_state_->GetPrefs() |
| webState:web_state()]; |
| suggestion_controller_ = |
| [[FormSuggestionController alloc] initWithWebState:web_state() |
| providers:@[ autofill_agent_ ]]; |
| |
| InfoBarManagerImpl::CreateForWebState(web_state()); |
| infobars::InfoBarManager* infobar_manager = |
| InfoBarManagerImpl::FromWebState(web_state()); |
| autofill_client_ = std::make_unique<TestAutofillClient>( |
| browser_state_.get(), web_state(), infobar_manager, autofill_agent_); |
| |
| std::string locale("en"); |
| autofill::AutofillDriverIOSFactory::CreateForWebState( |
| web_state(), autofill_client_.get(), /*autofill_agent=*/nil, locale); |
| |
| autofill_manager_injector_ = |
| std::make_unique<TestAutofillManagerInjector<TestAutofillManager>>( |
| web_state()); |
| } |
| |
| void FormStructureBrowserTest::TearDown() { |
| web::test::WaitForBackgroundTasks(); |
| web_state_.reset(); |
| } |
| |
| bool FormStructureBrowserTest::LoadHtmlWithoutSubresourcesAndInitRendererIds( |
| const std::string& html) { |
| if (!web::test::LoadHtmlWithoutSubresources(base::SysUTF8ToNSString(html), |
| web_state())) { |
| return false; |
| } |
| |
| autofill::FormUtilJavaScriptFeature* feature = |
| autofill::FormUtilJavaScriptFeature::GetInstance(); |
| return WaitUntilConditionOrTimeout(kWaitForJSCompletionTimeout, ^bool { |
| web::WebFramesManager* frames_manager = |
| feature->GetWebFramesManager(web_state()); |
| return frames_manager->GetMainWebFrame() != nullptr; |
| }); |
| } |
| |
| void FormStructureBrowserTest::GenerateResults(const std::string& input, |
| std::string* output) { |
| ASSERT_TRUE(LoadHtmlWithoutSubresourcesAndInitRendererIds(input)); |
| base::ThreadPoolInstance::Get()->FlushForTesting(); |
| TestAutofillManager* autofill_manager = |
| autofill_manager_injector_->GetForMainFrame(); |
| ASSERT_NE(nullptr, autofill_manager); |
| ASSERT_TRUE(autofill_manager->waiter().Wait(1)); |
| *output = FormStructuresToString(autofill_manager->form_structures()); |
| } |
| |
| std::string FormStructureBrowserTest::FormStructuresToString( |
| const std::map<FormGlobalId, std::unique_ptr<FormStructure>>& forms) { |
| std::vector<std::string> forms_string; |
| // The forms are sorted by their global ID, which should make the order |
| // deterministic. |
| for (const auto& form_kv : forms) { |
| std::string form_string; |
| const auto* form = form_kv.second.get(); |
| std::map<std::string, int> section_to_index; |
| for (const auto& field : *form) { |
| std::string name = base::UTF16ToUTF8(field->name()); |
| if (base::StartsWith(name, "gChrome~field~", |
| base::CompareCase::SENSITIVE)) { |
| // The name has been generated by iOS JavaScript. Output an empty name |
| // to have a behavior similar to other platforms. |
| name = ""; |
| } |
| std::string section = field->section().ToString(); |
| if (base::StartsWith(section, "gChrome~field~", |
| base::CompareCase::SENSITIVE)) { |
| // The name has been generated by iOS JavaScript. Output an empty name |
| // to have a behavior similar to other platforms. |
| size_t first_underscore = section.find_first_of('_'); |
| section = section.substr(first_underscore); |
| } |
| if (field->section().is_from_fieldidentifier()) { |
| // Normalize the section by replacing the unique but platform-dependent |
| // integers in `field->section` with consecutive unique integers. |
| // The section string is of the form "fieldname_id1_id2-suffix", where |
| // id1, id2 are platform-dependent and thus need to be substituted. |
| size_t last_underscore = section.find_last_of('_'); |
| size_t second_last_underscore = |
| section.find_last_of('_', last_underscore - 1); |
| int new_section_index = static_cast<int>(section_to_index.size() + 1); |
| int section_index = |
| section_to_index.insert(std::make_pair(section, new_section_index)) |
| .first->second; |
| if (second_last_underscore != std::string::npos) { |
| section = base::StringPrintf( |
| "%s%d", section.substr(0, second_last_underscore + 1).c_str(), |
| section_index); |
| } |
| } |
| form_string += base::StrCat({field->Type().ToStringView(), " | ", name, |
| " | ", base::UTF16ToUTF8(field->label()), |
| " | ", base::UTF16ToUTF8(field->value()), |
| " | ", section, "\n"}); |
| } |
| forms_string.push_back(form_string); |
| } |
| sort(forms_string.begin(), forms_string.end()); |
| return base::JoinString(forms_string, "\n"); |
| } |
| |
| namespace { |
| |
| // To disable a data driven test, please add the name of the test file |
| // (i.e., "NNN_some_site.html") as a literal to the initializer_list given |
| // to the failing_test_names constructor. |
| const auto& GetFailingTestNames() { |
| static std::set<std::string> failing_test_names{ |
| // TODO(crbug.com/40266699): These pages contains iframes. Until filling |
| // across iframes is also supported on iOS, iOS has has different |
| // expectations compared to non-iOS platforms. |
| "049_register_ebay.com.html", |
| "148_payment_dickblick.com.html", |
| // TODO(crbug.com/40229922): These pages contain labels which are only |
| // inferred by the label detection improvements that haven't been |
| // implemented on iOS. |
| "074_register_threadless.com.html", |
| "097_register_alaskaair.com.html", |
| "115_checkout_walgreens.com.html", |
| "116_cc_checkout_walgreens.com.html", |
| "150_checkout_venus.com_search_field.html", |
| }; |
| return failing_test_names; |
| } |
| |
| } // namespace |
| |
| // If disabling a test, prefer to add the name names of the specific test cases |
| // to GetFailingTestNames(), directly above, instead of renaming the test to |
| // DISABLED_DataDrivenHeuristics. |
| TEST_P(FormStructureBrowserTest, DataDrivenHeuristics) { |
| bool is_expected_to_pass = |
| !base::Contains(GetFailingTestNames(), GetParam().BaseName().value()); |
| RunOneDataDrivenTest(GetParam(), GetIOSOutputDirectory(), |
| is_expected_to_pass); |
| } |
| |
| INSTANTIATE_TEST_SUITE_P(AllForms, |
| FormStructureBrowserTest, |
| testing::ValuesIn(GetTestFiles())); |
| |
| } // namespace autofill |