| // 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/webui/help_app_ui/search/search_handler.h" |
| #include <cstddef> |
| |
| #include "ash/webui/help_app_ui/search/search_concept.h" |
| #include "ash/webui/help_app_ui/search/search_tag_registry.h" |
| #include "base/files/file_util.h" |
| #include "base/files/scoped_temp_dir.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "base/test/metrics/histogram_tester.h" |
| #include "base/test/task_environment.h" |
| #include "base/test/test_future.h" |
| #include "mojo/public/cpp/bindings/remote.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| |
| namespace ash::help_app { |
| namespace { |
| |
| class FakeObserver : public mojom::SearchResultsObserver { |
| public: |
| FakeObserver() = default; |
| ~FakeObserver() override = default; |
| |
| mojo::PendingRemote<mojom::SearchResultsObserver> GenerateRemote() { |
| mojo::PendingRemote<mojom::SearchResultsObserver> remote; |
| receiver_.Bind(remote.InitWithNewPipeAndPassReceiver()); |
| return remote; |
| } |
| |
| size_t num_calls() const { return num_calls_; } |
| |
| private: |
| // mojom::SearchResultsObserver: |
| void OnSearchResultAvailabilityChanged() override { ++num_calls_; } |
| |
| size_t num_calls_ = 0; |
| mojo::Receiver<mojom::SearchResultsObserver> receiver_{this}; |
| }; |
| |
| } // namespace |
| |
| class HelpAppSearchHandlerTest : public testing::Test { |
| protected: |
| HelpAppSearchHandlerTest() |
| : search_tag_registry_(local_search_service_proxy_.get()), |
| handler_(&search_tag_registry_, local_search_service_proxy_.get()) {} |
| ~HelpAppSearchHandlerTest() override = default; |
| |
| // testing::Test: |
| void SetUp() override { |
| handler_.BindInterface(handler_remote_.BindNewPipeAndPassReceiver()); |
| |
| handler_remote_->Observe(search_results_observer_.GenerateRemote()); |
| handler_remote_.FlushForTesting(); |
| |
| ASSERT_TRUE(temp_dir_.CreateUniqueTempDir()); |
| } |
| |
| void SetupInitialPersistenceSearchConcepts() { |
| SearchConcept persistence(GetPersistencePath()); |
| std::vector<mojom::SearchConceptPtr> search_concepts; |
| mojom::SearchConceptPtr new_concept_1 = mojom::SearchConcept::New( |
| /*id=*/"test-id-1", |
| /*title=*/u"Title 1", |
| /*main_category=*/u"Help", |
| /*tags=*/std::vector<std::u16string>{u"Test tag", u"Tag 2"}, |
| /*tag_locale=*/"en", |
| /*url_path_with_parameters=*/"help", |
| /*locale=*/""); |
| mojom::SearchConceptPtr new_concept_2 = mojom::SearchConcept::New( |
| /*id=*/"test-id-2", |
| /*title=*/u"Title 2", |
| /*main_category=*/u"Help", |
| /*tags=*/std::vector<std::u16string>{u"Another test tag"}, |
| /*tag_locale=*/"en", |
| /*url_path_with_parameters=*/"help", |
| /*locale=*/""); |
| search_concepts.push_back(std::move(new_concept_1)); |
| search_concepts.push_back(std::move(new_concept_2)); |
| persistence.UpdateSearchConcepts(search_concepts); |
| task_environment_.RunUntilIdle(); |
| EXPECT_TRUE(base::PathExists(GetPersistencePath())); |
| } |
| |
| void SimulateWebDataUpdate() { |
| std::vector<mojom::SearchConceptPtr> new_search_concepts; |
| mojom::SearchConceptPtr new_concept_1 = mojom::SearchConcept::New( |
| /*id=*/"test-id-1", |
| /*title=*/u"Title 1", |
| /*main_category=*/u"Help", |
| /*tags=*/std::vector<std::u16string>{u"Printing"}, |
| /*tag_locale=*/"en", |
| /*url_path_with_parameters=*/"help", |
| /*locale=*/""); |
| new_search_concepts.push_back(std::move(new_concept_1)); |
| Update(std::move(new_search_concepts)); |
| handler_remote_.FlushForTesting(); |
| task_environment_.RunUntilIdle(); |
| } |
| |
| base::FilePath GetTempPath() { return temp_dir_.GetPath(); } |
| |
| base::FilePath GetPersistencePath() { |
| return temp_dir_.GetPath() |
| .AppendASCII("help_app/") |
| .AppendASCII("persistence.pb"); |
| } |
| |
| void OnRead(size_t expected_size, |
| std::vector<mojom::SearchConceptPtr> search_concepts) { |
| EXPECT_EQ(search_concepts.size(), expected_size); |
| } |
| |
| base::OnceCallback<void(std::vector<mojom::SearchConceptPtr>)> ReadCallback( |
| size_t expected_size) { |
| return base::BindOnce(&HelpAppSearchHandlerTest::OnRead, |
| base::Unretained(this), expected_size); |
| } |
| |
| std::vector<mojom::SearchResultPtr> Search(const std::u16string& query, |
| int32_t max_num_results) { |
| base::test::TestFuture<std::vector<mojom::SearchResultPtr>> future; |
| handler_remote_->Search(query, max_num_results, future.GetCallback()); |
| return future.Take(); |
| } |
| |
| void Update(std::vector<mojom::SearchConceptPtr> search_concepts) { |
| base::test::TestFuture<void> future; |
| handler_remote_->Update(std::move(search_concepts), future.GetCallback()); |
| EXPECT_TRUE(future.Wait()); |
| } |
| |
| base::test::TaskEnvironment task_environment_; |
| |
| std::unique_ptr<local_search_service::LocalSearchServiceProxy> |
| local_search_service_proxy_ = |
| std::make_unique<local_search_service::LocalSearchServiceProxy>( |
| /*for_testing=*/true); |
| SearchTagRegistry search_tag_registry_; |
| SearchHandler handler_; |
| mojo::Remote<mojom::SearchHandler> handler_remote_; |
| FakeObserver search_results_observer_; |
| base::ScopedTempDir temp_dir_; |
| }; |
| |
| TEST_F(HelpAppSearchHandlerTest, UpdateAndSearch) { |
| // Add some search tags. |
| std::vector<mojom::SearchConceptPtr> search_concepts; |
| mojom::SearchConceptPtr new_concept_1 = mojom::SearchConcept::New( |
| /*id=*/"test-id-1", |
| /*title=*/u"Title 1", |
| /*main_category=*/u"Help", |
| /*tags=*/std::vector<std::u16string>{u"Test tag", u"Tag 2"}, |
| /*tag_locale=*/"en", |
| /*url_path_with_parameters=*/"help", |
| /*locale=*/""); |
| mojom::SearchConceptPtr new_concept_2 = mojom::SearchConcept::New( |
| /*id=*/"test-id-2", |
| /*title=*/u"Title 2", |
| /*main_category=*/u"Help", |
| /*tags=*/std::vector<std::u16string>{u"Another test tag"}, |
| /*tag_locale=*/"en", |
| /*url_path_with_parameters=*/"help", |
| /*locale=*/""); |
| search_concepts.push_back(std::move(new_concept_1)); |
| search_concepts.push_back(std::move(new_concept_2)); |
| |
| Update(std::move(search_concepts)); |
| handler_remote_.FlushForTesting(); |
| task_environment_.RunUntilIdle(); |
| |
| EXPECT_EQ(1u, search_results_observer_.num_calls()); |
| |
| std::vector<mojom::SearchResultPtr> search_results; |
| |
| // 2 results should be available for a "test tag" query. |
| search_results = Search(u"test tag", |
| /*max_num_results=*/3u); |
| EXPECT_EQ(search_results.size(), 2u); |
| |
| // Limit results to 1 max and ensure that only 1 result is returned. |
| search_results = Search(u"test tag", |
| /*max_num_results=*/1u); |
| EXPECT_EQ(search_results.size(), 1u); |
| |
| // Search for a query which should return no results. |
| search_results = Search(u"QueryWithNoResults", |
| /*max_num_results=*/3u); |
| EXPECT_TRUE(search_results.empty()); |
| } |
| |
| TEST_F(HelpAppSearchHandlerTest, SearchResultMetadata) { |
| // Add some search tags. |
| std::vector<mojom::SearchConceptPtr> search_concepts; |
| mojom::SearchConceptPtr new_concept_1 = mojom::SearchConcept::New( |
| /*id=*/"test-id-1", |
| /*title=*/u"Title 1", |
| /*main_category=*/u"Help", |
| /*tags=*/std::vector<std::u16string>{u"Test tag", u"Printing"}, |
| /*tag_locale=*/"en", |
| /*url_path_with_parameters=*/"help", |
| /*locale=*/""); |
| search_concepts.push_back(std::move(new_concept_1)); |
| |
| Update(std::move(search_concepts)); |
| handler_remote_.FlushForTesting(); |
| task_environment_.RunUntilIdle(); |
| |
| std::vector<mojom::SearchResultPtr> search_results; |
| search_results = Search(u"Printing", |
| /*max_num_results=*/3u); |
| |
| EXPECT_EQ(search_results.size(), 1u); |
| EXPECT_EQ(search_results[0]->id, "test-id-1"); |
| EXPECT_EQ(search_results[0]->title, u"Title 1"); |
| EXPECT_EQ(search_results[0]->main_category, u"Help"); |
| EXPECT_EQ(search_results[0]->locale, ""); |
| EXPECT_GT(search_results[0]->relevance_score, 0.01); |
| } |
| |
| TEST_F(HelpAppSearchHandlerTest, SearchResultOrdering) { |
| // Add some search tags. |
| std::vector<mojom::SearchConceptPtr> search_concepts; |
| mojom::SearchConceptPtr new_concept_1 = mojom::SearchConcept::New( |
| /*id=*/"test-id-less", |
| /*title=*/u"Title 1", |
| /*main_category=*/u"Help", |
| /*tags=*/std::vector<std::u16string>{u"less relevant concept"}, |
| /*tag_locale=*/"en", |
| /*url_path_with_parameters=*/"help", |
| /*locale=*/""); |
| mojom::SearchConceptPtr new_concept_2 = mojom::SearchConcept::New( |
| /*id=*/"test-id-more", |
| /*title=*/u"Title 2", |
| /*main_category=*/u"Help", |
| /*tags=*/std::vector<std::u16string>{u"more relevant tag", u"Tag"}, |
| /*tag_locale=*/"en", |
| /*url_path_with_parameters=*/"help", |
| /*locale=*/""); |
| search_concepts.push_back(std::move(new_concept_1)); |
| search_concepts.push_back(std::move(new_concept_2)); |
| |
| Update(std::move(search_concepts)); |
| handler_remote_.FlushForTesting(); |
| task_environment_.RunUntilIdle(); |
| |
| std::vector<mojom::SearchResultPtr> search_results; |
| search_results = Search(u"relevant tag", |
| /*max_num_results=*/3u); |
| |
| // The more relevant concept should be first, but the other concept still has |
| // some relevance. |
| ASSERT_EQ(search_results.size(), 2u); |
| EXPECT_EQ(search_results[0]->id, "test-id-more"); |
| EXPECT_EQ(search_results[1]->id, "test-id-less"); |
| EXPECT_GT(search_results[0]->relevance_score, |
| search_results[1]->relevance_score); |
| EXPECT_GT(search_results[1]->relevance_score, 0.01); |
| } |
| |
| TEST_F(HelpAppSearchHandlerTest, SearchStatusNotReadyAndEmptyIndex) { |
| base::HistogramTester histogram_tester; |
| std::vector<mojom::SearchResultPtr> search_results; |
| |
| // Search without updating the index. |
| search_results = Search(u"test query", /*max_num_results=*/3u); |
| |
| EXPECT_TRUE(search_results.empty()); |
| // 0 is kNotReadyAndEmptyIndex. |
| histogram_tester.ExpectUniqueSample( |
| "Discover.SearchHandler.SearchResultStatus", 0, 1); |
| } |
| |
| TEST_F(HelpAppSearchHandlerTest, SearchStatusReadyAndSuccess) { |
| // Add one item to the search index. |
| std::vector<mojom::SearchConceptPtr> search_concepts; |
| mojom::SearchConceptPtr new_concept_1 = mojom::SearchConcept::New( |
| /*id=*/"test-id-1", |
| /*title=*/u"Title 1", |
| /*main_category=*/u"Help", |
| /*tags=*/std::vector<std::u16string>{u"Test tag", u"Printing"}, |
| /*tag_locale=*/"en", |
| /*url_path_with_parameters=*/"help", |
| /*locale=*/""); |
| search_concepts.push_back(std::move(new_concept_1)); |
| Update(std::move(search_concepts)); |
| handler_remote_.FlushForTesting(); |
| task_environment_.RunUntilIdle(); |
| |
| base::HistogramTester histogram_tester; |
| std::vector<mojom::SearchResultPtr> search_results; |
| |
| search_results = Search(u"Printing", /*max_num_results=*/3u); |
| |
| EXPECT_EQ(search_results.size(), 1u); |
| // 2 is kReadyAndSuccess. |
| histogram_tester.ExpectUniqueSample( |
| "Discover.SearchHandler.SearchResultStatus", 2, 1); |
| } |
| |
| TEST_F(HelpAppSearchHandlerTest, SearchStatusReadyAndEmptyIndex) { |
| // Update using an empty list. This can happen if there is no localized |
| // content for the current locale. |
| std::vector<mojom::SearchConceptPtr> search_concepts; |
| Update(std::move(search_concepts)); |
| handler_remote_.FlushForTesting(); |
| task_environment_.RunUntilIdle(); |
| |
| base::HistogramTester histogram_tester; |
| std::vector<mojom::SearchResultPtr> search_results; |
| |
| search_results = Search(u"Printing", /*max_num_results=*/3u); |
| |
| EXPECT_TRUE(search_results.empty()); |
| // 3 is kReadyAndEmptyIndex. |
| histogram_tester.ExpectUniqueSample( |
| "Discover.SearchHandler.SearchResultStatus", 3, 1); |
| } |
| |
| TEST_F(HelpAppSearchHandlerTest, SearchStatusReadyAndOtherStatus) { |
| // Add one item to the search index. |
| std::vector<mojom::SearchConceptPtr> search_concepts; |
| mojom::SearchConceptPtr new_concept_1 = mojom::SearchConcept::New( |
| /*id=*/"test-id-1", |
| /*title=*/u"Title 1", |
| /*main_category=*/u"Help", |
| /*tags=*/std::vector<std::u16string>{u"Test tag", u"Printing"}, |
| /*tag_locale=*/"en", |
| /*url_path_with_parameters=*/"help", |
| /*locale=*/""); |
| search_concepts.push_back(std::move(new_concept_1)); |
| Update(std::move(search_concepts)); |
| handler_remote_.FlushForTesting(); |
| task_environment_.RunUntilIdle(); |
| |
| base::HistogramTester histogram_tester; |
| std::vector<mojom::SearchResultPtr> search_results; |
| |
| // Searching with an empty query results in a different status: kEmptyQuery. |
| search_results = Search(u"", /*max_num_results=*/3u); |
| |
| EXPECT_TRUE(search_results.empty()); |
| // 4 is kReadyAndOtherStatus. |
| histogram_tester.ExpectUniqueSample( |
| "Discover.SearchHandler.SearchResultStatus", 4, 1); |
| } |
| |
| TEST_F(HelpAppSearchHandlerTest, InitializeWithoutPersistence) { |
| // Load when persistence not exist. |
| EXPECT_FALSE(base::PathExists(GetPersistencePath())); |
| handler_.OnProfileDirAvailable(GetTempPath()); |
| handler_remote_.FlushForTesting(); |
| task_environment_.RunUntilIdle(); |
| |
| // Nothing is updated. |
| EXPECT_EQ(0u, search_results_observer_.num_calls()); |
| |
| // Web data comes. |
| SimulateWebDataUpdate(); |
| |
| // Updated from web data. |
| EXPECT_EQ(1u, search_results_observer_.num_calls()); |
| |
| // Check the persistence is generated. |
| EXPECT_TRUE(base::PathExists(GetPersistencePath())); |
| SearchConcept persistence(GetPersistencePath()); |
| persistence.GetSearchConcepts(ReadCallback(1u)); |
| } |
| |
| TEST_F(HelpAppSearchHandlerTest, InitializeWithPersistence) { |
| // Add persistence to disk. |
| SetupInitialPersistenceSearchConcepts(); |
| |
| // Load from persistence. |
| handler_.OnProfileDirAvailable(GetTempPath()); |
| handler_remote_.FlushForTesting(); |
| task_environment_.RunUntilIdle(); |
| |
| // Updated from persistence. |
| EXPECT_EQ(1u, search_results_observer_.num_calls()); |
| |
| std::vector<mojom::SearchResultPtr> search_results; |
| |
| // There should be results. |
| search_results = Search(u"test tag", |
| /*max_num_results=*/3u); |
| EXPECT_EQ(search_results.size(), 2u); |
| } |
| |
| TEST_F(HelpAppSearchHandlerTest, PersistenceUpdateWithNewData) { |
| // Add persistence to disk. |
| SetupInitialPersistenceSearchConcepts(); |
| |
| // Load from persistence. |
| handler_.OnProfileDirAvailable(GetTempPath()); |
| handler_remote_.FlushForTesting(); |
| task_environment_.RunUntilIdle(); |
| |
| // Updated from persistence. |
| EXPECT_EQ(1u, search_results_observer_.num_calls()); |
| |
| // Web data comes. |
| SimulateWebDataUpdate(); |
| |
| // Updated from new data. |
| EXPECT_EQ(2u, search_results_observer_.num_calls()); |
| |
| std::vector<mojom::SearchResultPtr> search_results; |
| |
| // There should be new results. |
| search_results = Search(u"Printing", /*max_num_results=*/3u); |
| EXPECT_EQ(search_results.size(), 1u); |
| |
| // There should be no old results. |
| search_results = Search(u"test tag", |
| /*max_num_results=*/3u); |
| EXPECT_EQ(search_results.size(), 0u); |
| |
| // Check the persistence is also updated. |
| SearchConcept persistence(GetPersistencePath()); |
| persistence.GetSearchConcepts(ReadCallback(1u)); |
| } |
| |
| TEST_F(HelpAppSearchHandlerTest, NewDataComesBeforePersistenceLoad) { |
| // Add persistence to disk. |
| SetupInitialPersistenceSearchConcepts(); |
| |
| // Web data comes. |
| SimulateWebDataUpdate(); |
| |
| // Updated from web data. |
| EXPECT_EQ(1u, search_results_observer_.num_calls()); |
| |
| // Load from persistence after the new data comes. |
| handler_.OnProfileDirAvailable(GetTempPath()); |
| handler_remote_.FlushForTesting(); |
| task_environment_.RunUntilIdle(); |
| |
| // No update from persistence. |
| EXPECT_EQ(1u, search_results_observer_.num_calls()); |
| |
| std::vector<mojom::SearchResultPtr> search_results; |
| |
| // There should be new results. |
| search_results = Search(u"Printing", /*max_num_results=*/3u); |
| EXPECT_EQ(search_results.size(), 1u); |
| |
| // There should be no persistence results. |
| search_results = Search(u"test tag", |
| /*max_num_results=*/3u); |
| EXPECT_EQ(search_results.size(), 0u); |
| |
| // Check the persistence is also updated. |
| SearchConcept persistence(GetPersistencePath()); |
| persistence.GetSearchConcepts(ReadCallback(1u)); |
| } |
| |
| } // namespace ash::help_app |