| // 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 "third_party/blink/renderer/core/html/anchor_element_metrics.h" |
| |
| #include "base/optional.h" |
| #include "base/test/scoped_feature_list.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| #include "third_party/blink/public/common/features.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/html/html_anchor_element.h" |
| #include "third_party/blink/renderer/core/html/html_iframe_element.h" |
| #include "third_party/blink/renderer/core/paint/paint_layer_scrollable_area.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/wtf/text/atomic_string.h" |
| |
| namespace blink { |
| |
| class AnchorElementMetricsTest : public SimTest { |
| public: |
| static constexpr int kViewportWidth = 400; |
| static constexpr int kViewportHeight = 600; |
| |
| // Helper function to test IsUrlIncrementedByOne(). |
| bool IsIncrementedByOne(const String& source, const String& target) { |
| SimRequest main_resource(source, "text/html"); |
| LoadURL(source); |
| main_resource.Complete("<a id='anchor' href=''>example</a>"); |
| HTMLAnchorElement* anchor_element = |
| ToHTMLAnchorElement(GetDocument().getElementById("anchor")); |
| anchor_element->SetHref(AtomicString(target)); |
| |
| return AnchorElementMetrics::MaybeReportClickedMetricsOnClick( |
| anchor_element) |
| .value() |
| .GetIsUrlIncrementedByOne(); |
| } |
| |
| protected: |
| AnchorElementMetricsTest() = default; |
| |
| void SetUp() override { |
| SimTest::SetUp(); |
| WebView().MainFrameWidget()->Resize( |
| WebSize(kViewportWidth, kViewportHeight)); |
| feature_list_.InitAndEnableFeature(features::kRecordAnchorMetricsClicked); |
| } |
| |
| base::test::ScopedFeatureList feature_list_; |
| }; |
| |
| // Test for IsUrlIncrementedByOne(). |
| TEST_F(AnchorElementMetricsTest, IsUrlIncrementedByOne) { |
| EXPECT_TRUE( |
| IsIncrementedByOne("http://example.com/p1", "http://example.com/p2")); |
| EXPECT_TRUE(IsIncrementedByOne("http://example.com/?p=9", |
| "http://example.com/?p=10")); |
| EXPECT_TRUE(IsIncrementedByOne("http://example.com/?p=12", |
| "http://example.com/?p=13")); |
| EXPECT_TRUE(IsIncrementedByOne("http://example.com/p9/cat1", |
| "http://example.com/p10/cat1")); |
| EXPECT_FALSE( |
| IsIncrementedByOne("http://example.com/1", "https://example.com/2")); |
| EXPECT_FALSE( |
| IsIncrementedByOne("http://example.com/1", "http://google.com/2")); |
| EXPECT_FALSE( |
| IsIncrementedByOne("http://example.com/p1", "http://example.com/p1")); |
| EXPECT_FALSE( |
| IsIncrementedByOne("http://example.com/p2", "http://example.com/p1")); |
| EXPECT_FALSE(IsIncrementedByOne("http://example.com/p9/cat1", |
| "http://example.com/p10/cat2")); |
| } |
| |
| // Test that Finch can control the collection of anchor element metrics. |
| TEST_F(AnchorElementMetricsTest, FinchControl) { |
| HistogramTester histogram_tester; |
| |
| SimRequest resource("https://example.com/", "text/html"); |
| LoadURL("https://example.com/"); |
| resource.Complete("<a id='anchor' href='https://google.com/'>google</a>"); |
| HTMLAnchorElement* anchor_element = |
| ToHTMLAnchorElement(GetDocument().getElementById("anchor")); |
| |
| // With feature kRecordAnchorMetricsClicked disabled, we should not see any |
| // count in histograms. |
| base::test::ScopedFeatureList disabled_feature_list; |
| disabled_feature_list.InitAndDisableFeature( |
| features::kRecordAnchorMetricsClicked); |
| AnchorElementMetrics::MaybeReportClickedMetricsOnClick(anchor_element); |
| histogram_tester.ExpectTotalCount("AnchorElementMetrics.Clicked.RatioArea", |
| 0); |
| |
| // If we enable feature kRecordAnchorMetricsClicked, we should see count is 1 |
| // in histograms. |
| base::test::ScopedFeatureList enabled_feature_list; |
| enabled_feature_list.InitAndEnableFeature( |
| features::kRecordAnchorMetricsClicked); |
| AnchorElementMetrics::MaybeReportClickedMetricsOnClick(anchor_element); |
| histogram_tester.ExpectTotalCount("AnchorElementMetrics.Clicked.RatioArea", |
| 1); |
| } |
| |
| // Test that non-HTTP URLs are not reported. |
| TEST_F(AnchorElementMetricsTest, NonHTTPOnClick) { |
| HistogramTester histogram_tester; |
| |
| // Tests that an HTTPS page with a data anchor is not reported when the anchor |
| // is clicked. |
| SimRequest http_resource("https://example.com/", "text/html"); |
| LoadURL("https://example.com/"); |
| http_resource.Complete("<a id='anchor' href='data://google.com/'>google</a>"); |
| HTMLAnchorElement* anchor_element = |
| ToHTMLAnchorElement(GetDocument().getElementById("anchor")); |
| |
| AnchorElementMetrics::MaybeReportClickedMetricsOnClick(anchor_element); |
| histogram_tester.ExpectTotalCount("AnchorElementMetrics.Clicked.RatioArea", |
| 0); |
| |
| // Tests that a data page with an HTTPS anchor is not reported when the anchor |
| // is clicked. |
| LoadURL( |
| "data:text/html,<a id='anchor' href='https://google.com/'>google</a>"); |
| anchor_element = ToHTMLAnchorElement(GetDocument().getElementById("anchor")); |
| |
| AnchorElementMetrics::MaybeReportClickedMetricsOnClick(anchor_element); |
| histogram_tester.ExpectTotalCount("AnchorElementMetrics.Clicked.RatioArea", |
| 0); |
| |
| // Tests that an HTTPS page with an HTTPS anchor is reported when the anchor |
| // is clicked. |
| SimRequest http_resource_2("https://example.com/", "text/html"); |
| LoadURL("https://example.com/"); |
| http_resource_2.Complete( |
| "<a id='anchor' href='https://google.com/'>google</a>"); |
| anchor_element = ToHTMLAnchorElement(GetDocument().getElementById("anchor")); |
| |
| AnchorElementMetrics::MaybeReportClickedMetricsOnClick(anchor_element); |
| histogram_tester.ExpectTotalCount("AnchorElementMetrics.Clicked.RatioArea", |
| 1); |
| } |
| |
| // The main frame contains an anchor element, which contains an image element. |
| TEST_F(AnchorElementMetricsTest, AnchorFeatureImageLink) { |
| SimRequest main_resource("https://example.com/", "text/html"); |
| |
| LoadURL("https://example.com/"); |
| |
| main_resource.Complete(String::Format( |
| R"HTML( |
| <body style='margin: 0px'> |
| <div style='height: %dpx;'></div> |
| <a id='anchor' href="https://example.com/page2"> |
| <img height="300" width="200"> |
| </a> |
| <div style='height: %d;'></div> |
| </body>)HTML", |
| kViewportHeight / 2, 10 * kViewportHeight)); |
| |
| Element* anchor = GetDocument().getElementById("anchor"); |
| HTMLAnchorElement* anchor_element = ToHTMLAnchorElement(anchor); |
| |
| auto feature = |
| AnchorElementMetrics::MaybeReportClickedMetricsOnClick(anchor_element) |
| .value(); |
| EXPECT_FLOAT_EQ(0.25, feature.GetRatioArea()); |
| EXPECT_FLOAT_EQ(0.25, feature.GetRatioVisibleArea()); |
| EXPECT_FLOAT_EQ(0.5, feature.GetRatioDistanceTopToVisibleTop()); |
| EXPECT_FLOAT_EQ(0.75, feature.GetRatioDistanceCenterToVisibleTop()); |
| EXPECT_FLOAT_EQ(0.5, feature.GetRatioDistanceRootTop()); |
| EXPECT_FLOAT_EQ(10, feature.GetRatioDistanceRootBottom()); |
| EXPECT_FALSE(feature.GetIsInIframe()); |
| EXPECT_TRUE(feature.GetContainsImage()); |
| EXPECT_TRUE(feature.GetIsSameHost()); |
| EXPECT_FALSE(feature.GetIsUrlIncrementedByOne()); |
| } |
| |
| // The main frame contains an anchor element. |
| // Features of the element are extracted. |
| // Then the test scrolls down to check features again. |
| TEST_F(AnchorElementMetricsTest, AnchorFeatureExtract) { |
| SimRequest main_resource("https://example.com/", "text/html"); |
| |
| LoadURL("https://example.com/"); |
| |
| main_resource.Complete(String::Format( |
| R"HTML( |
| <body style='margin: 0px'> |
| <div style='height: %dpx;'></div> |
| <a id='anchor' href="https://b.example.com">example</a> |
| <div style='height: %d;'></div> |
| </body>)HTML", |
| 2 * kViewportHeight, 10 * kViewportHeight)); |
| |
| Element* anchor = GetDocument().getElementById("anchor"); |
| HTMLAnchorElement* anchor_element = ToHTMLAnchorElement(anchor); |
| |
| auto feature = |
| AnchorElementMetrics::MaybeReportClickedMetricsOnClick(anchor_element) |
| .value(); |
| EXPECT_GT(feature.GetRatioArea(), 0); |
| EXPECT_FLOAT_EQ(feature.GetRatioDistanceRootTop(), 2); |
| EXPECT_FLOAT_EQ(feature.GetRatioDistanceTopToVisibleTop(), 2); |
| EXPECT_EQ(feature.GetIsInIframe(), false); |
| |
| // Element not in the viewport. |
| EXPECT_GT(feature.GetRatioArea(), 0); |
| EXPECT_FLOAT_EQ(0, feature.GetRatioVisibleArea()); |
| EXPECT_FLOAT_EQ(2, feature.GetRatioDistanceTopToVisibleTop()); |
| EXPECT_LT(2, feature.GetRatioDistanceCenterToVisibleTop()); |
| EXPECT_FLOAT_EQ(2, feature.GetRatioDistanceRootTop()); |
| EXPECT_FLOAT_EQ(10, feature.GetRatioDistanceRootBottom()); |
| EXPECT_FALSE(feature.GetIsInIframe()); |
| EXPECT_FALSE(feature.GetContainsImage()); |
| EXPECT_FALSE(feature.GetIsSameHost()); |
| EXPECT_FALSE(feature.GetIsUrlIncrementedByOne()); |
| |
| // Scroll down to the anchor element. |
| GetDocument().View()->LayoutViewport()->SetScrollOffset( |
| ScrollOffset(0, kViewportHeight * 1.5), kProgrammaticScroll); |
| |
| auto feature2 = |
| AnchorElementMetrics::MaybeReportClickedMetricsOnClick(anchor_element) |
| .value(); |
| EXPECT_LT(0, feature2.GetRatioVisibleArea()); |
| EXPECT_FLOAT_EQ(0.5, feature2.GetRatioDistanceTopToVisibleTop()); |
| EXPECT_LT(0.5, feature2.GetRatioDistanceCenterToVisibleTop()); |
| EXPECT_FLOAT_EQ(2, feature2.GetRatioDistanceRootTop()); |
| EXPECT_FLOAT_EQ(10, feature2.GetRatioDistanceRootBottom()); |
| } |
| |
| // The main frame contains an iframe. The iframe contains an anchor element. |
| // Features of the element are extracted. |
| // Then the test scrolls down in the main frame to check features again. |
| // Then the test scrolls down in the iframe to check features again. |
| TEST_F(AnchorElementMetricsTest, AnchorFeatureInIframe) { |
| SimRequest main_resource("https://example.com/page1", "text/html"); |
| SimRequest iframe_resource("https://example.com/iframe.html", "text/html"); |
| SimSubresourceRequest image_resource("https://example.com/cat.png", |
| "image/png"); |
| |
| LoadURL("https://example.com/page1"); |
| |
| main_resource.Complete(String::Format( |
| R"HTML( |
| <body style='margin: 0px'> |
| <div style='height: %dpx;'></div> |
| <iframe id='iframe' src='https://example.com/iframe.html' |
| style='width: 300px; height: %dpx; |
| border-style: none; padding: 0px; margin: 0px;'></iframe> |
| <div style='height: %dpx;'></div> |
| </body>)HTML", |
| 2 * kViewportHeight, kViewportHeight / 2, 10 * kViewportHeight)); |
| |
| iframe_resource.Complete(String::Format( |
| R"HTML( |
| <body style='margin: 0px'> |
| <div style='height: %dpx;'></div> |
| <a id='anchor' href="https://example.com/page2">example</a> |
| <div style='height: %dpx;'></div> |
| </body>)HTML", |
| kViewportHeight / 2, 5 * kViewportHeight)); |
| |
| Element* iframe = GetDocument().getElementById("iframe"); |
| HTMLIFrameElement* iframe_element = ToHTMLIFrameElement(iframe); |
| Frame* sub = iframe_element->ContentFrame(); |
| LocalFrame* subframe = ToLocalFrame(sub); |
| |
| Element* anchor = subframe->GetDocument()->getElementById("anchor"); |
| HTMLAnchorElement* anchor_element = ToHTMLAnchorElement(anchor); |
| |
| auto feature = |
| AnchorElementMetrics::MaybeReportClickedMetricsOnClick(anchor_element) |
| .value(); |
| EXPECT_LT(0, feature.GetRatioArea()); |
| EXPECT_FLOAT_EQ(0, feature.GetRatioVisibleArea()); |
| EXPECT_FLOAT_EQ(2.5, feature.GetRatioDistanceTopToVisibleTop()); |
| EXPECT_LT(2.5, feature.GetRatioDistanceCenterToVisibleTop()); |
| EXPECT_FLOAT_EQ(2.5, feature.GetRatioDistanceRootTop()); |
| EXPECT_TRUE(feature.GetIsInIframe()); |
| EXPECT_FALSE(feature.GetContainsImage()); |
| EXPECT_TRUE(feature.GetIsSameHost()); |
| EXPECT_TRUE(feature.GetIsUrlIncrementedByOne()); |
| |
| // Scroll down the main frame. |
| GetDocument().View()->LayoutViewport()->SetScrollOffset( |
| ScrollOffset(0, kViewportHeight * 1.8), kProgrammaticScroll); |
| |
| auto feature2 = |
| AnchorElementMetrics::MaybeReportClickedMetricsOnClick(anchor_element) |
| .value(); |
| EXPECT_LT(0, feature2.GetRatioVisibleArea()); |
| EXPECT_FLOAT_EQ(0.7, feature2.GetRatioDistanceTopToVisibleTop()); |
| EXPECT_FLOAT_EQ(2.5, feature2.GetRatioDistanceRootTop()); |
| |
| // Scroll down inside iframe. Now the anchor element is visible. |
| subframe->View()->LayoutViewport()->SetScrollOffset( |
| ScrollOffset(0, kViewportHeight * 0.2), kProgrammaticScroll); |
| |
| auto feature3 = |
| AnchorElementMetrics::MaybeReportClickedMetricsOnClick(anchor_element) |
| .value(); |
| EXPECT_LT(0, feature3.GetRatioVisibleArea()); |
| EXPECT_FLOAT_EQ(0.5, feature3.GetRatioDistanceTopToVisibleTop()); |
| EXPECT_FLOAT_EQ(2.5, feature3.GetRatioDistanceRootTop()); |
| // The distance is expected to be 10.2 - height of the anchor element. |
| EXPECT_GT(10.2, feature3.GetRatioDistanceRootBottom()); |
| } |
| |
| TEST_F(AnchorElementMetricsTest, AnchorFeatureInIframeNonHttp) { |
| SimRequest main_resource("content://example.com/page1", "text/html"); |
| SimRequest iframe_resource("https://example.com/iframe.html", "text/html"); |
| SimSubresourceRequest image_resource("https://example.com/cat.png", |
| "image/png"); |
| |
| LoadURL("content://example.com/page1"); |
| |
| main_resource.Complete(String::Format( |
| R"HTML( |
| <body style='margin: 0px'> |
| <div style='height: %dpx;'></div> |
| <iframe id='iframe' src='https://example.com/iframe.html' |
| style='width: 300px; height: %dpx; |
| border-style: none; padding: 0px; margin: 0px;'></iframe> |
| <div style='height: %dpx;'></div> |
| </body>)HTML", |
| 2 * kViewportHeight, kViewportHeight / 2, 10 * kViewportHeight)); |
| |
| iframe_resource.Complete(String::Format( |
| R"HTML( |
| <body style='margin: 0px'> |
| <div style='height: %dpx;'></div> |
| <a id='anchor' href="https://example.com/page2">example</a> |
| <div style='height: %dpx;'></div> |
| </body>)HTML", |
| kViewportHeight / 2, 5 * kViewportHeight)); |
| |
| Element* iframe = GetDocument().getElementById("iframe"); |
| HTMLIFrameElement* iframe_element = ToHTMLIFrameElement(iframe); |
| Frame* sub = iframe_element->ContentFrame(); |
| LocalFrame* subframe = ToLocalFrame(sub); |
| |
| Element* anchor = subframe->GetDocument()->getElementById("anchor"); |
| HTMLAnchorElement* anchor_element = ToHTMLAnchorElement(anchor); |
| |
| EXPECT_FALSE( |
| AnchorElementMetrics::MaybeReportClickedMetricsOnClick(anchor_element) |
| .has_value()); |
| } |
| |
| } // namespace blink |