| // Copyright 2016 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 "core/frame/FrameHost.h" |
| #include "core/frame/FrameView.h" |
| #include "core/frame/RootFrameViewport.h" |
| #include "core/frame/TopControls.h" |
| #include "core/html/HTMLFrameOwnerElement.h" |
| #include "core/layout/LayoutBox.h" |
| #include "core/layout/api/LayoutViewItem.h" |
| #include "core/layout/compositing/PaintLayerCompositor.h" |
| #include "core/page/Page.h" |
| #include "core/page/scrolling/RootScrollerController.h" |
| #include "core/page/scrolling/TopDocumentRootScrollerController.h" |
| #include "core/paint/PaintLayerScrollableArea.h" |
| #include "platform/testing/URLTestHelpers.h" |
| #include "platform/testing/UnitTestHelpers.h" |
| #include "public/platform/Platform.h" |
| #include "public/platform/WebURLLoaderMockFactory.h" |
| #include "public/web/WebCache.h" |
| #include "public/web/WebConsoleMessage.h" |
| #include "public/web/WebRemoteFrame.h" |
| #include "public/web/WebScriptSource.h" |
| #include "public/web/WebSettings.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| #include "web/WebLocalFrameImpl.h" |
| #include "web/WebRemoteFrameImpl.h" |
| #include "web/tests/FrameTestHelpers.h" |
| #include "wtf/Vector.h" |
| |
| using blink::testing::runPendingTasks; |
| using testing::Mock; |
| |
| namespace blink { |
| |
| namespace { |
| |
| class RootScrollerTest : public ::testing::Test { |
| public: |
| RootScrollerTest() : m_baseURL("http://www.test.com/") { |
| registerMockedHttpURLLoad("overflow-scrolling.html"); |
| registerMockedHttpURLLoad("root-scroller.html"); |
| registerMockedHttpURLLoad("root-scroller-iframe.html"); |
| registerMockedHttpURLLoad("root-scroller-child.html"); |
| } |
| |
| ~RootScrollerTest() override { |
| m_featuresBackup.restore(); |
| Platform::current()->getURLLoaderMockFactory()->unregisterAllURLs(); |
| WebCache::clear(); |
| } |
| |
| WebViewImpl* initialize(const std::string& pageName, |
| FrameTestHelpers::TestWebViewClient* client) { |
| RuntimeEnabledFeatures::setSetRootScrollerEnabled(true); |
| |
| m_helper.initializeAndLoad(m_baseURL + pageName, true, nullptr, client, |
| nullptr, &configureSettings); |
| |
| // Initialize top controls to be shown. |
| webViewImpl()->resizeWithTopControls(IntSize(400, 400), 50, true); |
| webViewImpl()->topControls().setShownRatio(1); |
| |
| mainFrameView()->updateAllLifecyclePhases(); |
| |
| return webViewImpl(); |
| } |
| |
| WebViewImpl* initialize(const std::string& pageName) { |
| return initialize(pageName, &m_client); |
| } |
| |
| static void configureSettings(WebSettings* settings) { |
| settings->setJavaScriptEnabled(true); |
| settings->setAcceleratedCompositingEnabled(true); |
| settings->setPreferCompositingToLCDTextEnabled(true); |
| // Android settings. |
| settings->setViewportEnabled(true); |
| settings->setViewportMetaEnabled(true); |
| settings->setShrinksViewportContentToFit(true); |
| settings->setMainFrameResizesAreOrientationChanges(true); |
| } |
| |
| void registerMockedHttpURLLoad(const std::string& fileName) { |
| URLTestHelpers::registerMockedURLFromBaseURL( |
| WebString::fromUTF8(m_baseURL.c_str()), |
| WebString::fromUTF8(fileName.c_str())); |
| } |
| |
| void executeScript(const WebString& code) { |
| mainWebFrame()->executeScript(WebScriptSource(code)); |
| mainWebFrame()->view()->updateAllLifecyclePhases(); |
| runPendingTasks(); |
| } |
| |
| WebViewImpl* webViewImpl() const { return m_helper.webView(); } |
| |
| FrameHost& frameHost() const { |
| return m_helper.webView()->page()->frameHost(); |
| } |
| |
| LocalFrame* mainFrame() const { |
| return webViewImpl()->mainFrameImpl()->frame(); |
| } |
| |
| WebLocalFrame* mainWebFrame() const { return webViewImpl()->mainFrameImpl(); } |
| |
| FrameView* mainFrameView() const { |
| return webViewImpl()->mainFrameImpl()->frame()->view(); |
| } |
| |
| VisualViewport& visualViewport() const { |
| return frameHost().visualViewport(); |
| } |
| |
| TopControls& topControls() const { return frameHost().topControls(); } |
| |
| Element* effectiveRootScroller(Document* doc) const { |
| return doc->rootScrollerController()->effectiveRootScroller(); |
| } |
| |
| WebGestureEvent generateTouchGestureEvent(WebInputEvent::Type type, |
| int deltaX = 0, |
| int deltaY = 0) { |
| return generateGestureEvent(type, WebGestureDeviceTouchscreen, deltaX, |
| deltaY); |
| } |
| |
| WebGestureEvent generateWheelGestureEvent(WebInputEvent::Type type, |
| int deltaX = 0, |
| int deltaY = 0) { |
| return generateGestureEvent(type, WebGestureDeviceTouchpad, deltaX, deltaY); |
| } |
| |
| protected: |
| WebGestureEvent generateGestureEvent(WebInputEvent::Type type, |
| WebGestureDevice device, |
| int deltaX, |
| int deltaY) { |
| WebGestureEvent event; |
| event.type = type; |
| event.sourceDevice = device; |
| event.x = 100; |
| event.y = 100; |
| if (type == WebInputEvent::GestureScrollUpdate) { |
| event.data.scrollUpdate.deltaX = deltaX; |
| event.data.scrollUpdate.deltaY = deltaY; |
| } |
| return event; |
| } |
| |
| std::string m_baseURL; |
| FrameTestHelpers::TestWebViewClient m_client; |
| FrameTestHelpers::WebViewHelper m_helper; |
| RuntimeEnabledFeatures::Backup m_featuresBackup; |
| }; |
| |
| // Test that no root scroller element is set if setRootScroller isn't called on |
| // any elements. The document element should be the default effective root |
| // scroller. |
| TEST_F(RootScrollerTest, TestDefaultRootScroller) { |
| initialize("overflow-scrolling.html"); |
| |
| ASSERT_EQ(nullptr, mainFrame()->document()->rootScroller()); |
| |
| Element* htmlElement = mainFrame()->document()->documentElement(); |
| EXPECT_EQ(htmlElement, effectiveRootScroller(mainFrame()->document())); |
| } |
| |
| class OverscrollTestWebViewClient : public FrameTestHelpers::TestWebViewClient { |
| public: |
| MOCK_METHOD4(didOverscroll, |
| void(const WebFloatSize&, |
| const WebFloatSize&, |
| const WebFloatPoint&, |
| const WebFloatSize&)); |
| }; |
| |
| // Tests that setting an element as the root scroller causes it to control url |
| // bar hiding and overscroll. |
| TEST_F(RootScrollerTest, TestSetRootScroller) { |
| OverscrollTestWebViewClient client; |
| initialize("root-scroller.html", &client); |
| |
| Element* container = mainFrame()->document()->getElementById("container"); |
| TrackExceptionState exceptionState; |
| mainFrame()->document()->setRootScroller(container, exceptionState); |
| ASSERT_EQ(container, mainFrame()->document()->rootScroller()); |
| |
| // Content is 1000x1000, WebView size is 400x400 so max scroll is 600px. |
| double maximumScroll = 600; |
| |
| webViewImpl()->handleInputEvent( |
| generateTouchGestureEvent(WebInputEvent::GestureScrollBegin)); |
| |
| { |
| // Scrolling over the #container DIV should cause the top controls to |
| // hide. |
| EXPECT_FLOAT_EQ(1, topControls().shownRatio()); |
| webViewImpl()->handleInputEvent(generateTouchGestureEvent( |
| WebInputEvent::GestureScrollUpdate, 0, -topControls().height())); |
| EXPECT_FLOAT_EQ(0, topControls().shownRatio()); |
| } |
| |
| { |
| // Make sure we're actually scrolling the DIV and not the FrameView. |
| webViewImpl()->handleInputEvent( |
| generateTouchGestureEvent(WebInputEvent::GestureScrollUpdate, 0, -100)); |
| EXPECT_FLOAT_EQ(100, container->scrollTop()); |
| EXPECT_FLOAT_EQ(0, mainFrameView()->scrollPositionDouble().y()); |
| } |
| |
| { |
| // Scroll 50 pixels past the end. Ensure we report the 50 pixels as |
| // overscroll. |
| EXPECT_CALL(client, didOverscroll(WebFloatSize(0, 50), WebFloatSize(0, 50), |
| WebFloatPoint(100, 100), WebFloatSize())); |
| webViewImpl()->handleInputEvent( |
| generateTouchGestureEvent(WebInputEvent::GestureScrollUpdate, 0, -550)); |
| EXPECT_FLOAT_EQ(maximumScroll, container->scrollTop()); |
| EXPECT_FLOAT_EQ(0, mainFrameView()->scrollPositionDouble().y()); |
| Mock::VerifyAndClearExpectations(&client); |
| } |
| |
| { |
| // Continue the gesture overscroll. |
| EXPECT_CALL(client, didOverscroll(WebFloatSize(0, 20), WebFloatSize(0, 70), |
| WebFloatPoint(100, 100), WebFloatSize())); |
| webViewImpl()->handleInputEvent( |
| generateTouchGestureEvent(WebInputEvent::GestureScrollUpdate, 0, -20)); |
| EXPECT_FLOAT_EQ(maximumScroll, container->scrollTop()); |
| EXPECT_FLOAT_EQ(0, mainFrameView()->scrollPositionDouble().y()); |
| Mock::VerifyAndClearExpectations(&client); |
| } |
| |
| webViewImpl()->handleInputEvent( |
| generateTouchGestureEvent(WebInputEvent::GestureScrollEnd)); |
| |
| { |
| // Make sure a new gesture scroll still won't scroll the frameview and |
| // overscrolls. |
| webViewImpl()->handleInputEvent( |
| generateTouchGestureEvent(WebInputEvent::GestureScrollBegin)); |
| |
| EXPECT_CALL(client, didOverscroll(WebFloatSize(0, 30), WebFloatSize(0, 30), |
| WebFloatPoint(100, 100), WebFloatSize())); |
| webViewImpl()->handleInputEvent( |
| generateTouchGestureEvent(WebInputEvent::GestureScrollUpdate, 0, -30)); |
| EXPECT_FLOAT_EQ(maximumScroll, container->scrollTop()); |
| EXPECT_FLOAT_EQ(0, mainFrameView()->scrollPositionDouble().y()); |
| Mock::VerifyAndClearExpectations(&client); |
| |
| webViewImpl()->handleInputEvent( |
| generateTouchGestureEvent(WebInputEvent::GestureScrollEnd)); |
| } |
| |
| { |
| // Scrolling up should show the top controls. |
| webViewImpl()->handleInputEvent( |
| generateTouchGestureEvent(WebInputEvent::GestureScrollBegin)); |
| |
| EXPECT_FLOAT_EQ(0, topControls().shownRatio()); |
| webViewImpl()->handleInputEvent( |
| generateTouchGestureEvent(WebInputEvent::GestureScrollUpdate, 0, 30)); |
| EXPECT_FLOAT_EQ(0.6, topControls().shownRatio()); |
| |
| webViewImpl()->handleInputEvent( |
| generateTouchGestureEvent(WebInputEvent::GestureScrollEnd)); |
| } |
| |
| // Reset manually to avoid lifetime issues with custom WebViewClient. |
| m_helper.reset(); |
| } |
| |
| // Tests that removing the element that is the root scroller from the DOM tree |
| // doesn't remove it as the root scroller but it does change the effective root |
| // scroller. |
| TEST_F(RootScrollerTest, TestRemoveRootScrollerFromDom) { |
| initialize("root-scroller.html"); |
| |
| ASSERT_EQ(nullptr, mainFrame()->document()->rootScroller()); |
| |
| Element* container = mainFrame()->document()->getElementById("container"); |
| TrackExceptionState exceptionState; |
| mainFrame()->document()->setRootScroller(container, exceptionState); |
| |
| EXPECT_EQ(container, mainFrame()->document()->rootScroller()); |
| EXPECT_EQ(container, effectiveRootScroller(mainFrame()->document())); |
| |
| mainFrame()->document()->body()->removeChild(container); |
| mainFrameView()->updateAllLifecyclePhases(); |
| |
| EXPECT_EQ(container, mainFrame()->document()->rootScroller()); |
| EXPECT_NE(container, effectiveRootScroller(mainFrame()->document())); |
| } |
| |
| // Tests that setting an element that isn't a valid scroller as the root |
| // scroller doesn't change the effective root scroller. |
| TEST_F(RootScrollerTest, TestSetRootScrollerOnInvalidElement) { |
| initialize("root-scroller.html"); |
| |
| { |
| // Set to a non-block element. Should be rejected and a console message |
| // logged. |
| Element* element = mainFrame()->document()->getElementById("nonBlock"); |
| TrackExceptionState exceptionState; |
| mainFrame()->document()->setRootScroller(element, exceptionState); |
| mainFrameView()->updateAllLifecyclePhases(); |
| EXPECT_EQ(element, mainFrame()->document()->rootScroller()); |
| EXPECT_NE(element, effectiveRootScroller(mainFrame()->document())); |
| } |
| |
| { |
| // Set to an element with no size. |
| Element* element = mainFrame()->document()->getElementById("empty"); |
| TrackExceptionState exceptionState; |
| mainFrame()->document()->setRootScroller(element, exceptionState); |
| mainFrameView()->updateAllLifecyclePhases(); |
| EXPECT_EQ(element, mainFrame()->document()->rootScroller()); |
| EXPECT_NE(element, effectiveRootScroller(mainFrame()->document())); |
| } |
| } |
| |
| // Test that the effective root scroller resets to the default element when the |
| // current root scroller element becomes invalid as a scroller. |
| TEST_F(RootScrollerTest, TestRootScrollerBecomesInvalid) { |
| initialize("root-scroller.html"); |
| |
| Element* htmlElement = mainFrame()->document()->documentElement(); |
| Element* container = mainFrame()->document()->getElementById("container"); |
| TrackExceptionState exceptionState; |
| |
| ASSERT_EQ(nullptr, mainFrame()->document()->rootScroller()); |
| ASSERT_EQ(htmlElement, effectiveRootScroller(mainFrame()->document())); |
| |
| { |
| mainFrame()->document()->setRootScroller(container, exceptionState); |
| mainFrameView()->updateAllLifecyclePhases(); |
| |
| EXPECT_EQ(container, mainFrame()->document()->rootScroller()); |
| EXPECT_EQ(container, effectiveRootScroller(mainFrame()->document())); |
| |
| executeScript( |
| "document.querySelector('#container').style.display = 'inline'"); |
| mainFrameView()->updateAllLifecyclePhases(); |
| |
| EXPECT_EQ(container, mainFrame()->document()->rootScroller()); |
| EXPECT_EQ(htmlElement, effectiveRootScroller(mainFrame()->document())); |
| } |
| |
| executeScript("document.querySelector('#container').style.display = 'block'"); |
| mainFrame()->document()->setRootScroller(nullptr, exceptionState); |
| mainFrameView()->updateAllLifecyclePhases(); |
| EXPECT_EQ(nullptr, mainFrame()->document()->rootScroller()); |
| EXPECT_EQ(htmlElement, effectiveRootScroller(mainFrame()->document())); |
| |
| { |
| mainFrame()->document()->setRootScroller(container, exceptionState); |
| mainFrameView()->updateAllLifecyclePhases(); |
| |
| EXPECT_EQ(container, mainFrame()->document()->rootScroller()); |
| EXPECT_EQ(container, effectiveRootScroller(mainFrame()->document())); |
| |
| executeScript("document.querySelector('#container').style.width = '98%'"); |
| mainFrameView()->updateAllLifecyclePhases(); |
| |
| EXPECT_EQ(container, mainFrame()->document()->rootScroller()); |
| EXPECT_EQ(htmlElement, effectiveRootScroller(mainFrame()->document())); |
| } |
| } |
| |
| // Tests that setting the root scroller of the top document to an element that |
| // belongs to a nested document works. |
| TEST_F(RootScrollerTest, TestSetRootScrollerOnElementInIframe) { |
| initialize("root-scroller-iframe.html"); |
| |
| ASSERT_EQ(nullptr, mainFrame()->document()->rootScroller()); |
| |
| { |
| // Trying to set an element from a nested document should fail. |
| HTMLFrameOwnerElement* iframe = toHTMLFrameOwnerElement( |
| mainFrame()->document()->getElementById("iframe")); |
| Element* innerContainer = |
| iframe->contentDocument()->getElementById("container"); |
| |
| TrackExceptionState exceptionState; |
| mainFrame()->document()->setRootScroller(innerContainer, exceptionState); |
| mainFrameView()->updateAllLifecyclePhases(); |
| |
| EXPECT_EQ(innerContainer, mainFrame()->document()->rootScroller()); |
| EXPECT_EQ(innerContainer, effectiveRootScroller(mainFrame()->document())); |
| } |
| |
| { |
| // Setting the iframe itself should also work. |
| HTMLFrameOwnerElement* iframe = toHTMLFrameOwnerElement( |
| mainFrame()->document()->getElementById("iframe")); |
| |
| TrackExceptionState exceptionState; |
| mainFrame()->document()->setRootScroller(iframe, exceptionState); |
| mainFrameView()->updateAllLifecyclePhases(); |
| |
| EXPECT_EQ(iframe, mainFrame()->document()->rootScroller()); |
| EXPECT_EQ(iframe, effectiveRootScroller(mainFrame()->document())); |
| } |
| } |
| |
| // Tests that setting a valid element as the root scroller on a document within |
| // an iframe works as expected. |
| TEST_F(RootScrollerTest, TestRootScrollerWithinIframe) { |
| initialize("root-scroller-iframe.html"); |
| |
| ASSERT_EQ(nullptr, mainFrame()->document()->rootScroller()); |
| |
| { |
| HTMLFrameOwnerElement* iframe = toHTMLFrameOwnerElement( |
| mainFrame()->document()->getElementById("iframe")); |
| |
| EXPECT_EQ(iframe->contentDocument()->documentElement(), |
| effectiveRootScroller(iframe->contentDocument())); |
| |
| Element* innerContainer = |
| iframe->contentDocument()->getElementById("container"); |
| TrackExceptionState exceptionState; |
| iframe->contentDocument()->setRootScroller(innerContainer, exceptionState); |
| mainFrameView()->updateAllLifecyclePhases(); |
| |
| EXPECT_EQ(innerContainer, iframe->contentDocument()->rootScroller()); |
| EXPECT_EQ(innerContainer, effectiveRootScroller(iframe->contentDocument())); |
| } |
| } |
| |
| // Tests that setting an iframe as the root scroller makes the iframe the |
| // effective root scroller in the parent frame. |
| TEST_F(RootScrollerTest, SetRootScrollerIframeBecomesEffective) { |
| initialize("root-scroller-iframe.html"); |
| ASSERT_EQ(nullptr, mainFrame()->document()->rootScroller()); |
| |
| { |
| NonThrowableExceptionState nonThrow; |
| |
| // Try to set the root scroller in the main frame to be the iframe |
| // element. |
| HTMLFrameOwnerElement* iframe = toHTMLFrameOwnerElement( |
| mainFrame()->document()->getElementById("iframe")); |
| |
| mainFrame()->document()->setRootScroller(iframe, nonThrow); |
| |
| EXPECT_EQ(iframe, mainFrame()->document()->rootScroller()); |
| EXPECT_EQ(iframe, mainFrame() |
| ->document() |
| ->rootScrollerController() |
| ->effectiveRootScroller()); |
| |
| Element* container = iframe->contentDocument()->getElementById("container"); |
| |
| iframe->contentDocument()->setRootScroller(container, nonThrow); |
| |
| EXPECT_EQ(container, iframe->contentDocument()->rootScroller()); |
| EXPECT_EQ(container, iframe->contentDocument() |
| ->rootScrollerController() |
| ->effectiveRootScroller()); |
| EXPECT_EQ(iframe, mainFrame()->document()->rootScroller()); |
| EXPECT_EQ(iframe, mainFrame() |
| ->document() |
| ->rootScrollerController() |
| ->effectiveRootScroller()); |
| } |
| } |
| |
| // Tests that the global root scroller is correctly calculated when getting the |
| // root scroller layer and that the viewport apply scroll is set on it. |
| TEST_F(RootScrollerTest, SetRootScrollerIframeUsesCorrectLayerAndCallback) { |
| // TODO(bokan): The expectation and actual in the checks here are backwards. |
| initialize("root-scroller-iframe.html"); |
| ASSERT_EQ(nullptr, mainFrame()->document()->rootScroller()); |
| |
| HTMLFrameOwnerElement* iframe = toHTMLFrameOwnerElement( |
| mainFrame()->document()->getElementById("iframe")); |
| Element* container = iframe->contentDocument()->getElementById("container"); |
| |
| const TopDocumentRootScrollerController& mainController = |
| mainFrame()->document()->frameHost()->globalRootScrollerController(); |
| |
| NonThrowableExceptionState nonThrow; |
| |
| // No root scroller set, the documentElement should be the effective root |
| // and the main FrameView's scroll layer should be the layer to use. |
| { |
| EXPECT_EQ(mainController.rootScrollerLayer(), |
| mainFrameView()->layerForScrolling()); |
| EXPECT_TRUE(mainController.isViewportScrollCallback( |
| mainFrame()->document()->documentElement()->getApplyScroll())); |
| } |
| |
| // Set a root scroller in the iframe. Since the main document didn't set a |
| // root scroller, the global root scroller shouldn't change. |
| { |
| iframe->contentDocument()->setRootScroller(container, nonThrow); |
| mainFrameView()->updateAllLifecyclePhases(); |
| |
| EXPECT_EQ(mainController.rootScrollerLayer(), |
| mainFrameView()->layerForScrolling()); |
| EXPECT_TRUE(mainController.isViewportScrollCallback( |
| mainFrame()->document()->documentElement()->getApplyScroll())); |
| } |
| |
| // Setting the iframe as the root scroller in the main frame should now |
| // link the root scrollers so the container should now be the global root |
| // scroller. |
| { |
| mainFrame()->document()->setRootScroller(iframe, nonThrow); |
| mainFrameView()->updateAllLifecyclePhases(); |
| |
| ScrollableArea* containerScroller = |
| static_cast<PaintInvalidationCapableScrollableArea*>( |
| toLayoutBox(container->layoutObject())->getScrollableArea()); |
| |
| EXPECT_EQ(mainController.rootScrollerLayer(), |
| containerScroller->layerForScrolling()); |
| EXPECT_FALSE(mainController.isViewportScrollCallback( |
| mainFrame()->document()->documentElement()->getApplyScroll())); |
| EXPECT_TRUE( |
| mainController.isViewportScrollCallback(container->getApplyScroll())); |
| } |
| |
| // Unsetting the root scroller in the iframe should reset its effective |
| // root scroller to the iframe's documentElement and thus the iframe's |
| // documentElement becomes the global root scroller. |
| { |
| iframe->contentDocument()->setRootScroller(nullptr, nonThrow); |
| mainFrameView()->updateAllLifecyclePhases(); |
| EXPECT_EQ(mainController.rootScrollerLayer(), |
| iframe->contentDocument()->view()->layerForScrolling()); |
| EXPECT_FALSE( |
| mainController.isViewportScrollCallback(container->getApplyScroll())); |
| EXPECT_FALSE(mainController.isViewportScrollCallback( |
| mainFrame()->document()->documentElement()->getApplyScroll())); |
| EXPECT_TRUE(mainController.isViewportScrollCallback( |
| iframe->contentDocument()->documentElement()->getApplyScroll())); |
| } |
| |
| // Finally, unsetting the main frame's root scroller should reset it to the |
| // documentElement and corresponding layer. |
| { |
| mainFrame()->document()->setRootScroller(nullptr, nonThrow); |
| mainFrameView()->updateAllLifecyclePhases(); |
| EXPECT_EQ(mainController.rootScrollerLayer(), |
| mainFrameView()->layerForScrolling()); |
| EXPECT_TRUE(mainController.isViewportScrollCallback( |
| mainFrame()->document()->documentElement()->getApplyScroll())); |
| EXPECT_FALSE( |
| mainController.isViewportScrollCallback(container->getApplyScroll())); |
| EXPECT_FALSE(mainController.isViewportScrollCallback( |
| iframe->contentDocument()->documentElement()->getApplyScroll())); |
| } |
| } |
| |
| TEST_F(RootScrollerTest, TestSetRootScrollerCausesViewportLayerChange) { |
| // TODO(bokan): Need a test that changing root scrollers actually sets the |
| // outer viewport layer on the compositor, even in the absence of other |
| // compositing changes. crbug.com/505516 |
| } |
| |
| // Tests that trying to set an element as the root scroller of a document inside |
| // an iframe fails when that element belongs to the parent document. |
| // TODO(bokan): Recent changes mean this is now possible but should be fixed. |
| TEST_F(RootScrollerTest, |
| DISABLED_TestSetRootScrollerOnElementFromOutsideIframe) { |
| initialize("root-scroller-iframe.html"); |
| |
| ASSERT_EQ(nullptr, mainFrame()->document()->rootScroller()); |
| { |
| // Try to set the the root scroller of the child document to be the |
| // <iframe> element in the parent document. |
| HTMLFrameOwnerElement* iframe = toHTMLFrameOwnerElement( |
| mainFrame()->document()->getElementById("iframe")); |
| NonThrowableExceptionState nonThrow; |
| Element* body = mainFrame()->document()->querySelector("body", nonThrow); |
| |
| EXPECT_EQ(nullptr, iframe->contentDocument()->rootScroller()); |
| |
| TrackExceptionState exceptionState; |
| iframe->contentDocument()->setRootScroller(iframe, exceptionState); |
| |
| EXPECT_EQ(iframe, iframe->contentDocument()->rootScroller()); |
| |
| // Try to set the root scroller of the child document to be the |
| // <body> element of the parent document. |
| iframe->contentDocument()->setRootScroller(body, exceptionState); |
| |
| EXPECT_EQ(body, iframe->contentDocument()->rootScroller()); |
| } |
| } |
| |
| // Do a basic sanity check that setting as root scroller an iframe that's remote |
| // doesn't crash or otherwise fail catastrophically. |
| TEST_F(RootScrollerTest, RemoteIFrame) { |
| FrameTestHelpers::TestWebRemoteFrameClient remoteFrameClient; |
| initialize("root-scroller-iframe.html"); |
| |
| // Initialization: Replace the iframe with a remote frame. |
| { |
| WebRemoteFrame* remoteFrame = |
| WebRemoteFrame::create(WebTreeScopeType::Document, &remoteFrameClient); |
| WebFrame* childFrame = mainWebFrame()->firstChild(); |
| childFrame->swap(remoteFrame); |
| } |
| |
| // Set the root scroller in the local main frame to the iframe (which is |
| // remote). |
| { |
| Element* iframe = mainFrame()->document()->getElementById("iframe"); |
| NonThrowableExceptionState nonThrow; |
| mainFrame()->document()->setRootScroller(iframe, nonThrow); |
| EXPECT_EQ(iframe, mainFrame()->document()->rootScroller()); |
| } |
| |
| // Reset explicitly to prevent lifetime issues with the RemoteFrameClient. |
| m_helper.reset(); |
| } |
| |
| // Do a basic sanity check that the scrolling and root scroller machinery |
| // doesn't fail catastrophically in site isolation when the main frame is |
| // remote. Setting a root scroller in OOPIF isn't implemented yet but we should |
| // still scroll as before and not crash. |
| TEST_F(RootScrollerTest, RemoteMainFrame) { |
| FrameTestHelpers::TestWebRemoteFrameClient remoteClient; |
| FrameTestHelpers::TestWebWidgetClient webWidgetClient; |
| WebFrameWidget* widget; |
| WebLocalFrameImpl* localFrame; |
| |
| initialize("root-scroller-iframe.html"); |
| |
| // Initialization: Set the main frame to be a RemoteFrame and add a local |
| // child. |
| { |
| webViewImpl()->setMainFrame(remoteClient.frame()); |
| WebRemoteFrame* root = webViewImpl()->mainFrame()->toWebRemoteFrame(); |
| root->setReplicatedOrigin(SecurityOrigin::createUnique()); |
| WebFrameOwnerProperties properties; |
| localFrame = FrameTestHelpers::createLocalChild( |
| root, "frameName", nullptr, nullptr, nullptr, properties); |
| |
| FrameTestHelpers::loadFrame(localFrame, |
| m_baseURL + "root-scroller-child.html"); |
| widget = localFrame->frameWidget(); |
| widget->resize(WebSize(400, 400)); |
| } |
| |
| Document* document = localFrame->frameView()->frame().document(); |
| Element* container = document->getElementById("container"); |
| |
| // Try scrolling in the iframe. |
| { |
| widget->handleInputEvent( |
| generateWheelGestureEvent(WebInputEvent::GestureScrollBegin)); |
| widget->handleInputEvent( |
| generateWheelGestureEvent(WebInputEvent::GestureScrollUpdate, 0, -100)); |
| widget->handleInputEvent( |
| generateWheelGestureEvent(WebInputEvent::GestureScrollEnd)); |
| EXPECT_EQ(100, container->scrollTop()); |
| } |
| |
| // Set the container Element as the root scroller. |
| { |
| NonThrowableExceptionState nonThrow; |
| document->setRootScroller(container, nonThrow); |
| EXPECT_EQ(container, document->rootScroller()); |
| } |
| |
| // Try scrolling in the iframe now that it has a root scroller set. |
| { |
| widget->handleInputEvent( |
| generateWheelGestureEvent(WebInputEvent::GestureScrollBegin)); |
| widget->handleInputEvent( |
| generateWheelGestureEvent(WebInputEvent::GestureScrollUpdate, 0, -100)); |
| widget->handleInputEvent( |
| generateWheelGestureEvent(WebInputEvent::GestureScrollEnd)); |
| |
| // TODO(bokan): This doesn't work right now because we notice in |
| // Element::nativeApplyScroll that the container is the |
| // effectiveRootScroller but the only way we expect to get to |
| // nativeApplyScroll is if the effective scroller had its applyScroll |
| // ViewportScrollCallback removed. Keep the scrolls to guard crashes |
| // but the expectations on when a ViewportScrollCallback have changed |
| // and should be updated. |
| // EXPECT_EQ(200, container->scrollTop()); |
| } |
| |
| // Reset explicitly to prevent lifetime issues with the RemoteFrameClient. |
| m_helper.reset(); |
| } |
| |
| // Tests that clipping layers belonging to any compositors in the ancestor chain |
| // of the global root scroller have their masking bit removed. |
| TEST_F(RootScrollerTest, RemoveClippingOnCompositorLayers) { |
| initialize("root-scroller-iframe.html"); |
| |
| HTMLFrameOwnerElement* iframe = toHTMLFrameOwnerElement( |
| mainFrame()->document()->getElementById("iframe")); |
| Element* container = iframe->contentDocument()->getElementById("container"); |
| |
| RootScrollerController* mainController = |
| mainFrame()->document()->rootScrollerController(); |
| RootScrollerController* childController = |
| iframe->contentDocument()->rootScrollerController(); |
| TopDocumentRootScrollerController& globalController = |
| frameHost().globalRootScrollerController(); |
| |
| PaintLayerCompositor* mainCompositor = |
| mainFrameView()->layoutViewItem().compositor(); |
| PaintLayerCompositor* childCompositor = |
| iframe->contentDocument()->view()->layoutViewItem().compositor(); |
| |
| NonThrowableExceptionState nonThrow; |
| |
| // No root scroller set, on the main frame the root content layer should |
| // clip. Additionally, on the child frame, the overflow controls host and |
| // container layers should also clip. |
| { |
| EXPECT_TRUE( |
| mainCompositor->rootContentLayer()->platformLayer()->masksToBounds()); |
| EXPECT_FALSE( |
| mainCompositor->rootGraphicsLayer()->platformLayer()->masksToBounds()); |
| EXPECT_FALSE( |
| mainCompositor->containerLayer()->platformLayer()->masksToBounds()); |
| |
| EXPECT_TRUE( |
| childCompositor->rootContentLayer()->platformLayer()->masksToBounds()); |
| EXPECT_TRUE( |
| childCompositor->rootGraphicsLayer()->platformLayer()->masksToBounds()); |
| EXPECT_TRUE( |
| childCompositor->containerLayer()->platformLayer()->masksToBounds()); |
| } |
| |
| // Now set the root scrollers such that the container in the iframe is the |
| // global root scroller. All the previously clipping layers in both paint |
| // layer compositors should no longer clip. |
| { |
| iframe->contentDocument()->setRootScroller(container, nonThrow); |
| mainFrame()->document()->setRootScroller(iframe, nonThrow); |
| mainFrameView()->updateAllLifecyclePhases(); |
| |
| ASSERT_EQ(iframe, mainController->effectiveRootScroller()); |
| ASSERT_EQ(container, childController->effectiveRootScroller()); |
| |
| EXPECT_FALSE( |
| mainCompositor->rootContentLayer()->platformLayer()->masksToBounds()); |
| EXPECT_FALSE( |
| mainCompositor->rootGraphicsLayer()->platformLayer()->masksToBounds()); |
| EXPECT_FALSE( |
| mainCompositor->containerLayer()->platformLayer()->masksToBounds()); |
| |
| EXPECT_FALSE( |
| childCompositor->rootContentLayer()->platformLayer()->masksToBounds()); |
| EXPECT_FALSE( |
| childCompositor->rootGraphicsLayer()->platformLayer()->masksToBounds()); |
| EXPECT_FALSE( |
| childCompositor->containerLayer()->platformLayer()->masksToBounds()); |
| } |
| |
| // Now reset the iframe's root scroller. Since the iframe itself is now the |
| // global root scroller we want it to behave as if it were the main frame, |
| // which means it should clip only on its root content layer. |
| { |
| iframe->contentDocument()->setRootScroller(nullptr, nonThrow); |
| mainFrameView()->updateAllLifecyclePhases(); |
| |
| ASSERT_EQ(iframe, mainController->effectiveRootScroller()); |
| ASSERT_EQ(iframe->contentDocument()->documentElement(), |
| childController->effectiveRootScroller()); |
| ASSERT_EQ(iframe->contentDocument()->documentElement(), |
| globalController.globalRootScroller()); |
| |
| EXPECT_FALSE( |
| mainCompositor->rootContentLayer()->platformLayer()->masksToBounds()); |
| EXPECT_FALSE( |
| mainCompositor->rootGraphicsLayer()->platformLayer()->masksToBounds()); |
| EXPECT_FALSE( |
| mainCompositor->containerLayer()->platformLayer()->masksToBounds()); |
| |
| EXPECT_TRUE( |
| childCompositor->rootContentLayer()->platformLayer()->masksToBounds()); |
| EXPECT_FALSE( |
| childCompositor->rootGraphicsLayer()->platformLayer()->masksToBounds()); |
| EXPECT_FALSE( |
| childCompositor->containerLayer()->platformLayer()->masksToBounds()); |
| } |
| |
| // Now reset the main frame's root scroller. Its compositor should go back |
| // to clipping as well. Because the iframe is now no longer the global root |
| // scroller, it should go back to clipping its overflow host and container |
| // layers. This checks that we invalidate the compositing state even though |
| // the iframe's effective root scroller hasn't changed. |
| |
| { |
| mainFrame()->document()->setRootScroller(nullptr, nonThrow); |
| mainFrameView()->updateAllLifecyclePhases(); |
| |
| ASSERT_EQ(mainFrame()->document()->documentElement(), |
| mainController->effectiveRootScroller()); |
| ASSERT_EQ(iframe->contentDocument()->documentElement(), |
| childController->effectiveRootScroller()); |
| ASSERT_EQ(mainFrame()->document()->documentElement(), |
| globalController.globalRootScroller()); |
| |
| EXPECT_TRUE( |
| mainCompositor->rootContentLayer()->platformLayer()->masksToBounds()); |
| EXPECT_FALSE( |
| mainCompositor->rootGraphicsLayer()->platformLayer()->masksToBounds()); |
| EXPECT_FALSE( |
| mainCompositor->containerLayer()->platformLayer()->masksToBounds()); |
| |
| EXPECT_TRUE( |
| childCompositor->rootContentLayer()->platformLayer()->masksToBounds()); |
| EXPECT_TRUE( |
| childCompositor->rootGraphicsLayer()->platformLayer()->masksToBounds()); |
| EXPECT_TRUE( |
| childCompositor->containerLayer()->platformLayer()->masksToBounds()); |
| } |
| |
| // Set the iframe back as the main frame's root scroller. Since its the |
| // global root scroller again, it should clip like the root frame. This |
| // checks that we invalidate the compositing state even though the iframe's |
| // effective root scroller hasn't changed. |
| { |
| mainFrame()->document()->setRootScroller(iframe, nonThrow); |
| mainFrameView()->updateAllLifecyclePhases(); |
| |
| ASSERT_EQ(iframe, mainController->effectiveRootScroller()); |
| ASSERT_EQ(iframe->contentDocument()->documentElement(), |
| childController->effectiveRootScroller()); |
| ASSERT_EQ(iframe->contentDocument()->documentElement(), |
| globalController.globalRootScroller()); |
| |
| EXPECT_FALSE( |
| mainCompositor->rootContentLayer()->platformLayer()->masksToBounds()); |
| EXPECT_FALSE( |
| mainCompositor->rootGraphicsLayer()->platformLayer()->masksToBounds()); |
| EXPECT_FALSE( |
| mainCompositor->containerLayer()->platformLayer()->masksToBounds()); |
| |
| EXPECT_TRUE( |
| childCompositor->rootContentLayer()->platformLayer()->masksToBounds()); |
| EXPECT_FALSE( |
| childCompositor->rootGraphicsLayer()->platformLayer()->masksToBounds()); |
| EXPECT_FALSE( |
| childCompositor->containerLayer()->platformLayer()->masksToBounds()); |
| } |
| |
| // Set just the iframe's root scroller. We should stop clipping the |
| // iframe's compositor's layers but not the main frame's. |
| { |
| mainFrame()->document()->setRootScroller(nullptr, nonThrow); |
| iframe->contentDocument()->setRootScroller(container, nonThrow); |
| mainFrameView()->updateAllLifecyclePhases(); |
| |
| ASSERT_EQ(mainFrame()->document()->documentElement(), |
| mainController->effectiveRootScroller()); |
| ASSERT_EQ(container, childController->effectiveRootScroller()); |
| |
| EXPECT_TRUE( |
| mainCompositor->rootContentLayer()->platformLayer()->masksToBounds()); |
| EXPECT_FALSE( |
| mainCompositor->rootGraphicsLayer()->platformLayer()->masksToBounds()); |
| EXPECT_FALSE( |
| mainCompositor->containerLayer()->platformLayer()->masksToBounds()); |
| |
| EXPECT_FALSE( |
| childCompositor->rootContentLayer()->platformLayer()->masksToBounds()); |
| EXPECT_FALSE( |
| childCompositor->rootGraphicsLayer()->platformLayer()->masksToBounds()); |
| EXPECT_FALSE( |
| childCompositor->containerLayer()->platformLayer()->masksToBounds()); |
| } |
| } |
| |
| // Tests that removing the root scroller element from the DOM resets the |
| // effective root scroller without waiting for any lifecycle events. |
| TEST_F(RootScrollerTest, RemoveRootScrollerFromDom) { |
| initialize("root-scroller-iframe.html"); |
| |
| { |
| HTMLFrameOwnerElement* iframe = toHTMLFrameOwnerElement( |
| mainFrame()->document()->getElementById("iframe")); |
| Element* innerContainer = |
| iframe->contentDocument()->getElementById("container"); |
| |
| NonThrowableExceptionState exceptionState; |
| mainFrame()->document()->setRootScroller(iframe, exceptionState); |
| iframe->contentDocument()->setRootScroller(innerContainer, exceptionState); |
| mainFrameView()->updateAllLifecyclePhases(); |
| |
| ASSERT_EQ(iframe, mainFrame()->document()->rootScroller()); |
| ASSERT_EQ(iframe, effectiveRootScroller(mainFrame()->document())); |
| ASSERT_EQ(innerContainer, iframe->contentDocument()->rootScroller()); |
| ASSERT_EQ(innerContainer, effectiveRootScroller(iframe->contentDocument())); |
| |
| iframe->contentDocument()->body()->setInnerHTML("", exceptionState); |
| |
| // If the root scroller wasn't updated by the DOM removal above, this |
| // will touch the disposed root scroller's ScrollableArea. |
| mainFrameView()->getRootFrameViewport()->serviceScrollAnimations(0); |
| |
| EXPECT_EQ(iframe->contentDocument()->documentElement(), |
| effectiveRootScroller(iframe->contentDocument())); |
| } |
| } |
| |
| } // namespace |
| |
| } // namespace blink |