blob: 9f2ffb3e0b27d2793bc951b167d37f2c0ea91bd8 [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.h"
#include <memory>
#include <string>
#include <vector>
#include "base/callback.h"
#include "base/memory/weak_ptr.h"
#include "base/optional.h"
#include "base/strings/string_util.h"
#include "base/strings/utf_string_conversions.h"
#include "base/test/mock_callback.h"
#include "base/test/scoped_task_environment.h"
#include "chrome/browser/password_manager/password_accessory_view_interface.h"
#include "chrome/browser/password_manager/password_generation_dialog_view_interface.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/password_generation_util.h"
#include "components/autofill/core/common/signatures_util.h"
#include "components/favicon/core/test/mock_favicon_service.h"
#include "components/password_manager/core/browser/password_generation_manager.h"
#include "components/password_manager/core/browser/stub_password_manager_client.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"
namespace {
using autofill::FillingStatus;
using autofill::PasswordForm;
using autofill::password_generation::PasswordGenerationUIData;
using base::ASCIIToUTF16;
using base::UTF16ToWide;
using testing::_;
using testing::AnyNumber;
using testing::ByMove;
using testing::ElementsAre;
using testing::Mock;
using testing::NotNull;
using testing::PrintToString;
using testing::Return;
using testing::StrictMock;
using AccessoryItem = PasswordAccessoryViewInterface::AccessoryItem;
using ItemType = AccessoryItem::Type;
constexpr char kExampleSite[] = "https://example.com";
constexpr char kExampleDomain[] = "example.com";
// The mock view mocks the platform-specific implementation. That also means
// that we have to care about the lifespan of the Controller because that would
// usually be responsibility of the view.
class MockPasswordAccessoryView : public PasswordAccessoryViewInterface {
public:
MockPasswordAccessoryView() = default;
MOCK_METHOD1(OnItemsAvailable, void(const std::vector<AccessoryItem>& items));
MOCK_METHOD1(OnFillingTriggered, void(const base::string16& textToFill));
MOCK_METHOD0(OnViewDestroyed, void());
MOCK_METHOD1(OnAutomaticGenerationStatusChanged, void(bool));
MOCK_METHOD0(CloseAccessorySheet, void());
MOCK_METHOD0(OpenKeyboard, void());
private:
DISALLOW_COPY_AND_ASSIGN(MockPasswordAccessoryView);
};
class MockPasswordManagerDriver
: public password_manager::StubPasswordManagerDriver {
public:
MockPasswordManagerDriver() = default;
MOCK_METHOD0(GetPasswordGenerationManager,
password_manager::PasswordGenerationManager*());
private:
DISALLOW_COPY_AND_ASSIGN(MockPasswordManagerDriver);
};
class MockPasswordGenerationManager
: public password_manager::PasswordGenerationManager {
public:
MockPasswordGenerationManager(password_manager::PasswordManagerClient* client,
password_manager::PasswordManagerDriver* driver)
: password_manager::PasswordGenerationManager(client, driver) {}
MOCK_METHOD5(GeneratePassword,
base::string16(const GURL&,
autofill::FormSignature,
autofill::FieldSignature,
uint32_t,
uint32_t*));
private:
DISALLOW_COPY_AND_ASSIGN(MockPasswordGenerationManager);
};
// Mock modal dialog view used to bypass the need of a valid top level window.
class MockPasswordGenerationDialogView
: public PasswordGenerationDialogViewInterface {
public:
MockPasswordGenerationDialogView() = default;
MOCK_METHOD1(Show, void(base::string16&));
private:
DISALLOW_COPY_AND_ASSIGN(MockPasswordGenerationDialogView);
};
// Pretty prints input for the |MatchesItem| matcher.
std::string PrintItem(const base::string16& text,
const base::string16& description,
bool is_password,
ItemType type) {
return "has text \"" + base::UTF16ToUTF8(text) + "\" and description \"" +
base::UTF16ToUTF8(description) + "\" and is " +
(is_password ? "" : "not ") +
"a password "
"and type is " +
PrintToString(static_cast<int>(type));
}
// Compares whether a given AccessoryItem is a label with the given text.
MATCHER_P(MatchesLabel, text, PrintItem(text, text, false, ItemType::LABEL)) {
return arg.text == text && arg.is_password == false &&
arg.content_description == text && arg.itemType == ItemType::LABEL;
}
// Compares whether a given AccessoryItem is a label with the given text.
MATCHER(IsDivider, "is a divider") {
return arg.text.empty() && arg.is_password == false &&
arg.content_description.empty() && arg.itemType == ItemType::DIVIDER;
}
// Compares whether a given AccessoryItem is a label with the given text.
MATCHER_P(MatchesOption, text, PrintItem(text, text, false, ItemType::OPTION)) {
return arg.text == text && arg.is_password == false &&
arg.content_description == text && arg.itemType == ItemType::OPTION;
}
// Compares whether a given AccessoryItem had the given properties.
MATCHER_P4(MatchesItem,
text,
description,
is_password,
itemType,
PrintItem(text, description, is_password, itemType)) {
return arg.text == text && arg.is_password == is_password &&
arg.content_description == description && arg.itemType == itemType;
}
// 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);
}
PasswordGenerationUIData GetTestGenerationUIData1() {
PasswordForm form;
form.form_data = autofill::FormData();
form.form_data.action = GURL("http://www.example1.com/accounts/Login");
form.form_data.origin = GURL("http://www.example1.com/accounts/LoginAuth");
PasswordGenerationUIData data;
data.password_form = form;
data.generation_element = ASCIIToUTF16("testelement1");
data.max_length = 10;
return data;
}
PasswordGenerationUIData GetTestGenerationUIData2() {
PasswordForm form;
form.form_data = autofill::FormData();
form.form_data.action = GURL("http://www.example2.com/accounts/Login");
form.form_data.origin = GURL("http://www.example2.com/accounts/LoginAuth");
PasswordGenerationUIData data;
data.password_form = form;
data.generation_element = ASCIIToUTF16("testelement2");
data.max_length = 11;
return data;
}
} // namespace
// Automagically used to pretty-print AccessoryItems. Must be in same namespace.
void PrintTo(const AccessoryItem& item, std::ostream* os) {
*os << "has text \"" << UTF16ToWide(item.text) << "\" and description \""
<< UTF16ToWide(item.content_description) << "\" and is "
<< (item.is_password ? "" : "not ") << "a password and type is "
<< PrintToString(static_cast<int>(item.itemType));
}
// Automagically used to pretty-print item vectors. Must be in same namespace.
void PrintTo(const std::vector<AccessoryItem>& items, std::ostream* os) {
*os << "has " << items.size() << " elements where\n";
for (size_t index = 0; index < items.size(); ++index) {
*os << "element #" << index << " ";
PrintTo(items[index], os);
*os << "\n";
}
}
class PasswordAccessoryControllerTest : public ChromeRenderViewHostTestHarness {
public:
PasswordAccessoryControllerTest()
: mock_favicon_service_(
std::make_unique<
testing::StrictMock<favicon::MockFaviconService>>()) {}
void SetUp() override {
ChromeRenderViewHostTestHarness::SetUp();
NavigateAndCommit(GURL(kExampleSite));
PasswordAccessoryController::CreateForWebContentsForTesting(
web_contents(),
std::make_unique<StrictMock<MockPasswordAccessoryView>>(),
mock_dialog_factory_.Get(), favicon_service());
NavigateAndCommit(GURL(kExampleSite));
EXPECT_CALL(*view(), CloseAccessorySheet()).Times(AnyNumber());
EXPECT_CALL(*view(), OpenKeyboard()).Times(AnyNumber());
}
PasswordAccessoryController* controller() {
return PasswordAccessoryController::FromWebContents(web_contents());
}
MockPasswordAccessoryView* view() {
return static_cast<MockPasswordAccessoryView*>(controller()->view());
}
const base::MockCallback<PasswordAccessoryController::CreateDialogFactory>&
mock_dialog_factory() {
return mock_dialog_factory_;
}
favicon::MockFaviconService* favicon_service() {
return mock_favicon_service_.get();
}
private:
base::MockCallback<PasswordAccessoryController::CreateDialogFactory>
mock_dialog_factory_;
std::unique_ptr<testing::StrictMock<favicon::MockFaviconService>>
mock_favicon_service_;
};
TEST_F(PasswordAccessoryControllerTest, IsNotRecreatedForSameWebContents) {
PasswordAccessoryController* initial_controller =
PasswordAccessoryController::FromWebContents(web_contents());
EXPECT_NE(nullptr, initial_controller);
PasswordAccessoryController::CreateForWebContents(web_contents());
EXPECT_EQ(PasswordAccessoryController::FromWebContents(web_contents()),
initial_controller);
}
TEST_F(PasswordAccessoryControllerTest, TransformsMatchesToSuggestions) {
controller()->SavePasswordsForOrigin({CreateEntry("Ben", "S3cur3").first},
url::Origin::Create(GURL(kExampleSite)));
EXPECT_CALL(*view(),
OnItemsAvailable(ElementsAre(
MatchesLabel(passwords_title_str(kExampleDomain)),
MatchesItem(ASCIIToUTF16("Ben"), ASCIIToUTF16("Ben"), false,
ItemType::SUGGESTION),
MatchesItem(ASCIIToUTF16("S3cur3"), password_for_str("Ben"),
true, ItemType::NON_INTERACTIVE_SUGGESTION),
IsDivider(), MatchesOption(manage_passwords_str()))));
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(
*view(),
OnItemsAvailable(ElementsAre(
MatchesLabel(passwords_title_str(kExampleDomain)),
MatchesItem(no_user_str(), no_user_str(), false,
ItemType::NON_INTERACTIVE_SUGGESTION),
MatchesItem(ASCIIToUTF16("S3cur3"), password_for_str(no_user_str()),
true, ItemType::NON_INTERACTIVE_SUGGESTION),
IsDivider(), MatchesOption(manage_passwords_str()))));
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(*view(),
OnItemsAvailable(ElementsAre(
MatchesLabel(passwords_title_str(kExampleDomain)),
MatchesItem(ASCIIToUTF16("Alf"), ASCIIToUTF16("Alf"), false,
ItemType::SUGGESTION),
MatchesItem(ASCIIToUTF16("PWD"), password_for_str("Alf"),
true, ItemType::NON_INTERACTIVE_SUGGESTION),
MatchesItem(ASCIIToUTF16("Ben"), ASCIIToUTF16("Ben"), false,
ItemType::SUGGESTION),
MatchesItem(ASCIIToUTF16("S3cur3"), password_for_str("Ben"),
true, ItemType::NON_INTERACTIVE_SUGGESTION),
MatchesItem(ASCIIToUTF16("Cat"), ASCIIToUTF16("Cat"), false,
ItemType::SUGGESTION),
MatchesItem(ASCIIToUTF16("M1@u"), password_for_str("Cat"),
true, ItemType::NON_INTERACTIVE_SUGGESTION),
MatchesItem(ASCIIToUTF16("Zebra"), ASCIIToUTF16("Zebra"),
false, ItemType::SUGGESTION),
MatchesItem(ASCIIToUTF16("M3h"), password_for_str("Zebra"),
true, ItemType::NON_INTERACTIVE_SUGGESTION),
IsDivider(), MatchesOption(manage_passwords_str()))));
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(*view(),
OnItemsAvailable(ElementsAre(
MatchesLabel(passwords_title_str(kExampleDomain)),
MatchesItem(ASCIIToUTF16("Ben"), ASCIIToUTF16("Ben"), false,
ItemType::SUGGESTION),
MatchesItem(ASCIIToUTF16("S3cur3"), password_for_str("Ben"),
true, ItemType::NON_INTERACTIVE_SUGGESTION),
IsDivider(), MatchesOption(manage_passwords_str()))));
controller()->RefreshSuggestionsForField(
url::Origin::Create(GURL(kExampleSite)),
/*is_fillable=*/true,
/*is_fillable=*/false);
}
TEST_F(PasswordAccessoryControllerTest, ProvidesEmptySuggestionsMessage) {
controller()->SavePasswordsForOrigin({},
url::Origin::Create(GURL(kExampleSite)));
EXPECT_CALL(
*view(),
OnItemsAvailable(
ElementsAre(MatchesLabel(passwords_empty_str(kExampleDomain)),
IsDivider(), MatchesOption(manage_passwords_str()))));
controller()->RefreshSuggestionsForField(
url::Origin::Create(GURL(kExampleSite)),
/*is_fillable=*/true,
/*is_password_field=*/false);
}
// TODO(fhorschig): Check for recorded metrics here or similar to this.
TEST_F(PasswordAccessoryControllerTest, ClosesViewOnSuccessfullFillingOnly) {
// If the filling wasn't successful, no call is expected.
EXPECT_CALL(*view(), CloseAccessorySheet()).Times(0);
EXPECT_CALL(*view(), OpenKeyboard()).Times(0);
controller()->OnFilledIntoFocusedField(FillingStatus::ERROR_NOT_ALLOWED);
controller()->OnFilledIntoFocusedField(FillingStatus::ERROR_NO_VALID_FIELD);
// If the filling completed successfully, let the view know.
EXPECT_CALL(*view(), OpenKeyboard());
controller()->OnFilledIntoFocusedField(FillingStatus::SUCCESS);
}
// TODO(fhorschig): Check for recorded metrics here or similar to this.
TEST_F(PasswordAccessoryControllerTest, ClosesViewWhenRefreshingSuggestions) {
// Ignore Items - only the closing calls are interesting here.
EXPECT_CALL(*view(), OnItemsAvailable(_)).Times(AnyNumber());
EXPECT_CALL(*view(), CloseAccessorySheet());
EXPECT_CALL(*view(), OpenKeyboard()).Times(0); // Don't touch the keyboard!
controller()->RefreshSuggestionsForField(
url::Origin::Create(GURL(kExampleSite)),
/*is_fillable=*/false,
/*is_password_field=*/false);
}
TEST_F(PasswordAccessoryControllerTest, RelaysAutomaticGenerationAvailable) {
EXPECT_CALL(*view(), OnAutomaticGenerationStatusChanged(true));
controller()->OnAutomaticGenerationStatusChanged(
true, GetTestGenerationUIData1(), nullptr);
}
TEST_F(PasswordAccessoryControllerTest, RelaysAutmaticGenerationUnavailable) {
EXPECT_CALL(*view(), OnAutomaticGenerationStatusChanged(false));
controller()->OnAutomaticGenerationStatusChanged(false, base::nullopt,
nullptr);
}
// Tests that if AutomaticGenerationStatusChanged(true) is called for different
// password forms, the form and field signatures used for password generation
// are updated.
TEST_F(PasswordAccessoryControllerTest,
UpdatesSignaturesForDifferentGenerationForms) {
MockPasswordManagerDriver mock_driver;
password_manager::StubPasswordManagerClient stub_client;
MockPasswordGenerationManager mock_generation_manager(&stub_client,
&mock_driver);
// Called twice for different forms.
EXPECT_CALL(*view(), OnAutomaticGenerationStatusChanged(true)).Times(2);
controller()->OnAutomaticGenerationStatusChanged(
true, GetTestGenerationUIData1(), (&mock_driver)->AsWeakPtr());
PasswordGenerationUIData new_ui_data = GetTestGenerationUIData2();
controller()->OnAutomaticGenerationStatusChanged(true, new_ui_data,
(&mock_driver)->AsWeakPtr());
autofill::FormSignature form_signature =
autofill::CalculateFormSignature(new_ui_data.password_form.form_data);
autofill::FieldSignature field_signature =
autofill::CalculateFieldSignatureByNameAndType(
new_ui_data.generation_element, "password");
std::unique_ptr<MockPasswordGenerationDialogView> dialog_view =
std::make_unique<MockPasswordGenerationDialogView>();
MockPasswordGenerationDialogView* raw_dialog_view = dialog_view.get();
base::string16 generated_password = ASCIIToUTF16("t3stp@ssw0rd");
EXPECT_CALL(mock_dialog_factory(), Run)
.WillOnce(Return(ByMove(std::move(dialog_view))));
EXPECT_CALL(mock_driver, GetPasswordGenerationManager())
.WillOnce(Return(&mock_generation_manager));
EXPECT_CALL(mock_generation_manager,
GeneratePassword(_, form_signature, field_signature,
uint32_t(new_ui_data.max_length), _))
.WillOnce(Return(generated_password));
EXPECT_CALL(*raw_dialog_view, Show(generated_password));
controller()->OnGenerationRequested();
}
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(*view(),
OnItemsAvailable(ElementsAre(
MatchesLabel(passwords_title_str(kExampleDomain)),
MatchesItem(ASCIIToUTF16("Ben"), ASCIIToUTF16("Ben"), false,
ItemType::SUGGESTION),
MatchesItem(ASCIIToUTF16("S3cur3"), password_for_str("Ben"),
true, ItemType::NON_INTERACTIVE_SUGGESTION),
IsDivider(), MatchesOption(manage_passwords_str()))));
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(*view(),
OnItemsAvailable(ElementsAre(
MatchesLabel(passwords_title_str(kExampleDomain)),
MatchesItem(ASCIIToUTF16("Ben"), ASCIIToUTF16("Ben"), false,
ItemType::SUGGESTION),
MatchesItem(ASCIIToUTF16("S3cur3"), password_for_str("Ben"),
true, ItemType::SUGGESTION),
IsDivider(), MatchesOption(manage_passwords_str()))));
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(*view(),
OnItemsAvailable(ElementsAre(
MatchesLabel(passwords_title_str(kExampleDomain)),
MatchesItem(ASCIIToUTF16("Ben"), ASCIIToUTF16("Ben"), false,
ItemType::SUGGESTION),
MatchesItem(ASCIIToUTF16("S3cur3"), password_for_str("Ben"),
true, ItemType::NON_INTERACTIVE_SUGGESTION),
IsDivider(), MatchesOption(manage_passwords_str()))));
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(*view(),
OnItemsAvailable(ElementsAre(
MatchesLabel(passwords_title_str(kExampleDomain)),
MatchesItem(ASCIIToUTF16("Alf"), ASCIIToUTF16("Alf"), false,
ItemType::SUGGESTION),
MatchesItem(ASCIIToUTF16("M3lm4k"), password_for_str("Alf"),
true, ItemType::NON_INTERACTIVE_SUGGESTION),
IsDivider(), MatchesOption(manage_passwords_str()))));
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(*view(),
OnItemsAvailable(ElementsAre(
MatchesLabel(passwords_title_str(kExampleDomain)),
MatchesItem(ASCIIToUTF16("Ben"), ASCIIToUTF16("Ben"), false,
ItemType::SUGGESTION),
MatchesItem(ASCIIToUTF16("S3cur3"), password_for_str("Ben"),
true, ItemType::NON_INTERACTIVE_SUGGESTION),
IsDivider(), MatchesOption(manage_passwords_str()))));
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(*view(),
OnItemsAvailable(ElementsAre(
MatchesLabel(passwords_empty_str(kExampleDomain)),
IsDivider(), MatchesOption(manage_passwords_str()))));
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(*view(),
OnItemsAvailable(ElementsAre(
MatchesLabel(passwords_title_str(kExampleDomain)),
MatchesItem(ASCIIToUTF16("Ben"), ASCIIToUTF16("Ben"), false,
ItemType::SUGGESTION),
MatchesItem(ASCIIToUTF16("S3cur3"), password_for_str("Ben"),
true, ItemType::NON_INTERACTIVE_SUGGESTION),
IsDivider(), MatchesOption(manage_passwords_str()))));
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(*view(),
OnItemsAvailable(ElementsAre(
MatchesLabel(passwords_empty_str("random.other-site.org")),
IsDivider(), MatchesOption(manage_passwords_str()))));
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(*view(), OnItemsAvailable(_));
controller()->RefreshSuggestionsForField(
url::Origin::Create(GURL(kExampleSite)),
/*is_fillable=*/true,
/*is_password_field=*/false);
EXPECT_CALL(*favicon_service(),
GetFaviconImageForPageURL(GURL(kExampleSite), _, _))
.WillOnce(favicon::PostReply<3>(favicon_base::FaviconImageResult()));
EXPECT_CALL(mock_callback, Run);
controller()->GetFavicon(mock_callback.Get());
base::RunLoop().RunUntilIdle();
}
TEST_F(PasswordAccessoryControllerTest, RequestsFaviconsOnceForOneOrigin) {
base::MockCallback<base::OnceCallback<void(const gfx::Image&)>> mock_callback;
EXPECT_CALL(*view(), OnItemsAvailable(_));
controller()->RefreshSuggestionsForField(
url::Origin::Create(GURL(kExampleSite)),
/*is_fillable=*/true,
/*is_password_field=*/false);
EXPECT_CALL(*favicon_service(),
GetFaviconImageForPageURL(GURL(kExampleSite), _, _))
.WillOnce(favicon::PostReply<3>(favicon_base::FaviconImageResult()));
EXPECT_CALL(mock_callback, Run).Times(2);
controller()->GetFavicon(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(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::FaviconImageResult non_empty_result;
SkBitmap bitmap;
bitmap.allocN32Pixels(32, 32);
non_empty_result.image = gfx::Image::CreateFrom1xBitmap(bitmap);
// Populate the cache by requesting a favicon.
EXPECT_CALL(*view(), OnItemsAvailable(_));
controller()->RefreshSuggestionsForField(
url::Origin::Create(GURL(kExampleSite)),
/*is_fillable=*/true,
/*is_password_field=*/false);
EXPECT_CALL(*favicon_service(),
GetFaviconImageForPageURL(GURL(kExampleSite), _, _))
.WillOnce(favicon::PostReply<3>(non_empty_result));
EXPECT_CALL(mock_callback, Run).Times(1);
controller()->GetFavicon(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(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(*view(), OnItemsAvailable(_));
controller()->RefreshSuggestionsForField(
url::Origin::Create(GURL(kExampleSite)), true, false);
// The cache was cleared, so now the service has to be queried again.
EXPECT_CALL(*favicon_service(),
GetFaviconImageForPageURL(GURL(kExampleSite), _, _))
.WillOnce(favicon::PostReply<3>(non_empty_result));
EXPECT_CALL(mock_callback, Run).Times(1);
controller()->GetFavicon(mock_callback.Get());
base::RunLoop().RunUntilIdle();
}
TEST_F(PasswordAccessoryControllerTest, NoFaviconCallbacksWhenOriginChanges) {
base::MockCallback<base::OnceCallback<void(const gfx::Image&)>> mock_callback;
EXPECT_CALL(*view(), OnItemsAvailable(_)).Times(2);
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(),
GetFaviconImageForPageURL(GURL(kExampleSite), _, _))
.WillOnce(favicon::PostReply<3>(favicon_base::FaviconImageResult()));
EXPECT_CALL(mock_callback, Run).Times(0);
controller()->GetFavicon(mock_callback.Get());
controller()->RefreshSuggestionsForField(
url::Origin::Create(GURL("https://other.frame.com/")), true, false);
base::RunLoop().RunUntilIdle();
}