| // 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/constants/ash_features.h" |
| #include "ash/public/cpp/app_list/app_list_features.h" |
| #include "ash/public/cpp/test/test_app_list_color_provider.h" |
| #include "ash/style/ash_color_provider.h" |
| #include "base/strings/stringprintf.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "base/test/scoped_feature_list.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 = 80; |
| constexpr gfx::Insets kInlineAnswerBorder(12); |
| |
| // 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 testing::WithParamInterface<bool> { |
| public: |
| SearchResultListViewTest() = default; |
| |
| SearchResultListViewTest(const SearchResultListViewTest&) = delete; |
| SearchResultListViewTest& operator=(const SearchResultListViewTest&) = delete; |
| |
| ~SearchResultListViewTest() override = default; |
| |
| // Overridden from testing::Test: |
| void SetUp() override { |
| scoped_feature_list_.InitWithFeatureState(features::kProductivityLauncher, |
| IsProductivityLauncherEnabled()); |
| views::test::WidgetTest::SetUp(); |
| widget_ = CreateTopLevelPlatformWidget(); |
| |
| default_view_ = std::make_unique<SearchResultListView>( |
| nullptr, &view_delegate_, nullptr, |
| SearchResultView::SearchResultViewType::kDefault, true, absl::nullopt); |
| default_view_->SetListType( |
| SearchResultListView::SearchResultListType::kBestMatch); |
| default_view_->SetActive(true); |
| |
| answer_card_view_ = std::make_unique<SearchResultListView>( |
| nullptr, &view_delegate_, nullptr, |
| SearchResultView::SearchResultViewType::kAnswerCard, true, |
| absl::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: |
| bool IsProductivityLauncherEnabled() const { return GetParam(); } |
| 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* 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 (auto* 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(); |
| } |
| |
| 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::ET_KEY_PRESSED, 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: |
| base::test::ScopedFeatureList scoped_feature_list_; |
| TestAppListColorProvider color_provider_; // Needed by AppListView. |
| AshColorProvider |
| ash_color_provider_; // Needed by SearchResultInlineIconView. |
| AppListTestViewDelegate view_delegate_; |
| std::unique_ptr<SearchResultListView> default_view_; |
| std::unique_ptr<SearchResultListView> answer_card_view_; |
| views::Widget* widget_; |
| }; |
| |
| // Run search result list view tests with and without productivity launcher |
| // enabled. |
| INSTANTIATE_TEST_SUITE_P(All, SearchResultListViewTest, testing::Bool()); |
| |
| TEST_P(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_P(SearchResultListViewTest, KeyboardShortcutResult) { |
| if (!IsProductivityLauncherEnabled()) |
| return; |
| |
| 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_P(SearchResultListViewTest, KeyboardShortcutAnswerCard) { |
| if (!IsProductivityLauncherEnabled()) |
| return; |
| |
| 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()); |
| } |
| |
| TEST_P(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_P(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_P(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_P(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 |