// 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 "third_party/blink/renderer/core/layout/scroll_anchor.h" #include "build/build_config.h" #include "third_party/blink/public/common/input/web_mouse_event.h" #include "third_party/blink/renderer/core/dom/static_node_list.h" #include "third_party/blink/renderer/core/frame/root_frame_viewport.h" #include "third_party/blink/renderer/core/frame/visual_viewport.h" #include "third_party/blink/renderer/core/geometry/dom_rect.h" #include "third_party/blink/renderer/core/layout/layout_box.h" #include "third_party/blink/renderer/core/page/print_context.h" #include "third_party/blink/renderer/core/paint/paint_layer_scrollable_area.h" #include "third_party/blink/renderer/core/scroll/scroll_animator_base.h" #include "third_party/blink/renderer/core/testing/core_unit_test_helper.h" #include "third_party/blink/renderer/platform/testing/histogram_tester.h" #include "third_party/blink/renderer/platform/testing/runtime_enabled_features_test_helpers.h" namespace blink { using Corner = ScrollAnchor::Corner; class ScrollAnchorTest : public testing::WithParamInterface, private ScopedLayoutNGForTest, public RenderingTest { public: ScrollAnchorTest() : ScopedLayoutNGForTest(GetParam()) {} protected: void SetUp() override { EnableCompositing(); RenderingTest::SetUp(); } void Update() { // TODO(skobes): Use SimTest instead of RenderingTest and move into // Source/web? UpdateAllLifecyclePhasesForTest(); } ScrollableArea* LayoutViewport() { return GetDocument().View()->LayoutViewport(); } VisualViewport& GetVisualViewport() { return GetDocument().View()->GetPage()->GetVisualViewport(); } ScrollableArea* ScrollerForElement(Element* element) { return ToLayoutBox(element->GetLayoutObject())->GetScrollableArea(); } ScrollAnchor& GetScrollAnchor(ScrollableArea* scroller) { DCHECK(scroller->IsPaintLayerScrollableArea()); return *(scroller->GetScrollAnchor()); } void SetHeight(Element* element, int height) { element->setAttribute(html_names::kStyleAttr, 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()); } void ValidateSerializedAnchor(const String& expected_selector, const LayoutPoint& expected_offset) { SerializedAnchor serialized = GetScrollAnchor(LayoutViewport()).GetSerializedAnchor(); EXPECT_TRUE(serialized.IsValid()); EXPECT_EQ(serialized.selector, expected_selector); EXPECT_EQ(serialized.relative_offset, expected_offset); StaticElementList* ele_list = GetDocument().QuerySelectorAll(AtomicString(serialized.selector)); EXPECT_EQ(ele_list->length(), 1u); } Scrollbar* VerticalScrollbarForElement(Element* element) { return ToLayoutBox(element->GetLayoutObject()) ->GetScrollableArea() ->VerticalScrollbar(); } void MouseDownOnVerticalScrollbar(Scrollbar* scrollbar) { DCHECK_EQ(true, scrollbar->GetTheme().AllowsHitTest()); int thumb_center = scrollbar->GetTheme().ThumbPosition(*scrollbar) + scrollbar->GetTheme().ThumbLength(*scrollbar) / 2; scrollbar_drag_point_ = gfx::PointF(scrollbar->GetScrollableArea() ->ConvertFromScrollbarToContainingEmbeddedContentView( *scrollbar, IntPoint(0, thumb_center))); scrollbar->MouseDown(blink::WebMouseEvent( blink::WebInputEvent::kMouseDown, *scrollbar_drag_point_, *scrollbar_drag_point_, blink::WebPointerProperties::Button::kLeft, 0, blink::WebInputEvent::kNoModifiers, base::TimeTicks::Now())); } void MouseDragVerticalScrollbar(Scrollbar* scrollbar, float scroll_delta_y) { DCHECK(scrollbar_drag_point_); ScrollableArea* scroller = scrollbar->GetScrollableArea(); scrollbar_drag_point_->Offset( 0, scroll_delta_y * (scrollbar->GetTheme().TrackLength(*scrollbar) - scrollbar->GetTheme().ThumbLength(*scrollbar)) / (scroller->MaximumScrollOffset().Height() - scroller->MinimumScrollOffset().Height())); scrollbar->MouseMoved(blink::WebMouseEvent( blink::WebInputEvent::kMouseMove, *scrollbar_drag_point_, *scrollbar_drag_point_, blink::WebPointerProperties::Button::kLeft, 0, blink::WebInputEvent::kNoModifiers, base::TimeTicks::Now())); } void MouseUpOnVerticalScrollbar(Scrollbar* scrollbar) { DCHECK(scrollbar_drag_point_); scrollbar->MouseDown(blink::WebMouseEvent( blink::WebInputEvent::kMouseUp, *scrollbar_drag_point_, *scrollbar_drag_point_, blink::WebPointerProperties::Button::kLeft, 0, blink::WebInputEvent::kNoModifiers, base::TimeTicks::Now())); scrollbar_drag_point_.reset(); } base::Optional scrollbar_drag_point_; }; INSTANTIATE_TEST_SUITE_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(R"HTML(
abc
def
)HTML"); ScrollableArea* viewport = LayoutViewport(); // Scroll position not adjusted, metric not updated. ScrollLayoutViewport(ScrollOffset(0, 150)); histogram_tester.ExpectTotalCount("Layout.ScrollAnchor.AdjustedScrollOffset", 0); histogram_tester.ExpectTotalCount( "Layout.ScrollAnchor.TimeToComputeAnchorNodeSelector", 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()); GetScrollAnchor(viewport).GetSerializedAnchor(); histogram_tester.ExpectTotalCount( "Layout.ScrollAnchor.TimeToComputeAnchorNodeSelector", 1); // 7 == "#block2".length() histogram_tester.ExpectUniqueSample( "Layout.ScrollAnchor.SerializedAnchorSelectorLength", 7, 1); // Clear the current anchor so that we can test restoration histograms. // Restoration only proceeds if there isn't an existing anchor. GetScrollAnchor(viewport).Clear(); SerializedAnchor bad_anchor("##foobar", LayoutPoint(0, 0)); EXPECT_FALSE(GetScrollAnchor(LayoutViewport()).RestoreAnchor(bad_anchor)); histogram_tester.ExpectBucketCount("Layout.ScrollAnchor.RestorationStatus", ScrollAnchor::kFailedBadSelector, 1); SerializedAnchor bad_anchor2("#bl", LayoutPoint(0, 0)); EXPECT_FALSE(GetScrollAnchor(LayoutViewport()).RestoreAnchor(bad_anchor2)); histogram_tester.ExpectBucketCount("Layout.ScrollAnchor.RestorationStatus", ScrollAnchor::kFailedNoMatches, 1); SerializedAnchor bad_anchor3("script", LayoutPoint(0, -1000)); EXPECT_FALSE(GetScrollAnchor(LayoutViewport()).RestoreAnchor(bad_anchor3)); histogram_tester.ExpectBucketCount("Layout.ScrollAnchor.RestorationStatus", ScrollAnchor::kFailedNoValidMatches, 1); SerializedAnchor serialized_anchor("#block1", LayoutPoint(0, 0)); EXPECT_TRUE( GetScrollAnchor(LayoutViewport()).RestoreAnchor(serialized_anchor)); histogram_tester.ExpectTotalCount("Layout.ScrollAnchor.TimeToRestoreAnchor", 4); histogram_tester.ExpectBucketCount("Layout.ScrollAnchor.RestorationStatus", ScrollAnchor::kSuccess, 1); } // TODO(skobes): Convert this to web-platform-tests when visual viewport API is // launched (http://crbug.com/635031). TEST_P(ScrollAnchorTest, VisualViewportAnchors) { SetBodyInnerHTML(R"HTML(
This is a scroll anchoring test
)HTML"); 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(R"HTML(
abc
def
)HTML"); 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), mojom::blink::ScrollType::kUser); EXPECT_EQ(nullptr, GetScrollAnchor(viewport).AnchorObject()); } TEST_P(ScrollAnchorTest, AncestorClearingWithSiblingReference) { SetBodyInnerHTML(R"HTML(
)HTML"); 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(R"HTML(
abc
def
)HTML"); ScrollableArea* viewport = LayoutViewport(); ScrollLayoutViewport(ScrollOffset(0, 100)); GetDocument().getElementById("block1")->setAttribute(html_names::kStyleAttr, "height: 50.6px"); Update(); EXPECT_EQ(101, viewport->ScrollOffsetInt().Height()); } TEST_P(ScrollAnchorTest, AvoidStickyAnchorWhichMovesWithScroll) { SetBodyInnerHTML(R"HTML(
abc
def
)HTML"); ScrollableArea* viewport = LayoutViewport(); ScrollLayoutViewport(ScrollOffset(0, 60)); GetDocument().getElementById("block1")->setAttribute(html_names::kStyleAttr, "height: 100px"); Update(); EXPECT_EQ(60, viewport->ScrollOffsetInt().Height()); } TEST_P(ScrollAnchorTest, AnchorWithLayerInScrollingDiv) { SetBodyInnerHTML(R"HTML(
abc
def
)HTML"); ScrollableArea* scroller = ScrollerForElement(GetDocument().getElementById("scroller")); Element* block1 = GetDocument().getElementById("block1"); Element* block2 = GetDocument().getElementById("block2"); scroller->ScrollBy(ScrollOffset(0, 150), mojom::blink::ScrollType::kUser); // 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()); } TEST_P(ScrollAnchorTest, AnchorWhileDraggingScrollbar) { // Dragging the scrollbar is inherently inaccurate. Allow many pixels slop in // the scroll position. const int kScrollbarDragAccuracy = 10; USE_NON_OVERLAY_SCROLLBARS(); SetBodyInnerHTML(R"HTML(
abc
def
)HTML"); Element* scroller_element = GetDocument().getElementById("scroller"); ScrollableArea* scroller = ScrollerForElement(scroller_element); Element* block1 = GetDocument().getElementById("block1"); Element* block2 = GetDocument().getElementById("block2"); Scrollbar* scrollbar = VerticalScrollbarForElement(scroller_element); scroller->MouseEnteredScrollbar(*scrollbar); MouseDownOnVerticalScrollbar(scrollbar); MouseDragVerticalScrollbar(scrollbar, 150); EXPECT_NEAR(150, scroller->GetScrollOffset().Height(), kScrollbarDragAccuracy); // In this layout pass we will anchor to #block2 which has its own PaintLayer. SetHeight(block1, 200); EXPECT_NEAR(250, scroller->ScrollOffsetInt().Height(), kScrollbarDragAccuracy); EXPECT_EQ(block2->GetLayoutObject(), GetScrollAnchor(scroller).AnchorObject()); // If we continue dragging the scroller should scroll from the newly anchored // position. MouseDragVerticalScrollbar(scrollbar, 10); EXPECT_NEAR(260, scroller->ScrollOffsetInt().Height(), kScrollbarDragAccuracy); MouseUpOnVerticalScrollbar(scrollbar); } // 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(R"HTML(
)HTML"); 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), mojom::blink::ScrollType::kUser); 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(R"HTML(
)HTML"); 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(R"HTML(
)HTML"); Element* scroller = GetDocument().getElementById("scroller"); scroller->setScrollTop(100); GetDocument().getElementById("spacer")->setAttribute(html_names::kStyleAttr, "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(R"HTML(
)HTML"); Element* root_scroller_element = GetDocument().getElementById("rootscroller"); NonThrowableExceptionState non_throw; GetDocument().setRootScroller(root_scroller_element, non_throw); UpdateAllLifecyclePhasesForTest(); 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(R"HTML(
abc
def
)HTML"); 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()); } TEST_P(ScrollAnchorTest, SerializeAnchorSimple) { SetBodyInnerHTML(R"HTML(
abc
def
")HTML"); ScrollLayoutViewport(ScrollOffset(0, 150)); ValidateSerializedAnchor("#block2", LayoutPoint(0, -50)); } TEST_P(ScrollAnchorTest, SerializeAnchorUsesTagname) { SetBodyInnerHTML(R"HTML(
abc def
)HTML"); ScrollLayoutViewport(ScrollOffset(0, 150)); ValidateSerializedAnchor("#ancestor>span", LayoutPoint(0, -50)); } TEST_P(ScrollAnchorTest, SerializeAnchorSetsIsAnchorBit) { SetBodyInnerHTML(R"HTML(
abc
")HTML"); ScrollLayoutViewport(ScrollOffset(0, 50)); ValidateSerializedAnchor("#anchor", LayoutPoint(0, -50)); Element* s1 = GetDocument().getElementById("s1"); Element* anchor = GetDocument().getElementById("anchor"); // Remove the anchor. If the IsScrollAnchorOBject bit is set as it should be, // the anchor object will get cleaned up correctly. s1->RemoveChild(anchor); // Trigger a re-layout, which will crash if it wasn't properly cleaned up when // removing it from the DOM. ScrollLayoutViewport(ScrollOffset(0, 25)); } TEST_P(ScrollAnchorTest, SerializeAnchorSetsSavedRelativeOffset) { SetBodyInnerHTML(R"HTML(
abc
def
")HTML"); ScrollLayoutViewport(ScrollOffset(0, 150)); GetScrollAnchor(LayoutViewport()).Clear(); ValidateSerializedAnchor("#block2", LayoutPoint(0, -50)); SetHeight(GetDocument().getElementById("block1"), 200); EXPECT_EQ(LayoutViewport()->ScrollOffsetInt().Height(), 250); } TEST_P(ScrollAnchorTest, SerializeAnchorUsesClassname) { SetBodyInnerHTML(R"HTML(
abc def
)HTML"); ScrollLayoutViewport(ScrollOffset(0, 150)); ValidateSerializedAnchor("#ancestor>.barbaz", LayoutPoint(0, -50)); } TEST_P(ScrollAnchorTest, SerializeAnchorUsesNthChild) { SetBodyInnerHTML(R"HTML(
abc def
)HTML"); ScrollLayoutViewport(ScrollOffset(0, 150)); ValidateSerializedAnchor("#ancestor>:nth-child(2)", LayoutPoint(0, -50)); } TEST_P(ScrollAnchorTest, SerializeAnchorUsesLeastSpecificSelector) { SetBodyInnerHTML(R"HTML(
abc
def
ghi
)HTML"); ScrollLayoutViewport(ScrollOffset(0, 250)); ValidateSerializedAnchor("#ancestor>:nth-child(3)>.foobar>div", LayoutPoint(0, -50)); } TEST_P(ScrollAnchorTest, SerializeAnchorWithNoIdAttribute) { SetBodyInnerHTML(R"HTML(
abc
def
ghi
)HTML"); ScrollLayoutViewport(ScrollOffset(0, 250)); ValidateSerializedAnchor("html>body>div>:nth-child(3)>.foobar>div", LayoutPoint(0, -50)); } TEST_P(ScrollAnchorTest, SerializeAnchorChangesWithScroll) { SetBodyInnerHTML(R"HTML(
abc def
)HTML"); ScrollLayoutViewport(ScrollOffset(0, 50)); ValidateSerializedAnchor("#ancestor>.foobar", LayoutPoint(0, -50)); ScrollLayoutViewport(ScrollOffset(0, 100)); ValidateSerializedAnchor("#ancestor>.barbaz", LayoutPoint(0, -50)); ScrollLayoutViewport(ScrollOffset(0, -100)); ValidateSerializedAnchor("#ancestor>.foobar", LayoutPoint(0, -50)); ScrollLayoutViewport(ScrollOffset(0, -49)); ValidateSerializedAnchor("#ancestor>.foobar", LayoutPoint(0, -1)); } TEST_P(ScrollAnchorTest, SerializeAnchorVerticalWritingMode) { SetBodyInnerHTML(R"HTML(
abc
def
)HTML"); ScrollLayoutViewport(ScrollOffset(50, 0)); ValidateSerializedAnchor("html>body>.foobar", LayoutPoint(-50, 0)); ScrollLayoutViewport(ScrollOffset(25, 0)); ValidateSerializedAnchor("html>body>.foobar", LayoutPoint(-75, 0)); ScrollLayoutViewport(ScrollOffset(75, 0)); ValidateSerializedAnchor("html>body>.barbaz", LayoutPoint(-50, 0)); } TEST_P(ScrollAnchorTest, SerializeAnchorQualifiedTagName) { SetBodyInnerHTML(R"HTML(
foobar
abc)HTML"); ScrollLayoutViewport(ScrollOffset(0, 150)); ValidateSerializedAnchor("html>body>ns\\:div", LayoutPoint(0, -50)); } TEST_P(ScrollAnchorTest, SerializeAnchorLimitsSelectorLength) { StringBuilder builder; builder.Append(""); builder.Append("
foobar
"); builder.Append("<"); for (int i = 0; i <= kMaxSerializedSelectorLength; i++) { builder.Append("a"); } builder.Append(" style='display:block; height:100px;'/>"); SetBodyInnerHTML(builder.ToString()); ScrollLayoutViewport(ScrollOffset(0, 150)); SerializedAnchor serialized = GetScrollAnchor(LayoutViewport()).GetSerializedAnchor(); EXPECT_FALSE(serialized.IsValid()); } TEST_P(ScrollAnchorTest, SerializeAnchorIgnoresDuplicatedId) { SetBodyInnerHTML(R"HTML(
abc def
)HTML"); ScrollLayoutViewport(ScrollOffset(0, 150)); ValidateSerializedAnchor("html>body>:nth-child(3)>.barbaz", LayoutPoint(0, -50)); } TEST_P(ScrollAnchorTest, SerializeAnchorFailsForPseudoElement) { SetBodyInnerHTML(R"HTML(
abc
def
)HTML"); ScrollLayoutViewport(ScrollOffset(0, 50)); EXPECT_FALSE(GetScrollAnchor(LayoutViewport()).AnchorObject()); } TEST_P(ScrollAnchorTest, RestoreAnchorSimple) { SetBodyInnerHTML( "" "
abc
" "
def
"); EXPECT_FALSE(GetScrollAnchor(LayoutViewport()).AnchorObject()); SerializedAnchor serialized_anchor("#block2", LayoutPoint(0, 0)); EXPECT_TRUE( GetScrollAnchor(LayoutViewport()).RestoreAnchor(serialized_anchor)); EXPECT_EQ(LayoutViewport()->ScrollOffsetInt().Height(), 100); SetHeight(GetDocument().getElementById("block1"), 200); EXPECT_EQ(LayoutViewport()->ScrollOffsetInt().Height(), 200); SetHeight(GetDocument().getElementById("block1"), 50); EXPECT_EQ(LayoutViewport()->ScrollOffsetInt().Height(), 50); } TEST_P(ScrollAnchorTest, RestoreAnchorNonTrivialSelector) { SetBodyInnerHTML(R"HTML(
abc
abc
def
ghi
)HTML"); SerializedAnchor serialized_anchor("#ancestor>:nth-child(3)>.foobar>div", LayoutPoint(0, -50)); EXPECT_TRUE( GetScrollAnchor(LayoutViewport()).RestoreAnchor(serialized_anchor)); EXPECT_EQ(LayoutViewport()->ScrollOffsetInt().Height(), 350); SetHeight(GetDocument().getElementById("block1"), 200); EXPECT_EQ(LayoutViewport()->ScrollOffsetInt().Height(), 450); } TEST_P(ScrollAnchorTest, RestoreAnchorFailsForInvalidSelectors) { SetBodyInnerHTML( "" "
abc
" "
def
"); EXPECT_FALSE(GetScrollAnchor(LayoutViewport()).AnchorObject()); SerializedAnchor serialized_anchor("article", LayoutPoint(0, 0)); EXPECT_FALSE( GetScrollAnchor(LayoutViewport()).RestoreAnchor(serialized_anchor)); SerializedAnchor serialized_anchor_2("", LayoutPoint(0, 0)); EXPECT_FALSE( GetScrollAnchor(LayoutViewport()).RestoreAnchor(serialized_anchor_2)); SerializedAnchor serialized_anchor_3("foobar", LayoutPoint(0, 0)); EXPECT_FALSE( GetScrollAnchor(LayoutViewport()).RestoreAnchor(serialized_anchor_3)); } // Ensure that when the serialized selector refers to a non-box, non-text // element(meaning its corresponding LayoutObject can't be the anchor object) // that restoration will still succeed. TEST_P(ScrollAnchorTest, RestoreAnchorSucceedsForNonBoxNonTextElement) { SetBodyInnerHTML( "" "
abc
" "some code"); EXPECT_FALSE(GetScrollAnchor(LayoutViewport()).AnchorObject()); SerializedAnchor serialized_anchor("html>body>code", LayoutPoint(0, 0)); EXPECT_TRUE( GetScrollAnchor(LayoutViewport()).RestoreAnchor(serialized_anchor)); EXPECT_EQ(LayoutViewport()->ScrollOffsetInt().Height(), 100); SetHeight(GetDocument().getElementById("block1"), 200); EXPECT_EQ(LayoutViewport()->ScrollOffsetInt().Height(), 200); SerializedAnchor serialized = GetScrollAnchor(LayoutViewport()).GetSerializedAnchor(); ValidateSerializedAnchor("html>body>code", LayoutPoint(0, 0)); } TEST_P(ScrollAnchorTest, RestoreAnchorSucceedsWhenScriptForbidden) { SetBodyInnerHTML( "" "
abc
" "
def
"); EXPECT_FALSE(GetScrollAnchor(LayoutViewport()).AnchorObject()); SerializedAnchor serialized_anchor("#block2", LayoutPoint(0, 0)); ScriptForbiddenScope scope; EXPECT_TRUE( GetScrollAnchor(LayoutViewport()).RestoreAnchor(serialized_anchor)); EXPECT_EQ(LayoutViewport()->ScrollOffsetInt().Height(), 100); } TEST_P(ScrollAnchorTest, RestoreAnchorSucceedsWithExistingAnchorObject) { SetBodyInnerHTML( "" "
abc
" "
def
"); EXPECT_FALSE(GetScrollAnchor(LayoutViewport()).AnchorObject()); SerializedAnchor serialized_anchor("#block1", LayoutPoint(0, 0)); EXPECT_TRUE( GetScrollAnchor(LayoutViewport()).RestoreAnchor(serialized_anchor)); EXPECT_TRUE(GetScrollAnchor(LayoutViewport()).AnchorObject()); EXPECT_EQ(LayoutViewport()->ScrollOffsetInt().Height(), 0); EXPECT_TRUE( GetScrollAnchor(LayoutViewport()).RestoreAnchor(serialized_anchor)); EXPECT_TRUE(GetScrollAnchor(LayoutViewport()).AnchorObject()); EXPECT_EQ(LayoutViewport()->ScrollOffsetInt().Height(), 0); } TEST_P(ScrollAnchorTest, DeleteAnonymousBlockCrash) { SetBodyInnerHTML(R"HTML(
torsk
)HTML"); // Removing #deleteMe will also remove the anonymous block around the text // node. This would cause NG to point to dead layout objects, prior to // https://chromium-review.googlesource.com/1193868 and therefore crash. ScrollLayoutViewport(ScrollOffset(0, 20000)); GetDocument().getElementById("deleteMe")->remove(); Update(); } TEST_P(ScrollAnchorTest, ClampAdjustsAnchorAnimation) { SetBodyInnerHTML(R"HTML(
)HTML"); LayoutViewport()->SetScrollOffset(ScrollOffset(0, 2000), mojom::blink::ScrollType::kUser); Update(); GetDocument().getElementById("hidden")->setAttribute(html_names::kStyleAttr, "display:block"); GetDocument().UpdateStyleAndLayout(DocumentUpdateReason::kTest); #if !defined(OS_MACOSX) EXPECT_EQ(IntSize(0, 200), LayoutViewport() ->GetScrollAnimator() .ImplOnlyAnimationAdjustmentForTesting()); #endif GetDocument().getElementById("hidden")->setAttribute(html_names::kStyleAttr, ""); GetDocument().UpdateStyleAndLayout(DocumentUpdateReason::kTest); // The clamping scroll after resizing layout overflow to be smaller // should adjust the animation back to 0. EXPECT_EQ(IntSize(0, 0), LayoutViewport() ->GetScrollAnimator() .ImplOnlyAnimationAdjustmentForTesting()); } }