blob: d9a18a0dc6c6698039203934d70189da92a1eec6 [file] [log] [blame]
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/navigation_predictor/navigation_predictor.h"
#include <map>
#include <memory>
#include <string>
#include <utility>
#include "base/run_loop.h"
#include "base/strings/string_number_conversions.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/scoped_feature_list.h"
#include "chrome/test/base/chrome_render_view_host_test_harness.h"
#include "components/ukm/test_ukm_recorder.h"
#include "content/public/browser/web_contents.h"
#include "mojo/public/cpp/bindings/pending_receiver.h"
#include "mojo/public/cpp/bindings/receiver.h"
#include "mojo/public/cpp/bindings/remote.h"
#include "services/metrics/public/cpp/ukm_builders.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/public/common/features.h"
#include "third_party/blink/public/mojom/loader/navigation_predictor.mojom.h"
#include "url/gurl.h"
namespace {
class TestNavigationPredictor : public NavigationPredictor {
public:
TestNavigationPredictor(
mojo::PendingReceiver<AnchorElementMetricsHost> receiver,
content::WebContents* web_contents,
bool init_feature_list)
: NavigationPredictor(web_contents),
receiver_(this, std::move(receiver)) {
if (init_feature_list) {
const std::vector<base::Feature> features = {
blink::features::kNavigationPredictor};
feature_list_.InitWithFeatures(features, {});
}
}
~TestNavigationPredictor() override {}
const std::map<GURL, int>& GetAreaRankMap() const { return area_rank_map_; }
int calls_to_prefetch() const { return calls_to_prefetch_; }
base::Optional<GURL> prefetched_url() const { return prefetched_url_; }
private:
double CalculateAnchorNavigationScore(
const blink::mojom::AnchorElementMetrics& metrics,
int area_rank) const override {
area_rank_map_.emplace(metrics.target_url, area_rank);
return 100 * metrics.ratio_area;
}
// Blackholes the first prerender.
void Prefetch(prerender::PrerenderManager* prerender_manager,
const GURL& url_to_prefetch) override {
prefetched_url_ = url_to_prefetch;
calls_to_prefetch_ += 1;
}
// Maps from target URL to area rank of the anchor element.
mutable std::map<GURL, int> area_rank_map_;
base::test::ScopedFeatureList feature_list_;
// Used to bind Mojo interface
mojo::Receiver<AnchorElementMetricsHost> receiver_;
int calls_to_prefetch_ = 0;
base::Optional<GURL> prefetched_url_;
};
class TestNavigationPredictorBasedOnScroll : public TestNavigationPredictor {
public:
TestNavigationPredictorBasedOnScroll(
mojo::PendingReceiver<AnchorElementMetricsHost> receiver,
content::WebContents* web_contents,
bool init_feature_list)
: TestNavigationPredictor(std::move(receiver),
web_contents,
init_feature_list) {}
~TestNavigationPredictorBasedOnScroll() override {}
// Override CalculateAnchorNavigationScore so the only metric that has
// any weight is the ratio distance to the top of the document.
double CalculateAnchorNavigationScore(
const blink::mojom::AnchorElementMetrics& metrics,
int area_rank) const override {
return metrics.ratio_distance_root_top;
}
};
class NavigationPredictorTest : public ChromeRenderViewHostTestHarness {
public:
NavigationPredictorTest() = default;
~NavigationPredictorTest() override = default;
// Helper function to generate mojom metrics.
blink::mojom::AnchorElementMetricsPtr CreateMetricsPtr(
const std::string& source_url,
const std::string& target_url,
float ratio_area) const {
auto metrics = blink::mojom::AnchorElementMetrics::New();
metrics->source_url = GURL(source_url);
metrics->target_url = GURL(target_url);
metrics->ratio_area = ratio_area;
return metrics;
}
gfx::Size GetDefaultViewport() { return gfx::Size(600, 800); }
blink::mojom::AnchorElementMetricsHost* predictor_service() const {
return predictor_service_.get();
}
TestNavigationPredictor* predictor_service_helper() const {
return predictor_service_helper_.get();
}
base::Optional<GURL> prefetch_url() const {
return predictor_service_helper_->prefetched_url();
}
bool prefetch_url_prefetched() {
return predictor_service_helper_->prefetched_url().has_value();
}
protected:
void SetUp() override {
ChromeRenderViewHostTestHarness::SetUp();
predictor_service_helper_ = std::make_unique<TestNavigationPredictor>(
predictor_service_.BindNewPipeAndPassReceiver(), web_contents(),
!field_trial_initiated_);
}
void SetupFieldTrial(base::Optional<int> prefetch_url_score_threshold,
base::Optional<bool> prefetch_after_preconnect) {
if (field_trial_initiated_)
return;
field_trial_initiated_ = true;
const std::string kTrialName = "TrialFoo2";
const std::string kGroupName = "GroupFoo2"; // Value not used
std::map<std::string, std::string> params;
if (prefetch_url_score_threshold.has_value()) {
params["prefetch_url_score_threshold"] =
base::NumberToString(prefetch_url_score_threshold.value());
}
if (prefetch_after_preconnect.has_value()) {
params["prefetch_after_preconnect"] =
prefetch_after_preconnect.value() ? "true" : "false";
}
scoped_feature_list.InitAndEnableFeatureWithParameters(
blink::features::kNavigationPredictor, params);
}
mojo::Remote<blink::mojom::AnchorElementMetricsHost> predictor_service_;
std::unique_ptr<TestNavigationPredictor> predictor_service_helper_;
private:
base::test::ScopedFeatureList scoped_feature_list;
bool field_trial_initiated_ = false;
};
} // namespace
// Basic test to check the ReportAnchorElementMetricsOnClick method can be
// called.
TEST_F(NavigationPredictorTest, ReportAnchorElementMetricsOnClick) {
base::HistogramTester histogram_tester;
auto metrics =
CreateMetricsPtr("https://example.com", "https://google.com", 0.1);
predictor_service()->ReportAnchorElementMetricsOnClick(std::move(metrics));
base::RunLoop().RunUntilIdle();
histogram_tester.ExpectUniqueSample(
"NavigationPredictor.LinkClickedPrerenderResult",
NavigationPredictor::PrerenderResult::kCrossOriginNotSeen, 1);
}
// Test that ReportAnchorElementMetricsOnLoad method can be called.
TEST_F(NavigationPredictorTest, ReportAnchorElementMetricsOnLoad) {
base::HistogramTester histogram_tester;
auto metrics =
CreateMetricsPtr("https://example.com", "https://google.com", 0.1);
std::vector<blink::mojom::AnchorElementMetricsPtr> metrics_vector;
metrics_vector.push_back(std::move(metrics));
predictor_service()->ReportAnchorElementMetricsOnLoad(
std::move(metrics_vector), GetDefaultViewport());
base::RunLoop().RunUntilIdle();
histogram_tester.ExpectTotalCount(
"AnchorElementMetrics.Visible.NumberOfAnchorElementsAfterMerge", 1);
}
// Test that if source/target url is not http or https, no score will be
// calculated.
TEST_F(NavigationPredictorTest,
BadUrlReportAnchorElementMetricsOnClick_ftp_src) {
base::HistogramTester histogram_tester;
auto metrics =
CreateMetricsPtr("ftp://example.com", "https://google.com", 0.1);
predictor_service()->ReportAnchorElementMetricsOnClick(std::move(metrics));
base::RunLoop().RunUntilIdle();
histogram_tester.ExpectTotalCount(
"AnchorElementMetrics.Visible.NumberOfAnchorElementsAfterMerge", 0);
}
// Test that if source/target url is not http or https, no navigation score will
// be calculated.
TEST_F(NavigationPredictorTest,
BadUrlReportAnchorElementMetricsOnLoad_ftp_target) {
base::HistogramTester histogram_tester;
auto metrics =
CreateMetricsPtr("https://example.com", "ftp://google.com", 0.1);
std::vector<blink::mojom::AnchorElementMetricsPtr> metrics_vector;
metrics_vector.push_back(std::move(metrics));
predictor_service()->ReportAnchorElementMetricsOnLoad(
std::move(metrics_vector), GetDefaultViewport());
base::RunLoop().RunUntilIdle();
histogram_tester.ExpectTotalCount(
"AnchorElementMetrics.Visible.NumberOfAnchorElementsAfterMerge", 0);
}
// Test that if the target url is not https, no navigation score will
// be calculated.
TEST_F(NavigationPredictorTest,
BadUrlReportAnchorElementMetricsOnLoad_http_target) {
base::HistogramTester histogram_tester;
auto metrics =
CreateMetricsPtr("https://example.com", "http://google.com", 0.1);
std::vector<blink::mojom::AnchorElementMetricsPtr> metrics_vector;
metrics_vector.push_back(std::move(metrics));
predictor_service()->ReportAnchorElementMetricsOnLoad(
std::move(metrics_vector), GetDefaultViewport());
base::RunLoop().RunUntilIdle();
histogram_tester.ExpectTotalCount(
"AnchorElementMetrics.Visible.NumberOfAnchorElementsAfterMerge", 0);
}
// Test that if the source url is not https, no navigation score will
// be calculated.
TEST_F(NavigationPredictorTest,
BadUrlReportAnchorElementMetricsOnLoad_http_src) {
base::HistogramTester histogram_tester;
auto metrics =
CreateMetricsPtr("http://example.com", "https://google.com", 0.1);
std::vector<blink::mojom::AnchorElementMetricsPtr> metrics_vector;
metrics_vector.push_back(std::move(metrics));
predictor_service()->ReportAnchorElementMetricsOnLoad(
std::move(metrics_vector), GetDefaultViewport());
base::RunLoop().RunUntilIdle();
histogram_tester.ExpectTotalCount(
"AnchorElementMetrics.Visible.NumberOfAnchorElementsAfterMerge", 0);
}
TEST_F(NavigationPredictorTest, Merge_UniqueAnchorElements) {
base::HistogramTester histogram_tester;
const std::string source = "https://example.com";
const std::string href_xlarge = "https://example.com/xlarge";
const std::string href_large = "https://google.com/large";
const std::string href_medium = "https://google.com/medium";
const std::string href_small = "https://google.com/small";
const std::string href_xsmall = "https://google.com/xsmall";
std::vector<blink::mojom::AnchorElementMetricsPtr> metrics;
metrics.push_back(CreateMetricsPtr(source, href_xsmall, 0.01));
metrics.push_back(CreateMetricsPtr(source, href_large, 0.01));
metrics.push_back(CreateMetricsPtr(source, href_xlarge, 0.01));
metrics.push_back(CreateMetricsPtr(source, href_small, 0.01));
metrics.push_back(CreateMetricsPtr(source, href_medium, 0.01));
int number_of_metrics_sent = metrics.size();
predictor_service()->ReportAnchorElementMetricsOnLoad(std::move(metrics),
GetDefaultViewport());
base::RunLoop().RunUntilIdle();
histogram_tester.ExpectUniqueSample(
"AnchorElementMetrics.Visible.NumberOfAnchorElementsAfterMerge",
number_of_metrics_sent, 1);
}
TEST_F(NavigationPredictorTest, Merge_DuplicateAnchorElements) {
base::HistogramTester histogram_tester;
const std::string source = "https://example.com";
const std::string href = "https://example.com/xlarge";
// URLs that differ only in ref fragments should be merged.
const std::string href_ref_1 = "https://example.com/xlarge#";
const std::string href_ref_2 = "https://example.com/xlarge#ref_foo";
const std::string href_query_1 = "https://example.com/xlarge?q=foo";
const std::string href_query_ref = "https://example.com/xlarge?q=foo#ref_bar";
std::vector<blink::mojom::AnchorElementMetricsPtr> metrics;
metrics.push_back(CreateMetricsPtr(source, href, 0.01));
metrics.push_back(CreateMetricsPtr(source, href_ref_1, 0.01));
metrics.push_back(CreateMetricsPtr(source, href_ref_2, 0.01));
metrics.push_back(CreateMetricsPtr(source, href_query_1, 0.01));
metrics.push_back(CreateMetricsPtr(source, href_query_ref, 0.01));
predictor_service()->ReportAnchorElementMetricsOnLoad(std::move(metrics),
GetDefaultViewport());
base::RunLoop().RunUntilIdle();
// Only two anchor elements are unique: |href| and |href_query_1|.
histogram_tester.ExpectUniqueSample(
"AnchorElementMetrics.Visible.NumberOfAnchorElementsAfterMerge", 2, 1);
}
TEST_F(NavigationPredictorTest, Merge_AnchorElementSameAsDocumentURL) {
base::HistogramTester histogram_tester;
const std::string source = "https://example.com/";
const std::string href = "https://example.com/";
// URLs that differ only in ref fragments should be merged.
const std::string href_ref_1 = "https://example.com/#ref=foo";
const std::string href_ref_2 = "https://example.com/xlarge#ref=foo";
const std::string href_query_1 = "https://example.com/xlarge?q=foo";
const std::string href_query_ref = "https://example.com/xlarge?q=foo#ref_bar";
std::vector<blink::mojom::AnchorElementMetricsPtr> metrics;
metrics.push_back(CreateMetricsPtr(source, href, 0.01));
metrics.push_back(CreateMetricsPtr(source, href_ref_1, 0.01));
metrics.push_back(CreateMetricsPtr(source, href_ref_2, 0.01));
metrics.push_back(CreateMetricsPtr(source, href_query_1, 0.01));
metrics.push_back(CreateMetricsPtr(source, href_query_ref, 0.01));
predictor_service()->ReportAnchorElementMetricsOnLoad(std::move(metrics),
GetDefaultViewport());
base::RunLoop().RunUntilIdle();
// Only two anchor elements are unique and different from the document URL:
// |href_ref_2| and |href_query_1|.
histogram_tester.ExpectUniqueSample(
"AnchorElementMetrics.Visible.NumberOfAnchorElementsAfterMerge", 2, 1);
}
// In this test, multiple anchor element metrics are sent to
// ReportAnchorElementMetricsOnLoad. Test that CalculateAnchorNavigationScore
// works.
TEST_F(NavigationPredictorTest, MultipleAnchorElementMetricsOnLoad) {
base::HistogramTester histogram_tester;
const std::string source = "https://example.com";
const std::string href_xlarge = "https://example.com/xlarge";
const std::string href_large = "https://google.com/large";
const std::string href_medium = "https://google.com/medium";
const std::string href_small = "https://google.com/small";
const std::string href_xsmall = "https://google.com/xsmall";
const std::string http_href_xsmall = "http://google.com/xsmall";
std::vector<blink::mojom::AnchorElementMetricsPtr> metrics;
metrics.push_back(CreateMetricsPtr(source, href_xsmall, 0.01));
metrics.push_back(CreateMetricsPtr(source, http_href_xsmall, 0.01));
metrics.push_back(CreateMetricsPtr(source, href_large, 0.08));
metrics.push_back(CreateMetricsPtr(source, href_xlarge, 0.1));
metrics.push_back(CreateMetricsPtr(source, href_small, 0.02));
metrics.push_back(CreateMetricsPtr(source, href_medium, 0.05));
int number_of_metrics_sent = metrics.size();
predictor_service()->ReportAnchorElementMetricsOnLoad(std::move(metrics),
GetDefaultViewport());
base::RunLoop().RunUntilIdle();
const std::map<GURL, int>& area_rank_map =
predictor_service_helper()->GetAreaRankMap();
// Exclude the http anchor element from |number_of_metrics_sent|.
EXPECT_EQ(number_of_metrics_sent - 1, static_cast<int>(area_rank_map.size()));
EXPECT_EQ(0, area_rank_map.find(GURL(href_xlarge))->second);
EXPECT_EQ(1, area_rank_map.find(GURL(href_large))->second);
EXPECT_EQ(2, area_rank_map.find(GURL(href_medium))->second);
EXPECT_EQ(3, area_rank_map.find(GURL(href_small))->second);
EXPECT_EQ(4, area_rank_map.find(GURL(href_xsmall))->second);
EXPECT_EQ(area_rank_map.end(), area_rank_map.find(GURL(http_href_xsmall)));
}
// URL with highest prefetch score is from the same origin. Prefetch is done.
TEST_F(NavigationPredictorTest, ActionTaken_SameOrigin_Prefetch) {
const std::string source = "https://example.com";
const std::string same_origin_href_small = "https://example.com/small";
const std::string same_origin_href_large = "https://example.com/large";
const std::string diff_origin_href_xsmall = "https://example2.com/xsmall";
std::vector<blink::mojom::AnchorElementMetricsPtr> metrics;
metrics.push_back(CreateMetricsPtr(source, same_origin_href_large, 1));
metrics.push_back(CreateMetricsPtr(source, same_origin_href_small, 0.01));
metrics.push_back(CreateMetricsPtr(source, diff_origin_href_xsmall, 0.01));
base::HistogramTester histogram_tester;
predictor_service()->ReportAnchorElementMetricsOnLoad(std::move(metrics),
GetDefaultViewport());
base::RunLoop().RunUntilIdle();
histogram_tester.ExpectUniqueSample(
"NavigationPredictor.OnNonDSE.ActionTaken",
NavigationPredictor::Action::kPrefetch, 1);
auto metrics_clicked = CreateMetricsPtr(source, same_origin_href_small, 0.01);
predictor_service()->ReportAnchorElementMetricsOnClick(
std::move(metrics_clicked));
base::RunLoop().RunUntilIdle();
// Override Prefetch blackholes the request, so expect is reported as skipped.
histogram_tester.ExpectUniqueSample(
"NavigationPredictor.LinkClickedPrerenderResult",
NavigationPredictor::PrerenderResult::kSameOriginPrefetchSkipped, 1);
}
// URL with highest prefetch score is from a different origin. So, prefetch is
// not done, only preconnect.
TEST_F(NavigationPredictorTest, ActionTaken_SameOrigin_Prefetch_NotSameOrigin) {
const std::string source = "https://example.com";
const std::string same_origin_href_small = "https://example.com/small";
const std::string same_origin_href_large = "https://example.com/large";
const std::string diff_origin_href_xlarge = "https://example2.com/xlarge";
std::vector<blink::mojom::AnchorElementMetricsPtr> metrics;
metrics.push_back(CreateMetricsPtr(source, same_origin_href_large, 1));
metrics.push_back(CreateMetricsPtr(source, same_origin_href_small, 0.01));
metrics.push_back(CreateMetricsPtr(source, diff_origin_href_xlarge, 10));
base::HistogramTester histogram_tester;
predictor_service()->ReportAnchorElementMetricsOnLoad(std::move(metrics),
GetDefaultViewport());
base::RunLoop().RunUntilIdle();
histogram_tester.ExpectUniqueSample(
"NavigationPredictor.OnNonDSE.ActionTaken",
NavigationPredictor::Action::kNone, 1);
EXPECT_FALSE(prefetch_url().has_value());
auto metrics_clicked = CreateMetricsPtr(source, same_origin_href_small, 0.01);
predictor_service()->ReportAnchorElementMetricsOnClick(
std::move(metrics_clicked));
base::RunLoop().RunUntilIdle();
}
TEST_F(NavigationPredictorTest,
ActionTaken_SameOrigin_DifferentScheme_Prefetch) {
const std::string source = "https://example.com";
const std::string same_origin_href_small = "http://example.com/small";
const std::string diff_origin_href_xlarge = "https://example2.com/xlarge";
std::vector<blink::mojom::AnchorElementMetricsPtr> metrics;
metrics.push_back(CreateMetricsPtr(source, same_origin_href_small, 0.01));
metrics.push_back(CreateMetricsPtr(source, diff_origin_href_xlarge, 1));
base::HistogramTester histogram_tester;
predictor_service()->ReportAnchorElementMetricsOnLoad(std::move(metrics),
GetDefaultViewport());
base::RunLoop().RunUntilIdle();
histogram_tester.ExpectUniqueSample(
"NavigationPredictor.OnNonDSE.ActionTaken",
NavigationPredictor::Action::kNone, 1);
EXPECT_FALSE(prefetch_url().has_value());
}
class NavigationPredictorSendUkmMetricsEnabledTest
: public NavigationPredictorTest {
public:
NavigationPredictorSendUkmMetricsEnabledTest() {
SetupFieldTrial(base::nullopt, base::nullopt);
}
void SetUp() override {
ChromeRenderViewHostTestHarness::SetUp();
predictor_service_helper_ = std::make_unique<TestNavigationPredictor>(
predictor_service_.BindNewPipeAndPassReceiver(), web_contents(), false);
}
struct TestMetrics {
float ratio_area;
float ratio_distance_root_top;
bool is_in_iframe;
bool is_url_incremented_by_one;
bool contains_image;
bool is_same_host;
};
// Helper function to generate mojom metrics.
blink::mojom::AnchorElementMetricsPtr CreateMetricsPtrWithAllMetrics(
const std::string& source_url,
const std::string& target_url,
const struct TestMetrics metrics_vector) const {
auto metrics = blink::mojom::AnchorElementMetrics::New();
metrics->source_url = GURL(source_url);
metrics->target_url = GURL(target_url);
metrics->ratio_area = metrics_vector.ratio_area / 100;
metrics->ratio_distance_root_top =
metrics_vector.ratio_distance_root_top / 100;
metrics->is_in_iframe = metrics_vector.is_in_iframe;
metrics->is_url_incremented_by_one =
metrics_vector.is_url_incremented_by_one;
metrics->contains_image = metrics_vector.contains_image;
metrics->is_same_host = metrics_vector.is_same_host;
return metrics;
}
std::vector<int> GetUKMMetricsAsVector(
int ukm_entry_index,
ukm::TestAutoSetUkmRecorder& ukm_recorder) {
using UkmEntry = ukm::builders::NavigationPredictorAnchorElementMetrics;
auto* entry =
ukm_recorder.GetEntriesByName(UkmEntry::kEntryName)[ukm_entry_index];
std::vector<int> metrics = {
*ukm_recorder.GetEntryMetric(entry,
UkmEntry::kPercentClickableAreaName),
*ukm_recorder.GetEntryMetric(entry,
UkmEntry::kPercentVerticalDistanceName),
*ukm_recorder.GetEntryMetric(entry, UkmEntry::kIsInIframeName),
*ukm_recorder.GetEntryMetric(entry,
UkmEntry::kIsURLIncrementedByOneName),
*ukm_recorder.GetEntryMetric(entry, UkmEntry::kContainsImageName),
*ukm_recorder.GetEntryMetric(entry, UkmEntry::kSameOriginName)};
return metrics;
}
int GetUKMIndex(int ukm_entry_index,
ukm::TestAutoSetUkmRecorder& ukm_recorder) {
using UkmEntry = ukm::builders::NavigationPredictorAnchorElementMetrics;
auto* entry =
ukm_recorder.GetEntriesByName(UkmEntry::kEntryName)[ukm_entry_index];
return *ukm_recorder.GetEntryMetric(entry, UkmEntry::kAnchorIndexName);
}
std::vector<int> GetTestMetricsAsVector(struct TestMetrics test_metrics) {
std::vector<int> metrics_vector = {
test_metrics.ratio_area, test_metrics.ratio_distance_root_top,
test_metrics.is_in_iframe, test_metrics.is_url_incremented_by_one,
test_metrics.contains_image, test_metrics.is_same_host};
return metrics_vector;
}
};
// Checks that per-link metrics are sent to the UKM on page load, and that
// the bits are packed correctly.
TEST_F(NavigationPredictorSendUkmMetricsEnabledTest, SendLinkUkmMetrics) {
ukm::TestAutoSetUkmRecorder test_ukm_recorder;
const std::string source = "https://example.com";
struct TestMetrics anchor_1_metrics, anchor_2_metrics, anchor_3_metrics;
anchor_1_metrics = {100.f, 10.f, false, true, true, true};
anchor_2_metrics = {50.f, 20.f, false, false, true, true};
anchor_3_metrics = {5.f, 30.f, false, true, false, false};
std::vector<blink::mojom::AnchorElementMetricsPtr> metrics;
metrics.push_back(CreateMetricsPtrWithAllMetrics(
source, "https://example.com/large", anchor_1_metrics));
metrics.push_back(CreateMetricsPtrWithAllMetrics(
source, "https://example.com/small", anchor_2_metrics));
metrics.push_back(CreateMetricsPtrWithAllMetrics(
source, "https://neworigin.com/xsmall", anchor_3_metrics));
predictor_service()->ReportAnchorElementMetricsOnLoad(std::move(metrics),
GetDefaultViewport());
base::RunLoop().RunUntilIdle();
EXPECT_EQ(4ul, test_ukm_recorder.entries_count());
std::vector<std::vector<int>> vector_actual;
vector_actual.push_back(GetUKMMetricsAsVector(0, test_ukm_recorder));
vector_actual.push_back(GetUKMMetricsAsVector(1, test_ukm_recorder));
vector_actual.push_back(GetUKMMetricsAsVector(2, test_ukm_recorder));
EXPECT_THAT(vector_actual,
::testing::Contains(GetTestMetricsAsVector(anchor_1_metrics)));
EXPECT_THAT(vector_actual,
::testing::Contains(GetTestMetricsAsVector(anchor_2_metrics)));
EXPECT_THAT(vector_actual,
::testing::Contains(GetTestMetricsAsVector(anchor_3_metrics)));
std::vector<int> vector_indeces;
vector_indeces.push_back(GetUKMIndex(0, test_ukm_recorder));
vector_indeces.push_back(GetUKMIndex(1, test_ukm_recorder));
vector_indeces.push_back(GetUKMIndex(2, test_ukm_recorder));
EXPECT_THAT(vector_indeces, ::testing::Contains(1));
EXPECT_THAT(vector_indeces, ::testing::Contains(2));
EXPECT_THAT(vector_indeces, ::testing::Contains(3));
}
// Checks that metrics about which link was clicked are sent to the ukm
// on-click, and that that index corresponds to a valid url.
TEST_F(NavigationPredictorSendUkmMetricsEnabledTest, SendClickUkmMetrics) {
using ClickUkmEntry = ukm::builders::NavigationPredictorPageLinkClick;
using LoadUkmEntry = ukm::builders::NavigationPredictorAnchorElementMetrics;
ukm::TestAutoSetUkmRecorder test_ukm_recorder;
const std::string source = "https://example1.com";
const std::string clicked_url = "https://example1.com/large";
std::vector<blink::mojom::AnchorElementMetricsPtr> metrics;
metrics.push_back(CreateMetricsPtr(source, clicked_url, 1));
metrics.push_back(CreateMetricsPtr(source, "https://example2.com/small", .5));
metrics.push_back(
CreateMetricsPtr(source, "https://neworigin.com/xsmall", .05));
predictor_service()->ReportAnchorElementMetricsOnLoad(std::move(metrics),
GetDefaultViewport());
base::RunLoop().RunUntilIdle();
// Because NavigationPredictor randomizes the list of all anchor elements,
// retrieve the index of the element the test clicks on by looking at the
// UKM entry in NavigationPredictorAnchorElementMetrics that has the same
// ratio area.
auto all_ukm_entries =
test_ukm_recorder.GetEntriesByName(LoadUkmEntry::kEntryName);
int index = -1;
for (auto* entry : all_ukm_entries) {
int entry_ratio_area = static_cast<int>(*test_ukm_recorder.GetEntryMetric(
entry, LoadUkmEntry::kPercentClickableAreaName));
if (entry_ratio_area == 100) {
index = static_cast<int>(*test_ukm_recorder.GetEntryMetric(
entry, LoadUkmEntry::kAnchorIndexName));
break;
}
}
auto metrics_clicked = CreateMetricsPtr(source, clicked_url, 0.01);
predictor_service()->ReportAnchorElementMetricsOnClick(
std::move(metrics_clicked));
base::RunLoop().RunUntilIdle();
test_ukm_recorder.ExpectEntryMetric(
test_ukm_recorder.GetEntriesByName(ClickUkmEntry::kEntryName)[0],
ClickUkmEntry::kAnchorElementIndexName, index);
}
// Checks that per-page link aggregate information is sent to the UKM on page
// load.
TEST_F(NavigationPredictorSendUkmMetricsEnabledTest, SendAggregateUkmMetrics) {
using UkmEntry = ukm::builders::NavigationPredictorPageLinkMetrics;
ukm::TestAutoSetUkmRecorder test_ukm_recorder;
const std::string source = "https://example.com";
const std::string same_origin_href_large = "https://example.com/large";
std::vector<blink::mojom::AnchorElementMetricsPtr> metrics;
metrics.push_back(CreateMetricsPtr(source, same_origin_href_large, 1));
predictor_service()->ReportAnchorElementMetricsOnLoad(std::move(metrics),
GetDefaultViewport());
base::RunLoop().RunUntilIdle();
// There should be one ukm entry for the PageLinkMetrics aggregate
// information, and another for the AnchorElementMetrics event associated with
// the one anchor element in |metrics|.
EXPECT_EQ(2ul, test_ukm_recorder.entries_count());
ukm::TestUkmRecorder::ExpectEntryMetric(
test_ukm_recorder.GetEntriesByName(UkmEntry::kEntryName)[0],
UkmEntry::kNumberOfAnchors_TotalName, 1);
ukm::TestUkmRecorder::ExpectEntryMetric(
test_ukm_recorder.GetEntriesByName(UkmEntry::kEntryName)[0],
UkmEntry::kNumberOfAnchors_SameHostName, 0);
ukm::TestUkmRecorder::ExpectEntryMetric(
test_ukm_recorder.GetEntriesByName(UkmEntry::kEntryName)[0],
UkmEntry::kNumberOfAnchors_ContainsImageName, 0);
ukm::TestUkmRecorder::ExpectEntryMetric(
test_ukm_recorder.GetEntriesByName(UkmEntry::kEntryName)[0],
UkmEntry::kNumberOfAnchors_InIframeName, 0);
ukm::TestUkmRecorder::ExpectEntryMetric(
test_ukm_recorder.GetEntriesByName(UkmEntry::kEntryName)[0],
UkmEntry::kNumberOfAnchors_URLIncrementedName, 0);
ukm::TestUkmRecorder::EntryHasMetric(
test_ukm_recorder.GetEntriesByName(UkmEntry::kEntryName)[0],
UkmEntry::kTotalClickableSpaceName);
ukm::TestUkmRecorder::EntryHasMetric(
test_ukm_recorder.GetEntriesByName(UkmEntry::kEntryName)[0],
UkmEntry::kMedianLinkLocationName);
ukm::TestUkmRecorder::EntryHasMetric(
test_ukm_recorder.GetEntriesByName(UkmEntry::kEntryName)[0],
UkmEntry::kViewport_HeightName);
ukm::TestUkmRecorder::EntryHasMetric(
test_ukm_recorder.GetEntriesByName(UkmEntry::kEntryName)[0],
UkmEntry::kViewport_WidthName);
}
class NavigationPredictorPrefetchAfterPreconnectEnabledTest
: public NavigationPredictorTest {
public:
NavigationPredictorPrefetchAfterPreconnectEnabledTest() {
SetupFieldTrial(base::nullopt, true);
}
void SetUp() override {
ChromeRenderViewHostTestHarness::SetUp();
predictor_service_helper_ =
std::make_unique<TestNavigationPredictorBasedOnScroll>(
predictor_service_.BindNewPipeAndPassReceiver(), web_contents(),
false);
}
blink::mojom::AnchorElementMetricsPtr CreateMetricsPtrWithRatioDistance(
const std::string& source_url,
const std::string& target_url,
float ratio_area,
float ratio_distance) const {
auto metrics = blink::mojom::AnchorElementMetrics::New();
metrics->source_url = GURL(source_url);
metrics->target_url = GURL(target_url);
metrics->ratio_area = ratio_area;
metrics->ratio_distance_root_top = ratio_distance;
return metrics;
}
};
// Test that a prefetch after preconnect occurs only when the current tab is
// in the foreground, and that it does not occur multiple times for the same
// URL.
TEST_F(NavigationPredictorPrefetchAfterPreconnectEnabledTest,
PrefetchWithTabs) {
const std::string source = "https://example.com";
const std::string same_origin_href_large = "https://example.com/large";
std::vector<blink::mojom::AnchorElementMetricsPtr> metrics;
metrics.push_back(CreateMetricsPtr(source, same_origin_href_large, 1));
// Hide the tab and load the page. The URL should not be prefetched.
web_contents()->WasHidden();
predictor_service()->ReportAnchorElementMetricsOnLoad(std::move(metrics),
GetDefaultViewport());
base::RunLoop().RunUntilIdle();
EXPECT_FALSE(prefetch_url_prefetched());
EXPECT_EQ(predictor_service_helper_->calls_to_prefetch(), 0);
// Making the tab visible should start a prefetch.
web_contents()->WasShown();
EXPECT_TRUE(prefetch_url_prefetched());
EXPECT_EQ(predictor_service_helper_->calls_to_prefetch(), 1);
// Switching a tab from HIDDEN to VISIBLE should not start a prefetch
// if a prefetch already occurred for that URL.
web_contents()->WasHidden();
web_contents()->WasShown();
EXPECT_TRUE(prefetch_url_prefetched());
EXPECT_EQ(predictor_service_helper_->calls_to_prefetch(), 1);
}
// Framework for testing cases where prefetch is effectively
// disabled by setting |prefetch_url_score_threshold| to too high.
class NavigationPredictorPrefetchDisabledTest : public NavigationPredictorTest {
public:
NavigationPredictorPrefetchDisabledTest() {
SetupFieldTrial(101 /* prefetch_url_score_threshold */, base::nullopt);
}
void SetUp() override {
ChromeRenderViewHostTestHarness::SetUp();
predictor_service_helper_ = std::make_unique<TestNavigationPredictor>(
predictor_service_.BindNewPipeAndPassReceiver(), web_contents(), false);
}
};
// Disables prefetch and loads a page where the preconnect score of the document
// origin is highest among all origins. Verifies that navigation predictor
// preconnects to the document origin.
TEST_F(NavigationPredictorPrefetchDisabledTest,
ActionTaken_SameOrigin_Prefetch_BelowThreshold) {
const std::string source = "https://example.com";
const std::string same_origin_href_small = "https://example.com/small";
const std::string same_origin_href_large = "https://example.com/large";
// Cross origin anchor element is small. This should result in example.com to
// have the highest preconnect score.
const std::string diff_origin_href_xsmall = "https://example2.com/xsmall";
std::vector<blink::mojom::AnchorElementMetricsPtr> metrics;
metrics.push_back(CreateMetricsPtr(source, same_origin_href_large, 1));
metrics.push_back(CreateMetricsPtr(source, same_origin_href_small, 0.01));
metrics.push_back(CreateMetricsPtr(source, diff_origin_href_xsmall, 0.0001));
base::HistogramTester histogram_tester;
predictor_service()->ReportAnchorElementMetricsOnLoad(std::move(metrics),
GetDefaultViewport());
base::RunLoop().RunUntilIdle();
histogram_tester.ExpectUniqueSample(
"NavigationPredictor.OnNonDSE.ActionTaken",
NavigationPredictor::Action::kNone, 1);
EXPECT_FALSE(prefetch_url().has_value());
auto metrics_clicked = CreateMetricsPtr(source, same_origin_href_small, 0.01);
predictor_service()->ReportAnchorElementMetricsOnClick(
std::move(metrics_clicked));
base::RunLoop().RunUntilIdle();
}
// Disables prefetch and loads a page where the preconnect score of a cross
// origin is highest among all origins.
TEST_F(NavigationPredictorPrefetchDisabledTest,
ActionTaken_PreconnectHighScoreIsCrossOrigin) {
const std::string source = "https://example.com";
const std::string same_origin_href_small = "https://example.com/small";
const std::string same_origin_href_large = "https://example.com/large";
// Cross origin anchor element is large. This should result in example2.com to
// have the highest preconnect score.
const std::string diff_origin_href_xlarge = "https://example2.com/xlarge";
std::vector<blink::mojom::AnchorElementMetricsPtr> metrics;
metrics.push_back(CreateMetricsPtr(source, same_origin_href_large, 1));
metrics.push_back(CreateMetricsPtr(source, same_origin_href_small, 0.01));
metrics.push_back(CreateMetricsPtr(source, diff_origin_href_xlarge, 10));
base::HistogramTester histogram_tester;
predictor_service()->ReportAnchorElementMetricsOnLoad(std::move(metrics),
GetDefaultViewport());
base::RunLoop().RunUntilIdle();
histogram_tester.ExpectUniqueSample(
"NavigationPredictor.OnNonDSE.ActionTaken",
NavigationPredictor::Action::kNone, 1);
EXPECT_FALSE(prefetch_url().has_value());
auto metrics_clicked = CreateMetricsPtr(source, same_origin_href_small, 0.01);
predictor_service()->ReportAnchorElementMetricsOnClick(
std::move(metrics_clicked));
base::RunLoop().RunUntilIdle();
}
// Framework for testing cases where preconnect and prefetch are effectively
// disabled by setting their thresholds as too high.
class NavigationPredictorPreconnectPrefetchDisabledTest
: public NavigationPredictorTest {
public:
NavigationPredictorPreconnectPrefetchDisabledTest() {
SetupFieldTrial(101 /* prefetch_url_score_threshold */, base::nullopt);
}
void SetUp() override {
ChromeRenderViewHostTestHarness::SetUp();
predictor_service_helper_ = std::make_unique<TestNavigationPredictor>(
predictor_service_.BindNewPipeAndPassReceiver(), web_contents(), false);
}
};
// Only preconnect should happen when a prefetch score is below the threshold.
TEST_F(NavigationPredictorPreconnectPrefetchDisabledTest,
ActionTaken_SameOrigin_Prefetch_BelowThreshold) {
const std::string source = "https://example.com";
const std::string same_origin_href_small = "https://example.com/small";
const std::string same_origin_href_large = "https://example.com/large";
const std::string diff_origin_href_xsmall = "https://example2.com/xsmall";
std::vector<blink::mojom::AnchorElementMetricsPtr> metrics;
metrics.push_back(CreateMetricsPtr(source, same_origin_href_large, 1));
metrics.push_back(CreateMetricsPtr(source, same_origin_href_small, 0.01));
metrics.push_back(CreateMetricsPtr(source, diff_origin_href_xsmall, 0.0001));
base::HistogramTester histogram_tester;
predictor_service()->ReportAnchorElementMetricsOnLoad(std::move(metrics),
GetDefaultViewport());
base::RunLoop().RunUntilIdle();
histogram_tester.ExpectUniqueSample(
"NavigationPredictor.OnNonDSE.ActionTaken",
NavigationPredictor::Action::kNone, 1);
EXPECT_FALSE(prefetch_url().has_value());
auto metrics_clicked = CreateMetricsPtr(source, same_origin_href_small, 0.01);
predictor_service()->ReportAnchorElementMetricsOnClick(
std::move(metrics_clicked));
base::RunLoop().RunUntilIdle();
}