// 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 "base/test/scoped_feature_list.h" #include "build/build_config.h" #include "components/shared_highlighting/core/common/shared_highlighting_features.h" #include "testing/gtest/include/gtest/gtest.h" #include "third_party/blink/public/common/input/web_menu_source_type.h" #include "third_party/blink/public/platform/scheduler/test/renderer_scheduler_test_support.h" #include "third_party/blink/public/platform/scheduler/web_thread_scheduler.h" #include "third_party/blink/public/public_buildflags.h" #include "third_party/blink/renderer/bindings/core/v8/v8_font_face_descriptors.h" #include "third_party/blink/renderer/bindings/core/v8/v8_mouse_event_init.h" #include "third_party/blink/renderer/bindings/core/v8/v8_union_arraybuffer_arraybufferview_string.h" #include "third_party/blink/renderer/core/css/font_face_set_document.h" #include "third_party/blink/renderer/core/dom/element.h" #include "third_party/blink/renderer/core/dom/range.h" #include "third_party/blink/renderer/core/editing/ephemeral_range.h" #include "third_party/blink/renderer/core/editing/frame_selection.h" #include "third_party/blink/renderer/core/editing/markers/document_marker_controller.h" #include "third_party/blink/renderer/core/frame/local_dom_window.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/frame/location.h" #include "third_party/blink/renderer/core/frame/web_local_frame_impl.h" #include "third_party/blink/renderer/core/html/html_element.h" #include "third_party/blink/renderer/core/html/html_frame_owner_element.h" #include "third_party/blink/renderer/core/input/context_menu_allowed_scope.h" #include "third_party/blink/renderer/core/input/event_handler.h" #include "third_party/blink/renderer/core/layout/layout_object.h" #include "third_party/blink/renderer/core/loader/document_loader.h" #include "third_party/blink/renderer/core/loader/empty_clients.h" #include "third_party/blink/renderer/core/page/context_menu_controller.h" #include "third_party/blink/renderer/core/page/scrolling/text_fragment_finder.h" #include "third_party/blink/renderer/core/paint/paint_layer_scrollable_area.h" #include "third_party/blink/renderer/core/scroll/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/unit_test_helpers.h" #if BUILDFLAG(ENABLE_UNHANDLED_TAP) #include "third_party/blink/public/mojom/unhandled_tap_notifier/unhandled_tap_notifier.mojom-blink.h" #include "third_party/blink/renderer/platform/testing/testing_platform_support.h" #endif // BUILDFLAG(ENABLE_UNHANDLED_TAP) namespace blink { namespace { using test::RunPendingTasks; class TextFragmentAnchorTest : public SimTest { public: void SetUp() override { SimTest::SetUp(); WebView().MainFrameViewWidget()->Resize(gfx::Size(800, 600)); } void RunAsyncMatchingTasks() { auto* scheduler = blink::scheduler::WebThreadScheduler::MainThreadScheduler(); blink::scheduler::RunIdleTasksForTesting(scheduler, base::BindOnce([]() {})); RunPendingTasks(); } 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(); } ScrollableArea* LayoutViewport() { return GetDocument().View()->LayoutViewport(); } IntRect ViewportRect() { return IntRect(IntPoint(), LayoutViewport()->VisibleContentRect().Size()); } IntRect BoundingRectInFrame(Node& node) { return node.GetLayoutObject()->AbsoluteBoundingBoxRect(); } 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); WebView().MainFrameWidget()->ProcessInputEventSynchronouslyForTesting( WebCoalescedInputEvent(event, ui::LatencyInfo()), base::DoNothing()); } void SimulateRightClick(int x, int y) { WebMouseEvent event(WebInputEvent::Type::kMouseDown, gfx::PointF(x, y), gfx::PointF(x, y), WebPointerProperties::Button::kRight, 0, WebInputEvent::Modifiers::kLeftButtonDown, base::TimeTicks::Now()); event.SetFrameScale(1); WebView().MainFrameWidget()->ProcessInputEventSynchronouslyForTesting( WebCoalescedInputEvent(event, ui::LatencyInfo()), base::DoNothing()); } void SimulateTap(int x, int y) { InjectEvent(WebInputEvent::Type::kTouchStart, x, y); InjectEvent(WebInputEvent::Type::kTouchEnd, x, y); InjectEvent(WebInputEvent::Type::kGestureTapDown, x, y); InjectEvent(WebInputEvent::Type::kGestureTapUnconfirmed, x, y); InjectEvent(WebInputEvent::Type::kGestureShowPress, x, y); InjectEvent(WebInputEvent::Type::kGestureTap, x, y); } void LoadAhem() { scoped_refptr shared_buffer = test::ReadFromFile(test::CoreTestDataPath("Ahem.ttf")); auto* buffer = MakeGarbageCollected( DOMArrayBuffer::Create(shared_buffer)); FontFace* ahem = FontFace::Create(GetDocument().GetFrame()->DomWindow(), "Ahem", buffer, FontFaceDescriptors::Create()); ScriptState* script_state = ToScriptStateForMainWorld(GetDocument().GetFrame()); DummyExceptionStateForTesting exception_state; FontFaceSetDocument::From(GetDocument()) ->addForBinding(script_state, ahem, exception_state); } private: void InjectEvent(WebInputEvent::Type type, int x, int y) { if (WebInputEvent::IsGestureEventType(type)) { WebGestureEvent event(type, WebInputEvent::kNoModifiers, base::TimeTicks::Now(), WebGestureDevice::kTouchscreen); event.SetPositionInWidget(gfx::PointF(x, y)); event.SetPositionInScreen(gfx::PointF(x, y)); event.SetFrameScale(1); WebView().MainFrameWidget()->ProcessInputEventSynchronouslyForTesting( WebCoalescedInputEvent(event, ui::LatencyInfo()), base::DoNothing()); } else if (WebInputEvent::IsTouchEventType(type)) { WebTouchEvent event(type, WebInputEvent::kNoModifiers, base::TimeTicks::Now()); event.SetFrameScale(1); WebPointerProperties pointer(0, WebPointerProperties::PointerType::kTouch, WebPointerProperties::Button::kNoButton, gfx::PointF(x, y), gfx::PointF(x, y)); event.touches[0] = pointer; if (type == WebInputEvent::Type::kTouchStart) event.touches[0].state = WebTouchPoint::State::kStatePressed; else if (type == WebInputEvent::Type::kTouchEnd) event.touches[0].state = WebTouchPoint::State::kStateReleased; WebView().MainFrameWidget()->ProcessInputEventSynchronouslyForTesting( WebCoalescedInputEvent(event, ui::LatencyInfo()), base::DoNothing()); WebView().MainFrameWidget()->DispatchBufferedTouchEvents(); } else { NOTREACHED() << "Only needed to support Gesture/Touch until now. " "Implement others if new modality is needed."; } } }; // Basic test case, ensure we scroll the matching text into view. TEST_F(TextFragmentAnchorTest, BasicSmokeTest) { SimRequest request("https://example.com/test.html#:~:text=test", "text/html"); LoadURL("https://example.com/test.html#:~:text=test"); request.Complete(R"HTML(

This is a test page

)HTML"); RunAsyncMatchingTasks(); // Render two frames to handle the async step added by the beforematch event. Compositor().BeginFrame(); Compositor().BeginFrame(); Element& p = *GetDocument().getElementById("text"); EXPECT_EQ(p, *GetDocument().CssTarget()); EXPECT_TRUE(ViewportRect().Contains(BoundingRectInFrame(p))) << "

Element wasn't scrolled into view, viewport's scroll offset: " << LayoutViewport()->GetScrollOffset().ToString(); } // Make sure a non-matching string doesn't cause scroll and the fragment is // removed when completed. TEST_F(TextFragmentAnchorTest, NonMatchingString) { SimRequest request("https://example.com/test.html#:~:text=unicorn", "text/html"); LoadURL("https://example.com/test.html#:~:text=unicorn"); request.Complete(R"HTML(

This is a test page

)HTML"); RunAsyncMatchingTasks(); // Render two frames to handle the async step added by the beforematch event. Compositor().BeginFrame(); BeginEmptyFrame(); EXPECT_EQ(ScrollOffset(), LayoutViewport()->GetScrollOffset()); // Force a layout GetDocument().body()->setAttribute(html_names::kStyleAttr, "height: 1300px"); Compositor().BeginFrame(); EXPECT_EQ(nullptr, GetDocument().CssTarget()); EXPECT_FALSE(GetDocument().View()->GetFragmentAnchor()); EXPECT_TRUE(GetDocument().Markers().Markers().IsEmpty()); } // Ensure multiple matches will scroll the first into view. TEST_F(TextFragmentAnchorTest, MultipleMatches) { SimRequest request("https://example.com/test.html#:~:text=test", "text/html"); LoadURL("https://example.com/test.html#:~:text=test"); request.Complete(R"HTML(

This is a test page

This is a test page

)HTML"); RunAsyncMatchingTasks(); // Render two frames to handle the async step added by the beforematch event. Compositor().BeginFrame(); Compositor().BeginFrame(); Element& first = *GetDocument().getElementById("first"); EXPECT_EQ(first, *GetDocument().CssTarget()); EXPECT_TRUE(ViewportRect().Contains(BoundingRectInFrame(first))) << "First

wasn't scrolled into view, viewport's scroll offset: " << LayoutViewport()->GetScrollOffset().ToString(); // Ensure we only report one marker. EXPECT_EQ(1u, GetDocument().Markers().Markers().size()); } // Ensure matching works inside nested blocks. TEST_F(TextFragmentAnchorTest, NestedBlocks) { SimRequest request("https://example.com/test.html#:~:text=test", "text/html"); LoadURL("https://example.com/test.html#:~:text=test"); request.Complete(R"HTML(

Some non-matching text

This is a test page

)HTML"); RunAsyncMatchingTasks(); // Render two frames to handle the async step added by the beforematch event. Compositor().BeginFrame(); Compositor().BeginFrame(); Element& match = *GetDocument().getElementById("match"); EXPECT_EQ(match, *GetDocument().CssTarget()); EXPECT_TRUE(ViewportRect().Contains(BoundingRectInFrame(match))) << "

wasn't scrolled into view, viewport's scroll offset: " << LayoutViewport()->GetScrollOffset().ToString(); } // Ensure multiple texts are highlighted and the first is scrolled into // view. TEST_F(TextFragmentAnchorTest, MultipleTextFragments) { SimRequest request("https://example.com/test.html#:~:text=test&text=more", "text/html"); LoadURL("https://example.com/test.html#:~:text=test&text=more"); request.Complete(R"HTML(

This is a test page

This is some more text

)HTML"); RunAsyncMatchingTasks(); // Render two frames to handle the async step added by the beforematch event. Compositor().BeginFrame(); Compositor().BeginFrame(); Element& first = *GetDocument().getElementById("first"); EXPECT_EQ(first, *GetDocument().CssTarget()); EXPECT_TRUE(ViewportRect().Contains(BoundingRectInFrame(first))) << "First

wasn't scrolled into view, viewport's scroll offset: " << LayoutViewport()->GetScrollOffset().ToString(); EXPECT_EQ(2u, GetDocument().Markers().Markers().size()); } // Ensure we scroll the second text into view if the first isn't found. TEST_F(TextFragmentAnchorTest, FirstTextFragmentNotFound) { SimRequest request("https://example.com/test.html#:~:text=test&text=more", "text/html"); LoadURL("https://example.com/test.html#:~:text=test&text=more"); request.Complete(R"HTML(

This is a page

This is some more text

)HTML"); RunAsyncMatchingTasks(); // Render two frames to handle the async step added by the beforematch event. Compositor().BeginFrame(); Compositor().BeginFrame(); Element& second = *GetDocument().getElementById("second"); EXPECT_EQ(second, *GetDocument().CssTarget()); EXPECT_TRUE(ViewportRect().Contains(BoundingRectInFrame(second))) << "Second

wasn't scrolled into view, viewport's scroll offset: " << LayoutViewport()->GetScrollOffset().ToString(); EXPECT_EQ(1u, GetDocument().Markers().Markers().size()); } // Ensure we still scroll the first text into view if the second isn't // found. TEST_F(TextFragmentAnchorTest, OnlyFirstTextFragmentFound) { SimRequest request("https://example.com/test.html#:~:text=test&text=more", "text/html"); LoadURL("https://example.com/test.html#:~:text=test&text=more"); request.Complete(R"HTML(

This is a test page

)HTML"); RunAsyncMatchingTasks(); // Render two frames to handle the async step added by the beforematch event. Compositor().BeginFrame(); Compositor().BeginFrame(); Element& p = *GetDocument().getElementById("text"); EXPECT_EQ(p, *GetDocument().CssTarget()); EXPECT_TRUE(ViewportRect().Contains(BoundingRectInFrame(p))) << "

Element wasn't scrolled into view, viewport's scroll offset: " << LayoutViewport()->GetScrollOffset().ToString(); EXPECT_EQ(1u, GetDocument().Markers().Markers().size()); } // Make sure multiple non-matching strings doesn't cause scroll and the fragment // is removed when completed. TEST_F(TextFragmentAnchorTest, MultipleNonMatchingStrings) { SimRequest request( "https://example.com/" "test.html#:~:text=unicorn&text=cookie&text=cat", "text/html"); LoadURL( "https://example.com/" "test.html#:~:text=unicorn&text=cookie&text=cat"); request.Complete(R"HTML(

This is a test page

)HTML"); RunAsyncMatchingTasks(); // Render two frames to handle the async step added by the beforematch event. Compositor().BeginFrame(); BeginEmptyFrame(); EXPECT_EQ(ScrollOffset(), LayoutViewport()->GetScrollOffset()); // Force a layout GetDocument().body()->setAttribute(html_names::kStyleAttr, "height: 1300px"); Compositor().BeginFrame(); EXPECT_EQ(nullptr, GetDocument().CssTarget()); EXPECT_FALSE(GetDocument().View()->GetFragmentAnchor()); EXPECT_TRUE(GetDocument().Markers().Markers().IsEmpty()); } // Test matching a text range within the same element TEST_F(TextFragmentAnchorTest, SameElementTextRange) { SimRequest request("https://example.com/test.html#:~:text=This,page", "text/html"); LoadURL("https://example.com/test.html#:~:text=This,page"); request.Complete(R"HTML(

This is a test page

)HTML"); RunAsyncMatchingTasks(); // Render two frames to handle the async step added by the beforematch event. Compositor().BeginFrame(); Compositor().BeginFrame(); EXPECT_EQ(*GetDocument().getElementById("text"), *GetDocument().CssTarget()); EXPECT_EQ(1u, GetDocument().Markers().Markers().size()); // Expect marker on "This is a test page". auto* text = To(GetDocument().getElementById("text")->firstChild()); DocumentMarkerVector markers = GetDocument().Markers().MarkersFor( *text, DocumentMarker::MarkerTypes::TextFragment()); ASSERT_EQ(1u, markers.size()); EXPECT_EQ(0u, markers.at(0)->StartOffset()); EXPECT_EQ(19u, markers.at(0)->EndOffset()); } // Test matching a text range across two neighboring elements TEST_F(TextFragmentAnchorTest, NeighboringElementTextRange) { SimRequest request("https://example.com/test.html#:~:text=test,paragraph", "text/html"); LoadURL("https://example.com/test.html#:~:text=test,paragraph"); request.Complete(R"HTML(

This is a test page

with another paragraph of text

)HTML"); RunAsyncMatchingTasks(); // Render two frames to handle the async step added by the beforematch event. Compositor().BeginFrame(); Compositor().BeginFrame(); EXPECT_EQ(*GetDocument().body(), *GetDocument().CssTarget()); EXPECT_EQ(2u, GetDocument().Markers().Markers().size()); // Expect marker on "test page" auto* text1 = To(GetDocument().getElementById("text1")->firstChild()); DocumentMarkerVector markers = GetDocument().Markers().MarkersFor( *text1, DocumentMarker::MarkerTypes::TextFragment()); ASSERT_EQ(1u, markers.size()); EXPECT_EQ(10u, markers.at(0)->StartOffset()); EXPECT_EQ(19u, markers.at(0)->EndOffset()); // Expect marker on "with another paragraph" auto* text2 = To(GetDocument().getElementById("text2")->firstChild()); markers = GetDocument().Markers().MarkersFor( *text2, DocumentMarker::MarkerTypes::TextFragment()); ASSERT_EQ(1u, markers.size()); EXPECT_EQ(0u, markers.at(0)->StartOffset()); EXPECT_EQ(22u, markers.at(0)->EndOffset()); } // Test matching a text range from an element to a deeper nested element TEST_F(TextFragmentAnchorTest, DifferentDepthElementTextRange) { SimRequest request("https://example.com/test.html#:~:text=test,paragraph", "text/html"); LoadURL("https://example.com/test.html#:~:text=test,paragraph"); request.Complete(R"HTML(

This is a test page

with another paragraph of text

)HTML"); RunAsyncMatchingTasks(); // Render two frames to handle the async step added by the beforematch event. Compositor().BeginFrame(); Compositor().BeginFrame(); EXPECT_EQ(*GetDocument().body(), *GetDocument().CssTarget()); EXPECT_EQ(2u, GetDocument().Markers().Markers().size()); // Expect marker on "test page" auto* text1 = To(GetDocument().getElementById("text1")->firstChild()); DocumentMarkerVector markers = GetDocument().Markers().MarkersFor( *text1, DocumentMarker::MarkerTypes::TextFragment()); ASSERT_EQ(1u, markers.size()); EXPECT_EQ(10u, markers.at(0)->StartOffset()); EXPECT_EQ(19u, markers.at(0)->EndOffset()); // Expect marker on "with another paragraph" auto* text2 = To(GetDocument().getElementById("text2")->firstChild()); markers = GetDocument().Markers().MarkersFor( *text2, DocumentMarker::MarkerTypes::TextFragment()); ASSERT_EQ(1u, markers.size()); EXPECT_EQ(0u, markers.at(0)->StartOffset()); EXPECT_EQ(22u, markers.at(0)->EndOffset()); } // Ensure that we don't match anything if endText is not found. TEST_F(TextFragmentAnchorTest, TextRangeEndTextNotFound) { SimRequest request("https://example.com/test.html#:~:text=test,cat", "text/html"); LoadURL("https://example.com/test.html#:~:text=test,cat"); request.Complete(R"HTML(

This is a test page

)HTML"); RunAsyncMatchingTasks(); EXPECT_EQ(nullptr, GetDocument().CssTarget()); EXPECT_EQ(0u, GetDocument().Markers().Markers().size()); EXPECT_EQ(ScrollOffset(), LayoutViewport()->GetScrollOffset()); } // Test matching multiple text ranges TEST_F(TextFragmentAnchorTest, MultipleTextRanges) { SimRequest request( "https://example.com/" "test.html#:~:text=test,with&text=paragraph,text", "text/html"); LoadURL( "https://example.com/" "test.html#:~:text=test,with&text=paragraph,text"); request.Complete(R"HTML(

This is a test page

with another paragraph of text

)HTML"); RunAsyncMatchingTasks(); // Render two frames to handle the async step added by the beforematch event. Compositor().BeginFrame(); Compositor().BeginFrame(); EXPECT_EQ(*GetDocument().body(), *GetDocument().CssTarget()); EXPECT_EQ(3u, GetDocument().Markers().Markers().size()); // Expect marker on "test page" auto* text1 = To(GetDocument().getElementById("text1")->firstChild()); DocumentMarkerVector markers = GetDocument().Markers().MarkersFor( *text1, DocumentMarker::MarkerTypes::TextFragment()); ASSERT_EQ(1u, markers.size()); EXPECT_EQ(10u, markers.at(0)->StartOffset()); EXPECT_EQ(19u, markers.at(0)->EndOffset()); // Expect markers on "with" and "paragraph of text" auto* text2 = To(GetDocument().getElementById("text2")->firstChild()); markers = GetDocument().Markers().MarkersFor( *text2, DocumentMarker::MarkerTypes::TextFragment()); ASSERT_EQ(2u, markers.size()); EXPECT_EQ(0u, markers.at(0)->StartOffset()); EXPECT_EQ(4u, markers.at(0)->EndOffset()); EXPECT_EQ(13u, markers.at(1)->StartOffset()); EXPECT_EQ(30u, markers.at(1)->EndOffset()); } // Ensure we scroll to the beginning of a text range larger than the viewport. TEST_F(TextFragmentAnchorTest, DistantElementTextRange) { SimRequest request("https://example.com/test.html#:~:text=test,paragraph", "text/html"); LoadURL("https://example.com/test.html#:~:text=test,paragraph"); request.Complete(R"HTML(

This is a test page

with another paragraph of text

)HTML"); RunAsyncMatchingTasks(); // Render two frames to handle the async step added by the beforematch event. Compositor().BeginFrame(); Compositor().BeginFrame(); Element& p = *GetDocument().getElementById("text"); EXPECT_TRUE(ViewportRect().Contains(BoundingRectInFrame(p))) << "

Element wasn't scrolled into view, viewport's scroll offset: " << LayoutViewport()->GetScrollOffset().ToString(); EXPECT_EQ(2u, GetDocument().Markers().Markers().size()); } // Test a text range with both context terms in the same element. TEST_F(TextFragmentAnchorTest, TextRangeWithContext) { SimRequest request( "https://example.com/test.html#:~:text=This-,is,test,-page", "text/html"); LoadURL("https://example.com/test.html#:~:text=This-,is,test,-page"); request.Complete(R"HTML(

This is a test page

)HTML"); RunAsyncMatchingTasks(); Compositor().BeginFrame(); EXPECT_EQ(*GetDocument().getElementById("text"), *GetDocument().CssTarget()); EXPECT_EQ(1u, GetDocument().Markers().Markers().size()); // Expect marker on "is a test". auto* text = To(GetDocument().getElementById("text")->firstChild()); DocumentMarkerVector markers = GetDocument().Markers().MarkersFor( *text, DocumentMarker::MarkerTypes::TextFragment()); ASSERT_EQ(1u, markers.size()); EXPECT_EQ(5u, markers.at(0)->StartOffset()); EXPECT_EQ(14u, markers.at(0)->EndOffset()); } // Ensure that we do not match a text range if the prefix is not found. TEST_F(TextFragmentAnchorTest, PrefixNotFound) { SimRequest request( "https://example.com/test.html#:~:text=prefix-,is,test,-page", "text/html"); LoadURL("https://example.com/test.html#:~:text=prefix-,is,test,-page"); request.Complete(R"HTML(

This is a test page

)HTML"); // Render two frames to handle the async step added by the beforematch event. Compositor().BeginFrame(); BeginEmptyFrame(); RunAsyncMatchingTasks(); EXPECT_EQ(nullptr, GetDocument().CssTarget()); EXPECT_EQ(0u, GetDocument().Markers().Markers().size()); } // Ensure that we do not match a text range if the suffix is not found. TEST_F(TextFragmentAnchorTest, SuffixNotFound) { SimRequest request( "https://example.com/test.html#:~:text=This-,is,test,-suffix", "text/html"); LoadURL("https://example.com/test.html#:~:text=This-,is,test,-suffix"); request.Complete(R"HTML(

This is a test page

)HTML"); RunAsyncMatchingTasks(); EXPECT_EQ(nullptr, GetDocument().CssTarget()); EXPECT_EQ(0u, GetDocument().Markers().Markers().size()); } // Test a text range with context terms in different elements TEST_F(TextFragmentAnchorTest, TextRangeWithCrossElementContext) { SimRequest request( "https://example.com/test.html#:~:text=Header%202-,A,text,-Footer%201", "text/html"); LoadURL( "https://example.com/" "test.html#:~:text=Header%202-,A,text,-Footer%201"); request.Complete(R"HTML(

Header 1

A string of text

Footer 1

Header 2

A string of text

Footer 1

Header 2

A string of text

Footer 2

)HTML"); RunAsyncMatchingTasks(); Compositor().BeginFrame(); EXPECT_EQ(*GetDocument().getElementById("expected"), *GetDocument().CssTarget()); EXPECT_EQ(1u, GetDocument().Markers().Markers().size()); // Expect marker on the expected "A string of text". auto* text = To(GetDocument().getElementById("expected")->firstChild()); DocumentMarkerVector markers = GetDocument().Markers().MarkersFor( *text, DocumentMarker::MarkerTypes::TextFragment()); ASSERT_EQ(1u, markers.size()); EXPECT_EQ(0u, markers.at(0)->StartOffset()); EXPECT_EQ(16u, markers.at(0)->EndOffset()); } // Test context terms separated by elements and whitespace TEST_F(TextFragmentAnchorTest, CrossElementAndWhitespaceContext) { SimRequest request( "https://example.com/" "test.html#:~:text=List%202-,Cat,-Good%20cat", "text/html"); LoadURL( "https://example.com/" "test.html#:~:text=List%202-,Cat,-Good%20cat"); request.Complete(R"HTML(

List 1

Cat

 Good cat

List 2

Cat

 Good cat

List 2

Cat

 Bad cat

)HTML"); RunAsyncMatchingTasks(); Compositor().BeginFrame(); EXPECT_EQ(*GetDocument().getElementById("expected"), *GetDocument().CssTarget()); EXPECT_EQ(1u, GetDocument().Markers().Markers().size()); // Expect marker on the expected "cat". auto* text = To(GetDocument().getElementById("expected")->firstChild()); DocumentMarkerVector markers = GetDocument().Markers().MarkersFor( *text, DocumentMarker::MarkerTypes::TextFragment()); ASSERT_EQ(1u, markers.size()); EXPECT_EQ(0u, markers.at(0)->StartOffset()); EXPECT_EQ(3u, markers.at(0)->EndOffset()); } // Test context terms separated by empty sibling and parent elements TEST_F(TextFragmentAnchorTest, CrossEmptySiblingAndParentElementContext) { SimRequest request( "https://example.com/" "test.html#:~:text=prefix-,match,-suffix", "text/html"); LoadURL( "https://example.com/" "test.html#:~:text=prefix-,match,-suffix"); request.Complete(R"HTML(

prefix


 

match


 

suffix

)HTML"); RunAsyncMatchingTasks(); Compositor().BeginFrame(); EXPECT_EQ(*GetDocument().getElementById("expected"), *GetDocument().CssTarget()); EXPECT_EQ(1u, GetDocument().Markers().Markers().size()); // Expect marker on "match". auto* text = To(GetDocument().getElementById("expected")->firstChild()); DocumentMarkerVector markers = GetDocument().Markers().MarkersFor( *text, DocumentMarker::MarkerTypes::TextFragment()); ASSERT_EQ(1u, markers.size()); EXPECT_EQ(0u, markers.at(0)->StartOffset()); EXPECT_EQ(5u, markers.at(0)->EndOffset()); } // Ensure we scroll to text when its prefix and suffix are out of view. TEST_F(TextFragmentAnchorTest, DistantElementContext) { SimRequest request( "https://example.com/test.html#:~:text=Prefix-,Cats,-Suffix", "text/html"); LoadURL("https://example.com/test.html#:~:text=Prefix-,Cats,-Suffix"); request.Complete(R"HTML(

Cats

Prefix

Cats

Suffix

)HTML"); RunAsyncMatchingTasks(); // Render two frames to handle the async step added by the beforematch event. Compositor().BeginFrame(); Compositor().BeginFrame(); Element& p = *GetDocument().getElementById("text"); EXPECT_TRUE(ViewportRect().Contains(BoundingRectInFrame(p))) << "

Element wasn't scrolled into view, viewport's scroll offset: " << LayoutViewport()->GetScrollOffset().ToString(); EXPECT_EQ(1u, GetDocument().Markers().Markers().size()); } // Test specifying just one of the prefix and suffix TEST_F(TextFragmentAnchorTest, OneContextTerm) { SimRequest request( "https://example.com/" "test.html#:~:text=test-,page&text=page,-with%20real%20content", "text/html"); LoadURL( "https://example.com/" "test.html#:~:text=test-,page&text=page,-with%20real%20content"); request.Complete(R"HTML(

This is a test page

Not a page with real content

)HTML"); RunAsyncMatchingTasks(); Compositor().BeginFrame(); EXPECT_EQ(*GetDocument().getElementById("text1"), *GetDocument().CssTarget()); // Expect marker on the first "page" auto* text1 = To(GetDocument().getElementById("text1")->firstChild()); DocumentMarkerVector markers = GetDocument().Markers().MarkersFor( *text1, DocumentMarker::MarkerTypes::TextFragment()); ASSERT_EQ(1u, markers.size()); EXPECT_EQ(15u, markers.at(0)->StartOffset()); EXPECT_EQ(19u, markers.at(0)->EndOffset()); // Expect marker on the second "page" auto* text2 = To(GetDocument().getElementById("text2")->firstChild()); markers = GetDocument().Markers().MarkersFor( *text2, DocumentMarker::MarkerTypes::TextFragment()); ASSERT_EQ(1u, markers.size()); EXPECT_EQ(6u, markers.at(0)->StartOffset()); EXPECT_EQ(10u, markers.at(0)->EndOffset()); } class TextFragmentAnchorScrollTest : public TextFragmentAnchorTest, public testing::WithParamInterface { protected: bool IsUserScrollType() { return GetParam() == mojom::blink::ScrollType::kCompositor || GetParam() == mojom::blink::ScrollType::kUser; } }; INSTANTIATE_TEST_SUITE_P( ScrollTypes, TextFragmentAnchorScrollTest, 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 a user scroll cancels the scroll into view. TEST_P(TextFragmentAnchorScrollTest, ScrollCancelled) { SimRequest request("https://example.com/test.html#:~:text=test", "text/html"); SimSubresourceRequest css_request("https://example.com/test.css", "text/css"); SimSubresourceRequest img_request("https://example.com/test.png", "image/png"); LoadURL("https://example.com/test.html#:~:text=test"); request.Complete(R"HTML(

This is a test page

)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 in the next begin frame. css_request.Complete("p { visibility: visible; top: 1001px; }"); img_request.Complete(""); RunAsyncMatchingTasks(); // Render two frames to handle the async step added by the beforematch event. Compositor().BeginFrame(); Element& p = *GetDocument().getElementById("text"); // If the scroll was a user scroll then we shouldn't try to keep the fragment // in view. Otherwise, we should. if (IsUserScrollType()) { EXPECT_FALSE(ViewportRect().Contains(BoundingRectInFrame(p))); } else { EXPECT_TRUE(ViewportRect().Contains(BoundingRectInFrame(p))); } EXPECT_EQ(p, *GetDocument().CssTarget()); EXPECT_EQ(1u, GetDocument().Markers().Markers().size()); // Expect marker on "test" auto* text = To(p.firstChild()); DocumentMarkerVector markers = GetDocument().Markers().MarkersFor( *text, DocumentMarker::MarkerTypes::TextFragment()); ASSERT_EQ(1u, markers.size()); EXPECT_EQ(10u, markers.at(0)->StartOffset()); EXPECT_EQ(14u, markers.at(0)->EndOffset()); } // Test that user scrolling dismisses the highlight. TEST_P(TextFragmentAnchorScrollTest, DismissTextHighlightOnUserScroll) { base::test::ScopedFeatureList feature_list_; feature_list_.InitAndDisableFeature( shared_highlighting::kSharedHighlightingV2); SimRequest request( "https://example.com/" "test.html#:~:text=test%20page&text=more%20text", "text/html"); LoadURL( "https://example.com/" "test.html#:~:text=test%20page&text=more%20text"); request.Complete(R"HTML(

This is a test page

With some more text

)HTML"); RunAsyncMatchingTasks(); // Render two frames to handle the async step added by the beforematch event. Compositor().BeginFrame(); Compositor().BeginFrame(); ASSERT_EQ(2u, GetDocument().Markers().Markers().size()); mojom::blink::ScrollType scroll_type = GetParam(); LayoutViewport()->ScrollBy(ScrollOffset(0, -10), scroll_type); Compositor().BeginFrame(); if (IsUserScrollType()) { EXPECT_EQ(0u, GetDocument().Markers().Markers().size()); EXPECT_FALSE(GetDocument().View()->GetFragmentAnchor()); } else { EXPECT_EQ(2u, GetDocument().Markers().Markers().size()); EXPECT_TRUE(GetDocument().View()->GetFragmentAnchor()); } } // Test that user scrolling doesn't dismiss the highlight, when the // SharedHighlightingV2 flag is enabled. TEST_P(TextFragmentAnchorScrollTest, DontDismissTextHighlightOnUserScroll) { base::test::ScopedFeatureList feature_list_; feature_list_.InitAndEnableFeature( shared_highlighting::kSharedHighlightingV2); SimRequest request( "https://example.com/" "test.html#:~:text=test%20page&text=more%20text", "text/html"); LoadURL( "https://example.com/" "test.html#:~:text=test%20page&text=more%20text"); request.Complete(R"HTML(

This is a test page

With some more text

)HTML"); RunAsyncMatchingTasks(); // Render two frames to handle the async step added by the beforematch event. Compositor().BeginFrame(); Compositor().BeginFrame(); ASSERT_EQ(2u, GetDocument().Markers().Markers().size()); mojom::blink::ScrollType scroll_type = GetParam(); LayoutViewport()->ScrollBy(ScrollOffset(0, -10), scroll_type); Compositor().BeginFrame(); EXPECT_EQ(2u, GetDocument().Markers().Markers().size()); EXPECT_TRUE(GetDocument().View()->GetFragmentAnchor()); } // Ensure that the text fragment anchor has no effect in an iframe. This is // disabled in iframes by design, for security reasons. TEST_F(TextFragmentAnchorTest, DisabledInIframes) { SimRequest main_request("https://example.com/test.html", "text/html"); SimRequest child_request("https://example.com/child.html#:~:text=test", "text/html"); LoadURL("https://example.com/test.html"); main_request.Complete(R"HTML( )HTML"); child_request.Complete(R"HTML(

test

)HTML"); RunAsyncMatchingTasks(); // Render two frames to handle the async step added by the beforematch event. Compositor().BeginFrame(); BeginEmptyFrame(); Element* iframe = GetDocument().getElementById("iframe"); auto* child_frame = To(To(iframe)->ContentFrame()); EXPECT_EQ(nullptr, GetDocument().CssTarget()); EXPECT_EQ(ScrollOffset(), child_frame->View()->GetScrollableArea()->GetScrollOffset()); } // Similarly to the iframe case, we also want to prevent activating a text // fragment anchor inside a window.opened window. TEST_F(TextFragmentAnchorTest, DisabledInWindowOpen) { String destination = "https://example.com/child.html#:~:text=test"; SimRequest main_request("https://example.com/test.html", "text/html"); SimRequest child_request(destination, "text/html"); LoadURL("https://example.com/test.html"); main_request.Complete(R"HTML( )HTML"); // Render two frames to handle the async step added by the beforematch event. Compositor().BeginFrame(); BeginEmptyFrame(); LocalDOMWindow* main_window = GetDocument().GetFrame()->DomWindow(); ScriptState* script_state = ToScriptStateForMainWorld(main_window->GetFrame()); ScriptState::Scope entered_context_scope(script_state); LocalDOMWindow* child_window = To( main_window->open(script_state->GetIsolate(), destination, "frame1", "", ASSERT_NO_EXCEPTION)); ASSERT_TRUE(child_window); RunPendingTasks(); child_request.Complete(R"HTML(

test

)HTML"); RunAsyncMatchingTasks(); EXPECT_EQ(nullptr, child_window->document()->CssTarget()); LocalFrameView* child_view = child_window->GetFrame()->View(); EXPECT_EQ(ScrollOffset(), child_view->GetScrollableArea()->GetScrollOffset()); } // Ensure that the text fragment anchor is not activated by same-document script // navigations. TEST_F(TextFragmentAnchorTest, DisabledInSamePageNavigation) { SimRequest main_request("https://example.com/test.html", "text/html"); LoadURL("https://example.com/test.html"); main_request.Complete(R"HTML(

test

)HTML"); RunAsyncMatchingTasks(); // Render two frames to handle the async step added by the beforematch event. Compositor().BeginFrame(); BeginEmptyFrame(); ASSERT_EQ(ScrollOffset(), GetDocument().View()->GetScrollableArea()->GetScrollOffset()); ScriptState* script_state = ToScriptStateForMainWorld(GetDocument().GetFrame()); ScriptState::Scope entered_context_scope(script_state); GetDocument().GetFrame()->DomWindow()->location()->setHash( script_state->GetIsolate(), ":~:text=test", ASSERT_NO_EXCEPTION); RunAsyncMatchingTasks(); EXPECT_EQ(nullptr, GetDocument().CssTarget()); EXPECT_EQ(ScrollOffset(), LayoutViewport()->GetScrollOffset()); } // Ensure matching is case insensitive. TEST_F(TextFragmentAnchorTest, CaseInsensitive) { SimRequest request("https://example.com/test.html#:~:text=Test", "text/html"); LoadURL("https://example.com/test.html#:~:text=Test"); request.Complete(R"HTML(

test

)HTML"); RunAsyncMatchingTasks(); // Render two frames to handle the async step added by the beforematch event. Compositor().BeginFrame(); Compositor().BeginFrame(); Element& p = *GetDocument().getElementById("text"); EXPECT_TRUE(ViewportRect().Contains(BoundingRectInFrame(p))) << "

Element wasn't scrolled into view, viewport's scroll offset: " << LayoutViewport()->GetScrollOffset().ToString(); EXPECT_EQ(1u, GetDocument().Markers().Markers().size()); } // Test that the fragment anchor stays centered in view throughout loading. TEST_F(TextFragmentAnchorTest, TargetStaysInView) { SimRequest main_request("https://example.com/test.html#:~:text=test", "text/html"); SimRequest image_request("https://example.com/image.svg", "image/svg+xml"); LoadURL("https://example.com/test.html#:~:text=test"); main_request.Complete(R"HTML(

test

)HTML"); RunAsyncMatchingTasks(); Compositor().BeginFrame(); Compositor().BeginFrame(); EXPECT_FALSE(GetDocument().IsLoadCompleted()); EXPECT_TRUE(GetDocument().HasFinishedParsing()); ScrollOffset first_scroll_offset = LayoutViewport()->GetScrollOffset(); ASSERT_NE(ScrollOffset(), first_scroll_offset); Element& p = *GetDocument().getElementById("text"); IntRect first_bounding_rect = BoundingRectInFrame(p); EXPECT_TRUE(ViewportRect().Contains(first_bounding_rect)); // Load an image that pushes the target text out of view image_request.Complete(R"SVG( )SVG"); RunPendingTasks(); EXPECT_TRUE(GetDocument().IsLoadCompleted()); EXPECT_TRUE(GetDocument().HasFinishedParsing()); RunAsyncMatchingTasks(); Compositor().BeginFrame(); Compositor().BeginFrame(); // Ensure the target text is still in view and stayed centered ASSERT_NE(first_scroll_offset, LayoutViewport()->GetScrollOffset()); EXPECT_TRUE(ViewportRect().Contains(BoundingRectInFrame(p))); EXPECT_EQ(first_bounding_rect, BoundingRectInFrame(p)); EXPECT_EQ(1u, GetDocument().Markers().Markers().size()); } // Test that overlapping text ranges results in only the first one highlighted TEST_F(TextFragmentAnchorTest, OverlappingTextRanges) { SimRequest request( "https://example.com/test.html#:~:text=This,test&text=is,page", "text/html"); LoadURL("https://example.com/test.html#:~:text=This,test&text=is,page"); request.Complete(R"HTML(

This is a test page

)HTML"); RunAsyncMatchingTasks(); // Render two frames to handle the async step added by the beforematch event. Compositor().BeginFrame(); Compositor().BeginFrame(); EXPECT_EQ(1u, GetDocument().Markers().Markers().size()); // Expect marker on "This is a test". auto* text = To(GetDocument().getElementById("text")->firstChild()); DocumentMarkerVector markers = GetDocument().Markers().MarkersFor( *text, DocumentMarker::MarkerTypes::TextFragment()); ASSERT_EQ(1u, markers.size()); EXPECT_EQ(0u, markers.at(0)->StartOffset()); EXPECT_EQ(14u, markers.at(0)->EndOffset()); } // Test matching a space to   character. TEST_F(TextFragmentAnchorTest, SpaceMatchesNbsp) { 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(

This is a test page

)HTML"); RunAsyncMatchingTasks(); // Render two frames to handle the async step added by the beforematch event. Compositor().BeginFrame(); Compositor().BeginFrame(); Element& p = *GetDocument().getElementById("text"); EXPECT_TRUE(ViewportRect().Contains(BoundingRectInFrame(p))) << "

Element wasn't scrolled into view, viewport's scroll offset: " << LayoutViewport()->GetScrollOffset().ToString(); EXPECT_EQ(1u, GetDocument().Markers().Markers().size()); } // Test matching text with a CSS text transform. TEST_F(TextFragmentAnchorTest, CSSTextTransform) { 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(

This is a test page

)HTML"); RunAsyncMatchingTasks(); // Render two frames to handle the async step added by the beforematch event. Compositor().BeginFrame(); Compositor().BeginFrame(); Element& p = *GetDocument().getElementById("text"); EXPECT_TRUE(ViewportRect().Contains(BoundingRectInFrame(p))) << "

Element wasn't scrolled into view, viewport's scroll offset: " << LayoutViewport()->GetScrollOffset().ToString(); EXPECT_EQ(1u, GetDocument().Markers().Markers().size()); } // Test that we scroll the element fragment into view if we don't find a match. TEST_F(TextFragmentAnchorTest, NoMatchFoundFallsBackToElementFragment) { SimRequest request("https://example.com/test.html#element:~:text=cats", "text/html"); LoadURL("https://example.com/test.html#element:~:text=cats"); request.Complete(R"HTML(

This is a test page

Some text
)HTML"); RunAsyncMatchingTasks(); Compositor().BeginFrame(); // The TextFragmentAnchor needs another frame to invoke the element anchor Compositor().BeginFrame(); RunAsyncMatchingTasks(); EXPECT_EQ(GetDocument().Url(), "https://example.com/test.html#element"); Element& p = *GetDocument().getElementById("element"); EXPECT_EQ(p, *GetDocument().CssTarget()); EXPECT_TRUE(ViewportRect().Contains(BoundingRectInFrame(p))) << "

Element wasn't scrolled into view, viewport's scroll offset: " << LayoutViewport()->GetScrollOffset().ToString(); } // Test that we don't match partial words at the beginning or end of the text. TEST_F(TextFragmentAnchorTest, CheckForWordBoundary) { SimRequest request( "https://example.com/" "test.html#:~:text=This%20is%20a%20te&tagetText=st%20page", "text/html"); LoadURL( "https://example.com/" "test.html#:~:text=This%20is%20a%20te&tagetText=st%20page"); request.Complete(R"HTML(

This is a test page

)HTML"); RunAsyncMatchingTasks(); EXPECT_EQ(nullptr, GetDocument().CssTarget()); EXPECT_EQ(ScrollOffset(), LayoutViewport()->GetScrollOffset()); EXPECT_TRUE(GetDocument().Markers().Markers().IsEmpty()); } // Test that we don't match partial words with context TEST_F(TextFragmentAnchorTest, CheckForWordBoundaryWithContext) { SimRequest request("https://example.com/test.html#:~:text=est-,page", "text/html"); LoadURL("https://example.com/test.html#:~:text=est-,page"); request.Complete(R"HTML(

This is a test page

)HTML"); RunAsyncMatchingTasks(); EXPECT_EQ(nullptr, GetDocument().CssTarget()); EXPECT_EQ(ScrollOffset(), LayoutViewport()->GetScrollOffset()); EXPECT_TRUE(GetDocument().Markers().Markers().IsEmpty()); } // Test that we correctly match a whole word when it appears as a partial word // earlier in the page. TEST_F(TextFragmentAnchorTest, CheckForWordBoundaryWithPartialWord) { SimRequest request("https://example.com/test.html#:~:text=tes,age", "text/html"); LoadURL("https://example.com/test.html#:~:text=tes,age"); request.Complete(R"HTML(

This is a test page

This is a tes age

)HTML"); RunAsyncMatchingTasks(); // Render two frames to handle the async step added by the beforematch event. Compositor().BeginFrame(); Compositor().BeginFrame(); Element& p = *GetDocument().getElementById("second"); EXPECT_EQ(p, *GetDocument().CssTarget()); EXPECT_TRUE(ViewportRect().Contains(BoundingRectInFrame(p))) << "Should have scrolled

into view but didn't, scroll offset: " << LayoutViewport()->GetScrollOffset().ToString(); // Expect marker on only "tes age" EXPECT_EQ(1u, GetDocument().Markers().Markers().size()); DocumentMarkerVector markers = GetDocument().Markers().MarkersFor( *To(p.firstChild()), DocumentMarker::MarkerTypes::TextFragment()); ASSERT_EQ(1u, markers.size()); EXPECT_EQ(10u, markers.at(0)->StartOffset()); EXPECT_EQ(17u, markers.at(0)->EndOffset()); } // Test dismissing the text highlight with a click TEST_F(TextFragmentAnchorTest, DismissTextHighlightWithClick) { base::test::ScopedFeatureList feature_list_; feature_list_.InitAndDisableFeature( shared_highlighting::kSharedHighlightingV2); SimRequest request( "https://example.com/" "test.html#:~:text=test%20page&text=more%20text", "text/html"); LoadURL( "https://example.com/" "test.html#:~:text=test%20page&text=more%20text"); request.Complete(R"HTML(

This is a test page

With some more text

)HTML"); RunAsyncMatchingTasks(); // Render two frames to handle the async step added by the beforematch event. Compositor().BeginFrame(); Compositor().BeginFrame(); KURL url = GetDocument() .GetFrame() ->Loader() .GetDocumentLoader() ->GetHistoryItem() ->Url(); EXPECT_EQ( "https://example.com/test.html#:~:text=test%20page&text=more%20text", url.GetString()); EXPECT_EQ(2u, GetDocument().Markers().Markers().size()); SimulateClick(100, 100); EXPECT_EQ(0u, GetDocument().Markers().Markers().size()); // Ensure the fragment is uninstalled EXPECT_FALSE(GetDocument().View()->GetFragmentAnchor()); url = GetDocument() .GetFrame() ->Loader() .GetDocumentLoader() ->GetHistoryItem() ->Url(); EXPECT_EQ("https://example.com/test.html", url.GetString()); } // Test not dismissing the text highlight with a click, if the // SharedHighlightingV2 flag is enabled. TEST_F(TextFragmentAnchorTest, DontDismissTextHighlightWithClick) { base::test::ScopedFeatureList feature_list_; feature_list_.InitAndEnableFeature( shared_highlighting::kSharedHighlightingV2); SimRequest request( "https://example.com/" "test.html#:~:text=test%20page&text=more%20text", "text/html"); LoadURL( "https://example.com/" "test.html#:~:text=test%20page&text=more%20text"); request.Complete(R"HTML(

This is a test page

With some more text

)HTML"); RunAsyncMatchingTasks(); // Render two frames to handle the async step added by the beforematch event. Compositor().BeginFrame(); Compositor().BeginFrame(); EXPECT_EQ(2u, GetDocument().Markers().Markers().size()); SimulateClick(100, 100); EXPECT_EQ(2u, GetDocument().Markers().Markers().size()); // Ensure the fragment is still installed EXPECT_TRUE(GetDocument().View()->GetFragmentAnchor()); } // Test dismissing the text highlight with a tap TEST_F(TextFragmentAnchorTest, DismissTextHighlightWithTap) { base::test::ScopedFeatureList feature_list_; feature_list_.InitAndDisableFeature( shared_highlighting::kSharedHighlightingV2); SimRequest request( "https://example.com/" "test.html#:~:text=test%20page&text=more%20text", "text/html"); LoadURL( "https://example.com/" "test.html#:~:text=test%20page&text=more%20text"); request.Complete(R"HTML(

This is a test page

With some more text

)HTML"); RunAsyncMatchingTasks(); // Render two frames to handle the async step added by the beforematch event. Compositor().BeginFrame(); Compositor().BeginFrame(); KURL url = GetDocument() .GetFrame() ->Loader() .GetDocumentLoader() ->GetHistoryItem() ->Url(); EXPECT_EQ( "https://example.com/test.html#:~:text=test%20page&text=more%20text", url.GetString()); EXPECT_EQ(2u, GetDocument().Markers().Markers().size()); SimulateTap(100, 100); EXPECT_EQ(0u, GetDocument().Markers().Markers().size()); // Ensure the fragment is uninstalled EXPECT_FALSE(GetDocument().View()->GetFragmentAnchor()); url = GetDocument() .GetFrame() ->Loader() .GetDocumentLoader() ->GetHistoryItem() ->Url(); EXPECT_EQ("https://example.com/test.html", url.GetString()); } // Test not dismissing the text highlight with a tap, if the // SharedHighlightingV2 flag is enabled. TEST_F(TextFragmentAnchorTest, DontDismissTextHighlightWithTap) { base::test::ScopedFeatureList feature_list_; feature_list_.InitAndEnableFeature( shared_highlighting::kSharedHighlightingV2); SimRequest request( "https://example.com/" "test.html#:~:text=test%20page&text=more%20text", "text/html"); LoadURL( "https://example.com/" "test.html#:~:text=test%20page&text=more%20text"); request.Complete(R"HTML(

This is a test page

With some more text

)HTML"); RunAsyncMatchingTasks(); // Render two frames to handle the async step added by the beforematch event. Compositor().BeginFrame(); Compositor().BeginFrame(); EXPECT_EQ(2u, GetDocument().Markers().Markers().size()); SimulateTap(100, 100); EXPECT_EQ(2u, GetDocument().Markers().Markers().size()); // Ensure the fragment is installed EXPECT_TRUE(GetDocument().View()->GetFragmentAnchor()); } // Test that we don't dismiss a text highlight before it's scrolled into view TEST_F(TextFragmentAnchorTest, DismissTextHighlightOutOfView) { base::test::ScopedFeatureList feature_list_; feature_list_.InitAndDisableFeature( shared_highlighting::kSharedHighlightingV2); 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(

This is a test page

)HTML"); ASSERT_EQ(0u, GetDocument().Markers().Markers().size()); SimulateClick(100, 100); // 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(); Compositor().BeginFrame(); EXPECT_EQ(1u, GetDocument().Markers().Markers().size()); // Click to dismiss SimulateClick(100, 100); EXPECT_EQ(0u, GetDocument().Markers().Markers().size()); EXPECT_FALSE(GetDocument().View()->GetFragmentAnchor()); } // Test dismissing a text highlight that didn't require a scroll into view TEST_F(TextFragmentAnchorTest, DismissTextHighlightInView) { base::test::ScopedFeatureList feature_list_; feature_list_.InitAndDisableFeature( shared_highlighting::kSharedHighlightingV2); SimRequest request( "https://example.com/" "test.html#:~:text=test%20page&text=more%20text", "text/html"); LoadURL( "https://example.com/" "test.html#:~:text=test%20page&text=more%20text"); request.Complete(R"HTML(

This is a test page

)HTML"); RunAsyncMatchingTasks(); Compositor().BeginFrame(); EXPECT_EQ(ScrollOffset(), LayoutViewport()->GetScrollOffset()); EXPECT_EQ(1u, GetDocument().Markers().Markers().size()); SimulateTap(100, 100); EXPECT_EQ(0u, GetDocument().Markers().Markers().size()); // Ensure the fragment is uninstalled EXPECT_FALSE(GetDocument().View()->GetFragmentAnchor()); } // Test that the fragment directive delimiter :~: works properly and is stripped // from the URL. TEST_F(TextFragmentAnchorTest, FragmentDirectiveDelimiter) { SimRequest request("https://example.com/test.html#:~:text=test", "text/html"); LoadURL("https://example.com/test.html#:~:text=test"); request.Complete(R"HTML(

This is a test page

)HTML"); RunAsyncMatchingTasks(); // Render two frames to handle the async step added by the beforematch event. Compositor().BeginFrame(); Compositor().BeginFrame(); EXPECT_EQ(1u, GetDocument().Markers().Markers().size()); EXPECT_EQ(GetDocument().Url(), "https://example.com/test.html"); } // Test that a :~: fragment directive is scrolled into view and is stripped from // the URL when there's also a valid element fragment. TEST_F(TextFragmentAnchorTest, FragmentDirectiveDelimiterWithElementFragment) { SimRequest request("https://example.com/test.html#element:~:text=test", "text/html"); LoadURL("https://example.com/test.html#element:~:text=test"); request.Complete(R"HTML(

This is a test page

Some text
)HTML"); RunAsyncMatchingTasks(); // Render two frames to handle the async step added by the beforematch event. Compositor().BeginFrame(); Compositor().BeginFrame(); EXPECT_EQ(GetDocument().Url(), "https://example.com/test.html#element"); Element& p = *GetDocument().getElementById("text"); EXPECT_EQ(p, *GetDocument().CssTarget()); EXPECT_TRUE(ViewportRect().Contains(BoundingRectInFrame(p))) << "

Element wasn't scrolled into view, viewport's scroll offset: " << LayoutViewport()->GetScrollOffset().ToString(); } // Test that a fragment directive is stripped from the URL even if it is not a // text directive. TEST_F(TextFragmentAnchorTest, IdFragmentWithFragmentDirective) { SimRequest request("https://example.com/test.html#element:~:id", "text/html"); LoadURL("https://example.com/test.html#element:~:id"); request.Complete(R"HTML(

This is a test page

Some text
)HTML"); RunAsyncMatchingTasks(); Compositor().BeginFrame(); Element& p = *GetDocument().getElementById("element"); EXPECT_EQ(p, *GetDocument().CssTarget()); EXPECT_TRUE(ViewportRect().Contains(BoundingRectInFrame(p))) << "Should have scrolled
into view but didn't, scroll offset: " << LayoutViewport()->GetScrollOffset().ToString(); } // Ensure we can match inside of a element. TEST_F(TextFragmentAnchorTest, TextDirectiveInSvg) { SimRequest request("https://example.com/test.html#:~:text=test", "text/html"); LoadURL("https://example.com/test.html#:~:text=test"); request.Complete(R"HTML( This is a test page )HTML"); RunAsyncMatchingTasks(); // Render two frames to handle the async step added by the beforematch event. Compositor().BeginFrame(); Compositor().BeginFrame(); Element& text = *GetDocument().getElementById("text"); EXPECT_EQ(text, *GetDocument().CssTarget()); EXPECT_TRUE(ViewportRect().Contains(BoundingRectInFrame(text))) << " Element wasn't scrolled into view, viewport's scroll offset: " << LayoutViewport()->GetScrollOffset().ToString(); EXPECT_EQ(1u, GetDocument().Markers().Markers().size()); } // Ensure we restore the text highlight on page reload // TODO(bokan): This test is disabled as this functionality was suppressed in // https://crrev.com/c/2135407; it would be better addressed by providing a // highlight-only function. See the TODO in // https://wicg.github.io/ScrollToTextFragment/#restricting-the-text-fragment TEST_F(TextFragmentAnchorTest, DISABLED_HighlightOnReload) { SimRequest request("https://example.com/test.html#:~:text=test", "text/html"); LoadURL("https://example.com/test.html#:~:text=test"); const String& html = R"HTML(

This is a test page

)HTML"; request.Complete(html); RunAsyncMatchingTasks(); // Render two frames to handle the async step added by the beforematch event. Compositor().BeginFrame(); Compositor().BeginFrame(); EXPECT_EQ(1u, GetDocument().Markers().Markers().size()); // Tap to dismiss the highlight. SimulateClick(10, 10); EXPECT_EQ(0u, GetDocument().Markers().Markers().size()); // Reload the page and expect the highlight to be restored. SimRequest reload_request("https://example.com/test.html#:~:text=test", "text/html"); MainFrame().StartReload(WebFrameLoadType::kReload); reload_request.Complete(html); // Render two frames to handle the async step added by the beforematch event. Compositor().BeginFrame(); Compositor().BeginFrame(); EXPECT_EQ(*GetDocument().getElementById("text"), *GetDocument().CssTarget()); EXPECT_EQ(1u, GetDocument().Markers().Markers().size()); } // Ensure that we can have text directives combined with non-text directives TEST_F(TextFragmentAnchorTest, NonTextDirectives) { SimRequest request( "https://example.com/test.html#:~:text=test&directive&text=more", "text/html"); LoadURL("https://example.com/test.html#:~:text=test&directive&text=more"); request.Complete(R"HTML(

This is a test page

This is some more text

)HTML"); RunPendingTasks(); // Render two frames to handle the async step added by the beforematch event. Compositor().BeginFrame(); Compositor().BeginFrame(); RunAsyncMatchingTasks(); Element& first = *GetDocument().getElementById("first"); EXPECT_TRUE(ViewportRect().Contains(BoundingRectInFrame(first))) << "First

wasn't scrolled into view, viewport's scroll offset: " << LayoutViewport()->GetScrollOffset().ToString(); EXPECT_EQ(2u, GetDocument().Markers().Markers().size()); } // Test that the text directive applies :target styling TEST_F(TextFragmentAnchorTest, CssTarget) { SimRequest main_request("https://example.com/test.html#:~:text=test", "text/html"); SimRequest css_request("https://example.com/test.css", "text/css"); LoadURL("https://example.com/test.html#:~:text=test"); main_request.Complete(R"HTML(

test

)HTML"); css_request.Complete(R"CSS( :target { margin-top: 2000px; } )CSS"); RunPendingTasks(); // Render two frames to handle the async step added by the beforematch event. Compositor().BeginFrame(); BeginEmptyFrame(); Element& p = *GetDocument().getElementById("text"); EXPECT_TRUE(ViewportRect().Contains(BoundingRectInFrame(p))); EXPECT_EQ(1u, GetDocument().Markers().Markers().size()); } // Ensure the text fragment anchor matching only occurs after the page becomes // visible. TEST_F(TextFragmentAnchorTest, PageVisibility) { WebView().SetVisibilityState(mojom::blink::PageVisibilityState::kHidden, /*initial_state=*/true); SimRequest request("https://example.com/test.html#:~:text=test", "text/html"); LoadURL("https://example.com/test.html#:~:text=test"); request.Complete(R"HTML(

This is a test page

)HTML"); RunAsyncMatchingTasks(); // Render two frames and ensure matching and scrolling does not occur. BeginEmptyFrame(); BeginEmptyFrame(); Element& p = *GetDocument().getElementById("text"); EXPECT_FALSE(ViewportRect().Contains(BoundingRectInFrame(p))); EXPECT_EQ(0u, GetDocument().Markers().Markers().size()); EXPECT_EQ(nullptr, GetDocument().CssTarget()); // Set the page visible and verify the match. WebView().SetVisibilityState(mojom::blink::PageVisibilityState::kVisible, /*initial_state=*/false); BeginEmptyFrame(); BeginEmptyFrame(); EXPECT_TRUE(ViewportRect().Contains(BoundingRectInFrame(p))); EXPECT_EQ(1u, GetDocument().Markers().Markers().size()); EXPECT_EQ(p, *GetDocument().CssTarget()); } // Regression test for https://crbug.com/1147568. Make sure a page setting // manual scroll restoration doesn't cause the fragment to avoid scrolling on // the initial load. TEST_F(TextFragmentAnchorTest, ManualRestorationDoesntBlockFragment) { SimRequest request("https://example.com/test.html#:~:text=test", "text/html"); LoadURL("https://example.com/test.html#:~:text=test"); request.Complete(R"HTML(

This is a test page

)HTML"); RunAsyncMatchingTasks(); // Render two frames and ensure matching and scrolling does not occur. BeginEmptyFrame(); BeginEmptyFrame(); Element& p = *GetDocument().getElementById("text"); EXPECT_TRUE(ViewportRect().Contains(BoundingRectInFrame(p))); } // Regression test for https://crbug.com/1147453. Ensure replaceState doesn't // clobber the text fragment token and allows fragment to scroll. TEST_F(TextFragmentAnchorTest, ReplaceStateDoesntBlockFragment) { SimRequest request("https://example.com/test.html#:~:text=test", "text/html"); LoadURL("https://example.com/test.html#:~:text=test"); request.Complete(R"HTML(

This is a test page

)HTML"); RunAsyncMatchingTasks(); // Render two frames and ensure matching and scrolling does not occur. BeginEmptyFrame(); BeginEmptyFrame(); Element& p = *GetDocument().getElementById("text"); EXPECT_TRUE(ViewportRect().Contains(BoundingRectInFrame(p))); } // Test that a text directive can match across comment nodes TEST_F(TextFragmentAnchorTest, MatchAcrossCommentNode) { SimRequest request("https://example.com/test.html#:~:text=abcdef", "text/html"); LoadURL("https://example.com/test.html#:~:text=abcdef"); request.Complete(R"HTML(
abcdef
)HTML"); RunAsyncMatchingTasks(); // Render two frames to handle the async step added by the beforematch event. Compositor().BeginFrame(); Compositor().BeginFrame(); Element& div = *GetDocument().getElementById("text"); EXPECT_EQ(div, *GetDocument().CssTarget()); EXPECT_TRUE(ViewportRect().Contains(BoundingRectInFrame(div))); EXPECT_EQ(2u, GetDocument().Markers().Markers().size()); } // Test that selection is successful for same prefix and text start. TEST_F(TextFragmentAnchorTest, SamePrefixAndText) { SimRequest request("https://example.com/test.html#:~:text=foo-,foo,-bar", "text/html"); LoadURL("https://example.com/test.html#:~:text=foo-,foo,-bar"); request.Complete(R"HTML(
foo foo foo bar bar bar
)HTML"); RunAsyncMatchingTasks(); // Render two frames to handle the async step added by the beforematch event. Compositor().BeginFrame(); Compositor().BeginFrame(); Element& div = *GetDocument().getElementById("text"); EXPECT_EQ(div, *GetDocument().CssTarget()); EXPECT_TRUE(ViewportRect().Contains(BoundingRectInFrame(div))); EXPECT_EQ(1u, GetDocument().Markers().Markers().size()); } // Checks that selection in the same text node is considerered uninterrupted. TEST_F(TextFragmentAnchorTest, IsInSameUninterruptedBlock_OneTextNode) { SimRequest request("https://example.com/test.html", "text/html"); LoadURL("https://example.com/test.html"); request.Complete(R"HTML(
First paragraph text
)HTML"); Node* first_paragraph = GetDocument().getElementById("first")->firstChild(); const auto& start = PositionInFlatTree(first_paragraph, 0); const auto& end = PositionInFlatTree(first_paragraph, 15); ASSERT_EQ("First paragraph", PlainText(EphemeralRangeInFlatTree(start, end))); EXPECT_TRUE(TextFragmentFinder::IsInSameUninterruptedBlock(start, end)); } // Checks that selection in the same text node with nested non-block element is // considerered uninterrupted. TEST_F(TextFragmentAnchorTest, IsInSameUninterruptedBlock_NonBlockInterruption) { SimRequest request("https://example.com/test.html", "text/html"); LoadURL("https://example.com/test.html"); request.Complete(R"HTML(
First styled text paragraph text
)HTML"); Node* first_paragraph = GetDocument().getElementById("first")->firstChild(); const auto& start = PositionInFlatTree(first_paragraph, 0); const auto& end = PositionInFlatTree(first_paragraph->nextSibling()->nextSibling(), 10); ASSERT_EQ("First styled text paragraph", PlainText(EphemeralRangeInFlatTree(start, end))); EXPECT_TRUE(TextFragmentFinder::IsInSameUninterruptedBlock(start, end)); } // Checks that selection in the same text node with nested block element is // considerered interrupted. TEST_F(TextFragmentAnchorTest, IsInSameUninterruptedBlock_BlockInterruption) { SimRequest request("https://example.com/test.html", "text/html"); LoadURL("https://example.com/test.html"); request.Complete(R"HTML(
First
block text
paragraph text
)HTML"); Node* first_paragraph = GetDocument().getElementById("first")->firstChild(); const auto& start = PositionInFlatTree(first_paragraph, 0); const auto& end = PositionInFlatTree(first_paragraph->nextSibling()->nextSibling(), 10); ASSERT_EQ("First\nblock text\nparagraph", PlainText(EphemeralRangeInFlatTree(start, end))); EXPECT_FALSE(TextFragmentFinder::IsInSameUninterruptedBlock(start, end)); } TEST_F(TextFragmentAnchorTest, OpenedFromHighlightDoesNotSelectAdditionalText) { base::test::ScopedFeatureList feature_list_; feature_list_.InitAndEnableFeature( shared_highlighting::kSharedHighlightingV2); SimRequest request("https://www.test.com/#:~:text=First%20test,page%20three", "text/html"); LoadURL("https://www.test.com/#:~:text=First%20test,page%20three"); request.Complete(R"HTML(

First test page one

Second test page two

Third test page three

Fourth test page four

)HTML"); RunAsyncMatchingTasks(); Compositor().BeginFrame(); Element* middle_element = GetDocument().getElementById("two"); Element* last_element = GetDocument().getElementById("four"); WebView().GetSettings()->SetEditingBehavior( mojom::EditingBehavior::kEditingMacBehavior); // Create a mouse event in the middle of

two. WebMouseEvent mouse_down_event(WebInputEvent::Type::kMouseDown, WebInputEvent::kNoModifiers, WebInputEvent::GetStaticTimeStampForTests()); const DOMRect* middle_rect = middle_element->getBoundingClientRect(); gfx::PointF middle_elem_point(((middle_rect->left() + 1)), ((middle_rect->top() + 1))); mouse_down_event.SetPositionInWidget(middle_elem_point.x(), middle_elem_point.y()); mouse_down_event.SetPositionInScreen(middle_elem_point.x(), middle_elem_point.y()); mouse_down_event.click_count = 1; mouse_down_event.button = WebMouseEvent::Button::kRight; // Corresponding release event (Windows shows context menu on release). WebMouseEvent mouse_up_event(mouse_down_event); mouse_up_event.SetType(WebInputEvent::Type::kMouseUp); WebView().MainFrameViewWidget()->HandleInputEvent( WebCoalescedInputEvent(mouse_down_event, ui::LatencyInfo())); WebView().MainFrameViewWidget()->HandleInputEvent( WebCoalescedInputEvent(mouse_up_event, ui::LatencyInfo())); // No additional text should be selected. FrameSelection& selection = GetDocument().GetFrame()->Selection(); EXPECT_TRUE(selection.SelectedText().IsEmpty()); // Create a mouse event at the center of

four. const DOMRect* last_rect = last_element->getBoundingClientRect(); gfx::PointF last_elem_point(((last_rect->left() + 1)), ((last_rect->top() + 1))); mouse_down_event.SetPositionInWidget(last_elem_point.x(), last_elem_point.y()); mouse_down_event.SetPositionInScreen(last_elem_point.x(), last_elem_point.y()); // Corresponding release event (Windows shows context menu on release). WebMouseEvent last_mouse_up_event(mouse_down_event); last_mouse_up_event.SetType(WebInputEvent::Type::kMouseUp); WebView().MainFrameViewWidget()->HandleInputEvent( WebCoalescedInputEvent(mouse_down_event, ui::LatencyInfo())); WebView().MainFrameViewWidget()->HandleInputEvent( WebCoalescedInputEvent(last_mouse_up_event, ui::LatencyInfo())); // The text underneath the cursor should be selected. EXPECT_FALSE(selection.SelectedText().IsEmpty()); } // Test that on Android, a user can display a context menu by tapping on // a text fragment, when the TextFragmentTapOpensContextMenu // RuntimeEnabledFeature is enabled. TEST_F(TextFragmentAnchorTest, ShouldOpenContextMenuOnTap) { base::test::ScopedFeatureList feature_list_; feature_list_.InitAndEnableFeature( shared_highlighting::kSharedHighlightingV2); LoadAhem(); SimRequest request( "https://example.com/" "test.html#:~:text=this%20is%20a%20test%20page", "text/html"); LoadURL( "https://example.com/" "test.html#:~:text=this%20is%20a%20test%20page"); request.Complete(R"HTML(

This is a test page

Second test page two

)HTML"); RunAsyncMatchingTasks(); ContextMenuAllowedScope context_menu_allowed_scope; Compositor().BeginFrame(); EXPECT_FALSE(GetDocument() .GetPage() ->GetContextMenuController() .ContextMenuNodeForFrame(GetDocument().GetFrame())); Range* range = Range::Create(GetDocument()); range->setStart(GetDocument().getElementById("first"), 0, IGNORE_EXCEPTION_FOR_TESTING); range->setEnd(GetDocument().getElementById("first"), 1, IGNORE_EXCEPTION_FOR_TESTING); ASSERT_EQ("This is a test page", range->GetText()); IntPoint tap_point = range->BoundingBox().Center(); SimulateTap(tap_point.X(), tap_point.Y()); if (RuntimeEnabledFeatures::TextFragmentTapOpensContextMenuEnabled()) { EXPECT_TRUE(GetDocument() .GetPage() ->GetContextMenuController() .ContextMenuNodeForFrame(GetDocument().GetFrame())); } else { EXPECT_FALSE(GetDocument() .GetPage() ->GetContextMenuController() .ContextMenuNodeForFrame(GetDocument().GetFrame())); } GetDocument().GetPage()->GetContextMenuController().ClearContextMenu(); range->setStart(GetDocument().getElementById("two"), 0, IGNORE_EXCEPTION_FOR_TESTING); range->setEndAfter(GetDocument().getElementById("two"), IGNORE_EXCEPTION_FOR_TESTING); ASSERT_EQ("Second test page two", range->GetText()); tap_point = range->BoundingBox().Center(); SimulateTap(tap_point.X(), tap_point.Y()); EXPECT_FALSE(GetDocument() .GetPage() ->GetContextMenuController() .ContextMenuNodeForFrame(GetDocument().GetFrame())); } #if BUILDFLAG(ENABLE_UNHANDLED_TAP) // Mock implementation of the UnhandledTapNotifier Mojo receiver, for testing // the ShowUnhandledTapUIIfNeeded notification. class MockUnhandledTapNotifierImpl : public mojom::blink::UnhandledTapNotifier { public: MockUnhandledTapNotifierImpl() = default; void Bind(mojo::ScopedMessagePipeHandle handle) { receiver_.Bind(mojo::PendingReceiver( std::move(handle))); } void ShowUnhandledTapUIIfNeeded( mojom::blink::UnhandledTapInfoPtr unhandled_tap_info) override { was_unhandled_tap_ = true; } bool WasUnhandledTap() const { return was_unhandled_tap_; } bool ReceiverIsBound() const { return receiver_.is_bound(); } void Reset() { was_unhandled_tap_ = false; receiver_.reset(); } private: bool was_unhandled_tap_ = false; mojo::Receiver receiver_{this}; }; #endif // BUILDFLAG(ENABLE_UNHANDLED_TAP) #if BUILDFLAG(ENABLE_UNHANDLED_TAP) // Test that on Android, when a user taps on a text, ShouldNotRequestUnhandled // does not get triggered. When a user taps on a highlight, no text should be // selected. RuntimeEnabledFeature is enabled. TEST_F(TextFragmentAnchorTest, ShouldNotRequestUnhandledTapNotifierWhenTapOnTextFragment) { base::test::ScopedFeatureList feature_list_; feature_list_.InitAndEnableFeature( shared_highlighting::kSharedHighlightingV2); LoadAhem(); SimRequest request( "https://example.com/" "test.html#:~:text=this%20is%20a%20test%20page", "text/html"); LoadURL( "https://example.com/" "test.html#:~:text=this%20is%20a%20test%20page"); request.Complete(R"HTML(

This is a test page

Second test page two

)HTML"); RunAsyncMatchingTasks(); MockUnhandledTapNotifierImpl mock_notifier; GetDocument().GetFrame()->GetBrowserInterfaceBroker().SetBinderForTesting( mojom::blink::UnhandledTapNotifier::Name_, WTF::BindRepeating(&MockUnhandledTapNotifierImpl::Bind, WTF::Unretained(&mock_notifier))); Compositor().BeginFrame(); Range* range = Range::Create(GetDocument()); range->setStart(GetDocument().getElementById("first"), 0, IGNORE_EXCEPTION_FOR_TESTING); range->setEnd(GetDocument().getElementById("first"), 1, IGNORE_EXCEPTION_FOR_TESTING); ASSERT_EQ("This is a test page", range->GetText()); mock_notifier.Reset(); IntPoint tap_point = range->BoundingBox().Center(); SimulateTap(tap_point.X(), tap_point.Y()); base::RunLoop().RunUntilIdle(); if (RuntimeEnabledFeatures::TextFragmentTapOpensContextMenuEnabled()) { EXPECT_FALSE(mock_notifier.WasUnhandledTap()); EXPECT_FALSE(mock_notifier.ReceiverIsBound()); } else { EXPECT_TRUE(mock_notifier.WasUnhandledTap()); EXPECT_TRUE(mock_notifier.ReceiverIsBound()); } range->setStart(GetDocument().getElementById("two"), 0, IGNORE_EXCEPTION_FOR_TESTING); range->setEndAfter(GetDocument().getElementById("two"), IGNORE_EXCEPTION_FOR_TESTING); ASSERT_EQ("Second test page two", range->GetText()); mock_notifier.Reset(); tap_point = range->BoundingBox().Center(); SimulateTap(tap_point.X(), tap_point.Y()); base::RunLoop().RunUntilIdle(); EXPECT_TRUE(mock_notifier.WasUnhandledTap()); EXPECT_TRUE(mock_notifier.ReceiverIsBound()); } #endif // BUILDFLAG(ENABLE_UNHANDLED_TAP) TEST_F(TextFragmentAnchorTest, TapOpeningContextMenuWithDirtyLifecycleNoCrash) { ScopedTextFragmentTapOpensContextMenuForTest tap_opens_context_menu(true); base::test::ScopedFeatureList feature_list_; feature_list_.InitAndEnableFeature( shared_highlighting::kSharedHighlightingV2); SimRequest request( "https://example.com/" "test.html#:~:text=This%20is%20just%20example", "text/html"); LoadURL( "https://example.com/" "test.html#:~:text=This%20is%20just%20example"); request.Complete(R"HTML( This is just example text that will wrap.
)HTML"); RunAsyncMatchingTasks(); ContextMenuAllowedScope context_menu_allowed_scope; Compositor().BeginFrame(); EXPECT_FALSE(GetDocument() .GetPage() ->GetContextMenuController() .ContextMenuNodeForFrame(GetDocument().GetFrame())); Node* first_paragraph = GetDocument().body()->firstChild(); const auto& start = Position(first_paragraph, 0); const auto& end = Position(first_paragraph, 27); ASSERT_EQ("This is just example", PlainText(EphemeralRange(start, end))); Range* range = CreateRange(EphemeralRange(start, end)); IntPoint tap_point = range->BoundingBox().Center(); SimulateTap(tap_point.X(), tap_point.Y()); // Expect that we won't see the context menu because we preventDefaulted the // mouseup but this test passes if it doesn't crash. EXPECT_FALSE(GetDocument() .GetPage() ->GetContextMenuController() .ContextMenuNodeForFrame(GetDocument().GetFrame())); } } // namespace } // namespace blink