blob: 6bce2a00d2ce4864dbd3b123a1bb33b566bac0a1 [file] [log] [blame]
// Copyright 2019 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/scroll/scroll_animator.h"
#include "base/test/bind.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/public/web/web_script_source.h"
#include "third_party/blink/renderer/core/css/css_style_declaration.h"
#include "third_party/blink/renderer/core/dom/document.h"
#include "third_party/blink/renderer/core/dom/element.h"
#include "third_party/blink/renderer/core/frame/local_dom_window.h"
#include "third_party/blink/renderer/core/frame/local_frame.h"
#include "third_party/blink/renderer/core/frame/local_frame_view.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/frame/web_local_frame_impl.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/paint/paint_layer.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/scroll/scrollable_area.h"
#include "third_party/blink/renderer/core/testing/sim/sim_request.h"
#include "third_party/blink/renderer/core/testing/sim/sim_test.h"
#include "third_party/blink/renderer/platform/testing/unit_test_helpers.h"
namespace blink {
namespace {
const double kBeginFrameDelaySeconds =
(base::FeatureList::IsEnabled(features::kImpulseScrollAnimations) ? 1.5
: 0.5);
}
class FractionalScrollSimTest : public SimTest {
public:
FractionalScrollSimTest() : fractional_scroll_offsets_for_test_(true) {}
private:
ScopedFractionalScrollOffsetsForTest fractional_scroll_offsets_for_test_;
};
TEST_F(FractionalScrollSimTest, GetBoundingClientRectAtFractional) {
WebView().MainFrameViewWidget()->Resize(gfx::Size(800, 600));
SimRequest request("https://example.com/test.html", "text/html");
LoadURL("https://example.com/test.html");
request.Complete(R"HTML(
<!DOCTYPE html>
<style>
body, html {
margin: 0;
height: 2000px;
width: 2000px;
}
div {
position: absolute;
left: 800px;
top: 600px;
width: 100px;
height: 100px;
}
</style>
<body>
<div id="target"></div>
</body>
)HTML");
Compositor().BeginFrame();
// Scroll on the layout viewport.
GetDocument().View()->GetScrollableArea()->SetScrollOffset(
FloatSize(700.5f, 500.6f), mojom::blink::ScrollType::kProgrammatic,
mojom::blink::ScrollBehavior::kInstant);
Compositor().BeginFrame();
Element* target = GetDocument().getElementById("target");
DOMRect* rect = target->getBoundingClientRect();
const float kOneLayoutUnit = 1.f / kFixedPointDenominator;
EXPECT_NEAR(LayoutUnit(800.f - 700.5f), rect->left(), kOneLayoutUnit);
EXPECT_NEAR(LayoutUnit(600.f - 500.6f), rect->top(), kOneLayoutUnit);
}
TEST_F(FractionalScrollSimTest, NoRepaintOnScrollFromSubpixel) {
WebView().MainFrameViewWidget()->Resize(gfx::Size(800, 600));
SimRequest request("https://example.com/test.html", "text/html");
LoadURL("https://example.com/test.html");
request.Complete(R"HTML(
<!DOCTYPE html>
<style>
body {
height: 4000px;
}
#container {
will-change:transform;
margin-top: 300px;
}
#child {
height: 100px;
width: 100px;
transform: translateY(-0.5px);
background-color: coral;
}
#fixed {
position: fixed;
top: 0;
width: 100px;
height: 20px;
background-color: dodgerblue
}
</style>
<!-- Need fixed element because of PLSA::UpdateCompositingLayersAfterScroll
will invalidate compositing due to potential overlap changes. -->
<div id="fixed"></div>
<div id="container">
<div id="child"></div>
</div>
)HTML");
Compositor().BeginFrame();
auto* container_layer =
To<LayoutBoxModelObject>(
GetDocument().getElementById("container")->GetLayoutObject())
->Layer()
->GraphicsLayerBacking();
container_layer->ResetTrackedRasterInvalidations();
GetDocument().View()->SetTracksRasterInvalidations(true);
// Scroll on the layout viewport.
GetDocument().View()->GetScrollableArea()->SetScrollOffset(
FloatSize(0.f, 100.5f), mojom::blink::ScrollType::kProgrammatic,
mojom::blink::ScrollBehavior::kInstant);
Compositor().BeginFrame();
EXPECT_FALSE(
container_layer->GetRasterInvalidationTracking()->HasInvalidations());
GetDocument().View()->SetTracksRasterInvalidations(false);
}
// Verifies that the sticky constraints are correctly computed when the scroll
// offset is fractional. Ensures any kind of layout unit snapping is
// consistent.
TEST_F(FractionalScrollSimTest, StickyDoesntOscillate) {
WebView().MainFrameWidget()->Resize(gfx::Size(800, 600));
SimRequest request("https://example.com/test.html", "text/html");
LoadURL("https://example.com/test.html");
request.Complete(R"HTML(
<!DOCTYPE html>
<style>
#sticky {
position: sticky; top: 0; width: 100px; height: 100px;
}
body {
margin: 0;
height: 300vh;
}
#padding {
height: 8px;
width: 100%;
}
</style>
<div id='padding'></div>
<div id='sticky'></div>
)HTML");
Compositor().BeginFrame();
const float kOneLayoutUnitF = LayoutUnit::Epsilon();
Element* sticky = GetDocument().getElementById("sticky");
// Try sub-layout-unit scroll offsets. The sticky box shouldn't move.
for (int i = 0; i < 3; ++i) {
GetDocument().View()->GetScrollableArea()->ScrollBy(
ScrollOffset(0.f, kOneLayoutUnitF / 4.f),
mojom::blink::ScrollType::kProgrammatic);
Compositor().BeginFrame();
EXPECT_EQ(8, sticky->getBoundingClientRect()->top());
}
// This offset is specifically chosen since it doesn't land on a LayoutUnit
// boundary and reproduced https://crbug.com/1010961.
GetDocument().View()->GetScrollableArea()->SetScrollOffset(
FloatSize(0.f, 98.8675308f), mojom::blink::ScrollType::kProgrammatic,
mojom::blink::ScrollBehavior::kInstant);
Compositor().BeginFrame();
EXPECT_EQ(0, sticky->getBoundingClientRect()->top());
// Incrementally scroll from here, making sure the sticky position remains
// fixed.
for (int i = 0; i < 4; ++i) {
GetDocument().View()->GetScrollableArea()->ScrollBy(
ScrollOffset(0.f, kOneLayoutUnitF / 3.f),
mojom::blink::ScrollType::kProgrammatic);
Compositor().BeginFrame();
EXPECT_EQ(0, sticky->getBoundingClientRect()->top());
}
}
class ScrollAnimatorSimTest : public SimTest {};
// Test that the callback of user scroll will be executed when the animation
// finishes at ScrollAnimator::TickAnimation for root frame user scroll at the
// layout viewport.
TEST_F(ScrollAnimatorSimTest, TestRootFrameLayoutViewportUserScrollCallBack) {
GetDocument().GetFrame()->GetSettings()->SetScrollAnimatorEnabled(true);
WebView().MainFrameViewWidget()->Resize(gfx::Size(800, 500));
SimRequest request("https://example.com/test.html", "text/html");
LoadURL("https://example.com/test.html");
request.Complete(R"HTML(
<!DOCTYPE html>
<style>
body, html {
margin: 0;
height: 500vh;
}
</style>
<body>
</body>
)HTML");
Compositor().BeginFrame();
WebView().MainFrameWidget()->SetFocus(true);
WebView().SetIsActive(true);
// Scroll on the layout viewport.
bool finished = false;
GetDocument().View()->GetScrollableArea()->UserScroll(
ScrollGranularity::kScrollByLine, FloatSize(100, 300),
ScrollableArea::ScrollCallback(
base::BindLambdaForTesting([&]() { finished = true; })));
Compositor().BeginFrame();
ASSERT_FALSE(finished);
// The callback is executed when the animation finishes at
// ScrollAnimator::TickAnimation.
Compositor().BeginFrame();
Compositor().BeginFrame(kBeginFrameDelaySeconds);
ASSERT_TRUE(finished);
}
// Test that the callback of user scroll will be executed when the animation
// finishes at ScrollAnimator::TickAnimation for root frame user scroll at the
// visual viewport.
TEST_F(ScrollAnimatorSimTest, TestRootFrameVisualViewporUserScrollCallBack) {
GetDocument().GetFrame()->GetSettings()->SetScrollAnimatorEnabled(true);
WebView().MainFrameViewWidget()->Resize(gfx::Size(800, 500));
SimRequest request("https://example.com/test.html", "text/html");
LoadURL("https://example.com/test.html");
request.Complete(R"HTML(
<!DOCTYPE html>
<style>
body, html {
margin: 0;
height: 500vh;
}
</style>
<body>
</body>
)HTML");
Compositor().BeginFrame();
WebView().MainFrameWidget()->SetFocus(true);
WebView().SetIsActive(true);
WebView().SetPageScaleFactor(2);
// Scroll on the visual viewport.
bool finished = false;
GetDocument().View()->GetScrollableArea()->UserScroll(
ScrollGranularity::kScrollByLine, FloatSize(100, 300),
ScrollableArea::ScrollCallback(
base::BindLambdaForTesting([&]() { finished = true; })));
Compositor().BeginFrame();
ASSERT_FALSE(finished);
// The callback is executed when the animation finishes at
// ScrollAnimator::TickAnimation.
Compositor().BeginFrame();
Compositor().BeginFrame(kBeginFrameDelaySeconds);
ASSERT_TRUE(finished);
}
// Test that the callback of user scroll will be executed when the animation
// finishes at ScrollAnimator::TickAnimation for root frame user scroll at both
// the layout and visual viewport.
TEST_F(ScrollAnimatorSimTest, TestRootFrameBothViewportsUserScrollCallBack) {
GetDocument().GetFrame()->GetSettings()->SetScrollAnimatorEnabled(true);
WebView().MainFrameViewWidget()->Resize(gfx::Size(800, 500));
SimRequest request("https://example.com/test.html", "text/html");
LoadURL("https://example.com/test.html");
request.Complete(R"HTML(
<!DOCTYPE html>
<style>
body, html {
margin: 0;
height: 500vh;
}
</style>
<body>
</body>
)HTML");
Compositor().BeginFrame();
WebView().MainFrameWidget()->SetFocus(true);
WebView().SetIsActive(true);
WebView().SetPageScaleFactor(2);
// Scroll on both the layout and visual viewports.
bool finished = false;
GetDocument().View()->GetScrollableArea()->UserScroll(
ScrollGranularity::kScrollByLine, FloatSize(0, 1000),
ScrollableArea::ScrollCallback(
base::BindLambdaForTesting([&]() { finished = true; })));
Compositor().BeginFrame();
ASSERT_FALSE(finished);
// The callback is executed when the animation finishes at
// ScrollAnimator::TickAnimation.
Compositor().BeginFrame();
Compositor().BeginFrame(kBeginFrameDelaySeconds);
ASSERT_TRUE(finished);
}
// Test that the callback of user scroll will be executed when the animation
// finishes at ScrollAnimator::TickAnimation for div user scroll.
#if defined(ADDRESS_SANITIZER) || defined(THREAD_SANITIZER)
// Flaky under sanitizers, see http://crbug.com/1092550
#define MAYBE_TestDivUserScrollCallBack DISABLED_TestDivUserScrollCallBack
#else
#define MAYBE_TestDivUserScrollCallBack TestDivUserScrollCallBack
#endif
TEST_F(ScrollAnimatorSimTest, MAYBE_TestDivUserScrollCallBack) {
GetDocument().GetSettings()->SetScrollAnimatorEnabled(true);
WebView().MainFrameViewWidget()->Resize(gfx::Size(800, 500));
SimRequest request("https://example.com/test.html", "text/html");
LoadURL("https://example.com/test.html");
request.Complete(R"HTML(
<!DOCTYPE html>
<style>
#scroller {
width: 100px;
height: 100px;
overflow: auto;
}
#overflow {
height: 500px;
width: 500px;
}
</style>
<div id="scroller">
<div id="overflow"></div>
</div>
)HTML");
Compositor().BeginFrame();
WebView().MainFrameWidget()->SetFocus(true);
WebView().SetIsActive(true);
Element* scroller = GetDocument().getElementById("scroller");
bool finished = false;
PaintLayerScrollableArea* scrollable_area =
To<LayoutBox>(scroller->GetLayoutObject())->GetScrollableArea();
scrollable_area->UserScroll(
ScrollGranularity::kScrollByLine, FloatSize(0, 100),
ScrollableArea::ScrollCallback(
base::BindLambdaForTesting([&]() { finished = true; })));
Compositor().BeginFrame();
ASSERT_FALSE(finished);
// The callback is executed when the animation finishes at
// ScrollAnimator::TickAnimation.
Compositor().BeginFrame(kBeginFrameDelaySeconds);
ASSERT_TRUE(finished);
}
// Test that the callback of user scroll will be executed in
// ScrollAnimatorBase::UserScroll when animation is disabled.
TEST_F(ScrollAnimatorSimTest, TestUserScrollCallBackAnimatorDisabled) {
GetDocument().GetFrame()->GetSettings()->SetScrollAnimatorEnabled(false);
WebView().MainFrameViewWidget()->Resize(gfx::Size(800, 500));
SimRequest request("https://example.com/test.html", "text/html");
LoadURL("https://example.com/test.html");
request.Complete(R"HTML(
<!DOCTYPE html>
<style>
body, html {
margin: 0;
height: 500vh;
}
</style>
<body>
</body>
)HTML");
Compositor().BeginFrame();
WebView().MainFrameWidget()->SetFocus(true);
WebView().SetIsActive(true);
bool finished = false;
GetDocument().View()->GetScrollableArea()->UserScroll(
ScrollGranularity::kScrollByLine, FloatSize(0, 300),
ScrollableArea::ScrollCallback(
base::BindLambdaForTesting([&]() { finished = true; })));
Compositor().BeginFrame();
ASSERT_TRUE(finished);
}
// Test that the callback of user scroll will be executed when the animation is
// canceled because performing a programmatic scroll in the middle of a user
// scroll will cancel the animation.
TEST_F(ScrollAnimatorSimTest, TestRootFrameUserScrollCallBackCancelAnimation) {
GetDocument().GetFrame()->GetSettings()->SetScrollAnimatorEnabled(true);
WebView().MainFrameViewWidget()->Resize(gfx::Size(800, 500));
SimRequest request("https://example.com/test.html", "text/html");
LoadURL("https://example.com/test.html");
request.Complete(R"HTML(
<!DOCTYPE html>
<style>
body, html {
margin: 0;
height: 500vh;
}
</style>
<body>
</body>
)HTML");
Compositor().BeginFrame();
WebView().MainFrameWidget()->SetFocus(true);
WebView().SetIsActive(true);
// Scroll on the layout viewport.
bool finished = false;
GetDocument().View()->GetScrollableArea()->UserScroll(
ScrollGranularity::kScrollByLine, FloatSize(100, 300),
ScrollableArea::ScrollCallback(
base::BindLambdaForTesting([&]() { finished = true; })));
Compositor().BeginFrame();
ASSERT_FALSE(finished);
// Programmatic scroll will cancel the current user scroll animation and the
// callback will be executed.
GetDocument().View()->GetScrollableArea()->SetScrollOffset(
ScrollOffset(0, 300), mojom::blink::ScrollType::kProgrammatic,
mojom::blink::ScrollBehavior::kSmooth, ScrollableArea::ScrollCallback());
Compositor().BeginFrame();
ASSERT_TRUE(finished);
}
class ScrollInfacesUseCounterSimTest : public SimTest {
public:
// Reload the page, set direction and writing-mode, then check the initial
// useCounted status.
void Reset(const String& direction, const String& writing_mode) {
SimRequest request("https://example.com/test.html", "text/html");
LoadURL("https://example.com/test.html");
request.Complete(R"HTML(
<!DOCTYPE html>
<style>
#scroller {
width: 100px;
height: 100px;
overflow: scroll;
}
#content {
width: 300;
height: 300;
}
</style>
<div id="scroller"><div id="content"></div></div>
)HTML");
auto& document = GetDocument();
auto* style = document.getElementById("scroller")->style();
style->setProperty(&Window(), "direction", direction, String(),
ASSERT_NO_EXCEPTION);
style->setProperty(&Window(), "writing-mode", writing_mode, String(),
ASSERT_NO_EXCEPTION);
Compositor().BeginFrame();
EXPECT_FALSE(document.IsUseCounted(
WebFeature::
kElementWithLeftwardOrUpwardOverflowDirection_ScrollLeftOrTop));
EXPECT_FALSE(document.IsUseCounted(
WebFeature::
kElementWithLeftwardOrUpwardOverflowDirection_ScrollLeftOrTopSetPositive));
}
// Check if Element.scrollLeft/Top could trigger useCounter as expected.
void CheckScrollLeftOrTop(const String& command, bool exppected_use_counted) {
String scroll_command =
"document.querySelector('#scroller')." + command + ";";
MainFrame().ExecuteScriptAndReturnValue(WebScriptSource(scroll_command));
auto& document = GetDocument();
EXPECT_EQ(
exppected_use_counted,
document.IsUseCounted(
WebFeature::
kElementWithLeftwardOrUpwardOverflowDirection_ScrollLeftOrTop));
EXPECT_FALSE(document.IsUseCounted(
WebFeature::
kElementWithLeftwardOrUpwardOverflowDirection_ScrollLeftOrTopSetPositive));
}
// Check if Element.setScrollLeft/Top could trigger useCounter as expected.
void CheckSetScrollLeftOrTop(const String& command,
bool exppected_use_counted) {
String scroll_command =
"document.querySelector('#scroller')." + command + " = -1;";
MainFrame().ExecuteScriptAndReturnValue(WebScriptSource(scroll_command));
auto& document = GetDocument();
EXPECT_EQ(
exppected_use_counted,
document.IsUseCounted(
WebFeature::
kElementWithLeftwardOrUpwardOverflowDirection_ScrollLeftOrTop));
EXPECT_FALSE(document.IsUseCounted(
WebFeature::
kElementWithLeftwardOrUpwardOverflowDirection_ScrollLeftOrTopSetPositive));
scroll_command = "document.querySelector('#scroller')." + command + " = 1;";
MainFrame().ExecuteScriptAndReturnValue(WebScriptSource(scroll_command));
EXPECT_EQ(
exppected_use_counted,
document.IsUseCounted(
WebFeature::
kElementWithLeftwardOrUpwardOverflowDirection_ScrollLeftOrTopSetPositive));
}
// Check if Element.scrollTo/scroll could trigger useCounter as expected.
void CheckScrollTo(const String& command, bool exppected_use_counted) {
String scroll_command =
"document.querySelector('#scroller')." + command + "(-1, -1);";
MainFrame().ExecuteScriptAndReturnValue(WebScriptSource(scroll_command));
auto& document = GetDocument();
EXPECT_EQ(
exppected_use_counted,
document.IsUseCounted(
WebFeature::
kElementWithLeftwardOrUpwardOverflowDirection_ScrollLeftOrTop));
EXPECT_FALSE(document.IsUseCounted(
WebFeature::
kElementWithLeftwardOrUpwardOverflowDirection_ScrollLeftOrTopSetPositive));
scroll_command =
"document.querySelector('#scroller')." + command + "(1, 1);";
MainFrame().ExecuteScriptAndReturnValue(WebScriptSource(scroll_command));
EXPECT_EQ(
exppected_use_counted,
document.IsUseCounted(
WebFeature::
kElementWithLeftwardOrUpwardOverflowDirection_ScrollLeftOrTopSetPositive));
}
};
struct TestCase {
String direction;
String writingMode;
bool scrollLeftUseCounted;
bool scrollTopUseCounted;
};
TEST_F(ScrollInfacesUseCounterSimTest, ScrollTestAll) {
v8::HandleScope handle_scope(v8::Isolate::GetCurrent());
WebView().MainFrameViewWidget()->Resize(gfx::Size(800, 600));
const Vector<TestCase> test_cases = {
{"ltr", "horizontal-tb", false, false},
{"rtl", "horizontal-tb", true, false},
{"ltr", "vertical-lr", false, false},
{"rtl", "vertical-lr", false, true},
{"ltr", "vertical-rl", true, false},
{"rtl", "vertical-rl", true, true},
};
for (const TestCase& test_case : test_cases) {
Reset(test_case.direction, test_case.writingMode);
CheckScrollLeftOrTop("scrollLeft", test_case.scrollLeftUseCounted);
Reset(test_case.direction, test_case.writingMode);
CheckSetScrollLeftOrTop("scrollLeft", test_case.scrollLeftUseCounted);
Reset(test_case.direction, test_case.writingMode);
CheckScrollLeftOrTop("scrollTop", test_case.scrollTopUseCounted);
Reset(test_case.direction, test_case.writingMode);
CheckSetScrollLeftOrTop("scrollTop", test_case.scrollTopUseCounted);
bool expectedScrollUseCounted =
test_case.scrollLeftUseCounted || test_case.scrollTopUseCounted;
Reset(test_case.direction, test_case.writingMode);
CheckScrollTo("scrollTo", expectedScrollUseCounted);
Reset(test_case.direction, test_case.writingMode);
CheckScrollTo("scroll", expectedScrollUseCounted);
Reset(test_case.direction, test_case.writingMode);
CheckScrollTo("scrollBy", false);
}
}
class ScrollPositionsInNonDefaultWritingModeSimTest : public SimTest {};
// Verify that scrollIntoView() does not trigger the use counter
// kElementWithLeftwardOrUpwardOverflowDirection_ScrollLeftOrTopSetPositive
// and can be used to feature detect the convention of scroll coordinates.
TEST_F(ScrollPositionsInNonDefaultWritingModeSimTest,
ScrollIntoViewAndCounters) {
SimRequest main_resource("https://example.com/", "text/html");
SimRequest child_frame_resource("https://example.com/subframe.html",
"text/html");
LoadURL("https://example.com/");
// Load a page that performs feature detection of scroll behavior by relying
// on scrollIntoView().
main_resource.Complete(
R"HTML(
<body>
<div style="direction: rtl; position: fixed; left: 0; top: 0; overflow: hidden; width: 1px; height: 1px;"><div style="width: 2px; height: 1px;"><div style="display: inline-block; width: 1px;"></div><div style="display: inline-block; width: 1px;"></div></div></div>
<script>
var scroller = document.body.firstElementChild;
scroller.firstElementChild.children[0].scrollIntoView();
var right = scroller.scrollLeft;
scroller.firstElementChild.children[1].scrollIntoView();
var left = scroller.scrollLeft;
if (left < right)
console.log("decreasing");
if (left < 0)
console.log("nonpositive");
</script>
</body>)HTML");
Compositor().BeginFrame();
test::RunPendingTasks();
// Per the CSSOM specification, the standard behavior is:
// - decreasing coordinates when scrolling leftward.
// - nonpositive coordinates for leftward scroller.
EXPECT_TRUE(ConsoleMessages().Contains("decreasing"));
EXPECT_TRUE(ConsoleMessages().Contains("nonpositive"));
// Reading scrollLeft triggers the first counter:
EXPECT_TRUE(GetDocument().IsUseCounted(
WebFeature::
kElementWithLeftwardOrUpwardOverflowDirection_ScrollLeftOrTop));
// However, calling scrollIntoView() should not trigger the second counter:
EXPECT_FALSE(GetDocument().IsUseCounted(
WebFeature::
kElementWithLeftwardOrUpwardOverflowDirection_ScrollLeftOrTopSetPositive));
}
} // namespace blink