// 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.
#ifdef UNSAFE_BUFFERS_BUILD
// TODO(crbug.com/40285824): Remove this and convert code to safer constructs.
#pragma allow_unsafe_buffers
#endif
#include "chrome/browser/ash/app_list/search/search_controller.h"
#include <memory>
#include <string>
#include <vector>
#include "ash/constants/ash_features.h"
#include "ash/constants/ash_pref_names.h"
#include "ash/public/cpp/app_list/app_list_types.h"
#include "base/containers/to_vector.h"
#include "base/memory/raw_ptr.h"
#include "base/ranges/algorithm.h"
#include "base/test/bind.h"
#include "base/time/time.h"
#include "chrome/browser/ash/app_list/search/chrome_search_result.h"
#include "chrome/browser/ash/app_list/search/common/types_util.h"
#include "chrome/browser/ash/app_list/search/ranking/launch_data.h"
#include "chrome/browser/ash/app_list/search/ranking/ranker_manager.h"
#include "chrome/browser/ash/app_list/search/search_controller.h"
#include "chrome/browser/ash/app_list/search/search_provider.h"
#include "chrome/browser/ash/app_list/search/test/search_controller_test_util.h"
#include "chrome/browser/ash/app_list/search/test/test_ranker_manager.h"
#include "chrome/browser/ash/app_list/search/test/test_search_provider.h"
#include "chrome/browser/ash/app_list/search/types.h"
#include "chrome/browser/ash/app_list/test/fake_app_list_model_updater.h"
#include "chrome/browser/ash/app_list/test/test_app_list_controller_delegate.h"
#include "chrome/test/base/testing_profile.h"
#include "components/prefs/pref_service.h"
#include "components/prefs/scoped_user_pref_update.h"
#include "content/public/test/browser_task_environment.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/display/test/test_screen.h"
namespace app_list::test {
namespace {
using testing::ElementsAreArray;
using testing::UnorderedElementsAreArray;
using Category = ash::AppListSearchResultCategory;
using ControlCategory = ash::AppListSearchControlCategory;
using DisplayType = ash::SearchResultDisplayType;
using Result = ash::AppListSearchResultType;
LaunchData CreateFakeLaunchData(const std::string& id) {
app_list::LaunchData launch;
launch.id = id;
launch.result_type = ash::AppListSearchResultType::kPlayStoreApp;
launch.score = 0.0;
return launch;
}
class FakeObserver : public SearchController::Observer {
public:
FakeObserver() = default;
~FakeObserver() override = default;
void OnResultsAdded(
const std::u16string& query,
const std::vector<KeywordInfo>& extracted_keyword_info,
const std::vector<const ChromeSearchResult*>& results) override {
results_added_ = true;
}
bool results_added() const { return results_added_; }
private:
bool results_added_ = false;
};
} // namespace
class SearchControllerTest : public testing::Test {
public:
SearchControllerTest()
: task_environment_(base::test::TaskEnvironment::TimeSource::MOCK_TIME) {}
SearchControllerTest(const SearchControllerTest&) = delete;
SearchControllerTest& operator=(const SearchControllerTest&) = delete;
~SearchControllerTest() override = default;
void SetUp() override {
search_controller_ = std::make_unique<SearchController>(
/*model_updater=*/&model_updater_,
/*list_controller=*/&list_controller_,
/*notifier=*/nullptr, &profile_,
/*federated_service_controller_*/ nullptr);
search_controller_->Initialize();
auto ranker_manager = std::make_unique<TestRankerManager>(&profile_);
ranker_manager_ = ranker_manager.get();
search_controller_->set_ranker_manager_for_test(std::move(ranker_manager));
}
void ExpectIdOrder(std::vector<std::string> expected_ids) {
const auto& actual_results = model_updater_.search_results();
EXPECT_EQ(actual_results.size(), expected_ids.size());
EXPECT_THAT(base::ToVector(actual_results, &ChromeSearchResult::id),
ElementsAreArray(expected_ids));
}
// Compares expected category burn-in iteration numbers to those recorded
// within the search controller. The expected list should not include
// categories for which burn-in number is unset (i.e. = -1).
void ExpectCategoriesToBurnInIterations(
std::vector<std::pair<Category, int>>
expected_categories_to_burn_in_iteration) {
const auto& actual_categories_list =
search_controller_->categories_for_test();
std::vector<std::pair<Category, int>>
actual_categories_to_burn_in_iteration;
for (const auto& category : actual_categories_list) {
if (category.burn_in_iteration != -1) {
actual_categories_to_burn_in_iteration.emplace_back(
category.category, category.burn_in_iteration);
}
}
EXPECT_THAT(
actual_categories_to_burn_in_iteration,
UnorderedElementsAreArray(expected_categories_to_burn_in_iteration));
}
void ExpectIdsToBurnInIterations(std::vector<std::pair<std::string, int>>
expected_ids_to_burn_in_iteration) {
const auto& actual_ids_to_burn_in_iteration =
std::vector<std::pair<std::string, int>>(
search_controller_->burn_in_controller_for_test()
->ids_to_burn_in_iteration_for_test()
.begin(),
search_controller_->burn_in_controller_for_test()
->ids_to_burn_in_iteration_for_test()
.end());
ASSERT_EQ(actual_ids_to_burn_in_iteration.size(),
expected_ids_to_burn_in_iteration.size());
EXPECT_THAT(actual_ids_to_burn_in_iteration,
UnorderedElementsAreArray(expected_ids_to_burn_in_iteration));
}
void Wait() { task_environment_.RunUntilIdle(); }
// Add a wait period in milliseconds to allow results collected from search
// providers. The default period is 1000 milliseconds (i.e., 1 second).
void WaitInMilliseconds(int n = 1000) {
task_environment_.FastForwardBy(base::Milliseconds(n));
}
protected:
content::BrowserTaskEnvironment task_environment_;
display::test::TestScreen test_screen_{/*create_dispay=*/true,
/*register_screen=*/true};
TestingProfile profile_;
FakeAppListModelUpdater model_updater_{&profile_, /*order_delegate=*/nullptr};
std::unique_ptr<SearchController> search_controller_;
::test::TestAppListControllerDelegate list_controller_{};
// Owned by |search_controller_|.
raw_ptr<TestRankerManager, DanglingUntriaged> ranker_manager_{nullptr};
};
// Tests that long queries are truncated to the maximum allowed query length.
TEST_F(SearchControllerTest, TruncateLongQuery) {
std::u16string long_query(kMaxAllowedQueryLength + 1, u'a');
search_controller_->StartSearch(long_query);
EXPECT_EQ(search_controller_->get_query(),
long_query.substr(0, kMaxAllowedQueryLength));
}
// Tests that best matches are ordered first, and categories are ignored when
// ranking within best match.
TEST_F(SearchControllerTest, BestMatchesOrderedAboveOtherResults) {
auto results_1 = MakeListResults(
{"a", "b", "c", "d"},
{Category::kWeb, Category::kWeb, Category::kApps, Category::kWeb},
{0, -1, 1, -1}, {0.4, 0.7, 0.2, 0.8});
ranker_manager_->SetCategoryRanks(
{{Category::kApps, 0.4}, {Category::kWeb, 0.2}});
search_controller_->StartSearch(u"abc");
// Simulate a provider returning and containing the first set of results. A
// single provider wouldn't return many results like this, but that's
// unimportant for the test.
search_controller_->SetResults(Result::kOmnibox, std::move(results_1));
WaitInMilliseconds();
// Expect that:
// - best matches are ordered first,
// - best matches are ordered by best match rank,
// - categories are ignored within best match.
ExpectIdOrder({"a", "c", "d", "b"});
// Simulate the arrival of another result into the best match category. Its
// best match rank takes precedence over its relevance score in determining
// its rank within the best matches.
auto results_2 = MakeListResults({"e"}, {Category::kFiles}, {2}, {0.9});
search_controller_->SetResults(Result::kFileSearch, std::move(results_2));
ExpectIdOrder({"a", "c", "e", "d", "b"});
}
TEST_F(SearchControllerTest,
BurnInIterationNumbersTrackedInQuerySearch_Results) {
// This test focuses on the book-keeping of burn-in iteration numbers for
// individual results, and ignores the effect that these numbers can have on
// final sorting of the categories or results lists.
ranker_manager_->SetCategoryRanks({{Category::kFiles, 0.1}});
// Set up some results from two different providers.
auto file_results = MakeListResults({"a"}, {Category::kFiles}, {-1}, {0.9});
auto app_results = MakeListResults({"b"}, {Category::kApps}, {-1}, {0.1});
// Set up results from a third different provider. This provider will first
// return one set of results, then later return an updated set of results.
auto web_results_first_arrival = MakeListResults(
{"c", "d"}, {Category::kWeb, Category::kWeb}, {-1, -1}, {0.2, 0.1});
auto web_results_second_arrival = MakeListResults(
{"c", "d", "e"}, {Category::kWeb, Category::kWeb, Category::kWeb},
{-1, -1, -1}, {0.2, 0.1, 0.4});
// Simulate starting a search.
search_controller_->StartSearch(u"abc");
// Simulate providers returning results within the burn-in period.
search_controller_->SetResults(Result::kFileSearch, std::move(file_results));
ExpectIdsToBurnInIterations({{"a", 0}});
search_controller_->SetResults(Result::kInstalledApp, std::move(app_results));
ExpectIdsToBurnInIterations({{"a", 0}, {"b", 0}});
// Simulate a provider returning results after the burn-in period.
WaitInMilliseconds();
search_controller_->SetResults(Result::kOmnibox,
std::move(web_results_first_arrival));
ExpectIdsToBurnInIterations({{"a", 0}, {"b", 0}, {"c", 1}, {"d", 1}});
// Simulate a provider returning for a second time. The burn-in iteration
// number for previously seen results is preserved, while that of newly seen
// results is incremented.
search_controller_->SetResults(Result::kOmnibox,
std::move(web_results_second_arrival));
ExpectIdsToBurnInIterations(
{{"a", 0}, {"b", 0}, {"c", 1}, {"d", 1}, {"e", 2}});
}
TEST_F(SearchControllerTest,
BurnInIterationNumbersTrackedInQuerySearch_Categories) {
// This test focuses on the book-keeping of burn-in iteration numbers for
// categories, and ignores the effect that these numbers can have on final
// sorting of the categories or results lists.
ranker_manager_->SetCategoryRanks({{Category::kFiles, 0.1}});
// Set up some results from four different providers. Only their categories
// are relevant, and individual result scores are not.
auto file_results = MakeListResults({"a"}, {Category::kFiles}, {-1}, {0.9});
auto app_results = MakeListResults({"b"}, {Category::kApps}, {-1}, {0.1});
// This provider will first return one set of results, then later return an
// updated set of results.
auto web_results_first_arrival = MakeListResults(
{"c", "d"}, {Category::kWeb, Category::kWeb}, {-1, -1}, {0.2, 0.1});
auto web_results_second_arrival = MakeListResults(
{"c", "d", "e"}, {Category::kWeb, Category::kWeb, Category::kWeb},
{-1, -1, -1}, {0.2, 0.1, 0.4});
auto settings_results =
MakeListResults({"f"}, {Category::kSettings}, {-1}, {0.8});
// Simulate starting a search.
search_controller_->StartSearch(u"abc");
// Simulate providers returning results within the burn-in period.
search_controller_->SetResults(Result::kFileSearch, std::move(file_results));
ExpectCategoriesToBurnInIterations({{Category::kFiles, 0}});
search_controller_->SetResults(Result::kInstalledApp, std::move(app_results));
ExpectCategoriesToBurnInIterations(
{{Category::kFiles, 0}, {Category::kApps, 0}});
// Simulate a third provider returning results after the burn-in period.
WaitInMilliseconds();
search_controller_->SetResults(Result::kOmnibox,
std::move(web_results_first_arrival));
ExpectCategoriesToBurnInIterations(
{{Category::kFiles, 0}, {Category::kApps, 0}, {Category::kWeb, 1}});
// Simulate the third provider returning for a second time. The burn-in
// iteration number for that category is not updated.
search_controller_->SetResults(Result::kOmnibox,
std::move(web_results_second_arrival));
ExpectCategoriesToBurnInIterations(
{{Category::kFiles, 0}, {Category::kApps, 0}, {Category::kWeb, 1}});
// Simulate a fourth provider returning for the first time.
search_controller_->SetResults(Result::kOsSettings,
std::move(settings_results));
ExpectCategoriesToBurnInIterations({{Category::kFiles, 0},
{Category::kApps, 0},
{Category::kWeb, 1},
{Category::kSettings, 3}});
}
// Tests that categories which arrive pre-burn-in are ordered correctly, and
// their results are grouped together and ordered by score.
TEST_F(SearchControllerTest, CategoriesOrderedCorrectlyPreBurnIn) {
ranker_manager_->SetCategoryRanks(
{{Category::kFiles, 0.3}, {Category::kWeb, 0.2}, {Category::kApps, 0.1}});
auto file_results = MakeListResults({"a"}, {Category::kFiles}, {-1}, {0.9});
auto web_results = MakeListResults(
{"c", "d", "b"}, {Category::kWeb, Category::kWeb, Category::kWeb},
{-1, -1, -1}, {0.2, 0.1, 0.4});
auto app_results = MakeListResults({"e"}, {Category::kApps}, {-1}, {0.1});
// Simulate starting a search.
search_controller_->StartSearch(u"abc");
// Simulate several providers returning results pre-burn-in.
search_controller_->SetResults(Result::kOmnibox, std::move(web_results));
search_controller_->SetResults(Result::kInstalledApp, std::move(app_results));
search_controller_->SetResults(Result::kFileSearch, std::move(file_results));
WaitInMilliseconds();
ExpectIdOrder({"a", "b", "c", "d", "e"});
}
// Tests that categories which arrive post-burn-in are ordered correctly, and
// their results are grouped together and ordered by score.
TEST_F(SearchControllerTest, CategoriesOrderedCorrectlyPostBurnIn) {
ranker_manager_->SetCategoryRanks(
{{Category::kFiles, 0.3}, {Category::kWeb, 0.2}, {Category::kApps, 0.1}});
auto web_results = MakeListResults(
{"b", "c", "a"}, {Category::kWeb, Category::kWeb, Category::kWeb},
{-1, -1, -1}, {0.2, 0.1, 0.4});
auto app_results = MakeListResults(
{"e", "d"}, {Category::kApps, Category::kApps}, {-1, -1}, {0.7, 0.9});
auto file_results = MakeListResults({"f"}, {Category::kFiles}, {-1}, {0.8});
// Simulate starting a search.
search_controller_->StartSearch(u"abc");
// Simulate several providers returning results post-burn-in.
WaitInMilliseconds();
search_controller_->SetResults(Result::kOmnibox, std::move(web_results));
ExpectIdOrder({"a", "b", "c"});
search_controller_->SetResults(Result::kInstalledApp, std::move(app_results));
ExpectIdOrder({"a", "b", "c", "d", "e"});
search_controller_->SetResults(Result::kFileSearch, std::move(file_results));
ExpectIdOrder({"a", "b", "c", "d", "e", "f"});
}
// Tests that categories are ordered correctly, where some categories arrive
// pre-burn-in and others arrive post-burn-in. Test that their results are
// grouped together and ordered by score.
TEST_F(
SearchControllerTest,
CategoriesOrderedCorrectly_PreAndPostBurnIn_OneProviderReturnPerCategory) {
ranker_manager_->SetCategoryRanks(
{{Category::kFiles, 0.3}, {Category::kWeb, 0.2}, {Category::kApps, 0.1}});
auto web_results = MakeListResults(
{"c", "d", "b"}, {Category::kWeb, Category::kWeb, Category::kWeb},
{-1, -1, -1}, {0.3, 0.2, 0.4});
auto app_results = MakeListResults({"e"}, {Category::kApps}, {-1}, {0.1});
auto file_results = MakeListResults({"a"}, {Category::kFiles}, {-1}, {0.9});
// Simulate starting a search.
search_controller_->StartSearch(u"abc");
// Simulate a provider returning results within the burn-in period.
search_controller_->SetResults(Result::kOmnibox, std::move(web_results));
ExpectIdOrder({});
// Expect results to appear after burn-in period has elapsed.
WaitInMilliseconds();
ExpectIdOrder({"b", "c", "d"});
// Simulate several providers returning results after the burn-in period.
search_controller_->SetResults(Result::kInstalledApp, std::move(app_results));
ExpectIdOrder({"b", "c", "d", "e"});
search_controller_->SetResults(Result::kFileSearch, std::move(file_results));
ExpectIdOrder({"b", "c", "d", "e", "a"});
}
// Tests that the Search and Assistant category is ordered correctly when it
// arrives pre-burn-in, and remains correctly ordered when further categories
// arrive.
//
// At the time of its arrival, Search and Assistant should initially be pinned
// to the bottom of the categories list, but later-arriving categories should
// appear below Search and Assistant.
TEST_F(SearchControllerTest,
CategoriesOrderedCorrectly_SearchAndAssistantPinnedToBottomOfPreBurnIn) {
ranker_manager_->SetCategoryRanks({{Category::kFiles, 0.3},
{Category::kSearchAndAssistant, 0.2},
{Category::kApps, 0.1}});
auto search_and_assistant_results = MakeListResults(
{"a", "b", "c"},
{Category::kSearchAndAssistant, Category::kSearchAndAssistant,
Category::kSearchAndAssistant},
{-1, -1, -1}, {0.3, 0.5, 0.4});
auto file_results = MakeListResults({"d"}, {Category::kFiles}, {-1}, {0.2});
auto app_results = MakeListResults({"e"}, {Category::kApps}, {-1}, {0.1});
// Simulate starting a search.
search_controller_->StartSearch(u"abc");
// Simulate two providers (including Search and Assistant) returning within
// the burn-in period.
search_controller_->SetResults(Result::kAssistantText,
std::move(search_and_assistant_results));
search_controller_->SetResults(Result::kFileSearch, std::move(file_results));
ExpectIdOrder({});
// Expect results to appear after burn-in period has elapsed. Expect the
// Search and Assistant category to appear at the bottom.
WaitInMilliseconds();
ExpectIdOrder({"d", "b", "c", "a"});
// Simulate a provider returning results after the burn-in period. Expect the
// new category to appear below Search and Assistant.
search_controller_->SetResults(Result::kInstalledApp, std::move(app_results));
ExpectIdOrder({"d", "b", "c", "a", "e"});
}
// Tests that results are ordered correctly, where results are of a single
// category, and originate from a single provider which returns multiple times
// both pre- and post-burn-in.
TEST_F(
SearchControllerTest,
ResultsOrderedCorrectly_PreAndPostBurnIn_SingleProviderReturnsMultipleTimes) {
ranker_manager_->SetCategoryRanks({{Category::kWeb, 0.2}});
auto web_results_1 = MakeListResults(
{"b", "c", "a"}, {Category::kWeb, Category::kWeb, Category::kWeb},
{-1, -1, -1}, {0.2, 0.1, 0.3});
auto web_results_2 = MakeListResults(
{"b", "c", "a", "d"},
{Category::kWeb, Category::kWeb, Category::kWeb, Category::kWeb},
{-1, -1, -1, -1}, {0.2, 0.1, 0.3, 0.4});
auto web_results_3 =
MakeListResults({"b", "c", "a", "d", "e"},
{Category::kWeb, Category::kWeb, Category::kWeb,
Category::kWeb, Category::kWeb},
{-1, -1, -1, -1, -1}, {0.2, 0.1, 0.3, 0.4, 0.5});
// Simulate starting a search.
search_controller_->StartSearch(u"abc");
// Simulate the provider returning results within the burn-in period.
search_controller_->SetResults(Result::kOmnibox, std::move(web_results_1));
ExpectIdOrder({});
// Expect results to appear after burn-in period has elapsed.
WaitInMilliseconds();
ExpectIdOrder({"a", "b", "c"});
// When a single provider returns multiple times for a category, sorting by
// result burn-in iteration number takes precedence over sorting by result
// score.
//
// Simulate the provider returning results twice after the burn-in period.
search_controller_->SetResults(Result::kOmnibox, std::move(web_results_2));
ExpectIdOrder({"a", "b", "c", "d"});
search_controller_->SetResults(Result::kOmnibox, std::move(web_results_3));
ExpectIdOrder({"a", "b", "c", "d", "e"});
}
// Tests that results are ordered correctly, where results are of a single
// category, and originate from multiple providers. Providers return a single
// time, pre- or post-burn-in.
TEST_F(
SearchControllerTest,
ResultsOrderedCorrectly_PreAndPostBurnIn_MultipleProvidersReturnToSingleCategory) {
ranker_manager_->SetCategoryRanks({{Category::kWeb, 0.2}});
auto installed_app_results = MakeListResults(
{"b", "c", "a"}, {Category::kApps, Category::kApps, Category::kApps},
{-1, -1, -1}, {0.3, 0.2, 0.4});
auto play_store_app_results = MakeListResults(
{"e", "d"}, {Category::kApps, Category::kApps}, {-1, -1}, {0.1, 0.5});
auto internal_app_results =
MakeListResults({"f"}, {Category::kApps}, {-1}, {0.9});
// Simulate starting a search.
search_controller_->StartSearch(u"abc");
// Simulate a provider returning results within the burn-in period.
search_controller_->SetResults(Result::kInstalledApp,
std::move(installed_app_results));
ExpectIdOrder({});
// Expect results to appear after burn-in period has elapsed.
WaitInMilliseconds();
ExpectIdOrder({"a", "b", "c"});
// When there are multiple providers returning for a category, sorting by
// burn-in iteration number takes precedence over sorting by result score.
//
// Simulate two other providers returning results after the burn-in period.
search_controller_->SetResults(Result::kPlayStoreApp,
std::move(play_store_app_results));
ExpectIdOrder({"a", "b", "c", "d", "e"});
search_controller_->SetResults(Result::kInternalApp,
std::move(internal_app_results));
ExpectIdOrder({"a", "b", "c", "d", "e", "f"});
}
TEST_F(SearchControllerTest, FirstSearchResultsNotShownInSecondSearch) {
ranker_manager_->SetCategoryRanks({{Category::kApps, 0.1}});
auto provider = std::make_unique<TestSearchProvider>(Result::kInstalledApp,
base::Seconds(1));
auto* provider_ptr = provider.get();
search_controller_->AddProvider(std::move(provider));
// Start the first search.
provider_ptr->SetNextResults(
MakeListResults({"AAA"}, {Category::kApps}, {-1}, {0.1}));
search_controller_->StartSearch(u"A");
ExpectIdOrder({});
// Provider has returned and the A result should be published.
WaitInMilliseconds();
ExpectIdOrder({"AAA"});
provider_ptr->SetNextResults({});
search_controller_->ClearSearch();
// Start the second search.
provider_ptr->SetNextResults(
MakeListResults({"BBB"}, {Category::kApps}, {-1}, {0.1}));
search_controller_->StartSearch(u"B");
// The B result is not ready yet, and the A result should *not* have been
// published.
ExpectIdOrder({});
// Provider has returned and the B result should be published.
WaitInMilliseconds();
ExpectIdOrder({"BBB"});
}
TEST_F(SearchControllerTest, ZeroStateResultsNotOverridingBurnIn) {
ranker_manager_->SetCategoryRanks({{Category::kWeb, 0.2}});
auto web_results = MakeListResults(
{"b", "c", "a"}, {Category::kWeb, Category::kWeb, Category::kWeb},
{-1, -1, -1}, {0.2, 0.1, 0.3});
auto zero_state_provider = std::make_unique<TestSearchProvider>(
Result::kZeroStateApp, base::Milliseconds(20));
zero_state_provider->SetNextResults(MakeResults(
{"zero"}, {DisplayType::kRecentApps}, {Category::kApps}, {-1}, {0.5}));
search_controller_->AddProvider(std::move(zero_state_provider));
// Simluate zero state search.
search_controller_->StartZeroState(base::DoNothing(), base::Milliseconds(50));
// Simulate starting a search.
search_controller_->StartSearch(u"abc");
// Simulate the provider returning results within the burn-in period.
search_controller_->SetResults(Result::kOmnibox, std::move(web_results));
ExpectIdOrder({});
// Fast-forward time so zero state provider returns results, and zero state
// timeout fires.
WaitInMilliseconds(50);
// The burn-in period has not elapsed, so no results should have been
// published.
ExpectIdOrder({});
// Expect results to appear after burn-in period has elapsed.
WaitInMilliseconds();
ExpectIdOrder({"zero", "a", "b", "c"});
}
TEST_F(SearchControllerTest, ZeroStateResultsAreBlocked) {
ranker_manager_->SetCategoryRanks({{Category::kApps, 0.1}});
// Set up five providers, three provide zero-state results, one of which is
// very slow.
auto provider_a = std::make_unique<TestSearchProvider>(Result::kZeroStateApp,
base::Seconds(1));
auto provider_b = std::make_unique<TestSearchProvider>(Result::kZeroStateFile,
base::Seconds(2));
auto provider_c = std::make_unique<TestSearchProvider>(Result::kOsSettings,
base::Seconds(1));
auto provider_d =
std::make_unique<TestSearchProvider>(Result::kOmnibox, base::Seconds(4));
auto provider_e = std::make_unique<TestSearchProvider>(
Result::kZeroStateDrive, base::Seconds(5));
// NOTE: The particular result categories do not matter, but display type does
// impact published result sort order.
provider_a->SetNextResults(MakeResults({"a"}, {DisplayType::kRecentApps},
{Category::kApps}, {-1}, {0.3}));
provider_b->SetNextResults(MakeResults({"b"}, {DisplayType::kContinue},
{Category::kFiles}, {-1}, {0.2}));
provider_c->SetNextResults(
MakeListResults({"c"}, {Category::kApps}, {-1}, {0.1}));
provider_d->SetNextResults(
MakeListResults({"d"}, {Category::kApps}, {-1}, {0.4}));
provider_e->SetNextResults(
MakeResults({"e", "f"}, {DisplayType::kContinue, DisplayType::kContinue},
{Category::kApps, Category::kApps}, {-1, -1}, {0.6, 0.5}));
search_controller_->AddProvider(std::move(provider_a));
search_controller_->AddProvider(std::move(provider_b));
search_controller_->AddProvider(std::move(provider_c));
search_controller_->AddProvider(std::move(provider_d));
search_controller_->AddProvider(std::move(provider_e));
// Start search so non zero state test providers run.
search_controller_->StartSearch(u"xyz");
// Start the zero-state session. When on-done is called, we should have
// results from all but the slowest provider.
search_controller_->StartZeroState(base::BindLambdaForTesting([&]() {
ExpectIdOrder({"a", "b", "c"});
}),
base::Seconds(3));
// The fast provider has returned but shouldn't have published.
WaitInMilliseconds();
ExpectIdOrder({});
// Verify results are not published if a non-zero state provider (provider_c)
// returns results.
WaitInMilliseconds();
ExpectIdOrder({});
// Fast forward time enough for the zero state callback to run.
WaitInMilliseconds();
ExpectIdOrder({"a", "b", "c"});
// At this point, provider "d" finished, but the results are not published
// because d provider supplies non-zero state results, and zero state search
// is in progress - in practice, non-zero state providers should not start
// during zero state search, but test provider runs either way.
WaitInMilliseconds();
ExpectIdOrder({"a", "b", "c"});
// The latecomer should still be added when it arrives - note that the list
// of ids includes non-zero state result set since the results were last
// published.
// Note that results "c" and "d" are trailing due to their later burn-in
// iteration.
WaitInMilliseconds(2000);
ExpectIdOrder({"e", "f", "a", "b", "c", "d"});
}
TEST_F(SearchControllerTest, ZeroStateResultsGetTimedOut) {
ranker_manager_->SetCategoryRanks({{Category::kApps, 0.1}});
auto provider_a = std::make_unique<TestSearchProvider>(Result::kZeroStateApp,
base::Seconds(1));
auto provider_b = std::make_unique<TestSearchProvider>(Result::kZeroStateFile,
base::Seconds(3));
provider_a->SetNextResults(
MakeListResults({"a"}, {Category::kApps}, {-1}, {0.3}));
provider_b->SetNextResults(
MakeListResults({"b"}, {Category::kFiles}, {-1}, {0.2}));
search_controller_->AddProvider(std::move(provider_a));
search_controller_->AddProvider(std::move(provider_b));
search_controller_->StartZeroState(
base::BindLambdaForTesting([&]() { ExpectIdOrder({"a"}); }),
base::Seconds(2));
// The fast provider has returned but shouldn't have published.
WaitInMilliseconds();
ExpectIdOrder({});
// The timeout finished, the fast provider's result should be published.
WaitInMilliseconds();
ExpectIdOrder({"a"});
// The slow provider should still publish when it returns.
WaitInMilliseconds();
ExpectIdOrder({"a", "b"});
}
TEST_F(SearchControllerTest, ContinueRanksDriveAboveLocal) {
if (ash::features::UseMixedFileLauncherContinueSection()) {
return;
}
// Use the full ranking stack.
search_controller_->set_ranker_manager_for_test(
std::make_unique<RankerManager>(&profile_));
auto drive_provider = std::make_unique<TestSearchProvider>(
Result::kZeroStateDrive, base::Seconds(0));
auto local_provider = std::make_unique<TestSearchProvider>(
Result::kZeroStateFile, base::Seconds(0));
drive_provider->SetNextResults(MakeListResults(
{"drive_a", "drive_b"}, {Category::kUnknown, Category::kUnknown},
{-1, -1}, {0.45, 0.1}));
local_provider->SetNextResults(MakeListResults(
{"local_a", "local_b"}, {Category::kUnknown, Category::kUnknown},
{-1, -1}, {0.5, 0.4}));
search_controller_->AddProvider(std::move(local_provider));
search_controller_->AddProvider(std::move(drive_provider));
search_controller_->StartZeroState(base::DoNothing(), base::Seconds(1));
Wait();
ExpectIdOrder({"drive_a", "drive_b", "local_a", "local_b"});
}
// Tests that the desks admin templates always higher than any other type of
// providers.
TEST_F(SearchControllerTest, ContinueRanksAdminTemplateAboveHelpAppAndDrive) {
// Use the full ranking stack.
search_controller_->set_ranker_manager_for_test(
std::make_unique<RankerManager>(&profile_));
auto desks_admin_template = std::make_unique<TestSearchProvider>(
Result::kDesksAdminTemplate, base::Seconds(0));
auto zero_state_help_app = std::make_unique<TestSearchProvider>(
Result::kZeroStateHelpApp, base::Seconds(0));
auto drive_provider = std::make_unique<TestSearchProvider>(
Result::kZeroStateDrive, base::Seconds(0));
desks_admin_template->SetNextResults(MakeResults(
{"template_a", "template_b"},
{DisplayType::kContinue, DisplayType::kContinue},
{Category::kUnknown, Category::kUnknown}, {-1, -1}, {0.2, 0.1}));
zero_state_help_app->SetNextResults(
MakeResults({"explore_a", "explore_b"},
{DisplayType::kContinue, DisplayType::kContinue},
{Category::kHelp, Category::kHelp}, {-1, -1}, {0.5, 0.4}));
drive_provider->SetNextResults(MakeResults(
{"drive_a", "drive_b"}, {DisplayType::kContinue, DisplayType::kContinue},
{Category::kFiles, Category::kFiles}, {-1, -1}, {0.5, 0.4}));
search_controller_->AddProvider(std::move(drive_provider));
search_controller_->AddProvider(std::move(zero_state_help_app));
search_controller_->AddProvider(std::move(desks_admin_template));
search_controller_->StartZeroState(base::DoNothing(), base::Seconds(1));
Wait();
ExpectIdOrder({"template_a", "template_b", "explore_a", "explore_b",
"drive_a", "drive_b"});
}
TEST_F(SearchControllerTest, FindSearchResultByIdAndOpenIt) {
auto results_1 = MakeListResults(
{"a", "b", "c", "d"},
{Category::kWeb, Category::kGames, Category::kApps, Category::kWeb},
{0, -1, 1, -1}, {0.4, 0.7, 0.2, 0.8});
search_controller_->StartSearch(u"abc");
search_controller_->SetResults(Result::kOmnibox, std::move(results_1));
WaitInMilliseconds();
// Return nullptr if result cannot be found.
ChromeSearchResult* result = search_controller_->FindSearchResult("e");
EXPECT_EQ(result, nullptr);
// Return the result with the target id.
result = search_controller_->FindSearchResult("b");
EXPECT_EQ(result->id(), "b");
EXPECT_EQ(result->category(), Category::kGames);
search_controller_->OpenResult(result, ui::EF_NONE);
// We expect that |DismissView()| in |list_controller_| to be called.
EXPECT_TRUE(list_controller_.did_dismiss_view());
}
TEST_F(SearchControllerTest, InvokeResult) {
auto results_1 = MakeListResults(
{"a", "b", "c", "d"},
{Category::kWeb, Category::kGames, Category::kApps, Category::kWeb},
{0, -1, 1, -1}, {0.4, 0.7, 0.2, 0.8});
search_controller_->StartSearch(u"abc");
search_controller_->SetResults(Result::kOmnibox, std::move(results_1));
WaitInMilliseconds();
ExpectIdOrder({"a", "c", "d", "b"});
// Return the result with the target id.
ChromeSearchResult* result = search_controller_->FindSearchResult("b");
EXPECT_EQ(result->id(), "b");
EXPECT_EQ(result->category(), Category::kGames);
// The result should be removed if the invoke action is |kRemove|.
search_controller_->InvokeResultAction(result,
ash::SearchResultActionType::kRemove);
WaitInMilliseconds();
ExpectIdOrder({"a", "c", "d"});
}
TEST_F(SearchControllerTest, ResultWithSameScore) {
auto results_1 = MakeListResults(
{"d", "c", "b", "a"},
{Category::kWeb, Category::kWeb, Category::kWeb, Category::kWeb},
{-1, -1, -1, -1}, {0.4, 0.4, 0.4, 0.4});
search_controller_->StartSearch(u"abc");
search_controller_->SetResults(Result::kOmnibox, std::move(results_1));
WaitInMilliseconds();
// Results from same category with the same display score will be sorted in
// alphabet order to avoid flipping.
ExpectIdOrder({"a", "b", "c", "d"});
auto results_2 = MakeFileResults(
{"d", "c", "b", "a"}, {"file.txt", "file.txt", "file.txt", "file.txt"},
{"dir_d/", "dir_c/", "dir_b/", "dir_a/"},
{DisplayType::kImage, DisplayType::kImage, DisplayType::kImage,
DisplayType::kImage},
{-1, -1, -1, -1}, {0.4, 0.4, 0.4, 0.4});
search_controller_->StartSearch(u"abc");
search_controller_->SetResults(Result::kImageSearch, std::move(results_2));
WaitInMilliseconds();
// File results from same display types with the same display score will be
// sorted in order of file path to avoid flipping.
ExpectIdOrder({"a", "b", "c", "d"});
}
TEST_F(SearchControllerTest, Train) {
auto results_1 = MakeListResults(
{"a", "b", "c", "d"},
{Category::kWeb, Category::kGames, Category::kApps, Category::kWeb},
{0, -1, 1, -1}, {0.4, 0.7, 0.2, 0.8});
search_controller_->StartSearch(u"abc");
search_controller_->SetResults(Result::kOmnibox, std::move(results_1));
WaitInMilliseconds();
search_controller_->Train(CreateFakeLaunchData("e"));
// We expect that |Train()| in |ranker_manager_| to be called.
EXPECT_TRUE(ranker_manager_->did_train());
}
TEST_F(SearchControllerTest, NotifyObserverWhenPublished) {
// Create two fake observers.
FakeObserver observer1;
FakeObserver observer2;
search_controller_->Publish();
WaitInMilliseconds();
// Do not observe if the observers are not added.
EXPECT_FALSE(observer1.results_added());
EXPECT_FALSE(observer2.results_added());
// Add two observers and remove observer2.
search_controller_->AddObserver(&observer1);
search_controller_->AddObserver(&observer2);
search_controller_->RemoveObserver(&observer2);
search_controller_->Publish();
WaitInMilliseconds();
// We expect observer1 to observe while observer2 is not.
EXPECT_TRUE(observer1.results_added());
EXPECT_FALSE(observer2.results_added());
}
TEST_F(SearchControllerTest, ProviderIsFilteredWithSearchControl) {
base::test::ScopedFeatureList scoped_feature_list_;
scoped_feature_list_.InitWithFeatures(
{ash::features::kLauncherSearchControl,
ash::features::kFeatureManagementLocalImageSearch},
{});
const Result result_categories[] = {
Result::kAnswerCard, Result::kDriveSearch, Result::kAppShortcutV2,
Result::kFileSearch, Result::kArcAppShortcut, Result::kImageSearch,
Result::kGames, Result::kAssistantText, Result::kArcAppShortcut,
};
const SearchCategory search_categories[] = {
SearchCategory::kTest /*always returns results*/,
SearchCategory::kApps,
SearchCategory::kAppShortcuts,
SearchCategory::kFiles,
SearchCategory::kGames,
SearchCategory::kHelp,
SearchCategory::kImages,
SearchCategory::kPlayStore,
SearchCategory::kWeb,
};
std::vector<TestSearchProvider*> provider_ptrs;
for (int i = 0; i < 9; ++i) {
// The result type needs to be unique.
auto provider = std::make_unique<TestSearchProvider>(
result_categories[i], base::Milliseconds(20), search_categories[i]);
provider_ptrs.push_back(provider.get());
search_controller_->AddProvider(std::move(provider));
}
ASSERT_EQ(provider_ptrs.size(), 9u);
ScopedDictPrefUpdate pref_update(
profile_.GetPrefs(), ash::prefs::kLauncherSearchCategoryControlStatus);
const auto toggleable_categories =
search_controller_->GetToggleableCategories();
// Disable the toggleable categories.
for (const ControlCategory control_category : toggleable_categories) {
pref_update->Set(ash::GetAppListControlCategoryName(control_category),
false);
}
for (size_t i = 0; i < provider_ptrs.size(); ++i) {
provider_ptrs[i]->SetNextResults(MakeListResults(
{base::StringPrintf("AAA%zu", i)}, {Category::kApps}, {-1}, {0.1}));
}
search_controller_->StartSearch(u"A");
WaitInMilliseconds();
ExpectIdOrder({"AAA0"});
search_controller_->ClearSearch();
for (size_t i = 1; i < provider_ptrs.size(); ++i) {
for (size_t j = 0; j < provider_ptrs.size(); ++j) {
provider_ptrs[j]->SetNextResults(MakeListResults(
{base::StringPrintf("AAA%zu", j)}, {Category::kApps}, {-1}, {0.1}));
}
// Starts search with control enabled.
pref_update->Set(
ash::GetAppListControlCategoryName(MapSearchCategoryToControlCategory(
provider_ptrs[i]->search_category())),
true);
search_controller_->StartSearch(u"A");
WaitInMilliseconds();
ExpectIdOrder({"AAA0", base::StringPrintf("AAA%zu", i)});
pref_update->Set(
ash::GetAppListControlCategoryName(MapSearchCategoryToControlCategory(
provider_ptrs[i]->search_category())),
false);
search_controller_->ClearSearch();
}
}
} // namespace app_list::test