chromium/ash/app_list/views/paged_apps_grid_view_unittest.cc

// Copyright 2021 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/paged_apps_grid_view.h"

#include <list>
#include <utility>

#include "ash/app_list/app_list_controller_impl.h"
#include "ash/app_list/app_list_model_provider.h"
#include "ash/app_list/apps_grid_row_change_animator.h"
#include "ash/app_list/model/app_list_model.h"
#include "ash/app_list/test/app_list_test_helper.h"
#include "ash/app_list/test/test_focus_change_listener.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_toast_container_view.h"
#include "ash/app_list/views/app_list_toast_view.h"
#include "ash/app_list/views/apps_container_view.h"
#include "ash/app_list/views/apps_grid_view_test_api.h"
#include "ash/app_list/views/search_box_view.h"
#include "ash/public/cpp/app_list/app_list_features.h"
#include "ash/public/cpp/pagination/pagination_model.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/functional/callback_forward.h"
#include "base/functional/callback_helpers.h"
#include "base/memory/raw_ptr.h"
#include "base/test/bind.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/task_environment.h"
#include "ui/compositor/layer.h"
#include "ui/compositor/scoped_animation_duration_scale_mode.h"
#include "ui/compositor/test/layer_animation_stopped_waiter.h"
#include "ui/gfx/geometry/vector2d.h"
#include "ui/views/accessibility/view_accessibility.h"
#include "ui/views/animation/bounds_animator.h"
#include "ui/views/controls/button/label_button.h"
#include "ui/views/controls/textfield/textfield.h"

namespace ash {
namespace {

class PageFlipWaiter : public PaginationModelObserver {
 public:
  explicit PageFlipWaiter(PaginationModel* model) : model_(model) {
    model_->AddObserver(this);
  }
  ~PageFlipWaiter() override { model_->RemoveObserver(this); }

  void Wait() {
    ui_run_loop_ = std::make_unique<base::RunLoop>();
    ui_run_loop_->Run();
  }

 private:
  void SelectedPageChanged(int old_selected, int new_selected) override {
    ui_run_loop_->QuitWhenIdle();
  }

  std::unique_ptr<base::RunLoop> ui_run_loop_;
  raw_ptr<PaginationModel> model_ = nullptr;
};

}  // namespace

class PagedAppsGridViewTest : public AshTestBase,
                              public testing::WithParamInterface<bool> {
 public:
  PagedAppsGridViewTest()
      : AshTestBase(base::test::TaskEnvironment::TimeSource::MOCK_TIME) {}
  ~PagedAppsGridViewTest() override = default;

  void SetUp() override {
    scoped_feature_list_.InitWithFeatureState(
        app_list_features::kDragAndDropRefactor, GetParam());
    AshTestBase::SetUp();

    ash::TabletModeControllerTestApi().EnterTabletMode();
    grid_test_api_ = std::make_unique<test::AppsGridViewTestApi>(
        GetAppListTestHelper()->GetRootPagedAppsGridView());
  }

  AppListItemView* StartDragOnItemView(AppListItemView* item) {
    auto* generator = GetEventGenerator();
    generator->MoveMouseTo(item->GetBoundsInScreen().CenterPoint());
    generator->PressLeftButton();
    EXPECT_TRUE(item->FireMouseDragTimerForTest());
    return item;
  }

  AppListItemView* StartDragOnItemViewAtVisualIndex(int page, int slot) {
    return StartDragOnItemView(
        grid_test_api_->GetViewAtVisualIndex(page, slot));
  }

  PagedAppsGridView* GetPagedAppsGridView() {
    return GetAppListTestHelper()->GetRootPagedAppsGridView();
  }

  void UpdateLayout() {
    GetAppListTestHelper()
        ->GetAppsContainerView()
        ->GetWidget()
        ->LayoutRootViewIfNecessary();
  }

  void OnReorderAnimationDone(base::OnceClosure closure,
                              bool aborted,
                              AppListGridAnimationStatus status) {
    EXPECT_FALSE(aborted);
    EXPECT_EQ(AppListGridAnimationStatus::kReorderFadeIn, status);
    std::move(closure).Run();
  }

  int GetNumberOfRowChangeLayersForTest() {
    return GetPagedAppsGridView()
        ->row_change_animator_->GetNumberOfRowChangeLayersForTest();
  }

  bool IsRowChangeAnimatorAnimating() {
    return GetPagedAppsGridView()->row_change_animator_->IsAnimating();
  }

  void WaitForItemLayerAnimations() {
    ui::LayerAnimationStoppedWaiter animation_waiter;
    const views::ViewModelT<AppListItemView>* view_model =
        GetPagedAppsGridView()->view_model();

    for (size_t i = 0; i < view_model->view_size(); i++) {
      if (view_model->view_at(i)->layer())
        animation_waiter.Wait(view_model->view_at(i)->layer());
    }
  }

  // Sorts app list with the specified order. If `wait` is true, wait for the
  // reorder animation to complete.
  void SortAppList(const std::optional<AppListSortOrder>& order, bool wait) {
    AppListController::Get()->UpdateAppListWithNewTemporarySortOrder(
        order,
        /*animate=*/true, /*update_position_closure=*/base::DoNothing());

    if (!wait)
      return;

    base::RunLoop run_loop;
    GetAppListTestHelper()
        ->GetAppsContainerView()
        ->apps_grid_view()
        ->AddReorderCallbackForTest(base::BindRepeating(
            &PagedAppsGridViewTest::OnReorderAnimationDone,
            base::Unretained(this), run_loop.QuitClosure()));
    run_loop.Run();
  }

  std::unique_ptr<test::AppsGridViewTestApi> grid_test_api_;
  base::test::ScopedFeatureList scoped_feature_list_;
};

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

// Tests with app list nudge enabled.
class PagedAppsGridViewWithNudgeTest : public PagedAppsGridViewTest {
 public:
  void SetUp() override {
    PagedAppsGridViewTest::SetUp();
    GetAppListTestHelper()->DisableAppListNudge(false);
    // Update the toast container to make sure the nudge is shown, if required.
    GetAppListTestHelper()
        ->GetAppsContainerView()
        ->toast_container()
        ->MaybeUpdateReorderNudgeView();
  }
};

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

TEST_P(PagedAppsGridViewTest, CreatePage) {
  PagedAppsGridView* apps_grid_view =
      GetAppListTestHelper()->GetRootPagedAppsGridView();

  // 20 items fills the first page.
  GetAppListTestHelper()->AddAppItems(20);
  EXPECT_EQ(1, apps_grid_view->pagination_model()->total_pages());
  EXPECT_EQ(20, grid_test_api_->AppsOnPage(0));

  // Adding 1 item creates a second page.
  GetAppListTestHelper()->AddAppItems(1);
  EXPECT_EQ(2, apps_grid_view->pagination_model()->total_pages());
  EXPECT_EQ(20, grid_test_api_->AppsOnPage(0));
  EXPECT_EQ(1, grid_test_api_->AppsOnPage(1));
}

// Test that the first page of the root level paged apps grid holds less apps to
// accommodate the recent apps which are show at the top of the first page. Then
// check that the subsequent page holds more apps.
TEST_P(PagedAppsGridViewTest, PageMaxAppCounts) {
  GetAppListTestHelper()->AddAppItems(40);

  // Add some recent apps and re-layout so the first page of the apps grid has
  // less rows to accommodate.
  GetAppListTestHelper()->AddRecentApps(4);
  GetAppListTestHelper()->GetAppsContainerView()->ResetForShowApps();
  UpdateLayout();

  // There should be a total of 40 items in the item list.
  AppListItemList* item_list =
      AppListModelProvider::Get()->model()->top_level_item_list();
  ASSERT_EQ(40u, item_list->item_count());

  // The first page should be maxed at 15 apps, the second page maxed at 20
  // apps, and the third page should hold the leftover 5 apps totalling to 40
  // apps.
  EXPECT_EQ(15, grid_test_api_->AppsOnPage(0));
  EXPECT_EQ(20, grid_test_api_->AppsOnPage(1));
  EXPECT_EQ(5, grid_test_api_->AppsOnPage(2));
}

// Test that the grid dimensions change according to differently sized displays.
// The number of rows should change depending on the display height and the
// first page should most of the time have less rows to accommodate the recents
// apps.
TEST_P(PagedAppsGridViewTest, GridDimensionsChangesWithDisplaySize) {
  // Add some recent apps to take up space on the first page.
  GetAppListTestHelper()->AddAppItems(4);
  GetAppListTestHelper()->AddRecentApps(4);
  GetAppListTestHelper()->GetAppsContainerView()->ResetForShowApps();

  // Test with a display in landscape mode.
  UpdateDisplay("1000x600");
  EXPECT_EQ(3, GetPagedAppsGridView()->GetFirstPageRowsForTesting());
  EXPECT_EQ(4, GetPagedAppsGridView()->GetRowsForTesting());
  EXPECT_EQ(5, GetPagedAppsGridView()->cols());

  // Test with a display in landscape mode with less height. This should have
  // less rows.
  UpdateDisplay("1000x500");
  EXPECT_EQ(2, GetPagedAppsGridView()->GetFirstPageRowsForTesting());
  EXPECT_EQ(3, GetPagedAppsGridView()->GetRowsForTesting());
  EXPECT_EQ(5, GetPagedAppsGridView()->cols());

  // Test with a display in landscape mode a with a little more height. This
  // should have equal rows on the first and second pages.
  UpdateDisplay("1600x910");
  EXPECT_EQ(4, GetPagedAppsGridView()->GetFirstPageRowsForTesting());
  EXPECT_EQ(4, GetPagedAppsGridView()->GetRowsForTesting());
  EXPECT_EQ(5, GetPagedAppsGridView()->cols());

  // Test with a display in landscape mode with more height. This should have
  // more rows.
  UpdateDisplay("1400x1100");
  EXPECT_EQ(4, GetPagedAppsGridView()->GetFirstPageRowsForTesting());
  EXPECT_EQ(5, GetPagedAppsGridView()->GetRowsForTesting());
  EXPECT_EQ(5, GetPagedAppsGridView()->cols());

  // Test with a display in portrait mode.
  UpdateDisplay("700x1100");
  EXPECT_EQ(4, GetPagedAppsGridView()->GetFirstPageRowsForTesting());
  EXPECT_EQ(5, GetPagedAppsGridView()->GetRowsForTesting());
  EXPECT_EQ(5, GetPagedAppsGridView()->cols());

  // Test with a display in portrait mode with more height. This should have
  // more rows.
  UpdateDisplay("700x1400");
  EXPECT_EQ(5, GetPagedAppsGridView()->GetFirstPageRowsForTesting());
  EXPECT_EQ(6, GetPagedAppsGridView()->GetRowsForTesting());
  EXPECT_EQ(5, GetPagedAppsGridView()->cols());
}

// Test that an app cannot be dragged to create a new page when the remove empty
// space flag is enabled.
TEST_P(PagedAppsGridViewTest, DragItemToNextPage) {
  PaginationModel* pagination_model =
      GetAppListTestHelper()->GetRootPagedAppsGridView()->pagination_model();

  // Populate with enough apps to fill 2 pages.
  GetAppListTestHelper()->AddAppItems(35);
  EXPECT_EQ(2, pagination_model->total_pages());
  GetPagedAppsGridView()->GetWidget()->LayoutRootViewIfNecessary();

  const gfx::Rect apps_grid_bounds =
      GetPagedAppsGridView()->GetBoundsInScreen();
  gfx::Point next_page_point =
      gfx::Point(apps_grid_bounds.width() / 2, apps_grid_bounds.bottom() - 1);
  auto page_flip_waiter = std::make_unique<PageFlipWaiter>(pagination_model);

  // Drag the item at page 0 slot 0 to the next page.
  StartDragOnItemViewAtVisualIndex(0, 0);

  // Test that dragging an item to just past the bottom of the background
  // card causes a page flip.
  std::list<base::OnceClosure> drag_page_flip;
  drag_page_flip.push_back(base::BindLambdaForTesting([&]() {
    // Start cardified  state.
    GetEventGenerator()->MoveMouseBy(10, 10);
    GetEventGenerator()->MoveMouseTo(next_page_point);
  }));
  drag_page_flip.push_back(base::BindLambdaForTesting([&]() {
    page_flip_waiter->Wait();
    GetEventGenerator()->ReleaseLeftButton();
  }));
  MaybeRunDragAndDropSequenceForAppList(&drag_page_flip, /*is_touch=*/false);

  // With the drag complete, check that page 1 is now selected.
  EXPECT_EQ(1, pagination_model->selected_page());

  // Drag the item at page 1 slot 0 to the next page and hold it there.
  StartDragOnItemViewAtVisualIndex(1, 0);

  std::list<base::OnceClosure> drag_does_nothing;
  drag_does_nothing.push_back(base::BindLambdaForTesting([&]() {
    // Start cardified  state.
    GetEventGenerator()->MoveMouseBy(10, 10);
    GetEventGenerator()->MoveMouseTo(next_page_point);
  }));
  drag_does_nothing.push_back(base::BindLambdaForTesting([&]() {
    task_environment()->FastForwardBy(base::Seconds(2));
    GetEventGenerator()->ReleaseLeftButton();
  }));
  MaybeRunDragAndDropSequenceForAppList(&drag_does_nothing, /*is_touch=*/false);

  // With the drag complete, check that page 1 is still selected, because a new
  // page cannot be created.
  EXPECT_EQ(1, pagination_model->selected_page());
}

// Test that dragging an app item just above or just below the background card
// of the selected page will trigger a page flip.
TEST_P(PagedAppsGridViewTest, PageFlipBufferSizedByBackgroundCard) {
  PaginationModel* pagination_model =
      GetAppListTestHelper()->GetRootPagedAppsGridView()->pagination_model();

  // Populate with enough apps to fill 2 pages.
  GetAppListTestHelper()->AddAppItems(30);
  EXPECT_EQ(2, pagination_model->total_pages());
  GetPagedAppsGridView()->GetWidget()->LayoutRootViewIfNecessary();

  auto page_flip_waiter = std::make_unique<PageFlipWaiter>(pagination_model);

  // Drag down to the next page.
  StartDragOnItemViewAtVisualIndex(0, 0);

  std::list<base::OnceClosure> drag_page_flip_down;
  // Test that dragging an item to just past the bottom of the background
  // card causes a page flip.
  drag_page_flip_down.push_back(base::BindLambdaForTesting([&]() {
    // Start cardified  state.
    GetEventGenerator()->MoveMouseBy(10, 10);
    gfx::Point bottom_of_card = GetPagedAppsGridView()
                                    ->GetBackgroundCardBoundsForTesting(0)
                                    .bottom_left();
    bottom_of_card.Offset(0, 1);
    views::View::ConvertPointToScreen(GetPagedAppsGridView(), &bottom_of_card);
    GetEventGenerator()->MoveMouseTo(bottom_of_card);
  }));
  drag_page_flip_down.push_back(base::BindLambdaForTesting([&]() {
    page_flip_waiter->Wait();
    GetEventGenerator()->ReleaseLeftButton();
  }));
  MaybeRunDragAndDropSequenceForAppList(&drag_page_flip_down,
                                        /*is_touch=*/false);

  EXPECT_EQ(1, pagination_model->selected_page());

  // Drag up to the previous page.
  StartDragOnItemViewAtVisualIndex(1, 0);

  std::list<base::OnceClosure> drag_page_flip_top;
  // Test that dragging an item to just past the top of the background
  // card causes a page flip.
  drag_page_flip_top.push_back(base::BindLambdaForTesting([&]() {
    // Start cardified  state.
    GetEventGenerator()->MoveMouseBy(10, 10);
    gfx::Point top_of_card =
        GetPagedAppsGridView()->GetBackgroundCardBoundsForTesting(1).origin();
    top_of_card.Offset(0, -1);
    views::View::ConvertPointToScreen(GetPagedAppsGridView(), &top_of_card);
    GetEventGenerator()->MoveMouseTo(top_of_card);
  }));
  drag_page_flip_top.push_back(base::BindLambdaForTesting([&]() {
    page_flip_waiter->Wait();
    GetEventGenerator()->ReleaseLeftButton();
  }));
  MaybeRunDragAndDropSequenceForAppList(&drag_page_flip_top,
                                        /*is_touch=*/false);

  EXPECT_EQ(0, pagination_model->selected_page());
}

// Test that dragging an item to just past the top of the first page
// background card does not cause a page flip.
TEST_P(PagedAppsGridViewTest, NoPageFlipUpOnFirstPage) {
  PaginationModel* pagination_model =
      GetAppListTestHelper()->GetRootPagedAppsGridView()->pagination_model();

  // Populate with enough apps to fill 2 pages.
  GetAppListTestHelper()->AddAppItems(30);
  EXPECT_EQ(2, pagination_model->total_pages());
  GetPagedAppsGridView()->GetWidget()->LayoutRootViewIfNecessary();

  // Drag down to the next page.
  StartDragOnItemViewAtVisualIndex(0, 0);

  std::list<base::OnceClosure> tasks;
  // Drag an item to just past the top of the first page background card.
  tasks.push_back(base::BindLambdaForTesting([&]() {
    // Start cardified  state.
    GetEventGenerator()->MoveMouseBy(10, 10);
    gfx::Point top_of_first_card =
        GetPagedAppsGridView()->GetBackgroundCardBoundsForTesting(0).origin();
    top_of_first_card.Offset(0, -1);

    views::View::ConvertPointToScreen(GetPagedAppsGridView(),
                                      &top_of_first_card);
    GetEventGenerator()->MoveMouseTo(top_of_first_card);
  }));
  tasks.push_back(base::BindLambdaForTesting([&]() {
    task_environment()->FastForwardBy(base::Seconds(2));
    GetEventGenerator()->ReleaseLeftButton();
  }));
  MaybeRunDragAndDropSequenceForAppList(&tasks, /*is_touch=*/false);

  // Selected page should still be at the first page.
  EXPECT_EQ(0, pagination_model->selected_page());
}

// Test that dragging an item to just past the bottom of the last background
// card does not cause a page flip.
TEST_P(PagedAppsGridViewTest, NoPageFlipDownOnLastPage) {
  PaginationModel* pagination_model =
      GetAppListTestHelper()->GetRootPagedAppsGridView()->pagination_model();

  // Populate with enough apps to fill 2 pages.
  GetAppListTestHelper()->AddAppItems(30);
  EXPECT_EQ(2, pagination_model->total_pages());
  GetPagedAppsGridView()->GetWidget()->LayoutRootViewIfNecessary();

  // Select the last page.
  pagination_model->SelectPage(pagination_model->total_pages() - 1, false);
  EXPECT_EQ(1, pagination_model->selected_page());

  // Drag down to the next page.
  StartDragOnItemViewAtVisualIndex(1, 0);

  std::list<base::OnceClosure> tasks;
  // Drag an item to just past the bottom of the last background card.
  tasks.push_back(base::BindLambdaForTesting([&]() {
    // Start cardified  state.
    GetEventGenerator()->MoveMouseBy(10, 10);
  }));
  tasks.push_back(base::BindLambdaForTesting([&]() {
    gfx::Point bottom_of_last_card = GetPagedAppsGridView()
                                         ->GetBackgroundCardBoundsForTesting(1)
                                         .bottom_left();
    bottom_of_last_card.Offset(0, 1);
    views::View::ConvertPointToScreen(GetPagedAppsGridView(),
                                      &bottom_of_last_card);
    GetEventGenerator()->MoveMouseTo(bottom_of_last_card);
  }));
  tasks.push_back(base::BindLambdaForTesting([&]() {
    task_environment()->FastForwardBy(base::Seconds(2));
    GetEventGenerator()->ReleaseLeftButton();
  }));
  MaybeRunDragAndDropSequenceForAppList(&tasks, /*is_touch=*/false);

  // Selected page should not have changed and should still be the last page.
  EXPECT_EQ(1, pagination_model->selected_page());
}

// Test that the first page of the root level paged apps grid holds less apps to
// accommodate the recent apps, which are shown at the top of the first page,
// and the app list nudge, which is shown right above the apps grid view. Then
// check that the subsequent page holds more apps.
TEST_P(PagedAppsGridViewWithNudgeTest, PageMaxAppCounts) {
  GetAppListTestHelper()->AddAppItems(40);

  // Add some recent apps and re-layout so the first page of the apps grid has
  // less rows to accommodate.
  GetAppListTestHelper()->AddRecentApps(4);
  GetAppListTestHelper()->GetAppsContainerView()->ResetForShowApps();
  UpdateLayout();

  // There should be a total of 40 items in the item list.
  AppListItemList* item_list =
      AppListModelProvider::Get()->model()->top_level_item_list();
  ASSERT_EQ(40u, item_list->item_count());

  // With the recent apps and app list reorder nudge, the first page should be
  // maxed at 10 apps, the second page maxed at 20 apps, and the third page
  // should hold the leftover 10 apps totalling to 40 apps.
  EXPECT_EQ(10, grid_test_api_->AppsOnPage(0));
  EXPECT_EQ(20, grid_test_api_->AppsOnPage(1));
  EXPECT_EQ(10, grid_test_api_->AppsOnPage(2));
}

// Test that the grid dimensions change according to differently sized displays.
// The number of rows should change depending on the display height and the
// first page should most of the time have less rows to accommodate the recents
// apps. With the app list nudge enabled in this test, the number of rows
// showing could be less to accommodate the toast nudge.
TEST_P(PagedAppsGridViewWithNudgeTest, GridDimensionsChangesWithDisplaySize) {
  // Add some recent apps to take up space on the first page.
  GetAppListTestHelper()->AddAppItems(4);
  GetAppListTestHelper()->AddRecentApps(4);
  GetAppListTestHelper()->GetAppsContainerView()->ResetForShowApps();

  // Test with a display in landscape mode.
  UpdateDisplay("1000x600");
  EXPECT_EQ(2, GetPagedAppsGridView()->GetFirstPageRowsForTesting());
  EXPECT_EQ(4, GetPagedAppsGridView()->GetRowsForTesting());
  EXPECT_EQ(5, GetPagedAppsGridView()->cols());

  // Test with a display in landscape mode with less height. This should have
  // less rows.
  UpdateDisplay("1000x500");
  EXPECT_EQ(1, GetPagedAppsGridView()->GetFirstPageRowsForTesting());
  EXPECT_EQ(3, GetPagedAppsGridView()->GetRowsForTesting());
  EXPECT_EQ(5, GetPagedAppsGridView()->cols());

  // Test with a display in landscape mode with more height. This should have
  // more rows.
  UpdateDisplay("1400x1100");
  EXPECT_EQ(4, GetPagedAppsGridView()->GetFirstPageRowsForTesting());
  EXPECT_EQ(5, GetPagedAppsGridView()->GetRowsForTesting());
  EXPECT_EQ(5, GetPagedAppsGridView()->cols());

  // Test with a display in portrait mode.
  UpdateDisplay("700x1100");
  EXPECT_EQ(4, GetPagedAppsGridView()->GetFirstPageRowsForTesting());
  EXPECT_EQ(5, GetPagedAppsGridView()->GetRowsForTesting());
  EXPECT_EQ(5, GetPagedAppsGridView()->cols());

  // Test with a display in portrait mode with more height. This should have
  // more rows.
  UpdateDisplay("700x1400");
  EXPECT_EQ(5, GetPagedAppsGridView()->GetFirstPageRowsForTesting());
  EXPECT_EQ(6, GetPagedAppsGridView()->GetRowsForTesting());
  EXPECT_EQ(5, GetPagedAppsGridView()->cols());
}

TEST_P(PagedAppsGridViewTest, SortAppsMakesA11yAnnouncement) {
  auto* helper = GetAppListTestHelper();
  helper->AddAppItems(5);
  helper->GetAppsContainerView()->ResetForShowApps();

  views::View* announcement_view = helper->GetAccessibilityAnnounceView();
  ASSERT_TRUE(announcement_view);

  // Add a callback to wait for an accessibility event.
  ax::mojom::Event event = ax::mojom::Event::kNone;
  base::RunLoop run_loop;
  announcement_view->GetViewAccessibility().set_accessibility_events_callback(
      base::BindLambdaForTesting([&](const ui::AXPlatformNodeDelegate* unused,
                                     const ax::mojom::Event event_in) {
        event = event_in;
        run_loop.Quit();
      }));

  // Simulate sorting the apps. Because `run_loop` waits for the a11y event,
  // it is unnecessary to wait for app list sort.
  SortAppList(AppListSortOrder::kNameAlphabetical, /*wait=*/false);

  run_loop.Run();

  // An alert fired with a message.
  EXPECT_EQ(event, ax::mojom::Event::kAlert);
  ui::AXNodeData node_data;
  announcement_view->GetViewAccessibility().GetAccessibleNodeData(&node_data);
  EXPECT_EQ(node_data.GetStringAttribute(ax::mojom::StringAttribute::kName),
            "Apps are sorted by name");
}

// Verifies that sorting app list with an app item focused works as expected.
TEST_P(PagedAppsGridViewTest, SortAppsWithItemFocused) {
  ui::ScopedAnimationDurationScaleMode scope_duration(
      ui::ScopedAnimationDurationScaleMode::NON_ZERO_DURATION);

  // Show an app list with enough apps to create multiple pages.
  auto* helper = GetAppListTestHelper();
  helper->AddAppItems(grid_test_api_->AppsOnPage(0) + 50);
  helper->AddRecentApps(5);
  helper->GetAppsContainerView()->ResetForShowApps();

  PaginationModel* pagination_model =
      helper->GetRootPagedAppsGridView()->pagination_model();
  EXPECT_GT(pagination_model->total_pages(), 1);

  AppsContainerView* container_view = helper->GetAppsContainerView();
  AppListToastContainerView* reorder_undo_toast_container =
      container_view->toast_container();
  EXPECT_FALSE(reorder_undo_toast_container->IsToastVisible());

  SortAppList(AppListSortOrder::kNameAlphabetical, /*wait=*/true);

  // After sorting, the undo toast should be visible.
  EXPECT_TRUE(reorder_undo_toast_container->IsToastVisible());

  views::View* first_item =
      grid_test_api_->GetViewAtVisualIndex(/*page=*/0, /*slot=*/0);
  first_item->RequestFocus();

  // Install the focus listener before reorder.
  TestFocusChangeListener listener(
      helper->GetRootPagedAppsGridView()->GetFocusManager());

  // Wait until the fade out animation ends.
  {
    AppListController::Get()->UpdateAppListWithNewTemporarySortOrder(
        AppListSortOrder::kColor,
        /*animate=*/true, /*update_position_closure=*/base::DoNothing());

    base::RunLoop run_loop;
    container_view->apps_grid_view()->AddFadeOutAnimationDoneClosureForTest(
        run_loop.QuitClosure());
    run_loop.Run();
  }

  // Verify that the reorder undo toast's layer opacity does not change.
  EXPECT_EQ(1.f, reorder_undo_toast_container->layer()->opacity());

  // Verify that the focus moves twice. It first goes to the search box during
  // the animation and then the undo button on the undo toast after the end of
  // animation.
  EXPECT_EQ(2, listener.focus_change_count());
  EXPECT_FALSE(first_item->HasFocus());
  EXPECT_TRUE(reorder_undo_toast_container->GetToastButton()->HasFocus());

  // Simulate the sort undo by setting the new order to nullopt. The focus
  // should be on the search box after undoing the sort.
  SortAppList(std::nullopt, /*wait=*/true);
  EXPECT_TRUE(helper->GetSearchBoxView()->search_box()->HasFocus());
}

// Verify on the paged apps grid the undo toast should show after scrolling.
TEST_P(PagedAppsGridViewTest, ScrollToShowUndoToastWhenSorting) {
  ui::ScopedAnimationDurationScaleMode scope_duration(
      ui::ScopedAnimationDurationScaleMode::NON_ZERO_DURATION);

  // Show an app list with enough apps to create multiple pages.
  auto* helper = GetAppListTestHelper();
  helper->AddAppItems(grid_test_api_->AppsOnPage(0) + 50);
  helper->AddRecentApps(5);
  helper->GetAppsContainerView()->ResetForShowApps();

  PaginationModel* pagination_model =
      helper->GetRootPagedAppsGridView()->pagination_model();
  const int total_pages = pagination_model->total_pages();
  EXPECT_GT(total_pages, 1);

  AppsContainerView* container_view =
      GetAppListTestHelper()->GetAppsContainerView();
  AppListToastContainerView* reorder_undo_toast_container =
      container_view->toast_container();
  EXPECT_FALSE(reorder_undo_toast_container->IsToastVisible());

  SortAppList(AppListSortOrder::kNameAlphabetical, /*wait=*/true);

  // After sorting, the undo toast should be visible.
  EXPECT_TRUE(reorder_undo_toast_container->IsToastVisible());

  pagination_model->SelectPage(total_pages - 1, /*animate=*/false);

  // After selecting the last page, the undo toast should be out of the apps
  // container's view port.
  const gfx::Rect apps_container_screen_bounds =
      container_view->GetBoundsInScreen();
  EXPECT_FALSE(apps_container_screen_bounds.Contains(
      reorder_undo_toast_container->GetBoundsInScreen()));

  SortAppList(AppListSortOrder::kColor, /*wait=*/true);

  // After sorting, the undo toast should still be visible.
  EXPECT_TRUE(reorder_undo_toast_container->IsToastVisible());

  // The undo toast should be within the apps container's view port.
  EXPECT_TRUE(apps_container_screen_bounds.Contains(
      reorder_undo_toast_container->GetBoundsInScreen()));
}

// Test tapping on the close button to dismiss the reorder toast. Also make sure
// that items animate upward to take the place of the closed toast.
TEST_P(PagedAppsGridViewTest, CloseReorderToast) {
  ui::ScopedAnimationDurationScaleMode scope_duration(
      ui::ScopedAnimationDurationScaleMode::NON_ZERO_DURATION);

  auto* helper = GetAppListTestHelper();
  helper->AddAppItems(50);
  helper->AddRecentApps(5);
  helper->GetAppsContainerView()->ResetForShowApps();

  // Trigger a sort to show the reorder toast.
  SortAppList(AppListSortOrder::kNameAlphabetical, /*wait=*/true);

  AppListToastContainerView* toast_container =
      helper->GetAppsContainerView()->toast_container();
  EXPECT_TRUE(toast_container->GetToastButton()->HasFocus());
  EXPECT_TRUE(toast_container->IsToastVisible());
  EXPECT_EQ(2, GetPagedAppsGridView()->GetFirstPageRowsForTesting());

  // Tap on the close button to remove the toast.
  GestureTapOn(toast_container->GetCloseButton());

  // Wait for the toast to finish fade out animation.
  EXPECT_EQ(toast_container->toast_view()->layer()->GetTargetOpacity(), 0.0f);
  ui::LayerAnimationStoppedWaiter().Wait(
      toast_container->toast_view()->layer());

  const views::ViewModelT<AppListItemView>* view_model =
      GetPagedAppsGridView()->view_model();

  // Item views should animate upwards to take the place of the closed reorder
  // toast.
  for (size_t i = 1; i < view_model->view_size(); i++) {
    AppListItemView* item_view = view_model->view_at(i);
    // The items off screen on the second page should not animate.
    if (i >= grid_test_api_->TilesPerPageInPagedGrid(0)) {
      EXPECT_FALSE(GetPagedAppsGridView()->IsAnimatingView(item_view));
      continue;
    }

    // Make sure that no between rows animation is occurring by checking that
    // all items are animating upward vertically and not horizontally.
    EXPECT_TRUE(GetPagedAppsGridView()->IsAnimatingView(item_view));
    gfx::RectF bounds(item_view->GetMirroredBounds());
    bounds = item_view->layer()->transform().MapRect(bounds);
    gfx::Rect current_bounds_in_animation = gfx::ToRoundedRect(bounds);
    EXPECT_GT(current_bounds_in_animation.y(), item_view->bounds().y());
    EXPECT_EQ(current_bounds_in_animation.x(), item_view->bounds().x());
  }

  // Verify that another row appears once the toast is closed.
  EXPECT_EQ(3, GetPagedAppsGridView()->GetFirstPageRowsForTesting());
  EXPECT_FALSE(toast_container->IsToastVisible());
}

// Test that when quickly dragging and removing the last item from a folder, the
// item view layers which are created when entering cardified state are
// destroyed once the exit cardified item animations are complete.
TEST_P(PagedAppsGridViewTest, DestroyLayersOnDragLastItemFromFolder) {
  ui::ScopedAnimationDurationScaleMode scope_duration(
      ui::ScopedAnimationDurationScaleMode::NON_ZERO_DURATION);
  GetAppListTestHelper()->model()->CreateSingleItemFolder("folder_id",
                                                          "Item_0");
  GetAppListTestHelper()->model()->PopulateApps(5);
  UpdateLayout();

  auto* generator = GetEventGenerator();
  auto* helper = GetAppListTestHelper();

  GetPagedAppsGridView()->SetCardifiedStateEndedTestCallback(
      base::BindLambdaForTesting(
          [&]() { LOG(ERROR) << "wowee TESTING OnCardifiedStateEnded!!!"; }));

  // Open the folder.
  EXPECT_TRUE(GetPagedAppsGridView()->GetItemViewAt(0)->is_folder());
  LeftClickOn(GetPagedAppsGridView()->GetItemViewAt(0));
  ASSERT_TRUE(helper->IsInFolderView());

  // Wait for folder opening animations to complete.
  base::RunLoop folder_animation_waiter;
  helper->GetAppsContainerView()
      ->app_list_folder_view()
      ->SetAnimationDoneTestCallback(base::BindLambdaForTesting(
          [&]() { folder_animation_waiter.Quit(); }));
  folder_animation_waiter.Run();

  AppListItemView* item_view = helper->GetFullscreenFolderView()
                                   ->items_grid_view()
                                   ->view_model()
                                   ->view_at(0);

  StartDragOnItemView(item_view);

  std::list<base::OnceClosure> tasks;
  // Move the mouse outside of the folder.
  tasks.push_back(base::BindLambdaForTesting([&]() {
    generator->MoveMouseTo(helper->GetAppsContainerView()
                               ->app_list_folder_view()
                               ->GetBoundsInScreen()
                               .bottom_center() +
                           gfx::Vector2d(0, item_view->height()));
    if (GetParam()) {
      // Generate OnDragExit() event for the folder apps grid view.
      generator->MoveMouseBy(10, 10);
    }
    ASSERT_TRUE(helper->GetFullscreenFolderView()
                    ->items_grid_view()
                    ->FireFolderItemReparentTimerForTest());
    GetEventGenerator()->ReleaseLeftButton();
  }));
  MaybeRunDragAndDropSequenceForAppList(&tasks, /*is_touch=*/false);

  ASSERT_FALSE(helper->IsInFolderView());

  const views::ViewModelT<AppListItemView>* view_model =
      GetPagedAppsGridView()->view_model();
  EXPECT_EQ(6u, view_model->view_size());

  // Ensure all items have layers right after ending drag.
  for (size_t i = 0; i < view_model->view_size(); i++)
    EXPECT_TRUE(view_model->view_at(i)->layer());

  WaitForItemLayerAnimations();

  // When each item's layer animation is complete, their layers should have been
  // removed.
  for (size_t i = 0; i < view_model->view_size(); i++)
    EXPECT_FALSE(view_model->view_at(i)->layer());

  EXPECT_FALSE(GetPagedAppsGridView()->IsItemAnimationRunning());
}

// Test that when quickly dragging an item into a second page, and then into the
// search box while the reorder animation is running, does not results in a
// crash.
TEST_P(PagedAppsGridViewTest, EnterSearchBoxDuringDragNoCrash) {
  const size_t kTotalApps = grid_test_api_->TilesPerPageInPagedGrid(0) + 1;
  GetAppListTestHelper()->model()->PopulateApps(kTotalApps);
  UpdateLayout();

  PaginationModel* pagination_model =
      GetAppListTestHelper()->GetRootPagedAppsGridView()->pagination_model();
  EXPECT_EQ(0, pagination_model->selected_page());
  EXPECT_EQ(2, pagination_model->total_pages());

  auto* generator = GetEventGenerator();

  AppListItemView* item_view = GetPagedAppsGridView()->GetItemViewAt(0);

  StartDragOnItemView(item_view);

  std::list<base::OnceClosure> tasks;

  // Move to the second page.
  tasks.push_back(base::BindLambdaForTesting([&]() {
    generator->MoveMouseTo(
        GetPagedAppsGridView()->GetBoundsInScreen().bottom_left() +
        gfx::Vector2d(0, -1));
    EXPECT_TRUE(GetPagedAppsGridView()->cardified_state_for_testing());
    auto page_flip_waiter = std::make_unique<PageFlipWaiter>(pagination_model);
    page_flip_waiter->Wait();
    // Second page should be selected.
    EXPECT_EQ(1, pagination_model->selected_page());
  }));
  // Trigger animation for reordering, and move to the search box while it is
  // still animating.
  tasks.push_back(base::BindLambdaForTesting([&]() {
    ui::ScopedAnimationDurationScaleMode scope_duration(
        ui::ScopedAnimationDurationScaleMode::NON_ZERO_DURATION);
    generator->MoveMouseTo(
        GetPagedAppsGridView()->GetBoundsInScreen().CenterPoint());
    ASSERT_TRUE(GetPagedAppsGridView()->reorder_timer_for_test()->IsRunning());
    GetPagedAppsGridView()->reorder_timer_for_test()->FireNow();
    generator->MoveMouseTo(GetAppListTestHelper()
                               ->GetSearchBoxView()
                               ->GetBoundsInScreen()
                               .CenterPoint());
  }));
  // Release drag, required by the drag and drop controller
  tasks.push_back(base::BindLambdaForTesting(
      [&]() { GetEventGenerator()->ReleaseLeftButton(); }));
  MaybeRunDragAndDropSequenceForAppList(&tasks, /*is_touch=*/false);
}

// Test the case of beginning an item drag and then immediately ending the drag.
// This will cause the entering cardified state animations to get interrupted by
// the exiting animations. It could be possible that this animation interrupt
// triggers `OnCardifiedStateEnded()` twice, so test that cardified state ended
// only happens once.
TEST_P(PagedAppsGridViewTest, QuicklyDragAndDropItem) {
  ui::ScopedAnimationDurationScaleMode scope_duration(
      ui::ScopedAnimationDurationScaleMode::NON_ZERO_DURATION);
  GetAppListTestHelper()->model()->PopulateApps(5);
  UpdateLayout();

  auto* generator = GetEventGenerator();

  // Set the callback to count how many times the cardified state is ended.
  int number_of_times_cardified_state_ended = 0;
  GetPagedAppsGridView()->SetCardifiedStateEndedTestCallback(
      base::BindLambdaForTesting(
          [&]() { number_of_times_cardified_state_ended++; }));

  const views::ViewModelT<AppListItemView>* view_model =
      GetPagedAppsGridView()->view_model();

  // Drag down to the next page.
  StartDragOnItemView(view_model->view_at(1));

  std::list<base::OnceClosure> tasks;
  // Drag the item a short distance and immediately release drag.
  tasks.push_back(base::BindLambdaForTesting([&]() {
    generator->MoveMouseBy(100, 100);
    GetEventGenerator()->ReleaseLeftButton();
  }));
  MaybeRunDragAndDropSequenceForAppList(&tasks, /*is_touch=*/false);

  EXPECT_FALSE(IsRowChangeAnimatorAnimating());

  WaitForItemLayerAnimations();

  // When each item's layer animation is complete, their layers should have been
  // removed.
  for (size_t i = 0; i < view_model->view_size(); i++)
    EXPECT_FALSE(view_model->view_at(i)->layer());
  EXPECT_FALSE(GetPagedAppsGridView()->IsItemAnimationRunning());

  // Now that cardified item animations are complete, make sure that
  // `OnCardifiedStateEnded()` is only called once.
  EXPECT_EQ(1, number_of_times_cardified_state_ended);
}

// When quickly dragging and dropping an item from one row to another, test that
// row change animations are not interrupted during cardified state exit.
TEST_P(PagedAppsGridViewTest, QuicklyDragAndDropItemToNewRow) {
  ui::ScopedAnimationDurationScaleMode scope_duration(
      ui::ScopedAnimationDurationScaleMode::NON_ZERO_DURATION);
  GetAppListTestHelper()->model()->PopulateApps(10);
  UpdateLayout();

  auto* generator = GetEventGenerator();

  // Set the callback to count how many times the cardified state is ended.
  int number_of_times_cardified_state_ended = 0;
  GetPagedAppsGridView()->SetCardifiedStateEndedTestCallback(
      base::BindLambdaForTesting(
          [&]() { number_of_times_cardified_state_ended++; }));

  const views::ViewModelT<AppListItemView>* view_model =
      GetPagedAppsGridView()->view_model();

  // Drag down to the next page.
  StartDragOnItemView(view_model->view_at(1));

  std::list<base::OnceClosure> tasks;
  // Quickly drag the item from the first row to the second row, which should
  // cause a row change animation when the drag is released.
  tasks.push_back(base::BindLambdaForTesting([&]() {
    gfx::Point second_row_drag_point =
        view_model->view_at(5)->GetBoundsInScreen().right_center();
    second_row_drag_point.Offset(50, 0);
    generator->MoveMouseTo(second_row_drag_point);
    GetEventGenerator()->ReleaseLeftButton();
  }));
  MaybeRunDragAndDropSequenceForAppList(&tasks, /*is_touch=*/false);

  // There should be a row change animation happening.
  EXPECT_TRUE(IsRowChangeAnimatorAnimating());
  EXPECT_EQ(1, GetNumberOfRowChangeLayersForTest());

  // Fast forward and make sure that the row change animator was not interrupted
  // and is still animating.
  task_environment()->FastForwardBy(base::Milliseconds(100));
  EXPECT_TRUE(IsRowChangeAnimatorAnimating());
  EXPECT_EQ(1, GetNumberOfRowChangeLayersForTest());

  WaitForItemLayerAnimations();

  // When each item's layer animation is complete, their layers should have been
  // removed.
  for (size_t i = 0; i < view_model->view_size(); i++)
    EXPECT_FALSE(view_model->view_at(i)->layer());
  EXPECT_FALSE(GetPagedAppsGridView()->IsItemAnimationRunning());
  EXPECT_FALSE(IsRowChangeAnimatorAnimating());
  EXPECT_EQ(0, GetNumberOfRowChangeLayersForTest());

  // Now that cardified item animations are complete, make sure that
  // `OnCardifiedStateEnded()` is only called once.
  EXPECT_EQ(1, number_of_times_cardified_state_ended);
}

TEST_P(PagedAppsGridViewTest, CardifiedEnterAnimationInterruptedByExit) {
  ui::ScopedAnimationDurationScaleMode scope_duration(
      ui::ScopedAnimationDurationScaleMode::NON_ZERO_DURATION);
  GetAppListTestHelper()->model()->PopulateApps(5);
  UpdateLayout();

  AppListItemView* item_view = GetPagedAppsGridView()->view_model()->view_at(0);
  EXPECT_GE(GetPagedAppsGridView()->view_model()->view_size(), 2u);
  auto* generator = GetEventGenerator();

  StartDragOnItemView(GetPagedAppsGridView()->view_model()->view_at(1));

  std::list<base::OnceClosure> first_animation_completes;
  first_animation_completes.push_back(base::BindLambdaForTesting([&]() {
    // Start cardified state.
    generator->MoveMouseBy(10, 10);
    EXPECT_TRUE(GetPagedAppsGridView()->cardified_state_for_testing());
    EXPECT_TRUE(item_view->layer()->GetAnimator()->is_animating());
  }));
  // Check that the first item completes the entering cardified state
  // animation.
  first_animation_completes.push_back(base::BindLambdaForTesting([&]() {
    WaitForItemLayerAnimations();
    EXPECT_FALSE(item_view->layer()->GetAnimator()->is_animating());
    GetEventGenerator()->ReleaseLeftButton();
  }));
  MaybeRunDragAndDropSequenceForAppList(&first_animation_completes,
                                        /*is_touch=*/false);

  EXPECT_FALSE(GetPagedAppsGridView()->cardified_state_for_testing());

  // With the item view animating from its completed cardified position, to the
  // non-cardified position, check that the layer transform is not identity.
  EXPECT_TRUE(item_view->layer()->GetAnimator()->is_animating());
  EXPECT_FALSE(item_view->layer()->transform().IsIdentity());

  WaitForItemLayerAnimations();
  EXPECT_FALSE(item_view->layer());

  StartDragOnItemView(GetPagedAppsGridView()->view_model()->view_at(1));

  std::list<base::OnceClosure> animation_not_completes;
  // Exit cardified state, without waiting for the enter animation to complete.
  animation_not_completes.push_back(base::BindLambdaForTesting([&]() {
    // Start cardified state.
    generator->MoveMouseBy(10, 10);
    EXPECT_TRUE(GetPagedAppsGridView()->cardified_state_for_testing());
    GetEventGenerator()->ReleaseLeftButton();
  }));
  MaybeRunDragAndDropSequenceForAppList(&animation_not_completes,
                                        /*is_touch=*/false);

  // With the item view animating from its current position at the start of the
  // begin cardified state, to its non-cardified position, the layer transform
  // should be the identity transform, indicating a smoothly interrupted
  // animation.
  EXPECT_TRUE(item_view->layer()->GetAnimator()->is_animating());
  EXPECT_TRUE(item_view->layer()->transform().IsIdentity());

  WaitForItemLayerAnimations();
  EXPECT_FALSE(item_view->layer());
}

// Test that a first page item released outside of the grid with second page
// shown will visually change back to the first page.
TEST_P(PagedAppsGridViewTest, DragOutsideOfNextPageSelectsOriginalPage) {
  const size_t kTotalApps = grid_test_api_->TilesPerPageInPagedGrid(0) + 1;
  GetAppListTestHelper()->model()->PopulateApps(kTotalApps);
  UpdateLayout();

  PaginationModel* pagination_model =
      GetAppListTestHelper()->GetRootPagedAppsGridView()->pagination_model();
  EXPECT_EQ(0, pagination_model->selected_page());
  EXPECT_EQ(2, pagination_model->total_pages());

  auto* item_view = GetPagedAppsGridView()->view_model()->view_at(0);
  auto* generator = GetEventGenerator();
  std::list<base::OnceClosure> tasks;

  // Start dragging an item on the first page.
  StartDragOnItemView(item_view);

  // Exit cardified state, without waiting for the enter animation to complete.
  tasks.push_back(base::BindLambdaForTesting([&]() {
    generator->MoveMouseTo(
        GetPagedAppsGridView()->GetBoundsInScreen().bottom_left() +
        gfx::Vector2d(0, -1));
    EXPECT_TRUE(GetPagedAppsGridView()->cardified_state_for_testing());
  }));
  tasks.push_back(base::BindLambdaForTesting([&]() {
    auto page_flip_waiter = std::make_unique<PageFlipWaiter>(pagination_model);
    page_flip_waiter->Wait();
    ui::ScopedAnimationDurationScaleMode non_zero_duration_mode(
        ui::ScopedAnimationDurationScaleMode::NON_ZERO_DURATION);
    // Second page should be selected.
    EXPECT_EQ(1, pagination_model->selected_page());
  }));
  // Move the mouse down to be completely below the grid view. Releasing
  // a drag here will move the item back to its starting position.
  tasks.push_back(base::BindLambdaForTesting([&]() {
    generator->MoveMouseBy(0, 50);
    EXPECT_EQ(1, pagination_model->selected_page());
  }));
  tasks.push_back(base::BindLambdaForTesting([&]() {
    // End Drag
    GetEventGenerator()->ReleaseLeftButton();
  }));
  MaybeRunDragAndDropSequenceForAppList(&tasks, /*is_touch=*/false);

  WaitForItemLayerAnimations();
  UpdateLayout();

  // First page should be selected.
  EXPECT_EQ(0, pagination_model->selected_page());

  // Dragged item should be in starting position.
  EXPECT_EQ(item_view, grid_test_api_->GetViewAtIndex(GridIndex(0, 0)));

  auto app_list_item_view_visible = [this](const views::View* view) -> bool {
    return GetPagedAppsGridView()
        ->GetWidget()
        ->GetWindowBoundsInScreen()
        .Contains(view->GetBoundsInScreen());
  };

  // It is possible that the selected page is correct but visually never
  // changes, so check that the dragged 'item_view' is visible, and the item
  // on the second page is not visible.
  EXPECT_TRUE(app_list_item_view_visible(item_view));
  EXPECT_FALSE(app_list_item_view_visible(
      grid_test_api_->GetViewAtIndex(GridIndex(1, 0))));
}

}  // namespace ash