blob: c295089d564c66f96d786ae3dd03c27ea2a25b1a [file] [log] [blame]
// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/new_tab_page/modules/history_clusters/history_clusters_module_service.h"
#include <string>
#include <vector>
#include "base/run_loop.h"
#include "base/strings/utf_string_conversions.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/mock_callback.h"
#include "base/test/scoped_feature_list.h"
#include "base/time/time.h"
#include "chrome/browser/cart/cart_service.h"
#include "chrome/browser/cart/cart_service_factory.h"
#include "chrome/browser/history/history_service_factory.h"
#include "chrome/browser/history_clusters/history_clusters_service_factory.h"
#include "chrome/browser/search_engines/template_url_service_factory.h"
#include "chrome/test/base/testing_profile.h"
#include "components/history/core/browser/history_context.h"
#include "components/history/core/browser/history_types.h"
#include "components/history/core/test/history_service_test_util.h"
#include "components/history_clusters/core/history_clusters_service_task.h"
#include "components/history_clusters/core/history_clusters_types.h"
#include "components/history_clusters/core/test_history_clusters_service.h"
#include "components/history_clusters/public/mojom/history_cluster_types.mojom.h"
#include "components/search/ntp_features.h"
#include "components/search_engines/template_url_service.h"
#include "components/strings/grit/components_strings.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/base/l10n/l10n_util.h"
namespace {
class MockCartService : public CartService {
public:
explicit MockCartService(Profile* profile) : CartService(profile) {}
MOCK_METHOD2(HasActiveCartForURL,
void(const GURL& url, base::OnceCallback<void(bool)> callback));
};
constexpr char kSampleNonSearchUrl[] = "https://www.foo.com/";
constexpr char kSampleSearchUrl[] = "https://default-engine.com/search?q=foo";
const TemplateURLService::Initializer kTemplateURLData[] = {
{"default-engine.com", "http://default-engine.com/search?q={searchTerms}",
"Default"},
{"non-default-engine.com", "http://non-default-engine.com?q={searchTerms}",
"Not Default"},
};
class HistoryClustersModuleServiceTest : public testing::Test {
public:
HistoryClustersModuleServiceTest() {
scoped_feature_list_.InitAndEnableFeature(
ntp_features::kNtpHistoryClustersModuleFetchClustersUntilExhausted);
}
void SetUp() override {
testing::Test::SetUp();
TestingProfile::Builder profile_builder;
profile_builder.AddTestingFactory(
HistoryServiceFactory::GetInstance(),
HistoryServiceFactory::GetDefaultFactory());
testing_profile_ = profile_builder.Build();
test_history_clusters_service_ =
std::make_unique<history_clusters::TestHistoryClustersService>();
mock_cart_service_ =
std::make_unique<MockCartService>(testing_profile_.get());
template_url_service_ = std::make_unique<TemplateURLService>(
kTemplateURLData, std::size(kTemplateURLData));
history_clusters_module_service_ =
std::make_unique<HistoryClustersModuleService>(
test_history_clusters_service_.get(), mock_cart_service_.get(),
template_url_service_.get(),
/*optimization_guide_keyed_service=*/nullptr);
}
history_clusters::TestHistoryClustersService&
test_history_clusters_service() {
return *test_history_clusters_service_;
}
MockCartService& mock_cart_service() { return *mock_cart_service_; }
HistoryClustersModuleService& service() {
return *history_clusters_module_service_;
}
std::vector<history::Cluster> GetClusters() {
std::vector<history::Cluster> clusters;
base::RunLoop run_loop;
service().GetClusters(base::BindOnce(
[](base::RunLoop* run_loop, std::vector<history::Cluster>* out_clusters,
std::vector<history::Cluster> clusters) {
*out_clusters = std::move(clusters);
run_loop->Quit();
},
&run_loop, &clusters));
run_loop.Run();
return clusters;
}
private:
content::BrowserTaskEnvironment task_environment_;
std::unique_ptr<TestingProfile> testing_profile_;
std::unique_ptr<history_clusters::TestHistoryClustersService>
test_history_clusters_service_;
std::unique_ptr<MockCartService> mock_cart_service_;
std::unique_ptr<TemplateURLService> template_url_service_;
std::unique_ptr<HistoryClustersModuleService>
history_clusters_module_service_;
base::test::ScopedFeatureList scoped_feature_list_;
};
history::ClusterVisit SampleVisitForURL(
GURL url,
bool has_url_keyed_image = true,
const std::vector<std::string>& related_searches = {}) {
history::VisitRow visit_row;
visit_row.visit_id = 1;
visit_row.visit_time = base::Time::Now();
visit_row.is_known_to_sync = true;
auto content_annotations = history::VisitContentAnnotations();
content_annotations.has_url_keyed_image = has_url_keyed_image;
content_annotations.related_searches = related_searches;
history::AnnotatedVisit annotated_visit;
annotated_visit.visit_row = std::move(visit_row);
annotated_visit.content_annotations = std::move(content_annotations);
std::string kSampleUrl = url.spec();
history::ClusterVisit sample_visit;
sample_visit.url_for_display = base::UTF8ToUTF16(kSampleUrl);
sample_visit.normalized_url = url;
sample_visit.annotated_visit = std::move(annotated_visit);
sample_visit.score = 1.0f;
return sample_visit;
}
history::Cluster SampleCluster(int id,
int srp_visits,
int non_srp_visits,
const std::vector<std::string> related_searches =
{"fruits", "red fruits", "healthy fruits"}) {
history::ClusterVisit sample_srp_visit =
SampleVisitForURL(GURL(kSampleSearchUrl), false);
sample_srp_visit.score = 1.0;
history::ClusterVisit sample_non_srp_visit =
SampleVisitForURL(GURL(kSampleNonSearchUrl), true, related_searches);
sample_non_srp_visit.score = 0.9;
std::vector<history::ClusterVisit> visits;
visits.insert(visits.end(), srp_visits, sample_srp_visit);
visits.insert(visits.end(), non_srp_visits, sample_non_srp_visit);
std::string kSampleLabel = "LabelOne";
return history::Cluster(id, std::move(visits),
{{u"apples", history::ClusterKeywordData()},
{u"Red Oranges", history::ClusterKeywordData()}},
/*should_show_on_prominent_ui_surfaces=*/true,
/*label=*/
l10n_util::GetStringFUTF16(
IDS_HISTORY_CLUSTERS_CLUSTER_LABEL_SEARCH_TERMS,
base::UTF8ToUTF16(kSampleLabel)));
}
history::Cluster SampleCluster(int srp_visits,
int non_srp_visits,
const std::vector<std::string> related_searches =
{"fruits", "red fruits", "healthy fruits"}) {
return SampleCluster(1, srp_visits, non_srp_visits, related_searches);
}
TEST_F(HistoryClustersModuleServiceTest, GetClusters) {
base::HistogramTester histogram_tester;
const int kSampleClusterCount = 3;
std::vector<history::Cluster> sample_clusters;
for (int i = 0; i < kSampleClusterCount; i++) {
sample_clusters.push_back(
SampleCluster(i, /*srp_visits=*/1, /*non_srp_visits=*/2));
}
test_history_clusters_service().SetClustersToReturn(
sample_clusters, /*exhausted_all_visits=*/false);
std::vector<history::Cluster> clusters = GetClusters();
ASSERT_EQ(3u, clusters.size());
for (unsigned int i = 0; i < kSampleClusterCount; i++) {
const auto& cluster = clusters[i];
ASSERT_EQ(3u, cluster.visits.size());
for (size_t u = 1; u < cluster.visits.size(); u++) {
EXPECT_EQ(kSampleNonSearchUrl,
base::UTF16ToASCII(cluster.visits[u].url_for_display));
}
}
histogram_tester.ExpectUniqueSample(
"NewTabPage.HistoryClusters.ExhaustedEligibleClusters", false, 1);
histogram_tester.ExpectUniqueSample(
"NewTabPage.HistoryClusters.IneligibleReason", 0, 1);
histogram_tester.ExpectUniqueSample(
"NewTabPage.HistoryClusters.HasClusterToShow", true, 1);
histogram_tester.ExpectUniqueSample(
"NewTabPage.HistoryClusters.NumClusterCandidates", 3, 1);
histogram_tester.ExpectUniqueSample("NewTabPage.HistoryClusters.NumVisits", 3,
1);
histogram_tester.ExpectUniqueSample(
"NewTabPage.HistoryClusters.NumRelatedSearches", 3, 1);
}
TEST_F(HistoryClustersModuleServiceTest, ClusterVisitsCulled) {
base::HistogramTester histogram_tester;
const history::Cluster kSampleCluster =
SampleCluster(/*srp_visits=*/3, /*non_srp_visits=*/3);
const std::vector<history::Cluster> kSampleClusters = {kSampleCluster};
test_history_clusters_service().SetClustersToReturn(
kSampleClusters, /*exhausted_all_visits=*/true);
std::vector<history::Cluster> clusters = GetClusters();
ASSERT_EQ(1u, clusters.size());
auto& cluster = clusters[0];
ASSERT_EQ(kSampleCluster.label.value(), cluster.label);
ASSERT_EQ(4u, cluster.visits.size());
ASSERT_EQ(kSampleSearchUrl,
base::UTF16ToASCII(cluster.visits[0].url_for_display));
for (size_t i = 1; i < cluster.visits.size(); i++) {
ASSERT_EQ(kSampleNonSearchUrl,
base::UTF16ToASCII(cluster.visits[i].url_for_display));
}
histogram_tester.ExpectUniqueSample(
"NewTabPage.HistoryClusters.ExhaustedEligibleClusters", true, 1);
histogram_tester.ExpectUniqueSample(
"NewTabPage.HistoryClusters.HasClusterToShow", true, 1);
histogram_tester.ExpectUniqueSample(
"NewTabPage.HistoryClusters.NumClusterCandidates", 1, 1);
histogram_tester.ExpectUniqueSample("NewTabPage.HistoryClusters.NumVisits", 4,
1);
histogram_tester.ExpectUniqueSample(
"NewTabPage.HistoryClusters.NumRelatedSearches", 3, 1);
}
TEST_F(HistoryClustersModuleServiceTest, IneligibleClusterNonProminent) {
base::HistogramTester histogram_tester;
history::Cluster kSampleCluster =
SampleCluster(/*srp_visits=*/0, /*non_srp_visits=*/3);
kSampleCluster.should_show_on_prominent_ui_surfaces = false;
test_history_clusters_service().SetClustersToReturn({kSampleCluster});
std::vector<history::Cluster> clusters = GetClusters();
ASSERT_TRUE(clusters.empty());
histogram_tester.ExpectUniqueSample(
"NewTabPage.HistoryClusters.IneligibleReason", 2, 1);
histogram_tester.ExpectUniqueSample(
"NewTabPage.HistoryClusters.HasClusterToShow", false, 1);
histogram_tester.ExpectUniqueSample(
"NewTabPage.HistoryClusters.NumClusterCandidates", 0, 1);
}
TEST_F(HistoryClustersModuleServiceTest, IneligibleClusterNoSRPVisit) {
base::HistogramTester histogram_tester;
const history::Cluster kSampleCluster =
SampleCluster(/*srp_visits=*/0, /*non_srp_visits=*/3);
test_history_clusters_service().SetClustersToReturn({kSampleCluster});
std::vector<history::Cluster> clusters = GetClusters();
ASSERT_TRUE(clusters.empty());
histogram_tester.ExpectUniqueSample(
"NewTabPage.HistoryClusters.IneligibleReason", 3, 1);
histogram_tester.ExpectUniqueSample(
"NewTabPage.HistoryClusters.HasClusterToShow", false, 1);
histogram_tester.ExpectUniqueSample(
"NewTabPage.HistoryClusters.NumClusterCandidates", 0, 1);
}
TEST_F(HistoryClustersModuleServiceTest, IneligibleClusterInsufficientVisits) {
base::HistogramTester histogram_tester;
const history::Cluster kSampleCluster =
SampleCluster(/*srp_visits=*/1, /*non_srp_visits=*/1);
test_history_clusters_service().SetClustersToReturn({kSampleCluster});
std::vector<history::Cluster> clusters = GetClusters();
ASSERT_TRUE(clusters.empty());
histogram_tester.ExpectUniqueSample(
"NewTabPage.HistoryClusters.IneligibleReason", 4, 1);
histogram_tester.ExpectUniqueSample(
"NewTabPage.HistoryClusters.HasClusterToShow", false, 1);
histogram_tester.ExpectUniqueSample(
"NewTabPage.HistoryClusters.NumClusterCandidates", 0, 1);
}
TEST_F(HistoryClustersModuleServiceTest, IneligibleClusterInsufficientImages) {
base::HistogramTester histogram_tester;
history::ClusterVisit sample_srp_visit =
SampleVisitForURL(GURL(kSampleSearchUrl), false);
history::ClusterVisit sample_non_srp_visit =
SampleVisitForURL(GURL(kSampleNonSearchUrl), false);
const history::Cluster kSampleCluster = history::Cluster(
1, {sample_srp_visit, sample_non_srp_visit, sample_non_srp_visit},
{{u"apples", history::ClusterKeywordData()},
{u"Red Oranges", history::ClusterKeywordData()}},
/*should_show_on_prominent_ui_surfaces=*/true,
/*label=*/
l10n_util::GetStringFUTF16(
IDS_HISTORY_CLUSTERS_CLUSTER_LABEL_SEARCH_TERMS, u"Red fruits"));
test_history_clusters_service().SetClustersToReturn({kSampleCluster});
std::vector<history::Cluster> clusters = GetClusters();
ASSERT_TRUE(clusters.empty());
histogram_tester.ExpectUniqueSample(
"NewTabPage.HistoryClusters.IneligibleReason", 5, 1);
histogram_tester.ExpectUniqueSample(
"NewTabPage.HistoryClusters.HasClusterToShow", false, 1);
histogram_tester.ExpectUniqueSample(
"NewTabPage.HistoryClusters.NumClusterCandidates", 0, 1);
}
TEST_F(HistoryClustersModuleServiceTest,
IneligibleClusterInsufficientRelatedSearches) {
base::HistogramTester histogram_tester;
const history::Cluster kSampleCluster = SampleCluster(
/*id=*/1, /*srp_visits=*/1, /*non_srp_visits=*/2,
/*related_searches=*/{});
test_history_clusters_service().SetClustersToReturn({kSampleCluster});
std::vector<history::Cluster> clusters = GetClusters();
ASSERT_TRUE(clusters.empty());
histogram_tester.ExpectUniqueSample(
"NewTabPage.HistoryClusters.IneligibleReason", 6, 1);
histogram_tester.ExpectUniqueSample(
"NewTabPage.HistoryClusters.HasClusterToShow", false, 1);
histogram_tester.ExpectUniqueSample(
"NewTabPage.HistoryClusters.NumClusterCandidates", 0, 1);
}
TEST_F(HistoryClustersModuleServiceTest, NoClusters) {
base::HistogramTester histogram_tester;
test_history_clusters_service().SetClustersToReturn(
{}, /*exhausted_all_visits=*/true);
std::vector<history::Cluster> clusters = GetClusters();
ASSERT_TRUE(clusters.empty());
histogram_tester.ExpectUniqueSample(
"NewTabPage.HistoryClusters.IneligibleReason", 1, 1);
histogram_tester.ExpectUniqueSample(
"NewTabPage.HistoryClusters.HasClusterToShow", false, 1);
histogram_tester.ExpectUniqueSample(
"NewTabPage.HistoryClusters.NumClusterCandidates", 0, 1);
}
TEST_F(HistoryClustersModuleServiceTest, NoClustersDoesRefetch) {
base::HistogramTester histogram_tester;
test_history_clusters_service().SetClustersToReturn(
{}, /*exhausted_all_visits=*/false);
std::vector<history::Cluster> clusters = GetClusters();
ASSERT_TRUE(clusters.empty());
// The first time should return false but the second query is true.
histogram_tester.ExpectBucketCount(
"NewTabPage.HistoryClusters.ExhaustedEligibleClusters", false, 1);
histogram_tester.ExpectBucketCount(
"NewTabPage.HistoryClusters.ExhaustedEligibleClusters", true, 1);
// The bottom metrics should only record once when it's exhausted.
histogram_tester.ExpectUniqueSample(
"NewTabPage.HistoryClusters.IneligibleReason", 1, 1);
histogram_tester.ExpectUniqueSample(
"NewTabPage.HistoryClusters.HasClusterToShow", false, 1);
histogram_tester.ExpectUniqueSample(
"NewTabPage.HistoryClusters.NumClusterCandidates", 0, 1);
}
class HistoryClustersModuleServiceCartTest
: public HistoryClustersModuleServiceTest {
public:
HistoryClustersModuleServiceCartTest() {
features_.InitAndEnableFeature(ntp_features::kNtpChromeCartModule);
}
private:
base::test::ScopedFeatureList features_;
};
TEST_F(HistoryClustersModuleServiceCartTest, CheckClusterHasCart) {
base::HistogramTester histogram_tester;
std::string kSampleLabel = "LabelOne";
const GURL url_A = GURL("https://www.foo.com");
const GURL url_B = GURL("https://www.bar.com");
const GURL url_C = GURL("https://www.baz.com");
MockCartService& cart_service = mock_cart_service();
const std::vector<std::string> visit_related_searches = {
"fruits", "red fruits", "healthy fruits"};
const history::Cluster cluster =
history::Cluster(1,
{SampleVisitForURL(GURL(kSampleSearchUrl), false, {}),
SampleVisitForURL(url_A, true, visit_related_searches),
SampleVisitForURL(url_B, true, visit_related_searches),
SampleVisitForURL(url_C, true, visit_related_searches)},
{{u"apples", history::ClusterKeywordData()},
{u"Red Oranges", history::ClusterKeywordData()}},
/*should_show_on_prominent_ui_surfaces=*/true,
/*label=*/base::UTF8ToUTF16(kSampleLabel));
test_history_clusters_service().SetClustersToReturn({cluster});
// Vectors to capture mocked method args.
std::vector<GURL> urls;
std::vector<base::OnceCallback<void(bool)>> callbacks;
EXPECT_CALL(cart_service, HasActiveCartForURL(testing::_, testing::_))
.Times(cluster.visits.size())
.WillRepeatedly(testing::WithArgs<0, 1>(testing::Invoke(
[&urls, &callbacks](GURL url,
base::OnceCallback<void(bool)> callback) -> void {
urls.push_back(url);
callbacks.push_back(std::move(callback));
})));
GetClusters();
// Simulate one URL being identified as having a cart.
std::move(callbacks[0]).Run(true);
for (size_t i = 1; i < callbacks.size(); i++) {
std::move(callbacks[i]).Run(false);
}
histogram_tester.ExpectBucketCount(
"NewTabPage.HistoryClusters.HasCartForTopCluster", true, 1);
urls.clear();
callbacks.clear();
EXPECT_CALL(cart_service, HasActiveCartForURL(testing::_, testing::_))
.Times(cluster.visits.size())
.WillRepeatedly(testing::WithArgs<0, 1>(testing::Invoke(
[&urls, &callbacks](GURL url,
base::OnceCallback<void(bool)> callback) -> void {
urls.push_back(url);
callbacks.push_back(std::move(callback));
})));
GetClusters();
// Simulate none URL being identified as having a cart.
for (size_t i = 0; i < callbacks.size(); i++) {
std::move(callbacks[i]).Run(false);
}
histogram_tester.ExpectBucketCount(
"NewTabPage.HistoryClusters.HasCartForTopCluster", false, 1);
histogram_tester.ExpectTotalCount(
"NewTabPage.HistoryClusters.HasCartForTopCluster", 2);
}
class HistoryClustersModuleServiceDoesNotRefetchTest
: public HistoryClustersModuleServiceTest {
public:
HistoryClustersModuleServiceDoesNotRefetchTest() {
features_.InitAndDisableFeature(
ntp_features::kNtpHistoryClustersModuleFetchClustersUntilExhausted);
}
private:
base::test::ScopedFeatureList features_;
};
TEST_F(HistoryClustersModuleServiceDoesNotRefetchTest, NoClustersDoesRefetch) {
base::HistogramTester histogram_tester;
test_history_clusters_service().SetClustersToReturn(
{}, /*exhausted_all_visits=*/false);
std::vector<history::Cluster> clusters = GetClusters();
ASSERT_TRUE(clusters.empty());
// There should only be one query to the History Clusters service.
histogram_tester.ExpectUniqueSample(
"NewTabPage.HistoryClusters.ExhaustedEligibleClusters", false, 1);
// The bottom metrics should be recorded when we are done fetching.
histogram_tester.ExpectUniqueSample(
"NewTabPage.HistoryClusters.IneligibleReason", 1, 1);
histogram_tester.ExpectUniqueSample(
"NewTabPage.HistoryClusters.HasClusterToShow", false, 1);
histogram_tester.ExpectUniqueSample(
"NewTabPage.HistoryClusters.NumClusterCandidates", 0, 1);
}
} // namespace