chromium/components/autofill/ios/browser/autofill_xhr_sumission_detection_unittest.mm

// Copyright 2024 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#import <optional>
#import <set>

#import "base/ranges/algorithm.h"
#import "base/test/metrics/histogram_tester.h"
#import "base/test/scoped_feature_list.h"
#import "base/test/task_environment.h"
#import "base/time/time.h"
#import "components/autofill/core/browser/browser_autofill_manager.h"
#import "components/autofill/core/browser/test_autofill_client.h"
#import "components/autofill/core/common/field_data_manager.h"
#import "components/autofill/core/common/form_data.h"
#import "components/autofill/core/common/form_field_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_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/test_autofill_manager_injector.h"
#import "components/autofill/ios/common/field_data_manager_factory_ios.h"
#import "ios/web/public/test/fakes/fake_web_frame.h"
#import "ios/web/public/test/fakes/fake_web_frames_manager.h"
#import "ios/web/public/test/fakes/fake_web_state.h"
#import "testing/gtest/include/gtest/gtest.h"
#import "testing/platform_test.h"

using ::testing::ElementsAre;
using ::testing::Property;

namespace autofill {

class TestingAutofillManager : public BrowserAutofillManager {
 public:
  explicit TestingAutofillManager(AutofillDriverIOS* driver)
      : BrowserAutofillManager(driver, "en-US") {}

  void OnFormSubmitted(const FormData& form,
                       const bool known_success,
                       const mojom::SubmissionSource source) override {
    submitted_form_ = form;
    BrowserAutofillManager::OnFormSubmitted(form, known_success, source);
  }

  const std::optional<FormData>& submitted_form() const {
    return submitted_form_;
  }

 private:
  std::optional<FormData> submitted_form_ = std::nullopt;
};

// Test fixture for validating async form submission detection logic in
// AutofillDriverIOS.
class AutofillXHRSubmissionDetectionTest : public PlatformTest {
 public:
  void SetUp() override {
    PlatformTest::SetUp();

    histogram_tester_ = std::make_unique<base::HistogramTester>();

    // Setup fake frames injection in the content world used by Autofill
    // features.
    auto web_frames_manager = std::make_unique<web::FakeWebFramesManager>();
    web_frames_manager_ = web_frames_manager.get();
    web::ContentWorld content_world =
        AutofillJavaScriptFeature::GetInstance()->GetSupportedContentWorld();
    web_state_.SetWebFramesManager(content_world,
                                   std::move(web_frames_manager));

    // Driver factory needs to exist before any call to
    // `AutofillDriverIOS::FromWebStateAndWebFrame`, or we crash.
    autofill::AutofillDriverIOSFactory::CreateForWebState(
        &web_state_, &autofill_client_, /*bridge=*/nil,
        /*locale=*/"en");

    // Replace AutofillManager with the test implementation.
    autofill_manager_injector_ =
        std::make_unique<TestAutofillManagerInjector<TestingAutofillManager>>(
            &web_state_);

    // Inject a fake main frame.
    auto main_frame =
        web::FakeWebFrame::CreateMainWebFrame(GURL("https://example.com"));

    web_frames_manager_->AddWebFrame(std::move(main_frame));
  }

  web::WebFrame* main_frame() { return web_frames_manager_->GetMainWebFrame(); }

  AutofillDriverIOS* main_frame_driver() {
    return AutofillDriverIOS::FromWebStateAndWebFrame(&web_state_,
                                                      main_frame());
  }

  TestingAutofillManager& main_frame_manager() {
    return static_cast<TestingAutofillManager&>(
        main_frame_driver()->GetAutofillManager());
  }

  base::test::TaskEnvironment task_environment_;
  autofill::TestAutofillClient autofill_client_;
  web::FakeWebState web_state_;
  raw_ptr<web::FakeWebFramesManager> web_frames_manager_;
  std::unique_ptr<TestAutofillManagerInjector<TestingAutofillManager>>
      autofill_manager_injector_;
  std::unique_ptr<base::HistogramTester> histogram_tester_;
};

// Tests that typing values in forms and removing them triggers a submission
// detection.
TEST_F(AutofillXHRSubmissionDetectionTest,
       SubmissionDetectedAfterLastInteractedFormRemoved) {
  // Create two dummy FormData to simulate interaction and removal.
  FormData form_data1;
  form_data1.set_renderer_id(FormRendererId(1));
  FormFieldData form_field_data1;
  form_field_data1.set_renderer_id(FieldRendererId(2));
  form_field_data1.set_host_form_id(form_data1.renderer_id());
  form_data1.set_fields({form_field_data1});

  FormData form_data2;
  form_data2.set_renderer_id(FormRendererId(3));
  FormFieldData form_field_data2;
  form_field_data2.set_renderer_id(FieldRendererId(4));
  form_field_data2.set_host_form_id(form_data2.renderer_id());
  FormFieldData form_field_data3;
  form_field_data3.set_renderer_id(FieldRendererId(5));
  form_field_data3.set_host_form_id(form_data2.renderer_id());

  // Simulate typing in the first form.
  auto* autofill_driver = main_frame_driver();
  ASSERT_TRUE(autofill_driver);
  autofill_driver->TextFieldDidChange(form_data1, form_field_data1.global_id(),
                                      base::TimeTicks::Now());
  // Simulate typing in the first field of the second form.
  form_field_data2.set_value(u"value2");
  form_data2.set_fields({form_field_data2, form_field_data3});
  autofill_driver->TextFieldDidChange(form_data2, form_field_data2.global_id(),
                                      base::TimeTicks::Now());

  // Simulate typing on the other field of the second form.
  form_field_data3.set_value(u"value3");
  form_data2.set_fields({form_field_data2, form_field_data3});
  autofill_driver->TextFieldDidChange(form_data2, form_field_data3.global_id(),
                                      base::TimeTicks::Now());
  // Simulate forms removal.
  autofill_driver->FormsRemoved(
      /*removed_forms=*/{form_data1.renderer_id(), form_data2.renderer_id()},
      /*removed_unowned_fields=*/{});

  // Validate that last interacted form was detected as submitted and sent to
  // AutofillManager.
  auto& autofill_manager = main_frame_manager();
  ASSERT_TRUE(autofill_manager.submitted_form());
  // Check that the submitted form has the values "typed" in each field.
  EXPECT_TRUE(
      FormData::DeepEqual(*autofill_manager.submitted_form(), form_data2));
  EXPECT_THAT(autofill_manager.submitted_form()->fields(),
              ElementsAre(Property(&FormFieldData::value, u"value2"),
                          Property(&FormFieldData::value, u"value3")));

  histogram_tester_->ExpectUniqueSample(
      /*name=*/kAutofillSubmissionDetectionSourceHistogram,
      /*sample=*/mojom::SubmissionSource::XHR_SUCCEEDED,
      /*expected_bucket_count=*/1);
  histogram_tester_->ExpectUniqueSample(
      /*name=*/kFormSubmissionAfterFormRemovalHistogram, /*sample=*/true,
      /*expected_bucket_count=*/1);
  histogram_tester_->ExpectUniqueSample(
      /*name=*/kFormlessSubmissionAfterFormRemovalHistogram, /*sample=*/false,
      /*expected_bucket_count=*/1);
  histogram_tester_->ExpectUniqueSample(
      /*name=*/kFormRemovalRemovedFormsHistogram, /*sample=*/2,
      /*expected_bucket_count=*/1);
  histogram_tester_->ExpectUniqueSample(
      /*name=*/kFormRemovalRemovedUnownedFieldsHistogram, /*sample=*/0,
      /*expected_bucket_count=*/1);
}

// Tests that autofilling a form and removing it triggers a submission
// detection.
TEST_F(AutofillXHRSubmissionDetectionTest,
       SubmissionDetectedAfterLastAutofilledFormRemoved) {
  // Create a dummy FormData to simulate interaction and removal.
  FormData form_data;
  form_data.set_renderer_id(FormRendererId(1));
  FormFieldData form_field_data;
  form_field_data.set_renderer_id(FieldRendererId(2));
  form_field_data.set_host_form_id(form_data.renderer_id());
  form_field_data.set_value(u"value");
  form_data.set_fields({form_field_data});

  // Simulate autofilling the form.
  auto* autofill_driver = main_frame_driver();
  ASSERT_TRUE(autofill_driver);
  autofill_driver->DidFillAutofillFormData(form_data, base::TimeTicks::Now());

  // Simulate form removal.
  autofill_driver->FormsRemoved(/*removed_forms=*/{form_data.renderer_id()},
                                /*removed_unowned_fields=*/{});

  // Validate that the form was detected as submitted and sent to
  // AutofillManager.
  auto& autofill_manager = main_frame_manager();
  ASSERT_TRUE(autofill_manager.submitted_form());
  EXPECT_TRUE(
      FormData::DeepEqual(*autofill_manager.submitted_form(), form_data));
  EXPECT_THAT(autofill_manager.submitted_form()->fields(),
              ElementsAre(Property(&FormFieldData::value, u"value")));

  histogram_tester_->ExpectUniqueSample(
      /*name=*/kAutofillSubmissionDetectionSourceHistogram,
      /*sample=*/mojom::SubmissionSource::XHR_SUCCEEDED,
      /*expected_count=*/1);
  histogram_tester_->ExpectUniqueSample(
      /*name=*/kFormSubmissionAfterFormRemovalHistogram, /*sample=*/true,
      /*expected_bucket_count=*/1);
  histogram_tester_->ExpectUniqueSample(
      /*name=*/kFormlessSubmissionAfterFormRemovalHistogram, /*sample=*/false,
      /*expected_bucket_count=*/1);
  histogram_tester_->ExpectUniqueSample(
      /*name=*/kFormRemovalRemovedFormsHistogram, /*sample=*/1,
      /*expected_bucket_count=*/1);
  histogram_tester_->ExpectUniqueSample(
      /*name=*/kFormRemovalRemovedUnownedFieldsHistogram, /*sample=*/0,
      /*expected_bucket_count=*/1);
}

// Tests that typing values in formless fields and then removing the last
// interacted one triggers a submission detection.
TEST_F(AutofillXHRSubmissionDetectionTest,
       SubmissionDetectedAfterFormlessFieldsRemoved) {
  // Create a dummy formless FormData to simulate interaction and removal.
  FormData form_data;
  // Explicitly setting "formless form" renderer id for clarity.
  form_data.set_renderer_id(FormRendererId(0));
  // Create two fields.
  FormFieldData form_field_data1;
  form_field_data1.set_renderer_id(FieldRendererId(1));
  form_field_data1.set_host_form_id(form_data.renderer_id());
  FormFieldData form_field_data2;
  form_field_data2.set_renderer_id(FieldRendererId(2));
  form_field_data2.set_host_form_id(form_data.renderer_id());
  form_data.set_fields({form_field_data1, form_field_data2});

  // Simulate the user updating the first field.
  auto* autofill_driver = main_frame_driver();
  ASSERT_TRUE(autofill_driver);
  form_field_data1.set_value(u"value1");
  form_data.set_fields({form_field_data1, form_field_data2});
  autofill_driver->TextFieldDidChange(form_data, form_field_data1.global_id(),
                                      base::TimeTicks::Now());

  // Simulate the user updating the second field.
  form_field_data2.set_value(u"value2");
  form_data.set_fields({form_field_data1, form_field_data2});
  autofill_driver->TextFieldDidChange(form_data, form_field_data2.global_id(),
                                      base::TimeTicks::Now());

  // Simulate the removal of the first formless field.
  autofill_driver->FormsRemoved(
      /*removed_forms=*/{},
      /*removed_unowned_fields=*/{form_field_data1.renderer_id()});

  // No submission detected because the first field did not receive the last
  // user interaction.
  auto& autofill_manager = main_frame_manager();
  EXPECT_FALSE(autofill_manager.submitted_form());

  histogram_tester_->ExpectTotalCount(
      /*name=*/kAutofillSubmissionDetectionSourceHistogram,
      /*expected_count=*/0);
  histogram_tester_->ExpectUniqueSample(
      /*name=*/kFormSubmissionAfterFormRemovalHistogram, /*sample=*/false,
      /*expected_bucket_count=*/1);
  histogram_tester_->ExpectTotalCount(
      /*name=*/kFormlessSubmissionAfterFormRemovalHistogram,
      /*expected_count=*/0);
  histogram_tester_->ExpectUniqueSample(
      /*name=*/kFormRemovalRemovedFormsHistogram, /*sample=*/0,
      /*expected_bucket_count=*/1);
  histogram_tester_->ExpectUniqueSample(
      /*name=*/kFormRemovalRemovedUnownedFieldsHistogram, /*sample=*/1,
      /*expected_bucket_count=*/1);

  // Reset histogram stats and measure second removal event.
  histogram_tester_ = std::make_unique<base::HistogramTester>();

  // Simulate the removal of the second field.
  autofill_driver->FormsRemoved(
      /*removed_forms=*/{},
      /*removed_unowned_fields=*/{form_field_data2.renderer_id()});

  // Validate that the formless form was detected as submitted and sent to
  // AutofillManager.
  ASSERT_TRUE(autofill_manager.submitted_form());
  EXPECT_TRUE(
      FormData::DeepEqual(*autofill_manager.submitted_form(), form_data));
  EXPECT_THAT(autofill_manager.submitted_form()->fields(),
              ElementsAre(Property(&FormFieldData::value, u"value1"),
                          Property(&FormFieldData::value, u"value2")));

  histogram_tester_->ExpectUniqueSample(
      /*name=*/kAutofillSubmissionDetectionSourceHistogram,
      /*sample=*/mojom::SubmissionSource::XHR_SUCCEEDED,
      /*expected_count=*/1);
  histogram_tester_->ExpectUniqueSample(
      /*name=*/kFormSubmissionAfterFormRemovalHistogram, /*sample=*/true,
      /*expected_bucket_count=*/1);
  histogram_tester_->ExpectUniqueSample(
      /*name=*/kFormlessSubmissionAfterFormRemovalHistogram, /*sample=*/true,
      /*expected_bucket_count=*/1);
  histogram_tester_->ExpectUniqueSample(
      /*name=*/kFormRemovalRemovedFormsHistogram, /*sample=*/0,
      /*expected_bucket_count=*/1);
  histogram_tester_->ExpectUniqueSample(
      /*name=*/kFormRemovalRemovedUnownedFieldsHistogram, /*sample=*/1,
      /*expected_bucket_count=*/1);
}

// Tests that no submission is detected if a form is removed without user
// interactions with it.
TEST_F(AutofillXHRSubmissionDetectionTest,
       NoSubmissionDetectedAfterFormRemovedWithoutInteractions) {
  // Create a dummy FormData to simulate removal.
  FormData form_data;
  form_data.set_renderer_id(FormRendererId(1));
  FormFieldData form_field_data;
  form_field_data.set_renderer_id(FieldRendererId(2));
  form_field_data.set_host_form_id(form_data.renderer_id());
  form_data.set_fields({form_field_data});

  auto* autofill_driver = main_frame_driver();
  ASSERT_TRUE(autofill_driver);
  // Simulate form removal without interactions.
  autofill_driver->FormsRemoved(/*removed_forms=*/{form_data.renderer_id()},
                                /*removed_unowned_fields=*/{});

  // Validate that no form was sent to AutfillManager as submitted.
  auto& autofill_manager = main_frame_manager();
  EXPECT_FALSE(autofill_manager.submitted_form());

  histogram_tester_->ExpectTotalCount(
      /*name=*/kAutofillSubmissionDetectionSourceHistogram,
      /*expected_count=*/0);
  histogram_tester_->ExpectUniqueSample(
      /*name=*/kFormSubmissionAfterFormRemovalHistogram, /*sample=*/false,
      /*expected_bucket_count=*/1);
  histogram_tester_->ExpectTotalCount(
      /*name=*/kFormlessSubmissionAfterFormRemovalHistogram,
      /*expected_count=*/0);
  histogram_tester_->ExpectUniqueSample(
      /*name=*/kFormRemovalRemovedFormsHistogram, /*sample=*/1,
      /*expected_bucket_count=*/1);
  histogram_tester_->ExpectUniqueSample(
      /*name=*/kFormRemovalRemovedUnownedFieldsHistogram, /*sample=*/0,
      /*expected_bucket_count=*/1);
}

// Tests that a removed form detected as submitted is updated with data from
// FieldDataManager.
TEST_F(AutofillXHRSubmissionDetectionTest,
       SubmittedFormUpdatedFromFieldDataManager) {
  // Create a dummy FormData to simulate interaction and removal.
  FormData form_data;
  form_data.set_renderer_id(FormRendererId(1));
  FormFieldData form_field_data;
  form_field_data.set_renderer_id(FieldRendererId(2));
  form_field_data.set_host_form_id(form_data.renderer_id());
  form_field_data.set_value(u"value1");
  form_data.set_fields({form_field_data});

  // Simulate the user updating the form field.
  auto* autofill_driver = main_frame_driver();
  ASSERT_TRUE(autofill_driver);
  autofill_driver->TextFieldDidChange(form_data, form_field_data.global_id(),
                                      base::TimeTicks::Now());

  // Update the form field in FieldDataManager.
  std::u16string data_manager_value = u"value2";
  auto data_manager_mask = FieldPropertiesFlags::kUserTyped;
  FieldDataManager* fieldDataManager =
      autofill::FieldDataManagerFactoryIOS::FromWebFrame(main_frame());
  fieldDataManager->UpdateFieldDataMap(form_field_data.renderer_id(),
                                       data_manager_value, data_manager_mask);

  // Simulate form removal.
  autofill_driver->FormsRemoved(/*removed_forms=*/{form_data.renderer_id()},
                                /*removed_unowned_fields=*/{});

  // Validate that form was detected as submitted and sent to
  // AutofillManager with the field data updated with the value in
  // FieldDataManager.
  auto& autofill_manager = main_frame_manager();
  ASSERT_TRUE(autofill_manager.submitted_form());
  EXPECT_TRUE(
      FormData::DeepEqual(form_data, *autofill_manager.submitted_form()));
  EXPECT_THAT(autofill_manager.submitted_form()->fields(),
              ElementsAre(Property(&FormFieldData::value, u"value2")));
}

}  // namespace autofill