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> Good cat</p>
691 </div>
692 <h1> List 2 </h1>
693 <div>
694 <p id="expected">Cat</p>
695 <p> Good cat</p>
696 </div>
697 <h1> List 2 </h1>
698 <div>
699 <p>Cat</p>
700 <p> 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> </p>
735 <div id="expected">match</div>
736 <p><br> </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   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 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