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