blob: 60e58d7a6f792814e153818b217b46b0f6abb112 [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/paint/PaintLayerScrollableArea.h"
#include "platform/testing/HistogramTester.h"
namespace blink {
using Corner = ScrollAnchor::Corner;
class ScrollAnchorTest : public RenderingTest {
public:
ScrollAnchorTest() {
RuntimeEnabledFeatures::setScrollAnchoringEnabled(true);
}
~ScrollAnchorTest() {
RuntimeEnabledFeatures::setScrollAnchoringEnabled(false);
}
protected:
void update() {
// TODO(skobes): Use SimTest instead of RenderingTest and move into
// Source/web?
document().view()->updateAllLifecyclePhases();
}
ScrollableArea* layoutViewport() {
return document().view()->layoutViewportScrollableArea();
}
VisualViewport& visualViewport() {
return document().view()->page()->frameHost().visualViewport();
}
ScrollableArea* scrollerForElement(Element* element) {
return toLayoutBox(element->layoutObject())->getScrollableArea();
}
ScrollAnchor& scrollAnchor(ScrollableArea* scroller) {
ASSERT(scroller->isFrameView() || scroller->isPaintLayerScrollableArea());
return *(scroller->scrollAnchor());
}
void setHeight(Element* element, int height) {
element->setAttribute(HTMLNames::styleAttr,
AtomicString(String::format("height: %dpx", height)));
update();
}
void scrollLayoutViewport(ScrollOffset delta) {
Element* scrollingElement = document().scrollingElement();
if (delta.width())
scrollingElement->setScrollLeft(scrollingElement->scrollLeft() +
delta.width());
if (delta.height())
scrollingElement->setScrollTop(scrollingElement->scrollTop() +
delta.height());
}
};
// 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_F(ScrollAnchorTest, UMAMetricUpdated) {
HistogramTester histogramTester;
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));
histogramTester.expectTotalCount("Layout.ScrollAnchor.AdjustedScrollOffset",
0);
// Height changed, verify metric updated once.
setHeight(document().getElementById("block1"), 200);
histogramTester.expectUniqueSample("Layout.ScrollAnchor.AdjustedScrollOffset",
1, 1);
EXPECT_EQ(250, viewport->scrollOffsetInt().height());
EXPECT_EQ(document().getElementById("block2")->layoutObject(),
scrollAnchor(viewport).anchorObject());
}
TEST_F(ScrollAnchorTest, Basic) {
setBodyInnerHTML(
"<style> body { height: 1000px } div { height: 100px } </style>"
"<div id='block1'>abc</div>"
"<div id='block2'>def</div>");
ScrollableArea* viewport = layoutViewport();
// No anchor at origin (0,0).
EXPECT_EQ(nullptr, scrollAnchor(viewport).anchorObject());
scrollLayoutViewport(ScrollOffset(0, 150));
setHeight(document().getElementById("block1"), 200);
EXPECT_EQ(250, viewport->scrollOffsetInt().height());
EXPECT_EQ(document().getElementById("block2")->layoutObject(),
scrollAnchor(viewport).anchorObject());
// ScrollableArea::userScroll should clear the anchor.
viewport->userScroll(ScrollByPrecisePixel, FloatSize(0, 100));
EXPECT_EQ(nullptr, scrollAnchor(viewport).anchorObject());
}
TEST_F(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* lViewport = layoutViewport();
VisualViewport& vViewport = visualViewport();
vViewport.setScale(2.0);
// No anchor at origin (0,0).
EXPECT_EQ(nullptr, scrollAnchor(lViewport).anchorObject());
// Scroll the visual viewport to bring #text to the top.
int top = document().getElementById("text")->getBoundingClientRect()->top();
vViewport.setLocation(FloatPoint(0, top));
setHeight(document().getElementById("div"), 10);
EXPECT_EQ(document().getElementById("text")->layoutObject(),
scrollAnchor(lViewport).anchorObject());
EXPECT_EQ(top - 90, vViewport.scrollOffsetInt().height());
setHeight(document().getElementById("div"), 100);
EXPECT_EQ(document().getElementById("text")->layoutObject(),
scrollAnchor(lViewport).anchorObject());
EXPECT_EQ(top, vViewport.scrollOffsetInt().height());
// Scrolling the visual viewport should clear the anchor.
vViewport.setLocation(FloatPoint(0, 0));
EXPECT_EQ(nullptr, scrollAnchor(lViewport).anchorObject());
}
// Test that we ignore the clipped content when computing visibility otherwise
// we may end up with an anchor that we think is in the viewport but is not.
TEST_F(ScrollAnchorTest, ClippedScrollersSkipped) {
setBodyInnerHTML(
"<style>"
" body { height: 2000px; }"
" #scroller { overflow: scroll; width: 500px; height: 300px; }"
" .anchor {"
" position:relative; height: 100px; width: 150px;"
" background-color: #afa; border: 1px solid gray;"
" }"
" #forceScrolling { height: 500px; background-color: #fcc; }"
"</style>"
"<div id='scroller'>"
" <div id='innerChanger'></div>"
" <div id='innerAnchor' class='anchor'></div>"
" <div id='forceScrolling'></div>"
"</div>"
"<div id='outerChanger'></div>"
"<div id='outerAnchor' class='anchor'></div>");
ScrollableArea* scroller =
scrollerForElement(document().getElementById("scroller"));
ScrollableArea* viewport = layoutViewport();
document().getElementById("scroller")->setScrollTop(100);
scrollLayoutViewport(ScrollOffset(0, 350));
setHeight(document().getElementById("innerChanger"), 200);
setHeight(document().getElementById("outerChanger"), 150);
EXPECT_EQ(300, scroller->scrollOffsetInt().height());
EXPECT_EQ(document().getElementById("innerAnchor")->layoutObject(),
scrollAnchor(scroller).anchorObject());
EXPECT_EQ(500, viewport->scrollOffsetInt().height());
EXPECT_EQ(document().getElementById("outerAnchor")->layoutObject(),
scrollAnchor(viewport).anchorObject());
}
// Test that scroll anchoring causes no visible jump when a layout change
// (such as removal of a DOM element) changes the scroll bounds.
TEST_F(ScrollAnchorTest, AnchoringWhenContentRemoved) {
setBodyInnerHTML(
"<style>"
" #changer { height: 1500px; }"
" #anchor {"
" width: 150px; height: 1000px; background-color: pink;"
" }"
"</style>"
"<div id='changer'></div>"
"<div id='anchor'></div>");
ScrollableArea* viewport = layoutViewport();
scrollLayoutViewport(ScrollOffset(0, 1600));
setHeight(document().getElementById("changer"), 0);
EXPECT_EQ(100, viewport->scrollOffsetInt().height());
EXPECT_EQ(document().getElementById("anchor")->layoutObject(),
scrollAnchor(viewport).anchorObject());
}
// Test that scroll anchoring causes no visible jump when a layout change
// (such as removal of a DOM element) changes the scroll bounds of a scrolling
// div.
TEST_F(ScrollAnchorTest, AnchoringWhenContentRemovedFromScrollingDiv) {
setBodyInnerHTML(
"<style>"
" #scroller { height: 500px; width: 200px; overflow: scroll; }"
" #changer { height: 1500px; }"
" #anchor {"
" width: 150px; height: 1000px; overflow: scroll;"
" }"
"</style>"
"<div id='scroller'>"
" <div id='changer'></div>"
" <div id='anchor'></div>"
"</div>");
ScrollableArea* scroller =
scrollerForElement(document().getElementById("scroller"));
document().getElementById("scroller")->setScrollTop(1600);
setHeight(document().getElementById("changer"), 0);
EXPECT_EQ(100, scroller->scrollOffsetInt().height());
EXPECT_EQ(document().getElementById("anchor")->layoutObject(),
scrollAnchor(scroller).anchorObject());
}
TEST_F(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));
document().getElementById("block1")->setAttribute(HTMLNames::styleAttr,
"height: 50.6px");
update();
EXPECT_EQ(101, viewport->scrollOffsetInt().height());
}
TEST_F(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(document().getElementById("scroller"));
Element* block1 = document().getElementById("block1");
Element* block2 = document().getElementById("block2");
scroller->scrollBy(ScrollOffset(0, 150), UserScroll);
// 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->layoutObject(), scrollAnchor(scroller).anchorObject());
// Test that the anchor object can be destroyed without affecting the scroll
// position.
block2->remove();
update();
EXPECT_EQ(250, scroller->scrollOffsetInt().height());
}
TEST_F(ScrollAnchorTest, ExcludeAnonymousCandidates) {
setBodyInnerHTML(
"<style>"
" body { height: 3500px }"
" #div {"
" position: relative; background-color: pink;"
" top: 5px; left: 5px; width: 100px; height: 3500px;"
" }"
" #inline { padding-left: 10px }"
"</style>"
"<div id='div'>"
" <a id='inline'>text</a>"
" <p id='block'>Some text</p>"
"</div>"
"<div id=a>after</div>");
ScrollableArea* viewport = layoutViewport();
Element* inlineElem = document().getElementById("inline");
EXPECT_TRUE(inlineElem->layoutObject()->parent()->isAnonymous());
// Scroll #div into view, making anonymous block a viable candidate.
document().getElementById("div")->scrollIntoView();
// Trigger layout and verify that we don't anchor to the anonymous block.
setHeight(document().getElementById("a"), 100);
update();
EXPECT_EQ(inlineElem->layoutObject()->slowFirstChild(),
scrollAnchor(viewport).anchorObject());
}
TEST_F(ScrollAnchorTest, FullyContainedInlineBlock) {
// Exercises every WalkStatus value:
// html, body -> Constrain
// #outer -> Continue
// #ib1, br -> Skip
// #ib2 -> Return
setBodyInnerHTML(
"<style>"
" body { height: 1000px }"
" #outer { line-height: 100px }"
" #ib1, #ib2 { display: inline-block }"
"</style>"
"<span id=outer>"
" <span id=ib1>abc</span>"
" <br><br>"
" <span id=ib2>def</span>"
"</span>");
scrollLayoutViewport(ScrollOffset(0, 150));
Element* ib1 = document().getElementById("ib1");
ib1->setAttribute(HTMLNames::styleAttr, "line-height: 150px");
update();
EXPECT_EQ(document().getElementById("ib2")->layoutObject(),
scrollAnchor(layoutViewport()).anchorObject());
}
TEST_F(ScrollAnchorTest, TextBounds) {
setBodyInnerHTML(
"<style>"
" body {"
" position: absolute;"
" font-size: 100px;"
" width: 200px;"
" height: 1000px;"
" line-height: 100px;"
" }"
"</style>"
"abc <b id=b>def</b> ghi"
"<div id=a>after</div>");
scrollLayoutViewport(ScrollOffset(0, 150));
setHeight(document().getElementById("a"), 100);
EXPECT_EQ(document().getElementById("b")->layoutObject()->slowFirstChild(),
scrollAnchor(layoutViewport()).anchorObject());
}
TEST_F(ScrollAnchorTest, ExcludeFixedPosition) {
setBodyInnerHTML(
"<style>"
" body { height: 1000px; padding: 20px; }"
" div { position: relative; top: 100px; }"
" #f { position: fixed }"
"</style>"
"<div id=f>fixed</div>"
"<div id=c>content</div>"
"<div id=a>after</div>");
scrollLayoutViewport(ScrollOffset(0, 50));
setHeight(document().getElementById("a"), 100);
EXPECT_EQ(document().getElementById("c")->layoutObject(),
scrollAnchor(layoutViewport()).anchorObject());
}
// This test verifies that position:absolute elements that stick to the viewport
// are not selected as anchors.
TEST_F(ScrollAnchorTest, ExcludeAbsolutePositionThatSticksToViewport) {
setBodyInnerHTML(
"<style>"
" body { margin: 0; }"
" #scroller { overflow: scroll; width: 500px; height: 400px; }"
" #space { height: 1000px; }"
" #abs {"
" position: absolute; background-color: red;"
" width: 100px; height: 100px;"
" }"
" #rel {"
" position: relative; background-color: green;"
" left: 50px; top: 100px; width: 100px; height: 75px;"
" }"
"</style>"
"<div id='scroller'><div id='space'>"
" <div id='abs'></div>"
" <div id='rel'></div>"
" <div id=a>after</div>"
"</div></div>");
Element* scrollerElement = document().getElementById("scroller");
ScrollableArea* scroller = scrollerForElement(scrollerElement);
Element* absPos = document().getElementById("abs");
Element* relPos = document().getElementById("rel");
scroller->scrollBy(ScrollOffset(0, 25), UserScroll);
setHeight(document().getElementById("a"), 100);
// When the scroller is position:static, the anchor cannot be
// position:absolute.
EXPECT_EQ(relPos->layoutObject(), scrollAnchor(scroller).anchorObject());
scrollerElement->setAttribute(HTMLNames::styleAttr, "position: relative");
update();
scroller->scrollBy(ScrollOffset(0, 25), UserScroll);
setHeight(document().getElementById("a"), 125);
// When the scroller is position:relative, the anchor may be
// position:absolute.
EXPECT_EQ(absPos->layoutObject(), scrollAnchor(scroller).anchorObject());
}
// Test that we descend into zero-height containers that have overflowing
// content.
TEST_F(ScrollAnchorTest, DescendsIntoContainerWithOverflow) {
setBodyInnerHTML(
"<style>"
" body { height: 1000; }"
" #outer { width: 300px; }"
" #zeroheight { height: 0px; }"
" #changer { height: 100px; background-color: red; }"
" #bottom { margin-top: 600px; }"
"</style>"
"<div id='outer'>"
" <div id='zeroheight'>"
" <div id='changer'></div>"
" <div id='bottom'>bottom</div>"
" </div>"
"</div>");
ScrollableArea* viewport = layoutViewport();
scrollLayoutViewport(ScrollOffset(0, 200));
setHeight(document().getElementById("changer"), 200);
EXPECT_EQ(300, viewport->scrollOffsetInt().height());
EXPECT_EQ(document().getElementById("bottom")->layoutObject(),
scrollAnchor(viewport).anchorObject());
}
// Test that we descend into zero-height containers that have floating content.
TEST_F(ScrollAnchorTest, DescendsIntoContainerWithFloat) {
setBodyInnerHTML(
"<style>"
" body { height: 1000; }"
" #outer { width: 300px; }"
" #outer:after { content: ' '; clear:both; display: table; }"
" #float {"
" float: left; background-color: #ccc;"
" height: 500px; width: 100%;"
" }"
" #inner { height: 21px; background-color:#7f0; }"
"</style>"
"<div id='outer'>"
" <div id='zeroheight'>"
" <div id='float'>"
" <div id='inner'></div>"
" </div>"
" </div>"
"</div>"
"<div id=a>after</div>");
EXPECT_EQ(0,
toLayoutBox(document().getElementById("zeroheight")->layoutObject())
->size()
.height());
ScrollableArea* viewport = layoutViewport();
scrollLayoutViewport(ScrollOffset(0, 200));
setHeight(document().getElementById("a"), 100);
EXPECT_EQ(200, viewport->scrollOffsetInt().height());
EXPECT_EQ(document().getElementById("float")->layoutObject(),
scrollAnchor(viewport).anchorObject());
}
// This test verifies that scroll anchoring is disabled when any element within
// the main scroller changes its in-flow state.
TEST_F(ScrollAnchorTest, ChangeInFlowStateDisablesAnchoringForMainScroller) {
setBodyInnerHTML(
"<style>"
" body { height: 1000px; }"
" #header { background-color: #F5B335; height: 50px; width: 100%; }"
" #content { background-color: #D3D3D3; height: 200px; }"
"</style>"
"<div id='header'></div>"
"<div id='content'></div>");
ScrollableArea* viewport = layoutViewport();
scrollLayoutViewport(ScrollOffset(0, 200));
document().getElementById("header")->setAttribute(HTMLNames::styleAttr,
"position: fixed;");
update();
EXPECT_EQ(200, viewport->scrollOffsetInt().height());
}
// This test verifies that scroll anchoring is disabled when any element within
// a scrolling div changes its in-flow state.
TEST_F(ScrollAnchorTest, ChangeInFlowStateDisablesAnchoringForScrollingDiv) {
setBodyInnerHTML(
"<style>"
" #container { position: relative; width: 500px; }"
" #scroller { height: 200px; overflow: scroll; }"
" #changer { background-color: #F5B335; height: 50px; width: 100%; }"
" #anchor { background-color: #D3D3D3; height: 300px; }"
"</style>"
"<div id='container'>"
" <div id='scroller'>"
" <div id='changer'></div>"
" <div id='anchor'></div>"
" </div>"
"</div>");
ScrollableArea* scroller =
scrollerForElement(document().getElementById("scroller"));
document().getElementById("scroller")->setScrollTop(100);
document().getElementById("changer")->setAttribute(HTMLNames::styleAttr,
"position: absolute;");
update();
EXPECT_EQ(100, scroller->scrollOffsetInt().height());
}
TEST_F(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 = document().getElementById("scroller");
scroller->setScrollTop(100);
setHeight(document().getElementById("before"), 100);
EXPECT_EQ(150, scrollerForElement(scroller)->scrollOffsetInt().height());
}
TEST_F(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 = document().getElementById("scroller");
scroller->setScrollTop(100);
document().getElementById("spacer")->setAttribute(HTMLNames::styleAttr,
"margin-top: 50px");
update();
EXPECT_EQ(100, scrollerForElement(scroller)->scrollOffsetInt().height());
}
// Test then an element and its children are not selected as the anchor when
// it has the overflow-anchor property set to none.
TEST_F(ScrollAnchorTest, OptOutElement) {
setBodyInnerHTML(
"<style>"
" body { height: 1000px }"
" .div {"
" height: 100px; width: 100px;"
" border: 1px solid gray; background-color: #afa;"
" }"
" #innerDiv {"
" height: 50px; width: 50px;"
" border: 1px solid gray; background-color: pink;"
" }"
"</style>"
"<div id='changer'></div>"
"<div class='div' id='firstDiv'><div id='innerDiv'></div></div>"
"<div class='div' id='secondDiv'></div>");
ScrollableArea* viewport = layoutViewport();
scrollLayoutViewport(ScrollOffset(0, 50));
// No opt-out.
setHeight(document().getElementById("changer"), 100);
EXPECT_EQ(150, viewport->scrollOffsetInt().height());
EXPECT_EQ(document().getElementById("innerDiv")->layoutObject(),
scrollAnchor(viewport).anchorObject());
// Clear anchor and opt-out element.
scrollLayoutViewport(ScrollOffset(0, 10));
document()
.getElementById("firstDiv")
->setAttribute(HTMLNames::styleAttr,
AtomicString("overflow-anchor: none"));
update();
// Opted out element and it's children skipped.
setHeight(document().getElementById("changer"), 200);
EXPECT_EQ(260, viewport->scrollOffsetInt().height());
EXPECT_EQ(document().getElementById("secondDiv")->layoutObject(),
scrollAnchor(viewport).anchorObject());
}
TEST_F(ScrollAnchorTest,
SuppressAnchorNodeAncestorChangingLayoutAffectingProperty) {
setBodyInnerHTML(
"<style> body { height: 1000px } div { height: 100px } </style>"
"<div id='block1'>abc</div>");
ScrollableArea* viewport = layoutViewport();
scrollLayoutViewport(ScrollOffset(0, 50));
document().body()->setAttribute(HTMLNames::styleAttr, "padding-top: 20px");
update();
EXPECT_EQ(50, viewport->scrollOffsetInt().height());
EXPECT_EQ(nullptr, scrollAnchor(viewport).anchorObject());
}
TEST_F(ScrollAnchorTest, AnchorNodeAncestorChangingNonLayoutAffectingProperty) {
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));
document().body()->setAttribute(HTMLNames::styleAttr, "color: red");
setHeight(document().getElementById("block1"), 200);
EXPECT_EQ(250, viewport->scrollOffsetInt().height());
EXPECT_EQ(document().getElementById("block2")->layoutObject(),
scrollAnchor(viewport).anchorObject());
}
TEST_F(ScrollAnchorTest, TransformIsLayoutAffecting) {
setBodyInnerHTML(
"<style>"
" body { height: 1000px }"
" #block1 { height: 100px }"
"</style>"
"<div id='block1'>abc</div>"
"<div id=a>after</div>");
ScrollableArea* viewport = layoutViewport();
scrollLayoutViewport(ScrollOffset(0, 50));
document().getElementById("block1")->setAttribute(
HTMLNames::styleAttr, "transform: matrix(1, 0, 0, 1, 25, 25);");
update();
document().getElementById("block1")->setAttribute(
HTMLNames::styleAttr, "transform: matrix(1, 0, 0, 1, 50, 50);");
setHeight(document().getElementById("a"), 100);
update();
EXPECT_EQ(50, viewport->scrollOffsetInt().height());
EXPECT_EQ(nullptr, scrollAnchor(viewport).anchorObject());
}
TEST_F(ScrollAnchorTest, OptOutBody) {
setBodyInnerHTML(
"<style>"
" body { height: 2000px; overflow-anchor: none; }"
" #scroller { overflow: scroll; width: 500px; height: 300px; }"
" .anchor {"
" position:relative; height: 100px; width: 150px;"
" background-color: #afa; border: 1px solid gray;"
" }"
" #forceScrolling { height: 500px; background-color: #fcc; }"
"</style>"
"<div id='outerChanger'></div>"
"<div id='outerAnchor' class='anchor'></div>"
"<div id='scroller'>"
" <div id='innerChanger'></div>"
" <div id='innerAnchor' class='anchor'></div>"
" <div id='forceScrolling'></div>"
"</div>");
ScrollableArea* scroller =
scrollerForElement(document().getElementById("scroller"));
ScrollableArea* viewport = layoutViewport();
document().getElementById("scroller")->setScrollTop(100);
scrollLayoutViewport(ScrollOffset(0, 100));
setHeight(document().getElementById("innerChanger"), 200);
setHeight(document().getElementById("outerChanger"), 150);
// Scroll anchoring should apply within #scroller.
EXPECT_EQ(300, scroller->scrollOffsetInt().height());
EXPECT_EQ(document().getElementById("innerAnchor")->layoutObject(),
scrollAnchor(scroller).anchorObject());
// Scroll anchoring should not apply within main frame.
EXPECT_EQ(100, viewport->scrollOffsetInt().height());
EXPECT_EQ(nullptr, scrollAnchor(viewport).anchorObject());
}
TEST_F(ScrollAnchorTest, OptOutScrollingDiv) {
setBodyInnerHTML(
"<style>"
" body { height: 2000px; }"
" #scroller {"
" overflow: scroll; width: 500px; height: 300px;"
" overflow-anchor: none;"
" }"
" .anchor {"
" position:relative; height: 100px; width: 150px;"
" background-color: #afa; border: 1px solid gray;"
" }"
" #forceScrolling { height: 500px; background-color: #fcc; }"
"</style>"
"<div id='outerChanger'></div>"
"<div id='outerAnchor' class='anchor'></div>"
"<div id='scroller'>"
" <div id='innerChanger'></div>"
" <div id='innerAnchor' class='anchor'></div>"
" <div id='forceScrolling'></div>"
"</div>");
ScrollableArea* scroller =
scrollerForElement(document().getElementById("scroller"));
ScrollableArea* viewport = layoutViewport();
document().getElementById("scroller")->setScrollTop(100);
scrollLayoutViewport(ScrollOffset(0, 100));
setHeight(document().getElementById("innerChanger"), 200);
setHeight(document().getElementById("outerChanger"), 150);
// Scroll anchoring should not apply within #scroller.
EXPECT_EQ(100, scroller->scrollOffsetInt().height());
EXPECT_EQ(nullptr, scrollAnchor(scroller).anchorObject());
// Scroll anchoring should apply within main frame.
EXPECT_EQ(250, viewport->scrollOffsetInt().height());
EXPECT_EQ(document().getElementById("outerAnchor")->layoutObject(),
scrollAnchor(viewport).anchorObject());
}
TEST_F(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* rootScrollerElement = document().getElementById("rootscroller");
NonThrowableExceptionState nonThrow;
document().setRootScroller(rootScrollerElement, nonThrow);
document().view()->updateAllLifecyclePhases();
ScrollableArea* scroller = scrollerForElement(rootScrollerElement);
// By making the #rootScroller DIV the rootScroller, it should become the
// layout viewport on the RootFrameViewport.
ASSERT_EQ(scroller,
&document().view()->getRootFrameViewport()->layoutViewport());
// The #rootScroller DIV's anchor should have the RootFrameViewport set as
// the scroller, rather than the FrameView's anchor.
rootScrollerElement->setScrollTop(600);
setHeight(document().getElementById("firstChild"), 1000);
// Scroll anchoring should be applied to #rootScroller.
EXPECT_EQ(1000, scroller->scrollOffset().height());
EXPECT_EQ(document().getElementById("target")->layoutObject(),
scrollAnchor(scroller).anchorObject());
// Scroll anchoring should not apply within main frame.
EXPECT_EQ(0, layoutViewport()->scrollOffset().height());
EXPECT_EQ(nullptr, scrollAnchor(layoutViewport()).anchorObject());
}
class ScrollAnchorCornerTest : public ScrollAnchorTest {
protected:
void checkCorner(Corner corner,
ScrollOffset startOffset,
ScrollOffset expectedAdjustment) {
ScrollableArea* viewport = layoutViewport();
Element* element = document().getElementById("changer");
viewport->setScrollOffset(startOffset, UserScroll);
element->setAttribute(HTMLNames::classAttr, "change");
update();
ScrollOffset endPos = startOffset;
endPos += expectedAdjustment;
EXPECT_EQ(endPos, viewport->scrollOffset());
EXPECT_EQ(document().getElementById("a")->layoutObject(),
scrollAnchor(viewport).anchorObject());
EXPECT_EQ(corner, scrollAnchor(viewport).corner());
element->removeAttribute(HTMLNames::classAttr);
update();
}
};
// Verify that we anchor to the top left corner of an element for LTR.
TEST_F(ScrollAnchorCornerTest, CornersLTR) {
setBodyInnerHTML(
"<style>"
" body { position: relative; width: 1220px; height: 920px; }"
" #a { width: 400px; height: 300px; }"
" .change { height: 100px; }"
"</style>"
"<div id='changer'></div>"
"<div id='a'></div>");
checkCorner(Corner::TopLeft, ScrollOffset(20, 20), ScrollOffset(0, 100));
}
// Verify that we anchor to the top left corner of an anchor element for
// vertical-lr writing mode.
TEST_F(ScrollAnchorCornerTest, CornersVerticalLR) {
setBodyInnerHTML(
"<style>"
" html { writing-mode: vertical-lr; }"
" body { position: relative; width: 1220px; height: 920px; }"
" #a { width: 400px; height: 300px; }"
" .change { width: 100px; }"
"</style>"
"<div id='changer'></div>"
"<div id='a'></div>");
checkCorner(Corner::TopLeft, ScrollOffset(20, 20), ScrollOffset(100, 0));
}
// Verify that we anchor to the top right corner of an anchor element for RTL.
TEST_F(ScrollAnchorCornerTest, CornersRTL) {
setBodyInnerHTML(
"<style>"
" html { direction: rtl; }"
" body { position: relative; width: 1220px; height: 920px; }"
" #a { width: 400px; height: 300px; }"
" .change { height: 100px; }"
"</style>"
"<div id='changer'></div>"
"<div id='a'></div>");
checkCorner(Corner::TopRight, ScrollOffset(-20, 20), ScrollOffset(0, 100));
}
// Verify that we anchor to the top right corner of an anchor element for
// vertical-lr writing mode.
TEST_F(ScrollAnchorCornerTest, CornersVerticalRL) {
setBodyInnerHTML(
"<style>"
" html { writing-mode: vertical-rl; }"
" body { position: relative; width: 1220px; height: 920px; }"
" #a { width: 400px; height: 300px; }"
" .change { width: 100px; }"
"</style>"
"<div id='changer'></div>"
"<div id='a'></div>");
checkCorner(Corner::TopRight, ScrollOffset(-20, 20), ScrollOffset(-100, 0));
}
TEST_F(ScrollAnchorTest, IgnoreNonBlockLayoutAxis) {
setBodyInnerHTML(
"<style>"
" body {"
" margin: 0; line-height: 0;"
" width: 1200px; height: 1200px;"
" }"
" div {"
" width: 100px; height: 100px;"
" border: 5px solid gray;"
" display: inline-block;"
" box-sizing: border-box;"
" }"
"</style>"
"<div id='a'></div><br>"
"<div id='b'></div><div id='c'></div>");
ScrollableArea* viewport = layoutViewport();
scrollLayoutViewport(ScrollOffset(150, 0));
Element* a = document().getElementById("a");
Element* b = document().getElementById("b");
Element* c = document().getElementById("c");
a->setAttribute(HTMLNames::styleAttr, "height: 150px");
update();
EXPECT_EQ(ScrollOffset(150, 0), viewport->scrollOffset());
EXPECT_EQ(nullptr, scrollAnchor(viewport).anchorObject());
scrollLayoutViewport(ScrollOffset(0, 50));
a->setAttribute(HTMLNames::styleAttr, "height: 200px");
b->setAttribute(HTMLNames::styleAttr, "width: 150px");
update();
EXPECT_EQ(ScrollOffset(150, 100), viewport->scrollOffset());
EXPECT_EQ(c->layoutObject(), scrollAnchor(viewport).anchorObject());
a->setAttribute(HTMLNames::styleAttr, "height: 100px");
b->setAttribute(HTMLNames::styleAttr, "width: 100px");
document().documentElement()->setAttribute(HTMLNames::styleAttr,
"writing-mode: vertical-rl");
document().scrollingElement()->setScrollLeft(0);
document().scrollingElement()->setScrollTop(0);
scrollLayoutViewport(ScrollOffset(0, 150));
a->setAttribute(HTMLNames::styleAttr, "width: 150px");
update();
EXPECT_EQ(ScrollOffset(0, 150), viewport->scrollOffset());
EXPECT_EQ(nullptr, scrollAnchor(viewport).anchorObject());
scrollLayoutViewport(ScrollOffset(-50, 0));
a->setAttribute(HTMLNames::styleAttr, "width: 200px");
b->setAttribute(HTMLNames::styleAttr, "height: 150px");
update();
EXPECT_EQ(ScrollOffset(-100, 150), viewport->scrollOffset());
EXPECT_EQ(c->layoutObject(), scrollAnchor(viewport).anchorObject());
}
}