1 // Copyright 2017 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 "third_party/blink/renderer/core/page/spatial_navigation.h"
6
7 #include "testing/gtest/include/gtest/gtest.h"
8 #include "third_party/blink/public/common/input/web_keyboard_event.h"
9 #include "third_party/blink/renderer/core/frame/frame_test_helpers.h"
10 #include "third_party/blink/renderer/core/frame/local_frame.h"
11 #include "third_party/blink/renderer/core/frame/visual_viewport.h"
12 #include "third_party/blink/renderer/core/frame/web_local_frame_impl.h"
13 #include "third_party/blink/renderer/core/frame/web_remote_frame_impl.h"
14 #include "third_party/blink/renderer/core/html/forms/html_select_element.h"
15 #include "third_party/blink/renderer/core/input/event_handler.h"
16 #include "third_party/blink/renderer/core/page/spatial_navigation_controller.h"
17 #include "third_party/blink/renderer/core/testing/core_unit_test_helper.h"
18 #include "third_party/blink/renderer/core/testing/sim/sim_request.h"
19 #include "third_party/blink/renderer/core/testing/sim/sim_test.h"
20 #include "third_party/blink/renderer/platform/testing/runtime_enabled_features_test_helpers.h"
21 #include "third_party/blink/renderer/platform/testing/url_test_helpers.h"
22 #include "ui/events/keycodes/dom/dom_key.h"
23
24 namespace blink {
25
26 class SpatialNavigationTest : public RenderingTest {
27 public:
SpatialNavigationTest()28 SpatialNavigationTest()
29 : RenderingTest(MakeGarbageCollected<SingleChildLocalFrameClient>()) {}
30
TopOfVisualViewport()31 PhysicalRect TopOfVisualViewport() {
32 PhysicalRect visual_viewport = RootViewport(&GetFrame());
33 visual_viewport.SetY(visual_viewport.Y() - 1);
34 visual_viewport.SetHeight(LayoutUnit(0));
35 return visual_viewport;
36 }
37
BottomOfVisualViewport()38 PhysicalRect BottomOfVisualViewport() {
39 PhysicalRect visual_viewport = RootViewport(&GetFrame());
40 visual_viewport.SetY(visual_viewport.Bottom() + 1);
41 visual_viewport.SetHeight(LayoutUnit(0));
42 return visual_viewport;
43 }
44
LeftSideOfVisualViewport()45 PhysicalRect LeftSideOfVisualViewport() {
46 PhysicalRect visual_viewport = RootViewport(&GetFrame());
47 visual_viewport.SetX(visual_viewport.X() - 1);
48 visual_viewport.SetWidth(LayoutUnit(0));
49 return visual_viewport;
50 }
51
RightSideOfVisualViewport()52 PhysicalRect RightSideOfVisualViewport() {
53 PhysicalRect visual_viewport = RootViewport(&GetFrame());
54 visual_viewport.SetX(visual_viewport.Right() + 1);
55 visual_viewport.SetWidth(LayoutUnit(0));
56 return visual_viewport;
57 }
58
AssertUseSidesOfVisualViewport(Node * focus_node)59 void AssertUseSidesOfVisualViewport(Node* focus_node) {
60 EXPECT_EQ(SearchOrigin(RootViewport(&GetFrame()), focus_node,
61 SpatialNavigationDirection::kUp),
62 BottomOfVisualViewport());
63 EXPECT_EQ(SearchOrigin(RootViewport(&GetFrame()), focus_node,
64 SpatialNavigationDirection::kDown),
65 TopOfVisualViewport());
66 EXPECT_EQ(SearchOrigin(RootViewport(&GetFrame()), focus_node,
67 SpatialNavigationDirection::kLeft),
68 RightSideOfVisualViewport());
69 EXPECT_EQ(SearchOrigin(RootViewport(&GetFrame()), focus_node,
70 SpatialNavigationDirection::kRight),
71 LeftSideOfVisualViewport());
72 }
73
AssertNormalizedHeight(Element * e,int line_height,bool will_shrink)74 void AssertNormalizedHeight(Element* e, int line_height, bool will_shrink) {
75 PhysicalRect search_origin =
76 SearchOrigin(RootViewport(e->GetDocument().GetFrame()), e,
77 SpatialNavigationDirection::kDown);
78 PhysicalRect uncropped = NodeRectInRootFrame(e);
79
80 // SearchOrigin uses the normalized height.
81 // If |e| is line broken, SearchOrigin should only use the first line.
82 PhysicalRect normalized =
83 ShrinkInlineBoxToLineBox(*e->GetLayoutObject(), uncropped);
84 EXPECT_EQ(search_origin, normalized);
85 if (will_shrink) {
86 EXPECT_LT(search_origin.Height(), uncropped.Height());
87 EXPECT_EQ(search_origin.Height(), line_height);
88 EXPECT_EQ(search_origin.X(), uncropped.X());
89 EXPECT_EQ(search_origin.Y(), uncropped.Y());
90 EXPECT_EQ(search_origin.Width(), uncropped.Width());
91 } else {
92 EXPECT_EQ(search_origin, uncropped);
93 }
94
95 // Focus candidates will also use normalized heights.
96 // If |e| is line broken, the rect should still include all lines.
97 normalized = ShrinkInlineBoxToLineBox(*e->GetLayoutObject(), uncropped,
98 LineBoxes(*e->GetLayoutObject()));
99 FocusCandidate candidate(e, SpatialNavigationDirection::kDown);
100 EXPECT_EQ(normalized, candidate.rect_in_root_frame);
101 }
102
HasSameSearchOriginRectAndCandidateRect(Element * a)103 bool HasSameSearchOriginRectAndCandidateRect(Element* a) {
104 PhysicalRect a_origin =
105 SearchOrigin(RootViewport(a->GetDocument().GetFrame()), a,
106 SpatialNavigationDirection::kDown);
107 FocusCandidate a_candidate(a, SpatialNavigationDirection::kDown);
108 return a_candidate.rect_in_root_frame == a_origin;
109 }
110
Intersects(Element * a,Element * b)111 bool Intersects(Element* a, Element* b) {
112 PhysicalRect a_origin =
113 SearchOrigin(RootViewport(a->GetDocument().GetFrame()), a,
114 SpatialNavigationDirection::kDown);
115 PhysicalRect b_origin =
116 SearchOrigin(RootViewport(b->GetDocument().GetFrame()), b,
117 SpatialNavigationDirection::kDown);
118
119 return a_origin.Intersects(b_origin);
120 }
121 };
122
TEST_F(SpatialNavigationTest,RootFramesVisualViewport)123 TEST_F(SpatialNavigationTest, RootFramesVisualViewport) {
124 // Test RootViewport with a pinched viewport.
125 VisualViewport& visual_viewport = GetFrame().GetPage()->GetVisualViewport();
126 visual_viewport.SetScale(2);
127 visual_viewport.SetLocation(FloatPoint(200, 200));
128
129 LocalFrameView* root_frame_view = GetFrame().LocalFrameRoot().View();
130 const PhysicalRect roots_visible_doc_rect(
131 root_frame_view->GetScrollableArea()->VisibleContentRect());
132 // Convert the root frame's visible rect from document space -> frame space.
133 // For the root frame, frame space == root frame space, obviously.
134 PhysicalRect viewport_rect_of_root_frame =
135 root_frame_view->DocumentToFrame(roots_visible_doc_rect);
136
137 EXPECT_EQ(viewport_rect_of_root_frame, RootViewport(&GetFrame()));
138 }
139
TEST_F(SpatialNavigationTest,FindContainerWhenEnclosingContainerIsDocument)140 TEST_F(SpatialNavigationTest, FindContainerWhenEnclosingContainerIsDocument) {
141 SetBodyInnerHTML(
142 "<!DOCTYPE html>"
143 "<a id='child'>link</a>");
144
145 Element* child_element = GetDocument().getElementById("child");
146 Node* enclosing_container = ScrollableAreaOrDocumentOf(child_element);
147
148 EXPECT_EQ(enclosing_container, GetDocument());
149 EXPECT_TRUE(IsScrollableAreaOrDocument(enclosing_container));
150 }
151
TEST_F(SpatialNavigationTest,FindContainerWhenEnclosingContainerIsIframe)152 TEST_F(SpatialNavigationTest, FindContainerWhenEnclosingContainerIsIframe) {
153 SetBodyInnerHTML(
154 "<!DOCTYPE html>"
155 "<style>"
156 " iframe {"
157 " width: 100px;"
158 " height: 100px;"
159 " }"
160 "</style>"
161 "<iframe id='iframe'></iframe>");
162
163 SetChildFrameHTML(
164 "<!DOCTYPE html>"
165 "<a>link</a>");
166
167 UpdateAllLifecyclePhasesForTest();
168 Element* iframe = GetDocument().QuerySelector("iframe");
169 Element* link = ChildDocument().QuerySelector("a");
170 Node* enclosing_container = ScrollableAreaOrDocumentOf(link);
171
172 EXPECT_FALSE(IsOffscreen(iframe));
173 EXPECT_FALSE(IsOffscreen(&ChildDocument()));
174 EXPECT_FALSE(IsOffscreen(link));
175
176 EXPECT_EQ(enclosing_container, ChildDocument());
177 EXPECT_TRUE(IsScrollableAreaOrDocument(enclosing_container));
178 }
179
TEST_F(SpatialNavigationTest,FindContainerWhenEnclosingContainerIsScrollableOverflowBox)180 TEST_F(SpatialNavigationTest,
181 FindContainerWhenEnclosingContainerIsScrollableOverflowBox) {
182 GetDocument().SetCompatibilityMode(Document::kQuirksMode);
183 SetBodyInnerHTML(
184 "<!DOCTYPE html>"
185 "<style>"
186 " #content {"
187 " margin-top: 200px;" // Outside the div's viewport.
188 " }"
189 " #container {"
190 " height: 100px;"
191 " overflow: scroll;"
192 " }"
193 "</style>"
194 "<div id='container'>"
195 " <div id='content'>some text here</div>"
196 "</div>");
197
198 Element* content = GetDocument().getElementById("content");
199 Element* container = GetDocument().getElementById("container");
200 Node* enclosing_container = ScrollableAreaOrDocumentOf(content);
201
202 // TODO(crbug.com/889840):
203 // VisibleBoundsInVisualViewport does not (yet) take div-clipping into
204 // account. The node is off screen, but nevertheless VBIVV returns a non-
205 // empty rect. If you fix VisibleBoundsInVisualViewport, change to
206 // EXPECT_TRUE here and stop using LayoutObject in IsOffscreen().
207 EXPECT_FALSE(
208 content->VisibleBoundsInVisualViewport().IsEmpty()); // EXPECT_TRUE.
209
210 EXPECT_TRUE(IsOffscreen(content));
211 EXPECT_FALSE(IsOffscreen(container));
212
213 EXPECT_EQ(enclosing_container, container);
214 EXPECT_TRUE(IsScrollableAreaOrDocument(enclosing_container));
215 }
216
TEST_F(SpatialNavigationTest,ZooomPutsElementOffScreen)217 TEST_F(SpatialNavigationTest, ZooomPutsElementOffScreen) {
218 SetBodyInnerHTML(
219 "<!DOCTYPE html>"
220 "<button id='a'>hello</button><br>"
221 "<button id='b' style='margin-top: 70%'>bello</button>");
222
223 Element* a = GetDocument().getElementById("a");
224 Element* b = GetDocument().getElementById("b");
225 EXPECT_FALSE(IsOffscreen(a));
226 EXPECT_FALSE(IsOffscreen(b));
227
228 // Now, test IsOffscreen with a pinched viewport.
229 VisualViewport& visual_viewport = GetFrame().GetPage()->GetVisualViewport();
230 visual_viewport.SetScale(2);
231 // #b is no longer visible.
232 EXPECT_FALSE(IsOffscreen(a));
233 EXPECT_TRUE(IsOffscreen(b));
234 }
235
TEST_F(SpatialNavigationTest,RootViewportRespectsVisibleSize)236 TEST_F(SpatialNavigationTest, RootViewportRespectsVisibleSize) {
237 EXPECT_EQ(RootViewport(&GetFrame()), PhysicalRect(0, 0, 800, 600));
238
239 VisualViewport& visual_viewport = GetFrame().GetPage()->GetVisualViewport();
240 visual_viewport.SetSize({123, 123});
241 EXPECT_EQ(RootViewport(&GetFrame()), PhysicalRect(0, 0, 123, 123));
242 }
243
TEST_F(SpatialNavigationTest,StartAtVisibleFocusedElement)244 TEST_F(SpatialNavigationTest, StartAtVisibleFocusedElement) {
245 SetBodyInnerHTML("<button id='b'>hello</button>");
246 Element* b = GetDocument().getElementById("b");
247
248 EXPECT_EQ(SearchOrigin(RootViewport(&GetFrame()), b,
249 SpatialNavigationDirection::kDown),
250 NodeRectInRootFrame(b));
251 }
252
TEST_F(SpatialNavigationTest,StartAtVisibleFocusedScroller)253 TEST_F(SpatialNavigationTest, StartAtVisibleFocusedScroller) {
254 SetBodyInnerHTML(
255 "<!DOCTYPE html>"
256 "<style>"
257 " #content {"
258 " margin-top: 200px;" // Outside the div's viewport.
259 " }"
260 " #scroller {"
261 " height: 100px;"
262 " overflow: scroll;"
263 " }"
264 "</style>"
265 "<div id='scroller'>"
266 " <div id='content'>some text here</div>"
267 "</div>");
268
269 Element* scroller = GetDocument().getElementById("scroller");
270 EXPECT_EQ(SearchOrigin(RootViewport(&GetFrame()), scroller,
271 SpatialNavigationDirection::kDown),
272 NodeRectInRootFrame(scroller));
273 }
274
TEST_F(SpatialNavigationTest,StartAtVisibleFocusedIframe)275 TEST_F(SpatialNavigationTest, StartAtVisibleFocusedIframe) {
276 SetBodyInnerHTML(
277 "<!DOCTYPE html>"
278 "<style>"
279 " iframe {"
280 " width: 100px;"
281 " height: 100px;"
282 " }"
283 "</style>"
284 "<iframe id='iframe'></iframe>");
285
286 SetChildFrameHTML(
287 "<!DOCTYPE html>"
288 "<div>some text here</div>");
289
290 Element* iframe = GetDocument().getElementById("iframe");
291 EXPECT_EQ(SearchOrigin(RootViewport(&GetFrame()), iframe,
292 SpatialNavigationDirection::kDown),
293 NodeRectInRootFrame(iframe));
294 }
295
TEST_F(SpatialNavigationTest,StartAtTopWhenGoingDownwardsWithoutFocus)296 TEST_F(SpatialNavigationTest, StartAtTopWhenGoingDownwardsWithoutFocus) {
297 EXPECT_EQ(PhysicalRect(0, -1, 111, 0),
298 SearchOrigin({0, 0, 111, 222}, nullptr,
299 SpatialNavigationDirection::kDown));
300
301 EXPECT_EQ(SearchOrigin(RootViewport(&GetFrame()), nullptr,
302 SpatialNavigationDirection::kDown),
303 TopOfVisualViewport());
304 }
305
TEST_F(SpatialNavigationTest,StartAtBottomWhenGoingUpwardsWithoutFocus)306 TEST_F(SpatialNavigationTest, StartAtBottomWhenGoingUpwardsWithoutFocus) {
307 EXPECT_EQ(
308 PhysicalRect(0, 222 + 1, 111, 0),
309 SearchOrigin({0, 0, 111, 222}, nullptr, SpatialNavigationDirection::kUp));
310
311 EXPECT_EQ(SearchOrigin(RootViewport(&GetFrame()), nullptr,
312 SpatialNavigationDirection::kUp),
313 BottomOfVisualViewport());
314 }
315
TEST_F(SpatialNavigationTest,StartAtLeftSideWhenGoingEastWithoutFocus)316 TEST_F(SpatialNavigationTest, StartAtLeftSideWhenGoingEastWithoutFocus) {
317 EXPECT_EQ(PhysicalRect(-1, 0, 0, 222),
318 SearchOrigin({0, 0, 111, 222}, nullptr,
319 SpatialNavigationDirection::kRight));
320
321 EXPECT_EQ(SearchOrigin(RootViewport(&GetFrame()), nullptr,
322 SpatialNavigationDirection::kRight),
323 LeftSideOfVisualViewport());
324 }
325
TEST_F(SpatialNavigationTest,StartAtRightSideWhenGoingWestWithoutFocus)326 TEST_F(SpatialNavigationTest, StartAtRightSideWhenGoingWestWithoutFocus) {
327 EXPECT_EQ(PhysicalRect(111 + 1, 0, 0, 222),
328 SearchOrigin({0, 0, 111, 222}, nullptr,
329 SpatialNavigationDirection::kLeft));
330
331 EXPECT_EQ(SearchOrigin(RootViewport(&GetFrame()), nullptr,
332 SpatialNavigationDirection::kLeft),
333 RightSideOfVisualViewport());
334 }
335
TEST_F(SpatialNavigationTest,StartAtBottomWhenGoingUpwardsAndFocusIsOffscreen)336 TEST_F(SpatialNavigationTest,
337 StartAtBottomWhenGoingUpwardsAndFocusIsOffscreen) {
338 SetBodyInnerHTML(
339 "<button id='b' style='margin-top: 120%;'>B</button>"); // Outside the
340 // visual
341 // viewport.
342 Element* b = GetDocument().getElementById("b");
343 EXPECT_TRUE(IsOffscreen(b));
344
345 EXPECT_EQ(SearchOrigin(RootViewport(&GetFrame()), b,
346 SpatialNavigationDirection::kUp),
347 BottomOfVisualViewport());
348 }
349
TEST_F(SpatialNavigationTest,StartAtContainersEdge)350 TEST_F(SpatialNavigationTest, StartAtContainersEdge) {
351 SetBodyInnerHTML(
352 "<!DOCTYPE html>"
353 "<style>"
354 " div {"
355 " height: 100px;"
356 " width: 100px;"
357 " overflow: scroll;"
358 " }"
359 " button {"
360 " margin-top: 200px;" // Outside the div's viewport.
361 " }"
362 "</style>"
363 "<div id='container'>"
364 " <button id='b'>B</button>"
365 "</div>");
366
367 Element* b = GetDocument().getElementById("b");
368 const Element* container = GetDocument().getElementById("container");
369 const PhysicalRect container_box = NodeRectInRootFrame(container);
370
371 // TODO(crbug.com/889840):
372 // VisibleBoundsInVisualViewport does not (yet) take div-clipping into
373 // account. The node is off screen, but nevertheless VBIVV returns a non-
374 // empty rect. If you fix VisibleBoundsInVisualViewport, change to
375 // EXPECT_TRUE here and stop using LayoutObject in IsOffscreen().
376 EXPECT_FALSE(b->VisibleBoundsInVisualViewport().IsEmpty()); // EXPECT_TRUE.
377 EXPECT_TRUE(IsOffscreen(b));
378
379 // Go down.
380 PhysicalRect container_top_edge = container_box;
381 container_top_edge.SetHeight(LayoutUnit(0));
382 container_top_edge.SetY(container_top_edge.Y() - 1);
383 EXPECT_EQ(SearchOrigin(RootViewport(&GetFrame()), b,
384 SpatialNavigationDirection::kDown),
385 container_top_edge);
386
387 // Go up.
388 PhysicalRect container_bottom_edge = container_box;
389 container_bottom_edge.SetHeight(LayoutUnit(0));
390 container_bottom_edge.SetY(container_bottom_edge.Right() + 1);
391 EXPECT_EQ(SearchOrigin(RootViewport(&GetFrame()), b,
392 SpatialNavigationDirection::kUp),
393 container_bottom_edge);
394
395 // Go right.
396 PhysicalRect container_leftmost_edge = container_box;
397 container_leftmost_edge.SetWidth(LayoutUnit(0));
398 container_leftmost_edge.SetX(container_leftmost_edge.X() - 1);
399 EXPECT_EQ(SearchOrigin(RootViewport(&GetFrame()), b,
400 SpatialNavigationDirection::kRight),
401 container_leftmost_edge);
402
403 // Go left.
404 PhysicalRect container_rightmost_edge = container_box;
405 container_rightmost_edge.SetX(container_bottom_edge.Right() + 1);
406 container_rightmost_edge.SetWidth(LayoutUnit(0));
407 EXPECT_EQ(SearchOrigin(RootViewport(&GetFrame()), b,
408 SpatialNavigationDirection::kLeft),
409 container_rightmost_edge);
410 }
411
TEST_F(SpatialNavigationTest,StartFromDocEdgeWhenFocusIsClippedInOffscreenScroller)412 TEST_F(SpatialNavigationTest,
413 StartFromDocEdgeWhenFocusIsClippedInOffscreenScroller) {
414 SetBodyInnerHTML(
415 "<!DOCTYPE html>"
416 "<style>"
417 " div {"
418 " margin-top: 120%;" // Outside the visual viewport.
419 " height: 100px;"
420 " width: 100px;"
421 " overflow: scroll;"
422 " }"
423 " button {"
424 " margin-top: 300px;" // Outside the div's scrollport.
425 " }"
426 "</style>"
427 "<div id='scroller'>"
428 " <button id='b'>B</button>"
429 "</div>");
430
431 Element* scroller = GetDocument().getElementById("scroller");
432 Element* b = GetDocument().getElementById("b");
433
434 EXPECT_TRUE(IsOffscreen(scroller));
435 EXPECT_TRUE(IsOffscreen(b));
436
437 EXPECT_EQ(SearchOrigin(RootViewport(&GetFrame()), b,
438 SpatialNavigationDirection::kUp),
439 BottomOfVisualViewport());
440 EXPECT_EQ(SearchOrigin(RootViewport(&GetFrame()), b,
441 SpatialNavigationDirection::kDown),
442 TopOfVisualViewport());
443 }
444
TEST_F(SpatialNavigationTest,StartFromDocEdgeWhenFocusIsClippedInNestedOffscreenScroller)445 TEST_F(SpatialNavigationTest,
446 StartFromDocEdgeWhenFocusIsClippedInNestedOffscreenScroller) {
447 SetBodyInnerHTML(
448 "<!DOCTYPE html>"
449 "<style>"
450 " div {"
451 " margin-top: 120%;" // Outside the visual viewport.
452 " height: 100px;"
453 " width: 100px;"
454 " overflow: scroll;"
455 "}"
456 "a {"
457 " display: block;"
458 " margin-top: 300px;"
459 "}"
460 "</style>"
461 "<div id='scroller1'>"
462 " <div id='scroller2'>"
463 " <a id='link'>link</a>"
464 " </div>"
465 "</div>");
466
467 Element* scroller1 = GetDocument().getElementById("scroller1");
468 Element* scroller2 = GetDocument().getElementById("scroller2");
469 Element* link = GetDocument().getElementById("link");
470
471 EXPECT_TRUE(IsScrollableAreaOrDocument(scroller1));
472 EXPECT_TRUE(IsScrollableAreaOrDocument(scroller2));
473 EXPECT_TRUE(IsOffscreen(scroller1));
474 EXPECT_TRUE(IsOffscreen(scroller1));
475 EXPECT_TRUE(IsOffscreen(link));
476
477 AssertUseSidesOfVisualViewport(link);
478 }
479
TEST_F(SpatialNavigationTest,PartiallyVisible)480 TEST_F(SpatialNavigationTest, PartiallyVisible) {
481 // <button>'s bottom is clipped.
482 SetBodyInnerHTML("<button id='b' style='height: 900px;'>B</button>");
483 Element* b = GetDocument().getElementById("b");
484
485 EXPECT_FALSE(IsOffscreen(b)); // <button> is not completely offscreen.
486
487 PhysicalRect button_in_root_frame = NodeRectInRootFrame(b);
488
489 EXPECT_EQ(SearchOrigin(RootViewport(&GetFrame()), b,
490 SpatialNavigationDirection::kUp),
491 Intersection(button_in_root_frame, RootViewport(&GetFrame())));
492
493 // Do some scrolling.
494 ScrollableArea* root_scroller = GetDocument().View()->GetScrollableArea();
495 root_scroller->SetScrollOffset(ScrollOffset(0, 600),
496 mojom::blink::ScrollType::kProgrammatic);
497 PhysicalRect button_after_scroll = NodeRectInRootFrame(b);
498 ASSERT_NE(button_in_root_frame,
499 button_after_scroll); // As we scrolled, the
500 // <button>'s position in
501 // the root frame changed.
502
503 // <button>'s top is clipped.
504 EXPECT_FALSE(IsOffscreen(b)); // <button> is not completely offscreen.
505 EXPECT_EQ(SearchOrigin(RootViewport(&GetFrame()), b,
506 SpatialNavigationDirection::kUp),
507 Intersection(button_after_scroll, RootViewport(&GetFrame())));
508 }
509
TEST_F(SpatialNavigationTest,StartFromDocEdgeWhenOffscreenIframeDisplaysFocus)510 TEST_F(SpatialNavigationTest,
511 StartFromDocEdgeWhenOffscreenIframeDisplaysFocus) {
512 SetBodyInnerHTML(
513 "<!DOCTYPE html>"
514 "<style>"
515 " iframe {"
516 " margin-top: 120%;" // Outside the visual viewport.
517 " height: 100px;"
518 " width: 100px;"
519 " }"
520 "</style>"
521 "<iframe id='iframe'></iframe>");
522
523 SetChildFrameHTML(
524 "<!DOCTYPE html>"
525 "<a id='link'>link</a>");
526
527 UpdateAllLifecyclePhasesForTest();
528 Element* link = ChildDocument().QuerySelector("a");
529 Element* iframe = GetDocument().QuerySelector("iframe");
530
531 // The <iframe> is not displayed in the visual viewport. In other words, it is
532 // being offscreen. And so is also its content, the <a>.
533 EXPECT_TRUE(IsOffscreen(iframe));
534 EXPECT_TRUE(IsOffscreen(&ChildDocument()));
535 EXPECT_TRUE(IsOffscreen(link));
536
537 AssertUseSidesOfVisualViewport(link);
538 }
539
TEST_F(SpatialNavigationTest,DivsCanClipIframes)540 TEST_F(SpatialNavigationTest, DivsCanClipIframes) {
541 SetBodyInnerHTML(
542 "<!DOCTYPE html>"
543 "<style>"
544 " div {"
545 " height: 100px;"
546 " width: 100px;"
547 " overflow: scroll;"
548 " }"
549 " iframe {"
550 " margin-top: 200px;" // Outside the div's viewport.
551 " height: 50px;"
552 " width: 50px;"
553 " }"
554 "</style>"
555 "<div>"
556 " <iframe id='iframe'></iframe>"
557 "</div>");
558
559 SetChildFrameHTML(
560 "<!DOCTYPE html>"
561 "<a>link</a>");
562
563 UpdateAllLifecyclePhasesForTest();
564 Element* div = GetDocument().QuerySelector("div");
565 Element* iframe = GetDocument().QuerySelector("iframe");
566 Element* link = ChildDocument().QuerySelector("a");
567 EXPECT_FALSE(IsOffscreen(div));
568
569 // TODO(crbug.com/889840):
570 // VisibleBoundsInVisualViewport does not (yet) take div-clipping into
571 // account. The node is off screen, but nevertheless VBIVV returns a non-
572 // empty rect. If you fix VisibleBoundsInVisualViewport, change to
573 // EXPECT_TRUE here and stop using LayoutObject in IsOffscreen().
574 EXPECT_FALSE(
575 iframe->VisibleBoundsInVisualViewport().IsEmpty()); // EXPECT_TRUE.
576
577 // The <iframe> is not displayed in the visual viewport because it is clipped
578 // by the div. In other words, it is being offscreen. And so is also its
579 // content, the <a>.
580 EXPECT_TRUE(IsOffscreen(iframe));
581 EXPECT_TRUE(IsOffscreen(&ChildDocument()));
582 EXPECT_TRUE(IsOffscreen(link));
583 }
584
TEST_F(SpatialNavigationTest,PartiallyVisibleIFrame)585 TEST_F(SpatialNavigationTest, PartiallyVisibleIFrame) {
586 // <a> is off screen. The <iframe> is visible, but partially off screen.
587 SetBodyInnerHTML(
588 "<!DOCTYPE html>"
589 "<style>"
590 " iframe {"
591 " width: 200%;"
592 " height: 100px;"
593 " }"
594 "</style>"
595 "<iframe id='iframe'></iframe>");
596
597 SetChildFrameHTML(
598 "<!DOCTYPE html>"
599 "<style>"
600 " #child {"
601 " margin-left: 120%;"
602 " }"
603 "</style>"
604 "<a id='child'>link</a>");
605
606 UpdateAllLifecyclePhasesForTest();
607 Element* child_element = ChildDocument().getElementById("child");
608 Node* enclosing_container = ScrollableAreaOrDocumentOf(child_element);
609 EXPECT_EQ(enclosing_container, ChildDocument());
610
611 EXPECT_TRUE(IsOffscreen(child_element)); // Completely offscreen.
612 EXPECT_FALSE(IsOffscreen(enclosing_container)); // Partially visible.
613
614 PhysicalRect iframe = NodeRectInRootFrame(enclosing_container);
615
616 // When searching downwards we start at activeElement's
617 // container's (here: the iframe's) topmost visible edge.
618 EXPECT_EQ(SearchOrigin(RootViewport(&GetFrame()), child_element,
619 SpatialNavigationDirection::kDown),
620 OppositeEdge(SpatialNavigationDirection::kDown,
621 Intersection(iframe, RootViewport(&GetFrame()))));
622
623 // When searching upwards we start at activeElement's
624 // container's (here: the iframe's) bottommost visible edge.
625 EXPECT_EQ(SearchOrigin(RootViewport(&GetFrame()), child_element,
626 SpatialNavigationDirection::kUp),
627 OppositeEdge(SpatialNavigationDirection::kUp,
628 Intersection(iframe, RootViewport(&GetFrame()))));
629
630 // When searching eastwards, "to the right", we start at activeElement's
631 // container's (here: the iframe's) leftmost visible edge.
632 EXPECT_EQ(SearchOrigin(RootViewport(&GetFrame()), child_element,
633 SpatialNavigationDirection::kRight),
634 OppositeEdge(SpatialNavigationDirection::kRight,
635 Intersection(iframe, RootViewport(&GetFrame()))));
636
637 // When searching westwards, "to the left", we start at activeElement's
638 // container's (here: the iframe's) rightmost visible edge.
639 EXPECT_EQ(SearchOrigin(RootViewport(&GetFrame()), child_element,
640 SpatialNavigationDirection::kLeft),
641 OppositeEdge(SpatialNavigationDirection::kLeft,
642 Intersection(iframe, RootViewport(&GetFrame()))));
643 }
644
TEST_F(SpatialNavigationTest,BottomOfPinchedViewport)645 TEST_F(SpatialNavigationTest, BottomOfPinchedViewport) {
646 PhysicalRect origin = SearchOrigin(RootViewport(&GetFrame()), nullptr,
647 SpatialNavigationDirection::kUp);
648 EXPECT_EQ(origin.Height(), 0);
649 EXPECT_EQ(origin.Width(), GetFrame().View()->Width());
650 EXPECT_EQ(origin.X(), 0);
651 EXPECT_EQ(origin.Y(), GetFrame().View()->Height() + 1);
652 EXPECT_EQ(origin, BottomOfVisualViewport());
653
654 // Now, test SearchOrigin with a pinched viewport.
655 VisualViewport& visual_viewport = GetFrame().GetPage()->GetVisualViewport();
656 visual_viewport.SetScale(2);
657 visual_viewport.SetLocation(FloatPoint(200, 200));
658 origin = SearchOrigin(RootViewport(&GetFrame()), nullptr,
659 SpatialNavigationDirection::kUp);
660 EXPECT_EQ(origin.Height(), 0);
661 EXPECT_LT(origin.Width(), GetFrame().View()->Width());
662 EXPECT_GT(origin.X(), 0);
663 EXPECT_LT(origin.Y(), GetFrame().View()->Height() + 1);
664 EXPECT_EQ(origin, BottomOfVisualViewport());
665 }
666
TEST_F(SpatialNavigationTest,StraightTextNoFragments)667 TEST_F(SpatialNavigationTest, StraightTextNoFragments) {
668 LoadAhem();
669 SetBodyInnerHTML(
670 "<!DOCTYPE html>"
671 "<style>"
672 " body {font: 10px/10px Ahem; width: 500px}"
673 "</style>"
674 "<a href='#' id='a'>blaaaaa blaaaaa blaaaaa</a>");
675 Element* a = GetDocument().getElementById("a");
676 EXPECT_FALSE(IsFragmentedInline(*a->GetLayoutObject()));
677 }
678
TEST_F(SpatialNavigationTest,LineBrokenTextHasFragments)679 TEST_F(SpatialNavigationTest, LineBrokenTextHasFragments) {
680 LoadAhem();
681 SetBodyInnerHTML(
682 "<!DOCTYPE html>"
683 "<style>"
684 " body {font: 10px/10px Ahem; width: 40px}"
685 "</style>"
686 "<a href='#' id='a'>blaaaaa blaaaaa blaaaaa</a>");
687 Element* a = GetDocument().getElementById("a");
688 EXPECT_TRUE(IsFragmentedInline(*a->GetLayoutObject()));
689 }
690
TEST_F(SpatialNavigationTest,ManyClientRectsButNotLineBrokenText)691 TEST_F(SpatialNavigationTest, ManyClientRectsButNotLineBrokenText) {
692 SetBodyInnerHTML(
693 "<!DOCTYPE html>"
694 "<style>"
695 " div {width: 20px; height: 20px;}"
696 "</style>"
697 "<a href='#' id='a'><div></div></a>");
698 Element* a = GetDocument().getElementById("a");
699 EXPECT_FALSE(IsFragmentedInline(*a->GetLayoutObject()));
700 }
701
TEST_F(SpatialNavigationTest,UseTheFirstFragment)702 TEST_F(SpatialNavigationTest, UseTheFirstFragment) {
703 LoadAhem();
704 SetBodyInnerHTML(
705 "<!DOCTYPE html>"
706 "<style>"
707 " body {font: 10px/10px Ahem; margin: 0; width: 50px;}"
708 "</style>"
709 "<a href='#' id='a'>12345 12</a>");
710 Element* a = GetDocument().getElementById("a");
711 EXPECT_TRUE(IsFragmentedInline(*a->GetLayoutObject()));
712
713 // Search downards.
714 PhysicalRect origin_down = SearchOrigin(RootViewport(&GetFrame()), a,
715 SpatialNavigationDirection::kDown);
716 PhysicalRect origin_fragment =
717 SearchOriginFragment(NodeRectInRootFrame(a), *a->GetLayoutObject(),
718 SpatialNavigationDirection::kDown);
719 EXPECT_EQ(origin_down, origin_fragment);
720 EXPECT_EQ(origin_down.Height(), 10);
721 EXPECT_EQ(origin_down.Width(), 50);
722 EXPECT_EQ(origin_down.X(), 0);
723 EXPECT_EQ(origin_down.Y(), 0);
724
725 // Search upwards.
726 PhysicalRect origin_up = SearchOrigin(RootViewport(&GetFrame()), a,
727 SpatialNavigationDirection::kUp);
728 PhysicalRect origin_fragment_up =
729 SearchOriginFragment(NodeRectInRootFrame(a), *a->GetLayoutObject(),
730 SpatialNavigationDirection::kUp);
731 EXPECT_EQ(origin_up, origin_fragment_up);
732 EXPECT_EQ(origin_up.Height(), 10);
733 EXPECT_EQ(origin_up.Width(), 20);
734 EXPECT_EQ(origin_up.X(), 0);
735 EXPECT_EQ(origin_up.Y(), 10);
736
737 // Search from the top fragment.
738 PhysicalRect origin_left = SearchOrigin(RootViewport(&GetFrame()), a,
739 SpatialNavigationDirection::kLeft);
740 EXPECT_EQ(origin_left, origin_down);
741
742 // Search from the bottom fragment.
743 PhysicalRect origin_right = SearchOrigin(RootViewport(&GetFrame()), a,
744 SpatialNavigationDirection::kRight);
745 EXPECT_EQ(origin_right, origin_up);
746 }
747
TEST_F(SpatialNavigationTest,InlineImageLink)748 TEST_F(SpatialNavigationTest, InlineImageLink) {
749 LoadAhem();
750 SetBodyInnerHTML(
751 "<!DOCTYPE html>"
752 "<body style='font: 17px Ahem;'>"
753 "<a id='a'><img id='pic' width='50' height='50'></a>"
754 "</body>");
755 Element* a = GetDocument().getElementById("a");
756 EXPECT_TRUE(HasSameSearchOriginRectAndCandidateRect(a));
757 PhysicalRect uncropped_link = NodeRectInRootFrame(a);
758 EXPECT_EQ(uncropped_link.Width(), 50);
759 EXPECT_EQ(uncropped_link.Height(), 50);
760
761 // The link gets its img's dimensions.
762 PhysicalRect search_origin = SearchOrigin(RootViewport(&GetFrame()), a,
763 SpatialNavigationDirection::kDown);
764 EXPECT_EQ(search_origin, uncropped_link);
765 }
766
TEST_F(SpatialNavigationTest,InlineImageLinkWithLineHeight)767 TEST_F(SpatialNavigationTest, InlineImageLinkWithLineHeight) {
768 LoadAhem();
769 SetBodyInnerHTML(
770 "<!DOCTYPE html>"
771 "<body style='font: 17px Ahem; line-height: 13px;'>"
772 "<a id='a'><img id='pic' width='50' height='50'></a>"
773 "</body>");
774 Element* a = GetDocument().getElementById("a");
775 EXPECT_TRUE(HasSameSearchOriginRectAndCandidateRect(a));
776 PhysicalRect uncropped_link = NodeRectInRootFrame(a);
777 EXPECT_EQ(uncropped_link.Width(), 50);
778 EXPECT_EQ(uncropped_link.Height(), 50);
779
780 // The link gets its img's dimensions.
781 PhysicalRect search_origin = SearchOrigin(RootViewport(&GetFrame()), a,
782 SpatialNavigationDirection::kDown);
783 EXPECT_EQ(search_origin, uncropped_link);
784 }
785
TEST_F(SpatialNavigationTest,InlineImageTextLinkWithLineHeight)786 TEST_F(SpatialNavigationTest, InlineImageTextLinkWithLineHeight) {
787 LoadAhem();
788 SetBodyInnerHTML(
789 "<!DOCTYPE html>"
790 "<div style='font: 16px Ahem; line-height: 13px;'>"
791 "<a id='a'><img width='30' height='30' id='replacedinline'>aaa</a> "
792 "<a id='b'>b</a><br/>"
793 "<a id='c'>cccccccc</a>"
794 "</div>");
795 Element* a = GetDocument().getElementById("a");
796 Element* b = GetDocument().getElementById("b");
797 Element* c = GetDocument().getElementById("c");
798 EXPECT_TRUE(HasSameSearchOriginRectAndCandidateRect(a));
799 EXPECT_TRUE(HasSameSearchOriginRectAndCandidateRect(b));
800 EXPECT_TRUE(HasSameSearchOriginRectAndCandidateRect(c));
801
802 // The link gets its img's height.
803 PhysicalRect search_origin = SearchOrigin(RootViewport(&GetFrame()), a,
804 SpatialNavigationDirection::kDown);
805 EXPECT_EQ(search_origin.Height(), 30);
806
807 EXPECT_FALSE(Intersects(a, c));
808 EXPECT_FALSE(Intersects(b, c));
809 }
810
TEST_F(SpatialNavigationTest,InlineLinkWithInnerBlock)811 TEST_F(SpatialNavigationTest, InlineLinkWithInnerBlock) {
812 LoadAhem();
813 SetBodyInnerHTML(
814 "<!DOCTYPE html>"
815 "<div style='font: 20px Ahem; line-height: 16px;'>"
816 "<a id='a'>a<span style='display: inline-block; width: 40px; height: "
817 "45px; color: red'>a</span>a</a><a id='b'>bbb</a><br/>"
818 "<a id='c'>cccccccc</a>"
819 "</div>");
820 Element* a = GetDocument().getElementById("a");
821 Element* b = GetDocument().getElementById("b");
822 Element* c = GetDocument().getElementById("c");
823 EXPECT_TRUE(HasSameSearchOriginRectAndCandidateRect(a));
824 EXPECT_TRUE(HasSameSearchOriginRectAndCandidateRect(b));
825 EXPECT_TRUE(HasSameSearchOriginRectAndCandidateRect(c));
826
827 // The link gets its inner block's height.
828 PhysicalRect search_origin = SearchOrigin(RootViewport(&GetFrame()), a,
829 SpatialNavigationDirection::kDown);
830 EXPECT_EQ(search_origin.Height(), 45);
831
832 EXPECT_FALSE(Intersects(a, c));
833 EXPECT_FALSE(Intersects(b, c));
834 }
835
TEST_F(SpatialNavigationTest,NoOverlappingLinks)836 TEST_F(SpatialNavigationTest, NoOverlappingLinks) {
837 LoadAhem();
838 SetBodyInnerHTML(
839 "<!DOCTYPE html>"
840 "<div style='font: 17px Ahem;'>"
841 " <a id='a'>aaa</a> <a id='b'>bbb</a><br/>"
842 " <a id='c'>cccccccc</a>"
843 "</div>");
844 Element* a = GetDocument().getElementById("a");
845 Element* b = GetDocument().getElementById("b");
846 Element* c = GetDocument().getElementById("c");
847 AssertNormalizedHeight(a, 17, false);
848 AssertNormalizedHeight(b, 17, false);
849 AssertNormalizedHeight(c, 17, false);
850 EXPECT_TRUE(HasSameSearchOriginRectAndCandidateRect(a));
851 EXPECT_TRUE(HasSameSearchOriginRectAndCandidateRect(b));
852 EXPECT_TRUE(HasSameSearchOriginRectAndCandidateRect(c));
853 EXPECT_FALSE(Intersects(a, b));
854 EXPECT_FALSE(Intersects(a, c));
855 }
856
TEST_F(SpatialNavigationTest,OverlappingLinks)857 TEST_F(SpatialNavigationTest, OverlappingLinks) {
858 LoadAhem();
859 SetBodyInnerHTML(
860 "<!DOCTYPE html>"
861 "<div style='font: 16px Ahem; line-height: 13px;'>"
862 " <a id='a'>aaa</a> <a id='b'>bbb</a><br/>"
863 " <a id='c'>cccccccc</a>"
864 "</div>");
865 Element* a = GetDocument().getElementById("a");
866 Element* b = GetDocument().getElementById("b");
867 Element* c = GetDocument().getElementById("c");
868 // SpatNav will use the line box's height.
869 AssertNormalizedHeight(a, 13, true);
870 AssertNormalizedHeight(b, 13, true);
871 AssertNormalizedHeight(c, 13, true);
872 EXPECT_TRUE(HasSameSearchOriginRectAndCandidateRect(a));
873 EXPECT_TRUE(HasSameSearchOriginRectAndCandidateRect(b));
874 EXPECT_TRUE(HasSameSearchOriginRectAndCandidateRect(c));
875 EXPECT_FALSE(Intersects(a, b));
876 EXPECT_FALSE(Intersects(a, c));
877 }
878
TEST_F(SpatialNavigationTest,UseInlineBoxHeightWhenShorter)879 TEST_F(SpatialNavigationTest, UseInlineBoxHeightWhenShorter) {
880 LoadAhem();
881 SetBodyInnerHTML(
882 "<!DOCTYPE html>"
883 "<div style='font: 17px Ahem; line-height: 20px'>"
884 " <a id='a'>aaa</a> <a id='b'>bbb</a><br/>"
885 " <a id='c'>cccccccc</a>"
886 "</div>");
887 Element* a = GetDocument().getElementById("a");
888 Element* b = GetDocument().getElementById("b");
889 Element* c = GetDocument().getElementById("c");
890 // SpatNav will use the inline boxes' height (17px) when it's shorter than
891 // their line box (20px).
892 AssertNormalizedHeight(a, 17, false);
893 AssertNormalizedHeight(b, 17, false);
894 AssertNormalizedHeight(c, 17, false);
895 EXPECT_TRUE(HasSameSearchOriginRectAndCandidateRect(a));
896 EXPECT_TRUE(HasSameSearchOriginRectAndCandidateRect(b));
897 EXPECT_TRUE(HasSameSearchOriginRectAndCandidateRect(c));
898 EXPECT_FALSE(Intersects(a, b));
899 EXPECT_FALSE(Intersects(a, c));
900 }
901
TEST_F(SpatialNavigationTest,LineBrokenLink)902 TEST_F(SpatialNavigationTest, LineBrokenLink) {
903 LoadAhem();
904 SetBodyInnerHTML(
905 "<!DOCTYPE html>"
906 "<style>"
907 " body {font: 10px Ahem; line-height: 12px; width: 40px}"
908 "</style>"
909 "<a id='a'>bla bla bla</a>");
910 Element* a = GetDocument().getElementById("a");
911 ASSERT_TRUE(IsFragmentedInline(*a->GetLayoutObject()));
912 ASSERT_EQ(LineBoxes(*a->GetLayoutObject()), 3);
913 PhysicalRect search_origin =
914 SearchOrigin(RootViewport(a->GetDocument().GetFrame()), a,
915 SpatialNavigationDirection::kDown);
916 // The line box (12px) is bigger than the inline box (10px).
917 EXPECT_EQ(search_origin.Height(), 10);
918
919 // A line broken link's search origin will only be the first or last line box.
920 // The candidate rect will still contain all line boxes.
921 EXPECT_FALSE(HasSameSearchOriginRectAndCandidateRect(a));
922
923 FocusCandidate candidate(a, SpatialNavigationDirection::kDown);
924 PhysicalRect uncropped = NodeRectInRootFrame(a);
925 EXPECT_EQ(uncropped, candidate.rect_in_root_frame);
926 EXPECT_EQ(candidate.rect_in_root_frame.Height(), 12 + 12 + 10);
927 }
928
TEST_F(SpatialNavigationTest,NormalizedLineBrokenLink)929 TEST_F(SpatialNavigationTest, NormalizedLineBrokenLink) {
930 LoadAhem();
931 SetBodyInnerHTML(
932 "<!DOCTYPE html>"
933 "<style>"
934 " body {font: 10px Ahem; line-height: 7px; width: 40px}"
935 "</style>"
936 "<a id='a'>bla bla bla</a>");
937 Element* a = GetDocument().getElementById("a");
938 ASSERT_TRUE(IsFragmentedInline(*a->GetLayoutObject()));
939 ASSERT_EQ(LineBoxes(*a->GetLayoutObject()), 3);
940 PhysicalRect search_origin =
941 SearchOrigin(RootViewport(a->GetDocument().GetFrame()), a,
942 SpatialNavigationDirection::kDown);
943 // The line box (7px) is smaller than the inline box (10px).
944 EXPECT_EQ(search_origin.Height(), 7);
945
946 // A line broken link's search origin will only be the first or last line box.
947 // The candidate rect will still contain all line boxes.
948 EXPECT_FALSE(HasSameSearchOriginRectAndCandidateRect(a));
949
950 FocusCandidate candidate(a, SpatialNavigationDirection::kDown);
951 PhysicalRect uncropped = NodeRectInRootFrame(a);
952 EXPECT_LT(candidate.rect_in_root_frame.Height(), uncropped.Height());
953 EXPECT_EQ(candidate.rect_in_root_frame.Height(), 3 * 7);
954 }
955
TEST_F(SpatialNavigationTest,NormalizedLineBrokenLinkWithImg)956 TEST_F(SpatialNavigationTest, NormalizedLineBrokenLinkWithImg) {
957 LoadAhem();
958 SetBodyInnerHTML(
959 "<!DOCTYPE html>"
960 "<style>"
961 "body {font: 10px Ahem; line-height: 7px;}"
962 "</style>"
963 "<div style='width: 40px'>"
964 "<a id='a'>aa<img width='10' height='24' src=''>a aaaa</a>"
965 "<a id='b'>bb</a>"
966 "</div>");
967 Element* a = GetDocument().getElementById("a");
968 Element* b = GetDocument().getElementById("b");
969 ASSERT_TRUE(IsFragmentedInline(*a->GetLayoutObject()));
970 ASSERT_FALSE(IsFragmentedInline(*b->GetLayoutObject()));
971 ASSERT_EQ(LineBoxes(*a->GetLayoutObject()), 2);
972 ASSERT_EQ(LineBoxes(*b->GetLayoutObject()), 1);
973
974 // A line broken link's search origin will only be the first or last line box.
975 // The candidate rect will still contain all line boxes.
976 EXPECT_FALSE(HasSameSearchOriginRectAndCandidateRect(a));
977 EXPECT_FALSE(Intersects(a, b));
978 }
979
TEST_F(SpatialNavigationTest,PaddedInlineLinkOverlapping)980 TEST_F(SpatialNavigationTest, PaddedInlineLinkOverlapping) {
981 LoadAhem();
982 SetBodyInnerHTML(
983 "<!DOCTYPE html>"
984 "<div style='font: 18px Ahem; line-height: 13px;'>"
985 " <a id='a' style='padding: 10px;'>aaa</a>"
986 " <a id='b'>bbb</a><br/>"
987 " <a id='c'>cccccccc</a>"
988 "</div>");
989 Element* a = GetDocument().getElementById("a");
990 Element* b = GetDocument().getElementById("b");
991 Element* c = GetDocument().getElementById("c");
992 // Padding doesn't grow |a|'s line box.
993 AssertNormalizedHeight(a, 13, true);
994 AssertNormalizedHeight(b, 13, true);
995 AssertNormalizedHeight(c, 13, true);
996 EXPECT_TRUE(HasSameSearchOriginRectAndCandidateRect(a));
997 EXPECT_TRUE(HasSameSearchOriginRectAndCandidateRect(b));
998 EXPECT_TRUE(HasSameSearchOriginRectAndCandidateRect(c));
999 EXPECT_FALSE(Intersects(a, b));
1000 EXPECT_FALSE(Intersects(a, c));
1001 }
1002
TEST_F(SpatialNavigationTest,PaddedInlineBlockLinkOverlapping)1003 TEST_F(SpatialNavigationTest, PaddedInlineBlockLinkOverlapping) {
1004 LoadAhem();
1005 SetBodyInnerHTML(
1006 "<!DOCTYPE html>"
1007 "<div style='font: 18px Ahem; line-height: 13px;'>"
1008 " <a id='a' style='display: inline-block; padding: 10px;'>aaa</a>"
1009 " <a id='b'>bbb</a><br/>"
1010 " <a id='c'>cccccccc</a>"
1011 "</div>");
1012 Element* a = GetDocument().getElementById("a");
1013 Element* b = GetDocument().getElementById("b");
1014 Element* c = GetDocument().getElementById("c");
1015 EXPECT_TRUE(HasSameSearchOriginRectAndCandidateRect(a));
1016 EXPECT_TRUE(HasSameSearchOriginRectAndCandidateRect(b));
1017 EXPECT_TRUE(HasSameSearchOriginRectAndCandidateRect(c));
1018 EXPECT_FALSE(Intersects(a, b));
1019 EXPECT_FALSE(Intersects(a, c));
1020 }
1021
TEST_F(SpatialNavigationTest,BoxWithLineHeight)1022 TEST_F(SpatialNavigationTest, BoxWithLineHeight) {
1023 LoadAhem();
1024 SetBodyInnerHTML(
1025 "<!DOCTYPE html>"
1026 "<div style='font: 16px Ahem; line-height: 13px;' id='block'>"
1027 " aaa bbb<br/>"
1028 " <a id='c'>cccccccc</a>"
1029 "</div>");
1030 Element* block = GetDocument().getElementById("block");
1031 Element* c = GetDocument().getElementById("c");
1032 ASSERT_TRUE(Intersects(block, c));
1033
1034 // The block's inner line-height does not change the block's outer dimensions.
1035 PhysicalRect search_origin = SearchOrigin(RootViewport(&GetFrame()), block,
1036 SpatialNavigationDirection::kDown);
1037 PhysicalRect uncropped = NodeRectInRootFrame(block);
1038 PhysicalRect normalized =
1039 ShrinkInlineBoxToLineBox(*block->GetLayoutObject(), uncropped);
1040 EXPECT_EQ(search_origin, uncropped);
1041 EXPECT_EQ(normalized, uncropped);
1042 EXPECT_TRUE(HasSameSearchOriginRectAndCandidateRect(block));
1043 }
1044
TEST_F(SpatialNavigationTest,ReplacedInlineElement)1045 TEST_F(SpatialNavigationTest, ReplacedInlineElement) {
1046 LoadAhem();
1047 SetBodyInnerHTML(
1048 "<!DOCTYPE html>"
1049 "<body style='font: 16px Ahem; line-height: 13px;'>"
1050 " <img width='20' height='20' id='pic'> bbb<br/>"
1051 " <a id='c'>cccccccc</a>"
1052 "</body>");
1053 Element* pic = GetDocument().getElementById("pic");
1054 Element* c = GetDocument().getElementById("c");
1055 EXPECT_FALSE(Intersects(pic, c));
1056
1057 // The line-height around the img does not change the img's outer dimensions.
1058 PhysicalRect search_origin = SearchOrigin(RootViewport(&GetFrame()), pic,
1059 SpatialNavigationDirection::kDown);
1060 PhysicalRect uncropped = NodeRectInRootFrame(pic);
1061 PhysicalRect normalized =
1062 ShrinkInlineBoxToLineBox(*pic->GetLayoutObject(), uncropped);
1063 EXPECT_EQ(search_origin, uncropped);
1064 EXPECT_EQ(normalized, uncropped);
1065 EXPECT_EQ(search_origin.Width(), 20);
1066 EXPECT_EQ(search_origin.Height(), 20);
1067 EXPECT_TRUE(HasSameSearchOriginRectAndCandidateRect(pic));
1068 }
1069
TEST_F(SpatialNavigationTest,VerticalText)1070 TEST_F(SpatialNavigationTest, VerticalText) {
1071 LoadAhem();
1072 SetBodyInnerHTML(
1073 "<!DOCTYPE html>"
1074 "<div style='font: 14px/14px Ahem; line-height: 12px; writing-mode: "
1075 "vertical-lr; height: 160px'>"
1076 "<a id='a'>aaaaaaaaaaa</a>"
1077 "<a id='b'>bbb</a> <a id='c'>cccccc</a>"
1078 "</div>");
1079 Element* a = GetDocument().getElementById("a");
1080 Element* b = GetDocument().getElementById("b");
1081 Element* c = GetDocument().getElementById("c");
1082 EXPECT_TRUE(HasSameSearchOriginRectAndCandidateRect(a));
1083 EXPECT_TRUE(HasSameSearchOriginRectAndCandidateRect(b));
1084 EXPECT_TRUE(HasSameSearchOriginRectAndCandidateRect(c));
1085 EXPECT_FALSE(Intersects(a, b));
1086 EXPECT_FALSE(Intersects(a, c));
1087
1088 PhysicalRect search_origin = SearchOrigin(RootViewport(&GetFrame()), a,
1089 SpatialNavigationDirection::kDown);
1090 ASSERT_EQ(search_origin.Height(), 14 * 11);
1091 EXPECT_EQ(search_origin.Width(), 12); // The logical line-height.
1092 }
1093
TEST_F(SpatialNavigationTest,TopOfPinchedViewport)1094 TEST_F(SpatialNavigationTest, TopOfPinchedViewport) {
1095 PhysicalRect origin = SearchOrigin(RootViewport(&GetFrame()), nullptr,
1096 SpatialNavigationDirection::kDown);
1097 EXPECT_EQ(origin.Height(), 0);
1098 EXPECT_EQ(origin.Width(), GetFrame().View()->Width());
1099 EXPECT_EQ(origin.X(), 0);
1100 EXPECT_EQ(origin.Y(), -1);
1101 EXPECT_EQ(origin, TopOfVisualViewport());
1102
1103 // Now, test SearchOrigin with a pinched viewport.
1104 VisualViewport& visual_viewport = GetFrame().GetPage()->GetVisualViewport();
1105 visual_viewport.SetScale(2);
1106 visual_viewport.SetLocation(FloatPoint(200, 200));
1107 origin = SearchOrigin(RootViewport(&GetFrame()), nullptr,
1108 SpatialNavigationDirection::kDown);
1109 EXPECT_EQ(origin.Height(), 0);
1110 EXPECT_LT(origin.Width(), GetFrame().View()->Width());
1111 EXPECT_GT(origin.X(), 0);
1112 EXPECT_GT(origin.Y(), -1);
1113 EXPECT_EQ(origin, TopOfVisualViewport());
1114 }
1115
TEST_F(SpatialNavigationTest,HasRemoteFrame)1116 TEST_F(SpatialNavigationTest, HasRemoteFrame) {
1117 frame_test_helpers::WebViewHelper helper;
1118 helper.InitializeAndLoad("about:blank");
1119
1120 WebViewImpl* webview = helper.GetWebView();
1121 WebURL base_url = url_test_helpers::ToKURL("http://www.test.com/");
1122 frame_test_helpers::LoadHTMLString(webview->MainFrameImpl(),
1123 "<!DOCTYPE html>"
1124 "<iframe id='iframe'></iframe>",
1125 base_url);
1126
1127 webview->ResizeWithBrowserControls(gfx::Size(400, 400), 50, 0, false);
1128 UpdateAllLifecyclePhasesForTest();
1129
1130 Element* iframe =
1131 webview->MainFrameImpl()->GetFrame()->GetDocument()->getElementById(
1132 "iframe");
1133 EXPECT_FALSE(HasRemoteFrame(iframe));
1134
1135 webview->MainFrameImpl()->FirstChild()->Swap(
1136 frame_test_helpers::CreateRemote());
1137 EXPECT_TRUE(HasRemoteFrame(iframe));
1138 }
1139
1140 class SpatialNavigationWithFocuslessModeTest
1141 : public SpatialNavigationTest,
1142 public ::testing::WithParamInterface<bool> {
1143 public:
SpatialNavigationWithFocuslessModeTest()1144 SpatialNavigationWithFocuslessModeTest() : use_focusless_mode_(GetParam()) {}
1145
SetUp()1146 void SetUp() override {
1147 SpatialNavigationTest::SetUp();
1148 GetDocument().GetSettings()->SetSpatialNavigationEnabled(true);
1149 }
1150
1151 private:
1152 ScopedFocuslessSpatialNavigationForTest use_focusless_mode_;
1153 };
1154
1155 INSTANTIATE_TEST_SUITE_P(All,
1156 SpatialNavigationWithFocuslessModeTest,
1157 ::testing::Bool());
1158
TEST_P(SpatialNavigationWithFocuslessModeTest,PressEnterKeyActiveElement)1159 TEST_P(SpatialNavigationWithFocuslessModeTest, PressEnterKeyActiveElement) {
1160 SetBodyInnerHTML("<button id='b'>hello</button>");
1161
1162 Element* b = GetDocument().getElementById("b");
1163
1164 // Move interest to button.
1165 WebKeyboardEvent arrow_down{WebInputEvent::Type::kRawKeyDown,
1166 WebInputEvent::kNoModifiers,
1167 WebInputEvent::GetStaticTimeStampForTests()};
1168 arrow_down.dom_key = ui::DomKey::ARROW_DOWN;
1169 GetDocument().GetFrame()->GetEventHandler().KeyEvent(arrow_down);
1170
1171 arrow_down.SetType(WebInputEvent::Type::kKeyUp);
1172 GetDocument().GetFrame()->GetEventHandler().KeyEvent(arrow_down);
1173
1174 EXPECT_FALSE(b->IsActive());
1175
1176 // Enter key down add :active state to element.
1177 WebKeyboardEvent enter{WebInputEvent::Type::kRawKeyDown,
1178 WebInputEvent::kNoModifiers,
1179 WebInputEvent::GetStaticTimeStampForTests()};
1180 enter.dom_key = ui::DomKey::ENTER;
1181 GetDocument().GetFrame()->GetEventHandler().KeyEvent(enter);
1182 EXPECT_TRUE(b->IsActive());
1183
1184 // Enter key up remove :active state to element.
1185 enter.SetType(WebInputEvent::Type::kKeyUp);
1186 GetDocument().GetFrame()->GetEventHandler().KeyEvent(enter);
1187 EXPECT_FALSE(b->IsActive());
1188 }
1189
1190 class FocuslessSpatialNavigationSimTest : public SimTest {
1191 public:
FocuslessSpatialNavigationSimTest()1192 FocuslessSpatialNavigationSimTest() : use_focusless_mode_(true) {}
1193
SetUp()1194 void SetUp() override {
1195 SimTest::SetUp();
1196 WebView().GetPage()->GetSettings().SetSpatialNavigationEnabled(true);
1197 }
1198
SimulateKeyPress(int dom_key)1199 void SimulateKeyPress(int dom_key) {
1200 WebKeyboardEvent event{WebInputEvent::Type::kRawKeyDown,
1201 WebInputEvent::kNoModifiers,
1202 WebInputEvent::GetStaticTimeStampForTests()};
1203 event.dom_key = dom_key;
1204 WebView().MainFrameWidget()->HandleInputEvent(
1205 WebCoalescedInputEvent(event, ui::LatencyInfo()));
1206
1207 if (dom_key == ui::DomKey::ENTER) {
1208 event.SetType(WebInputEvent::Type::kChar);
1209 WebView().MainFrameWidget()->HandleInputEvent(
1210 WebCoalescedInputEvent(event, ui::LatencyInfo()));
1211 }
1212
1213 event.SetType(WebInputEvent::Type::kKeyUp);
1214 WebView().MainFrameWidget()->HandleInputEvent(
1215 WebCoalescedInputEvent(event, ui::LatencyInfo()));
1216 }
1217
1218 ScopedFocuslessSpatialNavigationForTest use_focusless_mode_;
1219 };
1220
1221 // Tests that opening a <select> popup works by pressing enter from
1222 // "interested" mode, without being focused.
TEST_F(FocuslessSpatialNavigationSimTest,OpenSelectPopup)1223 TEST_F(FocuslessSpatialNavigationSimTest, OpenSelectPopup) {
1224 // This test requires PagePopup since we're testing opening the <select> drop
1225 // down so skip this test on platforms (i.e. Android) that don't use this.
1226 if (!RuntimeEnabledFeatures::PagePopupEnabled())
1227 return;
1228
1229 WebView().MainFrameViewWidget()->Resize(gfx::Size(800, 600));
1230 WebView().MainFrameWidget()->SetFocus(true);
1231 WebView().SetIsActive(true);
1232
1233 SimRequest request("https://example.com/test.html", "text/html");
1234 LoadURL("https://example.com/test.html");
1235 request.Complete(R"HTML(
1236 <!DOCTYPE html>
1237 <select id="target">
1238 <option>A</option>
1239 <option>B</option>
1240 <option>C</option>
1241 </select>
1242 )HTML");
1243 Compositor().BeginFrame();
1244
1245 auto* select = To<HTMLSelectElement>(GetDocument().getElementById("target"));
1246 SimulateKeyPress(ui::DomKey::ARROW_DOWN);
1247
1248 SpatialNavigationController& spat_nav_controller =
1249 GetDocument().GetPage()->GetSpatialNavigationController();
1250
1251 ASSERT_EQ(select, spat_nav_controller.GetInterestedElement());
1252 ASSERT_NE(select, GetDocument().ActiveElement());
1253 ASSERT_FALSE(select->PopupIsVisible());
1254
1255 // The enter key should cause the popup to open.
1256 SimulateKeyPress(ui::DomKey::ENTER);
1257 EXPECT_TRUE(select->PopupIsVisible());
1258 }
1259
1260 } // namespace blink
1261