summaryrefslogtreecommitdiff
path: root/chromium/chrome/renderer/autofill/password_generation_agent_browsertest.cc
diff options
context:
space:
mode:
Diffstat (limited to 'chromium/chrome/renderer/autofill/password_generation_agent_browsertest.cc')
-rw-r--r--chromium/chrome/renderer/autofill/password_generation_agent_browsertest.cc1193
1 files changed, 1193 insertions, 0 deletions
diff --git a/chromium/chrome/renderer/autofill/password_generation_agent_browsertest.cc b/chromium/chrome/renderer/autofill/password_generation_agent_browsertest.cc
new file mode 100644
index 00000000000..96a335c934a
--- /dev/null
+++ b/chromium/chrome/renderer/autofill/password_generation_agent_browsertest.cc
@@ -0,0 +1,1193 @@
+// Copyright 2013 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include <string.h>
+
+#include <memory>
+
+#include "base/bind.h"
+#include "base/macros.h"
+#include "base/run_loop.h"
+#include "base/strings/stringprintf.h"
+#include "base/strings/utf_string_conversions.h"
+#include "base/test/metrics/histogram_tester.h"
+#include "chrome/renderer/autofill/fake_mojo_password_manager_driver.h"
+#include "chrome/renderer/autofill/fake_password_generation_driver.h"
+#include "chrome/renderer/autofill/password_generation_test_utils.h"
+#include "chrome/test/base/chrome_render_view_test.h"
+#include "components/autofill/content/renderer/autofill_agent.h"
+#include "components/autofill/content/renderer/form_autofill_util.h"
+#include "components/autofill/content/renderer/password_generation_agent.h"
+#include "components/autofill/content/renderer/test_password_autofill_agent.h"
+#include "components/autofill/core/common/autofill_switches.h"
+#include "components/autofill/core/common/form_data.h"
+#include "components/autofill/core/common/password_generation_util.h"
+#include "components/password_manager/core/common/password_manager_features.h"
+#include "content/public/renderer/render_frame.h"
+#include "content/public/renderer/render_view.h"
+#include "services/service_manager/public/cpp/interface_provider.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "third_party/blink/public/common/associated_interfaces/associated_interface_provider.h"
+#include "third_party/blink/public/platform/web_string.h"
+#include "third_party/blink/public/web/web_document.h"
+#include "third_party/blink/public/web/web_local_frame.h"
+#include "third_party/blink/public/web/web_widget.h"
+#include "ui/events/keycodes/keyboard_codes.h"
+
+using autofill::mojom::FocusedFieldType;
+using base::ASCIIToUTF16;
+using blink::WebDocument;
+using blink::WebElement;
+using blink::WebInputElement;
+using blink::WebNode;
+using blink::WebString;
+using testing::_;
+using testing::AnyNumber;
+using testing::AtMost;
+
+namespace autofill {
+
+constexpr char kSigninFormHTML[] =
+ "<FORM name = 'blah' action = 'http://www.random.com/'> "
+ " <INPUT type = 'text' id = 'username'/> "
+ " <INPUT type = 'password' id = 'password'/> "
+ " <INPUT type = 'button' id = 'dummy'/> "
+ " <INPUT type = 'submit' value = 'LOGIN' />"
+ "</FORM>";
+
+constexpr char kAccountCreationFormHTML[] =
+ "<FORM id = 'blah' action = 'http://www.random.com/pa/th?q=1&p=3#first'> "
+ " <INPUT type = 'text' id = 'username'/> "
+ " <INPUT type = 'password' id = 'first_password' size = 5/>"
+ " <INPUT type = 'password' id = 'second_password' size = 5/> "
+ " <INPUT type = 'text' id = 'address'/> "
+ " <INPUT type = 'button' id = 'dummy'/> "
+ " <INPUT type = 'submit' value = 'LOGIN' />"
+ "</FORM>";
+
+constexpr char kAccountCreationNoForm[] =
+ "<INPUT type = 'text' id = 'username'/> "
+ "<INPUT type = 'password' id = 'first_password' size = 5/>"
+ "<INPUT type = 'password' id = 'second_password' size = 5/> "
+ "<INPUT type = 'text' id = 'address'/> "
+ "<INPUT type = 'button' id = 'dummy'/> "
+ "<INPUT type = 'submit' value = 'LOGIN' />";
+
+constexpr char kAccountCreationNoIds[] =
+ "<FORM action = 'http://www.random.com/pa/th?q=1&p=3#first'> "
+ " <INPUT type = 'text'/> "
+ " <INPUT type = 'password' class='first_password'/>"
+ " <INPUT type = 'password' class='second_password'/> "
+ " <INPUT type = 'text'/> "
+ " <INPUT type = 'button' id = 'dummy'/> "
+ " <INPUT type = 'submit' value = 'LOGIN'/>"
+ "</FORM>";
+
+constexpr char kDisabledElementAccountCreationFormHTML[] =
+ "<FORM name = 'blah' action = 'http://www.random.com/'> "
+ " <INPUT type = 'text' id = 'username'/> "
+ " <INPUT type = 'password' id = 'first_password' "
+ " autocomplete = 'off' size = 5/>"
+ " <INPUT type = 'password' id = 'second_password' size = 5/> "
+ " <INPUT type = 'text' id = 'address'/> "
+ " <INPUT type = 'text' id = 'disabled' disabled/> "
+ " <INPUT type = 'button' id = 'dummy'/> "
+ " <INPUT type = 'submit' value = 'LOGIN' />"
+ "</FORM>";
+
+constexpr char kHiddenPasswordAccountCreationFormHTML[] =
+ "<FORM name = 'blah' action = 'http://www.random.com/'> "
+ " <INPUT type = 'text' id = 'username'/> "
+ " <INPUT type = 'password' id = 'first_password'/> "
+ " <INPUT type = 'password' id = 'second_password' style='display:none'/> "
+ " <INPUT type = 'button' id = 'dummy'/> "
+ " <INPUT type = 'submit' value = 'LOGIN' />"
+ "</FORM>";
+
+constexpr char kMultipleAccountCreationFormHTML[] =
+ "<FORM name = 'login' action = 'http://www.random.com/'> "
+ " <INPUT type = 'text' id = 'random'/> "
+ " <INPUT type = 'text' id = 'username'/> "
+ " <INPUT type = 'password' id = 'password'/> "
+ " <INPUT type = 'button' id = 'dummy'/> "
+ " <INPUT type = 'submit' value = 'LOGIN' />"
+ "</FORM>"
+ "<FORM name = 'signup' action = 'http://www.random.com/signup'> "
+ " <INPUT type = 'text' id = 'username'/> "
+ " <INPUT type = 'password' id = 'first_password' "
+ " autocomplete = 'off' size = 5/>"
+ " <INPUT type = 'password' id = 'second_password' size = 5/> "
+ " <INPUT type = 'text' id = 'address'/> "
+ " <INPUT type = 'submit' value = 'LOGIN' />"
+ "</FORM>";
+
+constexpr char kPasswordChangeFormHTML[] =
+ "<FORM name = 'ChangeWithUsernameForm' action = 'http://www.bidule.com'> "
+ " <INPUT type = 'text' id = 'username'/> "
+ " <INPUT type = 'password' id = 'password'/> "
+ " <INPUT type = 'password' id = 'newpassword'/> "
+ " <INPUT type = 'password' id = 'confirmpassword'/> "
+ " <INPUT type = 'button' id = 'dummy'/> "
+ " <INPUT type = 'submit' value = 'Login'/> "
+ "</FORM>";
+
+constexpr char kPasswordFormAndSpanHTML[] =
+ "<FORM name = 'blah' action = 'http://www.random.com/pa/th?q=1&p=3#first'>"
+ " <INPUT type = 'text' id = 'username'/> "
+ " <INPUT type = 'password' id = 'password'/> "
+ " <INPUT type = 'button' id = 'dummy'/> "
+ "</FORM>"
+ "<SPAN id='span'>Text to click on</SPAN>";
+
+class PasswordGenerationAgentTest : public ChromeRenderViewTest {
+ public:
+ enum AutomaticGenerationStatus {
+ kNotReported,
+ kAvailable,
+ };
+ enum class GenerationAvailableForFormStatus {
+ kAvailable,
+ kUnavailable,
+ };
+
+ PasswordGenerationAgentTest() = default;
+
+ // ChromeRenderViewTest:
+ void RegisterMainFrameRemoteInterfaces() override;
+ void SetUp() override;
+ void TearDown() override;
+
+ void LoadHTMLWithUserGesture(const char* html);
+ void FocusField(const char* element_id);
+ void ExpectAutomaticGenerationAvailable(const char* element_id,
+ AutomaticGenerationStatus available);
+ void ExpectGenerationElementLostFocus(const char* new_element_id);
+ void ExpectFormClassifierVoteReceived(
+ bool received,
+ const base::string16& expected_generation_element);
+ void SelectGenerationFallbackAndExpect(bool available);
+
+ void BindPasswordManagerDriver(mojo::ScopedInterfaceEndpointHandle handle);
+ void BindPasswordManagerClient(mojo::ScopedInterfaceEndpointHandle handle);
+
+ // Callback for UserTriggeredGeneratePassword.
+ MOCK_METHOD1(UserTriggeredGeneratePasswordReply,
+ void(const base::Optional<
+ autofill::password_generation::PasswordGenerationUIData>&));
+
+ FakeMojoPasswordManagerDriver fake_driver_;
+ testing::StrictMock<FakePasswordGenerationDriver> fake_pw_client_;
+
+ private:
+ DISALLOW_COPY_AND_ASSIGN(PasswordGenerationAgentTest);
+};
+
+void PasswordGenerationAgentTest::RegisterMainFrameRemoteInterfaces() {
+ // Because the test cases only involve the main frame in this test,
+ // the fake password client is only used for the main frame.
+ blink::AssociatedInterfaceProvider* remote_associated_interfaces =
+ view_->GetMainRenderFrame()->GetRemoteAssociatedInterfaces();
+ remote_associated_interfaces->OverrideBinderForTesting(
+ mojom::PasswordGenerationDriver::Name_,
+ base::BindRepeating(
+ &PasswordGenerationAgentTest::BindPasswordManagerClient,
+ base::Unretained(this)));
+ remote_associated_interfaces->OverrideBinderForTesting(
+ mojom::PasswordManagerDriver::Name_,
+ base::BindRepeating(
+ &PasswordGenerationAgentTest::BindPasswordManagerDriver,
+ base::Unretained(this)));
+}
+
+void PasswordGenerationAgentTest::SetUp() {
+ ChromeRenderViewTest::SetUp();
+
+ // TODO(crbug/862989): Remove workaround preventing non-test classes to bind
+ // fake_driver_ or fake_pw_client_.
+ password_autofill_agent_->GetPasswordManagerDriver();
+ password_generation_->RequestPasswordManagerClientForTesting();
+ base::RunLoop().RunUntilIdle(); // Executes binding the interfaces.
+ // Reject all requests to bind driver/client to anything but the test class:
+ view_->GetMainRenderFrame()
+ ->GetRemoteAssociatedInterfaces()
+ ->OverrideBinderForTesting(
+ mojom::PasswordGenerationDriver::Name_,
+ base::BindRepeating([](mojo::ScopedInterfaceEndpointHandle handle) {
+ handle.reset();
+ }));
+ view_->GetMainRenderFrame()
+ ->GetRemoteAssociatedInterfaces()
+ ->OverrideBinderForTesting(
+ mojom::PasswordManagerDriver::Name_,
+ base::BindRepeating([](mojo::ScopedInterfaceEndpointHandle handle) {
+ handle.reset();
+ }));
+
+ // Necessary for focus changes to work correctly and dispatch blur events
+ // when a field was previously focused.
+ GetWebWidget()->SetFocus(true);
+}
+
+void PasswordGenerationAgentTest::TearDown() {
+ // Unloading the document may trigger the event.
+ EXPECT_CALL(fake_pw_client_, GenerationElementLostFocus()).Times(AtMost(1));
+ ChromeRenderViewTest::TearDown();
+}
+
+void PasswordGenerationAgentTest::LoadHTMLWithUserGesture(const char* html) {
+ LoadHTML(html);
+
+ // Enable show-ime event when element is focused by indicating that a user
+ // gesture has been processed since load.
+ EXPECT_TRUE(SimulateElementClick("dummy"));
+}
+
+void PasswordGenerationAgentTest::FocusField(const char* element_id) {
+ WebDocument document = GetMainFrame()->GetDocument();
+ blink::WebElement element =
+ document.GetElementById(blink::WebString::FromUTF8(element_id));
+ ASSERT_FALSE(element.IsNull());
+ ExecuteJavaScriptForTests(
+ base::StringPrintf("document.getElementById('%s').focus();", element_id)
+ .c_str());
+}
+
+void PasswordGenerationAgentTest::ExpectAutomaticGenerationAvailable(
+ const char* element_id,
+ AutomaticGenerationStatus status) {
+ SCOPED_TRACE(testing::Message()
+ << "element_id = " << element_id << "available = " << status);
+ if (status == kNotReported) {
+ EXPECT_CALL(fake_pw_client_, AutomaticGenerationAvailable(_)).Times(0);
+ } else {
+ EXPECT_CALL(fake_pw_client_, AutomaticGenerationAvailable(_));
+ }
+
+ FocusField(element_id);
+ base::RunLoop().RunUntilIdle();
+ testing::Mock::VerifyAndClearExpectations(&fake_pw_client_);
+
+ // Check that aria-autocomplete attribute is set correctly.
+ if (status == kAvailable) {
+ WebDocument doc = GetMainFrame()->GetDocument();
+ WebElement element = doc.GetElementById(WebString::FromUTF8(element_id));
+ EXPECT_EQ("list", element.GetAttribute("aria-autocomplete"));
+ }
+}
+
+void PasswordGenerationAgentTest::ExpectGenerationElementLostFocus(
+ const char* new_element_id) {
+ EXPECT_CALL(fake_pw_client_, GenerationElementLostFocus());
+ FocusField(new_element_id);
+ base::RunLoop().RunUntilIdle();
+ testing::Mock::VerifyAndClearExpectations(&fake_pw_client_);
+}
+
+void PasswordGenerationAgentTest::ExpectFormClassifierVoteReceived(
+ bool received,
+ const base::string16& expected_generation_element) {
+ base::RunLoop().RunUntilIdle();
+ if (received) {
+ ASSERT_TRUE(fake_driver_.called_save_generation_field());
+ EXPECT_EQ(expected_generation_element,
+ fake_driver_.save_generation_field());
+ } else {
+ ASSERT_FALSE(fake_driver_.called_save_generation_field());
+ }
+
+ fake_driver_.reset_save_generation_field();
+}
+
+void PasswordGenerationAgentTest::SelectGenerationFallbackAndExpect(
+ bool available) {
+ if (available) {
+ EXPECT_CALL(*this,
+ UserTriggeredGeneratePasswordReply(testing::Ne(base::nullopt)));
+ } else {
+ EXPECT_CALL(*this,
+ UserTriggeredGeneratePasswordReply(testing::Eq(base::nullopt)));
+ }
+ password_generation_->UserTriggeredGeneratePassword(base::BindOnce(
+ &PasswordGenerationAgentTest::UserTriggeredGeneratePasswordReply,
+ base::Unretained(this)));
+ testing::Mock::VerifyAndClearExpectations(this);
+}
+
+void PasswordGenerationAgentTest::BindPasswordManagerDriver(
+ mojo::ScopedInterfaceEndpointHandle handle) {
+ fake_driver_.BindReceiver(
+ mojo::PendingAssociatedReceiver<mojom::PasswordManagerDriver>(
+ std::move(handle)));
+}
+
+void PasswordGenerationAgentTest::BindPasswordManagerClient(
+ mojo::ScopedInterfaceEndpointHandle handle) {
+ fake_pw_client_.BindReceiver(
+ mojo::PendingAssociatedReceiver<mojom::PasswordGenerationDriver>(
+ std::move(handle)));
+}
+
+class PasswordGenerationAgentTestForHtmlAnnotation
+ : public PasswordGenerationAgentTest {
+ public:
+ PasswordGenerationAgentTestForHtmlAnnotation() = default;
+
+ void SetUp() override {
+ base::CommandLine::ForCurrentProcess()->AppendSwitch(
+ switches::kShowAutofillSignatures);
+ PasswordGenerationAgentTest::SetUp();
+ }
+
+ void TestAnnotateForm(bool has_form_tag);
+
+ private:
+ DISALLOW_COPY_AND_ASSIGN(PasswordGenerationAgentTestForHtmlAnnotation);
+};
+
+void PasswordGenerationAgentTestForHtmlAnnotation::TestAnnotateForm(
+ bool has_form_tag) {
+ SCOPED_TRACE(testing::Message() << "has_form_tag = " << has_form_tag);
+ const char* kHtmlForm =
+ has_form_tag ? kAccountCreationFormHTML : kAccountCreationNoForm;
+ LoadHTMLWithUserGesture(kHtmlForm);
+ SetFoundFormEligibleForGeneration(
+ password_generation_, GetMainFrame()->GetDocument(),
+ "first_password" /* new_passwod_id */, nullptr /* confirm_password_id*/);
+ ExpectAutomaticGenerationAvailable("first_password", kAvailable);
+ WebDocument document = GetMainFrame()->GetDocument();
+
+ const char* kFormSignature =
+ has_form_tag ? "3524919054660658462" : "7671707438749847833";
+ if (has_form_tag) {
+ // Check the form signature is set in the <form> tag.
+ blink::WebElement form_element =
+ document.GetElementById(blink::WebString::FromUTF8("blah"));
+ ASSERT_FALSE(form_element.IsNull());
+ blink::WebString form_signature =
+ form_element.GetAttribute(blink::WebString::FromUTF8("form_signature"));
+ ASSERT_FALSE(form_signature.IsNull());
+ EXPECT_EQ(kFormSignature, form_signature.Ascii());
+ }
+
+ // Check field signatures and form signature are set in the <input>s.
+ blink::WebElement username_element =
+ document.GetElementById(blink::WebString::FromUTF8("username"));
+ ASSERT_FALSE(username_element.IsNull());
+ blink::WebString username_signature = username_element.GetAttribute(
+ blink::WebString::FromUTF8("field_signature"));
+ ASSERT_FALSE(username_signature.IsNull());
+ EXPECT_EQ("239111655", username_signature.Ascii());
+ blink::WebString form_signature_in_username = username_element.GetAttribute(
+ blink::WebString::FromUTF8("form_signature"));
+ EXPECT_EQ(kFormSignature, form_signature_in_username.Ascii());
+
+ blink::WebElement password_element =
+ document.GetElementById(blink::WebString::FromUTF8("first_password"));
+ ASSERT_FALSE(password_element.IsNull());
+ blink::WebString password_signature = password_element.GetAttribute(
+ blink::WebString::FromUTF8("field_signature"));
+ ASSERT_FALSE(password_signature.IsNull());
+ EXPECT_EQ("3933215845", password_signature.Ascii());
+ blink::WebString form_signature_in_password = password_element.GetAttribute(
+ blink::WebString::FromUTF8("form_signature"));
+ EXPECT_EQ(kFormSignature, form_signature_in_password.Ascii());
+
+ // Check the generation element is marked.
+ blink::WebString generation_mark = password_element.GetAttribute(
+ blink::WebString::FromUTF8("password_creation_field"));
+ ASSERT_FALSE(generation_mark.IsNull());
+ EXPECT_EQ("1", generation_mark.Utf8());
+
+ blink::WebElement confirmation_password_element =
+ document.GetElementById(blink::WebString::FromUTF8("second_password"));
+}
+
+TEST_F(PasswordGenerationAgentTest, HiddenSecondPasswordDetectionTest) {
+ // Hidden fields are not treated differently.
+ LoadHTMLWithUserGesture(kHiddenPasswordAccountCreationFormHTML);
+ SetFoundFormEligibleForGeneration(
+ password_generation_, GetMainFrame()->GetDocument(),
+ "first_password" /* new_passwod_id */, nullptr /* confirm_password_id*/);
+ ExpectAutomaticGenerationAvailable("first_password", kAvailable);
+}
+
+TEST_F(PasswordGenerationAgentTest, DetectionTestNoForm) {
+ LoadHTMLWithUserGesture(kAccountCreationNoForm);
+ SetFoundFormEligibleForGeneration(
+ password_generation_, GetMainFrame()->GetDocument(),
+ "first_password" /* new_passwod_id */, nullptr /* confirm_password_id*/);
+
+ ExpectAutomaticGenerationAvailable("first_password", kAvailable);
+ ExpectGenerationElementLostFocus("second_password");
+}
+
+TEST_F(PasswordGenerationAgentTest, FillTest) {
+ // Add event listeners for password fields.
+ std::vector<base::string16> variables_to_check;
+ std::string events_registration_script =
+ CreateScriptToRegisterListeners("first_password", &variables_to_check) +
+ CreateScriptToRegisterListeners("second_password", &variables_to_check);
+
+ // Make sure that we are enabled before loading HTML.
+ std::string html =
+ std::string(kAccountCreationFormHTML) + events_registration_script;
+ // Begin with no gesture and therefore no focused element.
+ LoadHTMLWithUserGesture(html.c_str());
+ WebDocument document = GetMainFrame()->GetDocument();
+ SetFoundFormEligibleForGeneration(password_generation_,
+ GetMainFrame()->GetDocument(),
+ "first_password" /* new_passwod_id */,
+ "second_password" /* confirm_password_id*/);
+ ExpectAutomaticGenerationAvailable("first_password", kAvailable);
+
+ WebElement element =
+ document.GetElementById(WebString::FromUTF8("first_password"));
+ ASSERT_FALSE(element.IsNull());
+ WebInputElement first_password_element = element.To<WebInputElement>();
+ element = document.GetElementById(WebString::FromUTF8("second_password"));
+ ASSERT_FALSE(element.IsNull());
+ WebInputElement second_password_element = element.To<WebInputElement>();
+
+ // Both password fields should be empty.
+ EXPECT_TRUE(first_password_element.Value().IsNull());
+ EXPECT_TRUE(second_password_element.Value().IsNull());
+
+ base::string16 password = base::ASCIIToUTF16("random_password");
+ EXPECT_CALL(fake_pw_client_,
+ PresaveGeneratedPassword(testing::Field(
+ &autofill::PasswordForm::password_value, password)));
+
+ password_generation_->GeneratedPasswordAccepted(password);
+
+ // Password fields are filled out and set as being autofilled.
+ EXPECT_EQ(password, first_password_element.Value().Utf16());
+ EXPECT_EQ(password, second_password_element.Value().Utf16());
+ EXPECT_TRUE(first_password_element.IsAutofilled());
+ EXPECT_TRUE(second_password_element.IsAutofilled());
+
+ // Make sure all events are called.
+ for (const base::string16& variable : variables_to_check) {
+ int value;
+ EXPECT_TRUE(ExecuteJavaScriptAndReturnIntValue(variable, &value));
+ EXPECT_EQ(1, value) << variable;
+ }
+
+ // Check that focus returns to previously focused element.
+ element = document.GetElementById(WebString::FromUTF8("address"));
+ ASSERT_FALSE(element.IsNull());
+ EXPECT_EQ(element, document.FocusedElement());
+}
+
+TEST_F(PasswordGenerationAgentTest, EditingTest) {
+ LoadHTMLWithUserGesture(kAccountCreationFormHTML);
+ SetFoundFormEligibleForGeneration(password_generation_,
+ GetMainFrame()->GetDocument(),
+ "first_password" /* new_passwod_id */,
+ "second_password" /* confirm_password_id*/);
+ ExpectAutomaticGenerationAvailable("first_password", kAvailable);
+
+ WebDocument document = GetMainFrame()->GetDocument();
+ WebElement element =
+ document.GetElementById(WebString::FromUTF8("first_password"));
+ ASSERT_FALSE(element.IsNull());
+ WebInputElement first_password_element = element.To<WebInputElement>();
+ element = document.GetElementById(WebString::FromUTF8("second_password"));
+ ASSERT_FALSE(element.IsNull());
+ WebInputElement second_password_element = element.To<WebInputElement>();
+
+ base::string16 password = base::ASCIIToUTF16("random_password");
+ EXPECT_CALL(fake_pw_client_,
+ PresaveGeneratedPassword(testing::Field(
+ &autofill::PasswordForm::password_value, password)));
+
+ password_generation_->GeneratedPasswordAccepted(password);
+
+ // Passwords start out the same.
+ EXPECT_EQ(password, first_password_element.Value().Utf16());
+ EXPECT_EQ(password, second_password_element.Value().Utf16());
+
+ // After editing the first field they are still the same.
+ std::string edited_password_ascii = "edited_password";
+ base::string16 edited_password = base::ASCIIToUTF16(edited_password_ascii);
+ EXPECT_CALL(fake_pw_client_,
+ PresaveGeneratedPassword(testing::Field(
+ &autofill::PasswordForm::password_value, edited_password)));
+ EXPECT_CALL(fake_pw_client_, ShowPasswordEditingPopup(_, _, _));
+ SimulateUserInputChangeForElement(&first_password_element,
+ edited_password_ascii);
+ EXPECT_EQ(edited_password, first_password_element.Value().Utf16());
+ EXPECT_EQ(edited_password, second_password_element.Value().Utf16());
+ EXPECT_TRUE(first_password_element.IsAutofilled());
+ EXPECT_TRUE(second_password_element.IsAutofilled());
+
+ // Verify that password mirroring works correctly even when the password
+ // is deleted.
+ EXPECT_CALL(fake_pw_client_, PasswordNoLongerGenerated(_));
+ EXPECT_CALL(fake_pw_client_, AutomaticGenerationAvailable(_));
+ SimulateUserInputChangeForElement(&first_password_element, std::string());
+ EXPECT_EQ(base::string16(), first_password_element.Value().Utf16());
+ EXPECT_EQ(base::string16(), second_password_element.Value().Utf16());
+ EXPECT_FALSE(first_password_element.IsAutofilled());
+ EXPECT_FALSE(second_password_element.IsAutofilled());
+}
+
+TEST_F(PasswordGenerationAgentTest, EditingEventsTest) {
+ LoadHTMLWithUserGesture(kAccountCreationFormHTML);
+ SetFoundFormEligibleForGeneration(
+ password_generation_, GetMainFrame()->GetDocument(),
+ "first_password" /* new_passwod_id */, nullptr /* confirm_password_id*/);
+
+ // Generate password.
+ ExpectAutomaticGenerationAvailable("first_password", kAvailable);
+ base::string16 password = base::ASCIIToUTF16("random_password");
+ EXPECT_CALL(fake_pw_client_,
+ PresaveGeneratedPassword(testing::Field(
+ &autofill::PasswordForm::password_value, password)));
+ password_generation_->GeneratedPasswordAccepted(password);
+ fake_pw_client_.Flush();
+ testing::Mock::VerifyAndClearExpectations(&fake_pw_client_);
+
+ // Start removing characters one by one and observe the events sent to the
+ // browser.
+ EXPECT_CALL(fake_pw_client_, ShowPasswordEditingPopup(_, _, _));
+ FocusField("first_password");
+ fake_pw_client_.Flush();
+ testing::Mock::VerifyAndClearExpectations(&fake_pw_client_);
+ size_t max_chars_to_delete_before_editing =
+ password.length() -
+ PasswordGenerationAgent::kMinimumLengthForEditedPassword;
+ for (size_t i = 0; i < max_chars_to_delete_before_editing; ++i) {
+ password.erase(password.end() - 1);
+ EXPECT_CALL(fake_pw_client_,
+ PresaveGeneratedPassword(testing::Field(
+ &autofill::PasswordForm::password_value, password)));
+ SimulateUserTypingASCIICharacter(ui::VKEY_BACK, true);
+ fake_pw_client_.Flush();
+ fake_driver_.Flush();
+ EXPECT_EQ(FocusedFieldType::kFillablePasswordField,
+ fake_driver_.last_focused_field_type());
+ testing::Mock::VerifyAndClearExpectations(&fake_pw_client_);
+ }
+
+ // Delete one more character and move back to the generation state.
+ EXPECT_CALL(fake_pw_client_, PasswordNoLongerGenerated(_));
+ EXPECT_CALL(fake_pw_client_, AutomaticGenerationAvailable(_));
+ SimulateUserTypingASCIICharacter(ui::VKEY_BACK, true);
+ fake_pw_client_.Flush();
+ // Last focused element shouldn't change while editing.
+ fake_driver_.Flush();
+ EXPECT_EQ(FocusedFieldType::kFillablePasswordField,
+ fake_driver_.last_focused_field_type());
+}
+
+TEST_F(PasswordGenerationAgentTest, UnblacklistedMultipleTest) {
+ // Receive two not blacklisted messages, one is for account creation form and
+ // the other is not. Show password generation icon.
+ LoadHTMLWithUserGesture(kAccountCreationFormHTML);
+ SetFoundFormEligibleForGeneration(
+ password_generation_, GetMainFrame()->GetDocument(),
+ "first_password" /* new_passwod_id */, nullptr /* confirm_password_id*/);
+ ExpectAutomaticGenerationAvailable("first_password", kAvailable);
+}
+
+TEST_F(PasswordGenerationAgentTest, AccountCreationFormsDetectedTest) {
+ // Did not receive account creation forms detected message. Don't show
+ // password generation icon.
+ LoadHTMLWithUserGesture(kAccountCreationFormHTML);
+
+ // Receive the account creation forms detected message. Show password
+ // generation icon.
+ LoadHTMLWithUserGesture(kAccountCreationFormHTML);
+ SetFoundFormEligibleForGeneration(
+ password_generation_, GetMainFrame()->GetDocument(),
+ "first_password" /* new_passwod_id */, nullptr /* confirm_password_id*/);
+ ExpectAutomaticGenerationAvailable("first_password", kAvailable);
+}
+
+TEST_F(PasswordGenerationAgentTest, MaximumCharsForGenerationOffer) {
+ base::HistogramTester histogram_tester;
+
+ LoadHTMLWithUserGesture(kAccountCreationFormHTML);
+ SetFoundFormEligibleForGeneration(
+ password_generation_, GetMainFrame()->GetDocument(),
+ "first_password" /* new_passwod_id */, nullptr /* confirm_password_id*/);
+ // There should now be a message to show the UI.
+ ExpectAutomaticGenerationAvailable("first_password", kAvailable);
+
+ WebDocument document = GetMainFrame()->GetDocument();
+ WebElement element =
+ document.GetElementById(WebString::FromUTF8("first_password"));
+ ASSERT_FALSE(element.IsNull());
+ WebInputElement first_password_element = element.To<WebInputElement>();
+
+ // Make a password just under maximum offer size.
+ // Due to implementation details it's OK to get one more trigger for the
+ // automatic generation.
+ EXPECT_CALL(fake_pw_client_, AutomaticGenerationAvailable(_))
+ .Times(AtMost(1));
+ SimulateUserInputChangeForElement(
+ &first_password_element,
+ std::string(PasswordGenerationAgent::kMaximumCharsForGenerationOffer,
+ 'a'));
+ testing::Mock::VerifyAndClearExpectations(&fake_pw_client_);
+
+ // Simulate a user typing a password just over maximum offer size.
+ EXPECT_CALL(fake_pw_client_, PasswordGenerationRejectedByTyping());
+ SimulateUserTypingASCIICharacter('a', true);
+ // There should now be a message that generation was rejected.
+ fake_pw_client_.Flush();
+
+ // Simulate the user deleting characters. The generation popup should be
+ // shown again.
+ EXPECT_CALL(fake_pw_client_, AutomaticGenerationAvailable(_));
+ SimulateUserTypingASCIICharacter(ui::VKEY_BACK, true);
+ fake_pw_client_.Flush();
+ testing::Mock::VerifyAndClearExpectations(&fake_pw_client_);
+
+ // Change focus. Bubble should be hidden.
+ EXPECT_CALL(fake_pw_client_, GenerationElementLostFocus());
+ ExecuteJavaScriptForTests("document.getElementById('username').focus();");
+ fake_pw_client_.Flush();
+
+ // Focusing the password field will bring up the generation UI again.
+ EXPECT_CALL(fake_pw_client_, AutomaticGenerationAvailable(_));
+ ExecuteJavaScriptForTests(
+ "document.getElementById('first_password').focus();");
+ fake_pw_client_.Flush();
+ testing::Mock::VerifyAndClearExpectations(&fake_pw_client_);
+
+ // Loading a different page triggers UMA stat upload. Verify that only one
+ // display event is sent.
+ EXPECT_CALL(fake_pw_client_, GenerationElementLostFocus());
+ LoadHTMLWithUserGesture(kSigninFormHTML);
+
+ histogram_tester.ExpectBucketCount(
+ "PasswordGeneration.Event",
+ autofill::password_generation::GENERATION_POPUP_SHOWN, 1);
+}
+
+TEST_F(PasswordGenerationAgentTest, MinimumLengthForEditedPassword) {
+ LoadHTMLWithUserGesture(kAccountCreationFormHTML);
+ SetFoundFormEligibleForGeneration(
+ password_generation_, GetMainFrame()->GetDocument(),
+ "first_password" /* new_passwod_id */, nullptr /* confirm_password_id*/);
+ ExpectAutomaticGenerationAvailable("first_password", kAvailable);
+
+ // Generate a new password.
+ base::string16 password = base::ASCIIToUTF16("random_password");
+
+ EXPECT_CALL(fake_pw_client_,
+ PresaveGeneratedPassword(testing::Field(
+ &autofill::PasswordForm::password_value, password)));
+ password_generation_->GeneratedPasswordAccepted(password);
+ fake_pw_client_.Flush();
+ testing::Mock::VerifyAndClearExpectations(&fake_pw_client_);
+
+ // Delete most of the password.
+ EXPECT_CALL(fake_pw_client_, ShowPasswordEditingPopup(_, _, _));
+ EXPECT_CALL(fake_pw_client_, AutomaticGenerationAvailable(_)).Times(0);
+ FocusField("first_password");
+ size_t max_chars_to_delete =
+ password.length() -
+ PasswordGenerationAgent::kMinimumLengthForEditedPassword;
+ EXPECT_CALL(fake_pw_client_, PresaveGeneratedPassword(testing::_))
+ .Times(testing::AtLeast(1));
+ for (size_t i = 0; i < max_chars_to_delete; ++i)
+ SimulateUserTypingASCIICharacter(ui::VKEY_BACK, false);
+ fake_pw_client_.Flush();
+ testing::Mock::VerifyAndClearExpectations(&fake_pw_client_);
+
+ // Delete one more character. The state should move to offering generation.
+ EXPECT_CALL(fake_pw_client_, AutomaticGenerationAvailable(_));
+ EXPECT_CALL(fake_pw_client_, PasswordNoLongerGenerated(testing::_));
+ SimulateUserTypingASCIICharacter(ui::VKEY_BACK, true);
+ fake_pw_client_.Flush();
+ testing::Mock::VerifyAndClearExpectations(&fake_pw_client_);
+
+ // The first password field is still non empty. The second one should be
+ // cleared.
+ WebDocument document = GetMainFrame()->GetDocument();
+ WebElement element =
+ document.GetElementById(WebString::FromUTF8("first_password"));
+ ASSERT_FALSE(element.IsNull());
+ WebInputElement first_password_element = element.To<WebInputElement>();
+ element = document.GetElementById(WebString::FromUTF8("second_password"));
+ ASSERT_FALSE(element.IsNull());
+ WebInputElement second_password_element = element.To<WebInputElement>();
+ EXPECT_NE(base::string16(), first_password_element.Value().Utf16());
+ EXPECT_EQ(base::string16(), second_password_element.Value().Utf16());
+}
+
+TEST_F(PasswordGenerationAgentTest, DynamicFormTest) {
+ LoadHTMLWithUserGesture(kSigninFormHTML);
+
+ ExecuteJavaScriptForTests(
+ "var form = document.createElement('form');"
+ "form.action='http://www.random.com';"
+ "var username = document.createElement('input');"
+ "username.type = 'text';"
+ "username.id = 'dynamic_username';"
+ "var first_password = document.createElement('input');"
+ "first_password.type = 'password';"
+ "first_password.id = 'first_password';"
+ "first_password.name = 'first_password';"
+ "var second_password = document.createElement('input');"
+ "second_password.type = 'password';"
+ "second_password.id = 'second_password';"
+ "second_password.name = 'second_password';"
+ "form.appendChild(username);"
+ "form.appendChild(first_password);"
+ "form.appendChild(second_password);"
+ "document.body.appendChild(form);");
+ WaitForAutofillDidAssociateFormControl();
+
+ // This needs to come after the DOM has been modified.
+ SetFoundFormEligibleForGeneration(
+ password_generation_, GetMainFrame()->GetDocument(),
+ "first_password" /* new_passwod_id */, nullptr /* confirm_password_id*/);
+
+ // TODO(gcasto): I'm slightly worried about flakes in this test where
+ // didAssociateFormControls() isn't called. If this turns out to be a problem
+ // adding a call to OnDynamicFormsSeen(GetMainFrame()) will fix it, though
+ // it will weaken the test.
+ ExpectAutomaticGenerationAvailable("first_password", kAvailable);
+}
+
+// Losing focus should not trigger a password generation popup.
+TEST_F(PasswordGenerationAgentTest, BlurTest) {
+ LoadHTMLWithUserGesture(kDisabledElementAccountCreationFormHTML);
+ SetFoundFormEligibleForGeneration(
+ password_generation_, GetMainFrame()->GetDocument(),
+ "first_password" /* new_passwod_id */, nullptr /* confirm_password_id*/);
+
+ // Focus on the first password field: password generation popup should show
+ // up.
+ ExpectAutomaticGenerationAvailable("first_password", kAvailable);
+
+ // Remove focus from everywhere by clicking an unfocusable element: password
+ // generation popup should not show up.
+ EXPECT_CALL(fake_pw_client_, GenerationElementLostFocus());
+ EXPECT_TRUE(SimulateElementClick("disabled"));
+ fake_pw_client_.Flush();
+}
+
+TEST_F(PasswordGenerationAgentTest, ChangePasswordFormDetectionTest) {
+ // Verify that generation is shown on correct field after message receiving.
+ LoadHTMLWithUserGesture(kPasswordChangeFormHTML);
+ ExpectAutomaticGenerationAvailable("password", kNotReported);
+ ExpectAutomaticGenerationAvailable("newpassword", kNotReported);
+ ExpectAutomaticGenerationAvailable("confirmpassword", kNotReported);
+
+ SetFoundFormEligibleForGeneration(password_generation_,
+ GetMainFrame()->GetDocument(),
+ "newpassword" /* new_passwod_id */,
+ "confirmpassword" /* confirm_password_id*/);
+ ExpectAutomaticGenerationAvailable("password", kNotReported);
+ ExpectAutomaticGenerationAvailable("newpassword", kAvailable);
+ ExpectGenerationElementLostFocus("confirmpassword");
+}
+
+TEST_F(PasswordGenerationAgentTest, ManualGenerationInFormTest) {
+ LoadHTMLWithUserGesture(kSigninFormHTML);
+ SimulateElementRightClick("password");
+ SelectGenerationFallbackAndExpect(true);
+ // Re-focusing a password field for which manual generation was requested
+ // should not re-trigger generation.
+ ExpectAutomaticGenerationAvailable("password", kNotReported);
+}
+
+TEST_F(PasswordGenerationAgentTest, ManualGenerationNoFormTest) {
+ LoadHTMLWithUserGesture(kAccountCreationNoForm);
+ SimulateElementRightClick("first_password");
+ SelectGenerationFallbackAndExpect(true);
+}
+
+TEST_F(PasswordGenerationAgentTest, ManualGenerationDoesntSuppressAutomatic) {
+ LoadHTMLWithUserGesture(kAccountCreationFormHTML);
+ SetFoundFormEligibleForGeneration(
+ password_generation_, GetMainFrame()->GetDocument(),
+ "first_password" /* new_passwod_id */, nullptr /* confirm_password_id*/);
+ ExpectAutomaticGenerationAvailable("first_password", kAvailable);
+ // The browser may show a standard password dropdown with the "Generate"
+ // option. In this case manual generation is triggered.
+ SelectGenerationFallbackAndExpect(true);
+
+ // Move the focus away to somewhere.
+ ExpectGenerationElementLostFocus("address");
+
+ // Moving the focus back should trigger the automatic generation again.
+ ExpectAutomaticGenerationAvailable("first_password", kAvailable);
+}
+
+TEST_F(PasswordGenerationAgentTest, ManualGenerationNoIds) {
+ LoadHTMLWithUserGesture(kAccountCreationNoIds);
+ WebDocument document = GetMainFrame()->GetDocument();
+
+ ExecuteJavaScriptForTests(
+ "document.getElementsByClassName('first_password')[0].focus();");
+ WebInputElement first_password_element =
+ document.FocusedElement().To<WebInputElement>();
+ ASSERT_FALSE(first_password_element.IsNull());
+ SelectGenerationFallbackAndExpect(true);
+
+ // Simulate that the user accepts a generated password.
+ base::string16 password = ASCIIToUTF16("random_password");
+ EXPECT_CALL(fake_pw_client_,
+ PresaveGeneratedPassword(testing::Field(
+ &autofill::PasswordForm::password_value, password)));
+ password_generation_->GeneratedPasswordAccepted(password);
+
+ // Check that the first password field is autofilled with the generated
+ // password.
+ EXPECT_EQ(password, first_password_element.Value().Utf16());
+ EXPECT_TRUE(first_password_element.IsAutofilled());
+
+ // Check that the second password field is autofilled with the generated
+ // password (since it is chosen as a confirmation password field).
+ ExecuteJavaScriptForTests(
+ "document.getElementsByClassName('second_password')[0].focus();");
+ WebInputElement second_password_element =
+ document.FocusedElement().To<WebInputElement>();
+ ASSERT_FALSE(second_password_element.IsNull());
+ EXPECT_EQ(password, second_password_element.Value().Utf16());
+ EXPECT_TRUE(second_password_element.IsAutofilled());
+}
+
+TEST_F(PasswordGenerationAgentTest, PresavingGeneratedPassword) {
+ const struct {
+ const char* form;
+ const char* generation_element;
+ } kTestCases[] = {{kAccountCreationFormHTML, "first_password"},
+ {kAccountCreationNoForm, "first_password"},
+ {kPasswordChangeFormHTML, "newpassword"}};
+ for (auto& test_case : kTestCases) {
+ SCOPED_TRACE(testing::Message("form: ") << test_case.form);
+ LoadHTMLWithUserGesture(test_case.form);
+ // To be able to work with input elements outside <form>'s, use manual
+ // generation.
+ SimulateElementRightClick(test_case.generation_element);
+ SelectGenerationFallbackAndExpect(true);
+
+ base::string16 password = base::ASCIIToUTF16("random_password");
+ EXPECT_CALL(fake_pw_client_,
+ PresaveGeneratedPassword(testing::Field(
+ &autofill::PasswordForm::password_value, password)));
+ password_generation_->GeneratedPasswordAccepted(password);
+ base::RunLoop().RunUntilIdle();
+
+ EXPECT_CALL(fake_pw_client_, ShowPasswordEditingPopup(_, _, _));
+ FocusField(test_case.generation_element);
+ EXPECT_CALL(fake_pw_client_, PresaveGeneratedPassword(testing::_));
+ SimulateUserTypingASCIICharacter('a', true);
+ base::RunLoop().RunUntilIdle();
+
+ EXPECT_CALL(fake_pw_client_, GenerationElementLostFocus());
+ FocusField("username");
+ EXPECT_CALL(fake_pw_client_, PresaveGeneratedPassword(testing::_));
+ SimulateUserTypingASCIICharacter('X', true);
+ base::RunLoop().RunUntilIdle();
+ testing::Mock::VerifyAndClearExpectations(&fake_pw_client_);
+
+ EXPECT_CALL(fake_pw_client_, ShowPasswordEditingPopup(_, _, _));
+ FocusField(test_case.generation_element);
+ EXPECT_CALL(fake_pw_client_, PasswordNoLongerGenerated(testing::_));
+ for (size_t i = 0; i < password.length(); ++i)
+ SimulateUserTypingASCIICharacter(ui::VKEY_BACK, false);
+ SimulateUserTypingASCIICharacter(ui::VKEY_BACK, true);
+ base::RunLoop().RunUntilIdle();
+
+ EXPECT_CALL(fake_pw_client_, PresaveGeneratedPassword(testing::_)).Times(0);
+ EXPECT_CALL(fake_pw_client_, GenerationElementLostFocus());
+ FocusField("username");
+ SimulateUserTypingASCIICharacter('Y', true);
+ base::RunLoop().RunUntilIdle();
+ testing::Mock::VerifyAndClearExpectations(&fake_pw_client_);
+ }
+}
+
+TEST_F(PasswordGenerationAgentTest, FallbackForSaving) {
+ LoadHTMLWithUserGesture(kAccountCreationFormHTML);
+ SimulateElementRightClick("first_password");
+ SelectGenerationFallbackAndExpect(true);
+ EXPECT_EQ(0, fake_driver_.called_show_manual_fallback_for_saving_count());
+ base::string16 password = base::ASCIIToUTF16("random_password");
+ EXPECT_CALL(fake_pw_client_,
+ PresaveGeneratedPassword(testing::Field(
+ &autofill::PasswordForm::password_value, password)))
+ .WillOnce(testing::InvokeWithoutArgs([this]() {
+ // Make sure that generation event was propagated to the browser before
+ // the fallback showing. Otherwise, the fallback for saving provides a
+ // save bubble instead of a confirmation bubble.
+ EXPECT_EQ(0,
+ fake_driver_.called_show_manual_fallback_for_saving_count());
+ }));
+ password_generation_->GeneratedPasswordAccepted(password);
+ fake_driver_.Flush();
+ // Two fallback requests are expected because generation changes either new
+ // password and confirmation fields.
+ EXPECT_EQ(2, fake_driver_.called_show_manual_fallback_for_saving_count());
+}
+
+TEST_F(PasswordGenerationAgentTest, FormClassifierDisabled) {
+ LoadHTMLWithUserGesture(kSigninFormHTML);
+ ExpectFormClassifierVoteReceived(false /* vote is not expected */,
+ base::string16());
+}
+
+TEST_F(PasswordGenerationAgentTest, RevealPassword) {
+ // Checks that revealed password is masked when the field lost focus.
+ // Test cases: user click on another input field and on non-focusable element.
+ LoadHTMLWithUserGesture(kPasswordFormAndSpanHTML);
+ SetFoundFormEligibleForGeneration(
+ password_generation_, GetMainFrame()->GetDocument(),
+ "password" /* new_passwod_id */, nullptr /* confirm_password_id*/);
+ const char* kGenerationElementId = "password";
+ const char* kSpanId = "span";
+ const char* kTextFieldId = "username";
+
+ ExpectAutomaticGenerationAvailable(kGenerationElementId, kAvailable);
+ base::string16 password = base::ASCIIToUTF16("long_pwd");
+ EXPECT_CALL(fake_pw_client_,
+ PresaveGeneratedPassword(testing::Field(
+ &autofill::PasswordForm::password_value, password)));
+ password_generation_->GeneratedPasswordAccepted(password);
+
+ for (bool clickOnInputField : {false, true}) {
+ SCOPED_TRACE(testing::Message("clickOnInputField = ") << clickOnInputField);
+ // Click on the generation field to reveal the password value.
+ EXPECT_CALL(fake_pw_client_, ShowPasswordEditingPopup(_, _, _));
+ FocusField(kGenerationElementId);
+ fake_pw_client_.Flush();
+
+ WebDocument document = GetMainFrame()->GetDocument();
+ blink::WebElement element = document.GetElementById(
+ blink::WebString::FromUTF8(kGenerationElementId));
+ ASSERT_FALSE(element.IsNull());
+ blink::WebInputElement input = element.To<WebInputElement>();
+ EXPECT_TRUE(input.ShouldRevealPassword());
+
+ // Click on another HTML element.
+ const char* const click_target_name =
+ clickOnInputField ? kTextFieldId : kSpanId;
+ EXPECT_CALL(fake_pw_client_, GenerationElementLostFocus());
+ EXPECT_TRUE(SimulateElementClick(click_target_name));
+ EXPECT_FALSE(input.ShouldRevealPassword());
+ fake_pw_client_.Flush();
+ testing::Mock::VerifyAndClearExpectations(&fake_pw_client_);
+ }
+}
+
+TEST_F(PasswordGenerationAgentTest, JavascriptClearedTheField) {
+ LoadHTMLWithUserGesture(kAccountCreationFormHTML);
+ SetFoundFormEligibleForGeneration(
+ password_generation_, GetMainFrame()->GetDocument(),
+ "first_password" /* new_passwod_id */, nullptr /* confirm_password_id*/);
+
+ const char kGenerationElementId[] = "first_password";
+ ExpectAutomaticGenerationAvailable(kGenerationElementId, kAvailable);
+ base::string16 password = base::ASCIIToUTF16("long_pwd");
+ EXPECT_CALL(fake_pw_client_,
+ PresaveGeneratedPassword(testing::Field(
+ &autofill::PasswordForm::password_value, password)));
+ password_generation_->GeneratedPasswordAccepted(password);
+
+ EXPECT_CALL(fake_pw_client_, PasswordNoLongerGenerated(testing::_));
+ EXPECT_CALL(fake_pw_client_, AutomaticGenerationAvailable(_));
+ ExecuteJavaScriptForTests(
+ "document.getElementById('first_password').value = '';");
+ FocusField(kGenerationElementId);
+ base::RunLoop().RunUntilIdle();
+}
+
+TEST_F(PasswordGenerationAgentTest, GenerationFallbackTest) {
+ LoadHTMLWithUserGesture(kAccountCreationFormHTML);
+ WebDocument document = GetMainFrame()->GetDocument();
+ WebElement element =
+ document.GetElementById(WebString::FromUTF8("first_password"));
+ ASSERT_FALSE(element.IsNull());
+ WebInputElement first_password_element = element.To<WebInputElement>();
+ EXPECT_TRUE(first_password_element.Value().IsNull());
+ SimulateElementRightClick("first_password");
+ SelectGenerationFallbackAndExpect(true);
+ EXPECT_TRUE(first_password_element.Value().IsNull());
+}
+
+TEST_F(PasswordGenerationAgentTest, GenerationFallback_NoFocusedElement) {
+ // Checks the fallback doesn't cause a crash just in case no password element
+ // had focus so far.
+ LoadHTMLWithUserGesture(kAccountCreationFormHTML);
+ SelectGenerationFallbackAndExpect(false);
+}
+
+TEST_F(PasswordGenerationAgentTest, AutofillToGenerationField) {
+ LoadHTMLWithUserGesture(kAccountCreationFormHTML);
+ SetFoundFormEligibleForGeneration(
+ password_generation_, GetMainFrame()->GetDocument(),
+ "first_password" /* new_passwod_id */, nullptr /* confirm_password_id*/);
+ ExpectAutomaticGenerationAvailable("first_password", kAvailable);
+
+ WebDocument document = GetMainFrame()->GetDocument();
+ WebElement element =
+ document.GetElementById(WebString::FromUTF8("first_password"));
+ ASSERT_FALSE(element.IsNull());
+ const WebInputElement input_element = element.To<WebInputElement>();
+ // Since password isn't generated (just suitable field was detected),
+ // |OnFieldAutofilled| wouldn't trigger any actions.
+ EXPECT_CALL(fake_pw_client_, PasswordNoLongerGenerated(testing::_)).Times(0);
+ password_generation_->OnFieldAutofilled(input_element);
+}
+
+TEST_F(PasswordGenerationAgentTestForHtmlAnnotation, AnnotateForm) {
+ TestAnnotateForm(true);
+}
+
+TEST_F(PasswordGenerationAgentTestForHtmlAnnotation, AnnotateNoForm) {
+ TestAnnotateForm(false);
+}
+
+TEST_F(PasswordGenerationAgentTest, PasswordUnmaskedUntilCompleteDeletion) {
+ LoadHTMLWithUserGesture(kAccountCreationFormHTML);
+ SetFoundFormEligibleForGeneration(
+ password_generation_, GetMainFrame()->GetDocument(),
+ "first_password" /* new_passwod_id */, nullptr /* confirm_password_id*/);
+
+ constexpr char kGenerationElementId[] = "first_password";
+
+ // Generate a new password.
+ ExpectAutomaticGenerationAvailable(kGenerationElementId, kAvailable);
+ base::string16 password = base::ASCIIToUTF16("random_password");
+ EXPECT_CALL(fake_pw_client_,
+ PresaveGeneratedPassword(testing::Field(
+ &autofill::PasswordForm::password_value, password)));
+ password_generation_->GeneratedPasswordAccepted(password);
+ fake_pw_client_.Flush();
+ testing::Mock::VerifyAndClearExpectations(&fake_pw_client_);
+
+ // Delete characters of the generated password until only
+ // |kMinimumLengthForEditedPassword| - 1 chars remain.
+ EXPECT_CALL(fake_pw_client_, ShowPasswordEditingPopup(_, _, _));
+ FocusField(kGenerationElementId);
+ EXPECT_CALL(fake_pw_client_, PasswordNoLongerGenerated(testing::_));
+ EXPECT_CALL(fake_pw_client_, AutomaticGenerationAvailable(_));
+ size_t max_chars_to_delete =
+ password.length() -
+ PasswordGenerationAgent::kMinimumLengthForEditedPassword + 1;
+ for (size_t i = 0; i < max_chars_to_delete; ++i)
+ SimulateUserTypingASCIICharacter(ui::VKEY_BACK, false);
+ base::RunLoop().RunUntilIdle();
+ fake_pw_client_.Flush();
+ // The remaining characters no longer count as a generated password, so
+ // generation should be offered again.
+
+ // Check that the characters remain unmasked.
+ WebDocument document = GetMainFrame()->GetDocument();
+ blink::WebElement element =
+ document.GetElementById(blink::WebString::FromUTF8(kGenerationElementId));
+ ASSERT_FALSE(element.IsNull());
+ blink::WebInputElement input = element.To<WebInputElement>();
+ EXPECT_TRUE(input.ShouldRevealPassword());
+
+ // Delete the rest of the characters. The field should now mask new
+ // characters. Due to implementation details it's possible to get pings about
+ // password generation available.
+ EXPECT_CALL(fake_pw_client_, AutomaticGenerationAvailable(_))
+ .Times(AnyNumber());
+ for (size_t i = 0;
+ i < PasswordGenerationAgent::kMinimumLengthForEditedPassword; ++i)
+ SimulateUserTypingASCIICharacter(ui::VKEY_BACK, false);
+ base::RunLoop().RunUntilIdle();
+ EXPECT_FALSE(input.ShouldRevealPassword());
+}
+
+TEST_F(PasswordGenerationAgentTest, ShortPasswordMaskedAfterChangingFocus) {
+ LoadHTMLWithUserGesture(kPasswordFormAndSpanHTML);
+ constexpr char kGenerationElementId[] = "password";
+ SetFoundFormEligibleForGeneration(password_generation_,
+ GetMainFrame()->GetDocument(),
+ kGenerationElementId /* new_passwod_id */,
+ nullptr /* confirm_password_id*/);
+
+ // Generate a new password.
+ ExpectAutomaticGenerationAvailable(kGenerationElementId, kAvailable);
+ base::string16 password = base::ASCIIToUTF16("random_password");
+ EXPECT_CALL(fake_pw_client_,
+ PresaveGeneratedPassword(testing::Field(
+ &autofill::PasswordForm::password_value, password)));
+ password_generation_->GeneratedPasswordAccepted(password);
+ fake_pw_client_.Flush();
+ testing::Mock::VerifyAndClearExpectations(&fake_pw_client_);
+
+ // Delete characters of the generated password until only
+ // |kMinimumLengthForEditedPassword| - 1 chars remain.
+ EXPECT_CALL(fake_pw_client_, ShowPasswordEditingPopup(_, _, _));
+ FocusField(kGenerationElementId);
+ EXPECT_CALL(fake_pw_client_, PasswordNoLongerGenerated(testing::_));
+ size_t max_chars_to_delete =
+ password.length() -
+ PasswordGenerationAgent::kMinimumLengthForEditedPassword + 1;
+ EXPECT_CALL(fake_pw_client_, AutomaticGenerationAvailable(_));
+ for (size_t i = 0; i < max_chars_to_delete; ++i)
+ SimulateUserTypingASCIICharacter(ui::VKEY_BACK, false);
+ // The remaining characters no longer count as a generated password, so
+ // generation should be offered again.
+ base::RunLoop().RunUntilIdle();
+ fake_pw_client_.Flush();
+ testing::Mock::VerifyAndClearExpectations(&fake_pw_client_);
+
+ // Check that the characters remain unmasked.
+ WebDocument document = GetMainFrame()->GetDocument();
+ blink::WebElement element =
+ document.GetElementById(blink::WebString::FromUTF8(kGenerationElementId));
+ ASSERT_FALSE(element.IsNull());
+ blink::WebInputElement input = element.To<WebInputElement>();
+ EXPECT_TRUE(input.ShouldRevealPassword());
+
+ // Focus another element on the page. The password should be masked.
+ EXPECT_CALL(fake_pw_client_, GenerationElementLostFocus());
+ ASSERT_TRUE(SimulateElementClick("span"));
+ EXPECT_FALSE(input.ShouldRevealPassword());
+
+ // Focus the password field again. As the remaining characters are not
+ // a generated password, they should remain masked.
+ ExpectAutomaticGenerationAvailable(kGenerationElementId, kAvailable);
+ EXPECT_FALSE(input.ShouldRevealPassword());
+}
+
+TEST_F(PasswordGenerationAgentTest, GenerationAvailableByRendererIds) {
+ LoadHTMLWithUserGesture(kMultipleAccountCreationFormHTML);
+
+ constexpr const char* kPasswordElementsIds[] = {"password", "first_password",
+ "second_password"};
+
+ WebDocument document = GetMainFrame()->GetDocument();
+ std::vector<WebInputElement> password_elements;
+ for (const char* id : kPasswordElementsIds) {
+ WebElement element = document.GetElementById(WebString::FromUTF8(id));
+ WebInputElement* input = ToWebInputElement(&element);
+ ASSERT_TRUE(input);
+ password_elements.push_back(*input);
+ }
+
+ // Simulate that the browser informs about eligible for generation form.
+ // Check that generation is available only on new password field of this form.
+ PasswordFormGenerationData generation_data;
+ generation_data.new_password_renderer_id =
+ password_elements[0].UniqueRendererFormControlId();
+
+ password_generation_->FoundFormEligibleForGeneration(generation_data);
+ ExpectAutomaticGenerationAvailable(kPasswordElementsIds[0], kAvailable);
+ ExpectGenerationElementLostFocus(kPasswordElementsIds[1]);
+ ExpectAutomaticGenerationAvailable(kPasswordElementsIds[2], kNotReported);
+
+ // Simulate that the browser informs about the second eligible for generation
+ // form. Check that generation is available on both forms.
+ generation_data.new_password_renderer_id =
+ password_elements[2].UniqueRendererFormControlId();
+ password_generation_->FoundFormEligibleForGeneration(generation_data);
+ ExpectAutomaticGenerationAvailable(kPasswordElementsIds[0], kAvailable);
+ ExpectGenerationElementLostFocus(kPasswordElementsIds[1]);
+ ExpectAutomaticGenerationAvailable(kPasswordElementsIds[2], kAvailable);
+}
+
+} // namespace autofill