blob: c513b99811f4ce2d7ed0abd404f40defd1c7054c [file] [log] [blame]
// Copyright 2018 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/navigation_predictor/navigation_predictor.h"
#include <map>
#include <memory>
#include <string>
#include <utility>
#include "base/memory/raw_ptr.h"
#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/browser/page_load_metrics/observers/page_anchors_metrics_observer.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-forward.h"
#include "third_party/blink/public/mojom/loader/navigation_predictor.mojom.h"
#include "url/gurl.h"
namespace {
class NavigationPredictorTest : public ChromeRenderViewHostTestHarness {
public:
NavigationPredictorTest() = default;
~NavigationPredictorTest() override = default;
// Helper function to generate mojom metrics.
blink::mojom::AnchorElementMetricsPtr CreateMetricsPtr() {
auto metrics = blink::mojom::AnchorElementMetrics::New();
metrics->anchor_id = next_id_++;
metrics->source_url = GURL("https://example.com");
metrics->target_url = GURL("https://google.com");
metrics->ratio_area = 0.1;
return metrics;
}
gfx::Size GetDefaultViewport() { return gfx::Size(600, 800); }
blink::mojom::AnchorElementMetricsHost* predictor_service() const {
return predictor_service_.get();
}
protected:
void SetUp() override {
// To avoid tsan data race test flakes, this needs to happen before
// ChromeRenderViewHostTestHarness::SetUp causes tasks on other threads
// to check if a feature is enabled.
SetupFieldTrial();
ChromeRenderViewHostTestHarness::SetUp();
NavigationPredictor::Create(
main_rfh(), predictor_service_.BindNewPipeAndPassReceiver());
}
void SetupFieldTrial() {
if (field_trial_initiated_)
return;
field_trial_initiated_ = true;
// Report all anchors to avoid non-deterministic behavior.
std::map<std::string, std::string> params;
params["random_anchor_sampling_period"] = "1";
scoped_feature_list_.InitAndEnableFeatureWithParameters(
blink::features::kNavigationPredictor, params);
}
private:
base::test::ScopedFeatureList scoped_feature_list_;
mojo::Remote<blink::mojom::AnchorElementMetricsHost> predictor_service_;
int next_id_ = 0;
bool field_trial_initiated_ = false;
};
} // namespace
// Basic test to check the ReportNewAnchorElements method aggregates
// metric data correctly.
TEST_F(NavigationPredictorTest, ReportNewAnchorElements) {
std::vector<blink::mojom::AnchorElementMetricsPtr> metrics;
metrics.push_back(CreateMetricsPtr());
metrics[0]->ratio_distance_top_to_visible_top = 10;
metrics[0]->viewport_size = GetDefaultViewport();
predictor_service()->ReportNewAnchorElements(std::move(metrics));
base::RunLoop().RunUntilIdle();
PageAnchorsMetricsObserver::AnchorsData::CreateForWebContents(web_contents());
PageAnchorsMetricsObserver::AnchorsData* data =
PageAnchorsMetricsObserver::AnchorsData::FromWebContents(web_contents());
EXPECT_EQ(1u, data->number_of_anchors_);
EXPECT_EQ(0u, data->number_of_anchors_contains_image_);
EXPECT_EQ(0u, data->number_of_anchors_in_iframe_);
EXPECT_EQ(0u, data->number_of_anchors_same_host_);
EXPECT_EQ(0u, data->number_of_anchors_url_incremented_);
EXPECT_EQ(10, data->total_clickable_space_);
EXPECT_EQ(10 * 100, data->MedianLinkLocation());
EXPECT_EQ(GetDefaultViewport().height(), data->viewport_height_);
EXPECT_EQ(GetDefaultViewport().width(), data->viewport_width_);
metrics.clear();
metrics.push_back(CreateMetricsPtr());
metrics[0]->contains_image = true;
predictor_service()->ReportNewAnchorElements(std::move(metrics));
base::RunLoop().RunUntilIdle();
EXPECT_EQ(2u, data->number_of_anchors_);
EXPECT_EQ(1u, data->number_of_anchors_contains_image_);
EXPECT_EQ(0u, data->number_of_anchors_in_iframe_);
EXPECT_EQ(0u, data->number_of_anchors_same_host_);
EXPECT_EQ(0u, data->number_of_anchors_url_incremented_);
EXPECT_EQ(20, data->total_clickable_space_);
EXPECT_EQ(5 * 100, data->MedianLinkLocation());
metrics.clear();
metrics.push_back(CreateMetricsPtr());
metrics[0]->is_in_iframe = true;
predictor_service()->ReportNewAnchorElements(std::move(metrics));
base::RunLoop().RunUntilIdle();
EXPECT_EQ(3u, data->number_of_anchors_);
EXPECT_EQ(1u, data->number_of_anchors_contains_image_);
EXPECT_EQ(1u, data->number_of_anchors_in_iframe_);
EXPECT_EQ(0u, data->number_of_anchors_same_host_);
EXPECT_EQ(0u, data->number_of_anchors_url_incremented_);
EXPECT_EQ(30, data->total_clickable_space_);
EXPECT_EQ(0, data->MedianLinkLocation());
metrics.clear();
metrics.push_back(CreateMetricsPtr());
metrics[0]->is_same_host = true;
predictor_service()->ReportNewAnchorElements(std::move(metrics));
base::RunLoop().RunUntilIdle();
EXPECT_EQ(4u, data->number_of_anchors_);
EXPECT_EQ(1u, data->number_of_anchors_contains_image_);
EXPECT_EQ(1u, data->number_of_anchors_in_iframe_);
EXPECT_EQ(1u, data->number_of_anchors_same_host_);
EXPECT_EQ(0u, data->number_of_anchors_url_incremented_);
EXPECT_EQ(40, data->total_clickable_space_);
EXPECT_EQ(0, data->MedianLinkLocation());
metrics.clear();
metrics.push_back(CreateMetricsPtr());
metrics[0]->is_url_incremented_by_one = true;
metrics[0]->ratio_area = 0.05;
predictor_service()->ReportNewAnchorElements(std::move(metrics));
base::RunLoop().RunUntilIdle();
EXPECT_EQ(5u, data->number_of_anchors_);
EXPECT_EQ(1u, data->number_of_anchors_contains_image_);
EXPECT_EQ(1u, data->number_of_anchors_in_iframe_);
EXPECT_EQ(1u, data->number_of_anchors_same_host_);
EXPECT_EQ(1u, data->number_of_anchors_url_incremented_);
EXPECT_EQ(45, data->total_clickable_space_);
EXPECT_EQ(0, data->MedianLinkLocation());
}
TEST_F(NavigationPredictorTest, ReportSameAnchorElementTwice) {
std::vector<blink::mojom::AnchorElementMetricsPtr> metrics;
metrics.push_back(CreateMetricsPtr());
uint32_t anchor_id = metrics[0]->anchor_id;
predictor_service()->ReportNewAnchorElements(std::move(metrics));
base::RunLoop().RunUntilIdle();
metrics.clear();
// Report the same anchor again, it should be ignored.
metrics.push_back(CreateMetricsPtr());
metrics[0]->anchor_id = anchor_id;
predictor_service()->ReportNewAnchorElements(std::move(metrics));
base::RunLoop().RunUntilIdle();
PageAnchorsMetricsObserver::AnchorsData::CreateForWebContents(web_contents());
PageAnchorsMetricsObserver::AnchorsData* data =
PageAnchorsMetricsObserver::AnchorsData::FromWebContents(web_contents());
EXPECT_EQ(1u, data->number_of_anchors_);
}
// Basic test to check the ReportNewAnchorElements method can be
// called with multiple anchors at once.
TEST_F(NavigationPredictorTest, ReportNewAnchorElementsMultipleAnchors) {
std::vector<blink::mojom::AnchorElementMetricsPtr> metrics;
metrics.push_back(CreateMetricsPtr());
metrics[0]->ratio_distance_top_to_visible_top = 10;
metrics.push_back(CreateMetricsPtr());
metrics[1]->contains_image = true;
metrics[1]->viewport_size = GetDefaultViewport();
predictor_service()->ReportNewAnchorElements(std::move(metrics));
base::RunLoop().RunUntilIdle();
PageAnchorsMetricsObserver::AnchorsData::CreateForWebContents(web_contents());
PageAnchorsMetricsObserver::AnchorsData* data =
PageAnchorsMetricsObserver::AnchorsData::FromWebContents(web_contents());
EXPECT_EQ(2u, data->number_of_anchors_);
EXPECT_EQ(1u, data->number_of_anchors_contains_image_);
EXPECT_EQ(0u, data->number_of_anchors_in_iframe_);
EXPECT_EQ(0u, data->number_of_anchors_same_host_);
EXPECT_EQ(0u, data->number_of_anchors_url_incremented_);
EXPECT_EQ(20, data->total_clickable_space_);
EXPECT_EQ(5 * 100, data->MedianLinkLocation());
EXPECT_EQ(GetDefaultViewport().height(), data->viewport_height_);
EXPECT_EQ(GetDefaultViewport().width(), data->viewport_width_);
}
class MetricsBuilder {
public:
explicit MetricsBuilder(NavigationPredictorTest* tester) : tester_(tester) {}
void AddElementsEnteredViewport(size_t num_elems) {
for (size_t i = 0; i < num_elems; i++) {
metrics_.push_back(tester_->CreateMetricsPtr());
entered_viewport_.push_back(
blink::mojom::AnchorElementEnteredViewport::New());
entered_viewport_.back()->anchor_id = metrics_.back()->anchor_id;
}
}
void Run() {
size_t num_entered_viewport = entered_viewport_.size();
tester_->predictor_service()->ReportNewAnchorElements(std::move(metrics_));
tester_->predictor_service()->ReportAnchorElementsEnteredViewport(
std::move(entered_viewport_));
metrics_.clear();
entered_viewport_.clear();
base::RunLoop().RunUntilIdle();
using UkmEntry = ukm::builders::NavigationPredictorAnchorElementMetrics;
ukm_entries_ = ukm_recorder_.GetEntriesByName(UkmEntry::kEntryName);
EXPECT_EQ(num_entered_viewport, ukm_entries_.size());
}
uint64_t Entry(size_t idx, const char* name) {
return *ukm_recorder_.GetEntryMetric(ukm_entries_[idx], name);
}
blink::mojom::AnchorElementMetricsPtr& Metrics(size_t index) {
return metrics_[index];
}
private:
raw_ptr<NavigationPredictorTest> tester_;
ukm::TestAutoSetUkmRecorder ukm_recorder_;
std::vector<blink::mojom::AnchorElementMetricsPtr> metrics_;
std::vector<blink::mojom::AnchorElementEnteredViewportPtr> entered_viewport_;
std::vector<const ukm::mojom::UkmEntry*> ukm_entries_;
};
TEST_F(NavigationPredictorTest,
ReportAnchorElementsEnteredViewportContainsImage) {
MetricsBuilder builder(this);
builder.AddElementsEnteredViewport(2);
builder.Metrics(0)->contains_image = false;
builder.Metrics(1)->contains_image = true;
builder.Run();
using UkmEntry = ukm::builders::NavigationPredictorAnchorElementMetrics;
EXPECT_EQ(0u, builder.Entry(0, UkmEntry::kContainsImageName));
EXPECT_EQ(1u, builder.Entry(1, UkmEntry::kContainsImageName));
}
TEST_F(NavigationPredictorTest, ReportAnchorElementsEnteredViewportIsInIframe) {
MetricsBuilder builder(this);
builder.AddElementsEnteredViewport(2);
builder.Metrics(0)->is_in_iframe = false;
builder.Metrics(1)->is_in_iframe = true;
builder.Run();
using UkmEntry = ukm::builders::NavigationPredictorAnchorElementMetrics;
EXPECT_EQ(0u, builder.Entry(0, UkmEntry::kIsInIframeName));
EXPECT_EQ(1u, builder.Entry(1, UkmEntry::kIsInIframeName));
}
TEST_F(NavigationPredictorTest,
ReportAnchorElementsEnteredViewportIsURLIncrementedByOne) {
MetricsBuilder builder(this);
builder.AddElementsEnteredViewport(2);
builder.Metrics(0)->is_url_incremented_by_one = false;
builder.Metrics(1)->is_url_incremented_by_one = true;
builder.Run();
using UkmEntry = ukm::builders::NavigationPredictorAnchorElementMetrics;
EXPECT_EQ(0u, builder.Entry(0, UkmEntry::kIsURLIncrementedByOneName));
EXPECT_EQ(1u, builder.Entry(1, UkmEntry::kIsURLIncrementedByOneName));
}
TEST_F(NavigationPredictorTest, ReportAnchorElementsEnteredViewportSameOrigin) {
MetricsBuilder builder(this);
builder.AddElementsEnteredViewport(2);
builder.Metrics(0)->is_same_host = false;
builder.Metrics(1)->is_same_host = true;
builder.Run();
using UkmEntry = ukm::builders::NavigationPredictorAnchorElementMetrics;
EXPECT_EQ(0u, builder.Entry(0, UkmEntry::kSameOriginName));
EXPECT_EQ(1u, builder.Entry(1, UkmEntry::kSameOriginName));
}
TEST_F(NavigationPredictorTest,
ReportAnchorElementsEnteredViewportRatioDistanceRootTop) {
MetricsBuilder builder(this);
builder.AddElementsEnteredViewport(1);
builder.Metrics(0)->ratio_distance_root_top = 0.21;
builder.Run();
using UkmEntry = ukm::builders::NavigationPredictorAnchorElementMetrics;
EXPECT_EQ(10u, builder.Entry(0, UkmEntry::kPercentClickableAreaName));
EXPECT_EQ(20u, builder.Entry(0, UkmEntry::kPercentVerticalDistanceName));
}
TEST_F(NavigationPredictorTest,
ReportAnchorElementsEnteredViewportHasTextSibling) {
MetricsBuilder builder(this);
builder.AddElementsEnteredViewport(2);
builder.Metrics(0)->has_text_sibling = false;
builder.Metrics(1)->has_text_sibling = true;
builder.Run();
using UkmEntry = ukm::builders::NavigationPredictorAnchorElementMetrics;
EXPECT_EQ(0u, builder.Entry(0, UkmEntry::kHasTextSiblingName));
EXPECT_EQ(1u, builder.Entry(1, UkmEntry::kHasTextSiblingName));
}
TEST_F(NavigationPredictorTest, ReportAnchorElementsEnteredViewportFontSize) {
MetricsBuilder builder(this);
builder.AddElementsEnteredViewport(3);
builder.Metrics(0)->font_size_px = 4;
builder.Metrics(1)->font_size_px = 12;
builder.Metrics(2)->font_size_px = 20;
builder.Run();
using UkmEntry = ukm::builders::NavigationPredictorAnchorElementMetrics;
EXPECT_EQ(1u, builder.Entry(0, UkmEntry::kFontSizeName));
EXPECT_EQ(2u, builder.Entry(1, UkmEntry::kFontSizeName));
EXPECT_EQ(3u, builder.Entry(2, UkmEntry::kFontSizeName));
}
TEST_F(NavigationPredictorTest, ReportAnchorElementsEnteredViewportIsBold) {
MetricsBuilder builder(this);
builder.AddElementsEnteredViewport(2);
builder.Metrics(0)->font_weight = 500;
builder.Metrics(1)->font_weight = 501;
builder.Run();
using UkmEntry = ukm::builders::NavigationPredictorAnchorElementMetrics;
EXPECT_EQ(0u, builder.Entry(0, UkmEntry::kIsBoldName));
EXPECT_EQ(1u, builder.Entry(1, UkmEntry::kIsBoldName));
}
TEST_F(NavigationPredictorTest, ReportAnchorElementsEnteredViewportPathLength) {
MetricsBuilder builder(this);
builder.AddElementsEnteredViewport(6);
builder.Metrics(0)->target_url = GURL("https://foo.com/");
builder.Metrics(1)->target_url = GURL("https://foo.com/2");
builder.Metrics(2)->target_url = GURL("https://foo.com/10chars__");
builder.Metrics(3)->target_url = GURL("https://foo.com/20chars____________");
builder.Metrics(4)->target_url = GURL("https://foo.com/21chars_____________");
builder.Metrics(5)->target_url = GURL(
"https://foo.com/"
"120chars________________________________________________________________"
"_______________________________________________");
builder.Run();
using UkmEntry = ukm::builders::NavigationPredictorAnchorElementMetrics;
EXPECT_EQ(0u, builder.Entry(0, UkmEntry::kPathLengthName));
EXPECT_EQ(0u, builder.Entry(1, UkmEntry::kPathLengthName));
EXPECT_EQ(10u, builder.Entry(2, UkmEntry::kPathLengthName));
EXPECT_EQ(20u, builder.Entry(3, UkmEntry::kPathLengthName));
EXPECT_EQ(20u, builder.Entry(4, UkmEntry::kPathLengthName));
EXPECT_EQ(100u, builder.Entry(5, UkmEntry::kPathLengthName));
}
TEST_F(NavigationPredictorTest, ReportAnchorElementsEnteredViewportPathDepth) {
MetricsBuilder builder(this);
builder.AddElementsEnteredViewport(5);
builder.Metrics(0)->target_url = GURL("https://foo.com/");
builder.Metrics(1)->target_url = GURL("https://foo.com/1");
builder.Metrics(2)->target_url = GURL("https://foo.com/2/");
builder.Metrics(3)->target_url = GURL("https://foo.com/1/2/3/4/5");
builder.Metrics(4)->target_url = GURL("https://foo.com/1/2/3/4/5/6");
builder.Run();
using UkmEntry = ukm::builders::NavigationPredictorAnchorElementMetrics;
EXPECT_EQ(1u, builder.Entry(0, UkmEntry::kPathDepthName));
EXPECT_EQ(1u, builder.Entry(1, UkmEntry::kPathDepthName));
EXPECT_EQ(2u, builder.Entry(2, UkmEntry::kPathDepthName));
EXPECT_EQ(5u, builder.Entry(3, UkmEntry::kPathDepthName));
EXPECT_EQ(5u, builder.Entry(4, UkmEntry::kPathDepthName));
}
TEST_F(NavigationPredictorTest, ReportAnchorElementClick) {
ukm::TestAutoSetUkmRecorder ukm_recorder;
std::vector<blink::mojom::AnchorElementMetricsPtr> metrics;
metrics.push_back(CreateMetricsPtr());
metrics.push_back(CreateMetricsPtr());
int anchor_id_0 = metrics[0]->anchor_id;
GURL target_url = metrics[0]->target_url;
int anchor_id_1 = metrics[1]->anchor_id;
predictor_service()->ReportNewAnchorElements(std::move(metrics));
auto click = blink::mojom::AnchorElementClick::New();
click->anchor_id = anchor_id_0;
click->target_url = target_url;
predictor_service()->ReportAnchorElementClick(std::move(click));
base::RunLoop().RunUntilIdle();
using UkmEntry = ukm::builders::NavigationPredictorPageLinkClick;
auto entries = ukm_recorder.GetEntriesByName(UkmEntry::kEntryName);
EXPECT_EQ(1u, entries.size());
auto* entry = entries[0];
auto get_metric = [&](auto name) {
return *ukm_recorder.GetEntryMetric(entry, name);
};
EXPECT_EQ(0, get_metric(UkmEntry::kAnchorElementIndexName));
EXPECT_EQ(1, get_metric(UkmEntry::kHrefUnchangedName));
click = blink::mojom::AnchorElementClick::New();
click->anchor_id = anchor_id_1;
// Pretend the page changed the URL since we first saw it.
click->target_url = GURL("https://changed.com");
predictor_service()->ReportAnchorElementClick(std::move(click));
base::RunLoop().RunUntilIdle();
entries = ukm_recorder.GetEntriesByName(UkmEntry::kEntryName);
EXPECT_EQ(2u, entries.size());
entry = entries[1];
EXPECT_EQ(1, get_metric(UkmEntry::kAnchorElementIndexName));
EXPECT_EQ(0, get_metric(UkmEntry::kHrefUnchangedName));
}
TEST_F(NavigationPredictorTest, ReportAnchorElementClickMoreThan10Clicks) {
ukm::TestAutoSetUkmRecorder ukm_recorder;
std::vector<blink::mojom::AnchorElementMetricsPtr> metrics;
metrics.push_back(CreateMetricsPtr());
int anchor_id = metrics[0]->anchor_id;
predictor_service()->ReportNewAnchorElements(std::move(metrics));
auto add_click = [&]() {
auto click = blink::mojom::AnchorElementClick::New();
click->anchor_id = anchor_id;
predictor_service()->ReportAnchorElementClick(std::move(click));
base::RunLoop().RunUntilIdle();
};
using UkmEntry = ukm::builders::NavigationPredictorPageLinkClick;
for (size_t i = 1; i <= 10; i++) {
add_click();
auto entries = ukm_recorder.GetEntriesByName(UkmEntry::kEntryName);
EXPECT_EQ(i, entries.size());
}
// Don't log more than 10 clicks.
for (size_t i = 1; i <= 10; i++) {
add_click();
auto entries = ukm_recorder.GetEntriesByName(UkmEntry::kEntryName);
EXPECT_EQ(10u, entries.size());
}
}
class MockNavigationPredictorForTesting : public NavigationPredictor {
public:
using AnchorId = NavigationPredictor::AnchorId;
static MockNavigationPredictorForTesting* Create(
content::RenderFrameHost* render_frame_host,
mojo::PendingReceiver<blink::mojom::AnchorElementMetricsHost> receiver) {
// The object is bound to the lifetime of the |render_frame_host| and the
// mojo connection. See DocumentService for details.
return new MockNavigationPredictorForTesting(*render_frame_host,
std::move(receiver));
}
const std::
unordered_map<AnchorId, UserInteractions, typename AnchorId::Hasher>&
user_interactions() const {
return user_interactions_;
}
absl::optional<base::TimeDelta> navigation_start_to_click() {
return navigation_start_to_click_;
}
private:
MockNavigationPredictorForTesting(
content::RenderFrameHost& render_frame_host,
mojo::PendingReceiver<blink::mojom::AnchorElementMetricsHost> receiver)
: NavigationPredictor(render_frame_host, std::move(receiver)) {}
};
TEST_F(NavigationPredictorTest, AnchorElementEnteredAndLeftViewport) {
mojo::Remote<blink::mojom::AnchorElementMetricsHost> predictor_service;
auto* predictor_service_host = MockNavigationPredictorForTesting::Create(
main_rfh(), predictor_service.BindNewPipeAndPassReceiver());
auto report_anchor_element_left_viewport =
[&predictor_service](
const MockNavigationPredictorForTesting::AnchorId& anchor_id,
const base::TimeDelta& time_in_viewport) {
std::vector<blink::mojom::AnchorElementLeftViewportPtr> metrics;
metrics.push_back(blink::mojom::AnchorElementLeftViewport::New(
static_cast<uint32_t>(anchor_id), time_in_viewport));
predictor_service->ReportAnchorElementsLeftViewport(std::move(metrics));
base::RunLoop().RunUntilIdle();
};
auto report_anchor_element_entered_viewport =
[&predictor_service](
const MockNavigationPredictorForTesting::AnchorId& anchor_id,
const base::TimeDelta& navigation_start_to_entered_viewport) {
std::vector<blink::mojom::AnchorElementEnteredViewportPtr> metrics;
metrics.push_back(blink::mojom::AnchorElementEnteredViewport::New(
static_cast<uint32_t>(anchor_id),
navigation_start_to_entered_viewport));
predictor_service->ReportAnchorElementsEnteredViewport(
std::move(metrics));
base::RunLoop().RunUntilIdle();
};
MockNavigationPredictorForTesting::AnchorId anchor_id{1};
// Anchor element entered the viewport for the first time. Check user
// interaction data to see if it is registered.
const auto navigation_start_to_entered_viewport_1 = base::Milliseconds(150);
report_anchor_element_entered_viewport(
anchor_id, navigation_start_to_entered_viewport_1);
EXPECT_EQ(1u, predictor_service_host->user_interactions().size());
const auto& user_interactions =
predictor_service_host->user_interactions().at(anchor_id);
EXPECT_TRUE(user_interactions.is_in_viewport);
EXPECT_TRUE(
user_interactions.last_navigation_start_to_entered_viewport.has_value());
EXPECT_EQ(navigation_start_to_entered_viewport_1,
user_interactions.last_navigation_start_to_entered_viewport);
// Anchor element left the viewport for the first time.
const auto time_in_viewport_1 = base::Milliseconds(100);
report_anchor_element_left_viewport(anchor_id, time_in_viewport_1);
EXPECT_EQ(1u, predictor_service_host->user_interactions().size());
EXPECT_FALSE(user_interactions.is_in_viewport);
EXPECT_FALSE(
user_interactions.last_navigation_start_to_entered_viewport.has_value());
EXPECT_TRUE(user_interactions.max_time_in_viewport.has_value());
EXPECT_EQ(time_in_viewport_1, user_interactions.max_time_in_viewport);
// Anchor element entered the viewport for a second time. It should update the
// existing user interaction data.
const auto navigation_start_to_entered_viewport_2 = base::Milliseconds(350);
report_anchor_element_entered_viewport(
anchor_id, navigation_start_to_entered_viewport_2);
EXPECT_EQ(1u, predictor_service_host->user_interactions().size());
EXPECT_TRUE(user_interactions.is_in_viewport);
EXPECT_EQ(navigation_start_to_entered_viewport_2,
user_interactions.last_navigation_start_to_entered_viewport);
// Anchor element left the viewport for a second time. It should update the
// time_in_viewport to max(time_in_viewport_1, time_in_viewport_2).
const auto time_in_viewport_2 = base::Milliseconds(200);
report_anchor_element_left_viewport(anchor_id, time_in_viewport_2);
EXPECT_EQ(1u, predictor_service_host->user_interactions().size());
// max(time_in_viewport_1, time_in_viewport_2) = time_in_viewport_2
EXPECT_EQ(time_in_viewport_2, user_interactions.max_time_in_viewport);
// Anchor element left the viewport for the third time. It should not affect
// the entered_viewport_to_left_viewport.
const auto time_in_viewport_3 = base::Milliseconds(120);
report_anchor_element_left_viewport(anchor_id, time_in_viewport_3);
EXPECT_EQ(1u, predictor_service_host->user_interactions().size());
// max(time_in_viewport_1, time_in_viewport_2, time_in_viewport_3) =
// time_in_viewport_2
EXPECT_EQ(time_in_viewport_2, user_interactions.max_time_in_viewport);
}
TEST_F(NavigationPredictorTest, AnchorElementPointerOverAndHover) {
mojo::Remote<blink::mojom::AnchorElementMetricsHost> predictor_service;
auto* predictor_service_host = MockNavigationPredictorForTesting::Create(
main_rfh(), predictor_service.BindNewPipeAndPassReceiver());
auto report_pointer_over =
[&predictor_service](
const MockNavigationPredictorForTesting::AnchorId& anchor_id,
const base::TimeDelta& navigation_start_to_pointer_over) {
predictor_service->ReportAnchorElementPointerOver(
blink::mojom::AnchorElementPointerOver::New(
static_cast<uint32_t>(anchor_id),
navigation_start_to_pointer_over));
base::RunLoop().RunUntilIdle();
};
auto report_pointer_out =
[&predictor_service](
const MockNavigationPredictorForTesting::AnchorId& anchor_id,
const base::TimeDelta& hover_dwell_time) {
blink::mojom::AnchorElementPointerOutPtr metrics =
blink::mojom::AnchorElementPointerOut::New(
static_cast<uint32_t>(anchor_id), hover_dwell_time);
predictor_service->ReportAnchorElementPointerOut(std::move(metrics));
base::RunLoop().RunUntilIdle();
};
MockNavigationPredictorForTesting::AnchorId anchor_id{1};
// Pointer started hovering over the anchor element for the first time. Check
// user interaction data to see if it is registered.
const auto navigation_start_to_pointer_over_1 = base::Milliseconds(150);
report_pointer_over(anchor_id, navigation_start_to_pointer_over_1);
EXPECT_EQ(1u, predictor_service_host->user_interactions().size());
const auto& user_interactions =
predictor_service_host->user_interactions().at(anchor_id);
EXPECT_TRUE(user_interactions.is_hovered);
EXPECT_TRUE(
user_interactions.last_navigation_start_to_pointer_over.has_value());
EXPECT_EQ(navigation_start_to_pointer_over_1,
user_interactions.last_navigation_start_to_pointer_over);
// Pointer stopped hovering over the anchor element for the first time.
const auto hover_dwell_time_1 = base::Milliseconds(100);
report_pointer_out(anchor_id, hover_dwell_time_1);
EXPECT_EQ(1u, predictor_service_host->user_interactions().size());
EXPECT_FALSE(user_interactions.is_hovered);
EXPECT_FALSE(
user_interactions.last_navigation_start_to_pointer_over.has_value());
EXPECT_TRUE(user_interactions.max_hover_dwell_time.has_value());
EXPECT_EQ(hover_dwell_time_1, user_interactions.max_hover_dwell_time);
// Pointer started hovering over the anchor element for a second time. It
// should update the existing user interaction data.
const auto navigation_start_to_pointer_over_2 = base::Milliseconds(450);
report_pointer_over(anchor_id, navigation_start_to_pointer_over_2);
EXPECT_EQ(1u, predictor_service_host->user_interactions().size());
EXPECT_TRUE(user_interactions.is_hovered);
EXPECT_TRUE(
user_interactions.last_navigation_start_to_pointer_over.has_value());
EXPECT_EQ(navigation_start_to_pointer_over_2,
user_interactions.last_navigation_start_to_pointer_over);
// Pointer stopped hovering over the anchor element for a second time. It
// should update the max_hover_dwell_time to max(hover_dwell_time_1,
// hover_dwell_time_2).
const auto hover_dwell_time_2 = base::Milliseconds(200);
report_pointer_out(anchor_id, hover_dwell_time_2);
EXPECT_EQ(1u, predictor_service_host->user_interactions().size());
EXPECT_FALSE(user_interactions.is_hovered);
EXPECT_FALSE(
user_interactions.last_navigation_start_to_pointer_over.has_value());
EXPECT_TRUE(user_interactions.max_hover_dwell_time.has_value());
// max(hover_dwell_time_1, hover_dwell_time_2) = hover_dwell_time_2
EXPECT_EQ(hover_dwell_time_2, user_interactions.max_hover_dwell_time);
// Pointer stopped hovering over the anchor element for a third time. It
// should not affect the max_hover_dwell_time.
const auto hover_dwell_time_3 = base::Milliseconds(50);
report_pointer_out(anchor_id, hover_dwell_time_3);
EXPECT_EQ(1u, predictor_service_host->user_interactions().size());
EXPECT_FALSE(user_interactions.is_hovered);
EXPECT_FALSE(
user_interactions.last_navigation_start_to_pointer_over.has_value());
EXPECT_TRUE(user_interactions.max_hover_dwell_time.has_value());
// max((hover_dwell_time_1, hover_dwell_time_2, hover_dwell_time_3) =
// hover_dwell_time_2
EXPECT_EQ(hover_dwell_time_2, user_interactions.max_hover_dwell_time);
}
TEST_F(NavigationPredictorTest, NavigationStartToClick) {
mojo::Remote<blink::mojom::AnchorElementMetricsHost> predictor_service;
auto* predictor_service_host = MockNavigationPredictorForTesting::Create(
main_rfh(), predictor_service.BindNewPipeAndPassReceiver());
EXPECT_FALSE(predictor_service_host->navigation_start_to_click().has_value());
const auto navigation_start_to_click = base::Milliseconds(200);
auto click = blink::mojom::AnchorElementClick::New();
click->anchor_id = 1;
click->target_url = GURL("https://example.com/test.html");
click->navigation_start_to_click = navigation_start_to_click;
predictor_service->ReportAnchorElementClick(std::move(click));
base::RunLoop().RunUntilIdle();
EXPECT_EQ(navigation_start_to_click,
predictor_service_host->navigation_start_to_click());
}