| // Copyright 2016 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include <string> |
| |
| #include "ash/constants/notifier_catalogs.h" |
| #include "ash/public/cpp/shelf_config.h" |
| #include "ash/public/cpp/system/scoped_toast_pause.h" |
| #include "ash/public/cpp/system/toast_data.h" |
| #include "ash/root_window_controller.h" |
| #include "ash/screen_util.h" |
| #include "ash/session/session_controller_impl.h" |
| #include "ash/shelf/hotseat_widget.h" |
| #include "ash/shelf/shelf.h" |
| #include "ash/shelf/shelf_layout_manager.h" |
| #include "ash/shelf/shelf_widget.h" |
| #include "ash/shell.h" |
| #include "ash/strings/grit/ash_strings.h" |
| #include "ash/system/toast/toast_manager_impl.h" |
| #include "ash/test/ash_test_base.h" |
| #include "ash/wm/tablet_mode/tablet_mode_controller_test_api.h" |
| #include "ash/wm/work_area_insets.h" |
| #include "base/memory/raw_ptr.h" |
| #include "base/run_loop.h" |
| #include "base/strings/string_number_conversions.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "base/task/single_thread_task_runner.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 "components/session_manager/session_manager_types.h" |
| #include "ui/base/l10n/l10n_util.h" |
| #include "ui/compositor/layer.h" |
| #include "ui/compositor/layer_animator.h" |
| #include "ui/compositor/scoped_animation_duration_scale_mode.h" |
| #include "ui/compositor/test/layer_animation_stopped_waiter.h" |
| #include "ui/compositor/test/test_utils.h" |
| #include "ui/display/manager/display_manager.h" |
| #include "ui/views/controls/button/button.h" |
| #include "ui/views/widget/widget.h" |
| #include "ui/wm/core/window_util.h" |
| |
| namespace { |
| |
| constexpr char kToastShownCountHistogramName[] = |
| "Ash.NotifierFramework.Toast.ShownCount"; |
| |
| constexpr char kToastTimeInQueueHistogramName[] = |
| "Ash.NotifierFramework.Toast.TimeInQueue"; |
| |
| constexpr char kToastDismissedWithin2s[] = |
| "Ash.NotifierFramework.Toast.Dismissed.Within2s"; |
| |
| constexpr char kToastDismissedWithin7s[] = |
| "Ash.NotifierFramework.Toast.Dismissed.Within7s"; |
| |
| constexpr char kToastDismissedAfter7s[] = |
| "Ash.NotifierFramework.Toast.Dismissed.After7s"; |
| |
| // Wait for the layer animation to be completed. |
| void WaitForAnimationEnded(ui::Layer* layer) { |
| ui::LayerAnimationStoppedWaiter animation_waiter; |
| animation_waiter.Wait(layer); |
| |
| // Force a frame then wait, ensuring there is one more frame presented after |
| // animation finishes to allow animation throughput data to be passed from |
| // cc to ui. |
| ui::Compositor* compositor = layer->GetCompositor(); |
| compositor->ScheduleFullRedraw(); |
| EXPECT_TRUE(ui::WaitForNextFrameToBePresented(compositor)); |
| } |
| |
| // Waits for a time delta `time`. |
| void WaitForTimeDelta(base::TimeDelta time) { |
| base::RunLoop run_loop; |
| base::SingleThreadTaskRunner::GetCurrentDefault()->PostDelayedTask( |
| FROM_HERE, run_loop.QuitClosure(), time); |
| run_loop.Run(); |
| } |
| |
| } // namespace |
| |
| namespace ash { |
| |
| class ToastManagerImplTest : public AshTestBase, |
| public testing::WithParamInterface<bool> { |
| public: |
| ToastManagerImplTest() |
| : AshTestBase(base::test::TaskEnvironment::TimeSource::MOCK_TIME) {} |
| |
| ToastManagerImplTest(const ToastManagerImplTest&) = delete; |
| ToastManagerImplTest& operator=(const ToastManagerImplTest&) = delete; |
| |
| ~ToastManagerImplTest() override = default; |
| |
| void SetUp() override { |
| AshTestBase::SetUp(); |
| |
| manager_ = Shell::Get()->toast_manager(); |
| |
| manager_->ResetSerialForTesting(); |
| EXPECT_EQ(0, GetToastSerial()); |
| |
| // Start in the ACTIVE (logged-in) state. |
| ChangeLockState(false); |
| SetShouldLockScreenAutomatically(false); |
| } |
| |
| protected: |
| ToastManagerImpl* manager() { return manager_; } |
| |
| int GetToastSerial() { return manager_->serial_for_testing(); } |
| |
| // Some toasts can display on multiple root windows, so the caller can use |
| // `root_window` to target a toast on a specific root window. |
| ToastOverlay* GetCurrentOverlay( |
| aura::Window* root_window = Shell::GetRootWindowForNewWindows()) { |
| return manager_->GetCurrentOverlayForTesting(root_window); |
| } |
| |
| gfx::Rect GetToastBounds() { |
| return GetCurrentWidget()->GetWindowBoundsInScreen(); |
| } |
| |
| views::Widget* GetCurrentWidget( |
| aura::Window* root_window = Shell::GetRootWindowForNewWindows()) { |
| ToastOverlay* overlay = GetCurrentOverlay(root_window); |
| return overlay ? overlay->widget_for_testing() : nullptr; |
| } |
| |
| std::u16string GetCurrentText( |
| aura::Window* root_window = Shell::GetRootWindowForNewWindows()) { |
| ToastOverlay* overlay = GetCurrentOverlay(root_window); |
| return overlay ? overlay->text_ : std::u16string(); |
| } |
| |
| void ClickDismissButton( |
| aura::Window* root_window = Shell::GetRootWindowForNewWindows()) { |
| views::Button* dismiss_button = |
| GetCurrentOverlay(root_window)->button_for_testing(); |
| |
| auto* event_generator = GetEventGenerator(); |
| event_generator->MoveMouseTo( |
| dismiss_button->GetBoundsInScreen().CenterPoint()); |
| event_generator->ClickLeftButton(); |
| } |
| |
| std::string ShowToast(const std::string& text, |
| base::TimeDelta duration, |
| bool visible_on_lock_screen = false, |
| const ToastCatalogName catalog_name = |
| ToastCatalogName::kTestCatalogName) { |
| std::string id = "TOAST_ID_" + base::NumberToString(serial_++); |
| manager()->Show(ToastData(id, catalog_name, base::ASCIIToUTF16(text), |
| duration, visible_on_lock_screen)); |
| return id; |
| } |
| |
| std::string ShowToastWithDismiss( |
| const std::string& text, |
| base::TimeDelta duration, |
| const std::u16string& dismiss_text = u"Dismiss") { |
| std::string id = "TOAST_ID_" + base::NumberToString(serial_++); |
| ToastData toast_data(id, ToastCatalogName::kTestCatalogName, |
| base::ASCIIToUTF16(text), duration); |
| toast_data.button_type = ToastData::ButtonType::kTextButton; |
| toast_data.button_text = dismiss_text; |
| manager()->Show(std::move(toast_data)); |
| return id; |
| } |
| |
| void CancelToast(const std::string& id) { manager()->Cancel(id); } |
| |
| void ReplaceToast(const std::string& id, |
| const std::string& text, |
| base::TimeDelta duration, |
| bool visible_on_lock_screen = false, |
| const ToastCatalogName catalog_name = |
| ToastCatalogName::kTestCatalogName) { |
| manager()->Show(ToastData(id, catalog_name, base::ASCIIToUTF16(text), |
| duration, visible_on_lock_screen)); |
| } |
| |
| void ChangeLockState(bool lock) { |
| SessionInfo info; |
| info.state = lock ? session_manager::SessionState::LOCKED |
| : session_manager::SessionState::ACTIVE; |
| Shell::Get()->session_controller()->SetSessionInfo(info); |
| } |
| |
| bool IsToastShown(const std::string& id) { |
| return manager()->IsToastShown(id); |
| } |
| |
| private: |
| raw_ptr<ToastManagerImpl, DanglingUntriaged> manager_ = nullptr; |
| unsigned int serial_ = 0; |
| }; |
| |
| TEST_F(ToastManagerImplTest, ShowAndCloseAutomatically) { |
| // A toast with custom duration closes after its duration plus one second. |
| base::TimeDelta custom_duration = base::Milliseconds(10); |
| ShowToast("id", custom_duration); |
| EXPECT_TRUE(GetCurrentOverlay()); |
| task_environment()->FastForwardBy(custom_duration + base::Seconds(1)); |
| EXPECT_FALSE(GetCurrentOverlay()); |
| |
| // A toast with "infinite" duration closes after its duration plus one second. |
| ShowToast("id", ToastData::kInfiniteDuration); |
| EXPECT_TRUE(GetCurrentOverlay()); |
| task_environment()->FastForwardBy(ToastData::kInfiniteDuration + |
| base::Seconds(1)); |
| EXPECT_FALSE(GetCurrentOverlay()); |
| } |
| |
| TEST_F(ToastManagerImplTest, ShowAndCloseManually) { |
| ShowToastWithDismiss("DUMMY", ToastData::kInfiniteDuration, u"Dismiss"); |
| |
| EXPECT_EQ(1, GetToastSerial()); |
| |
| EXPECT_FALSE(GetCurrentWidget()->GetLayer()->GetAnimator()->is_animating()); |
| |
| ClickDismissButton(); |
| |
| EXPECT_EQ(nullptr, GetCurrentOverlay()); |
| } |
| |
| TEST_F(ToastManagerImplTest, ShowAndCloseManuallyDuringAnimation) { |
| ui::ScopedAnimationDurationScaleMode slow_animation_duration( |
| ui::ScopedAnimationDurationScaleMode::SLOW_DURATION); |
| |
| ASSERT_TRUE(task_environment()->UsesMockTime()); |
| |
| ShowToastWithDismiss("DUMMY", ToastData::kInfiniteDuration, u"Dismiss"); |
| EXPECT_TRUE(GetCurrentWidget()->GetLayer()->GetAnimator()->is_animating()); |
| task_environment()->FastForwardBy(base::Milliseconds(10)); |
| |
| EXPECT_EQ(1, GetToastSerial()); |
| EXPECT_TRUE(GetCurrentWidget()->GetLayer()->GetAnimator()->is_animating()); |
| |
| // Try to close it during animation. |
| ClickDismissButton(); |
| |
| task_environment()->FastForwardBy(base::Seconds(10)); |
| base::RunLoop().RunUntilIdle(); |
| } |
| |
| TEST_F(ToastManagerImplTest, ShowToastWithScopedToastPause) { |
| auto scoped_toast_pause = manager()->CreateScopedPause(); |
| |
| // If a `ScopedToastPause` exists, the toast should not be shown. |
| ShowToast("DUMMY", base::Milliseconds(10)); |
| EXPECT_EQ(0, GetToastSerial()); |
| EXPECT_FALSE(GetCurrentOverlay()); |
| |
| // Even if the `ScopedToastPause` is destroyed, the toast doesn't exist. |
| scoped_toast_pause.reset(); |
| EXPECT_EQ(0, GetToastSerial()); |
| EXPECT_FALSE(GetCurrentOverlay()); |
| } |
| |
| TEST_F(ToastManagerImplTest, CancelToastWithScopedToastPause) { |
| ShowToast("DUMMY", base::Milliseconds(10)); |
| EXPECT_EQ(1, GetToastSerial()); |
| |
| // Creates a `ScopedToastPause` and all toasts will be cleared immediately. |
| manager()->CreateScopedPause(); |
| EXPECT_FALSE(GetCurrentOverlay()); |
| } |
| |
| TEST_F(ToastManagerImplTest, QueueToasts) { |
| const base::TimeDelta kDelay = ToastData::kMinimumDuration; |
| |
| std::string id1 = ShowToast("TEXT1", kDelay); |
| std::string id2 = ShowToast("TEXT2", kDelay); |
| std::string id3 = ShowToast("TEXT3", kDelay); |
| |
| EXPECT_EQ(1, GetToastSerial()); |
| EXPECT_TRUE(IsToastShown(id1)); |
| |
| task_environment()->FastForwardBy(kDelay); |
| while (GetToastSerial() != 2) { |
| base::RunLoop().RunUntilIdle(); |
| } |
| EXPECT_TRUE(IsToastShown(id2)); |
| |
| task_environment()->FastForwardBy(kDelay); |
| while (GetToastSerial() != 3) { |
| base::RunLoop().RunUntilIdle(); |
| } |
| EXPECT_TRUE(IsToastShown(id3)); |
| } |
| |
| TEST_F(ToastManagerImplTest, PositionWithVisibleBottomShelf) { |
| Shelf* shelf = GetPrimaryShelf(); |
| EXPECT_EQ(ShelfAlignment::kBottom, shelf->alignment()); |
| EXPECT_EQ(SHELF_VISIBLE, shelf->GetVisibilityState()); |
| |
| ShowToast("DUMMY", ToastData::kInfiniteDuration); |
| EXPECT_EQ(1, GetToastSerial()); |
| |
| gfx::Rect toast_bounds = GetToastBounds(); |
| gfx::Rect root_bounds = |
| screen_util::GetDisplayBoundsWithShelf(shelf->GetWindow()); |
| |
| EXPECT_TRUE(toast_bounds.Intersects( |
| GetPrimaryWorkAreaInsets()->user_work_area_bounds())); |
| EXPECT_EQ(root_bounds.right(), toast_bounds.right() + ToastOverlay::kOffset); |
| |
| gfx::Rect shelf_bounds = shelf->GetIdealBounds(); |
| EXPECT_FALSE(toast_bounds.Intersects(shelf_bounds)); |
| EXPECT_EQ(shelf_bounds.y() - ToastOverlay::kOffset, toast_bounds.bottom()); |
| EXPECT_EQ( |
| root_bounds.bottom() - shelf_bounds.height() - ToastOverlay::kOffset, |
| toast_bounds.bottom()); |
| } |
| |
| TEST_F(ToastManagerImplTest, PositionWithHotseatShown) { |
| Shelf* shelf = GetPrimaryShelf(); |
| HotseatWidget* hotseat = GetPrimaryShelf()->hotseat_widget(); |
| |
| EXPECT_EQ(ShelfAlignment::kBottom, shelf->alignment()); |
| EXPECT_EQ(SHELF_VISIBLE, shelf->GetVisibilityState()); |
| |
| ash::TabletModeControllerTestApi().EnterTabletMode(); |
| ShowToast("DUMMY", ToastData::kInfiniteDuration); |
| |
| gfx::Rect toast_bounds = GetToastBounds(); |
| gfx::Rect hotseat_bounds = hotseat->GetWindowBoundsInScreen(); |
| |
| EXPECT_EQ(hotseat->state(), HotseatState::kShownHomeLauncher); |
| EXPECT_FALSE(toast_bounds.Intersects(hotseat_bounds)); |
| EXPECT_EQ(hotseat->GetTargetBounds().y() - |
| GetPrimaryWorkAreaInsets()->user_work_area_bounds().y() - |
| ToastOverlay::kOffset, |
| toast_bounds.bottom()); |
| } |
| |
| TEST_F(ToastManagerImplTest, PositionWithHotseatExtended) { |
| Shelf* shelf = GetPrimaryShelf(); |
| HotseatWidget* hotseat = GetPrimaryShelf()->hotseat_widget(); |
| |
| EXPECT_EQ(ShelfAlignment::kBottom, shelf->alignment()); |
| EXPECT_EQ(SHELF_VISIBLE, shelf->GetVisibilityState()); |
| |
| ash::TabletModeControllerTestApi().EnterTabletMode(); |
| hotseat->SetState(HotseatState::kExtended); |
| ShowToast("DUMMY", ToastData::kInfiniteDuration); |
| |
| gfx::Rect toast_bounds = GetToastBounds(); |
| gfx::Rect hotseat_bounds = hotseat->GetWindowBoundsInScreen(); |
| |
| EXPECT_FALSE(toast_bounds.Intersects(hotseat_bounds)); |
| EXPECT_EQ(GetPrimaryWorkAreaInsets()->user_work_area_bounds().height() - |
| hotseat->GetHotseatSize() - ToastOverlay::kOffset - |
| ShelfConfig::Get()->hotseat_bottom_padding(), |
| toast_bounds.bottom()); |
| } |
| |
| TEST_F(ToastManagerImplTest, PositionWithHotseatShownForMultipleMonitors) { |
| UpdateDisplay("600x400,600x400"); |
| Shelf* shelf = GetPrimaryShelf(); |
| HotseatWidget* hotseat = GetPrimaryShelf()->hotseat_widget(); |
| |
| EXPECT_EQ(ShelfAlignment::kBottom, shelf->alignment()); |
| EXPECT_EQ(SHELF_VISIBLE, shelf->GetVisibilityState()); |
| |
| ash::TabletModeControllerTestApi().EnterTabletMode(); |
| display_manager()->SetMirrorMode(display::MirrorMode::kOff, std::nullopt); |
| |
| ShowToast("DUMMY", ToastData::kInfiniteDuration); |
| |
| gfx::Rect toast_bounds = GetToastBounds(); |
| gfx::Rect hotseat_bounds = hotseat->GetWindowBoundsInScreen(); |
| |
| EXPECT_EQ(hotseat->state(), HotseatState::kShownHomeLauncher); |
| EXPECT_FALSE(toast_bounds.Intersects(hotseat_bounds)); |
| EXPECT_EQ(hotseat->GetTargetBounds().y() - |
| GetPrimaryWorkAreaInsets()->user_work_area_bounds().y() - |
| ToastOverlay::kOffset, |
| toast_bounds.bottom()); |
| } |
| |
| // Tests that `ToastOverlay`'s are cleaned up properly on shutdown with hotseat |
| // extended on multi-monitor |
| TEST_F(ToastManagerImplTest, ShutdownWithExtendedHotseat) { |
| UpdateDisplay("600x400,600x400"); |
| Shelf* const shelf = |
| Shell::GetRootWindowControllerWithDisplayId(GetSecondaryDisplay().id()) |
| ->shelf(); |
| EXPECT_EQ(ShelfAlignment::kBottom, shelf->alignment()); |
| EXPECT_EQ(SHELF_VISIBLE, shelf->GetVisibilityState()); |
| |
| ash::TabletModeControllerTestApi().EnterTabletMode(); |
| display_manager()->SetMirrorMode(display::MirrorMode::kOff, std::nullopt); |
| |
| std::unique_ptr<aura::Window> window( |
| CreateTestWindow(gfx::Rect(700, 100, 200, 200))); |
| |
| GetPrimaryShelf()->hotseat_widget()->SetState(HotseatState::kExtended); |
| |
| ShowToast("DUMMY", ToastData::kInfiniteDuration); |
| |
| // Shutdown, there should be no crash. |
| } |
| |
| // Tests that toasts that observe UnifiedSystemTray and are shown in |
| // multiple displays are properly destroyed after disconnecting a monitor. |
| TEST_F(ToastManagerImplTest, ToastsOnMultipleMonitors) { |
| UpdateDisplay("800x700,800x700"); |
| auto* toast_manager = manager(); |
| |
| std::string toast_id = "TOAST_ID_" + base::NumberToString(GetToastSerial()); |
| |
| // Create a basic toast with `ToastData::kDefaultToastDuration` as duration. |
| ToastData toast_data(toast_id, ToastCatalogName::kTestCatalogName, |
| /*text=*/u""); |
| |
| // Indicate that the toast will show on all root windows. |
| toast_data.show_on_all_root_windows = true; |
| |
| toast_manager->Show(std::move(toast_data)); |
| ASSERT_TRUE(toast_manager->IsToastShown(toast_id)); |
| for (aura::Window* root_window : Shell::GetAllRootWindows()) { |
| ASSERT_TRUE(GetCurrentOverlay(root_window)); |
| } |
| |
| // Wait for half of the toast duration to elapse. |
| WaitForTimeDelta(ToastData::kDefaultToastDuration / 2); |
| |
| // Remove a display to trigger the destruction of a toast overlay. |
| UpdateDisplay("800x700"); |
| ASSERT_EQ(1u, Shell::GetAllRootWindows().size()); |
| ASSERT_TRUE(toast_manager->IsToastShown(toast_id)); |
| |
| // No crash should happen. |
| } |
| |
| TEST_F(ToastManagerImplTest, PositionWithHotseatExtendedOnSecondMonitor) { |
| UpdateDisplay("600x400,700x400"); |
| RootWindowController* const secondary_root_window_controller = |
| Shell::GetRootWindowControllerWithDisplayId(GetSecondaryDisplay().id()); |
| Shelf* const shelf = secondary_root_window_controller->shelf(); |
| HotseatWidget* hotseat = shelf->hotseat_widget(); |
| |
| EXPECT_EQ(ShelfAlignment::kBottom, shelf->alignment()); |
| EXPECT_EQ(SHELF_VISIBLE, shelf->GetVisibilityState()); |
| |
| ash::TabletModeControllerTestApi().EnterTabletMode(); |
| display_manager()->SetMirrorMode(display::MirrorMode::kOff, std::nullopt); |
| |
| std::unique_ptr<aura::Window> window( |
| CreateTestWindow(gfx::Rect(700, 100, 200, 200))); |
| shelf->hotseat_widget()->set_manually_extended(true); |
| shelf->shelf_widget()->shelf_layout_manager()->UpdateVisibilityState( |
| /*force_layout=*/false); |
| |
| EXPECT_EQ(hotseat->state(), HotseatState::kExtended); |
| |
| ShowToast("DUMMY", ToastData::kInfiniteDuration); |
| |
| gfx::Rect toast_bounds = GetToastBounds(); |
| gfx::Rect hotseat_bounds = hotseat->GetWindowBoundsInScreen(); |
| |
| EXPECT_EQ(hotseat->state(), HotseatState::kExtended); |
| EXPECT_FALSE(toast_bounds.Intersects(hotseat_bounds)); |
| EXPECT_EQ(hotseat->GetTargetBounds().y() - |
| secondary_root_window_controller->work_area_insets() |
| ->user_work_area_bounds() |
| .y() - |
| ToastOverlay::kOffset, |
| toast_bounds.bottom()); |
| } |
| |
| TEST_F(ToastManagerImplTest, PositionWithHotseatExtendedOnAnotherMonitor) { |
| UpdateDisplay("600x400,700x400"); |
| RootWindowController* const secondary_root_window_controller = |
| Shell::GetRootWindowControllerWithDisplayId(GetSecondaryDisplay().id()); |
| Shelf* const shelf = secondary_root_window_controller->shelf(); |
| HotseatWidget* hotseat = shelf->hotseat_widget(); |
| |
| EXPECT_EQ(ShelfAlignment::kBottom, shelf->alignment()); |
| EXPECT_EQ(SHELF_VISIBLE, shelf->GetVisibilityState()); |
| |
| ash::TabletModeControllerTestApi().EnterTabletMode(); |
| display_manager()->SetMirrorMode(display::MirrorMode::kOff, std::nullopt); |
| |
| // Create two windows, one on each display. The window creation order should |
| // result in the window on the primary display being active. |
| std::unique_ptr<aura::Window> window( |
| CreateTestWindow(gfx::Rect(700, 100, 200, 200))); |
| std::unique_ptr<aura::Window> primary_display_window( |
| CreateTestWindow(gfx::Rect(0, 100, 200, 200))); |
| |
| // Extend the hotseat on the secondary display. |
| shelf->hotseat_widget()->set_manually_extended(true); |
| shelf->shelf_widget()->shelf_layout_manager()->UpdateVisibilityState( |
| /*force_layout=*/false); |
| EXPECT_EQ(hotseat->state(), HotseatState::kExtended); |
| |
| // Show the toast - should be shown on the primary display (on the display |
| // with the latest active window). |
| ShowToast("DUMMY", ToastData::kInfiniteDuration); |
| |
| const gfx::Rect toast_bounds = GetCurrentWidget()->GetWindowBoundsInScreen(); |
| const gfx::Rect primary_work_area_bounds = |
| GetPrimaryWorkAreaInsets()->user_work_area_bounds(); |
| |
| EXPECT_TRUE(primary_work_area_bounds.Contains(toast_bounds)); |
| EXPECT_EQ(primary_work_area_bounds.bottom() - ToastOverlay::kOffset, |
| toast_bounds.bottom()); |
| } |
| |
| TEST_F(ToastManagerImplTest, PositionWithAutoHiddenBottomShelf) { |
| std::unique_ptr<aura::Window> window( |
| CreateTestWindowInShellWithBounds(gfx::Rect(1, 2, 3, 4))); |
| |
| Shelf* shelf = GetPrimaryShelf(); |
| EXPECT_EQ(ShelfAlignment::kBottom, shelf->alignment()); |
| shelf->SetAutoHideBehavior(ShelfAutoHideBehavior::kAlways); |
| EXPECT_EQ(SHELF_AUTO_HIDE_HIDDEN, shelf->GetAutoHideState()); |
| |
| ShowToast("DUMMY", ToastData::kInfiniteDuration); |
| EXPECT_EQ(1, GetToastSerial()); |
| |
| gfx::Rect toast_bounds = GetToastBounds(); |
| gfx::Rect root_bounds = |
| screen_util::GetDisplayBoundsWithShelf(shelf->GetWindow()); |
| |
| EXPECT_TRUE(toast_bounds.Intersects( |
| GetPrimaryWorkAreaInsets()->user_work_area_bounds())); |
| EXPECT_EQ(root_bounds.right(), toast_bounds.right() + ToastOverlay::kOffset); |
| EXPECT_EQ(root_bounds.bottom() - |
| ShelfConfig::Get()->hidden_shelf_in_screen_portion() - |
| ToastOverlay::kOffset, |
| toast_bounds.bottom()); |
| |
| // Hide the window so the shelf is shown, the toast baseline should update. |
| window->Hide(); |
| toast_bounds = GetToastBounds(); |
| |
| EXPECT_EQ(SHELF_AUTO_HIDE_SHOWN, shelf->GetAutoHideState()); |
| EXPECT_EQ(root_bounds.bottom() - ShelfConfig::Get()->shelf_size() - |
| ToastOverlay::kOffset, |
| toast_bounds.bottom()); |
| } |
| |
| TEST_F(ToastManagerImplTest, PositionWithHiddenBottomShelf) { |
| Shelf* shelf = GetPrimaryShelf(); |
| EXPECT_EQ(ShelfAlignment::kBottom, shelf->alignment()); |
| shelf->SetAutoHideBehavior(ShelfAutoHideBehavior::kAlwaysHidden); |
| EXPECT_EQ(SHELF_HIDDEN, shelf->GetVisibilityState()); |
| |
| ShowToast("DUMMY", ToastData::kInfiniteDuration); |
| EXPECT_EQ(1, GetToastSerial()); |
| |
| gfx::Rect toast_bounds = GetToastBounds(); |
| gfx::Rect root_bounds = |
| screen_util::GetDisplayBoundsWithShelf(shelf->GetWindow()); |
| |
| EXPECT_TRUE(toast_bounds.Intersects( |
| GetPrimaryWorkAreaInsets()->user_work_area_bounds())); |
| EXPECT_EQ(root_bounds.right(), toast_bounds.right() + ToastOverlay::kOffset); |
| EXPECT_EQ(root_bounds.bottom() - ToastOverlay::kOffset, |
| toast_bounds.bottom()); |
| } |
| |
| // Tests that toasts follow the shelf when aligning it to the side. |
| // Toasts should stay at center of the work area if side aligned toasts are not |
| // enabled. |
| TEST_F(ToastManagerImplTest, PositionWithVisibleSideShelf) { |
| Shelf* shelf = GetPrimaryShelf(); |
| EXPECT_EQ(SHELF_VISIBLE, shelf->GetVisibilityState()); |
| |
| ShowToast("DUMMY", ToastData::kInfiniteDuration); |
| EXPECT_EQ(1, GetToastSerial()); |
| |
| gfx::Rect work_area_bounds; |
| gfx::Rect shelf_bounds; |
| |
| shelf->SetAlignment(ShelfAlignment::kLeft); |
| work_area_bounds = GetPrimaryWorkAreaInsets()->user_work_area_bounds(); |
| shelf_bounds = shelf->GetIdealBounds(); |
| EXPECT_FALSE(GetToastBounds().Intersects(shelf_bounds)); |
| EXPECT_EQ(work_area_bounds.x(), GetToastBounds().x() - ToastOverlay::kOffset); |
| |
| shelf->SetAlignment(ShelfAlignment::kRight); |
| work_area_bounds = GetPrimaryWorkAreaInsets()->user_work_area_bounds(); |
| shelf_bounds = shelf->GetIdealBounds(); |
| EXPECT_FALSE(GetToastBounds().Intersects(shelf_bounds)); |
| EXPECT_EQ(work_area_bounds.right(), |
| GetToastBounds().right() + ToastOverlay::kOffset); |
| } |
| |
| TEST_F(ToastManagerImplTest, PositionWithUnifiedDesktop) { |
| display_manager()->SetUnifiedDesktopEnabled(true); |
| UpdateDisplay("1000x500,0+600-100x500"); |
| |
| Shelf* shelf = GetPrimaryShelf(); |
| EXPECT_EQ(ShelfAlignment::kBottom, shelf->alignment()); |
| EXPECT_EQ(SHELF_VISIBLE, shelf->GetVisibilityState()); |
| |
| ShowToast("DUMMY", ToastData::kInfiniteDuration); |
| EXPECT_EQ(1, GetToastSerial()); |
| |
| gfx::Rect toast_bounds = GetToastBounds(); |
| gfx::Rect root_bounds = |
| screen_util::GetDisplayBoundsWithShelf(shelf->GetWindow()); |
| |
| EXPECT_TRUE(toast_bounds.Intersects( |
| GetPrimaryWorkAreaInsets()->user_work_area_bounds())); |
| EXPECT_TRUE(root_bounds.Contains(toast_bounds)); |
| EXPECT_EQ(root_bounds.right(), toast_bounds.right() + ToastOverlay::kOffset); |
| |
| gfx::Rect shelf_bounds = shelf->GetIdealBounds(); |
| EXPECT_FALSE(toast_bounds.Intersects(shelf_bounds)); |
| EXPECT_EQ(shelf_bounds.y() - ToastOverlay::kOffset, toast_bounds.bottom()); |
| EXPECT_EQ( |
| root_bounds.bottom() - shelf_bounds.height() - ToastOverlay::kOffset, |
| toast_bounds.bottom()); |
| } |
| |
| TEST_F(ToastManagerImplTest, CancelToast) { |
| std::string id1 = ShowToast("TEXT1", ToastData::kInfiniteDuration); |
| std::string id2 = ShowToast("TEXT2", ToastData::kInfiniteDuration); |
| std::string id3 = ShowToast("TEXT3", ToastData::kInfiniteDuration); |
| |
| // Confirm that the first toast is shown. |
| EXPECT_TRUE(IsToastShown(id1)); |
| |
| // Cancel the queued toast and confirm the first toast is still visible. |
| CancelToast(id2); |
| EXPECT_TRUE(IsToastShown(id1)); |
| EXPECT_FALSE(IsToastShown(id2)); |
| EXPECT_FALSE(IsToastShown(id3)); |
| |
| // Cancel the shown toast and confirm the next toast is visible. |
| CancelToast(id1); |
| EXPECT_FALSE(IsToastShown(id1)); |
| EXPECT_FALSE(IsToastShown(id2)); |
| EXPECT_TRUE(IsToastShown(id3)); |
| |
| // Cancel the shown toast and confirm there are no more toasts. |
| CancelToast(id3); |
| EXPECT_FALSE(IsToastShown(id1)); |
| EXPECT_FALSE(IsToastShown(id2)); |
| EXPECT_FALSE(IsToastShown(id3)); |
| EXPECT_FALSE(GetCurrentOverlay()); |
| |
| // Confirm that 2 toasts were shown. |
| EXPECT_EQ(2, GetToastSerial()); |
| } |
| |
| TEST_F(ToastManagerImplTest, ReplaceContentsOfQueuedToast) { |
| std::string id1 = ShowToast(/*text=*/"TEXT1", ToastData::kInfiniteDuration); |
| std::string id2 = ShowToast(/*text=*/"TEXT2", ToastData::kInfiniteDuration); |
| |
| // Confirm that the first toast is shown. |
| EXPECT_EQ(u"TEXT1", GetCurrentText()); |
| EXPECT_EQ(1, GetToastSerial()); |
| |
| // Replace the contents of the queued toast. |
| ReplaceToast(id2, /*text=*/"TEXT2_updated", ToastData::kInfiniteDuration); |
| |
| // Confirm that the shown toast is still visible. |
| EXPECT_EQ(u"TEXT1", GetCurrentText()); |
| EXPECT_EQ(1, GetToastSerial()); |
| |
| // Cancel the shown toast. |
| CancelToast(id1); |
| |
| // Confirm that the next toast is visible with the updated text. |
| EXPECT_EQ(u"TEXT2_updated", GetCurrentText()); |
| EXPECT_EQ(2, GetToastSerial()); |
| } |
| |
| TEST_F(ToastManagerImplTest, ReplaceContentsOfCurrentToast) { |
| std::string id1 = ShowToast(/*text=*/"TEXT1", ToastData::kInfiniteDuration); |
| std::string id2 = ShowToast(/*text=*/"TEXT2", ToastData::kInfiniteDuration); |
| |
| // Confirm that the first toast is shown. |
| EXPECT_EQ(u"TEXT1", GetCurrentText()); |
| EXPECT_EQ(1, GetToastSerial()); |
| |
| // Replace the contents of the current toast showing. |
| ReplaceToast(id1, /*text=*/"TEXT1_updated", ToastData::kInfiniteDuration); |
| |
| // Confirm that the new toast content is visible. The toast serial should be |
| // different, indicating the original toast's timeout won't close the new |
| // toast's. |
| EXPECT_EQ(u"TEXT1_updated", GetCurrentText()); |
| EXPECT_EQ(2, GetToastSerial()); |
| |
| // Cancel the shown toast. |
| CancelToast(id1); |
| |
| // Confirm that the second toast is now showing. |
| EXPECT_EQ(u"TEXT2", GetCurrentText()); |
| EXPECT_EQ(3, GetToastSerial()); |
| } |
| |
| TEST_F(ToastManagerImplTest, |
| ReplaceContentsOfCurrentToastBeforePriorReplacementFinishes) { |
| // By default, the animation duration is zero in tests. Set the animation |
| // duration to non-zero so that toasts don't immediately close. |
| ui::ScopedAnimationDurationScaleMode animation_duration( |
| ui::ScopedAnimationDurationScaleMode::NON_ZERO_DURATION); |
| |
| std::string id1 = ShowToast(/*text=*/"TEXT1", ToastData::kInfiniteDuration); |
| std::string id2 = ShowToast(/*text=*/"TEXT2", ToastData::kInfiniteDuration); |
| |
| // Confirm that the first toast is shown. |
| EXPECT_EQ(u"TEXT1", GetCurrentText()); |
| EXPECT_EQ(1, GetToastSerial()); |
| |
| // Replace the contents of the current toast showing. This will start the |
| // animation to close the current toast. |
| ReplaceToast(id1, /*text=*/"TEXT1_updated", ToastData::kInfiniteDuration); |
| |
| // Before the current toast's closing animation has finished, replace the |
| // toast with another toast. |
| ReplaceToast(id1, /*text=*/"TEXT1_updated2", ToastData::kInfiniteDuration); |
| |
| // Wait until the first toast's closing animation has finished. See |
| // crbug/1347919 |
| WaitForAnimationEnded(GetCurrentWidget()->GetLayer()); |
| |
| // Confirm that the most recent toast content is visible. The toast serial |
| // should be different, indicating the original toast's timeout won't close |
| // the new toast's. |
| EXPECT_EQ(u"TEXT1_updated2", GetCurrentText()); |
| EXPECT_EQ(2, GetToastSerial()); |
| |
| // Cancel the shown toast and wait for the animation to finish. See |
| // crbug/1347919. |
| CancelToast(id1); |
| WaitForAnimationEnded(GetCurrentWidget()->GetLayer()); |
| |
| // Confirm that the toast now showing corresponds with id2. |
| EXPECT_EQ(u"TEXT2", GetCurrentText()); |
| EXPECT_EQ(3, GetToastSerial()); |
| } |
| |
| TEST_F(ToastManagerImplTest, ToastDismissedOnSessionStateChanges) { |
| // Show a toast supported on the lock screen in the unlocked screen. |
| std::string id1 = ShowToast("TEXT1", ToastData::kInfiniteDuration, |
| /*visible_on_lock_screen=*/true); |
| EXPECT_TRUE(GetCurrentOverlay()); |
| |
| // Simulate device lock, toast should be dismissed. |
| ChangeLockState(true); |
| EXPECT_FALSE(GetCurrentOverlay()); |
| |
| // Simulate device unlock, overlay should not be visible. |
| ChangeLockState(false); |
| EXPECT_FALSE(GetCurrentOverlay()); |
| |
| // Try to show a new toast from within the lock screen, toast should be |
| // immediately shown. |
| ChangeLockState(true); |
| std::string id2 = ShowToast("TEXT1", ToastData::kInfiniteDuration, |
| /*visible_on_lock_screen=*/true); |
| EXPECT_TRUE(GetCurrentOverlay()); |
| |
| // Unlock, toast should be dismissed. |
| ChangeLockState(false); |
| EXPECT_FALSE(GetCurrentOverlay()); |
| } |
| |
| TEST_F(ToastManagerImplTest, ToastNotSupportedOnLockScreen) { |
| // Show a toast that is not supported on the lock screen. |
| std::string id1 = ShowToast("TEXT1", ToastData::kInfiniteDuration, |
| /*visible_on_lock_screen=*/false); |
| EXPECT_TRUE(GetCurrentOverlay()); |
| |
| // Simulate device lock, overlay should be dismissed. |
| ChangeLockState(true); |
| EXPECT_FALSE(GetCurrentOverlay()); |
| |
| // Try to show a new toast from within the lock screen, toast request will be |
| // ignored. |
| ChangeLockState(true); |
| std::string id2 = ShowToast("TEXT1", ToastData::kInfiniteDuration, |
| /*visible_on_lock_screen=*/false); |
| EXPECT_FALSE(GetCurrentOverlay()); |
| |
| // Unlock, overlay should not be visible. |
| ChangeLockState(false); |
| EXPECT_FALSE(GetCurrentOverlay()); |
| } |
| |
| TEST_F(ToastManagerImplTest, ShownCountMetric) { |
| base::HistogramTester histogram_tester; |
| |
| const ToastCatalogName catalog_name_1 = static_cast<ToastCatalogName>(1); |
| const ToastCatalogName catalog_name_2 = static_cast<ToastCatalogName>(2); |
| const base::TimeDelta duration = base::Seconds(2); |
| constexpr char text[] = "sample text"; |
| |
| // Show Toast with catalog_name_1. |
| std::string id1 = ShowToast(text, duration, |
| /*visible_on_lock_screen=*/false, catalog_name_1); |
| histogram_tester.ExpectBucketCount(kToastShownCountHistogramName, |
| catalog_name_1, 1); |
| |
| // Replace existing toast a couple of times. |
| ReplaceToast(id1, text, duration, |
| /*visible_on_lock_screen=*/false, catalog_name_1); |
| ReplaceToast(id1, text, duration, |
| /*visible_on_lock_screen=*/false, catalog_name_1); |
| histogram_tester.ExpectBucketCount(kToastShownCountHistogramName, |
| catalog_name_1, 3); |
| |
| // Try to show toast with catalog_name_2 right after last toast was shown. |
| ShowToast(text, duration, /*visible_on_lock_screen=*/false, catalog_name_2); |
| |
| // Fast forward the toast's duration so the queued toast is shown. |
| task_environment()->FastForwardBy(duration); |
| histogram_tester.ExpectBucketCount(kToastShownCountHistogramName, |
| catalog_name_2, 1); |
| } |
| |
| TEST_F(ToastManagerImplTest, TimeInQueueMetric) { |
| base::HistogramTester histogram_tester; |
| |
| const ToastCatalogName catalog_name_1 = static_cast<ToastCatalogName>(1); |
| const ToastCatalogName catalog_name_2 = static_cast<ToastCatalogName>(2); |
| const base::TimeDelta duration = base::Seconds(2); |
| constexpr char text[] = "sample text"; |
| |
| // Show Toast with catalog_name_1. |
| std::string id1 = ShowToast(text, duration, /*visible_on_lock_screen=*/false, |
| catalog_name_1); |
| |
| // 'TimeInQueue' is zero since there were no toasts in the queue. |
| histogram_tester.ExpectTimeBucketCount(kToastTimeInQueueHistogramName, |
| base::Seconds(0), 1); |
| |
| // Replace existing toast a couple of times. |
| ReplaceToast(id1, text, duration, |
| /*visible_on_lock_screen=*/false, catalog_name_1); |
| ReplaceToast(id1, text, duration, |
| /*visible_on_lock_screen=*/false, catalog_name_1); |
| |
| // 'TimeInQueue' is zero since the same toast was replaced. |
| histogram_tester.ExpectTimeBucketCount(kToastTimeInQueueHistogramName, |
| base::Seconds(0), 3); |
| |
| // Try to show toast with catalog_name_2 right after last toast was shown. |
| ShowToast(text, duration, /*visible_on_lock_screen=*/false, catalog_name_2); |
| |
| // Fast forward the toast's duration so the queued toast is shown. |
| task_environment()->FastForwardBy(duration); |
| |
| // 'TimeInQueue' records the toast's duration since the second toast was |
| // queued right after the first one was shown. |
| histogram_tester.ExpectTimeBucketCount(kToastTimeInQueueHistogramName, |
| duration, 1); |
| } |
| |
| TEST_F(ToastManagerImplTest, UserJourneyTimeMetric) { |
| base::HistogramTester histogram_tester; |
| |
| const ToastCatalogName catalog_name = ToastCatalogName::kTestCatalogName; |
| const base::TimeDelta duration = base::Seconds(6); |
| constexpr char text[] = "sample text"; |
| |
| // Show Toast and wait for it to dismiss by time-out. |
| ShowToast(text, duration); |
| task_environment()->FastForwardBy(duration); |
| histogram_tester.ExpectBucketCount(kToastDismissedWithin7s, catalog_name, 1); |
| |
| // Show toast and replace it right after. |
| std::string id = ShowToast(text, duration); |
| ReplaceToast(id, text, duration); |
| task_environment()->FastForwardBy(duration); |
| |
| // Replaced toast was dismissed within 2s. |
| histogram_tester.ExpectBucketCount(kToastDismissedWithin2s, catalog_name, 1); |
| histogram_tester.ExpectBucketCount(kToastDismissedWithin7s, catalog_name, 2); |
| |
| // Show a toast with infinite duration. |
| ShowToastWithDismiss(text, ToastData::kInfiniteDuration); |
| task_environment()->FastForwardBy(duration + base::Seconds(2)); |
| ClickDismissButton(); |
| |
| // Toast with dismiss button was dismissed after 7s. |
| histogram_tester.ExpectBucketCount(kToastDismissedAfter7s, catalog_name, 1); |
| } |
| |
| // Table-driven test that checks whether a toast's expired callback is run when |
| // a toast is closed when the toast manager cancels the toast, when the toast |
| // duration cancels the toast, and when the dismiss button is pressed. |
| TEST_F(ToastManagerImplTest, ExpiredCallbackRunsWhenToastOverlayClosed) { |
| // Covers possible ways that a toast can be cancelled. |
| enum class CancellationSource { |
| kToastManager, |
| kDismissButton, |
| kToastDuration, |
| }; |
| |
| struct { |
| const std::string scope_trace; |
| const CancellationSource source; |
| } kTestCases[] = { |
| {"Cancel toast through the toast manager", |
| CancellationSource::kToastManager}, |
| {"Cancel toast by pressing the dismiss button", |
| CancellationSource::kDismissButton}, |
| {"Cancel toast by letting duration elapse", |
| CancellationSource::kToastDuration}, |
| }; |
| |
| auto* toast_manager = manager(); |
| |
| for (const auto& test_case : kTestCases) { |
| SCOPED_TRACE(test_case.scope_trace); |
| std::string toast_id = "TOAST_ID_" + base::NumberToString(GetToastSerial()); |
| |
| // Create data for a toast that matches the test case. If the test case is |
| // not `kToastDuration`, duration should be infinite, and if the test case |
| // is not `kDismissButton` then we do not need a dismiss button on the |
| // toast. |
| ToastData toast_data( |
| toast_id, ToastCatalogName::kTestCatalogName, |
| /*text=*/u"", |
| /*duration=*/test_case.source == CancellationSource::kToastDuration |
| ? ToastData::kDefaultToastDuration |
| : ToastData::kInfiniteDuration, |
| /*visible_on_lock_screen=*/false, |
| /*has_dismiss_button=*/test_case.source == |
| CancellationSource::kDismissButton); |
| |
| // Bind a lambda that will change a value to tell us whether the expired |
| // callback ran. |
| bool expired_callback_ran = false; |
| toast_data.expired_callback = base::BindLambdaForTesting( |
| [&expired_callback_ran]() { expired_callback_ran = true; }); |
| toast_manager->Show(std::move(toast_data)); |
| |
| switch (test_case.source) { |
| case CancellationSource::kToastManager: { |
| toast_manager->Cancel(toast_id); |
| break; |
| } |
| case CancellationSource::kDismissButton: { |
| ClickDismissButton(); |
| break; |
| } |
| case CancellationSource::kToastDuration: { |
| WaitForTimeDelta(ToastData::kDefaultToastDuration); |
| break; |
| } |
| } |
| |
| EXPECT_TRUE(expired_callback_ran); |
| } |
| } |
| |
| // Tests that a toast that is created with `ToastData::persist_on_hover` set to |
| // true will not expire while the mouse is hovering over it. |
| TEST_F(ToastManagerImplTest, ToastsCanPersistOnHover) { |
| std::string toast_id = "TOAST_ID_" + base::NumberToString(GetToastSerial()); |
| |
| ToastData toast_data(toast_id, ToastCatalogName::kTestCatalogName, |
| /*text=*/u""); |
| toast_data.persist_on_hover = true; |
| |
| auto* toast_manager = manager(); |
| toast_manager->Show(std::move(toast_data)); |
| EXPECT_TRUE(toast_manager->IsToastShown(toast_id)); |
| |
| // Wait for half of the toast duration to elapse. |
| WaitForTimeDelta(ToastData::kDefaultToastDuration / 2); |
| |
| // Hover the mouse over the toast to stop the expiration countdown timer. |
| views::Widget* widget = GetCurrentWidget(); |
| const gfx::Point toast_center = |
| widget->GetNativeWindow()->GetBoundsInScreen().CenterPoint(); |
| auto* event_generator = GetEventGenerator(); |
| event_generator->MoveMouseTo(toast_center); |
| ASSERT_TRUE(widget->GetRootView()->IsMouseHovered()); |
| |
| // Wait for the remainder of the default toast duration. At this point the |
| // toast would normally expire, but because the mouse is hovered over it, it |
| // will not. |
| WaitForTimeDelta(ToastData::kDefaultToastDuration / 2); |
| ASSERT_TRUE(toast_manager->IsToastShown(toast_id)); |
| |
| // Move the mouse away to resume the expiration countdown timer. |
| event_generator->MoveMouseTo(gfx::Point(0, 0)); |
| ASSERT_FALSE(widget->GetRootView()->IsMouseHovered()); |
| |
| // Wait for the toast to expire now that the toast is no longer hovered. |
| WaitForTimeDelta(ToastData::kDefaultToastDuration / 2); |
| EXPECT_FALSE(toast_manager->IsToastShown(toast_id)); |
| } |
| |
| // Table-driven test that checks that toasts designated to show on all windows |
| // correctly show and close on all root windows. |
| TEST_F(ToastManagerImplTest, ShowAndCloseToastsOnAllRootWindows) { |
| UpdateDisplay("800x700,800x700"); |
| |
| // Covers possible ways that a toast can be cancelled. |
| enum class CancellationSource { |
| kToastManager, |
| kDismissButton, |
| kToastDuration, |
| }; |
| |
| struct { |
| const std::string scope_trace; |
| const CancellationSource source; |
| } kTestCases[] = { |
| {"Cancel toast through the toast manager", |
| CancellationSource::kToastManager}, |
| {"Cancel toast by pressing the dismiss button", |
| CancellationSource::kDismissButton}, |
| {"Cancel toast by letting duration elapse", |
| CancellationSource::kToastDuration}, |
| }; |
| |
| auto* toast_manager = manager(); |
| const aura::Window::Windows root_windows = Shell::GetAllRootWindows(); |
| |
| for (const auto& test_case : kTestCases) { |
| SCOPED_TRACE(test_case.scope_trace); |
| std::string toast_id = "TOAST_ID_" + base::NumberToString(GetToastSerial()); |
| |
| // Create data for a toast that matches the test case. If the test case is |
| // not `kToastDuration`, duration should be infinite, and if the test case |
| // is not `kDismissButton` then we do not need a dismiss button on the |
| // toast. |
| ToastData toast_data( |
| toast_id, ToastCatalogName::kTestCatalogName, |
| /*text=*/u"", |
| /*duration=*/test_case.source == CancellationSource::kToastDuration |
| ? ToastData::kDefaultToastDuration |
| : ToastData::kInfiniteDuration, |
| /*visible_on_lock_screen=*/false, |
| /*has_dismiss_button=*/test_case.source == |
| CancellationSource::kDismissButton); |
| |
| // Indicate that the toast will show on all root windows. |
| toast_data.show_on_all_root_windows = true; |
| toast_manager->Show(std::move(toast_data)); |
| |
| for (aura::Window* root_window : root_windows) { |
| EXPECT_TRUE(GetCurrentOverlay(root_window)); |
| } |
| |
| switch (test_case.source) { |
| case CancellationSource::kToastManager: { |
| toast_manager->Cancel(toast_id); |
| break; |
| } |
| case CancellationSource::kDismissButton: { |
| ClickDismissButton(); |
| break; |
| } |
| case CancellationSource::kToastDuration: { |
| base::RunLoop run_loop; |
| base::SingleThreadTaskRunner::GetCurrentDefault()->PostDelayedTask( |
| FROM_HERE, run_loop.QuitClosure(), |
| ToastData::kDefaultToastDuration); |
| run_loop.Run(); |
| break; |
| } |
| } |
| |
| for (aura::Window* root_window : root_windows) { |
| EXPECT_FALSE(GetCurrentOverlay(root_window)); |
| } |
| } |
| } |
| |
| // This tests that toasts that are designated to persist on hover and appear on |
| // all root windows will not close when one of the toast instances is hovered. |
| TEST_F(ToastManagerImplTest, ToastsThatPersistOnHoverOnAllRootWindows) { |
| UpdateDisplay("800x700,800x700"); |
| auto* toast_manager = manager(); |
| const aura::Window::Windows root_windows = Shell::GetAllRootWindows(); |
| |
| std::string toast_id = "TOAST_ID_" + base::NumberToString(GetToastSerial()); |
| |
| // Create a basic toast with `ToastData::kDefaultToastDuration` as duration. |
| ToastData toast_data(toast_id, ToastCatalogName::kTestCatalogName, |
| /*text=*/u""); |
| |
| // Indicate that the toast will show on all root windows and persist on hover. |
| toast_data.show_on_all_root_windows = true; |
| toast_data.persist_on_hover = true; |
| toast_manager->Show(std::move(toast_data)); |
| ASSERT_TRUE(toast_manager->IsToastShown(toast_id)); |
| |
| for (aura::Window* root_window : root_windows) { |
| ASSERT_TRUE(GetCurrentOverlay(root_window)); |
| } |
| |
| // Wait for half of the toast duration to elapse. |
| WaitForTimeDelta(ToastData::kDefaultToastDuration / 2); |
| |
| // Hover the mouse over the toast instance on a root window (in this case the |
| // default is `Shell::GetRootWindowForNewWindows()`) to stop the expiration |
| // countdown timer. |
| views::Widget* widget = GetCurrentWidget(); |
| const gfx::Point toast_center = |
| widget->GetNativeWindow()->GetBoundsInScreen().CenterPoint(); |
| auto* event_generator = GetEventGenerator(); |
| event_generator->MoveMouseTo(toast_center); |
| ASSERT_TRUE(widget->GetRootView()->IsMouseHovered()); |
| |
| // Wait for the other half of the toast duration to elapse. Because the mouse |
| // is hovering over one of the toast instances, all toasts instances should |
| // remain open after this time. |
| WaitForTimeDelta(ToastData::kDefaultToastDuration / 2); |
| |
| for (aura::Window* root_window : root_windows) { |
| EXPECT_TRUE(GetCurrentOverlay(root_window)); |
| } |
| |
| // Move the mouse away to resume the expiration countdown timer. |
| event_generator->MoveMouseTo(gfx::Point(0, 0)); |
| ASSERT_FALSE(widget->GetRootView()->IsMouseHovered()); |
| |
| // Wait for the other half of the toast duration to elapse. This time, because |
| // the mouse has been moved away from the toast, all toast instances should be |
| // gone. |
| WaitForTimeDelta(ToastData::kDefaultToastDuration / 2); |
| |
| for (aura::Window* root_window : root_windows) { |
| EXPECT_FALSE(GetCurrentOverlay(root_window)); |
| } |
| } |
| |
| // This tests that multi-monitor toast instances do not call the |
| // `expired_callback_` when the root window is removed. |
| TEST_F(ToastManagerImplTest, ExpiredCallbackNotCalledOnRootWindowRemoved) { |
| UpdateDisplay("800x700,800x700"); |
| auto* toast_manager = manager(); |
| |
| std::string toast_id = "TOAST_ID_" + base::NumberToString(GetToastSerial()); |
| |
| // Create a basic toast with `ToastData::kDefaultToastDuration` as duration. |
| ToastData toast_data(toast_id, ToastCatalogName::kTestCatalogName, |
| /*text=*/u""); |
| |
| // Indicate that the toast will show on all root windows. |
| toast_data.show_on_all_root_windows = true; |
| |
| // Bind a lambda that will change a value to tell us whether the expired |
| // callback ran. |
| bool expired_callback_ran = false; |
| toast_data.expired_callback = base::BindLambdaForTesting( |
| [&expired_callback_ran]() { expired_callback_ran = true; }); |
| toast_manager->Show(std::move(toast_data)); |
| ASSERT_TRUE(toast_manager->IsToastShown(toast_id)); |
| |
| for (aura::Window* root_window : Shell::GetAllRootWindows()) { |
| ASSERT_TRUE(GetCurrentOverlay(root_window)); |
| } |
| |
| // Wait for half of the toast duration to elapse. |
| WaitForTimeDelta(ToastData::kDefaultToastDuration / 2); |
| |
| // Remove a display to trigger the destruction of a toast overlay. |
| // `expired_callback_ran` should still be false. |
| UpdateDisplay("800x700"); |
| ASSERT_EQ(1u, Shell::GetAllRootWindows().size()); |
| ASSERT_TRUE(toast_manager->IsToastShown(toast_id)); |
| EXPECT_FALSE(expired_callback_ran); |
| |
| // Wait for the other half of the toast duration to elapse. |
| WaitForTimeDelta(ToastData::kDefaultToastDuration / 2); |
| EXPECT_FALSE(toast_manager->IsToastShown(toast_id)); |
| EXPECT_TRUE(expired_callback_ran); |
| } |
| |
| // Tests that toasts are properly closed if they only exist in a secondary |
| // display that gets removed e.g. by monitor disconnecteded. |
| TEST_F(ToastManagerImplTest, SingleDisplayToastDestroyedOnRootWindowRemoved) { |
| // Add a secondary display, and set it to be the active display so toasts are |
| // added here. |
| UpdateDisplay("800x700,800x700"); |
| display::Screen::Get()->SetDisplayForNewWindows(GetSecondaryDisplay().id()); |
| |
| auto* toast_manager = manager(); |
| std::string toast_id = "TOAST_ID_" + base::NumberToString(GetToastSerial()); |
| |
| // Create a basic toast with `ToastData::kDefaultToastDuration` as duration. |
| ToastData toast_data(toast_id, ToastCatalogName::kTestCatalogName, |
| /*text=*/u""); |
| |
| // Indicate that the toast will not show on all root windows. |
| toast_data.show_on_all_root_windows = false; |
| |
| // Bind a lambda that will change a value to tell us whether the expired |
| // callback ran. |
| bool expired_callback_ran = false; |
| toast_data.expired_callback = base::BindLambdaForTesting( |
| [&expired_callback_ran]() { expired_callback_ran = true; }); |
| toast_manager->Show(std::move(toast_data)); |
| ASSERT_TRUE(toast_manager->IsToastShown(toast_id)); |
| |
| // Remove a display to trigger the destruction of a toast overlay. Since this |
| // is the only instance of the toast, `expired_callback_ran` should be true. |
| UpdateDisplay("800x700"); |
| ASSERT_EQ(1u, Shell::GetAllRootWindows().size()); |
| EXPECT_FALSE(toast_manager->IsToastShown(toast_id)); |
| EXPECT_TRUE(expired_callback_ran); |
| } |
| |
| // This tests that new instances of a multi-monitor toast are spawned with the |
| // correct duration and correct persisting state. |
| TEST_F(ToastManagerImplTest, |
| AllRootWindowToastsCreatedWithCorrectDurationAndPersistState) { |
| // Start with display at 800x700 to maintain cursor position when adding root |
| // windows. |
| UpdateDisplay("800x700"); |
| auto* toast_manager = manager(); |
| |
| std::string toast_id = "TOAST_ID_" + base::NumberToString(GetToastSerial()); |
| |
| // Create a basic toast with `ToastData::kDefaultToastDuration` as duration. |
| ToastData toast_data(toast_id, ToastCatalogName::kTestCatalogName, |
| /*text=*/u""); |
| |
| // Indicate that the toast will show on all root windows and persist on hover. |
| toast_data.show_on_all_root_windows = true; |
| toast_data.persist_on_hover = true; |
| toast_manager->Show(std::move(toast_data)); |
| ASSERT_TRUE(toast_manager->IsToastShown(toast_id)); |
| |
| // Wait for half of the toast duration to elapse. |
| WaitForTimeDelta(ToastData::kDefaultToastDuration / 2); |
| |
| // Hover over the active toast instance to stop the expiration timer. |
| views::Widget* widget = GetCurrentWidget(); |
| const gfx::Point toast_center = |
| widget->GetNativeWindow()->GetBoundsInScreen().CenterPoint(); |
| auto* event_generator = GetEventGenerator(); |
| event_generator->MoveMouseTo(toast_center); |
| ASSERT_TRUE(widget->GetRootView()->IsMouseHovered()); |
| |
| // Add a new root window while hovering over the initial toast instance. Both |
| // toasts should still be persisting on hover. |
| UpdateDisplay("800x700,800x700"); |
| ASSERT_TRUE(widget->GetRootView()->IsMouseHovered()); |
| |
| // Wait for the remaining half of the toast duration to elapse. Neither toast |
| // instance should be destroyed. |
| WaitForTimeDelta(ToastData::kDefaultToastDuration / 2); |
| |
| for (aura::Window* root_window : Shell::GetAllRootWindows()) { |
| EXPECT_TRUE(GetCurrentOverlay(root_window)); |
| } |
| |
| // Unhover the mouse an add a third root window. |
| event_generator->MoveMouseTo(gfx::Point(0, 0)); |
| ASSERT_FALSE(widget->GetRootView()->IsMouseHovered()); |
| UpdateDisplay("800x700,800x700,800x700"); |
| |
| // Wait for the remaining half of the toast duration to elapse. At this point |
| // all three toast instances should be destroyed. |
| WaitForTimeDelta(ToastData::kDefaultToastDuration / 2); |
| base::RunLoop().RunUntilIdle(); |
| |
| for (aura::Window* root_window : Shell::GetAllRootWindows()) { |
| EXPECT_FALSE(GetCurrentOverlay(root_window)); |
| } |
| } |
| |
| // Tests that an offset is added to shift the overlay baseline up when |
| // toasts are side aligned and a slider bubble is shown. |
| // Overlay baseline is unchanged when toasts are not side aligned. |
| TEST_F(ToastManagerImplTest, BaselineUpdatesAfterSliderBubbleShown) { |
| ShowToast("DUMMY", ToastData::kInfiniteDuration); |
| const int previous_baseline = GetToastBounds().bottom(); |
| |
| // The difference between baselines for side aligned toasts after showing a |
| // slider bubble should be the slider bubble height + a default spacing |
| // offset. Baseline remains unchanged with center aligned toasts. |
| GetPrimaryUnifiedSystemTray()->ShowVolumeSliderBubble(); |
| auto* slider_view = GetPrimaryUnifiedSystemTray()->GetSliderView(); |
| ASSERT_TRUE(slider_view); |
| EXPECT_EQ(slider_view->height() + ToastOverlay::kOffset, |
| previous_baseline - GetToastBounds().bottom()); |
| |
| // Baseline returns to previous value when the slider bubble is closed. |
| GetPrimaryUnifiedSystemTray()->CloseSecondaryBubbles(); |
| EXPECT_EQ(GetToastBounds().bottom(), previous_baseline); |
| } |
| |
| } // namespace ash |