blob: fb58878c5c41c87dbb101b999adb893d7f1b6c99 [file] [log] [blame]
// 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 "bindings/core/v8/NodeOrString.h"
#include "core/dom/ClientRect.h"
#include "core/frame/BrowserControls.h"
#include "core/frame/LocalFrameView.h"
#include "core/frame/RootFrameViewport.h"
#include "core/frame/VisualViewport.h"
#include "core/frame/WebLocalFrameBase.h"
#include "core/html/HTMLFrameOwnerElement.h"
#include "core/layout/LayoutBox.h"
#include "core/layout/api/LayoutViewItem.h"
#include "core/layout/compositing/CompositedLayerMapping.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/PaintLayer.h"
#include "core/paint/PaintLayerScrollableArea.h"
#include "platform/testing/URLTestHelpers.h"
#include "platform/testing/UnitTestHelpers.h"
#include "platform/wtf/Vector.h"
#include "public/platform/Platform.h"
#include "public/platform/WebCoalescedInputEvent.h"
#include "public/platform/WebURLLoaderMockFactory.h"
#include "public/web/WebConsoleMessage.h"
#include "public/web/WebHitTestResult.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/WebRemoteFrameImpl.h"
#include "web/tests/FrameTestHelpers.h"
using blink::testing::RunPendingTasks;
using testing::Mock;
namespace blink {
namespace {
class RootScrollerTest : public ::testing::Test {
public:
RootScrollerTest() : base_url_("http://www.test.com/") {
RegisterMockedHttpURLLoad("overflow-scrolling.html");
RegisterMockedHttpURLLoad("root-scroller.html");
RegisterMockedHttpURLLoad("root-scroller-rotation.html");
RegisterMockedHttpURLLoad("root-scroller-iframe.html");
RegisterMockedHttpURLLoad("root-scroller-child.html");
}
~RootScrollerTest() override {
features_backup_.restore();
Platform::Current()
->GetURLLoaderMockFactory()
->UnregisterAllURLsAndClearMemoryCache();
}
WebViewBase* Initialize(const std::string& page_name,
FrameTestHelpers::TestWebViewClient* client) {
return InitializeInternal(base_url_ + page_name, client);
}
WebViewBase* Initialize(const std::string& page_name) {
return InitializeInternal(base_url_ + page_name, &client_);
}
WebViewBase* Initialize() {
return InitializeInternal("about:blank", &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& file_name) {
URLTestHelpers::RegisterMockedURLLoadFromBase(
WebString::FromUTF8(base_url_), testing::WebTestDataPath(),
WebString::FromUTF8(file_name));
}
void ExecuteScript(const WebString& code) {
ExecuteScript(code, *MainWebFrame());
}
void ExecuteScript(const WebString& code, WebLocalFrame& frame) {
frame.ExecuteScript(WebScriptSource(code));
frame.View()->UpdateAllLifecyclePhases();
RunPendingTasks();
}
WebViewBase* GetWebView() const { return helper_.WebView(); }
Page& GetPage() const { return *helper_.WebView()->GetPage(); }
LocalFrame* MainFrame() const {
return GetWebView()->MainFrameImpl()->GetFrame();
}
WebLocalFrame* MainWebFrame() const { return GetWebView()->MainFrameImpl(); }
LocalFrameView* MainFrameView() const {
return GetWebView()->MainFrameImpl()->GetFrame()->View();
}
VisualViewport& GetVisualViewport() const {
return GetPage().GetVisualViewport();
}
BrowserControls& GetBrowserControls() const {
return GetPage().GetBrowserControls();
}
Node* EffectiveRootScroller(Document* doc) const {
return &doc->GetRootScrollerController().EffectiveRootScroller();
}
WebCoalescedInputEvent GenerateTouchGestureEvent(WebInputEvent::Type type,
int delta_x = 0,
int delta_y = 0) {
return GenerateGestureEvent(type, kWebGestureDeviceTouchscreen, delta_x,
delta_y);
}
WebCoalescedInputEvent GenerateWheelGestureEvent(WebInputEvent::Type type,
int delta_x = 0,
int delta_y = 0) {
return GenerateGestureEvent(type, kWebGestureDeviceTouchpad, delta_x,
delta_y);
}
protected:
WebCoalescedInputEvent GenerateGestureEvent(WebInputEvent::Type type,
WebGestureDevice device,
int delta_x,
int delta_y) {
WebGestureEvent event(type, WebInputEvent::kNoModifiers,
WebInputEvent::kTimeStampForTesting);
event.source_device = device;
event.x = 100;
event.y = 100;
if (type == WebInputEvent::kGestureScrollUpdate) {
event.data.scroll_update.delta_x = delta_x;
event.data.scroll_update.delta_y = delta_y;
}
return WebCoalescedInputEvent(event);
}
WebViewBase* InitializeInternal(const std::string& url,
FrameTestHelpers::TestWebViewClient* client) {
RuntimeEnabledFeatures::setSetRootScrollerEnabled(true);
helper_.InitializeAndLoad(url, true, nullptr, client, nullptr,
&ConfigureSettings);
// Initialize browser controls to be shown.
GetWebView()->ResizeWithBrowserControls(IntSize(400, 400), 50, true);
GetWebView()->GetBrowserControls().SetShownRatio(1);
MainFrameView()->UpdateAllLifecyclePhases();
return GetWebView();
}
std::string base_url_;
FrameTestHelpers::TestWebViewClient client_;
FrameTestHelpers::WebViewHelper helper_;
RuntimeEnabledFeatures::Backup features_backup_;
};
// Test that no root scroller element is set if setRootScroller isn't called on
// any elements. The document Node should be the default effective root
// scroller.
TEST_F(RootScrollerTest, TestDefaultRootScroller) {
Initialize("overflow-scrolling.html");
RootScrollerController& controller =
MainFrame()->GetDocument()->GetRootScrollerController();
ASSERT_EQ(nullptr, MainFrame()->GetDocument()->rootScroller());
EXPECT_EQ(MainFrame()->GetDocument(),
EffectiveRootScroller(MainFrame()->GetDocument()));
Element* html_element = MainFrame()->GetDocument()->documentElement();
EXPECT_TRUE(controller.ScrollsViewport(*html_element));
}
// Make sure that replacing the documentElement doesn't change the effective
// root scroller when no root scroller is set.
TEST_F(RootScrollerTest, defaultEffectiveRootScrollerIsDocumentNode) {
Initialize("root-scroller.html");
Document* document = MainFrame()->GetDocument();
Element* iframe = document->createElement("iframe");
EXPECT_EQ(MainFrame()->GetDocument(),
EffectiveRootScroller(MainFrame()->GetDocument()));
// Replace the documentElement with the iframe. The effectiveRootScroller
// should remain the same.
NonThrowableExceptionState non_throw;
HeapVector<NodeOrString> nodes;
nodes.push_back(NodeOrString::fromNode(iframe));
document->documentElement()->ReplaceWith(nodes, non_throw);
MainFrameView()->UpdateAllLifecyclePhases();
EXPECT_EQ(MainFrame()->GetDocument(),
EffectiveRootScroller(MainFrame()->GetDocument()));
}
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()->GetDocument()->getElementById("container");
MainFrame()->GetDocument()->setRootScroller(container);
ASSERT_EQ(container, MainFrame()->GetDocument()->rootScroller());
// Content is 1000x1000, WebView size is 400x400 but hiding the top controls
// makes it 400x450 so max scroll is 550px.
double maximum_scroll = 550;
GetWebView()->HandleInputEvent(
GenerateTouchGestureEvent(WebInputEvent::kGestureScrollBegin));
{
// Scrolling over the #container DIV should cause the browser controls to
// hide.
EXPECT_FLOAT_EQ(1, GetBrowserControls().ShownRatio());
GetWebView()->HandleInputEvent(
GenerateTouchGestureEvent(WebInputEvent::kGestureScrollUpdate, 0,
-GetBrowserControls().Height()));
EXPECT_FLOAT_EQ(0, GetBrowserControls().ShownRatio());
}
{
// Make sure we're actually scrolling the DIV and not the LocalFrameView.
GetWebView()->HandleInputEvent(GenerateTouchGestureEvent(
WebInputEvent::kGestureScrollUpdate, 0, -100));
EXPECT_FLOAT_EQ(100, container->scrollTop());
EXPECT_FLOAT_EQ(0, MainFrameView()->GetScrollOffset().Height());
}
{
// 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()));
GetWebView()->HandleInputEvent(GenerateTouchGestureEvent(
WebInputEvent::kGestureScrollUpdate, 0, -500));
EXPECT_FLOAT_EQ(maximum_scroll, container->scrollTop());
EXPECT_FLOAT_EQ(0, MainFrameView()->GetScrollOffset().Height());
Mock::VerifyAndClearExpectations(&client);
}
{
// Continue the gesture overscroll.
EXPECT_CALL(client, DidOverscroll(WebFloatSize(0, 20), WebFloatSize(0, 70),
WebFloatPoint(100, 100), WebFloatSize()));
GetWebView()->HandleInputEvent(
GenerateTouchGestureEvent(WebInputEvent::kGestureScrollUpdate, 0, -20));
EXPECT_FLOAT_EQ(maximum_scroll, container->scrollTop());
EXPECT_FLOAT_EQ(0, MainFrameView()->GetScrollOffset().Height());
Mock::VerifyAndClearExpectations(&client);
}
GetWebView()->HandleInputEvent(
GenerateTouchGestureEvent(WebInputEvent::kGestureScrollEnd));
{
// Make sure a new gesture scroll still won't scroll the frameview and
// overscrolls.
GetWebView()->HandleInputEvent(
GenerateTouchGestureEvent(WebInputEvent::kGestureScrollBegin));
EXPECT_CALL(client, DidOverscroll(WebFloatSize(0, 30), WebFloatSize(0, 30),
WebFloatPoint(100, 100), WebFloatSize()));
GetWebView()->HandleInputEvent(
GenerateTouchGestureEvent(WebInputEvent::kGestureScrollUpdate, 0, -30));
EXPECT_FLOAT_EQ(maximum_scroll, container->scrollTop());
EXPECT_FLOAT_EQ(0, MainFrameView()->GetScrollOffset().Height());
Mock::VerifyAndClearExpectations(&client);
GetWebView()->HandleInputEvent(
GenerateTouchGestureEvent(WebInputEvent::kGestureScrollEnd));
}
{
// Scrolling up should show the browser controls.
GetWebView()->HandleInputEvent(
GenerateTouchGestureEvent(WebInputEvent::kGestureScrollBegin));
EXPECT_FLOAT_EQ(0, GetBrowserControls().ShownRatio());
GetWebView()->HandleInputEvent(
GenerateTouchGestureEvent(WebInputEvent::kGestureScrollUpdate, 0, 30));
EXPECT_FLOAT_EQ(0.6, GetBrowserControls().ShownRatio());
GetWebView()->HandleInputEvent(
GenerateTouchGestureEvent(WebInputEvent::kGestureScrollEnd));
}
// Reset manually to avoid lifetime issues with custom WebViewClient.
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()->GetDocument()->rootScroller());
Element* container = MainFrame()->GetDocument()->getElementById("container");
MainFrame()->GetDocument()->setRootScroller(container);
EXPECT_EQ(container, MainFrame()->GetDocument()->rootScroller());
EXPECT_EQ(container, EffectiveRootScroller(MainFrame()->GetDocument()));
MainFrame()->GetDocument()->body()->RemoveChild(container);
MainFrameView()->UpdateAllLifecyclePhases();
EXPECT_EQ(container, MainFrame()->GetDocument()->rootScroller());
EXPECT_NE(container, EffectiveRootScroller(MainFrame()->GetDocument()));
}
// 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()->GetDocument()->getElementById("nonBlock");
MainFrame()->GetDocument()->setRootScroller(element);
MainFrameView()->UpdateAllLifecyclePhases();
EXPECT_EQ(element, MainFrame()->GetDocument()->rootScroller());
EXPECT_NE(element, EffectiveRootScroller(MainFrame()->GetDocument()));
}
{
// Set to an element with no size.
Element* element = MainFrame()->GetDocument()->getElementById("empty");
MainFrame()->GetDocument()->setRootScroller(element);
MainFrameView()->UpdateAllLifecyclePhases();
EXPECT_EQ(element, MainFrame()->GetDocument()->rootScroller());
EXPECT_NE(element, EffectiveRootScroller(MainFrame()->GetDocument()));
}
}
// Test that the effective root scroller resets to the document Node when the
// current root scroller element becomes invalid as a scroller.
TEST_F(RootScrollerTest, TestRootScrollerBecomesInvalid) {
Initialize("root-scroller.html");
Element* container = MainFrame()->GetDocument()->getElementById("container");
ASSERT_EQ(nullptr, MainFrame()->GetDocument()->rootScroller());
ASSERT_EQ(MainFrame()->GetDocument(),
EffectiveRootScroller(MainFrame()->GetDocument()));
{
MainFrame()->GetDocument()->setRootScroller(container);
MainFrameView()->UpdateAllLifecyclePhases();
EXPECT_EQ(container, MainFrame()->GetDocument()->rootScroller());
EXPECT_EQ(container, EffectiveRootScroller(MainFrame()->GetDocument()));
ExecuteScript(
"document.querySelector('#container').style.display = 'inline'");
MainFrameView()->UpdateAllLifecyclePhases();
EXPECT_EQ(container, MainFrame()->GetDocument()->rootScroller());
EXPECT_EQ(MainFrame()->GetDocument(),
EffectiveRootScroller(MainFrame()->GetDocument()));
}
ExecuteScript("document.querySelector('#container').style.display = 'block'");
MainFrame()->GetDocument()->setRootScroller(nullptr);
MainFrameView()->UpdateAllLifecyclePhases();
EXPECT_EQ(nullptr, MainFrame()->GetDocument()->rootScroller());
EXPECT_EQ(MainFrame()->GetDocument(),
EffectiveRootScroller(MainFrame()->GetDocument()));
{
MainFrame()->GetDocument()->setRootScroller(container);
MainFrameView()->UpdateAllLifecyclePhases();
EXPECT_EQ(container, MainFrame()->GetDocument()->rootScroller());
EXPECT_EQ(container, EffectiveRootScroller(MainFrame()->GetDocument()));
ExecuteScript("document.querySelector('#container').style.width = '98%'");
MainFrameView()->UpdateAllLifecyclePhases();
EXPECT_EQ(container, MainFrame()->GetDocument()->rootScroller());
EXPECT_EQ(MainFrame()->GetDocument(),
EffectiveRootScroller(MainFrame()->GetDocument()));
}
}
// 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()->GetDocument()->rootScroller());
{
// Trying to set an element from a nested document should fail.
HTMLFrameOwnerElement* iframe = ToHTMLFrameOwnerElement(
MainFrame()->GetDocument()->getElementById("iframe"));
Element* inner_container =
iframe->contentDocument()->getElementById("container");
MainFrame()->GetDocument()->setRootScroller(inner_container);
MainFrameView()->UpdateAllLifecyclePhases();
EXPECT_EQ(inner_container, MainFrame()->GetDocument()->rootScroller());
EXPECT_EQ(inner_container,
EffectiveRootScroller(MainFrame()->GetDocument()));
}
{
// Setting the iframe itself should also work.
HTMLFrameOwnerElement* iframe = ToHTMLFrameOwnerElement(
MainFrame()->GetDocument()->getElementById("iframe"));
MainFrame()->GetDocument()->setRootScroller(iframe);
MainFrameView()->UpdateAllLifecyclePhases();
EXPECT_EQ(iframe, MainFrame()->GetDocument()->rootScroller());
EXPECT_EQ(iframe, EffectiveRootScroller(MainFrame()->GetDocument()));
}
}
// 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()->GetDocument()->rootScroller());
{
HTMLFrameOwnerElement* iframe = ToHTMLFrameOwnerElement(
MainFrame()->GetDocument()->getElementById("iframe"));
EXPECT_EQ(iframe->contentDocument(),
EffectiveRootScroller(iframe->contentDocument()));
Element* inner_container =
iframe->contentDocument()->getElementById("container");
iframe->contentDocument()->setRootScroller(inner_container);
MainFrameView()->UpdateAllLifecyclePhases();
EXPECT_EQ(inner_container, iframe->contentDocument()->rootScroller());
EXPECT_EQ(inner_container,
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()->GetDocument()->rootScroller());
{
NonThrowableExceptionState non_throw;
// Try to set the root scroller in the main frame to be the iframe
// element.
HTMLFrameOwnerElement* iframe = ToHTMLFrameOwnerElement(
MainFrame()->GetDocument()->getElementById("iframe"));
MainFrame()->GetDocument()->setRootScroller(iframe, non_throw);
EXPECT_EQ(iframe, MainFrame()->GetDocument()->rootScroller());
EXPECT_EQ(iframe, EffectiveRootScroller(MainFrame()->GetDocument()));
Element* container = iframe->contentDocument()->getElementById("container");
iframe->contentDocument()->setRootScroller(container, non_throw);
EXPECT_EQ(container, iframe->contentDocument()->rootScroller());
EXPECT_EQ(container, EffectiveRootScroller(iframe->contentDocument()));
EXPECT_EQ(iframe, MainFrame()->GetDocument()->rootScroller());
EXPECT_EQ(iframe, EffectiveRootScroller(MainFrame()->GetDocument()));
}
}
// 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()->GetDocument()->rootScroller());
HTMLFrameOwnerElement* iframe = ToHTMLFrameOwnerElement(
MainFrame()->GetDocument()->getElementById("iframe"));
Element* container = iframe->contentDocument()->getElementById("container");
const TopDocumentRootScrollerController& main_controller =
MainFrame()->GetDocument()->GetPage()->GlobalRootScrollerController();
NonThrowableExceptionState non_throw;
// No root scroller set, the documentElement should be the effective root
// and the main LocalFrameView's scroll layer should be the layer to use.
{
EXPECT_EQ(
main_controller.RootScrollerLayer(),
MainFrameView()->LayoutViewportScrollableArea()->LayerForScrolling());
EXPECT_TRUE(main_controller.IsViewportScrollCallback(
MainFrame()->GetDocument()->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, non_throw);
MainFrameView()->UpdateAllLifecyclePhases();
EXPECT_EQ(
main_controller.RootScrollerLayer(),
MainFrameView()->LayoutViewportScrollableArea()->LayerForScrolling());
EXPECT_TRUE(main_controller.IsViewportScrollCallback(
MainFrame()->GetDocument()->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()->GetDocument()->setRootScroller(iframe, non_throw);
MainFrameView()->UpdateAllLifecyclePhases();
ScrollableArea* container_scroller =
static_cast<PaintInvalidationCapableScrollableArea*>(
ToLayoutBox(container->GetLayoutObject())->GetScrollableArea());
EXPECT_EQ(main_controller.RootScrollerLayer(),
container_scroller->LayerForScrolling());
EXPECT_FALSE(main_controller.IsViewportScrollCallback(
MainFrame()->GetDocument()->documentElement()->GetApplyScroll()));
EXPECT_TRUE(
main_controller.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, non_throw);
MainFrameView()->UpdateAllLifecyclePhases();
EXPECT_EQ(main_controller.RootScrollerLayer(),
iframe->contentDocument()
->View()
->LayoutViewportScrollableArea()
->LayerForScrolling());
EXPECT_FALSE(
main_controller.IsViewportScrollCallback(container->GetApplyScroll()));
EXPECT_FALSE(main_controller.IsViewportScrollCallback(
MainFrame()->GetDocument()->documentElement()->GetApplyScroll()));
EXPECT_TRUE(main_controller.IsViewportScrollCallback(
iframe->contentDocument()->documentElement()->GetApplyScroll()));
}
// Finally, unsetting the main frame's root scroller should reset it to the
// documentElement and corresponding layer.
{
MainFrame()->GetDocument()->setRootScroller(nullptr, non_throw);
MainFrameView()->UpdateAllLifecyclePhases();
EXPECT_EQ(
main_controller.RootScrollerLayer(),
MainFrameView()->LayoutViewportScrollableArea()->LayerForScrolling());
EXPECT_TRUE(main_controller.IsViewportScrollCallback(
MainFrame()->GetDocument()->documentElement()->GetApplyScroll()));
EXPECT_FALSE(
main_controller.IsViewportScrollCallback(container->GetApplyScroll()));
EXPECT_FALSE(main_controller.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()->GetDocument()->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()->GetDocument()->getElementById("iframe"));
NonThrowableExceptionState non_throw;
Element* body =
MainFrame()->GetDocument()->QuerySelector("body", non_throw);
EXPECT_EQ(nullptr, iframe->contentDocument()->rootScroller());
iframe->contentDocument()->setRootScroller(iframe);
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);
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 remote_frame_client;
Initialize("root-scroller-iframe.html");
// Initialization: Replace the iframe with a remote frame.
{
WebRemoteFrame* remote_frame = WebRemoteFrame::Create(
WebTreeScopeType::kDocument, &remote_frame_client);
WebFrame* child_frame = MainWebFrame()->FirstChild();
child_frame->Swap(remote_frame);
}
// Set the root scroller in the local main frame to the iframe (which is
// remote).
{
Element* iframe = MainFrame()->GetDocument()->getElementById("iframe");
NonThrowableExceptionState non_throw;
MainFrame()->GetDocument()->setRootScroller(iframe, non_throw);
EXPECT_EQ(iframe, MainFrame()->GetDocument()->rootScroller());
}
// Reset explicitly to prevent lifetime issues with the RemoteFrameClient.
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 remote_client;
FrameTestHelpers::TestWebWidgetClient web_widget_client;
WebFrameWidget* widget;
WebLocalFrameBase* local_frame;
Initialize("root-scroller-iframe.html");
// Initialization: Set the main frame to be a RemoteFrame and add a local
// child.
{
GetWebView()->SetMainFrame(remote_client.GetFrame());
WebRemoteFrame* root = GetWebView()->MainFrame()->ToWebRemoteFrame();
root->SetReplicatedOrigin(SecurityOrigin::CreateUnique());
WebFrameOwnerProperties properties;
local_frame = FrameTestHelpers::CreateLocalChild(
root, "frameName", nullptr, nullptr, nullptr, properties);
FrameTestHelpers::LoadFrame(local_frame,
base_url_ + "root-scroller-child.html");
widget = local_frame->FrameWidget();
widget->Resize(WebSize(400, 400));
}
Document* document = local_frame->GetFrameView()->GetFrame().GetDocument();
Element* container = document->getElementById("container");
// Try scrolling in the iframe.
{
widget->HandleInputEvent(
GenerateWheelGestureEvent(WebInputEvent::kGestureScrollBegin));
widget->HandleInputEvent(GenerateWheelGestureEvent(
WebInputEvent::kGestureScrollUpdate, 0, -100));
widget->HandleInputEvent(
GenerateWheelGestureEvent(WebInputEvent::kGestureScrollEnd));
EXPECT_EQ(100, container->scrollTop());
}
// Set the container Element as the root scroller.
{
NonThrowableExceptionState non_throw;
document->setRootScroller(container, non_throw);
EXPECT_EQ(container, document->rootScroller());
}
// Try scrolling in the iframe now that it has a root scroller set.
{
widget->HandleInputEvent(
GenerateWheelGestureEvent(WebInputEvent::kGestureScrollBegin));
widget->HandleInputEvent(GenerateWheelGestureEvent(
WebInputEvent::kGestureScrollUpdate, 0, -100));
widget->HandleInputEvent(
GenerateWheelGestureEvent(WebInputEvent::kGestureScrollEnd));
// 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.
helper_.Reset();
}
// 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()->GetDocument()->getElementById("iframe"));
Element* inner_container =
iframe->contentDocument()->getElementById("container");
MainFrame()->GetDocument()->setRootScroller(iframe);
iframe->contentDocument()->setRootScroller(inner_container);
MainFrameView()->UpdateAllLifecyclePhases();
ASSERT_EQ(iframe, MainFrame()->GetDocument()->rootScroller());
ASSERT_EQ(iframe, EffectiveRootScroller(MainFrame()->GetDocument()));
ASSERT_EQ(inner_container, iframe->contentDocument()->rootScroller());
ASSERT_EQ(inner_container,
EffectiveRootScroller(iframe->contentDocument()));
iframe->contentDocument()->body()->setInnerHTML("");
// 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);
}
}
// Tests that we still have a global root scroller layer when the HTML element
// has no layout object. crbug.com/637036.
TEST_F(RootScrollerTest, DocumentElementHasNoLayoutObject) {
Initialize("overflow-scrolling.html");
// There's no rootScroller set on this page so we should default to the
// document Node, which means we should use the layout viewport. Ensure this
// happens even if the <html> element has no LayoutObject.
ExecuteScript("document.documentElement.style.display = 'none';");
const TopDocumentRootScrollerController& global_controller =
MainFrame()->GetDocument()->GetPage()->GlobalRootScrollerController();
EXPECT_EQ(MainFrame()->GetDocument()->documentElement(),
global_controller.GlobalRootScroller());
EXPECT_EQ(
MainFrameView()->LayoutViewportScrollableArea()->LayerForScrolling(),
global_controller.RootScrollerLayer());
}
// On Android, the main scrollbars are owned by the visual viewport and the
// LocalFrameView's disabled. This functionality should extend to a rootScroller
// that isn't the main LocalFrameView.
TEST_F(RootScrollerTest, UseVisualViewportScrollbars) {
Initialize("root-scroller.html");
Element* container = MainFrame()->GetDocument()->getElementById("container");
NonThrowableExceptionState non_throw;
MainFrame()->GetDocument()->setRootScroller(container, non_throw);
MainFrameView()->UpdateAllLifecyclePhases();
ScrollableArea* container_scroller =
static_cast<PaintInvalidationCapableScrollableArea*>(
ToLayoutBox(container->GetLayoutObject())->GetScrollableArea());
EXPECT_FALSE(container_scroller->HorizontalScrollbar());
EXPECT_FALSE(container_scroller->VerticalScrollbar());
EXPECT_GT(container_scroller->MaximumScrollOffset().Width(), 0);
EXPECT_GT(container_scroller->MaximumScrollOffset().Height(), 0);
}
// On Android, the main scrollbars are owned by the visual viewport and the
// LocalFrameView's disabled. This functionality should extend to a rootScroller
// that's a nested iframe.
TEST_F(RootScrollerTest, UseVisualViewportScrollbarsIframe) {
Initialize("root-scroller-iframe.html");
Element* iframe = MainFrame()->GetDocument()->getElementById("iframe");
LocalFrame* child_frame =
ToLocalFrame(ToHTMLFrameOwnerElement(iframe)->ContentFrame());
NonThrowableExceptionState non_throw;
MainFrame()->GetDocument()->setRootScroller(iframe, non_throw);
WebLocalFrame* child_web_frame =
MainWebFrame()->FirstChild()->ToWebLocalFrame();
ExecuteScript(
"document.getElementById('container').style.width = '200%';"
"document.getElementById('container').style.height = '200%';",
*child_web_frame);
MainFrameView()->UpdateAllLifecyclePhases();
ScrollableArea* container_scroller = child_frame->View();
EXPECT_FALSE(container_scroller->HorizontalScrollbar());
EXPECT_FALSE(container_scroller->VerticalScrollbar());
EXPECT_GT(container_scroller->MaximumScrollOffset().Width(), 0);
EXPECT_GT(container_scroller->MaximumScrollOffset().Height(), 0);
}
TEST_F(RootScrollerTest, TopControlsAdjustmentAppliedToRootScroller) {
Initialize();
WebURL base_url = URLTestHelpers::ToKURL("http://www.test.com/");
FrameTestHelpers::LoadHTMLString(GetWebView()->MainFrame(),
"<!DOCTYPE html>"
"<style>"
" body, html {"
" width: 100%;"
" height: 100%;"
" margin: 0px;"
" }"
" #container {"
" width: 100%;"
" height: 100%;"
" overflow: auto;"
" }"
"</style>"
"<div id='container'>"
" <div style='height:1000px'>test</div>"
"</div>",
base_url);
GetWebView()->ResizeWithBrowserControls(IntSize(400, 400), 50, true);
MainFrameView()->UpdateAllLifecyclePhases();
Element* container = MainFrame()->GetDocument()->getElementById("container");
MainFrame()->GetDocument()->setRootScroller(container, ASSERT_NO_EXCEPTION);
ScrollableArea* container_scroller =
static_cast<PaintInvalidationCapableScrollableArea*>(
ToLayoutBox(container->GetLayoutObject())->GetScrollableArea());
// Hide the top controls and scroll down maximally. We should account for the
// change in maximum scroll offset due to the top controls hiding. That is,
// since the controls are hidden, the "content area" is taller so the maximum
// scroll offset should shrink.
ASSERT_EQ(1000 - 400, container_scroller->MaximumScrollOffset().Height());
GetWebView()->HandleInputEvent(
GenerateTouchGestureEvent(WebInputEvent::kGestureScrollBegin));
ASSERT_EQ(1, GetBrowserControls().ShownRatio());
GetWebView()->HandleInputEvent(GenerateTouchGestureEvent(
WebInputEvent::kGestureScrollUpdate, 0, -GetBrowserControls().Height()));
ASSERT_EQ(0, GetBrowserControls().ShownRatio());
EXPECT_EQ(1000 - 450, container_scroller->MaximumScrollOffset().Height());
GetWebView()->HandleInputEvent(
GenerateTouchGestureEvent(WebInputEvent::kGestureScrollUpdate, 0, -3000));
EXPECT_EQ(1000 - 450, container_scroller->GetScrollOffset().Height());
GetWebView()->HandleInputEvent(
GenerateTouchGestureEvent(WebInputEvent::kGestureScrollEnd));
GetWebView()->ResizeWithBrowserControls(IntSize(400, 450), 50, false);
EXPECT_EQ(1000 - 450, container_scroller->MaximumScrollOffset().Height());
}
TEST_F(RootScrollerTest, RotationAnchoring) {
Initialize("root-scroller-rotation.html");
ScrollableArea* container_scroller;
{
GetWebView()->ResizeWithBrowserControls(IntSize(250, 1000), 0, true);
MainFrameView()->UpdateAllLifecyclePhases();
Element* container =
MainFrame()->GetDocument()->getElementById("container");
NonThrowableExceptionState non_throw;
MainFrame()->GetDocument()->setRootScroller(container, non_throw);
MainFrameView()->UpdateAllLifecyclePhases();
container_scroller = static_cast<PaintInvalidationCapableScrollableArea*>(
ToLayoutBox(container->GetLayoutObject())->GetScrollableArea());
}
Element* target = MainFrame()->GetDocument()->getElementById("target");
// Zoom in and scroll the viewport so that the target is fully in the
// viewport and the visual viewport is fully scrolled within the layout
// viepwort.
{
int scroll_x = 250 * 4;
int scroll_y = 1000 * 4;
GetWebView()->SetPageScaleFactor(2);
GetWebView()->HandleInputEvent(
GenerateTouchGestureEvent(WebInputEvent::kGestureScrollBegin));
GetWebView()->HandleInputEvent(GenerateTouchGestureEvent(
WebInputEvent::kGestureScrollUpdate, -scroll_x, -scroll_y));
GetWebView()->HandleInputEvent(
GenerateTouchGestureEvent(WebInputEvent::kGestureScrollEnd));
// The visual viewport should be 1.5 screens scrolled so that the target
// occupies the bottom quadrant of the layout viewport.
ASSERT_EQ((250 * 3) / 2, container_scroller->GetScrollOffset().Width());
ASSERT_EQ((1000 * 3) / 2, container_scroller->GetScrollOffset().Height());
// The visual viewport should have scrolled the last half layout viewport.
ASSERT_EQ((250) / 2, GetVisualViewport().GetScrollOffset().Width());
ASSERT_EQ((1000) / 2, GetVisualViewport().GetScrollOffset().Height());
}
// Now do a rotation resize.
GetWebView()->ResizeWithBrowserControls(IntSize(1000, 250), 50, false);
MainFrameView()->UpdateAllLifecyclePhases();
// The visual viewport should remain fully filled by the target.
ClientRect* rect = target->getBoundingClientRect();
EXPECT_EQ(rect->left(), GetVisualViewport().GetScrollOffset().Width());
EXPECT_EQ(rect->top(), GetVisualViewport().GetScrollOffset().Height());
}
// Tests that we don't crash if the default documentElement isn't a valid root
// scroller. This can happen in some edge cases where documentElement isn't
// <html>. crbug.com/668553.
TEST_F(RootScrollerTest, InvalidDefaultRootScroller) {
Initialize("overflow-scrolling.html");
Document* document = MainFrame()->GetDocument();
Element* br = document->createElement("br");
document->ReplaceChild(br, document->documentElement());
MainFrameView()->UpdateAllLifecyclePhases();
Element* html = document->createElement("html");
Element* body = document->createElement("body");
html->AppendChild(body);
body->AppendChild(br);
document->AppendChild(html);
MainFrameView()->UpdateAllLifecyclePhases();
}
// Makes sure that when an iframe becomes the effective root scroller, its
// FrameView stops sizing layout to the frame rect and uses its parent's layout
// size instead. This allows matching the layout size semantics of the root
// FrameView since its layout size can differ from the frame rect due to
// resizes by the URL bar.
TEST_F(RootScrollerTest, IFrameRootScrollerGetsNonFixedLayoutSize) {
Initialize("root-scroller-iframe.html");
MainFrameView()->UpdateAllLifecyclePhases();
Document* document = MainFrame()->GetDocument();
HTMLFrameOwnerElement* iframe = ToHTMLFrameOwnerElement(
MainFrame()->GetDocument()->getElementById("iframe"));
LocalFrameView* iframe_view = ToLocalFrame(iframe->ContentFrame())->View();
ASSERT_EQ(IntSize(400, 400), iframe_view->GetLayoutSize());
ASSERT_EQ(IntSize(400, 400), iframe_view->Size());
ASSERT_TRUE(iframe_view->LayoutSizeFixedToFrameSize());
// Make the iframe the rootscroller. This should cause the iframe's layout
// size to be manually controlled.
{
document->setRootScroller(iframe);
EXPECT_FALSE(iframe_view->LayoutSizeFixedToFrameSize());
MainFrameView()->UpdateAllLifecyclePhases();
EXPECT_EQ(IntSize(400, 400), iframe_view->GetLayoutSize());
EXPECT_EQ(IntSize(400, 400), iframe_view->Size());
}
// Hide the URL bar, the iframe's frame rect should expand but the layout
// size should remain the same.
{
GetWebView()->ResizeWithBrowserControls(IntSize(400, 450), 50, false);
MainFrameView()->UpdateAllLifecyclePhases();
EXPECT_EQ(IntSize(400, 400), iframe_view->GetLayoutSize());
EXPECT_EQ(IntSize(400, 450), iframe_view->Size());
}
// Simulate a rotation. This time the layout size should reflect the resize.
{
GetWebView()->ResizeWithBrowserControls(IntSize(450, 400), 50, false);
MainFrameView()->UpdateAllLifecyclePhases();
EXPECT_EQ(IntSize(450, 350), iframe_view->GetLayoutSize());
EXPECT_EQ(IntSize(450, 400), iframe_view->Size());
// "Un-rotate" for following tests.
GetWebView()->ResizeWithBrowserControls(IntSize(400, 450), 50, false);
MainFrameView()->UpdateAllLifecyclePhases();
}
// Show the URL bar again. The frame rect should match the viewport.
{
GetWebView()->ResizeWithBrowserControls(IntSize(400, 400), 50, true);
MainFrameView()->UpdateAllLifecyclePhases();
EXPECT_EQ(IntSize(400, 400), iframe_view->GetLayoutSize());
EXPECT_EQ(IntSize(400, 400), iframe_view->Size());
}
// Hide the URL bar and reset the rootScroller. The iframe should go back to
// tracking layout size by frame rect.
{
GetWebView()->ResizeWithBrowserControls(IntSize(400, 450), 50, false);
MainFrameView()->UpdateAllLifecyclePhases();
EXPECT_EQ(IntSize(400, 400), iframe_view->GetLayoutSize());
EXPECT_EQ(IntSize(400, 450), iframe_view->Size());
document->setRootScroller(nullptr);
EXPECT_TRUE(iframe_view->LayoutSizeFixedToFrameSize());
MainFrameView()->UpdateAllLifecyclePhases();
EXPECT_EQ(IntSize(400, 400), iframe_view->GetLayoutSize());
EXPECT_EQ(IntSize(400, 400), iframe_view->Size());
}
}
// Ensure that removing the root scroller element causes an update to the RFV's
// layout viewport immediately since old layout viewport is now part of a
// detached layout hierarchy.
TEST_F(RootScrollerTest, ImmediateUpdateOfLayoutViewport) {
Initialize("root-scroller-iframe.html");
Document* document = MainFrame()->GetDocument();
HTMLFrameOwnerElement* iframe = ToHTMLFrameOwnerElement(
MainFrame()->GetDocument()->getElementById("iframe"));
document->setRootScroller(iframe);
MainFrameView()->UpdateAllLifecyclePhases();
RootScrollerController& main_controller =
MainFrame()->GetDocument()->GetRootScrollerController();
LocalFrame* iframe_local_frame = ToLocalFrame(iframe->ContentFrame());
EXPECT_EQ(iframe, &main_controller.EffectiveRootScroller());
EXPECT_EQ(iframe_local_frame->View()->LayoutViewportScrollableArea(),
&MainFrameView()->GetRootFrameViewport()->LayoutViewport());
// Remove the <iframe> and make sure the layout viewport reverts to the
// LocalFrameView without a layout.
iframe->remove();
EXPECT_EQ(MainFrameView()->LayoutViewportScrollableArea(),
&MainFrameView()->GetRootFrameViewport()->LayoutViewport());
}
class RootScrollerHitTest : public RootScrollerTest {
public:
void CheckHitTestAtBottomOfScreen() {
HideTopControlsWithMaximalScroll();
// Do a hit test at the very bottom of the screen. This should be outside
// the root scroller's LayoutBox since inert top controls won't resize the
// ICB but, since we expaned the clip, we should still be able to hit the
// target.
WebPoint point(200, 445);
WebSize tap_area(20, 20);
WebHitTestResult result =
GetWebView()->HitTestResultForTap(point, tap_area);
Node* hit_node = result.GetNode().Unwrap<Node>();
Element* target = MainFrame()->GetDocument()->getElementById("target");
ASSERT_TRUE(target);
EXPECT_EQ(target, hit_node);
}
private:
void HideTopControlsWithMaximalScroll() {
// Do a scroll gesture that hides the top controls and scrolls all the way
// to the bottom.
GetWebView()->HandleInputEvent(
GenerateTouchGestureEvent(WebInputEvent::kGestureScrollBegin));
ASSERT_EQ(1, GetBrowserControls().ShownRatio());
GetWebView()->HandleInputEvent(
GenerateTouchGestureEvent(WebInputEvent::kGestureScrollUpdate, 0,
-GetBrowserControls().Height()));
ASSERT_EQ(0, GetBrowserControls().ShownRatio());
GetWebView()->HandleInputEvent(GenerateTouchGestureEvent(
WebInputEvent::kGestureScrollUpdate, 0, -100000));
GetWebView()->HandleInputEvent(
GenerateTouchGestureEvent(WebInputEvent::kGestureScrollEnd));
GetWebView()->ResizeWithBrowserControls(IntSize(400, 450), 50, false);
}
};
// Test that hit testing in the area revealed at the bottom of the screen
// revealed by hiding the URL bar works properly when using a root scroller
// when the target and scroller are in the same PaintLayer.
TEST_F(RootScrollerHitTest, HitTestInAreaRevealedByURLBarSameLayer) {
// Add a target at the bottom of the root scroller that's the size of the url
// bar. We'll test that hiding the URL bar appropriately adjusts clipping so
// that we can hit this target.
Initialize();
WebURL baseURL = URLTestHelpers::ToKURL("http://www.test.com/");
FrameTestHelpers::LoadHTMLString(GetWebView()->MainFrame(),
"<!DOCTYPE html>"
"<style>"
" body, html {"
" height: 100%;"
" margin: 0px;"
" }"
" #spacer {"
" height: 1000px;"
" }"
" #container {"
" position: absolute;"
" width: 100%;"
" height: 100%;"
" overflow: auto;"
" }"
" #target {"
" width: 100%;"
" height: 50px;"
" }"
"</style>"
"<div id='container'>"
" <div id='spacer'></div>"
" <div id='target'></div>"
"</div>",
baseURL);
Document* document = MainFrame()->GetDocument();
Element* container = document->getElementById("container");
Element* target = document->getElementById("target");
document->setRootScroller(container);
// This test checks hit testing while the target is in the same PaintLayer as
// the root scroller.
ASSERT_EQ(ToLayoutBox(target->GetLayoutObject())->EnclosingLayer(),
ToLayoutBox(container->GetLayoutObject())->Layer());
CheckHitTestAtBottomOfScreen();
}
// Test that hit testing in the area revealed at the bottom of the screen
// revealed by hiding the URL bar works properly when using a root scroller
// when the target and scroller are in different PaintLayers.
TEST_F(RootScrollerHitTest, HitTestInAreaRevealedByURLBarDifferentLayer) {
// Add a target at the bottom of the root scroller that's the size of the url
// bar. We'll test that hiding the URL bar appropriately adjusts clipping so
// that we can hit this target.
Initialize();
WebURL baseURL = URLTestHelpers::ToKURL("http://www.test.com/");
FrameTestHelpers::LoadHTMLString(GetWebView()->MainFrame(),
"<!DOCTYPE html>"
"<style>"
" body, html {"
" height: 100%;"
" margin: 0px;"
" }"
" #spacer {"
" height: 1000px;"
" }"
" #container {"
" position: absolute;"
" width: 100%;"
" height: 100%;"
" overflow: auto;"
" }"
" #target {"
" width: 100%;"
" height: 50px;"
" will-change: transform;"
" }"
"</style>"
"<div id='container'>"
" <div id='spacer'></div>"
" <div id='target'></div>"
"</div>",
baseURL);
Document* document = MainFrame()->GetDocument();
Element* container = document->getElementById("container");
Element* target = document->getElementById("target");
document->setRootScroller(container);
// Ensure the target and container weren't put into the same layer.
ASSERT_NE(ToLayoutBox(target->GetLayoutObject())->EnclosingLayer(),
ToLayoutBox(container->GetLayoutObject())->Layer());
CheckHitTestAtBottomOfScreen();
}
} // namespace
} // namespace blink