chromium/ash/app_list/views/app_list_bubble_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/app_list_bubble_view.h"

#include <list>
#include <memory>
#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/model/app_list_test_model.h"
#include "ash/app_list/model/search/test_search_result.h"
#include "ash/app_list/test/app_list_test_helper.h"
#include "ash/app_list/test_app_list_client.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_folder_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/assistant/app_list_bubble_assistant_page.h"
#include "ash/app_list/views/continue_section_view.h"
#include "ash/app_list/views/continue_task_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/assistant/model/assistant_ui_model.h"
#include "ash/controls/scroll_view_gradient_helper.h"
#include "ash/public/cpp/app_list/app_list_features.h"
#include "ash/public/cpp/assistant/controller/assistant_ui_controller.h"
#include "ash/public/cpp/style/color_provider.h"
#include "ash/public/cpp/test/assistant_test_api.h"
#include "ash/shell.h"
#include "ash/style/ash_color_provider.h"
#include "ash/system/notification_center/notification_center_tray.h"
#include "ash/system/tray/tray_constants.h"
#include "ash/system/unified/unified_system_tray.h"
#include "ash/test/ash_test_base.h"
#include "base/run_loop.h"
#include "base/strings/string_number_conversions.h"
#include "base/test/bind.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/scoped_feature_list.h"
#include "chromeos/ash/services/assistant/public/cpp/assistant_enums.h"
#include "chromeos/constants/chromeos_features.h"
#include "testing/gtest/include/gtest/gtest.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/compositor/test/test_utils.h"
#include "ui/events/event_constants.h"
#include "ui/events/keycodes/keyboard_codes_posix.h"
#include "ui/gfx/geometry/point.h"
#include "ui/gfx/geometry/rect.h"
#include "ui/message_center/message_center.h"
#include "ui/message_center/public/cpp/notification.h"
#include "ui/message_center/public/cpp/notifier_id.h"
#include "ui/views/accessibility/view_accessibility.h"
#include "ui/views/controls/button/label_button.h"
#include "ui/views/controls/image_view.h"
#include "ui/views/controls/scroll_view.h"
#include "ui/views/controls/textfield/textfield.h"
#include "ui/views/view_utils.h"
#include "ui/views/widget/widget.h"

using views::Widget;

namespace ash {
namespace {

constexpr int kBorderInset = 1;

SearchModel* GetSearchModel() {
  return AppListModelProvider::Get()->search_model();
}

void AddSearchResult(const std::string& id, const std::u16string& title) {
  auto search_result = std::make_unique<TestSearchResult>();
  search_result->set_result_id(id);
  search_result->set_display_type(SearchResultDisplayType::kList);
  search_result->SetTitle(title);
  search_result->set_best_match(true);
  GetSearchModel()->results()->Add(std::move(search_result));
}

AppListBubblePresenter* GetBubblePresenter() {
  return Shell::Get()->app_list_controller()->bubble_presenter_for_test();
}

views::View* GetSearchBoxSeparator() {
  return GetBubblePresenter()->bubble_view_for_test()->separator_for_test();
}

AssistantVisibility GetAssistantVisibility() {
  return AssistantUiController::Get()->GetModel()->visibility();
}

bool IsAnimatingProperty(
    ui::Layer* layer,
    ui::LayerAnimationElement::AnimatableProperty property) {
  auto* animator = layer->GetAnimator();
  return animator && animator->IsAnimatingProperty(property);
}

class AppListBubbleViewTest : public AshTestBase {
 public:
  AppListBubbleViewTest() = default;
  ~AppListBubbleViewTest() override = default;

  // Simulates the Assistant being enabled.
  void SimulateAssistantEnabled() {
    assistant_test_api_ = AssistantTestApi::Create();
    assistant_test_api_->EnableAssistantAndWait();
  }

  // Shows the app list on the primary display.
  void ShowAppList() { GetAppListTestHelper()->ShowAppList(); }

  void DismissAppList() { GetAppListTestHelper()->Dismiss(); }

  void AddContinueSuggestionResult(int num_suggestions) {
    GetAppListTestHelper()->AddContinueSuggestionResults(num_suggestions);
  }

  void AddRecentApps(int num_apps) {
    GetAppListTestHelper()->AddRecentApps(num_apps);
  }

  void AddAppItems(int num_items) {
    GetAppListTestHelper()->model()->PopulateApps(num_items);
  }

  void AddFolderWithApps(int count) {
    GetAppListTestHelper()->model()->CreateAndPopulateFolderWithApps(count);
  }

  SearchBoxView* GetSearchBoxView() {
    return GetAppListTestHelper()->GetBubbleSearchBoxView();
  }

  AppListBubbleAppsPage* GetAppsPage() {
    return GetAppListTestHelper()->GetBubbleAppsPage();
  }

  ContinueSectionView* GetContinueSectionView() {
    return GetAppListTestHelper()->GetBubbleContinueSectionView();
  }

  RecentAppsView* GetRecentAppsView() {
    return GetAppListTestHelper()->GetBubbleRecentAppsView();
  }

  AppListToastContainerView* GetToastContainerView() {
    return GetAppsPage()->toast_container_for_test();
  }

  ScrollableAppsGridView* GetAppsGridView() {
    return GetAppListTestHelper()->GetScrollableAppsGridView();
  }

  AppListBubbleSearchPage* GetSearchPage() {
    return GetAppListTestHelper()->GetBubbleSearchPage();
  }

  AppListBubbleAssistantPage* GetAssistantPage() {
    return GetAppListTestHelper()->GetBubbleAssistantPage();
  }

  views::View* GetFocusedView() {
    return GetAppListTestHelper()
        ->GetBubbleView()
        ->GetFocusManager()
        ->GetFocusedView();
  }

  bool IsNotificationBubbleShown() {
    return GetPrimaryShelf()
        ->GetStatusAreaWidget()
        ->notification_center_tray()
        ->IsBubbleShown();
  }

  const char* GetFocusedViewName() {
    auto* view = GetFocusedView();
    return view ? view->GetClassName() : "none";
  }

  std::unique_ptr<AssistantTestApi> assistant_test_api_;
};

class AppListBubbleViewDragTest : public AppListBubbleViewTest,
                                  public testing::WithParamInterface<bool> {
 public:
  AppListBubbleViewDragTest() = default;
  ~AppListBubbleViewDragTest() override = default;

  void SetUp() override {
    scoped_feature_list_.InitWithFeatureState(
        app_list_features::kDragAndDropRefactor, GetParam());
    AppListBubbleViewTest::SetUp();
  }
  base::test::ScopedFeatureList scoped_feature_list_;
};
INSTANTIATE_TEST_SUITE_P(All, AppListBubbleViewDragTest, testing::Bool());

TEST_F(AppListBubbleViewTest, LayerConfiguration) {
  ShowAppList();

  // Verify that nothing has changed the layer configuration.
  ui::Layer* layer = GetBubblePresenter()->bubble_view_for_test()->layer();
  ASSERT_TRUE(layer);
  EXPECT_FALSE(layer->fills_bounds_opaquely());
  EXPECT_TRUE(layer->is_fast_rounded_corner());
  EXPECT_EQ(layer->background_blur(), ColorProvider::kBackgroundBlurSigma);
}

// Tests some basic layout coordinates, because we don't have screenshot tests.
// See go/cros-launcher-spec for layout.
TEST_F(AppListBubbleViewTest, Layout) {
  ShowAppList();

  // The view has a background.
  auto* app_list_bubble_view = GetAppListTestHelper()->GetBubbleView();
  EXPECT_TRUE(app_list_bubble_view->background());

  // Check the bounds of the search box search icon.
  auto* search_box_view = GetSearchBoxView();
  auto* search_icon = search_box_view->search_icon();
  gfx::Rect search_icon_bounds =
      search_icon->ConvertRectToWidget(search_icon->GetLocalBounds());
  EXPECT_EQ("17,19 20x20", search_icon_bounds.ToString());

  // Check height of search box view.
  EXPECT_EQ(56, search_box_view->height());

  // The separator is immediately under the search box.
  gfx::Point separator_origin;
  views::View::ConvertPointToWidget(GetSearchBoxSeparator(), &separator_origin);
  EXPECT_EQ(kBorderInset, separator_origin.x());
  EXPECT_EQ(kBorderInset + search_box_view->height(), separator_origin.y());
}

TEST_F(AppListBubbleViewTest,
       ShowingBubbleUpdatesContinueSectionAndRecentApps) {
  // Show the app list with 3 tasks and 4 recent apps.
  AddContinueSuggestionResult(3);
  AddRecentApps(4);
  AddAppItems(5);
  ShowAppList();

  // Hide the app list. The widget and views are cached.
  DismissAppList();

  // While the app list is hidden, update to have 4 tasks and 5 recent apps.
  GetAppListTestHelper()->GetSearchResults()->DeleteAll();
  AddContinueSuggestionResult(4);
  AddRecentApps(5);
  ShowAppList();

  // Continue section and recent apps have the updated item counts.
  EXPECT_EQ(GetContinueSectionView()->GetTasksSuggestionsCount(), 4u);
  EXPECT_EQ(GetRecentAppsView()->GetItemViewCount(), 5);
}

TEST_F(AppListBubbleViewTest, OpeningBubbleTriggersAnimations) {
  // Enable animations.
  ui::ScopedAnimationDurationScaleMode duration(
      ui::ScopedAnimationDurationScaleMode::NON_ZERO_DURATION);

  // Enable app list nudge to test the animation.
  GetAppListTestHelper()->DisableAppListNudge(false);

  // Show an app list with all sections.
  AddContinueSuggestionResult(4);
  AddRecentApps(5);
  AddAppItems(5);
  ShowAppList();

  // The bubble view starts animating.
  auto* bubble_view = GetAppListTestHelper()->GetBubbleView();
  ui::Layer* bubble_layer = bubble_view->layer();
  EXPECT_TRUE(IsAnimatingProperty(
      bubble_layer, ui::LayerAnimationElement::AnimatableProperty::BOUNDS));
  EXPECT_TRUE(IsAnimatingProperty(
      bubble_layer, ui::LayerAnimationElement::AnimatableProperty::OPACITY));
  // Layer top edge animates up.
  EXPECT_GT(bubble_layer->bounds().y(), bubble_view->y());

  // Each section view animates, starting with a translation down, therefore
  // visually moving up.
  views::View* view = GetContinueSectionView();
  EXPECT_TRUE(IsAnimatingProperty(
      view->layer(), ui::LayerAnimationElement::AnimatableProperty::TRANSFORM));
  EXPECT_FLOAT_EQ(view->layer()->transform().To2dTranslation().y(), 20.f);

  view = GetRecentAppsView();
  EXPECT_TRUE(IsAnimatingProperty(
      view->layer(), ui::LayerAnimationElement::AnimatableProperty::TRANSFORM));
  EXPECT_FLOAT_EQ(view->layer()->transform().To2dTranslation().y(), 40.f);

  view = GetToastContainerView();
  EXPECT_TRUE(IsAnimatingProperty(
      view->layer(), ui::LayerAnimationElement::AnimatableProperty::TRANSFORM));
  EXPECT_FLOAT_EQ(view->layer()->transform().To2dTranslation().y(), 60.f);

  view = GetAppsGridView();
  EXPECT_TRUE(IsAnimatingProperty(
      view->layer(), ui::LayerAnimationElement::AnimatableProperty::TRANSFORM));
  EXPECT_FLOAT_EQ(view->layer()->transform().To2dTranslation().y(), 80.f);
}

TEST_F(AppListBubbleViewTest, OpeningBubbleWithSideShelfTriggersAnimations) {
  // Enable animations.
  ui::ScopedAnimationDurationScaleMode duration(
      ui::ScopedAnimationDurationScaleMode::NON_ZERO_DURATION);

  // Enable app list nudge to test the animation.
  GetAppListTestHelper()->DisableAppListNudge(false);

  // Enable side shelf.
  GetPrimaryShelf()->SetAlignment(ShelfAlignment::kRight);

  // Show an app list with all sections.
  AddContinueSuggestionResult(4);
  AddRecentApps(5);
  AddAppItems(5);
  ShowAppList();

  // The bubble view starts animating.
  auto* bubble_view = GetAppListTestHelper()->GetBubbleView();
  ui::Layer* bubble_layer = bubble_view->layer();
  EXPECT_TRUE(IsAnimatingProperty(
      bubble_layer, ui::LayerAnimationElement::AnimatableProperty::BOUNDS));
  EXPECT_TRUE(IsAnimatingProperty(
      bubble_layer, ui::LayerAnimationElement::AnimatableProperty::OPACITY));
  // Layer top edge animates down.
  EXPECT_LT(bubble_layer->bounds().y(), bubble_view->y());

  // Each section view animates, starting with a translation down, therefore
  // visually moving down.
  views::View* view = GetContinueSectionView();
  EXPECT_TRUE(IsAnimatingProperty(
      view->layer(), ui::LayerAnimationElement::AnimatableProperty::TRANSFORM));
  EXPECT_FLOAT_EQ(view->layer()->transform().To2dTranslation().y(), -20.f);

  view = GetRecentAppsView();
  EXPECT_TRUE(IsAnimatingProperty(
      view->layer(), ui::LayerAnimationElement::AnimatableProperty::TRANSFORM));
  EXPECT_FLOAT_EQ(view->layer()->transform().To2dTranslation().y(), -40.f);

  view = GetToastContainerView();
  EXPECT_TRUE(IsAnimatingProperty(
      view->layer(), ui::LayerAnimationElement::AnimatableProperty::TRANSFORM));
  EXPECT_FLOAT_EQ(view->layer()->transform().To2dTranslation().y(), -60.f);

  view = GetAppsGridView();
  EXPECT_TRUE(IsAnimatingProperty(
      view->layer(), ui::LayerAnimationElement::AnimatableProperty::TRANSFORM));
  EXPECT_FLOAT_EQ(view->layer()->transform().To2dTranslation().y(), -80.f);
}

TEST_F(AppListBubbleViewTest, ShowAnimationCreatesAndDestroysLayers) {
  // Enable animations.
  ui::ScopedAnimationDurationScaleMode duration(
      ui::ScopedAnimationDurationScaleMode::NON_ZERO_DURATION);

  // Enable app list nudge to test the animation.
  GetAppListTestHelper()->DisableAppListNudge(false);

  // Show an app list with all sections.
  AddContinueSuggestionResult(4);
  AddRecentApps(5);
  AddAppItems(5);
  ShowAppList();

  // The animating sections have layers created.
  auto* continue_section =
      GetAppListTestHelper()->GetBubbleContinueSectionView();
  EXPECT_TRUE(continue_section->layer());
  auto* recent_apps = GetRecentAppsView();
  EXPECT_TRUE(recent_apps->layer());
  auto* separator = GetAppsPage()->separator_for_test();
  EXPECT_TRUE(separator->layer());
  auto* toast_container = GetToastContainerView();
  EXPECT_TRUE(toast_container->layer());
  auto* apps_grid_view = GetAppsGridView();
  EXPECT_TRUE(apps_grid_view->layer());

  // Finish the animation.
  ui::LayerAnimationStoppedWaiter().Wait(apps_grid_view->layer());

  // Temporary layers are cleaned up.
  EXPECT_FALSE(continue_section->layer());
  EXPECT_FALSE(recent_apps->layer());
  EXPECT_FALSE(separator->layer());
  EXPECT_FALSE(toast_container->layer());

  // The apps grid view always has a layer, it still exists.
  EXPECT_TRUE(apps_grid_view->layer());
}

TEST_F(AppListBubbleViewTest, ShowAnimationDestroysAndRestoresShadow) {
  // Enable animations.
  ui::ScopedAnimationDurationScaleMode duration(
      ui::ScopedAnimationDurationScaleMode::NON_ZERO_DURATION);

  AddAppItems(20);
  ShowAppList();

  // Shadow is suppressed during show animation for performance.
  auto* app_list_bubble_view = GetAppListTestHelper()->GetBubbleView();
  EXPECT_FALSE(app_list_bubble_view->view_shadow_for_test());

  // Finish the animation.
  auto* apps_grid_view = GetAppsGridView();
  ui::LayerAnimationStoppedWaiter().Wait(apps_grid_view->layer());

  // Shadow is restored - when kJelly is enabled, no shadow is expected, for
  // consistency with bubbles in system tray area.
  EXPECT_FALSE(app_list_bubble_view->view_shadow_for_test());
}

TEST_F(AppListBubbleViewTest, ShowAnimationRecordsSmoothnessHistogram) {
  base::HistogramTester histograms;

  // Enable animations.
  ui::ScopedAnimationDurationScaleMode duration(
      ui::ScopedAnimationDurationScaleMode::NON_ZERO_DURATION);

  // Show an app list with just the apps grid.
  AddAppItems(5);
  ShowAppList();

  // Wait for the animation to finish.
  ui::Layer* layer = GetAppsGridView()->layer();
  ui::LayerAnimationStoppedWaiter().Wait(layer);

  // Ensure there is one more frame presented after animation finishes to allow
  // animation throughput data to be passed from cc to ui.
  layer->GetCompositor()->ScheduleFullRedraw();
  EXPECT_TRUE(ui::WaitForNextFrameToBePresented(layer->GetCompositor()));

  // Smoothness was recorded.
  histograms.ExpectTotalCount(
      "Apps.ClamshellLauncher.AnimationSmoothness.OpenAppsPage", 1);
  histograms.ExpectTotalCount("Apps.ClamshellLauncher.AnimationSmoothness.Open",
                              1);
}

TEST_F(AppListBubbleViewTest, HideAnimationsRecordsSmoothnessHistogram) {
  base::HistogramTester histograms;

  // Show the app list without animation.
  AddAppItems(5);
  ShowAppList();

  // Enable animations.
  ui::ScopedAnimationDurationScaleMode duration(
      ui::ScopedAnimationDurationScaleMode::NON_ZERO_DURATION);

  AppListBubbleView* view = GetBubblePresenter()->bubble_view_for_test();
  ui::Layer* layer = view->layer();

  // Run the hide animation and wait for it to finish.
  view->StartHideAnimation(/*is_side_shelf=*/false, base::DoNothing());
  ui::LayerAnimationStoppedWaiter().Wait(layer);

  // Ensure there is one more frame presented after animation finishes to allow
  // animation throughput data to be passed from cc to ui.
  layer->GetCompositor()->ScheduleFullRedraw();
  EXPECT_TRUE(ui::WaitForNextFrameToBePresented(layer->GetCompositor()));

  // Smoothness was recorded.
  histograms.ExpectTotalCount(
      "Apps.ClamshellLauncher.AnimationSmoothness.Close", 1);
}

TEST_F(AppListBubbleViewTest, AssistantScreenshotClosesBubbleWithoutAnimation) {
  SimulateAssistantEnabled();
  AddAppItems(5);

  // Show the app list without animation.
  ShowAppList();

  // Switch to the assistant page.
  LeftClickOn(GetSearchBoxView()->assistant_button());

  // Enable animations.
  ui::ScopedAnimationDurationScaleMode duration(
      ui::ScopedAnimationDurationScaleMode::NON_ZERO_DURATION);

  // Simulate the app list being closed by taking a screenshot with assistant.
  // This makes AppListControllerImpl::ShouldDismissImmediately() return true.
  AssistantUiController::Get()->ToggleUi(
      std::nullopt, assistant::AssistantExitPoint::kScreenshot);

  // The bubble dismissed immediately so it is not animating.
  ui::Layer* bubble_layer = GetAppListTestHelper()->GetBubbleView()->layer();
  ASSERT_TRUE(bubble_layer);
  EXPECT_FALSE(IsAnimatingProperty(
      bubble_layer, ui::LayerAnimationElement::AnimatableProperty::BOUNDS));
  EXPECT_FALSE(IsAnimatingProperty(
      bubble_layer, ui::LayerAnimationElement::AnimatableProperty::OPACITY));

  // App list is closed.
  EXPECT_FALSE(Shell::Get()->app_list_controller()->IsVisible());
}

TEST_F(AppListBubbleViewTest, ShutdownDuringHideAnimationDoesNotCrash) {
  base::HistogramTester histograms;

  // Enable animations.
  ui::ScopedAnimationDurationScaleMode duration(
      ui::ScopedAnimationDurationScaleMode::NON_ZERO_DURATION);

  // Show the app list and wait for the show animation to finish.
  AddAppItems(5);
  ShowAppList();
  ui::LayerAnimationStoppedWaiter().Wait(GetAppsGridView()->layer());

  // Dismiss the app list, but don't wait for the animation to finish.
  GetAppListTestHelper()->Dismiss();

  // No crash.
}

TEST_F(AppListBubbleViewTest, OpeningBubbleFocusesSearchBox) {
  ShowAppList();

  SearchBoxView* search_box_view = GetSearchBoxView();
  EXPECT_TRUE(search_box_view->search_box()->HasFocus());
  EXPECT_TRUE(search_box_view->is_search_box_active());
}

TEST_F(AppListBubbleViewTest, SearchBoxCloseButtonVisibleLongQuery) {
  ShowAppList();

  SearchBoxView* search_box_view = GetSearchBoxView();
  EXPECT_TRUE(search_box_view->search_box()->HasFocus());
  EXPECT_TRUE(search_box_view->is_search_box_active());

  // Enter a query, and verify that search results page is shown.
  PressAndReleaseKey(ui::VKEY_A);

  EXPECT_TRUE(GetSearchPage()->GetVisible());
  EXPECT_TRUE(
      search_box_view->filter_and_close_button_container()->GetVisible());
  for (int i = 0; i < 100; ++i) {
    PressAndReleaseKey(ui::VKEY_A);
  }
  // Close button should be visible for long queries and within search box
  // view bounds.
  EXPECT_TRUE(
      search_box_view->filter_and_close_button_container()->GetVisible());
  EXPECT_TRUE(search_box_view->GetBoundsInScreen().Contains(
      search_box_view->close_button()->GetBoundsInScreen()));
}

TEST_F(AppListBubbleViewTest, OpeningBubbleTwiceFocusesSearchBox) {
  AddAppItems(1);
  ShowAppList();

  // Click on an item, which takes focus out of the search field.
  AppListItemView* item =
      GetAppListTestHelper()->GetScrollableAppsGridView()->GetItemViewAt(0);
  LeftClickOn(item);
  SearchBoxView* search_box_view = GetSearchBoxView();
  ASSERT_FALSE(search_box_view->search_box()->HasFocus());

  // The app list view and widget are cached after this close.
  DismissAppList();

  // Search box is focused on next show.
  ShowAppList();
  EXPECT_TRUE(search_box_view->search_box()->HasFocus());
  EXPECT_TRUE(search_box_view->is_search_box_active());
}

TEST_F(AppListBubbleViewTest, ClosingBubbleClearsSearch) {
  AddAppItems(1);
  ShowAppList();

  // Enter a query, and verify that search results page is shown.
  PressAndReleaseKey(ui::VKEY_A);

  EXPECT_FALSE(GetAppsPage()->GetVisible());
  EXPECT_TRUE(GetSearchPage()->GetVisible());
  EXPECT_FALSE(GetAssistantPage()->GetVisible());

  views::Textfield* search_box_input = GetSearchBoxView()->search_box();
  EXPECT_TRUE(search_box_input->HasFocus());
  EXPECT_EQ(u"a", search_box_input->GetText());
  TestAppListClient* client = GetAppListTestHelper()->app_list_client();
  EXPECT_EQ(std::vector<std::u16string>({u"a"}),
            client->GetAndResetPastSearchQueries());

  // The app list view and widget are cached after this close.
  DismissAppList();
  EXPECT_EQ(std::vector<std::u16string>({u""}),
            client->GetAndResetPastSearchQueries());

  // Search box is empty on next show.
  ShowAppList();
  EXPECT_TRUE(GetAppsPage()->GetVisible());
  EXPECT_FALSE(GetSearchPage()->GetVisible());
  EXPECT_FALSE(GetAssistantPage()->GetVisible());

  search_box_input = GetSearchBoxView()->search_box();
  EXPECT_TRUE(search_box_input->HasFocus());
  EXPECT_EQ(u"", search_box_input->GetText());
}

TEST_F(AppListBubbleViewTest, SearchBoxTextUsesPrimaryTextColor) {
  ShowAppList();

  views::Textfield* search_box = GetSearchBoxView()->search_box();
  EXPECT_EQ(search_box->GetTextColor(),
            AshColorProvider::Get()->GetContentLayerColor(
                AshColorProvider::ContentLayerType::kTextColorPrimary));
}

TEST_F(AppListBubbleViewTest, SearchBoxShowsAssistantButton) {
  SimulateAssistantEnabled();
  ShowAppList();

  // By default the assistant button is visible.
  SearchBoxView* view = GetSearchBoxView();
  EXPECT_TRUE(view->edge_button_container()->GetVisible());
  EXPECT_FALSE(view->filter_and_close_button_container()->GetVisible());

  // Typing text shows the close button instead.
  PressAndReleaseKey(ui::VKEY_A);
  EXPECT_FALSE(view->edge_button_container()->GetVisible());
  EXPECT_TRUE(view->filter_and_close_button_container()->GetVisible());
}

TEST_F(AppListBubbleViewTest, ClickingAssistantButtonShowsAssistantPage) {
  SimulateAssistantEnabled();
  ShowAppList();
  ASSERT_EQ(AssistantVisibility::kClosed, GetAssistantVisibility());

  SearchBoxView* search_box = GetSearchBoxView();
  LeftClickOn(search_box->assistant_button());

  EXPECT_FALSE(search_box->GetVisible());
  EXPECT_FALSE(GetSearchBoxSeparator()->GetVisible());
  EXPECT_FALSE(GetAppsPage()->GetVisible());
  EXPECT_FALSE(GetSearchPage()->GetVisible());
  EXPECT_TRUE(GetAssistantPage()->GetVisible());

  // Assistant was notified of visibility change.
  EXPECT_EQ(AssistantVisibility::kVisible, GetAssistantVisibility());
}

TEST_F(AppListBubbleViewTest, AssistantPageLayout) {
  SimulateAssistantEnabled();
  ShowAppList();
  LeftClickOn(GetSearchBoxView()->assistant_button());

  // Assistant not have a background so the blurred launcher is visible
  // underneath the AppListBubbleAssistantPage view.
  EXPECT_FALSE(GetAssistantPage()->GetBackground());

  // Assistant fills the bubble view, so that any suggestion chips will appear
  // at the bottom.
  auto* app_list_bubble_view = GetAppListTestHelper()->GetBubbleView();
  gfx::Rect expected_bounds = app_list_bubble_view->bounds();
  expected_bounds.Inset(kBorderInset);
  EXPECT_EQ(GetAssistantPage()->bounds(), expected_bounds);
}

TEST_F(AppListBubbleViewTest, SearchBoxCloseButton) {
  ShowAppList();
  PressAndReleaseKey(ui::VKEY_A);
  TestAppListClient* const app_list_client =
      GetAppListTestHelper()->app_list_client();
  EXPECT_EQ(std::vector<std::u16string>({u"a"}),
            app_list_client->GetAndResetPastSearchQueries());

  // Close button is visible after typing text.
  SearchBoxView* search_box_view = GetSearchBoxView();
  search_box_view->GetWidget()->LayoutRootViewIfNecessary();
  EXPECT_TRUE(
      search_box_view->filter_and_close_button_container()->GetVisible());
  EXPECT_FALSE(search_box_view->search_box()->GetText().empty());

  // Clicking the close button clears the search, but the search box is still
  // focused/active.
  LeftClickOn(search_box_view->close_button());
  EXPECT_EQ(std::vector<std::u16string>({u""}),
            app_list_client->GetAndResetPastSearchQueries());
  EXPECT_FALSE(
      search_box_view->filter_and_close_button_container()->GetVisible());
  EXPECT_TRUE(search_box_view->search_box()->GetText().empty());
  EXPECT_TRUE(search_box_view->search_box()->HasFocus());
  EXPECT_TRUE(search_box_view->is_search_box_active());
}

TEST_F(AppListBubbleViewTest, AppsPageShownByDefault) {
  ShowAppList();

  EXPECT_TRUE(GetAppsPage()->GetVisible());
  EXPECT_FALSE(GetSearchPage()->GetVisible());
  EXPECT_FALSE(GetAssistantPage()->GetVisible());
}

TEST_F(AppListBubbleViewTest, TypingTextShowsSearchPage) {
  ShowAppList();

  AppListBubbleAppsPage* apps_page = GetAppsPage();
  AppListBubbleSearchPage* search_page = GetSearchPage();

  // Type some text.
  PressAndReleaseKey(ui::VKEY_A);

  // Search page is shown.
  EXPECT_FALSE(apps_page->GetVisible());
  EXPECT_TRUE(search_page->GetVisible());

  // Backspace to remove the text.
  PressAndReleaseKey(ui::VKEY_BACK);

  // Apps page is shown.
  EXPECT_TRUE(apps_page->GetVisible());
  EXPECT_FALSE(search_page->GetVisible());
}

TEST_F(AppListBubbleViewTest, TypingTextStartsSearch) {
  ShowAppList();

  PressAndReleaseKey(ui::VKEY_A);

  TestAppListClient* client = GetAppListTestHelper()->app_list_client();
  EXPECT_EQ(std::vector<std::u16string>({u"a"}),
            client->GetAndResetPastSearchQueries());

  PressAndReleaseKey(ui::VKEY_B);
  EXPECT_EQ(std::vector<std::u16string>({u"ab"}),
            client->GetAndResetPastSearchQueries());

  PressAndReleaseKey(ui::VKEY_BACK);
  EXPECT_EQ(std::vector<std::u16string>({u"a"}),
            client->GetAndResetPastSearchQueries());
}

TEST_F(AppListBubbleViewTest, BackActionsClearSearch) {
  ShowAppList();
  SearchBoxView* search_box_view = GetSearchBoxView();

  PressAndReleaseKey(ui::VKEY_A);
  EXPECT_FALSE(search_box_view->search_box()->GetText().empty());

  PressAndReleaseKey(ui::VKEY_BROWSER_BACK);
  EXPECT_TRUE(search_box_view->search_box()->GetText().empty());
  EXPECT_TRUE(search_box_view->is_search_box_active());

  PressAndReleaseKey(ui::VKEY_A);
  EXPECT_FALSE(search_box_view->search_box()->GetText().empty());

  PressAndReleaseKey(ui::VKEY_ESCAPE);
  EXPECT_TRUE(search_box_view->search_box()->GetText().empty());
  EXPECT_TRUE(search_box_view->is_search_box_active());
}

TEST_F(AppListBubbleViewTest, BackActionsCloseAppList) {
  ShowAppList();
  GetAppListTestHelper()->CheckVisibility(true);

  PressAndReleaseKey(ui::VKEY_BROWSER_BACK);
  GetAppListTestHelper()->CheckVisibility(false);

  ShowAppList();
  GetAppListTestHelper()->CheckVisibility(true);

  PressAndReleaseKey(ui::VKEY_ESCAPE);
  GetAppListTestHelper()->CheckVisibility(false);
}

TEST_F(AppListBubbleViewTest, BackActionsCloseFolder) {
  AddFolderWithApps(3);
  ShowAppList();

  AppListItemView* folder_item =
      GetAppListTestHelper()->GetScrollableAppsGridView()->GetItemViewAt(0);

  LeftClickOn(folder_item);
  EXPECT_TRUE(GetAppListTestHelper()->IsInFolderView());
  EXPECT_TRUE(GetAppListTestHelper()->GetBubbleFolderView()->GetVisible());

  // Folder closed.
  PressAndReleaseKey(ui::VKEY_BROWSER_BACK);
  EXPECT_TRUE(GetBubblePresenter()->IsShowing());
  EXPECT_FALSE(GetAppListTestHelper()->IsInFolderView());
  EXPECT_FALSE(GetAppListTestHelper()->GetBubbleFolderView()->GetVisible());

  LeftClickOn(folder_item);
  EXPECT_TRUE(GetAppListTestHelper()->IsInFolderView());
  EXPECT_TRUE(GetAppListTestHelper()->GetBubbleFolderView()->GetVisible());

  // Folder closed.
  PressAndReleaseKey(ui::VKEY_ESCAPE);
  EXPECT_TRUE(GetBubblePresenter()->IsShowing());
  EXPECT_FALSE(GetAppListTestHelper()->IsInFolderView());
  EXPECT_FALSE(GetAppListTestHelper()->GetBubbleFolderView()->GetVisible());
}

TEST_F(AppListBubbleViewTest, BackActionWithSelectedItemSelectsFolder) {
  AddFolderWithApps(3);
  ShowAppList();

  AppListItemView* folder_item =
      GetAppListTestHelper()->GetScrollableAppsGridView()->GetItemViewAt(0);

  LeftClickOn(folder_item);
  EXPECT_TRUE(GetAppListTestHelper()->IsInFolderView());
  EXPECT_TRUE(GetAppListTestHelper()->GetBubbleFolderView()->GetVisible());

  // Focus on first item in folder
  PressAndReleaseKey(ui::VKEY_TAB);

  // Folder closed.
  PressAndReleaseKey(ui::VKEY_BROWSER_BACK);

  ScrollableAppsGridView* grid_view =
      GetAppListTestHelper()->GetScrollableAppsGridView();
  EXPECT_TRUE(grid_view->has_selected_view());
  EXPECT_TRUE(grid_view->selected_view() == folder_item);
}

TEST_F(AppListBubbleViewTest, CanSelectSearchResults) {
  ShowAppList();

  // Can't select results, search page isn't visible.
  AppListBubbleView* view = GetBubblePresenter()->bubble_view_for_test();
  EXPECT_FALSE(view->CanSelectSearchResults());

  // Typing a key switches to the search page, but we still don't have results.
  PressAndReleaseKey(ui::VKEY_A);
  EXPECT_FALSE(view->CanSelectSearchResults());

  // Search results becoming available allows keyboard selection.
  AddSearchResult("id", u"title");
  base::RunLoop().RunUntilIdle();  // Update search model observers.
  EXPECT_TRUE(view->CanSelectSearchResults());
}

TEST_F(AppListBubbleViewTest, DownArrowMovesFocusToApps) {
  // Add an app, but no "Continue" suggestions.
  AddAppItems(1);
  ShowAppList();

  auto* apps_grid_view = GetAppListTestHelper()->GetScrollableAppsGridView();
  AppListItemView* app_item = apps_grid_view->GetItemViewAt(0);
  SearchBoxView* search_box_view = GetSearchBoxView();
  EXPECT_TRUE(search_box_view->search_box()->HasFocus());

  // Pressing down arrow moves focus into apps.
  PressAndReleaseKey(ui::VKEY_DOWN);
  EXPECT_FALSE(search_box_view->search_box()->HasFocus());
  EXPECT_TRUE(apps_grid_view->IsSelectedView(app_item));
  EXPECT_TRUE(app_item->HasFocus());

  // Pressing up arrow moves focus back to search box.
  PressAndReleaseKey(ui::VKEY_UP);
  EXPECT_TRUE(search_box_view->search_box()->HasFocus());
  EXPECT_FALSE(apps_grid_view->has_selected_view());
  EXPECT_FALSE(app_item->HasFocus());
}

// Exercises AssistantButtonFocusSkipper.
TEST_F(AppListBubbleViewTest, DownAndUpArrowSkipsAssistantButton) {
  SimulateAssistantEnabled();
  // Add an app, but no "Continue" suggestions.
  AddAppItems(1);
  ShowAppList();

  auto* apps_grid_view = GetAppListTestHelper()->GetScrollableAppsGridView();
  AppListItemView* app_item = apps_grid_view->GetItemViewAt(0);
  SearchBoxView* search_box_view = GetSearchBoxView();
  EXPECT_TRUE(search_box_view->search_box()->HasFocus());

  // Pressing down arrow moves focus into apps.
  PressAndReleaseKey(ui::VKEY_DOWN);
  EXPECT_FALSE(search_box_view->search_box()->HasFocus());
  EXPECT_FALSE(search_box_view->assistant_button()->HasFocus());
  EXPECT_TRUE(app_item->HasFocus());

  // Pressing up arrow moves focus back to search box.
  PressAndReleaseKey(ui::VKEY_UP);
  EXPECT_TRUE(search_box_view->search_box()->HasFocus());
  EXPECT_FALSE(search_box_view->assistant_button()->HasFocus());
  EXPECT_FALSE(app_item->HasFocus());

  // Tab key moves focus to assistant button.
  PressAndReleaseKey(ui::VKEY_TAB);
  EXPECT_FALSE(search_box_view->search_box()->HasFocus());
  EXPECT_TRUE(search_box_view->assistant_button()->HasFocus());

  // Shift-tab moves focus back to search box.
  PressAndReleaseKey(ui::VKEY_TAB, ui::EF_SHIFT_DOWN);
  EXPECT_TRUE(search_box_view->search_box()->HasFocus());
  EXPECT_FALSE(search_box_view->assistant_button()->HasFocus());
}

TEST_F(AppListBubbleViewTest, DownArrowSelectsRecentsThenApps) {
  // Create enough apps to require scrolling.
  AddAppItems(50);
  // Create enough recent apps that the recents section will show.
  const int kNumRecentApps = 5;
  AddRecentApps(kNumRecentApps);
  ShowAppList();

  // Pressing down arrow once moves focus into recent apps.
  auto* focus_manager = GetAppsPage()->GetFocusManager();
  PressAndReleaseKey(ui::VKEY_DOWN);
  EXPECT_TRUE(GetRecentAppsView()->Contains(focus_manager->GetFocusedView()));

  // Pressing down arrow again moves focus into the apps grid.
  PressAndReleaseKey(ui::VKEY_DOWN);
  auto* apps_grid = GetAppListTestHelper()->GetScrollableAppsGridView();
  EXPECT_TRUE(apps_grid->Contains(focus_manager->GetFocusedView()));
}

TEST_F(AppListBubbleViewTest, DownArrowFromRecentsSelectsSameColumnInAppsGrid) {
  AddRecentApps(5);
  AddAppItems(5);
  ShowAppList();

  for (int column = 0; column < 5; column++) {
    // Pressing down arrow from an item in recent apps selects the app in the
    // same column in the apps grid.
    AppListItemView* recent_app = GetRecentAppsView()->GetItemViewAt(column);
    recent_app->RequestFocus();
    ASSERT_TRUE(recent_app->HasFocus());

    PressAndReleaseKey(ui::VKEY_DOWN);

    AppListItemView* app = GetAppsGridView()->GetItemViewAt(column);
    EXPECT_TRUE(app->HasFocus()) << "Focus mismatch for column " << column;
  }
}

TEST_F(AppListBubbleViewTest, DownArrowFromToastKeepsSameColumnInAppsGrid) {
  AddRecentApps(5);
  AddAppItems(5);
  ShowAppList();

  AppListController::Get()->UpdateAppListWithNewTemporarySortOrder(
      AppListSortOrder::kColor,
      /*animate=*/false, /*update_position_closure=*/base::OnceClosure());
  ASSERT_TRUE(GetToastContainerView()->GetToastButton());

  for (int column = 0; column < 5; column++) {
    // Pressing down arrow from an item in recent apps selects the app in the
    // same column in the apps grid.
    AppListItemView* recent_app = GetRecentAppsView()->GetItemViewAt(column);
    recent_app->RequestFocus();
    ASSERT_TRUE(recent_app->HasFocus());

    PressAndReleaseKey(ui::VKEY_DOWN);
    EXPECT_TRUE(GetToastContainerView()->GetToastButton()->HasFocus());

    PressAndReleaseKey(ui::VKEY_DOWN);
    AppListItemView* app = GetAppsGridView()->GetItemViewAt(column);
    EXPECT_TRUE(app->HasFocus()) << "Focus mismatch for column " << column;
  }
}

TEST_F(AppListBubbleViewTest, DownArrowFromRecentsSelectsLastColumnInAppsGrid) {
  AddRecentApps(5);
  AddFolderWithApps(2);
  AddFolderWithApps(3);
  ShowAppList();

  // There are only 2 folders, and hence 2 columns, in the top level apps grid.
  auto* apps_grid_view = GetAppsGridView();
  ASSERT_EQ(2u, apps_grid_view->view_model()->view_size());

  // Focus the 5th recent app.
  auto* recent_apps_view = GetRecentAppsView();
  ASSERT_EQ(5, recent_apps_view->GetItemViewCount());
  recent_apps_view->GetItemViewAt(4)->RequestFocus();

  PressAndReleaseKey(ui::VKEY_DOWN);

  // There's no 5th column in the apps grid, so the 2nd item is selected.
  AppListItemView* item = GetAppsGridView()->GetItemViewAt(1);
  EXPECT_TRUE(item->HasFocus());
}

TEST_F(AppListBubbleViewTest, UpArrowFromRecentsSelectsContinueTasks) {
  AddContinueSuggestionResult(4);
  AddRecentApps(5);
  AddAppItems(5);
  ShowAppList();

  ContinueTaskView* last_continue_task =
      GetContinueSectionView()->GetTaskViewAtForTesting(3);
  auto* recent_apps_view = GetRecentAppsView();

  // Pressing 'up' from any column in recent apps moves to the last continue
  // task.
  for (int column = 0; column < 5; ++column) {
    recent_apps_view->GetItemViewAt(column)->RequestFocus();

    PressAndReleaseKey(ui::VKEY_UP);

    EXPECT_TRUE(views::IsViewClass<ContinueTaskView>(GetFocusedView()))
        << GetFocusedViewName();
    EXPECT_TRUE(last_continue_task->HasFocus());
  }
}

TEST_F(AppListBubbleViewTest, UpArrowFromAppsGridSelectsSameColumnInRecents) {
  AddRecentApps(5);
  AddAppItems(5);
  ShowAppList();

  for (int column = 0; column < 5; column++) {
    // Pressing up arrow from an item in the apps grid selects the app in the
    // same column in the recents list.
    AppListItemView* app = GetAppsGridView()->GetItemViewAt(column);
    app->RequestFocus();
    ASSERT_TRUE(app->HasFocus());

    PressAndReleaseKey(ui::VKEY_UP);

    EXPECT_TRUE(GetRecentAppsView()->GetItemViewAt(column)->HasFocus())
        << "Focus mismatch for column " << column;
  }
}

TEST_F(AppListBubbleViewTest, UpArrowFromToastKeepsSameColumnInAppsGrid) {
  AddRecentApps(5);
  AddAppItems(5);
  ShowAppList();

  AppListController::Get()->UpdateAppListWithNewTemporarySortOrder(
      AppListSortOrder::kColor,
      /*animate=*/false, /*update_position_closure=*/base::OnceClosure());
  ASSERT_TRUE(GetToastContainerView()->GetToastButton());

  for (int column = 0; column < 5; column++) {
    // Pressing up arrow from an item in the apps grid selects the app in the
    // same column in the recents list.
    AppListItemView* app = GetAppsGridView()->GetItemViewAt(column);
    app->RequestFocus();
    ASSERT_TRUE(app->HasFocus());

    PressAndReleaseKey(ui::VKEY_UP);
    EXPECT_TRUE(GetToastContainerView()->GetToastButton()->HasFocus());

    PressAndReleaseKey(ui::VKEY_UP);
    EXPECT_TRUE(GetRecentAppsView()->GetItemViewAt(column)->HasFocus())
        << "Focus mismatch for column " << column;
  }
}

TEST_F(AppListBubbleViewTest, UpArrowFromAppsGridSelectsLastColumnInRecents) {
  // Add 4 columns of recents, but 5 columns of apps.
  AddRecentApps(4);
  AddAppItems(5);
  ShowAppList();

  // Select the app in the last column of the apps grid.
  GetAppsGridView()->GetItemViewAt(4)->RequestFocus();

  PressAndReleaseKey(ui::VKEY_UP);

  // The last app in recents is selected.
  EXPECT_TRUE(GetRecentAppsView()->GetItemViewAt(3)->HasFocus());
}

TEST_F(AppListBubbleViewTest,
       UpArrowFromAppsGridWithNoRecentsSelectsContinueTasks) {
  AddContinueSuggestionResult(4);
  // Don't add recents.
  AddAppItems(5);
  ShowAppList();
  GetAppsGridView()->GetItemViewAt(0)->RequestFocus();

  PressAndReleaseKey(ui::VKEY_UP);

  auto* focus_manager = GetAppsPage()->GetFocusManager();
  EXPECT_TRUE(
      GetContinueSectionView()->Contains(focus_manager->GetFocusedView()))
      << GetFocusedViewName();
}

TEST_F(AppListBubbleViewTest, DownArrowMovesFocusToContinueTasks) {
  // Add an app, and some "Continue" suggestions.
  AddAppItems(1);
  // Create enough recent apps that the recents section will show.
  AddContinueSuggestionResult(4);
  ShowAppList();

  auto* apps_grid_view = GetAppListTestHelper()->GetScrollableAppsGridView();

  SearchBoxView* search_box_view = GetSearchBoxView();
  EXPECT_TRUE(search_box_view->search_box()->HasFocus());

  // Pressing down arrow twice moves focus through the two rows of continue
  // tasks. It does not trigger ScrollView scrolling.
  auto* focus_manager = GetAppsPage()->GetFocusManager();
  auto* continue_section = GetContinueSectionView();
  for (int i = 0; i < 2; i++) {
    PressAndReleaseKey(ui::VKEY_DOWN);
    EXPECT_TRUE(continue_section->Contains(focus_manager->GetFocusedView()));
  }

  // Pressing down arrow again moves focus into the apps grid.
  PressAndReleaseKey(ui::VKEY_DOWN);
  EXPECT_TRUE(apps_grid_view->Contains(focus_manager->GetFocusedView()));
}

TEST_F(AppListBubbleViewTest, ClickOnFolderOpensFolder) {
  AddFolderWithApps(3);
  ShowAppList();

  AppListItemView* folder_item = GetAppsGridView()->GetItemViewAt(0);
  LeftClickOn(folder_item);

  // Folder opened.
  EXPECT_TRUE(GetAppListTestHelper()->IsInFolderView());
  EXPECT_TRUE(GetAppListTestHelper()->GetBubbleFolderView()->GetVisible());
}

TEST_F(AppListBubbleViewTest, FolderClosedOnAppListDismiss) {
  AddFolderWithApps(3);
  ShowAppList();

  AppListItemView* folder_item = GetAppsGridView()->GetItemViewAt(0);
  LeftClickOn(folder_item);
  ASSERT_TRUE(GetAppListTestHelper()->IsInFolderView());
  ASSERT_TRUE(GetAppListTestHelper()->GetBubbleFolderView()->GetVisible());

  // The bubble view and widget are cached after dismiss.
  DismissAppList();

  // The folder is closed when the app list is reopened.
  ShowAppList();
  EXPECT_FALSE(GetAppListTestHelper()->IsInFolderView());
  EXPECT_FALSE(GetAppListTestHelper()->GetBubbleFolderView()->GetVisible());
}

TEST_F(AppListBubbleViewTest, FolderClosedAfterInvokingAssistant) {
  SimulateAssistantEnabled();
  AddFolderWithApps(3);
  ShowAppList();

  AppListItemView* folder_item = GetAppsGridView()->GetItemViewAt(0);
  LeftClickOn(folder_item);
  ASSERT_TRUE(GetAppListTestHelper()->IsInFolderView());
  ASSERT_TRUE(GetAppListTestHelper()->GetBubbleFolderView()->GetVisible());

  PressAndReleaseKey(ui::VKEY_ASSISTANT);
  EXPECT_TRUE(GetAssistantPage()->GetVisible());
  EXPECT_FALSE(GetAppListTestHelper()->IsInFolderView());
  EXPECT_FALSE(GetAppListTestHelper()->GetBubbleFolderView()->GetVisible());
}

TEST_F(AppListBubbleViewTest, LargeFolderViewFitsInsideMainBubble) {
  // Create more apps than fit in the default sized folder.
  AddFolderWithApps(30);
  ShowAppList();

  AppListItemView* folder_item =
      GetAppListTestHelper()->GetScrollableAppsGridView()->GetItemViewAt(0);
  LeftClickOn(folder_item);

  // The folder fits inside the bubble.
  gfx::Rect folder_bounds =
      GetAppListTestHelper()->GetBubbleFolderView()->GetBoundsInScreen();
  gfx::Rect bubble_bounds =
      GetBubblePresenter()->bubble_view_for_test()->GetBoundsInScreen();
  EXPECT_TRUE(bubble_bounds.Contains(folder_bounds));

  // The top and bottom of the folder are inset from the bubble top and bottom.
  constexpr int kExpectedInset = 16;
  EXPECT_EQ(folder_bounds.y(), bubble_bounds.y() + kExpectedInset);
  EXPECT_EQ(folder_bounds.bottom(), bubble_bounds.bottom() - kExpectedInset);
}

TEST_F(AppListBubbleViewTest, ClickOutsideFolderClosesFolder) {
  AddFolderWithApps(3);
  ShowAppList();

  AppListItemView* folder_item =
      GetAppListTestHelper()->GetScrollableAppsGridView()->GetItemViewAt(0);
  LeftClickOn(folder_item);

  auto* folder_view = GetAppListTestHelper()->GetBubbleFolderView();
  gfx::Point outside_view =
      folder_view->GetBoundsInScreen().bottom_right() + gfx::Vector2d(10, 10);
  GetEventGenerator()->MoveMouseTo(outside_view);
  GetEventGenerator()->ClickLeftButton();

  // Folder closed.
  EXPECT_FALSE(GetAppListTestHelper()->IsInFolderView());
  EXPECT_FALSE(GetAppListTestHelper()->GetBubbleFolderView()->GetVisible());
}

TEST_P(AppListBubbleViewDragTest, ReparentDragOutOfFolderClosesFolder) {
  AddFolderWithApps(3);
  ShowAppList();

  AppListItemView* folder_item =
      GetAppListTestHelper()->GetScrollableAppsGridView()->GetItemViewAt(0);
  LeftClickOn(folder_item);

  // Drag the first app from the folder's app grid.
  auto* folder_view = GetAppListTestHelper()->GetBubbleFolderView();
  AppListItemView* app_item = folder_view->items_grid_view()->GetItemViewAt(0);
  auto* generator = GetEventGenerator();
  generator->MoveMouseTo(app_item->GetBoundsInScreen().CenterPoint());
  generator->PressLeftButton();
  app_item->FireMouseDragTimerForTest();

  // Drag item out of folder view.
  std::list<base::OnceClosure> tasks;
  tasks.push_back(base::BindLambdaForTesting([&]() {
    gfx::Point outside_view =
        folder_view->GetBoundsInScreen().bottom_right() + gfx::Vector2d(10, 10);
    generator->MoveMouseTo(outside_view);
    generator->MoveMouseBy(10, 10);
    folder_view->items_grid_view()->FireFolderItemReparentTimerForTest();
    // Folder visually closed.
    EXPECT_FALSE(GetAppListTestHelper()->IsInFolderView());
    if (!GetParam()) {
      // Folder is still "visible" because the drag has not ended for the old
      // drag and drop flow. On drag and drop refactor, the drag ends when
      // exiting the view.
      EXPECT_TRUE(GetAppListTestHelper()->GetBubbleFolderView()->GetVisible());
    }
  }));
  tasks.push_back(base::BindLambdaForTesting([&]() {  // End the drag.
    generator->ReleaseLeftButton();
    EXPECT_FALSE(GetAppListTestHelper()->GetBubbleFolderView()->GetVisible());
  }));
  MaybeRunDragAndDropSequenceForAppList(&tasks, /*is_touch=*/false);

  // End the drag.
  generator->ReleaseLeftButton();
  EXPECT_FALSE(GetAppListTestHelper()->GetBubbleFolderView()->GetVisible());
}

TEST_P(AppListBubbleViewDragTest, DragItemInsideFolderDoesNotSelectItem) {
  AddFolderWithApps(3);
  ShowAppList();

  AppListItemView* folder_item =
      GetAppListTestHelper()->GetScrollableAppsGridView()->GetItemViewAt(0);
  LeftClickOn(folder_item);

  // Drag the first app inside the folder's app grid.
  auto* folder_view = GetAppListTestHelper()->GetBubbleFolderView();
  AppListItemView* first_app = folder_view->items_grid_view()->GetItemViewAt(0);
  auto* generator = GetEventGenerator();
  generator->MoveMouseTo(first_app->GetBoundsInScreen().CenterPoint());
  generator->PressLeftButton();
  first_app->FireMouseDragTimerForTest();

  // Quickly drag and release the app.
  std::list<base::OnceClosure> tasks;
  tasks.push_back(base::BindLambdaForTesting([&]() {
    generator->MoveMouseBy(100, 100);
    generator->ReleaseLeftButton();
    // Nothing is selected or focused.
    EXPECT_FALSE(folder_view->items_grid_view()->has_selected_view());
    EXPECT_FALSE(GetFocusedView()) << GetFocusedViewName();
  }));
  MaybeRunDragAndDropSequenceForAppList(&tasks, /*is_touch=*/false);
}

TEST_F(AppListBubbleViewTest, OpenFolderWithMouseDoesNotFocusItem) {
  AddFolderWithApps(3);
  ShowAppList();

  AppListItemView* folder_item = GetAppsGridView()->GetItemViewAt(0);
  LeftClickOn(folder_item);

  AppsGridView* items_grid_view =
      GetAppListTestHelper()->GetBubbleFolderView()->items_grid_view();
  EXPECT_FALSE(items_grid_view->has_selected_view());
  EXPECT_FALSE(GetFocusedView()) << GetFocusedViewName();
}

// Verifies that keyboard focus stays inside an open folder. If this test breaks
// then one of the DisableFocusForShowingActiveFolder() methods needs to be
// updated to include the incorrectly focused view.
TEST_F(AppListBubbleViewTest, PressingTabMovesFocusInsideFolder) {
  // Ensure all sections are showing, so the test verifies that none of these
  // sections (or the hide continue section button) take focus.
  AddContinueSuggestionResult(4);
  AddRecentApps(5);
  AddFolderWithApps(3);
  ShowAppList();

  // Force the sorting toast to show.
  AppListController::Get()->UpdateAppListWithNewTemporarySortOrder(
      AppListSortOrder::kColor,
      /*animate=*/false, /*update_position_closure=*/base::OnceClosure());
  ASSERT_TRUE(GetToastContainerView()->GetToastButton());

  // Open the folder.
  AppListItemView* folder_item = GetAppsGridView()->GetItemViewAt(0);
  LeftClickOn(folder_item);

  PressAndReleaseKey(ui::VKEY_TAB);

  // First item is selected.
  auto* folder_view = GetAppListTestHelper()->GetBubbleFolderView();
  AppsGridView* items_grid_view = folder_view->items_grid_view();
  EXPECT_TRUE(items_grid_view->has_selected_view());
  EXPECT_EQ(items_grid_view->GetItemViewAt(0), GetFocusedView())
      << GetFocusedViewName();

  // Repeatedly pressing tab keeps focus inside the folder view.
  for (int i = 0; i < 10; i++) {
    PressAndReleaseKey(ui::VKEY_TAB);
    EXPECT_TRUE(folder_view->Contains(GetFocusedView()))
        << GetFocusedViewName();
  }
}

// Verifies that focus does not move from a folder to the privacy notice. This
// is a separate test from PressingTabMovesFocusInsideFolder because that test
// verifies the sorting nudge, and the launcher only shows one nudge at a time.
TEST_F(AppListBubbleViewTest, PressingTabInFolderDoesNotFocusPrivacyNotice) {
  // Force the continue section privacy toast to show.
  AppListNudgeController::SetPrivacyNoticeAcceptedForTest(false);
  AddContinueSuggestionResult(4);
  AddFolderWithApps(3);
  ShowAppList();

  // Privacy notice is visible.
  auto* privacy_notice = GetContinueSectionView()->GetPrivacyNoticeForTest();
  ASSERT_TRUE(privacy_notice);
  ASSERT_TRUE(privacy_notice->GetVisible());

  // Open the folder.
  AppListItemView* folder_item = GetAppsGridView()->GetItemViewAt(0);
  LeftClickOn(folder_item);

  // Repeatedly pressing tab keeps focus inside the folder view. In particular,
  // it does not focus the privacy toast.
  auto* folder_view = GetAppListTestHelper()->GetBubbleFolderView();
  for (int i = 0; i < 10; i++) {
    PressAndReleaseKey(ui::VKEY_TAB);
    EXPECT_TRUE(folder_view->Contains(GetFocusedView()))
        << GetFocusedViewName();
  }
}

TEST_F(AppListBubbleViewTest, OpeningFolderRemovesOtherViewsFromAccessibility) {
  AddContinueSuggestionResult(4);
  AddRecentApps(5);
  AddFolderWithApps(5);
  ShowAppList();

  // Force the sorting toast to show.
  AppListController::Get()->UpdateAppListWithNewTemporarySortOrder(
      AppListSortOrder::kColor,
      /*animate=*/false, /*update_position_closure=*/base::OnceClosure());
  ASSERT_TRUE(GetToastContainerView()->GetToastButton());

  // Open the folder.
  AppListItemView* folder_item = GetAppsGridView()->GetItemViewAt(0);
  LeftClickOn(folder_item);

  auto* search_box = GetSearchBoxView();
  EXPECT_TRUE(search_box->GetViewAccessibility().GetIsIgnored());
  EXPECT_TRUE(search_box->GetViewAccessibility().IsLeaf());
  auto* continue_section = GetContinueSectionView();
  EXPECT_TRUE(continue_section->GetViewAccessibility().GetIsIgnored());
  EXPECT_TRUE(continue_section->GetViewAccessibility().IsLeaf());
  auto* recent_apps = GetRecentAppsView();
  EXPECT_TRUE(recent_apps->GetViewAccessibility().GetIsIgnored());
  EXPECT_TRUE(recent_apps->GetViewAccessibility().IsLeaf());
  auto* toast_container = GetToastContainerView();
  EXPECT_TRUE(toast_container->GetViewAccessibility().GetIsIgnored());
  EXPECT_TRUE(toast_container->GetViewAccessibility().IsLeaf());
  auto* apps_grid = GetAppsGridView();
  EXPECT_TRUE(apps_grid->GetViewAccessibility().GetIsIgnored());
  EXPECT_TRUE(apps_grid->GetViewAccessibility().IsLeaf());

  // Close the folder.
  PressAndReleaseKey(ui::VKEY_ESCAPE);

  EXPECT_FALSE(search_box->GetViewAccessibility().GetIsIgnored());
  EXPECT_FALSE(search_box->GetViewAccessibility().IsLeaf());
  EXPECT_FALSE(continue_section->GetViewAccessibility().GetIsIgnored());
  EXPECT_FALSE(continue_section->GetViewAccessibility().IsLeaf());
  EXPECT_FALSE(recent_apps->GetViewAccessibility().GetIsIgnored());
  EXPECT_FALSE(recent_apps->GetViewAccessibility().IsLeaf());
  EXPECT_FALSE(toast_container->GetViewAccessibility().GetIsIgnored());
  EXPECT_FALSE(toast_container->GetViewAccessibility().IsLeaf());
  EXPECT_FALSE(apps_grid->GetViewAccessibility().GetIsIgnored());
  EXPECT_FALSE(apps_grid->GetViewAccessibility().IsLeaf());
}

TEST_F(AppListBubbleViewTest, OpenFolderWithKeyboardFocusesFirstItem) {
  AddFolderWithApps(3);
  ShowAppList();

  AppListItemView* folder_item = GetAppsGridView()->GetItemViewAt(0);
  folder_item->RequestFocus();
  PressAndReleaseKey(ui::VKEY_RETURN);

  // First item is selected and focused.
  AppsGridView* items_grid_view =
      GetAppListTestHelper()->GetBubbleFolderView()->items_grid_view();
  AppListItemView* first_item = items_grid_view->GetItemViewAt(0);
  EXPECT_TRUE(items_grid_view->has_selected_view());
  EXPECT_TRUE(items_grid_view->IsSelectedView(first_item));
  EXPECT_TRUE(first_item->HasFocus()) << GetFocusedViewName();
}

TEST_F(AppListBubbleViewTest, CloseFolderWithNoSelectedItemFocusesSearchBox) {
  AddFolderWithApps(3);
  ShowAppList();

  AppListItemView* folder_item = GetAppsGridView()->GetItemViewAt(0);
  LeftClickOn(folder_item);

  auto* folder_view = GetAppListTestHelper()->GetBubbleFolderView();
  ASSERT_FALSE(folder_view->items_grid_view()->has_selected_view());

  // Close the folder.
  PressAndReleaseKey(ui::VKEY_ESCAPE);

  SearchBoxView* search_box_view = GetSearchBoxView();
  EXPECT_TRUE(search_box_view->search_box()->HasFocus())
      << GetFocusedViewName();
  EXPECT_TRUE(search_box_view->is_search_box_active());
}

TEST_F(AppListBubbleViewTest, CloseFolderWithSelectedItemFocusesFolderItem) {
  AddFolderWithApps(3);
  ShowAppList();

  AppListItemView* folder_item = GetAppsGridView()->GetItemViewAt(0);
  LeftClickOn(folder_item);

  auto* folder_view = GetAppListTestHelper()->GetBubbleFolderView();
  folder_view->items_grid_view()->GetItemViewAt(0)->RequestFocus();
  ASSERT_TRUE(folder_view->items_grid_view()->has_selected_view());

  // Close the folder.
  PressAndReleaseKey(ui::VKEY_ESCAPE);

  // Folder item is selected and focused.
  auto* root_apps_grid_view = GetAppsGridView();
  EXPECT_TRUE(root_apps_grid_view->has_selected_view());
  EXPECT_TRUE(root_apps_grid_view->IsSelectedView(folder_item));
  EXPECT_TRUE(folder_item->HasFocus()) << GetFocusedViewName();
}

TEST_F(AppListBubbleViewTest, ScrollInFolderHeaderScrollsFolder) {
  // Add a folder with enough apps that its grid will be scrollable.
  AddFolderWithApps(30);
  ShowAppList();

  // Open the folder and get the initial scroll position.
  AppListItemView* folder_item = GetAppsGridView()->GetItemViewAt(0);
  LeftClickOn(folder_item);
  auto* folder_view = GetAppListTestHelper()->GetBubbleFolderView();
  auto* scroll_view = folder_view->scroll_view_for_test();
  const int initial_scroll_offset = scroll_view->GetVisibleRect().y();

  // Simulate a mouse wheel scroll up event in the folder header.
  auto* generator = GetEventGenerator();
  generator->MoveMouseTo(
      folder_view->folder_header_view()->GetBoundsInScreen().CenterPoint());
  generator->MoveMouseWheel(0, -10);

  // The view scrolled.
  const int final_scroll_offset = scroll_view->GetVisibleRect().y();
  EXPECT_GT(final_scroll_offset, initial_scroll_offset);
}

gfx::Rect GetStartFadeRect(const gfx::LinearGradient& gradient_mask,
                           gfx::Rect layer_bounds) {
  // Vertical gradient from top to bottom.
  EXPECT_EQ(gradient_mask.angle(), -90);

  // No top gradient
  if (!cc::MathUtil::IsWithinEpsilon(gradient_mask.steps()[0].fraction, 0.f))
    return gfx::Rect();

  float fade_height = gradient_mask.steps()[1].fraction * layer_bounds.height();
  return gfx::Rect(layer_bounds.origin(),
                   gfx::Size(layer_bounds.width(), fade_height));
}

gfx::Rect GetEndFadeRect(const gfx::LinearGradient& gradient_mask,
                         gfx::Rect layer_bounds) {
  // Vertical gradient from top to bottom.
  EXPECT_EQ(gradient_mask.angle(), -90);

  // No bottom gradient
  if (!cc::MathUtil::IsWithinEpsilon(
          gradient_mask.steps()[gradient_mask.step_count() - 1].fraction,
          1.f)) {
    return gfx::Rect();
  }

  float fade_height =
      (1.f - gradient_mask.steps()[gradient_mask.step_count() - 2].fraction) *
      layer_bounds.height();
  float fade_y = layer_bounds.bottom() - fade_height;
  return gfx::Rect(layer_bounds.x(), fade_y, layer_bounds.width(), fade_height);
}

TEST_F(AppListBubbleViewTest, AutoScrollToFitViewOnFocus) {
  // Show an app list with enough apps to fill the page and trigger a gradient
  // at the bottom.
  AddAppItems(50);
  ShowAppList();

  // Scroll view gradient mask layer is created.
  auto* scroll_view = GetAppsPage()->scroll_view();
  EXPECT_FALSE(scroll_view->layer()->gradient_mask().IsEmpty());
  const int rows = base::ClampFloor(50.0 / GetAppsGridView()->cols());

  // Focus the first item on the last row.
  for (int i = 0; i < rows; i++)
    PressAndReleaseKey(ui::VKEY_DOWN);

  gfx::Rect app_view_bounds = GetAppsGridView()
                                  ->GetFocusManager()
                                  ->GetFocusedView()
                                  ->GetBoundsInScreen();
  const gfx::LinearGradient& gradient_mask =
      GetAppsPage()->gradient_helper_for_test()->gradient_mask_for_test();
  gfx::Rect gradient_mask_bounds_start =
      GetStartFadeRect(gradient_mask, scroll_view->GetVisibleRect());
  gfx::Rect gradient_mask_bounds_end =
      GetEndFadeRect(gradient_mask, scroll_view->GetVisibleRect());
  views::View::ConvertRectToScreen(scroll_view, &gradient_mask_bounds_start);
  views::View::ConvertRectToScreen(scroll_view, &gradient_mask_bounds_end);

  // The gradient mask should not obscure the focused app view.
  EXPECT_FALSE(gradient_mask_bounds_start.Intersects(app_view_bounds));
  EXPECT_FALSE(gradient_mask_bounds_end.Intersects(app_view_bounds));

  // Press down arrow two more times to move focus to the first row again.
  PressAndReleaseKey(ui::VKEY_DOWN);
  PressAndReleaseKey(ui::VKEY_DOWN);

  ASSERT_TRUE(GetAppsGridView()->GetItemViewAt(0)->HasFocus());

  app_view_bounds = GetAppsGridView()
                        ->GetFocusManager()
                        ->GetFocusedView()
                        ->GetBoundsInScreen();
  gradient_mask_bounds_start =
      GetStartFadeRect(gradient_mask, scroll_view->GetVisibleRect());
  gradient_mask_bounds_end =
      GetEndFadeRect(gradient_mask, scroll_view->GetVisibleRect());
  views::View::ConvertRectToScreen(scroll_view, &gradient_mask_bounds_start);
  views::View::ConvertRectToScreen(scroll_view, &gradient_mask_bounds_end);

  // The gradient mask should not obscure the focused app view.
  EXPECT_FALSE(gradient_mask_bounds_start.Intersects(app_view_bounds));
  EXPECT_FALSE(gradient_mask_bounds_end.Intersects(app_view_bounds));
}

TEST_P(AppListBubbleViewDragTest, AutoScrollOnTopOfTheBubble) {
  // Show an app list with enough apps to fill the page and trigger a gradient
  // at the bottom.
  const int kTotalAppItems = 50;
  AddAppItems(kTotalAppItems);
  ShowAppList();
  const int rows =
      base::ClampFloor(1.0f * kTotalAppItems / GetAppsGridView()->cols());

  // Focus the first item on the last row.
  for (int i = 0; i < rows; i++) {
    PressAndReleaseKey(ui::VKEY_DOWN);
  }

  // Drag the last app from the app grid.
  AppListItemView* app_item =
      GetAppsGridView()->GetItemViewAt(kTotalAppItems - 1);
  auto* generator = GetEventGenerator();
  generator->MoveMouseTo(app_item->GetBoundsInScreen().CenterPoint());
  generator->PressLeftButton();
  app_item->FireMouseDragTimerForTest();

  gfx::Point top_of_the_bubble = GetBubblePresenter()
                                     ->bubble_view_for_test()
                                     ->GetBoundsInScreen()
                                     .top_center();

  std::list<base::OnceClosure> tasks;
  tasks.push_back(base::BindLambdaForTesting([&]() {
    // Drag app  outside of the bubble. The scroll timer should not be running.
    gfx::Point bubble_view_outside(top_of_the_bubble);
    bubble_view_outside.Offset(0, -20);
    generator->MoveMouseTo(bubble_view_outside);
    ASSERT_FALSE(GetAppsGridView()->auto_scroll_timer_for_test()->IsRunning());
  }));
  tasks.push_back(base::BindLambdaForTesting([&]() {
    // Enter the apps grid bubble should start scrolling up.
    generator->MoveMouseTo(top_of_the_bubble);
    EXPECT_TRUE(GetAppsGridView()->auto_scroll_timer_for_test()->IsRunning());
  }));
  tasks.push_back(
      base::BindLambdaForTesting([&]() { generator->ReleaseLeftButton(); }));
  MaybeRunDragAndDropSequenceForAppList(&tasks, /*is_touch=*/false);
}

// Verifies that hidden app list bubble view does not attempt to change its
// active page when app list model gets cleared.
TEST_F(AppListBubbleViewTest, HiddenAppListPageNotSetDuringShutdown) {
  // Show app list so AppListBubbleView gets created.
  AddAppItems(5);
  ShowAppList();

  DismissAppList();
  EXPECT_EQ(AppListBubblePage::kNone,
            GetAppListTestHelper()->GetBubbleView()->current_page_for_test());

  AppListModelProvider::Get()->ClearActiveModel();
  EXPECT_EQ(AppListBubblePage::kNone,
            GetAppListTestHelper()->GetBubbleView()->current_page_for_test());
}

// Regression test for https://crbug.com/1313140
TEST_F(AppListBubbleViewTest, CanOpenMessageCenterWithKeyboardShortcut) {
  // Add a notification so there's something to focus in the message center.
  auto notification = std::make_unique<message_center::Notification>(
      message_center::NOTIFICATION_TYPE_SIMPLE, "id", u"Title", u"Message",
      ui::ImageModel(), /*display_source=*/std::u16string(), GURL(),
      message_center::NotifierId(), message_center::RichNotificationData(),
      /*delegate=*/nullptr);
  message_center::MessageCenter::Get()->AddNotification(
      std::move(notification));

  // Message center starts closed.
  ASSERT_FALSE(IsNotificationBubbleShown());

  // Open the launcher and do a search.
  AddAppItems(1);
  ShowAppList();
  PressAndReleaseKey(ui::VKEY_A);

  // Search box has focus.
  views::Textfield* search_box_input = GetSearchBoxView()->search_box();
  ASSERT_TRUE(search_box_input->HasFocus());

  // Enable animations.
  ui::ScopedAnimationDurationScaleMode duration(
      ui::ScopedAnimationDurationScaleMode::NON_ZERO_DURATION);

  // Move focus to the message center notification area with Alt-Shift-N. The
  // message center will open and the app list will dismiss.
  PressAndReleaseKey(ui::VKEY_N, ui::EF_ALT_DOWN | ui::EF_SHIFT_DOWN);

  // Wait for the app list hide animation to finish.
  AppListBubbleView* view = GetBubblePresenter()->bubble_view_for_test();
  ui::LayerAnimationStoppedWaiter().Wait(view->layer());

  // Search box did not steal focus.
  EXPECT_FALSE(search_box_input->HasFocus());

  // Message center is still open.
  EXPECT_TRUE(IsNotificationBubbleShown());
}

}  // namespace
}  // namespace ash