// Copyright 2018 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/ios/form_util/form_activity_tab_helper.h"

#import <optional>

#import "base/feature_list.h"
#import "base/strings/sys_string_conversions.h"
#import "base/test/bind.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/with_feature_override.h"
#import "base/time/time.h"
#import "base/unguessable_token.h"
#import "components/autofill/core/common/autofill_features.h"
#import "components/autofill/core/common/autofill_test_utils.h"
#import "components/autofill/core/common/form_data.h"
#import "components/autofill/core/common/form_field_data.h"
#import "components/autofill/core/common/unique_ids.h"
#import "components/autofill/ios/browser/autofill_java_script_feature.h"
#import "components/autofill/ios/browser/autofill_util.h"
#import "components/autofill/ios/common/autofill_optimization_features.h"
#import "components/autofill/ios/common/features.h"
#import "components/autofill/ios/common/javascript_feature_util.h"
#import "components/autofill/ios/form_util/autofill_form_features_java_script_feature.h"
#import "components/autofill/ios/form_util/autofill_test_with_web_state.h"
#import "components/autofill/ios/form_util/form_activity_observer.h"
#import "components/autofill/ios/form_util/form_handlers_java_script_feature.h"
#import "components/autofill/ios/form_util/programmatic_form_submission_handler_java_script_feature.h"
#import "components/autofill/ios/form_util/renderer_id_test_util.h"
#import "components/autofill/ios/form_util/test_form_activity_observer.h"
#import "ios/web/public/js_messaging/java_script_feature.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/fakes/fake_web_client.h"
#import "ios/web/public/test/fakes/fake_web_state_observer_util.h"
#import "ios/web/public/test/js_test_util.h"
#import "ios/web/public/test/web_test_with_web_state.h"

namespace autofill {

namespace {

using base::test::WithFeatureOverride;
using base::test::ios::kWaitForJSCompletionTimeout;
using base::test::ios::WaitUntilConditionOrTimeout;
using test::FormDataEq;
using test::kTrackFormMutationsDelayInMs;
using test::WithoutUnserializedData;
using ::testing::Each;
using ::testing::ElementsAre;
using ::testing::Field;
using ::testing::IsEmpty;
using ::testing::IsTrue;
using ::testing::Not;
using ::testing::StrEq;
using ::testing::UnorderedElementsAre;
using ::testing::VariantWith;
using web::JavaScriptFeature;
using web::WebFrame;

// HTML containing one form with a text field and a submit button.
constexpr NSString* kTestHTMLForm = @"<form name='form-name'>"
                                     "<input type='text' id='text'/>"
                                     "<input type='submit' id='button'/>"
                                     "</form>";
// HTML containing one form wrapping two iframe elements.
constexpr NSString* kTestHTMLFormWithIframes =
    @"<form name='form-name'>"
     "<iframe id='frame1' srcdoc='<form><input type=\"text\" name=\"username\" "
     "id=\"username\"></form>'></iframe>"
     "<iframe id='frame2' srcdoc='<form><input type=\"password\" "
     "name=\"password\" id=\"password\"></form>'></iframe>"
     "</form>";

// Returns the `FormData` representation of the form in `kTestHTMLForm`.
[[nodiscard]] FormData BuildTestFormData(std::string frame_id) {
  std::optional<base::UnguessableToken> host_frame =
      DeserializeJavaScriptFrameId(frame_id);
  GURL url = GURL("https://chromium.test/");

  FormData test_form_data;
  test_form_data.set_name(u"form-name");
  test_form_data.set_main_frame_origin(url::Origin::Create(url));
  test_form_data.set_url(url);
  test_form_data.set_action(url);
  test_form_data.set_name_attribute(u"form-name");
  test_form_data.set_renderer_id(FormRendererId(1));
  test_form_data.set_host_frame(LocalFrameToken(*host_frame));

  FormFieldData test_field_data;
  test_field_data.set_name(u"text");
  test_field_data.set_origin(url::Origin::Create(url));
  test_field_data.set_form_control_type(FormControlType::kInputText);
  test_field_data.set_host_frame(LocalFrameToken(*host_frame));
  test_field_data.set_host_form_id(test_form_data.renderer_id());
  test_field_data.set_host_form_signature(
      FormSignature(14616800528926333697ul));
  test_field_data.set_renderer_id(FieldRendererId(2));
  test_field_data.set_id_attribute(u"text");
  test_field_data.set_max_length(FormFieldData::kDefaultMaxLength);

  test_form_data.set_fields({test_field_data});

  return test_form_data;
}

}  // namespace

// Tests fixture for FormActivityTabHelper class.
class FormActivityTabHelperTest : public AutofillTestWithWebState {
 public:
  FormActivityTabHelperTest()
      : AutofillTestWithWebState(std::make_unique<web::FakeWebClient>()) {
    web::FakeWebClient* web_client =
        static_cast<web::FakeWebClient*>(GetWebClient());
    std::vector<JavaScriptFeature*> features = {
        FormHandlersJavaScriptFeature::GetInstance(),
        AutofillJavaScriptFeature::GetInstance(),
        ProgrammaticFormSubmissionHandlerJavaScriptFeature::GetInstance(),
    };
    web_client->SetJavaScriptFeatures(features);
  }

  void SetUp() override {
    web::WebTestWithWebState::SetUp();

    FormActivityTabHelper* tab_helper =
        FormActivityTabHelper::GetOrCreateForWebState(web_state());
    observer_ = std::make_unique<TestFormActivityObserver>(web_state());
    tab_helper->AddObserver(observer_.get());
  }

  void TearDown() override {
    FormActivityTabHelper* tab_helper =
        FormActivityTabHelper::GetOrCreateForWebState(web_state());
    tab_helper->RemoveObserver(observer_.get());
    web::WebTestWithWebState::TearDown();
  }

 protected:
  WebFrame* WaitForMainFrame(web::ContentWorld content_world) {
    __block WebFrame* main_frame = nullptr;
    EXPECT_TRUE(WaitUntilConditionOrTimeout(kWaitForJSCompletionTimeout, ^bool {
      web::WebFramesManager* frames_manager =
          web_state()->GetWebFramesManager(content_world);
      main_frame = frames_manager->GetMainWebFrame();
      return main_frame != nullptr;
    }));
    return main_frame;
  }

  WebFrame* WaitForMainFrame() {
    return WaitForMainFrame(ContentWorldForAutofillJavascriptFeatures());
  }

  // Verifies the form activity params received after a form mutation.
  void ValidateParamsAfterFormChangedEvent(const FormActivityParams& params) {
    FormActivityParams expected_activity_params;
    expected_activity_params.frame_id = WaitForMainFrame()->GetFrameId();
    expected_activity_params.is_main_frame = true;
    expected_activity_params.type =
        FormActivityParams::ActivityType::kFormChanged;

    EXPECT_EQ(params, expected_activity_params);
  }

  // Returns the value of `__gCrHasBeenPassword` symbol for the specified HTML
  // element
  id GetHasBeenPasswordForElement(NSString* element_id) {
    NSString* script =
        [NSString stringWithFormat:@"document.getElementById('%@')[Symbol.for('"
                                   @"__gCrHasBeenPassword')]",
                                   element_id];
    return ExecuteJavaScript(script);
  }

  base::HistogramTester histogram_tester_;
  std::unique_ptr<TestFormActivityObserver> observer_;
};

// Tests that a password input element added dynamically is marked as has been
// password.
TEST_F(FormActivityTabHelperTest, TestPasswordSymbolSetOnNewElement) {
  base::test::ScopedFeatureList feature_list(kAutofillTrackPasswordFieldsIos);

  // Load an empty page so main_frame exists.
  LoadHtml(@"<div />");

  web::WebFramesManager* frames_manager =
      AutofillJavaScriptFeature::GetInstance()->GetWebFramesManager(
          web_state());
  web::WebFrame* main_frame = frames_manager->GetMainWebFrame();
  ASSERT_TRUE(main_frame);

  FormHandlersJavaScriptFeature::GetInstance()->TrackFormMutations(
      main_frame, /*mutation_tracking_delay=*/200);

  // Adds a password input in the page to see if the mutation callback
  // will set the attribute correctly.
  ExecuteJavaScript(
      @"document.body.innerHTML = '<input type=\"password\" id=\"pw\"/>';");
  ExecuteJavaScript(
      @"__gCrWeb.getRegisteredApi('autofill').getFunction('extractForms')("
      @"false);");

  EXPECT_TRUE(WaitUntilConditionOrTimeout(kWaitForJSCompletionTimeout, ^bool {
    return [GetHasBeenPasswordForElement(@"pw") isEqual:@YES];
  }));
}

// Tests that an input is marked as has been password when the type changes to
// password.
TEST_F(FormActivityTabHelperTest, TestPasswordSymbolSetOnTypeChange) {
  base::test::ScopedFeatureList feature_list(kAutofillTrackPasswordFieldsIos);

  LoadHtml(@"<input type='text' id='user'/>"
            "<input type='email' id='email'/>"
            "<input type='password' id='pw'/>");

  web::WebFramesManager* frames_manager =
      AutofillJavaScriptFeature::GetInstance()->GetWebFramesManager(
          web_state());
  web::WebFrame* main_frame = frames_manager->GetMainWebFrame();
  ASSERT_TRUE(main_frame);

  FormHandlersJavaScriptFeature::GetInstance()->TrackFormMutations(
      main_frame, /*mutation_tracking_delay=*/200);
  ExecuteJavaScript(
      @"__gCrWeb.getRegisteredApi('autofill').getFunction('extractForms')("
      @"false);");

  // Loading the page should have set the attribute since the input is a
  // password.
  EXPECT_TRUE(WaitUntilConditionOrTimeout(kWaitForJSCompletionTimeout, ^bool {
    return [GetHasBeenPasswordForElement(@"pw") isEqual:@YES];
  }));

  // Change the type to text to simulate `Show Password`.
  ExecuteJavaScript(@"document.getElementById('pw').type = 'text';"
                     "document.getElementById('email').remove();");

  // The input still have the attribute set correctly.
  EXPECT_TRUE(WaitUntilConditionOrTimeout(kWaitForJSCompletionTimeout, ^bool {
    return [GetHasBeenPasswordForElement(@"pw") isEqual:@YES];
  }));
}

// Tests that a password input is not marked as has been password if the feature
// is disabled.
TEST_F(FormActivityTabHelperTest, TestPasswordSymbolFeatureDisabled) {
  base::test::ScopedFeatureList feature_list;
  feature_list.InitAndDisableFeature(kAutofillTrackPasswordFieldsIos);

  LoadHtml(@"<input type='password' id='pw'/>");

  web::WebFramesManager* frames_manager =
      AutofillJavaScriptFeature::GetInstance()->GetWebFramesManager(
          web_state());
  web::WebFrame* main_frame = frames_manager->GetMainWebFrame();
  ASSERT_TRUE(main_frame);

  FormHandlersJavaScriptFeature::GetInstance()->TrackFormMutations(
      main_frame, /*mutation_tracking_delay=*/200);
  ExecuteJavaScript(
      @"__gCrWeb.getRegisteredApi('autofill').getFunction('extractForms')("
      @"false);");

  // The Has Been Password symbol is not set since the feature is disabled
  EXPECT_TRUE(WaitUntilConditionOrTimeout(kWaitForJSCompletionTimeout, ^bool {
    return GetHasBeenPasswordForElement(@"pw") == nil;
  }));
}

// Tests that observer is called on form submission using submit control.
TEST_F(FormActivityTabHelperTest, TestObserverDocumentSubmitted) {
  LoadHtml(kTestHTMLForm);

  WebFrame* main_frame = WaitForMainFrame();
  ASSERT_TRUE(main_frame);

  ASSERT_FALSE(observer_->submit_document_info());

  FormData test_form_data = BuildTestFormData(main_frame->GetFrameId());

  ExecuteJavaScript(@"document.getElementById('button').click();");
  ASSERT_TRUE(observer_->submit_document_info());
  EXPECT_EQ(main_frame->GetFrameId(),
            observer_->submit_document_info()->sender_frame_id);
  EXPECT_THAT(
      WithoutUnserializedData(observer_->submit_document_info()->form_data),
      FormDataEq(WithoutUnserializedData(test_form_data)));

  EXPECT_FALSE(observer_->submit_document_info()->has_user_gesture);

  histogram_tester_.ExpectUniqueSample(kProgrammaticFormSubmissionHistogram,
                                       false, 1);

  histogram_tester_.ExpectUniqueSample(kFormSubmissionOutcomeHistogram, 0, 1);
  histogram_tester_.ExpectTotalCount(kInvalidSubmittedFormReasonHistogram, 0);

  // Verify that there isn't any form activity metric recorded as the form
  // submit signals aren't covered.
  histogram_tester_.ExpectTotalCount("Autofill.iOS.FormActivity.DropCount", 0);
  histogram_tester_.ExpectTotalCount("Autofill.iOS.FormActivity.SendCount", 0);
  histogram_tester_.ExpectTotalCount("Autofill.iOS.FormActivity.SendRatio", 0);
}

// Tests that submit event from same-origin iframe correctly delivered to
// WebStateObserver.
TEST_F(FormActivityTabHelperTest, FormSubmittedFromSameOriginIFrame) {
  LoadHtml(@"<iframe id='frame1'></iframe>");
  ExecuteJavaScript(
      @"document.getElementById('frame1').contentDocument.body.innerHTML = "
       "'<form id=\"form1\">"
       "<input type=\"password\" name=\"password\" id=\"id2\">"
       "<input type=\"submit\" id=\"submit_input\"/>"
       "</form>'");
  ExecuteJavaScript(
      @"document.getElementById('frame1').contentDocument.getElementById('"
      @"submit_input').click();");
  TestSubmitDocumentInfo* info = observer_->submit_document_info();
  ASSERT_TRUE(info);
  EXPECT_EQ(u"form1", info->form_data.name());
}

// Tests that observer is called on form activity (input event).
// TODO(crbug.com/40902648): Disabled test due to bot failure. Re-enable when
// fixed.
TEST_F(FormActivityTabHelperTest,
       DISABLED_TestObserverFormActivityFrameMessaging) {
  LoadHtml(@"<form name='form-name'>"
            "<input type='input' name='field-name' id='fieldid'/>"
            "</form>");

  WebFrame* main_frame = WaitForMainFrame();
  ASSERT_TRUE(main_frame);

  ASSERT_FALSE(observer_->form_activity_info());
  // First call will set document.activeElement (which is usually set by user
  // action. Second call will trigger the message.
  ExecuteJavaScript(@"document.getElementById('fieldid').focus();");
  ASSERT_FALSE(observer_->form_activity_info());
  ExecuteJavaScript(@"document.getElementById('fieldid').focus();");
  ASSERT_TRUE(base::test::ios::WaitUntilConditionOrTimeout(
      base::test::ios::kWaitForJSCompletionTimeout, ^bool {
        return observer_->form_activity_info() != nullptr;
      }));
  EXPECT_EQ(main_frame->GetFrameId(),
            observer_->form_activity_info()->sender_frame_id);
  EXPECT_EQ("form-name",
            observer_->form_activity_info()->form_activity.form_name);
  EXPECT_EQ(FormActivityParams::FieldType::kText,
            observer_->form_activity_info()->form_activity.field_type);
  EXPECT_EQ(FormActivityParams::ActivityType::kFocus,
            observer_->form_activity_info()->form_activity.type);
  EXPECT_EQ("", observer_->form_activity_info()->form_activity.value);
  EXPECT_TRUE(observer_->form_activity_info()->form_activity.is_main_frame);
  EXPECT_TRUE(observer_->form_activity_info()->form_activity.has_user_gesture);
}

// Tests that keyup event is not delivered to WebStateObserver if the element is
// not focused.
TEST_F(FormActivityTabHelperTest, KeyUpEventNotFocused) {
  LoadHtml(@"<input id='test'/>");
  ASSERT_FALSE(observer_->form_activity_info());
  ExecuteJavaScript(@"var e = document.getElementById('test');"
                     "var ev = new KeyboardEvent('keyup', {bubbles:true});"
                     "e.dispatchEvent(ev);");

  // Pump the run loop to get the renderer response.
  WaitForBackgroundTasks();

  TestFormActivityInfo* info = observer_->form_activity_info();
  ASSERT_FALSE(info);
}

// Tests that focus event correctly delivered to WebStateObserver.
TEST_F(FormActivityTabHelperTest, FocusMainFrame) {
  LoadHtml(@"<form>"
            "<input type='text' name='username' id='id1'>"
            "<input type='password' name='password' id='id2'>"
            "</form>");
  ASSERT_FALSE(observer_->form_activity_info());
  ExecuteJavaScript(@"var el = document.getElementById('id1');"
                    @"el.focus();"
                    @"el.dispatchEvent(new Event('focus', {bubbles: true}));");
  TestFormActivityObserver* block_observer = observer_.get();
  ASSERT_TRUE(WaitUntilConditionOrTimeout(kWaitForJSCompletionTimeout, ^{
    return block_observer->form_activity_info() != nullptr;
  }));
  TestFormActivityInfo* info = observer_->form_activity_info();
  ASSERT_TRUE(info);
  EXPECT_EQ(FormActivityParams::ActivityType::kFocus, info->form_activity.type);
  EXPECT_FALSE(info->form_activity.input_missing);
}

// Tests that focus event from same-origin iframe correctly delivered to
// WebStateObserver.
TEST_F(FormActivityTabHelperTest, FocusSameOriginIFrame) {
  LoadHtml(@"<iframe id='frame1'></iframe>");
  ExecuteJavaScript(
      @"document.getElementById('frame1').contentDocument.body.innerHTML = "
       "'<form>"
       "<input type=\"text\" name=\"username\" id=\"id1\">"
       "<input type=\"password\" name=\"password\" id=\"id2\">"
       "</form>'");

  ExecuteJavaScript(
      @"var doc = document.getElementById('frame1').contentDocument;"
      @"var el = doc.getElementById('id1');"
      @"el.focus();"
      @"el.dispatchEvent(new Event('focus', {bubbles: true}));");
  TestFormActivityObserver* block_observer = observer_.get();
  ASSERT_TRUE(WaitUntilConditionOrTimeout(kWaitForJSCompletionTimeout, ^{
    return block_observer->form_activity_info() != nullptr;
  }));
  TestFormActivityInfo* info = observer_->form_activity_info();
  ASSERT_TRUE(info);
  EXPECT_EQ(FormActivityParams::ActivityType::kFocus, info->form_activity.type);
  EXPECT_FALSE(info->form_activity.input_missing);
}

// Tests that a new element that contains 'form' in the tag name does not
// trigger a form_changed event.
TEST_F(FormActivityTabHelperTest, AddCustomElement) {
  LoadHtml(@"<body></body>");
  web::WebFrame* main_frame = WaitForMainFrame();
  ASSERT_TRUE(main_frame);
  TrackFormMutations(main_frame);

  ExecuteJavaScript(@"var form = document.createElement('my-form');"
                    @"document.body.appendChild(form);");

  // Check that no activity is observed upon JS completion.
  TestFormActivityObserver* block_observer = observer_.get();
  __block TestFormActivityInfo* info = nil;
  EXPECT_FALSE(WaitUntilConditionOrTimeout(kWaitForJSCompletionTimeout, ^{
    info = block_observer->form_activity_info();
    return info != nil;
  }));
}

// Test fixture verifying the behavior of FormActivityTabHelper when handling
// form mutation events.
class FormMutationTest : public base::test::WithFeatureOverride,
                         public FormActivityTabHelperTest {
 public:
  FormMutationTest()
      : base::test::WithFeatureOverride(
            features::kAutofillTrackFormMutationsOptimizationIos) {}
  void SetUp() override { FormActivityTabHelperTest::SetUp(); }

 protected:
  // Loads the specified HTML content, prepares for form mutation tracking.
  void LoadHtmlForMutationTest(NSString* html) {
    LoadHtml(html);
    web::WebFrame* main_frame = WaitForMainFrame();
    ASSERT_TRUE(main_frame);
    TrackFormMutations(main_frame);
    // Force fetching forms to set the elements renderer IDs.
    // Element IDs are set on demand, the first time they are queried for an
    // element. Setting them here make the tests easier to maintain because the
    // elements in a DOM will get the ID assigned in the order they appear in
    // the document.
    ASSERT_TRUE(SetUpUniqueIDs());
  }

  /**
   * Removes the specified HTML element from the DOM and returns the
   * corresponding form removal parameters.
   *  - `element_id`  The ID of the HTML element to remove.
   * Retruns an `std::optional` containing the `FormRemovalParams` if the
   * removal was successful and the event was received within the timeout;
   * otherwise, an empty `std::optional`.
   */
  std::optional<FormRemovalParams> RemoveElement(NSString* element_id) {
    ExecuteJavaScript(
        [NSString stringWithFormat:@"document.getElementById('%@').remove();",
                                   element_id]);

    TestFormActivityObserver* block_observer = observer_.get();
    __block TestFormRemovalInfo* info = nil;

    // Wait for form removal message delivery.
    bool form_removal_info_received = WaitUntilConditionOrTimeout(
        base::Milliseconds(kTrackFormMutationsDelayInMs * 2), ^{
          info = block_observer->form_removal_info();
          return info != nil;
        });

    if (!form_removal_info_received) {
      return std::nullopt;
    }

    web::WebFrame* main_frame = WaitForMainFrame();
    CHECK(main_frame);

    EXPECT_EQ(main_frame->GetFrameId(), info->sender_frame_id);
    EXPECT_THAT(info->form_removal_params.frame_id,
                StrEq(main_frame->GetFrameId()));

    return info->form_removal_params;
  }

  // Forces fetching forms in the main frame which sets renderer IDs in the
  // relevant forms and fields. IDs are set in the order the elements appear in
  // the DOM tree.
  bool SetUpUniqueIDs() {
    WebFrame* main_frame = WaitForMainFrame();
    if (!main_frame) {
      return false;
    }

    __block bool finished = false;
    AutofillJavaScriptFeature::GetInstance()->FetchForms(
        main_frame, base::BindOnce(^(NSString* result) {
          finished = true;
        }));

    return WaitUntilConditionOrTimeout(kWaitForJSCompletionTimeout, ^bool {
      return finished;
    });
  }
};

// Tests that observer is called on form removal.
TEST_P(FormMutationTest, PasswordFormRemovalRegistered) {
  LoadHtmlForMutationTest(
      @"<form name=\"form1\" id=\"form1\">"
       "<input type=\"text\" name=\"username\" id=\"id1\">"
       "<input type=\"password\" name=\"password\" id=\"id2\">"
       "<input type=\"submit\" id=\"submit_input\"/>"
       "</form>");

  ASSERT_FALSE(observer_->form_removal_info());

  std::optional<FormRemovalParams> form_removal_params =
      RemoveElement(/*element_id=*/@"form1");
  ASSERT_TRUE(form_removal_params);

  EXPECT_THAT(form_removal_params.value().removed_forms,
              ElementsAre(FormRendererId(1)));
  EXPECT_THAT(form_removal_params.value().removed_unowned_fields, IsEmpty());

  if (IsParamFeatureEnabled()) {
    // Wait for the second event (form_changed) to be received.
    ASSERT_TRUE(WaitUntilConditionOrTimeout(kWaitForJSCompletionTimeout, ^bool {
      return observer_->form_activity_info() != nullptr;
    }));
    ValidateParamsAfterFormChangedEvent(
        observer_->form_activity_info()->form_activity);

    histogram_tester_.ExpectUniqueSample("Autofill.iOS.FormActivity.DropCount",
                                         /*sample=*/0,
                                         /*expected_bucket_count=*/1);
    histogram_tester_.ExpectUniqueSample("Autofill.iOS.FormActivity.SendCount",
                                         /*sample=*/2,
                                         /*expected_bucket_count=*/1);
    histogram_tester_.ExpectUniqueSample("Autofill.iOS.FormActivity.SendRatio",
                                         /*sample=*/100,
                                         /*expected_bucket_count=*/1);
  } else {
    histogram_tester_.ExpectUniqueSample("Autofill.iOS.FormActivity.DropCount",
                                         /*sample=*/0,
                                         /*expected_bucket_count=*/1);
    histogram_tester_.ExpectUniqueSample("Autofill.iOS.FormActivity.SendCount",
                                         /*sample=*/1,
                                         /*expected_bucket_count=*/1);
    histogram_tester_.ExpectUniqueSample("Autofill.iOS.FormActivity.SendRatio",
                                         /*sample=*/100,
                                         /*expected_bucket_count=*/1);

    // Validate that only one removal event is received.
    ASSERT_FALSE(WaitUntilConditionOrTimeout(
        base::Milliseconds(kTrackFormMutationsDelayInMs * 2), ^bool {
          return observer_->number_of_events_received() > 1;
        }));
  }
}

// Tests that removing non-password form triggers
// 'form_removed" event.
TEST_P(FormMutationTest, RemoveNonPasswordForm) {
  // Load html with one form.
  LoadHtmlForMutationTest(@"<form id='form1'>"
                           "<input type='text'>"
                           "</form>");

  std::optional<FormRemovalParams> form_removal_params =
      RemoveElement(/*element_id=*/@"form1");

  ASSERT_TRUE(form_removal_params);
  EXPECT_THAT(form_removal_params.value().removed_forms,
              ElementsAre(FormRendererId(1)));
  EXPECT_THAT(form_removal_params.value().removed_unowned_fields, IsEmpty());
}

// Tests that removing multiple forms triggers
// 'form_removed" event.
TEST_P(FormMutationTest, RemoveMultipleForms) {
  // Load html with multiple forms.
  LoadHtmlForMutationTest(@"<div id='div'>"
                           "<form id='form1'>"
                           "<input type='password'>"
                           "</form>"
                           "<form id='form2'>"
                           "<input type='text'>"
                           "</form>"
                           "<form id='form3'>"
                           "<input type='email'>"
                           "</form>"
                           "</div>");

  std::optional<FormRemovalParams> form_removal_params =
      RemoveElement(/*element_id=*/@"div");

  ASSERT_TRUE(form_removal_params);

  const FormRendererId form1_id = FormRendererId(1);
  const FormRendererId form2_id = FormRendererId(3);
  const FormRendererId form3_id = FormRendererId(5);

  EXPECT_THAT(form_removal_params.value().removed_forms,
              UnorderedElementsAre(form1_id, form2_id, form3_id));

  EXPECT_THAT(form_removal_params.value().removed_unowned_fields, IsEmpty());
}

// Tests that removing unowned password fields triggers 'password_form_removed"
// event.
TEST_P(FormMutationTest, RemoveFormlessPasswordFields) {
  LoadHtmlForMutationTest(
      @"<body><div>"
       "<input type=\"password\" name=\"password\" id=\"pw\">"
       "<input type=\"submit\" id=\"submit_input\"/>"
       "</div></body>");

  web::WebFrame* main_frame = WaitForMainFrame();
  ASSERT_TRUE(main_frame);

  std::optional<FormRemovalParams> form_removal_params =
      RemoveElement(/*element_id=*/@"pw");

  ASSERT_TRUE(form_removal_params);
  EXPECT_THAT(form_removal_params.value().removed_forms, IsEmpty());
  EXPECT_THAT(form_removal_params.value().removed_unowned_fields,
              ElementsAre(FieldRendererId(1)));
  EXPECT_THAT(form_removal_params.value().frame_id, Not(IsEmpty()));

  histogram_tester_.ExpectUniqueSample("Autofill.iOS.FormActivity.DropCount",
                                       /*sample=*/0,
                                       /*expected_bucket_count=*/1);
  histogram_tester_.ExpectUniqueSample("Autofill.iOS.FormActivity.SendCount",
                                       /*sample=*/1,
                                       /*expected_bucket_count=*/1);
  histogram_tester_.ExpectUniqueSample("Autofill.iOS.FormActivity.SendRatio",
                                       /*sample=*/100,
                                       /*expected_bucket_count=*/1);

  // Validate that only one removal event is received.
  ASSERT_FALSE(WaitUntilConditionOrTimeout(
      base::Milliseconds(kTrackFormMutationsDelayInMs * 2), ^bool {
        return observer_->number_of_events_received() > 1;
      }));
}

// Tests that removing multiple forms and formless fields triggers
// 'form_removed" event.
TEST_P(FormMutationTest, RemoveMultipleFormsAndFormlessFields) {
  // Load html with multiple forms and formless fields.
  LoadHtmlForMutationTest(@"<div id='div'>"
                           "<form id='form1'>"
                           "<input type='password'/>"
                           "</form>"
                           "<form id='form2'>"
                           "<input type='text'/>"
                           "</form>"
                           "<form id='form3'>"
                           "<input type='email'/>"
                           "</form>"
                           "<input id='password' type='password'/>"
                           "<input id='text' type='text'/>"
                           "</div>");

  std::optional<FormRemovalParams> form_removal_params =
      RemoveElement(/*element_id=*/@"div");

  ASSERT_TRUE(form_removal_params);

  const FormRendererId form1_id = FormRendererId(1);
  const FormRendererId form2_id = FormRendererId(3);
  const FormRendererId form3_id = FormRendererId(5);
  const FieldRendererId password_id = FieldRendererId(7);
  const FieldRendererId text_id = FieldRendererId(8);

  EXPECT_THAT(form_removal_params.value().removed_forms,
              UnorderedElementsAre(form1_id, form2_id, form3_id));
  EXPECT_THAT(form_removal_params.value().removed_unowned_fields,
              UnorderedElementsAre(password_id, text_id));
}
// Tests that removing a form control element and adding a new one in the same
// mutations batch is notified with a message for each mutation, sent
// back-to-back.
TEST_P(FormMutationTest, RemovedAndAddedFormsRegistered) {
  // Basic HTML page in which we add a HTML form.
  NSString* const html = @"<html><body><form id=\"form1\">"
                          "<input type=\"password\"></form></body></html>";
  LoadHtmlForMutationTest(html);

  web::WebFrame* main_frame = WaitForMainFrame();
  ASSERT_TRUE(main_frame);

  ASSERT_FALSE(observer_->form_removal_info());
  ASSERT_FALSE(observer_->form_activity_info());

  // Make a script to create a new form and replace the old form with it.
  NSString* const replace_form_JS =
      @"const newForm = document.createElement('form'); "
       "newForm.id = 'form2'; "
       "const oldForm = document.forms[0]; "
       "oldForm.parentNode.replaceChild(newForm, oldForm);";

  // Replace the form to trigger an added and a removed form mutation event
  // batched together.
  ExecuteJavaScript(replace_form_JS);

  // Wait until the first message is received.
  ASSERT_TRUE(WaitUntilConditionOrTimeout(kWaitForJSCompletionTimeout, ^bool() {
    return observer_->number_of_events_received() == 1;
  }));

  // The removed form message is always the first posted.
  ASSERT_TRUE(observer_->form_removal_info());
  FormRemovalParams form_removal_params =
      observer_->form_removal_info()->form_removal_params;
  EXPECT_THAT(form_removal_params.frame_id, Not(IsEmpty()));
  EXPECT_THAT(form_removal_params.removed_unowned_fields, IsEmpty());
  EXPECT_THAT(form_removal_params.removed_forms,
              ElementsAre(FormRendererId(1)));

  // Wait until the next message is received.
  EXPECT_TRUE(WaitUntilConditionOrTimeout(kWaitForJSCompletionTimeout, ^bool() {
    return observer_->number_of_events_received() == 2;
  }));

  ASSERT_TRUE(observer_->form_activity_info());
  ValidateParamsAfterFormChangedEvent(
      observer_->form_activity_info()->form_activity);

  histogram_tester_.ExpectUniqueSample("Autofill.iOS.FormActivity.DropCount",
                                       /*sample=*/0,
                                       /*expected_bucket_count=*/1);
  histogram_tester_.ExpectUniqueSample("Autofill.iOS.FormActivity.SendCount",
                                       /*sample=*/2,
                                       /*expected_bucket_count=*/1);
  histogram_tester_.ExpectUniqueSample("Autofill.iOS.FormActivity.SendRatio",
                                       /*sample=*/100,
                                       /*expected_bucket_count=*/1);
}

// Tests that messages that were batched and dropped are correctly recorded as
// such.
TEST_P(FormMutationTest, RemovedAndAddedFormsRegistered_WithDroppedMessages) {
  // Basic HTML page with 2 password forms and one formless password form.
  NSString* const html = @"<html><body><form id=\"form1\">"
                          "<input type=\"password\"></form>"
                          "<form id=\"form2\"><input type=\"password\"></form>"
                          "<input id=\"input1\" type=\"password\">"
                          "</body></html>";
  LoadHtmlForMutationTest(html);

  web::WebFrame* main_frame = WaitForMainFrame();
  ASSERT_TRUE(main_frame);

  ASSERT_FALSE(observer_->form_removal_info());
  ASSERT_FALSE(observer_->form_activity_info());

  // Make a script that batches 2 messages and ignore all other cases once full.
  NSString* const add_and_remove_form_JS =
      @"const parentNode = document.forms[0].parentNode; "
       // Add a generic form and remove a password form, both of which will be
       // notified in the same batch.
       "parentNode.appendChild(document.createElement('form')); "
       "const form1 = document.getElementById('form1'); "
       "form1.remove(); "
       // Form transformations from here should be ignored.
       // Add non-password form and remove it, 2 notifications dropped.
       "parentNode.appendChild(document.createElement('form')).remove(); "
       // Remove formless password input, 1 notification dropped.
       "document.getElementById('input1').remove();"
       // Remove password form, 1 notification dropped.
       "document.getElementById('form2').remove();";

  ExecuteJavaScript(add_and_remove_form_JS);

  // Wait on all the messages in the batch.
  ASSERT_TRUE(WaitUntilConditionOrTimeout(kWaitForJSCompletionTimeout, ^bool {
    return observer_->form_removal_info() != nullptr &&
           observer_->form_activity_info() != nullptr;
  }));

  if (base::FeatureList::IsEnabled(
          features::kAutofillTrackFormMutationsOptimizationIos)) {
    EXPECT_THAT(
        observer_->form_removal_info()->form_removal_params.removed_forms,
        UnorderedElementsAre(FormRendererId(1), FormRendererId(3)));
    EXPECT_THAT(observer_->form_removal_info()
                    ->form_removal_params.removed_unowned_fields,
                ElementsAre(FieldRendererId(5)));
    ValidateParamsAfterFormChangedEvent(
        observer_->form_activity_info()->form_activity);

    histogram_tester_.ExpectUniqueSample("Autofill.iOS.FormActivity.DropCount",
                                         /*sample=*/0,
                                         /*expected_bucket_count=*/1);
    histogram_tester_.ExpectUniqueSample("Autofill.iOS.FormActivity.SendCount",
                                         /*sample=*/2,
                                         /*expected_bucket_count=*/1);
    histogram_tester_.ExpectUniqueSample("Autofill.iOS.FormActivity.SendRatio",
                                         /*sample=*/100,
                                         /*expected_bucket_count=*/1);
  } else {
    EXPECT_THAT(
        observer_->form_removal_info()->form_removal_params.removed_forms,
        ElementsAre(FormRendererId(1)));
    EXPECT_THAT(observer_->form_removal_info()
                    ->form_removal_params.removed_unowned_fields,
                IsEmpty());
    ValidateParamsAfterFormChangedEvent(
        observer_->form_activity_info()->form_activity);

    histogram_tester_.ExpectUniqueSample("Autofill.iOS.FormActivity.DropCount",
                                         /*sample=*/4,
                                         /*expected_bucket_count=*/1);
    histogram_tester_.ExpectUniqueSample("Autofill.iOS.FormActivity.SendCount",
                                         /*sample=*/2,
                                         /*expected_bucket_count=*/1);
    histogram_tester_.ExpectUniqueSample("Autofill.iOS.FormActivity.SendRatio",
                                         /*sample=*/33,
                                         /*expected_bucket_count=*/1);
  }
}

// Tests that removing input fields triggers the right events.
TEST_P(FormMutationTest, RemoveFormlessFields) {
  LoadHtmlForMutationTest(@"<body><div id='div'>"
                           "<input type='password' id='password'/>"
                           "<input type='text' id='text'/>"
                           "<input type='submit' id='submit_input'/>"
                           "<input type='email' id='email'/>"
                           "<input type='tel' id='phone'/>"
                           "<input type='url' id='url'/>"
                           "<input type='number' id='number'/>"
                           "<input type='checkbox' id='checkbox' />"
                           "<input type='radio' id='radio'/>"
                           "<select id='select'>"
                           "  <option value='v1'>v1</option>"
                           "  <option value='v2'>v2</option>"
                           "</select>"
                           "<textarea id='textarea'/>"
                           "</div></body>");

  std::optional<FormRemovalParams> form_removal_params =
      RemoveElement(/*element_id=*/@"div");

  ASSERT_TRUE(form_removal_params);
  EXPECT_TRUE(form_removal_params.value().removed_forms.empty());

  const FieldRendererId password_id = FieldRendererId(1);
  const FieldRendererId text_id = FieldRendererId(2);
  const FieldRendererId email_id = FieldRendererId(3);
  const FieldRendererId phone_id = FieldRendererId(4);
  const FieldRendererId url_id = FieldRendererId(5);
  const FieldRendererId number_id = FieldRendererId(6);
  const FieldRendererId select_id = FieldRendererId(7);
  const FieldRendererId textarea_id = FieldRendererId(8);

  EXPECT_THAT(form_removal_params.value().removed_forms, IsEmpty());

  EXPECT_THAT(form_removal_params.value().removed_unowned_fields,
              UnorderedElementsAre(password_id, text_id, email_id, phone_id,
                                   url_id, number_id, select_id, textarea_id));
}

// Tests that a new form triggers form_changed event.
TEST_P(FormMutationTest, AddForm) {
  LoadHtmlForMutationTest(@"<body></body>");

  web::WebFrame* main_frame = WaitForMainFrame();
  ASSERT_TRUE(main_frame);

  ExecuteJavaScript(@"var form = document.createElement('form');"
                    @"document.body.appendChild(form);");
  TestFormActivityObserver* block_observer = observer_.get();
  __block TestFormActivityInfo* info = nil;
  ASSERT_TRUE(WaitUntilConditionOrTimeout(kWaitForJSCompletionTimeout, ^{
    info = block_observer->form_activity_info();
    return info != nil;
  }));

  ValidateParamsAfterFormChangedEvent(info->form_activity);
}

// Tests that mutations happening in separate event loop tasks before the
// scheduling delay elapses are throttled and recorded as dropped.
TEST_P(FormMutationTest, OptimizedFormMutations_ThrottledAcrossTicks) {
  LoadHtmlForMutationTest(@"<body></body>");

  WebFrame* main_frame = WaitForMainFrame();
  ASSERT_TRUE(main_frame);

  // 1. Trigger the first mutation task.
  ExecuteJavaScript(@"var form1 = document.createElement('form');"
                    @"document.body.appendChild(form1);");

  // 2. Trigger the second mutation task immediately, without waiting for
  // background tasks or timers to settle.
  ExecuteJavaScript(@"var form2 = document.createElement('form');"
                    @"document.body.appendChild(form2);");

  // 3. Wait for the native side to receive the single permitted event.
  TestFormActivityObserver* block_observer = observer_.get();
  ASSERT_TRUE(WaitUntilConditionOrTimeout(kWaitForJSCompletionTimeout, ^{
    return block_observer->form_activity_info() != nullptr;
  }));

  // Allow any other potential scheduled messages to clear.
  base::test::ios::SpinRunLoopWithMinDelay(
      base::Milliseconds(kTrackFormMutationsDelayInMs * 2));

  // Confirm only 1 event was received by the observer (the second was dropped).
  EXPECT_EQ(observer_->number_of_events_received(), 1);

  bool optimization_enabled = base::FeatureList::IsEnabled(
      autofill::features::kAutofillTrackFormMutationsOptimizationIos);
  int expected_drop_count = optimization_enabled ? 0 : 1;
  histogram_tester_.ExpectUniqueSample("Autofill.iOS.FormActivity.DropCount",
                                       /*sample=*/expected_drop_count,
                                       /*expected_bucket_count=*/1);
  histogram_tester_.ExpectUniqueSample("Autofill.iOS.FormActivity.SendCount",
                                       /*sample=*/1,
                                       /*expected_bucket_count=*/1);
}

INSTANTIATE_FEATURE_OVERRIDE_TEST_SUITE(FormMutationTest);

class FormContentEditableTest : public base::test::WithFeatureOverride,
                                public FormActivityTabHelperTest {
 public:
  FormContentEditableTest()
      : base::test::WithFeatureOverride(kAutofillSupportContentEditableIos) {}
};

// Tests focusing a contenteditable element when the feature is enabled vs
// disabled.
TEST_P(FormContentEditableTest, FocusContentEditable) {
  LoadHtml(@"<div contenteditable='true' id='editable'>Hello</div>");
  ASSERT_FALSE(observer_->form_activity_info());
  ExecuteJavaScript(@"var el = document.getElementById('editable');"
                    @"el.focus();"
                    @"el.dispatchEvent(new Event('focus', {bubbles: true}));");
  TestFormActivityObserver* block_observer = observer_.get();
  if (IsParamFeatureEnabled()) {
    ASSERT_TRUE(WaitUntilConditionOrTimeout(kWaitForJSCompletionTimeout, ^{
      return block_observer->form_activity_info() != nullptr;
    }));
    TestFormActivityInfo* info = observer_->form_activity_info();
    ASSERT_TRUE(info);
    EXPECT_EQ(FormActivityParams::ActivityType::kFocus,
              info->form_activity.type);
    EXPECT_EQ(FormActivityParams::FieldType::kContentEditable,
              info->form_activity.field_type);
    EXPECT_EQ("Hello", info->form_activity.value);
  } else {
    // Use 50ms to reduce waiting time for expected timeout.
    EXPECT_FALSE(WaitUntilConditionOrTimeout(base::Milliseconds(50), ^{
      return block_observer->form_activity_info() != nullptr;
    }));
  }
}

// Tests that focusing a child element inside a contenteditable container
// correctly delivers a focus form activity event when enabled, and no event
// when disabled.
TEST_P(FormContentEditableTest, FocusContentEditableChildElement) {
  LoadHtml(@"<div contenteditable='true' id='editable'>"
           @"Hello <b id='child'>world</b></div>");
  ASSERT_FALSE(observer_->form_activity_info());
  ExecuteJavaScript(
      @"var container = document.getElementById('editable');"
      @"var child = document.getElementById('child');"
      @"container.focus();"
      @"child.dispatchEvent(new Event('focus', {bubbles: true}));");
  TestFormActivityObserver* block_observer = observer_.get();
  if (IsParamFeatureEnabled()) {
    ASSERT_TRUE(WaitUntilConditionOrTimeout(kWaitForJSCompletionTimeout, ^{
      return block_observer->form_activity_info() != nullptr;
    }));
    TestFormActivityInfo* info = observer_->form_activity_info();
    ASSERT_TRUE(info);
    EXPECT_EQ(FormActivityParams::ActivityType::kFocus,
              info->form_activity.type);
    EXPECT_EQ(FormActivityParams::FieldType::kContentEditable,
              info->form_activity.field_type);
    EXPECT_EQ("world", info->form_activity.value);
  } else {
    // Use 50ms to reduce waiting time for expected timeout.
    EXPECT_FALSE(WaitUntilConditionOrTimeout(base::Milliseconds(50), ^{
      return block_observer->form_activity_info() != nullptr;
    }));
  }
}

// Tests focusing sibling contenteditable elements when enabled vs disabled.
TEST_P(FormContentEditableTest, FocusContentEditableSiblingElement) {
  LoadHtml(@"<div contenteditable='true' id='editable'>Hello</div>"
           @"<div contenteditable='true' id='sibling'>Other</div>");
  ASSERT_FALSE(observer_->form_activity_info());
  ExecuteJavaScript(
      @"var editable = document.getElementById('editable');"
      @"editable.focus();"
      @"editable.dispatchEvent(new Event('focus', {bubbles: true}));");
  TestFormActivityObserver* block_observer = observer_.get();
  if (IsParamFeatureEnabled()) {
    ASSERT_TRUE(WaitUntilConditionOrTimeout(kWaitForJSCompletionTimeout, ^{
      return block_observer->form_activity_info() != nullptr;
    }));
    EXPECT_EQ("Hello", observer_->form_activity_info()->form_activity.value);

    // Focus the sibling element.
    ExecuteJavaScript(
        @"var sibling = document.getElementById('sibling');"
        @"sibling.focus();"
        @"sibling.dispatchEvent(new Event('focus', {bubbles: true}));");
    ASSERT_TRUE(WaitUntilConditionOrTimeout(kWaitForJSCompletionTimeout, ^{
      return block_observer->form_activity_info() != nullptr &&
             block_observer->form_activity_info()->form_activity.value ==
                 "Other";
    }));
    TestFormActivityInfo* info = observer_->form_activity_info();
    ASSERT_TRUE(info);
    EXPECT_EQ(FormActivityParams::ActivityType::kFocus,
              info->form_activity.type);
    EXPECT_EQ(FormActivityParams::FieldType::kContentEditable,
              info->form_activity.field_type);
    EXPECT_EQ("Other", info->form_activity.value);
  } else {
    // Use 50ms to reduce waiting time for expected timeout.
    EXPECT_FALSE(WaitUntilConditionOrTimeout(base::Milliseconds(50), ^{
      return block_observer->form_activity_info() != nullptr;
    }));
  }
}

INSTANTIATE_FEATURE_OVERRIDE_TEST_SUITE(FormContentEditableTest);

// Test fixture verifying the behavior of FormActivityTabHelper when handling
// mutations involving form control elements.
class FormMutationFormControlElements
    : public FormActivityTabHelperTest,
      public testing::WithParamInterface<std::string> {};

// Tests that adding a formless control element is notified as a form changed
// mutation.
TEST_P(FormMutationFormControlElements, AddedFormlessControlElement) {
  // Basic HTML page in which we add HTML form control elements.
  NSString* const html = @"<html><body></body></html>";
  LoadHtml(html);

  web::WebFrame* main_frame = WaitForMainFrame();
  ASSERT_TRUE(main_frame);

  std::string element_tag = GetParam();
  TrackFormMutations(main_frame);

  // Add the element to the page.
  NSString* const insert_element_JS = [NSString
      stringWithFormat:@"const element = document.createElement('%@'); "
                        "document.body.append(element); ",
                       base::SysUTF8ToNSString(element_tag)];

  ExecuteJavaScript(insert_element_JS);

  ASSERT_TRUE(WaitUntilConditionOrTimeout(kWaitForJSCompletionTimeout, ^bool() {
    return observer_->form_activity_info() != nullptr;
  }));

  ValidateParamsAfterFormChangedEvent(
      observer_->form_activity_info()->form_activity);
}

// Tests that adding a form control element is notified as a form changed
// mutation.
TEST_P(FormMutationFormControlElements, AddedFormControlElement) {
  // Basic HTML page in which we add HTML form control elements.
  NSString* const html = @"<html><body><form id='form'></form></body></html>";
  LoadHtml(html);

  web::WebFrame* main_frame = WaitForMainFrame();
  ASSERT_TRUE(main_frame);

  std::string element_tag = GetParam();
  TrackFormMutations(main_frame);

  // Add the element to the page.
  NSString* const insert_element_JS = [NSString
      stringWithFormat:@"const element = document.createElement('%@'); "
                        "document.getElementById('form').append(element); ",
                       base::SysUTF8ToNSString(element_tag)];

  ExecuteJavaScript(insert_element_JS);

  ASSERT_TRUE(WaitUntilConditionOrTimeout(kWaitForJSCompletionTimeout, ^bool() {
    return observer_->form_activity_info() != nullptr;
  }));

  ValidateParamsAfterFormChangedEvent(
      observer_->form_activity_info()->form_activity);
}

INSTANTIATE_TEST_SUITE_P(
    /* No InstantiationName */,
    FormMutationFormControlElements,
    ::testing::Values("form", "input", "select", "option", "textarea"));

// Test fixture for testing programmatic form submissions with/without Autofill
// in the isolated world enabled.
// TODO(crbug.com/359538514): Remove this fixture and move test back to
// FormActivityTabHelperTest once isolated world for Autofill is launched.
class FormSubmittedHookTest : public FormActivityTabHelperTest {
 public:
  FormSubmittedHookTest() : FormActivityTabHelperTest() {
    web::FakeWebClient* web_client =
        static_cast<web::FakeWebClient*>(GetWebClient());

    renderer_id_feature_ = test::CreateRendererIdTestJavaScriptFeature();

    web_client->SetJavaScriptFeatures({
        FormHandlersJavaScriptFeature::GetInstance(),
        AutofillJavaScriptFeature::GetInstance(),
        ProgrammaticFormSubmissionHandlerJavaScriptFeature::GetInstance(),
        renderer_id_feature_.get(),
    });
  }

 protected:
  // Runs the form extraction script and returns true if the script ran
  // successfully.
  bool FetchForms() {
    WebFrame* main_frame = WaitForMainFrame();
    if (!main_frame) {
      return false;
    }
    __block bool finished = false;
    AutofillJavaScriptFeature::GetInstance()->FetchForms(
        main_frame, base::BindOnce(^(NSString* result) {
          finished = true;
        }));

    return WaitUntilConditionOrTimeout(kWaitForJSCompletionTimeout, ^bool {
      return finished;
    });
  }

 private:
  std::unique_ptr<web::JavaScriptFeature> renderer_id_feature_;
};

// Validate that programmatic form submissions are detected and sent to
// observers of the tab helper.
TEST_F(FormSubmittedHookTest, TestFormSubmittedHook) {
  LoadHtml(kTestHTMLForm);

  WebFrame* main_frame = WaitForMainFrame();
  ASSERT_TRUE(main_frame);

  web::test::ExecuteJavaScriptForFeature(
      web_state(),
      @"var form = document.forms[0];"
       "__gCrWeb.getRegisteredApi('renderer_id_test').getFunction('"
       "setUniqueIDIfNeeded')(form);"
       "var input = document.getElementById('text');"
       "__gCrWeb.getRegisteredApi('renderer_id_test').getFunction('"
       "setUniqueIDIfNeeded')(input);",
      AutofillJavaScriptFeature::GetInstance());

  ASSERT_FALSE(observer_->submit_document_info());

  // Make sure the script triggering the submission hooks runs in the page
  // content world.
  web::test::ExecuteJavaScriptForFeature(
      web_state(), @"document.forms[0].submit();",
      ProgrammaticFormSubmissionHandlerJavaScriptFeature::GetInstance());

  FormData test_form_data = BuildTestFormData(main_frame->GetFrameId());

  ASSERT_TRUE(observer_->submit_document_info());
  EXPECT_EQ(main_frame->GetFrameId(),
            observer_->submit_document_info()->sender_frame_id);
  EXPECT_THAT(
      WithoutUnserializedData(observer_->submit_document_info()->form_data),
      FormDataEq(WithoutUnserializedData(test_form_data)));
  EXPECT_FALSE(observer_->submit_document_info()->has_user_gesture);

  histogram_tester_.ExpectUniqueSample(kProgrammaticFormSubmissionHistogram,
                                       true, 1);

  // Verify that there isn't any form activity metric recorded as the form
  // submit signals aren't covered.
  histogram_tester_.ExpectTotalCount("Autofill.iOS.FormActivity.DropCount", 0);
  histogram_tester_.ExpectTotalCount("Autofill.iOS.FormActivity.SendCount", 0);
  histogram_tester_.ExpectTotalCount("Autofill.iOS.FormActivity.SendRatio", 0);
}

// Validate that programmatic form submissions are detected and sent to
// observers of the tab helper.
TEST_F(FormSubmittedHookTest, TestFormSubmittedHookAcrossIframes) {
  LoadHtml(kTestHTMLFormWithIframes);

  WebFrame* main_frame = WaitForMainFrame();
  ASSERT_TRUE(main_frame);

  // Trigger form extraction so child frame tokens are set in the isolated
  // world. Page scripts will be able to access the tokens through the fallback
  // stored in the DOM.
  FetchForms();

  ASSERT_FALSE(observer_->submit_document_info());

  // Run script triggering the submission hooks in the page
  // content world.
  web::test::ExecuteJavaScriptForFeature(
      web_state(), @"document.forms[0].submit();",
      ProgrammaticFormSubmissionHandlerJavaScriptFeature::GetInstance());

  ASSERT_TRUE(observer_->submit_document_info());
  const FormData& submitted_form = observer_->submit_document_info()->form_data;
  // Make sure child frames are populated.

  EXPECT_THAT(submitted_form.child_frames(),
              Each(Field(&FrameTokenWithPredecessor::token,
                         VariantWith<RemoteFrameToken>(IsTrue()))));
}

}  // namespace autofill
