#include <memory>
#include <tuple>
#include "base/functional/bind.h"
#include "base/run_loop.h"
#include "build/build_config.h"
#include "cc/base/switches.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.h"
#include "content/common/input/synthetic_gesture_controller.h"
#include "content/common/input/synthetic_gesture_params.h"
#include "content/common/input/synthetic_gesture_target.h"
#include "content/common/input/synthetic_smooth_scroll_gesture.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"
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";
const std::string kMirroredScrollersDataURL = R"HTML(
data:text/html;charset=utf-8,<!DOCTYPE html>
<meta name=viewport content="width=device-width, minimum-scale=1">
<style>
body, p { margin: 0 }
.s { overflow: scroll; border: 1px solid black;
width: 400px; height: 300px }
.sp { height: 1200px; width: 900px }
</style>
<div id=s1 class=s><p class=sp>SCROLLER</p></div>
<div id=s2 class=s><p class=sp>MIRROR</p></div>
<script>
s1.onscroll = () => { s2.scrollTo(0, s1.scrollTop) }
onload = () => { document.title = "ready" }
</script>
)HTML";
}
namespace content {
class ScrollBehaviorBrowserTest : public ContentBrowserTest {
public:
ScrollBehaviorBrowserTest() = default;
ScrollBehaviorBrowserTest(const ScrollBehaviorBrowserTest&) = delete;
ScrollBehaviorBrowserTest& operator=(const ScrollBehaviorBrowserTest&) =
delete;
~ScrollBehaviorBrowserTest() override = default;
RenderWidgetHostImpl* GetWidgetHost() {
return RenderWidgetHostImpl::From(shell()
->web_contents()
->GetPrimaryMainFrame()
->GetRenderViewHost()
->GetWidget());
}
void OnSyntheticGestureCompleted(SyntheticGesture::Result result) {
EXPECT_EQ(SyntheticGesture::GESTURE_FINISHED, result);
run_loop_->Quit();
}
protected:
void SetUpCommandLine(base::CommandLine* command_line) override {
command_line->AppendSwitchASCII(
switches::kCCScrollAnimationDurationForTesting, "1");
}
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);
std::ignore = watcher.WaitAndGetTitle();
HitTestRegionObserver observer(host->GetFrameSinkId());
observer.WaitForHitTestData();
}
WebContentsImpl* web_contents() const {
return static_cast<WebContentsImpl*>(shell()->web_contents());
}
void SimulateScroll(content::mojom::GestureSourceType gesture_source_type,
int scroll_delta_x,
int scroll_delta_y,
bool blocking = true) {
auto scroll_update_watcher = std::make_unique<InputMsgWatcher>(
GetWidgetHost(), blink::WebInputEvent::Type::kGestureScrollEnd);
constexpr int kSpeedInstant = 400000;
SyntheticSmoothScrollGestureParams params;
params.gesture_source_type = gesture_source_type;
params.anchor = gfx::PointF(50, 50);
params.speed_in_pixels_s = kSpeedInstant;
params.distances.push_back(gfx::Vector2d(-scroll_delta_x, -scroll_delta_y));
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)));
if (blocking)
run_loop_->Run();
}
void WaitForScrollToStart(const std::string& script) {
constexpr int kExpectedScrollTop = 5;
MainThreadFrameObserver frame_observer(GetWidgetHost());
while (EvalJs(shell(), script).ExtractDouble() < kExpectedScrollTop) {
frame_observer.Wait();
}
}
void WaitUntilLessThan(const std::string& script,
double starting_scroll_top) {
MainThreadFrameObserver frame_observer(GetWidgetHost());
double current = EvalJs(shell(), script).ExtractDouble();
constexpr int kThreshold = 20;
while (current >= starting_scroll_top) {
ASSERT_LT(current, starting_scroll_top + kThreshold);
frame_observer.Wait();
current = EvalJs(shell(), script).ExtractDouble();
}
}
void ValueHoldsAt(const std::string& scroll_top_script,
double scroll_top,
double tolerance = 0) {
MainThreadFrameObserver frame_observer(GetWidgetHost());
int frame_count = 10;
while (frame_count > 0) {
EXPECT_NEAR(EvalJs(shell(), scroll_top_script).ExtractDouble(),
scroll_top, tolerance);
frame_observer.Wait();
frame_count--;
}
}
double AssertScrollEndedAtPosition(const std::string& script,
double target_position,
double tolerance) {
MainThreadFrameObserver frame_observer(GetWidgetHost());
double scroll_top = EvalJs(shell(), script).ExtractDouble();
while (std::abs(target_position - scroll_top) > tolerance) {
scroll_top = EvalJs(shell(), script).ExtractDouble();
frame_observer.Wait();
}
ValueHoldsAt(script, target_position, 1);
return scroll_top;
}
std::unique_ptr<base::RunLoop> run_loop_;
};
IN_PROC_BROWSER_TEST_F(ScrollBehaviorBrowserTest,
DISABLED_InstantScriptScrollAbortsSmoothScriptScroll) {
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 = EvalJs(shell(), scroll_top_script).ExtractDouble();
ASSERT_GT(scroll_top, 0);
EXPECT_TRUE(ExecJs(shell()->web_contents(), "element.scrollTop = 0;"));
ValueHoldsAt(scroll_top_script, 0);
}
IN_PROC_BROWSER_TEST_F(ScrollBehaviorBrowserTest,
InstantScriptScrollAdjustsSmoothWheelScroll) {
LoadURL(kOverflowScrollDataURL);
SimulateScroll(content::mojom::GestureSourceType::kMouseInput, 0, 100,
false);
WaitForScrollToStart("element.scrollTop");
EXPECT_TRUE(ExecJs(shell()->web_contents(), "element.scrollBy(0, -5);"));
AssertScrollEndedAtPosition("element.scrollTop", 95, 1);
}
IN_PROC_BROWSER_TEST_F(ScrollBehaviorBrowserTest,
SmoothWheelScrollCompletesWithScriptedMirror) {
LoadURL(kMirroredScrollersDataURL);
SimulateScroll(content::mojom::GestureSourceType::kMouseInput, 0, 200,
false);
WaitForScrollToStart("s1.scrollTop");
AssertScrollEndedAtPosition("s1.scrollTop", 200, 1);
AssertScrollEndedAtPosition("s2.scrollTop", 200, 1);
}
IN_PROC_BROWSER_TEST_F(ScrollBehaviorBrowserTest,
OneSmoothScriptScrollAbortsAnother_Element) {
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 = EvalJs(shell(), scroll_top_script).ExtractDouble();
ASSERT_GT(scroll_top, 0);
EXPECT_TRUE(ExecJs(shell()->web_contents(),
"element.scrollTo({top: 0, behavior: 'smooth'});"));
WaitUntilLessThan(scroll_top_script, scroll_top);
double new_scroll_top = EvalJs(shell(), scroll_top_script).ExtractDouble();
EXPECT_LT(new_scroll_top, scroll_top);
}
IN_PROC_BROWSER_TEST_F(ScrollBehaviorBrowserTest,
DISABLED_TouchScrollAbortsSmoothScriptScroll) {
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 = EvalJs(shell(), scroll_top_script).ExtractDouble();
ASSERT_GT(scroll_top, 0);
ASSERT_LT(scroll_top, kIntermediateScrollOffset);
SimulateScroll(content::mojom::GestureSourceType::kTouchInput, 0, -100);
ValueHoldsAt(scroll_top_script, 0);
}
IN_PROC_BROWSER_TEST_F(ScrollBehaviorBrowserTest,
DISABLED_WheelScrollAbortsSmoothScriptScroll) {
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 = EvalJs(shell(), scroll_top_script).ExtractDouble();
ASSERT_GT(scroll_top, 0);
ASSERT_LT(scroll_top, kIntermediateScrollOffset);
SimulateScroll(content::mojom::GestureSourceType::kMouseInput, 0, -30);
#if BUILDFLAG(IS_MAC) || BUILDFLAG(IS_ANDROID)
ValueHoldsAt(scroll_top_script, 0);
#else
WaitUntilLessThan(scroll_top_script, scroll_top);
double new_scroll_top = EvalJs(shell(), scroll_top_script).ExtractDouble();
EXPECT_LT(new_scroll_top, scroll_top);
EXPECT_GT(new_scroll_top, 0);
#endif
}
IN_PROC_BROWSER_TEST_F(ScrollBehaviorBrowserTest,
DISABLED_OneSmoothScriptScrollAbortsAnother_Document) {
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 = EvalJs(shell(), scroll_top_script).ExtractDouble();
ASSERT_GT(scroll_top, 0);
EXPECT_TRUE(ExecJs(shell()->web_contents(),
"window.scrollTo({top: 0, behavior: 'smooth'});"));
WaitUntilLessThan(scroll_top_script, scroll_top);
double new_scroll_top = EvalJs(shell(), scroll_top_script).ExtractDouble();
EXPECT_LT(new_scroll_top, scroll_top);
}
IN_PROC_BROWSER_TEST_F(ScrollBehaviorBrowserTest,
DISABLED_OneSmoothScriptScrollAbortsAnother_Subframe) {
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 = EvalJs(shell(), scroll_top_script).ExtractDouble();
ASSERT_GT(scroll_top, 0);
EXPECT_TRUE(
ExecJs(shell()->web_contents(),
"subframe.contentWindow.scrollTo({top: 0, behavior: 'smooth'});"));
WaitUntilLessThan(scroll_top_script, scroll_top);
double new_scroll_top = EvalJs(shell(), scroll_top_script).ExtractDouble();
EXPECT_LT(new_scroll_top, scroll_top);
}
}