Commit a3b1d7d2 authored by Mohamed Amir Yosef's avatar Mohamed Amir Yosef Committed by Commit Bot

Reland "[Passwords] Introduce SaveUpdateWithAccountStore bubble"

This is a reland of d7682c6e
after fixing the failing test.

Original change's description:
> [Passwords] Introduce SaveUpdateWithAccountStore bubble
>
> This is a mechnical fork of the existing SaveUpdate bubble without
> any change in functionality.
> Follow up CLs will introduce extra funcationlaity and change in the UI
> elements.
>
> Bug: 1044038
> Change-Id: I1ab6b08a9097b4f7adc06b53317676b42b3a2bf4
> Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2033072
> Reviewed-by: Vasilii Sukhanov <vasilii@chromium.org>
> Commit-Queue: Mohamed Amir Yosef <mamir@chromium.org>
> Cr-Commit-Position: refs/heads/master@{#738913}

TBR=vasilii@chromium.org

Bug: 1044038
Change-Id: I94abd07edf903d6c1982dffc3caa64ea8d332333
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2041661Reviewed-by: default avatarMohamed Amir Yosef <mamir@chromium.org>
Commit-Queue: Mohamed Amir Yosef <mamir@chromium.org>
Cr-Commit-Position: refs/heads/master@{#738990}
parent 4d7982ba
......@@ -1076,6 +1076,8 @@ jumbo_static_library("ui") {
"passwords/bubble_controllers/password_bubble_controller_base.h",
"passwords/bubble_controllers/save_update_bubble_controller.cc",
"passwords/bubble_controllers/save_update_bubble_controller.h",
"passwords/bubble_controllers/save_update_with_account_store_bubble_controller.cc",
"passwords/bubble_controllers/save_update_with_account_store_bubble_controller.h",
"passwords/bubble_controllers/sign_in_promo_bubble_controller.cc",
"passwords/bubble_controllers/sign_in_promo_bubble_controller.h",
"passwords/credential_leak_dialog_controller.h",
......@@ -3143,6 +3145,8 @@ jumbo_static_library("ui") {
"views/passwords/password_items_view.h",
"views/passwords/password_save_update_view.cc",
"views/passwords/password_save_update_view.h",
"views/passwords/password_save_update_with_account_store_view.cc",
"views/passwords/password_save_update_with_account_store_view.h",
"views/payments/contact_info_editor_view_controller.cc",
"views/payments/contact_info_editor_view_controller.h",
"views/payments/credit_card_editor_view_controller.cc",
......
// Copyright 2020 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/ui/passwords/bubble_controllers/save_update_with_account_store_bubble_controller.h"
#include "base/metrics/field_trial_params.h"
#include "base/time/default_clock.h"
#include "chrome/browser/password_manager/password_store_factory.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/sync/profile_sync_service_factory.h"
#include "chrome/browser/ui/passwords/manage_passwords_view_utils.h"
#include "chrome/browser/ui/passwords/passwords_model_delegate.h"
#include "chrome/grit/generated_resources.h"
#include "chrome/grit/theme_resources.h"
#include "components/password_manager/core/browser/password_bubble_experiment.h"
#include "components/password_manager/core/browser/password_form_metrics_recorder.h"
#include "components/password_manager/core/browser/password_manager_metrics_util.h"
#include "components/password_manager/core/browser/password_store.h"
#include "components/password_manager/core/common/password_manager_features.h"
#include "components/password_manager/core/common/password_manager_pref_names.h"
#include "components/prefs/pref_service.h"
#include "components/sync/driver/sync_service.h"
#include "content/public/browser/web_contents.h"
#include "ui/base/l10n/l10n_util.h"
#if defined(PASSWORD_STORE_SELECT_ENABLED)
#include "components/password_manager/core/browser/password_feature_manager.h"
#endif // defined(PASSWORD_STORE_SELECT_ENABLED)
namespace {
namespace metrics_util = password_manager::metrics_util;
using Store = autofill::PasswordForm::Store;
password_manager::metrics_util::UIDisplayDisposition ComputeDisplayDisposition(
PasswordBubbleControllerBase::DisplayReason display_reason,
password_manager::ui::State state) {
if (display_reason ==
PasswordBubbleControllerBase::DisplayReason::kUserAction) {
switch (state) {
case password_manager::ui::PENDING_PASSWORD_STATE:
return metrics_util::MANUAL_WITH_PASSWORD_PENDING;
case password_manager::ui::PENDING_PASSWORD_UPDATE_STATE:
return metrics_util::MANUAL_WITH_PASSWORD_PENDING_UPDATE;
default:
NOTREACHED();
return metrics_util::MANUAL_WITH_PASSWORD_PENDING_UPDATE;
}
} else {
switch (state) {
case password_manager::ui::PENDING_PASSWORD_STATE:
return metrics_util::AUTOMATIC_WITH_PASSWORD_PENDING;
case password_manager::ui::PENDING_PASSWORD_UPDATE_STATE:
return metrics_util::AUTOMATIC_WITH_PASSWORD_PENDING_UPDATE;
default:
NOTREACHED();
return metrics_util::AUTOMATIC_WITH_PASSWORD_PENDING;
}
}
}
void CleanStatisticsForSite(Profile* profile, const GURL& origin) {
DCHECK(profile);
password_manager::PasswordStore* password_store =
PasswordStoreFactory::GetForProfile(profile,
ServiceAccessType::IMPLICIT_ACCESS)
.get();
password_store->RemoveSiteStats(origin.GetOrigin());
}
std::vector<autofill::PasswordForm> DeepCopyForms(
const std::vector<std::unique_ptr<autofill::PasswordForm>>& forms) {
std::vector<autofill::PasswordForm> result;
result.reserve(forms.size());
std::transform(forms.begin(), forms.end(), std::back_inserter(result),
[](const std::unique_ptr<autofill::PasswordForm>& form) {
return *form;
});
return result;
}
bool IsSyncUser(Profile* profile) {
const syncer::SyncService* sync_service =
ProfileSyncServiceFactory::GetForProfile(profile);
return password_bubble_experiment::IsSmartLockUser(sync_service);
}
} // namespace
SaveUpdateWithAccountStoreBubbleController::
SaveUpdateWithAccountStoreBubbleController(
base::WeakPtr<PasswordsModelDelegate> delegate,
PasswordBubbleControllerBase::DisplayReason display_reason)
: PasswordBubbleControllerBase(
delegate,
ComputeDisplayDisposition(display_reason, delegate->GetState())),
display_disposition_(
ComputeDisplayDisposition(display_reason, delegate->GetState())),
password_revealing_requires_reauth_(false),
are_passwords_revealed_when_bubble_is_opened_(false),
enable_editing_(false),
dismissal_reason_(metrics_util::NO_DIRECT_INTERACTION),
clock_(base::DefaultClock::GetInstance()) {
state_ = delegate_->GetState();
DCHECK(state_ == password_manager::ui::PENDING_PASSWORD_STATE ||
state_ == password_manager::ui::PENDING_PASSWORD_UPDATE_STATE);
origin_ = delegate_->GetOrigin();
pending_password_ = delegate_->GetPendingPassword();
existing_credentials_ = DeepCopyForms(delegate_->GetCurrentForms());
if (state_ == password_manager::ui::PENDING_PASSWORD_STATE) {
interaction_stats_.origin_domain = origin_.GetOrigin();
interaction_stats_.username_value = pending_password_.username_value;
const password_manager::InteractionsStats* stats =
delegate_->GetCurrentInteractionStats();
if (stats) {
DCHECK_EQ(interaction_stats_.username_value, stats->username_value);
DCHECK_EQ(interaction_stats_.origin_domain, stats->origin_domain);
interaction_stats_.dismissal_count = stats->dismissal_count;
}
}
if (delegate_->ArePasswordsRevealedWhenBubbleIsOpened()) {
are_passwords_revealed_when_bubble_is_opened_ = true;
delegate_->OnPasswordsRevealed();
}
// The condition for the password reauth:
// If the bubble opened after reauth -> no more reauth necessary, otherwise
// If a password was autofilled -> require reauth to view it, otherwise
// Require reauth iff the user opened the bubble manually and it's not the
// manual saving state. The manual saving state as well as automatic prompt
// are temporary states, therefore, it's better for the sake of convenience
// for the user not to break the UX with the reauth prompt.
password_revealing_requires_reauth_ =
!are_passwords_revealed_when_bubble_is_opened_ &&
(pending_password_.form_has_autofilled_value ||
(!delegate_->BubbleIsManualFallbackForSaving() &&
display_reason ==
PasswordBubbleControllerBase::DisplayReason::kUserAction));
enable_editing_ = delegate_->GetCredentialSource() !=
password_manager::metrics_util::CredentialSourceType::
kCredentialManagementAPI;
// Compute the title.
PasswordTitleType type =
state_ == password_manager::ui::PENDING_PASSWORD_UPDATE_STATE
? PasswordTitleType::UPDATE_PASSWORD
: (pending_password_.federation_origin.opaque()
? PasswordTitleType::SAVE_PASSWORD
: PasswordTitleType::SAVE_ACCOUNT);
GetSavePasswordDialogTitleTextAndLinkRange(GetWebContents()->GetVisibleURL(),
origin_, type, &title_);
}
SaveUpdateWithAccountStoreBubbleController::
~SaveUpdateWithAccountStoreBubbleController() {
if (!interaction_reported_)
OnBubbleClosing();
}
void SaveUpdateWithAccountStoreBubbleController::OnSaveClicked() {
DCHECK(state_ == password_manager::ui::PENDING_PASSWORD_STATE ||
state_ == password_manager::ui::PENDING_PASSWORD_UPDATE_STATE);
dismissal_reason_ = metrics_util::CLICKED_SAVE;
if (delegate_) {
CleanStatisticsForSite(GetProfile(), origin_);
delegate_->SavePassword(pending_password_.username_value,
pending_password_.password_value);
}
}
void SaveUpdateWithAccountStoreBubbleController::OnNopeUpdateClicked() {
DCHECK_EQ(password_manager::ui::PENDING_PASSWORD_UPDATE_STATE, state_);
dismissal_reason_ = metrics_util::CLICKED_CANCEL;
if (delegate_)
delegate_->OnNopeUpdateClicked();
}
void SaveUpdateWithAccountStoreBubbleController::OnNeverForThisSiteClicked() {
DCHECK_EQ(password_manager::ui::PENDING_PASSWORD_STATE, state_);
dismissal_reason_ = metrics_util::CLICKED_NEVER;
if (delegate_) {
CleanStatisticsForSite(GetProfile(), origin_);
delegate_->NeverSavePassword();
}
}
void SaveUpdateWithAccountStoreBubbleController::OnCredentialEdited(
base::string16 new_username,
base::string16 new_password) {
DCHECK(state_ == password_manager::ui::PENDING_PASSWORD_STATE ||
state_ == password_manager::ui::PENDING_PASSWORD_UPDATE_STATE);
pending_password_.username_value = std::move(new_username);
pending_password_.password_value = std::move(new_password);
}
bool SaveUpdateWithAccountStoreBubbleController::IsCurrentStateUpdate() const {
DCHECK(state_ == password_manager::ui::PENDING_PASSWORD_UPDATE_STATE ||
state_ == password_manager::ui::PENDING_PASSWORD_STATE);
return std::any_of(existing_credentials_.begin(), existing_credentials_.end(),
[this](const autofill::PasswordForm& form) {
return form.username_value ==
pending_password_.username_value;
});
}
bool SaveUpdateWithAccountStoreBubbleController::ShouldShowFooter() const {
return (state_ == password_manager::ui::PENDING_PASSWORD_UPDATE_STATE ||
state_ == password_manager::ui::PENDING_PASSWORD_STATE) &&
IsSyncUser(GetProfile());
}
int SaveUpdateWithAccountStoreBubbleController::GetTopIllustration(
bool dark_mode) const {
if (state_ == password_manager::ui::PENDING_PASSWORD_UPDATE_STATE ||
state_ == password_manager::ui::PENDING_PASSWORD_STATE) {
int image = base::GetFieldTrialParamByFeatureAsInt(
password_manager::features::kPasswordSaveIllustration, "image", 0);
switch (image) {
case 1:
return dark_mode ? IDR_SAVE_PASSWORD1_DARK : IDR_SAVE_PASSWORD1;
case 2:
return dark_mode ? IDR_SAVE_PASSWORD2_DARK : IDR_SAVE_PASSWORD2;
case 3:
return dark_mode ? IDR_SAVE_PASSWORD3_DARK : IDR_SAVE_PASSWORD3;
default:
return 0;
}
}
return 0;
}
bool SaveUpdateWithAccountStoreBubbleController::RevealPasswords() {
bool reveal_immediately = !password_revealing_requires_reauth_ ||
(delegate_ && delegate_->AuthenticateUser());
if (reveal_immediately)
delegate_->OnPasswordsRevealed();
return reveal_immediately;
}
#if defined(PASSWORD_STORE_SELECT_ENABLED)
void SaveUpdateWithAccountStoreBubbleController::OnToggleAccountStore(
bool is_checked) {
delegate_->GetPasswordFeatureManager()->SetDefaultPasswordStore(
is_checked ? Store::kAccountStore : Store::kProfileStore);
}
bool SaveUpdateWithAccountStoreBubbleController::IsUsingAccountStore() {
return delegate_->GetPasswordFeatureManager()->GetDefaultPasswordStore() ==
Store::kAccountStore;
}
#endif // defined(PASSWORD_STORE_SELECT_ENABLED)
void SaveUpdateWithAccountStoreBubbleController::ReportInteractions() {
DCHECK(state_ == password_manager::ui::PENDING_PASSWORD_UPDATE_STATE ||
state_ == password_manager::ui::PENDING_PASSWORD_STATE);
if (state_ == password_manager::ui::PENDING_PASSWORD_STATE) {
// Update the statistics for the save password bubble.
Profile* profile = GetProfile();
if (profile) {
if (dismissal_reason_ == metrics_util::NO_DIRECT_INTERACTION &&
display_disposition_ ==
metrics_util::AUTOMATIC_WITH_PASSWORD_PENDING) {
if (interaction_stats_.dismissal_count <
std::numeric_limits<decltype(
interaction_stats_.dismissal_count)>::max())
interaction_stats_.dismissal_count++;
interaction_stats_.update_time = clock_->Now();
password_manager::PasswordStore* password_store =
PasswordStoreFactory::GetForProfile(
profile, ServiceAccessType::IMPLICIT_ACCESS)
.get();
password_store->AddSiteStats(interaction_stats_);
}
}
}
// Log UMA histograms.
if (state_ == password_manager::ui::PENDING_PASSWORD_UPDATE_STATE) {
metrics_util::LogUpdateUIDismissalReason(dismissal_reason_);
} else if (state_ == password_manager::ui::PENDING_PASSWORD_STATE) {
metrics_util::LogSaveUIDismissalReason(dismissal_reason_);
}
// Update the delegate so that it can send votes to the server.
// Send a notification if there was no interaction with the bubble.
bool no_interaction =
dismissal_reason_ == metrics_util::NO_DIRECT_INTERACTION;
if (no_interaction && delegate_) {
delegate_->OnNoInteraction();
}
// Record UKM statistics on dismissal reason.
if (metrics_recorder_)
metrics_recorder_->RecordUIDismissalReason(dismissal_reason_);
}
base::string16 SaveUpdateWithAccountStoreBubbleController::GetTitle() const {
return title_;
}
// Copyright 2020 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.
#ifndef CHROME_BROWSER_UI_PASSWORDS_BUBBLE_CONTROLLERS_SAVE_UPDATE_WITH_ACCOUNT_STORE_BUBBLE_CONTROLLER_H_
#define CHROME_BROWSER_UI_PASSWORDS_BUBBLE_CONTROLLERS_SAVE_UPDATE_WITH_ACCOUNT_STORE_BUBBLE_CONTROLLER_H_
#include "base/memory/weak_ptr.h"
#include "chrome/browser/ui/passwords/bubble_controllers/password_bubble_controller_base.h"
#include "components/password_manager/core/browser/manage_passwords_referrer.h"
#include "components/password_manager/core/browser/statistics_table.h"
#include "components/password_manager/core/common/password_manager_ui.h"
class PasswordsModelDelegate;
namespace base {
class Clock;
}
// This controller provides data and actions for the
// PasswordSaveUpdateWithAccountStoreView.
class SaveUpdateWithAccountStoreBubbleController
: public PasswordBubbleControllerBase {
public:
explicit SaveUpdateWithAccountStoreBubbleController(
base::WeakPtr<PasswordsModelDelegate> delegate,
DisplayReason display_reason);
~SaveUpdateWithAccountStoreBubbleController() override;
// Called by the view code when the save/update button is clicked by the user.
void OnSaveClicked();
// Called by the view code when the "Nope" button in clicked by the user in
// update bubble.
void OnNopeUpdateClicked();
// Called by the view code when the "Never for this site." button in clicked
// by the user.
void OnNeverForThisSiteClicked();
// Called by the view code when username or password is corrected using
// the username correction or password selection features in PendingView.
void OnCredentialEdited(base::string16 new_username,
base::string16 new_password);
// The password bubble can switch its state between "save" and "update"
// depending on the user input. |state_| only captures the correct state on
// creation. This method returns true iff the current state is "update".
bool IsCurrentStateUpdate() const;
// Returns true iff the bubble is supposed to show the footer about syncing
// to Google account.
bool ShouldShowFooter() const;
// Returns the ID of the picture to show above the title.
int GetTopIllustration(bool dark_mode) const;
// Returns true if passwords revealing is not locked or re-authentication is
// not available on the given platform. Otherwise, the method schedules
// re-authentication and bubble reopen (the current bubble will be destroyed),
// and returns false immediately. New bubble will reveal the passwords if the
// re-authentication is successful.
bool RevealPasswords();
#if defined(PASSWORD_STORE_SELECT_ENABLED)
// Called by the view when the account store checkbox is toggled.
void OnToggleAccountStore(bool is_checked);
// Returns true iff the password account store is used.
bool IsUsingAccountStore();
#endif // defined(PASSWORD_STORE_SELECT_ENABLED)
password_manager::ui::State state() const { return state_; }
const autofill::PasswordForm& pending_password() const {
return pending_password_;
}
bool are_passwords_revealed_when_bubble_is_opened() const {
return are_passwords_revealed_when_bubble_is_opened_;
}
bool enable_editing() const { return enable_editing_; }
#if defined(UNIT_TEST)
void set_clock(base::Clock* clock) { clock_ = clock; }
void allow_passwords_revealing() {
password_revealing_requires_reauth_ = false;
}
bool password_revealing_requires_reauth() const {
return password_revealing_requires_reauth_;
}
#endif
private:
// PasswordBubbleControllerBase methods:
base::string16 GetTitle() const override;
void ReportInteractions() override;
// URL of the page from where this bubble was triggered.
GURL origin_;
password_manager::ui::State state_;
base::string16 title_;
autofill::PasswordForm pending_password_;
std::vector<autofill::PasswordForm> existing_credentials_;
password_manager::InteractionsStats interaction_stats_;
password_manager::metrics_util::UIDisplayDisposition display_disposition_;
// True iff password revealing should require re-auth for privacy reasons.
bool password_revealing_requires_reauth_;
// True iff bubble should pop up with revealed password value.
bool are_passwords_revealed_when_bubble_is_opened_;
// True iff username/password editing should be enabled.
bool enable_editing_;
// Dismissal reason for a password bubble.
password_manager::metrics_util::UIDismissalReason dismissal_reason_;
// Used to retrieve the current time, in base::Time units.
base::Clock* clock_;
};
#endif // CHROME_BROWSER_UI_PASSWORDS_BUBBLE_CONTROLLERS_SAVE_UPDATE_WITH_ACCOUNT_STORE_BUBBLE_CONTROLLER_H_
// Copyright 2020 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/ui/passwords/bubble_controllers/save_update_with_account_store_bubble_controller.h"
#include <memory>
#include <string>
#include <utility>
#include <vector>
#include "base/bind.h"
#include "base/metrics/histogram_samples.h"
#include "base/run_loop.h"
#include "base/strings/utf_string_conversions.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/simple_test_clock.h"
#include "chrome/browser/password_manager/password_store_factory.h"
#include "chrome/browser/ui/passwords/passwords_model_delegate_mock.h"
#include "chrome/test/base/testing_profile.h"
#include "components/password_manager/core/browser/mock_password_store.h"
#include "components/password_manager/core/browser/password_form_metrics_recorder.h"
#include "components/password_manager/core/browser/password_manager_metrics_util.h"
#include "components/password_manager/core/browser/password_manager_test_utils.h"
#include "components/password_manager/core/browser/statistics_table.h"
#include "components/password_manager/core/common/credential_manager_types.h"
#include "components/password_manager/core/common/password_manager_ui.h"
#include "components/ukm/test_ukm_recorder.h"
#include "content/public/browser/web_contents.h"
#include "content/public/test/browser_task_environment.h"
#include "content/public/test/test_renderer_host.h"
#include "content/public/test/web_contents_tester.h"
#include "services/metrics/public/cpp/ukm_recorder.h"
#include "services/metrics/public/cpp/ukm_source.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
using ::testing::_;
using ::testing::Return;
using ::testing::ReturnRef;
namespace {
constexpr ukm::SourceId kTestSourceId = 0x1234;
constexpr char kSiteOrigin[] = "http://example.com/login";
constexpr char kUsername[] = "Admin";
constexpr char kUsernameExisting[] = "User";
constexpr char kUsernameNew[] = "User585";
constexpr char kPassword[] = "AdminPass";
constexpr char kPasswordEdited[] = "asDfjkl;";
constexpr char kUIDismissalReasonGeneralMetric[] =
"PasswordManager.UIDismissalReason";
constexpr char kUIDismissalReasonSaveMetric[] =
"PasswordManager.SaveUIDismissalReason";
constexpr char kUIDismissalReasonUpdateMetric[] =
"PasswordManager.UpdateUIDismissalReason";
} // namespace
class SaveUpdateWithAccountStoreBubbleControllerTest : public ::testing::Test {
public:
SaveUpdateWithAccountStoreBubbleControllerTest() = default;
~SaveUpdateWithAccountStoreBubbleControllerTest() override = default;
void SetUp() override {
test_web_contents_ =
content::WebContentsTester::CreateTestWebContents(&profile_, nullptr);
mock_delegate_ =
std::make_unique<testing::NiceMock<PasswordsModelDelegateMock>>();
ON_CALL(*mock_delegate_, GetPasswordFormMetricsRecorder())
.WillByDefault(Return(nullptr));
PasswordStoreFactory::GetInstance()->SetTestingFactoryAndUse(
profile(),
base::BindRepeating(
&password_manager::BuildPasswordStore<
content::BrowserContext,
testing::StrictMock<password_manager::MockPasswordStore>>));
pending_password_.origin = GURL(kSiteOrigin);
pending_password_.signon_realm = kSiteOrigin;
pending_password_.username_value = base::ASCIIToUTF16(kUsername);
pending_password_.password_value = base::ASCIIToUTF16(kPassword);
}
void TearDown() override {
// Reset the delegate first. It can happen if the user closes the tab.
mock_delegate_.reset();
controller_.reset();
}
TestingProfile* profile() { return &profile_; }
password_manager::MockPasswordStore* GetStore() {
return static_cast<password_manager::MockPasswordStore*>(
PasswordStoreFactory::GetInstance()
->GetForProfile(profile(), ServiceAccessType::EXPLICIT_ACCESS)
.get());
}
PasswordsModelDelegateMock* delegate() { return mock_delegate_.get(); }
SaveUpdateWithAccountStoreBubbleController* controller() {
return controller_.get();
}
autofill::PasswordForm& pending_password() { return pending_password_; }
const autofill::PasswordForm& pending_password() const {
return pending_password_;
}
void SetUpWithState(password_manager::ui::State state,
PasswordBubbleControllerBase::DisplayReason reason);
void PretendPasswordWaiting(
PasswordBubbleControllerBase::DisplayReason reason =
PasswordBubbleControllerBase::DisplayReason::kAutomatic);
void PretendUpdatePasswordWaiting();
void DestroyModelAndVerifyControllerExpectations();
void DestroyModelExpectReason(
password_manager::metrics_util::UIDismissalReason dismissal_reason);
static password_manager::InteractionsStats GetTestStats();
std::vector<std::unique_ptr<autofill::PasswordForm>> GetCurrentForms() const;
private:
content::BrowserTaskEnvironment task_environment_;
content::RenderViewHostTestEnabler rvh_enabler_;
TestingProfile profile_;
std::unique_ptr<content::WebContents> test_web_contents_;
std::unique_ptr<SaveUpdateWithAccountStoreBubbleController> controller_;
std::unique_ptr<PasswordsModelDelegateMock> mock_delegate_;
autofill::PasswordForm pending_password_;
};
void SaveUpdateWithAccountStoreBubbleControllerTest::SetUpWithState(
password_manager::ui::State state,
PasswordBubbleControllerBase::DisplayReason reason) {
GURL origin(kSiteOrigin);
EXPECT_CALL(*delegate(), GetOrigin()).WillOnce(ReturnRef(origin));
EXPECT_CALL(*delegate(), GetState()).WillRepeatedly(Return(state));
EXPECT_CALL(*delegate(), OnBubbleShown());
EXPECT_CALL(*delegate(), GetWebContents())
.WillRepeatedly(Return(test_web_contents_.get()));
controller_.reset(new SaveUpdateWithAccountStoreBubbleController(
mock_delegate_->AsWeakPtr(), reason));
ASSERT_TRUE(testing::Mock::VerifyAndClearExpectations(delegate()));
EXPECT_CALL(*delegate(), GetWebContents())
.WillRepeatedly(Return(test_web_contents_.get()));
}
void SaveUpdateWithAccountStoreBubbleControllerTest::PretendPasswordWaiting(
PasswordBubbleControllerBase::DisplayReason reason) {
EXPECT_CALL(*delegate(), GetPendingPassword())
.WillOnce(ReturnRef(pending_password()));
password_manager::InteractionsStats stats = GetTestStats();
EXPECT_CALL(*delegate(), GetCurrentInteractionStats())
.WillOnce(Return(&stats));
std::vector<std::unique_ptr<autofill::PasswordForm>> forms =
GetCurrentForms();
EXPECT_CALL(*delegate(), GetCurrentForms()).WillOnce(ReturnRef(forms));
SetUpWithState(password_manager::ui::PENDING_PASSWORD_STATE, reason);
}
void SaveUpdateWithAccountStoreBubbleControllerTest::
PretendUpdatePasswordWaiting() {
EXPECT_CALL(*delegate(), GetPendingPassword())
.WillOnce(ReturnRef(pending_password()));
std::vector<std::unique_ptr<autofill::PasswordForm>> forms =
GetCurrentForms();
auto current_form =
std::make_unique<autofill::PasswordForm>(pending_password());
current_form->password_value = base::ASCIIToUTF16("old_password");
forms.push_back(std::move(current_form));
EXPECT_CALL(*delegate(), GetCurrentForms()).WillOnce(ReturnRef(forms));
SetUpWithState(password_manager::ui::PENDING_PASSWORD_UPDATE_STATE,
PasswordBubbleControllerBase::DisplayReason::kAutomatic);
}
void SaveUpdateWithAccountStoreBubbleControllerTest::
DestroyModelAndVerifyControllerExpectations() {
EXPECT_CALL(*delegate(), OnBubbleHidden());
controller_->OnBubbleClosing();
ASSERT_TRUE(testing::Mock::VerifyAndClearExpectations(delegate()));
controller_.reset();
}
void SaveUpdateWithAccountStoreBubbleControllerTest::DestroyModelExpectReason(
password_manager::metrics_util::UIDismissalReason dismissal_reason) {
base::HistogramTester histogram_tester;
password_manager::ui::State state = controller_->state();
std::string histogram(kUIDismissalReasonGeneralMetric);
if (state == password_manager::ui::PENDING_PASSWORD_STATE)
histogram = kUIDismissalReasonSaveMetric;
else if (state == password_manager::ui::PENDING_PASSWORD_UPDATE_STATE)
histogram = kUIDismissalReasonUpdateMetric;
DestroyModelAndVerifyControllerExpectations();
histogram_tester.ExpectUniqueSample(histogram, dismissal_reason, 1);
}
// static
password_manager::InteractionsStats
SaveUpdateWithAccountStoreBubbleControllerTest::GetTestStats() {
password_manager::InteractionsStats result;
result.origin_domain = GURL(kSiteOrigin).GetOrigin();
result.username_value = base::ASCIIToUTF16(kUsername);
result.dismissal_count = 5;
result.update_time = base::Time::FromTimeT(1);
return result;
}
std::vector<std::unique_ptr<autofill::PasswordForm>>
SaveUpdateWithAccountStoreBubbleControllerTest::GetCurrentForms() const {
autofill::PasswordForm form(pending_password());
form.username_value = base::ASCIIToUTF16(kUsernameExisting);
form.password_value = base::ASCIIToUTF16("123456");
autofill::PasswordForm preferred_form(pending_password());
preferred_form.username_value = base::ASCIIToUTF16("preferred_username");
preferred_form.password_value = base::ASCIIToUTF16("654321");
std::vector<std::unique_ptr<autofill::PasswordForm>> forms;
forms.push_back(std::make_unique<autofill::PasswordForm>(form));
forms.push_back(std::make_unique<autofill::PasswordForm>(preferred_form));
return forms;
}
TEST_F(SaveUpdateWithAccountStoreBubbleControllerTest,
CloseWithoutInteraction) {
PretendPasswordWaiting();
EXPECT_EQ(password_manager::ui::PENDING_PASSWORD_STATE,
controller()->state());
base::SimpleTestClock clock;
base::Time now = base::Time::Now();
clock.SetNow(now);
controller()->set_clock(&clock);
password_manager::InteractionsStats stats = GetTestStats();
stats.dismissal_count++;
stats.update_time = now;
EXPECT_CALL(*GetStore(), AddSiteStatsImpl(stats));
EXPECT_CALL(*delegate(), OnNoInteraction());
EXPECT_CALL(*delegate(), SavePassword(_, _)).Times(0);
EXPECT_CALL(*delegate(), NeverSavePassword()).Times(0);
DestroyModelExpectReason(
password_manager::metrics_util::NO_DIRECT_INTERACTION);
}
TEST_F(SaveUpdateWithAccountStoreBubbleControllerTest, ClickSave) {
PretendPasswordWaiting();
EXPECT_TRUE(controller()->enable_editing());
EXPECT_FALSE(controller()->IsCurrentStateUpdate());
EXPECT_CALL(*GetStore(), RemoveSiteStatsImpl(GURL(kSiteOrigin).GetOrigin()));
EXPECT_CALL(*delegate(), OnPasswordsRevealed()).Times(0);
EXPECT_CALL(*delegate(), SavePassword(pending_password().username_value,
pending_password().password_value));
EXPECT_CALL(*delegate(), NeverSavePassword()).Times(0);
EXPECT_CALL(*delegate(), OnNopeUpdateClicked()).Times(0);
controller()->OnSaveClicked();
DestroyModelExpectReason(password_manager::metrics_util::CLICKED_SAVE);
}
TEST_F(SaveUpdateWithAccountStoreBubbleControllerTest, ClickSaveInUpdateState) {
PretendUpdatePasswordWaiting();
// Edit username, now it's a new credential.
controller()->OnCredentialEdited(base::ASCIIToUTF16(kUsernameNew),
base::ASCIIToUTF16(kPasswordEdited));
EXPECT_FALSE(controller()->IsCurrentStateUpdate());
EXPECT_CALL(*GetStore(), RemoveSiteStatsImpl(GURL(kSiteOrigin).GetOrigin()));
EXPECT_CALL(*delegate(), SavePassword(base::ASCIIToUTF16(kUsernameNew),
base::ASCIIToUTF16(kPasswordEdited)));
EXPECT_CALL(*delegate(), NeverSavePassword()).Times(0);
EXPECT_CALL(*delegate(), OnNopeUpdateClicked()).Times(0);
controller()->OnSaveClicked();
DestroyModelExpectReason(password_manager::metrics_util::CLICKED_SAVE);
}
TEST_F(SaveUpdateWithAccountStoreBubbleControllerTest, ClickNever) {
PretendPasswordWaiting();
EXPECT_CALL(*GetStore(), RemoveSiteStatsImpl(GURL(kSiteOrigin).GetOrigin()));
EXPECT_CALL(*delegate(), SavePassword(_, _)).Times(0);
EXPECT_CALL(*delegate(), NeverSavePassword());
controller()->OnNeverForThisSiteClicked();
EXPECT_EQ(password_manager::ui::PENDING_PASSWORD_STATE,
controller()->state());
DestroyModelExpectReason(password_manager::metrics_util::CLICKED_NEVER);
}
TEST_F(SaveUpdateWithAccountStoreBubbleControllerTest, ClickUpdate) {
PretendUpdatePasswordWaiting();
EXPECT_TRUE(controller()->enable_editing());
EXPECT_TRUE(controller()->IsCurrentStateUpdate());
EXPECT_CALL(*GetStore(), RemoveSiteStatsImpl(GURL(kSiteOrigin).GetOrigin()));
EXPECT_CALL(*delegate(), OnPasswordsRevealed()).Times(0);
EXPECT_CALL(*delegate(), SavePassword(pending_password().username_value,
pending_password().password_value));
EXPECT_CALL(*delegate(), NeverSavePassword()).Times(0);
EXPECT_CALL(*delegate(), OnNopeUpdateClicked()).Times(0);
controller()->OnSaveClicked();
DestroyModelExpectReason(password_manager::metrics_util::CLICKED_SAVE);
}
TEST_F(SaveUpdateWithAccountStoreBubbleControllerTest, ClickUpdateInSaveState) {
PretendPasswordWaiting();
// Edit username, now it's an existing credential.
controller()->OnCredentialEdited(base::ASCIIToUTF16(kUsernameExisting),
base::ASCIIToUTF16(kPasswordEdited));
EXPECT_TRUE(controller()->IsCurrentStateUpdate());
EXPECT_CALL(*GetStore(), RemoveSiteStatsImpl(GURL(kSiteOrigin).GetOrigin()));
EXPECT_CALL(*delegate(), SavePassword(base::ASCIIToUTF16(kUsernameExisting),
base::ASCIIToUTF16(kPasswordEdited)));
EXPECT_CALL(*delegate(), NeverSavePassword()).Times(0);
EXPECT_CALL(*delegate(), OnNopeUpdateClicked()).Times(0);
controller()->OnSaveClicked();
DestroyModelExpectReason(password_manager::metrics_util::CLICKED_SAVE);
}
TEST_F(SaveUpdateWithAccountStoreBubbleControllerTest,
GetInitialUsername_MatchedUsername) {
PretendUpdatePasswordWaiting();
EXPECT_EQ(base::UTF8ToUTF16(kUsername),
controller()->pending_password().username_value);
}
TEST_F(SaveUpdateWithAccountStoreBubbleControllerTest, EditCredential) {
PretendPasswordWaiting();
EXPECT_CALL(*GetStore(), RemoveSiteStatsImpl(GURL(kSiteOrigin).GetOrigin()));
const base::string16 kExpectedUsername = base::UTF8ToUTF16("new_username");
const base::string16 kExpectedPassword = base::UTF8ToUTF16("new_password");
controller()->OnCredentialEdited(kExpectedUsername, kExpectedPassword);
EXPECT_EQ(kExpectedUsername, controller()->pending_password().username_value);
EXPECT_EQ(kExpectedPassword, controller()->pending_password().password_value);
EXPECT_CALL(*delegate(), SavePassword(kExpectedUsername, kExpectedPassword));
EXPECT_CALL(*delegate(), NeverSavePassword()).Times(0);
controller()->OnSaveClicked();
DestroyModelAndVerifyControllerExpectations();
}
// Verify that URL keyed metrics are properly recorded.
TEST_F(SaveUpdateWithAccountStoreBubbleControllerTest, RecordUKMs) {
using BubbleDismissalReason =
password_manager::PasswordFormMetricsRecorder::BubbleDismissalReason;
using BubbleTrigger =
password_manager::PasswordFormMetricsRecorder::BubbleTrigger;
using password_manager::metrics_util::CredentialSourceType;
using UkmEntry = ukm::builders::PasswordForm;
// |credential_management_api| defines whether credentials originate from the
// credential management API.
for (const bool credential_management_api : {false, true}) {
// |update| defines whether this is an update or a save bubble.
for (const bool update : {false, true}) {
for (const auto interaction :
{BubbleDismissalReason::kAccepted, BubbleDismissalReason::kDeclined,
BubbleDismissalReason::kIgnored}) {
SCOPED_TRACE(testing::Message()
<< "update = " << update
<< ", interaction = " << static_cast<int64_t>(interaction)
<< ", credential management api ="
<< credential_management_api);
ukm::TestAutoSetUkmRecorder test_ukm_recorder;
{
// Setup metrics recorder
auto recorder = base::MakeRefCounted<
password_manager::PasswordFormMetricsRecorder>(
true /*is_main_frame_secure*/, kTestSourceId);
// Exercise bubble.
ON_CALL(*delegate(), GetPasswordFormMetricsRecorder())
.WillByDefault(Return(recorder.get()));
ON_CALL(*delegate(), GetCredentialSource())
.WillByDefault(
Return(credential_management_api
? CredentialSourceType::kCredentialManagementAPI
: CredentialSourceType::kPasswordManager));
if (update)
PretendUpdatePasswordWaiting();
else
PretendPasswordWaiting();
if (interaction == BubbleDismissalReason::kAccepted) {
EXPECT_CALL(*GetStore(),
RemoveSiteStatsImpl(GURL(kSiteOrigin).GetOrigin()));
EXPECT_CALL(*delegate(),
SavePassword(pending_password().username_value,
pending_password().password_value));
controller()->OnSaveClicked();
} else if (interaction == BubbleDismissalReason::kDeclined &&
update) {
EXPECT_CALL(*delegate(), SavePassword(_, _)).Times(0);
controller()->OnNopeUpdateClicked();
} else if (interaction == BubbleDismissalReason::kDeclined &&
!update) {
EXPECT_CALL(*GetStore(),
RemoveSiteStatsImpl(GURL(kSiteOrigin).GetOrigin()));
EXPECT_CALL(*delegate(), SavePassword(_, _)).Times(0);
EXPECT_CALL(*delegate(), NeverSavePassword());
controller()->OnNeverForThisSiteClicked();
} else if (interaction == BubbleDismissalReason::kIgnored && update) {
EXPECT_CALL(*delegate(), SavePassword(_, _)).Times(0);
EXPECT_CALL(*delegate(), NeverSavePassword()).Times(0);
} else if (interaction == BubbleDismissalReason::kIgnored &&
!update) {
EXPECT_CALL(*GetStore(), AddSiteStatsImpl(testing::_));
EXPECT_CALL(*delegate(), OnNoInteraction());
EXPECT_CALL(*delegate(), SavePassword(_, _)).Times(0);
EXPECT_CALL(*delegate(), NeverSavePassword()).Times(0);
} else {
NOTREACHED();
}
DestroyModelAndVerifyControllerExpectations();
}
ASSERT_TRUE(testing::Mock::VerifyAndClearExpectations(delegate()));
// Flush async calls on password store.
base::RunLoop().RunUntilIdle();
ASSERT_TRUE(testing::Mock::VerifyAndClearExpectations(GetStore()));
// Verify metrics.
const auto& entries =
test_ukm_recorder.GetEntriesByName(UkmEntry::kEntryName);
EXPECT_EQ(1u, entries.size());
for (const auto* entry : entries) {
EXPECT_EQ(kTestSourceId, entry->source_id);
test_ukm_recorder.ExpectEntryMetric(
entry,
update ? UkmEntry::kUpdating_Prompt_ShownName
: UkmEntry::kSaving_Prompt_ShownName,
1);
test_ukm_recorder.ExpectEntryMetric(
entry,
update ? UkmEntry::kUpdating_Prompt_TriggerName
: UkmEntry::kSaving_Prompt_TriggerName,
static_cast<int64_t>(
credential_management_api
? BubbleTrigger::kCredentialManagementAPIAutomatic
: BubbleTrigger::kPasswordManagerSuggestionAutomatic));
test_ukm_recorder.ExpectEntryMetric(
entry,
update ? UkmEntry::kUpdating_Prompt_InteractionName
: UkmEntry::kSaving_Prompt_InteractionName,
static_cast<int64_t>(interaction));
}
}
}
}
}
class SaveUpdateWithAccountStoreBubbleControllerPasswordRevealingTest
: public SaveUpdateWithAccountStoreBubbleControllerTest,
public testing::WithParamInterface<
std::tuple<bool /*is manual fallback*/,
bool /*form has autofilled value*/,
bool /*does os support user authentication*/,
PasswordBubbleControllerBase::DisplayReason>> {};
TEST_P(SaveUpdateWithAccountStoreBubbleControllerPasswordRevealingTest,
EyeIcon_ReauthForPasswordsRevealing) {
bool is_manual_fallback_for_saving = std::get<0>(GetParam());
bool form_has_autofilled_value = std::get<1>(GetParam());
bool does_os_support_user_auth = std::get<2>(GetParam());
PasswordBubbleControllerBase::DisplayReason display_reason =
std::get<3>(GetParam());
// That state is impossible.
if (is_manual_fallback_for_saving &&
(display_reason ==
PasswordBubbleControllerBase::DisplayReason::kAutomatic))
SUCCEED();
SCOPED_TRACE(
testing::Message()
<< "is_manual_fallback_for_saving = " << is_manual_fallback_for_saving
<< " form_has_autofilled_value = " << form_has_autofilled_value
<< " display_reason = "
<< (display_reason ==
PasswordBubbleControllerBase::DisplayReason::kAutomatic
? "AUTOMATIC"
: "USER_ACTION"));
pending_password().form_has_autofilled_value = form_has_autofilled_value;
EXPECT_CALL(*delegate(), ArePasswordsRevealedWhenBubbleIsOpened())
.WillOnce(Return(false));
EXPECT_CALL(*delegate(), BubbleIsManualFallbackForSaving())
.WillRepeatedly(Return(is_manual_fallback_for_saving));
PretendPasswordWaiting(display_reason);
bool reauth_expected = form_has_autofilled_value;
if (!reauth_expected) {
reauth_expected =
!is_manual_fallback_for_saving &&
display_reason ==
PasswordBubbleControllerBase::DisplayReason::kUserAction;
}
EXPECT_EQ(reauth_expected,
controller()->password_revealing_requires_reauth());
// delegate()->AuthenticateUser() is called only when reauth is expected.
EXPECT_CALL(*delegate(), AuthenticateUser())
.Times(reauth_expected)
.WillOnce(Return(!does_os_support_user_auth));
if (reauth_expected) {
EXPECT_EQ(controller()->RevealPasswords(), !does_os_support_user_auth);
} else {
EXPECT_TRUE(controller()->RevealPasswords());
}
}
INSTANTIATE_TEST_SUITE_P(
SaveUpdateWithAccountStoreBubbleController,
SaveUpdateWithAccountStoreBubbleControllerPasswordRevealingTest,
testing::Combine(
testing::Bool(),
testing::Bool(),
testing::Bool(),
testing::Values(
PasswordBubbleControllerBase::DisplayReason::kAutomatic,
PasswordBubbleControllerBase::DisplayReason::kUserAction)));
TEST_F(SaveUpdateWithAccountStoreBubbleControllerTest,
EyeIcon_BubbleReopenedAfterAuth) {
// Checks re-authentication is not needed if the bubble is opened right after
// successful authentication.
pending_password().form_has_autofilled_value = true;
// After successful authentication this value is set to true.
EXPECT_CALL(*delegate(), ArePasswordsRevealedWhenBubbleIsOpened())
.WillOnce(Return(true));
PretendPasswordWaiting(
PasswordBubbleControllerBase::DisplayReason::kUserAction);
EXPECT_FALSE(controller()->password_revealing_requires_reauth());
EXPECT_TRUE(controller()->RevealPasswords());
}
TEST_F(SaveUpdateWithAccountStoreBubbleControllerTest,
PasswordsRevealedReported) {
PretendPasswordWaiting();
EXPECT_CALL(*delegate(), OnPasswordsRevealed());
EXPECT_TRUE(controller()->RevealPasswords());
}
TEST_F(SaveUpdateWithAccountStoreBubbleControllerTest,
PasswordsRevealedReportedAfterReauth) {
// The bubble is opened after reauthentication and the passwords are revealed.
pending_password().form_has_autofilled_value = true;
// After successful authentication this value is set to true.
EXPECT_CALL(*delegate(), ArePasswordsRevealedWhenBubbleIsOpened())
.WillOnce(Return(true));
EXPECT_CALL(*delegate(), OnPasswordsRevealed());
PretendPasswordWaiting(
PasswordBubbleControllerBase::DisplayReason::kUserAction);
}
TEST_F(SaveUpdateWithAccountStoreBubbleControllerTest, DisableEditing) {
EXPECT_CALL(*delegate(), BubbleIsManualFallbackForSaving())
.WillRepeatedly(Return(false));
EXPECT_CALL(*delegate(), GetCredentialSource())
.WillOnce(Return(password_manager::metrics_util::CredentialSourceType::
kCredentialManagementAPI));
PretendPasswordWaiting();
EXPECT_FALSE(controller()->enable_editing());
}
......@@ -104,9 +104,8 @@ class ManagePasswordsUIController
return bubble_status_ == BubbleStatus::SHOULD_POP_UP;
}
// TODO(crbug.com/1044034): Rename to GetControllerDelegateProxy after the
// refactoring of ManagePasswordsBubbleModel is done.
base::WeakPtr<PasswordsModelDelegate> GetModelDelegateProxy();
// virtual to be overridden in tests.
virtual base::WeakPtr<PasswordsModelDelegate> GetModelDelegateProxy();
// PasswordsModelDelegate:
content::WebContents* GetWebContents() const override;
......
......@@ -48,8 +48,10 @@ LocationBarBubbleDelegateView::LocationBarBubbleDelegateView(
// Add observer to close the bubble if the fullscreen state changes.
if (web_contents) {
Browser* browser = chrome::FindBrowserWithWebContents(web_contents);
fullscreen_observer_.Add(
browser->exclusive_access_manager()->fullscreen_controller());
// |browser| can be null in tests.
if (browser)
fullscreen_observer_.Add(
browser->exclusive_access_manager()->fullscreen_controller());
}
}
......
// Copyright 2020 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/ui/views/passwords/password_save_update_with_account_store_view.h"
#include <algorithm>
#include <memory>
#include <utility>
#include <vector>
#include "base/strings/utf_string_conversions.h"
#include "build/build_config.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/passwords/manage_passwords_view_utils.h"
#include "chrome/browser/ui/passwords/password_dialog_prompts.h"
#include "chrome/browser/ui/passwords/passwords_model_delegate.h"
#include "chrome/browser/ui/views/accessibility/non_accessible_image_view.h"
#include "chrome/browser/ui/views/chrome_layout_provider.h"
#include "chrome/browser/ui/views/chrome_typography.h"
#include "chrome/browser/ui/views/passwords/credentials_item_view.h"
#include "chrome/browser/ui/views/passwords/password_items_view.h"
#include "chrome/grit/generated_resources.h"
#include "chrome/grit/theme_resources.h"
#include "content/public/browser/storage_partition.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/models/combobox_model.h"
#include "ui/base/models/combobox_model_observer.h"
#include "ui/base/models/simple_combobox_model.h"
#include "ui/base/resource/resource_bundle.h"
#include "ui/gfx/color_utils.h"
#include "ui/gfx/paint_vector_icon.h"
#include "ui/views/bubble/bubble_frame_view.h"
#include "ui/views/controls/button/image_button.h"
#include "ui/views/controls/button/md_text_button.h"
#include "ui/views/controls/editable_combobox/editable_combobox.h"
#include "ui/views/controls/textfield/textfield.h"
#include "ui/views/layout/fill_layout.h"
#include "ui/views/layout/grid_layout.h"
#include "ui/views/layout/layout_provider.h"
#include "ui/views/view.h"
#if defined(PASSWORD_STORE_SELECT_ENABLED)
#include "base/feature_list.h"
#include "components/password_manager/core/common/password_manager_features.h"
#include "ui/views/controls/button/checkbox.h"
#endif // defined(PASSWORD_STORE_SELECT_ENABLED)
namespace {
enum PasswordSaveUpdateWithAccountStoreViewColumnSetType {
// | | (LEADING, FILL) | | (FILL, FILL) | |
// Used for the username/password line of the bubble, for the pending view.
DOUBLE_VIEW_COLUMN_SET_USERNAME,
DOUBLE_VIEW_COLUMN_SET_PASSWORD,
// | | (LEADING, FILL) | | (FILL, FILL) | | (TRAILING, FILL) | |
// Used for the password line of the bubble, for the pending view.
// Views are label, password and the eye icon.
TRIPLE_VIEW_COLUMN_SET,
};
// Construct an appropriate ColumnSet for the given |type|, and add it
// to |layout|.
void BuildColumnSet(views::GridLayout* layout,
PasswordSaveUpdateWithAccountStoreViewColumnSetType type) {
views::ColumnSet* column_set = layout->AddColumnSet(type);
const int column_divider = ChromeLayoutProvider::Get()->GetDistanceMetric(
views::DISTANCE_RELATED_CONTROL_HORIZONTAL);
switch (type) {
case DOUBLE_VIEW_COLUMN_SET_USERNAME:
case DOUBLE_VIEW_COLUMN_SET_PASSWORD:
column_set->AddColumn(views::GridLayout::LEADING, views::GridLayout::FILL,
views::GridLayout::kFixedSize,
views::GridLayout::USE_PREF, 0, 0);
column_set->AddPaddingColumn(views::GridLayout::kFixedSize,
column_divider);
column_set->AddColumn(views::GridLayout::FILL, views::GridLayout::FILL,
1.0, views::GridLayout::USE_PREF, 0, 0);
break;
case TRIPLE_VIEW_COLUMN_SET:
column_set->AddColumn(views::GridLayout::LEADING, views::GridLayout::FILL,
views::GridLayout::kFixedSize,
views::GridLayout::USE_PREF, 0, 0);
column_set->AddPaddingColumn(views::GridLayout::kFixedSize,
column_divider);
column_set->AddColumn(views::GridLayout::FILL, views::GridLayout::FILL,
1.0, views::GridLayout::USE_PREF, 0, 0);
column_set->AddPaddingColumn(views::GridLayout::kFixedSize,
column_divider);
column_set->AddColumn(
views::GridLayout::TRAILING, views::GridLayout::FILL,
views::GridLayout::kFixedSize, views::GridLayout::USE_PREF, 0, 0);
break;
}
}
// Builds a credential row, adds the given elements to the layout.
// |password_view_button| is an optional field. If it is a nullptr, a
// DOUBLE_VIEW_COLUMN_SET_PASSWORD will be used for password row instead of
// TRIPLE_VIEW_COLUMN_SET.
void BuildCredentialRows(
views::GridLayout* layout,
std::unique_ptr<views::View> username_field,
std::unique_ptr<views::View> password_field,
std::unique_ptr<views::ToggleImageButton> password_view_button) {
// Username row.
BuildColumnSet(layout, DOUBLE_VIEW_COLUMN_SET_USERNAME);
layout->StartRow(views::GridLayout::kFixedSize,
DOUBLE_VIEW_COLUMN_SET_USERNAME);
std::unique_ptr<views::Label> username_label(new views::Label(
l10n_util::GetStringUTF16(IDS_PASSWORD_MANAGER_USERNAME_LABEL),
views::style::CONTEXT_LABEL, views::style::STYLE_PRIMARY));
username_label->SetHorizontalAlignment(gfx::HorizontalAlignment::ALIGN_LEFT);
std::unique_ptr<views::Label> password_label(new views::Label(
l10n_util::GetStringUTF16(IDS_PASSWORD_MANAGER_PASSWORD_LABEL),
views::style::CONTEXT_LABEL, views::style::STYLE_PRIMARY));
password_label->SetHorizontalAlignment(gfx::HorizontalAlignment::ALIGN_LEFT);
int labels_width = std::max(username_label->GetPreferredSize().width(),
password_label->GetPreferredSize().width());
int fields_height = std::max(username_field->GetPreferredSize().height(),
password_field->GetPreferredSize().height());
layout->AddView(std::move(username_label), 1, 1, views::GridLayout::LEADING,
views::GridLayout::FILL, labels_width, 0);
layout->AddView(std::move(username_field), 1, 1, views::GridLayout::FILL,
views::GridLayout::FILL, 0, fields_height);
layout->AddPaddingRow(views::GridLayout::kFixedSize,
ChromeLayoutProvider::Get()->GetDistanceMetric(
DISTANCE_CONTROL_LIST_VERTICAL));
// Password row.
PasswordSaveUpdateWithAccountStoreViewColumnSetType type =
password_view_button ? TRIPLE_VIEW_COLUMN_SET
: DOUBLE_VIEW_COLUMN_SET_PASSWORD;
BuildColumnSet(layout, type);
layout->StartRow(views::GridLayout::kFixedSize, type);
layout->AddView(std::move(password_label), 1, 1, views::GridLayout::LEADING,
views::GridLayout::FILL, labels_width, 0);
layout->AddView(std::move(password_field), 1, 1, views::GridLayout::FILL,
views::GridLayout::FILL, 0, fields_height);
// The eye icon is also added to the layout if it was passed.
if (password_view_button) {
layout->AddView(std::move(password_view_button));
}
}
// Create a vector which contains only the values in |items| and no elements.
std::vector<base::string16> ToValues(
const autofill::ValueElementVector& items) {
std::vector<base::string16> passwords;
passwords.reserve(items.size());
for (auto& pair : items)
passwords.push_back(pair.first);
return passwords;
}
std::unique_ptr<views::ToggleImageButton> CreatePasswordViewButton(
views::ButtonListener* listener,
bool are_passwords_revealed) {
auto button = std::make_unique<views::ToggleImageButton>(listener);
button->SetFocusForPlatform();
button->SetInstallFocusRingOnFocus(true);
button->set_request_focus_on_press(true);
button->SetTooltipText(
l10n_util::GetStringUTF16(IDS_MANAGE_PASSWORDS_SHOW_PASSWORD));
button->SetToggledTooltipText(
l10n_util::GetStringUTF16(IDS_MANAGE_PASSWORDS_HIDE_PASSWORD));
button->SetImage(views::ImageButton::STATE_NORMAL,
*ui::ResourceBundle::GetSharedInstance().GetImageSkiaNamed(
IDR_SHOW_PASSWORD_HOVER));
button->SetToggledImage(
views::ImageButton::STATE_NORMAL,
ui::ResourceBundle::GetSharedInstance().GetImageSkiaNamed(
IDR_HIDE_PASSWORD_HOVER));
button->SetImageHorizontalAlignment(views::ImageButton::ALIGN_CENTER);
button->SetImageVerticalAlignment(views::ImageButton::ALIGN_MIDDLE);
button->SetToggled(are_passwords_revealed);
return button;
}
// Creates an EditableCombobox from |PasswordForm.all_possible_passwords| or
// even just |PasswordForm.password_value|.
std::unique_ptr<views::EditableCombobox> CreatePasswordEditableCombobox(
const autofill::PasswordForm& form,
bool are_passwords_revealed) {
DCHECK(!form.IsFederatedCredential());
std::vector<base::string16> passwords =
form.all_possible_passwords.empty()
? std::vector<base::string16>(/*n=*/1, form.password_value)
: ToValues(form.all_possible_passwords);
base::EraseIf(passwords, [](const base::string16& password) {
return password.empty();
});
bool display_arrow = !passwords.empty();
auto combobox = std::make_unique<views::EditableCombobox>(
std::make_unique<ui::SimpleComboboxModel>(std::move(passwords)),
/*filter_on_edit=*/false, /*show_on_empty=*/true,
views::EditableCombobox::Type::kPassword, views::style::CONTEXT_BUTTON,
STYLE_PRIMARY_MONOSPACED, display_arrow);
combobox->SetText(form.password_value);
combobox->RevealPasswords(are_passwords_revealed);
combobox->SetAccessibleName(
l10n_util::GetStringUTF16(IDS_PASSWORD_MANAGER_PASSWORD_LABEL));
return combobox;
}
std::unique_ptr<views::View> CreateHeaderImage(int image_id) {
auto image_view = std::make_unique<NonAccessibleImageView>();
image_view->SetImage(
*ui::ResourceBundle::GetSharedInstance().GetImageSkiaNamed(image_id));
gfx::Size preferred_size = image_view->GetPreferredSize();
if (preferred_size.width()) {
float scale =
static_cast<float>(ChromeLayoutProvider::Get()->GetDistanceMetric(
DISTANCE_BUBBLE_PREFERRED_WIDTH)) /
preferred_size.width();
preferred_size = gfx::ScaleToRoundedSize(preferred_size, scale);
image_view->SetImageSize(preferred_size);
}
return image_view;
}
#if defined(PASSWORD_STORE_SELECT_ENABLED)
views::Checkbox* MaybeAppendAccountCheckboxRow(
views::GridLayout* layout,
bool uses_account_store,
int height,
views::ButtonListener* listener) {
if (!base::FeatureList::IsEnabled(
password_manager::features::kEnablePasswordsAccountStorageSavingUi)) {
return nullptr;
}
auto account_store_checkbox = std::make_unique<views::Checkbox>(
base::ASCIIToUTF16("Store passwords in account store?"), listener);
account_store_checkbox->SetChecked(uses_account_store);
BuildColumnSet(layout, DOUBLE_VIEW_COLUMN_SET_PASSWORD);
layout->StartRow(views::GridLayout::kFixedSize,
DOUBLE_VIEW_COLUMN_SET_PASSWORD);
return layout->AddView(std::move(account_store_checkbox), 1, 1,
views::GridLayout::FILL, views::GridLayout::FILL, 0,
height);
}
#endif // defined(PASSWORD_STORE_SELECT_ENABLED)
} // namespace
PasswordSaveUpdateWithAccountStoreView::PasswordSaveUpdateWithAccountStoreView(
content::WebContents* web_contents,
views::View* anchor_view,
DisplayReason reason)
: PasswordBubbleViewBase(web_contents,
anchor_view,
/*auto_dismissable=*/false),
controller_(
PasswordsModelDelegateFromWebContents(web_contents),
reason == AUTOMATIC
? PasswordBubbleControllerBase::DisplayReason::kAutomatic
: PasswordBubbleControllerBase::DisplayReason::kUserAction),
is_update_bubble_(controller_.state() ==
password_manager::ui::PENDING_PASSWORD_UPDATE_STATE),
username_dropdown_(nullptr),
password_view_button_(nullptr),
password_dropdown_(nullptr),
are_passwords_revealed_(
controller_.are_passwords_revealed_when_bubble_is_opened()) {
DCHECK(controller_.state() == password_manager::ui::PENDING_PASSWORD_STATE ||
controller_.state() ==
password_manager::ui::PENDING_PASSWORD_UPDATE_STATE);
const autofill::PasswordForm& password_form = controller_.pending_password();
if (password_form.IsFederatedCredential()) {
// The credential to be saved doesn't contain password but just the identity
// provider (e.g. "Sign in with Google"). Thus, the layout is different.
SetLayoutManager(std::make_unique<views::FillLayout>());
std::pair<base::string16, base::string16> titles =
GetCredentialLabelsForAccountChooser(password_form);
CredentialsItemView* credential_view = new CredentialsItemView(
this, titles.first, titles.second, &password_form,
content::BrowserContext::GetDefaultStoragePartition(
controller_.GetProfile())
->GetURLLoaderFactoryForBrowserProcess()
.get());
credential_view->SetEnabled(false);
AddChildView(credential_view);
} else {
std::unique_ptr<views::EditableCombobox> username_dropdown =
CreateUsernameEditableCombobox(password_form);
username_dropdown->set_listener(this);
std::unique_ptr<views::EditableCombobox> password_dropdown =
CreatePasswordEditableCombobox(password_form, are_passwords_revealed_);
password_dropdown->set_listener(this);
std::unique_ptr<views::ToggleImageButton> password_view_button =
CreatePasswordViewButton(this, are_passwords_revealed_);
views::GridLayout* layout =
SetLayoutManager(std::make_unique<views::GridLayout>());
username_dropdown_ = username_dropdown.get();
password_dropdown_ = password_dropdown.get();
password_view_button_ = password_view_button.get();
BuildCredentialRows(layout, std::move(username_dropdown),
std::move(password_dropdown),
std::move(password_view_button));
#if defined(PASSWORD_STORE_SELECT_ENABLED)
account_store_checkbox_ = MaybeAppendAccountCheckboxRow(
layout, controller_.IsUsingAccountStore(),
password_dropdown_->GetPreferredSize().height(), this);
#endif // defined(PASSWORD_STORE_SELECT_ENABLED)
}
DialogDelegate::SetFootnoteView(CreateFooterView());
UpdateDialogButtons();
}
PasswordSaveUpdateWithAccountStoreView::
~PasswordSaveUpdateWithAccountStoreView() = default;
PasswordBubbleControllerBase*
PasswordSaveUpdateWithAccountStoreView::GetController() {
return &controller_;
}
const PasswordBubbleControllerBase*
PasswordSaveUpdateWithAccountStoreView::GetController() const {
return &controller_;
}
bool PasswordSaveUpdateWithAccountStoreView::Accept() {
UpdateUsernameAndPasswordInModel();
controller_.OnSaveClicked();
return true;
}
bool PasswordSaveUpdateWithAccountStoreView::Cancel() {
UpdateUsernameAndPasswordInModel();
if (is_update_bubble_) {
controller_.OnNopeUpdateClicked();
return true;
}
controller_.OnNeverForThisSiteClicked();
return true;
}
bool PasswordSaveUpdateWithAccountStoreView::Close() {
return true;
}
void PasswordSaveUpdateWithAccountStoreView::ButtonPressed(
views::Button* sender,
const ui::Event& event) {
#if defined(PASSWORD_STORE_SELECT_ENABLED)
DCHECK(sender);
if (sender == account_store_checkbox_) {
controller_.OnToggleAccountStore(account_store_checkbox_->GetChecked());
return;
}
#endif // defined(PASSWORD_STORE_SELECT_ENABLED)
DCHECK(sender == password_view_button_);
TogglePasswordVisibility();
}
void PasswordSaveUpdateWithAccountStoreView::OnContentChanged(
views::EditableCombobox* editable_combobox) {
bool is_update_state_before = controller_.IsCurrentStateUpdate();
bool is_ok_button_enabled_before =
IsDialogButtonEnabled(ui::DIALOG_BUTTON_OK);
UpdateUsernameAndPasswordInModel();
// Maybe the buttons should be updated.
if (is_update_state_before != controller_.IsCurrentStateUpdate() ||
is_ok_button_enabled_before !=
IsDialogButtonEnabled(ui::DIALOG_BUTTON_OK)) {
UpdateDialogButtons();
DialogModelChanged();
}
}
gfx::Size PasswordSaveUpdateWithAccountStoreView::CalculatePreferredSize()
const {
const int width = ChromeLayoutProvider::Get()->GetDistanceMetric(
DISTANCE_BUBBLE_PREFERRED_WIDTH) -
margins().width();
return gfx::Size(width, GetHeightForWidth(width));
}
views::View* PasswordSaveUpdateWithAccountStoreView::GetInitiallyFocusedView() {
if (username_dropdown_ && username_dropdown_->GetText().empty())
return username_dropdown_;
View* initial_view = PasswordBubbleViewBase::GetInitiallyFocusedView();
// |initial_view| will normally be the 'Save' button, but in case it's not
// focusable, we return nullptr so the Widget doesn't give focus to the next
// focusable View, which would be |username_dropdown_|, and which would bring
// up the menu without a user interaction. We only allow initial focus on
// |username_dropdown_| above, when the text is empty.
return (initial_view && initial_view->IsFocusable()) ? initial_view : nullptr;
}
bool PasswordSaveUpdateWithAccountStoreView::IsDialogButtonEnabled(
ui::DialogButton button) const {
return button != ui::DIALOG_BUTTON_OK ||
controller_.pending_password().IsFederatedCredential() ||
!controller_.pending_password().password_value.empty();
}
gfx::ImageSkia PasswordSaveUpdateWithAccountStoreView::GetWindowIcon() {
return gfx::ImageSkia();
}
bool PasswordSaveUpdateWithAccountStoreView::ShouldShowWindowIcon() const {
return false;
}
bool PasswordSaveUpdateWithAccountStoreView::ShouldShowCloseButton() const {
return true;
}
void PasswordSaveUpdateWithAccountStoreView::AddedToWidget() {
static_cast<views::Label*>(GetBubbleFrameView()->title())
->SetAllowCharacterBreak(true);
}
void PasswordSaveUpdateWithAccountStoreView::OnThemeChanged() {
if (int id = controller_.GetTopIllustration(
color_utils::IsDark(GetBubbleFrameView()->GetBackgroundColor()))) {
GetBubbleFrameView()->SetHeaderView(CreateHeaderImage(id));
}
}
void PasswordSaveUpdateWithAccountStoreView::TogglePasswordVisibility() {
if (!are_passwords_revealed_ && !controller_.RevealPasswords())
return;
are_passwords_revealed_ = !are_passwords_revealed_;
password_view_button_->SetToggled(are_passwords_revealed_);
DCHECK(password_dropdown_);
password_dropdown_->RevealPasswords(are_passwords_revealed_);
}
void PasswordSaveUpdateWithAccountStoreView::
UpdateUsernameAndPasswordInModel() {
if (!username_dropdown_ && !password_dropdown_)
return;
base::string16 new_username = controller_.pending_password().username_value;
base::string16 new_password = controller_.pending_password().password_value;
if (username_dropdown_) {
new_username = username_dropdown_->GetText();
base::TrimString(new_username, base::ASCIIToUTF16(" "), &new_username);
}
if (password_dropdown_)
new_password = password_dropdown_->GetText();
controller_.OnCredentialEdited(std::move(new_username),
std::move(new_password));
}
void PasswordSaveUpdateWithAccountStoreView::UpdateDialogButtons() {
DialogDelegate::set_buttons(
(ui::DIALOG_BUTTON_OK | ui::DIALOG_BUTTON_CANCEL));
DialogDelegate::set_button_label(
ui::DIALOG_BUTTON_OK,
l10n_util::GetStringUTF16(controller_.IsCurrentStateUpdate()
? IDS_PASSWORD_MANAGER_UPDATE_BUTTON
: IDS_PASSWORD_MANAGER_SAVE_BUTTON));
DialogDelegate::set_button_label(
ui::DIALOG_BUTTON_CANCEL,
l10n_util::GetStringUTF16(
is_update_bubble_ ? IDS_PASSWORD_MANAGER_CANCEL_BUTTON
: IDS_PASSWORD_MANAGER_BUBBLE_BLACKLIST_BUTTON));
}
std::unique_ptr<views::View>
PasswordSaveUpdateWithAccountStoreView::CreateFooterView() {
if (!controller_.ShouldShowFooter())
return nullptr;
auto label = std::make_unique<views::Label>(
l10n_util::GetStringUTF16(IDS_SAVE_PASSWORD_FOOTER),
ChromeTextContext::CONTEXT_BODY_TEXT_SMALL,
views::style::STYLE_SECONDARY);
label->SetMultiLine(true);
label->SetHorizontalAlignment(gfx::ALIGN_LEFT);
return label;
}
// Copyright 2020 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.
#ifndef CHROME_BROWSER_UI_VIEWS_PASSWORDS_PASSWORD_SAVE_UPDATE_WITH_ACCOUNT_STORE_VIEW_H_
#define CHROME_BROWSER_UI_VIEWS_PASSWORDS_PASSWORD_SAVE_UPDATE_WITH_ACCOUNT_STORE_VIEW_H_
#include "chrome/browser/ui/passwords/bubble_controllers/save_update_with_account_store_bubble_controller.h"
#include "chrome/browser/ui/views/passwords/password_bubble_view_base.h"
#include "ui/views/controls/button/button.h"
#include "ui/views/controls/editable_combobox/editable_combobox_listener.h"
#include "ui/views/view.h"
namespace views {
class EditableCombobox;
class ToggleImageButton;
#if defined(PASSWORD_STORE_SELECT_ENABLED)
class Checkbox;
#endif // defined(PASSWORD_STORE_SELECT_ENABLED)
} // namespace views
// A view offering the user the ability to save or update credentials (depending
// on |is_update_bubble|) either in the profile and/or account stores. Contains
// a username and password field, and in case of a saving a destination picker.
// In addition, it contains a "Save"/"Update" button and a "Never"/"Nope"
// button.
class PasswordSaveUpdateWithAccountStoreView
: public PasswordBubbleViewBase,
public views::ButtonListener,
public views::EditableComboboxListener {
public:
PasswordSaveUpdateWithAccountStoreView(content::WebContents* web_contents,
views::View* anchor_view,
DisplayReason reason);
private:
~PasswordSaveUpdateWithAccountStoreView() override;
// PasswordBubbleViewBase
PasswordBubbleControllerBase* GetController() override;
const PasswordBubbleControllerBase* GetController() const override;
// views::ButtonListener:
void ButtonPressed(views::Button* sender, const ui::Event& event) override;
// views::EditableComboboxListener:
// Used for both the username and password editable comboboxes.
void OnContentChanged(views::EditableCombobox* editable_combobox) override;
// PasswordBubbleViewBase:
gfx::Size CalculatePreferredSize() const override;
views::View* GetInitiallyFocusedView() override;
bool IsDialogButtonEnabled(ui::DialogButton button) const override;
gfx::ImageSkia GetWindowIcon() override;
bool ShouldShowWindowIcon() const override;
bool ShouldShowCloseButton() const override;
bool Accept() override;
bool Cancel() override;
bool Close() override;
// View:
void AddedToWidget() override;
void OnThemeChanged() override;
void TogglePasswordVisibility();
void UpdateUsernameAndPasswordInModel();
void UpdateDialogButtons();
std::unique_ptr<views::View> CreateFooterView();
SaveUpdateWithAccountStoreBubbleController controller_;
// True iff it is an update password bubble on creation. False iff it is a
// save bubble.
const bool is_update_bubble_;
views::EditableCombobox* username_dropdown_;
views::ToggleImageButton* password_view_button_;
// The view for the password value.
views::EditableCombobox* password_dropdown_;
#if defined(PASSWORD_STORE_SELECT_ENABLED)
views::Checkbox* account_store_checkbox_ = nullptr;
#endif // defined(PASSWORD_STORE_SELECT_ENABLED)
bool are_passwords_revealed_;
};
#endif // CHROME_BROWSER_UI_VIEWS_PASSWORDS_PASSWORD_SAVE_UPDATE_WITH_ACCOUNT_STORE_VIEW_H_
// Copyright 2020 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/ui/views/passwords/password_save_update_with_account_store_view.h"
#include "chrome/browser/password_manager/password_store_factory.h"
#include "chrome/browser/ui/passwords/manage_passwords_ui_controller.h"
#include "chrome/browser/ui/passwords/passwords_model_delegate_mock.h"
#include "chrome/test/base/testing_profile.h"
#include "chrome/test/views/chrome_views_test_base.h"
#include "components/password_manager/core/browser/mock_password_feature_manager.h"
#include "components/password_manager/core/browser/mock_password_store.h"
#include "components/password_manager/core/browser/password_manager_test_utils.h"
#include "content/public/test/web_contents_tester.h"
using ::testing::NiceMock;
using ::testing::Return;
using ::testing::ReturnRef;
class TestManagePasswordsUIController : public ManagePasswordsUIController {
public:
explicit TestManagePasswordsUIController(content::WebContents* web_contents);
base::WeakPtr<PasswordsModelDelegate> GetModelDelegateProxy() override {
return weak_ptr_factory_.GetWeakPtr();
}
private:
NiceMock<PasswordsModelDelegateMock> model_delegate_mock_;
base::WeakPtrFactory<PasswordsModelDelegate> weak_ptr_factory_;
autofill::PasswordForm pending_password_;
std::vector<std::unique_ptr<autofill::PasswordForm>> current_forms_;
NiceMock<password_manager::MockPasswordFeatureManager> feature_manager_;
};
TestManagePasswordsUIController::TestManagePasswordsUIController(
content::WebContents* web_contents)
: ManagePasswordsUIController(web_contents),
weak_ptr_factory_(&model_delegate_mock_) {
// Do not silently replace an existing ManagePasswordsUIController
// because it unregisters itself in WebContentsDestroyed().
EXPECT_FALSE(web_contents->GetUserData(UserDataKey()));
web_contents->SetUserData(UserDataKey(), base::WrapUnique(this));
ON_CALL(model_delegate_mock_, GetOrigin)
.WillByDefault(ReturnRef(pending_password_.origin));
ON_CALL(model_delegate_mock_, GetState)
.WillByDefault(Return(password_manager::ui::PENDING_PASSWORD_STATE));
ON_CALL(model_delegate_mock_, GetPendingPassword)
.WillByDefault(ReturnRef(pending_password_));
ON_CALL(model_delegate_mock_, GetCurrentForms)
.WillByDefault(ReturnRef(current_forms_));
ON_CALL(model_delegate_mock_, GetWebContents)
.WillByDefault(Return(web_contents));
ON_CALL(model_delegate_mock_, GetPasswordFeatureManager)
.WillByDefault(Return(&feature_manager_));
ON_CALL(feature_manager_, GetDefaultPasswordStore)
.WillByDefault(Return(autofill::PasswordForm::Store::kAccountStore));
}
class PasswordSaveUpdateWithAccountStoreViewTest : public ChromeViewsTestBase {
public:
PasswordSaveUpdateWithAccountStoreViewTest();
~PasswordSaveUpdateWithAccountStoreViewTest() override = default;
void CreateViewAndShow();
void TearDown() override {
view_->GetWidget()->CloseWithReason(
views::Widget::ClosedReason::kCloseButtonClicked);
anchor_widget_.reset();
ChromeViewsTestBase::TearDown();
}
PasswordSaveUpdateWithAccountStoreView* view() { return view_; }
private:
TestingProfile profile_;
std::unique_ptr<content::WebContents> test_web_contents_;
std::unique_ptr<views::Widget> anchor_widget_;
PasswordSaveUpdateWithAccountStoreView* view_;
};
PasswordSaveUpdateWithAccountStoreViewTest::
PasswordSaveUpdateWithAccountStoreViewTest() {
PasswordStoreFactory::GetInstance()->SetTestingFactoryAndUse(
&profile_,
base::BindRepeating(
&password_manager::BuildPasswordStore<
content::BrowserContext,
testing::NiceMock<password_manager::MockPasswordStore>>));
test_web_contents_ =
content::WebContentsTester::CreateTestWebContents(&profile_, nullptr);
// Create the test UIController here so that it's bound to
// |test_web_contents_|, and will be retrieved correctly via
// ManagePasswordsUIController::FromWebContents in
// PasswordsModelDelegateFromWebContents().
new TestManagePasswordsUIController(test_web_contents_.get());
}
void PasswordSaveUpdateWithAccountStoreViewTest::CreateViewAndShow() {
// The bubble needs the parent as an anchor.
views::Widget::InitParams params =
CreateParams(views::Widget::InitParams::TYPE_WINDOW);
params.ownership = views::Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET;
anchor_widget_ = std::make_unique<views::Widget>();
anchor_widget_->Init(std::move(params));
anchor_widget_->Show();
view_ = new PasswordSaveUpdateWithAccountStoreView(
test_web_contents_.get(), anchor_widget_->GetContentsView(),
LocationBarBubbleDelegateView::AUTOMATIC);
views::BubbleDialogDelegateView::CreateBubble(view_)->Show();
}
TEST_F(PasswordSaveUpdateWithAccountStoreViewTest, HasTitleAndTwoButtons) {
CreateViewAndShow();
EXPECT_TRUE(view()->ShouldShowWindowTitle());
EXPECT_TRUE(view()->GetOkButton());
EXPECT_TRUE(view()->GetCancelButton());
}
......@@ -4110,6 +4110,7 @@ test("unit_tests") {
"../browser/ui/passwords/bubble_controllers/generation_confirmation_bubble_controller_unittest.cc",
"../browser/ui/passwords/bubble_controllers/items_bubble_controller_unittest.cc",
"../browser/ui/passwords/bubble_controllers/save_update_bubble_controller_unittest.cc",
"../browser/ui/passwords/bubble_controllers/save_update_with_account_store_bubble_controller_unittest.cc",
"../browser/ui/passwords/bubble_controllers/sign_in_promo_bubble_controller_unittest.cc",
"../browser/ui/passwords/credential_leak_dialog_controller_impl_unittest.cc",
"../browser/ui/passwords/credential_manager_dialog_controller_impl_unittest.cc",
......@@ -4292,6 +4293,7 @@ test("unit_tests") {
"../browser/ui/toolbar/media_router_contextual_menu_unittest.cc",
"../browser/ui/toolbar/mock_media_router_action_controller.cc",
"../browser/ui/toolbar/mock_media_router_action_controller.h",
"../browser/ui/views/passwords/password_save_update_with_account_store_view_unittest.cc",
"../common/media_router/discovery/media_sink_internal_unittest.cc",
"../common/media_router/discovery/media_sink_service_base_unittest.cc",
"../common/media_router/mojom/media_router_mojom_traits_unittest.cc",
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment