1 // Copyright 2019 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4 
5 #include "build/build_config.h"
6 #include "testing/gtest/include/gtest/gtest.h"
7 #include "third_party/blink/public/platform/scheduler/test/renderer_scheduler_test_support.h"
8 #include "third_party/blink/renderer/core/dom/element.h"
9 #include "third_party/blink/renderer/core/editing/markers/document_marker_controller.h"
10 #include "third_party/blink/renderer/core/frame/local_dom_window.h"
11 #include "third_party/blink/renderer/core/frame/local_frame.h"
12 #include "third_party/blink/renderer/core/frame/local_frame_view.h"
13 #include "third_party/blink/renderer/core/frame/location.h"
14 #include "third_party/blink/renderer/core/frame/web_local_frame_impl.h"
15 #include "third_party/blink/renderer/core/html/html_element.h"
16 #include "third_party/blink/renderer/core/html/html_frame_owner_element.h"
17 #include "third_party/blink/renderer/core/input/event_handler.h"
18 #include "third_party/blink/renderer/core/layout/layout_object.h"
19 #include "third_party/blink/renderer/core/paint/paint_layer_scrollable_area.h"
20 #include "third_party/blink/renderer/core/scroll/scrollable_area.h"
21 #include "third_party/blink/renderer/core/testing/sim/sim_request.h"
22 #include "third_party/blink/renderer/core/testing/sim/sim_test.h"
23 #include "third_party/blink/renderer/platform/testing/unit_test_helpers.h"
24 
25 namespace blink {
26 
27 namespace {
28 
29 using test::RunPendingTasks;
30 
31 class TextFragmentAnchorTest : public SimTest {
32  public:
SetUp()33   void SetUp() override {
34     SimTest::SetUp();
35     WebView().MainFrameWidget()->Resize(WebSize(800, 600));
36   }
37 
RunAsyncMatchingTasks()38   void RunAsyncMatchingTasks() {
39     auto* scheduler =
40         ThreadScheduler::Current()->GetWebMainThreadSchedulerForTest();
41     blink::scheduler::RunIdleTasksForTesting(scheduler,
42                                              base::BindOnce([]() {}));
43     RunPendingTasks();
44   }
45 
LayoutViewport()46   ScrollableArea* LayoutViewport() {
47     return GetDocument().View()->LayoutViewport();
48   }
49 
ViewportRect()50   IntRect ViewportRect() {
51     return IntRect(IntPoint(), LayoutViewport()->VisibleContentRect().Size());
52   }
53 
BoundingRectInFrame(Node & node)54   IntRect BoundingRectInFrame(Node& node) {
55     return node.GetLayoutObject()->AbsoluteBoundingBoxRect();
56   }
57 
SimulateClick(int x,int y)58   void SimulateClick(int x, int y) {
59     WebMouseEvent event(WebInputEvent::kMouseDown, gfx::PointF(x, y),
60                         gfx::PointF(x, y), WebPointerProperties::Button::kLeft,
61                         0, WebInputEvent::Modifiers::kLeftButtonDown,
62                         base::TimeTicks::Now());
63     event.SetFrameScale(1);
64     GetDocument().GetFrame()->GetEventHandler().HandleMousePressEvent(event);
65   }
66 
SimulateTap(int x,int y)67   void SimulateTap(int x, int y) {
68     WebGestureEvent event(WebInputEvent::kGestureTap,
69                           WebInputEvent::kNoModifiers, base::TimeTicks::Now(),
70                           WebGestureDevice::kTouchscreen);
71     event.SetPositionInWidget(gfx::PointF(x, y));
72     event.SetPositionInScreen(gfx::PointF(x, y));
73     event.SetFrameScale(1);
74     GetDocument().GetFrame()->GetEventHandler().HandleGestureEvent(event);
75   }
76 };
77 
78 // Basic test case, ensure we scroll the matching text into view.
TEST_F(TextFragmentAnchorTest,BasicSmokeTest)79 TEST_F(TextFragmentAnchorTest, BasicSmokeTest) {
80   SimRequest request("https://example.com/test.html#:~:text=test", "text/html");
81   LoadURL("https://example.com/test.html#:~:text=test");
82   request.Complete(R"HTML(
83     <!DOCTYPE html>
84     <style>
85       body {
86         height: 1200px;
87       }
88       p {
89         position: absolute;
90         top: 1000px;
91       }
92     </style>
93     <p id="text">This is a test page</p>
94   )HTML");
95   RunAsyncMatchingTasks();
96 
97   Compositor().BeginFrame();
98 
99   Element& p = *GetDocument().getElementById("text");
100 
101   EXPECT_EQ(p, *GetDocument().CssTarget());
102   EXPECT_TRUE(ViewportRect().Contains(BoundingRectInFrame(p)))
103       << "<p> Element wasn't scrolled into view, viewport's scroll offset: "
104       << LayoutViewport()->GetScrollOffset().ToString();
105 }
106 
107 // Make sure a non-matching string doesn't cause scroll and the fragment is
108 // removed when completed.
TEST_F(TextFragmentAnchorTest,NonMatchingString)109 TEST_F(TextFragmentAnchorTest, NonMatchingString) {
110   SimRequest request("https://example.com/test.html#:~:text=unicorn",
111                      "text/html");
112   LoadURL("https://example.com/test.html#:~:text=unicorn");
113   request.Complete(R"HTML(
114     <!DOCTYPE html>
115     <style>
116       body {
117         height: 1200px;
118       }
119       p {
120         position: absolute;
121         top: 1000px;
122       }
123     </style>
124     <p id="text">This is a test page</p>
125   )HTML");
126   RunAsyncMatchingTasks();
127 
128   Compositor().BeginFrame();
129 
130   EXPECT_EQ(ScrollOffset(), LayoutViewport()->GetScrollOffset());
131 
132   // Force a layout
133   GetDocument().body()->setAttribute(html_names::kStyleAttr, "height: 1300px");
134   Compositor().BeginFrame();
135 
136   EXPECT_EQ(nullptr, GetDocument().CssTarget());
137   EXPECT_FALSE(GetDocument().View()->GetFragmentAnchor());
138   EXPECT_TRUE(GetDocument().Markers().Markers().IsEmpty());
139 }
140 
141 // Ensure multiple matches will scroll the first into view.
TEST_F(TextFragmentAnchorTest,MultipleMatches)142 TEST_F(TextFragmentAnchorTest, MultipleMatches) {
143   SimRequest request("https://example.com/test.html#:~:text=test", "text/html");
144   LoadURL("https://example.com/test.html#:~:text=test");
145   request.Complete(R"HTML(
146     <!DOCTYPE html>
147     <style>
148       body {
149         height: 2200px;
150       }
151       #first {
152         position: absolute;
153         top: 1000px;
154       }
155       #second {
156         position: absolute;
157         top: 2000px;
158       }
159     </style>
160     <p id="first">This is a test page</p>
161     <p id="second">This is a test page</p>
162   )HTML");
163   RunAsyncMatchingTasks();
164 
165   Compositor().BeginFrame();
166 
167   Element& first = *GetDocument().getElementById("first");
168 
169   EXPECT_EQ(first, *GetDocument().CssTarget());
170   EXPECT_TRUE(ViewportRect().Contains(BoundingRectInFrame(first)))
171       << "First <p> wasn't scrolled into view, viewport's scroll offset: "
172       << LayoutViewport()->GetScrollOffset().ToString();
173 
174   // Ensure we only report one marker.
175   EXPECT_EQ(1u, GetDocument().Markers().Markers().size());
176 }
177 
178 // Ensure matching works inside nested blocks.
TEST_F(TextFragmentAnchorTest,NestedBlocks)179 TEST_F(TextFragmentAnchorTest, NestedBlocks) {
180   SimRequest request("https://example.com/test.html#:~:text=test", "text/html");
181   LoadURL("https://example.com/test.html#:~:text=test");
182   request.Complete(R"HTML(
183     <!DOCTYPE html>
184     <style>
185       #spacer {
186         height: 1000px;
187       }
188     </style>
189     <body>
190       <div id="spacer">
191         Some non-matching text
192       </div>
193       <div>
194         <p id="match">This is a test page</p>
195       </div>
196     </body>
197   )HTML");
198   RunAsyncMatchingTasks();
199 
200   Compositor().BeginFrame();
201 
202   Element& match = *GetDocument().getElementById("match");
203 
204   EXPECT_EQ(match, *GetDocument().CssTarget());
205   EXPECT_TRUE(ViewportRect().Contains(BoundingRectInFrame(match)))
206       << "<p> wasn't scrolled into view, viewport's scroll offset: "
207       << LayoutViewport()->GetScrollOffset().ToString();
208 }
209 
210 // Ensure multiple texts are highlighted and the first is scrolled into
211 // view.
TEST_F(TextFragmentAnchorTest,MultipleTextFragments)212 TEST_F(TextFragmentAnchorTest, MultipleTextFragments) {
213   SimRequest request("https://example.com/test.html#:~:text=test&text=more",
214                      "text/html");
215   LoadURL("https://example.com/test.html#:~:text=test&text=more");
216   request.Complete(R"HTML(
217     <!DOCTYPE html>
218     <style>
219       body {
220         height: 2200px;
221       }
222       #first {
223         position: absolute;
224         top: 1000px;
225       }
226       #second {
227         position: absolute;
228         top: 2000px;
229       }
230     </style>
231     <p id="first">This is a test page</p>
232     <p id="second">This is some more text</p>
233   )HTML");
234   RunAsyncMatchingTasks();
235 
236   Compositor().BeginFrame();
237 
238   Element& first = *GetDocument().getElementById("first");
239 
240   EXPECT_EQ(first, *GetDocument().CssTarget());
241   EXPECT_TRUE(ViewportRect().Contains(BoundingRectInFrame(first)))
242       << "First <p> wasn't scrolled into view, viewport's scroll offset: "
243       << LayoutViewport()->GetScrollOffset().ToString();
244 
245   EXPECT_EQ(2u, GetDocument().Markers().Markers().size());
246 }
247 
248 // Ensure we scroll the second text into view if the first isn't found.
TEST_F(TextFragmentAnchorTest,FirstTextFragmentNotFound)249 TEST_F(TextFragmentAnchorTest, FirstTextFragmentNotFound) {
250   SimRequest request("https://example.com/test.html#:~:text=test&text=more",
251                      "text/html");
252   LoadURL("https://example.com/test.html#:~:text=test&text=more");
253   request.Complete(R"HTML(
254     <!DOCTYPE html>
255     <style>
256       body {
257         height: 2200px;
258       }
259       #first {
260         position: absolute;
261         top: 1000px;
262       }
263       #second {
264         position: absolute;
265         top: 2000px;
266       }
267     </style>
268     <p id="first">This is a page</p>
269     <p id="second">This is some more text</p>
270   )HTML");
271   RunAsyncMatchingTasks();
272 
273   Compositor().BeginFrame();
274 
275   Element& second = *GetDocument().getElementById("second");
276 
277   EXPECT_EQ(second, *GetDocument().CssTarget());
278   EXPECT_TRUE(ViewportRect().Contains(BoundingRectInFrame(second)))
279       << "Second <p> wasn't scrolled into view, viewport's scroll offset: "
280       << LayoutViewport()->GetScrollOffset().ToString();
281 
282   EXPECT_EQ(1u, GetDocument().Markers().Markers().size());
283 }
284 
285 // Ensure we still scroll the first text into view if the second isn't
286 // found.
TEST_F(TextFragmentAnchorTest,OnlyFirstTextFragmentFound)287 TEST_F(TextFragmentAnchorTest, OnlyFirstTextFragmentFound) {
288   SimRequest request("https://example.com/test.html#:~:text=test&text=more",
289                      "text/html");
290   LoadURL("https://example.com/test.html#:~:text=test&text=more");
291   request.Complete(R"HTML(
292     <!DOCTYPE html>
293     <style>
294       body {
295         height: 1200px;
296       }
297       p {
298         position: absolute;
299         top: 1000px;
300       }
301     </style>
302     <p id="text">This is a test page</p>
303   )HTML");
304   RunAsyncMatchingTasks();
305 
306   Compositor().BeginFrame();
307 
308   Element& p = *GetDocument().getElementById("text");
309 
310   EXPECT_EQ(p, *GetDocument().CssTarget());
311   EXPECT_TRUE(ViewportRect().Contains(BoundingRectInFrame(p)))
312       << "<p> Element wasn't scrolled into view, viewport's scroll offset: "
313       << LayoutViewport()->GetScrollOffset().ToString();
314 
315   EXPECT_EQ(1u, GetDocument().Markers().Markers().size());
316 }
317 
318 // Make sure multiple non-matching strings doesn't cause scroll and the fragment
319 // is removed when completed.
TEST_F(TextFragmentAnchorTest,MultipleNonMatchingStrings)320 TEST_F(TextFragmentAnchorTest, MultipleNonMatchingStrings) {
321   SimRequest request(
322       "https://example.com/"
323       "test.html#:~:text=unicorn&text=cookie&text=cat",
324       "text/html");
325   LoadURL(
326       "https://example.com/"
327       "test.html#:~:text=unicorn&text=cookie&text=cat");
328   request.Complete(R"HTML(
329     <!DOCTYPE html>
330     <style>
331       body {
332         height: 1200px;
333       }
334       p {
335         position: absolute;
336         top: 1000px;
337       }
338     </style>
339     <p id="text">This is a test page</p>
340   )HTML");
341   RunAsyncMatchingTasks();
342 
343   Compositor().BeginFrame();
344 
345   EXPECT_EQ(ScrollOffset(), LayoutViewport()->GetScrollOffset());
346 
347   // Force a layout
348   GetDocument().body()->setAttribute(html_names::kStyleAttr, "height: 1300px");
349   Compositor().BeginFrame();
350 
351   EXPECT_EQ(nullptr, GetDocument().CssTarget());
352   EXPECT_FALSE(GetDocument().View()->GetFragmentAnchor());
353   EXPECT_TRUE(GetDocument().Markers().Markers().IsEmpty());
354 }
355 
356 // Test matching a text range within the same element
TEST_F(TextFragmentAnchorTest,SameElementTextRange)357 TEST_F(TextFragmentAnchorTest, SameElementTextRange) {
358   SimRequest request("https://example.com/test.html#:~:text=This,page",
359                      "text/html");
360   LoadURL("https://example.com/test.html#:~:text=This,page");
361   request.Complete(R"HTML(
362     <!DOCTYPE html>
363     <style>
364       body {
365         height: 1200px;
366       }
367       p {
368         position: absolute;
369         top: 1000px;
370       }
371     </style>
372     <p id="text">This is a test page</p>
373   )HTML");
374   RunAsyncMatchingTasks();
375 
376   Compositor().BeginFrame();
377 
378   EXPECT_EQ(*GetDocument().getElementById("text"), *GetDocument().CssTarget());
379   EXPECT_EQ(1u, GetDocument().Markers().Markers().size());
380 
381   // Expect marker on "This is a test page".
382   auto* text = To<Text>(GetDocument().getElementById("text")->firstChild());
383   DocumentMarkerVector markers = GetDocument().Markers().MarkersFor(
384       *text, DocumentMarker::MarkerTypes::TextFragment());
385   ASSERT_EQ(1u, markers.size());
386   EXPECT_EQ(0u, markers.at(0)->StartOffset());
387   EXPECT_EQ(19u, markers.at(0)->EndOffset());
388 }
389 
390 // Test matching a text range across two neighboring elements
TEST_F(TextFragmentAnchorTest,NeighboringElementTextRange)391 TEST_F(TextFragmentAnchorTest, NeighboringElementTextRange) {
392   SimRequest request("https://example.com/test.html#:~:text=test,paragraph",
393                      "text/html");
394   LoadURL("https://example.com/test.html#:~:text=test,paragraph");
395   request.Complete(R"HTML(
396     <!DOCTYPE html>
397     <style>
398       body {
399         height: 1200px;
400       }
401       p {
402         position: absolute;
403         top: 1000px;
404       }
405     </style>
406     <p id="text1">This is a test page</p>
407     <p id="text2">with another paragraph of text</p>
408   )HTML");
409   RunAsyncMatchingTasks();
410 
411   Compositor().BeginFrame();
412 
413   EXPECT_EQ(*GetDocument().body(), *GetDocument().CssTarget());
414   EXPECT_EQ(2u, GetDocument().Markers().Markers().size());
415 
416   // Expect marker on "test page"
417   auto* text1 = To<Text>(GetDocument().getElementById("text1")->firstChild());
418   DocumentMarkerVector markers = GetDocument().Markers().MarkersFor(
419       *text1, DocumentMarker::MarkerTypes::TextFragment());
420   ASSERT_EQ(1u, markers.size());
421   EXPECT_EQ(10u, markers.at(0)->StartOffset());
422   EXPECT_EQ(19u, markers.at(0)->EndOffset());
423 
424   // Expect marker on "with another paragraph"
425   auto* text2 = To<Text>(GetDocument().getElementById("text2")->firstChild());
426   markers = GetDocument().Markers().MarkersFor(
427       *text2, DocumentMarker::MarkerTypes::TextFragment());
428   ASSERT_EQ(1u, markers.size());
429   EXPECT_EQ(0u, markers.at(0)->StartOffset());
430   EXPECT_EQ(22u, markers.at(0)->EndOffset());
431 }
432 
433 // Test matching a text range from an element to a deeper nested element
TEST_F(TextFragmentAnchorTest,DifferentDepthElementTextRange)434 TEST_F(TextFragmentAnchorTest, DifferentDepthElementTextRange) {
435   SimRequest request("https://example.com/test.html#:~:text=test,paragraph",
436                      "text/html");
437   LoadURL("https://example.com/test.html#:~:text=test,paragraph");
438   request.Complete(R"HTML(
439     <!DOCTYPE html>
440     <style>
441       body {
442         height: 1200px;
443       }
444       p {
445         position: absolute;
446         top: 1000px;
447       }
448     </style>
449     <p id="text1">This is a test page</p>
450     <div>
451       <p id="text2">with another paragraph of text</p>
452     </div>
453   )HTML");
454   RunAsyncMatchingTasks();
455 
456   Compositor().BeginFrame();
457 
458   EXPECT_EQ(*GetDocument().body(), *GetDocument().CssTarget());
459   EXPECT_EQ(2u, GetDocument().Markers().Markers().size());
460 
461   // Expect marker on "test page"
462   auto* text1 = To<Text>(GetDocument().getElementById("text1")->firstChild());
463   DocumentMarkerVector markers = GetDocument().Markers().MarkersFor(
464       *text1, DocumentMarker::MarkerTypes::TextFragment());
465   ASSERT_EQ(1u, markers.size());
466   EXPECT_EQ(10u, markers.at(0)->StartOffset());
467   EXPECT_EQ(19u, markers.at(0)->EndOffset());
468 
469   // Expect marker on "with another paragraph"
470   auto* text2 = To<Text>(GetDocument().getElementById("text2")->firstChild());
471   markers = GetDocument().Markers().MarkersFor(
472       *text2, DocumentMarker::MarkerTypes::TextFragment());
473   ASSERT_EQ(1u, markers.size());
474   EXPECT_EQ(0u, markers.at(0)->StartOffset());
475   EXPECT_EQ(22u, markers.at(0)->EndOffset());
476 }
477 
478 // Ensure that we don't match anything if endText is not found.
TEST_F(TextFragmentAnchorTest,TextRangeEndTextNotFound)479 TEST_F(TextFragmentAnchorTest, TextRangeEndTextNotFound) {
480   SimRequest request("https://example.com/test.html#:~:text=test,cat",
481                      "text/html");
482   LoadURL("https://example.com/test.html#:~:text=test,cat");
483   request.Complete(R"HTML(
484     <!DOCTYPE html>
485     <style>
486       body {
487         height: 1200px;
488       }
489       p {
490         position: absolute;
491         top: 1000px;
492       }
493     </style>
494     <p id="text">This is a test page</p>
495   )HTML");
496   RunAsyncMatchingTasks();
497 
498   EXPECT_EQ(nullptr, GetDocument().CssTarget());
499   EXPECT_EQ(0u, GetDocument().Markers().Markers().size());
500   EXPECT_EQ(ScrollOffset(), LayoutViewport()->GetScrollOffset());
501 }
502 
503 // Test matching multiple text ranges
TEST_F(TextFragmentAnchorTest,MultipleTextRanges)504 TEST_F(TextFragmentAnchorTest, MultipleTextRanges) {
505   SimRequest request(
506       "https://example.com/"
507       "test.html#:~:text=test,with&text=paragraph,text",
508       "text/html");
509   LoadURL(
510       "https://example.com/"
511       "test.html#:~:text=test,with&text=paragraph,text");
512   request.Complete(R"HTML(
513     <!DOCTYPE html>
514     <style>
515       body {
516         height: 1200px;
517       }
518       p {
519         position: absolute;
520         top: 1000px;
521       }
522     </style>
523     <p id="text1">This is a test page</p>
524     <div>
525       <p id="text2">with another paragraph of text</p>
526     </div>
527   )HTML");
528   RunAsyncMatchingTasks();
529 
530   Compositor().BeginFrame();
531 
532   EXPECT_EQ(*GetDocument().body(), *GetDocument().CssTarget());
533   EXPECT_EQ(3u, GetDocument().Markers().Markers().size());
534 
535   // Expect marker on "test page"
536   auto* text1 = To<Text>(GetDocument().getElementById("text1")->firstChild());
537   DocumentMarkerVector markers = GetDocument().Markers().MarkersFor(
538       *text1, DocumentMarker::MarkerTypes::TextFragment());
539   ASSERT_EQ(1u, markers.size());
540   EXPECT_EQ(10u, markers.at(0)->StartOffset());
541   EXPECT_EQ(19u, markers.at(0)->EndOffset());
542 
543   // Expect markers on "with" and "paragraph of text"
544   auto* text2 = To<Text>(GetDocument().getElementById("text2")->firstChild());
545   markers = GetDocument().Markers().MarkersFor(
546       *text2, DocumentMarker::MarkerTypes::TextFragment());
547   ASSERT_EQ(2u, markers.size());
548   EXPECT_EQ(0u, markers.at(0)->StartOffset());
549   EXPECT_EQ(4u, markers.at(0)->EndOffset());
550   EXPECT_EQ(13u, markers.at(1)->StartOffset());
551   EXPECT_EQ(30u, markers.at(1)->EndOffset());
552 }
553 
554 // Ensure we scroll to the beginning of a text range larger than the viewport.
TEST_F(TextFragmentAnchorTest,DistantElementTextRange)555 TEST_F(TextFragmentAnchorTest, DistantElementTextRange) {
556   SimRequest request("https://example.com/test.html#:~:text=test,paragraph",
557                      "text/html");
558   LoadURL("https://example.com/test.html#:~:text=test,paragraph");
559   request.Complete(R"HTML(
560     <!DOCTYPE html>
561     <style>
562       p {
563         margin-top: 3000px;
564       }
565     </style>
566     <p id="text">This is a test page</p>
567     <p>with another paragraph of text</p>
568   )HTML");
569   RunAsyncMatchingTasks();
570 
571   Compositor().BeginFrame();
572 
573   Element& p = *GetDocument().getElementById("text");
574   EXPECT_TRUE(ViewportRect().Contains(BoundingRectInFrame(p)))
575       << "<p> Element wasn't scrolled into view, viewport's scroll offset: "
576       << LayoutViewport()->GetScrollOffset().ToString();
577   EXPECT_EQ(2u, GetDocument().Markers().Markers().size());
578 }
579 
580 // Test a text range with both context terms in the same element.
TEST_F(TextFragmentAnchorTest,TextRangeWithContext)581 TEST_F(TextFragmentAnchorTest, TextRangeWithContext) {
582   SimRequest request(
583       "https://example.com/test.html#:~:text=This-,is,test,-page", "text/html");
584   LoadURL("https://example.com/test.html#:~:text=This-,is,test,-page");
585   request.Complete(R"HTML(
586     <!DOCTYPE html>
587     <p id="text">This is a test page</p>
588   )HTML");
589   RunAsyncMatchingTasks();
590 
591   Compositor().BeginFrame();
592 
593   EXPECT_EQ(*GetDocument().getElementById("text"), *GetDocument().CssTarget());
594   EXPECT_EQ(1u, GetDocument().Markers().Markers().size());
595 
596   // Expect marker on "is a test".
597   auto* text = To<Text>(GetDocument().getElementById("text")->firstChild());
598   DocumentMarkerVector markers = GetDocument().Markers().MarkersFor(
599       *text, DocumentMarker::MarkerTypes::TextFragment());
600   ASSERT_EQ(1u, markers.size());
601   EXPECT_EQ(5u, markers.at(0)->StartOffset());
602   EXPECT_EQ(14u, markers.at(0)->EndOffset());
603 }
604 
605 // Ensure that we do not match a text range if the prefix is not found.
TEST_F(TextFragmentAnchorTest,PrefixNotFound)606 TEST_F(TextFragmentAnchorTest, PrefixNotFound) {
607   SimRequest request(
608       "https://example.com/test.html#:~:text=prefix-,is,test,-page",
609       "text/html");
610   LoadURL("https://example.com/test.html#:~:text=prefix-,is,test,-page");
611   request.Complete(R"HTML(
612     <!DOCTYPE html>
613     <p id="text">This is a test page</p>
614   )HTML");
615   Compositor().BeginFrame();
616 
617   RunAsyncMatchingTasks();
618 
619   EXPECT_EQ(nullptr, GetDocument().CssTarget());
620   EXPECT_EQ(0u, GetDocument().Markers().Markers().size());
621 }
622 
623 // Ensure that we do not match a text range if the suffix is not found.
TEST_F(TextFragmentAnchorTest,SuffixNotFound)624 TEST_F(TextFragmentAnchorTest, SuffixNotFound) {
625   SimRequest request(
626       "https://example.com/test.html#:~:text=This-,is,test,-suffix",
627       "text/html");
628   LoadURL("https://example.com/test.html#:~:text=This-,is,test,-suffix");
629   request.Complete(R"HTML(
630     <!DOCTYPE html>
631     <p id="text">This is a test page</p>
632   )HTML");
633   RunAsyncMatchingTasks();
634 
635   EXPECT_EQ(nullptr, GetDocument().CssTarget());
636   EXPECT_EQ(0u, GetDocument().Markers().Markers().size());
637 }
638 
639 // Test a text range with context terms in different elements
TEST_F(TextFragmentAnchorTest,TextRangeWithCrossElementContext)640 TEST_F(TextFragmentAnchorTest, TextRangeWithCrossElementContext) {
641   SimRequest request(
642       "https://example.com/test.html#:~:text=Header%202-,A,text,-Footer%201",
643       "text/html");
644   LoadURL(
645       "https://example.com/"
646       "test.html#:~:text=Header%202-,A,text,-Footer%201");
647   request.Complete(R"HTML(
648     <!DOCTYPE html>
649     <h1>Header 1</h1>
650     <p>A string of text</p>
651     <p>Footer 1</p>
652     <h1>Header 2</h1>
653     <p id="expected">A string of text</p>
654     <p>Footer 1</p>
655     <h1>Header 2</h1>
656     <p>A string of text</p>
657     <p>Footer 2</p>
658   )HTML");
659   RunAsyncMatchingTasks();
660 
661   Compositor().BeginFrame();
662 
663   EXPECT_EQ(*GetDocument().getElementById("expected"),
664             *GetDocument().CssTarget());
665   EXPECT_EQ(1u, GetDocument().Markers().Markers().size());
666 
667   // Expect marker on the expected "A string of text".
668   auto* text = To<Text>(GetDocument().getElementById("expected")->firstChild());
669   DocumentMarkerVector markers = GetDocument().Markers().MarkersFor(
670       *text, DocumentMarker::MarkerTypes::TextFragment());
671   ASSERT_EQ(1u, markers.size());
672   EXPECT_EQ(0u, markers.at(0)->StartOffset());
673   EXPECT_EQ(16u, markers.at(0)->EndOffset());
674 }
675 
676 // Test context terms separated by elements and whitespace
TEST_F(TextFragmentAnchorTest,CrossElementAndWhitespaceContext)677 TEST_F(TextFragmentAnchorTest, CrossElementAndWhitespaceContext) {
678   SimRequest request(
679       "https://example.com/"
680       "test.html#:~:text=List%202-,Cat,-Good%20cat",
681       "text/html");
682   LoadURL(
683       "https://example.com/"
684       "test.html#:~:text=List%202-,Cat,-Good%20cat");
685   request.Complete(R"HTML(
686     <!DOCTYPE html>
687     <h1> List 1 </h1>
688     <div>
689       <p>Cat</p>
690       <p>&nbsp;Good cat</p>
691     </div>
692     <h1> List 2 </h1>
693     <div>
694       <p id="expected">Cat</p>
695       <p>&nbsp;Good cat</p>
696     </div>
697     <h1> List 2 </h1>
698     <div>
699       <p>Cat</p>
700       <p>&nbsp;Bad cat</p>
701     </div>
702   )HTML");
703   RunAsyncMatchingTasks();
704 
705   Compositor().BeginFrame();
706 
707   EXPECT_EQ(*GetDocument().getElementById("expected"),
708             *GetDocument().CssTarget());
709   EXPECT_EQ(1u, GetDocument().Markers().Markers().size());
710 
711   // Expect marker on the expected "cat".
712   auto* text = To<Text>(GetDocument().getElementById("expected")->firstChild());
713   DocumentMarkerVector markers = GetDocument().Markers().MarkersFor(
714       *text, DocumentMarker::MarkerTypes::TextFragment());
715   ASSERT_EQ(1u, markers.size());
716   EXPECT_EQ(0u, markers.at(0)->StartOffset());
717   EXPECT_EQ(3u, markers.at(0)->EndOffset());
718 }
719 
720 // Test context terms separated by empty sibling and parent elements
TEST_F(TextFragmentAnchorTest,CrossEmptySiblingAndParentElementContext)721 TEST_F(TextFragmentAnchorTest, CrossEmptySiblingAndParentElementContext) {
722   SimRequest request(
723       "https://example.com/"
724       "test.html#:~:text=prefix-,match,-suffix",
725       "text/html");
726   LoadURL(
727       "https://example.com/"
728       "test.html#:~:text=prefix-,match,-suffix");
729   request.Complete(R"HTML(
730     <!DOCTYPE html>
731     <div>
732       <p>prefix</p>
733     <div>
734     <p><br>&nbsp;</p>
735     <div id="expected">match</div>
736     <p><br>&nbsp;</p>
737     <div>
738       <p>suffix</p>
739     <div>
740   )HTML");
741   RunAsyncMatchingTasks();
742 
743   Compositor().BeginFrame();
744 
745   EXPECT_EQ(*GetDocument().getElementById("expected"),
746             *GetDocument().CssTarget());
747   EXPECT_EQ(1u, GetDocument().Markers().Markers().size());
748 
749   // Expect marker on "match".
750   auto* text = To<Text>(GetDocument().getElementById("expected")->firstChild());
751   DocumentMarkerVector markers = GetDocument().Markers().MarkersFor(
752       *text, DocumentMarker::MarkerTypes::TextFragment());
753   ASSERT_EQ(1u, markers.size());
754   EXPECT_EQ(0u, markers.at(0)->StartOffset());
755   EXPECT_EQ(5u, markers.at(0)->EndOffset());
756 }
757 
758 // Ensure we scroll to text when its prefix and suffix are out of view.
TEST_F(TextFragmentAnchorTest,DistantElementContext)759 TEST_F(TextFragmentAnchorTest, DistantElementContext) {
760   SimRequest request(
761       "https://example.com/test.html#:~:text=Prefix-,Cats,-Suffix",
762       "text/html");
763   LoadURL("https://example.com/test.html#:~:text=Prefix-,Cats,-Suffix");
764   request.Complete(R"HTML(
765     <!DOCTYPE html>
766     <style>
767       p {
768         margin-top: 3000px;
769       }
770     </style>
771     <p>Cats</p>
772     <p>Prefix</p>
773     <p id="text">Cats</p>
774     <p>Suffix</p>
775   )HTML");
776   RunAsyncMatchingTasks();
777 
778   Compositor().BeginFrame();
779 
780   Element& p = *GetDocument().getElementById("text");
781   EXPECT_TRUE(ViewportRect().Contains(BoundingRectInFrame(p)))
782       << "<p> Element wasn't scrolled into view, viewport's scroll offset: "
783       << LayoutViewport()->GetScrollOffset().ToString();
784   EXPECT_EQ(1u, GetDocument().Markers().Markers().size());
785 }
786 
787 // Test specifying just one of the prefix and suffix
TEST_F(TextFragmentAnchorTest,OneContextTerm)788 TEST_F(TextFragmentAnchorTest, OneContextTerm) {
789   SimRequest request(
790       "https://example.com/"
791       "test.html#:~:text=test-,page&text=page,-with%20real%20content",
792       "text/html");
793   LoadURL(
794       "https://example.com/"
795       "test.html#:~:text=test-,page&text=page,-with%20real%20content");
796   request.Complete(R"HTML(
797     <!DOCTYPE html>
798     <p id="text1">This is a test page</p>
799     <p id="text2">Not a page with real content</p>
800   )HTML");
801   RunAsyncMatchingTasks();
802 
803   Compositor().BeginFrame();
804 
805   EXPECT_EQ(*GetDocument().getElementById("text1"), *GetDocument().CssTarget());
806 
807   // Expect marker on the first "page"
808   auto* text1 = To<Text>(GetDocument().getElementById("text1")->firstChild());
809   DocumentMarkerVector markers = GetDocument().Markers().MarkersFor(
810       *text1, DocumentMarker::MarkerTypes::TextFragment());
811   ASSERT_EQ(1u, markers.size());
812   EXPECT_EQ(15u, markers.at(0)->StartOffset());
813   EXPECT_EQ(19u, markers.at(0)->EndOffset());
814 
815   // Expect marker on the second "page"
816   auto* text2 = To<Text>(GetDocument().getElementById("text2")->firstChild());
817   markers = GetDocument().Markers().MarkersFor(
818       *text2, DocumentMarker::MarkerTypes::TextFragment());
819   ASSERT_EQ(1u, markers.size());
820   EXPECT_EQ(6u, markers.at(0)->StartOffset());
821   EXPECT_EQ(10u, markers.at(0)->EndOffset());
822 }
823 
824 // Test that a user scroll cancels the scroll into view.
TEST_F(TextFragmentAnchorTest,ScrollCancelled)825 TEST_F(TextFragmentAnchorTest, ScrollCancelled) {
826   SimRequest request("https://example.com/test.html#:~:text=test", "text/html");
827   SimSubresourceRequest css_request("https://example.com/test.css", "text/css");
828   SimSubresourceRequest img_request("https://example.com/test.png",
829                                     "image/png");
830   LoadURL("https://example.com/test.html#:~:text=test");
831   request.Complete(R"HTML(
832     <!DOCTYPE html>
833     <style>
834       body {
835         height: 1200px;
836       }
837       p {
838         position: absolute;
839         top: 1000px;
840         visibility: hidden;
841       }
842     </style>
843     <link rel=stylesheet href=test.css>
844     <p id="text">This is a test page</p>
845     <img src="test.png">
846   )HTML");
847 
848   Compositor().PaintFrame();
849   if (!RuntimeEnabledFeatures::BlockHTMLParserOnStyleSheetsEnabled()) {
850     GetDocument().View()->LayoutViewport()->ScrollBy(
851         ScrollOffset(0, 100), mojom::blink::ScrollType::kUser);
852     // Set the target text to visible and change its position to cause a layout
853     // and invoke the fragment anchor in the next begin frame.
854     css_request.Complete("p { visibility: visible; top: 1001px; }");
855     img_request.Complete("");
856   } else {
857     // Set the target text to visible and change its position to cause a layout
858     // and invoke the fragment anchor in the next begin frame.
859     css_request.Complete("p { visibility: visible; top: 1001px; }");
860     RunPendingTasks();
861     Compositor().BeginFrame();
862     Element& p = *GetDocument().getElementById("text");
863 
864     // We should have invoked the fragment and scrolled the <p> into view, but
865     // load should not yet be complete due to the image.
866     EXPECT_TRUE(ViewportRect().Contains(BoundingRectInFrame(p)));
867     ASSERT_FALSE(GetDocument().IsLoadCompleted());
868 
869     // Before invoking again, perform a user scroll. This should abort future
870     // scrolls during fragment invocation.
871     GetDocument().View()->LayoutViewport()->SetScrollOffset(
872         ScrollOffset(0, 0), mojom::blink::ScrollType::kUser);
873     ASSERT_FALSE(ViewportRect().Contains(BoundingRectInFrame(p)));
874 
875     img_request.Complete("");
876     RunPendingTasks();
877     ASSERT_TRUE(GetDocument().IsLoadCompleted());
878   }
879 
880   RunAsyncMatchingTasks();
881 
882   Compositor().BeginFrame();
883 
884   Element& p = *GetDocument().getElementById("text");
885   EXPECT_FALSE(ViewportRect().Contains(BoundingRectInFrame(p)));
886 
887   EXPECT_EQ(p, *GetDocument().CssTarget());
888   EXPECT_EQ(1u, GetDocument().Markers().Markers().size());
889 
890   // Expect marker on "test"
891   auto* text = To<Text>(p.firstChild());
892   DocumentMarkerVector markers = GetDocument().Markers().MarkersFor(
893       *text, DocumentMarker::MarkerTypes::TextFragment());
894   ASSERT_EQ(1u, markers.size());
895   EXPECT_EQ(10u, markers.at(0)->StartOffset());
896   EXPECT_EQ(14u, markers.at(0)->EndOffset());
897 }
898 
899 // Ensure that the text fragment anchor has no effect in an iframe. This is
900 // disabled in iframes by design, for security reasons.
TEST_F(TextFragmentAnchorTest,DisabledInIframes)901 TEST_F(TextFragmentAnchorTest, DisabledInIframes) {
902   SimRequest main_request("https://example.com/test.html", "text/html");
903   SimRequest child_request("https://example.com/child.html#:~:text=test",
904                            "text/html");
905   LoadURL("https://example.com/test.html");
906   main_request.Complete(R"HTML(
907     <!DOCTYPE html>
908     <iframe id="iframe" src="child.html#:~:text=test"></iframe>
909   )HTML");
910 
911   child_request.Complete(R"HTML(
912     <!DOCTYPE html>
913     <style>
914       p {
915         margin-top: 1000px;
916       }
917     </style>
918     <p>
919       test
920     </p>
921   )HTML");
922   RunAsyncMatchingTasks();
923 
924   Compositor().BeginFrame();
925 
926   Element* iframe = GetDocument().getElementById("iframe");
927   auto* child_frame =
928       To<LocalFrame>(To<HTMLFrameOwnerElement>(iframe)->ContentFrame());
929 
930   EXPECT_EQ(nullptr, GetDocument().CssTarget());
931   EXPECT_EQ(ScrollOffset(),
932             child_frame->View()->GetScrollableArea()->GetScrollOffset());
933 }
934 
935 // Similarly to the iframe case, we also want to prevent activating a text
936 // fragment anchor inside a window.opened window.
TEST_F(TextFragmentAnchorTest,DisabledInWindowOpen)937 TEST_F(TextFragmentAnchorTest, DisabledInWindowOpen) {
938   String destination = "https://example.com/child.html#:~:text=test";
939 
940   SimRequest main_request("https://example.com/test.html", "text/html");
941   SimRequest child_request(destination, "text/html");
942   LoadURL("https://example.com/test.html");
943   main_request.Complete(R"HTML(
944     <!DOCTYPE html>
945   )HTML");
946   Compositor().BeginFrame();
947 
948   LocalDOMWindow* main_window = GetDocument().GetFrame()->DomWindow();
949 
950   ScriptState* script_state =
951       ToScriptStateForMainWorld(main_window->GetFrame());
952   ScriptState::Scope entered_context_scope(script_state);
953   LocalDOMWindow* child_window = To<LocalDOMWindow>(
954       main_window->open(script_state->GetIsolate(), destination, "frame1", "",
955                         ASSERT_NO_EXCEPTION));
956   ASSERT_TRUE(child_window);
957 
958   RunPendingTasks();
959   child_request.Complete(R"HTML(
960     <!DOCTYPE html>
961     <style>
962       p {
963         margin-top: 1000px;
964       }
965     </style>
966     <p>
967       test
968     </p>
969   )HTML");
970 
971   RunAsyncMatchingTasks();
972 
973   EXPECT_EQ(nullptr, child_window->document()->CssTarget());
974 
975   LocalFrameView* child_view = child_window->GetFrame()->View();
976   EXPECT_EQ(ScrollOffset(), child_view->GetScrollableArea()->GetScrollOffset());
977 }
978 
979 // Ensure that the text fragment anchor is not activated by same-document script
980 // navigations.
TEST_F(TextFragmentAnchorTest,DisabledInSamePageNavigation)981 TEST_F(TextFragmentAnchorTest, DisabledInSamePageNavigation) {
982   SimRequest main_request("https://example.com/test.html", "text/html");
983   LoadURL("https://example.com/test.html");
984   main_request.Complete(R"HTML(
985     <!DOCTYPE html>
986     <style>
987       p {
988         margin-top: 1000px;
989       }
990     </style>
991     <p>
992       test
993     </p>
994   )HTML");
995   RunAsyncMatchingTasks();
996 
997   Compositor().BeginFrame();
998 
999   ASSERT_EQ(ScrollOffset(),
1000             GetDocument().View()->GetScrollableArea()->GetScrollOffset());
1001 
1002   ScriptState* script_state =
1003       ToScriptStateForMainWorld(GetDocument().GetFrame());
1004   ScriptState::Scope entered_context_scope(script_state);
1005   GetDocument().GetFrame()->DomWindow()->location()->setHash(
1006       script_state->GetIsolate(), ":~:text=test", ASSERT_NO_EXCEPTION);
1007   RunAsyncMatchingTasks();
1008 
1009   EXPECT_EQ(nullptr, GetDocument().CssTarget());
1010   EXPECT_EQ(ScrollOffset(), LayoutViewport()->GetScrollOffset());
1011 }
1012 
1013 // Ensure matching is case insensitive.
TEST_F(TextFragmentAnchorTest,CaseInsensitive)1014 TEST_F(TextFragmentAnchorTest, CaseInsensitive) {
1015   SimRequest request("https://example.com/test.html#:~:text=Test", "text/html");
1016   LoadURL("https://example.com/test.html#:~:text=Test");
1017   request.Complete(R"HTML(
1018     <!DOCTYPE html>
1019     <style>
1020       body {
1021         height: 1200px;
1022       }
1023       p {
1024         position: absolute;
1025         top: 1000px;
1026       }
1027     </style>
1028     <p id="text">test</p>
1029   )HTML");
1030   RunAsyncMatchingTasks();
1031 
1032   Compositor().BeginFrame();
1033 
1034   Element& p = *GetDocument().getElementById("text");
1035 
1036   EXPECT_TRUE(ViewportRect().Contains(BoundingRectInFrame(p)))
1037       << "<p> Element wasn't scrolled into view, viewport's scroll offset: "
1038       << LayoutViewport()->GetScrollOffset().ToString();
1039 
1040   EXPECT_EQ(1u, GetDocument().Markers().Markers().size());
1041 }
1042 
1043 // Test that the fragment anchor stays centered in view throughout loading.
TEST_F(TextFragmentAnchorTest,TargetStaysInView)1044 TEST_F(TextFragmentAnchorTest, TargetStaysInView) {
1045   SimRequest main_request("https://example.com/test.html#:~:text=test",
1046                           "text/html");
1047   SimRequest image_request("https://example.com/image.svg", "image/svg+xml");
1048   LoadURL("https://example.com/test.html#:~:text=test");
1049   main_request.Complete(R"HTML(
1050     <!DOCTYPE html>
1051     <style>
1052       p {
1053         margin-top: 1000px;
1054       }
1055     </style>
1056     <img src="image.svg">
1057     <p id="text">test</p>
1058   )HTML");
1059   RunAsyncMatchingTasks();
1060 
1061   Compositor().PaintFrame();
1062 
1063   ScrollOffset first_scroll_offset = LayoutViewport()->GetScrollOffset();
1064   ASSERT_NE(ScrollOffset(), first_scroll_offset);
1065 
1066   Element& p = *GetDocument().getElementById("text");
1067   IntRect first_bounding_rect = BoundingRectInFrame(p);
1068   EXPECT_TRUE(ViewportRect().Contains(first_bounding_rect));
1069 
1070   // Load an image that pushes the target text out of view
1071   image_request.Complete(R"SVG(
1072     <svg xmlns="http://www.w3.org/2000/svg" width="200" height="2000">
1073       <rect fill="green" width="200" height="2000"/>
1074     </svg>
1075   )SVG");
1076   RunAsyncMatchingTasks();
1077   Compositor().BeginFrame();
1078 
1079   // Ensure the target text is still in view and stayed centered
1080   ASSERT_NE(first_scroll_offset, LayoutViewport()->GetScrollOffset());
1081   EXPECT_TRUE(ViewportRect().Contains(BoundingRectInFrame(p)));
1082   EXPECT_EQ(first_bounding_rect, BoundingRectInFrame(p));
1083 
1084   EXPECT_EQ(1u, GetDocument().Markers().Markers().size());
1085 }
1086 
1087 // Test that overlapping text ranges results in only the first one highlighted
1088 TEST_F(TextFragmentAnchorTest, OverlappingTextRanges) {
1089   SimRequest request(
1090       "https://example.com/test.html#:~:text=This,test&text=is,page",
1091       "text/html");
1092   LoadURL("https://example.com/test.html#:~:text=This,test&text=is,page");
1093   request.Complete(R"HTML(
1094     <!DOCTYPE html>
1095     <style>
1096       body {
1097         height: 1200px;
1098       }
1099       p {
1100         position: absolute;
1101         top: 1000px;
1102       }
1103     </style>
1104     <p id="text">This is a test page</p>
1105   )HTML");
1106   RunAsyncMatchingTasks();
1107 
1108   Compositor().BeginFrame();
1109 
1110   EXPECT_EQ(1u, GetDocument().Markers().Markers().size());
1111 
1112   // Expect marker on "This is a test".
1113   auto* text = To<Text>(GetDocument().getElementById("text")->firstChild());
1114   DocumentMarkerVector markers = GetDocument().Markers().MarkersFor(
1115       *text, DocumentMarker::MarkerTypes::TextFragment());
1116   ASSERT_EQ(1u, markers.size());
1117   EXPECT_EQ(0u, markers.at(0)->StartOffset());
1118   EXPECT_EQ(14u, markers.at(0)->EndOffset());
1119 }
1120 
1121 // Test matching a space to &nbsp character.
TEST_F(TextFragmentAnchorTest,SpaceMatchesNbsp)1122 TEST_F(TextFragmentAnchorTest, SpaceMatchesNbsp) {
1123   SimRequest request("https://example.com/test.html#:~:text=test%20page",
1124                      "text/html");
1125   LoadURL("https://example.com/test.html#:~:text=test%20page");
1126   request.Complete(R"HTML(
1127     <!DOCTYPE html>
1128     <style>
1129       body {
1130         height: 1200px;
1131       }
1132       p {
1133         position: absolute;
1134         top: 1000px;
1135       }
1136     </style>
1137     <p id="text">This is a test&nbsp;page</p>
1138   )HTML");
1139   RunAsyncMatchingTasks();
1140 
1141   Compositor().BeginFrame();
1142 
1143   Element& p = *GetDocument().getElementById("text");
1144 
1145   EXPECT_TRUE(ViewportRect().Contains(BoundingRectInFrame(p)))
1146       << "<p> Element wasn't scrolled into view, viewport's scroll offset: "
1147       << LayoutViewport()->GetScrollOffset().ToString();
1148 
1149   EXPECT_EQ(1u, GetDocument().Markers().Markers().size());
1150 }
1151 
1152 // Test matching text with a CSS text transform.
TEST_F(TextFragmentAnchorTest,CSSTextTransform)1153 TEST_F(TextFragmentAnchorTest, CSSTextTransform) {
1154   SimRequest request("https://example.com/test.html#:~:text=test%20page",
1155                      "text/html");
1156   LoadURL("https://example.com/test.html#:~:text=test%20page");
1157   request.Complete(R"HTML(
1158     <!DOCTYPE html>
1159     <style>
1160       body {
1161         height: 1200px;
1162       }
1163       p {
1164         position: absolute;
1165         top: 1000px;
1166         text-transform: uppercase;
1167       }
1168     </style>
1169     <p id="text">This is a test page</p>
1170   )HTML");
1171   RunAsyncMatchingTasks();
1172 
1173   Compositor().BeginFrame();
1174 
1175   Element& p = *GetDocument().getElementById("text");
1176 
1177   EXPECT_TRUE(ViewportRect().Contains(BoundingRectInFrame(p)))
1178       << "<p> Element wasn't scrolled into view, viewport's scroll offset: "
1179       << LayoutViewport()->GetScrollOffset().ToString();
1180 
1181   EXPECT_EQ(1u, GetDocument().Markers().Markers().size());
1182 }
1183 
1184 // Test that we scroll the element fragment into view if we don't find a match.
TEST_F(TextFragmentAnchorTest,NoMatchFoundFallsBackToElementFragment)1185 TEST_F(TextFragmentAnchorTest, NoMatchFoundFallsBackToElementFragment) {
1186   SimRequest request("https://example.com/test.html#element:~:text=cats",
1187                      "text/html");
1188   LoadURL("https://example.com/test.html#element:~:text=cats");
1189   request.Complete(R"HTML(
1190     <!DOCTYPE html>
1191     <style>
1192       body {
1193         height: 2200px;
1194       }
1195       #text {
1196         position: absolute;
1197         top: 1000px;
1198       }
1199       #element {
1200         position: absolute;
1201         top: 2000px;
1202       }
1203     </style>
1204     <p>This is a test page</p>
1205     <div id="element">Some text</div>
1206   )HTML");
1207   RunAsyncMatchingTasks();
1208 
1209   Compositor().BeginFrame();
1210 
1211   // The TextFragmentAnchor needs another frame to invoke the element anchor
1212   Compositor().BeginFrame();
1213   RunAsyncMatchingTasks();
1214 
1215   EXPECT_EQ(GetDocument().Url(), "https://example.com/test.html#element");
1216 
1217   Element& p = *GetDocument().getElementById("element");
1218 
1219   EXPECT_EQ(p, *GetDocument().CssTarget());
1220   EXPECT_TRUE(ViewportRect().Contains(BoundingRectInFrame(p)))
1221       << "<p> Element wasn't scrolled into view, viewport's scroll offset: "
1222       << LayoutViewport()->GetScrollOffset().ToString();
1223 }
1224 
1225 // Test that we don't match partial words at the beginning or end of the text.
TEST_F(TextFragmentAnchorTest,CheckForWordBoundary)1226 TEST_F(TextFragmentAnchorTest, CheckForWordBoundary) {
1227   SimRequest request(
1228       "https://example.com/"
1229       "test.html#:~:text=This%20is%20a%20te&tagetText=st%20page",
1230       "text/html");
1231   LoadURL(
1232       "https://example.com/"
1233       "test.html#:~:text=This%20is%20a%20te&tagetText=st%20page");
1234   request.Complete(R"HTML(
1235     <!DOCTYPE html>
1236     <style>
1237       body {
1238         height: 1200px;
1239       }
1240       p {
1241         position: absolute;
1242         top: 1000px;
1243       }
1244     </style>
1245     <p id="text">This is a test page</p>
1246   )HTML");
1247   RunAsyncMatchingTasks();
1248 
1249   EXPECT_EQ(nullptr, GetDocument().CssTarget());
1250   EXPECT_EQ(ScrollOffset(), LayoutViewport()->GetScrollOffset());
1251   EXPECT_TRUE(GetDocument().Markers().Markers().IsEmpty());
1252 }
1253 
1254 // Test that we don't match partial words with context
TEST_F(TextFragmentAnchorTest,CheckForWordBoundaryWithContext)1255 TEST_F(TextFragmentAnchorTest, CheckForWordBoundaryWithContext) {
1256   SimRequest request("https://example.com/test.html#:~:text=est-,page",
1257                      "text/html");
1258   LoadURL("https://example.com/test.html#:~:text=est-,page");
1259   request.Complete(R"HTML(
1260     <!DOCTYPE html>
1261     <style>
1262       body {
1263         height: 1200px;
1264       }
1265       p {
1266         position: absolute;
1267         top: 1000px;
1268       }
1269     </style>
1270     <p id="text">This is a test page</p>
1271   )HTML");
1272   RunAsyncMatchingTasks();
1273 
1274   EXPECT_EQ(nullptr, GetDocument().CssTarget());
1275   EXPECT_EQ(ScrollOffset(), LayoutViewport()->GetScrollOffset());
1276   EXPECT_TRUE(GetDocument().Markers().Markers().IsEmpty());
1277 }
1278 
1279 // Test that we correctly match a whole word when it appears as a partial word
1280 // earlier in the page.
TEST_F(TextFragmentAnchorTest,CheckForWordBoundaryWithPartialWord)1281 TEST_F(TextFragmentAnchorTest, CheckForWordBoundaryWithPartialWord) {
1282   SimRequest request("https://example.com/test.html#:~:text=tes,age",
1283                      "text/html");
1284   LoadURL("https://example.com/test.html#:~:text=tes,age");
1285   request.Complete(R"HTML(
1286     <!DOCTYPE html>
1287     <style>
1288       body {
1289         height: 1200px;
1290       }
1291       #first {
1292         position: absolute;
1293         top: 1000px;
1294       }
1295       #second {
1296         position: absolute;
1297         top: 2000px;
1298       }
1299     </style>
1300     <p id="first">This is a test page</p>
1301     <p id="second">This is a tes age</p>
1302   )HTML");
1303   RunAsyncMatchingTasks();
1304 
1305   Compositor().BeginFrame();
1306 
1307   Element& p = *GetDocument().getElementById("second");
1308 
1309   EXPECT_EQ(p, *GetDocument().CssTarget());
1310   EXPECT_TRUE(ViewportRect().Contains(BoundingRectInFrame(p)))
1311       << "Should have scrolled <p> into view but didn't, scroll offset: "
1312       << LayoutViewport()->GetScrollOffset().ToString();
1313 
1314   // Expect marker on only "tes age"
1315   EXPECT_EQ(1u, GetDocument().Markers().Markers().size());
1316   DocumentMarkerVector markers = GetDocument().Markers().MarkersFor(
1317       *To<Text>(p.firstChild()), DocumentMarker::MarkerTypes::TextFragment());
1318   ASSERT_EQ(1u, markers.size());
1319   EXPECT_EQ(10u, markers.at(0)->StartOffset());
1320   EXPECT_EQ(17u, markers.at(0)->EndOffset());
1321 }
1322 
1323 // Test dismissing the text highlight with a click
TEST_F(TextFragmentAnchorTest,DismissTextHighlightWithClick)1324 TEST_F(TextFragmentAnchorTest, DismissTextHighlightWithClick) {
1325   SimRequest request(
1326       "https://example.com/"
1327       "test.html#:~:text=test%20page&text=more%20text",
1328       "text/html");
1329   LoadURL(
1330       "https://example.com/"
1331       "test.html#:~:text=test%20page&text=more%20text");
1332   request.Complete(R"HTML(
1333     <!DOCTYPE html>
1334     <style>
1335       body {
1336         height: 2200px;
1337       }
1338       #first {
1339         position: absolute;
1340         top: 1000px;
1341       }
1342       #second {
1343         position: absolute;
1344         top: 2000px;
1345       }
1346     </style>
1347     <p id="first">This is a test page</p>
1348     <p id="second">With some more text</p>
1349   )HTML");
1350   RunAsyncMatchingTasks();
1351 
1352   Compositor().BeginFrame();
1353 
1354   EXPECT_EQ(2u, GetDocument().Markers().Markers().size());
1355 
1356   SimulateClick(100, 100);
1357 
1358   EXPECT_EQ(0u, GetDocument().Markers().Markers().size());
1359 
1360   // Ensure the fragment is uninstalled
1361   EXPECT_FALSE(GetDocument().View()->GetFragmentAnchor());
1362 }
1363 
1364 // Test dismissing the text highlight with a tap
TEST_F(TextFragmentAnchorTest,DismissTextHighlightWithTap)1365 TEST_F(TextFragmentAnchorTest, DismissTextHighlightWithTap) {
1366   SimRequest request(
1367       "https://example.com/"
1368       "test.html#:~:text=test%20page&text=more%20text",
1369       "text/html");
1370   LoadURL(
1371       "https://example.com/"
1372       "test.html#:~:text=test%20page&text=more%20text");
1373   request.Complete(R"HTML(
1374     <!DOCTYPE html>
1375     <style>
1376       body {
1377         height: 2200px;
1378       }
1379       #first {
1380         position: absolute;
1381         top: 1000px;
1382       }
1383       #second {
1384         position: absolute;
1385         top: 2000px;
1386       }
1387     </style>
1388     <p id="first">This is a test page</p>
1389     <p id="second">With some more text</p>
1390   )HTML");
1391   RunAsyncMatchingTasks();
1392 
1393   Compositor().BeginFrame();
1394 
1395   EXPECT_EQ(2u, GetDocument().Markers().Markers().size());
1396 
1397   SimulateTap(100, 100);
1398 
1399   EXPECT_EQ(0u, GetDocument().Markers().Markers().size());
1400 
1401   // Ensure the fragment is uninstalled
1402   EXPECT_FALSE(GetDocument().View()->GetFragmentAnchor());
1403 }
1404 
1405 // Test that we don't dismiss a text highlight before it's scrolled into view
TEST_F(TextFragmentAnchorTest,DismissTextHighlightOutOfView)1406 TEST_F(TextFragmentAnchorTest, DismissTextHighlightOutOfView) {
1407   SimRequest request("https://example.com/test.html#:~:text=test", "text/html");
1408   SimSubresourceRequest css_request("https://example.com/test.css", "text/css");
1409   LoadURL("https://example.com/test.html#:~:text=test");
1410   request.Complete(R"HTML(
1411     <!DOCTYPE html>
1412     <style>
1413       body {
1414         height: 1200px;
1415       }
1416       p {
1417         position: absolute;
1418         top: 1000px;
1419         visibility: hidden;
1420       }
1421     </style>
1422     <link rel=stylesheet href=test.css>
1423     <p id="text">This is a test page</p>
1424   )HTML");
1425 
1426   Compositor().PaintFrame();
1427   ASSERT_EQ(0u, GetDocument().Markers().Markers().size());
1428   SimulateClick(100, 100);
1429 
1430   // Set the target text to visible and change its position to cause a layout
1431   // and invoke the fragment anchor.
1432   css_request.Complete("p { visibility: visible; top: 1001px; }");
1433   RunAsyncMatchingTasks();
1434 
1435   Compositor().BeginFrame();
1436 
1437   EXPECT_EQ(1u, GetDocument().Markers().Markers().size());
1438 
1439   // Click to dismiss
1440   SimulateClick(100, 100);
1441   EXPECT_EQ(0u, GetDocument().Markers().Markers().size());
1442   EXPECT_FALSE(GetDocument().View()->GetFragmentAnchor());
1443 }
1444 
1445 // Test dismissing a text highlight that didn't require a scroll into view
TEST_F(TextFragmentAnchorTest,DismissTextHighlightInView)1446 TEST_F(TextFragmentAnchorTest, DismissTextHighlightInView) {
1447   SimRequest request(
1448       "https://example.com/"
1449       "test.html#:~:text=test%20page&text=more%20text",
1450       "text/html");
1451   LoadURL(
1452       "https://example.com/"
1453       "test.html#:~:text=test%20page&text=more%20text");
1454   request.Complete(R"HTML(
1455     <!DOCTYPE html>
1456     <style>
1457       body {
1458         height: 1200px;
1459       }
1460       p {
1461         position: absolute;
1462         top: 100px;
1463       }
1464     </style>
1465     <p>This is a test page</p>
1466   )HTML");
1467   RunAsyncMatchingTasks();
1468 
1469   Compositor().BeginFrame();
1470 
1471   EXPECT_EQ(ScrollOffset(), LayoutViewport()->GetScrollOffset());
1472   EXPECT_EQ(1u, GetDocument().Markers().Markers().size());
1473 
1474   SimulateTap(100, 100);
1475 
1476   EXPECT_EQ(0u, GetDocument().Markers().Markers().size());
1477 
1478   // Ensure the fragment is uninstalled
1479   EXPECT_FALSE(GetDocument().View()->GetFragmentAnchor());
1480 }
1481 
1482 // Test that the fragment directive delimiter :~: works properly and is stripped
1483 // from the URL.
TEST_F(TextFragmentAnchorTest,FragmentDirectiveDelimiter)1484 TEST_F(TextFragmentAnchorTest, FragmentDirectiveDelimiter) {
1485   SimRequest request("https://example.com/test.html#:~:text=test", "text/html");
1486   LoadURL("https://example.com/test.html#:~:text=test");
1487   request.Complete(R"HTML(
1488     <!DOCTYPE html>
1489     <style>
1490       body {
1491         height: 1200px;
1492       }
1493       p {
1494         position: absolute;
1495         top: 1000px;
1496       }
1497     </style>
1498     <p id="text">This is a test page</p>
1499   )HTML");
1500   RunAsyncMatchingTasks();
1501 
1502   Compositor().BeginFrame();
1503 
1504   EXPECT_EQ(1u, GetDocument().Markers().Markers().size());
1505 
1506   EXPECT_EQ(GetDocument().Url(), "https://example.com/test.html");
1507 }
1508 
1509 // Test that a :~: fragment directive is scrolled into view and is stripped from
1510 // the URL when there's also a valid element fragment.
TEST_F(TextFragmentAnchorTest,FragmentDirectiveDelimiterWithElementFragment)1511 TEST_F(TextFragmentAnchorTest, FragmentDirectiveDelimiterWithElementFragment) {
1512   SimRequest request("https://example.com/test.html#element:~:text=test",
1513                      "text/html");
1514   LoadURL("https://example.com/test.html#element:~:text=test");
1515   request.Complete(R"HTML(
1516     <!DOCTYPE html>
1517     <style>
1518       body {
1519         height: 2200px;
1520       }
1521       #text {
1522         position: absolute;
1523         top: 1000px;
1524       }
1525       #element {
1526         position: absolute;
1527         top: 2000px;
1528       }
1529     </style>
1530     <p id="text">This is a test page</p>
1531     <div id="element">Some text</div>
1532   )HTML");
1533   RunAsyncMatchingTasks();
1534 
1535   Compositor().BeginFrame();
1536 
1537   EXPECT_EQ(GetDocument().Url(), "https://example.com/test.html#element");
1538 
1539   Element& p = *GetDocument().getElementById("text");
1540 
1541   EXPECT_EQ(p, *GetDocument().CssTarget());
1542   EXPECT_TRUE(ViewportRect().Contains(BoundingRectInFrame(p)))
1543       << "<p> Element wasn't scrolled into view, viewport's scroll offset: "
1544       << LayoutViewport()->GetScrollOffset().ToString();
1545 }
1546 
1547 // Test that a fragment directive is stripped from the URL even if it is not a
1548 // text directive.
TEST_F(TextFragmentAnchorTest,IdFragmentWithFragmentDirective)1549 TEST_F(TextFragmentAnchorTest, IdFragmentWithFragmentDirective) {
1550   SimRequest request("https://example.com/test.html#element:~:id", "text/html");
1551   LoadURL("https://example.com/test.html#element:~:id");
1552   request.Complete(R"HTML(
1553     <!DOCTYPE html>
1554     <style>
1555       body {
1556         height: 2200px;
1557       }
1558       p {
1559         position: absolute;
1560         top: 1000px;
1561       }
1562       div {
1563         position: absolute;
1564         top: 2000px;
1565       }
1566     </style>
1567     <p id="element">This is a test page</p>
1568     <div id="element:~:id">Some text</div>
1569   )HTML");
1570   RunAsyncMatchingTasks();
1571 
1572   Compositor().BeginFrame();
1573 
1574   Element& p = *GetDocument().getElementById("element");
1575 
1576   EXPECT_EQ(p, *GetDocument().CssTarget());
1577   EXPECT_TRUE(ViewportRect().Contains(BoundingRectInFrame(p)))
1578       << "Should have scrolled <div> into view but didn't, scroll offset: "
1579       << LayoutViewport()->GetScrollOffset().ToString();
1580 }
1581 
1582 // Ensure we can match <text> inside of a <svg> element.
TEST_F(TextFragmentAnchorTest,TextDirectiveInSvg)1583 TEST_F(TextFragmentAnchorTest, TextDirectiveInSvg) {
1584   SimRequest request("https://example.com/test.html#:~:text=test", "text/html");
1585   LoadURL("https://example.com/test.html#:~:text=test");
1586   request.Complete(R"HTML(
1587     <!DOCTYPE html>
1588     <style>
1589       body {
1590         height: 1200px;
1591       }
1592       svg {
1593         position: absolute;
1594         top: 1000px;
1595       }
1596     </style>
1597     <svg><text id="text" x="0" y="15">This is a test page</text></svg>
1598   )HTML");
1599   RunAsyncMatchingTasks();
1600 
1601   Compositor().BeginFrame();
1602 
1603   Element& text = *GetDocument().getElementById("text");
1604 
1605   EXPECT_EQ(text, *GetDocument().CssTarget());
1606   EXPECT_TRUE(ViewportRect().Contains(BoundingRectInFrame(text)))
1607       << "<text> Element wasn't scrolled into view, viewport's scroll offset: "
1608       << LayoutViewport()->GetScrollOffset().ToString();
1609 
1610   EXPECT_EQ(1u, GetDocument().Markers().Markers().size());
1611 }
1612 
1613 // Ensure we restore the text highlight on page reload
1614 // TODO(bokan): This test is disabled as this functionality was suppressed in
1615 // https://crrev.com/c/2135407; it would be better addressed by providing a
1616 // highlight-only function. See the TODO in
1617 // https://wicg.github.io/ScrollToTextFragment/#restricting-the-text-fragment
TEST_F(TextFragmentAnchorTest,DISABLED_HighlightOnReload)1618 TEST_F(TextFragmentAnchorTest, DISABLED_HighlightOnReload) {
1619   SimRequest request("https://example.com/test.html#:~:text=test", "text/html");
1620   LoadURL("https://example.com/test.html#:~:text=test");
1621   const String& html = R"HTML(
1622     <!DOCTYPE html>
1623     <style>
1624       body {
1625         height: 1200px;
1626       }
1627       p {
1628         position: absolute;
1629         top: 1000px;
1630       }
1631     </style>
1632     <p id="text">This is a test page</p>
1633   )HTML";
1634   request.Complete(html);
1635   RunAsyncMatchingTasks();
1636 
1637   Compositor().BeginFrame();
1638 
1639   EXPECT_EQ(1u, GetDocument().Markers().Markers().size());
1640 
1641   // Tap to dismiss the highlight.
1642   SimulateClick(10, 10);
1643   EXPECT_EQ(0u, GetDocument().Markers().Markers().size());
1644 
1645   // Reload the page and expect the highlight to be restored.
1646   SimRequest reload_request("https://example.com/test.html#:~:text=test",
1647                             "text/html");
1648   MainFrame().StartReload(WebFrameLoadType::kReload);
1649   reload_request.Complete(html);
1650 
1651   Compositor().BeginFrame();
1652   RunAsyncMatchingTasks();
1653 
1654   EXPECT_EQ(*GetDocument().getElementById("text"), *GetDocument().CssTarget());
1655   EXPECT_EQ(1u, GetDocument().Markers().Markers().size());
1656 }
1657 
1658 // Ensure that we can have text directives combined with non-text directives
TEST_F(TextFragmentAnchorTest,NonTextDirectives)1659 TEST_F(TextFragmentAnchorTest, NonTextDirectives) {
1660   SimRequest request(
1661       "https://example.com/test.html#:~:text=test&directive&text=more",
1662       "text/html");
1663   LoadURL("https://example.com/test.html#:~:text=test&directive&text=more");
1664   request.Complete(R"HTML(
1665     <!DOCTYPE html>
1666     <style>
1667       body {
1668         height: 2200px;
1669       }
1670       #first {
1671         position: absolute;
1672         top: 1000px;
1673       }
1674       #second {
1675         position: absolute;
1676         top: 2000px;
1677       }
1678     </style>
1679     <p id="first">This is a test page</p>
1680     <p id="second">This is some more text</p>
1681   )HTML");
1682   Compositor().BeginFrame();
1683 
1684   RunAsyncMatchingTasks();
1685 
1686   Element& first = *GetDocument().getElementById("first");
1687 
1688   EXPECT_TRUE(ViewportRect().Contains(BoundingRectInFrame(first)))
1689       << "First <p> wasn't scrolled into view, viewport's scroll offset: "
1690       << LayoutViewport()->GetScrollOffset().ToString();
1691 
1692   EXPECT_EQ(2u, GetDocument().Markers().Markers().size());
1693 }
1694 
1695 // Test that the text directive applies :target styling
TEST_F(TextFragmentAnchorTest,CssTarget)1696 TEST_F(TextFragmentAnchorTest, CssTarget) {
1697   SimRequest main_request("https://example.com/test.html#:~:text=test",
1698                           "text/html");
1699   SimRequest css_request("https://example.com/test.css", "text/css");
1700   LoadURL("https://example.com/test.html#:~:text=test");
1701   main_request.Complete(R"HTML(
1702     <!DOCTYPE html>
1703     <style>
1704       p {
1705         margin-top: 1000px;
1706       }
1707     </style>
1708     <link rel="stylesheet" href="test.css">
1709     <p id="text">test</p>
1710   )HTML");
1711 
1712   // With BlockHTMLParserOnStyleSheetsEnabled, the text fragment anchor won't be
1713   // invoked until the CSS is loaded. Otherwise, we test the behavior where the
1714   // text fragment anchor is invoked before and after the stylesheet is applied.
1715   if (!RuntimeEnabledFeatures::BlockHTMLParserOnStyleSheetsEnabled()) {
1716     Compositor().PaintFrame();
1717     ScrollOffset first_scroll_offset = LayoutViewport()->GetScrollOffset();
1718     ASSERT_NE(ScrollOffset(), first_scroll_offset);
1719 
1720     Element& p = *GetDocument().getElementById("text");
1721     IntRect first_bounding_rect = BoundingRectInFrame(p);
1722     EXPECT_TRUE(ViewportRect().Contains(first_bounding_rect));
1723 
1724     // Load CSS that has target styling that moves the text out of view
1725     css_request.Complete(R"CSS(
1726       :target {
1727         margin-top: 2000px;
1728       }
1729     )CSS");
1730     RunPendingTasks();
1731     Compositor().BeginFrame();
1732 
1733     // Ensure the target text is still in view and stayed centered
1734     ASSERT_NE(first_scroll_offset, LayoutViewport()->GetScrollOffset());
1735     EXPECT_EQ(first_bounding_rect, BoundingRectInFrame(p));
1736   } else {
1737     css_request.Complete(R"CSS(
1738       :target {
1739         margin-top: 2000px;
1740       }
1741     )CSS");
1742     RunPendingTasks();
1743     Compositor().BeginFrame();
1744   }
1745 
1746   Element& p = *GetDocument().getElementById("text");
1747   EXPECT_TRUE(ViewportRect().Contains(BoundingRectInFrame(p)));
1748   EXPECT_EQ(1u, GetDocument().Markers().Markers().size());
1749 }
1750 
1751 }  // namespace
1752 
1753 }  // namespace blink
1754