| // Copyright 2017 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #import "base/test/ios/wait_util.h" |
| #import "components/autofill/ios/form_util/autofill_test_with_web_state.h" |
| #include "components/autofill/ios/form_util/form_activity_tab_helper.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" |
| #include "components/autofill/ios/form_util/test_form_activity_observer.h" |
| #import "ios/web/public/browser_state.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/js_test_util.h" |
| #import "ios/web/public/test/web_test_with_web_state.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| |
| #if !defined(__has_feature) || !__has_feature(objc_arc) |
| #error "This file requires ARC support." |
| #endif |
| |
| using autofill::FieldRendererId; |
| using autofill::FormRendererId; |
| using base::test::ios::WaitUntilConditionOrTimeout; |
| using base::test::ios::kWaitForJSCompletionTimeout; |
| |
| // Text fixture to test password controller. |
| class FormJsTest : public AutofillTestWithWebState { |
| public: |
| FormJsTest() |
| : AutofillTestWithWebState(std::make_unique<web::FakeWebClient>()) { |
| web::FakeWebClient* web_client = |
| static_cast<web::FakeWebClient*>(GetWebClient()); |
| web_client->SetJavaScriptFeatures( |
| {autofill::FormUtilJavaScriptFeature::GetInstance(), |
| autofill::FormHandlersJavaScriptFeature::GetInstance()}); |
| } |
| |
| void SetUp() override { |
| web::WebTestWithWebState::SetUp(); |
| observer_ = |
| std::make_unique<autofill::TestFormActivityObserver>(web_state()); |
| autofill::FormActivityTabHelper::GetOrCreateForWebState(web_state()) |
| ->AddObserver(observer_.get()); |
| } |
| |
| void TearDown() override { |
| autofill::FormActivityTabHelper::GetOrCreateForWebState(web_state()) |
| ->RemoveObserver(observer_.get()); |
| web::WebTestWithWebState::TearDown(); |
| } |
| |
| protected: |
| web::WebFrame* WaitForMainFrame() { |
| __block web::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; |
| } |
| |
| std::unique_ptr<autofill::TestFormActivityObserver> observer_; |
| }; |
| |
| // Tests that keyup event correctly delivered to WebStateObserver if the element |
| // is focused. |
| TEST_F(FormJsTest, KeyUpEventFocused) { |
| LoadHtml(@"<p><input id='test'/></p>"); |
| ASSERT_FALSE(observer_->form_activity_info()); |
| ExecuteJavaScript( |
| @"var e = document.getElementById('test');" |
| "e.focus();" |
| "var ev = new KeyboardEvent('keyup', {bubbles:true});" |
| "e.dispatchEvent(ev);"); |
| 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("keyup", info->form_activity.type); |
| EXPECT_FALSE(info->form_activity.input_missing); |
| } |
| |
| // Tests that keyup event is not delivered to WebStateObserver if the element is |
| // not focused. |
| TEST_F(FormJsTest, KeyUpEventNotFocused) { |
| LoadHtml(@"<p><input id='test'/></p>"); |
| ASSERT_FALSE(observer_->form_activity_info()); |
| ExecuteJavaScript( |
| @"var e = document.getElementById('test');" |
| "var ev = new KeyboardEvent('keyup', {bubbles:true});" |
| "e.dispatchEvent(ev);"); |
| WaitForBackgroundTasks(); |
| autofill::TestFormActivityInfo* info = observer_->form_activity_info(); |
| ASSERT_FALSE(info); |
| } |
| |
| // Tests that focus event correctly delivered to WebStateObserver. |
| TEST_F(FormJsTest, 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 submit event correctly delivered to WebStateObserver. |
| TEST_F(FormJsTest, FormSubmitMainFrame) { |
| LoadHtml( |
| @"<form id='form1'>" |
| "<input type='password'>" |
| "<input type='submit' id='submit_input'/>" |
| "</form>"); |
| ASSERT_FALSE(observer_->submit_document_info()); |
| ExecuteJavaScript(@"document.getElementById('submit_input').click();"); |
| autofill::TestSubmitDocumentInfo* info = observer_->submit_document_info(); |
| ASSERT_TRUE(info); |
| EXPECT_EQ("form1", info->form_name); |
| } |
| |
| // Tests that focus event from same-origin iframe correctly delivered to |
| // WebStateObserver. |
| TEST_F(FormJsTest, 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 submit event from same-origin iframe correctly delivered to |
| // WebStateObserver. |
| TEST_F(FormJsTest, FormSameOriginIFrame) { |
| 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 a new form triggers form_changed event. |
| TEST_F(FormJsTest, AddForm) { |
| LoadHtml(@"<body></body>"); |
| |
| web::WebFrame* main_frame = WaitForMainFrame(); |
| ASSERT_TRUE(main_frame); |
| SetUpForUniqueIds(main_frame); |
| TrackFormMutations(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; |
| })); |
| EXPECT_EQ("form_changed", info->form_activity.type); |
| EXPECT_FALSE(info->form_activity.input_missing); |
| } |
| |
| // Tests that a new input element triggers form_changed event. |
| TEST_F(FormJsTest, AddInput) { |
| LoadHtml(@"<form id='formId'/>"); |
| |
| web::WebFrame* main_frame = WaitForMainFrame(); |
| ASSERT_TRUE(main_frame); |
| TrackFormMutations(main_frame); |
| |
| ExecuteJavaScript( |
| @"var input = document.createElement('input');" |
| @"document.getElementById('formId').appendChild(input);"); |
| autofill::TestFormActivityObserver* block_observer = observer_.get(); |
| __block autofill::TestFormActivityInfo* info = nil; |
| ASSERT_TRUE(WaitUntilConditionOrTimeout(kWaitForJSCompletionTimeout, ^{ |
| info = block_observer->form_activity_info(); |
| return info != nil; |
| })); |
| EXPECT_EQ("form_changed", info->form_activity.type); |
| EXPECT_FALSE(info->form_activity.input_missing); |
| } |
| |
| // Tests that a new select element triggers form_changed event. |
| TEST_F(FormJsTest, AddSelect) { |
| LoadHtml(@"<form id='formId'/>"); |
| |
| web::WebFrame* main_frame = WaitForMainFrame(); |
| ASSERT_TRUE(main_frame); |
| TrackFormMutations(main_frame); |
| |
| ExecuteJavaScript( |
| @"var select = document.createElement('select');" |
| @"document.getElementById('formId').appendChild(select);"); |
| autofill::TestFormActivityObserver* block_observer = observer_.get(); |
| __block autofill::TestFormActivityInfo* info = nil; |
| ASSERT_TRUE(WaitUntilConditionOrTimeout(kWaitForJSCompletionTimeout, ^{ |
| info = block_observer->form_activity_info(); |
| return info != nil; |
| })); |
| EXPECT_EQ("form_changed", info->form_activity.type); |
| EXPECT_FALSE(info->form_activity.input_missing); |
| } |
| |
| // Tests that a new option element triggers form_changed event. |
| TEST_F(FormJsTest, AddOption) { |
| LoadHtml( |
| @"<form>" |
| "<select id='select1'><option value='CA'>CA</option></select>" |
| "</form>"); |
| |
| web::WebFrame* main_frame = WaitForMainFrame(); |
| ASSERT_TRUE(main_frame); |
| TrackFormMutations(main_frame); |
| |
| ExecuteJavaScript( |
| @"var option = document.createElement('option');" |
| @"document.getElementById('select1').appendChild(option);"); |
| autofill::TestFormActivityObserver* block_observer = observer_.get(); |
| __block autofill::TestFormActivityInfo* info = nil; |
| ASSERT_TRUE(WaitUntilConditionOrTimeout(kWaitForJSCompletionTimeout, ^{ |
| info = block_observer->form_activity_info(); |
| return info != nil; |
| })); |
| EXPECT_EQ("form_changed", info->form_activity.type); |
| EXPECT_FALSE(info->form_activity.input_missing); |
| } |
| |
| // Tests that removing password form triggers 'password_form_removed" event. |
| TEST_F(FormJsTest, RemoveForm) { |
| LoadHtml(@"<form id=\"form1\">" |
| "<input type=\"text\" name=\"username\" id=\"id1\">" |
| "<input type=\"password\" name=\"password\" id=\"id2\">" |
| "<input type=\"submit\" id=\"submit_input\"/>" |
| "</form>"); |
| |
| web::WebFrame* main_frame = WaitForMainFrame(); |
| ASSERT_TRUE(main_frame); |
| SetUpForUniqueIds(main_frame); |
| TrackFormMutations(main_frame); |
| |
| ExecuteJavaScript(@"var form1 = document.getElementById('form1');" |
| @"__gCrWeb.fill.setUniqueIDIfNeeded(form1);" |
| @"form1.parentNode.removeChild(form1);"); |
| autofill::TestFormActivityObserver* block_observer = observer_.get(); |
| __block autofill::TestFormRemovalInfo* info = nil; |
| ASSERT_TRUE(WaitUntilConditionOrTimeout(kWaitForJSCompletionTimeout, ^{ |
| info = block_observer->form_removal_info(); |
| return info != nil; |
| })); |
| EXPECT_FALSE(info->form_removal_params.input_missing); |
| EXPECT_EQ(FormRendererId(1), info->form_removal_params.unique_form_id); |
| } |
| |
| // Tests that removing unowned password fields triggers 'password_form_removed" |
| // event. |
| TEST_F(FormJsTest, RemoveFormlessPasswordFields) { |
| LoadHtml(@"<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); |
| SetUpForUniqueIds(main_frame); |
| TrackFormMutations(main_frame); |
| |
| ExecuteJavaScript(@"var password = document.getElementById('pw');" |
| @"__gCrWeb.fill.setUniqueIDIfNeeded(password);" |
| @"password.parentNode.removeChild(password);"); |
| |
| autofill::TestFormActivityObserver* block_observer = observer_.get(); |
| __block autofill::TestFormRemovalInfo* info = nil; |
| ASSERT_TRUE(WaitUntilConditionOrTimeout(kWaitForJSCompletionTimeout, ^{ |
| info = block_observer->form_removal_info(); |
| return info != nil; |
| })); |
| EXPECT_FALSE(info->form_removal_params.input_missing); |
| EXPECT_FALSE(info->form_removal_params.unique_form_id); |
| std::vector<FieldRendererId> expected_removed_ids = {FieldRendererId(1)}; |
| EXPECT_EQ(info->form_removal_params.removed_unowned_fields, |
| expected_removed_ids); |
| } |
| |
| // Tests that a new element that contains 'form' in the tag name does not |
| // trigger a form_changed event. |
| TEST_F(FormJsTest, 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; |
| })); |
| } |