blob: af7534ae0e604f69495287e135b9b9725d878cab [file] [log] [blame]
// Copyright 2020 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 <memory>
#include "base/bind.h"
#include "base/run_loop.h"
#include "build/build_config.h"
#include "cc/base/switches.h"
#include "content/browser/renderer_host/input/synthetic_gesture.h"
#include "content/browser/renderer_host/input/synthetic_gesture_controller.h"
#include "content/browser/renderer_host/input/synthetic_gesture_target.h"
#include "content/browser/renderer_host/input/synthetic_smooth_scroll_gesture.h"
#include "content/browser/renderer_host/render_widget_host_impl.h"
#include "content/browser/web_contents/web_contents_impl.h"
#include "content/common/input/synthetic_gesture_params.h"
#include "content/common/input/synthetic_smooth_scroll_gesture_params.h"
#include "content/public/browser/render_view_host.h"
#include "content/public/common/content_switches.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "content/public/test/content_browser_test.h"
#include "content/public/test/content_browser_test_utils.h"
#include "content/public/test/hit_test_region_observer.h"
#include "content/shell/browser/shell.h"
#include "third_party/blink/public/common/features.h"
#include "third_party/blink/public/common/switches.h"
#include "ui/base/ui_base_features.h"
#include "ui/native_theme/native_theme_features.h"
namespace {
constexpr int kIntermediateScrollOffset = 25;
const std::string kOverflowScrollDataURL = R"HTML(
data:text/html;charset=utf-8,
<!DOCTYPE html>
<meta name='viewport' content='width=device-width, minimum-scale=1'>
<style>
%23container {
width: 200px;
height: 200px;
overflow: scroll;
}
%23content {
width: 7500px;
height: 7500px;
background-color: blue;
}
</style>
<div id="container">
<div id="content"></div>
</div>
<script>
var element = document.getElementById('container');
window.onload = function() {
document.title='ready';
}
</script>
)HTML";
const std::string kMainFrameScrollDataURL = R"HTML(
data:text/html;charset=utf-8,
<!DOCTYPE html>
<meta name='viewport' content='width=device-width, minimum-scale=1'>
<style>
%23scrollableDiv {
width: 500px;
height: 10000px;
background-color: blue;
}
</style>
<div id='scrollableDiv'></div>
<script>
window.onload = function() {
document.title='ready';
}
</script>
)HTML";
const std::string kSubframeScrollDataURL = R"HTML(
data:text/html;charset=utf-8,
<!DOCTYPE html>
<meta name='viewport' content='width=device-width, minimum-scale=1'>
<style>
%23subframe {
width: 200px;
height: 200px;
}
</style>
<body onload="document.title='ready'">
<iframe id='subframe' srcdoc="
<style>
%23content {
width: 7500px;
height: 7500px;
background-color: blue;
}
</style>
<div id='content'></div>">
</iframe>
</body>
<script>
var subframe = document.getElementById('subframe');
</script>
)HTML";
} // namespace
namespace content {
// This test is to verify that in-progress smooth scrolls stops when
// interrupted by an instant scroll, another smooth scroll, a touch scroll, or
// a mouse wheel scroll on an overflow:scroll element, main frame and subframe.
class ScrollBehaviorBrowserTest : public ContentBrowserTest,
public testing::WithParamInterface<bool> {
public:
ScrollBehaviorBrowserTest() : disable_threaded_scrolling_(GetParam()) {}
~ScrollBehaviorBrowserTest() override = default;
RenderWidgetHostImpl* GetWidgetHost() {
return RenderWidgetHostImpl::From(shell()
->web_contents()
->GetMainFrame()
->GetRenderViewHost()
->GetWidget());
}
void OnSyntheticGestureCompleted(SyntheticGesture::Result result) {
EXPECT_EQ(SyntheticGesture::GESTURE_FINISHED, result);
run_loop_->Quit();
}
protected:
void SetUpCommandLine(base::CommandLine* command_line) override {
ContentBrowserTest::SetUpCommandLine(command_line);
if (disable_threaded_scrolling_) {
command_line->AppendSwitch(blink::switches::kDisableThreadedScrolling);
}
// Set the scroll animation duration to 5 seconds so that we ensure
// the second scroll happens before the scroll animation finishes.
command_line->AppendSwitchASCII(
cc::switches::kCCScrollAnimationDurationForTesting, "5");
}
void LoadURL(const std::string page_url) {
const GURL data_url(page_url);
EXPECT_TRUE(NavigateToURL(shell(), data_url));
RenderWidgetHostImpl* host = GetWidgetHost();
host->GetView()->SetSize(gfx::Size(400, 400));
std::u16string ready_title(u"ready");
TitleWatcher watcher(shell()->web_contents(), ready_title);
ignore_result(watcher.WaitAndGetTitle());
HitTestRegionObserver observer(host->GetFrameSinkId());
// Wait for the hit test data to be ready
observer.WaitForHitTestData();
}
double ExecuteScriptAndExtractDouble(const std::string& script) {
return EvalJs(shell(), script).ExtractDouble();
}
WebContentsImpl* web_contents() const {
return static_cast<WebContentsImpl*>(shell()->web_contents());
}
// The scroll delta values are in the viewport direction. Positive
// scroll_delta_y means scroll down, positive scroll_delta_x means scroll
// right.
void SimulateScroll(content::mojom::GestureSourceType gesture_source_type,
int scroll_delta_x,
int scroll_delta_y) {
auto scroll_update_watcher = std::make_unique<InputMsgWatcher>(
GetWidgetHost(), blink::WebInputEvent::Type::kGestureScrollEnd);
// This speed affects only the rate at which the requested scroll delta is
// sent from the synthetic gesture controller, and doesn't affect the speed
// of the animation in the renderer.
constexpr int kSpeedInstant = 400000;
SyntheticSmoothScrollGestureParams params;
params.gesture_source_type = gesture_source_type;
params.anchor = gfx::PointF(50, 50);
params.distances.push_back(gfx::Vector2d(-scroll_delta_x, -scroll_delta_y));
params.speed_in_pixels_s = kSpeedInstant;
params.granularity = ui::ScrollGranularity::kScrollByPixel;
run_loop_ = std::make_unique<base::RunLoop>();
auto gesture = std::make_unique<SyntheticSmoothScrollGesture>(params);
GetWidgetHost()->QueueSyntheticGesture(
std::move(gesture),
base::BindOnce(&ScrollBehaviorBrowserTest::OnSyntheticGestureCompleted,
base::Unretained(this)));
run_loop_->Run();
}
void WaitForScrollToStart(const std::string& script) {
// When the first smooth scroll starts and scroll to 5 pixels, we will
// send the second scroll to interrupt the current smooth scroll.
constexpr int kExpectedScrollTop = 5;
MainThreadFrameObserver frame_observer(GetWidgetHost());
while (ExecuteScriptAndExtractDouble(script) < kExpectedScrollTop)
frame_observer.Wait();
}
void WaitUntilLessThan(const std::string& script,
double starting_scroll_top) {
// For the scroll interruption, we want to make sure that the first smooth
// scroll animation stops right away, and the second scroll starts.
MainThreadFrameObserver frame_observer(GetWidgetHost());
double current = ExecuteScriptAndExtractDouble(script);
// If the animation doesn't reverse within this number of pixels we fail the
// test.
constexpr int kThreshold = 20;
while (current >= starting_scroll_top) {
ASSERT_LT(current, starting_scroll_top + kThreshold);
frame_observer.Wait();
current = ExecuteScriptAndExtractDouble(script);
}
}
void ValueHoldsAt(const std::string& scroll_top_script, double scroll_top) {
// This function checks that the scroll top value holds at the given value
// for 10 frames.
MainThreadFrameObserver frame_observer(GetWidgetHost());
int frame_count = 5;
while (frame_count > 0) {
ASSERT_EQ(ExecuteScriptAndExtractDouble(scroll_top_script), scroll_top);
frame_observer.Wait();
frame_count--;
}
}
std::unique_ptr<base::RunLoop> run_loop_;
bool disable_threaded_scrolling_ = false;
DISALLOW_COPY_AND_ASSIGN(ScrollBehaviorBrowserTest);
};
INSTANTIATE_TEST_SUITE_P(All, ScrollBehaviorBrowserTest, ::testing::Bool());
// This tests that a in-progress smooth scroll on an overflow:scroll element
// stops when interrupted by an instant scroll.
IN_PROC_BROWSER_TEST_P(ScrollBehaviorBrowserTest,
OverflowScrollInterruptedByInstantScroll) {
// TODO(crbug.com/1133492): the last animation is committed after we set the
// scrollTop even when we cancel the animation, so the final scrollTop value
// is not 0, we need to fix it.
if (!disable_threaded_scrolling_)
return;
LoadURL(kOverflowScrollDataURL);
EXPECT_TRUE(ExecJs(shell()->web_contents(),
"element.scrollTo({top: 100, behavior: 'smooth'});"));
std::string scroll_top_script = "element.scrollTop";
WaitForScrollToStart(scroll_top_script);
double scroll_top = ExecuteScriptAndExtractDouble(scroll_top_script);
ASSERT_GT(scroll_top, 0);
ASSERT_LT(scroll_top, kIntermediateScrollOffset);
// When interrupted by an instant scroll, the in-progress smooth scrolls stop.
EXPECT_TRUE(ExecJs(shell()->web_contents(), "element.scrollTop = 0;"));
// Instant scroll does not cause animation, it scroll to 0 right away.
ValueHoldsAt(scroll_top_script, 0);
}
// This tests that a in-progress smooth scroll on an overflow:scroll element
// stops when interrupted by another smooth scroll.
IN_PROC_BROWSER_TEST_P(ScrollBehaviorBrowserTest,
OverflowScrollInterruptedBySmoothScroll) {
LoadURL(kOverflowScrollDataURL);
EXPECT_TRUE(ExecJs(shell()->web_contents(),
"element.scrollTo({top: 100, behavior: 'smooth'});"));
std::string scroll_top_script = "element.scrollTop";
WaitForScrollToStart(scroll_top_script);
double scroll_top = ExecuteScriptAndExtractDouble(scroll_top_script);
ASSERT_GT(scroll_top, 0);
ASSERT_LT(scroll_top, kIntermediateScrollOffset);
// When interrupted by a smooth scroll, the in-progress smooth scrolls stop.
EXPECT_TRUE(ExecJs(shell()->web_contents(),
"element.scrollTo({top: 0, behavior: 'smooth'});"));
WaitUntilLessThan(scroll_top_script, scroll_top);
double new_scroll_top = ExecuteScriptAndExtractDouble(scroll_top_script);
EXPECT_LT(new_scroll_top, scroll_top);
EXPECT_GT(new_scroll_top, 0);
}
// This tests that a in-progress smooth scroll on an overflow:scroll element
// stops when interrupted by a touch scroll.
IN_PROC_BROWSER_TEST_P(ScrollBehaviorBrowserTest,
OverflowScrollInterruptedByTouchScroll) {
// TODO(crbug.com/1116647): compositing scroll should be able to cancel a
// running programmatic scroll.
if (!disable_threaded_scrolling_)
return;
LoadURL(kOverflowScrollDataURL);
EXPECT_TRUE(ExecJs(shell()->web_contents(),
"element.scrollTo({top: 100, behavior: 'smooth'});"));
std::string scroll_top_script = "element.scrollTop";
WaitForScrollToStart(scroll_top_script);
double scroll_top = ExecuteScriptAndExtractDouble(scroll_top_script);
ASSERT_GT(scroll_top, 0);
ASSERT_LT(scroll_top, kIntermediateScrollOffset);
// When interrupted by a touch scroll, the in-progress smooth scrolls stop.
SimulateScroll(content::mojom::GestureSourceType::kTouchInput, 0, -100);
// The touch scroll should cause scroll to 0 and cancel the animation, so
// make sure the value stays at 0.
ValueHoldsAt(scroll_top_script, 0);
}
// This tests that a in-progress smooth scroll on an overflow:scroll element
// stops when interrupted by a mouse wheel scroll.
// Flaky, mainly on Mac, but also on other slower builders/testers:
// https://crbug.com/1175392
IN_PROC_BROWSER_TEST_P(ScrollBehaviorBrowserTest,
DISABLED_OverflowScrollInterruptedByWheelScroll) {
// TODO(crbug.com/1116647): compositing scroll should be able to cancel a
// running programmatic scroll.
if (!disable_threaded_scrolling_)
return;
LoadURL(kOverflowScrollDataURL);
EXPECT_TRUE(ExecJs(shell()->web_contents(),
"element.scrollTo({top: 100, behavior: 'smooth'});"));
std::string scroll_top_script = "element.scrollTop";
WaitForScrollToStart(scroll_top_script);
double scroll_top = ExecuteScriptAndExtractDouble(scroll_top_script);
ASSERT_GT(scroll_top, 0);
ASSERT_LT(scroll_top, kIntermediateScrollOffset);
// When interrupted by a wheel scroll, the in-progress smooth scrolls stop.
SimulateScroll(content::mojom::GestureSourceType::kMouseInput, 0, -30);
// Smooth scrolling is disabled for wheel scroll on Mac.
// https://crbug.com/574283.
#if defined(OS_MAC) || defined(OS_ANDROID)
ValueHoldsAt(scroll_top_script, 0);
#else
WaitUntilLessThan(scroll_top_script, scroll_top);
double new_scroll_top = ExecuteScriptAndExtractDouble(scroll_top_script);
EXPECT_LT(new_scroll_top, scroll_top);
EXPECT_GT(new_scroll_top, 0);
#endif
}
// This tests that a in-progress smooth scroll on the main frame stops when
// interrupted by another smooth scroll.
IN_PROC_BROWSER_TEST_P(ScrollBehaviorBrowserTest,
MainFrameScrollInterruptedBySmoothScroll) {
LoadURL(kMainFrameScrollDataURL);
EXPECT_TRUE(ExecJs(shell()->web_contents(),
"window.scrollTo({top: 100, behavior: 'smooth'});"));
std::string scroll_top_script = "document.scrollingElement.scrollTop";
WaitForScrollToStart(scroll_top_script);
double scroll_top = ExecuteScriptAndExtractDouble(scroll_top_script);
ASSERT_GT(scroll_top, 0);
ASSERT_LT(scroll_top, kIntermediateScrollOffset);
// When interrupted by a smooth scroll, the in-progress smooth scrolls stop.
EXPECT_TRUE(ExecJs(shell()->web_contents(),
"window.scrollTo({top: 0, behavior: 'smooth'});"));
WaitUntilLessThan(scroll_top_script, scroll_top);
double new_scroll_top = ExecuteScriptAndExtractDouble(scroll_top_script);
EXPECT_LT(new_scroll_top, scroll_top);
EXPECT_GT(new_scroll_top, 0);
}
// This tests that a in-progress smooth scroll on a subframe stops when
// interrupted by another smooth scroll.
IN_PROC_BROWSER_TEST_P(ScrollBehaviorBrowserTest,
SubframeScrollInterruptedBySmoothScroll) {
LoadURL(kSubframeScrollDataURL);
EXPECT_TRUE(ExecJs(
shell()->web_contents(),
"subframe.contentWindow.scrollTo({top: 100, behavior: 'smooth'});"));
std::string scroll_top_script =
"subframe.contentDocument.scrollingElement.scrollTop";
WaitForScrollToStart(scroll_top_script);
double scroll_top = ExecuteScriptAndExtractDouble(scroll_top_script);
ASSERT_GT(scroll_top, 0);
ASSERT_LT(scroll_top, kIntermediateScrollOffset);
// When interrupted by a smooth scroll, the in-progress smooth scrolls stop.
EXPECT_TRUE(
ExecJs(shell()->web_contents(),
"subframe.contentWindow.scrollTo({top: 0, behavior: 'smooth'});"));
WaitUntilLessThan(scroll_top_script, scroll_top);
double new_scroll_top = ExecuteScriptAndExtractDouble(scroll_top_script);
EXPECT_LT(new_scroll_top, scroll_top);
EXPECT_GT(new_scroll_top, 0);
}
} // namespace content