chromium/ios/chrome/browser/autofill/model/form_structure_browsertest.mm

// 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 "components/autofill/core/browser/form_structure.h"

#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/heuristic_source.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/autofill/ui_bundled/chrome_autofill_client_ios.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/paths/paths.h"
#import "ios/chrome/browser/shared/model/profile/test/test_profile_ios.h"
#import "ios/chrome/browser/sync/model/ios_user_event_service_factory.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_ = std::move(builder).Build();

  web::WebState::CreateParams params(browser_state_.get());
  web_state_ = web::WebState::Create(params);
  feature_list_.InitWithFeatures(
      // Enabled
      {
          // TODO(crbug.com/40741721): Remove once shared labels are launched.
          features::kAutofillEnableSupportForParsingWithSharedLabels,
          features::kAutofillPageLanguageDetection,
          // TODO(crbug.com/40220393): Remove once launched.
          features::kAutofillEnableSupportForPhoneNumberTrunkTypes,
          features::kAutofillInferCountryCallingCode,
          // TODO(crbug.com/40266396): Remove once launched.
          features::kAutofillEnableExpirationDateImprovements,
      },
      // 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) {
  if (GetActiveHeuristicSource() != HeuristicSource::kLegacy) {
    GTEST_SKIP() << "DataDrivenHeuristics tests are only supported with legacy "
                    "parsing patterns";
  }
  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