history_clusters_util_unittest.cc 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364
  1. // Copyright 2022 The Chromium Authors. All rights reserved.
  2. // Use of this source code is governed by a BSD-style license that can be
  3. // found in the LICENSE file.
  4. #include "components/history_clusters/core/history_clusters_util.h"
  5. #include "base/strings/stringprintf.h"
  6. #include "components/history_clusters/core/config.h"
  7. #include "components/history_clusters/core/history_clusters_service_test_api.h"
  8. #include "testing/gmock/include/gmock/gmock.h"
  9. #include "testing/gtest/include/gtest/gtest.h"
  10. namespace history_clusters {
  11. namespace {
  12. TEST(HistoryClustersUtilTest, ComputeURLForDeduping) {
  13. EXPECT_EQ(ComputeURLForDeduping(GURL("https://www.google.com/")),
  14. "https://google.com/")
  15. << "Strip off WWW.";
  16. EXPECT_EQ(ComputeURLForDeduping(GURL("http://google.com/")),
  17. "https://google.com/")
  18. << "Normalizes scheme to https.";
  19. EXPECT_EQ(
  20. ComputeURLForDeduping(GURL("https://google.com/path?foo=bar#reftag")),
  21. "https://google.com/path")
  22. << "Strips ref and query, leaves path.";
  23. EXPECT_EQ(
  24. ComputeURLForDeduping(GURL("http://www.google.com/path?foo=bar#reftag")),
  25. "https://google.com/path")
  26. << "Does all of the above at once.";
  27. EXPECT_EQ(ComputeURLForDeduping(GURL("https://google.com/path")),
  28. "https://google.com/path")
  29. << "Sanity check when no replacements needed.";
  30. }
  31. TEST(HistoryClustersUtilTest, ComputeURLKeywordForLookup) {
  32. EXPECT_EQ(ComputeURLKeywordForLookup(GURL("http://www.google.com/")),
  33. "http://google.com/")
  34. << "Strip off WWW.";
  35. EXPECT_EQ(ComputeURLKeywordForLookup(GURL("https://google.com/")),
  36. "http://google.com/")
  37. << "Normalizes scheme to http.";
  38. EXPECT_EQ(
  39. ComputeURLKeywordForLookup(GURL("http://google.com/path?foo=bar#reftag")),
  40. "http://google.com/path")
  41. << "Strips ref and query, leaves path.";
  42. EXPECT_EQ(ComputeURLKeywordForLookup(
  43. GURL("https://www.google.com/path?foo=bar#reftag")),
  44. "http://google.com/path")
  45. << "Does all of the above at once.";
  46. EXPECT_EQ(ComputeURLKeywordForLookup(GURL("http://google.com/path")),
  47. "http://google.com/path")
  48. << "Sanity check when no replacements needed.";
  49. }
  50. TEST(HistoryClustersUtilTest, FilterClustersMatchingQuery) {
  51. std::vector<history::Cluster> all_clusters;
  52. all_clusters.push_back(
  53. history::Cluster(1,
  54. {
  55. GetHardcodedClusterVisit(2),
  56. GetHardcodedClusterVisit(1),
  57. },
  58. {{u"apples", history::ClusterKeywordData()},
  59. {u"Red Oranges", history::ClusterKeywordData()}},
  60. /*should_show_on_prominent_ui_surfaces=*/false,
  61. /*label=*/u"LabelOne"));
  62. all_clusters.push_back(
  63. history::Cluster(2,
  64. {
  65. GetHardcodedClusterVisit(2),
  66. },
  67. {},
  68. /*should_show_on_prominent_ui_surfaces=*/true,
  69. /*label=*/u"LabelTwo"));
  70. struct TestData {
  71. std::string query;
  72. const bool expect_first_cluster;
  73. const bool expect_second_cluster;
  74. } test_data[] = {
  75. // Empty query should get both clusters, even the non-prominent one,
  76. // because this function only filters for query, and ignores whether the
  77. // cluster is prominent or not.
  78. {"", true, true},
  79. // Non matching query should get none.
  80. {"non_matching_query", false, false},
  81. // Query matching one cluster.
  82. {"oran", true, false},
  83. // This verifies the memory doesn't flicker away as the user is typing
  84. // out: "red oran" one key at a time. Also tests out multi-term queries.
  85. {"red", true, false},
  86. {"red ", true, false},
  87. {"red o", true, false},
  88. {"red or", true, false},
  89. {"red ora", true, false},
  90. {"red oran", true, false},
  91. // Verify that we can search by URL.
  92. {"goog", true, false},
  93. // Verify we can search by page title, even mismatching case.
  94. {"code", true, true},
  95. // Verify that we match if the query spans the title and URL of a single
  96. // visit.
  97. {"goog search", true, false},
  98. // Verify that we DON'T match if the query spans the title and URL of a
  99. // multiple visits.
  100. {"goog code", false, false},
  101. // Verify that we DON'T match if the query spans both the visit and
  102. // keywords.
  103. {"goog red", false, false},
  104. // Verify that we can find clusters by label.
  105. {"labeltwo", false, true},
  106. };
  107. for (size_t i = 0; i < std::size(test_data); ++i) {
  108. SCOPED_TRACE(base::StringPrintf("Testing case i=%d, query=%s",
  109. static_cast<int>(i),
  110. test_data[i].query.c_str()));
  111. auto clusters = all_clusters;
  112. ApplySearchQuery(test_data[i].query, clusters);
  113. size_t expected_size =
  114. static_cast<size_t>(test_data[i].expect_first_cluster) +
  115. static_cast<size_t>(test_data[i].expect_second_cluster);
  116. ASSERT_EQ(clusters.size(), expected_size);
  117. if (test_data[i].expect_first_cluster) {
  118. EXPECT_EQ(clusters[0].cluster_id, 1);
  119. }
  120. if (test_data[i].expect_second_cluster) {
  121. const auto& cluster =
  122. test_data[i].expect_first_cluster ? clusters[1] : clusters[0];
  123. EXPECT_EQ(cluster.cluster_id, 2);
  124. }
  125. }
  126. }
  127. TEST(HistoryClustersUtilTest, PromoteMatchingVisitsAboveNonMatchingVisits) {
  128. std::vector<history::Cluster> all_clusters;
  129. all_clusters.push_back(
  130. history::Cluster(0,
  131. {
  132. GetHardcodedClusterVisit(1),
  133. GetHardcodedClusterVisit(2),
  134. },
  135. {{u"apples", history::ClusterKeywordData()},
  136. {u"Red Oranges", history::ClusterKeywordData()}},
  137. /*should_show_on_prominent_ui_surfaces=*/false));
  138. // No promotion when we match a keyword.
  139. {
  140. std::vector clusters = all_clusters;
  141. ApplySearchQuery("apples", clusters);
  142. ASSERT_EQ(clusters.size(), 1U);
  143. ASSERT_EQ(clusters[0].visits.size(), 2U);
  144. EXPECT_EQ(clusters[0].visits[0].annotated_visit.visit_row.visit_id, 1);
  145. EXPECT_FLOAT_EQ(clusters[0].visits[0].score, 0.5);
  146. EXPECT_EQ(clusters[0].visits[1].annotated_visit.visit_row.visit_id, 2);
  147. EXPECT_FLOAT_EQ(clusters[0].visits[1].score, 0.5);
  148. }
  149. // Promote the second visit over the first if we match the second visit.
  150. {
  151. std::vector clusters = all_clusters;
  152. ApplySearchQuery("git", clusters);
  153. ASSERT_EQ(clusters.size(), 1U);
  154. ASSERT_EQ(clusters[0].visits.size(), 2U);
  155. EXPECT_EQ(clusters[0].visits[0].annotated_visit.visit_row.visit_id, 2);
  156. EXPECT_FLOAT_EQ(clusters[0].visits[0].score, 0.75);
  157. EXPECT_EQ(clusters[0].visits[1].annotated_visit.visit_row.visit_id, 1);
  158. EXPECT_FLOAT_EQ(clusters[0].visits[1].score, 0.25);
  159. }
  160. }
  161. TEST(HistoryClustersUtilTest, SortClustersWithinBatchForQuery) {
  162. std::vector<history::Cluster> all_clusters;
  163. all_clusters.push_back(
  164. history::Cluster(1,
  165. {
  166. GetHardcodedClusterVisit(1),
  167. GetHardcodedClusterVisit(2),
  168. },
  169. {{u"apples", history::ClusterKeywordData()},
  170. {u"Red Oranges", history::ClusterKeywordData()}},
  171. /*should_show_on_prominent_ui_surfaces=*/false));
  172. all_clusters.push_back(
  173. history::Cluster(2,
  174. {
  175. GetHardcodedClusterVisit(1),
  176. },
  177. {{u"search", history::ClusterKeywordData()}},
  178. /*should_show_on_prominent_ui_surfaces=*/false));
  179. // When the flag is off, leave the initial ordering alone.
  180. {
  181. Config config;
  182. config.sort_clusters_within_batch_for_query = false;
  183. SetConfigForTesting(config);
  184. std::vector clusters = all_clusters;
  185. ApplySearchQuery("search", clusters);
  186. ASSERT_EQ(clusters.size(), 2U);
  187. EXPECT_EQ(clusters[0].cluster_id, 1);
  188. EXPECT_EQ(clusters[1].cluster_id, 2);
  189. EXPECT_FLOAT_EQ(clusters[0].search_match_score, 0.5);
  190. EXPECT_FLOAT_EQ(clusters[1].search_match_score, 3.5);
  191. }
  192. // When the flag is on, second cluster should be sorted above the first one,
  193. // because the second one has a match on both the keyword and visit.
  194. {
  195. Config config;
  196. config.sort_clusters_within_batch_for_query = true;
  197. SetConfigForTesting(config);
  198. std::vector clusters = all_clusters;
  199. ApplySearchQuery("search", clusters);
  200. ASSERT_EQ(clusters.size(), 2U);
  201. EXPECT_EQ(clusters[0].cluster_id, 2);
  202. EXPECT_EQ(clusters[1].cluster_id, 1);
  203. EXPECT_FLOAT_EQ(clusters[0].search_match_score, 3.5);
  204. EXPECT_FLOAT_EQ(clusters[1].search_match_score, 0.5);
  205. }
  206. // With flag on, if both scores are equal, the ordering should be preserved.
  207. {
  208. Config config;
  209. config.sort_clusters_within_batch_for_query = true;
  210. SetConfigForTesting(config);
  211. std::vector clusters = all_clusters;
  212. ApplySearchQuery("google", clusters);
  213. ASSERT_EQ(clusters.size(), 2U);
  214. EXPECT_EQ(clusters[0].cluster_id, 1);
  215. EXPECT_EQ(clusters[1].cluster_id, 2);
  216. EXPECT_FLOAT_EQ(clusters[0].search_match_score, 0.5);
  217. EXPECT_FLOAT_EQ(clusters[1].search_match_score, 0.5);
  218. }
  219. }
  220. TEST(HistoryClustersUtilTest, HideAndCullLowScoringVisits) {
  221. std::vector<history::Cluster> all_clusters;
  222. // High scoring visits should always be above the fold.
  223. history::Cluster cluster1;
  224. cluster1.cluster_id = 4;
  225. cluster1.visits.push_back(GetHardcodedClusterVisit(1, 1));
  226. cluster1.visits.push_back(GetHardcodedClusterVisit(1, .8));
  227. cluster1.visits.push_back(GetHardcodedClusterVisit(1, .5));
  228. cluster1.visits.push_back(GetHardcodedClusterVisit(1, .5));
  229. cluster1.visits.push_back(GetHardcodedClusterVisit(1, .5));
  230. cluster1.keyword_to_data_map = {{u"keyword", history::ClusterKeywordData()}};
  231. // Low scoring visits should be above the fold only if they're one of top 4.
  232. history::Cluster cluster2;
  233. cluster2.cluster_id = 6;
  234. cluster2.visits.push_back(GetHardcodedClusterVisit(1, .4));
  235. cluster2.visits.push_back(GetHardcodedClusterVisit(1, .4));
  236. cluster2.visits.push_back(GetHardcodedClusterVisit(1, .4));
  237. cluster2.visits.push_back(GetHardcodedClusterVisit(1, .4));
  238. cluster2.visits.push_back(GetHardcodedClusterVisit(1, .4));
  239. cluster2.keyword_to_data_map = {{u"keyword", history::ClusterKeywordData()}};
  240. // 0 scoring visits should be above the fold only if they're 1st.
  241. history::Cluster cluster3;
  242. cluster3.cluster_id = 8;
  243. cluster3.visits.push_back(GetHardcodedClusterVisit(1, 0.0));
  244. cluster3.visits.push_back(GetHardcodedClusterVisit(1, 0.0));
  245. cluster3.keyword_to_data_map = {{u"keyword", history::ClusterKeywordData()}};
  246. all_clusters.push_back(cluster1);
  247. all_clusters.push_back(cluster2);
  248. all_clusters.push_back(cluster3);
  249. {
  250. Config config;
  251. config.drop_hidden_visits = true;
  252. SetConfigForTesting(config);
  253. auto clusters = all_clusters;
  254. HideAndCullLowScoringVisits(clusters);
  255. EXPECT_EQ(clusters[0].cluster_id, 4);
  256. auto& visits = clusters[0].visits;
  257. ASSERT_EQ(visits.size(), 5u);
  258. EXPECT_EQ(visits[0].hidden, false);
  259. EXPECT_EQ(visits[1].hidden, false);
  260. EXPECT_EQ(visits[2].hidden, false);
  261. EXPECT_EQ(visits[3].hidden, false);
  262. EXPECT_EQ(visits[4].hidden, false);
  263. EXPECT_EQ(clusters[1].cluster_id, 6);
  264. visits = clusters[1].visits;
  265. ASSERT_EQ(visits.size(), 4u);
  266. EXPECT_EQ(visits[0].hidden, false);
  267. EXPECT_EQ(visits[1].hidden, false);
  268. EXPECT_EQ(visits[2].hidden, false);
  269. EXPECT_EQ(visits[3].hidden, false);
  270. EXPECT_EQ(clusters[2].cluster_id, 8);
  271. ASSERT_EQ(clusters[2].visits.size(), 1u);
  272. EXPECT_EQ(clusters[2].visits[0].hidden, false);
  273. }
  274. {
  275. Config config;
  276. config.drop_hidden_visits = false;
  277. SetConfigForTesting(config);
  278. auto clusters = all_clusters;
  279. HideAndCullLowScoringVisits(clusters);
  280. EXPECT_EQ(clusters[0].cluster_id, 4);
  281. EXPECT_EQ(clusters[0].visits.size(), 5u);
  282. EXPECT_EQ(clusters[1].cluster_id, 6);
  283. const auto& visits = clusters[1].visits;
  284. ASSERT_EQ(visits.size(), 5u);
  285. EXPECT_EQ(visits[0].hidden, false);
  286. EXPECT_EQ(visits[1].hidden, false);
  287. EXPECT_EQ(visits[2].hidden, false);
  288. EXPECT_EQ(visits[3].hidden, false);
  289. EXPECT_EQ(visits[4].hidden, true);
  290. EXPECT_EQ(clusters[2].cluster_id, 8);
  291. ASSERT_EQ(clusters[2].visits.size(), 2u);
  292. EXPECT_EQ(clusters[2].visits[0].hidden, false);
  293. EXPECT_EQ(clusters[2].visits[1].hidden, true);
  294. }
  295. }
  296. TEST(HistoryClustersUtilTest, CoalesceRelatedSearches) {
  297. // canonical_visit has the same URL as Visit1.
  298. history::ClusterVisit visit1 = GetHardcodedClusterVisit(1);
  299. visit1.annotated_visit.content_annotations.related_searches.push_back(
  300. "search1");
  301. visit1.annotated_visit.content_annotations.related_searches.push_back(
  302. "search2");
  303. visit1.annotated_visit.content_annotations.related_searches.push_back(
  304. "search3");
  305. history::ClusterVisit visit2 = GetHardcodedClusterVisit(2);
  306. visit2.annotated_visit.content_annotations.related_searches.push_back(
  307. "search4");
  308. visit2.annotated_visit.content_annotations.related_searches.push_back(
  309. "search5");
  310. visit2.annotated_visit.content_annotations.related_searches.push_back(
  311. "search6");
  312. history::Cluster cluster;
  313. cluster.visits = {visit1, visit2};
  314. std::vector<history::Cluster> clusters;
  315. clusters.push_back(cluster);
  316. CoalesceRelatedSearches(clusters);
  317. EXPECT_THAT(clusters[0].related_searches,
  318. testing::ElementsAre("search1", "search2", "search3", "search4",
  319. "search5"));
  320. }
  321. } // namespace
  322. } // namespace history_clusters