blob: fde22789616a363e90af5eb8156607467c8deafc [file] [log] [blame]
// 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_(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();
}