chromium/ash/app_list/views/app_list_view_pixeltest.cc

// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "ash/app_list/model/search/test_search_result.h"
#include "ash/app_list/test/app_list_test_helper.h"
#include "ash/app_list/views/app_list_bubble_apps_page.h"
#include "ash/app_list/views/app_list_bubble_view.h"
#include "ash/app_list/views/app_list_search_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/assistant/test/assistant_ash_test_base.h"
#include "ash/assistant/ui/assistant_ui_constants.h"
#include "ash/assistant/ui/assistant_view_ids.h"
#include "ash/assistant/ui/main_stage/launcher_search_iph_view.h"
#include "ash/public/cpp/style/dark_light_mode_controller.h"
#include "ash/shelf/shelf.h"
#include "ash/shelf/shelf_navigation_widget.h"
#include "ash/shell.h"
#include "ash/test/ash_test_base.h"
#include "ash/test/pixel/ash_pixel_differ.h"
#include "ash/test/pixel/ash_pixel_test_init_params.h"
#include "ash/test/view_drawn_waiter.h"
#include "ash/wm/tablet_mode/tablet_mode_controller.h"
#include "base/run_loop.h"
#include "base/test/scoped_feature_list.h"
#include "components/feature_engagement/public/feature_constants.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/events/test/event_generator.h"
#include "ui/events/types/event_type.h"
#include "ui/views/controls/scroll_view.h"
#include "ui/views/controls/textfield/textfield_test_api.h"

namespace ash {

namespace {

using TestVariantsParam = std::tuple<bool, bool, bool>;

bool IsRtl(TestVariantsParam param) {
  return std::get<0>(param);
}

bool IsDarkMode(TestVariantsParam param) {
  return std::get<1>(param);
}

bool IsTabletMode(TestVariantsParam param) {
  return std::get<2>(param);
}

std::string GenerateTestSuffix(
    const testing::TestParamInfo<TestVariantsParam>& info) {
  std::string suffix;
  suffix.append(IsRtl(info.param) ? "rtl" : "ltr");
  suffix.append("_");
  suffix.append(IsDarkMode(info.param) ? "dark" : "light");
  suffix.append("_");
  suffix.append(IsTabletMode(info.param) ? "tablet" : "clamshell");
  return suffix;
}

void UseFixedPlaceholderTextAndHideCursor(SearchBoxView* search_box_view) {
  ASSERT_TRUE(search_box_view);

  // Use a fixed placeholder text instead of the one picked randomly to
  // avoid the test flakiness.
  search_box_view->UseFixedPlaceholderTextForTest();

  // Hide the search box cursor to avoid the flakiness due to the blinking.
  views::TextfieldTestApi(search_box_view->search_box())
      .SetCursorLayerOpacity(0.f);
}

}  // namespace

class AppListViewPixelRTLTest
    : public AshTestBase,
      public testing::WithParamInterface<std::tuple<bool /*is_rtl=*/>> {
 public:
  std::optional<pixel_test::InitParams> CreatePixelTestInitParams()
      const override {
    pixel_test::InitParams init_params;
    init_params.under_rtl = IsRtl();
    return init_params;
  }

  void ShowAppList() {
    AppListTestHelper* test_helper = GetAppListTestHelper();
    test_helper->ShowAppList();
  }

  void SetUpAnswerCardResult(SearchModel::SearchResults* results,
                             int init_id,
                             int new_result_count) {
    std::unique_ptr<TestSearchResult> result =
        std::make_unique<TestSearchResult>();
    result->set_result_id(base::NumberToString(init_id));
    result->set_display_type(ash::SearchResultDisplayType::kAnswerCard);
    result->SetTitle(u"Answer Card Title");
    result->set_display_score(1000);
    result->SetDetails(u"Answer Card Details");
    result->set_best_match(false);
    results->Add(std::move(result));

    // Adding results will schedule Update().
    base::RunLoop().RunUntilIdle();
  }

  void SetUpURLResult(SearchModel::SearchResults* results,
                      int init_id,
                      int new_result_count) {
    auto result = std::make_unique<TestSearchResult>();
    result->set_result_id(base::NumberToString(init_id));
    result->set_display_type(ash::SearchResultDisplayType::kList);

    std::vector<SearchResult::TextItem> title_text_vector;
    SearchResult::TextItem title_text_item_1(
        ash::SearchResultTextItemType::kString);
    title_text_item_1.SetText(u"youtube");
    title_text_item_1.SetTextTags({SearchResult::Tag(
        SearchResult::Tag::NONE, 0, result->details().length())});
    title_text_vector.push_back(title_text_item_1);
    result->SetTitleTextVector(title_text_vector);

    std::vector<SearchResult::TextItem> details_text_vector;
    SearchResult::TextItem details_text_item_1(
        ash::SearchResultTextItemType::kString);
    details_text_item_1.SetText(u"youtube.com");
    details_text_item_1.SetTextTags({SearchResult::Tag(
        SearchResult::Tag::URL, 0, result->details().length())});
    details_text_vector.push_back(details_text_item_1);
    result->SetDetailsTextVector(details_text_vector);

    result->SetAccessibleName(u"Accessible Name");
    result->set_result_id("Test Search Result");
    result->set_best_match(true);
    results->Add(std::move(result));

    // Adding results will schedule Update().
    base::RunLoop().RunUntilIdle();
  }

  std::vector<SearchResult::TextItem> BuildKeyboardShortcutTextVector() {
    std::vector<SearchResult::TextItem> keyboard_shortcut_text_vector;
    SearchResult::TextItem shortcut_text_item_1(
        ash::SearchResultTextItemType::kIconifiedText);
    shortcut_text_item_1.SetText(u"ctrl");
    shortcut_text_item_1.SetTextTags({});
    keyboard_shortcut_text_vector.push_back(shortcut_text_item_1);

    SearchResult::TextItem shortcut_text_item_2(
        ash::SearchResultTextItemType::kString);
    shortcut_text_item_2.SetText(u" + ");
    shortcut_text_item_2.SetTextTags({});
    keyboard_shortcut_text_vector.push_back(shortcut_text_item_2);

    SearchResult::TextItem shortcut_text_item_3(
        ash::SearchResultTextItemType::kIconCode);
    shortcut_text_item_3.SetIconCode(
        SearchResultTextItem::IconCode::kKeyboardShortcutSearch);
    keyboard_shortcut_text_vector.push_back(shortcut_text_item_3);

    SearchResult::TextItem shortcut_text_item_4(
        ash::SearchResultTextItemType::kIconCode);
    shortcut_text_item_4.SetIconCode(
        SearchResultTextItem::IconCode::kKeyboardShortcutLeft);
    keyboard_shortcut_text_vector.push_back(shortcut_text_item_4);

    return keyboard_shortcut_text_vector;
  }

  void SetUpKeyboardShortcutResult(SearchModel::SearchResults* results) {
    std::unique_ptr<TestSearchResult> result =
        std::make_unique<TestSearchResult>();
    result->set_display_type(ash::SearchResultDisplayType::kList);
    result->SetAccessibleName(u"Copy and Paste");
    result->SetTitle(u"Copy and Paste");
    result->SetDetails(u"Shortcuts");
    result->set_best_match(true);
    result->SetKeyboardShortcutTextVector(BuildKeyboardShortcutTextVector());
    results->Add(std::move(result));

    // Adding results will schedule Update().
    base::RunLoop().RunUntilIdle();
  }

  bool IsRtl() const { return std::get<0>(GetParam()); }
};

INSTANTIATE_TEST_SUITE_P(RTL,
                         AppListViewPixelRTLTest,
                         testing::Combine(testing::Bool()));

// Verifies Answer Card search results under the clamshell mode.
TEST_P(AppListViewPixelRTLTest, AnswerCardSearchResult) {
  ShowAppList();

  // Press a key to start a search.
  PressAndReleaseKey(ui::VKEY_A);
  // Populate answer card result.
  auto* test_helper = GetAppListTestHelper();
  SearchModel::SearchResults* results = test_helper->GetSearchResults();
  SetUpAnswerCardResult(results, /*init_id=*/1, /*new_result_count=*/1);
  test_helper->GetBubbleAppListSearchView()
      ->OnSearchResultContainerResultsChanged();
  // OnSearchResultContainerResultsChanged will schedule show animations().
  base::RunLoop().RunUntilIdle();

  UseFixedPlaceholderTextAndHideCursor(test_helper->GetSearchBoxView());
  EXPECT_TRUE(GetPixelDiffer()->CompareUiComponentsOnPrimaryScreen(
      "bubble_launcher_answer_card_search_results", 12,
      GetAppListTestHelper()->GetBubbleView(),
      GetPrimaryShelf()->navigation_widget()));
}

// Verifies URL results under the clamshell mode.
TEST_P(AppListViewPixelRTLTest, URLSearchResult) {
  ShowAppList();

  // Press a key to start a search.
  PressAndReleaseKey(ui::VKEY_Y);
  // Populate answer card result.
  auto* test_helper = GetAppListTestHelper();
  SearchModel::SearchResults* results = test_helper->GetSearchResults();
  SetUpURLResult(results, /*init_id=*/1, /*new_result_count=*/1);
  test_helper->GetBubbleAppListSearchView()
      ->OnSearchResultContainerResultsChanged();
  // OnSearchResultContainerResultsChanged will schedule show animations().
  base::RunLoop().RunUntilIdle();

  UseFixedPlaceholderTextAndHideCursor(test_helper->GetSearchBoxView());
  EXPECT_TRUE(GetPixelDiffer()->CompareUiComponentsOnPrimaryScreen(
      "bubble_launcher_url_search_results", 11,
      GetAppListTestHelper()->GetBubbleView(),
      GetPrimaryShelf()->navigation_widget()));
}

// Verifies keyboard shortcut results under the clamshell mode.
TEST_P(AppListViewPixelRTLTest, KeyboardShortcutSearchResult) {
  ShowAppList();

  // Press a key to start a search.
  PressAndReleaseKey(ui::VKEY_Y);
  // Populate answer card result.
  auto* test_helper = GetAppListTestHelper();
  SearchModel::SearchResults* results = test_helper->GetSearchResults();
  SetUpKeyboardShortcutResult(results);
  test_helper->GetBubbleAppListSearchView()
      ->OnSearchResultContainerResultsChanged();
  // OnSearchResultContainerResultsChanged will schedule show animations().
  base::RunLoop().RunUntilIdle();

  UseFixedPlaceholderTextAndHideCursor(test_helper->GetSearchBoxView());
  EXPECT_TRUE(GetPixelDiffer()->CompareUiComponentsOnPrimaryScreen(
      "bubble_launcher_ks_search_results", /*revision_number=*/3,
      GetAppListTestHelper()->GetBubbleView()));
}

// Verifies the app list view under the clamshell mode.
TEST_P(AppListViewPixelRTLTest, Basics) {
  GetAppListTestHelper()->AddAppItemsWithColorAndName(
      /*num_apps=*/2, AppListTestHelper::IconColorType::kAlternativeColor,
      /*set_name=*/true);
  ShowAppList();
  UseFixedPlaceholderTextAndHideCursor(
      GetAppListTestHelper()->GetSearchBoxView());
  EXPECT_TRUE(GetPixelDiffer()->CompareUiComponentsOnPrimaryScreen(
      "bubble_launcher_basics", 11, GetAppListTestHelper()->GetBubbleView(),
      GetPrimaryShelf()->navigation_widget()));
}

// Verifies that the app list gradient zones work as expected.
TEST_P(AppListViewPixelRTLTest, GradientZone) {
  GetAppListTestHelper()->AddAppItemsWithColorAndName(
      /*num_apps=*/22, AppListTestHelper::IconColorType::kAlternativeColor,
      /*set_name=*/true);
  ShowAppList();
  UseFixedPlaceholderTextAndHideCursor(
      GetAppListTestHelper()->GetSearchBoxView());
  views::ScrollView* scroll_view =
      GetAppListTestHelper()->GetBubbleAppsPage()->scroll_view();

  // Scroll the bubble app list so that some app list icons are beneath the
  // gradient zones.
  scroll_view->ScrollToPosition(scroll_view->vertical_scroll_bar(),
                                /*position=*/20);

  EXPECT_TRUE(GetPixelDiffer()->CompareUiComponentsOnPrimaryScreen(
      "bubble_launcher_gradient_zone", 11,
      GetAppListTestHelper()->GetBubbleView(),
      GetPrimaryShelf()->navigation_widget()));
}

class AppListViewLauncherSearchIphTest
    : public AssistantAshTestBase,
      public testing::WithParamInterface<TestVariantsParam> {
 public:
  std::optional<pixel_test::InitParams> CreatePixelTestInitParams()
      const override {
    pixel_test::InitParams init_params;
    init_params.under_rtl = IsRtl(GetParam());
    return init_params;
  }

  void SetUp() override {
    AssistantAshTestBase::SetUp();

    DarkLightModeController::Get()->SetDarkModeEnabledForTest(
        IsDarkMode(GetParam()));

    Shell::Get()->tablet_mode_controller()->SetEnabledForTest(
        IsTabletMode(GetParam()));

    // Set a testing text so that the pixel test can compare.
    LauncherSearchIphView::SetChipTextForTesting(u"chip");

    GetAppListTestHelper()->search_model()->SetWouldTriggerLauncherSearchIph(
        true);
  }
};

INSTANTIATE_TEST_SUITE_P(RTL,
                         AppListViewLauncherSearchIphTest,
                         testing::Combine(testing::Bool(),
                                          testing::Bool(),
                                          testing::Bool()),
                         &GenerateTestSuffix);

TEST_P(AppListViewLauncherSearchIphTest, Basic) {
  GetAppListTestHelper()->ShowAppList();
  raw_ptr<SearchBoxView> search_box_view =
      GetAppListTestHelper()->GetSearchBoxView();
  ASSERT_TRUE(search_box_view);
  LeftClickOn(search_box_view->assistant_button());

  auto* const iph_view = search_box_view->GetIphView();
  ASSERT_TRUE(iph_view);
  ViewDrawnWaiter().Wait(iph_view);

  UseFixedPlaceholderTextAndHideCursor(search_box_view);
  EXPECT_TRUE(GetPixelDiffer()->CompareUiComponentsOnPrimaryScreen(
      "launcher_search_iph", /*revision_number=*/4, search_box_view));
}

class AppListViewTabletPixelTest
    : public AshTestBase,
      public testing::WithParamInterface<std::tuple</*rtl=*/bool>> {
 public:
  // AshTestBase:
  std::optional<pixel_test::InitParams> CreatePixelTestInitParams()
      const override {
    pixel_test::InitParams init_params;
    init_params.under_rtl = IsRtl();
    return init_params;
  }

  void SetUp() override {
    AshTestBase::SetUp();

    Shell::Get()->tablet_mode_controller()->SetEnabledForTest(true);

    AppListTestHelper* test_helper = GetAppListTestHelper();
    test_helper->GetSearchBoxView()->UseFixedPlaceholderTextForTest();
    test_helper->AddAppItemsWithColorAndName(
        /*num_apps=*/32, AppListTestHelper::IconColorType::kAlternativeColor,
        /*set_name=*/true);
  }

 protected:
  bool IsRtl() const { return std::get<0>(GetParam()); }
};

INSTANTIATE_TEST_SUITE_P(RTL,
                         AppListViewTabletPixelTest,
                         testing::Combine(testing::Bool()));

// Verifies the default layout for tablet mode launcher.
TEST_P(AppListViewTabletPixelTest, Basic) {
  EXPECT_TRUE(GetPixelDiffer()->CompareUiComponentsOnPrimaryScreen(
      "tablet_launcher_basics", 13,
      GetAppListTestHelper()->GetAppsContainerView()));
}

// Verifies that the top gradient zone of the tablet mode launcher works
// correctly.
TEST_P(AppListViewTabletPixelTest, TopGradientZone) {
  test::AppsGridViewTestApi test_api(
      GetAppListTestHelper()->GetRootPagedAppsGridView());

  // Drag the first launcher page upwards so that some apps are within the
  // top gradient zone.
  const gfx::Point start_page_drag = test_api.GetViewAtIndex(GridIndex(0, 0))
                                         ->GetIconBoundsInScreen()
                                         .bottom_left();
  auto* generator = GetEventGenerator();
  generator->set_current_screen_location(start_page_drag);
  generator->PressTouch();
  generator->MoveTouchBy(0, -40);

  EXPECT_TRUE(GetPixelDiffer()->CompareUiComponentsOnPrimaryScreen(
      "tablet_launcher_top_gradient_zone", 11,
      GetAppListTestHelper()->GetAppsContainerView()));
}

// Verifies that the bottom gradient zone of the tablet mode launcher works
// correctly.
TEST_P(AppListViewTabletPixelTest, BottomGradientZone) {
  test::AppsGridViewTestApi test_api(
      GetAppListTestHelper()->GetRootPagedAppsGridView());

  // Drag the first launcher page upwards so that some apps are within the
  // bottom gradient zone.
  const gfx::Point start_page_drag = test_api.GetViewAtIndex(GridIndex(0, 0))
                                         ->GetIconBoundsInScreen()
                                         .bottom_left();
  auto* generator = GetEventGenerator();
  generator->set_current_screen_location(start_page_drag);
  generator->PressTouch();
  generator->MoveTouchBy(0, -90);

  EXPECT_TRUE(GetPixelDiffer()->CompareUiComponentsOnPrimaryScreen(
      "tablet_launcher_bottom_gradient_zone", 13,
      GetAppListTestHelper()->GetAppsContainerView()));
}

TEST_P(AppListViewTabletPixelTest, SearchBoxViewActive) {
  raw_ptr<SearchBoxView> search_box_view =
      GetAppListTestHelper()->GetSearchBoxView();
  search_box_view->SetSearchBoxActive(true, ui::EventType::kUnknown);

  EXPECT_TRUE(GetPixelDiffer()->CompareUiComponentsOnPrimaryScreen(
      "search_box_view_active", 7, search_box_view));
}

class AppListViewAssistantZeroStateTest
    : public AssistantAshTestBase,
      public testing::WithParamInterface<TestVariantsParam> {
 public:
  std::optional<pixel_test::InitParams> CreatePixelTestInitParams()
      const override {
    pixel_test::InitParams init_params;
    init_params.under_rtl = IsRtl(GetParam());
    return init_params;
  }

  void SetUp() override {
    scoped_feature_list_.InitWithFeatures(
        {feature_engagement::kIPHLauncherSearchHelpUiFeature}, {});

    AssistantAshTestBase::SetUp();
    DarkLightModeController::Get()->SetDarkModeEnabledForTest(
        IsDarkMode(GetParam()));
    Shell::Get()->tablet_mode_controller()->SetEnabledForTest(
        IsTabletMode(GetParam()));

    // Set a testing text so that the pixel test can compare.
    LauncherSearchIphView::SetChipTextForTesting(u"chip");
  }

 private:
  base::test::ScopedFeatureList scoped_feature_list_;
};

INSTANTIATE_TEST_SUITE_P(RTL,
                         AppListViewAssistantZeroStateTest,
                         testing::Combine(/*IsRtl=*/testing::Bool(),
                                          /*IsDarkMode=*/testing::Bool(),
                                          /*IsTabletMode=*/testing::Bool()),
                         &GenerateTestSuffix);

TEST_P(AppListViewAssistantZeroStateTest, Basic) {
  ShowAssistantUi();

  auto* const assistant_page_view = page_view();
  ViewDrawnWaiter().Wait(
      assistant_page_view->GetViewByID(AssistantViewID::kZeroStateView));

  EXPECT_TRUE(GetPixelDiffer()->CompareUiComponentsOnPrimaryScreen(
      "app_list_view_assistant_zero_state", 9,
      assistant_page_view->GetViewByID(AssistantViewID::kZeroStateView)));
}

}  // namespace ash