Commit dd2d3501 authored by David Bokan's avatar David Bokan Committed by Commit Bot

Fix autoscrolling coordinate-space bug

This bug would only affect the non-root-layer-scrolling path and only
when the page is scrolled. This CL fixes the issue and cleans up the
surrounding code a bit.

I also took the opportunity to cleanup the drag-and-drop autoscrolling
tests:
 - Modernized using testharness.js and new test style guidelines
 - I've based them all on the drag-and-drop-autoscroll-frame.js
   script for commonality.
 - All the tests now occur at a non-0 scroll offset to make sure we
   catch these kinds of coordinate space bugs.

Change-Id: I3191796917f23b2e9b2cc3f561813176fa2dec9a
Reviewed-on: https://chromium-review.googlesource.com/972148
Commit-Queue: David Bokan <bokan@chromium.org>
Reviewed-by: default avatarSandra Sun <sunyunjia@chromium.org>
Cr-Commit-Position: refs/heads/master@{#545076}
parent 45733135
For manual testing, drag and drop "Drop Me" to "Scrollable" area.
Check autoscroll by drag-and-drop
On success, you will see a series of "PASS" messages, followed by "TEST COMPLETE".
PASS scrollable.scrollTop > 0
PASS autoscroll stopped
state=START
state=NE
PASS document.scrollingElement.scrollLeft > 0 is true
PASS !document.scrollingElement.scrollTop is true
state=SE
PASS document.scrollingElement.scrollLeft > 0 is true
PASS document.scrollingElement.scrollTop > 0 is true
state=SW
PASS document.scrollingElement.scrollLeft < lastScrollLeft is true
PASS document.scrollingElement.scrollTop > 0 is true
state=NW
PASS document.scrollingElement.scrollLeft <= lastScrollLeft is true
PASS document.scrollingElement.scrollTop < lastScrollTop is true
PASS successfullyParsed is true
TEST COMPLETE
<!DOCTYPE html>
<script src="../../resources/testharness.js"></script>
<script src="../../resources/testharnessreport.js"></script>
<script>
window.addEventListener('message', function(event) {
document.body.outerHTML = event.data;
testRunner.notifyDone();
});
window.onload = () => {
fetch_tests_from_window(frames[0]);
}
</script>
<frameset cols="50%,%50%">
<frameset cols="50%,50%">
<frame src="resources/drag-and-drop-autoscroll-frame.html">
<frame>
</frameset>
Check autoscroll within an inner frame by drag-and-drop
On success, you will see a series of "PASS" messages, followed by "TEST COMPLETE".
PASS Autoscroll should have scrolled the iframe downwards, and did.
PASS iframe.contentDocument.scrollingElement.scrollTop < middleTermScrollOffset is true
PASS successfullyParsed is true
TEST COMPLETE
For manual testing, drag and drop "Drop Me" downwards and then upwards.
<!DOCTYPE html>
<head>
<style type="text/css">
#scrollable {
height: 200px;
overflow: auto;
border: solid 3px #cc0000;
font-size: 80px;
}
</style>
</head>
<body>
For manual testing, drag and drop "Drop Me" downwards and then upwards.
<script src="../../resources/js-test.js"></script>
<script src="../../resources/testharness.js"></script>
<script src="../../resources/testharnessreport.js"></script>
<script>
var x, y, middleTermScrollOffset;
var iframe, iframeDocument, draggable;
function setUpTest()
{
if (!window.eventSender) {
debug('Please run within DumpRenderTree');
return;
}
iframe = document.getElementById('scrollable');
// iframes start in readyState complete, but with the about:blank doc, make sure our doc is loaded.
if (iframe.contentWindow.location.href == "about:blank" || iframe.contentDocument.readyState != "complete")
iframe.onload = testIt;
else
testIt();
}
function testIt()
{
eventSender.dragMode = false;
iframe = document.getElementById('scrollable');
iframeDocument = iframe.contentDocument;
draggable = iframeDocument.getElementById('draggable');
iframeDocument.addEventListener("scroll", recordScroll);
// Grab draggable.
x = iframe.offsetLeft + draggable.offsetLeft + 7;
y = iframe.offsetTop + draggable.offsetTop + 7;
eventSender.mouseMoveTo(x, y);
eventSender.mouseDown();
// Move mouse to the bottom autoscroll border belt.
y = iframe.offsetTop + iframe.offsetHeight - 10;
eventSender.mouseMoveTo(x, y);
}
function recordScroll(e)
{
autoscrollTestPart1();
iframeDocument.removeEventListener("scroll", recordScroll);
}
function recordScroll2(e)
{
autoscrollTestPart2();
iframeDocument.removeEventListener("scroll", recordScroll);
}
function autoscrollTestPart1()
{
if (iframe.contentDocument.scrollingElement.scrollTop == 0) {
testFailed("Autoscroll should have scrolled the iframe downwards, but did not");
finishTest();
return;
}
testPassed("Autoscroll should have scrolled the iframe downwards, and did.");
middleTermScrollOffset = iframe.contentDocument.scrollingElement.scrollTop;
iframeDocument.addEventListener("scroll", recordScroll2);
// Move mouse to the upper autoscroll border belt.
y = iframe.offsetTop + 10;
eventSender.mouseMoveTo(x, y);
}
function autoscrollTestPart2()
{
shouldBeTrue("iframe.contentDocument.scrollingElement.scrollTop < middleTermScrollOffset")
finishTest();
}
function finishTest()
{
eventSender.mouseUp();
document.body.removeChild(iframe);
finishJSTest();
}
description('Check autoscroll within an inner frame by drag-and-drop');
window.jsTestIsAsync = true;
window.onload = setUpTest;
window.onload = () => {
fetch_tests_from_window(frames[0]);
}
</script>
<iframe id="scrollable" src="data:text/html,
<p id='draggable' draggable='true' style='cursor: hand;'>
<b>Drag me!</b>
</p>
Try to drag and drop the text above in the input element at the bottom of this iframe. It should scroll. Then, try the way back.
<br><br>more<br>more<br>more<br>more<br>more<br>more<br>more<br>more<br>more<br>more<br>more<br>more<br><input>
"></iframe>
</body>
<style>
iframe {
position: relative;
top: 100px;
width: 300px;
height: 300px;
}
</style>
For manual testing, drag and drop "Drop Me" downwards and then upwards.
<iframe src="resources/drag-and-drop-autoscroll-frame.html"></iframe>
state=START
state=NE
PASS document.scrollingElement.scrollLeft > 0 is true
PASS !document.scrollingElement.scrollTop is true
state=SE
PASS document.scrollingElement.scrollLeft > 0 is true
PASS document.scrollingElement.scrollTop > 0 is true
state=SW
PASS document.scrollingElement.scrollLeft < lastScrollLeft is true
PASS document.scrollingElement.scrollTop > 0 is true
state=NW
PASS document.scrollingElement.scrollLeft <= lastScrollLeft is true
PASS document.scrollingElement.scrollTop < lastScrollTop is true
PASS successfullyParsed is true
TEST COMPLETE
<html>
<head>
<style type="text/css">
#draggable {
padding: 5pt;
border: 3px solid #00cc00;
background: #00cccc;
width: 80px;
cursor: hand;
}
</style>
<!DOCTYPE html>
<script src="../../resources/testharness.js"></script>
<script src="../../resources/testharnessreport.js"></script>
<script src="resources/drag-and-drop-autoscroll-frame.js"></script>
</head>
<body>
<style>
#draggable {
padding: 5pt;
border: 3px solid #00cc00;
background: #00cccc;
width: 80px;
cursor: pointer;
}
#container {
position: relative;
left: 2000px;
top: 2000px;
}
#sample {
width: 2000px;
height: 2000px;
}
body,html {
margin: 0;
}
</style>
<div id="container">
<p id="description"></p>
Manual steps:
<ol>
<li>Drag "Drop Me" to edge of window</li>
<li>You should see scrolling</li>
</ol>
<div id="draggable" draggable="true">Drop Me</div>
<div id="scrollbars" style="overflow: scroll"><div>scrollbars</div></div>
<table id="sample" border="1" style="width:2000; height:2000;">
<tr><td width="50%">North West</td><td width="50%">North East</td></tr>
<tr><td width="50%">South West</td><td width="50%">South East</td></tr>
</table>
Manual steps:
<ol>
<li>Drag "Drop Me" to edge of window</li>
<li>You should see scrolling</li>
</ol>
<div id="draggable" draggable="true">Drop Me</div>
<div id="scrollbars" style="overflow: scroll"><div>scrollbars</div></div>
<table id="sample" border="1">
<tr><td width="50%">North West</td><td width="50%">North East</td></tr>
<tr><td width="50%">South West</td><td width="50%">South East</td></tr>
</table>
</div>
<div id="console"></div>
<script src="../../resources/js-test.js"></script>
<script>
if (window.testRunner)
window.jsTestIsAsync = true;
description('Check autoscroll of main frame by drag-and-drop');
</script>
</body>
</html>
......@@ -5,7 +5,7 @@
border: 3px solid #00cc00;
background: #00cccc;
width: 80px;
cursor: hand;
cursor: pointer;
}
#scrollable {
height: 200px;
......
<html>
<head>
<!DOCTYPE html>
<script src="../../resources/testharness.js"></script>
<script src="../../resources/testharnessreport.js"></script>
<script src="resources/drag-and-drop-autoscroll-frame.js"></script>
<style type="text/css">
#draggable {
padding: 5pt;
border: 3px solid #00cc00;
background: #00cccc;
width: 80px;
cursor: hand;
}
#scrollable {
height: 200px;
overflow: auto;
border: solid 3px #cc0000;
font-size: 80px;
}
</style>
<script>
function $(id) { return document.getElementById(id); }
function finishTest() {
eventSender.mouseUp();
$('container').innerHTML = '';
window.testRunner.notifyDone();
}
function testIt() {
var draggable = $('draggable');
var scrollable = $('scrollable');
if (!window.eventSender)
return;
eventSender.dragMode = false;
// Grab draggable
eventSender.mouseMoveTo(draggable.offsetLeft + 5, draggable.offsetTop + 5);
eventSender.mouseDown();
// Move mouse to autoscroll border belt.
eventSender.mouseMoveTo(scrollable.offsetLeft + 5, scrollable.offsetTop + scrollable.offsetHeight - 10);
var retryCount = 0;
var lastScrollTop = 0;
function checkScrolled()
{
if (scrollable.scrollTop > 0) {
testPassed('scrollable.scrollTop > 0');
lastScrollTop = scrollable.scrollTop;
// Cancel drag and drop by ESC key.
eventSender.keyDown('Escape');
retryCount = 0;
window.setTimeout(checkStopped, 50);
return;
}
++retryCount;
if (retryCount > 10) {
testFailed('No autoscroll');
finishTest();
return;
}
// Autoscroll is occurred evey 0.05 sec.
window.setTimeout(checkScrolled, 50);
#draggable {
padding: 5pt;
border: 3px solid #00cc00;
background: #00cccc;
width: 80px;
cursor: pointer;
}
#scrollable {
height: 400px;
width: 400px;
overflow: auto;
font-size: 80px;
}
function checkStopped()
{
if (lastScrollTop == scrollable.scrollTop) {
testPassed('autoscroll stopped');
finishTest();
return;
}
++retryCount;
if (retryCount > 10) {
testFailed('still autoscroll');
finishTest();
return;
}
lastScrollTop = scrollable.scrollTop;
window.setTimeout(checkStopped, 50);
#container {
position: relative;
left: 2000px;
top: 2000px;
}
checkScrolled();
}
function setUpTest()
{
var scrollable = $('scrollable');
for (var i = 0; i < 100; ++i) {
var line = document.createElement('div');
line.innerHTML = "line " + i;
scrollable.appendChild(line);
#sample {
width: 2000px;
height: 2000px;
}
if (!window.eventSender) {
console.log('Please run within DumpRenderTree');
return;
body {
margin: 0;
}
window.jsTestIsAsync = true;
window.setTimeout(testIt, 0);
}
</script>
</head>
<body>
</style>
For manual testing, drag and drop "Drop Me" to "Scrollable" area.
<div id="container">
<div id="draggable" draggable="true">Drop Me</div>
<div id="scrollbars" style="overflow: scroll"><div>scrollbars</div></div>
Scrollable
<div id="scrollable">
<div id="container">
<table id="sample" border="1">
<tr><td width="50%">North West</td><td width="50%">North East</td></tr>
<tr><td width="50%">South West</td><td width="50%">South East</td></tr>
</table>
</div>
</div>
</div>
<div id="console"></div>
<script src="../../resources/js-test.js"></script>
<script>
description('Check autoscroll by drag-and-drop');
setUpTest();
</script>
</body>
</html>
<html>
<head>
<!DOCTYPE html>
<script src="../../../resources/testharness.js"></script>
<script src="drag-and-drop-autoscroll-frame.js"></script>
<style type="text/css">
#draggable {
padding: 5pt;
border: 3px solid #00cc00;
background: #00cccc;
width: 80px;
cursor: hand;
}
#draggable {
padding: 5pt;
border: 3px solid #00cc00;
background: #00cccc;
width: 80px;
cursor: pointer;
}
#container {
position: relative;
left: 2000px;
top: 2000px;
}
#sample {
width: 2000px;
height: 2000px;
}
body,html {
margin: 0;
}
</style>
<script src="drag-and-drop-autoscroll-frame.js"></script>
</head>
<body>
<div id="container">
<p id="description"></p>
Manual steps:
<ol>
<li>Drag "Drop Me" to edge of window</li>
<li>You should see scrolling</li>
</ol>
<div id="draggable" draggable="true">Drop Me</div>
<div id="scrollbars" style="overflow: scroll"><div>scrollbars</div></div>
<table id="sample" border="1" style="width:2000; height:2000;">
<tr><td width="50%">North West</td><td width="50%">North East</td></tr>
<tr><td width="50%">South West</td><td width="50%">South East</td></tr>
</table>
Manual steps:
<ol>
<li>Drag "Drop Me" to edge of window</li>
<li>You should see scrolling</li>
</ol>
<div id="draggable" draggable="true">Drop Me</div>
<div id="scrollbars" style="overflow: scroll"><div>scrollbars</div></div>
<table id="sample" border="1">
<tr><td width="50%">North West</td><td width="50%">North East</td></tr>
<tr><td width="50%">South West</td><td width="50%">South East</td></tr>
</table>
</div>
<div id="console"></div>
<script src="../../../resources/js-test.js"></script>
<script>
if (window.testRunner)
window.jsTestIsAsync = true;
description('Check autoscroll of frame by drag-and-drop');
</script>
</body>
</html>
function $(id) { return document.getElementById(id); }
// Convert client coordinates in this frame into client coordinates of the root
// frame, usable with event sender.
function toRootWindow(rect) {
var w = window;
var curRect = {
left: rect.left,
top: rect.top,
right: rect.right,
bottom: rect.bottom
};
while(w.parent != w) {
var frameRect = w.frameElement.getBoundingClientRect();
curRect.left += frameRect.left;
curRect.right += frameRect.left;
curRect.top += frameRect.top;
curRect.bottom += frameRect.top;
w = window.parent;
}
return curRect
}
var lastScrollLeft;
var lastScrollTop;
window.onload = function() {
const test = async_test("DragAndDrop Autoscroll");
test.add_cleanup(() => {
eventSender.mouseUp();
});
var draggable = $('draggable');
var sample = $('sample');
var scrollBarWidth = $('scrollbars').offsetWidth - $('scrollbars').firstChild.offsetWidth;
var scrollBarHeight = $('scrollbars').offsetHeight - $('scrollbars').firstChild.offsetHeight;
var eastX = window.innerWidth - scrollBarWidth - 10;
var northY = 10;
var southY= window.innerHeight - scrollBarHeight - 10;
var westX = 10;
var scroller = $('scrollable');
var scrollerRect;
if (scroller) {
scrollerRect = scroller.getBoundingClientRect();
} else {
scroller = document.scrollingElement;
scrollerRect = {
left: 0,
top: 0,
right: window.innerWidth,
bottom: window.innerHeight,
};
}
scrollerRect = toRootWindow(scrollerRect);
var eastX = scrollerRect.right - scrollBarWidth - 10;
var northY = scrollerRect.top + 10;
var southY= scrollerRect.bottom - scrollBarHeight - 10;
var westX = scrollerRect.left + 10;
function moveTo(newState, x, y)
{
state = newState;
lastScrollLeft = document.scrollingElement.scrollLeft;
lastScrollTop = document.scrollingElement.scrollTop;
lastScrollLeft = scroller.scrollLeft;
lastScrollTop = scroller.scrollTop;
eventSender.mouseMoveTo(x, y);
}
var state = 'START';
function process(event)
{
debug('state=' + state);
switch (state) {
case 'NE':
shouldBeTrue('document.scrollingElement.scrollLeft > 0');
shouldBeTrue('!document.scrollingElement.scrollTop');
moveTo('SE', westX, southY);
test.step(() => {
assert_greater_than(
scroller.scrollLeft,
lastScrollLeft,
"NE should scroll right");
assert_less_than(
scroller.scrollTop,
lastScrollTop,
"NE should scroll up");
});
moveTo('SE', eastX, southY);
break;
case 'SE':
shouldBeTrue('document.scrollingElement.scrollLeft > 0');
shouldBeTrue('document.scrollingElement.scrollTop > 0');
test.step(() => {
assert_greater_than(
scroller.scrollLeft,
lastScrollLeft,
"SE should scroll right");
assert_greater_than(
scroller.scrollTop,
lastScrollTop,
"SE should scroll down");
});
moveTo('SW', westX, southY);
break;
case 'SW':
shouldBeTrue('document.scrollingElement.scrollLeft < lastScrollLeft');
shouldBeTrue('document.scrollingElement.scrollTop > 0');
test.step(() => {
assert_less_than(
scroller.scrollLeft,
lastScrollLeft,
"SW should scroll left");
assert_greater_than(
scroller.scrollTop,
lastScrollTop,
"SW should scroll down");
});
moveTo('NW', westX, northY);
break;
case 'NW':
shouldBeTrue('document.scrollingElement.scrollLeft <= lastScrollLeft');
shouldBeTrue('document.scrollingElement.scrollTop < lastScrollTop');
eventSender.mouseUp();
$('container').outerHTML = '';
if (window.parent !== window)
window.jsTestIsAsync = false;
finishJSTest();
if (!window.jsTestIsAsync)
window.parent.postMessage($('console').innerHTML, '*');
test.step(() => {
assert_less_than(
scroller.scrollLeft,
lastScrollLeft,
"NW should scroll left");
assert_less_than(
scroller.scrollTop,
lastScrollTop,
"NW should scroll up");
});
test.done();
state = 'DONE';
break;
case 'DONE':
......@@ -58,20 +126,27 @@ window.onload = function() {
moveTo('NE', eastX, northY);
break;
default:
testFailed('Bad state ' + state);
console.error('Bad state ' + state);
break;
}
};
if (!window.eventSender)
if (!window.eventSender) {
$("container").scrollIntoView();
return;
}
if (scroller === document.scrollingElement)
window.addEventListener('scroll', process);
else
scroller.addEventListener('scroll', process);
$("container").scrollIntoView();
eventSender.dragMode = false;
// Grab draggable
eventSender.mouseMoveTo(draggable.offsetLeft + 5, draggable.offsetTop + 5);
const draggable_rect = toRootWindow(draggable.getBoundingClientRect());
eventSender.mouseMoveTo(draggable_rect.left + 5, draggable_rect.top + 5);
eventSender.mouseDown();
window.onscroll = process;
process();
};
......@@ -1084,8 +1084,9 @@ bool LayoutBox::CanAutoscroll() const {
return CanBeScrolledAndHasScrollableArea();
}
// If specified point is in border belt, returned offset denotes direction of
// scrolling.
// If specified point is outside the border-belt-excluded box (the border box
// inset by the autoscroll activation threshold), returned offset denotes
// direction of scrolling.
IntSize LayoutBox::CalculateAutoscrollDirection(
const IntPoint& point_in_root_frame) const {
if (!GetFrame())
......@@ -1095,34 +1096,36 @@ IntSize LayoutBox::CalculateAutoscrollDirection(
if (!frame_view)
return IntSize();
LayoutRect box(AbsoluteBoundingBoxRect());
// TODO(bokan): This is wrong. Subtracting the scroll offset would get you to
// frame coordinates (pre-RLS) but *adding* the scroll offset to an absolute
// location never makes sense (and we assume below it's in content
// coordinates).
box.Move(View()->GetFrameView()->ScrollOffsetInt());
// Exclude scrollbars so the border belt (activation area) starts from the
// scrollbar-content edge rather than the window edge.
ExcludeScrollbars(box, kExcludeOverlayScrollbarSizeForHitTesting);
IntRect window_box =
View()->GetFrameView()->ContentsToRootFrame(PixelSnappedIntRect(box));
IntPoint window_autoscroll_point = point_in_root_frame;
if (window_autoscroll_point.X() < window_box.X() + kAutoscrollBeltSize)
window_autoscroll_point.Move(-kAutoscrollBeltSize, 0);
else if (window_autoscroll_point.X() >
window_box.MaxX() - kAutoscrollBeltSize)
window_autoscroll_point.Move(kAutoscrollBeltSize, 0);
if (window_autoscroll_point.Y() < window_box.Y() + kAutoscrollBeltSize)
window_autoscroll_point.Move(0, -kAutoscrollBeltSize);
else if (window_autoscroll_point.Y() >
window_box.MaxY() - kAutoscrollBeltSize)
window_autoscroll_point.Move(0, kAutoscrollBeltSize);
return window_autoscroll_point - point_in_root_frame;
LayoutRect absolute_scrolling_box;
if (!RuntimeEnabledFeatures::RootLayerScrollingEnabled() && IsLayoutView()) {
absolute_scrolling_box =
LayoutRect(frame_view->VisibleContentRect(kExcludeScrollbars));
} else {
absolute_scrolling_box = LayoutRect(AbsoluteBoundingBoxRect());
// Exclude scrollbars so the border belt (activation area) starts from the
// scrollbar-content edge rather than the window edge.
ExcludeScrollbars(absolute_scrolling_box,
kExcludeOverlayScrollbarSizeForHitTesting);
}
IntRect belt_box = View()->GetFrameView()->AbsoluteToRootFrame(
PixelSnappedIntRect(absolute_scrolling_box));
belt_box.Inflate(-kAutoscrollBeltSize);
IntPoint point = point_in_root_frame;
if (point.X() < belt_box.X())
point.Move(-kAutoscrollBeltSize, 0);
else if (point.X() > belt_box.MaxX())
point.Move(kAutoscrollBeltSize, 0);
if (point.Y() < belt_box.Y())
point.Move(0, -kAutoscrollBeltSize);
else if (point.Y() > belt_box.MaxY())
point.Move(0, kAutoscrollBeltSize);
return point - point_in_root_frame;
}
LayoutBox* LayoutBox::FindAutoscrollable(LayoutObject* layout_object) {
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment