chromium/ash/app_list/views/app_list_main_view_unittest.cc

// Copyright 2013 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/views/app_list_main_view.h"

#include <list>
#include <memory>
#include <string>
#include <utility>

#include "ash/app_list/app_list_model_provider.h"
#include "ash/app_list/model/app_list_test_model.h"
#include "ash/app_list/quick_app_access_model.h"
#include "ash/app_list/test/app_list_test_helper.h"
#include "ash/app_list/views/app_list_folder_view.h"
#include "ash/app_list/views/app_list_item_view.h"
#include "ash/app_list/views/app_list_view.h"
#include "ash/app_list/views/apps_container_view.h"
#include "ash/app_list/views/apps_grid_view.h"
#include "ash/app_list/views/contents_view.h"
#include "ash/app_list/views/page_switcher.h"
#include "ash/app_list/views/paged_apps_grid_view.h"
#include "ash/app_list/views/search_box_view.h"
#include "ash/public/cpp/app_list/app_list_features.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/memory/raw_ptr.h"
#include "base/ranges/algorithm.h"
#include "base/test/bind.h"
#include "base/test/scoped_feature_list.h"
#include "ui/compositor/layer.h"
#include "ui/events/base_event_utils.h"
#include "ui/events/keycodes/keyboard_codes_posix.h"
#include "ui/events/types/event_type.h"
#include "ui/views/controls/textfield/textfield.h"
#include "ui/views/test/button_test_api.h"
#include "ui/views/test/views_test_utils.h"
#include "ui/views/view_model.h"

namespace ash {

// Parameterized by drag and drop refactor enabled/disabled.
class AppListMainViewTest : public AshTestBase,
                            public testing::WithParamInterface<bool> {
 public:
  AppListMainViewTest() : is_drag_drop_refactor_enabled_(GetParam()) {}
  AppListMainViewTest(const AppListMainViewTest& other) = delete;
  AppListMainViewTest& operator=(const AppListMainViewTest& other) = delete;
  ~AppListMainViewTest() override = default;

  // testing::Test overrides:
  void SetUp() override {
    scoped_feature_list_.InitWithFeatureState(
        app_list_features::kDragAndDropRefactor,
        is_drag_drop_refactor_enabled_);
    AshTestBase::SetUp();

    // Create and show the app list in fullscreen apps grid state.
    // Tablet mode uses a fullscreen AppListMainView.
    auto* helper = GetAppListTestHelper();
    ash::TabletModeControllerTestApi().EnterTabletMode();
    app_list_view_ = helper->GetAppListView();
  }

  test::AppListTestModel* GetTestModel() {
    return GetAppListTestHelper()->model();
  }

  // |point| is in |grid_view|'s coordinates.
  AppListItemView* GetItemViewAtPointInGrid(AppsGridView* grid_view,
                                            const gfx::Point& point) {
    const auto& entries = grid_view->view_model()->entries();
    const auto iter =
        base::ranges::find_if(entries, [&point](const auto& entry) {
          return entry.view->bounds().Contains(point);
        });
    return iter == entries.end() ? nullptr
                                 : static_cast<AppListItemView*>(iter->view);
  }

  // |point| is in |grid_view|'s coordinates.
  void SimulateUpdateDragInGridView(AppsGridView* grid_view,
                                    AppListItemView* drag_view,
                                    const gfx::Point& point) {
    // NOTE: Assumes that the app list view window bounds match the root window
    // bounds.
    gfx::Point root_window_point = point;
    views::View::ConvertPointToScreen(grid_view, &root_window_point);
    GetEventGenerator()->MoveMouseTo(root_window_point);
  }

  AppListMainView* main_view() { return app_list_view_->app_list_main_view(); }

  ContentsView* contents_view() { return main_view()->contents_view(); }

  SearchBoxView* search_box_view() { return main_view()->search_box_view(); }

  AppsGridView* GetRootGridView() {
    return contents_view()->apps_container_view()->apps_grid_view();
  }

  AppListFolderView* GetFolderView() {
    return contents_view()->apps_container_view()->app_list_folder_view();
  }

  PageSwitcher* GetPageSwitcherView() {
    return contents_view()->apps_container_view()->page_switcher();
  }

  AppsGridView* GetFolderGridView() {
    return GetFolderView()->items_grid_view();
  }

  const views::ViewModelT<AppListItemView>* GetRootViewModel() {
    return GetRootGridView()->view_model();
  }

  const views::ViewModelT<AppListItemView>* GetFolderViewModel() {
    return GetFolderGridView()->view_model();
  }

  AppListItemView* CreateAndOpenSingleItemFolder() {
    // Prepare single folder with a single item in it.
    AppListFolderItem* folder_item =
        GetTestModel()->CreateSingleItemFolder("single_item_folder", "single");
    views::test::RunScheduledLayout(GetRootGridView());
    EXPECT_EQ(folder_item,
              GetTestModel()->FindFolderItem("single_item_folder"));
    EXPECT_EQ(AppListFolderItem::kItemType, folder_item->GetItemType());

    EXPECT_EQ(1u, GetRootViewModel()->view_size());
    AppListItemView* folder_item_view =
        static_cast<AppListItemView*>(GetRootViewModel()->view_at(0));
    EXPECT_EQ(folder_item_view->item(), folder_item);

    // Click on the folder to open it.
    EXPECT_FALSE(GetFolderView()->GetVisible());
    LeftClickOn(folder_item_view);

    EXPECT_TRUE(GetFolderView()->GetVisible());

    return folder_item_view;
  }

  AppListItemView* StartDragOnItemInFolderAt(int index_in_folder) {
    DCHECK(GetAppListTestHelper()->IsInFolderView());
    views::View* item_view = GetFolderViewModel()->view_at(index_in_folder);

    AppListItemView* view = GetItemViewAtPointInGrid(
        GetFolderGridView(), item_view->bounds().CenterPoint());
    DCHECK(view);
    EXPECT_EQ(view, item_view);

    GetEventGenerator()->MoveMouseTo(
        view->GetIconBoundsInScreen().CenterPoint());
    GetEventGenerator()->PressLeftButton();
    EXPECT_TRUE(view->FireMouseDragTimerForTest());
    return view;
  }

  AppListItemView* DragItemOutsideFolder(AppListItemView* item_view) {
    DCHECK(GetAppListTestHelper()->IsInFolderView());
    // Drag the item completely outside the folder bounds.
    GetEventGenerator()->MoveMouseTo(
        GetFolderGridView()->GetBoundsInScreen().bottom_right());
    GetEventGenerator()->MoveMouseBy(10, 10);

    // Fire reparent timer, which should start when the item exits the folder
    // bounds. The timer closes the folder view.
    EXPECT_TRUE(GetFolderGridView()->FireFolderItemReparentTimerForTest());

    // Generate OnDragExit/OnDragEnter
    GetEventGenerator()->MoveMouseTo(
        GetRootGridView()->GetBoundsInScreen().CenterPoint());

    // Note: with the old behaviour, the folder item is expected to remain
    // visible so it keeps getting drag events, but it should become
    // completely transparent.
    // The drag and drop refactor, expects the folder grid view to end drag once
    // the dragged view exits the host.
    EXPECT_EQ(!is_drag_drop_refactor_enabled_, GetFolderView()->GetVisible());
    if (!is_drag_drop_refactor_enabled_) {
      EXPECT_EQ(0.0f, GetFolderGridView()->layer()->opacity());
    }
    EXPECT_TRUE(GetRootGridView()->has_dragged_item());
    EXPECT_EQ(!is_drag_drop_refactor_enabled_,
              GetFolderGridView()->has_dragged_item());
    return item_view;
  }

  void RunInitialReparentChecks() {
    EXPECT_TRUE(GetRootGridView()->GetVisible());
    EXPECT_TRUE(GetFolderView()->GetVisible());
    EXPECT_FALSE(GetRootGridView()->has_dragged_item());
    EXPECT_TRUE(GetFolderGridView()->has_dragged_item());
  }

  void ClickButton(views::Button* button) {
    views::test::ButtonTestApi(button).NotifyClick(ui::MouseEvent(
        ui::EventType::kMousePressed, gfx::Point(), gfx::Point(),
        base::TimeTicks(), ui::EF_LEFT_MOUSE_BUTTON, ui::EF_LEFT_MOUSE_BUTTON));
  }

  bool is_drag_drop_refactor_enabled() {
    return is_drag_drop_refactor_enabled_;
  }

 protected:
  raw_ptr<AppListView, DanglingUntriaged> app_list_view_ =
      nullptr;  // Owned by native widget.
 private:
  const bool is_drag_drop_refactor_enabled_;
  base::test::ScopedFeatureList scoped_feature_list_;
};

INSTANTIATE_TEST_SUITE_P(All, AppListMainViewTest, testing::Bool());

// Tests that the close button becomes invisible after close button is clicked.
TEST_P(AppListMainViewTest, CloseButtonInvisibleAfterCloseButtonClicked) {
  PressAndReleaseKey(ui::VKEY_A);
  ClickButton(search_box_view()->close_button());
  EXPECT_FALSE(
      search_box_view()->filter_and_close_button_container()->GetVisible());
}

// Tests that the search box becomes empty after close button is clicked.
TEST_P(AppListMainViewTest, SearchBoxEmptyAfterCloseButtonClicked) {
  PressAndReleaseKey(ui::VKEY_A);
  ClickButton(search_box_view()->close_button());
  EXPECT_TRUE(search_box_view()->search_box()->GetText().empty());
}

// Tests that the search box is no longer active after close button is clicked.
TEST_P(AppListMainViewTest, SearchBoxActiveAfterCloseButtonClicked) {
  PressAndReleaseKey(ui::VKEY_A);
  ClickButton(search_box_view()->close_button());
  EXPECT_FALSE(search_box_view()->is_search_box_active());
}

// Tests changing the AppListModel when switching profiles.
TEST_P(AppListMainViewTest, ModelChanged) {
  const size_t kInitialItems = 2;
  GetTestModel()->PopulateApps(kInitialItems);
  EXPECT_EQ(kInitialItems, GetRootViewModel()->view_size());

  AppListModel* old_model = GetAppListTestHelper()->model();
  SearchModel* old_search_model = GetAppListTestHelper()->search_model();
  QuickAppAccessModel* old_quick_app_access_model =
      GetAppListTestHelper()->quick_app_access_model();

  // Simulate a profile switch (which switches the app list models).
  auto search_model = std::make_unique<SearchModel>();
  auto model = std::make_unique<test::AppListTestModel>();
  auto quick_app_access_model = std::make_unique<QuickAppAccessModel>();
  const size_t kReplacementItems = 5;
  model->PopulateApps(kReplacementItems);
  AppListModelProvider::Get()->SetActiveModel(model.get(), search_model.get(),
                                              quick_app_access_model.get());
  EXPECT_EQ(kReplacementItems, GetRootViewModel()->view_size());

  // Replace the old model so observers on `model` are removed before test
  // shutdown.
  AppListModelProvider::Get()->SetActiveModel(old_model, old_search_model,
                                              old_quick_app_access_model);
}

// Tests dragging an item out of a single item folder and dropping it onto the
// page switcher. Regression test for http://crbug.com/415530/.
TEST_P(AppListMainViewTest, DragReparentItemOntoPageSwitcher) {
  AppListItemView* folder_item_view = CreateAndOpenSingleItemFolder();
  ASSERT_TRUE(folder_item_view);

  // Number of apps to populate. Should provide more than 1 page of apps (5*4 =
  // 20).
  const size_t kNumApps = 30;
  GetTestModel()->PopulateApps(kNumApps);
  views::test::RunScheduledLayout(GetRootGridView());

  EXPECT_EQ(1u, GetFolderViewModel()->view_size());
  EXPECT_EQ(kNumApps + 1, GetRootViewModel()->view_size());

  AppListItemView* dragged = StartDragOnItemInFolderAt(0);

  auto* generator = GetEventGenerator();
  std::list<base::OnceClosure> tasks;
  tasks.push_back(
      base::BindLambdaForTesting([&]() { RunInitialReparentChecks(); }));
  tasks.push_back(
      base::BindLambdaForTesting([&]() { DragItemOutsideFolder(dragged); }));
  tasks.push_back(base::BindLambdaForTesting([&]() {
    // Drag the reparent item to the page switcher.
    gfx::Point point = GetPageSwitcherView()->GetLocalBounds().CenterPoint();
    views::View::ConvertPointToTarget(GetPageSwitcherView(),
                                      GetFolderGridView(), &point);
    SimulateUpdateDragInGridView(GetFolderGridView(), dragged, point);
  }));
  tasks.push_back(
      base::BindLambdaForTesting([&]() { generator->ReleaseLeftButton(); }));
  MaybeRunDragAndDropSequenceForAppList(&tasks, /*is_touch=*/false);

  // The folder should not be destroyed.
  EXPECT_EQ(kNumApps + 1, GetRootViewModel()->view_size());
  AppListFolderItem* const folder_item =
      GetTestModel()->FindFolderItem("single_item_folder");
  ASSERT_TRUE(folder_item);
  EXPECT_EQ(1u, folder_item->item_list()->item_count());
}

// Test that an interrupted drag while reparenting an item from a folder, when
// canceled via the root grid, correctly forwards the cancelation to the drag
// occurring from the folder.
TEST_P(AppListMainViewTest, MouseDragItemOutOfFolderWithCancel) {
  CreateAndOpenSingleItemFolder();
  AppListItemView* dragged = StartDragOnItemInFolderAt(0);

  std::list<base::OnceClosure> tasks;
  tasks.push_back(
      base::BindLambdaForTesting([&]() { RunInitialReparentChecks(); }));
  tasks.push_back(
      base::BindLambdaForTesting([&]() { DragItemOutsideFolder(dragged); }));
  tasks.push_back(base::BindLambdaForTesting([&]() {
    // Now add an item to the model, not in any folder, e.g., as if by Sync.
    GetTestModel()->CreateAndAddItem("Extra");
    // The drag operation is canceled.
    EXPECT_FALSE(GetRootGridView()->has_dragged_item());
    EXPECT_FALSE(GetFolderGridView()->has_dragged_item());
  }));
  if (is_drag_drop_refactor_enabled()) {
    tasks.push_back(base::BindLambdaForTesting([&]() {
      // Required by the drag and drop controller to end the loop, since the
      // action does not cancel the drag sequence.
      GetEventGenerator()->ReleaseLeftButton();
    }));
  }
  MaybeRunDragAndDropSequenceForAppList(&tasks, /*is_touch=*/false);

  // Additional mouse move operations should be ignored.
  gfx::Point point(1, 1);
  SimulateUpdateDragInGridView(GetFolderGridView(), dragged, point);
  EXPECT_FALSE(GetRootGridView()->has_dragged_item());
  EXPECT_FALSE(GetFolderGridView()->has_dragged_item());
}

// Test that dragging an app out of a single item folder and reparenting it
// back into its original folder results in a cancelled reparent. This is a
// regression test for http://crbug.com/429083.
TEST_P(AppListMainViewTest, ReparentSingleItemOntoSelf) {
  // Add a folder with 1 item.
  AppListItemView* folder_item_view = CreateAndOpenSingleItemFolder();
  std::string folder_id = folder_item_view->item()->id();

  // Add another top level app.
  GetTestModel()->PopulateApps(1);
  gfx::Point drag_point = folder_item_view->bounds().CenterPoint();

  views::View::ConvertPointToTarget(GetRootGridView(), GetFolderGridView(),
                                    &drag_point);

  AppListItemView* dragged = StartDragOnItemInFolderAt(0);

  auto* generator = GetEventGenerator();
  std::list<base::OnceClosure> tasks;
  tasks.push_back(
      base::BindLambdaForTesting([&]() { RunInitialReparentChecks(); }));
  tasks.push_back(
      base::BindLambdaForTesting([&]() { DragItemOutsideFolder(dragged); }));
  tasks.push_back(base::BindLambdaForTesting([&]() {
    // Drag the reparent item back into its folder.
    SimulateUpdateDragInGridView(GetFolderGridView(), dragged, drag_point);
  }));
  tasks.push_back(
      base::BindLambdaForTesting([&]() { generator->ReleaseLeftButton(); }));
  MaybeRunDragAndDropSequenceForAppList(&tasks, /*is_touch=*/false);

  // The app list model should remain unchanged.
  EXPECT_EQ(2u, GetRootViewModel()->view_size());
  EXPECT_EQ(folder_id, GetRootGridView()->GetItemViewAt(0)->item()->id());
  AppListFolderItem* const folder_item =
      GetTestModel()->FindFolderItem("single_item_folder");
  ASSERT_TRUE(folder_item);
  EXPECT_EQ(1u, folder_item->item_list()->item_count());
}

}  // namespace ash