| // Copyright 2020 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "components/omnibox/browser/most_visited_sites_provider.h" |
| |
| #include <list> |
| #include <memory> |
| #include <string> |
| |
| #include "base/test/metrics/histogram_tester.h" |
| #include "base/test/scoped_feature_list.h" |
| #include "base/test/task_environment.h" |
| #include "components/history/core/browser/top_sites.h" |
| #include "components/omnibox/browser/autocomplete_provider_listener.h" |
| #include "components/omnibox/browser/fake_autocomplete_provider_client.h" |
| #include "components/omnibox/browser/test_scheme_classifier.h" |
| #include "components/omnibox/common/omnibox_features.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| #include "third_party/metrics_proto/omnibox_event.pb.h" |
| #include "third_party/metrics_proto/omnibox_focus_type.pb.h" |
| #include "ui/base/device_form_factor.h" |
| |
| namespace { |
| struct TestData { |
| bool is_search; |
| history::MostVisitedURL entry; |
| }; |
| |
| class FakeTopSites : public history::TopSites { |
| public: |
| FakeTopSites() = default; |
| |
| // history::TopSites: |
| void GetMostVisitedURLs(GetMostVisitedURLsCallback callback) override { |
| callbacks_.push_back(std::move(callback)); |
| } |
| void SyncWithHistory() override {} |
| |
| bool HasBlockedUrls() const override { return !blocked_urls_.empty(); } |
| void AddBlockedUrl(const GURL& url) override { |
| blocked_urls_.insert(url.spec()); |
| } |
| void RemoveBlockedUrl(const GURL& url) override { |
| blocked_urls_.erase(url.spec()); |
| } |
| bool IsBlocked(const GURL& url) override { |
| return blocked_urls_.count(url.spec()) > 0; |
| } |
| void ClearBlockedUrls() override { blocked_urls_.clear(); } |
| bool IsFull() override { return false; } |
| bool loaded() const override { return false; } |
| history::PrepopulatedPageList GetPrepopulatedPages() override { |
| return history::PrepopulatedPageList(); |
| } |
| void OnNavigationCommitted(const GURL& url) override {} |
| |
| // RefcountedKeyedService: |
| void ShutdownOnUIThread() override {} |
| |
| // Only runs a single callback, so that the test can specify a different |
| // set per call. |
| // Returns true if there was a recipient to receive the URLs and the list was |
| // emitted, otherwise returns false. |
| bool EmitURLs(const std::vector<TestData>& data) { |
| if (callbacks_.empty()) |
| return false; |
| |
| history::MostVisitedURLList urls; |
| for (const auto& test_element : data) { |
| urls.push_back(test_element.entry); |
| } |
| |
| std::move(callbacks_.front()).Run(std::move(urls)); |
| callbacks_.pop_front(); |
| return true; |
| } |
| |
| const std::set<std::string>& blocked_urls() const { return blocked_urls_; } |
| |
| protected: |
| // A test-specific field for controlling when most visited callback is run |
| // after top sites have been requested. |
| std::list<GetMostVisitedURLsCallback> callbacks_; |
| std::set<std::string> blocked_urls_; |
| |
| ~FakeTopSites() override = default; |
| }; |
| |
| constexpr const auto* WEB_URL = u"https://example.com/"; |
| constexpr const auto* SRP_URL = u"https://www.google.com/?q=flowers"; |
| constexpr const auto* FTP_URL = u"ftp://just.for.filtering.com"; |
| |
| enum class ExpectedUiType { |
| kAggregateMatch, |
| kIndividualTiles |
| }; |
| |
| const std::vector<TestData> DefaultTestData() { |
| return {{false, {GURL("http://www.a.art/"), u"A art"}}, |
| {false, {GURL("http://www.b.biz/"), u"B biz"}}, |
| {false, {GURL("http://www.c.com/"), u"C com"}}, |
| {false, {GURL("http://www.d.de/"), u"D de"}}, |
| {true, {GURL("http://www.google.com/search?q=abc"), u"abc"}}}; |
| } |
| |
| } // namespace |
| |
| class MostVisitedSitesProviderTest : public testing::Test, |
| public AutocompleteProviderListener { |
| public: |
| void SetUp() override; |
| |
| protected: |
| // Construct AutocompleteInput object a hypothetical Omnibox session context. |
| // Does not run any validation on the supplied values, allowing any |
| // combination (including invalid ones) to be used to create AutocompleteInput |
| // context object. |
| AutocompleteInput BuildAutocompleteInput( |
| const std::u16string& input_url, |
| const std::u16string& current_url, |
| metrics::OmniboxEventProto::PageClassification page_class, |
| metrics::OmniboxFocusType focus_type) { |
| AutocompleteInput input(input_url, page_class, TestSchemeClassifier()); |
| input.set_focus_type(focus_type); |
| input.set_current_url(GURL(current_url)); |
| return input; |
| } |
| |
| // Helper method, constructing a valid AutocompleteInput object for a website |
| // visit. |
| AutocompleteInput BuildAutocompleteInputForWebOnFocus() { |
| return BuildAutocompleteInput(WEB_URL, WEB_URL, |
| metrics::OmniboxEventProto::OTHER, |
| metrics::OmniboxFocusType::INTERACTION_FOCUS); |
| } |
| |
| // Iterate over all matches offered by the Provider and verify these against |
| // the supplied list of History URLs. |
| void CheckMatchesEquivalentTo(const std::vector<TestData>& data, |
| ExpectedUiType ui_type); |
| |
| // Returns total number of all NAVSUGGEST and TILE_NAVSUGGEST elements. |
| size_t NumMostVisitedMatches(); |
| |
| // Returns the N-th match of a particular type, skipping over all matches of |
| // other types. If match of that type does not exist, or there are not enough |
| // elements of that type, this call returns null. |
| const AutocompleteMatch* GetMatch(AutocompleteMatchType::Type type, |
| size_t index); |
| |
| // AutocompleteProviderListener: |
| void OnProviderUpdate(bool updated_matches, |
| const AutocompleteProvider* provider) override; |
| |
| base::HistogramTester histogram_; |
| std::unique_ptr<base::test::SingleThreadTaskEnvironment> task_environment_; |
| FakeAutocompleteProviderClient client_; |
| scoped_refptr<FakeTopSites> top_sites_; |
| scoped_refptr<MostVisitedSitesProvider> provider_; |
| int provider_update_count_{}; |
| }; |
| |
| size_t MostVisitedSitesProviderTest::NumMostVisitedMatches() { |
| const auto& result = provider_->matches(); |
| size_t count = 0; |
| for (const auto& match : result) { |
| if ((match.type == AutocompleteMatchType::TILE_NAVSUGGEST) || |
| (match.type == AutocompleteMatchType::NAVSUGGEST) || |
| (match.type == AutocompleteMatchType::TILE_MOST_VISITED_SITE) || |
| (match.type == AutocompleteMatchType::TILE_REPEATABLE_QUERY)) { |
| ++count; |
| } |
| } |
| return count; |
| } |
| |
| const AutocompleteMatch* MostVisitedSitesProviderTest::GetMatch( |
| AutocompleteMatchType::Type type, |
| size_t index) { |
| const auto& result = provider_->matches(); |
| for (const auto& match : result) { |
| if (match.type == type) { |
| if (!index) |
| return &match; |
| --index; |
| } |
| } |
| return nullptr; |
| } |
| |
| void MostVisitedSitesProviderTest::CheckMatchesEquivalentTo( |
| const std::vector<TestData>& data, |
| ExpectedUiType ui_type) { |
| // Compare the AutocompleteResult against a set of URLs that we expect to see. |
| // Note that additional matches may be offered if other providers are also |
| // registered in the same category as MostVisitedSitesProvider. |
| // We ignore all matches that are not ours. |
| const auto& result = provider_->matches(); |
| |
| size_t match_index = 0; |
| |
| if (ui_type == ExpectedUiType::kAggregateMatch) { |
| ASSERT_EQ(1ul, NumMostVisitedMatches()) |
| << "Expected only one TILE_NAVSUGGEST match"; |
| for (const auto& match : result) { |
| if (match.type != AutocompleteMatchType::TILE_NAVSUGGEST) |
| continue; |
| EXPECT_TRUE(match.subtypes.contains( |
| omnibox::SUBTYPE_ZERO_PREFIX_LOCAL_FREQUENT_URLS)); |
| EXPECT_TRUE(match.subtypes.contains(omnibox::SUBTYPE_URL_BASED)); |
| const auto& tiles = match.suggest_tiles; |
| ASSERT_EQ(data.size(), tiles.size()) << "Wrong number of tiles reported"; |
| for (size_t index = 0u; index < data.size(); index++) { |
| EXPECT_EQ(data[index].entry.url, tiles[index].url) |
| << "Invalid Tile URL at position " << index; |
| EXPECT_EQ(data[index].entry.title, tiles[index].title) |
| << "Invalid Tile Title at position " << index; |
| } |
| break; |
| } |
| } else if (ui_type == ExpectedUiType::kIndividualTiles) { |
| ASSERT_EQ(data.size(), NumMostVisitedMatches()) |
| << "Unexpected number of TILE matches"; |
| int expected_relevance = 1600; // kMostVisitedTilesIndividualHighRelevance |
| for (const auto& match : result) { |
| if (data[match_index].is_search) { |
| EXPECT_EQ(match.type, AutocompleteMatchType::TILE_REPEATABLE_QUERY); |
| EXPECT_TRUE(match.subtypes.contains( |
| omnibox::SUBTYPE_ZERO_PREFIX_LOCAL_FREQUENT_QUERIES)); |
| } else { |
| EXPECT_EQ(match.type, AutocompleteMatchType::TILE_MOST_VISITED_SITE); |
| EXPECT_TRUE(match.subtypes.contains( |
| omnibox::SUBTYPE_ZERO_PREFIX_LOCAL_FREQUENT_URLS)); |
| EXPECT_TRUE(match.subtypes.contains(omnibox::SUBTYPE_URL_BASED)); |
| } |
| |
| EXPECT_EQ(data[match_index].entry.url, match.destination_url) |
| << "Invalid Match URL at position " << match_index; |
| EXPECT_EQ(data[match_index].entry.title, match.description) |
| << "Invalid Match Title at position " << match_index; |
| EXPECT_EQ(expected_relevance, match.relevance) |
| << "Invalid Match Relevance at position " << match_index; |
| ++match_index; |
| // Degrade relevance of partially visible and invisible matches. |
| if (match_index == 4 && |
| ui::GetDeviceFormFactor() == |
| ui::DeviceFormFactor::DEVICE_FORM_FACTOR_PHONE) { |
| expected_relevance = 100; // kMostVisitedTilesIndividualLowRelevance |
| } |
| --expected_relevance; |
| } |
| } |
| } |
| |
| void MostVisitedSitesProviderTest::SetUp() { |
| task_environment_ = |
| std::make_unique<base::test::SingleThreadTaskEnvironment>(); |
| top_sites_ = new FakeTopSites(); |
| |
| client_.set_top_sites(top_sites_); |
| |
| // For tests requiring direct interaction with the Provider. |
| provider_ = new MostVisitedSitesProvider(&client_, this); |
| } |
| |
| void MostVisitedSitesProviderTest::OnProviderUpdate( |
| bool updated_matches, |
| const AutocompleteProvider* provider) { |
| provider_update_count_++; |
| } |
| |
| TEST_F(MostVisitedSitesProviderTest, TestMostVisitedCallback) { |
| base::test::ScopedFeatureList features; |
| features.InitAndDisableFeature( |
| omnibox::kMostVisitedTilesHorizontalRenderGroup); |
| auto input = BuildAutocompleteInputForWebOnFocus(); |
| provider_->Start(input, true); |
| EXPECT_EQ(0u, NumMostVisitedMatches()); |
| auto test_data = DefaultTestData(); |
| EXPECT_TRUE(top_sites_->EmitURLs(test_data)); |
| CheckMatchesEquivalentTo(test_data, ExpectedUiType::kAggregateMatch); |
| EXPECT_EQ(1, provider_update_count_); |
| provider_->Stop(false, false); |
| |
| // Observe that subsequent request does not return stale data. |
| provider_->Start(input, true); |
| provider_->Stop(false, false); |
| // Since this provider's async logic is still in-flight (`EmitURLs()` has not |
| // been called yet), we should not be reporting anything from past runs. |
| EXPECT_EQ(0ul, NumMostVisitedMatches()); |
| EXPECT_EQ(1, provider_update_count_); |
| |
| // Most visited results arriving after Stop() has been called, ensure they |
| // are not displayed. |
| std::vector<TestData> new_urls{{ |
| false, |
| {GURL("http://www.g.gov/"), u"G gov"}, |
| }}; |
| EXPECT_TRUE(top_sites_->EmitURLs(new_urls)); |
| EXPECT_EQ(0ul, NumMostVisitedMatches()); |
| EXPECT_EQ(1, provider_update_count_); |
| |
| provider_->Start(input, true); |
| provider_->Stop(false, false); |
| provider_->Start(input, true); |
| |
| // Stale results (reported for the first of the two Start() requests) should |
| // be rejected. |
| EXPECT_TRUE(top_sites_->EmitURLs(DefaultTestData())); |
| EXPECT_EQ(0ul, NumMostVisitedMatches()); |
| EXPECT_EQ(1, provider_update_count_); |
| |
| // Results for the second Start() action should be recorded. |
| EXPECT_TRUE(top_sites_->EmitURLs(test_data)); |
| CheckMatchesEquivalentTo(test_data, ExpectedUiType::kAggregateMatch); |
| EXPECT_EQ(2, provider_update_count_); |
| provider_->Stop(false, false); |
| } |
| |
| TEST_F(MostVisitedSitesProviderTest, TestMostVisitedNavigateToSearchPage) { |
| provider_->Start(BuildAutocompleteInputForWebOnFocus(), true); |
| EXPECT_EQ(0u, NumMostVisitedMatches()); |
| // Stop() doesn't always get called. |
| |
| auto srp_input = BuildAutocompleteInput( |
| SRP_URL, SRP_URL, |
| metrics::OmniboxEventProto::SEARCH_RESULT_PAGE_NO_SEARCH_TERM_REPLACEMENT, |
| metrics::OmniboxFocusType::INTERACTION_FOCUS); |
| |
| provider_->Start(srp_input, true); |
| EXPECT_EQ(0u, NumMostVisitedMatches()); |
| |
| // Most visited results arriving after a new request has been started. |
| EXPECT_TRUE(top_sites_->EmitURLs(DefaultTestData())); |
| EXPECT_EQ(0u, NumMostVisitedMatches()); |
| } |
| |
| TEST_F(MostVisitedSitesProviderTest, AllowMostVisitedSitesSuggestions) { |
| using OEP = metrics::OmniboxEventProto; |
| using OFT = metrics::OmniboxFocusType; |
| |
| // MostVisited should never deal with prefix suggestions. |
| EXPECT_FALSE( |
| provider_->AllowMostVisitedSitesSuggestions(BuildAutocompleteInput( |
| WEB_URL, WEB_URL, OEP::OTHER, OFT::INTERACTION_DEFAULT))); |
| |
| // This should always be true, as otherwise we will break MostVisited. |
| EXPECT_TRUE( |
| provider_->AllowMostVisitedSitesSuggestions(BuildAutocompleteInput( |
| WEB_URL, WEB_URL, OEP::OTHER, OFT::INTERACTION_FOCUS))); |
| |
| // Verifies that non-permitted schemes are rejected. |
| EXPECT_FALSE( |
| provider_->AllowMostVisitedSitesSuggestions(BuildAutocompleteInput( |
| FTP_URL, FTP_URL, OEP::OTHER, OFT::INTERACTION_FOCUS))); |
| |
| // Offer MV sites when the User is visiting a website and deletes text. |
| EXPECT_TRUE( |
| provider_->AllowMostVisitedSitesSuggestions(BuildAutocompleteInput( |
| WEB_URL, WEB_URL, OEP::OTHER, OFT::INTERACTION_CLOBBER))); |
| } |
| |
| TEST_F(MostVisitedSitesProviderTest, NoSRPCoverage) { |
| using OEP = metrics::OmniboxEventProto; |
| using OFT = metrics::OmniboxFocusType; |
| |
| EXPECT_FALSE( |
| provider_->AllowMostVisitedSitesSuggestions(BuildAutocompleteInput( |
| WEB_URL, WEB_URL, OEP::SEARCH_RESULT_PAGE_NO_SEARCH_TERM_REPLACEMENT, |
| OFT::INTERACTION_FOCUS))); |
| } |
| |
| TEST_F(MostVisitedSitesProviderTest, TestCreateMostVisitedMatch) { |
| base::test::ScopedFeatureList features; |
| features.InitAndDisableFeature( |
| omnibox::kMostVisitedTilesHorizontalRenderGroup); |
| provider_->Start(BuildAutocompleteInputForWebOnFocus(), true); |
| EXPECT_EQ(0u, NumMostVisitedMatches()); |
| // Accept only direct TopSites data. |
| auto test_data = DefaultTestData(); |
| EXPECT_TRUE(top_sites_->EmitURLs(test_data)); |
| CheckMatchesEquivalentTo(test_data, ExpectedUiType::kAggregateMatch); |
| } |
| |
| TEST_F(MostVisitedSitesProviderTest, NoMatchesWhenNoMostVisitedSites) { |
| // Start with no URLs. |
| provider_->Start(BuildAutocompleteInputForWebOnFocus(), true); |
| EXPECT_EQ(0u, NumMostVisitedMatches()); |
| // Accept only direct TopSites data, confirm no matches are built. |
| EXPECT_TRUE(top_sites_->EmitURLs({})); |
| EXPECT_EQ(0u, NumMostVisitedMatches()); |
| } |
| |
| TEST_F(MostVisitedSitesProviderTest, |
| NoMatchesWhenTopSitesNotLoadedAndWantAsyncMatchesFalse) { |
| // Assume that top sites list has not been loaded yet from the DB. |
| ASSERT_FALSE(top_sites_->loaded()); |
| auto input = BuildAutocompleteInputForWebOnFocus(); |
| input.set_focus_type(metrics::OmniboxFocusType::INTERACTION_DEFAULT); |
| input.set_omit_asynchronous_matches(true); |
| provider_->Start(input, true); |
| EXPECT_TRUE(provider_->done()); |
| EXPECT_EQ(0u, NumMostVisitedMatches()); |
| // No callbacks should have been added due to early return. |
| EXPECT_FALSE(top_sites_->EmitURLs(DefaultTestData())); |
| EXPECT_EQ(0u, NumMostVisitedMatches()); |
| } |
| |
| TEST_F(MostVisitedSitesProviderTest, TestDeleteMostVisitedElement) { |
| base::test::ScopedFeatureList features; |
| features.InitAndDisableFeature( |
| omnibox::kMostVisitedTilesHorizontalRenderGroup); |
| // Make a copy (intentional - we'll modify this later) |
| provider_->Start(BuildAutocompleteInputForWebOnFocus(), true); |
| // Accept only direct TopSites data. |
| auto test_data = DefaultTestData(); |
| EXPECT_TRUE(top_sites_->EmitURLs(test_data)); |
| CheckMatchesEquivalentTo(test_data, ExpectedUiType::kAggregateMatch); |
| |
| // Commence delete. |
| histogram_.ExpectTotalCount("Omnibox.SuggestTiles.TileTypeCount.Search", 1); |
| histogram_.ExpectBucketCount("Omnibox.SuggestTiles.TileTypeCount.Search", 1, |
| 1); |
| histogram_.ExpectTotalCount("Omnibox.SuggestTiles.TileTypeCount.URL", 1); |
| histogram_.ExpectBucketCount("Omnibox.SuggestTiles.TileTypeCount.URL", 4, 1); |
| histogram_.ExpectTotalCount("Omnibox.SuggestTiles.DeletedTileIndex", 0); |
| auto* match = GetMatch(AutocompleteMatchType::TILE_NAVSUGGEST, 0); |
| ASSERT_NE(nullptr, match) << "No TILE_NAVSUGGEST Match found"; |
| provider_->DeleteMatchElement(*match, 1); |
| histogram_.ExpectTotalCount("Omnibox.SuggestTiles.DeletedTileIndex", 1); |
| histogram_.ExpectBucketCount("Omnibox.SuggestTiles.DeletedTileIndex", 1, 1); |
| // Note: TileTypeCounts are not emitted after deletion. |
| |
| // Observe that the URL is now blocked and removed from suggestion. |
| auto deleted_url = test_data[1].entry.url; |
| test_data.erase(test_data.begin() + 1); |
| CheckMatchesEquivalentTo(test_data, ExpectedUiType::kAggregateMatch); |
| EXPECT_TRUE(top_sites_->IsBlocked(deleted_url)); |
| } |
| |
| TEST_F(MostVisitedSitesProviderTest, NoMatchesWhenLastURLIsDeleted) { |
| base::test::ScopedFeatureList features; |
| features.InitAndDisableFeature( |
| omnibox::kMostVisitedTilesHorizontalRenderGroup); |
| |
| // Start with just one URL. |
| std::vector<TestData> urls{{ |
| {false, {GURL("http://www.a.art/"), u"A art"}}, |
| }}; |
| |
| provider_->Start(BuildAutocompleteInputForWebOnFocus(), true); |
| EXPECT_TRUE(top_sites_->EmitURLs(urls)); |
| CheckMatchesEquivalentTo(urls, ExpectedUiType::kAggregateMatch); |
| |
| // Commence delete of the only item that we have. |
| histogram_.ExpectTotalCount("Omnibox.SuggestTiles.TileTypeCount.Search", 1); |
| histogram_.ExpectBucketCount("Omnibox.SuggestTiles.TileTypeCount.Search", 0, |
| 1); |
| histogram_.ExpectTotalCount("Omnibox.SuggestTiles.TileTypeCount.URL", 1); |
| histogram_.ExpectBucketCount("Omnibox.SuggestTiles.TileTypeCount.URL", 1, 1); |
| histogram_.ExpectTotalCount("Omnibox.SuggestTiles.DeletedTileIndex", 0); |
| auto* match = GetMatch(AutocompleteMatchType::TILE_NAVSUGGEST, 0); |
| ASSERT_NE(nullptr, match) << "No TILE_NAVSUGGEST Match found"; |
| provider_->DeleteMatchElement(*match, 0); |
| histogram_.ExpectTotalCount("Omnibox.SuggestTiles.DeletedTileIndex", 1); |
| histogram_.ExpectBucketCount("Omnibox.SuggestTiles.DeletedTileIndex", 0, 1); |
| // Note: TileTypeCounts are not emitted after deletion. |
| |
| // Confirm no more NAVSUGGEST matches are offered. |
| EXPECT_EQ(0u, NumMostVisitedMatches()); |
| } |
| |
| TEST_F(MostVisitedSitesProviderTest, |
| TestCreateMostVisitedHorizontalGroupTiles) { |
| base::test::ScopedFeatureList features; |
| features.InitWithFeatures({omnibox::kMostVisitedTilesHorizontalRenderGroup}, |
| {}); |
| |
| provider_->Start(BuildAutocompleteInputForWebOnFocus(), true); |
| EXPECT_EQ(0u, NumMostVisitedMatches()); |
| // Accept only direct TopSites data. |
| auto test_data = DefaultTestData(); |
| EXPECT_TRUE(top_sites_->EmitURLs(test_data)); |
| CheckMatchesEquivalentTo(test_data, ExpectedUiType::kIndividualTiles); |
| } |