blob: e86d3b884f89b137a59494076561897d2de29536 [file] [log] [blame]
// Copyright 2015 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/layout/ScrollAnchor.h"
#include "core/dom/ClientRect.h"
#include "core/frame/VisualViewport.h"
#include "core/layout/LayoutBox.h"
#include "core/layout/LayoutTestHelper.h"
#include "core/page/PrintContext.h"
#include "core/paint/PaintLayerScrollableArea.h"
#include "platform/testing/HistogramTester.h"
#include "platform/testing/RuntimeEnabledFeaturesTestHelpers.h"
namespace blink {
using Corner = ScrollAnchor::Corner;
typedef bool TestParamRootLayerScrolling;
class ScrollAnchorTest
: public testing::WithParamInterface<TestParamRootLayerScrolling>,
private ScopedRootLayerScrollingForTest,
public RenderingTest {
public:
ScrollAnchorTest() : ScopedRootLayerScrollingForTest(GetParam()) {
RuntimeEnabledFeatures::SetScrollAnchoringEnabled(true);
}
~ScrollAnchorTest() {
RuntimeEnabledFeatures::SetScrollAnchoringEnabled(false);
}
protected:
void Update() {
// TODO(skobes): Use SimTest instead of RenderingTest and move into
// Source/web?
GetDocument().View()->UpdateAllLifecyclePhases();
}
ScrollableArea* LayoutViewport() {
return GetDocument().View()->LayoutViewportScrollableArea();
}
VisualViewport& GetVisualViewport() {
return GetDocument().View()->GetPage()->GetVisualViewport();
}
ScrollableArea* ScrollerForElement(Element* element) {
return ToLayoutBox(element->GetLayoutObject())->GetScrollableArea();
}
ScrollAnchor& GetScrollAnchor(ScrollableArea* scroller) {
DCHECK(scroller->IsLocalFrameView() ||
scroller->IsPaintLayerScrollableArea());
return *(scroller->GetScrollAnchor());
}
void SetHeight(Element* element, int height) {
element->setAttribute(HTMLNames::styleAttr,
AtomicString(String::Format("height: %dpx", height)));
Update();
}
void ScrollLayoutViewport(ScrollOffset delta) {
Element* scrolling_element = GetDocument().scrollingElement();
if (delta.Width())
scrolling_element->setScrollLeft(scrolling_element->scrollLeft() +
delta.Width());
if (delta.Height())
scrolling_element->setScrollTop(scrolling_element->scrollTop() +
delta.Height());
}
};
INSTANTIATE_TEST_CASE_P(All, ScrollAnchorTest, ::testing::Bool());
// TODO(ymalik): Currently, this should be the first test in the file to avoid
// failure when running with other tests. Dig into this more and fix.
TEST_P(ScrollAnchorTest, UMAMetricUpdated) {
HistogramTester histogram_tester;
SetBodyInnerHTML(
"<style> body { height: 1000px } div { height: 100px } </style>"
"<div id='block1'>abc</div>"
"<div id='block2'>def</div>");
ScrollableArea* viewport = LayoutViewport();
// Scroll position not adjusted, metric not updated.
ScrollLayoutViewport(ScrollOffset(0, 150));
histogram_tester.ExpectTotalCount("Layout.ScrollAnchor.AdjustedScrollOffset",
0);
// Height changed, verify metric updated once.
SetHeight(GetDocument().getElementById("block1"), 200);
histogram_tester.ExpectUniqueSample(
"Layout.ScrollAnchor.AdjustedScrollOffset", 1, 1);
EXPECT_EQ(250, viewport->ScrollOffsetInt().Height());
EXPECT_EQ(GetDocument().getElementById("block2")->GetLayoutObject(),
GetScrollAnchor(viewport).AnchorObject());
}
// TODO(skobes): Convert this to web-platform-tests when visual viewport API is
// launched (http://crbug.com/635031).
TEST_P(ScrollAnchorTest, VisualViewportAnchors) {
SetBodyInnerHTML(
"<style>"
" * { font-size: 1.2em; font-family: sans-serif; }"
" div { height: 100px; width: 20px; background-color: pink; }"
"</style>"
"<div id='div'></div>"
"<div id='text'><b>This is a scroll anchoring test</div>");
ScrollableArea* l_viewport = LayoutViewport();
VisualViewport& v_viewport = GetVisualViewport();
v_viewport.SetScale(2.0);
// No anchor at origin (0,0).
EXPECT_EQ(nullptr, GetScrollAnchor(l_viewport).AnchorObject());
// Scroll the visual viewport to bring #text to the top.
int top =
GetDocument().getElementById("text")->getBoundingClientRect()->top();
v_viewport.SetLocation(FloatPoint(0, top));
SetHeight(GetDocument().getElementById("div"), 10);
EXPECT_EQ(GetDocument().getElementById("text")->GetLayoutObject(),
GetScrollAnchor(l_viewport).AnchorObject());
EXPECT_EQ(top - 90, v_viewport.ScrollOffsetInt().Height());
SetHeight(GetDocument().getElementById("div"), 100);
EXPECT_EQ(GetDocument().getElementById("text")->GetLayoutObject(),
GetScrollAnchor(l_viewport).AnchorObject());
EXPECT_EQ(top, v_viewport.ScrollOffsetInt().Height());
// Scrolling the visual viewport should clear the anchor.
v_viewport.SetLocation(FloatPoint(0, 0));
EXPECT_EQ(nullptr, GetScrollAnchor(l_viewport).AnchorObject());
}
// Test that a non-anchoring scroll on scroller clears scroll anchors for all
// parent scrollers.
TEST_P(ScrollAnchorTest, ClearScrollAnchorsOnAncestors) {
SetBodyInnerHTML(
"<style>"
" body { height: 1000px } div { height: 200px }"
" #scroller { height: 100px; width: 200px; overflow: scroll; }"
"</style>"
"<div id='changer'>abc</div>"
"<div id='anchor'>def</div>"
"<div id='scroller'><div></div></div>");
ScrollableArea* viewport = LayoutViewport();
ScrollLayoutViewport(ScrollOffset(0, 250));
SetHeight(GetDocument().getElementById("changer"), 300);
EXPECT_EQ(350, viewport->ScrollOffsetInt().Height());
EXPECT_EQ(GetDocument().getElementById("anchor")->GetLayoutObject(),
GetScrollAnchor(viewport).AnchorObject());
// Scrolling the nested scroller should clear the anchor on the main frame.
ScrollableArea* scroller =
ScrollerForElement(GetDocument().getElementById("scroller"));
scroller->ScrollBy(ScrollOffset(0, 100), kUserScroll);
EXPECT_EQ(nullptr, GetScrollAnchor(viewport).AnchorObject());
}
TEST_P(ScrollAnchorTest, AncestorClearingWithSiblingReference) {
SetBodyInnerHTML(
"<style>"
".scroller {"
" overflow: scroll;"
" width: 400px;"
" height: 400px;"
"}"
".space {"
" width: 100px;"
" height: 600px;"
"}"
"</style>"
"<div id='s1' class='scroller'>"
" <div id='anchor' class='space'></div>"
"</div>"
"<div id='s2' class='scroller'>"
" <div class='space'></div>"
"</div>");
Element* s1 = GetDocument().getElementById("s1");
Element* s2 = GetDocument().getElementById("s2");
Element* anchor = GetDocument().getElementById("anchor");
// Set non-zero scroll offsets for #s1 and #document
s1->setScrollTop(100);
ScrollLayoutViewport(ScrollOffset(0, 100));
// Invalidate layout.
SetHeight(anchor, 500);
// This forces layout, during which both #s1 and #document will anchor to
// #anchor. Then the scroll clears #s2 and #document. Since #anchor is still
// referenced by #s1, its IsScrollAnchorObject bit must remain set.
s2->setScrollTop(100);
// This should clear #s1. If #anchor had its bit cleared already we would
// crash in update().
s1->RemoveChild(anchor);
Update();
}
TEST_P(ScrollAnchorTest, FractionalOffsetsAreRoundedBeforeComparing) {
SetBodyInnerHTML(
"<style> body { height: 1000px } </style>"
"<div id='block1' style='height: 50.4px'>abc</div>"
"<div id='block2' style='height: 100px'>def</div>");
ScrollableArea* viewport = LayoutViewport();
ScrollLayoutViewport(ScrollOffset(0, 100));
GetDocument().getElementById("block1")->setAttribute(HTMLNames::styleAttr,
"height: 50.6px");
Update();
EXPECT_EQ(101, viewport->ScrollOffsetInt().Height());
}
TEST_P(ScrollAnchorTest, AvoidStickyAnchorWhichMovesWithScroll) {
SetBodyInnerHTML(
"<style> body { height: 1000px } </style>"
"<div id='block1' style='height: 50px'>abc</div>"
"<div id='block2' style='height: 100px; position: sticky; top: 0;'>"
" def</div>");
ScrollableArea* viewport = LayoutViewport();
ScrollLayoutViewport(ScrollOffset(0, 60));
GetDocument().getElementById("block1")->setAttribute(HTMLNames::styleAttr,
"height: 100px");
Update();
EXPECT_EQ(60, viewport->ScrollOffsetInt().Height());
}
TEST_P(ScrollAnchorTest, AnchorWithLayerInScrollingDiv) {
SetBodyInnerHTML(
"<style>"
" #scroller { overflow: scroll; width: 500px; height: 400px; }"
" div { height: 100px }"
" #block2 { overflow: hidden }"
" #space { height: 1000px; }"
"</style>"
"<div id='scroller'><div id='space'>"
"<div id='block1'>abc</div>"
"<div id='block2'>def</div>"
"</div></div>");
ScrollableArea* scroller =
ScrollerForElement(GetDocument().getElementById("scroller"));
Element* block1 = GetDocument().getElementById("block1");
Element* block2 = GetDocument().getElementById("block2");
scroller->ScrollBy(ScrollOffset(0, 150), kUserScroll);
// In this layout pass we will anchor to #block2 which has its own PaintLayer.
SetHeight(block1, 200);
EXPECT_EQ(250, scroller->ScrollOffsetInt().Height());
EXPECT_EQ(block2->GetLayoutObject(),
GetScrollAnchor(scroller).AnchorObject());
// Test that the anchor object can be destroyed without affecting the scroll
// position.
block2->remove();
Update();
EXPECT_EQ(250, scroller->ScrollOffsetInt().Height());
}
// Verify that a nested scroller with a div that has its own PaintLayer can be
// removed without causing a crash. This test passes if it doesn't crash.
TEST_P(ScrollAnchorTest, RemoveScrollerWithLayerInScrollingDiv) {
SetBodyInnerHTML(
"<style>"
" body { height: 2000px }"
" #scroller { overflow: scroll; width: 500px; height: 400px}"
" #block1 { height: 100px; width: 100px; overflow: hidden}"
" #anchor { height: 1000px; }"
"</style>"
"<div id='changer1'></div>"
"<div id='scroller'>"
" <div id='changer2'></div>"
" <div id='block1'></div>"
" <div id='anchor'></div>"
"</div>");
ScrollableArea* viewport = LayoutViewport();
ScrollableArea* scroller =
ScrollerForElement(GetDocument().getElementById("scroller"));
Element* changer1 = GetDocument().getElementById("changer1");
Element* changer2 = GetDocument().getElementById("changer2");
Element* anchor = GetDocument().getElementById("anchor");
scroller->ScrollBy(ScrollOffset(0, 150), kUserScroll);
ScrollLayoutViewport(ScrollOffset(0, 50));
// In this layout pass both the inner and outer scroller will anchor to
// #anchor.
SetHeight(changer1, 100);
SetHeight(changer2, 100);
EXPECT_EQ(250, scroller->ScrollOffsetInt().Height());
EXPECT_EQ(anchor->GetLayoutObject(),
GetScrollAnchor(scroller).AnchorObject());
EXPECT_EQ(anchor->GetLayoutObject(),
GetScrollAnchor(viewport).AnchorObject());
// Test that the inner scroller can be destroyed without crashing.
GetDocument().getElementById("scroller")->remove();
Update();
}
TEST_P(ScrollAnchorTest, FlexboxDelayedClampingAlsoDelaysAdjustment) {
SetBodyInnerHTML(
"<style>"
" html { overflow: hidden; }"
" body {"
" position: absolute; display: flex;"
" top: 0; bottom: 0; margin: 0;"
" }"
" #scroller { overflow: auto; }"
" #spacer { width: 600px; height: 1200px; }"
" #before { height: 50px; }"
" #anchor {"
" width: 100px; height: 100px;"
" background-color: #8f8;"
" }"
"</style>"
"<div id='scroller'>"
" <div id='spacer'>"
" <div id='before'></div>"
" <div id='anchor'></div>"
" </div>"
"</div>");
Element* scroller = GetDocument().getElementById("scroller");
scroller->setScrollTop(100);
SetHeight(GetDocument().getElementById("before"), 100);
EXPECT_EQ(150, ScrollerForElement(scroller)->ScrollOffsetInt().Height());
}
TEST_P(ScrollAnchorTest, FlexboxDelayedAdjustmentRespectsSANACLAP) {
SetBodyInnerHTML(
"<style>"
" html { overflow: hidden; }"
" body {"
" position: absolute; display: flex;"
" top: 0; bottom: 0; margin: 0;"
" }"
" #scroller { overflow: auto; }"
" #spacer { width: 600px; height: 1200px; }"
" #anchor {"
" position: relative; top: 50px;"
" width: 100px; height: 100px;"
" background-color: #8f8;"
" }"
"</style>"
"<div id='scroller'>"
" <div id='spacer'>"
" <div id='anchor'></div>"
" </div>"
"</div>");
Element* scroller = GetDocument().getElementById("scroller");
scroller->setScrollTop(100);
GetDocument().getElementById("spacer")->setAttribute(HTMLNames::styleAttr,
"margin-top: 50px");
Update();
EXPECT_EQ(100, ScrollerForElement(scroller)->ScrollOffsetInt().Height());
}
// TODO(skobes): Convert this to web-platform-tests when document.rootScroller
// is launched (http://crbug.com/505516).
TEST_P(ScrollAnchorTest, NonDefaultRootScroller) {
SetBodyInnerHTML(
"<style>"
" ::-webkit-scrollbar {"
" width: 0px; height: 0px;"
" }"
" body, html {"
" margin: 0px; width: 100%; height: 100%;"
" }"
" #rootscroller {"
" overflow: scroll; width: 100%; height: 100%;"
" }"
" .spacer {"
" height: 600px; width: 100px;"
" }"
" #target {"
" height: 100px; width: 100px; background-color: red;"
" }"
"</style>"
"<div id='rootscroller'>"
" <div id='firstChild' class='spacer'></div>"
" <div id='target'></div>"
" <div class='spacer'></div>"
"</div>"
"<div class='spacer'></div>");
Element* root_scroller_element = GetDocument().getElementById("rootscroller");
NonThrowableExceptionState non_throw;
GetDocument().setRootScroller(root_scroller_element, non_throw);
GetDocument().View()->UpdateAllLifecyclePhases();
ScrollableArea* scroller = ScrollerForElement(root_scroller_element);
// By making the #rootScroller DIV the rootScroller, it should become the
// layout viewport on the RootFrameViewport.
ASSERT_EQ(scroller,
&GetDocument().View()->GetRootFrameViewport()->LayoutViewport());
// The #rootScroller DIV's anchor should have the RootFrameViewport set as
// the scroller, rather than the FrameView's anchor.
root_scroller_element->setScrollTop(600);
SetHeight(GetDocument().getElementById("firstChild"), 1000);
// Scroll anchoring should be applied to #rootScroller.
EXPECT_EQ(1000, scroller->GetScrollOffset().Height());
EXPECT_EQ(GetDocument().getElementById("target")->GetLayoutObject(),
GetScrollAnchor(scroller).AnchorObject());
// Scroll anchoring should not apply within main frame.
EXPECT_EQ(0, LayoutViewport()->GetScrollOffset().Height());
EXPECT_EQ(nullptr, GetScrollAnchor(LayoutViewport()).AnchorObject());
}
// This test verifies that scroll anchoring is disabled when the document is in
// printing mode.
TEST_P(ScrollAnchorTest, AnchoringDisabledForPrinting) {
SetBodyInnerHTML(
"<style> body { height: 1000px } div { height: 100px } </style>"
"<div id='block1'>abc</div>"
"<div id='block2'>def</div>");
ScrollableArea* viewport = LayoutViewport();
ScrollLayoutViewport(ScrollOffset(0, 150));
// This will trigger printing and layout.
PrintContext::NumberOfPages(GetDocument().GetFrame(), FloatSize(500, 500));
EXPECT_EQ(150, viewport->ScrollOffsetInt().Height());
EXPECT_EQ(nullptr, GetScrollAnchor(viewport).AnchorObject());
}
}