// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "ash/system/toast/anchored_nudge_manager_impl.h"
#include "ash/constants/ash_features.h"
#include "ash/public/cpp/system/anchored_nudge_data.h"
#include "ash/public/cpp/system/scoped_nudge_pause.h"
#include "ash/root_window_controller.h"
#include "ash/session/session_controller_impl.h"
#include "ash/shelf/hotseat_widget.h"
#include "ash/shelf/shelf.h"
#include "ash/shell.h"
#include "ash/style/system_toast_style.h"
#include "ash/system/toast/anchored_nudge.h"
#include "ash/system/toast/nudge_constants.h"
#include "ash/system/unified/unified_system_tray.h"
#include "ash/test/ash_test_base.h"
#include "ash/wm/tablet_mode/tablet_mode_controller_test_api.h"
#include "base/i18n/rtl.h"
#include "base/test/bind.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/task_environment.h"
#include "ui/aura/window.h"
#include "ui/compositor/scoped_animation_duration_scale_mode.h"
#include "ui/display/manager/display_manager.h"
#include "ui/gfx/geometry/vector2d.h"
#include "ui/views/bubble/bubble_border.h"
#include "ui/views/controls/button/button.h"
#include "ui/views/controls/button/label_button.h"
#include "ui/views/controls/label.h"
#include "ui/views/focus/focus_manager.h"
#include "ui/views/layout/flex_layout_view.h"
#include "ui/views/widget/widget.h"
namespace ash {
namespace {
constexpr NudgeCatalogName kTestCatalogName =
NudgeCatalogName::kTestCatalogName;
constexpr char kPrimaryButtonPressed[] =
"Ash.NotifierFramework.Nudge.PrimaryButtonPressed";
constexpr char kSecondaryButtonPressed[] =
"Ash.NotifierFramework.Nudge.SecondaryButtonPressed";
constexpr char kNudgeShownCount[] = "Ash.NotifierFramework.Nudge.ShownCount";
constexpr char kNudgeTimeToActionWithin1m[] =
"Ash.NotifierFramework.Nudge.TimeToAction.Within1m";
constexpr char kNudgeTimeToActionWithin1h[] =
"Ash.NotifierFramework.Nudge.TimeToAction.Within1h";
constexpr char kNudgeTimeToActionWithinSession[] =
"Ash.NotifierFramework.Nudge.TimeToAction.WithinSession";
constexpr base::TimeDelta kAnimationSettleDownDuration = base::Seconds(5);
void SetLockedState(bool locked) {
SessionInfo info;
info.state = locked ? session_manager::SessionState::LOCKED
: session_manager::SessionState::ACTIVE;
Shell::Get()->session_controller()->SetSessionInfo(info);
}
AnchoredNudgeManagerImpl* GetAnchoredNudgeManager() {
return Shell::Get()->anchored_nudge_manager();
}
// Creates an `AnchoredNudgeData` object with only the required elements.
AnchoredNudgeData CreateBaseNudgeData(
const std::string& id,
views::View* anchor_view,
const std::u16string& body_text = std::u16string()) {
return AnchoredNudgeData(id, kTestCatalogName, body_text, anchor_view);
}
void CancelNudge(const std::string& id) {
GetAnchoredNudgeManager()->Cancel(id);
}
AnchoredNudge* GetShownNudge(const std::string& id) {
return GetAnchoredNudgeManager()->GetShownNudgeForTest(id);
}
const std::u16string& GetNudgeBodyText(const std::string& id) {
return GetAnchoredNudgeManager()->GetNudgeBodyTextForTest(id);
}
views::LabelButton* GetNudgePrimaryButton(const std::string& id) {
return GetAnchoredNudgeManager()->GetNudgePrimaryButtonForTest(id);
}
views::LabelButton* GetNudgeSecondaryButton(const std::string& id) {
return GetAnchoredNudgeManager()->GetNudgeSecondaryButtonForTest(id);
}
AnchoredNudge* GetNudgeIfShown(const std::string& id) {
return GetAnchoredNudgeManager()->GetNudgeIfShown(id);
}
void PressTab() {
ui::test::EventGenerator generator(Shell::GetPrimaryRootWindow());
generator.PressKey(ui::KeyboardCode::VKEY_TAB, ui::EF_NONE);
}
} // namespace
class AnchoredNudgeManagerImplTest : public AshTestBase {
public:
AnchoredNudgeManagerImplTest()
: AshTestBase(base::test::TaskEnvironment::TimeSource::MOCK_TIME) {}
};
// Tests that a nudge can be shown and its contents are properly sent.
TEST_F(AnchoredNudgeManagerImplTest, ShowNudge_SingleNudge) {
std::unique_ptr<views::Widget> widget = CreateFramelessTestWidget();
// Set up nudge data contents.
const std::string id("id");
const std::u16string body_text = u"Body text";
auto* anchor_view = widget->SetContentsView(std::make_unique<views::View>());
auto nudge_data = CreateBaseNudgeData(id, anchor_view, body_text);
// Show a nudge.
GetAnchoredNudgeManager()->Show(nudge_data);
// Ensure the nudge is visible and has set the provided contents.
AnchoredNudge* nudge = GetShownNudge(id);
ASSERT_TRUE(nudge);
EXPECT_TRUE(nudge->GetVisible());
EXPECT_EQ(anchor_view, nudge->GetAnchorView());
// Ensure the nudge widget was not activated when shown.
EXPECT_FALSE(nudge->GetWidget()->IsActive());
// Cancel the nudge, expect it to be removed from the shown nudges map.
CancelNudge(id);
EXPECT_FALSE(GetShownNudge(id));
}
// Tests that two nudges can be shown on screen at the same time.
TEST_F(AnchoredNudgeManagerImplTest, ShowNudge_TwoNudges) {
std::unique_ptr<views::Widget> widget = CreateFramelessTestWidget();
auto* contents_view =
widget->SetContentsView(std::make_unique<views::View>());
// Set up nudge data contents.
const std::string id("id");
auto* anchor_view =
contents_view->AddChildView(std::make_unique<views::View>());
auto nudge_data = CreateBaseNudgeData(id, anchor_view);
const std::string id_2("id_2");
auto* anchor_view_2 =
contents_view->AddChildView(std::make_unique<views::View>());
auto nudge_data_2 = CreateBaseNudgeData(id_2, anchor_view_2);
// Show the first nudge, expect the first nudge shown.
GetAnchoredNudgeManager()->Show(nudge_data);
EXPECT_TRUE(GetShownNudge(id));
EXPECT_FALSE(GetShownNudge(id_2));
// Show the second nudge, expect both nudges shown.
GetAnchoredNudgeManager()->Show(nudge_data_2);
EXPECT_TRUE(GetShownNudge(id));
EXPECT_TRUE(GetShownNudge(id_2));
// Cancel the second nudge, expect the first nudge shown.
CancelNudge(id_2);
EXPECT_TRUE(GetShownNudge(id));
EXPECT_FALSE(GetShownNudge(id_2));
// Cancel the first nudge, expect no nudges shown.
CancelNudge(id);
EXPECT_FALSE(GetShownNudge(id));
EXPECT_FALSE(GetShownNudge(id_2));
}
// Tests that a nudge with buttons can be shown, execute callbacks and dismiss
// the nudge when the button is pressed.
TEST_F(AnchoredNudgeManagerImplTest, ShowNudge_WithButtons) {
base::HistogramTester histogram_tester;
std::unique_ptr<views::Widget> widget = CreateFramelessTestWidget();
// Set up nudge data contents.
const std::string id("id");
auto* anchor_view = widget->SetContentsView(std::make_unique<views::View>());
const std::u16string primary_button_text = u"Primary";
const std::u16string secondary_button_text = u"Secondary";
auto nudge_data = CreateBaseNudgeData(id, anchor_view);
// Add a primary button with no callbacks.
nudge_data.primary_button_text = primary_button_text;
// Show the nudge.
GetAnchoredNudgeManager()->Show(nudge_data);
AnchoredNudge* nudge = GetShownNudge(id);
// Ensure the nudge is visible and has set the provided contents.
ASSERT_TRUE(nudge);
ASSERT_TRUE(GetNudgePrimaryButton(id));
EXPECT_FALSE(GetNudgeSecondaryButton(id));
// Press the primary button, the nudge should have dismissed.
LeftClickOn(GetNudgePrimaryButton(id));
EXPECT_FALSE(GetShownNudge(id));
histogram_tester.ExpectBucketCount(kPrimaryButtonPressed, kTestCatalogName,
1);
// Add callbacks for the primary button.
bool primary_button_callback_ran = false;
nudge_data.primary_button_callback = base::BindLambdaForTesting(
[&primary_button_callback_ran] { primary_button_callback_ran = true; });
// Show the nudge again.
GetAnchoredNudgeManager()->Show(nudge_data);
nudge = GetShownNudge(id);
// Press the primary button, `primary_button_callback` should have executed,
// and the nudge should have dismissed.
LeftClickOn(GetNudgePrimaryButton(id));
EXPECT_TRUE(primary_button_callback_ran);
EXPECT_FALSE(GetShownNudge(id));
histogram_tester.ExpectBucketCount(kPrimaryButtonPressed, kTestCatalogName,
2);
// Add a secondary button with no callbacks.
nudge_data.secondary_button_text = secondary_button_text;
// Show the nudge again, now with a secondary button.
GetAnchoredNudgeManager()->Show(nudge_data);
nudge = GetShownNudge(id);
// Ensure the nudge has a secondary button.
ASSERT_TRUE(GetNudgeSecondaryButton(id));
// Press the secondary button, the nudge should have dismissed.
LeftClickOn(GetNudgeSecondaryButton(id));
EXPECT_FALSE(GetShownNudge(id));
histogram_tester.ExpectBucketCount(kSecondaryButtonPressed, kTestCatalogName,
1);
// Add a callback for the secondary button.
bool secondary_button_callback_ran = false;
nudge_data.secondary_button_callback =
base::BindLambdaForTesting([&secondary_button_callback_ran] {
secondary_button_callback_ran = true;
});
// Show the nudge again.
GetAnchoredNudgeManager()->Show(nudge_data);
nudge = GetShownNudge(id);
// Press the secondary button, `secondary_button_callback` should have
// executed, and the nudge should have dismissed.
LeftClickOn(GetNudgeSecondaryButton(id));
EXPECT_TRUE(secondary_button_callback_ran);
EXPECT_FALSE(GetShownNudge(id));
histogram_tester.ExpectBucketCount(kSecondaryButtonPressed, kTestCatalogName,
2);
}
// Tests that a nudge without an anchor view is shown on its default location.
TEST_F(AnchoredNudgeManagerImplTest, DefaultLocation) {
Shelf* shelf = GetPrimaryShelf();
display::Display primary_display = GetPrimaryDisplay();
gfx::Rect display_bounds = primary_display.bounds();
int shelf_size = ShelfConfig::Get()->shelf_size();
gfx::Rect nudge_bounds;
// Show nudge on its default location by not providing an anchor view.
const std::string id("id");
auto nudge_data = CreateBaseNudgeData(id, /*anchor_view=*/nullptr);
GetAnchoredNudgeManager()->Show(nudge_data);
// The nudge should be shown on the leading bottom corner of the work area,
// which for LTR languages is the bottom-left.
shelf->SetAlignment(ShelfAlignment::kBottom);
nudge_bounds = GetShownNudge(id)->GetWidget()->GetWindowBoundsInScreen();
EXPECT_EQ(nudge_bounds.x(), display_bounds.x());
EXPECT_EQ(nudge_bounds.bottom(), display_bounds.bottom() - shelf_size);
shelf->SetAlignment(ShelfAlignment::kLeft);
nudge_bounds = GetShownNudge(id)->GetWidget()->GetWindowBoundsInScreen();
EXPECT_EQ(nudge_bounds.x(), display_bounds.x() + shelf_size);
EXPECT_EQ(nudge_bounds.bottom(), display_bounds.bottom());
shelf->SetAlignment(ShelfAlignment::kRight);
nudge_bounds = GetShownNudge(id)->GetWidget()->GetWindowBoundsInScreen();
EXPECT_EQ(nudge_bounds.x(), display_bounds.x());
EXPECT_EQ(nudge_bounds.bottom(), display_bounds.bottom());
}
// Tests that a nudge without an anchor view is placed on the right on RTL.
TEST_F(AnchoredNudgeManagerImplTest, DefaultLocation_WithRTL) {
Shelf* shelf = GetPrimaryShelf();
display::Display primary_display = GetPrimaryDisplay();
gfx::Rect display_bounds = primary_display.bounds();
int shelf_size = ShelfConfig::Get()->shelf_size();
gfx::Rect nudge_bounds;
// Turn on RTL mode.
base::i18n::SetRTLForTesting(true);
base::RunLoop().RunUntilIdle();
EXPECT_TRUE(base::i18n::IsRTL());
// Show nudge on its default location by not providing an anchor view.
const std::string id("id");
auto nudge_data = CreateBaseNudgeData(id, /*anchor_view=*/nullptr);
GetAnchoredNudgeManager()->Show(nudge_data);
// The nudge should be shown on the leading bottom corner of the work area,
// which for RTL languages is the bottom-right.
shelf->SetAlignment(ShelfAlignment::kBottom);
nudge_bounds = GetShownNudge(id)->GetWidget()->GetWindowBoundsInScreen();
EXPECT_EQ(nudge_bounds.right(), display_bounds.right());
EXPECT_EQ(nudge_bounds.bottom(), display_bounds.bottom() - shelf_size);
shelf->SetAlignment(ShelfAlignment::kLeft);
nudge_bounds = GetShownNudge(id)->GetWidget()->GetWindowBoundsInScreen();
EXPECT_EQ(nudge_bounds.right(), display_bounds.right());
EXPECT_EQ(nudge_bounds.bottom(), display_bounds.bottom());
shelf->SetAlignment(ShelfAlignment::kRight);
nudge_bounds = GetShownNudge(id)->GetWidget()->GetWindowBoundsInScreen();
EXPECT_EQ(nudge_bounds.right(), display_bounds.right() - shelf_size);
EXPECT_EQ(nudge_bounds.bottom(), display_bounds.bottom());
// Turn off RTL mode.
base::i18n::SetRTLForTesting(false);
base::RunLoop().RunUntilIdle();
EXPECT_FALSE(base::i18n::IsRTL());
}
// Tests that a nudge without an anchor view updates its baseline based on the
// current hotseat state.
TEST_F(AnchoredNudgeManagerImplTest, DefaultLocation_WithHotseatShown) {
Shelf* shelf = GetPrimaryShelf();
HotseatWidget* hotseat = shelf->hotseat_widget();
display::Display primary_display = GetPrimaryDisplay();
gfx::Rect display_bounds = primary_display.bounds();
int shelf_size = ShelfConfig::Get()->shelf_size();
gfx::Rect nudge_bounds;
// Show nudge on its default location by not providing an anchor view.
const std::string id("id");
auto nudge_data = CreateBaseNudgeData(id, /*anchor_view=*/nullptr);
GetAnchoredNudgeManager()->Show(nudge_data);
// The nudge should be shown on the leading bottom corner of the work area.
nudge_bounds = GetShownNudge(id)->GetWidget()->GetWindowBoundsInScreen();
EXPECT_EQ(nudge_bounds.x(), display_bounds.x());
EXPECT_EQ(nudge_bounds.bottom(), display_bounds.bottom() - shelf_size);
// Test that the nudge updates its baseline when the hotseat is shown.
ash::TabletModeControllerTestApi().EnterTabletMode();
nudge_bounds = GetShownNudge(id)->GetWidget()->GetWindowBoundsInScreen();
EXPECT_EQ(hotseat->state(), HotseatState::kShownHomeLauncher);
EXPECT_EQ(nudge_bounds.bottom(),
hotseat->CalculateHotseatYInScreen(hotseat->state()));
}
// Tests that a nudge without an anchor view updates its baseline when the shelf
// hides itself.
TEST_F(AnchoredNudgeManagerImplTest, DefaultLocation_WithAutoHideShelf) {
Shelf* shelf = GetPrimaryShelf();
display::Display primary_display = GetPrimaryDisplay();
gfx::Rect display_bounds = primary_display.bounds();
gfx::Rect nudge_bounds;
// Show nudge on its default location by not providing an anchor view.
const std::string id("id");
auto nudge_data = CreateBaseNudgeData(id, /*anchor_view=*/nullptr);
GetAnchoredNudgeManager()->Show(nudge_data);
// The nudge should be shown on the leading bottom corner of the work area.
nudge_bounds = GetShownNudge(id)->GetWidget()->GetWindowBoundsInScreen();
EXPECT_EQ(nudge_bounds.x(), display_bounds.x());
EXPECT_EQ(nudge_bounds.bottom(),
display_bounds.bottom() - ShelfConfig::Get()->shelf_size());
// Test that the nudge updates its baseline when the shelf hides itself.
std::unique_ptr<aura::Window> window(
CreateTestWindowInShellWithBounds(gfx::Rect()));
shelf->SetAutoHideBehavior(ShelfAutoHideBehavior::kAlways);
nudge_bounds = GetShownNudge(id)->GetWidget()->GetWindowBoundsInScreen();
EXPECT_EQ(SHELF_AUTO_HIDE_HIDDEN, shelf->GetAutoHideState());
EXPECT_EQ(nudge_bounds.bottom(),
display_bounds.bottom() -
ShelfConfig::Get()->hidden_shelf_in_screen_portion());
}
// Tests that a nudge updates its location after zooming in/out the UI.
TEST_F(AnchoredNudgeManagerImplTest, DefaultLocation_Zoom) {
const int shelf_size = ShelfConfig::Get()->shelf_size();
gfx::Rect display_bounds;
gfx::Rect nudge_bounds;
// Show nudge on its default location by not providing an anchor view.
const std::string id("id");
auto nudge_data = CreateBaseNudgeData(id, /*anchor_view=*/nullptr);
GetAnchoredNudgeManager()->Show(nudge_data);
// Since no zoom factor has been set on the display, it should be 1.
const display::ManagedDisplayInfo& display =
display_manager()->GetDisplayInfo(GetPrimaryDisplay().id());
EXPECT_EQ(
display_manager()->GetDisplayForId(display.id()).device_scale_factor(),
1.f);
// The nudge should be shown on the bottom-left of the work area.
nudge_bounds = GetShownNudge(id)->GetWidget()->GetWindowBoundsInScreen();
display_bounds = GetPrimaryDisplay().bounds();
EXPECT_EQ(nudge_bounds.x(), display_bounds.x());
EXPECT_EQ(nudge_bounds.bottom(), display_bounds.bottom() - shelf_size);
// Set the device scale factor to 2.
constexpr float zoom_factor = 2.0f;
display_manager()->UpdateZoomFactor(display.id(), zoom_factor);
EXPECT_EQ(
display_manager()->GetDisplayForId(display.id()).device_scale_factor(),
zoom_factor);
// Nudge bounds should update accordingly.
nudge_bounds = GetShownNudge(id)->GetWidget()->GetWindowBoundsInScreen();
display_bounds = GetPrimaryDisplay().bounds();
EXPECT_EQ(nudge_bounds.x(), display_bounds.x());
EXPECT_EQ(nudge_bounds.bottom(), display_bounds.bottom() - shelf_size);
}
// Tests that attempting to show a nudge with an `id` that's in use cancels
// the existing nudge and replaces it with a new nudge.
TEST_F(AnchoredNudgeManagerImplTest, ShowNudge_NudgeWithIdAlreadyExists) {
std::unique_ptr<views::Widget> widget = CreateFramelessTestWidget();
auto* contents_view =
widget->SetContentsView(std::make_unique<views::View>());
// Set up nudge data contents.
const std::string id("id");
const std::u16string text = u"text";
auto* anchor_view =
contents_view->AddChildView(std::make_unique<views::View>());
auto nudge_data = CreateBaseNudgeData(id, anchor_view, text);
const std::u16string text_2 = u"text_2";
auto* anchor_view_2 =
contents_view->AddChildView(std::make_unique<views::View>());
auto nudge_data_2 = CreateBaseNudgeData(id, anchor_view_2, text_2);
// Show a nudge with some initial contents.
GetAnchoredNudgeManager()->Show(nudge_data);
// First nudge contents should be set.
AnchoredNudge* nudge = GetShownNudge(id);
ASSERT_TRUE(nudge);
EXPECT_EQ(text, GetNudgeBodyText(id));
EXPECT_EQ(anchor_view, nudge->GetAnchorView());
// Attempt to show a nudge with different contents but with the same id.
GetAnchoredNudgeManager()->Show(nudge_data_2);
// The previous nudge should be cancelled and replaced with the new nudge.
nudge = GetShownNudge(id);
ASSERT_TRUE(nudge);
EXPECT_EQ(text_2, GetNudgeBodyText(id));
EXPECT_EQ(anchor_view_2, nudge->GetAnchorView());
}
// Tests that a nudge is not created if its anchor view is not visible.
TEST_F(AnchoredNudgeManagerImplTest, ShowNudge_InvisibleAnchorView) {
std::unique_ptr<views::Widget> widget = CreateFramelessTestWidget();
// Set up nudge data contents.
const std::string id("id");
auto* anchor_view = widget->SetContentsView(std::make_unique<views::View>());
auto nudge_data = CreateBaseNudgeData(id, anchor_view);
// Set anchor view visibility to false.
anchor_view->SetVisible(false);
// Attempt to show nudge.
GetAnchoredNudgeManager()->Show(nudge_data);
// Anchor view is not visible, the nudge should not be created.
EXPECT_FALSE(GetShownNudge(id));
}
// Tests that a nudge is not created if its anchor view doesn't have a widget.
TEST_F(AnchoredNudgeManagerImplTest, ShowNudge_AnchorViewWithoutWidget) {
// Set up nudge data contents.
const std::string id("id");
auto contents_view = std::make_unique<views::View>();
auto* anchor_view =
contents_view->AddChildView(std::make_unique<views::View>());
auto nudge_data = CreateBaseNudgeData(id, anchor_view);
// Attempt to show nudge.
GetAnchoredNudgeManager()->Show(nudge_data);
// Anchor view does not have a widget, the nudge should not be created.
EXPECT_FALSE(GetShownNudge(id));
}
// Tests that a nudge is not created if its anchor view was deleted.
TEST_F(AnchoredNudgeManagerImplTest, ShowNudge_DeletedAnchorView) {
std::unique_ptr<views::Widget> widget = CreateFramelessTestWidget();
// Set up nudge data contents.
const std::string id("id");
auto contents_view = std::make_unique<views::View>();
auto* anchor_view =
contents_view->AddChildView(std::make_unique<views::View>());
widget->SetContentsView(contents_view.get());
auto nudge_data = CreateBaseNudgeData(id, anchor_view);
// Anchor view exists, the nudge should be created.
GetAnchoredNudgeManager()->Show(nudge_data);
EXPECT_TRUE(GetShownNudge(id));
// Delete the anchor view, the nudge should not be created.
contents_view->RemoveAllChildViews();
GetAnchoredNudgeManager()->Show(nudge_data);
EXPECT_FALSE(GetShownNudge(id));
}
// Tests that a nudge will not be shown if a `ScopedNudgePause` exists, and even
// if the `scoped_anchored_nudge_pause` gets destroyed, the nudge is dismissed
// and will not be saved.
TEST_F(AnchoredNudgeManagerImplTest, ShowNudge_ScopedNudgePause) {
std::unique_ptr<views::Widget> widget = CreateFramelessTestWidget();
// Set up nudge data contents.
const std::string id("id");
auto* anchor_view = widget->SetContentsView(std::make_unique<views::View>());
auto nudge_data = CreateBaseNudgeData(id, anchor_view);
// Set up a `ScopedNudgePause`.
auto scoped_anchored_nudge_pause =
GetAnchoredNudgeManager()->CreateScopedPause();
// Attempt to show nudge.
GetAnchoredNudgeManager()->Show(nudge_data);
// A `ScopedNudgePause` exists, the nudge should not be shown.
EXPECT_FALSE(GetShownNudge(id));
// Destroy the `ScopedNudgePause`, the nudge doesn't exist either.
scoped_anchored_nudge_pause.reset();
EXPECT_FALSE(GetShownNudge(id));
}
// Tests that if a `ScopedNudgePause` creates when a nudge is showing, the nudge
// will be dismissed immediately.
TEST_F(AnchoredNudgeManagerImplTest, CancelNudge_ScopedNudgePause) {
std::unique_ptr<views::Widget> widget = CreateFramelessTestWidget();
// Set up nudge data contents.
const std::string id("id");
auto* anchor_view = widget->SetContentsView(std::make_unique<views::View>());
auto nudge_data = CreateBaseNudgeData(id, anchor_view);
// The nudge will be shown.
GetAnchoredNudgeManager()->Show(nudge_data);
EXPECT_TRUE(GetShownNudge(id));
// After a `ScopedNudgePause` is created, the nudge will be closed
// immediately.
GetAnchoredNudgeManager()->CreateScopedPause();
EXPECT_FALSE(GetShownNudge(id));
}
// Tests that a nudge sets the appropriate arrow when it's set to be anchored to
// the shelf, and updates its arrow whenever the shelf alignment changes.
TEST_F(AnchoredNudgeManagerImplTest, NudgeAnchoredToShelf) {
std::unique_ptr<views::Widget> widget = CreateFramelessTestWidget();
// Set up nudge data contents.
const std::string id("id");
auto* anchor_view = widget->SetContentsView(std::make_unique<views::View>());
auto nudge_data = CreateBaseNudgeData(id, anchor_view);
// Make the nudge set its arrow based on the shelf's position.
nudge_data.anchored_to_shelf = true;
// Set shelf alignment to the left.
Shelf* shelf = GetPrimaryShelf();
EXPECT_EQ(shelf->alignment(), ShelfAlignment::kBottom);
EXPECT_EQ(shelf->GetVisibilityState(), SHELF_VISIBLE);
shelf->SetAlignment(ShelfAlignment::kLeft);
// Show a nudge, expect its arrow to be aligned with left shelf.
GetAnchoredNudgeManager()->Show(nudge_data);
EXPECT_TRUE(GetShownNudge(id));
EXPECT_EQ(GetShownNudge(id)->arrow(),
views::BubbleBorder::Arrow::LEFT_BOTTOM);
// Cancel the nudge, and show a new nudge with bottom shelf alignment.
GetAnchoredNudgeManager()->Cancel(id);
shelf->SetAlignment(ShelfAlignment::kBottom);
GetAnchoredNudgeManager()->Show(nudge_data);
EXPECT_EQ(GetShownNudge(id)->arrow(),
views::BubbleBorder::Arrow::BOTTOM_RIGHT);
// Change the shelf alignment to the right while the nudge is still open,
// nudge arrow should be updated.
shelf->SetAlignment(ShelfAlignment::kRight);
EXPECT_EQ(GetShownNudge(id)->arrow(),
views::BubbleBorder::Arrow::RIGHT_BOTTOM);
// Cancel the nudge and create a new nudge with an arrow that is not
// corner-anchored.
GetAnchoredNudgeManager()->Cancel(id);
nudge_data.arrow = views::BubbleBorder::Arrow::LEFT_CENTER;
GetAnchoredNudgeManager()->Show(nudge_data);
shelf->SetAlignment(ShelfAlignment::kLeft);
EXPECT_EQ(GetShownNudge(id)->arrow(),
views::BubbleBorder::Arrow::LEFT_CENTER);
shelf->SetAlignment(ShelfAlignment::kBottom);
EXPECT_EQ(GetShownNudge(id)->arrow(),
views::BubbleBorder::Arrow::BOTTOM_CENTER);
shelf->SetAlignment(ShelfAlignment::kRight);
EXPECT_EQ(GetShownNudge(id)->arrow(),
views::BubbleBorder::Arrow::RIGHT_CENTER);
}
// Tests that a nudge that is anchored to the shelf is not affected by shelf
// alignment changes of a display where the nudge does not exist.
TEST_F(AnchoredNudgeManagerImplTest,
NudgeAnchoredToShelf_WithASecondaryDisplay) {
// Add a secondary display.
UpdateDisplay("800x700,800x700");
RootWindowController* const secondary_root_window_controller =
Shell::GetRootWindowControllerWithDisplayId(GetSecondaryDisplay().id());
Shelf* shelf = GetPrimaryShelf();
// Set up nudge data contents.
const std::string id("id");
auto* anchor_view = shelf->status_area_widget()->unified_system_tray();
auto nudge_data = CreateBaseNudgeData(id, anchor_view);
// Make the nudge set its arrow based on the shelf's position.
nudge_data.anchored_to_shelf = true;
// Set shelf alignment to the left.
shelf->SetAlignment(ShelfAlignment::kLeft);
// Show a nudge, expect its arrow to be aligned with left shelf.
GetAnchoredNudgeManager()->Show(nudge_data);
EXPECT_TRUE(GetShownNudge(id));
EXPECT_EQ(views::BubbleBorder::Arrow::LEFT_BOTTOM,
GetShownNudge(id)->arrow());
// Test that changing the shelf alignment on the secondary display does not
// affect the nudge's arrow, since the nudge lives in the primary display.
secondary_root_window_controller->shelf()->SetAlignment(
ShelfAlignment::kBottom);
EXPECT_EQ(views::BubbleBorder::Arrow::LEFT_BOTTOM,
GetShownNudge(id)->arrow());
}
// Tests that a nudge that is anchored to the shelf maintains the shelf visible
// while the nudge is being shown and the shelf is on auto-hide.
TEST_F(AnchoredNudgeManagerImplTest, NudgeAnchoredToShelf_ShelfDoesNotHide) {
std::unique_ptr<views::Widget> widget = CreateFramelessTestWidget();
// Set up nudge data contents.
const std::string id("id");
auto* anchor_view = widget->SetContentsView(std::make_unique<views::View>());
auto nudge_data = CreateBaseNudgeData(id, anchor_view);
// Make the nudge maintain the shelf visible while it is showing.
nudge_data.anchored_to_shelf = true;
// Verify `shelf` is initially visible.
Shelf* shelf = GetPrimaryShelf();
EXPECT_TRUE(shelf->IsVisible());
// Set `shelf` to always auto-hide, it should not be visible.
shelf->SetAutoHideBehavior(ShelfAutoHideBehavior::kAlways);
EXPECT_FALSE(shelf->IsVisible());
// Show the nudge, `shelf` should be made visible while nudge is showing.
GetAnchoredNudgeManager()->Show(nudge_data);
EXPECT_TRUE(shelf->IsVisible());
// Cancel the nudge, `shelf` should be hidden again.
GetAnchoredNudgeManager()->Cancel(id);
EXPECT_FALSE(shelf->IsVisible());
}
// Tests that a nudge closes if its anchor view is made invisible.
TEST_F(AnchoredNudgeManagerImplTest, NudgeCloses_WhenAnchorViewIsHiding) {
std::unique_ptr<views::Widget> widget = CreateFramelessTestWidget();
// Set up nudge data contents.
const std::string id("id");
auto* anchor_view = widget->SetContentsView(std::make_unique<views::View>());
auto nudge_data = CreateBaseNudgeData(id, anchor_view);
// Show a nudge.
GetAnchoredNudgeManager()->Show(nudge_data);
EXPECT_TRUE(GetShownNudge(id));
// Set the anchor view visibility to false, the nudge should have closed.
anchor_view->SetVisible(false);
EXPECT_FALSE(GetShownNudge(id));
// Set the anchor view visibility to true, the nudge should not reappear.
anchor_view->SetVisible(true);
EXPECT_FALSE(GetShownNudge(id));
}
// Tests that a nudge closes if its anchor view is deleted.
TEST_F(AnchoredNudgeManagerImplTest, NudgeCloses_WhenAnchorViewIsDeleting) {
std::unique_ptr<views::Widget> widget = CreateFramelessTestWidget();
// Set up nudge data contents.
const std::string id("id");
auto* contents_view =
widget->SetContentsView(std::make_unique<views::View>());
auto* anchor_view =
contents_view->AddChildView(std::make_unique<views::View>());
auto nudge_data = CreateBaseNudgeData(id, anchor_view);
// Show a nudge.
GetAnchoredNudgeManager()->Show(nudge_data);
EXPECT_TRUE(GetShownNudge(id));
// Delete the anchor view, the nudge should have closed.
contents_view->RemoveAllChildViews();
EXPECT_FALSE(GetShownNudge(id));
}
// Tests that a nudge whose anchor view is a part of a secondary display closes
// when that display is removed.
TEST_F(AnchoredNudgeManagerImplTest,
NudgeCloses_WhenAnchorViewIsDeletingOnSecondaryDisplay) {
// Set up two displays.
UpdateDisplay("800x700,800x700");
RootWindowController* const secondary_root_window_controller =
Shell::GetRootWindowControllerWithDisplayId(GetSecondaryDisplay().id());
// Set up nudge data contents. The anchor view is a child of the secondary
// root window controller, so it will be deleted if the display is removed.
const std::string id("id");
auto* anchor_view = secondary_root_window_controller->shelf()
->status_area_widget()
->unified_system_tray();
auto nudge_data = CreateBaseNudgeData(id, anchor_view);
// Show the nudge in the secondary display.
GetAnchoredNudgeManager()->Show(nudge_data);
EXPECT_TRUE(GetShownNudge(id));
// Remove the secondary display, which deletes the anchor view.
UpdateDisplay("800x700");
// The anchor view was deleted, the nudge should have closed.
EXPECT_FALSE(GetShownNudge(id));
}
// Tests that a nudge closes if its anchor view widget is hiding.
TEST_F(AnchoredNudgeManagerImplTest, NudgeCloses_WhenAnchorViewWidgetIsHiding) {
std::unique_ptr<views::Widget> widget = CreateFramelessTestWidget();
// Set up nudge data contents.
const std::string id("id");
auto* anchor_view = widget->SetContentsView(std::make_unique<views::View>());
auto nudge_data = CreateBaseNudgeData(id, anchor_view);
// Show a nudge.
GetAnchoredNudgeManager()->Show(nudge_data);
EXPECT_TRUE(GetShownNudge(id));
// Hide the anchor view widget, the nudge should have closed.
widget->Hide();
EXPECT_FALSE(GetShownNudge(id));
// Show the anchor view widget, the nudge should not reappear.
widget->ShowInactive();
EXPECT_FALSE(GetShownNudge(id));
}
// Tests that a nudge is properly destroyed on shutdown.
TEST_F(AnchoredNudgeManagerImplTest, NudgeCloses_OnShutdown) {
std::unique_ptr<views::Widget> widget = CreateFramelessTestWidget();
// Set up nudge data contents.
const std::string id("id");
auto* anchor_view = widget->SetContentsView(std::make_unique<views::View>());
auto nudge_data = CreateBaseNudgeData(id, anchor_view);
// Show a nudge.
GetAnchoredNudgeManager()->Show(nudge_data);
EXPECT_TRUE(GetShownNudge(id));
// Nudge is left open, no crash.
}
// Tests that nudges expire after their dismiss timer reaches its end.
TEST_F(AnchoredNudgeManagerImplTest, NudgeCloses_WhenDismissTimerExpires) {
std::unique_ptr<views::Widget> widget = CreateFramelessTestWidget();
// Set up nudge data contents.
const std::string id("id");
auto* anchor_view = widget->SetContentsView(std::make_unique<views::View>());
auto nudge_data = CreateBaseNudgeData(id, anchor_view);
// Show a nudge with default duration.
GetAnchoredNudgeManager()->Show(nudge_data);
EXPECT_TRUE(GetShownNudge(id));
// The nudge should expire after `kNudgeDefaultDuration` has passed.
task_environment()->FastForwardBy(
AnchoredNudgeManagerImpl::kNudgeDefaultDuration + base::Seconds(1));
EXPECT_FALSE(GetShownNudge(id));
// Test that a nudge with medium duration lasts longer than
// `kNudgeDefaultDuration` but expires after `kNudgeMediumDuration`.
nudge_data.duration = NudgeDuration::kMediumDuration;
GetAnchoredNudgeManager()->Show(nudge_data);
EXPECT_TRUE(GetShownNudge(id));
task_environment()->FastForwardBy(
AnchoredNudgeManagerImpl::kNudgeDefaultDuration + base::Seconds(1));
EXPECT_TRUE(GetShownNudge(id));
task_environment()->FastForwardBy(
AnchoredNudgeManagerImpl::kNudgeMediumDuration);
EXPECT_FALSE(GetShownNudge(id));
// Test that a nudge with long duration lasts longer than
// `kNudgeMediumDuration` but expires after `kNudgeLongDuration`.
nudge_data.duration = NudgeDuration::kLongDuration;
GetAnchoredNudgeManager()->Show(nudge_data);
EXPECT_TRUE(GetShownNudge(id));
task_environment()->FastForwardBy(
AnchoredNudgeManagerImpl::kNudgeMediumDuration + base::Seconds(1));
EXPECT_TRUE(GetShownNudge(id));
task_environment()->FastForwardBy(
AnchoredNudgeManagerImpl::kNudgeLongDuration);
EXPECT_FALSE(GetShownNudge(id));
}
// Tests that nudge's default duration is updated to medium duration if the
// nudge has a long body text or a button.
TEST_F(AnchoredNudgeManagerImplTest, NudgeDefaultDurationIsUpdated) {
std::unique_ptr<views::Widget> widget = CreateFramelessTestWidget();
// Set up nudge data contents.
const std::string id("id");
const std::u16string long_body_text =
u"This is just a body text that has more than sixty characters.";
const std::u16string primary_button_text = u"first";
auto* anchor_view = widget->SetContentsView(std::make_unique<views::View>());
auto nudge_data = CreateBaseNudgeData(id, anchor_view);
// Show a nudge with default duration.
GetAnchoredNudgeManager()->Show(nudge_data);
EXPECT_TRUE(GetShownNudge(id));
// The nudge should expire after `kNudgeDefaultDuration`.
task_environment()->FastForwardBy(
AnchoredNudgeManagerImpl::kNudgeDefaultDuration + base::Seconds(1));
EXPECT_FALSE(GetShownNudge(id));
// Add a long body text and show the nudge again.
ASSERT_GE(static_cast<int>(long_body_text.length()),
AnchoredNudgeManagerImpl::kLongBodyTextLength);
nudge_data.body_text = long_body_text;
GetAnchoredNudgeManager()->Show(nudge_data);
EXPECT_TRUE(GetShownNudge(id));
// The nudge should not expire after `kNudgeDefaultDuration` has passed, but
// will expire after `kNudgeMediumDuration` since it has a long body text.
task_environment()->FastForwardBy(
AnchoredNudgeManagerImpl::kNudgeDefaultDuration + base::Seconds(1));
EXPECT_TRUE(GetShownNudge(id));
task_environment()->FastForwardBy(
AnchoredNudgeManagerImpl::kNudgeMediumDuration);
EXPECT_FALSE(GetShownNudge(id));
// Clear body text, add a button and show the nudge again.
nudge_data.body_text = std::u16string();
nudge_data.primary_button_text = primary_button_text;
GetAnchoredNudgeManager()->Show(nudge_data);
EXPECT_TRUE(GetShownNudge(id));
// The nudge should not expire after `kNudgeDefaultDuration` has passed, but
// will expire after `kNudgeMediumDuration` since it has a button.
task_environment()->FastForwardBy(
AnchoredNudgeManagerImpl::kNudgeDefaultDuration + base::Seconds(1));
EXPECT_TRUE(GetShownNudge(id));
task_environment()->FastForwardBy(
AnchoredNudgeManagerImpl::kNudgeMediumDuration);
EXPECT_FALSE(GetShownNudge(id));
// Set the duration to long and show the nudge again.
nudge_data.duration = NudgeDuration::kLongDuration;
GetAnchoredNudgeManager()->Show(nudge_data);
EXPECT_TRUE(GetShownNudge(id));
// Ensure the duration doesn't update back to medium if it was set to long.
task_environment()->FastForwardBy(
AnchoredNudgeManagerImpl::kNudgeMediumDuration + base::Seconds(1));
EXPECT_TRUE(GetShownNudge(id));
task_environment()->FastForwardBy(
AnchoredNudgeManagerImpl::kNudgeLongDuration);
EXPECT_FALSE(GetShownNudge(id));
}
// Tests that nudges are destroyed on session state changes.
TEST_F(AnchoredNudgeManagerImplTest, NudgeCloses_OnSessionStateChanged) {
std::unique_ptr<views::Widget> widget = CreateFramelessTestWidget();
// Set up nudge data contents.
const std::string id("id");
auto* anchor_view = widget->SetContentsView(std::make_unique<views::View>());
auto nudge_data = CreateBaseNudgeData(id, anchor_view);
// Show a nudge.
GetAnchoredNudgeManager()->Show(nudge_data);
EXPECT_TRUE(GetShownNudge(id));
// Lock screen, nudge should have closed.
SetLockedState(true);
EXPECT_FALSE(GetShownNudge(id));
// Show a nudge in the locked state.
GetAnchoredNudgeManager()->Show(nudge_data);
EXPECT_TRUE(GetShownNudge(id));
// Unlock screen, nudge should have closed.
SetLockedState(false);
EXPECT_FALSE(GetShownNudge(id));
}
// Tests that the nudge widget closes after its hide animation is completed.
TEST_F(AnchoredNudgeManagerImplTest, NudgeCloses_OnHideAnimationComplete) {
// Set animations to last a non-zero, faster than normal duration, since the
// regular duration may last longer in tests and cause flakiness.
ui::ScopedAnimationDurationScaleMode test_duration_mode(
ui::ScopedAnimationDurationScaleMode::NON_ZERO_DURATION);
std::unique_ptr<views::Widget> widget = CreateFramelessTestWidget();
// Set up nudge data contents.
const std::string id("id");
auto* anchor_view = widget->SetContentsView(std::make_unique<views::View>());
auto nudge_data = CreateBaseNudgeData(id, anchor_view);
// Show a nudge.
GetAnchoredNudgeManager()->Show(nudge_data);
EXPECT_TRUE(GetShownNudge(id));
// Cancel will trigger the hide animation, the nudge should still exist.
GetAnchoredNudgeManager()->Cancel(id);
EXPECT_TRUE(GetShownNudge(id));
// Attempt cancelling the nudge while hide animation is in progress, these
// calls should be ignored.
GetAnchoredNudgeManager()->Cancel(id);
GetAnchoredNudgeManager()->Cancel(id);
// Fast forward to complete hide animation, the nudge should have closed.
task_environment()->FastForwardBy(kAnimationSettleDownDuration);
ASSERT_FALSE(GetShownNudge(id));
}
TEST_F(AnchoredNudgeManagerImplTest, NudgeHideAnimationInterrupted_OnShutdown) {
// Set animations to last their normal duration.
ui::ScopedAnimationDurationScaleMode test_duration_mode(
ui::ScopedAnimationDurationScaleMode::NORMAL_DURATION);
std::unique_ptr<views::Widget> widget = CreateFramelessTestWidget();
// Set up nudge data contents.
const std::string id("id");
auto* anchor_view = widget->SetContentsView(std::make_unique<views::View>());
auto nudge_data = CreateBaseNudgeData(id, anchor_view);
// Show a nudge.
GetAnchoredNudgeManager()->Show(nudge_data);
EXPECT_TRUE(GetShownNudge(id));
// Cancel will trigger the hide animation, the nudge should still exist.
GetAnchoredNudgeManager()->Cancel(id);
EXPECT_TRUE(GetShownNudge(id));
// Nudge animation is interrupted on shutdown, no crash.
}
TEST_F(AnchoredNudgeManagerImplTest,
NudgeHideAnimationInterrupted_OnNudgeReplaced) {
// Set animations to last their normal duration.
ui::ScopedAnimationDurationScaleMode test_duration_mode(
ui::ScopedAnimationDurationScaleMode::NORMAL_DURATION);
std::unique_ptr<views::Widget> widget = CreateFramelessTestWidget();
// Set up nudge data contents.
const std::string id("id");
auto* anchor_view = widget->SetContentsView(std::make_unique<views::View>());
auto nudge_data = CreateBaseNudgeData(id, anchor_view);
// Show a nudge.
GetAnchoredNudgeManager()->Show(nudge_data);
EXPECT_TRUE(GetShownNudge(id));
// Cancel will trigger the hide animation, the nudge should still exist.
GetAnchoredNudgeManager()->Cancel(id);
EXPECT_TRUE(GetShownNudge(id));
// Attempt showing the same nudge again immediately. The hide animation should
// be interrupted, and the nudge will be replaced.
GetAnchoredNudgeManager()->Show(nudge_data);
task_environment()->FastForwardBy(kAnimationSettleDownDuration);
EXPECT_TRUE(GetShownNudge(id));
}
TEST_F(AnchoredNudgeManagerImplTest,
NudgeHideAnimationInterrupted_OnScopedPauseAdded) {
// Set animations to last their normal duration.
ui::ScopedAnimationDurationScaleMode test_duration_mode(
ui::ScopedAnimationDurationScaleMode::NORMAL_DURATION);
std::unique_ptr<views::Widget> widget = CreateFramelessTestWidget();
// Set up nudge data contents.
const std::string id("id");
auto* anchor_view = widget->SetContentsView(std::make_unique<views::View>());
auto nudge_data = CreateBaseNudgeData(id, anchor_view);
// Show a nudge.
GetAnchoredNudgeManager()->Show(nudge_data);
EXPECT_TRUE(GetShownNudge(id));
// Cancel will trigger the hide animation, the nudge should still exist.
GetAnchoredNudgeManager()->Cancel(id);
EXPECT_TRUE(GetShownNudge(id));
// Create a scoped nudge pause right after, which will close the nudge
// immediately interrupting its hide animation.
GetAnchoredNudgeManager()->CreateScopedPause();
EXPECT_FALSE(GetShownNudge(id));
}
// Tests that the dismiss timer is paused on hover so the nudge won't close.
TEST_F(AnchoredNudgeManagerImplTest, NudgePersists_OnHover) {
std::unique_ptr<views::Widget> widget = CreateFramelessTestWidget();
// Set up nudge data contents.
const std::string id("id");
auto* anchor_view = widget->SetContentsView(std::make_unique<views::View>());
auto nudge_data = CreateBaseNudgeData(id, anchor_view);
// Show a nudge.
GetAnchoredNudgeManager()->Show(nudge_data);
AnchoredNudge* nudge = GetShownNudge(id);
EXPECT_TRUE(nudge);
// Wait for half of the nudge's duration.
task_environment()->FastForwardBy(
AnchoredNudgeManagerImpl::kNudgeDefaultDuration / 2);
// Hover on the nudge and wait for its full duration. It should persist.
GetEventGenerator()->MoveMouseTo(
GetShownNudge(id)->GetBoundsInScreen().CenterPoint());
EXPECT_TRUE(nudge->IsMouseHovered());
task_environment()->FastForwardBy(
AnchoredNudgeManagerImpl::kNudgeDefaultDuration);
EXPECT_TRUE(nudge);
// Hover out of the nudge and wait its duration. It should be dismissed.
GetEventGenerator()->MoveMouseTo(gfx::Point(-100, -100));
EXPECT_FALSE(nudge->IsMouseHovered());
task_environment()->FastForwardBy(
AnchoredNudgeManagerImpl::kNudgeDefaultDuration / 2 + base::Seconds(1));
EXPECT_FALSE(GetShownNudge(id));
}
// Tests that the dismiss timer is paused when one of the nudge's children is
// focused so the nudge won't close.
TEST_F(AnchoredNudgeManagerImplTest, NudgePersists_OnFocus) {
std::unique_ptr<views::Widget> widget = CreateFramelessTestWidget();
// Set up nudge with a button so the nudge has a focusable child.
const std::string id("id");
auto nudge_data = CreateBaseNudgeData(id, /*anchor_view=*/nullptr);
nudge_data.primary_button_text = u"button";
// Show a nudge with a button.
GetAnchoredNudgeManager()->Show(nudge_data);
ASSERT_TRUE(GetShownNudge(id));
auto* button = GetNudgePrimaryButton(id);
ASSERT_TRUE(button);
// Focus on the nudge's button and wait for its full duration times two. It
// should persist.
button->RequestFocus();
EXPECT_TRUE(button->HasFocus());
task_environment()->FastForwardBy(
AnchoredNudgeManagerImpl::kNudgeDefaultDuration * 2);
EXPECT_TRUE(GetShownNudge(id));
// Focus out of the nudge and wait its full duration times two. It should be
// dismissed.
button->GetFocusManager()->ClearFocus();
EXPECT_FALSE(button->HasFocus());
task_environment()->FastForwardBy(
AnchoredNudgeManagerImpl::kNudgeDefaultDuration * 2);
EXPECT_FALSE(GetShownNudge(id));
}
// Tests that attempting to cancel a nudge with an invalid `id` should not
// have any effects.
TEST_F(AnchoredNudgeManagerImplTest, CancelNudgeWhichDoesNotExist) {
std::unique_ptr<views::Widget> widget = CreateFramelessTestWidget();
// Set up nudge data contents.
const std::string id("id");
const std::string id_2("id_2");
auto* anchor_view = widget->SetContentsView(std::make_unique<views::View>());
auto nudge_data = CreateBaseNudgeData(id, anchor_view);
// Show a nudge.
GetAnchoredNudgeManager()->Show(nudge_data);
EXPECT_TRUE(GetShownNudge(id));
// Attempt to cancel nudge with an `id` that does not exist. Should not have
// any effect.
CancelNudge(id_2);
EXPECT_TRUE(GetShownNudge(id));
// Cancel the shown nudge with its valid `id`.
CancelNudge(id);
EXPECT_FALSE(GetShownNudge(id));
// Attempt to cancel the same nudge again. Should not have any effect.
CancelNudge(id);
EXPECT_FALSE(GetShownNudge(id));
}
TEST_F(AnchoredNudgeManagerImplTest, ShownCountMetric) {
base::HistogramTester histogram_tester;
std::unique_ptr<views::Widget> widget = CreateFramelessTestWidget();
// Set up nudge data contents.
const std::string id("id");
auto* anchor_view = widget->SetContentsView(std::make_unique<views::View>());
auto nudge_data = CreateBaseNudgeData(id, anchor_view);
histogram_tester.ExpectBucketCount(kNudgeShownCount, kTestCatalogName, 0);
GetAnchoredNudgeManager()->Show(nudge_data);
histogram_tester.ExpectBucketCount(kNudgeShownCount, kTestCatalogName, 1);
GetAnchoredNudgeManager()->Show(nudge_data);
GetAnchoredNudgeManager()->Show(nudge_data);
histogram_tester.ExpectBucketCount(kNudgeShownCount, kTestCatalogName, 3);
}
TEST_F(AnchoredNudgeManagerImplTest, TimeToActionMetric) {
base::HistogramTester histogram_tester;
GetAnchoredNudgeManager()->ResetNudgeRegistryForTesting();
std::unique_ptr<views::Widget> widget = CreateFramelessTestWidget();
// Set up nudge data contents.
const std::string id("id");
auto* anchor_view = widget->SetContentsView(std::make_unique<views::View>());
auto nudge_data = CreateBaseNudgeData(id, anchor_view);
// Metric is not recorded if the nudge has not been shown.
GetAnchoredNudgeManager()->MaybeRecordNudgeAction(kTestCatalogName);
histogram_tester.ExpectBucketCount(kNudgeTimeToActionWithin1m,
kTestCatalogName, 0);
// Metric is recorded if the action is performed after the nudge was shown.
GetAnchoredNudgeManager()->Show(nudge_data);
task_environment()->FastForwardBy(base::Seconds(1));
GetAnchoredNudgeManager()->MaybeRecordNudgeAction(kTestCatalogName);
histogram_tester.ExpectBucketCount(kNudgeTimeToActionWithin1m,
kTestCatalogName, 1);
// Metric is not recorded if the nudge action is performed again without
// another nudge being shown.
GetAnchoredNudgeManager()->MaybeRecordNudgeAction(kTestCatalogName);
histogram_tester.ExpectBucketCount(kNudgeTimeToActionWithin1m,
kTestCatalogName, 1);
// Metric is recorded with the appropriate time range after showing nudge
// again and waiting enough time to fall into the "Within1h" time bucket.
GetAnchoredNudgeManager()->Show(nudge_data);
task_environment()->FastForwardBy(base::Minutes(2));
GetAnchoredNudgeManager()->MaybeRecordNudgeAction(kTestCatalogName);
histogram_tester.ExpectBucketCount(kNudgeTimeToActionWithin1h,
kTestCatalogName, 1);
// Metric is not recorded if the nudge action is performed again without
// another nudge being shown.
GetAnchoredNudgeManager()->MaybeRecordNudgeAction(kTestCatalogName);
histogram_tester.ExpectBucketCount(kNudgeTimeToActionWithin1h,
kTestCatalogName, 1);
// Metric is recorded with the appropriate time range after showing nudge
// again and waiting enough time to fall into the "WithinSession" time bucket.
GetAnchoredNudgeManager()->Show(nudge_data);
task_environment()->FastForwardBy(base::Hours(2));
GetAnchoredNudgeManager()->MaybeRecordNudgeAction(kTestCatalogName);
histogram_tester.ExpectBucketCount(kNudgeTimeToActionWithinSession,
kTestCatalogName, 1);
// Metric is not recorded if the nudge action is performed again without
// another nudge being shown.
GetAnchoredNudgeManager()->MaybeRecordNudgeAction(kTestCatalogName);
histogram_tester.ExpectBucketCount(kNudgeTimeToActionWithinSession,
kTestCatalogName, 1);
}
// Tests that a nudge is parented to its anchor view, which has a widget.
TEST_F(AnchoredNudgeManagerImplTest, SetParent_AnchorViewWithWidget) {
std::unique_ptr<views::Widget> widget = CreateFramelessTestWidget();
// Set up nudge data contents.
const std::string id("id");
auto contents_view = std::make_unique<views::View>();
auto* anchor_view =
contents_view->AddChildView(std::make_unique<views::View>());
widget->SetContentsView(contents_view.get());
auto nudge_data = CreateBaseNudgeData(id, anchor_view);
nudge_data.set_anchor_view_as_parent = true;
// Anchor view exists, the nudge should be created and parented by it.
GetAnchoredNudgeManager()->Show(nudge_data);
EXPECT_TRUE(GetShownNudge(id));
EXPECT_EQ(GetNudgeIfShown(id)->GetWidget()->GetNativeWindow()->parent(),
anchor_view->GetWidget()->GetNativeView()->parent());
}
// Tests that a nudge is not parented to its anchor view if
// `set_anchor_view_as_parent` is not set to true.
TEST_F(AnchoredNudgeManagerImplTest, NotSetParent_AnchorViewWithWidget) {
std::unique_ptr<views::Widget> widget = CreateFramelessTestWidget();
// Set up nudge data contents.
const std::string id("id");
auto contents_view = std::make_unique<views::View>();
auto* anchor_view =
contents_view->AddChildView(std::make_unique<views::View>());
widget->SetContentsView(contents_view.get());
auto nudge_data = CreateBaseNudgeData(id, anchor_view);
nudge_data.set_anchor_view_as_parent = false;
// Anchor view exists, the nudge should be created but not parented by it.
GetAnchoredNudgeManager()->Show(nudge_data);
EXPECT_TRUE(GetShownNudge(id));
EXPECT_NE(GetNudgeIfShown(id)->GetWidget()->GetNativeWindow()->parent(),
anchor_view->GetWidget()->GetNativeView()->parent());
}
// Tests that a nudge is not created if its anchor view doesn't have a widget
// but `set_anchor_view_as_parent` is set to true.
TEST_F(AnchoredNudgeManagerImplTest, SetParent_AnchorViewWithoutWidget) {
// Set up nudge data contents.
const std::string id("id");
auto contents_view = std::make_unique<views::View>();
auto* anchor_view =
contents_view->AddChildView(std::make_unique<views::View>());
auto nudge_data = CreateBaseNudgeData(id, anchor_view);
nudge_data.set_anchor_view_as_parent = true;
// Attempt to show nudge.
GetAnchoredNudgeManager()->Show(nudge_data);
// Anchor view does not have a widget, the nudge should not be created.
EXPECT_FALSE(GetShownNudge(id));
}
// Tests that a nudge receives focus when it has buttons, and skips focus
// traversal when it has no buttons.
TEST_F(AnchoredNudgeManagerImplTest, FocusTraversable) {
std::unique_ptr<views::Widget> widget = CreateFramelessTestWidget();
// Set widget contents.
views::View* view1;
views::View* view2;
views::View* view3;
widget->SetContentsView(
views::Builder<views::FlexLayoutView>()
.AddChildren(
views::Builder<views::LabelButton>()
.CopyAddressTo(&view1)
.SetFocusBehavior(views::View::FocusBehavior::ALWAYS),
views::Builder<views::LabelButton>()
.CopyAddressTo(&view2)
.SetFocusBehavior(views::View::FocusBehavior::ALWAYS),
views::Builder<views::LabelButton>()
.CopyAddressTo(&view3)
.SetFocusBehavior(views::View::FocusBehavior::ALWAYS))
.Build());
// Setup a nudge without buttons and set `view2` as the anchor view.
const std::string id("id");
auto nudge_data = CreateBaseNudgeData(id, view2);
GetAnchoredNudgeManager()->Show(nudge_data);
// Focus traversal should skip the nudge since it has no buttons.
PressTab();
EXPECT_EQ(widget->GetFocusManager()->GetFocusedView(), view1);
PressTab();
EXPECT_EQ(widget->GetFocusManager()->GetFocusedView(), view2);
PressTab();
EXPECT_EQ(widget->GetFocusManager()->GetFocusedView(), view3);
// When a nudge has buttons, focus traversal should go into them and cycle
// until the nudge is dismissed.
nudge_data.primary_button_text = u"button";
nudge_data.secondary_button_text = u"button";
GetAnchoredNudgeManager()->Show(nudge_data);
PressTab();
EXPECT_EQ(widget->GetFocusManager()->GetFocusedView(), view1);
PressTab();
EXPECT_EQ(widget->GetFocusManager()->GetFocusedView(), view2);
PressTab();
auto* nudge_widget = GetShownNudge(id)->GetWidget();
EXPECT_EQ(nudge_widget->GetFocusManager()->GetFocusedView(),
GetNudgeSecondaryButton(id));
PressTab();
EXPECT_EQ(nudge_widget->GetFocusManager()->GetFocusedView(),
GetNudgePrimaryButton(id));
PressTab();
EXPECT_EQ(nudge_widget->GetFocusManager()->GetFocusedView(),
GetNudgeSecondaryButton(id));
GetAnchoredNudgeManager()->Cancel(id);
EXPECT_EQ(widget->GetFocusManager()->GetFocusedView(), view2);
}
// Tests that a nudge is anchored at the bottom left corner of its anchor
// widget.
TEST_F(AnchoredNudgeManagerImplTest, AnchorInsideWidget_BottomLeft) {
std::unique_ptr<views::Widget> anchor_widget =
CreateTestWidget(views::Widget::InitParams::CLIENT_OWNS_WIDGET);
anchor_widget->SetBounds(
gfx::Rect(gfx::Point(100, 100), gfx::Size(300, 200)));
// Set up nudge data contents.
const std::string id("id");
auto nudge_data = CreateBaseNudgeData(id, /*anchor_view=*/nullptr);
nudge_data.anchor_widget = anchor_widget.get();
nudge_data.arrow = views::BubbleBorder::Arrow::BOTTOM_LEFT;
// `anchor_widget` exists, will anchor inside the widget.
GetAnchoredNudgeManager()->Show(nudge_data);
auto* nudge = GetShownNudge(id);
EXPECT_TRUE(nudge);
auto* nudge_widget = nudge->GetWidget();
EXPECT_TRUE(nudge_widget);
auto nudge_bounds = nudge_widget->GetWindowBoundsInScreen();
auto anchor_widget_bounds = anchor_widget->GetWindowBoundsInScreen();
// Compare the bounds alignment with the `kBubbleBorderInsets`.
EXPECT_EQ(nudge_bounds.bottom_left(),
anchor_widget_bounds.bottom_left() +
gfx::Vector2d(kBubbleBorderInsets.left(),
-kBubbleBorderInsets.bottom()));
}
// Tests that a nudge is anchored at the bottom right corner of its anchor
// widget.
TEST_F(AnchoredNudgeManagerImplTest, AnchorInsideWidget_BottomRight) {
std::unique_ptr<views::Widget> anchor_widget =
CreateTestWidget(views::Widget::InitParams::CLIENT_OWNS_WIDGET);
anchor_widget->SetBounds(
gfx::Rect(gfx::Point(100, 100), gfx::Size(300, 200)));
// Set up nudge data contents.
const std::string id("id");
auto nudge_data = CreateBaseNudgeData(id, /*anchor_view=*/nullptr);
nudge_data.anchor_widget = anchor_widget.get();
nudge_data.arrow = views::BubbleBorder::Arrow::BOTTOM_RIGHT;
// `anchor_widget` exists, will anchor inside the widget.
GetAnchoredNudgeManager()->Show(nudge_data);
auto* nudge = GetShownNudge(id);
EXPECT_TRUE(nudge);
auto* nudge_widget = nudge->GetWidget();
EXPECT_TRUE(nudge_widget);
auto nudge_bounds = nudge_widget->GetWindowBoundsInScreen();
auto anchor_widget_bounds = anchor_widget->GetWindowBoundsInScreen();
// Compare the bounds alignment with the `kBubbleBorderInsets`.
EXPECT_EQ(nudge_bounds.bottom_right(),
anchor_widget_bounds.bottom_right() +
gfx::Vector2d(-kBubbleBorderInsets.right(),
-kBubbleBorderInsets.bottom()));
}
// Tests that a nudge with an anchor widget is placed on the right on RTL.
TEST_F(AnchoredNudgeManagerImplTest, AnchorInsideWidget_WithRTL) {
// Turn on RTL mode.
base::i18n::SetRTLForTesting(true);
base::RunLoop().RunUntilIdle();
EXPECT_TRUE(base::i18n::IsRTL());
std::unique_ptr<views::Widget> anchor_widget =
CreateTestWidget(views::Widget::InitParams::CLIENT_OWNS_WIDGET);
anchor_widget->SetBounds(
gfx::Rect(gfx::Point(100, 100), gfx::Size(300, 200)));
// Set up nudge data contents.
const std::string id("id");
auto nudge_data = CreateBaseNudgeData(id, /*anchor_view=*/nullptr);
nudge_data.anchor_widget = anchor_widget.get();
nudge_data.arrow = views::BubbleBorder::Arrow::BOTTOM_LEFT;
// `anchor_widget` exists, will anchor inside the widget.
GetAnchoredNudgeManager()->Show(nudge_data);
auto* nudge = GetShownNudge(id);
EXPECT_TRUE(nudge);
auto* nudge_widget = nudge->GetWidget();
EXPECT_TRUE(nudge_widget);
auto nudge_bounds = nudge_widget->GetWindowBoundsInScreen();
auto anchor_widget_bounds = anchor_widget->GetWindowBoundsInScreen();
// Compare the bounds alignment with the `kBubbleBorderInsets`.
// The nudge should be shown on the leading bottom corner of the
// `anchor_widget`, which for RTL languages is the bottom-right.
EXPECT_EQ(nudge_bounds.bottom_right(),
anchor_widget_bounds.bottom_right() +
gfx::Vector2d(-kBubbleBorderInsets.right(),
-kBubbleBorderInsets.bottom()));
// Turn off RTL mode.
base::i18n::SetRTLForTesting(false);
base::RunLoop().RunUntilIdle();
EXPECT_FALSE(base::i18n::IsRTL());
}
// Tests that a nudge is anchored at the bottom left corner of its anchor
// widget in the tablet mode.
TEST_F(AnchoredNudgeManagerImplTest, AnchorInsideWidget_TabletMode) {
ash::TabletModeControllerTestApi().EnterTabletMode();
std::unique_ptr<views::Widget> anchor_widget =
CreateTestWidget(views::Widget::InitParams::CLIENT_OWNS_WIDGET);
anchor_widget->SetBounds(
gfx::Rect(gfx::Point(100, 100), gfx::Size(300, 200)));
// Set up nudge data contents.
const std::string id("id");
auto nudge_data = CreateBaseNudgeData(id, /*anchor_view=*/nullptr);
nudge_data.anchor_widget = anchor_widget.get();
nudge_data.arrow = views::BubbleBorder::Arrow::BOTTOM_LEFT;
// `anchor_widget` exists, will anchor inside the widget.
GetAnchoredNudgeManager()->Show(nudge_data);
auto* nudge = GetShownNudge(id);
EXPECT_TRUE(nudge);
auto* nudge_widget = nudge->GetWidget();
EXPECT_TRUE(nudge_widget);
auto nudge_bounds = nudge_widget->GetWindowBoundsInScreen();
auto anchor_widget_bounds = anchor_widget->GetWindowBoundsInScreen();
// Compare the bounds alignment with the `kBubbleBorderInsets`.
EXPECT_EQ(nudge_bounds.bottom_left(),
anchor_widget_bounds.bottom_left() +
gfx::Vector2d(kBubbleBorderInsets.left(),
-kBubbleBorderInsets.bottom()));
}
// Tests that the nudge is closed when the anchor widget is closed.
TEST_F(AnchoredNudgeManagerImplTest, NudgeClosedWhenAnchorWidgetClosed) {
std::unique_ptr<views::Widget> anchor_widget =
CreateTestWidget(views::Widget::InitParams::CLIENT_OWNS_WIDGET);
anchor_widget->SetBounds(
gfx::Rect(gfx::Point(100, 100), gfx::Size(300, 200)));
// Set up nudge data contents.
const std::string id("id");
auto nudge_data = CreateBaseNudgeData(id, /*anchor_view=*/nullptr);
nudge_data.anchor_widget = anchor_widget.get();
nudge_data.arrow = views::BubbleBorder::Arrow::BOTTOM_LEFT;
// `anchor_widget` exists, will anchor inside the widget.
GetAnchoredNudgeManager()->Show(nudge_data);
auto* nudge = GetShownNudge(id);
EXPECT_TRUE(nudge);
auto* nudge_widget = nudge->GetWidget();
EXPECT_TRUE(nudge_widget);
auto nudge_bounds = nudge_widget->GetWindowBoundsInScreen();
auto anchor_widget_bounds = anchor_widget->GetWindowBoundsInScreen();
// Compare the bounds alignment with the `kBubbleBorderInsets`.
EXPECT_EQ(nudge_bounds.bottom_left(),
anchor_widget_bounds.bottom_left() +
gfx::Vector2d(kBubbleBorderInsets.left(),
-kBubbleBorderInsets.bottom()));
// Close the `anchor_widget` should close the nudge as well.
anchor_widget->CloseNow();
nudge = GetShownNudge(id);
EXPECT_FALSE(nudge);
}
} // namespace ash