chromium/ash/app_list/apps_collections_controller_unittest.cc

// 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::kKioskApp,
                          user_manager::UserType::kPublicAccount,
                          user_manager::UserType::kRegular,
                          user_manager::UserType::kWebKioskApp),
        /*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);
  // Add a user based on test parameterization.
  const AccountId primary_account_id = AccountId::FromUserEmail("primary@test");
  TestSessionControllerClient* const session = GetSessionControllerClient();
  session->AddUserSession(primary_account_id.GetUserEmail(), GetUserType(),
                          /*provide_pref_service=*/true,
                          /*is_new_profile=*/IsNewUserLocally(),
                          /*given_name=*/std::string(), IsManagedUser());
  session->SwitchActiveUser(primary_account_id);

  // Activate the user session.
  session->SetSessionState(session_manager::SessionState::ACTIVE);

  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");
  // Add a user based on test parameterization.
  const AccountId secondary_account_id =
      AccountId::FromUserEmail("secondary@test");
  TestSessionControllerClient* const session = GetSessionControllerClient();
  session->AddUserSession(secondary_account_id.GetUserEmail(), GetUserType(),
                          /*provide_pref_service=*/true,
                          /*is_new_profile=*/IsNewUserLocally(),
                          /*given_name=*/std::string(), IsManagedUser());
  session->SwitchActiveUser(secondary_account_id);

  // Activate the user session.
  session->SetSessionState(session_manager::SessionState::ACTIVE);

  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);
    TestSessionControllerClient* session_controller =
        GetSessionControllerClient();
    session_controller->Reset();

    const AccountId& account_id = AccountId::FromUserEmail("primary@test");

    auto user_prefs = std::make_unique<TestingPrefServiceSimple>();
    RegisterUserProfilePrefs(user_prefs->registry(), /*country=*/"",
                             /*for_test=*/true);
    session_controller->AddUserSession("primary@test",
                                       user_manager::UserType::kRegular,
                                       /*provide_pref_service=*/false,
                                       /*is_new_profile=*/true);
    GetSessionControllerClient()->SetUserPrefService(account_id,
                                                     std::move(user_prefs));
    session_controller->SwitchActiveUser(account_id);
    session_controller->SetSessionState(session_manager::SessionState::ACTIVE);
  }

  // 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