blob: c893f89eb1819dc7567090e1e81a71dd4acc7d47 [file] [log] [blame]
// Copyright 2013 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "components/autofill/content/renderer/password_generation_agent.h"
#include <string.h>
#include <algorithm>
#include <array>
#include <memory>
#include <string>
#include <string_view>
#include "base/command_line.h"
#include "base/functional/bind.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 "build/build_config.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/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/form_field_data.h"
#include "components/autofill/core/common/password_generation_util.h"
#include "components/autofill/core/common/unique_ids.h"
#include "components/password_manager/core/common/password_manager_features.h"
#include "content/public/renderer/render_frame.h"
#include "mojo/public/cpp/bindings/associated_receiver_set.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_element.h"
#include "third_party/blink/public/web/web_frame_widget.h"
#include "third_party/blink/public/web/web_input_element.h"
#include "third_party/blink/public/web/web_local_frame.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::AtLeast;
using testing::AtMost;
using testing::Eq;
namespace autofill {
namespace {
// Utility method that tries to find a field in `form` whose `id_attribute`
// matches `id`. Returns nullptr if no such field exists.
const FormFieldData* FindFieldById(const FormData& form, std::string_view id) {
auto it = std::ranges::find(form.fields(), base::UTF8ToUTF16(id),
&FormFieldData::id_attribute);
return it != form.fields().end() ? &*it : nullptr;
}
class FakeContentAutofillDriver : public mojom::AutofillDriver {
public:
FakeContentAutofillDriver() = default;
~FakeContentAutofillDriver() override = default;
void BindReceiver(
mojo::PendingAssociatedReceiver<mojom::AutofillDriver> receiver) {
receivers_.Add(this, std::move(receiver));
}
void WaitForFormsSeen() {
forms_seen_run_loop_->Run();
forms_seen_run_loop_ = std::make_unique<base::RunLoop>();
}
private:
// mojom::AutofillDriver:
void FormsSeen(const std::vector<FormData>& updated_forms,
const std::vector<FormRendererId>& removed_forms) override {
forms_seen_run_loop_->Quit();
}
void FormSubmitted(const FormData& form,
mojom::SubmissionSource source) override {}
void CaretMovedInFormField(const FormData& form,
FieldRendererId field_id,
const gfx::Rect& caret_bounds) override {}
void TextFieldValueChanged(const FormData& form,
FieldRendererId field_id,
base::TimeTicks timestamp) override {}
void TextFieldDidScroll(const FormData& form,
FieldRendererId field_id) override {}
void SelectControlSelectionChanged(const FormData& form,
FieldRendererId field_id) override {}
void JavaScriptChangedAutofilledValue(
const FormData& form,
FieldRendererId field_id,
const std::u16string& old_value) override {}
void AskForValuesToFill(const FormData& form,
FieldRendererId field_id,
const gfx::Rect& caret_bounds,
AutofillSuggestionTriggerSource trigger_source,
const std::optional<PasswordSuggestionRequest>&
password_request) override {}
void HidePopup() override {}
void FocusOnNonFormField() override {}
void FocusOnFormField(const FormData& form,
FieldRendererId field_id) override {}
void DidAutofillForm(const FormData& form,
base::TimeTicks timestamp) override {}
void DidEndTextFieldEditing() override {}
void SelectFieldOptionsDidChange(const autofill::FormData& form) override {}
std::unique_ptr<base::RunLoop> forms_seen_run_loop_ =
std::make_unique<base::RunLoop>();
mojo::AssociatedReceiverSet<mojom::AutofillDriver> receivers_;
};
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 = 'text' id = 'hidden' style='display: none;'/> "
" <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 = 'text' id = 'hidden' style='display: none;'/> "
"<INPUT type = 'button' id = 'dummy'/> "
"<INPUT type = 'submit' value = 'LOGIN' />";
#if !BUILDFLAG(IS_ANDROID)
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>";
#endif // !BUILDFLAG(IS_ANDROID)
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,
};
PasswordGenerationAgentTest() = default;
PasswordGenerationAgentTest(const PasswordGenerationAgentTest&) = delete;
PasswordGenerationAgentTest& operator=(const PasswordGenerationAgentTest&) =
delete;
// ChromeRenderViewTest:
void RegisterMainFrameRemoteInterfaces() override;
void SetUp() override;
void TearDown() override;
void LoadHTMLWithUserGesture(const char* html);
WebElement GetElementById(std::string_view element_id);
WebInputElement GetInputElementById(std::string_view element_id);
void FocusField(const char* element_id);
void ExpectAutomaticGenerationAvailable(const char* element_id,
AutomaticGenerationStatus available);
void ExpectGenerationElementLostFocus(const char* new_element_id);
void ExpectEditingPopupOnFieldFocus(const char* element_id);
void ExpectFormClassifierVoteReceived(
bool received,
const std::u16string& expected_generation_element);
void SelectGenerationFallbackAndExpect(bool available);
void ExpectAttribute(const WebElement& element,
std::string_view attribute,
std::string_view expected_value);
void CheckPreviewedValue(const WebInputElement& element,
const std::u16string& value);
void BindAutofillDriver(mojo::ScopedInterfaceEndpointHandle handle);
void BindPasswordManagerDriver(mojo::ScopedInterfaceEndpointHandle handle);
void BindPasswordManagerClient(mojo::ScopedInterfaceEndpointHandle handle);
// Callback for TriggeredGeneratePassword.
MOCK_METHOD1(TriggeredGeneratePasswordReply,
void(const std::optional<
autofill::password_generation::PasswordGenerationUIData>&));
FakeContentAutofillDriver fake_autofill_driver_;
FakeMojoPasswordManagerDriver fake_driver_;
testing::StrictMock<FakePasswordGenerationDriver> fake_pw_client_;
};
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 =
GetMainRenderFrame()->GetRemoteAssociatedInterfaces();
remote_associated_interfaces->OverrideBinderForTesting(
mojom::AutofillDriver::Name_,
base::BindRepeating(&PasswordGenerationAgentTest::BindAutofillDriver,
base::Unretained(this)));
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.com/41401202): 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:
GetMainRenderFrame()
->GetRemoteAssociatedInterfaces()
->OverrideBinderForTesting(
mojom::PasswordGenerationDriver::Name_,
base::BindRepeating([](mojo::ScopedInterfaceEndpointHandle handle) {
handle.reset();
}));
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.
GetWebFrameWidget()->SetFocus(true);
}
void PasswordGenerationAgentTest::TearDown() {
// Unloading the document may trigger the event.
#if !BUILDFLAG(IS_ANDROID)
EXPECT_CALL(fake_pw_client_, GenerationElementLostFocus()).Times(AtMost(1));
#endif // !BUILDFLAG(IS_ANDROID)
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"));
}
WebElement PasswordGenerationAgentTest::GetElementById(
std::string_view element_id) {
WebDocument document = GetMainFrame()->GetDocument();
WebElement element =
document.GetElementById(blink::WebString::FromUTF8(element_id));
CHECK(element);
return element;
}
WebInputElement PasswordGenerationAgentTest::GetInputElementById(
std::string_view element_id) {
WebInputElement input_element =
GetElementById(element_id).To<WebInputElement>();
CHECK(input_element);
return input_element;
}
void PasswordGenerationAgentTest::FocusField(const char* element_id) {
ASSERT_TRUE(SimulateElementClick(element_id));
#if BUILDFLAG(IS_ANDROID)
// TODO(crbug.com/40820173): On Android, the JS above doesn't trigger the
// method below.
GetMainFrame()->AutofillClient()->DidCompleteFocusChangeInFrame();
#endif // BUILDFLAG(IS_ANDROID)
}
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 {
// TODO(crbug.com/40279043): Expect the call precisely once.
EXPECT_CALL(fake_pw_client_, AutomaticGenerationAvailable(_))
.Times(testing::AtLeast(1));
}
FocusField(element_id);
base::RunLoop().RunUntilIdle();
testing::Mock::VerifyAndClearExpectations(&fake_pw_client_);
// Check that aria-autocomplete attribute is set correctly.
if (status == kAvailable) {
WebElement element = GetElementById(element_id);
ExpectAttribute(element, "aria-autocomplete", "list");
}
}
void PasswordGenerationAgentTest::ExpectGenerationElementLostFocus(
const char* new_element_id) {
#if !BUILDFLAG(IS_ANDROID)
EXPECT_CALL(fake_pw_client_, GenerationElementLostFocus());
#endif // !BUILDFLAG(IS_ANDROID)
FocusField(new_element_id);
base::RunLoop().RunUntilIdle();
testing::Mock::VerifyAndClearExpectations(&fake_pw_client_);
}
void PasswordGenerationAgentTest::ExpectEditingPopupOnFieldFocus(
const char* new_element_id) {
#if !BUILDFLAG(IS_ANDROID)
EXPECT_CALL(fake_pw_client_, ShowPasswordEditingPopup).Times(AtLeast(1));
#endif // !BUILDFLAG(IS_ANDROID)
FocusField(new_element_id);
base::RunLoop().RunUntilIdle();
testing::Mock::VerifyAndClearExpectations(&fake_pw_client_);
}
void PasswordGenerationAgentTest::ExpectFormClassifierVoteReceived(
bool received,
const std::u16string& 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,
TriggeredGeneratePasswordReply(testing::Ne(std::nullopt)));
} else {
EXPECT_CALL(*this,
TriggeredGeneratePasswordReply(testing::Eq(std::nullopt)));
}
password_generation_->TriggeredGeneratePassword(base::BindOnce(
&PasswordGenerationAgentTest::TriggeredGeneratePasswordReply,
base::Unretained(this)));
testing::Mock::VerifyAndClearExpectations(this);
}
void PasswordGenerationAgentTest::ExpectAttribute(
const WebElement& element,
std::string_view attribute,
std::string_view expected_value) {
WebString actual_value =
element.GetAttribute(blink::WebString::FromUTF8(attribute));
ASSERT_FALSE(actual_value.IsNull());
EXPECT_EQ(expected_value, actual_value.Ascii());
}
void PasswordGenerationAgentTest::CheckPreviewedValue(
const WebInputElement& element,
const std::u16string& value) {
EXPECT_EQ(value, element.SuggestedValue().Utf16());
EXPECT_EQ(element.GetAutofillState(), blink::WebAutofillState::kPreviewed);
}
void PasswordGenerationAgentTest::BindAutofillDriver(
mojo::ScopedInterfaceEndpointHandle handle) {
fake_autofill_driver_.BindReceiver(
mojo::PendingAssociatedReceiver<mojom::AutofillDriver>(
std::move(handle)));
}
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)));
}
// Tests HTML forms' annotations (e.g. signatures, visibility). The parser's
// annotations are tested in PasswordManagerBrowserTest.ParserAnnotations.
class PasswordGenerationAgentTestForHtmlAnnotation
: public PasswordGenerationAgentTest {
public:
PasswordGenerationAgentTestForHtmlAnnotation() = default;
PasswordGenerationAgentTestForHtmlAnnotation(
const PasswordGenerationAgentTestForHtmlAnnotation&) = delete;
PasswordGenerationAgentTestForHtmlAnnotation& operator=(
const PasswordGenerationAgentTestForHtmlAnnotation&) = delete;
void SetUp() override {
base::CommandLine::ForCurrentProcess()->AppendSwitch(
switches::kShowAutofillSignatures);
PasswordGenerationAgentTest::SetUp();
}
void TestAnnotateForm(bool has_form_tag);
};
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(),
/*new_password_id=*/"first_password", /*confirm_password_id=*/nullptr);
ExpectAutomaticGenerationAvailable("first_password", kAvailable);
WebDocument document = GetMainFrame()->GetDocument();
const char* kFormSignature =
has_form_tag ? "17583149382422306905" : "17876980055561084954";
if (has_form_tag) {
// Check the form signature is set in the <form> tag.
WebElement form_element = GetElementById("blah");
ExpectAttribute(form_element, "form_signature", kFormSignature);
}
// Check field signatures and form signature are set in the <input>s.
WebElement username_element = GetElementById("username");
ExpectAttribute(username_element, "field_signature", "239111655");
ExpectAttribute(username_element, "form_signature", kFormSignature);
ExpectAttribute(username_element, "visibility_annotation", "true");
WebElement password_element = GetElementById("first_password");
ExpectAttribute(password_element, "field_signature", "3933215845");
ExpectAttribute(password_element, "form_signature", kFormSignature);
ExpectAttribute(password_element, "visibility_annotation", "true");
ExpectAttribute(password_element, "password_creation_field", "1");
WebElement hidden_element = GetElementById("hidden");
ExpectAttribute(hidden_element, "visibility_annotation", "false");
}
TEST_F(PasswordGenerationAgentTest, HiddenSecondPasswordDetectionTest) {
// Hidden fields are not treated differently.
LoadHTMLWithUserGesture(kHiddenPasswordAccountCreationFormHTML);
SetFoundFormEligibleForGeneration(
password_generation_, GetMainFrame()->GetDocument(),
/*new_password_id=*/"first_password", /*confirm_password_id=*/nullptr);
ExpectAutomaticGenerationAvailable("first_password", kAvailable);
}
TEST_F(PasswordGenerationAgentTest, DetectionTestNoForm) {
LoadHTMLWithUserGesture(kAccountCreationNoForm);
SetFoundFormEligibleForGeneration(
password_generation_, GetMainFrame()->GetDocument(),
/*new_password_id=*/"first_password", /*confirm_password_id=*/nullptr);
ExpectAutomaticGenerationAvailable("first_password", kAvailable);
ExpectGenerationElementLostFocus("second_password");
}
TEST_F(PasswordGenerationAgentTest, FillTest) {
// Add event listeners for password fields.
std::vector<std::u16string> 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(),
/*new_password_id=*/"first_password",
/*confirm_password_id=*/"second_password");
ExpectAutomaticGenerationAvailable("first_password", kAvailable);
WebInputElement first_password_element =
GetInputElementById("first_password");
WebInputElement second_password_element =
GetInputElementById("second_password");
// Both password fields should be empty.
EXPECT_TRUE(first_password_element.Value().IsNull());
EXPECT_TRUE(second_password_element.Value().IsNull());
std::u16string password = u"random_password";
EXPECT_CALL(fake_pw_client_, PresaveGeneratedPassword(_, Eq(password)));
password_generation_->GeneratedPasswordAccepted(password);
password_generation_->FocusNextFieldAfterPasswords();
// 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 std::u16string& variable : variables_to_check) {
int value;
EXPECT_TRUE(ExecuteJavaScriptAndReturnIntValue(variable, &value));
EXPECT_EQ(1, value) << variable;
}
// Check that focus returns to previously focused element.
WebInputElement element = GetInputElementById("address");
EXPECT_EQ(element, document.FocusedElement());
}
TEST_F(PasswordGenerationAgentTest, EditingTest) {
LoadHTMLWithUserGesture(kAccountCreationFormHTML);
SetFoundFormEligibleForGeneration(password_generation_,
GetMainFrame()->GetDocument(),
/*new_password_id=*/"first_password",
/*confirm_password_id=*/"second_password");
ExpectAutomaticGenerationAvailable("first_password", kAvailable);
WebInputElement first_password_element =
GetInputElementById("first_password");
WebInputElement second_password_element =
GetInputElementById("second_password");
std::u16string password = u"random_password";
EXPECT_CALL(fake_pw_client_, PresaveGeneratedPassword(_, Eq(password)));
password_generation_->GeneratedPasswordAccepted(password);
base::RunLoop().RunUntilIdle();
testing::Mock::VerifyAndClearExpectations(&fake_pw_client_);
// 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::u16string appended_chars = u"123";
std::u16string edited_password = password + appended_chars;
ExpectEditingPopupOnFieldFocus("first_password");
EXPECT_CALL(fake_pw_client_,
PresaveGeneratedPassword(_, Eq(edited_password)));
SimulateUserTypingASCIICharacter(ui::VKEY_END, /*flush_message_loop=*/false);
for (size_t i = 0; i < appended_chars.size(); i++) {
SimulateUserTypingASCIICharacter(appended_chars[i],
/*flush_message_loop=*/false);
}
base::RunLoop().RunUntilIdle();
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());
ASSERT_TRUE(fake_driver_.form_data_maybe_submitted().has_value());
EXPECT_THAT(FindFieldById(*fake_driver_.form_data_maybe_submitted(),
"first_password"),
testing::Property(&FormFieldData::value, edited_password));
EXPECT_THAT(FindFieldById(*fake_driver_.form_data_maybe_submitted(),
"second_password"),
testing::Property(&FormFieldData::value, edited_password));
base::RunLoop().RunUntilIdle();
testing::Mock::VerifyAndClearExpectations(&fake_pw_client_);
// 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(std::u16string(), first_password_element.Value().Utf16());
EXPECT_EQ(std::u16string(), second_password_element.Value().Utf16());
EXPECT_FALSE(first_password_element.IsAutofilled());
EXPECT_FALSE(second_password_element.IsAutofilled());
EXPECT_FALSE(first_password_element.ShouldRevealPassword());
EXPECT_FALSE(second_password_element.ShouldRevealPassword());
ASSERT_TRUE(fake_driver_.form_data_maybe_submitted().has_value());
EXPECT_THAT(FindFieldById(*fake_driver_.form_data_maybe_submitted(),
"first_password"),
testing::Property(&FormFieldData::value, std::u16string()));
EXPECT_THAT(FindFieldById(*fake_driver_.form_data_maybe_submitted(),
"second_password"),
testing::Property(&FormFieldData::value, std::u16string()));
}
TEST_F(PasswordGenerationAgentTest, EditingEventsTest) {
LoadHTMLWithUserGesture(kAccountCreationFormHTML);
SetFoundFormEligibleForGeneration(
password_generation_, GetMainFrame()->GetDocument(),
/*new_password_id=*/"first_password", /*confirm_password_id=*/nullptr);
// Generate password.
ExpectAutomaticGenerationAvailable("first_password", kAvailable);
std::u16string password = u"random_password";
EXPECT_CALL(fake_pw_client_, PresaveGeneratedPassword(_, Eq(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.
ExpectEditingPopupOnFieldFocus("first_password");
SimulateUserTypingASCIICharacter(ui::VKEY_END, true);
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(_, Eq(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, UnblocklistedMultipleTest) {
// Receive two not blocklisted messages, one is for account creation form and
// the other is not. Show password generation icon.
LoadHTMLWithUserGesture(kAccountCreationFormHTML);
SetFoundFormEligibleForGeneration(
password_generation_, GetMainFrame()->GetDocument(),
/*new_password_id=*/"first_password", /*confirm_password_id=*/nullptr);
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(),
/*new_password_id=*/"first_password", /*confirm_password_id=*/nullptr);
ExpectAutomaticGenerationAvailable("first_password", kAvailable);
}
TEST_F(PasswordGenerationAgentTest, MaximumCharsForGenerationOffer) {
base::HistogramTester histogram_tester;
LoadHTMLWithUserGesture(kAccountCreationFormHTML);
SetFoundFormEligibleForGeneration(
password_generation_, GetMainFrame()->GetDocument(),
/*new_password_id=*/"first_password", /*confirm_password_id=*/nullptr);
// There should now be a message to show the UI.
ExpectAutomaticGenerationAvailable("first_password", kAvailable);
// Simulate the user typing a character. The popup should disappear.
#if !BUILDFLAG(IS_ANDROID)
EXPECT_CALL(fake_pw_client_, PasswordGenerationRejectedByTyping);
#endif // !BUILDFLAG(IS_ANDROID)
WebInputElement first_password_element =
GetInputElementById("first_password");
SimulateUserInputChangeForElement(first_password_element, "a");
testing::Mock::VerifyAndClearExpectations(&fake_pw_client_);
// Simulate the user deleting a character. 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.
ExpectGenerationElementLostFocus("username");
fake_pw_client_.Flush();
// Focusing the password field will bring up the generation UI again.
EXPECT_CALL(fake_pw_client_, AutomaticGenerationAvailable(_))
.Times(AtLeast(1));
FocusField("first_password");
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.
#if !BUILDFLAG(IS_ANDROID)
EXPECT_CALL(fake_pw_client_, GenerationElementLostFocus());
#endif // !BUILDFLAG(IS_ANDROID)
LoadHTMLWithUserGesture(kSigninFormHTML);
fake_pw_client_.Flush();
histogram_tester.ExpectBucketCount(
"PasswordGeneration.Event",
autofill::password_generation::GENERATION_POPUP_SHOWN, 1);
}
TEST_F(PasswordGenerationAgentTest, MinimumLengthForEditedPassword) {
LoadHTMLWithUserGesture(kAccountCreationFormHTML);
SetFoundFormEligibleForGeneration(
password_generation_, GetMainFrame()->GetDocument(),
/*new_password_id=*/"first_password", /*confirm_password_id=*/nullptr);
ExpectAutomaticGenerationAvailable("first_password", kAvailable);
// Generate a new password.
std::u16string password = u"random_password";
EXPECT_CALL(fake_pw_client_, PresaveGeneratedPassword(_, Eq(password)));
password_generation_->GeneratedPasswordAccepted(password);
fake_pw_client_.Flush();
testing::Mock::VerifyAndClearExpectations(&fake_pw_client_);
ExpectEditingPopupOnFieldFocus("first_password");
// Delete most of the password.
EXPECT_CALL(fake_pw_client_, AutomaticGenerationAvailable(_)).Times(0);
SimulateUserTypingASCIICharacter(ui::VKEY_END, false);
size_t max_chars_to_delete =
password.length() -
PasswordGenerationAgent::kMinimumLengthForEditedPassword;
EXPECT_CALL(fake_pw_client_, PresaveGeneratedPassword(_, _))
.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.
WebInputElement first_password_element =
GetInputElementById("first_password");
WebInputElement second_password_element =
GetInputElementById("second_password");
EXPECT_NE(std::u16string(), first_password_element.Value().Utf16());
EXPECT_EQ(std::u16string(), second_password_element.Value().Utf16());
}
TEST_F(PasswordGenerationAgentTest, DynamicFormTest) {
LoadHTMLWithUserGesture(kSigninFormHTML);
fake_autofill_driver_.WaitForFormsSeen();
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);");
fake_autofill_driver_.WaitForFormsSeen();
// This needs to come after the DOM has been modified.
SetFoundFormEligibleForGeneration(
password_generation_, GetMainFrame()->GetDocument(),
/*new_password_id=*/"first_password", /*confirm_password_id=*/nullptr);
// 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(),
/*new_password_id=*/"first_password", /*confirm_password_id=*/nullptr);
// 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.
ExpectGenerationElementLostFocus("disabled");
}
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(),
/*new_password_id=*/"newpassword",
/*confirm_password_id=*/"confirmpassword");
ExpectAutomaticGenerationAvailable("password", kNotReported);
ExpectAutomaticGenerationAvailable("newpassword", kAvailable);
ExpectGenerationElementLostFocus("confirmpassword");
}
// These tests are for the right-click menu and it is not applicable to Android.
#if !BUILDFLAG(IS_ANDROID)
TEST_F(PasswordGenerationAgentTest, DesktopContextMenuGenerationInFormTest) {
LoadHTMLWithUserGesture(kSigninFormHTML);
WebInputElement first_password_element = GetInputElementById("password");
EXPECT_TRUE(first_password_element.Value().IsNull());
SimulateElementRightClick("password");
SelectGenerationFallbackAndExpect(true);
EXPECT_TRUE(first_password_element.Value().IsNull());
// Re-focusing a password field for which manual generation was requested
// should not re-trigger generation.
ExpectAutomaticGenerationAvailable("password", kNotReported);
}
TEST_F(PasswordGenerationAgentTest, DesktopContextMenuGenerationNoFormTest) {
LoadHTMLWithUserGesture(kAccountCreationNoForm);
SimulateElementRightClick("first_password");
SelectGenerationFallbackAndExpect(true);
}
TEST_F(PasswordGenerationAgentTest,
DesktopContextMenuGenerationDoesntSuppressAutomatic) {
LoadHTMLWithUserGesture(kAccountCreationFormHTML);
SetFoundFormEligibleForGeneration(
password_generation_, GetMainFrame()->GetDocument(),
/*new_password_id=*/"first_password", /*confirm_password_id=*/nullptr);
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, DesktopContextMenuGenerationNoIds) {
LoadHTMLWithUserGesture(kAccountCreationNoIds);
WebDocument document = GetMainFrame()->GetDocument();
ExecuteJavaScriptForTests(
"document.getElementsByClassName('first_password')[0].focus();");
WebInputElement first_password_element =
document.FocusedElement().To<WebInputElement>();
ASSERT_TRUE(first_password_element);
SelectGenerationFallbackAndExpect(true);
// Simulate that the user accepts a generated password.
std::u16string password = u"random_password";
EXPECT_CALL(fake_pw_client_, PresaveGeneratedPassword(_, Eq(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_TRUE(second_password_element);
EXPECT_EQ(password, second_password_element.Value().Utf16());
EXPECT_TRUE(second_password_element.IsAutofilled());
}
TEST_F(PasswordGenerationAgentTest,
DesktopContextMenuGeneration_NoFocusedElement) {
// Checks the fallback doesn't cause a crash just in case no password element
// had focus so far.
LoadHTMLWithUserGesture(kAccountCreationFormHTML);
SelectGenerationFallbackAndExpect(false);
}
// Test corner case when password field becomes readonly the moment you focus it
// and later becomes normal again.
TEST_F(PasswordGenerationAgentTest,
DesktopContextMenuManualGenerationOnReadonly) {
LoadHTMLWithUserGesture(kSigninFormHTML);
ExecuteJavaScriptForTests(
"document.getElementsByClassName('first_password')[0].setAttribute('"
"readonly', 'true');");
SimulateElementRightClick("password");
ExecuteJavaScriptForTests(
"document.getElementsByClassName('first_password')[0].removeAttribute('"
"readonly');");
SelectGenerationFallbackAndExpect(true);
}
// Tests that manual generation can be triggered on a text field, if heuristics
// signal that the field is a password field.
TEST_F(PasswordGenerationAgentTest,
ManualGenerationIsTriggeredOnTextFieldWithHeuristic) {
// The field type is text, but has password mention in the id/name attribute.
LoadHTML(
R"(
<input type="text" id="username-field" name="username-field">
<input type="text" id="password-field" name="password-field">
)");
SimulateElementRightClick("password-field");
SelectGenerationFallbackAndExpect(true);
// Simulate that the user accepts a generated password.
std::u16string password = u"random_password";
EXPECT_CALL(fake_pw_client_, PresaveGeneratedPassword(_, Eq(password)));
password_generation_->GeneratedPasswordAccepted(password);
WebInputElement password_element = GetInputElementById("password-field");
// Check that the password field is autofilled with the generated
// password.
EXPECT_EQ(password, password_element.Value().Utf16());
EXPECT_TRUE(password_element.IsAutofilled());
}
#endif // !BUILDFLAG(IS_ANDROID)
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);
FocusField(test_case.generation_element);
SelectGenerationFallbackAndExpect(true);
std::u16string password = u"random_password";
EXPECT_CALL(fake_pw_client_, PresaveGeneratedPassword(_, Eq(password)));
password_generation_->GeneratedPasswordAccepted(password);
base::RunLoop().RunUntilIdle();
ExpectEditingPopupOnFieldFocus(test_case.generation_element);
EXPECT_CALL(fake_pw_client_, PresaveGeneratedPassword(_, _));
SimulateUserTypingASCIICharacter('a', true);
base::RunLoop().RunUntilIdle();
ExpectGenerationElementLostFocus("username");
EXPECT_CALL(fake_pw_client_, PresaveGeneratedPassword(_, _));
SimulateUserTypingASCIICharacter('X', true);
base::RunLoop().RunUntilIdle();
testing::Mock::VerifyAndClearExpectations(&fake_pw_client_);
ExpectEditingPopupOnFieldFocus(test_case.generation_element);
SimulateUserTypingASCIICharacter(ui::VKEY_END, false);
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(_, _)).Times(0);
ExpectGenerationElementLostFocus("username");
SimulateUserTypingASCIICharacter('Y', true);
base::RunLoop().RunUntilIdle();
testing::Mock::VerifyAndClearExpectations(&fake_pw_client_);
}
}
TEST_F(PasswordGenerationAgentTest, FallbackForSaving) {
LoadHTMLWithUserGesture(kAccountCreationFormHTML);
FocusField("first_password");
SelectGenerationFallbackAndExpect(true);
EXPECT_EQ(0, fake_driver_.called_inform_about_user_input_count());
std::u16string password = u"random_password";
EXPECT_CALL(fake_pw_client_, PresaveGeneratedPassword(_, Eq(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_inform_about_user_input_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_inform_about_user_input_count());
}
TEST_F(PasswordGenerationAgentTest, AcceptAfterNavigation) {
LoadHTMLWithUserGesture(kAccountCreationFormHTML);
SetFoundFormEligibleForGeneration(
password_generation_, GetMainFrame()->GetDocument(),
/*new_password_id=*/"first_password", /*confirm_password_id=*/nullptr);
ExpectAutomaticGenerationAvailable("first_password", kAvailable);
// Navigation happens. Then browser UI accepts the generated password. It
// should not crash.
#if !BUILDFLAG(IS_ANDROID)
EXPECT_CALL(fake_pw_client_, GenerationElementLostFocus());
#endif // !BUILDFLAG(IS_ANDROID)
LoadHTMLWithUserGesture(kSigninFormHTML);
EXPECT_CALL(fake_pw_client_, PresaveGeneratedPassword).Times(0);
password_generation_->GeneratedPasswordAccepted(u"random_password");
}
TEST_F(PasswordGenerationAgentTest, FormClassifierDisabled) {
LoadHTMLWithUserGesture(kSigninFormHTML);
ExpectFormClassifierVoteReceived(false /* vote is not expected */,
std::u16string());
}
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(),
/*new_password_id=*/"password", /*confirm_password_id=*/nullptr);
const char* kGenerationElementId = "password";
const char* kSpanId = "span";
const char* kTextFieldId = "username";
ExpectAutomaticGenerationAvailable(kGenerationElementId, kAvailable);
std::u16string password = u"long_pwd";
EXPECT_CALL(fake_pw_client_, PresaveGeneratedPassword(_, Eq(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.
ExpectEditingPopupOnFieldFocus(kGenerationElementId);
fake_pw_client_.Flush();
WebInputElement input = GetInputElementById(kGenerationElementId);
EXPECT_TRUE(input.ShouldRevealPassword());
// Click on another HTML element.
const char* const click_target_name =
clickOnInputField ? kTextFieldId : kSpanId;
ExpectGenerationElementLostFocus(click_target_name);
EXPECT_FALSE(input.ShouldRevealPassword());
fake_pw_client_.Flush();
testing::Mock::VerifyAndClearExpectations(&fake_pw_client_);
}
}
TEST_F(PasswordGenerationAgentTest,
DoesNotResetGenerationWhenJavascriptClearedTheField) {
LoadHTMLWithUserGesture(kAccountCreationFormHTML);
SetFoundFormEligibleForGeneration(
password_generation_, GetMainFrame()->GetDocument(),
/*new_password_id=*/"first_password", /*confirm_password_id=*/nullptr);
const char kGenerationElementId[] = "first_password";
ExpectAutomaticGenerationAvailable(kGenerationElementId, kAvailable);
std::u16string password = u"long_pwd";
EXPECT_CALL(fake_pw_client_, PresaveGeneratedPassword(_, Eq(password)));
password_generation_->GeneratedPasswordAccepted(password);
// Should not reset the password generation as this is not drived by user.
// Some websites might clear the user data right before submission.
EXPECT_CALL(fake_pw_client_, PasswordNoLongerGenerated(_)).Times(0);
EXPECT_CALL(fake_pw_client_, AutomaticGenerationAvailable(_)).Times(0);
ExecuteJavaScriptForTests(
"document.getElementById('first_password').value = '';");
base::RunLoop().RunUntilIdle();
}
TEST_F(PasswordGenerationAgentTest,
ResetsGenerationWhenUserFocusesFieldClearedByJavascript) {
LoadHTMLWithUserGesture(kAccountCreationFormHTML);
SetFoundFormEligibleForGeneration(
password_generation_, GetMainFrame()->GetDocument(),
/*new_password_id=*/"first_password", /*confirm_password_id=*/nullptr);
const char kGenerationElementId[] = "first_password";
ExpectAutomaticGenerationAvailable(kGenerationElementId, kAvailable);
std::u16string password = u"long_pwd";
EXPECT_CALL(fake_pw_client_, PresaveGeneratedPassword(_, Eq(password)));
password_generation_->GeneratedPasswordAccepted(password);
// Should not reset the password generation as this is not drived by user.
// Some websites might clear the user data right before submission.
EXPECT_CALL(fake_pw_client_, PasswordNoLongerGenerated(testing::_)).Times(0);
EXPECT_CALL(fake_pw_client_, AutomaticGenerationAvailable(_)).Times(0);
ExecuteJavaScriptForTests(
"document.getElementById('first_password').value = '';");
base::RunLoop().RunUntilIdle();
// Should reset the password generation now, when user focuses an empty
// password field. Now we are sure that the form with the previously generated
// password won't be submitted.
EXPECT_CALL(fake_pw_client_, PasswordNoLongerGenerated(_)).Times(AtLeast(1));
EXPECT_CALL(fake_pw_client_, AutomaticGenerationAvailable(_))
.Times(AtLeast(1));
FocusField("first_password");
}
TEST_F(PasswordGenerationAgentTest, JavascriptClearedThePassword_TypeUsername) {
LoadHTMLWithUserGesture(kAccountCreationFormHTML);
constexpr char kGenerationElementId[] = "first_password";
SetFoundFormEligibleForGeneration(password_generation_,
GetMainFrame()->GetDocument(),
/*new_password_id=*/kGenerationElementId,
/*confirm_password_id=*/nullptr);
ExpectAutomaticGenerationAvailable(kGenerationElementId, kAvailable);
std::u16string password = u"long_pwd";
EXPECT_CALL(fake_pw_client_, PresaveGeneratedPassword(_, Eq(password)));
password_generation_->GeneratedPasswordAccepted(password);
// Edit some other field.
EXPECT_CALL(fake_pw_client_, PasswordNoLongerGenerated(testing::_));
#if !BUILDFLAG(IS_ANDROID)
EXPECT_CALL(fake_pw_client_, GenerationElementLostFocus());
#endif // !BUILDFLAG(IS_ANDROID)
ExecuteJavaScriptForTests(
"document.getElementById('first_password').value = '';");
FocusField("username");
SimulateUserTypingASCIICharacter('a', true);
}
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(),
/*new_password_id=*/"first_password", /*confirm_password_id=*/nullptr);
ExpectAutomaticGenerationAvailable("first_password", kAvailable);
WebDocument document = GetMainFrame()->GetDocument();
const WebInputElement input_element = GetInputElementById("first_password");
// 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(),
/*new_password_id=*/"first_password", /*confirm_password_id=*/nullptr);
constexpr char kGenerationElementId[] = "first_password";
// Generate a new password.
ExpectAutomaticGenerationAvailable(kGenerationElementId, kAvailable);
std::u16string password = u"random_password";
EXPECT_CALL(fake_pw_client_, PresaveGeneratedPassword(_, Eq(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.
ExpectEditingPopupOnFieldFocus(kGenerationElementId);
SimulateUserTypingASCIICharacter(ui::VKEY_END, false);
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();
WebInputElement input = GetInputElementById(kGenerationElementId);
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(),
/*new_password_id=*/kGenerationElementId,
/*confirm_password_id=*/nullptr);
// Generate a new password.
ExpectAutomaticGenerationAvailable(kGenerationElementId, kAvailable);
std::u16string password = u"random_password";
EXPECT_CALL(fake_pw_client_, PresaveGeneratedPassword(_, Eq(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.
ExpectEditingPopupOnFieldFocus(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.
WebInputElement input = GetInputElementById(kGenerationElementId);
EXPECT_TRUE(input.ShouldRevealPassword());
// Focus another element on the page. The password should be masked.
ExpectGenerationElementLostFocus("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 auto kPasswordElementsIds = std::to_array<const char*>({
"password",
"first_password",
"second_password",
});
WebDocument document = GetMainFrame()->GetDocument();
std::vector<WebInputElement> password_elements;
for (const char* id : kPasswordElementsIds) {
WebInputElement input = GetInputElementById(id);
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 =
autofill::form_util::GetFieldRendererId(password_elements[0]);
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 =
autofill::form_util::GetFieldRendererId(password_elements[2]);
password_generation_->FoundFormEligibleForGeneration(generation_data);
ExpectAutomaticGenerationAvailable(kPasswordElementsIds[0], kAvailable);
ExpectGenerationElementLostFocus(kPasswordElementsIds[1]);
ExpectAutomaticGenerationAvailable(kPasswordElementsIds[2], kAvailable);
}
TEST_F(PasswordGenerationAgentTest, SuggestionPreviewedAndClearedTest) {
LoadHTMLWithUserGesture(kAccountCreationFormHTML);
WebDocument document = GetMainFrame()->GetDocument();
SetFoundFormEligibleForGeneration(password_generation_,
GetMainFrame()->GetDocument(),
/*new_password_id=*/"first_password",
/*confirm_password_id=*/"second_password");
ExpectAutomaticGenerationAvailable("first_password", kAvailable);
WebInputElement first_password_element =
GetInputElementById("first_password");
WebInputElement second_password_element =
GetInputElementById("second_password");
std::u16string password = u"random_password";
password_generation_->PreviewGenerationSuggestion(password);
// Both password fields should have suggested values.
CheckPreviewedValue(first_password_element, password);
CheckPreviewedValue(second_password_element, password);
// Previewed suggestions should be successfully cleared upon request.
password_generation_->ClearPreviewedForm();
EXPECT_TRUE(first_password_element.SuggestedValue().IsNull());
EXPECT_TRUE(second_password_element.SuggestedValue().IsNull());
EXPECT_EQ(first_password_element.GetAutofillState(),
blink::WebAutofillState::kNotFilled);
EXPECT_EQ(second_password_element.GetAutofillState(),
blink::WebAutofillState::kNotFilled);
}
TEST_F(PasswordGenerationAgentTest, SuggestionPreviewedAndFilledTest) {
LoadHTMLWithUserGesture(kAccountCreationFormHTML);
WebDocument document = GetMainFrame()->GetDocument();
SetFoundFormEligibleForGeneration(password_generation_,
GetMainFrame()->GetDocument(),
/*new_password_id=*/"first_password",
/*confirm_password_id=*/"second_password");
ExpectAutomaticGenerationAvailable("first_password", kAvailable);
WebInputElement first_password_element =
GetInputElementById("first_password");
WebInputElement second_password_element =
GetInputElementById("second_password");
std::u16string password = u"random_password";
password_generation_->PreviewGenerationSuggestion(password);
// Both password fields should have suggested values.
CheckPreviewedValue(first_password_element, password);
CheckPreviewedValue(second_password_element, password);
// Previewed suggestion should be successfully cleared when the
// suggestion is accepted.
EXPECT_CALL(fake_pw_client_, PresaveGeneratedPassword(_, Eq(password)));
password_generation_->GeneratedPasswordAccepted(password);
EXPECT_TRUE(first_password_element.SuggestedValue().IsNull());
EXPECT_TRUE(second_password_element.SuggestedValue().IsNull());
EXPECT_EQ(first_password_element.GetAutofillState(),
blink::WebAutofillState::kAutofilled);
EXPECT_EQ(second_password_element.GetAutofillState(),
blink::WebAutofillState::kAutofilled);
}
TEST_F(PasswordGenerationAgentTest, AdvancesFocusToNextFieldAfterPasswords) {
constexpr char kGenerationElementId[] = "first_password";
LoadHTMLWithUserGesture(kAccountCreationFormHTML);
SetFoundFormEligibleForGeneration(password_generation_,
GetMainFrame()->GetDocument(),
/*new_password_id=*/kGenerationElementId,
/*confirm_password_id=*/"second_password");
ExpectAutomaticGenerationAvailable(kGenerationElementId, kAvailable);
password_generation_->FocusNextFieldAfterPasswords();
WebDocument document = GetMainFrame()->GetDocument();
EXPECT_EQ(document.FocusedElement(), GetElementById("address"));
}
TEST_F(PasswordGenerationAgentTest,
MasksPasswordAfterReplacingTextBySelectingAll) {
LoadHTMLWithUserGesture(kAccountCreationFormHTML);
SetFoundFormEligibleForGeneration(password_generation_,
GetMainFrame()->GetDocument(),
/*new_password_id=*/"first_password",
/*confirm_password_id=*/"second_password");
WebInputElement first_password_element =
GetInputElementById("first_password");
WebInputElement second_password_element =
GetInputElementById("second_password");
// Generate password.
ExpectAutomaticGenerationAvailable("first_password", kAvailable);
std::u16string password = u"random_password";
EXPECT_CALL(fake_pw_client_, PresaveGeneratedPassword(_, Eq(password)));
password_generation_->GeneratedPasswordAccepted(password);
fake_pw_client_.Flush();
testing::Mock::VerifyAndClearExpectations(&fake_pw_client_);
// Verify that values in both fields are mirrored.
EXPECT_EQ(first_password_element.Value().Utf16(), password);
EXPECT_EQ(second_password_element.Value().Utf16(), password);
// Focus first password field, select all characters and type a letter.
ExpectEditingPopupOnFieldFocus("first_password");
first_password_element.SetSelectionRange(0, password.length());
fake_pw_client_.Flush();
testing::Mock::VerifyAndClearExpectations(&fake_pw_client_);
#if !BUILDFLAG(IS_ANDROID)
EXPECT_CALL(fake_pw_client_, PasswordGenerationRejectedByTyping);
#endif // !BUILDFLAG(IS_ANDROID)
EXPECT_CALL(fake_pw_client_, PasswordNoLongerGenerated);
SimulateUserTypingASCIICharacter('X', /*flush_message_loop=*/true);
// First field should contain the typed letter, second field should be empty
// since the password is no longer generated. Both fields should be masked.
EXPECT_EQ(first_password_element.Value().Utf16(), u"X");
EXPECT_TRUE(second_password_element.Value().Utf16().empty());
EXPECT_FALSE(first_password_element.ShouldRevealPassword());
EXPECT_FALSE(second_password_element.ShouldRevealPassword());
}
TEST_F(PasswordGenerationAgentTest,
MasksPasswordAfterTypingWithGenerationPopupVisible) {
LoadHTMLWithUserGesture(kAccountCreationFormHTML);
SetFoundFormEligibleForGeneration(password_generation_,
GetMainFrame()->GetDocument(),
/*new_password_id=*/"first_password",
/*confirm_password_id=*/"second_password");
ExpectAutomaticGenerationAvailable("first_password", kAvailable);
// Type a character. This should result in popup being hidden and the password
// field masked.
#if !BUILDFLAG(IS_ANDROID)
EXPECT_CALL(fake_pw_client_, PasswordGenerationRejectedByTyping);
#endif // !BUILDFLAG(IS_ANDROID)
SimulateUserTypingASCIICharacter('X', /*flush_message_loop=*/true);
// First field should contain the typed letter, second field should be empty.
// Both fields should be masked.
WebInputElement first_password_element =
GetInputElementById("first_password");
WebInputElement second_password_element =
GetInputElementById("second_password");
EXPECT_EQ(first_password_element.Value().Utf16(), u"X");
EXPECT_TRUE(second_password_element.Value().Utf16().empty());
EXPECT_FALSE(first_password_element.ShouldRevealPassword());
EXPECT_FALSE(second_password_element.ShouldRevealPassword());
}
// Tests that automatic generation is suggested after the field has become a
// password field at least once.
TEST_F(PasswordGenerationAgentTest, AutomaticSuggestionOnHasBeenPasswordField) {
LoadHTML(
R"(
<input type="text" id="username-field" name="username-field">
<input type="password" id="password-field" name="password-field">
)");
constexpr char kPasswordElementId[] = "password-field";
WebInputElement password_field = GetInputElementById(kPasswordElementId);
// Simulate parser finding the field to trigger generation on.
SetFoundFormEligibleForGeneration(
password_generation_, GetMainFrame()->GetDocument(),
/*new_password_id=*/kPasswordElementId, /*confirm_password_id=*/nullptr);
// User clicks on reveal password button, changing the field type to
// text.
password_field.SetAttribute("type", "text");
// Automatic generation should still be available.
ExpectAutomaticGenerationAvailable(kPasswordElementId, kAvailable);
}
} // namespace
} // namespace autofill