chromium/ash/app_list/views/search_result_list_view_unittest.cc

// Copyright 2014 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/search_result_list_view.h"

#include <stddef.h>

#include <map>
#include <memory>
#include <utility>

#include "ash/app_list/app_list_test_view_delegate.h"
#include "ash/app_list/model/search/search_model.h"
#include "ash/app_list/model/search/test_search_result.h"
#include "ash/app_list/views/search_result_view.h"
#include "ash/style/ash_color_provider.h"
#include "base/memory/raw_ptr.h"
#include "base/strings/stringprintf.h"
#include "base/strings/utf_string_conversions.h"
#include "ui/views/controls/label.h"
#include "ui/views/layout/flex_layout_view.h"
#include "ui/views/test/views_test_base.h"
#include "ui/views/test/widget_test.h"

namespace ash {
namespace test {

namespace {
int kDefaultSearchItems = 3;

// Preferred sizing for different types of search result views.
constexpr int kPreferredWidth = 640;
constexpr int kDefaultViewHeight = 40;
constexpr int kInlineAnswerViewHeight = 88;
constexpr gfx::Insets kInlineAnswerBorder(16);

// SearchResultListType::SearchResultListType::AnswerCard, and
//  SearchResultListType::kBestMatch do not have associated categories.
constexpr int num_list_types_not_in_category = 2;
// SearchResult::Category::kUnknown does not have an associated list type.
constexpr int num_category_without_list_type = 1;

}  // namespace

class SearchResultListViewTest : public views::test::WidgetTest {
 public:
  SearchResultListViewTest() = default;

  SearchResultListViewTest(const SearchResultListViewTest&) = delete;
  SearchResultListViewTest& operator=(const SearchResultListViewTest&) = delete;

  ~SearchResultListViewTest() override = default;

  // Overridden from testing::Test:
  void SetUp() override {
    views::test::WidgetTest::SetUp();
    widget_ = CreateTopLevelPlatformWidget();

    default_view_ = std::make_unique<SearchResultListView>(
        &view_delegate_, nullptr,
        SearchResultView::SearchResultViewType::kDefault, std::nullopt);
    default_view_->SetListType(
        SearchResultListView::SearchResultListType::kBestMatch);
    default_view_->SetActive(true);

    answer_card_view_ = std::make_unique<SearchResultListView>(
        &view_delegate_, nullptr,
        SearchResultView::SearchResultViewType::kAnswerCard, std::nullopt);
    answer_card_view_->SetListType(
        SearchResultListView::SearchResultListType::kAnswerCard);
    answer_card_view_->SetActive(true);

    widget_->SetBounds(gfx::Rect(0, 0, 700, 500));
    widget_->GetContentsView()->AddChildView(default_view_.get());
    widget_->GetContentsView()->AddChildView(answer_card_view_.get());
    widget_->Show();
    default_view_->SetResults(GetResults());
    answer_card_view_->SetResults(GetResults());
  }

  void TearDown() override {
    default_view_.reset();
    answer_card_view_.reset();
    widget_->CloseNow();
    views::test::WidgetTest::TearDown();
  }

 protected:
  SearchResultListView* default_view() const { return default_view_.get(); }
  SearchResultListView* answer_card_view() const {
    return answer_card_view_.get();
  }

  SearchResultView* GetDefaultResultViewAt(int index) const {
    return default_view_->GetResultViewAt(index);
  }
  SearchResultView* GetAnswerCardResultViewAt(int index) const {
    return answer_card_view_->GetResultViewAt(index);
  }

  views::FlexLayoutView* GetKeyboardShortcutContents(
      SearchResultView* result_view) {
    return result_view->get_keyboard_shortcut_container_for_test();
  }

  views::FlexLayoutView* GetTitleContents(SearchResultView* result_view) {
    return result_view->get_title_container_for_test();
  }

  views::FlexLayoutView* GetProgressBarContents(SearchResultView* result_view) {
    return result_view->get_progress_bar_container_for_test();
  }

  views::FlexLayoutView* GetDetailsContents(SearchResultView* result_view) {
    return result_view->get_details_container_for_test();
  }

  views::Label* GetResultTextSeparatorLabel(SearchResultView* result_view) {
    return result_view->get_result_text_separator_label_for_test();
  }

  std::vector<SearchResultView*> GetAssistantResultViews() const {
    std::vector<SearchResultView*> results;
    for (ash::SearchResultView* view : default_view_->search_result_views_) {
      auto* result = view->result();
      if (result &&
          result->result_type() == AppListSearchResultType::kAssistantText)
        results.push_back(view);
    }
    return results;
  }

  SearchModel::SearchResults* GetResults() {
    return AppListModelProvider::Get()->search_model()->results();
  }

  void AddAssistantSearchResult() {
    SearchModel::SearchResults* results = GetResults();

    std::unique_ptr<TestSearchResult> assistant_result =
        std::make_unique<TestSearchResult>();
    assistant_result->set_result_type(
        ash::AppListSearchResultType::kAssistantText);
    assistant_result->set_display_type(ash::SearchResultDisplayType::kList);
    assistant_result->SetAccessibleName(u"Accessible Name");
    assistant_result->SetTitle(u"assistant result");
    results->Add(std::move(assistant_result));

    RunPendingMessages();
  }

  void SetUpSearchResults() {
    SearchModel::SearchResults* results = GetResults();
    for (int i = 0; i < kDefaultSearchItems; ++i) {
      std::unique_ptr<TestSearchResult> result =
          std::make_unique<TestSearchResult>();
      result->set_display_type(ash::SearchResultDisplayType::kList);
      result->SetAccessibleName(
          base::UTF8ToUTF16(base::StringPrintf("Result %d", i)));
      result->SetTitle(base::UTF8ToUTF16(base::StringPrintf("Result %d", i)));
      result->set_best_match(true);
      if (i < 2) {
        result->SetAccessibleName(
            base::UTF8ToUTF16(base::StringPrintf("Result %d, Detail", i)));
        result->SetDetails(u"Detail");
      }
      results->Add(std::move(result));
    }

    // Adding results will schedule Update().
    RunPendingMessages();
  }

  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::kIconifiedText);
    shortcut_text_item_3.SetText(u"a");
    shortcut_text_item_3.SetTextTags({});
    keyboard_shortcut_text_vector.push_back(shortcut_text_item_3);

    return keyboard_shortcut_text_vector;
  }

  void SetUpKeyboardShortcutResult() {
    SearchModel::SearchResults* results = GetResults();

    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().
    RunPendingMessages();
  }

  void SetUpKeyboardShortcutAnswerCard(bool long_title) {
    SearchModel::SearchResults* results = GetResults();
    std::unique_ptr<TestSearchResult> result =
        std::make_unique<TestSearchResult>();
    result->set_display_type(ash::SearchResultDisplayType::kAnswerCard);
    result->SetMultilineTitle(true);

    std::u16string title =
        long_title ? u"Arbitarily long answer card text to check multiline "
                     u"behavior and hiding of search result details text "
                   : u" Copy and Paste ";

    result->SetAccessibleName(title);
    result->SetTitle(title);
    result->SetDetails(u"Shortcuts");
    result->SetKeyboardShortcutTextVector(BuildKeyboardShortcutTextVector());
    results->Add(std::move(result));

    // Adding results will schedule Update().
    RunPendingMessages();
  }

  void SetupProgressBarAnswerCard() {
    SearchModel::SearchResults* results = GetResults();
    std::unique_ptr<TestSearchResult> result =
        std::make_unique<TestSearchResult>();
    result->set_display_type(ash::SearchResultDisplayType::kAnswerCard);
    result->set_best_match(true);

    result->SetAccessibleName(u"Memory 2.4GB | 7.6 GB total");
    result->SetDetails(u"Memory 2.4GB | 7.6 GB total");
    auto system_info_data =
        std::make_unique<ash::SystemInfoAnswerCardData>(0.5);

    result->SetSystemInfoAnswerCardData(*system_info_data.get());
    results->Add(std::move(result));

    // Adding results will schedule Update().
    RunPendingMessages();
  }

  int GetOpenResultCountAndReset(int ranking) {
    EXPECT_GT(view_delegate_.open_search_result_counts().count(ranking), 0u);
    int result = view_delegate_.open_search_result_counts()[ranking];
    view_delegate_.open_search_result_counts().clear();
    return result;
  }

  int GetUnifiedViewResultCount() const { return default_view_->num_results(); }

  void AddTestResult() {
    std::unique_ptr<TestSearchResult> result =
        std::make_unique<TestSearchResult>();
    result->set_display_type(ash::SearchResultDisplayType::kList);
    result->set_best_match(true);
    result->SetAccessibleName(u"Accessible Name");
    result->SetTitle(base::UTF8ToUTF16(
        base::StringPrintf("Added Result %d", GetUnifiedViewResultCount())));
    GetResults()->Add(std::move(result));
  }

  void DeleteResultAt(int index) { GetResults()->DeleteAt(index); }

  bool KeyPress(ui::KeyboardCode key_code) {
    ui::KeyEvent event(ui::EventType::kKeyPressed, key_code, ui::EF_NONE);
    return default_view_->OnKeyPressed(event);
  }

  void ExpectConsistent() {
    RunPendingMessages();
    SearchModel::SearchResults* results = GetResults();

    for (size_t i = 0; i < results->item_count(); ++i) {
      SearchResultView* result_view = GetDefaultResultViewAt(i);
      ASSERT_TRUE(result_view) << "result view at " << i;
      EXPECT_EQ(results->GetItemAt(i), result_view->result());
    }
  }

  void DoUpdate() { default_view()->DoUpdate(); }

 private:
  // Needed by SearchResultInlineIconView.
  AshColorProvider ash_color_provider_;
  AppListTestViewDelegate view_delegate_;
  std::unique_ptr<SearchResultListView> default_view_;
  std::unique_ptr<SearchResultListView> answer_card_view_;
  raw_ptr<views::Widget, DanglingUntriaged> widget_;
};

TEST_F(SearchResultListViewTest, SpokenFeedback) {
  SetUpSearchResults();

  // Result 0 has a detail text. Expect that the detail is appended to the
  // accessibility name.
  EXPECT_EQ(u"Result 0, Detail",
            GetDefaultResultViewAt(0)->ComputeAccessibleName());

  // Result 2 has no detail text.
  EXPECT_EQ(u"Result 2", GetDefaultResultViewAt(2)->ComputeAccessibleName());
}

TEST_F(SearchResultListViewTest, KeyboardShortcutResult) {
  default_view()->SetBounds(0, 0, kPreferredWidth, 400);
  SetUpKeyboardShortcutResult();

  EXPECT_EQ(u"Copy and Paste",
            GetDefaultResultViewAt(0)->ComputeAccessibleName());
  EXPECT_TRUE(
      GetKeyboardShortcutContents(GetDefaultResultViewAt(0))->GetVisible());
}

// Verifies that title, details, and keyboard shortcut contents are shown for
// keyboard shortcut answer cards normally but details are hidden for results
// with long titles.
TEST_F(SearchResultListViewTest, KeyboardShortcutAnswerCard) {
  default_view()->SetBounds(0, 0, kPreferredWidth, 400);
  SetUpKeyboardShortcutAnswerCard(/*long_title=*/false);
  // Title, details,and keyboard shortcut views should be visible.
  EXPECT_TRUE(GetTitleContents(GetAnswerCardResultViewAt(0))->GetVisible());
  EXPECT_TRUE(GetDetailsContents(GetAnswerCardResultViewAt(0))->GetVisible());
  EXPECT_TRUE(
      GetResultTextSeparatorLabel(GetAnswerCardResultViewAt(0))->GetVisible());
  EXPECT_TRUE(
      GetKeyboardShortcutContents(GetAnswerCardResultViewAt(0))->GetVisible());

  // Delete the previous result.
  DeleteResultAt(0);

  SetUpKeyboardShortcutAnswerCard(/*long_title=*/true);
  // Title and keyboard shortcut views should be visible. The details view
  // is hidden because the long title view becomes multiline and takes priority.
  EXPECT_TRUE(
      GetKeyboardShortcutContents(GetAnswerCardResultViewAt(0))->GetVisible());
  EXPECT_TRUE(GetTitleContents(GetAnswerCardResultViewAt(0))->GetVisible());

  EXPECT_FALSE(
      GetResultTextSeparatorLabel(GetAnswerCardResultViewAt(0))->GetVisible());

  EXPECT_FALSE(GetDetailsContents(GetAnswerCardResultViewAt(0))->GetVisible());
}

// Verifies that details and progress contents are shown for system info answer
// cards which are of bar chart type normally
TEST_F(SearchResultListViewTest, ProgressBarAnswerCardTest) {
  default_view()->SetBounds(0, 0, kPreferredWidth, 400);
  SetupProgressBarAnswerCard();  // Details,and progress bar views should be
                                 // visible.
  EXPECT_FALSE(GetTitleContents(GetAnswerCardResultViewAt(0))->GetVisible());
  EXPECT_TRUE(GetDetailsContents(GetAnswerCardResultViewAt(0))->GetVisible());
  EXPECT_TRUE(
      GetProgressBarContents(GetAnswerCardResultViewAt(0))->GetVisible());

  EXPECT_FALSE(
      GetResultTextSeparatorLabel(GetAnswerCardResultViewAt(0))->GetVisible());
  EXPECT_FALSE(
      GetKeyboardShortcutContents(GetAnswerCardResultViewAt(0))->GetVisible());
}

TEST_F(SearchResultListViewTest, CorrectEnumLength) {
  EXPECT_EQ(
      // Check that all types except for SearchResultListType::kUnified are
      // included in GetAllListTypesForCategoricalSearch.
      static_cast<int>(SearchResultListView::SearchResultListType::kMaxValue) +
          1 /*0 indexing offset*/,
      static_cast<int>(
          SearchResultListView::GetAllListTypesForCategoricalSearch().size()));
  // Check that all types in AppListSearchResultCategory are included in
  // SearchResultListType.
  EXPECT_EQ(
      static_cast<int>(SearchResultListView::SearchResultListType::kMaxValue) +
          1 /*0 indexing offset*/ - num_list_types_not_in_category,
      static_cast<int>(SearchResult::Category::kMaxValue) +
          1 /*0 indexing offset*/ - num_category_without_list_type);
}

TEST_F(SearchResultListViewTest, SearchResultViewLayout) {
  // Set SearchResultListView bounds and check views are default size.
  default_view()->SetBounds(0, 0, kPreferredWidth, 400);
  SetUpSearchResults();
  // Override search result types.
  GetDefaultResultViewAt(0)->SetSearchResultViewType(
      SearchResultView::SearchResultViewType::kDefault);
  GetDefaultResultViewAt(1)->SetSearchResultViewType(
      SearchResultView::SearchResultViewType::kAnswerCard);
  DoUpdate();

  EXPECT_EQ(gfx::Size(kPreferredWidth, kDefaultViewHeight),
            GetDefaultResultViewAt(0)->size());
  EXPECT_EQ(GetDefaultResultViewAt(0)->TitleAndDetailsOrientationForTest(),
            views::LayoutOrientation::kHorizontal);
  EXPECT_EQ(gfx::Size(kPreferredWidth, kInlineAnswerViewHeight),
            GetDefaultResultViewAt(1)->size());
  EXPECT_EQ(GetDefaultResultViewAt(1)->TitleAndDetailsOrientationForTest(),
            views::LayoutOrientation::kVertical);
}

TEST_F(SearchResultListViewTest, BorderTest) {
  default_view()->SetBounds(0, 0, kPreferredWidth, 400);
  SetUpSearchResults();
  DoUpdate();
  EXPECT_EQ(kInlineAnswerBorder,
            GetAnswerCardResultViewAt(0)->GetBorder()->GetInsets());
  EXPECT_EQ(gfx::Insets(), GetDefaultResultViewAt(0)->GetBorder()->GetInsets());
}

TEST_F(SearchResultListViewTest, ModelObservers) {
  SetUpSearchResults();
  ExpectConsistent();

  // Remove from end.
  DeleteResultAt(kDefaultSearchItems - 1);
  ExpectConsistent();

  AddTestResult();
  ExpectConsistent();

  // Remove from end.
  DeleteResultAt(kDefaultSearchItems - 1);
  ExpectConsistent();

  AddTestResult();
  ExpectConsistent();

  // Delete from start.
  DeleteResultAt(0);
  ExpectConsistent();
}

}  // namespace test
}  // namespace ash