blob: 09e2776850202f6f38793b650a43622265ddd9b1 [file] [log] [blame]
// 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;
}));
}