// 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/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/time/time.h"
#import "components/autofill/ios/browser/autofill_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/form_util_java_script_feature.h"
#import "components/autofill/ios/form_util/test_form_activity_observer.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"
using autofill::FieldRendererId;
using autofill::FormActivityParams;
using autofill::FormRemovalParams;
using autofill::FormRendererId;
using autofill::test::kTrackFormMutationsDelayInMs;
using base::test::ios::kWaitForJSCompletionTimeout;
using base::test::ios::WaitUntilConditionOrTimeout;
using ::testing::ElementsAre;
using ::testing::IsEmpty;
using ::testing::Not;
using ::testing::StrEq;
using ::testing::UnorderedElementsAre;
using web::WebFrame;
// Tests fixture for autofill::FormActivityTabHelper class.
class FormActivityTabHelperTest : public AutofillTestWithWebState {
public:
FormActivityTabHelperTest()
: AutofillTestWithWebState(std::make_unique<web::FakeWebClient>()) {
web::FakeWebClient* web_client =
static_cast<web::FakeWebClient*>(GetWebClient());
web_client->SetJavaScriptFeatures(
{autofill::FormUtilJavaScriptFeature::GetInstance(),
autofill::FormHandlersJavaScriptFeature::GetInstance(),
autofill::AutofillJavaScriptFeature::GetInstance()});
}
void SetUp() override {
web::WebTestWithWebState::SetUp();
autofill::FormActivityTabHelper* tab_helper =
autofill::FormActivityTabHelper::GetOrCreateForWebState(web_state());
observer_ =
std::make_unique<autofill::TestFormActivityObserver>(web_state());
tab_helper->AddObserver(observer_.get());
}
void TearDown() override {
autofill::FormActivityTabHelper* tab_helper =
autofill::FormActivityTabHelper::GetOrCreateForWebState(web_state());
tab_helper->RemoveObserver(observer_.get());
web::WebTestWithWebState::TearDown();
}
protected:
WebFrame* WaitForMainFrame() {
__block WebFrame* main_frame = nullptr;
EXPECT_TRUE(WaitUntilConditionOrTimeout(kWaitForJSCompletionTimeout, ^bool {
web::WebFramesManager* frames_manager =
autofill::FormUtilJavaScriptFeature::GetInstance()
->GetWebFramesManager(web_state());
main_frame = frames_manager->GetMainWebFrame();
return main_frame != nullptr;
}));
return main_frame;
}
// 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 = "form_changed";
EXPECT_EQ(params, expected_activity_params);
}
base::HistogramTester histogram_tester_;
std::unique_ptr<autofill::TestFormActivityObserver> observer_;
};
// Tests that observer is called on form submission using submit control.
TEST_F(FormActivityTabHelperTest, TestObserverDocumentSubmitted) {
LoadHtml(@"<form name='form-name'>"
"<input type='submit' id='submit'/>"
"</form>");
WebFrame* main_frame = WaitForMainFrame();
ASSERT_TRUE(main_frame);
ASSERT_FALSE(observer_->submit_document_info());
const std::string kTestFormName("form-name");
std::string mainFrameID = main_frame->GetFrameId();
const std::string kTestFormData =
std::string("[{\"name\":\"form-name\",\"origin\":\"https://chromium.test/"
"\",\"action\":\"https://chromium.test/\","
"\"name_attribute\":\"form-name\",\"id_attribute\":\"\","
"\"renderer_id\":\"1\",\"frame_id\":\"") +
mainFrameID + std::string("\"}]");
ExecuteJavaScript(@"document.getElementById('submit').click();");
ASSERT_TRUE(observer_->submit_document_info());
EXPECT_EQ(web_state(), observer_->submit_document_info()->web_state);
EXPECT_EQ(main_frame, observer_->submit_document_info()->sender_frame);
EXPECT_EQ(kTestFormName, observer_->submit_document_info()->form_name);
EXPECT_EQ(kTestFormData, observer_->submit_document_info()->form_data);
EXPECT_FALSE(observer_->submit_document_info()->has_user_gesture);
// 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 observer is called on form submission using submit() method.
TEST_F(FormActivityTabHelperTest, TestFormSubmittedHook) {
LoadHtml(@"<form name='form-name' id='form'>"
"<input type='submit'/>"
"</form>");
WebFrame* main_frame = WaitForMainFrame();
ASSERT_TRUE(main_frame);
ASSERT_FALSE(observer_->submit_document_info());
const std::string kTestFormName("form-name");
std::string mainFrameID = main_frame->GetFrameId();
const std::string kTestFormData =
std::string("[{\"name\":\"form-name\",\"origin\":\"https://chromium.test/"
"\",\"action\":\"https://chromium.test/\","
"\"name_attribute\":\"form-name\",\"id_attribute\":\"form\","
"\"renderer_id\":\"1\",\"frame_id\":\"") +
mainFrameID + std::string("\"}]");
ExecuteJavaScript(@"document.getElementById('form').submit();");
ASSERT_TRUE(observer_->submit_document_info());
EXPECT_EQ(web_state(), observer_->submit_document_info()->web_state);
EXPECT_EQ(main_frame, observer_->submit_document_info()->sender_frame);
EXPECT_EQ(kTestFormName, observer_->submit_document_info()->form_name);
EXPECT_EQ(kTestFormData, observer_->submit_document_info()->form_data);
EXPECT_FALSE(observer_->submit_document_info()->has_user_gesture);
// 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();");
autofill::TestSubmitDocumentInfo* info = observer_->submit_document_info();
ASSERT_TRUE(info);
EXPECT_EQ("form1", info->form_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(web_state(), observer_->form_activity_info()->web_state);
EXPECT_EQ(main_frame, observer_->form_activity_info()->sender_frame);
EXPECT_EQ("form-name",
observer_->form_activity_info()->form_activity.form_name);
EXPECT_EQ("text", observer_->form_activity_info()->form_activity.field_type);
EXPECT_EQ("focus", 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();
autofill::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(@"document.getElementById('id1').focus();");
autofill::TestFormActivityObserver* block_observer = observer_.get();
ASSERT_TRUE(WaitUntilConditionOrTimeout(kWaitForJSCompletionTimeout, ^{
return block_observer->form_activity_info() != nullptr;
}));
autofill::TestFormActivityInfo* info = observer_->form_activity_info();
ASSERT_TRUE(info);
EXPECT_EQ("focus", 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(
@"document.getElementById('frame1').contentDocument.getElementById('id1')"
@".focus()");
autofill::TestFormActivityObserver* block_observer = observer_.get();
ASSERT_TRUE(WaitUntilConditionOrTimeout(kWaitForJSCompletionTimeout, ^{
return block_observer->form_activity_info() != nullptr;
}));
autofill::TestFormActivityInfo* info = observer_->form_activity_info();
ASSERT_TRUE(info);
EXPECT_EQ("focus", 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.
autofill::TestFormActivityObserver* block_observer = observer_.get();
__block autofill::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 FormActivityTabHelperTest {
public:
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<autofill::FormRemovalParams> RemoveElement(
NSString* element_id) {
ExecuteJavaScript(
[NSString stringWithFormat:@"document.getElementById('%@').remove();",
element_id]);
autofill::TestFormActivityObserver* block_observer = observer_.get();
__block autofill::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(web_state(), info->web_state);
EXPECT_EQ(main_frame, info->sender_frame);
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;
autofill::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_F(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<autofill::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());
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_F(FormMutationTest, RemoveNonPasswordForm) {
// Load html with one form.
LoadHtmlForMutationTest(@"<form id='form1'>"
"<input type='text'>"
"</form>");
std::optional<autofill::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_F(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<autofill::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_F(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<autofill::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_F(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<autofill::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_F(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_F(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;
}));
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_F(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<autofill::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 checkbox_id = FieldRendererId(7);
const FieldRendererId radio_id = FieldRendererId(8);
const FieldRendererId select_id = FieldRendererId(9);
const FieldRendererId textarea_id = FieldRendererId(10);
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, checkbox_id, radio_id,
select_id, textarea_id));
}
// Tests that a new form triggers form_changed event.
TEST_F(FormMutationTest, AddForm) {
LoadHtmlForMutationTest(@"<body></body>");
web::WebFrame* main_frame = WaitForMainFrame();
ASSERT_TRUE(main_frame);
ExecuteJavaScript(@"var form = document.createElement('form');"
@"document.body.appendChild(form);");
autofill::TestFormActivityObserver* block_observer = observer_.get();
__block autofill::TestFormActivityInfo* info = nil;
ASSERT_TRUE(WaitUntilConditionOrTimeout(kWaitForJSCompletionTimeout, ^{
info = block_observer->form_activity_info();
return info != nil;
}));
ValidateParamsAfterFormChangedEvent(info->form_activity);
}
// 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"));