// 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 <memory>
#include <string>
#include <tuple>
#include <vector>
#include "ash/app_list/app_list_controller_impl.h"
#include "ash/app_list/model/app_list_item.h"
#include "ash/app_list/model/app_list_test_model.h"
#include "ash/app_list/model/search/search_model.h"
#include "ash/app_list/test/app_list_test_helper.h"
#include "ash/app_list/views/app_list_item_view.h"
#include "ash/app_list/views/apps_grid_view_test_api.h"
#include "ash/app_list/views/paged_apps_grid_view.h"
#include "ash/app_list/views/scrollable_apps_grid_view.h"
#include "ash/constants/ash_features.h"
#include "ash/drag_drop/drag_drop_controller.h"
#include "ash/drag_drop/drag_drop_controller_test_api.h"
#include "ash/public/cpp/app_list/app_list_features.h"
#include "ash/public/cpp/test/shell_test_api.h"
#include "ash/shell.h"
#include "ash/test/ash_test_base.h"
#include "ash/test/pixel/ash_pixel_differ.h"
#include "ash/test/pixel/ash_pixel_test_init_params.h"
#include "ash/wm/tablet_mode/tablet_mode_controller_test_api.h"
#include "base/functional/bind.h"
#include "base/strings/string_util.h"
#include "base/test/bind.h"
#include "base/test/scoped_feature_list.h"
#include "chromeos/constants/chromeos_features.h"
#include "ui/gfx/image/image_skia.h"
namespace ash {
class AppListItemViewPixelTestBase : public AshTestBase {
public:
AppListItemViewPixelTestBase(bool use_drag_drop_refactor,
bool use_folder_icon_refresh,
bool use_tablet_mode,
bool use_dense_ui,
bool use_rtl,
bool is_new_install,
bool has_notification,
bool enable_promise_icons)
: use_drag_drop_refactor_(use_drag_drop_refactor),
use_folder_icon_refresh_(use_folder_icon_refresh),
use_tablet_mode_(use_tablet_mode),
use_dense_ui_(use_dense_ui),
use_rtl_(use_rtl),
is_new_install_(is_new_install),
has_notification_(has_notification),
enable_promise_icons_(enable_promise_icons) {}
// AshTestBase:
std::optional<pixel_test::InitParams> CreatePixelTestInitParams()
const override {
pixel_test::InitParams init_params;
init_params.under_rtl = use_rtl();
return init_params;
}
// AshTestBase:
void SetUp() override {
scoped_feature_list_.InitWithFeatureStates(
{{app_list_features::kDragAndDropRefactor, use_drag_drop_refactor()},
{ash::features::kPromiseIcons, enable_promise_icons()}});
AshTestBase::SetUp();
// As per `app_list_config_provider.cc`, dense values are used for screens
// with width OR height <= 675.
UpdateDisplay(use_dense_ui() ? "800x600" : "1200x800");
if (use_drag_drop_refactor()) {
auto* drag_controller = ShellTestApi().drag_drop_controller();
drag_drop_controller_test_api_ =
std::make_unique<DragDropControllerTestApi>(drag_controller);
drag_controller->SetDisableNestedLoopForTesting(true);
}
}
void TearDown() override {
drag_drop_controller_test_api_.reset();
AshTestBase::TearDown();
}
// Creates multiple folders that contain from 1 app to `max_items` apps
// respectively.
void CreateFoldersContainingDifferentNumOfItems(int max_items) {
AppListFolderItem* folder_item;
for (int i = 1; i <= max_items; ++i) {
if (i == 1) {
folder_item = GetAppListTestHelper()->model()->CreateSingleItemFolder(
"folder_id", "item_id");
} else {
folder_item =
GetAppListTestHelper()->model()->CreateAndPopulateFolderWithApps(i);
}
// Update the notification state of the first app in the folder to
// simulate that there exists an app with notifications in the folder.
folder_item->item_list()->item_at(0)->UpdateNotificationBadge(
has_notification());
}
}
void CreateAppListItem(const std::string& name) {
AppListItem* item =
GetAppListTestHelper()->model()->CreateAndAddItem(name + "_id");
item->SetName(name);
item->SetIsNewInstall(is_new_install());
item->UpdateNotificationBadge(has_notification());
}
void ShowAppList() {
if (use_tablet_mode()) {
ash::TabletModeControllerTestApi().EnterTabletMode();
} else {
GetAppListTestHelper()->ShowAppList();
}
}
AppsGridView* GetAppsGridView() {
if (use_tablet_mode()) {
return GetAppListTestHelper()->GetRootPagedAppsGridView();
}
return GetAppListTestHelper()->GetScrollableAppsGridView();
}
AppListItemView* GetItemViewAt(size_t index) {
return GetAppsGridView()->GetItemViewAt(index);
}
std::string GenerateScreenshotName() {
std::vector<std::string> parameters = {
use_tablet_mode() ? "tablet_mode" : "clamshell_mode",
use_dense_ui() ? "dense_ui" : "regular_ui", use_rtl() ? "rtl" : "ltr",
is_new_install() ? "new_install=true" : "new_install=false",
has_notification() ? "has_notification=true"
: "has_notification=false"};
std::string stringified_params = base::JoinString(parameters, "|");
return base::JoinString({"app_list_item_view", stringified_params}, ".");
}
views::Widget* GetDraggedWidget() {
return use_drag_drop_refactor()
? drag_drop_controller_test_api_->drag_image_widget()
: GetAppsGridView()
->app_drag_icon_proxy_for_test()
->GetWidgetForTesting();
}
bool use_drag_drop_refactor() const { return use_drag_drop_refactor_; }
bool use_folder_icon_refresh() const { return use_folder_icon_refresh_; }
bool use_tablet_mode() const { return use_tablet_mode_; }
bool use_dense_ui() const { return use_dense_ui_; }
bool use_rtl() const { return use_rtl_; }
bool is_new_install() const { return is_new_install_; }
bool has_notification() const { return has_notification_; }
bool enable_promise_icons() const { return enable_promise_icons_; }
private:
const bool use_drag_drop_refactor_;
const bool use_folder_icon_refresh_;
const bool use_tablet_mode_;
const bool use_dense_ui_;
const bool use_rtl_;
const bool is_new_install_;
const bool has_notification_;
const bool enable_promise_icons_;
std::unique_ptr<DragDropControllerTestApi> drag_drop_controller_test_api_;
base::test::ScopedFeatureList scoped_feature_list_;
};
class AppListItemViewPixelTest
: public AppListItemViewPixelTestBase,
public testing::WithParamInterface<
std::tuple</*use_drag_drop_refactor=*/bool,
/*use_folder_icon_refresh=*/bool,
/*use_tablet_mode=*/bool,
/*use_dense_ui=*/bool,
/*use_rtl=*/bool,
/*is_new_install=*/bool,
/*has_notification=*/bool>> {
public:
AppListItemViewPixelTest()
: AppListItemViewPixelTestBase(
/*use_drag_drop_refactor=*/std::get<0>(GetParam()),
/*use_folder_icon_refresh=*/std::get<1>(GetParam()),
/*use_tablet_mode=*/std::get<2>(GetParam()),
/*use_dense_ui=*/std::get<3>(GetParam()),
/*use_rtl=*/std::get<4>(GetParam()),
/*is_new_install=*/std::get<5>(GetParam()),
/*has_notification=*/std::get<6>(GetParam()),
/*enable_promise_icons=*/false) {}
};
INSTANTIATE_TEST_SUITE_P(All,
AppListItemViewPixelTest,
testing::Combine(
/*use_drag_drop_refactor=*/testing::Bool(),
/*use_folder_icon_refresh=*/testing::Bool(),
/*use_tablet_mode=*/testing::Bool(),
/*use_dense_ui=*/testing::Bool(),
/*use_rtl=*/testing::Bool(),
/*is_new_install=*/testing::Bool(),
/*has_notification=*/testing::Bool()));
TEST_P(AppListItemViewPixelTest, AppListItemView) {
CreateAppListItem("App");
CreateAppListItem("App with a loooooooong name");
ShowAppList();
EXPECT_TRUE(GetPixelDiffer()->CompareUiComponentsOnPrimaryScreen(
GenerateScreenshotName(), /*revision_number=*/5, GetItemViewAt(0),
GetItemViewAt(1)));
}
// Verifies the layout of the item icons inside a folder.
TEST_P(AppListItemViewPixelTest, AppListFolderItemsLayoutInIcon) {
// Skip the case where the apps are newly installed as it doesn't change the
// folder icons.
if (!use_folder_icon_refresh() || is_new_install()) {
return;
}
// Reset any configs set by previous tests so that
// ItemIconInFolderIconMargin() in app_list_config.cc is correctly
// initialized. Can be removed if folder icon refresh is set as default.
AppListConfigProvider::Get().ResetForTesting();
// To test the item counter on folder icons, set the maximum number of the
// items in a folder to 5.
const int max_items_in_folder = 5;
CreateFoldersContainingDifferentNumOfItems(max_items_in_folder);
ShowAppList();
EXPECT_TRUE(GetPixelDiffer()->CompareUiComponentsOnPrimaryScreen(
GenerateScreenshotName(), /*revision_number=*/9, GetItemViewAt(0),
GetItemViewAt(1), GetItemViewAt(2), GetItemViewAt(3), GetItemViewAt(4)));
}
// Verifies the folder icon is extended when an app is dragged upon it.
TEST_P(AppListItemViewPixelTest, AppListFolderIconExtendedState) {
// Skip the case where the apps are newly installed as it doesn't change the
// folder icons.
if (!use_folder_icon_refresh() || is_new_install()) {
return;
}
// Reset any configs set by previous tests so that
// ItemIconInFolderIconMargin() in app_list_config.cc is correctly
// initialized. Can be removed if folder icon refresh is set as default.
AppListConfigProvider::Get().ResetForTesting();
// To test the item counter on folder icons, set the maximum number of the
// items in a folder to 5.
const int max_items_in_folder = 5;
CreateFoldersContainingDifferentNumOfItems(max_items_in_folder);
CreateAppListItem("App");
ShowAppList();
// For tablet mode, simulate that a drag starts and enter the cardified state.
if (use_tablet_mode()) {
GetAppListTestHelper()
->GetRootPagedAppsGridView()
->MaybeStartCardifiedView();
}
// Simulate that there is an app dragged onto it for each folder.
for (int i = 0; i < max_items_in_folder; ++i) {
GetItemViewAt(i)->OnDraggedViewEnter();
}
EXPECT_TRUE(GetPixelDiffer()->CompareUiComponentsOnPrimaryScreen(
GenerateScreenshotName(), /*revision_number=*/10, GetItemViewAt(0),
GetItemViewAt(1), GetItemViewAt(2), GetItemViewAt(3), GetItemViewAt(4)));
// Reset the states.
for (int i = 0; i < max_items_in_folder; ++i) {
GetItemViewAt(i)->OnDraggedViewExit();
}
if (use_tablet_mode()) {
GetAppListTestHelper()->GetRootPagedAppsGridView()->MaybeEndCardifiedView();
}
}
// Vefifies the dragged folder icon proxy is correctly created.
TEST_P(AppListItemViewPixelTest, DraggedAppListFolderIcon) {
// Skip the case where the apps are newly installed or have notifications as
// they don't change the folder icons.
if (!use_folder_icon_refresh() || is_new_install() || has_notification()) {
return;
}
// Reset any configs set by previous tests so that
// ItemIconInFolderIconMargin() in app_list_config.cc is correctly
// initialized. Can be removed if folder icon refresh is set as default.
AppListConfigProvider::Get().ResetForTesting();
// Set the maximum number of the items in a folder that we want to test to 4.
const int max_items_in_folder = 4;
CreateFoldersContainingDifferentNumOfItems(max_items_in_folder);
ShowAppList();
auto* event_generator = GetEventGenerator();
AppsGridView* apps_grid_view = GetAppsGridView();
gfx::Point grid_center = apps_grid_view->GetBoundsInScreen().CenterPoint();
// Create a folder item view list for folders with different number of items.
// This is used instead of GetItemViewAt() to prevent reordering while
// dragging each folder.
std::vector<AppListItemView*> folder_list;
for (int i = 0; i < max_items_in_folder; ++i) {
folder_list.push_back(GetItemViewAt(i));
}
auto verify_folder_widget =
[&](int number_of_items) {
std::string filename =
base::NumberToString(number_of_items) + "_items_folder";
EXPECT_TRUE(GetPixelDiffer()->CompareUiComponentsOnPrimaryScreen(
base::JoinString({GenerateScreenshotName(), filename}, "."),
/*revision_number=*/6, GetDraggedWidget()));
};
for (size_t i = 0; i < max_items_in_folder; ++i) {
gfx::Point folder_icon_center =
folder_list[i]->GetIconBoundsInScreen().CenterPoint();
std::list<base::OnceClosure> tasks;
tasks.push_back(base::BindLambdaForTesting([&]() {
if (use_tablet_mode()) {
event_generator->PressTouch(folder_icon_center);
folder_list[i]->FireTouchDragTimerForTest();
} else {
event_generator->MoveMouseTo(folder_icon_center);
event_generator->PressLeftButton();
folder_list[i]->FireMouseDragTimerForTest();
}
}));
tasks.push_back(base::BindLambdaForTesting([&]() {
if (use_tablet_mode()) {
event_generator->MoveTouch(grid_center);
} else {
event_generator->MoveMouseTo(grid_center);
}
test::AppsGridViewTestApi(apps_grid_view).WaitForItemMoveAnimationDone();
}));
tasks.push_back(base::BindLambdaForTesting(
[&]() { verify_folder_widget(/*number_of_items=*/i + 1); }));
tasks.push_back(base::BindLambdaForTesting([&]() {
if (use_tablet_mode()) {
event_generator->ReleaseTouch();
} else {
event_generator->ReleaseLeftButton();
}
}));
MaybeRunDragAndDropSequenceForAppList(&tasks,
/*is_touch=*/use_tablet_mode());
}
}
class AppListViewPromiseAppPixelTest
: public AppListItemViewPixelTestBase,
public testing::WithParamInterface<std::tuple<
/*use_tablet_mode=*/bool,
/*use_dense_ui=*/bool,
/*use_rtl=*/bool>> {
public:
AppListViewPromiseAppPixelTest()
: AppListItemViewPixelTestBase(
/*use_drag_drop_refactor=*/false,
/*use_folder_icon_refresh=*/false,
/*use_tablet_mode=*/std::get<0>(GetParam()),
/*use_dense_ui=*/std::get<1>(GetParam()),
/*use_rtl=*/std::get<2>(GetParam()),
/*is_new_install=*/false,
/*has_notification=*/false,
/*enable_promise_icons=*/true) {}
AppListItem* CreateAppListPromiseItem(const std::string& name) {
return GetAppListTestHelper()->model()->CreateAndAddPromiseItem(name +
"_id");
}
};
INSTANTIATE_TEST_SUITE_P(All,
AppListViewPromiseAppPixelTest,
testing::Combine(
/*use_tablet_mode=*/testing::Bool(),
/*use_dense_ui=*/testing::Bool(),
/*use_rtl=*/testing::Bool()));
TEST_P(AppListViewPromiseAppPixelTest, PromiseAppWaiting) {
// Reset any configs set by previous tests so that
// ItemIconInFolderIconMargin() in app_list_config.cc is correctly
// initialized. Can be removed if folder icon refresh is set as default.
AppListConfigProvider::Get().ResetForTesting();
CreateAppListPromiseItem("PromiseApp");
AppListItem* placeholder = CreateAppListPromiseItem("PromiseApp_placeholder");
placeholder->SetDefaultIconAndColor(placeholder->GetDefaultIcon(),
placeholder->GetDefaultIconColor(),
/*is_placeholder_icon=*/true);
ShowAppList();
EXPECT_EQ(GetItemViewAt(0)->item()->progress(), -1.0f);
EXPECT_EQ(GetItemViewAt(0)->item()->app_status(), AppStatus::kPending);
EXPECT_EQ(GetItemViewAt(1)->item()->progress(), -1.0f);
EXPECT_EQ(GetItemViewAt(1)->item()->app_status(), AppStatus::kPending);
EXPECT_TRUE(GetPixelDiffer()->CompareUiComponentsOnPrimaryScreen(
base::JoinString({"promise_app_waiting", GenerateScreenshotName()}, "."),
/*revision_number=*/3, GetItemViewAt(0), GetItemViewAt(1)));
}
TEST_P(AppListViewPromiseAppPixelTest, PromiseAppInstalling) {
// Reset any configs set by previous tests so that
// ItemIconInFolderIconMargin() in app_list_config.cc is correctly
// initialized. Can be removed if folder icon refresh is set as default.
AppListConfigProvider::Get().ResetForTesting();
AppListItem* item = CreateAppListPromiseItem("PromiseApp");
AppListItem* placeholder = CreateAppListPromiseItem("PromiseApp_placeholder");
placeholder->SetDefaultIconAndColor(placeholder->GetDefaultIcon(),
placeholder->GetDefaultIconColor(),
/*is_placeholder_icon=*/true);
// Start install progress bar.
item->SetAppStatus(AppStatus::kInstalling);
item->SetProgress(0.8f);
placeholder->SetAppStatus(AppStatus::kInstalling);
placeholder->SetProgress(0.8f);
ShowAppList();
EXPECT_EQ(GetItemViewAt(0)->item()->progress(), 0.8f);
EXPECT_EQ(GetItemViewAt(0)->item()->app_status(), AppStatus::kInstalling);
EXPECT_EQ(GetItemViewAt(1)->item()->progress(), 0.8f);
EXPECT_EQ(GetItemViewAt(1)->item()->app_status(), AppStatus::kInstalling);
EXPECT_TRUE(GetPixelDiffer()->CompareUiComponentsOnPrimaryScreen(
base::JoinString({"promise_app_installing", GenerateScreenshotName()},
"."),
/*revision_number=*/3, GetItemViewAt(0), GetItemViewAt(1)));
}
} // namespace ash