| // Copyright 2022 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "chromeos/ui/frame/multitask_menu/multitask_menu_nudge_controller.h" |
| |
| #include "ash/constants/ash_features.h" |
| #include "ash/constants/ash_pref_names.h" |
| #include "ash/constants/notifier_catalogs.h" |
| #include "ash/display/display_move_window_util.h" |
| #include "ash/frame/frame_view_ash.h" |
| #include "ash/frame/multitask_menu_nudge_delegate_ash.h" |
| #include "ash/session/session_controller_impl.h" |
| #include "ash/shell.h" |
| #include "ash/test/ash_test_base.h" |
| #include "ash/test/ash_test_util.h" |
| #include "ash/wm/desks/desk.h" |
| #include "ash/wm/desks/desks_test_util.h" |
| #include "ash/wm/splitview/split_view_controller.h" |
| #include "ash/wm/tablet_mode/tablet_mode_controller_test_api.h" |
| #include "ash/wm/tablet_mode/tablet_mode_multitask_cue_controller.h" |
| #include "ash/wm/tablet_mode/tablet_mode_multitask_menu_controller.h" |
| #include "ash/wm/tablet_mode/tablet_mode_window_manager.h" |
| #include "ash/wm/window_state.h" |
| #include "ash/wm/wm_event.h" |
| #include "base/test/metrics/histogram_tester.h" |
| #include "base/test/simple_test_clock.h" |
| #include "chromeos/ui/base/nudge_util.h" |
| #include "chromeos/ui/frame/caption_buttons/frame_caption_button_container_view.h" |
| #include "chromeos/ui/frame/immersive/immersive_fullscreen_controller.h" |
| #include "chromeos/ui/frame/immersive/immersive_fullscreen_controller_test_api.h" |
| #include "chromeos/ui/frame/multitask_menu/multitask_button.h" |
| #include "chromeos/ui/frame/multitask_menu/multitask_menu.h" |
| #include "chromeos/ui/frame/multitask_menu/multitask_menu_view_test_api.h" |
| #include "components/user_manager/fake_user_manager.h" |
| #include "components/user_manager/scoped_user_manager.h" |
| #include "ui/aura/window.h" |
| #include "ui/compositor/scoped_animation_duration_scale_mode.h" |
| #include "ui/display/screen.h" |
| #include "ui/gfx/geometry/size.h" |
| #include "ui/wm/core/window_util.h" |
| |
| namespace ash { |
| |
| namespace { |
| |
| // Returns the nudge controller associated with `window`. |
| chromeos::MultitaskMenuNudgeController* GetNudgeControllerForWindow( |
| aura::Window* window) { |
| if (display::Screen::Get()->InTabletMode()) { |
| return TabletModeControllerTestApi() |
| .tablet_mode_window_manager() |
| ->tablet_mode_multitask_menu_controller() |
| ->multitask_cue_controller() |
| ->nudge_controller_for_testing(); |
| } |
| |
| if (auto* frame = FrameViewAsh::Get(window)) { |
| return chromeos::FrameCaptionButtonContainerView::TestApi( |
| frame->GetHeaderView()->caption_button_container()) |
| .nudge_controller(); |
| } |
| |
| return nullptr; |
| } |
| |
| } // namespace |
| |
| class MultitaskMenuNudgeControllerTest : public AshTestBase { |
| public: |
| MultitaskMenuNudgeControllerTest() = default; |
| MultitaskMenuNudgeControllerTest(const MultitaskMenuNudgeControllerTest&) = |
| delete; |
| MultitaskMenuNudgeControllerTest& operator=( |
| const MultitaskMenuNudgeControllerTest&) = delete; |
| ~MultitaskMenuNudgeControllerTest() override = default; |
| |
| views::Widget* GetNudgeWidgetForWindow(aura::Window* window) { |
| chromeos::MultitaskMenuNudgeController* controller = |
| GetNudgeControllerForWindow(window); |
| return controller ? controller->nudge_widget_.get() : nullptr; |
| } |
| |
| void FireDismissNudgeTimer(aura::Window* window) { |
| if (chromeos::MultitaskMenuNudgeController* controller = |
| GetNudgeControllerForWindow(window)) { |
| controller->clamshell_nudge_dismiss_timer_.FireNow(); |
| } |
| } |
| |
| // AshTestBase: |
| void SetUp() override { |
| AshTestBase::SetUp(); |
| |
| chromeos::MultitaskMenuNudgeController::SetSuppressNudgeForTesting(false); |
| chromeos::MultitaskMenuNudgeController::SetOverrideClockForTesting( |
| &test_clock_); |
| |
| // Advance the test clock so we aren't at zero time. |
| test_clock_.Advance(base::Hours(50)); |
| } |
| |
| void TearDown() override { |
| chromeos::MultitaskMenuNudgeController::SetOverrideClockForTesting(nullptr); |
| |
| AshTestBase::TearDown(); |
| } |
| |
| protected: |
| // Tests that the tablet mode nudge bounds in screen are correct. |
| void ExpectCorrectTabletNudgeBounds(aura::Window* window) { |
| const gfx::Size size = |
| GetNudgeWidgetForWindow(window)->GetContentsView()->GetPreferredSize(); |
| const auto window_screen_bounds = window->GetBoundsInScreen(); |
| const int tablet_nudge_y_offset = |
| MultitaskMenuNudgeDelegateAsh::kTabletNudgeAdditionalYOffset + |
| TabletModeMultitaskCueController::kCueHeight + |
| TabletModeMultitaskCueController::kCueYOffset; |
| const gfx::Rect expected_bounds( |
| (window_screen_bounds.width() - size.width()) / 2 + |
| window_screen_bounds.x(), |
| tablet_nudge_y_offset + window_screen_bounds.y(), size.width(), |
| size.height()); |
| EXPECT_EQ(expected_bounds, |
| GetNudgeWidgetForWindow(window)->GetWindowBoundsInScreen()); |
| } |
| |
| base::SimpleTestClock test_clock_; |
| }; |
| |
| // Tests that there is no crash after toggling fullscreen on and off. Regression |
| // test for https://crbug.com/1341142. |
| TEST_F(MultitaskMenuNudgeControllerTest, NoCrashAfterFullscreening) { |
| auto window = CreateAppWindow(gfx::Rect(300, 300)); |
| ASSERT_TRUE(GetNudgeWidgetForWindow(window.get())); |
| |
| // Turn of animations for immersive mode, so we don't have to wait for the top |
| // container to hide on fullscreen. |
| auto* immersive_controller = chromeos::ImmersiveFullscreenController::Get( |
| views::Widget::GetWidgetForNativeView(window.get())); |
| chromeos::ImmersiveFullscreenControllerTestApi(immersive_controller) |
| .SetupForTest(); |
| |
| const WMEvent event(WM_EVENT_TOGGLE_FULLSCREEN); |
| WindowState::Get(window.get())->OnWMEvent(&event); |
| EXPECT_FALSE(GetNudgeWidgetForWindow(window.get())); |
| |
| // Window needs to be immersive enabled, but not revealed for the bug to |
| // reproduce. |
| ASSERT_TRUE(immersive_controller->IsEnabled()); |
| ASSERT_FALSE(immersive_controller->IsRevealed()); |
| |
| WindowState::Get(window.get())->OnWMEvent(&event); |
| EXPECT_FALSE(GetNudgeWidgetForWindow(window.get())); |
| } |
| |
| // Tests that there is no crash after floating a window via the multitask menu. |
| // Regression test for http://b/265189622. |
| TEST_F(MultitaskMenuNudgeControllerTest, |
| NoCrashAfterFloatingFromMultitaskMenu) { |
| auto window = CreateAppWindow(gfx::Rect(300, 300)); |
| ASSERT_TRUE(GetNudgeWidgetForWindow(window.get())); |
| |
| // Float the window from the multitask menu. Floating the window using the |
| // accelerator does not cause the crash mentioned in the bug because the |
| // presence of the multitask menu causes an activation change which leads to |
| // restacking that does not happen otherwise. |
| chromeos::MultitaskMenu* multitask_menu = |
| ShowAndWaitMultitaskMenuForWindow(window.get()); |
| |
| // After floating the window from the multitask menu, there is no crash. |
| LeftClickOn( |
| chromeos::MultitaskMenuViewTestApi(multitask_menu->multitask_menu_view()) |
| .GetFloatButton()); |
| EXPECT_TRUE(WindowState::Get(window.get())->IsFloated()); |
| } |
| |
| // Tests that there is no crash after entering tablet mode with the multitask |
| // menu created on the secondary display. Regression test for |
| // http://b/278165707. |
| TEST_F(MultitaskMenuNudgeControllerTest, |
| NoCrashAfterEnterTabletFromMultidisplay) { |
| UpdateDisplay("800x600,801+0-800x600"); |
| |
| auto window = CreateAppWindow(gfx::Rect(900, 0, 300, 300)); |
| ASSERT_EQ(Shell::GetAllRootWindows()[1], window->GetRootWindow()); |
| |
| // Ensure that the clamshell nudge is closed and advance the clock so that the |
| // tablet one will show. |
| FireDismissNudgeTimer(window.get()); |
| test_clock_.Advance(base::Hours(26)); |
| |
| // We use non zero duration since we want to mimic real behavior of stacking |
| // order changed on `window` before tablet mode is entered. |
| ui::ScopedAnimationDurationScaleMode scale_mode( |
| ui::ScopedAnimationDurationScaleMode::NON_ZERO_DURATION); |
| TabletModeControllerTestApi().EnterTabletMode(); |
| } |
| |
| // Tests that there is no crash after a window is placed such that the nudge |
| // widget should be offscreen. Regression test for http://b/282994793. |
| TEST_F(MultitaskMenuNudgeControllerTest, |
| NoCrashAfterActivatingMostlyOffscreenWindowMultidisplay) { |
| // Crash is multidisplay related since it involves switching root windows. |
| UpdateDisplay("1600x1000,1601+0-1200x1000"); |
| |
| // Create two windows so we can reactivate `window2` to simulate the crash |
| // because the window manager will shift `window2` onscreen if we try to |
| // create it offscreen directly. |
| auto window1 = CreateAppWindow(gfx::Rect(300, 300)); |
| auto window2 = CreateAppWindow(gfx::Rect(1000, 300)); |
| |
| // Place `window2` mostly offscreen on primary display, such that on |
| // activation, the nudge widget should not be seen. |
| window2->SetBounds(gfx::Rect(1400, 0, 1000, 300)); |
| ASSERT_EQ(Shell::GetAllRootWindows()[0], window2->GetRootWindow()); |
| |
| // The nudge widget was shown on `window1` since it was created first. Dismiss |
| // it and advance the clock so it will show on the next window activation. |
| ASSERT_TRUE(GetNudgeWidgetForWindow(window1.get())); |
| FireDismissNudgeTimer(window1.get()); |
| wm::ActivateWindow(window1.get()); |
| test_clock_.Advance(base::Hours(26)); |
| |
| // Activate `window2`. Verify that the nudge widget is not created since the |
| // anchor is invisible. |
| wm::ActivateWindow(window2.get()); |
| EXPECT_FALSE(GetNudgeWidgetForWindow(window2.get())); |
| } |
| |
| TEST_F(MultitaskMenuNudgeControllerTest, NudgeTimeout) { |
| auto window = CreateAppWindow(gfx::Rect(300, 300)); |
| ASSERT_TRUE(GetNudgeWidgetForWindow(window.get())); |
| |
| FireDismissNudgeTimer(window.get()); |
| EXPECT_FALSE(GetNudgeWidgetForWindow(window.get())); |
| } |
| |
| TEST_F(MultitaskMenuNudgeControllerTest, NoNudgeForNewUser) { |
| chromeos::MultitaskMenuNudgeController::SetSuppressNudgeForTesting(false); |
| |
| user_manager::TypedScopedUserManager<user_manager::FakeUserManager> |
| fake_user_manager{std::make_unique<user_manager::FakeUserManager>()}; |
| fake_user_manager->SetIsCurrentUserNew(true); |
| |
| auto window = CreateAppWindow(gfx::Rect(300, 300)); |
| EXPECT_FALSE(GetNudgeWidgetForWindow(window.get())); |
| } |
| |
| TEST_F(MultitaskMenuNudgeControllerTest, Metrics) { |
| base::HistogramTester histogram_tester; |
| |
| // Create and activate a window. Test the histogram is recorded. |
| auto window = CreateAppWindow(gfx::Rect(300, 300)); |
| ASSERT_TRUE(GetNudgeWidgetForWindow(window.get())); |
| EXPECT_EQ(1, histogram_tester.GetBucketCount( |
| chromeos::kNotifierFrameworkNudgeShownCountHistogram, |
| NudgeCatalogName::kMultitaskMenuClamshell)); |
| |
| // Simulate opening the multitask menu within 1 minute. Test that the |
| // "Within1m" histogram is recorded. |
| test_clock_.Advance(base::Seconds(50)); |
| GetNudgeControllerForWindow(window.get()) |
| ->OnMenuOpened(/*tablet_mode=*/false); |
| |
| const std::string kHistogramPrefix = |
| "Ash.NotifierFramework.Nudge.TimeToAction."; |
| base::HistogramTester::CountsMap expected_counts; |
| expected_counts[kHistogramPrefix + "Within1m"] = 1; |
| EXPECT_THAT(histogram_tester.GetTotalCountsForPrefix(kHistogramPrefix), |
| testing::ContainerEq(expected_counts)); |
| |
| // Once the user opens the multitask menu, the nudge is no longer shown. |
| // Forcefully reset it so we can proceed to the next test. Also advance the |
| // clock as the nudge only shows after 24 hours have elapsed. |
| Shell::Get()->session_controller()->GetActivePrefService()->SetInteger( |
| prefs::kMultitaskMenuNudgeClamshellShownCount, 0); |
| test_clock_.Advance(base::Days(2)); |
| |
| // Destroy the window and recreate and activate it. |
| window.reset(); |
| window = CreateAppWindow(gfx::Rect(300, 300)); |
| ASSERT_TRUE(GetNudgeWidgetForWindow(window.get())); |
| EXPECT_EQ(2, histogram_tester.GetBucketCount( |
| chromeos::kNotifierFrameworkNudgeShownCountHistogram, |
| NudgeCatalogName::kMultitaskMenuClamshell)); |
| |
| // Simulate opening the multitask menu within 1 hour. Test that the "Within1h" |
| // histogram is recorded. |
| test_clock_.Advance(base::Minutes(50)); |
| GetNudgeControllerForWindow(window.get()) |
| ->OnMenuOpened(/*tablet_mode=*/false); |
| expected_counts[kHistogramPrefix + "Within1h"] = 1; |
| EXPECT_THAT(histogram_tester.GetTotalCountsForPrefix(kHistogramPrefix), |
| testing::ContainerEq(expected_counts)); |
| |
| Shell::Get()->session_controller()->GetActivePrefService()->SetInteger( |
| prefs::kMultitaskMenuNudgeClamshellShownCount, 0); |
| test_clock_.Advance(base::Days(2)); |
| |
| window.reset(); |
| window = CreateAppWindow(gfx::Rect(300, 300)); |
| ASSERT_TRUE(GetNudgeWidgetForWindow(window.get())); |
| EXPECT_EQ(3, histogram_tester.GetBucketCount( |
| chromeos::kNotifierFrameworkNudgeShownCountHistogram, |
| NudgeCatalogName::kMultitaskMenuClamshell)); |
| |
| // Simulate opening the multitask menu after a long time. Test that the |
| // "WithinSession" histogram is recorded. |
| test_clock_.Advance(base::Hours(50)); |
| GetNudgeControllerForWindow(window.get()) |
| ->OnMenuOpened(/*tablet_mode=*/false); |
| expected_counts[kHistogramPrefix + "WithinSession"] = 1; |
| EXPECT_THAT(histogram_tester.GetTotalCountsForPrefix(kHistogramPrefix), |
| testing::ContainerEq(expected_counts)); |
| } |
| |
| // Tests that the nudge bounds is within display bounds when the associated |
| // window is maximized. |
| TEST_F(MultitaskMenuNudgeControllerTest, ClamshellNudgeBounds) { |
| auto window = CreateAppWindow(gfx::Rect(300, 300)); |
| ASSERT_TRUE(GetNudgeWidgetForWindow(window.get())); |
| |
| WindowState::Get(window.get())->Maximize(); |
| auto* nudge_widget = GetNudgeWidgetForWindow(window.get()); |
| ASSERT_TRUE(nudge_widget); |
| EXPECT_TRUE(display::Screen::Get() |
| ->GetDisplayNearestView(window.get()) |
| .work_area() |
| .Contains(nudge_widget->GetWindowBoundsInScreen())); |
| |
| // Cleanup some state for the next test. |
| FireDismissNudgeTimer(window.get()); |
| window.reset(); |
| test_clock_.Advance(base::Hours(26)); |
| |
| // Test the same thing in RTL. |
| base::i18n::SetRTLForTesting(true); |
| window = CreateAppWindow(gfx::Rect(300, 300)); |
| WindowState::Get(window.get())->Maximize(); |
| nudge_widget = GetNudgeWidgetForWindow(window.get()); |
| ASSERT_TRUE(nudge_widget); |
| EXPECT_TRUE(display::Screen::Get() |
| ->GetDisplayNearestView(window.get()) |
| .work_area() |
| .Contains(nudge_widget->GetWindowBoundsInScreen())); |
| } |
| |
| TEST_F(MultitaskMenuNudgeControllerTest, NudgeMultiDisplay) { |
| UpdateDisplay("800x700,801+0-800x700"); |
| ASSERT_EQ(2u, Shell::GetAllRootWindows().size()); |
| |
| auto window = CreateAppWindow(gfx::Rect(300, 300)); |
| ASSERT_TRUE(GetNudgeWidgetForWindow(window.get())); |
| |
| // Move the window using the shortcut. Test that the nudge is on the correct |
| // display. |
| display_move_window_util::HandleMoveActiveWindowBetweenDisplays(); |
| EXPECT_EQ(Shell::GetAllRootWindows()[1], GetNudgeWidgetForWindow(window.get()) |
| ->GetNativeWindow() |
| ->GetRootWindow()); |
| |
| // Drag from the caption the window to the other display. The nudge should be |
| // gone, but there is no crash. |
| display_move_window_util::HandleMoveActiveWindowBetweenDisplays(); |
| auto* event_generator = GetEventGenerator(); |
| event_generator->set_current_screen_location(gfx::Point(150, 10)); |
| event_generator->PressLeftButton(); |
| event_generator->MoveMouseTo(gfx::Point(1200, 0)); |
| EXPECT_FALSE(GetNudgeWidgetForWindow(window.get())); |
| } |
| |
| // Tests that based on preferences (shown count, and last shown time), the nudge |
| // may or may not be shown. |
| TEST_F(MultitaskMenuNudgeControllerTest, NudgePreferences) { |
| auto window = CreateAppWindow(gfx::Rect(300, 300)); |
| ASSERT_TRUE(GetNudgeWidgetForWindow(window.get())); |
| FireDismissNudgeTimer(window.get()); |
| ASSERT_FALSE(GetNudgeWidgetForWindow(window.get())); |
| |
| // Create the window. This does not show the nudge as 24 hours have not |
| // elapsed since the nudge was shown. |
| window.reset(); |
| window = CreateAppWindow(gfx::Rect(300, 300)); |
| ASSERT_FALSE(GetNudgeWidgetForWindow(window.get())); |
| |
| // Create the window again after waiting 25 hours. The nudge should now show |
| // for the second time. |
| test_clock_.Advance(base::Hours(25)); |
| window.reset(); |
| window = CreateAppWindow(gfx::Rect(300, 300)); |
| ASSERT_TRUE(GetNudgeWidgetForWindow(window.get())); |
| FireDismissNudgeTimer(window.get()); |
| ASSERT_FALSE(GetNudgeWidgetForWindow(window.get())); |
| |
| // Show the nudge for a third time. This will be the last time it is shown. |
| test_clock_.Advance(base::Hours(25)); |
| window.reset(); |
| window = CreateAppWindow(gfx::Rect(300, 300)); |
| ASSERT_TRUE(GetNudgeWidgetForWindow(window.get())); |
| FireDismissNudgeTimer(window.get()); |
| ASSERT_FALSE(GetNudgeWidgetForWindow(window.get())); |
| |
| // Advance the clock and attempt to show the nudge for a fourth time. Verify |
| // that it will not show. |
| test_clock_.Advance(base::Hours(25)); |
| window.reset(); |
| window = CreateAppWindow(gfx::Rect(300, 300)); |
| EXPECT_FALSE(GetNudgeWidgetForWindow(window.get())); |
| } |
| |
| // Tests that after the multitask menu is shown, the nudge does not show |
| // anymore. |
| TEST_F(MultitaskMenuNudgeControllerTest, MenuShown) { |
| // Create a window, the nudge is shown on new window activation. |
| auto window = CreateAppWindow(gfx::Rect(300, 300)); |
| ASSERT_TRUE(GetNudgeWidgetForWindow(window.get())); |
| |
| // When opening the multitask menu, the nudge should dismiss immediately. |
| std::ignore = ShowAndWaitMultitaskMenuForWindow(window.get()); |
| EXPECT_FALSE(GetNudgeWidgetForWindow(window.get())); |
| |
| // Advance the clock and then destroy the window and create a new window. |
| // Test that the nudge does not show up. |
| test_clock_.Advance(base::Hours(25)); |
| window.reset(); |
| window = CreateAppWindow(gfx::Rect(300, 300)); |
| EXPECT_FALSE(GetNudgeWidgetForWindow(window.get())); |
| } |
| |
| // Tests that the nudge gets properly hidden after switching desks with a |
| // floated window. Regression test for http://b/276786909. |
| TEST_F(MultitaskMenuNudgeControllerTest, FloatedWindowNudge) { |
| // Create a new desk. |
| NewDesk(); |
| ASSERT_TRUE(DesksController::Get()->desks()[0]->is_active()); |
| |
| // Create a floated window, the nudge is shown on new window activation. |
| auto window = CreateAppWindow(gfx::Rect(300, 300)); |
| PressAndReleaseKey(ui::VKEY_F, ui::EF_ALT_DOWN | ui::EF_COMMAND_DOWN); |
| ASSERT_TRUE(WindowState::Get(window.get())->IsFloated()); |
| ASSERT_TRUE(GetNudgeWidgetForWindow(window.get())); |
| |
| ActivateDesk(DesksController::Get()->desks()[1].get()); |
| EXPECT_FALSE(GetNudgeWidgetForWindow(window.get())); |
| } |
| |
| // Tests that the nudge works in tablet mode, and that its bounds in screen are |
| // correct. |
| TEST_F(MultitaskMenuNudgeControllerTest, TabletNudgeBounds) { |
| TabletModeControllerTestApi().EnterTabletMode(); |
| |
| // The widget should appear the first time a window is activated. |
| auto window = CreateAppWindow(); |
| ASSERT_TRUE(GetNudgeWidgetForWindow(window.get())); |
| |
| // Test that the widget is shown at the correct bounds when the window is |
| // first created. |
| ExpectCorrectTabletNudgeBounds(window.get()); |
| |
| auto* split_view_controller = |
| SplitViewController::Get(Shell::GetPrimaryRootWindow()); |
| |
| // Tests that the widget is shown at the correct bounds when the window is |
| // snapped in the primary position. |
| split_view_controller->SnapWindow(window.get(), SnapPosition::kPrimary); |
| ASSERT_TRUE(GetNudgeWidgetForWindow(window.get())); |
| ExpectCorrectTabletNudgeBounds(window.get()); |
| |
| // Tests that the widget is shown at the correct bounds when the window is |
| // snapped in the secondary position. |
| split_view_controller->SnapWindow(window.get(), SnapPosition::kSecondary); |
| ASSERT_TRUE(GetNudgeWidgetForWindow(window.get())); |
| ExpectCorrectTabletNudgeBounds(window.get()); |
| } |
| |
| // Tests that if a window gets destroyed while the nduge is showing in tablet |
| // mode, the nudge disappears and there is no crash. |
| TEST_F(MultitaskMenuNudgeControllerTest, TabletWindowDestroyedWhileNudgeShown) { |
| TabletModeControllerTestApi().EnterTabletMode(); |
| |
| auto window = CreateAppWindow(gfx::Rect(300, 300)); |
| ASSERT_TRUE(GetNudgeWidgetForWindow(window.get())); |
| |
| window.reset(); |
| EXPECT_FALSE(GetNudgeWidgetForWindow(window.get())); |
| } |
| |
| } // namespace ash |