chromium/ash/app_list/app_list_test_api.cc

// Copyright 2020 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/public/cpp/test/app_list_test_api.h"

#include <string>
#include <utility>
#include <vector>

#include "ash/app_list/app_list_bubble_presenter.h"
#include "ash/app_list/app_list_controller_impl.h"
#include "ash/app_list/app_list_model_provider.h"
#include "ash/app_list/app_list_presenter_impl.h"
#include "ash/app_list/app_list_public_test_util.h"
#include "ash/app_list/model/app_list_folder_item.h"
#include "ash/app_list/model/app_list_item.h"
#include "ash/app_list/model/app_list_model.h"
#include "ash/app_list/views/app_list_bubble_apps_page.h"
#include "ash/app_list/views/app_list_bubble_search_page.h"
#include "ash/app_list/views/app_list_bubble_view.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_main_view.h"
#include "ash/app_list/views/app_list_menu_model_adapter.h"
#include "ash/app_list/views/app_list_search_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/app_list_view.h"
#include "ash/app_list/views/apps_container_view.h"
#include "ash/app_list/views/apps_grid_context_menu.h"
#include "ash/app_list/views/apps_grid_view.h"
#include "ash/app_list/views/apps_grid_view_test_api.h"
#include "ash/app_list/views/contents_view.h"
#include "ash/app_list/views/continue_section_view.h"
#include "ash/app_list/views/paged_apps_grid_view.h"
#include "ash/app_list/views/recent_apps_view.h"
#include "ash/app_list/views/scrollable_apps_grid_view.h"
#include "ash/app_list/views/search_box_view.h"
#include "ash/app_list/views/search_result_list_view.h"
#include "ash/app_list/views/search_result_page_view.h"
#include "ash/constants/ash_pref_names.h"
#include "ash/public/cpp/accelerators.h"
#include "ash/shell.h"
#include "base/functional/callback.h"
#include "base/memory/raw_ptr.h"
#include "base/run_loop.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/aura/window_observer.h"
#include "ui/compositor/layer.h"
#include "ui/compositor/test/layer_animation_stopped_waiter.h"
#include "ui/events/test/event_generator.h"
#include "ui/views/controls/button/label_button.h"
#include "ui/views/controls/label.h"
#include "ui/views/controls/menu/menu_item_view.h"
#include "ui/views/controls/menu/submenu_view.h"
#include "ui/views/controls/scroll_view.h"
#include "ui/views/controls/textfield/textfield.h"
#include "ui/views/view_model.h"
#include "ui/views/view_utils.h"

namespace ash {

namespace {

// A global pointer to the disabler's instance. Used to ensure at most one
// disabler exists at a time.
class ScopedItemMoveAnimationDisabler;
ScopedItemMoveAnimationDisabler* g_disabler_ptr = nullptr;

// Returns the menu item indicated by `order` from a non-folder item menu.
views::MenuItemView* GetReorderOptionForNonFolderItemMenu(
    const views::MenuItemView* root_menu,
    ash::AppListSortOrder order) {
  // Get the last menu item index where the reorder submenu is.
  views::MenuItemView* reorder_item_view =
      root_menu->GetSubmenu()->GetLastItem();
  DCHECK_EQ(reorder_item_view->title(), u"Sort by");
  return reorder_item_view;
}

ash::AppListItemView* FindFolderItemView(ash::AppsGridView* apps_grid_view) {
  auto* model = apps_grid_view->view_model();
  for (size_t index = 0; index < model->view_size(); ++index) {
    ash::AppListItemView* current_view = model->view_at(index);
    if (current_view->is_folder())
      return current_view;
  }

  return nullptr;
}

ash::AppListItemView* FindNonFolderItemView(ash::AppsGridView* apps_grid_view) {
  auto* model = apps_grid_view->view_model();
  for (size_t index = 0; index < model->view_size(); ++index) {
    ash::AppListItemView* current_view = model->view_at(index);
    if (!current_view->is_folder())
      return current_view;
  }

  return nullptr;
}

// Returns the index of the specified sorting option.
size_t GetMenuIndexOfSortingOrder(ash::AppListSortOrder order) {
  switch (order) {
    case ash::AppListSortOrder::kNameAlphabetical:
      return 0;
    case ash::AppListSortOrder::kColor:
      return 1;
    case ash::AppListSortOrder::kNameReverseAlphabetical:
    case ash::AppListSortOrder::kCustom:
    case ash::AppListSortOrder::kAlphabeticalEphemeralAppFirst:
      NOTREACHED();
  }
}

views::MenuItemView* GetReorderOptionForAppListOrFolderItemMenu(
    const views::MenuItemView* root_menu,
    const ash::AppListSortOrder order) {
  views::MenuItemView* reorder_option = nullptr;
  switch (order) {
    case ash::AppListSortOrder::kNameAlphabetical:
      reorder_option = root_menu->GetSubmenu()->GetMenuItemAt(1);
      EXPECT_TRUE(reorder_option->title() == u"Name");
      break;
    case ash::AppListSortOrder::kColor:
      reorder_option = root_menu->GetSubmenu()->GetMenuItemAt(2);
      EXPECT_TRUE(reorder_option->title() == u"Color");
      break;
    case ash::AppListSortOrder::kNameReverseAlphabetical:
    case ash::AppListSortOrder::kCustom:
    case ash::AppListSortOrder::kAlphabeticalEphemeralAppFirst:
      NOTREACHED();
  }
  return reorder_option;
}

views::MenuItemView* ShowRootMenuAndReturn(
    ash::AppsGridView* apps_grid_view,
    AppListTestApi::MenuType menu_type,
    ui::test::EventGenerator* event_generator) {
  views::MenuItemView* root_menu = nullptr;

  EXPECT_GT(apps_grid_view->view_model()->view_size(), 0u);

  switch (menu_type) {
    case AppListTestApi::MenuType::kAppListPageMenu:
      event_generator->MoveMouseTo(
          apps_grid_view->GetBoundsInScreen().CenterPoint());
      event_generator->ClickRightButton();
      root_menu =
          apps_grid_view->context_menu_for_test()->root_menu_item_view();
      break;
    case AppListTestApi::MenuType::kAppListNonFolderItemMenu:
    case AppListTestApi::MenuType::kAppListFolderItemMenu:
      const bool is_folder_item =
          (menu_type == AppListTestApi::MenuType::kAppListFolderItemMenu);
      ash::AppListItemView* item_view =
          is_folder_item ? FindFolderItemView(apps_grid_view)
                         : FindNonFolderItemView(apps_grid_view);
      EXPECT_TRUE(item_view);
      event_generator->MoveMouseTo(
          item_view->GetBoundsInScreen().CenterPoint());
      event_generator->ClickRightButton();

      if (is_folder_item) {
        root_menu = item_view->context_menu_for_folder()->root_menu_item_view();
      } else {
        ash::AppListMenuModelAdapter* menu_model_adapter =
            item_view->item_menu_model_adapter();
        root_menu = menu_model_adapter->root_for_testing();
      }
      break;
  }

  EXPECT_TRUE(root_menu->SubmenuIsShowing());
  return root_menu;
}

PagedAppsGridView* GetPagedAppsGridView() {
  // This view only exists for tablet launcher and legacy peeking launcher.
  DCHECK(!ShouldUseBubbleAppList());
  return AppListView::TestApi(GetAppListView()).GetRootAppsGridView();
}

AppsContainerView* GetAppsContainerView() {
  return GetAppListView()
      ->app_list_main_view()
      ->contents_view()
      ->apps_container_view();
}

AppListFolderView* GetAppListFolderView() {
  // Handle the case that the app list bubble view is effective.
  if (ShouldUseBubbleAppList())
    return GetAppListBubbleView()->folder_view_for_test();

  return GetAppsContainerView()->app_list_folder_view();
}

AppListToastContainerView* GetToastContainerViewFromBubble() {
  return GetAppListBubbleView()
      ->apps_page_for_test()
      ->toast_container_for_test();
}

AppListToastContainerView* GetToastContainerViewFromFullscreenAppList() {
  return GetAppsContainerView()->toast_container();
}

RecentAppsView* GetRecentAppsView() {
  if (ShouldUseBubbleAppList())
    return GetAppListBubbleView()->apps_page_for_test()->recent_apps_for_test();

  return GetAppsContainerView()->GetRecentAppsView();
}

ContinueSectionView* GetContinueSectionView() {
  if (ShouldUseBubbleAppList()) {
    return GetAppListBubbleView()
        ->apps_page_for_test()
        ->GetContinueSectionView();
  }
  return GetAppsContainerView()->GetContinueSectionView();
}

AppListSearchView* GetSearchView() {
  if (ShouldUseBubbleAppList()) {
    return GetAppListBubbleView()->search_page()->search_view();
  }

  return GetAppListView()
      ->app_list_main_view()
      ->contents_view()
      ->search_result_page_view()
      ->search_view();
}

// AppListVisibilityChangedWaiter ----------------------------------------------

// Waits until the app list visibility changes.
class AppListVisibilityChangedWaiter : public AppListControllerObserver {
 public:
  AppListVisibilityChangedWaiter() = default;
  AppListVisibilityChangedWaiter(const AppListVisibilityChangedWaiter&) =
      delete;
  AppListVisibilityChangedWaiter& operator=(
      const AppListVisibilityChangedWaiter&) = delete;
  ~AppListVisibilityChangedWaiter() override {
    AppListController::Get()->RemoveObserver(this);
  }

  void Wait() {
    AppListController::Get()->AddObserver(this);
    run_loop_.Run();
  }

  // AppListControllerObserver:
  void OnAppListVisibilityChanged(bool shown, int64_t display_id) override {
    run_loop_.Quit();
  }

 private:
  base::RunLoop run_loop_;
};

// WindowAddedWaiter -----------------------------------------------------------

// Waits until a child window is added to a container window.
class WindowAddedWaiter : public aura::WindowObserver {
 public:
  explicit WindowAddedWaiter(aura::Window* container) : container_(container) {
    container_->AddObserver(this);
  }
  WindowAddedWaiter(const WindowAddedWaiter&) = delete;
  WindowAddedWaiter& operator=(const WindowAddedWaiter&) = delete;
  ~WindowAddedWaiter() override { container_->RemoveObserver(this); }

  void Wait() { run_loop_.Run(); }

  aura::Window* added_window() { return added_window_; }

 private:
  // aura::WindowObserver:
  void OnWindowAdded(aura::Window* new_window) override {
    added_window_ = new_window;
    DCHECK(run_loop_.IsRunningOnCurrentThread());
    run_loop_.Quit();
  }

  const raw_ptr<aura::Window> container_;
  raw_ptr<aura::Window> added_window_ = nullptr;
  base::RunLoop run_loop_;
};

// ScopedItemMoveAnimationDisabler ---------------------------------------------

// Disable the apps grid item move animation in scope.
class ScopedItemMoveAnimationDisabler {
 public:
  explicit ScopedItemMoveAnimationDisabler(AppsGridView* apps_grid)
      : apps_grid_(apps_grid) {
    DCHECK(!g_disabler_ptr);
    apps_grid_->set_enable_item_move_animation_for_test(false);
    g_disabler_ptr = this;
  }
  ScopedItemMoveAnimationDisabler(const ScopedItemMoveAnimationDisabler&) =
      delete;
  ScopedItemMoveAnimationDisabler& operator=(
      const ScopedItemMoveAnimationDisabler&) = delete;
  ~ScopedItemMoveAnimationDisabler() {
    apps_grid_->set_enable_item_move_animation_for_test(true);
    DCHECK(g_disabler_ptr);
    g_disabler_ptr = nullptr;
  }

 private:
  const raw_ptr<AppsGridView> apps_grid_;
};

}  // namespace

AppListTestApi::AppListTestApi() = default;
AppListTestApi::~AppListTestApi() = default;

AppListModel* AppListTestApi::GetAppListModel() {
  return AppListModelProvider::Get()->model();
}

void AppListTestApi::ShowBubbleAppListAndWait() {
  ash::AcceleratorController::Get()->PerformActionIfEnabled(
      AcceleratorAction::kToggleAppList, {});
  WaitForBubbleWindow(
      /*wait_for_opening_animation=*/true);
}

void AppListTestApi::WaitForBubbleWindow(bool wait_for_opening_animation) {
  WaitForBubbleWindowInRootWindow(Shell::GetPrimaryRootWindow(),
                                  wait_for_opening_animation);
}

void AppListTestApi::WaitForBubbleWindowInRootWindow(
    aura::Window* root_window,
    bool wait_for_opening_animation) {
  DCHECK(!Shell::Get()->IsInTabletMode());

  // Wait for the window only when the app list window does not exist.
  auto* app_list_controller = Shell::Get()->app_list_controller();
  if (!app_list_controller->GetWindow()) {
    // Wait for a child window to be added to the app list container.
    aura::Window* container =
        Shell::GetContainer(root_window, kShellWindowId_AppListContainer);
    WindowAddedWaiter waiter(container);
    waiter.Wait();

    // App list window exists.
    aura::Window* app_list_window = app_list_controller->GetWindow();
    DCHECK(app_list_window);
    DCHECK_EQ(app_list_window, waiter.added_window());
  }

  if (wait_for_opening_animation)
    WaitForAppListShowAnimation(/*is_bubble_window=*/true);
}

void AppListTestApi::WaitForAppListShowAnimation(bool is_bubble_window) {
  // Ensure that the app list is visible before waiting for animations.
  AppListController* controller = AppListControllerImpl::Get();
  if (!controller->IsVisible()) {
    AppListVisibilityChangedWaiter waiter;
    waiter.Wait();
    if (!controller->IsVisible())
      ADD_FAILURE() << "Launcher is not visible.";
  }

  // Wait for the app list window animation.
  aura::Window* app_list_window = controller->GetWindow();
  DCHECK(app_list_window);
  ui::LayerAnimationStoppedWaiter().Wait(app_list_window->layer());

  if (!is_bubble_window)
    return;

  DCHECK(!Shell::Get()->IsInTabletMode());

  ScrollableAppsGridView* scrollable_apps_grid_view =
      static_cast<ScrollableAppsGridView*>(GetTopLevelAppsGridView());
  if (!scrollable_apps_grid_view->layer())
    return;

  // Wait for the animation to show the bubble view.
  ui::LayerAnimationStoppedWaiter().Wait(GetAppListBubbleView()->layer());

  // Wait for the animation to show the apps page.
  ui::LayerAnimationStoppedWaiter().Wait(GetAppListBubbleView()
                                             ->apps_page_for_test()
                                             ->scroll_view()
                                             ->contents()
                                             ->layer());

  // Wait for the apps grid slide animation.
  ui::LayerAnimationStoppedWaiter().Wait(scrollable_apps_grid_view->layer());
}

bool AppListTestApi::HasApp(const std::string& app_id) {
  return GetAppListModel()->FindItem(app_id);
}

std::u16string AppListTestApi::GetAppListItemViewName(
    const std::string& item_id) {
  AppListItemView* item_view = GetTopLevelItemViewFromId(item_id);
  if (!item_view)
    return u"";

  return item_view->title()->GetText();
}

AppListItemView* AppListTestApi::GetTopLevelItemViewFromId(
    const std::string& item_id) {
  views::ViewModelT<AppListItemView>* view_model =
      GetTopLevelAppsGridView()->view_model();
  for (size_t i = 0; i < view_model->view_size(); ++i) {
    AppListItemView* app_list_item_view = view_model->view_at(i);
    if (app_list_item_view->item()->id() == item_id)
      return app_list_item_view;
  }

  return nullptr;
}

std::vector<std::string> AppListTestApi::GetTopLevelViewIdList() {
  std::vector<std::string> id_list;
  auto* view_model = GetTopLevelAppsGridView()->view_model();
  for (size_t i = 0; i < view_model->view_size(); ++i) {
    AppListItem* app_list_item = view_model->view_at(i)->item();
    if (app_list_item) {
      id_list.push_back(app_list_item->id());
    }
  }
  return id_list;
}

std::string AppListTestApi::CreateFolderWithApps(
    const std::vector<std::string>& apps) {
  // Only create a folder if there are two or more apps.
  DCHECK_GE(apps.size(), 2u);

  // Skip all item move animations during folder creation.
  ScopedItemMoveAnimationDisabler disabler(GetTopLevelAppsGridView());

  AppListModel* model = GetAppListModel();
  // Create a folder using the first two apps, and add the others to the
  // folder iteratively.
  std::string folder_id = model->MergeItems(apps[0], apps[1]);
  // Return early if MergeItems failed.
  if (folder_id.empty())
    return "";

  for (size_t i = 2; i < apps.size(); ++i)
    model->MergeItems(folder_id, apps[i]);
  return folder_id;
}

std::string AppListTestApi::GetFolderId(const std::string& app_id) {
  return GetAppListModel()->FindItem(app_id)->folder_id();
}

std::vector<std::string> AppListTestApi::GetAppIdsInFolder(
    const std::string& folder_id) {
  AppListItem* folder_item = GetAppListModel()->FindItem(folder_id);
  DCHECK(folder_item->is_folder());
  AppListItemList* folder_list =
      static_cast<AppListFolderItem*>(folder_item)->item_list();
  std::vector<std::string> id_list;
  for (size_t i = 0; i < folder_list->item_count(); ++i)
    id_list.push_back(folder_list->item_at(i)->id());
  return id_list;
}

void AppListTestApi::MoveItemToPosition(const std::string& item_id,
                                        const size_t to_index) {
  AppListItem* app_item = GetAppListModel()->FindItem(item_id);
  const std::string folder_id = app_item->folder_id();

  AppListItemList* item_list;
  std::vector<std::string> top_level_id_list = GetTopLevelViewIdList();
  // The app should be either at the top level or in a folder.
  if (folder_id.empty()) {
    // The app is at the top level.
    item_list = GetAppListModel()->top_level_item_list();
  } else {
    // The app is in the folder with |folder_id|.
    item_list = GetAppListModel()->FindFolderItem(folder_id)->item_list();
  }
  size_t from_index = 0;
  item_list->FindItemIndex(item_id, &from_index);
  item_list->MoveItem(from_index, to_index);
}

int AppListTestApi::GetTopListItemCount() {
  return GetAppListModel()->top_level_item_list()->item_count();
}

views::View* AppListTestApi::GetLastItemInAppsGridView() {
  AppsGridView* grid = GetTopLevelAppsGridView();
  return grid->view_model()->view_at(grid->view_model()->view_size() - 1);
}

PaginationModel* AppListTestApi::GetPaginationModel() {
  return GetPagedAppsGridView()->pagination_model();
}

AppsGridView* AppListTestApi::GetTopLevelAppsGridView() {
  if (ShouldUseBubbleAppList()) {
    return GetAppListBubbleView()
        ->apps_page_for_test()
        ->scrollable_apps_grid_view();
  }

  return GetPagedAppsGridView();
}

const AppsGridView* AppListTestApi::GetTopLevelAppsGridView() const {
  if (ShouldUseBubbleAppList()) {
    return GetAppListBubbleView()
        ->apps_page_for_test()
        ->scrollable_apps_grid_view();
  }

  return GetPagedAppsGridView();
}

AppsGridView* AppListTestApi::GetFolderAppsGridView() {
  return GetAppListFolderView()->items_grid_view();
}

bool AppListTestApi::IsFolderViewAnimating() const {
  return GetAppListFolderView()->IsAnimationRunning();
}

views::View* AppListTestApi::GetBubbleReorderUndoButton() {
  return GetToastContainerViewFromBubble()->GetToastButton();
}

views::View* AppListTestApi::GetFullscreenReorderUndoButton() {
  return GetToastContainerViewFromFullscreenAppList()->GetToastButton();
}

AppListToastType AppListTestApi::GetToastType() const {
  AppListToastContainerView* toast_container =
      ShouldUseBubbleAppList() ? GetToastContainerViewFromBubble()
                               : GetToastContainerViewFromFullscreenAppList();
  return toast_container->current_toast();
}

void AppListTestApi::SetFolderViewAnimationCallback(
    base::OnceClosure folder_animation_done_callback) {
  AppListFolderView* folder_view = GetAppListFolderView();
  folder_view->SetAnimationDoneTestCallback(base::BindOnce(
      [](AppListFolderView* folder_view,
         base::OnceClosure folder_animation_done_callback) {
        std::move(folder_animation_done_callback).Run();
      },
      folder_view, std::move(folder_animation_done_callback)));
}

views::View* AppListTestApi::GetToastContainerView() {
  if (ShouldUseBubbleAppList())
    return GetToastContainerViewFromBubble();

  return GetToastContainerViewFromFullscreenAppList();
}

void AppListTestApi::AddReorderAnimationCallback(
    AppsGridView::TestReorderDoneCallbackType callback) {
  GetTopLevelAppsGridView()->AddReorderCallbackForTest(std::move(callback));
}

void AppListTestApi::AddFadeOutAnimationStartClosure(
    base::OnceClosure closure) {
  GetTopLevelAppsGridView()->AddFadeOutAnimationStartClosureForTest(
      std::move(closure));
}

bool AppListTestApi::HasAnyWaitingReorderDoneCallback() const {
  return GetTopLevelAppsGridView()->HasAnyWaitingReorderDoneCallbackForTest();
}

void AppListTestApi::DisableAppListNudge(bool disable) {
  AppListNudgeController::SetReorderNudgeDisabledForTest(disable);
}

void AppListTestApi::SetContinueSectionPrivacyNoticeAccepted() {
  AppListNudgeController::SetPrivacyNoticeAcceptedForTest(true);
}

void AppListTestApi::ReorderItemInRootByDragAndDrop(int source_index,
                                                    int target_index) {
  test::AppsGridViewTestApi(GetTopLevelAppsGridView())
      .ReorderItemByDragAndDrop(source_index, target_index);
}

views::View* AppListTestApi::GetVisibleSearchResultView(int index) {
  views::View* app_list =
      ShouldUseBubbleAppList()
          ? static_cast<views::View*>(GetAppListBubbleView())
          : static_cast<views::View*>(GetAppListView());

  views::View::Views search_results;
  app_list->GetViewsInGroup(kSearchResultViewGroup, &search_results);

  int current_visible_index = -1;
  for (views::View* view : search_results) {
    if (view->GetVisible())
      ++current_visible_index;
    if (current_visible_index == index)
      return view;
  }
  return nullptr;
}

ash::AppListItemView* AppListTestApi::FindTopLevelFolderItemView() {
  return FindFolderItemView(GetTopLevelAppsGridView());
}

void AppListTestApi::VerifyTopLevelItemVisibility() {
  auto* view_model = GetTopLevelAppsGridView()->view_model();
  std::vector<std::string> invisible_item_names;
  for (size_t view_index = 0; view_index < view_model->view_size();
       ++view_index) {
    auto* item_view = view_model->view_at(view_index);
    if (!item_view->GetVisible())
      invisible_item_names.push_back(item_view->item()->name());
  }

  // Invisible items should be none.
  EXPECT_EQ(std::vector<std::string>(), invisible_item_names);
}

views::View* AppListTestApi::GetRecentAppAt(int index) {
  return GetRecentAppsView()->GetItemViewAt(index);
}

std::vector<ContinueTaskView*> AppListTestApi::GetContinueTaskViews() {
  std::vector<ContinueTaskView*> results;
  ContinueSectionView* const container = GetContinueSectionView();
  for (size_t i = 0; i < container->GetTasksSuggestionsCount(); ++i) {
    results.push_back(container->GetTaskViewAtForTesting(i));
  }
  return results;
}

std::vector<std::string> AppListTestApi::GetRecentAppIds() {
  std::vector<std::string> ids;
  RecentAppsView* recent_apps = GetRecentAppsView();
  for (int i = 0; i < recent_apps->GetItemViewCount(); ++i) {
    ids.push_back(recent_apps->GetItemViewAt(i)->item()->id());
  }
  return ids;
}

void AppListTestApi::SimulateSearch(const std::u16string& query) {
  views::Textfield* textfield = GetSearchBoxView()->search_box();
  textfield->SetText(u"");
  textfield->InsertText(
      query,
      ui::TextInputClient::InsertTextCursorBehavior::kMoveCursorAfterText);
}

SearchResultListView* AppListTestApi::GetTopVisibleSearchResultListView() {
  std::vector<raw_ptr<SearchResultContainerView, VectorExperimental>>
      result_containers = GetSearchView()->result_container_views_for_test();
  // Check that one of the `result_containers` is kApps.
  for (SearchResultContainerView* container : result_containers) {
    SearchResultListView* list_view =
        views::AsViewClass<SearchResultListView>(container);
    if (list_view && list_view->GetVisible()) {
      return list_view;
    }
  }
  return nullptr;
}

void AppListTestApi::ReorderByMouseClickAtContextMenuInAppsGrid(
    ash::AppsGridView* apps_grid_view,
    ash::AppListSortOrder order,
    MenuType menu_type,
    ui::test::EventGenerator* event_generator,
    ReorderAnimationEndState target_state,
    ReorderAnimationEndState* actual_state) {
  // Ensure that the apps grid layout is refreshed before showing the
  // context menu.
  apps_grid_view->GetWidget()->LayoutRootViewIfNecessary();

  // Custom order is not a menu option.
  EXPECT_NE(order, ash::AppListSortOrder::kCustom);

  views::MenuItemView* root_menu =
      ShowRootMenuAndReturn(apps_grid_view, menu_type, event_generator);

  // Get the "Name" or "Color" option.
  views::MenuItemView* reorder_option = nullptr;
  switch (menu_type) {
    case MenuType::kAppListPageMenu:
    case MenuType::kAppListFolderItemMenu:
      reorder_option =
          GetReorderOptionForAppListOrFolderItemMenu(root_menu, order);
      break;
    case MenuType::kAppListNonFolderItemMenu: {
      // The `reorder_option` cached here is the submenu of the options.
      views::MenuItemView* reorder_submenu =
          GetReorderOptionForNonFolderItemMenu(root_menu, order);
      event_generator->MoveMouseTo(
          reorder_submenu->GetBoundsInScreen().CenterPoint());
      event_generator->ClickLeftButton();
      reorder_option = reorder_submenu->GetSubmenu()->GetMenuItemAt(
          GetMenuIndexOfSortingOrder(order));
      break;
    }
  }

  gfx::Point point_on_option =
      reorder_option->GetBoundsInScreen().CenterPoint();

  RegisterReorderAnimationDoneCallback(actual_state);

  // Click at the sorting option.
  event_generator->MoveMouseTo(point_on_option);
  event_generator->ClickLeftButton();

  switch (target_state) {
    case ReorderAnimationEndState::kCompleted:
      // Wait until the reorder animation is done.
      WaitForReorderAnimationAndVerifyItemVisibility();
      break;
    case ReorderAnimationEndState::kFadeOutAborted:
      // The fade out animation starts synchronously so do not wait before
      // animation interruption.
      break;
    case ReorderAnimationEndState::kFadeInAborted:
      // Wait until the fade out animation is done. It ensures that the app
      // list is under fade in animation when animation interruption occurs.
      WaitForFadeOutAnimation();
      break;
  }
}

void AppListTestApi::ReorderByMouseClickAtToplevelAppsGridMenu(
    ash::AppListSortOrder order,
    MenuType menu_type,
    ui::test::EventGenerator* event_generator,
    ReorderAnimationEndState target_state,
    ReorderAnimationEndState* actual_state) {
  ReorderByMouseClickAtContextMenuInAppsGrid(GetTopLevelAppsGridView(), order,
                                             menu_type, event_generator,
                                             target_state, actual_state);
}

void AppListTestApi::ClickOnRedoButtonAndWaitForAnimation(
    ui::test::EventGenerator* event_generator) {
  ReorderAnimationEndState actual_state;
  RegisterReorderAnimationDoneCallback(&actual_state);

  // Mouse click at the undo button.
  views::View* reorder_undo_toast_button = nullptr;
  if (ShouldUseBubbleAppList())
    reorder_undo_toast_button = GetBubbleReorderUndoButton();
  else
    reorder_undo_toast_button = GetFullscreenReorderUndoButton();
  event_generator->MoveMouseTo(
      reorder_undo_toast_button->GetBoundsInScreen().CenterPoint());
  event_generator->ClickLeftButton();

  // Verify that the toast is under animation.
  EXPECT_TRUE(GetToastContainerView()->layer()->GetAnimator()->is_animating());

  WaitForReorderAnimationAndVerifyItemVisibility();
  EXPECT_EQ(ReorderAnimationEndState::kCompleted, actual_state);
}

void AppListTestApi::ClickOnCloseButtonAndWaitForToastAnimation(
    ui::test::EventGenerator* event_generator) {
  AppListToastContainerView* toast_container =
      ShouldUseBubbleAppList() ? GetToastContainerViewFromBubble()
                               : GetToastContainerViewFromFullscreenAppList();
  views::View* close_button = toast_container->GetCloseButton();
  event_generator->MoveMouseTo(close_button->GetBoundsInScreen().CenterPoint());
  event_generator->ClickLeftButton();

  // Wait until the toast fade out animation ends.
  ui::LayerAnimationStoppedWaiter animation_waiter;
  animation_waiter.Wait(toast_container->toast_view()->layer());
}

ui::Layer* AppListTestApi::GetAppListViewLayer() {
  return GetAppListView()->GetWidget()->GetNativeView()->layer();
}

void AppListTestApi::RegisterReorderAnimationDoneCallback(
    ReorderAnimationEndState* actual_state) {
  AddReorderAnimationCallback(base::BindRepeating(
      &AppListTestApi::OnReorderAnimationDone, weak_factory_.GetWeakPtr(),
      !ash::Shell::Get()->IsInTabletMode(), actual_state));
}

void AppListTestApi::OnReorderAnimationDone(bool for_bubble_app_list,
                                            ReorderAnimationEndState* result,
                                            bool abort,
                                            AppListGridAnimationStatus status) {
  DCHECK(status == AppListGridAnimationStatus::kReorderFadeOut ||
         status == AppListGridAnimationStatus::kReorderFadeIn);

  // Record the animation running result.
  if (abort) {
    if (status == AppListGridAnimationStatus::kReorderFadeOut)
      *result = ReorderAnimationEndState::kFadeOutAborted;
    else
      *result = ReorderAnimationEndState::kFadeInAborted;
  } else {
    EXPECT_EQ(AppListGridAnimationStatus::kReorderFadeIn, status);
    *result = ReorderAnimationEndState::kCompleted;

    // Verify that the toast container under the clamshell mode does not have
    // a layer after reorder animation completes.
    if (for_bubble_app_list)
      EXPECT_FALSE(GetToastContainerView()->layer());
  }

  // Callback can be registered without a running loop.
  if (run_loop_for_reorder_)
    run_loop_for_reorder_->Quit();
}

void AppListTestApi::WaitForReorderAnimationAndVerifyItemVisibility() {
  run_loop_for_reorder_ = std::make_unique<base::RunLoop>();
  run_loop_for_reorder_->Run();

  VerifyTopLevelItemVisibility();
}

void AppListTestApi::WaitForFadeOutAnimation() {
  ash::AppsGridView* apps_grid_view = GetTopLevelAppsGridView();

  if (apps_grid_view->grid_animation_status_for_test() !=
      AppListGridAnimationStatus::kReorderFadeOut) {
    // The apps grid is not under fade out animation so no op.
    return;
  }

  ASSERT_TRUE(!run_loop_for_reorder_ || !run_loop_for_reorder_->running());
  run_loop_for_reorder_ = std::make_unique<base::RunLoop>();
  apps_grid_view->AddFadeOutAnimationDoneClosureForTest(
      run_loop_for_reorder_->QuitClosure());
  run_loop_for_reorder_->Run();
}

}  // namespace ash