blob: 389491166fcf184686373496d1b2ef974905b8bd [file] [log] [blame]
// 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 <map>
#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 "build/build_config.h"
#include "components/history/core/browser/top_sites.h"
#include "components/omnibox/browser/autocomplete_controller.h"
#include "components/omnibox/browser/autocomplete_provider_listener.h"
#include "components/omnibox/browser/fake_autocomplete_provider_client.h"
#include "components/omnibox/browser/omnibox_field_trial.h"
#include "components/omnibox/browser/test_scheme_classifier.h"
#include "components/omnibox/common/omnibox_features.h"
#include "testing/gmock/include/gmock/gmock.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"
namespace {
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() {
if (callbacks_.empty())
return false;
std::move(callbacks_.front()).Run(std::move(urls_));
callbacks_.pop_front();
return true;
}
history::MostVisitedURLList& urls() { return urls_; }
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_;
history::MostVisitedURLList urls_;
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";
} // 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 history::MostVisitedURLList& urls,
bool expect_tiles);
// 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_;
scoped_refptr<FakeTopSites> top_sites_;
scoped_refptr<MostVisitedSitesProvider> provider_;
base::test::ScopedFeatureList features_;
std::unique_ptr<AutocompleteController> controller_;
};
size_t MostVisitedSitesProviderTest::NumMostVisitedMatches() {
// TODO(khalidpeer): Update to make direct use of the provider's matches.
const auto& result = controller_->result();
size_t count = 0;
for (const auto& match : result) {
if ((match.type == AutocompleteMatchType::TILE_NAVSUGGEST) ||
(match.type == AutocompleteMatchType::NAVSUGGEST)) {
++count;
}
}
return count;
}
const AutocompleteMatch* MostVisitedSitesProviderTest::GetMatch(
AutocompleteMatchType::Type type,
size_t index) {
// TODO(khalidpeer): Update to make direct use of the provider's matches.
const auto& result = controller_->result();
for (const auto& match : result) {
if (match.type == type) {
if (!index)
return &match;
--index;
}
}
return nullptr;
}
void MostVisitedSitesProviderTest::CheckMatchesEquivalentTo(
const history::MostVisitedURLList& urls,
bool expect_tiles) {
// 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.
// TODO(khalidpeer): Update to make direct use of the provider's matches.
const auto& result = controller_->result();
size_t match_index = 0;
if (expect_tiles) {
ASSERT_EQ(1ul, NumMostVisitedMatches())
<< "Expected only one TILE_NAVSUGGEST match";
for (const auto& match : result) {
if (match.type != AutocompleteMatchType::TILE_NAVSUGGEST)
continue;
const auto& tiles = match.suggest_tiles;
ASSERT_EQ(urls.size(), tiles.size()) << "Wrong number of tiles reported";
for (size_t index = 0u; index < urls.size(); index++) {
EXPECT_EQ(urls[index].url, tiles[index].url)
<< "Invalid Tile URL at position " << index;
EXPECT_EQ(urls[index].title, tiles[index].title)
<< "Invalid Tile Title at position " << index;
}
break;
}
} else {
ASSERT_EQ(urls.size(), NumMostVisitedMatches())
<< "Unexpected number of NAVSUGGEST matches";
for (const auto& match : result) {
if (match.type != AutocompleteMatchType::NAVSUGGEST)
continue;
EXPECT_EQ(urls[match_index].url, match.destination_url)
<< "Invalid Match URL at position " << match_index;
EXPECT_EQ(urls[match_index].title, match.description)
<< "Invalid Match Title at position " << match_index;
++match_index;
}
}
}
void MostVisitedSitesProviderTest::SetUp() {
task_environment_ =
std::make_unique<base::test::SingleThreadTaskEnvironment>();
top_sites_ = new FakeTopSites();
auto client = std::make_unique<FakeAutocompleteProviderClient>();
client->set_top_sites(top_sites_);
// For tests requiring direct interaction with the Provider.
provider_ = new MostVisitedSitesProvider(client.get(), this);
// For tests not requiring direct interaction with the Provider.
controller_ = std::make_unique<AutocompleteController>(
std::move(client), AutocompleteProvider::TYPE_MOST_VISITED_SITES);
// Inject a few URLs to test MostVisitedSitesProvider behavior.
std::array<history::MostVisitedURL, 5> test_data{{
{GURL("http://www.a.art/"), u"A art"},
{GURL("http://www.b.biz/"), u"B biz"},
{GURL("http://www.c.com/"), u"C com"},
{GURL("http://www.d.de/"), u"D de"},
{GURL("http://www.e.edu/"), u"E edu"},
}};
top_sites_->urls().assign(test_data.begin(), test_data.end());
}
void MostVisitedSitesProviderTest::OnProviderUpdate(
bool updated_matches,
const AutocompleteProvider* provider) {}
class MostVisitedSitesProviderWithMatchesTest
: public MostVisitedSitesProviderTest {
public:
void SetUp() override {
features_.InitAndDisableFeature(omnibox::kMostVisitedTiles);
MostVisitedSitesProviderTest::SetUp();
}
};
TEST_F(MostVisitedSitesProviderWithMatchesTest, TestMostVisitedCallback) {
auto input = BuildAutocompleteInputForWebOnFocus();
controller_->Start(input);
EXPECT_EQ(0u, NumMostVisitedMatches());
EXPECT_TRUE(top_sites_->EmitURLs());
CheckMatchesEquivalentTo(top_sites_->urls(), /* expect_tiles=*/false);
controller_->Stop(false);
controller_->Start(input);
controller_->Stop(false);
// Since this provider's async logic is still in-flight (`EmitURLs()` has not
// been called yet), the AutocompleteController will transfer old matches from
// the previous provider run.
CheckMatchesEquivalentTo(top_sites_->urls(), /* expect_tiles=*/false);
history::MostVisitedURLList old_urls = top_sites_->urls();
// Update the list of top sites so that we can clearly identify when
// matches have been transferred from the previous provider run.
std::array<history::MostVisitedURL, 1> new_urls{{
{GURL("http://www.g.gov/"), u"G gov"},
}};
top_sites_->urls().assign(new_urls.begin(), new_urls.end());
// Most visited results arriving after Stop() has been called, ensure they
// are not displayed.
EXPECT_TRUE(top_sites_->EmitURLs());
CheckMatchesEquivalentTo(old_urls, /* expect_tiles=*/false);
controller_->Start(input);
controller_->Stop(false);
controller_->Start(input);
// Stale results should get rejected.
EXPECT_TRUE(top_sites_->EmitURLs());
CheckMatchesEquivalentTo(old_urls, /* expect_tiles=*/false);
// Results for the second Start() action should be recorded.
EXPECT_TRUE(top_sites_->EmitURLs());
CheckMatchesEquivalentTo(top_sites_->urls(), /* expect_tiles=*/false);
controller_->Stop(false);
}
TEST_F(MostVisitedSitesProviderWithMatchesTest,
TestMostVisitedNavigateToSearchPage) {
controller_->Start(BuildAutocompleteInputForWebOnFocus());
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);
controller_->Start(srp_input);
EXPECT_EQ(0u, NumMostVisitedMatches());
// Most visited results arriving after a new request has been started.
EXPECT_TRUE(top_sites_->EmitURLs());
EXPECT_EQ(0u, NumMostVisitedMatches());
}
class ParameterizedMostVisitedSitesProviderTest
: public MostVisitedSitesProviderTest,
public ::testing::WithParamInterface<bool> {
void SetUp() override {
std::vector<base::Feature> enabled_features;
std::vector<base::Feature> disabled_features;
bool isEnabled = GetParam();
if (isEnabled) {
enabled_features.push_back(omnibox::kMostVisitedTiles);
enabled_features.push_back(omnibox::kOmniboxMostVisitedTilesOnSrp);
} else {
disabled_features.push_back(omnibox::kMostVisitedTiles);
disabled_features.push_back(omnibox::kOmniboxMostVisitedTilesOnSrp);
}
features_.InitWithFeatures(enabled_features, disabled_features);
MostVisitedSitesProviderTest::SetUp();
}
};
INSTANTIATE_TEST_SUITE_P(All,
ParameterizedMostVisitedSitesProviderTest,
::testing::Bool(),
[](const auto& info) {
return info.param ? "SingleMatchWithTiles"
: "IndividualMatches";
});
TEST_P(ParameterizedMostVisitedSitesProviderTest,
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)));
// Offer MV sites when the User searched for a query and focus on omnibox.
EXPECT_EQ(
GetParam(),
provider_->AllowMostVisitedSitesSuggestions(BuildAutocompleteInput(
WEB_URL, WEB_URL, OEP::SEARCH_RESULT_PAGE_NO_SEARCH_TERM_REPLACEMENT,
OFT::INTERACTION_FOCUS)));
}
TEST_P(ParameterizedMostVisitedSitesProviderTest, TestCreateMostVisitedMatch) {
controller_->Start(BuildAutocompleteInputForWebOnFocus());
EXPECT_EQ(0u, NumMostVisitedMatches());
// Accept only direct TopSites data.
EXPECT_TRUE(top_sites_->EmitURLs());
CheckMatchesEquivalentTo(top_sites_->urls(), GetParam());
}
TEST_P(ParameterizedMostVisitedSitesProviderTest,
NoMatchesWhenNoMostVisitedSites) {
// Start with no URLs.
top_sites_->urls().clear();
controller_->Start(BuildAutocompleteInputForWebOnFocus());
EXPECT_EQ(0u, NumMostVisitedMatches());
// Accept only direct TopSites data, confirm no matches are built.
EXPECT_TRUE(top_sites_->EmitURLs());
EXPECT_EQ(0u, NumMostVisitedMatches());
}
TEST_P(ParameterizedMostVisitedSitesProviderTest,
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);
controller_->Start(input);
EXPECT_TRUE(provider_->done());
EXPECT_EQ(0u, NumMostVisitedMatches());
// No callbacks should have been added due to early return.
EXPECT_FALSE(top_sites_->EmitURLs());
EXPECT_EQ(0u, NumMostVisitedMatches());
}
TEST_P(ParameterizedMostVisitedSitesProviderTest,
TestDeleteMostVisitedElement) {
// Make a copy (intentional - we'll modify this later)
auto urls = top_sites_->urls();
controller_->Start(BuildAutocompleteInputForWebOnFocus());
// Accept only direct TopSites data.
EXPECT_TRUE(top_sites_->EmitURLs());
CheckMatchesEquivalentTo(urls, GetParam());
// Commence delete.
if (GetParam()) {
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", 5,
1);
histogram_.ExpectTotalCount("Omnibox.SuggestTiles.DeletedTileIndex", 0);
auto* match = GetMatch(AutocompleteMatchType::TILE_NAVSUGGEST, 0);
ASSERT_NE(nullptr, match) << "No TILE_NAVSUGGEST Match found";
controller_->DeleteMatchElement(*match, 1);
histogram_.ExpectTotalCount("Omnibox.SuggestTiles.DeletedTileIndex", 1);
histogram_.ExpectBucketCount("Omnibox.SuggestTiles.DeletedTileIndex", 1, 1);
// Note: TileTypeCounts are not emitted after deletion.
} else {
auto* match = GetMatch(AutocompleteMatchType::NAVSUGGEST, 1);
ASSERT_NE(nullptr, match) << "No NAVSUGGEST Match found";
controller_->DeleteMatch(*match);
}
// Observe that the URL is now blocked and removed from suggestion.
auto deleted_url = urls[1].url;
urls.erase(urls.begin() + 1);
CheckMatchesEquivalentTo(urls, GetParam());
EXPECT_TRUE(top_sites_->IsBlocked(deleted_url));
}
TEST_P(ParameterizedMostVisitedSitesProviderTest,
NoMatchesWhenLastURLIsDeleted) {
// Start with just one URL.
auto& urls = top_sites_->urls();
urls.clear();
urls.emplace_back(GURL("http://www.a.art/"), u"A art");
controller_->Start(BuildAutocompleteInputForWebOnFocus());
EXPECT_TRUE(top_sites_->EmitURLs());
CheckMatchesEquivalentTo(urls, GetParam());
// Commence delete of the only item that we have.
if (GetParam()) {
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";
controller_->DeleteMatchElement(*match, 0);
histogram_.ExpectTotalCount("Omnibox.SuggestTiles.DeletedTileIndex", 1);
histogram_.ExpectBucketCount("Omnibox.SuggestTiles.DeletedTileIndex", 0, 1);
// Note: TileTypeCounts are not emitted after deletion.
} else {
auto* match = GetMatch(AutocompleteMatchType::NAVSUGGEST, 0);
ASSERT_NE(nullptr, match) << "No NAVSUGGEST Match found";
controller_->DeleteMatch(*match);
}
// Confirm no more NAVSUGGEST matches are offered.
EXPECT_EQ(0u, NumMostVisitedMatches());
}