#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/display/screen.h"
#include "ui/gfx/geometry/size.h"
#include "ui/gfx/scoped_animation_duration_scale_mode.h"
#include "ui/wm/core/window_util.h"
namespace ash {
namespace {
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;
}
}
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();
}
}
void SetUp() override {
AshTestBase::SetUp();
chromeos::MultitaskMenuNudgeController::SetSuppressNudgeForTesting(false);
chromeos::MultitaskMenuNudgeController::SetOverrideClockForTesting(
&test_clock_);
test_clock_.Advance(base::Hours(50));
}
void TearDown() override {
chromeos::MultitaskMenuNudgeController::SetOverrideClockForTesting(nullptr);
AshTestBase::TearDown();
}
protected:
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_;
};
TEST_F(MultitaskMenuNudgeControllerTest, NoCrashAfterFullscreening) {
auto window = CreateAppWindow(gfx::Rect(300, 300));
ASSERT_TRUE(GetNudgeWidgetForWindow(window.get()));
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()));
ASSERT_TRUE(immersive_controller->IsEnabled());
ASSERT_FALSE(immersive_controller->IsRevealed());
WindowState::Get(window.get())->OnWMEvent(&event);
EXPECT_FALSE(GetNudgeWidgetForWindow(window.get()));
}
TEST_F(MultitaskMenuNudgeControllerTest,
NoCrashAfterFloatingFromMultitaskMenu) {
auto window = CreateAppWindow(gfx::Rect(300, 300));
ASSERT_TRUE(GetNudgeWidgetForWindow(window.get()));
chromeos::MultitaskMenu* multitask_menu =
ShowAndWaitMultitaskMenuForWindow(window.get());
LeftClickOn(
chromeos::MultitaskMenuViewTestApi(multitask_menu->multitask_menu_view())
.GetFloatButton());
EXPECT_TRUE(WindowState::Get(window.get())->IsFloated());
}
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());
FireDismissNudgeTimer(window.get());
test_clock_.Advance(base::Hours(26));
gfx::ScopedAnimationDurationScaleMode scale_mode(
gfx::ScopedAnimationDurationScaleMode::NON_ZERO_DURATION);
TabletModeControllerTestApi().EnterTabletMode();
}
TEST_F(MultitaskMenuNudgeControllerTest,
NoCrashAfterActivatingMostlyOffscreenWindowMultidisplay) {
UpdateDisplay("1600x1000,1601+0-1200x1000");
auto window1 = CreateAppWindow(gfx::Rect(300, 300));
auto window2 = CreateAppWindow(gfx::Rect(1000, 300));
window2->SetBounds(gfx::Rect(1400, 0, 1000, 300));
ASSERT_EQ(Shell::GetAllRootWindows()[0], window2->GetRootWindow());
ASSERT_TRUE(GetNudgeWidgetForWindow(window1.get()));
FireDismissNudgeTimer(window1.get());
wm::ActivateWindow(window1.get());
test_clock_.Advance(base::Hours(26));
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;
auto window = CreateAppWindow(gfx::Rect(300, 300));
ASSERT_TRUE(GetNudgeWidgetForWindow(window.get()));
EXPECT_EQ(1, histogram_tester.GetBucketCount(
chromeos::kNotifierFrameworkNudgeShownCountHistogram,
NudgeCatalogName::kMultitaskMenuClamshell));
test_clock_.Advance(base::Seconds(50));
GetNudgeControllerForWindow(window.get())
->OnMenuOpened(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));
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(2, histogram_tester.GetBucketCount(
chromeos::kNotifierFrameworkNudgeShownCountHistogram,
NudgeCatalogName::kMultitaskMenuClamshell));
test_clock_.Advance(base::Minutes(50));
GetNudgeControllerForWindow(window.get())
->OnMenuOpened(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));
test_clock_.Advance(base::Hours(50));
GetNudgeControllerForWindow(window.get())
->OnMenuOpened(false);
expected_counts[kHistogramPrefix + "WithinSession"] = 1;
EXPECT_THAT(histogram_tester.GetTotalCountsForPrefix(kHistogramPrefix),
testing::ContainerEq(expected_counts));
}
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()));
FireDismissNudgeTimer(window.get());
window.reset();
test_clock_.Advance(base::Hours(26));
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()));
display_move_window_util::HandleMoveActiveWindowBetweenDisplays();
EXPECT_EQ(Shell::GetAllRootWindows()[1], GetNudgeWidgetForWindow(window.get())
->GetNativeWindow()
->GetRootWindow());
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()));
}
TEST_F(MultitaskMenuNudgeControllerTest, NudgePreferences) {
auto window = CreateAppWindow(gfx::Rect(300, 300));
ASSERT_TRUE(GetNudgeWidgetForWindow(window.get()));
FireDismissNudgeTimer(window.get());
ASSERT_FALSE(GetNudgeWidgetForWindow(window.get()));
window.reset();
window = CreateAppWindow(gfx::Rect(300, 300));
ASSERT_FALSE(GetNudgeWidgetForWindow(window.get()));
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()));
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()));
test_clock_.Advance(base::Hours(25));
window.reset();
window = CreateAppWindow(gfx::Rect(300, 300));
EXPECT_FALSE(GetNudgeWidgetForWindow(window.get()));
}
TEST_F(MultitaskMenuNudgeControllerTest, MenuShown) {
auto window = CreateAppWindow(gfx::Rect(300, 300));
ASSERT_TRUE(GetNudgeWidgetForWindow(window.get()));
std::ignore = ShowAndWaitMultitaskMenuForWindow(window.get());
EXPECT_FALSE(GetNudgeWidgetForWindow(window.get()));
test_clock_.Advance(base::Hours(25));
window.reset();
window = CreateAppWindow(gfx::Rect(300, 300));
EXPECT_FALSE(GetNudgeWidgetForWindow(window.get()));
}
TEST_F(MultitaskMenuNudgeControllerTest, FloatedWindowNudge) {
NewDesk();
ASSERT_TRUE(DesksController::Get()->desks()[0]->is_active());
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()));
}
TEST_F(MultitaskMenuNudgeControllerTest, TabletNudgeBounds) {
TabletModeControllerTestApi().EnterTabletMode();
auto window = CreateAppWindow();
ASSERT_TRUE(GetNudgeWidgetForWindow(window.get()));
ExpectCorrectTabletNudgeBounds(window.get());
auto* split_view_controller =
SplitViewController::Get(Shell::GetPrimaryRootWindow());
split_view_controller->SnapWindow(window.get(), SnapPosition::kPrimary);
ASSERT_TRUE(GetNudgeWidgetForWindow(window.get()));
ExpectCorrectTabletNudgeBounds(window.get());
split_view_controller->SnapWindow(window.get(), SnapPosition::kSecondary);
ASSERT_TRUE(GetNudgeWidgetForWindow(window.get()));
ExpectCorrectTabletNudgeBounds(window.get());
}
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()));
}
}