blob: f1300e5bfb9c6d8043176b913e013474dca5de05 [file] [log] [blame]
// Copyright 2019 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 "third_party/blink/renderer/core/page/scrolling/text_fragment_anchor_metrics.h"
#include "base/test/simple_test_tick_clock.h"
#include "components/ukm/test_ukm_recorder.h"
#include "services/metrics/public/cpp/ukm_builders.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/public/mojom/scroll/scroll_enums.mojom-blink.h"
#include "third_party/blink/public/platform/scheduler/test/renderer_scheduler_test_support.h"
#include "third_party/blink/renderer/core/frame/local_frame.h"
#include "third_party/blink/renderer/core/frame/local_frame_view.h"
#include "third_party/blink/renderer/core/input/event_handler.h"
#include "third_party/blink/renderer/core/page/scrolling/text_fragment_anchor.h"
#include "third_party/blink/renderer/core/paint/paint_layer_scrollable_area.h"
#include "third_party/blink/renderer/core/testing/scoped_fake_ukm_recorder.h"
#include "third_party/blink/renderer/core/testing/sim/sim_request.h"
#include "third_party/blink/renderer/core/testing/sim/sim_test.h"
#include "third_party/blink/renderer/platform/testing/histogram_tester.h"
#include "third_party/blink/renderer/platform/testing/unit_test_helpers.h"
namespace blink {
using test::RunPendingTasks;
const char kSuccessUkmMetric[] = "Success";
const char kSourceUkmMetric[] = "Source";
class TextFragmentAnchorMetricsTest : public SimTest {
public:
void SetUp() override {
SimTest::SetUp();
WebView().MainFrameViewWidget()->Resize(gfx::Size(800, 600));
}
void RunAsyncMatchingTasks() {
auto* scheduler =
ThreadScheduler::Current()->GetWebMainThreadSchedulerForTest();
blink::scheduler::RunIdleTasksForTesting(scheduler,
base::BindOnce([]() {}));
RunPendingTasks();
}
void SimulateClick(int x, int y) {
WebMouseEvent event(WebInputEvent::Type::kMouseDown, gfx::PointF(x, y),
gfx::PointF(x, y), WebPointerProperties::Button::kLeft,
0, WebInputEvent::Modifiers::kLeftButtonDown,
base::TimeTicks::Now());
event.SetFrameScale(1);
GetDocument().GetFrame()->GetEventHandler().HandleMousePressEvent(event);
}
void BeginEmptyFrame() {
// If a test case doesn't find a match and therefore doesn't schedule the
// beforematch event, we should still render a second frame as if we did
// schedule the event to retain test coverage.
// When the beforematch event is not scheduled, a DCHECK will fail on
// BeginFrame() because no event was scheduled, so we schedule an empty task
// here.
GetDocument().EnqueueAnimationFrameTask(WTF::Bind([]() {}));
Compositor().BeginFrame();
}
protected:
ukm::TestUkmRecorder* ukm_recorder() {
return scoped_fake_ukm_recorder_.recorder();
}
HistogramTester histogram_tester_;
ScopedFakeUkmRecorder scoped_fake_ukm_recorder_;
};
// Test UMA metrics collection
TEST_F(TextFragmentAnchorMetricsTest, UMAMetricsCollected) {
SimRequest request("https://example.com/test.html#:~:text=test&text=cat",
"text/html");
LoadURL("https://example.com/test.html#:~:text=test&text=cat");
request.Complete(R"HTML(
<!DOCTYPE html>
<style>
body {
height: 1200px;
}
p {
position: absolute;
top: 1000px;
}
</style>
<p>This is a test page</p>
<p>With ambiguous test content</p>
)HTML");
RunAsyncMatchingTasks();
// Render two frames to handle the async step added by the beforematch event.
Compositor().BeginFrame();
BeginEmptyFrame();
histogram_tester_.ExpectTotalCount("TextFragmentAnchor.Unknown.SelectorCount",
1);
histogram_tester_.ExpectUniqueSample(
"TextFragmentAnchor.Unknown.SelectorCount", 2, 1);
histogram_tester_.ExpectTotalCount("TextFragmentAnchor.Unknown.MatchRate", 1);
histogram_tester_.ExpectUniqueSample("TextFragmentAnchor.Unknown.MatchRate",
50, 1);
histogram_tester_.ExpectTotalCount(
"TextFragmentAnchor.Unknown.AmbiguousMatch", 1);
histogram_tester_.ExpectUniqueSample(
"TextFragmentAnchor.Unknown.AmbiguousMatch", 1, 1);
histogram_tester_.ExpectTotalCount(
"TextFragmentAnchor.Unknown.ScrollCancelled", 1);
histogram_tester_.ExpectUniqueSample(
"TextFragmentAnchor.Unknown.ScrollCancelled", 0, 1);
histogram_tester_.ExpectTotalCount(
"TextFragmentAnchor.Unknown.DidScrollIntoView", 1);
histogram_tester_.ExpectUniqueSample(
"TextFragmentAnchor.Unknown.DidScrollIntoView", 1, 1);
histogram_tester_.ExpectTotalCount(
"TextFragmentAnchor.Unknown.TimeToScrollIntoView", 1);
histogram_tester_.ExpectTotalCount(
"TextFragmentAnchor.Unknown.DirectiveLength", 1);
histogram_tester_.ExpectUniqueSample(
"TextFragmentAnchor.Unknown.DirectiveLength", 18, 1);
histogram_tester_.ExpectTotalCount(
"TextFragmentAnchor.Unknown.ExactTextLength", 1);
histogram_tester_.ExpectUniqueSample(
"TextFragmentAnchor.Unknown.ExactTextLength", 4, 1);
histogram_tester_.ExpectTotalCount(
"TextFragmentAnchor.Unknown.RangeMatchLength", 0);
histogram_tester_.ExpectTotalCount(
"TextFragmentAnchor.Unknown.StartTextLength", 0);
histogram_tester_.ExpectTotalCount("TextFragmentAnchor.Unknown.EndTextLength",
0);
histogram_tester_.ExpectTotalCount("TextFragmentAnchor.Unknown.Parameters",
1);
histogram_tester_.ExpectUniqueSample(
"TextFragmentAnchor.Unknown.Parameters",
static_cast<int>(
TextFragmentAnchorMetrics::TextFragmentAnchorParameters::kExactText),
1);
histogram_tester_.ExpectTotalCount(
"TextFragmentAnchor.Unknown.TimeToScrollToTop", 0);
histogram_tester_.ExpectTotalCount("TextFragmentAnchor.Unknown.ListItemMatch",
1);
histogram_tester_.ExpectUniqueSample(
"TextFragmentAnchor.Unknown.ListItemMatch", 0, 1);
histogram_tester_.ExpectTotalCount(
"TextFragmentAnchor.Unknown.TableCellMatch", 1);
histogram_tester_.ExpectUniqueSample(
"TextFragmentAnchor.Unknown.TableCellMatch", 0, 1);
histogram_tester_.ExpectTotalCount("TextFragmentAnchor.LinkOpenSource", 1);
histogram_tester_.ExpectUniqueSample("TextFragmentAnchor.LinkOpenSource", 0,
1);
}
// Test UMA metrics collection with search engine referrer.
TEST_F(TextFragmentAnchorMetricsTest, UMAMetricsCollectedSearchEngineReferrer) {
// Set the referrer to a known search engine URL. This should cause metrics
// to be reported for the SearchEngine variant of histograms.
SimRequest::Params params;
params.referrer = "https://www.bing.com";
SimRequest request("https://example.com/test.html#:~:text=test&text=cat",
"text/html", params);
LoadURL("https://example.com/test.html#:~:text=test&text=cat");
request.Complete(R"HTML(
<!DOCTYPE html>
<style>
body {
height: 1200px;
}
p {
position: absolute;
top: 1000px;
}
</style>
<p>This is a test page</p>
<p>With ambiguous test content</p>
)HTML");
RunAsyncMatchingTasks();
// Render two frames to handle the async step added by the beforematch event.
Compositor().BeginFrame();
BeginEmptyFrame();
histogram_tester_.ExpectTotalCount(
"TextFragmentAnchor.SearchEngine.SelectorCount", 1);
histogram_tester_.ExpectUniqueSample(
"TextFragmentAnchor.SearchEngine.SelectorCount", 2, 1);
histogram_tester_.ExpectTotalCount(
"TextFragmentAnchor.SearchEngine.MatchRate", 1);
histogram_tester_.ExpectUniqueSample(
"TextFragmentAnchor.SearchEngine.MatchRate", 50, 1);
histogram_tester_.ExpectTotalCount(
"TextFragmentAnchor.SearchEngine.AmbiguousMatch", 1);
histogram_tester_.ExpectUniqueSample(
"TextFragmentAnchor.SearchEngine.AmbiguousMatch", 1, 1);
histogram_tester_.ExpectTotalCount(
"TextFragmentAnchor.SearchEngine.ScrollCancelled", 1);
histogram_tester_.ExpectUniqueSample(
"TextFragmentAnchor.SearchEngine.ScrollCancelled", 0, 1);
histogram_tester_.ExpectTotalCount(
"TextFragmentAnchor.SearchEngine.DidScrollIntoView", 1);
histogram_tester_.ExpectUniqueSample(
"TextFragmentAnchor.SearchEngine.DidScrollIntoView", 1, 1);
histogram_tester_.ExpectTotalCount(
"TextFragmentAnchor.SearchEngine.TimeToScrollIntoView", 1);
histogram_tester_.ExpectTotalCount(
"TextFragmentAnchor.SearchEngine.DirectiveLength", 1);
histogram_tester_.ExpectUniqueSample(
"TextFragmentAnchor.SearchEngine.DirectiveLength", 18, 1);
histogram_tester_.ExpectTotalCount(
"TextFragmentAnchor.SearchEngine.ExactTextLength", 1);
histogram_tester_.ExpectUniqueSample(
"TextFragmentAnchor.SearchEngine.ExactTextLength", 4, 1);
histogram_tester_.ExpectTotalCount(
"TextFragmentAnchor.SearchEngine.RangeMatchLength", 0);
histogram_tester_.ExpectTotalCount(
"TextFragmentAnchor.SearchEngine.StartTextLength", 0);
histogram_tester_.ExpectTotalCount(
"TextFragmentAnchor.SearchEngine.EndTextLength", 0);
histogram_tester_.ExpectTotalCount(
"TextFragmentAnchor.SearchEngine.Parameters", 1);
histogram_tester_.ExpectUniqueSample(
"TextFragmentAnchor.SearchEngine.Parameters",
static_cast<int>(
TextFragmentAnchorMetrics::TextFragmentAnchorParameters::kExactText),
1);
histogram_tester_.ExpectTotalCount(
"TextFragmentAnchor.SearchEngine.TimeToScrollToTop", 0);
histogram_tester_.ExpectTotalCount(
"TextFragmentAnchor.SearchEngine.ListItemMatch", 1);
histogram_tester_.ExpectUniqueSample(
"TextFragmentAnchor.SearchEngine.ListItemMatch", 0, 1);
histogram_tester_.ExpectTotalCount(
"TextFragmentAnchor.SearchEngine.TableCellMatch", 1);
histogram_tester_.ExpectUniqueSample(
"TextFragmentAnchor.SearchEngine.TableCellMatch", 0, 1);
histogram_tester_.ExpectTotalCount("TextFragmentAnchor.LinkOpenSource", 1);
histogram_tester_.ExpectUniqueSample("TextFragmentAnchor.LinkOpenSource", 1,
1);
}
// Test UMA metrics collection when there is no match found with an unknown
// referrer.
TEST_F(TextFragmentAnchorMetricsTest, NoMatchFoundWithUnknownSource) {
SimRequest request("https://example.com/test.html#:~:text=cat", "text/html");
LoadURL("https://example.com/test.html#:~:text=cat");
request.Complete(R"HTML(
<!DOCTYPE html>
<style>
body {
height: 1200px;
}
p {
position: absolute;
top: 1000px;
}
</style>
<p>This is a test page</p>
)HTML");
RunAsyncMatchingTasks();
// Render two frames to handle the async step added by the beforematch event.
Compositor().BeginFrame();
BeginEmptyFrame();
histogram_tester_.ExpectTotalCount("TextFragmentAnchor.Unknown.SelectorCount",
1);
histogram_tester_.ExpectUniqueSample(
"TextFragmentAnchor.Unknown.SelectorCount", 1, 1);
histogram_tester_.ExpectTotalCount("TextFragmentAnchor.Unknown.MatchRate", 1);
histogram_tester_.ExpectUniqueSample("TextFragmentAnchor.Unknown.MatchRate",
0, 1);
histogram_tester_.ExpectTotalCount(
"TextFragmentAnchor.Unknown.AmbiguousMatch", 1);
histogram_tester_.ExpectUniqueSample(
"TextFragmentAnchor.Unknown.AmbiguousMatch", 0, 1);
histogram_tester_.ExpectTotalCount(
"TextFragmentAnchor.Unknown.ScrollCancelled", 1);
histogram_tester_.ExpectUniqueSample(
"TextFragmentAnchor.Unknown.ScrollCancelled", 0, 1);
histogram_tester_.ExpectTotalCount(
"TextFragmentAnchor.Unknown.DidScrollIntoView", 0);
histogram_tester_.ExpectTotalCount(
"TextFragmentAnchor.Unknown.TimeToScrollIntoView", 0);
histogram_tester_.ExpectTotalCount(
"TextFragmentAnchor.Unknown.DirectiveLength", 1);
histogram_tester_.ExpectUniqueSample(
"TextFragmentAnchor.Unknown.DirectiveLength", 8, 1);
histogram_tester_.ExpectTotalCount(
"TextFragmentAnchor.Unknown.ExactTextLength", 0);
histogram_tester_.ExpectTotalCount(
"TextFragmentAnchor.Unknown.RangeMatchLength", 0);
histogram_tester_.ExpectTotalCount(
"TextFragmentAnchor.Unknown.StartTextLength", 0);
histogram_tester_.ExpectTotalCount("TextFragmentAnchor.Unknown.EndTextLength",
0);
histogram_tester_.ExpectTotalCount("TextFragmentAnchor.Unknown.Parameters",
0);
histogram_tester_.ExpectTotalCount(
"TextFragmentAnchor.Unknown.TimeToScrollToTop", 0);
histogram_tester_.ExpectTotalCount("TextFragmentAnchor.Unknown.ListItemMatch",
0);
histogram_tester_.ExpectTotalCount(
"TextFragmentAnchor.Unknown.TableCellMatch", 0);
histogram_tester_.ExpectTotalCount("TextFragmentAnchor.LinkOpenSource", 1);
histogram_tester_.ExpectUniqueSample("TextFragmentAnchor.LinkOpenSource", 0,
1);
}
// Test UMA metrics collection when there is no match found with a Search Engine
// referrer.
TEST_F(TextFragmentAnchorMetricsTest, NoMatchFoundWithSearchEngineSource) {
// Set the referrer to a known search engine URL. This should cause metrics
// to be reported for the SearchEngine variant of histograms.
SimRequest::Params params;
params.referrer = "https://www.bing.com";
SimRequest request("https://example.com/test.html#:~:text=cat", "text/html",
params);
LoadURL("https://example.com/test.html#:~:text=cat");
request.Complete(R"HTML(
<!DOCTYPE html>
<style>
body {
height: 1200px;
}
p {
position: absolute;
top: 1000px;
}
</style>
<p>This is a test page</p>
)HTML");
RunAsyncMatchingTasks();
// Render two frames to handle the async step added by the beforematch event.
Compositor().BeginFrame();
BeginEmptyFrame();
histogram_tester_.ExpectTotalCount(
"TextFragmentAnchor.SearchEngine.SelectorCount", 1);
histogram_tester_.ExpectUniqueSample(
"TextFragmentAnchor.SearchEngine.SelectorCount", 1, 1);
histogram_tester_.ExpectTotalCount(
"TextFragmentAnchor.SearchEngine.MatchRate", 1);
histogram_tester_.ExpectUniqueSample(
"TextFragmentAnchor.SearchEngine.MatchRate", 0, 1);
histogram_tester_.ExpectTotalCount(
"TextFragmentAnchor.SearchEngine.AmbiguousMatch", 1);
histogram_tester_.ExpectUniqueSample(
"TextFragmentAnchor.SearchEngine.AmbiguousMatch", 0, 1);
histogram_tester_.ExpectTotalCount(
"TextFragmentAnchor.SearchEngine.ScrollCancelled", 1);
histogram_tester_.ExpectUniqueSample(
"TextFragmentAnchor.SearchEngine.ScrollCancelled", 0, 1);
histogram_tester_.ExpectTotalCount(
"TextFragmentAnchor.SearchEngine.DidScrollIntoView", 0);
histogram_tester_.ExpectTotalCount(
"TextFragmentAnchor.SearchEngine.TimeToScrollIntoView", 0);
histogram_tester_.ExpectTotalCount(
"TextFragmentAnchor.SearchEngine.DirectiveLength", 1);
histogram_tester_.ExpectUniqueSample(
"TextFragmentAnchor.SearchEngine.DirectiveLength", 8, 1);
histogram_tester_.ExpectTotalCount(
"TextFragmentAnchor.SearchEngine.ExactTextLength", 0);
histogram_tester_.ExpectTotalCount(
"TextFragmentAnchor.SearchEngine.RangeMatchLength", 0);
histogram_tester_.ExpectTotalCount(
"TextFragmentAnchor.SearchEngine.StartTextLength", 0);
histogram_tester_.ExpectTotalCount(
"TextFragmentAnchor.SearchEngine.EndTextLength", 0);
histogram_tester_.ExpectTotalCount(
"TextFragmentAnchor.SearchEngine.Parameters", 0);
histogram_tester_.ExpectTotalCount(
"TextFragmentAnchor.SearchEngine.TimeToScrollToTop", 0);
histogram_tester_.ExpectTotalCount(
"TextFragmentAnchor.SearchEngine.ListItemMatch", 0);
histogram_tester_.ExpectTotalCount(
"TextFragmentAnchor.SearchEngine.TableCellMatch", 0);
histogram_tester_.ExpectTotalCount("TextFragmentAnchor.LinkOpenSource", 1);
histogram_tester_.ExpectUniqueSample("TextFragmentAnchor.LinkOpenSource", 1,
1);
}
// Test that we don't collect any metrics when there is no text directive
TEST_F(TextFragmentAnchorMetricsTest, NoTextFragmentAnchor) {
SimRequest request("https://example.com/test.html", "text/html");
LoadURL("https://example.com/test.html");
request.Complete(R"HTML(
<!DOCTYPE html>
<p>This is a test page</p>
)HTML");
// Render two frames to handle the async step added by the beforematch event.
Compositor().BeginFrame();
BeginEmptyFrame();
RunAsyncMatchingTasks();
histogram_tester_.ExpectTotalCount("TextFragmentAnchor.Unknown.SelectorCount",
0);
histogram_tester_.ExpectTotalCount("TextFragmentAnchor.Unknown.MatchRate", 0);
histogram_tester_.ExpectTotalCount(
"TextFragmentAnchor.Unknown.AmbiguousMatch", 0);
histogram_tester_.ExpectTotalCount(
"TextFragmentAnchor.Unknown.ScrollCancelled", 0);
histogram_tester_.ExpectTotalCount(
"TextFragmentAnchor.Unknown.DidScrollIntoView", 0);
histogram_tester_.ExpectTotalCount(
"TextFragmentAnchor.Unknown.TimeToScrollIntoView", 0);
histogram_tester_.ExpectTotalCount(
"TextFragmentAnchor.Unknown.DirectiveLength", 0);
histogram_tester_.ExpectTotalCount(
"TextFragmentAnchor.Unknown.ExactTextLength", 0);
histogram_tester_.ExpectTotalCount(
"TextFragmentAnchor.Unknown.RangeMatchLength", 0);
histogram_tester_.ExpectTotalCount(
"TextFragmentAnchor.Unknown.StartTextLength", 0);
histogram_tester_.ExpectTotalCount("TextFragmentAnchor.Unknown.EndTextLength",
0);
histogram_tester_.ExpectTotalCount("TextFragmentAnchor.Unknown.Parameters",
0);
histogram_tester_.ExpectTotalCount(
"TextFragmentAnchor.Unknown.TimeToScrollToTop", 0);
histogram_tester_.ExpectTotalCount("TextFragmentAnchor.Unknown.ListItemMatch",
0);
histogram_tester_.ExpectTotalCount(
"TextFragmentAnchor.Unknown.TableCellMatch", 0);
histogram_tester_.ExpectTotalCount("TextFragmentAnchor.LinkOpenSource", 0);
}
// Test that the correct metrics are collected when we found a match but didn't
// need to scroll.
TEST_F(TextFragmentAnchorMetricsTest, MatchFoundNoScroll) {
SimRequest request("https://example.com/test.html#:~:text=test", "text/html");
LoadURL("https://example.com/test.html#:~:text=test");
request.Complete(R"HTML(
<!DOCTYPE html>
<p>This is a test page</p>
)HTML");
RunAsyncMatchingTasks();
// Render two frames to handle the async step added by the beforematch event.
Compositor().BeginFrame();
BeginEmptyFrame();
histogram_tester_.ExpectTotalCount("TextFragmentAnchor.Unknown.SelectorCount",
1);
histogram_tester_.ExpectUniqueSample(
"TextFragmentAnchor.Unknown.SelectorCount", 1, 1);
histogram_tester_.ExpectTotalCount("TextFragmentAnchor.Unknown.MatchRate", 1);
histogram_tester_.ExpectUniqueSample("TextFragmentAnchor.Unknown.MatchRate",
100, 1);
histogram_tester_.ExpectTotalCount(
"TextFragmentAnchor.Unknown.AmbiguousMatch", 1);
histogram_tester_.ExpectUniqueSample(
"TextFragmentAnchor.Unknown.AmbiguousMatch", 0, 1);
histogram_tester_.ExpectTotalCount(
"TextFragmentAnchor.Unknown.ScrollCancelled", 1);
histogram_tester_.ExpectUniqueSample(
"TextFragmentAnchor.Unknown.ScrollCancelled", 0, 1);
histogram_tester_.ExpectTotalCount(
"TextFragmentAnchor.Unknown.DidScrollIntoView", 1);
histogram_tester_.ExpectUniqueSample(
"TextFragmentAnchor.Unknown.DidScrollIntoView", 0, 1);
histogram_tester_.ExpectTotalCount(
"TextFragmentAnchor.Unknown.TimeToScrollIntoView", 1);
histogram_tester_.ExpectTotalCount(
"TextFragmentAnchor.Unknown.DirectiveLength", 1);
histogram_tester_.ExpectUniqueSample(
"TextFragmentAnchor.Unknown.DirectiveLength", 9, 1);
histogram_tester_.ExpectTotalCount(
"TextFragmentAnchor.Unknown.ExactTextLength", 1);
histogram_tester_.ExpectUniqueSample(
"TextFragmentAnchor.Unknown.ExactTextLength", 4, 1);
histogram_tester_.ExpectTotalCount(
"TextFragmentAnchor.Unknown.RangeMatchLength", 0);
histogram_tester_.ExpectTotalCount(
"TextFragmentAnchor.Unknown.StartTextLength", 0);
histogram_tester_.ExpectTotalCount("TextFragmentAnchor.Unknown.EndTextLength",
0);
histogram_tester_.ExpectTotalCount("TextFragmentAnchor.Unknown.Parameters",
1);
histogram_tester_.ExpectUniqueSample(
"TextFragmentAnchor.Unknown.Parameters",
static_cast<int>(
TextFragmentAnchorMetrics::TextFragmentAnchorParameters::kExactText),
1);
histogram_tester_.ExpectTotalCount(
"TextFragmentAnchor.Unknown.TimeToScrollToTop", 0);
histogram_tester_.ExpectTotalCount("TextFragmentAnchor.Unknown.ListItemMatch",
1);
histogram_tester_.ExpectUniqueSample(
"TextFragmentAnchor.Unknown.ListItemMatch", 0, 1);
histogram_tester_.ExpectTotalCount(
"TextFragmentAnchor.Unknown.TableCellMatch", 1);
histogram_tester_.ExpectUniqueSample(
"TextFragmentAnchor.Unknown.TableCellMatch", 0, 1);
histogram_tester_.ExpectTotalCount("TextFragmentAnchor.LinkOpenSource", 1);
histogram_tester_.ExpectUniqueSample("TextFragmentAnchor.LinkOpenSource", 0,
1);
}
// Test that the correct metrics are collected for all possible combinations of
// context terms on an exact text directive.
TEST_F(TextFragmentAnchorMetricsTest, ExactTextParameters) {
SimRequest request(
"https://example.com/"
"test.html#:~:text=this&text=is-,a&text=test,-page&text=with-,some,-"
"content",
"text/html");
LoadURL(
"https://example.com/"
"test.html#:~:text=this&text=is-,a&text=test,-page&text=with-,some,-"
"content");
request.Complete(R"HTML(
<!DOCTYPE html>
<p>This is a test page</p>
<p>With some content</p>
)HTML");
RunAsyncMatchingTasks();
Compositor().BeginFrame();
BeginEmptyFrame();
histogram_tester_.ExpectTotalCount("TextFragmentAnchor.Unknown.SelectorCount",
1);
histogram_tester_.ExpectUniqueSample(
"TextFragmentAnchor.Unknown.SelectorCount", 4, 1);
histogram_tester_.ExpectTotalCount("TextFragmentAnchor.Unknown.MatchRate", 1);
histogram_tester_.ExpectUniqueSample("TextFragmentAnchor.Unknown.MatchRate",
100, 1);
histogram_tester_.ExpectTotalCount(
"TextFragmentAnchor.Unknown.AmbiguousMatch", 1);
histogram_tester_.ExpectUniqueSample(
"TextFragmentAnchor.Unknown.AmbiguousMatch", 0, 1);
histogram_tester_.ExpectTotalCount(
"TextFragmentAnchor.Unknown.ScrollCancelled", 1);
histogram_tester_.ExpectUniqueSample(
"TextFragmentAnchor.Unknown.ScrollCancelled", 0, 1);
histogram_tester_.ExpectTotalCount(
"TextFragmentAnchor.Unknown.DidScrollIntoView", 1);
histogram_tester_.ExpectUniqueSample(
"TextFragmentAnchor.Unknown.DidScrollIntoView", 0, 1);
histogram_tester_.ExpectTotalCount(
"TextFragmentAnchor.Unknown.TimeToScrollIntoView", 1);
histogram_tester_.ExpectTotalCount(
"TextFragmentAnchor.Unknown.DirectiveLength", 1);
histogram_tester_.ExpectUniqueSample(
"TextFragmentAnchor.Unknown.DirectiveLength", 61, 1);
histogram_tester_.ExpectTotalCount(
"TextFragmentAnchor.Unknown.ExactTextLength", 4);
// "this", "test", "some"
histogram_tester_.ExpectBucketCount(
"TextFragmentAnchor.Unknown.ExactTextLength", 4, 3);
// "a"
histogram_tester_.ExpectBucketCount(
"TextFragmentAnchor.Unknown.ExactTextLength", 1, 1);
histogram_tester_.ExpectTotalCount(
"TextFragmentAnchor.Unknown.RangeMatchLength", 0);
histogram_tester_.ExpectTotalCount(
"TextFragmentAnchor.Unknown.StartTextLength", 0);
histogram_tester_.ExpectTotalCount("TextFragmentAnchor.Unknown.EndTextLength",
0);
histogram_tester_.ExpectTotalCount("TextFragmentAnchor.Unknown.Parameters",
4);
histogram_tester_.ExpectBucketCount(
"TextFragmentAnchor.Unknown.Parameters",
static_cast<int>(
TextFragmentAnchorMetrics::TextFragmentAnchorParameters::kExactText),
1);
histogram_tester_.ExpectBucketCount(
"TextFragmentAnchor.Unknown.Parameters",
static_cast<int>(TextFragmentAnchorMetrics::TextFragmentAnchorParameters::
kExactTextWithPrefix),
1);
histogram_tester_.ExpectBucketCount(
"TextFragmentAnchor.Unknown.Parameters",
static_cast<int>(TextFragmentAnchorMetrics::TextFragmentAnchorParameters::
kExactTextWithSuffix),
1);
histogram_tester_.ExpectBucketCount(
"TextFragmentAnchor.Unknown.Parameters",
static_cast<int>(TextFragmentAnchorMetrics::TextFragmentAnchorParameters::
kExactTextWithContext),
1);
histogram_tester_.ExpectTotalCount(
"TextFragmentAnchor.Unknown.TimeToScrollToTop", 0);
histogram_tester_.ExpectTotalCount("TextFragmentAnchor.Unknown.ListItemMatch",
4);
histogram_tester_.ExpectUniqueSample(
"TextFragmentAnchor.Unknown.ListItemMatch", 0, 4);
histogram_tester_.ExpectTotalCount(
"TextFragmentAnchor.Unknown.TableCellMatch", 4);
histogram_tester_.ExpectUniqueSample(
"TextFragmentAnchor.Unknown.TableCellMatch", 0, 4);
histogram_tester_.ExpectTotalCount("TextFragmentAnchor.LinkOpenSource", 1);
histogram_tester_.ExpectUniqueSample("TextFragmentAnchor.LinkOpenSource", 0,
1);
}
// Test that the correct metrics are collected for all possible combinations of
// context terms on a range text directive.
TEST_F(TextFragmentAnchorMetricsTest, TextRangeParameters) {
SimRequest request(
"https://example.com/"
"test.html#:~:text=this,is&text=a-,test,page&text=with,some,-content&"
"text=about-,nothing,at,-all",
"text/html");
LoadURL(
"https://example.com/"
"test.html#:~:text=this,is&text=a-,test,page&text=with,some,-content&"
"text=about-,nothing,at,-all");
request.Complete(R"HTML(
<!DOCTYPE html>
<p>This is a test page</p>
<p>With some content</p>
<p>About nothing at all</p>
)HTML");
RunAsyncMatchingTasks();
Compositor().BeginFrame();
BeginEmptyFrame();
histogram_tester_.ExpectTotalCount("TextFragmentAnchor.Unknown.SelectorCount",
1);
histogram_tester_.ExpectUniqueSample(
"TextFragmentAnchor.Unknown.SelectorCount", 4, 1);
histogram_tester_.ExpectTotalCount("TextFragmentAnchor.Unknown.MatchRate", 1);
histogram_tester_.ExpectUniqueSample("TextFragmentAnchor.Unknown.MatchRate",
100, 1);
histogram_tester_.ExpectTotalCount(
"TextFragmentAnchor.Unknown.AmbiguousMatch", 1);
histogram_tester_.ExpectUniqueSample(
"TextFragmentAnchor.Unknown.AmbiguousMatch", 0, 1);
histogram_tester_.ExpectTotalCount(
"TextFragmentAnchor.Unknown.ScrollCancelled", 1);
histogram_tester_.ExpectUniqueSample(
"TextFragmentAnchor.Unknown.ScrollCancelled", 0, 1);
histogram_tester_.ExpectTotalCount(
"TextFragmentAnchor.Unknown.DidScrollIntoView", 1);
histogram_tester_.ExpectUniqueSample(
"TextFragmentAnchor.Unknown.DidScrollIntoView", 0, 1);
histogram_tester_.ExpectTotalCount(
"TextFragmentAnchor.Unknown.TimeToScrollIntoView", 1);
histogram_tester_.ExpectTotalCount(
"TextFragmentAnchor.Unknown.DirectiveLength", 1);
histogram_tester_.ExpectUniqueSample(
"TextFragmentAnchor.Unknown.DirectiveLength", 82, 1);
histogram_tester_.ExpectTotalCount(
"TextFragmentAnchor.Unknown.ExactTextLength", 0);
histogram_tester_.ExpectTotalCount(
"TextFragmentAnchor.Unknown.RangeMatchLength", 4);
// "This is"
histogram_tester_.ExpectBucketCount(
"TextFragmentAnchor.Unknown.RangeMatchLength", 7, 1);
// "test page", "with some"
histogram_tester_.ExpectBucketCount(
"TextFragmentAnchor.Unknown.RangeMatchLength", 9, 2);
// "nothing at"
histogram_tester_.ExpectBucketCount(
"TextFragmentAnchor.Unknown.RangeMatchLength", 10, 1);
histogram_tester_.ExpectTotalCount(
"TextFragmentAnchor.Unknown.StartTextLength", 4);
// "this", "test", "with"
histogram_tester_.ExpectBucketCount(
"TextFragmentAnchor.Unknown.StartTextLength", 4, 3);
// "nothing"
histogram_tester_.ExpectBucketCount(
"TextFragmentAnchor.Unknown.StartTextLength", 7, 1);
histogram_tester_.ExpectTotalCount("TextFragmentAnchor.Unknown.EndTextLength",
4);
// "is", "at"
histogram_tester_.ExpectBucketCount(
"TextFragmentAnchor.Unknown.EndTextLength", 2, 2);
// "page", "some"
histogram_tester_.ExpectBucketCount(
"TextFragmentAnchor.Unknown.EndTextLength", 4, 2);
histogram_tester_.ExpectTotalCount("TextFragmentAnchor.Unknown.Parameters",
4);
histogram_tester_.ExpectBucketCount(
"TextFragmentAnchor.Unknown.Parameters",
static_cast<int>(
TextFragmentAnchorMetrics::TextFragmentAnchorParameters::kTextRange),
1);
histogram_tester_.ExpectBucketCount(
"TextFragmentAnchor.Unknown.Parameters",
static_cast<int>(TextFragmentAnchorMetrics::TextFragmentAnchorParameters::
kTextRangeWithPrefix),
1);
histogram_tester_.ExpectBucketCount(
"TextFragmentAnchor.Unknown.Parameters",
static_cast<int>(TextFragmentAnchorMetrics::TextFragmentAnchorParameters::
kTextRangeWithSuffix),
1);
histogram_tester_.ExpectBucketCount(
"TextFragmentAnchor.Unknown.Parameters",
static_cast<int>(TextFragmentAnchorMetrics::TextFragmentAnchorParameters::
kTextRangeWithContext),
1);
histogram_tester_.ExpectTotalCount(
"TextFragmentAnchor.Unknown.TimeToScrollToTop", 0);
histogram_tester_.ExpectTotalCount("TextFragmentAnchor.Unknown.ListItemMatch",
0);
histogram_tester_.ExpectTotalCount(
"TextFragmentAnchor.Unknown.TableCellMatch", 0);
histogram_tester_.ExpectTotalCount("TextFragmentAnchor.LinkOpenSource", 1);
histogram_tester_.ExpectUniqueSample("TextFragmentAnchor.LinkOpenSource", 0,
1);
}
class TextFragmentAnchorScrollMetricsTest
: public TextFragmentAnchorMetricsTest,
public testing::WithParamInterface<mojom::blink::ScrollType> {
protected:
bool IsUserScrollType() {
return GetParam() == mojom::blink::ScrollType::kCompositor ||
GetParam() == mojom::blink::ScrollType::kUser;
}
};
INSTANTIATE_TEST_SUITE_P(
ScrollTypes,
TextFragmentAnchorScrollMetricsTest,
testing::Values(mojom::blink::ScrollType::kUser,
mojom::blink::ScrollType::kProgrammatic,
mojom::blink::ScrollType::kClamping,
mojom::blink::ScrollType::kCompositor,
mojom::blink::ScrollType::kAnchoring,
mojom::blink::ScrollType::kSequenced));
// Test that the ScrollCancelled metric gets reported when a user scroll cancels
// the scroll into view.
TEST_P(TextFragmentAnchorScrollMetricsTest, ScrollCancelled) {
// This test isn't relevant with this flag enabled. When it's enabled,
// there's no way to block rendering and the fragment is installed and
// invoked as soon as parsing finishes which means the user cannot scroll
// before this point.
ScopedBlockHTMLParserOnStyleSheetsForTest block_parser(false);
SimRequest request("https://example.com/test.html#:~:text=test", "text/html");
SimSubresourceRequest css_request("https://example.com/test.css", "text/css");
LoadURL("https://example.com/test.html#:~:text=test");
request.Complete(R"HTML(
<!DOCTYPE html>
<style>
body {
height: 1200px;
}
p {
position: absolute;
top: 1000px;
visibility: hidden;
}
</style>
<link rel=stylesheet href=test.css>
<p>This is a test page</p>
)HTML");
GetDocument().View()->UpdateAllLifecyclePhasesForTest();
mojom::blink::ScrollType scroll_type = GetParam();
GetDocument().View()->LayoutViewport()->ScrollBy(ScrollOffset(0, 100),
scroll_type);
// Set the target text to visible and change its position to cause a layout
// and invoke the fragment anchor.
css_request.Complete("p { visibility: visible; top: 1001px; }");
RunAsyncMatchingTasks();
// Render two frames to handle the async step added by the beforematch event.
Compositor().BeginFrame();
BeginEmptyFrame();
histogram_tester_.ExpectTotalCount(
"TextFragmentAnchor.Unknown.ScrollCancelled", 1);
// A user scroll should have caused this to be canceled, other kinds of
// scrolls should have no effect.
if (IsUserScrollType()) {
histogram_tester_.ExpectUniqueSample(
"TextFragmentAnchor.Unknown.ScrollCancelled", 1, 1);
histogram_tester_.ExpectTotalCount(
"TextFragmentAnchor.Unknown.DidScrollIntoView", 0);
histogram_tester_.ExpectTotalCount(
"TextFragmentAnchor.Unknown.TimeToScrollIntoView", 0);
} else {
histogram_tester_.ExpectUniqueSample(
"TextFragmentAnchor.Unknown.ScrollCancelled", 0, 1);
histogram_tester_.ExpectTotalCount(
"TextFragmentAnchor.Unknown.DidScrollIntoView", 1);
histogram_tester_.ExpectTotalCount(
"TextFragmentAnchor.Unknown.TimeToScrollIntoView", 1);
}
histogram_tester_.ExpectTotalCount("TextFragmentAnchor.Unknown.SelectorCount",
1);
histogram_tester_.ExpectUniqueSample(
"TextFragmentAnchor.Unknown.SelectorCount", 1, 1);
histogram_tester_.ExpectTotalCount("TextFragmentAnchor.Unknown.MatchRate", 1);
histogram_tester_.ExpectUniqueSample("TextFragmentAnchor.Unknown.MatchRate",
100, 1);
histogram_tester_.ExpectTotalCount(
"TextFragmentAnchor.Unknown.AmbiguousMatch", 1);
histogram_tester_.ExpectUniqueSample(
"TextFragmentAnchor.Unknown.AmbiguousMatch", 0, 1);
histogram_tester_.ExpectTotalCount(
"TextFragmentAnchor.Unknown.DirectiveLength", 1);
histogram_tester_.ExpectUniqueSample(
"TextFragmentAnchor.Unknown.DirectiveLength", 9, 1);
histogram_tester_.ExpectTotalCount(
"TextFragmentAnchor.Unknown.ExactTextLength", 1);
histogram_tester_.ExpectUniqueSample(
"TextFragmentAnchor.Unknown.ExactTextLength", 4, 1);
histogram_tester_.ExpectTotalCount(
"TextFragmentAnchor.Unknown.RangeMatchLength", 0);
histogram_tester_.ExpectTotalCount(
"TextFragmentAnchor.Unknown.StartTextLength", 0);
histogram_tester_.ExpectTotalCount("TextFragmentAnchor.Unknown.EndTextLength",
0);
histogram_tester_.ExpectTotalCount("TextFragmentAnchor.Unknown.Parameters",
1);
histogram_tester_.ExpectUniqueSample(
"TextFragmentAnchor.Unknown.Parameters",
static_cast<int>(
TextFragmentAnchorMetrics::TextFragmentAnchorParameters::kExactText),
1);
histogram_tester_.ExpectTotalCount(
"TextFragmentAnchor.Unknown.TimeToScrollToTop", 0);
histogram_tester_.ExpectTotalCount("TextFragmentAnchor.Unknown.ListItemMatch",
1);
histogram_tester_.ExpectUniqueSample(
"TextFragmentAnchor.Unknown.ListItemMatch", 0, 1);
histogram_tester_.ExpectTotalCount(
"TextFragmentAnchor.Unknown.TableCellMatch", 1);
histogram_tester_.ExpectUniqueSample(
"TextFragmentAnchor.Unknown.TableCellMatch", 0, 1);
histogram_tester_.ExpectTotalCount("TextFragmentAnchor.LinkOpenSource", 1);
histogram_tester_.ExpectUniqueSample("TextFragmentAnchor.LinkOpenSource", 0,
1);
}
// Test that the user scrolling back to the top of the page reports metrics
TEST_P(TextFragmentAnchorScrollMetricsTest, TimeToScrollToTop) {
mojom::blink::ScrollType scroll_type = GetParam();
// Set the page to be initially hidden to delay the text fragment so that we
// can set the mock TickClock.
WebView().SetVisibilityState(mojom::blink::PageVisibilityState::kHidden,
/*initial_state=*/true);
SimRequest request("https://example.com/test.html#:~:text=test%20page",
"text/html");
LoadURL("https://example.com/test.html#:~:text=test%20page");
request.Complete(R"HTML(
<!DOCTYPE html>
<style>
body {
height: 2200px;
}
p {
position: absolute;
top: 1000px;
}
</style>
<p>This is a test page</p>
)HTML");
RunAsyncMatchingTasks();
BeginEmptyFrame();
BeginEmptyFrame();
// Set the test TickClock and then render the page visible to activate the
// text fragment.
base::SimpleTestTickClock tick_clock;
tick_clock.SetNowTicks(base::TimeTicks::Now());
static_cast<TextFragmentAnchor*>(GetDocument().View()->GetFragmentAnchor())
->SetTickClockForTesting(&tick_clock);
WebView().SetVisibilityState(mojom::blink::PageVisibilityState::kVisible,
/*initial_state=*/false);
BeginEmptyFrame();
BeginEmptyFrame();
histogram_tester_.ExpectTotalCount(
"TextFragmentAnchor.Unknown.TimeToScrollToTop", 0);
const int64_t time_to_scroll_to_top = 500;
tick_clock.Advance(base::TimeDelta::FromMilliseconds(time_to_scroll_to_top));
ASSERT_GT(GetDocument().View()->LayoutViewport()->GetScrollOffset().Height(),
100);
// Ensure scrolling but not to the top isn't counted.
{
GetDocument().View()->LayoutViewport()->ScrollBy(ScrollOffset(0, -20),
scroll_type);
histogram_tester_.ExpectTotalCount(
"TextFragmentAnchor.Unknown.TimeToScrollToTop", 0);
}
// Scroll to top and ensure the metric is recorded, but only for user type
// scrolls.
{
GetDocument().View()->LayoutViewport()->SetScrollOffset(ScrollOffset(),
scroll_type);
if (IsUserScrollType()) {
histogram_tester_.ExpectTotalCount(
"TextFragmentAnchor.Unknown.TimeToScrollToTop", 1);
histogram_tester_.ExpectUniqueSample(
"TextFragmentAnchor.Unknown.TimeToScrollToTop", time_to_scroll_to_top,
1);
} else {
histogram_tester_.ExpectTotalCount(
"TextFragmentAnchor.Unknown.TimeToScrollToTop", 0);
}
}
// Scroll down and then back up to the top again to ensure the metric is
// recorded only once.
{
GetDocument().View()->LayoutViewport()->SetScrollOffset(
ScrollOffset(0, 100), scroll_type);
GetDocument().View()->LayoutViewport()->SetScrollOffset(ScrollOffset(),
scroll_type);
if (IsUserScrollType()) {
histogram_tester_.ExpectTotalCount(
"TextFragmentAnchor.Unknown.TimeToScrollToTop", 1);
} else {
histogram_tester_.ExpectTotalCount(
"TextFragmentAnchor.Unknown.TimeToScrollToTop", 0);
}
}
}
// Test that the TapToDismiss feature gets use counted when the user taps to
// dismiss the text highlight
TEST_F(TextFragmentAnchorMetricsTest, TapToDismiss) {
SimRequest request("https://example.com/test.html#:~:text=test%20page",
"text/html");
LoadURL("https://example.com/test.html#:~:text=test%20page");
request.Complete(R"HTML(
<!DOCTYPE html>
<style>
body {
height: 2200px;
}
p {
position: absolute;
top: 1000px;
}
</style>
<p>This is a test page</p>
)HTML");
RunAsyncMatchingTasks();
// Render two frames to handle the async step added by the beforematch event.
Compositor().BeginFrame();
BeginEmptyFrame();
EXPECT_TRUE(GetDocument().IsUseCounted(WebFeature::kTextFragmentAnchor));
EXPECT_TRUE(
GetDocument().IsUseCounted(WebFeature::kTextFragmentAnchorMatchFound));
EXPECT_FALSE(
GetDocument().IsUseCounted(WebFeature::kTextFragmentAnchorTapToDismiss));
SimulateClick(100, 100);
EXPECT_TRUE(
GetDocument().IsUseCounted(WebFeature::kTextFragmentAnchorTapToDismiss));
}
// Test counting cases where the fragment directive fails to parse.
TEST_F(TextFragmentAnchorMetricsTest, InvalidFragmentDirective) {
const int kUncounted = 0;
const int kCounted = 1;
Vector<std::pair<String, int>> test_cases = {
{"", kUncounted},
{"#element", kUncounted},
{"#doesntExist", kUncounted},
{"#:~:element", kCounted},
{"#element:~:", kCounted},
{"#foo:~:bar", kCounted},
{"#:~:utext=foo", kCounted},
{"#:~:text=foo", kUncounted},
{"#:~:text=foo&invalid", kUncounted},
{"#foo:~:text=foo", kUncounted}};
for (auto test_case : test_cases) {
String url = "https://example.com/test.html" + test_case.first;
SimRequest request(url, "text/html");
LoadURL(url);
request.Complete(R"HTML(
<!DOCTYPE html>
<p id="element">This is a test page</p>
)HTML");
// Render two frames to handle the async step added by the beforematch
// event.
Compositor().BeginFrame();
BeginEmptyFrame();
RunAsyncMatchingTasks();
bool is_use_counted =
GetDocument().IsUseCounted(WebFeature::kInvalidFragmentDirective);
if (test_case.second == kCounted) {
EXPECT_TRUE(is_use_counted)
<< "Expected invalid directive in case: " << test_case.first;
} else {
EXPECT_FALSE(is_use_counted)
<< "Expected valid directive in case: " << test_case.first;
}
}
}
// Test recording of the ListItemMatch metric
TEST_F(TextFragmentAnchorMetricsTest, ListItemMatch) {
SimRequest request("https://example.com/test.html#:~:text=list", "text/html");
LoadURL("https://example.com/test.html#:~:text=list");
request.Complete(R"HTML(
<!DOCTYPE html>
<ul>
<li>Some test content</li>
<li>Within a list item</li>
</ul>
)HTML");
RunAsyncMatchingTasks();
BeginEmptyFrame();
BeginEmptyFrame();
histogram_tester_.ExpectTotalCount("TextFragmentAnchor.Unknown.ListItemMatch",
1);
histogram_tester_.ExpectUniqueSample(
"TextFragmentAnchor.Unknown.ListItemMatch", 1, 1);
}
// Test recording of the TableCellMatch metric
TEST_F(TextFragmentAnchorMetricsTest, TableCellMatch) {
SimRequest request("https://example.com/test.html#:~:text=table",
"text/html");
LoadURL("https://example.com/test.html#:~:text=table");
request.Complete(R"HTML(
<!DOCTYPE html>
<table>
<tr>
<td>Some test content</td>
<td>Within a table cell</td>
</tr>
</table>
)HTML");
RunAsyncMatchingTasks();
BeginEmptyFrame();
BeginEmptyFrame();
histogram_tester_.ExpectTotalCount(
"TextFragmentAnchor.Unknown.TableCellMatch", 1);
histogram_tester_.ExpectUniqueSample(
"TextFragmentAnchor.Unknown.TableCellMatch", 1, 1);
}
// Test recording of ListItemMatch for a match nested in a list item
TEST_F(TextFragmentAnchorMetricsTest, NestedListItemMatch) {
SimRequest request("https://example.com/test.html#:~:text=list", "text/html");
LoadURL("https://example.com/test.html#:~:text=list");
request.Complete(R"HTML(
<!DOCTYPE html>
<ol>
<li>Some test content</li>
<li>Within a <span>list</span> item</li>
</ol>
)HTML");
RunAsyncMatchingTasks();
BeginEmptyFrame();
BeginEmptyFrame();
histogram_tester_.ExpectTotalCount("TextFragmentAnchor.Unknown.ListItemMatch",
1);
histogram_tester_.ExpectUniqueSample(
"TextFragmentAnchor.Unknown.ListItemMatch", 1, 1);
}
// Test recording of TableCellMatch for a match nested in a table cell
TEST_F(TextFragmentAnchorMetricsTest, NestedTableCellMatch) {
SimRequest request("https://example.com/test.html#:~:text=table",
"text/html");
LoadURL("https://example.com/test.html#:~:text=table");
request.Complete(R"HTML(
<!DOCTYPE html>
<table>
<tr>
<td>Some test content</td>
<td>Within a <span>table</span> cell</td>
</tr>
</table>
)HTML");
RunAsyncMatchingTasks();
BeginEmptyFrame();
BeginEmptyFrame();
histogram_tester_.ExpectTotalCount(
"TextFragmentAnchor.Unknown.TableCellMatch", 1);
histogram_tester_.ExpectUniqueSample(
"TextFragmentAnchor.Unknown.TableCellMatch", 1, 1);
}
class TextFragmentRelatedMetricTest : public TextFragmentAnchorMetricsTest,
public testing::WithParamInterface<bool> {
public:
TextFragmentRelatedMetricTest() : text_fragment_anchors_state_(GetParam()) {}
private:
ScopedTextFragmentIdentifiersForTest text_fragment_anchors_state_;
};
// These tests will run with and without the TextFragmentIdentifiers feature
// enabled to ensure we collect metrics correctly under both situations.
INSTANTIATE_TEST_SUITE_P(All,
TextFragmentRelatedMetricTest,
testing::Values(false, true));
// Test that we correctly track failed vs. successful element-id lookups. We
// only count these in cases where we don't have a text directive, when the REF
// is enabled.
TEST_P(TextFragmentRelatedMetricTest, ElementIdSuccessFailureCounts) {
const int kUncounted = 0;
const int kFound = 1;
const int kNotFound = 2;
// When the TextFragmentAnchors feature is on, we should avoid counting the
// result of the element-id fragment if a text directive is successfully
// parsed. If the feature is off we treat the text directive as an element-id
// and should count the result.
const int kUncountedOrFound = GetParam() ? kUncounted : kFound;
// Note: We'll strip the fragment directive (i.e. anything after :~:) leaving
// just the element anchor. The fragment directive stripping behavior is now
// shipped unflagged so it should always be performed.
Vector<std::pair<String, int>> test_cases = {
{"", kUncounted},
{"#element", kFound},
{"#doesntExist", kNotFound},
// `:~:foo` will be stripped so #element will be found and #doesntexist
// ##element will be not found.
{"#element:~:foo", kFound},
{"#doesntexist:~:foo", kNotFound},
{"##element", kNotFound},
// If the feature is on, `:~:text=` will parse so we shouldn't count.
// Otherwise, it'll just be stripped so #element will be found.
{"#element:~:text=doesntexist", kUncountedOrFound},
{"#element:~:text=page", kUncountedOrFound},
// If the feature is on, `:~:text` is parsed so we don't count. If it's
// off the entire fragment is a directive that's stripped so no search is
// performed either.
{"#:~:text=doesntexist", kUncounted},
{"#:~:text=page", kUncounted},
{"#:~:text=name", kUncounted},
// If the feature is enabled, `:~:text` parses and we don't count the
// element-id. If the feature is off, we still strip the :~: directive
// and the remaining fragment does match an element id.
{"#element:~:text=name", kUncountedOrFound}};
const int kNotFoundSample = 0;
const int kFoundSample = 1;
const std::string histogram = "TextFragmentAnchor.ElementIdFragmentFound";
// Add counts to each histogram so that calls to GetBucketCount won't fail
// due to not finding the histogram.
UMA_HISTOGRAM_BOOLEAN(histogram, true);
UMA_HISTOGRAM_BOOLEAN(histogram, false);
int expected_found_count = 1;
int expected_not_found_count = 1;
for (auto test_case : test_cases) {
String url = "https://example.com/test.html" + test_case.first;
SimRequest request(url, "text/html");
LoadURL(url);
request.Complete(R"HTML(
<!DOCTYPE html>
<p id="element">This is a test page</p>
<p id=":~:text=name">This is a test page</p>
<p id="element:~:text=name">This is a test page</p>
)HTML");
// Render two frames to handle the async step added by the beforematch
// event.
Compositor().BeginFrame();
BeginEmptyFrame();
RunAsyncMatchingTasks();
auto not_found_count =
histogram_tester_.GetBucketCount(histogram, kNotFoundSample);
auto found_count =
histogram_tester_.GetBucketCount(histogram, kFoundSample);
int result = test_case.second;
if (result == kFound) {
++expected_found_count;
ASSERT_EQ(expected_found_count, found_count)
<< "ElementId should have been |Found| but did not UseCount on case: "
<< test_case.first;
ASSERT_EQ(expected_not_found_count, not_found_count)
<< "ElementId should have been |Found| but reported |NotFound| on "
"case: "
<< test_case.first;
} else if (result == kNotFound) {
++expected_not_found_count;
ASSERT_EQ(expected_not_found_count, not_found_count)
<< "ElementId should have been |NotFound| but did not UseCount on "
"case: "
<< test_case.first;
ASSERT_EQ(expected_found_count, found_count)
<< "ElementId should have been |NotFound| but reported |Found| on "
"case: "
<< test_case.first;
} else {
DCHECK_EQ(result, kUncounted);
ASSERT_EQ(expected_found_count, found_count)
<< "Case should not have been counted but reported |Found| on case: "
<< test_case.first;
ASSERT_EQ(expected_not_found_count, not_found_count)
<< "Case should not have been counted but reported |NotFound| on "
"case: "
<< test_case.first;
}
}
}
// Test counting occurrences of ~&~ in the URL fragment. Used for potentially
// using ~&~ as a delimiter. Can be removed once the feature ships.
TEST_P(TextFragmentRelatedMetricTest, TildeAmpersandTildeUseCounter) {
const int kUncounted = 0;
const int kCounted = 1;
Vector<std::pair<String, int>> test_cases = {{"", kUncounted},
{"#element", kUncounted},
{"#doesntExist", kUncounted},
{"#~&~element", kCounted},
{"#element~&~", kCounted},
{"#foo~&~bar", kCounted},
{"#foo~&~text=foo", kCounted}};
for (auto test_case : test_cases) {
String url = "https://example.com/test.html" + test_case.first;
SimRequest request(url, "text/html");
LoadURL(url);
request.Complete(R"HTML(
<!DOCTYPE html>
<p id="element">This is a test page</p>
)HTML");
// Render two frames to handle the async step added by the beforematch
// event.
Compositor().BeginFrame();
BeginEmptyFrame();
RunAsyncMatchingTasks();
bool is_use_counted =
GetDocument().IsUseCounted(WebFeature::kFragmentHasTildeAmpersandTilde);
if (test_case.second == kCounted) {
EXPECT_TRUE(is_use_counted)
<< "Expected to count ~&~ but didn't in case: " << test_case.first;
} else {
EXPECT_FALSE(is_use_counted)
<< "Expected not to count ~&~ but did in case: " << test_case.first;
}
}
}
// Test counting occurrences of ~@~ in the URL fragment. Used for potentially
// using ~@~ as a delimiter. Can be removed once the feature ships.
TEST_P(TextFragmentRelatedMetricTest, TildeAtTildeUseCounter) {
const int kUncounted = 0;
const int kCounted = 1;
Vector<std::pair<String, int>> test_cases = {{"", kUncounted},
{"#element", kUncounted},
{"#doesntExist", kUncounted},
{"#~@~element", kCounted},
{"#element~@~", kCounted},
{"#foo~@~bar", kCounted},
{"#foo~@~text=foo", kCounted}};
for (auto test_case : test_cases) {
String url = "https://example.com/test.html" + test_case.first;
SimRequest request(url, "text/html");
LoadURL(url);
request.Complete(R"HTML(
<!DOCTYPE html>
<p id="element">This is a test page</p>
)HTML");
// Render two frames to handle the async step added by the beforematch
// event.
Compositor().BeginFrame();
BeginEmptyFrame();
RunAsyncMatchingTasks();
bool is_use_counted =
GetDocument().IsUseCounted(WebFeature::kFragmentHasTildeAtTilde);
if (test_case.second == kCounted) {
EXPECT_TRUE(is_use_counted)
<< "Expected to count ~@~ but didn't in case: " << test_case.first;
} else {
EXPECT_FALSE(is_use_counted)
<< "Expected not to count ~@~ but did in case: " << test_case.first;
}
}
}
// Test counting occurrences of &delimiter? in the URL fragment. Used for
// potentially using &delimiter? as a delimiter. Can be removed once the
// feature ships.
TEST_P(TextFragmentRelatedMetricTest, AmpersandDelimiterQuestionUseCounter) {
const int kUncounted = 0;
const int kCounted = 1;
Vector<std::pair<String, int>> test_cases = {
{"", kUncounted},
{"#element", kUncounted},
{"#doesntExist", kUncounted},
{"#&delimiter?element", kCounted},
{"#element&delimiter?", kCounted},
{"#foo&delimiter?bar", kCounted},
{"#foo&delimiter?text=foo", kCounted}};
for (auto test_case : test_cases) {
String url = "https://example.com/test.html" + test_case.first;
SimRequest request(url, "text/html");
LoadURL(url);
request.Complete(R"HTML(
<!DOCTYPE html>
<p id="element">This is a test page</p>
)HTML");
// Render two frames to handle the async step added by the beforematch
// event.
Compositor().BeginFrame();
BeginEmptyFrame();
RunAsyncMatchingTasks();
bool is_use_counted = GetDocument().IsUseCounted(
WebFeature::kFragmentHasAmpersandDelimiterQuestion);
if (test_case.second == kCounted) {
EXPECT_TRUE(is_use_counted)
<< "Expected to count &delimiter? but didn't in case: "
<< test_case.first;
} else {
EXPECT_FALSE(is_use_counted)
<< "Expected not to count &delimiter? but did in case: "
<< test_case.first;
}
}
}
// Test counting occurrences of non-directive :~: in the URL fragment. Used to
// ensure :~: is web-compatible; can be removed once the feature ships.
TEST_P(TextFragmentRelatedMetricTest, NewDelimiterUseCounter) {
const int kUncounted = 0;
const int kCounted = 1;
Vector<std::pair<String, int>> test_cases = {{"", kUncounted},
{"#element", kUncounted},
{"#doesntExist", kUncounted},
{"#:~:element", kCounted},
{"#element:~:", kCounted},
{"#foo:~:bar", kCounted},
{"#:~:utext=foo", kCounted},
{"#:~:text=foo", kUncounted},
{"#foo:~:text=foo", kUncounted}};
for (auto test_case : test_cases) {
String url = "https://example.com/test.html" + test_case.first;
SimRequest request(url, "text/html");
LoadURL(url);
request.Complete(R"HTML(
<!DOCTYPE html>
<p id="element">This is a test page</p>
)HTML");
// Render two frames to handle the async step added by the beforematch
// event.
Compositor().BeginFrame();
BeginEmptyFrame();
RunAsyncMatchingTasks();
bool is_use_counted =
GetDocument().IsUseCounted(WebFeature::kFragmentHasColonTildeColon);
if (test_case.second == kCounted) {
EXPECT_TRUE(is_use_counted)
<< "Expected to count :~: but didn't in case: " << test_case.first;
} else {
EXPECT_FALSE(is_use_counted)
<< "Expected not to count :~: but did in case: " << test_case.first;
}
}
}
// Test use counting the document.fragmentDirective API
TEST_P(TextFragmentRelatedMetricTest, TextFragmentAPIUseCounter) {
SimRequest request("https://example.com/test.html", "text/html");
LoadURL("https://example.com/test.html");
request.Complete(R"HTML(
<!DOCTYPE html>
<script>
var textFragmentsSupported = typeof(document.fragmentDirective) == "object";
</script>
<p>This is a test page</p>
)HTML");
Compositor().BeginFrame();
RunAsyncMatchingTasks();
bool text_fragments_enabled = GetParam();
EXPECT_EQ(text_fragments_enabled,
GetDocument().IsUseCounted(
WebFeature::kV8Document_FragmentDirective_AttributeGetter));
}
// Test that simply activating a text fragment does not use count the API
TEST_P(TextFragmentRelatedMetricTest, TextFragmentActivationDoesNotCountAPI) {
SimRequest request("https://example.com/test.html#:~:text=test", "text/html");
LoadURL("https://example.com/test.html#:~:text=test");
request.Complete(R"HTML(
<!DOCTYPE html>
<p>This is a test page</p>
)HTML");
Compositor().BeginFrame();
RunAsyncMatchingTasks();
bool text_fragments_enabled = GetParam();
EXPECT_EQ(text_fragments_enabled,
GetDocument().IsUseCounted(WebFeature::kTextFragmentAnchor));
EXPECT_FALSE(GetDocument().IsUseCounted(
WebFeature::kV8Document_FragmentDirective_AttributeGetter));
}
// Test recording of the SpansMultipleBlocks metric. Records true because the
// range crosses an intervening block element.
TEST_F(TextFragmentAnchorMetricsTest, SpansMultipleBlocksInterveningBlock) {
SimRequest request("https://example.com/test.html#:~:text=start,end",
"text/html");
LoadURL("https://example.com/test.html#:~:text=start,end");
request.Complete(R"HTML(
<!DOCTYPE html>
<div>
start of text
<div>block</div>
text end
</div>
)HTML");
RunAsyncMatchingTasks();
BeginEmptyFrame();
BeginEmptyFrame();
histogram_tester_.ExpectTotalCount(
"TextFragmentAnchor.Unknown.SpansMultipleBlocks", 1);
histogram_tester_.ExpectUniqueSample(
"TextFragmentAnchor.Unknown.SpansMultipleBlocks", 1, 1);
}
// Test recording of the SpansMultipleBlocks metric. Records true because the
// range start and end are in different block elements.
TEST_F(TextFragmentAnchorMetricsTest, SpansMultipleBlocks) {
SimRequest request("https://example.com/test.html#:~:text=start,end",
"text/html");
LoadURL("https://example.com/test.html#:~:text=start,end");
request.Complete(R"HTML(
<!DOCTYPE html>
<div>
<div>start of text</div>
text end
</div>
)HTML");
RunAsyncMatchingTasks();
BeginEmptyFrame();
BeginEmptyFrame();
histogram_tester_.ExpectTotalCount(
"TextFragmentAnchor.Unknown.SpansMultipleBlocks", 1);
histogram_tester_.ExpectUniqueSample(
"TextFragmentAnchor.Unknown.SpansMultipleBlocks", 1, 1);
}
// Test recording of the SpansMultipleBlocks metric. Records false because the
// range start and end are in the same block element with no intervening block.
TEST_F(TextFragmentAnchorMetricsTest, SpansMultipleBlocksSingleBlock) {
SimRequest request("https://example.com/test.html#:~:text=start,end",
"text/html");
LoadURL("https://example.com/test.html#:~:text=start,end");
request.Complete(R"HTML(
<!DOCTYPE html>
<div>
start of <i>text</i>
text end
</div>
)HTML");
RunAsyncMatchingTasks();
BeginEmptyFrame();
BeginEmptyFrame();
histogram_tester_.ExpectTotalCount(
"TextFragmentAnchor.Unknown.SpansMultipleBlocks", 1);
histogram_tester_.ExpectUniqueSample(
"TextFragmentAnchor.Unknown.SpansMultipleBlocks", 0, 1);
}
// Tests that a LinkOpened UKM Event is recorded upon a successful fragment
// highlight.
TEST_F(TextFragmentAnchorMetricsTest, LinkOpenedSuccessUKM) {
SimRequest request("https://example.com/test.html#:~:text=test%20page",
"text/html");
LoadURL("https://example.com/test.html#:~:text=test%20page");
request.Complete(R"HTML(
<!DOCTYPE html>
<style>
body {
height: 2200px;
}
p {
position: absolute;
top: 1000px;
}
</style>
<p>This is a test page</p>
)HTML");
RunAsyncMatchingTasks();
// Render two frames to handle the async step added by the beforematch event.
Compositor().BeginFrame();
BeginEmptyFrame();
// Flush UKM logging mojo request.
RunPendingTasks();
auto entries = ukm_recorder()->GetEntriesByName(
ukm::builders::SharedHighlights_LinkOpened::kEntryName);
ASSERT_EQ(1u, entries.size());
const ukm::mojom::UkmEntry* entry = entries[0];
EXPECT_EQ(GetDocument().UkmSourceID(), entry->source_id);
ukm_recorder()->ExpectEntryMetric(entry, kSuccessUkmMetric,
/*expected_value=*/true);
EXPECT_TRUE(ukm_recorder()->GetEntryMetric(entry, kSourceUkmMetric));
}
// Tests that a LinkOpened UKM Event is recorded upon a failed fragment
// highlight.
TEST_F(TextFragmentAnchorMetricsTest, LinkOpenedFailedUKM) {
SimRequest request(
"https://example.com/test.html#:~:text=not%20on%20the%20page",
"text/html");
LoadURL("https://example.com/test.html#:~:text=not%20on%20the%20page");
request.Complete(R"HTML(
<!DOCTYPE html>
<style>
body {
height: 2200px;
}
p {
position: absolute;
top: 1000px;
}
</style>
<p>This is a test page</p>
)HTML");
RunAsyncMatchingTasks();
// Render two frames to handle the async step added by the beforematch event.
Compositor().BeginFrame();
BeginEmptyFrame();
// Flush UKM logging mojo request.
RunPendingTasks();
auto entries = ukm_recorder()->GetEntriesByName(
ukm::builders::SharedHighlights_LinkOpened::kEntryName);
ASSERT_EQ(1u, entries.size());
const ukm::mojom::UkmEntry* entry = entries[0];
EXPECT_EQ(GetDocument().UkmSourceID(), entry->source_id);
ukm_recorder()->ExpectEntryMetric(entry, kSuccessUkmMetric,
/*expected_value=*/false);
EXPECT_TRUE(ukm_recorder()->GetEntryMetric(entry, kSourceUkmMetric));
}
// Tests that loading a page that has a ForceLoadAtTop DocumentPolicy invokes
// the UseCounter.
TEST_F(TextFragmentAnchorMetricsTest, ForceLoadAtTopUseCounter) {
SimRequest::Params params;
params.response_http_headers.insert("Document-Policy", "force-load-at-top");
SimRequest request("https://example.com/test.html", "text/html", params);
LoadURL("https://example.com/test.html");
request.Complete(R"HTML(
<!DOCTYPE html>
<p>This is a test page</p>
)HTML");
RunAsyncMatchingTasks();
// Render two frames to handle the async step added by the beforematch event.
Compositor().BeginFrame();
BeginEmptyFrame();
EXPECT_TRUE(GetDocument().IsUseCounted(WebFeature::kForceLoadAtTop));
}
// Tests that loading a page that explicitly disables ForceLoadAtTop
// DocumentPolicy or has no DocumentPolicy doesn't invoke the UseCounter for
// ForceLoadAtTop.
TEST_F(TextFragmentAnchorMetricsTest, NoForceLoadAtTopUseCounter) {
SimRequest::Params params;
params.response_http_headers.insert("Document-Policy",
"no-force-load-at-top");
SimRequest request("https://example.com/test.html", "text/html", params);
LoadURL("https://example.com/test.html");
request.Complete(R"HTML(
<!DOCTYPE html>
<p>This is a test page</p>
)HTML");
RunAsyncMatchingTasks();
// Render two frames to handle the async step added by the beforematch event.
Compositor().BeginFrame();
BeginEmptyFrame();
EXPECT_FALSE(GetDocument().IsUseCounted(WebFeature::kForceLoadAtTop));
// Try without any DocumentPolicy headers.
SimRequest request2("https://example.com/test2.html", "text/html");
LoadURL("https://example.com/test2.html");
request2.Complete(R"HTML(
<!DOCTYPE html>
<p>This is a different test page</p>
)HTML");
RunAsyncMatchingTasks();
Compositor().BeginFrame();
BeginEmptyFrame();
EXPECT_FALSE(GetDocument().IsUseCounted(WebFeature::kForceLoadAtTop));
}
} // namespace blink