| // Copyright 2024 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/app_list/apps_collections_controller.h" |
| |
| #include <memory> |
| #include <optional> |
| #include <string> |
| #include <tuple> |
| #include <utility> |
| |
| #include "ash/app_list/test/app_list_test_helper.h" |
| #include "ash/app_list/views/app_list_bubble_apps_collections_page.h" |
| #include "ash/app_list/views/app_list_bubble_apps_page.h" |
| #include "ash/app_list/views/apps_collections_dismiss_dialog.h" |
| #include "ash/app_list/views/apps_grid_context_menu.h" |
| #include "ash/app_list/views/paged_apps_grid_view.h" |
| #include "ash/app_list/views/search_result_page_anchored_dialog.h" |
| #include "ash/app_menu/app_menu_model_adapter.h" |
| #include "ash/public/cpp/app_list/app_list_features.h" |
| #include "ash/public/cpp/ash_prefs.h" |
| #include "ash/root_window_controller.h" |
| #include "ash/shell.h" |
| #include "ash/test/ash_test_base.h" |
| #include "ash/wm/tablet_mode/tablet_mode_controller_test_api.h" |
| #include "base/test/metrics/histogram_tester.h" |
| #include "base/test/scoped_feature_list.h" |
| #include "ui/compositor/scoped_animation_duration_scale_mode.h" |
| #include "ui/views/controls/button/label_button.h" |
| #include "ui/views/controls/menu/menu_item_view.h" |
| #include "ui/views/controls/menu/submenu_view.h" |
| #include "ui/views/test/widget_test.h" |
| |
| namespace ash { |
| namespace { |
| |
| class AppsCollectionsControllerTest : public NoSessionAshTestBase { |
| public: |
| AppsCollectionsControllerTest() { |
| scoped_feature_list_.InitWithFeatures({app_list_features::kAppsCollections}, |
| {}); |
| } |
| |
| // NoSessionAshTestBase: |
| void SetUp() override { |
| NoSessionAshTestBase::SetUp(); |
| |
| GetTestAppListClient()->set_is_new_user(true); |
| SimulateNewUserFirstLogin("primary@test"); |
| } |
| |
| private: |
| base::test::ScopedFeatureList scoped_feature_list_; |
| }; |
| |
| TEST_F(AppsCollectionsControllerTest, |
| ShowAppsPageOnFirstShowAfterDismissingNudge) { |
| auto* helper = GetAppListTestHelper(); |
| helper->ShowAppList(); |
| base::HistogramTester histograms; |
| |
| histograms.ExpectBucketCount( |
| "Apps.AppList.AppsCollections.DismissedReason", |
| AppsCollectionsController::DismissReason::kExitNudge, 0); |
| |
| auto* apps_collections_page = helper->GetBubbleAppsCollectionsPage(); |
| AppListToastContainerView* toast_container = |
| apps_collections_page->GetToastContainerViewForTest(); |
| EXPECT_TRUE(toast_container->IsToastVisible()); |
| |
| // Click on close button to dismiss the toast. |
| LeftClickOn(toast_container->GetToastButton()); |
| EXPECT_FALSE(toast_container->IsToastVisible()); |
| |
| // Apps page is not visible. |
| EXPECT_FALSE(apps_collections_page->GetVisible()); |
| |
| histograms.ExpectBucketCount( |
| "Apps.AppList.AppsCollections.DismissedReason", |
| AppsCollectionsController::DismissReason::kExitNudge, 1); |
| |
| helper->Dismiss(); |
| helper->ShowAppList(); |
| |
| // Apps page is not visible. |
| EXPECT_FALSE(apps_collections_page->GetVisible()); |
| EXPECT_TRUE(helper->GetBubbleAppsPage()->GetVisible()); |
| |
| histograms.ExpectBucketCount( |
| "Apps.AppList.AppsCollections.DismissedReason", |
| AppsCollectionsController::DismissReason::kExitNudge, 1); |
| } |
| |
| TEST_F(AppsCollectionsControllerTest, ShowAppsPageAfterSortingGrid) { |
| auto* helper = GetAppListTestHelper(); |
| helper->ShowAppList(); |
| base::HistogramTester histograms; |
| |
| histograms.ExpectBucketCount( |
| "Apps.AppList.AppsCollections.DismissedReason", |
| AppsCollectionsController::DismissReason::kSorting, 0); |
| |
| auto* apps_collections_page = helper->GetBubbleAppsCollectionsPage(); |
| AppsGridContextMenu* context_menu = |
| apps_collections_page->context_menu_for_test(); |
| EXPECT_FALSE(context_menu->IsMenuShowing()); |
| |
| // Get a point in `apps_collections_page` that doesn't have an item on it. |
| const gfx::Point empty_space = |
| apps_collections_page->GetBoundsInScreen().CenterPoint(); |
| |
| // Open the menu to test the alphabetical sort option. |
| GetEventGenerator()->MoveMouseTo(empty_space); |
| GetEventGenerator()->ClickRightButton(); |
| EXPECT_TRUE(context_menu->IsMenuShowing()); |
| |
| // Click on any reorder option and accept the dialog. |
| views::MenuItemView* reorder_option = |
| context_menu->root_menu_item_view()->GetSubmenu()->GetMenuItemAt(1); |
| LeftClickOn(reorder_option); |
| ASSERT_TRUE(helper->GetBubbleSearchPageDialog()); |
| |
| views::Widget* widget = helper->GetBubbleSearchPageDialog()->widget(); |
| views::WidgetDelegate* widget_delegate = widget->widget_delegate(); |
| views::test::WidgetDestroyedWaiter widget_waiter(widget); |
| LeftClickOn(static_cast<AppsCollectionsDismissDialog*>(widget_delegate) |
| ->accept_button_for_test()); |
| widget_waiter.Wait(); |
| |
| // Apps collections page is not visible. |
| EXPECT_FALSE(apps_collections_page->GetVisible()); |
| EXPECT_EQ(AppListSortOrder::kNameAlphabetical, |
| helper->model()->requested_sort_order()); |
| |
| // Apps page is not visible. |
| EXPECT_FALSE(apps_collections_page->GetVisible()); |
| EXPECT_TRUE(helper->GetBubbleAppsPage()->GetVisible()); |
| |
| histograms.ExpectBucketCount( |
| "Apps.AppList.AppsCollections.DismissedReason", |
| AppsCollectionsController::DismissReason::kSorting, 1); |
| } |
| |
| // Tests that sorting on tablet mode updates apps collections. |
| TEST_F(AppsCollectionsControllerTest, |
| AppListSortOnTabletModeUpdatesAppsCollections) { |
| auto* helper = GetAppListTestHelper(); |
| helper->AddAppListItemsWithCollection(AppCollection::kEntertainment, 2); |
| helper->ShowAppList(); |
| EXPECT_EQ(AppListSortOrder::kCustom, helper->model()->requested_sort_order()); |
| |
| // Apps collections page is visible. |
| EXPECT_TRUE(helper->GetBubbleAppsCollectionsPage()->GetVisible()); |
| EXPECT_FALSE(helper->GetBubbleAppsPage()->GetVisible()); |
| |
| // Enter tablet mode. |
| TabletModeControllerTestApi().EnterTabletMode(); |
| |
| auto* apps_grid_view = helper->GetRootPagedAppsGridView(); |
| // Get a point in `apps_grid` that doesn't have an item on it. |
| const gfx::Point empty_space = |
| apps_grid_view->GetBoundsInScreen().CenterPoint(); |
| |
| // Open the menu to test the alphabetical sort option. |
| ui::test::EventGenerator* generator = GetEventGenerator(); |
| ui::GestureEvent long_press( |
| empty_space.x(), empty_space.y(), 0, base::TimeTicks(), |
| ui::GestureEventDetails(ui::EventType::kGestureLongPress)); |
| generator->Dispatch(&long_press); |
| GetAppListTestHelper()->WaitUntilIdle(); |
| |
| AppMenuModelAdapter* context_menu = |
| Shell::GetPrimaryRootWindowController()->menu_model_adapter_for_testing(); |
| ASSERT_TRUE(context_menu); |
| EXPECT_TRUE(context_menu->IsShowingMenu()); |
| |
| // Cache the current context menu view. |
| views::MenuItemView* reorder_submenu = |
| context_menu->root_for_testing()->GetSubmenu()->GetMenuItemAt(2); |
| ASSERT_EQ(reorder_submenu->title(), u"Sort by"); |
| GetEventGenerator()->GestureTapAt( |
| reorder_submenu->GetBoundsInScreen().CenterPoint()); |
| |
| // Click on any reorder option and accept the dialog. |
| views::MenuItemView* reorder_option = |
| reorder_submenu->GetSubmenu()->GetMenuItemAt(0); |
| ASSERT_EQ(reorder_option->title(), u"Name"); |
| GetEventGenerator()->GestureTapAt( |
| reorder_option->GetBoundsInScreen().CenterPoint()); |
| helper->WaitUntilIdle(); |
| EXPECT_EQ(AppListSortOrder::kNameAlphabetical, |
| helper->model()->requested_sort_order()); |
| |
| helper->model()->RequestCommitTemporarySortOrder(); |
| |
| // Leave tablet mode. |
| TabletModeControllerTestApi().LeaveTabletMode(); |
| |
| helper->ShowAppList(); |
| |
| // Apps collections page is visible. |
| EXPECT_FALSE(helper->GetBubbleAppsCollectionsPage()->GetVisible()); |
| EXPECT_TRUE(helper->GetBubbleAppsPage()->GetVisible()); |
| } |
| |
| // Verifies that the apps collections is not shown after the user logs back in |
| // again. |
| TEST_F(AppsCollectionsControllerTest, AppsCollectionsDismissedAfterRestart) { |
| auto* helper = GetAppListTestHelper(); |
| helper->ShowAppList(); |
| |
| EXPECT_TRUE(helper->GetBubbleAppsCollectionsPage()->GetVisible()); |
| |
| // Logout and simulate that the user logs back in again. |
| helper->Dismiss(); |
| ClearLogin(); |
| SimulateUserLogin({"primary@test"}); |
| |
| // The bubble should not be shown. |
| helper->ShowAppList(); |
| EXPECT_FALSE(helper->GetBubbleAppsCollectionsPage()->GetVisible()); |
| } |
| |
| // Class for tests of the `AppsCollectionsController` which are |
| // concerned with user eligibility, parameterized by: |
| // (a) whether the user should be considered "new" locally |
| // (b) whether the user is managed |
| // (c) the user type. |
| class AppsCollectionsControllerUserElegibilityTest |
| : public NoSessionAshTestBase, |
| public ::testing::WithParamInterface<std::tuple< |
| /*is_new_user_locally=*/bool, |
| /*is_managed_user=*/bool, |
| user_manager::UserType, |
| /*is_user_first_login_to_chromeos=*/std::optional<bool>>> { |
| public: |
| AppsCollectionsControllerUserElegibilityTest() { |
| scoped_feature_list_.InitWithFeatures({app_list_features::kAppsCollections}, |
| {}); |
| } |
| |
| // NoSessionAshTestBase: |
| void SetUp() override { |
| NoSessionAshTestBase::SetUp(); |
| GetTestAppListClient()->set_is_new_user(IsUserFirstLogInToChromeOS()); |
| } |
| |
| // Returns the user type based on test parameterization. |
| user_manager::UserType GetUserType() const { return std::get<2>(GetParam()); } |
| |
| // Returns whether the user is managed based on test parameterization. |
| bool IsManagedUser() const { return std::get<1>(GetParam()); } |
| |
| // Returns whether the user should be considered "new" locally based on test |
| // parameterization. |
| bool IsNewUserLocally() const { return std::get<0>(GetParam()); } |
| |
| // Returns whether the user should be considered "new" across all devices |
| // based on test parameterization. |
| std::optional<bool> IsUserFirstLogInToChromeOS() const { |
| return std::get<3>(GetParam()); |
| } |
| |
| private: |
| base::test::ScopedFeatureList scoped_feature_list_; |
| }; |
| |
| INSTANTIATE_TEST_SUITE_P( |
| All, |
| AppsCollectionsControllerUserElegibilityTest, |
| ::testing::Combine( |
| /*is_new_user_locally=*/::testing::Bool(), |
| /*is_managed_user=*/::testing::Bool(), |
| ::testing::Values(user_manager::UserType::kChild, |
| user_manager::UserType::kGuest, |
| user_manager::UserType::kKioskChromeApp, |
| user_manager::UserType::kPublicAccount, |
| user_manager::UserType::kRegular, |
| user_manager::UserType::kKioskWebApp), |
| /*is_user_first_login_to_chromeos=*/ |
| ::testing::Values(std::make_optional(true), |
| std::make_optional(false), |
| std::nullopt))); |
| |
| TEST_P(AppsCollectionsControllerUserElegibilityTest, EnforcesUserEligibility) { |
| // A user is eligible for showing AppsCollections if and only if the user |
| // satisfies the following conditions: |
| // (1) known to be "new" locally, and |
| // (2) not a managed user, and |
| // (3) a regular user. |
| // (4) a known to be 'new' user across the ChromeOS ecosystem. |
| const bool is_user_eligibility_expected = |
| IsNewUserLocally() && !IsManagedUser() && |
| GetUserType() == user_manager::UserType::kRegular && |
| IsUserFirstLogInToChromeOS().value_or(false); |
| |
| // Login based on test parameterization. |
| SimulateUserLogin({.display_email = "primary@test", |
| .user_type = GetUserType(), |
| .is_new_profile = IsNewUserLocally(), |
| .is_account_managed = IsManagedUser()}); |
| auto* helper = GetAppListTestHelper(); |
| helper->ShowAppList(); |
| |
| auto* apps_collections_page = helper->GetBubbleAppsCollectionsPage(); |
| EXPECT_EQ(apps_collections_page->GetVisible(), is_user_eligibility_expected); |
| } |
| |
| // Verifies that regardless of the user elegibility parameters, secondary users |
| // are not presented with apps collections. This is a self-imposed restriction. |
| TEST_P(AppsCollectionsControllerUserElegibilityTest, SecondaryUserNotElegible) { |
| SimulateNewUserFirstLogin("primary@test"); |
| // ogin a user based on test parameterization. |
| SimulateUserLogin({.display_email = "secondary@test", |
| .user_type = GetUserType(), |
| .is_new_profile = IsNewUserLocally(), |
| .is_account_managed = IsManagedUser()}); |
| |
| auto* helper = GetAppListTestHelper(); |
| helper->ShowAppList(); |
| |
| auto* apps_collections_page = helper->GetBubbleAppsCollectionsPage(); |
| EXPECT_FALSE(apps_collections_page->GetVisible()); |
| } |
| |
| // Class for tests of the `AppsCollectionsController` which are |
| // concerned with the experiment prefs. |
| class AppsCollectionsControllerPrefTest |
| : public NoSessionAshTestBase, |
| public ::testing::WithParamInterface<std::tuple< |
| /*is_apps_collections_active=*/bool, |
| /*is_counterfactual=*/bool, |
| /*is_modified_order=*/bool>> { |
| public: |
| AppsCollectionsControllerPrefTest() { |
| if (IsAppsCollectionsEnabled()) { |
| scoped_feature_list_.InitAndEnableFeatureWithParameters( |
| app_list_features::kAppsCollections, |
| {{"is-counterfactual", |
| IsAppsCollectionsEnabledCounterfactually() ? "true" : "false"}, |
| {"is-modified-order", |
| IsAppsCollectionsEnabledWithModifiedOrder() ? "true" : "false"}}); |
| } else { |
| scoped_feature_list_.InitAndDisableFeature( |
| app_list_features::kAppsCollections); |
| } |
| } |
| |
| // NoSessionAshTestBase: |
| void SetUp() override { |
| NoSessionAshTestBase::SetUp(); |
| |
| GetTestAppListClient()->set_is_new_user(true); |
| SimulateUserLogin( |
| {.display_email = "primary@test", .is_new_profile = true}); |
| } |
| |
| // Returns whether apps collectionns feature is enabled. |
| bool IsAppsCollectionsEnabled() const { return std::get<0>(GetParam()); } |
| |
| // Returns whether apps collections feature is enabled counterfactrually. |
| bool IsAppsCollectionsEnabledCounterfactually() const { |
| return IsAppsCollectionsEnabled() && std::get<1>(GetParam()); |
| } |
| // Returns whether apps collections feature is enabled counterfactrually. |
| bool IsAppsCollectionsEnabledWithModifiedOrder() const { |
| return IsAppsCollectionsEnabled() && std::get<2>(GetParam()); |
| } |
| |
| AppsCollectionsController::ExperimentalArm GetExpectedExperimentalArm() { |
| if (!IsAppsCollectionsEnabled()) { |
| return AppsCollectionsController::ExperimentalArm::kControl; |
| } |
| |
| if (IsAppsCollectionsEnabledCounterfactually()) { |
| return AppsCollectionsController::ExperimentalArm::kCounterfactual; |
| } |
| |
| return IsAppsCollectionsEnabledWithModifiedOrder() |
| ? AppsCollectionsController::ExperimentalArm::kModifiedOrder |
| : AppsCollectionsController::ExperimentalArm::kEnabled; |
| } |
| |
| private: |
| base::test::ScopedFeatureList scoped_feature_list_; |
| }; |
| |
| INSTANTIATE_TEST_SUITE_P(All, |
| AppsCollectionsControllerPrefTest, |
| ::testing::Combine( |
| /*is_apps_collections_active=*/::testing::Bool(), |
| /*is_counterfactual=*/::testing::Bool(), |
| /*is_modified_order=*/::testing::Bool())); |
| |
| // Verifies that the experimental arm for the user is calculated and stored |
| // correctly. |
| TEST_P(AppsCollectionsControllerPrefTest, GetExperimentalArm) { |
| EXPECT_EQ(AppsCollectionsController::Get()->GetUserExperimentalArm(), |
| AppsCollectionsController::ExperimentalArm::kDefaultValue); |
| |
| auto* helper = GetAppListTestHelper(); |
| helper->ShowAppList(); |
| |
| EXPECT_EQ(AppsCollectionsController::Get()->GetUserExperimentalArm(), |
| GetExpectedExperimentalArm()); |
| } |
| |
| } // namespace |
| } // namespace ash |