| // Copyright 2017 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "third_party/blink/renderer/core/page/spatial_navigation.h" |
| |
| #include "testing/gtest/include/gtest/gtest.h" |
| #include "third_party/blink/public/common/input/web_keyboard_event.h" |
| #include "third_party/blink/renderer/core/exported/web_remote_frame_impl.h" |
| #include "third_party/blink/renderer/core/frame/frame_test_helpers.h" |
| #include "third_party/blink/renderer/core/frame/local_frame.h" |
| #include "third_party/blink/renderer/core/frame/visual_viewport.h" |
| #include "third_party/blink/renderer/core/frame/web_local_frame_impl.h" |
| #include "third_party/blink/renderer/core/html/forms/html_select_element.h" |
| #include "third_party/blink/renderer/core/input/event_handler.h" |
| #include "third_party/blink/renderer/core/page/spatial_navigation_controller.h" |
| #include "third_party/blink/renderer/core/testing/core_unit_test_helper.h" |
| #include "third_party/blink/renderer/core/testing/sim/sim_request.h" |
| #include "third_party/blink/renderer/core/testing/sim/sim_test.h" |
| #include "third_party/blink/renderer/platform/testing/runtime_enabled_features_test_helpers.h" |
| #include "third_party/blink/renderer/platform/testing/url_test_helpers.h" |
| #include "ui/events/keycodes/dom/dom_key.h" |
| |
| namespace blink { |
| |
| class SpatialNavigationTest : public RenderingTest { |
| public: |
| SpatialNavigationTest() |
| : RenderingTest(MakeGarbageCollected<SingleChildLocalFrameClient>()) {} |
| |
| PhysicalRect TopOfVisualViewport() { |
| PhysicalRect visual_viewport = RootViewport(&GetFrame()); |
| visual_viewport.SetY(visual_viewport.Y() - 1); |
| visual_viewport.SetHeight(LayoutUnit(0)); |
| return visual_viewport; |
| } |
| |
| PhysicalRect BottomOfVisualViewport() { |
| PhysicalRect visual_viewport = RootViewport(&GetFrame()); |
| visual_viewport.SetY(visual_viewport.Bottom() + 1); |
| visual_viewport.SetHeight(LayoutUnit(0)); |
| return visual_viewport; |
| } |
| |
| PhysicalRect LeftSideOfVisualViewport() { |
| PhysicalRect visual_viewport = RootViewport(&GetFrame()); |
| visual_viewport.SetX(visual_viewport.X() - 1); |
| visual_viewport.SetWidth(LayoutUnit(0)); |
| return visual_viewport; |
| } |
| |
| PhysicalRect RightSideOfVisualViewport() { |
| PhysicalRect visual_viewport = RootViewport(&GetFrame()); |
| visual_viewport.SetX(visual_viewport.Right() + 1); |
| visual_viewport.SetWidth(LayoutUnit(0)); |
| return visual_viewport; |
| } |
| |
| void AssertUseSidesOfVisualViewport(Node* focus_node) { |
| EXPECT_EQ(SearchOrigin(RootViewport(&GetFrame()), focus_node, |
| SpatialNavigationDirection::kUp), |
| BottomOfVisualViewport()); |
| EXPECT_EQ(SearchOrigin(RootViewport(&GetFrame()), focus_node, |
| SpatialNavigationDirection::kDown), |
| TopOfVisualViewport()); |
| EXPECT_EQ(SearchOrigin(RootViewport(&GetFrame()), focus_node, |
| SpatialNavigationDirection::kLeft), |
| RightSideOfVisualViewport()); |
| EXPECT_EQ(SearchOrigin(RootViewport(&GetFrame()), focus_node, |
| SpatialNavigationDirection::kRight), |
| LeftSideOfVisualViewport()); |
| } |
| |
| void UpdateAllLifecyclePhases(LocalFrameView* frame_view) { |
| frame_view->UpdateAllLifecyclePhases(DocumentUpdateReason::kTest); |
| } |
| }; |
| |
| TEST_F(SpatialNavigationTest, RootFramesVisualViewport) { |
| // Test RootViewport with a pinched viewport. |
| VisualViewport& visual_viewport = GetFrame().GetPage()->GetVisualViewport(); |
| visual_viewport.SetScale(2); |
| visual_viewport.SetLocation(FloatPoint(200, 200)); |
| |
| LocalFrameView* root_frame_view = GetFrame().LocalFrameRoot().View(); |
| const PhysicalRect roots_visible_doc_rect( |
| root_frame_view->GetScrollableArea()->VisibleContentRect()); |
| // Convert the root frame's visible rect from document space -> frame space. |
| // For the root frame, frame space == root frame space, obviously. |
| PhysicalRect viewport_rect_of_root_frame = |
| root_frame_view->DocumentToFrame(roots_visible_doc_rect); |
| |
| EXPECT_EQ(viewport_rect_of_root_frame, RootViewport(&GetFrame())); |
| } |
| |
| TEST_F(SpatialNavigationTest, FindContainerWhenEnclosingContainerIsDocument) { |
| SetBodyInnerHTML( |
| "<!DOCTYPE html>" |
| "<a id='child'>link</a>"); |
| |
| Element* child_element = GetDocument().getElementById("child"); |
| Node* enclosing_container = ScrollableAreaOrDocumentOf(child_element); |
| |
| EXPECT_EQ(enclosing_container, GetDocument()); |
| EXPECT_TRUE(IsScrollableAreaOrDocument(enclosing_container)); |
| } |
| |
| TEST_F(SpatialNavigationTest, FindContainerWhenEnclosingContainerIsIframe) { |
| SetBodyInnerHTML( |
| "<!DOCTYPE html>" |
| "<style>" |
| " iframe {" |
| " width: 100px;" |
| " height: 100px;" |
| " }" |
| "</style>" |
| "<iframe id='iframe'></iframe>"); |
| |
| SetChildFrameHTML( |
| "<!DOCTYPE html>" |
| "<a>link</a>"); |
| |
| UpdateAllLifecyclePhases(ChildDocument().View()); |
| Element* iframe = GetDocument().QuerySelector("iframe"); |
| Element* link = ChildDocument().QuerySelector("a"); |
| Node* enclosing_container = ScrollableAreaOrDocumentOf(link); |
| |
| EXPECT_FALSE(IsOffscreen(iframe)); |
| EXPECT_FALSE(IsOffscreen(&ChildDocument())); |
| EXPECT_FALSE(IsOffscreen(link)); |
| |
| EXPECT_EQ(enclosing_container, ChildDocument()); |
| EXPECT_TRUE(IsScrollableAreaOrDocument(enclosing_container)); |
| } |
| |
| TEST_F(SpatialNavigationTest, |
| FindContainerWhenEnclosingContainerIsScrollableOverflowBox) { |
| GetDocument().SetCompatibilityMode(Document::kQuirksMode); |
| SetBodyInnerHTML( |
| "<!DOCTYPE html>" |
| "<style>" |
| " #content {" |
| " margin-top: 200px;" // Outside the div's viewport. |
| " }" |
| " #container {" |
| " height: 100px;" |
| " overflow: scroll;" |
| " }" |
| "</style>" |
| "<div id='container'>" |
| " <div id='content'>some text here</div>" |
| "</div>"); |
| |
| Element* content = GetDocument().getElementById("content"); |
| Element* container = GetDocument().getElementById("container"); |
| Node* enclosing_container = ScrollableAreaOrDocumentOf(content); |
| |
| // TODO(crbug.com/889840): |
| // VisibleBoundsInVisualViewport does not (yet) take div-clipping into |
| // account. The node is off screen, but nevertheless VBIVV returns a non- |
| // empty rect. If you fix VisibleBoundsInVisualViewport, change to |
| // EXPECT_TRUE here and stop using LayoutObject in IsOffscreen(). |
| EXPECT_FALSE( |
| content->VisibleBoundsInVisualViewport().IsEmpty()); // EXPECT_TRUE. |
| |
| EXPECT_TRUE(IsOffscreen(content)); |
| EXPECT_FALSE(IsOffscreen(container)); |
| |
| EXPECT_EQ(enclosing_container, container); |
| EXPECT_TRUE(IsScrollableAreaOrDocument(enclosing_container)); |
| } |
| |
| TEST_F(SpatialNavigationTest, ZooomPutsElementOffScreen) { |
| SetBodyInnerHTML( |
| "<!DOCTYPE html>" |
| "<button id='a'>hello</button><br>" |
| "<button id='b' style='margin-top: 70%'>bello</button>"); |
| |
| Element* a = GetDocument().getElementById("a"); |
| Element* b = GetDocument().getElementById("b"); |
| EXPECT_FALSE(IsOffscreen(a)); |
| EXPECT_FALSE(IsOffscreen(b)); |
| |
| // Now, test IsOffscreen with a pinched viewport. |
| VisualViewport& visual_viewport = GetFrame().GetPage()->GetVisualViewport(); |
| visual_viewport.SetScale(2); |
| // #b is no longer visible. |
| EXPECT_FALSE(IsOffscreen(a)); |
| EXPECT_TRUE(IsOffscreen(b)); |
| } |
| |
| TEST_F(SpatialNavigationTest, RootViewportRespectsVisibleSize) { |
| EXPECT_EQ(RootViewport(&GetFrame()), PhysicalRect(0, 0, 800, 600)); |
| |
| VisualViewport& visual_viewport = GetFrame().GetPage()->GetVisualViewport(); |
| visual_viewport.SetSize({123, 123}); |
| EXPECT_EQ(RootViewport(&GetFrame()), PhysicalRect(0, 0, 123, 123)); |
| } |
| |
| TEST_F(SpatialNavigationTest, StartAtVisibleFocusedElement) { |
| SetBodyInnerHTML("<button id='b'>hello</button>"); |
| Element* b = GetDocument().getElementById("b"); |
| |
| EXPECT_EQ(SearchOrigin(RootViewport(&GetFrame()), b, |
| SpatialNavigationDirection::kDown), |
| NodeRectInRootFrame(b)); |
| } |
| |
| TEST_F(SpatialNavigationTest, StartAtVisibleFocusedScroller) { |
| SetBodyInnerHTML( |
| "<!DOCTYPE html>" |
| "<style>" |
| " #content {" |
| " margin-top: 200px;" // Outside the div's viewport. |
| " }" |
| " #scroller {" |
| " height: 100px;" |
| " overflow: scroll;" |
| " }" |
| "</style>" |
| "<div id='scroller'>" |
| " <div id='content'>some text here</div>" |
| "</div>"); |
| |
| Element* scroller = GetDocument().getElementById("scroller"); |
| EXPECT_EQ(SearchOrigin(RootViewport(&GetFrame()), scroller, |
| SpatialNavigationDirection::kDown), |
| NodeRectInRootFrame(scroller)); |
| } |
| |
| TEST_F(SpatialNavigationTest, StartAtVisibleFocusedIframe) { |
| SetBodyInnerHTML( |
| "<!DOCTYPE html>" |
| "<style>" |
| " iframe {" |
| " width: 100px;" |
| " height: 100px;" |
| " }" |
| "</style>" |
| "<iframe id='iframe'></iframe>"); |
| |
| SetChildFrameHTML( |
| "<!DOCTYPE html>" |
| "<div>some text here</div>"); |
| |
| Element* iframe = GetDocument().getElementById("iframe"); |
| EXPECT_EQ(SearchOrigin(RootViewport(&GetFrame()), iframe, |
| SpatialNavigationDirection::kDown), |
| NodeRectInRootFrame(iframe)); |
| } |
| |
| TEST_F(SpatialNavigationTest, StartAtTopWhenGoingDownwardsWithoutFocus) { |
| EXPECT_EQ(PhysicalRect(0, -1, 111, 0), |
| SearchOrigin({0, 0, 111, 222}, nullptr, |
| SpatialNavigationDirection::kDown)); |
| |
| EXPECT_EQ(SearchOrigin(RootViewport(&GetFrame()), nullptr, |
| SpatialNavigationDirection::kDown), |
| TopOfVisualViewport()); |
| } |
| |
| TEST_F(SpatialNavigationTest, StartAtBottomWhenGoingUpwardsWithoutFocus) { |
| EXPECT_EQ( |
| PhysicalRect(0, 222 + 1, 111, 0), |
| SearchOrigin({0, 0, 111, 222}, nullptr, SpatialNavigationDirection::kUp)); |
| |
| EXPECT_EQ(SearchOrigin(RootViewport(&GetFrame()), nullptr, |
| SpatialNavigationDirection::kUp), |
| BottomOfVisualViewport()); |
| } |
| |
| TEST_F(SpatialNavigationTest, StartAtLeftSideWhenGoingEastWithoutFocus) { |
| EXPECT_EQ(PhysicalRect(-1, 0, 0, 222), |
| SearchOrigin({0, 0, 111, 222}, nullptr, |
| SpatialNavigationDirection::kRight)); |
| |
| EXPECT_EQ(SearchOrigin(RootViewport(&GetFrame()), nullptr, |
| SpatialNavigationDirection::kRight), |
| LeftSideOfVisualViewport()); |
| } |
| |
| TEST_F(SpatialNavigationTest, StartAtRightSideWhenGoingWestWithoutFocus) { |
| EXPECT_EQ(PhysicalRect(111 + 1, 0, 0, 222), |
| SearchOrigin({0, 0, 111, 222}, nullptr, |
| SpatialNavigationDirection::kLeft)); |
| |
| EXPECT_EQ(SearchOrigin(RootViewport(&GetFrame()), nullptr, |
| SpatialNavigationDirection::kLeft), |
| RightSideOfVisualViewport()); |
| } |
| |
| TEST_F(SpatialNavigationTest, |
| StartAtBottomWhenGoingUpwardsAndFocusIsOffscreen) { |
| SetBodyInnerHTML( |
| "<button id='b' style='margin-top: 120%;'>B</button>"); // Outside the |
| // visual |
| // viewport. |
| Element* b = GetDocument().getElementById("b"); |
| EXPECT_TRUE(IsOffscreen(b)); |
| |
| EXPECT_EQ(SearchOrigin(RootViewport(&GetFrame()), b, |
| SpatialNavigationDirection::kUp), |
| BottomOfVisualViewport()); |
| } |
| |
| TEST_F(SpatialNavigationTest, StartAtContainersEdge) { |
| SetBodyInnerHTML( |
| "<!DOCTYPE html>" |
| "<style>" |
| " div {" |
| " height: 100px;" |
| " width: 100px;" |
| " overflow: scroll;" |
| " }" |
| " button {" |
| " margin-top: 200px;" // Outside the div's viewport. |
| " }" |
| "</style>" |
| "<div id='container'>" |
| " <button id='b'>B</button>" |
| "</div>"); |
| |
| Element* b = GetDocument().getElementById("b"); |
| const Element* container = GetDocument().getElementById("container"); |
| const PhysicalRect container_box = NodeRectInRootFrame(container); |
| |
| // TODO(crbug.com/889840): |
| // VisibleBoundsInVisualViewport does not (yet) take div-clipping into |
| // account. The node is off screen, but nevertheless VBIVV returns a non- |
| // empty rect. If you fix VisibleBoundsInVisualViewport, change to |
| // EXPECT_TRUE here and stop using LayoutObject in IsOffscreen(). |
| EXPECT_FALSE(b->VisibleBoundsInVisualViewport().IsEmpty()); // EXPECT_TRUE. |
| EXPECT_TRUE(IsOffscreen(b)); |
| |
| // Go down. |
| PhysicalRect container_top_edge = container_box; |
| container_top_edge.SetHeight(LayoutUnit(0)); |
| container_top_edge.SetY(container_top_edge.Y() - 1); |
| EXPECT_EQ(SearchOrigin(RootViewport(&GetFrame()), b, |
| SpatialNavigationDirection::kDown), |
| container_top_edge); |
| |
| // Go up. |
| PhysicalRect container_bottom_edge = container_box; |
| container_bottom_edge.SetHeight(LayoutUnit(0)); |
| container_bottom_edge.SetY(container_bottom_edge.Right() + 1); |
| EXPECT_EQ(SearchOrigin(RootViewport(&GetFrame()), b, |
| SpatialNavigationDirection::kUp), |
| container_bottom_edge); |
| |
| // Go right. |
| PhysicalRect container_leftmost_edge = container_box; |
| container_leftmost_edge.SetWidth(LayoutUnit(0)); |
| container_leftmost_edge.SetX(container_leftmost_edge.X() - 1); |
| EXPECT_EQ(SearchOrigin(RootViewport(&GetFrame()), b, |
| SpatialNavigationDirection::kRight), |
| container_leftmost_edge); |
| |
| // Go left. |
| PhysicalRect container_rightmost_edge = container_box; |
| container_rightmost_edge.SetX(container_bottom_edge.Right() + 1); |
| container_rightmost_edge.SetWidth(LayoutUnit(0)); |
| EXPECT_EQ(SearchOrigin(RootViewport(&GetFrame()), b, |
| SpatialNavigationDirection::kLeft), |
| container_rightmost_edge); |
| } |
| |
| TEST_F(SpatialNavigationTest, |
| StartFromDocEdgeWhenFocusIsClippedInOffscreenScroller) { |
| SetBodyInnerHTML( |
| "<!DOCTYPE html>" |
| "<style>" |
| " div {" |
| " margin-top: 120%;" // Outside the visual viewport. |
| " height: 100px;" |
| " width: 100px;" |
| " overflow: scroll;" |
| " }" |
| " button {" |
| " margin-top: 300px;" // Outside the div's scrollport. |
| " }" |
| "</style>" |
| "<div id='scroller'>" |
| " <button id='b'>B</button>" |
| "</div>"); |
| |
| Element* scroller = GetDocument().getElementById("scroller"); |
| Element* b = GetDocument().getElementById("b"); |
| |
| EXPECT_TRUE(IsOffscreen(scroller)); |
| EXPECT_TRUE(IsOffscreen(b)); |
| |
| EXPECT_EQ(SearchOrigin(RootViewport(&GetFrame()), b, |
| SpatialNavigationDirection::kUp), |
| BottomOfVisualViewport()); |
| EXPECT_EQ(SearchOrigin(RootViewport(&GetFrame()), b, |
| SpatialNavigationDirection::kDown), |
| TopOfVisualViewport()); |
| } |
| |
| TEST_F(SpatialNavigationTest, |
| StartFromDocEdgeWhenFocusIsClippedInNestedOffscreenScroller) { |
| SetBodyInnerHTML( |
| "<!DOCTYPE html>" |
| "<style>" |
| " div {" |
| " margin-top: 120%;" // Outside the visual viewport. |
| " height: 100px;" |
| " width: 100px;" |
| " overflow: scroll;" |
| "}" |
| "a {" |
| " display: block;" |
| " margin-top: 300px;" |
| "}" |
| "</style>" |
| "<div id='scroller1'>" |
| " <div id='scroller2'>" |
| " <a id='link'>link</a>" |
| " </div>" |
| "</div>"); |
| |
| Element* scroller1 = GetDocument().getElementById("scroller1"); |
| Element* scroller2 = GetDocument().getElementById("scroller2"); |
| Element* link = GetDocument().getElementById("link"); |
| |
| EXPECT_TRUE(IsScrollableAreaOrDocument(scroller1)); |
| EXPECT_TRUE(IsScrollableAreaOrDocument(scroller2)); |
| EXPECT_TRUE(IsOffscreen(scroller1)); |
| EXPECT_TRUE(IsOffscreen(scroller1)); |
| EXPECT_TRUE(IsOffscreen(link)); |
| |
| AssertUseSidesOfVisualViewport(link); |
| } |
| |
| TEST_F(SpatialNavigationTest, PartiallyVisible) { |
| // <button>'s bottom is clipped. |
| SetBodyInnerHTML("<button id='b' style='height: 900px;'>B</button>"); |
| Element* b = GetDocument().getElementById("b"); |
| |
| EXPECT_FALSE(IsOffscreen(b)); // <button> is not completely offscreen. |
| |
| PhysicalRect button_in_root_frame = NodeRectInRootFrame(b); |
| |
| EXPECT_EQ(SearchOrigin(RootViewport(&GetFrame()), b, |
| SpatialNavigationDirection::kUp), |
| Intersection(button_in_root_frame, RootViewport(&GetFrame()))); |
| |
| // Do some scrolling. |
| ScrollableArea* root_scroller = GetDocument().View()->GetScrollableArea(); |
| root_scroller->SetScrollOffset(ScrollOffset(0, 600), |
| mojom::blink::ScrollType::kProgrammatic); |
| PhysicalRect button_after_scroll = NodeRectInRootFrame(b); |
| ASSERT_NE(button_in_root_frame, |
| button_after_scroll); // As we scrolled, the |
| // <button>'s position in |
| // the root frame changed. |
| |
| // <button>'s top is clipped. |
| EXPECT_FALSE(IsOffscreen(b)); // <button> is not completely offscreen. |
| EXPECT_EQ(SearchOrigin(RootViewport(&GetFrame()), b, |
| SpatialNavigationDirection::kUp), |
| Intersection(button_after_scroll, RootViewport(&GetFrame()))); |
| } |
| |
| TEST_F(SpatialNavigationTest, |
| StartFromDocEdgeWhenOffscreenIframeDisplaysFocus) { |
| SetBodyInnerHTML( |
| "<!DOCTYPE html>" |
| "<style>" |
| " iframe {" |
| " margin-top: 120%;" // Outside the visual viewport. |
| " height: 100px;" |
| " width: 100px;" |
| " }" |
| "</style>" |
| "<iframe id='iframe'></iframe>"); |
| |
| SetChildFrameHTML( |
| "<!DOCTYPE html>" |
| "<a id='link'>link</a>"); |
| |
| UpdateAllLifecyclePhases(ChildDocument().View()); |
| Element* link = ChildDocument().QuerySelector("a"); |
| Element* iframe = GetDocument().QuerySelector("iframe"); |
| |
| // The <iframe> is not displayed in the visual viewport. In other words, it is |
| // being offscreen. And so is also its content, the <a>. |
| EXPECT_TRUE(IsOffscreen(iframe)); |
| EXPECT_TRUE(IsOffscreen(&ChildDocument())); |
| EXPECT_TRUE(IsOffscreen(link)); |
| |
| AssertUseSidesOfVisualViewport(link); |
| } |
| |
| TEST_F(SpatialNavigationTest, DivsCanClipIframes) { |
| SetBodyInnerHTML( |
| "<!DOCTYPE html>" |
| "<style>" |
| " div {" |
| " height: 100px;" |
| " width: 100px;" |
| " overflow: scroll;" |
| " }" |
| " iframe {" |
| " margin-top: 200px;" // Outside the div's viewport. |
| " height: 50px;" |
| " width: 50px;" |
| " }" |
| "</style>" |
| "<div>" |
| " <iframe id='iframe'></iframe>" |
| "</div>"); |
| |
| SetChildFrameHTML( |
| "<!DOCTYPE html>" |
| "<a>link</a>"); |
| |
| UpdateAllLifecyclePhases(ChildDocument().View()); |
| Element* div = GetDocument().QuerySelector("div"); |
| Element* iframe = GetDocument().QuerySelector("iframe"); |
| Element* link = ChildDocument().QuerySelector("a"); |
| EXPECT_FALSE(IsOffscreen(div)); |
| |
| // TODO(crbug.com/889840): |
| // VisibleBoundsInVisualViewport does not (yet) take div-clipping into |
| // account. The node is off screen, but nevertheless VBIVV returns a non- |
| // empty rect. If you fix VisibleBoundsInVisualViewport, change to |
| // EXPECT_TRUE here and stop using LayoutObject in IsOffscreen(). |
| EXPECT_FALSE( |
| iframe->VisibleBoundsInVisualViewport().IsEmpty()); // EXPECT_TRUE. |
| |
| // The <iframe> is not displayed in the visual viewport because it is clipped |
| // by the div. In other words, it is being offscreen. And so is also its |
| // content, the <a>. |
| EXPECT_TRUE(IsOffscreen(iframe)); |
| EXPECT_TRUE(IsOffscreen(&ChildDocument())); |
| EXPECT_TRUE(IsOffscreen(link)); |
| } |
| |
| TEST_F(SpatialNavigationTest, PartiallyVisibleIFrame) { |
| // <a> is off screen. The <iframe> is visible, but partially off screen. |
| SetBodyInnerHTML( |
| "<!DOCTYPE html>" |
| "<style>" |
| " iframe {" |
| " width: 200%;" |
| " height: 100px;" |
| " }" |
| "</style>" |
| "<iframe id='iframe'></iframe>"); |
| |
| SetChildFrameHTML( |
| "<!DOCTYPE html>" |
| "<style>" |
| " #child {" |
| " margin-left: 120%;" |
| " }" |
| "</style>" |
| "<a id='child'>link</a>"); |
| |
| UpdateAllLifecyclePhases(ChildDocument().View()); |
| Element* child_element = ChildDocument().getElementById("child"); |
| Node* enclosing_container = ScrollableAreaOrDocumentOf(child_element); |
| EXPECT_EQ(enclosing_container, ChildDocument()); |
| |
| EXPECT_TRUE(IsOffscreen(child_element)); // Completely offscreen. |
| EXPECT_FALSE(IsOffscreen(enclosing_container)); // Partially visible. |
| |
| PhysicalRect iframe = NodeRectInRootFrame(enclosing_container); |
| |
| // When searching downwards we start at activeElement's |
| // container's (here: the iframe's) topmost visible edge. |
| EXPECT_EQ(SearchOrigin(RootViewport(&GetFrame()), child_element, |
| SpatialNavigationDirection::kDown), |
| OppositeEdge(SpatialNavigationDirection::kDown, |
| Intersection(iframe, RootViewport(&GetFrame())))); |
| |
| // When searching upwards we start at activeElement's |
| // container's (here: the iframe's) bottommost visible edge. |
| EXPECT_EQ(SearchOrigin(RootViewport(&GetFrame()), child_element, |
| SpatialNavigationDirection::kUp), |
| OppositeEdge(SpatialNavigationDirection::kUp, |
| Intersection(iframe, RootViewport(&GetFrame())))); |
| |
| // When searching eastwards, "to the right", we start at activeElement's |
| // container's (here: the iframe's) leftmost visible edge. |
| EXPECT_EQ(SearchOrigin(RootViewport(&GetFrame()), child_element, |
| SpatialNavigationDirection::kRight), |
| OppositeEdge(SpatialNavigationDirection::kRight, |
| Intersection(iframe, RootViewport(&GetFrame())))); |
| |
| // When searching westwards, "to the left", we start at activeElement's |
| // container's (here: the iframe's) rightmost visible edge. |
| EXPECT_EQ(SearchOrigin(RootViewport(&GetFrame()), child_element, |
| SpatialNavigationDirection::kLeft), |
| OppositeEdge(SpatialNavigationDirection::kLeft, |
| Intersection(iframe, RootViewport(&GetFrame())))); |
| } |
| |
| TEST_F(SpatialNavigationTest, BottomOfPinchedViewport) { |
| PhysicalRect origin = SearchOrigin(RootViewport(&GetFrame()), nullptr, |
| SpatialNavigationDirection::kUp); |
| EXPECT_EQ(origin.Height(), 0); |
| EXPECT_EQ(origin.Width(), GetFrame().View()->Width()); |
| EXPECT_EQ(origin.X(), 0); |
| EXPECT_EQ(origin.Y(), GetFrame().View()->Height() + 1); |
| EXPECT_EQ(origin, BottomOfVisualViewport()); |
| |
| // Now, test SearchOrigin with a pinched viewport. |
| VisualViewport& visual_viewport = GetFrame().GetPage()->GetVisualViewport(); |
| visual_viewport.SetScale(2); |
| visual_viewport.SetLocation(FloatPoint(200, 200)); |
| origin = SearchOrigin(RootViewport(&GetFrame()), nullptr, |
| SpatialNavigationDirection::kUp); |
| EXPECT_EQ(origin.Height(), 0); |
| EXPECT_LT(origin.Width(), GetFrame().View()->Width()); |
| EXPECT_GT(origin.X(), 0); |
| EXPECT_LT(origin.Y(), GetFrame().View()->Height() + 1); |
| EXPECT_EQ(origin, BottomOfVisualViewport()); |
| } |
| |
| TEST_F(SpatialNavigationTest, StraightTextNoFragments) { |
| LoadAhem(); |
| SetBodyInnerHTML( |
| "<!DOCTYPE html>" |
| "<style>" |
| " body {font: 10px/10px Ahem; width: 500px}" |
| "</style>" |
| "<a href='#' id='a'>blaaaaa blaaaaa blaaaaa</a>"); |
| Element* a = GetDocument().getElementById("a"); |
| EXPECT_FALSE(IsFragmentedInline(*a)); |
| } |
| |
| TEST_F(SpatialNavigationTest, LineBrokenTextHasFragments) { |
| LoadAhem(); |
| SetBodyInnerHTML( |
| "<!DOCTYPE html>" |
| "<style>" |
| " body {font: 10px/10px Ahem; width: 40px}" |
| "</style>" |
| "<a href='#' id='a'>blaaaaa blaaaaa blaaaaa</a>"); |
| Element* a = GetDocument().getElementById("a"); |
| EXPECT_TRUE(IsFragmentedInline(*a)); |
| } |
| |
| TEST_F(SpatialNavigationTest, ManyClientRectsButNotLineBrokenText) { |
| SetBodyInnerHTML( |
| "<!DOCTYPE html>" |
| "<style>" |
| " div {width: 20px; height: 20px;}" |
| "</style>" |
| "<a href='#' id='a'><div></div></a>"); |
| Element* a = GetDocument().getElementById("a"); |
| EXPECT_FALSE(IsFragmentedInline(*a)); |
| } |
| |
| TEST_F(SpatialNavigationTest, UseTheFirstFragment) { |
| LoadAhem(); |
| SetBodyInnerHTML( |
| "<!DOCTYPE html>" |
| "<style>" |
| " body {font: 10px/10px Ahem; margin: 0; width: 50px;}" |
| "</style>" |
| "<a href='#' id='a'>12345 12</a>"); |
| Element* a = GetDocument().getElementById("a"); |
| EXPECT_TRUE(IsFragmentedInline(*a)); |
| |
| // Search downards. |
| PhysicalRect origin_down = SearchOrigin(RootViewport(&GetFrame()), a, |
| SpatialNavigationDirection::kDown); |
| PhysicalRect origin_fragment = |
| SearchOriginFragment(NodeRectInRootFrame(a), *a->GetLayoutObject(), |
| SpatialNavigationDirection::kDown); |
| EXPECT_EQ(origin_down, origin_fragment); |
| EXPECT_EQ(origin_down.Height(), 10); |
| EXPECT_EQ(origin_down.Width(), 50); |
| EXPECT_EQ(origin_down.X(), 0); |
| EXPECT_EQ(origin_down.Y(), 0); |
| |
| // Search upwards. |
| PhysicalRect origin_up = SearchOrigin(RootViewport(&GetFrame()), a, |
| SpatialNavigationDirection::kUp); |
| PhysicalRect origin_fragment_up = |
| SearchOriginFragment(NodeRectInRootFrame(a), *a->GetLayoutObject(), |
| SpatialNavigationDirection::kUp); |
| EXPECT_EQ(origin_up, origin_fragment_up); |
| EXPECT_EQ(origin_up.Height(), 10); |
| EXPECT_EQ(origin_up.Width(), 20); |
| EXPECT_EQ(origin_up.X(), 0); |
| EXPECT_EQ(origin_up.Y(), 10); |
| |
| // Search from the top fragment. |
| PhysicalRect origin_left = SearchOrigin(RootViewport(&GetFrame()), a, |
| SpatialNavigationDirection::kLeft); |
| EXPECT_EQ(origin_left, origin_down); |
| |
| // Search from the bottom fragment. |
| PhysicalRect origin_right = SearchOrigin(RootViewport(&GetFrame()), a, |
| SpatialNavigationDirection::kRight); |
| EXPECT_EQ(origin_right, origin_up); |
| } |
| |
| TEST_F(SpatialNavigationTest, TopOfPinchedViewport) { |
| PhysicalRect origin = SearchOrigin(RootViewport(&GetFrame()), nullptr, |
| SpatialNavigationDirection::kDown); |
| EXPECT_EQ(origin.Height(), 0); |
| EXPECT_EQ(origin.Width(), GetFrame().View()->Width()); |
| EXPECT_EQ(origin.X(), 0); |
| EXPECT_EQ(origin.Y(), -1); |
| EXPECT_EQ(origin, TopOfVisualViewport()); |
| |
| // Now, test SearchOrigin with a pinched viewport. |
| VisualViewport& visual_viewport = GetFrame().GetPage()->GetVisualViewport(); |
| visual_viewport.SetScale(2); |
| visual_viewport.SetLocation(FloatPoint(200, 200)); |
| origin = SearchOrigin(RootViewport(&GetFrame()), nullptr, |
| SpatialNavigationDirection::kDown); |
| EXPECT_EQ(origin.Height(), 0); |
| EXPECT_LT(origin.Width(), GetFrame().View()->Width()); |
| EXPECT_GT(origin.X(), 0); |
| EXPECT_GT(origin.Y(), -1); |
| EXPECT_EQ(origin, TopOfVisualViewport()); |
| } |
| |
| TEST_F(SpatialNavigationTest, HasRemoteFrame) { |
| frame_test_helpers::WebViewHelper helper; |
| helper.InitializeAndLoad("about:blank"); |
| |
| WebViewImpl* webview = helper.GetWebView(); |
| WebURL base_url = url_test_helpers::ToKURL("http://www.test.com/"); |
| frame_test_helpers::LoadHTMLString(webview->MainFrameImpl(), |
| "<!DOCTYPE html>" |
| "<iframe id='iframe'></iframe>", |
| base_url); |
| |
| webview->ResizeWithBrowserControls(IntSize(400, 400), 50, 0, false); |
| UpdateAllLifecyclePhases(webview->MainFrameImpl()->GetFrame()->View()); |
| |
| Element* iframe = |
| webview->MainFrameImpl()->GetFrame()->GetDocument()->getElementById( |
| "iframe"); |
| EXPECT_FALSE(HasRemoteFrame(iframe)); |
| |
| webview->MainFrameImpl()->FirstChild()->Swap( |
| frame_test_helpers::CreateRemote()); |
| EXPECT_TRUE(HasRemoteFrame(iframe)); |
| } |
| |
| class SpatialNavigationWithFocuslessModeTest |
| : public SpatialNavigationTest, |
| public ::testing::WithParamInterface<bool> { |
| public: |
| SpatialNavigationWithFocuslessModeTest() : use_focusless_mode_(GetParam()) {} |
| |
| void SetUp() override { |
| SpatialNavigationTest::SetUp(); |
| GetDocument().GetSettings()->SetSpatialNavigationEnabled(true); |
| } |
| |
| private: |
| ScopedFocuslessSpatialNavigationForTest use_focusless_mode_; |
| }; |
| |
| INSTANTIATE_TEST_SUITE_P(All, |
| SpatialNavigationWithFocuslessModeTest, |
| ::testing::Bool()); |
| |
| TEST_P(SpatialNavigationWithFocuslessModeTest, PressEnterKeyActiveElement) { |
| SetBodyInnerHTML("<button id='b'>hello</button>"); |
| |
| Element* b = GetDocument().getElementById("b"); |
| |
| // Move interest to button. |
| WebKeyboardEvent arrow_down{WebInputEvent::kRawKeyDown, |
| WebInputEvent::kNoModifiers, |
| WebInputEvent::GetStaticTimeStampForTests()}; |
| arrow_down.dom_key = ui::DomKey::ARROW_DOWN; |
| GetDocument().GetFrame()->GetEventHandler().KeyEvent(arrow_down); |
| |
| arrow_down.SetType(WebInputEvent::kKeyUp); |
| GetDocument().GetFrame()->GetEventHandler().KeyEvent(arrow_down); |
| |
| EXPECT_FALSE(b->IsActive()); |
| |
| // Enter key down add :active state to element. |
| WebKeyboardEvent enter{WebInputEvent::kRawKeyDown, |
| WebInputEvent::kNoModifiers, |
| WebInputEvent::GetStaticTimeStampForTests()}; |
| enter.dom_key = ui::DomKey::ENTER; |
| GetDocument().GetFrame()->GetEventHandler().KeyEvent(enter); |
| EXPECT_TRUE(b->IsActive()); |
| |
| // Enter key up remove :active state to element. |
| enter.SetType(WebInputEvent::kKeyUp); |
| GetDocument().GetFrame()->GetEventHandler().KeyEvent(enter); |
| EXPECT_FALSE(b->IsActive()); |
| } |
| |
| class FocuslessSpatialNavigationSimTest : public SimTest { |
| public: |
| FocuslessSpatialNavigationSimTest() : use_focusless_mode_(true) {} |
| |
| void SetUp() override { |
| SimTest::SetUp(); |
| WebView().GetPage()->GetSettings().SetSpatialNavigationEnabled(true); |
| } |
| |
| void SimulateKeyPress(int dom_key) { |
| WebKeyboardEvent event{WebInputEvent::kRawKeyDown, |
| WebInputEvent::kNoModifiers, |
| WebInputEvent::GetStaticTimeStampForTests()}; |
| event.dom_key = dom_key; |
| WebView().MainFrameWidget()->HandleInputEvent( |
| WebCoalescedInputEvent(event)); |
| |
| if (dom_key == ui::DomKey::ENTER) { |
| event.SetType(WebInputEvent::kChar); |
| WebView().MainFrameWidget()->HandleInputEvent( |
| WebCoalescedInputEvent(event)); |
| } |
| |
| event.SetType(WebInputEvent::kKeyUp); |
| WebView().MainFrameWidget()->HandleInputEvent( |
| WebCoalescedInputEvent(event)); |
| } |
| |
| ScopedFocuslessSpatialNavigationForTest use_focusless_mode_; |
| }; |
| |
| // Tests that opening a <select> popup works by pressing enter from |
| // "interested" mode, without being focused. |
| TEST_F(FocuslessSpatialNavigationSimTest, OpenSelectPopup) { |
| // This test requires PagePopup since we're testing opening the <select> drop |
| // down so skip this test on platforms (i.e. Android) that don't use this. |
| if (!RuntimeEnabledFeatures::PagePopupEnabled()) |
| return; |
| |
| WebView().MainFrameWidget()->Resize(WebSize(800, 600)); |
| WebView().MainFrameWidget()->SetFocus(true); |
| WebView().SetIsActive(true); |
| |
| SimRequest request("https://example.com/test.html", "text/html"); |
| LoadURL("https://example.com/test.html"); |
| request.Complete(R"HTML( |
| <!DOCTYPE html> |
| <select id="target"> |
| <option>A</option> |
| <option>B</option> |
| <option>C</option> |
| </select> |
| )HTML"); |
| Compositor().BeginFrame(); |
| |
| auto* select = To<HTMLSelectElement>(GetDocument().getElementById("target")); |
| SimulateKeyPress(ui::DomKey::ARROW_DOWN); |
| |
| SpatialNavigationController& spat_nav_controller = |
| GetDocument().GetPage()->GetSpatialNavigationController(); |
| |
| ASSERT_EQ(select, spat_nav_controller.GetInterestedElement()); |
| ASSERT_NE(select, GetDocument().ActiveElement()); |
| ASSERT_FALSE(select->PopupIsVisible()); |
| |
| // The enter key should cause the popup to open. |
| SimulateKeyPress(ui::DomKey::ENTER); |
| EXPECT_TRUE(select->PopupIsVisible()); |
| } |
| |
| } // namespace blink |