| // Copyright 2018 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 "chrome/browser/password_manager/password_accessory_controller_impl.h" |
| |
| #include <algorithm> |
| #include <memory> |
| #include <ostream> |
| #include <string> |
| #include <vector> |
| |
| #include "base/callback.h" |
| #include "base/run_loop.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "base/test/metrics/histogram_tester.h" |
| #include "base/test/mock_callback.h" |
| #include "chrome/browser/autofill/mock_manual_filling_controller.h" |
| #include "chrome/grit/generated_resources.h" |
| #include "chrome/test/base/chrome_render_view_host_test_harness.h" |
| #include "components/autofill/core/common/password_form.h" |
| #include "components/autofill/core/common/signatures_util.h" |
| #include "components/favicon/core/test/mock_favicon_service.h" |
| #include "components/password_manager/core/browser/stub_password_manager_driver.h" |
| #include "components/strings/grit/components_strings.h" |
| #include "testing/gmock/include/gmock/gmock.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| #include "third_party/skia/include/core/SkBitmap.h" |
| #include "ui/base/l10n/l10n_util.h" |
| #include "ui/gfx/codec/png_codec.h" |
| |
| namespace { |
| using autofill::AccessorySheetData; |
| using autofill::FillingStatus; |
| using autofill::FooterCommand; |
| using autofill::PasswordForm; |
| using autofill::UserInfo; |
| using base::ASCIIToUTF16; |
| using testing::_; |
| using testing::ByMove; |
| using testing::Mock; |
| using testing::NiceMock; |
| using testing::Return; |
| using testing::StrictMock; |
| using FillingSource = ManualFillingController::FillingSource; |
| |
| constexpr char kExampleSite[] = "https://example.com"; |
| constexpr char kExampleDomain[] = "example.com"; |
| constexpr int kIconSize = 75; // An example size for favicons (=> 3.5*20px). |
| |
| // Helper class for AccessorySheetData objects creation. |
| // |
| // Example that creates a AccessorySheetData object with two UserInfo objects; |
| // the former has two fields, whereas the latter has three fields: |
| // AccessorySheetData data = AccessorySheetDataBuilder(title) |
| // .AddUserInfo() |
| // .AppendField(...) |
| // .AppendField(...) |
| // .AddUserInfo() |
| // .AppendField(...) |
| // .AppendField(...) |
| // .AppendField(...) |
| // .Build(); |
| class AccessorySheetDataBuilder { |
| public: |
| explicit AccessorySheetDataBuilder(const base::string16& title) |
| : accessory_sheet_data_(autofill::FallbackSheetType::PASSWORD, title) {} |
| |
| ~AccessorySheetDataBuilder() = default; |
| |
| // Adds a new UserInfo object to |accessory_sheet_data_|. |
| AccessorySheetDataBuilder& AddUserInfo() { |
| accessory_sheet_data_.add_user_info(UserInfo()); |
| return *this; |
| } |
| |
| // Appends a field to the last UserInfo object. |
| AccessorySheetDataBuilder& AppendField(const base::string16& display_text, |
| const base::string16& a11y_description, |
| bool is_obfuscated, |
| bool selectable) { |
| accessory_sheet_data_.mutable_user_info_list().back().add_field( |
| UserInfo::Field(display_text, a11y_description, is_obfuscated, |
| selectable)); |
| return *this; |
| } |
| |
| // Appends a new footer command to |accessory_sheet_data_|. |
| AccessorySheetDataBuilder& AppendFooterCommand( |
| const base::string16& display_text) { |
| accessory_sheet_data_.add_footer_command(FooterCommand(display_text)); |
| return *this; |
| } |
| |
| const AccessorySheetData& Build() { return accessory_sheet_data_; } |
| |
| private: |
| AccessorySheetData accessory_sheet_data_; |
| }; |
| |
| // Creates a new map entry in the |first| element of the returned pair. The |
| // |second| element holds the PasswordForm that the |first| element points to. |
| // That way, the pointer only points to a valid address in the called scope. |
| std::pair<std::pair<base::string16, const PasswordForm*>, |
| std::unique_ptr<const PasswordForm>> |
| CreateEntry(const std::string& username, const std::string& password) { |
| PasswordForm form; |
| form.username_value = ASCIIToUTF16(username); |
| form.password_value = ASCIIToUTF16(password); |
| std::unique_ptr<const PasswordForm> form_ptr( |
| new PasswordForm(std::move(form))); |
| auto username_form_pair = |
| std::make_pair(ASCIIToUTF16(username), form_ptr.get()); |
| return {std::move(username_form_pair), std::move(form_ptr)}; |
| } |
| |
| base::string16 password_for_str(const base::string16& user) { |
| return l10n_util::GetStringFUTF16( |
| IDS_PASSWORD_MANAGER_ACCESSORY_PASSWORD_DESCRIPTION, user); |
| } |
| |
| base::string16 password_for_str(const std::string& user) { |
| return password_for_str(ASCIIToUTF16(user)); |
| } |
| |
| base::string16 passwords_empty_str(const std::string& domain) { |
| return l10n_util::GetStringFUTF16( |
| IDS_PASSWORD_MANAGER_ACCESSORY_PASSWORD_LIST_EMPTY_MESSAGE, |
| ASCIIToUTF16(domain)); |
| } |
| |
| base::string16 passwords_title_str(const std::string& domain) { |
| return l10n_util::GetStringFUTF16( |
| IDS_PASSWORD_MANAGER_ACCESSORY_PASSWORD_LIST_TITLE, ASCIIToUTF16(domain)); |
| } |
| |
| base::string16 no_user_str() { |
| return l10n_util::GetStringUTF16(IDS_PASSWORD_MANAGER_EMPTY_LOGIN); |
| } |
| |
| base::string16 manage_passwords_str() { |
| return l10n_util::GetStringUTF16( |
| IDS_PASSWORD_MANAGER_ACCESSORY_ALL_PASSWORDS_LINK); |
| } |
| |
| // Creates a AccessorySheetDataBuilder object with a "Manage passwords..." |
| // footer. |
| AccessorySheetDataBuilder PasswordAccessorySheetDataBuilder( |
| const base::string16& title) { |
| AccessorySheetDataBuilder builder(title); |
| builder.AppendFooterCommand(manage_passwords_str()); |
| return builder; |
| } |
| |
| } // namespace |
| |
| // Automagically used to pretty-print UserInfo::Field. Must be in same |
| // namespace. |
| void PrintTo(const UserInfo::Field& field, std::ostream* os) { |
| *os << "(display text: \"" << base::UTF16ToUTF8(field.display_text()) |
| << "\", a11y_description: \"" |
| << base::UTF16ToUTF8(field.a11y_description()) << "\", is " |
| << (field.is_obfuscated() ? "" : "not ") << "obfuscated, is " |
| << (field.selectable() ? "" : "not ") << "selectable)"; |
| } |
| |
| // Automagically used to pretty-print UserInfo. Must be in same namespace. |
| void PrintTo(const UserInfo& user_info, std::ostream* os) { |
| *os << "["; |
| for (const UserInfo::Field& field : user_info.fields()) { |
| PrintTo(field, os); |
| *os << ", "; |
| } |
| *os << "]"; |
| } |
| |
| // Automagically used to pretty-print FooterCommand. Must be in same namespace. |
| void PrintTo(const FooterCommand& footer_command, std::ostream* os) { |
| *os << "(display text: \"" << base::UTF16ToUTF8(footer_command.display_text()) |
| << "\")"; |
| } |
| |
| // Automagically used to pretty-print AccessorySheetData. Must be in same |
| // namespace. |
| void PrintTo(const AccessorySheetData& data, std::ostream* os) { |
| *os << "has title: \"" << data.title() << "\", has user info list: ["; |
| for (const UserInfo& user_info : data.user_info_list()) { |
| PrintTo(user_info, os); |
| *os << ", "; |
| } |
| *os << "], has footer commands: ["; |
| for (const FooterCommand& footer_command : data.footer_commands()) { |
| PrintTo(footer_command, os); |
| *os << ", "; |
| } |
| *os << "]"; |
| } |
| |
| class PasswordAccessoryControllerTest : public ChromeRenderViewHostTestHarness { |
| public: |
| PasswordAccessoryControllerTest() |
| : mock_favicon_service_( |
| std::make_unique<StrictMock<favicon::MockFaviconService>>()) {} |
| |
| void SetUp() override { |
| ChromeRenderViewHostTestHarness::SetUp(); |
| NavigateAndCommit(GURL(kExampleSite)); |
| PasswordAccessoryControllerImpl::CreateForWebContentsForTesting( |
| web_contents(), mock_manual_filling_controller_.AsWeakPtr(), |
| favicon_service()); |
| NavigateAndCommit(GURL(kExampleSite)); |
| } |
| |
| PasswordAccessoryController* controller() { |
| return PasswordAccessoryControllerImpl::FromWebContents(web_contents()); |
| } |
| |
| |
| favicon::MockFaviconService* favicon_service() { |
| return mock_favicon_service_.get(); |
| } |
| |
| protected: |
| StrictMock<MockManualFillingController> mock_manual_filling_controller_; |
| |
| private: |
| std::unique_ptr<StrictMock<favicon::MockFaviconService>> |
| mock_favicon_service_; |
| }; |
| |
| TEST_F(PasswordAccessoryControllerTest, IsNotRecreatedForSameWebContents) { |
| PasswordAccessoryControllerImpl* initial_controller = |
| PasswordAccessoryControllerImpl::FromWebContents(web_contents()); |
| EXPECT_NE(nullptr, initial_controller); |
| PasswordAccessoryControllerImpl::CreateForWebContents(web_contents()); |
| EXPECT_EQ(PasswordAccessoryControllerImpl::FromWebContents(web_contents()), |
| initial_controller); |
| } |
| |
| TEST_F(PasswordAccessoryControllerTest, TransformsMatchesToSuggestions) { |
| controller()->SavePasswordsForOrigin({CreateEntry("Ben", "S3cur3").first}, |
| url::Origin::Create(GURL(kExampleSite))); |
| EXPECT_CALL( |
| mock_manual_filling_controller_, |
| RefreshSuggestionsForField( |
| /*is_fillable=*/true, |
| PasswordAccessorySheetDataBuilder(passwords_title_str(kExampleDomain)) |
| .AddUserInfo() |
| .AppendField(ASCIIToUTF16("Ben"), ASCIIToUTF16("Ben"), false, |
| true) |
| .AppendField(ASCIIToUTF16("S3cur3"), password_for_str("Ben"), |
| true, false) |
| .Build())); |
| EXPECT_CALL(mock_manual_filling_controller_, |
| Hide(FillingSource::PASSWORD_FALLBACKS)); |
| controller()->RefreshSuggestionsForField( |
| url::Origin::Create(GURL(kExampleSite)), |
| /*is_fillable=*/true, |
| /*is_password_field=*/false); |
| } |
| |
| TEST_F(PasswordAccessoryControllerTest, HintsToEmptyUserNames) { |
| controller()->SavePasswordsForOrigin({CreateEntry("", "S3cur3").first}, |
| url::Origin::Create(GURL(kExampleSite))); |
| |
| EXPECT_CALL( |
| mock_manual_filling_controller_, |
| RefreshSuggestionsForField( |
| /*is_fillable=*/true, |
| PasswordAccessorySheetDataBuilder(passwords_title_str(kExampleDomain)) |
| .AddUserInfo() |
| .AppendField(no_user_str(), no_user_str(), false, false) |
| .AppendField(ASCIIToUTF16("S3cur3"), |
| password_for_str(no_user_str()), true, false) |
| .Build())); |
| EXPECT_CALL(mock_manual_filling_controller_, |
| Hide(FillingSource::PASSWORD_FALLBACKS)); |
| controller()->RefreshSuggestionsForField( |
| url::Origin::Create(GURL(kExampleSite)), |
| /*is_fillable=*/true, |
| /*is_password_field=*/false); |
| } |
| |
| TEST_F(PasswordAccessoryControllerTest, SortsAlphabeticalDuringTransform) { |
| controller()->SavePasswordsForOrigin( |
| {CreateEntry("Ben", "S3cur3").first, CreateEntry("Zebra", "M3h").first, |
| CreateEntry("Alf", "PWD").first, CreateEntry("Cat", "M1@u").first}, |
| url::Origin::Create(GURL(kExampleSite))); |
| |
| EXPECT_CALL( |
| mock_manual_filling_controller_, |
| RefreshSuggestionsForField( |
| /*is_fillable=*/true, |
| PasswordAccessorySheetDataBuilder(passwords_title_str(kExampleDomain)) |
| .AddUserInfo() |
| .AppendField(ASCIIToUTF16("Alf"), ASCIIToUTF16("Alf"), false, |
| true) |
| .AppendField(ASCIIToUTF16("PWD"), password_for_str("Alf"), true, |
| false) |
| .AddUserInfo() |
| .AppendField(ASCIIToUTF16("Ben"), ASCIIToUTF16("Ben"), false, |
| true) |
| .AppendField(ASCIIToUTF16("S3cur3"), password_for_str("Ben"), |
| true, false) |
| .AddUserInfo() |
| .AppendField(ASCIIToUTF16("Cat"), ASCIIToUTF16("Cat"), false, |
| true) |
| .AppendField(ASCIIToUTF16("M1@u"), password_for_str("Cat"), true, |
| false) |
| .AddUserInfo() |
| .AppendField(ASCIIToUTF16("Zebra"), ASCIIToUTF16("Zebra"), false, |
| true) |
| .AppendField(ASCIIToUTF16("M3h"), password_for_str("Zebra"), true, |
| false) |
| .Build())); |
| EXPECT_CALL(mock_manual_filling_controller_, |
| Hide(FillingSource::PASSWORD_FALLBACKS)); |
| controller()->RefreshSuggestionsForField( |
| url::Origin::Create(GURL(kExampleSite)), |
| /*is_fillable=*/true, |
| /*is_password_field=*/false); |
| } |
| |
| TEST_F(PasswordAccessoryControllerTest, RepeatsSuggestionsForSameFrame) { |
| controller()->SavePasswordsForOrigin({CreateEntry("Ben", "S3cur3").first}, |
| url::Origin::Create(GURL(kExampleSite))); |
| |
| // Pretend that any input in the same frame was focused. |
| EXPECT_CALL( |
| mock_manual_filling_controller_, |
| RefreshSuggestionsForField( |
| /*is_fillable=*/true, |
| PasswordAccessorySheetDataBuilder(passwords_title_str(kExampleDomain)) |
| .AddUserInfo() |
| .AppendField(ASCIIToUTF16("Ben"), ASCIIToUTF16("Ben"), false, |
| true) |
| .AppendField(ASCIIToUTF16("S3cur3"), password_for_str("Ben"), |
| true, false) |
| .Build())); |
| EXPECT_CALL(mock_manual_filling_controller_, |
| Hide(FillingSource::PASSWORD_FALLBACKS)); |
| controller()->RefreshSuggestionsForField( |
| url::Origin::Create(GURL(kExampleSite)), |
| /*is_fillable=*/true, |
| /*is_password_field=*/false); |
| } |
| |
| TEST_F(PasswordAccessoryControllerTest, ProvidesEmptySuggestionsMessage) { |
| controller()->SavePasswordsForOrigin({}, |
| url::Origin::Create(GURL(kExampleSite))); |
| |
| EXPECT_CALL(mock_manual_filling_controller_, |
| RefreshSuggestionsForField( |
| /*is_fillable=*/true, PasswordAccessorySheetDataBuilder( |
| passwords_empty_str(kExampleDomain)) |
| .Build())); |
| EXPECT_CALL(mock_manual_filling_controller_, |
| Hide(FillingSource::PASSWORD_FALLBACKS)); |
| controller()->RefreshSuggestionsForField( |
| url::Origin::Create(GURL(kExampleSite)), |
| /*is_fillable=*/true, |
| /*is_password_field=*/false); |
| } |
| |
| TEST_F(PasswordAccessoryControllerTest, OnFilledIntoFocusedField) { |
| EXPECT_CALL(mock_manual_filling_controller_, |
| OnFilledIntoFocusedField(FillingStatus::ERROR_NOT_ALLOWED)); |
| controller()->OnFilledIntoFocusedField(FillingStatus::ERROR_NOT_ALLOWED); |
| |
| EXPECT_CALL(mock_manual_filling_controller_, |
| OnFilledIntoFocusedField(FillingStatus::ERROR_NO_VALID_FIELD)); |
| controller()->OnFilledIntoFocusedField(FillingStatus::ERROR_NO_VALID_FIELD); |
| |
| EXPECT_CALL(mock_manual_filling_controller_, |
| OnFilledIntoFocusedField(FillingStatus::SUCCESS)); |
| controller()->OnFilledIntoFocusedField(FillingStatus::SUCCESS); |
| } |
| |
| TEST_F(PasswordAccessoryControllerTest, PasswordFieldChangesSuggestionType) { |
| controller()->SavePasswordsForOrigin({CreateEntry("Ben", "S3cur3").first}, |
| url::Origin::Create(GURL(kExampleSite))); |
| // Pretend a username field was focused. This should result in non-interactive |
| // suggestion. |
| EXPECT_CALL( |
| mock_manual_filling_controller_, |
| RefreshSuggestionsForField( |
| /*is_fillable=*/true, |
| PasswordAccessorySheetDataBuilder(passwords_title_str(kExampleDomain)) |
| .AddUserInfo() |
| .AppendField(ASCIIToUTF16("Ben"), ASCIIToUTF16("Ben"), false, |
| true) |
| .AppendField(ASCIIToUTF16("S3cur3"), password_for_str("Ben"), |
| true, false) |
| .Build())); |
| EXPECT_CALL(mock_manual_filling_controller_, |
| Hide(FillingSource::PASSWORD_FALLBACKS)); |
| controller()->RefreshSuggestionsForField( |
| url::Origin::Create(GURL(kExampleSite)), |
| /*is_fillable=*/true, |
| /*is_password_field=*/false); |
| |
| // Pretend that we focus a password field now: By triggering a refresh with |
| // |is_password_field| set to true, all suggestions should become interactive. |
| EXPECT_CALL( |
| mock_manual_filling_controller_, |
| RefreshSuggestionsForField( |
| /*is_fillable=*/true, |
| PasswordAccessorySheetDataBuilder(passwords_title_str(kExampleDomain)) |
| .AddUserInfo() |
| .AppendField(ASCIIToUTF16("Ben"), ASCIIToUTF16("Ben"), false, |
| false) |
| .AppendField(ASCIIToUTF16("S3cur3"), password_for_str("Ben"), |
| true, true) |
| .Build())); |
| EXPECT_CALL(mock_manual_filling_controller_, |
| ShowWhenKeyboardIsVisible(FillingSource::PASSWORD_FALLBACKS)); |
| controller()->RefreshSuggestionsForField( |
| url::Origin::Create(GURL(kExampleSite)), |
| /*is_fillable=*/true, |
| /*is_password_field=*/true); |
| } |
| |
| TEST_F(PasswordAccessoryControllerTest, CachesIsReplacedByNewPasswords) { |
| controller()->SavePasswordsForOrigin({CreateEntry("Ben", "S3cur3").first}, |
| url::Origin::Create(GURL(kExampleSite))); |
| EXPECT_CALL( |
| mock_manual_filling_controller_, |
| RefreshSuggestionsForField( |
| /*is_fillable=*/true, |
| PasswordAccessorySheetDataBuilder(passwords_title_str(kExampleDomain)) |
| .AddUserInfo() |
| .AppendField(ASCIIToUTF16("Ben"), ASCIIToUTF16("Ben"), false, |
| true) |
| .AppendField(ASCIIToUTF16("S3cur3"), password_for_str("Ben"), |
| true, false) |
| .Build())); |
| EXPECT_CALL(mock_manual_filling_controller_, |
| Hide(FillingSource::PASSWORD_FALLBACKS)); |
| controller()->RefreshSuggestionsForField( |
| url::Origin::Create(GURL(kExampleSite)), |
| /*is_fillable=*/true, |
| /*is_password_field=*/false); |
| |
| controller()->SavePasswordsForOrigin({CreateEntry("Alf", "M3lm4k").first}, |
| url::Origin::Create(GURL(kExampleSite))); |
| EXPECT_CALL( |
| mock_manual_filling_controller_, |
| RefreshSuggestionsForField( |
| /*is_fillable=*/true, |
| PasswordAccessorySheetDataBuilder(passwords_title_str(kExampleDomain)) |
| .AddUserInfo() |
| .AppendField(ASCIIToUTF16("Alf"), ASCIIToUTF16("Alf"), false, |
| true) |
| .AppendField(ASCIIToUTF16("M3lm4k"), password_for_str("Alf"), |
| true, false) |
| .Build())); |
| EXPECT_CALL(mock_manual_filling_controller_, |
| Hide(FillingSource::PASSWORD_FALLBACKS)); |
| controller()->RefreshSuggestionsForField( |
| url::Origin::Create(GURL(kExampleSite)), |
| /*is_fillable=*/true, |
| /*is_password_field=*/false); |
| } |
| |
| TEST_F(PasswordAccessoryControllerTest, UnfillableFieldClearsSuggestions) { |
| controller()->SavePasswordsForOrigin({CreateEntry("Ben", "S3cur3").first}, |
| url::Origin::Create(GURL(kExampleSite))); |
| // Pretend a username field was focused. This should result in non-emtpy |
| // suggestions. |
| EXPECT_CALL( |
| mock_manual_filling_controller_, |
| RefreshSuggestionsForField( |
| /*is_fillable=*/true, |
| PasswordAccessorySheetDataBuilder(passwords_title_str(kExampleDomain)) |
| .AddUserInfo() |
| .AppendField(ASCIIToUTF16("Ben"), ASCIIToUTF16("Ben"), false, |
| true) |
| .AppendField(ASCIIToUTF16("S3cur3"), password_for_str("Ben"), |
| true, false) |
| .Build())); |
| EXPECT_CALL(mock_manual_filling_controller_, |
| Hide(FillingSource::PASSWORD_FALLBACKS)); |
| controller()->RefreshSuggestionsForField( |
| url::Origin::Create(GURL(kExampleSite)), |
| /*is_fillable=*/true, |
| /*is_password_field=*/false); |
| |
| // Pretend that the focus was lost or moved to an unfillable field. Now, only |
| // the empty state message should be sent. |
| EXPECT_CALL( |
| mock_manual_filling_controller_, |
| RefreshSuggestionsForField( |
| /*is_fillable=*/false, |
| PasswordAccessorySheetDataBuilder(passwords_empty_str(kExampleDomain)) |
| .Build())); |
| EXPECT_CALL(mock_manual_filling_controller_, |
| Hide(FillingSource::PASSWORD_FALLBACKS)); |
| controller()->RefreshSuggestionsForField( |
| url::Origin::Create(GURL(kExampleSite)), |
| /*is_fillable=*/false, |
| /*is_password_field=*/false); // Unused. |
| } |
| |
| TEST_F(PasswordAccessoryControllerTest, NavigatingMainFrameClearsSuggestions) { |
| // Set any, non-empty password list and pretend a username field was focused. |
| // This should result in non-emtpy suggestions. |
| controller()->SavePasswordsForOrigin({CreateEntry("Ben", "S3cur3").first}, |
| url::Origin::Create(GURL(kExampleSite))); |
| EXPECT_CALL( |
| mock_manual_filling_controller_, |
| RefreshSuggestionsForField( |
| /*is_fillable=*/true, |
| PasswordAccessorySheetDataBuilder(passwords_title_str(kExampleDomain)) |
| .AddUserInfo() |
| .AppendField(ASCIIToUTF16("Ben"), ASCIIToUTF16("Ben"), false, |
| true) |
| .AppendField(ASCIIToUTF16("S3cur3"), password_for_str("Ben"), |
| true, false) |
| .Build())); |
| EXPECT_CALL(mock_manual_filling_controller_, |
| Hide(FillingSource::PASSWORD_FALLBACKS)); |
| controller()->RefreshSuggestionsForField( |
| url::Origin::Create(GURL(kExampleSite)), |
| /*is_fillable=*/true, |
| /*is_password_field=*/false); |
| |
| // Pretend that the focus was lost or moved to an unfillable field. |
| NavigateAndCommit(GURL("https://random.other-site.org/")); |
| controller()->DidNavigateMainFrame(); |
| |
| // Now, only the empty state message should be sent. |
| EXPECT_CALL(mock_manual_filling_controller_, |
| RefreshSuggestionsForField( |
| /*is_fillable=*/true, |
| PasswordAccessorySheetDataBuilder( |
| passwords_empty_str("random.other-site.org")) |
| .Build())); |
| EXPECT_CALL(mock_manual_filling_controller_, |
| Hide(FillingSource::PASSWORD_FALLBACKS)); |
| controller()->RefreshSuggestionsForField( |
| url::Origin::Create(GURL("https://random.other-site.org/")), |
| /*is_fillable=*/true, |
| /*is_password_field=*/false); // Unused. |
| } |
| |
| TEST_F(PasswordAccessoryControllerTest, FetchFaviconForCurrentUrl) { |
| base::MockCallback<base::OnceCallback<void(const gfx::Image&)>> mock_callback; |
| |
| EXPECT_CALL(mock_manual_filling_controller_, |
| RefreshSuggestionsForField(/*is_fillable=*/true, _)); |
| EXPECT_CALL(mock_manual_filling_controller_, |
| Hide(FillingSource::PASSWORD_FALLBACKS)); |
| controller()->RefreshSuggestionsForField( |
| url::Origin::Create(GURL(kExampleSite)), |
| /*is_fillable=*/true, |
| /*is_password_field=*/false); |
| |
| EXPECT_CALL(*favicon_service(), GetRawFaviconForPageURL(GURL(kExampleSite), _, |
| kIconSize, _, _, _)) |
| .WillOnce(favicon::PostReply<6>(favicon_base::FaviconRawBitmapResult())); |
| EXPECT_CALL(mock_callback, Run); |
| controller()->GetFavicon(kIconSize, mock_callback.Get()); |
| base::RunLoop().RunUntilIdle(); |
| } |
| |
| TEST_F(PasswordAccessoryControllerTest, RequestsFaviconsOnceForOneOrigin) { |
| base::MockCallback<base::OnceCallback<void(const gfx::Image&)>> mock_callback; |
| |
| EXPECT_CALL(mock_manual_filling_controller_, RefreshSuggestionsForField( |
| /*is_fillable=*/true, _)); |
| EXPECT_CALL(mock_manual_filling_controller_, |
| Hide(FillingSource::PASSWORD_FALLBACKS)); |
| controller()->RefreshSuggestionsForField( |
| url::Origin::Create(GURL(kExampleSite)), |
| /*is_fillable=*/true, |
| /*is_password_field=*/false); |
| |
| EXPECT_CALL(*favicon_service(), GetRawFaviconForPageURL(GURL(kExampleSite), _, |
| kIconSize, _, _, _)) |
| .WillOnce(favicon::PostReply<6>(favicon_base::FaviconRawBitmapResult())); |
| EXPECT_CALL(mock_callback, Run).Times(2); |
| controller()->GetFavicon(kIconSize, mock_callback.Get()); |
| // The favicon service should already start to work on the request. |
| Mock::VerifyAndClearExpectations(favicon_service()); |
| |
| // This call is only enqueued (and the callback will be called afterwards). |
| controller()->GetFavicon(kIconSize, mock_callback.Get()); |
| |
| // After the async task is finished, both callbacks must be called. |
| base::RunLoop().RunUntilIdle(); |
| } |
| |
| TEST_F(PasswordAccessoryControllerTest, FaviconsAreCachedUntilNavigation) { |
| base::MockCallback<base::OnceCallback<void(const gfx::Image&)>> mock_callback; |
| |
| // We need a result with a non-empty image or it won't get cached. |
| favicon_base::FaviconRawBitmapResult non_empty_result; |
| SkBitmap bitmap; |
| bitmap.allocN32Pixels(kIconSize, kIconSize); |
| scoped_refptr<base::RefCountedBytes> data(new base::RefCountedBytes()); |
| gfx::PNGCodec::EncodeBGRASkBitmap(bitmap, false, &data->data()); |
| non_empty_result.bitmap_data = data; |
| non_empty_result.expired = false; |
| non_empty_result.pixel_size = gfx::Size(kIconSize, kIconSize); |
| non_empty_result.icon_type = favicon_base::IconType::kFavicon; |
| non_empty_result.icon_url = GURL(kExampleSite); |
| |
| // Populate the cache by requesting a favicon. |
| EXPECT_CALL(mock_manual_filling_controller_, |
| RefreshSuggestionsForField(/*is_fillable=*/true, _)); |
| EXPECT_CALL(mock_manual_filling_controller_, |
| Hide(FillingSource::PASSWORD_FALLBACKS)); |
| controller()->RefreshSuggestionsForField( |
| url::Origin::Create(GURL(kExampleSite)), |
| /*is_fillable=*/true, |
| /*is_password_field=*/false); |
| |
| EXPECT_CALL(*favicon_service(), GetRawFaviconForPageURL(GURL(kExampleSite), _, |
| kIconSize, _, _, _)) |
| .WillOnce(favicon::PostReply<6>(non_empty_result)); |
| EXPECT_CALL(mock_callback, Run).Times(1); |
| controller()->GetFavicon(kIconSize, mock_callback.Get()); |
| |
| base::RunLoop().RunUntilIdle(); |
| Mock::VerifyAndClearExpectations(&mock_callback); |
| |
| // This call is handled by the cache - no favicon service, no async request. |
| EXPECT_CALL(mock_callback, Run).Times(1); |
| controller()->GetFavicon(kIconSize, mock_callback.Get()); |
| Mock::VerifyAndClearExpectations(&mock_callback); |
| Mock::VerifyAndClearExpectations(favicon_service()); |
| |
| // The navigation to another origin clears the cache. |
| NavigateAndCommit(GURL("https://random.other-site.org/")); |
| controller()->DidNavigateMainFrame(); |
| NavigateAndCommit(GURL(kExampleSite)); // Same origin as intially. |
| controller()->DidNavigateMainFrame(); |
| EXPECT_CALL(mock_manual_filling_controller_, |
| RefreshSuggestionsForField(/*is_fillable=*/true, _)); |
| EXPECT_CALL(mock_manual_filling_controller_, |
| Hide(FillingSource::PASSWORD_FALLBACKS)); |
| controller()->RefreshSuggestionsForField( |
| url::Origin::Create(GURL(kExampleSite)), /*is_fillable=*/true, |
| /*is_password_field=*/false); |
| |
| // The cache was cleared, so now the service has to be queried again. |
| EXPECT_CALL(*favicon_service(), GetRawFaviconForPageURL(GURL(kExampleSite), _, |
| kIconSize, _, _, _)) |
| .WillOnce(favicon::PostReply<6>(non_empty_result)); |
| EXPECT_CALL(mock_callback, Run).Times(1); |
| controller()->GetFavicon(kIconSize, mock_callback.Get()); |
| base::RunLoop().RunUntilIdle(); |
| } |
| |
| TEST_F(PasswordAccessoryControllerTest, NoFaviconCallbacksWhenOriginChanges) { |
| base::MockCallback<base::OnceCallback<void(const gfx::Image&)>> mock_callback; |
| |
| EXPECT_CALL(mock_manual_filling_controller_, |
| RefreshSuggestionsForField(/*is_fillable=*/true, _)) |
| .Times(2); |
| EXPECT_CALL(mock_manual_filling_controller_, |
| Hide(FillingSource::PASSWORD_FALLBACKS)); |
| controller()->RefreshSuggestionsForField( |
| url::Origin::Create(GURL(kExampleSite)), true, false); |
| |
| // Right after starting the favicon request for example.com, another frame on |
| // the same site is focused. Even if the request is completed, the callback |
| // should not be called because the origin of the suggestions has changed. |
| EXPECT_CALL(*favicon_service(), GetRawFaviconForPageURL(GURL(kExampleSite), _, |
| kIconSize, _, _, _)) |
| .WillOnce(favicon::PostReply<6>(favicon_base::FaviconRawBitmapResult())); |
| EXPECT_CALL(mock_callback, Run).Times(0); |
| controller()->GetFavicon(kIconSize, mock_callback.Get()); |
| EXPECT_CALL(mock_manual_filling_controller_, |
| Hide(FillingSource::PASSWORD_FALLBACKS)); |
| controller()->RefreshSuggestionsForField( |
| url::Origin::Create(GURL("https://other.frame.com/")), true, false); |
| |
| base::RunLoop().RunUntilIdle(); |
| } |