summaryrefslogtreecommitdiffstats
path: root/testing/web-platform/tests/dom/events/scrolling
diff options
context:
space:
mode:
Diffstat (limited to 'testing/web-platform/tests/dom/events/scrolling')
-rw-r--r--testing/web-platform/tests/dom/events/scrolling/iframe-chains.html48
-rw-r--r--testing/web-platform/tests/dom/events/scrolling/input-text-scroll-event-when-using-arrow-keys.html71
-rw-r--r--testing/web-platform/tests/dom/events/scrolling/overscroll-deltas.html85
-rw-r--r--testing/web-platform/tests/dom/events/scrolling/overscroll-event-fired-to-document.html62
-rw-r--r--testing/web-platform/tests/dom/events/scrolling/overscroll-event-fired-to-element-with-overscroll-behavior.html92
-rw-r--r--testing/web-platform/tests/dom/events/scrolling/overscroll-event-fired-to-scrolled-element.html65
-rw-r--r--testing/web-platform/tests/dom/events/scrolling/overscroll-event-fired-to-window.html52
-rw-r--r--testing/web-platform/tests/dom/events/scrolling/scroll_support.js163
-rw-r--r--testing/web-platform/tests/dom/events/scrolling/scrollend-event-fired-after-sequence-of-scrolls.tentative.html63
-rw-r--r--testing/web-platform/tests/dom/events/scrolling/scrollend-event-fired-after-snap.html87
-rw-r--r--testing/web-platform/tests/dom/events/scrolling/scrollend-event-fired-for-programmatic-scroll.html135
-rw-r--r--testing/web-platform/tests/dom/events/scrolling/scrollend-event-fired-for-scrollIntoView.html124
-rw-r--r--testing/web-platform/tests/dom/events/scrolling/scrollend-event-fired-to-document.html70
-rw-r--r--testing/web-platform/tests/dom/events/scrolling/scrollend-event-fired-to-element-with-overscroll-behavior.html102
-rw-r--r--testing/web-platform/tests/dom/events/scrolling/scrollend-event-fired-to-scrolled-element.html68
-rw-r--r--testing/web-platform/tests/dom/events/scrolling/scrollend-event-fired-to-window.html55
-rw-r--r--testing/web-platform/tests/dom/events/scrolling/scrollend-event-for-user-scroll.html199
-rw-r--r--testing/web-platform/tests/dom/events/scrolling/scrollend-event-handler-content-attributes.html108
-rw-r--r--testing/web-platform/tests/dom/events/scrolling/scrollend-event-not-fired-after-removing-scroller.tentative.html84
19 files changed, 1733 insertions, 0 deletions
diff --git a/testing/web-platform/tests/dom/events/scrolling/iframe-chains.html b/testing/web-platform/tests/dom/events/scrolling/iframe-chains.html
new file mode 100644
index 0000000000..fb7d674aae
--- /dev/null
+++ b/testing/web-platform/tests/dom/events/scrolling/iframe-chains.html
@@ -0,0 +1,48 @@
+<!DOCTYPE html>
+<html>
+<head>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-actions.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<style>
+
+body { margin: 0; padding: 10px; }
+.space { height: 2000px; }
+
+#scroller {
+ border: 3px solid green;
+ position: absolute;
+ z-index: 0;
+ overflow: auto;
+ padding: 10px;
+ width: 250px;
+ height: 150px;
+}
+
+.ifr {
+ border: 3px solid blue;
+ width: 200px;
+ height: 100px;
+}
+
+</style>
+</head>
+<body>
+<div id=scroller>
+ <iframe srcdoc="SCROLL ME" class=ifr></iframe>
+ <div class=space></div>
+</div>
+<div class=space></div>
+<script>
+
+promise_test(async t => {
+ await new test_driver.Actions().scroll(50, 50, 0, 50).send();
+ // Allow the possibility the scroll is not fully synchronous
+ await t.step_wait(() => scroller.scrollTop === 50);
+}, "Wheel scroll in iframe chains to containing element.");
+
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/dom/events/scrolling/input-text-scroll-event-when-using-arrow-keys.html b/testing/web-platform/tests/dom/events/scrolling/input-text-scroll-event-when-using-arrow-keys.html
new file mode 100644
index 0000000000..f84e446527
--- /dev/null
+++ b/testing/web-platform/tests/dom/events/scrolling/input-text-scroll-event-when-using-arrow-keys.html
@@ -0,0 +1,71 @@
+<!doctype html>
+<html>
+<head>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-actions.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+
+</head>
+<body onload=runTest()>
+ <p>Moving the cursor using the arrow keys into an
+ input element fires scroll events when text has to scroll into view.
+ Uses arrow keys to move forward and backwards in the input
+ element.</p>
+ <input type="text" style='width: 50px'
+ value="Fooooooooooooooooooooooooooooooooooooooooooooooooo"/>
+ <textarea rows="4" cols="4">
+ Fooooooooooooooooooooooooooooooooooooooooooooooooo
+ </textarea>
+
+ <script>
+ async function moveCursorRightInsideElement(element, value){
+ var arrowRight = '\uE014';
+ for(var i=0;i<value;i++){
+ await test_driver.send_keys(element, arrowRight);
+ }
+ }
+
+ function runTest(){
+ promise_test(async(t) => { return new Promise(async (resolve, reject) => {
+ var input = document.getElementsByTagName('input')[0];
+ function handleScroll(){
+ resolve("Scroll Event successfully fired!");
+ }
+ input.addEventListener('scroll', handleScroll, false);
+ // move cursor to the right until the text scrolls
+ while(input.scrollLeft === 0){
+ await moveCursorRightInsideElement(input, 1);
+ }
+ // if there is no scroll event fired then test will fail by timeout
+ })},
+ /*
+ Moving the cursor using the arrow keys into an input element
+ fires scroll events when text has to scroll into view.
+ Uses arrow keys to move right in the input element.
+ */
+ "Scroll event fired for <input> element.");
+
+ promise_test(async(t) => { return new Promise(async (resolve, reject) => {
+ var textarea = document.getElementsByTagName('textarea')[0];
+ function handleScroll(){
+ resolve("Scroll Event successfully fired!");
+ }
+ textarea.addEventListener('scroll', handleScroll, false);
+ // move cursor to the right until the text scrolls
+ while(textarea.scrollLeft === 0){
+ await moveCursorRightInsideElement(textarea, 1);
+ }
+ // if there is no scroll event fired then test will fail by timeout
+ })},
+ /*
+ Moving the cursor using the arrow keys into a textarea element
+ fires scroll events when text has to scroll into view.
+ Uses arrow keys to move right in the textarea element.
+ */
+ "Scroll event fired for <textarea> element.");
+ }
+ </script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/dom/events/scrolling/overscroll-deltas.html b/testing/web-platform/tests/dom/events/scrolling/overscroll-deltas.html
new file mode 100644
index 0000000000..6f0b77f22e
--- /dev/null
+++ b/testing/web-platform/tests/dom/events/scrolling/overscroll-deltas.html
@@ -0,0 +1,85 @@
+<!DOCTYPE HTML>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-actions.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="scroll_support.js"></script>
+<style>
+#targetDiv {
+ width: 500px;
+ height: 500px;
+ background: red;
+}
+html, body {
+ /* Prevent any built-in browser overscroll features from consuming the scroll
+ * deltas */
+ overscroll-behavior: none;
+}
+
+</style>
+
+<body style="margin:0;" onload=runTest()>
+<div id="targetDiv">
+</div>
+</body>
+
+<script>
+var target_div = document.getElementById('targetDiv');
+var overscrolled_x_deltas = [];
+var overscrolled_y_deltas = [];
+var scrollend_received = false;
+
+function onOverscroll(event) {
+ overscrolled_x_deltas.push(event.deltaX);
+ overscrolled_y_deltas.push(event.deltaY);
+}
+
+function onScrollend(event) {
+ scrollend_received = true;
+}
+
+document.addEventListener("overscroll", onOverscroll);
+document.addEventListener("scrollend", onScrollend);
+
+function runTest() {
+ promise_test (async (t) => {
+ await waitForCompositorCommit();
+
+ // Scroll up on target div and wait for the doc to get overscroll event.
+ await touchScrollInTarget(300, target_div, 'up');
+ await waitFor(() => { return scrollend_received; },
+ 'Document did not receive scrollend event.');
+
+ // Even though we request 300 pixels of scroll, the API above doesn't
+ // guarantee how much scroll delta will be generated - different browsers
+ // can consume different amounts for "touch slop" (for example). Ensure the
+ // overscroll reaches at least 100 pixels which is a fairly conservative
+ // value.
+ assert_greater_than(overscrolled_y_deltas.length, 0, "There should be at least one overscroll events when overscrolling.");
+ assert_equals(overscrolled_x_deltas.filter(function(x){ return x!=0; }).length, 0, "The deltaX attribute must be 0 when there is no scrolling in x direction.");
+ assert_less_than_equal(Math.max(...overscrolled_y_deltas), 0, "The deltaY attribute must be <= 0 when there is overscrolling in up direction.");
+ assert_less_than_equal(Math.min(...overscrolled_y_deltas),-100, "The deltaY attribute must be the number of pixels overscrolled.");
+
+ await waitForCompositorCommit();
+ overscrolled_x_deltas = [];
+ overscrolled_y_deltas = [];
+ scrollend_received = false;
+
+ // Scroll left on target div and wait for the doc to get overscroll event.
+ await touchScrollInTarget(300, target_div, 'left');
+ await waitFor(() => { return scrollend_received; },
+ 'Document did not receive scrollend event.');
+
+ // TODO(bokan): It looks like Chrome inappropriately filters some scroll
+ // events despite |overscroll-behavior| being set to none. The overscroll
+ // amount here has been loosened but this should be fixed in Chrome.
+ // https://crbug.com/1112183.
+ assert_greater_than(overscrolled_y_deltas.length, 0, "There should be at least one overscroll events when overscrolling.");
+ assert_equals(overscrolled_y_deltas.filter(function(x){ return x!=0; }).length, 0, "The deltaY attribute must be 0 when there is no scrolling in y direction.");
+ assert_less_than_equal(Math.max(...overscrolled_x_deltas), 0, "The deltaX attribute must be <= 0 when there is overscrolling in left direction.");
+ assert_less_than_equal(Math.min(...overscrolled_x_deltas),-50, "The deltaX attribute must be the number of pixels overscrolled.");
+
+ }, 'Tests that the document gets overscroll event with right deltaX/Y attributes.');
+}
+</script>
diff --git a/testing/web-platform/tests/dom/events/scrolling/overscroll-event-fired-to-document.html b/testing/web-platform/tests/dom/events/scrolling/overscroll-event-fired-to-document.html
new file mode 100644
index 0000000000..c054ffca9c
--- /dev/null
+++ b/testing/web-platform/tests/dom/events/scrolling/overscroll-event-fired-to-document.html
@@ -0,0 +1,62 @@
+<!DOCTYPE HTML>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-actions.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="scroll_support.js"></script>
+<style>
+#targetDiv {
+ width: 200px;
+ height: 200px;
+ overflow: scroll;
+}
+
+#innerDiv {
+ width: 400px;
+ height: 400px;
+}
+</style>
+
+<body style="margin:0" onload=runTest()>
+<div id="targetDiv">
+ <div id="innerDiv">
+ </div>
+</div>
+</body>
+
+<script>
+var target_div = document.getElementById('targetDiv');
+var overscrolled_x_delta = 0;
+var overscrolled_y_delta = 0;
+function onOverscroll(event) {
+ assert_false(event.cancelable);
+ // overscroll events are bubbled when the target node is document.
+ assert_true(event.bubbles);
+ overscrolled_x_delta = event.deltaX;
+ overscrolled_y_delta = event.deltaY;
+}
+document.addEventListener("overscroll", onOverscroll);
+
+function runTest() {
+ promise_test (async (t) => {
+ // Make sure that no overscroll event is sent to target_div.
+ target_div.addEventListener("overscroll",
+ t.unreached_func("target_div got unexpected overscroll event."));
+ await waitForCompositorCommit();
+
+ // Scroll up on target div and wait for the doc to get overscroll event.
+ await touchScrollInTarget(300, target_div, 'up');
+ await waitFor(() => { return overscrolled_y_delta < 0; },
+ 'Document did not receive overscroll event after scroll up on target.');
+ assert_equals(target_div.scrollTop, 0);
+
+ // Scroll left on target div and wait for the doc to get overscroll event.
+ await touchScrollInTarget(300, target_div, 'left');
+ await waitFor(() => { return overscrolled_x_delta < 0; },
+ 'Document did not receive overscroll event after scroll left on target.');
+ assert_equals(target_div.scrollLeft, 0);
+ }, 'Tests that the document gets overscroll event when no element scrolls ' +
+ 'after touch scrolling.');
+}
+</script>
diff --git a/testing/web-platform/tests/dom/events/scrolling/overscroll-event-fired-to-element-with-overscroll-behavior.html b/testing/web-platform/tests/dom/events/scrolling/overscroll-event-fired-to-element-with-overscroll-behavior.html
new file mode 100644
index 0000000000..750080e656
--- /dev/null
+++ b/testing/web-platform/tests/dom/events/scrolling/overscroll-event-fired-to-element-with-overscroll-behavior.html
@@ -0,0 +1,92 @@
+<!DOCTYPE HTML>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-actions.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="scroll_support.js"></script>
+<style>
+#overscrollXDiv {
+ width: 600px;
+ height: 600px;
+ overflow: scroll;
+ overscroll-behavior-x: contain;
+}
+#overscrollYDiv {
+ width: 500px;
+ height: 500px;
+ overflow: scroll;
+ overscroll-behavior-y: none;
+}
+#targetDiv {
+ width: 400px;
+ height: 400px;
+ overflow: scroll;
+}
+.content {
+ width:800px;
+ height:800px;
+}
+</style>
+
+<body style="margin:0" onload=runTest()>
+<div id="overscrollXDiv">
+ <div class=content>
+ <div id="overscrollYDiv">
+ <div class=content>
+ <div id="targetDiv">
+ <div class="content">
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+</div>
+</body>
+
+<script>
+var target_div = document.getElementById('targetDiv');
+var overscrolled_x_delta = 0;
+var overscrolled_y_delta = 0;
+function onOverscrollX(event) {
+ assert_false(event.cancelable);
+ assert_false(event.bubbles);
+ overscrolled_x_delta = event.deltaX;
+}
+function onOverscrollY(event) {
+ assert_false(event.cancelable);
+ assert_false(event.bubbles);
+ overscrolled_y_delta = event.deltaY;
+}
+// Test that both "onoverscroll" and addEventListener("overscroll"... work.
+document.getElementById('overscrollXDiv').onoverscroll = onOverscrollX;
+document.getElementById('overscrollYDiv').
+ addEventListener("overscroll", onOverscrollY);
+
+function runTest() {
+ promise_test (async (t) => {
+ // Make sure that no overscroll event is sent to document or target_div.
+ document.addEventListener("overscroll",
+ t.unreached_func("Document got unexpected overscroll event."));
+ target_div.addEventListener("overscroll",
+ t.unreached_func("target_div got unexpected overscroll event."));
+ await waitForCompositorCommit();
+ // Scroll up on target div and wait for the element with overscroll-y to get
+ // overscroll event.
+ await touchScrollInTarget(300, target_div, 'up');
+ await waitFor(() => { return overscrolled_y_delta < 0; },
+ 'Expected element did not receive overscroll event after scroll up on ' +
+ 'target.');
+ assert_equals(target_div.scrollTop, 0);
+
+ // Scroll left on target div and wait for the element with overscroll-x to
+ // get overscroll event.
+ await touchScrollInTarget(300, target_div, 'left');
+ await waitFor(() => { return overscrolled_x_delta < 0; },
+ 'Expected element did not receive overscroll event after scroll left ' +
+ 'on target.');
+ assert_equals(target_div.scrollLeft, 0);
+ }, 'Tests that the last element in the cut scroll chain gets overscroll ' +
+ 'event when no element scrolls by touch.');
+}
+</script>
diff --git a/testing/web-platform/tests/dom/events/scrolling/overscroll-event-fired-to-scrolled-element.html b/testing/web-platform/tests/dom/events/scrolling/overscroll-event-fired-to-scrolled-element.html
new file mode 100644
index 0000000000..cfc782a809
--- /dev/null
+++ b/testing/web-platform/tests/dom/events/scrolling/overscroll-event-fired-to-scrolled-element.html
@@ -0,0 +1,65 @@
+<!DOCTYPE HTML>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-actions.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="scroll_support.js"></script>
+<style>
+#scrollableDiv {
+ width: 200px;
+ height: 200px;
+ overflow: scroll;
+}
+
+#innerDiv {
+ width: 400px;
+ height: 400px;
+}
+</style>
+
+<body style="margin:0" onload=runTest()>
+<div id="scrollableDiv">
+ <div id="innerDiv">
+ </div>
+</div>
+</body>
+
+<script>
+var scrolling_div = document.getElementById('scrollableDiv');
+var overscrolled_x_delta = 0;
+var overscrolled_y_delta = 0;
+function onOverscroll(event) {
+ assert_false(event.cancelable);
+ assert_false(event.bubbles);
+ overscrolled_x_delta = event.deltaX;
+ overscrolled_y_delta = event.deltaY;
+}
+scrolling_div.addEventListener("overscroll", onOverscroll);
+
+function runTest() {
+ promise_test (async (t) => {
+ // Make sure that no overscroll event is sent to document.
+ document.addEventListener("overscroll",
+ t.unreached_func("Document got unexpected overscroll event."));
+ await waitForCompositorCommit();
+
+ // Do a horizontal scroll and wait for overscroll event.
+ await touchScrollInTarget(300, scrolling_div , 'right');
+ await waitFor(() => { return overscrolled_x_delta > 0; },
+ 'Scroller did not receive overscroll event after horizontal scroll.');
+ assert_equals(scrolling_div.scrollWidth - scrolling_div.scrollLeft,
+ scrolling_div.clientWidth);
+
+ overscrolled_x_delta = 0;
+ overscrolled_y_delta = 0;
+
+ // Do a vertical scroll and wait for overscroll event.
+ await touchScrollInTarget(300, scrolling_div, 'down');
+ await waitFor(() => { return overscrolled_y_delta > 0; },
+ 'Scroller did not receive overscroll event after vertical scroll.');
+ assert_equals(scrolling_div.scrollHeight - scrolling_div.scrollTop,
+ scrolling_div.clientHeight);
+ }, 'Tests that the scrolled element gets overscroll event after fully scrolling by touch.');
+}
+</script>
diff --git a/testing/web-platform/tests/dom/events/scrolling/overscroll-event-fired-to-window.html b/testing/web-platform/tests/dom/events/scrolling/overscroll-event-fired-to-window.html
new file mode 100644
index 0000000000..ef5ae3daef
--- /dev/null
+++ b/testing/web-platform/tests/dom/events/scrolling/overscroll-event-fired-to-window.html
@@ -0,0 +1,52 @@
+<!DOCTYPE HTML>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-actions.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="scroll_support.js"></script>
+<style>
+#targetDiv {
+ width: 200px;
+ height: 200px;
+ overflow: scroll;
+}
+
+#innerDiv {
+ width: 400px;
+ height: 400px;
+}
+</style>
+
+<body style="margin:0" onload=runTest()>
+<div id="targetDiv">
+ <div id="innerDiv">
+ </div>
+</div>
+</body>
+
+<script>
+var target_div = document.getElementById('targetDiv');
+var window_received_overscroll = false;
+
+function onOverscroll(event) {
+ assert_false(event.cancelable);
+ // overscroll events targetting document are bubbled to the window.
+ assert_true(event.bubbles);
+ window_received_overscroll = true;
+}
+window.addEventListener("overscroll", onOverscroll);
+
+function runTest() {
+ promise_test (async (t) => {
+ // Make sure that no overscroll event is sent to target_div.
+ target_div.addEventListener("overscroll",
+ t.unreached_func("target_div got unexpected overscroll event."));
+ // Scroll up on target div and wait for the window to get overscroll event.
+ await touchScrollInTarget(300, target_div, 'up');
+ await waitFor(() => { return window_received_overscroll; },
+ 'Window did not receive overscroll event after scroll up on target.');
+ }, 'Tests that the window gets overscroll event when no element scrolls' +
+ 'after touch scrolling.');
+}
+</script>
diff --git a/testing/web-platform/tests/dom/events/scrolling/scroll_support.js b/testing/web-platform/tests/dom/events/scrolling/scroll_support.js
new file mode 100644
index 0000000000..169393e4c3
--- /dev/null
+++ b/testing/web-platform/tests/dom/events/scrolling/scroll_support.js
@@ -0,0 +1,163 @@
+async function waitForScrollendEvent(test, target, timeoutMs = 500) {
+ return new Promise((resolve, reject) => {
+ const timeoutCallback = test.step_timeout(() => {
+ reject(`No Scrollend event received for target ${target}`);
+ }, timeoutMs);
+ target.addEventListener('scrollend', (evt) => {
+ clearTimeout(timeoutCallback);
+ resolve(evt);
+ }, { once: true });
+ });
+}
+
+const MAX_FRAME = 700;
+const MAX_UNCHANGED_FRAMES = 20;
+
+// Returns a promise that resolves when the given condition is met or rejects
+// after MAX_FRAME animation frames.
+// TODO(crbug.com/1400399): deprecate. We should not use frame based waits in
+// WPT as frame rates may vary greatly in different testing environments.
+function waitFor(condition, error_message = 'Reaches the maximum frames.') {
+ return new Promise((resolve, reject) => {
+ function tick(frames) {
+ // We requestAnimationFrame either for MAX_FRAM frames or until condition
+ // is met.
+ if (frames >= MAX_FRAME)
+ reject(error_message);
+ else if (condition())
+ resolve();
+ else
+ requestAnimationFrame(tick.bind(this, frames + 1));
+ }
+ tick(0);
+ });
+}
+
+// TODO(crbug.com/1400446): Test driver should defer sending events until the
+// browser is ready. Also the term compositor-commit is misleading as not all
+// user-agents use a compositor process.
+function waitForCompositorCommit() {
+ return new Promise((resolve) => {
+ // rAF twice.
+ window.requestAnimationFrame(() => {
+ window.requestAnimationFrame(resolve);
+ });
+ });
+}
+
+// TODO(crbug.com/1400399): Deprecate as frame rates may vary greatly in
+// different test environments.
+function waitForAnimationEnd(getValue) {
+ var last_changed_frame = 0;
+ var last_position = getValue();
+ return new Promise((resolve, reject) => {
+ function tick(frames) {
+ // We requestAnimationFrame either for MAX_FRAME or until
+ // MAX_UNCHANGED_FRAMES with no change have been observed.
+ if (frames >= MAX_FRAME || frames - last_changed_frame > MAX_UNCHANGED_FRAMES) {
+ resolve();
+ } else {
+ current_value = getValue();
+ if (last_position != current_value) {
+ last_changed_frame = frames;
+ last_position = current_value;
+ }
+ requestAnimationFrame(tick.bind(this, frames + 1));
+ }
+ }
+ tick(0);
+ })
+}
+
+// Scrolls in target according to move_path with pauses in between
+function touchScrollInTargetSequentiallyWithPause(target, move_path, pause_time_in_ms = 100) {
+ const test_driver_actions = new test_driver.Actions()
+ .addPointer("pointer1", "touch")
+ .pointerMove(0, 0, {origin: target})
+ .pointerDown();
+
+ const substeps = 5;
+ let x = 0;
+ let y = 0;
+ // Do each move in 5 steps
+ for(let move of move_path) {
+ let step_x = (move.x - x) / substeps;
+ let step_y = (move.y - y) / substeps;
+ for(let step = 0; step < substeps; step++) {
+ x += step_x;
+ y += step_y;
+ test_driver_actions.pointerMove(x, y, {origin: target});
+ }
+ test_driver_actions.pause(pause_time_in_ms);
+ }
+
+ return test_driver_actions.pointerUp().send();
+}
+
+function touchScrollInTarget(pixels_to_scroll, target, direction, pause_time_in_ms = 100) {
+ var x_delta = 0;
+ var y_delta = 0;
+ const num_movs = 5;
+ if (direction == "down") {
+ y_delta = -1 * pixels_to_scroll / num_movs;
+ } else if (direction == "up") {
+ y_delta = pixels_to_scroll / num_movs;
+ } else if (direction == "right") {
+ x_delta = -1 * pixels_to_scroll / num_movs;
+ } else if (direction == "left") {
+ x_delta = pixels_to_scroll / num_movs;
+ } else {
+ throw("scroll direction '" + direction + "' is not expected, direction should be 'down', 'up', 'left' or 'right'");
+ }
+ return new test_driver.Actions()
+ .addPointer("pointer1", "touch")
+ .pointerMove(0, 0, {origin: target})
+ .pointerDown()
+ .pointerMove(x_delta, y_delta, {origin: target})
+ .pointerMove(2 * x_delta, 2 * y_delta, {origin: target})
+ .pointerMove(3 * x_delta, 3 * y_delta, {origin: target})
+ .pointerMove(4 * x_delta, 4 * y_delta, {origin: target})
+ .pointerMove(5 * x_delta, 5 * y_delta, {origin: target})
+ .pause(pause_time_in_ms)
+ .pointerUp()
+ .send();
+}
+
+// Trigger fling by doing pointerUp right after pointerMoves.
+function touchFlingInTarget(pixels_to_scroll, target, direction) {
+ touchScrollInTarget(pixels_to_scroll, target, direction, 0 /* pause_time */);
+}
+
+function mouseActionsInTarget(target, origin, delta, pause_time_in_ms = 100) {
+ return new test_driver.Actions()
+ .addPointer("pointer1", "mouse")
+ .pointerMove(origin.x, origin.y, { origin: target })
+ .pointerDown()
+ .pointerMove(origin.x + delta.x, origin.y + delta.y, { origin: target })
+ .pointerMove(origin.x + delta.x * 2, origin.y + delta.y * 2, { origin: target })
+ .pause(pause_time_in_ms)
+ .pointerUp()
+ .send();
+}
+
+// Returns a promise that resolves when the given condition holds for 10
+// animation frames or rejects if the condition changes to false within 10
+// animation frames.
+// TODO(crbug.com/1400399): Deprecate as frame rates may very greatly in
+// different test environments.
+function conditionHolds(condition, error_message = 'Condition is not true anymore.') {
+ const MAX_FRAME = 10;
+ return new Promise((resolve, reject) => {
+ function tick(frames) {
+ // We requestAnimationFrame either for 10 frames or until condition is
+ // violated.
+ if (frames >= MAX_FRAME)
+ resolve();
+ else if (!condition())
+ reject(error_message);
+ else
+ requestAnimationFrame(tick.bind(this, frames + 1));
+ }
+ tick(0);
+ });
+}
diff --git a/testing/web-platform/tests/dom/events/scrolling/scrollend-event-fired-after-sequence-of-scrolls.tentative.html b/testing/web-platform/tests/dom/events/scrolling/scrollend-event-fired-after-sequence-of-scrolls.tentative.html
new file mode 100644
index 0000000000..77bf029ced
--- /dev/null
+++ b/testing/web-platform/tests/dom/events/scrolling/scrollend-event-fired-after-sequence-of-scrolls.tentative.html
@@ -0,0 +1,63 @@
+<!DOCTYPE HTML>
+<meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-actions.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="scroll_support.js"></script>
+<style>
+#targetDiv {
+ width: 200px;
+ height: 200px;
+ overflow: scroll;
+}
+
+#innerDiv {
+ width: 500px;
+ height: 4000px;
+}
+</style>
+
+<body style="margin:0" onload=runTest()>
+<div id="targetDiv">
+ <div id="innerDiv">
+ </div>
+</div>
+</body>
+
+<script>
+const target_div = document.getElementById('targetDiv');
+let scrollend_arrived = false;
+let scrollend_event_count = 0;
+
+function onScrollEnd(event) {
+ assert_false(event.cancelable);
+ assert_false(event.bubbles);
+ scrollend_arrived = true;
+ scrollend_event_count += 1;
+}
+
+function runTest() {
+ promise_test (async (t) => {
+ // Make sure that no scrollend event is sent to document.
+ document.addEventListener("scrollend",
+ t.unreached_func("document got unexpected scrollend event."));
+ await waitForCompositorCommit();
+
+ // Scroll down & up & down on target div and wait for the target_div to get scrollend event.
+ target_div.addEventListener("scrollend", onScrollEnd);
+ const move_path = [
+ { x: 0, y: -300}, // down
+ { x: 0, y: -100}, // up
+ { x: 0, y: -400}, // down
+ { x: 0, y: -200}, // up
+ ];
+ await touchScrollInTargetSequentiallyWithPause(target_div, move_path, 150);
+
+ await waitFor(() => {return scrollend_arrived;},
+ 'target_div did not receive scrollend event after sequence of scrolls on target.');
+ assert_equals(scrollend_event_count, 1);
+ }, "Move down, up and down again, receive scrollend event only once");
+}
+</script>
diff --git a/testing/web-platform/tests/dom/events/scrolling/scrollend-event-fired-after-snap.html b/testing/web-platform/tests/dom/events/scrolling/scrollend-event-fired-after-snap.html
new file mode 100644
index 0000000000..03079ddc6c
--- /dev/null
+++ b/testing/web-platform/tests/dom/events/scrolling/scrollend-event-fired-after-snap.html
@@ -0,0 +1,87 @@
+<!DOCTYPE html>
+<meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-actions.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="scroll_support.js"></script>
+<style>
+div {
+ position: absolute;
+}
+#scroller {
+ width: 500px;
+ height: 500px;
+ overflow: scroll;
+ scroll-snap-type: both mandatory;
+ border: solid black 5px;
+}
+#space {
+ width: 2000px;
+ height: 2000px;
+}
+.target {
+ width: 200px;
+ height: 200px;
+ scroll-snap-align: start;
+ background-color: blue;
+}
+</style>
+
+<body style="margin:0" onload=runTests()>
+ <div id="scroller">
+ <div id="space"></div>
+ <div class="target" style="left: 0px; top: 0px;"></div>
+ <div class="target" style="left: 80px; top: 80px;"></div>
+ <div class="target" style="left: 200px; top: 200px;"></div>
+ </div>
+</body>
+
+<script>
+var scroller = document.getElementById("scroller");
+var space = document.getElementById("space");
+const MAX_FRAME_COUNT = 700;
+const MAX_UNCHANGED_FRAME = 20;
+
+function scrollTop() {
+ return scroller.scrollTop;
+}
+
+var scroll_arrived_after_scroll_end = false;
+var scroll_end_arrived = false;
+scroller.addEventListener("scroll", () => {
+ if (scroll_end_arrived)
+ scroll_arrived_after_scroll_end = true;
+});
+scroller.addEventListener("scrollend", () => {
+ scroll_end_arrived = true;
+});
+
+function runTests() {
+ promise_test (async () => {
+ await waitForCompositorCommit();
+ await touchScrollInTarget(100, scroller, 'down');
+ // Wait for the scroll snap animation to finish.
+ await waitForAnimationEnd(scrollTop);
+ await waitFor(() => { return scroll_end_arrived; });
+ // Verify that scroll snap animation has finished before firing scrollend event.
+ assert_false(scroll_arrived_after_scroll_end);
+ }, "Tests that scrollend is fired after scroll snap animation completion.");
+
+ promise_test (async () => {
+ // Reset scroll state.
+ scroller.scrollTo(0, 0);
+ await waitForCompositorCommit();
+ scroll_end_arrived = false;
+ scroll_arrived_after_scroll_end = false;
+
+ await touchFlingInTarget(50, scroller, 'down');
+ // Wait for the scroll snap animation to finish.
+ await waitForAnimationEnd(scrollTop);
+ await waitFor(() => { return scroll_end_arrived; });
+ // Verify that scroll snap animation has finished before firing scrollend event.
+ assert_false(scroll_arrived_after_scroll_end);
+ }, "Tests that scrollend is fired after fling snap animation completion.");
+}
+</script>
diff --git a/testing/web-platform/tests/dom/events/scrolling/scrollend-event-fired-for-programmatic-scroll.html b/testing/web-platform/tests/dom/events/scrolling/scrollend-event-fired-for-programmatic-scroll.html
new file mode 100644
index 0000000000..c6569e0beb
--- /dev/null
+++ b/testing/web-platform/tests/dom/events/scrolling/scrollend-event-fired-for-programmatic-scroll.html
@@ -0,0 +1,135 @@
+<!DOCTYPE HTML>
+<meta name="timeout" content="long">
+<meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-actions.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="scroll_support.js"></script>
+<style>
+html {
+ height: 3000px;
+ width: 3000px;
+}
+#targetDiv {
+ width: 200px;
+ height: 200px;
+ overflow: scroll;
+}
+
+#innerDiv {
+ width: 400px;
+ height: 400px;
+}
+</style>
+
+<body style="margin:0" onload=runTest()>
+<div id="targetDiv">
+ <div id="innerDiv">
+ </div>
+</div>
+</body>
+<script>
+var element_scrollend_arrived = false;
+var document_scrollend_arrived = false;
+
+function onElementScrollEnd(event) {
+ assert_false(event.cancelable);
+ assert_false(event.bubbles);
+ element_scrollend_arrived = true;
+}
+
+function onDocumentScrollEnd(event) {
+ assert_false(event.cancelable);
+ // scrollend events are bubbled when the target node is document.
+ assert_true(event.bubbles);
+ document_scrollend_arrived = true;
+}
+
+function callScrollFunction([scrollTarget, scrollFunction, args]) {
+ scrollTarget[scrollFunction](args);
+}
+
+function runTest() {
+ let root_element = document.scrollingElement;
+ let target_div = document.getElementById("targetDiv");
+
+ promise_test (async (t) => {
+ await waitForCompositorCommit();
+ target_div.addEventListener("scrollend", onElementScrollEnd);
+ document.addEventListener("scrollend", onDocumentScrollEnd);
+
+ let test_cases = [
+ [target_div, 200, 200, [target_div, "scrollTo", { top: 200, left: 200, behavior: "auto" }]],
+ [target_div, 0, 0, [target_div, "scrollTo", { top: 0, left: 0, behavior: "smooth" }]],
+ [root_element, 200, 200, [root_element, "scrollTo", { top: 200, left: 200, behavior: "auto" }]],
+ [root_element, 0, 0, [root_element, "scrollTo", { top: 0, left: 0, behavior: "smooth" }]],
+ [target_div, 200, 200, [target_div, "scrollBy", { top: 200, left: 200, behavior: "auto" }]],
+ [target_div, 0, 0, [target_div, "scrollBy", { top: -200, left: -200, behavior: "smooth" }]],
+ [root_element, 200, 200, [root_element, "scrollBy", { top: 200, left: 200, behavior: "auto" }]],
+ [root_element, 0, 0, [root_element, "scrollBy", { top: -200, left: -200, behavior: "smooth" }]]
+ ];
+
+ for(i = 0; i < test_cases.length; i++) {
+ let t = test_cases[i];
+ let target = t[0];
+ let expected_x = t[1];
+ let expected_y = t[2];
+ let scroll_datas = t[3];
+
+ callScrollFunction(scroll_datas);
+ await waitFor(() => { return element_scrollend_arrived || document_scrollend_arrived; }, target.tagName + "." + scroll_datas[1] + " did not receive scrollend event.");
+ if (target == root_element)
+ assert_false(element_scrollend_arrived);
+ else
+ assert_false(document_scrollend_arrived);
+ assert_equals(target.scrollLeft, expected_x, target.tagName + "." + scroll_datas[1] + " scrollLeft");
+ assert_equals(target.scrollTop, expected_y, target.tagName + "." + scroll_datas[1] + " scrollTop");
+
+ element_scrollend_arrived = false;
+ document_scrollend_arrived = false;
+ }
+ }, "Tests scrollend event for calling scroll functions.");
+
+ promise_test(async (t) => {
+ await waitForCompositorCommit();
+
+ let test_cases = [
+ [target_div, "scrollTop"],
+ [target_div, "scrollLeft"],
+ [root_element, "scrollTop"],
+ [root_element, "scrollLeft"]
+ ];
+ for (i = 0; i < test_cases.length; i++) {
+ let t = test_cases[i];
+ let target = t[0];
+ let attribute = t[1];
+ let position = 200;
+
+ target.style.scrollBehavior = "smooth";
+ target[attribute] = position;
+ await waitFor(() => { return element_scrollend_arrived || document_scrollend_arrived; }, target.tagName + "." + attribute + " did not receive scrollend event.");
+ if (target == root_element)
+ assert_false(element_scrollend_arrived);
+ else
+ assert_false(document_scrollend_arrived);
+ assert_equals(target[attribute], position, target.tagName + "." + attribute + " ");
+ element_scrollend_arrived = false;
+ document_scrollend_arrived = false;
+
+ await waitForCompositorCommit();
+ target.style.scrollBehavior = "auto";
+ target[attribute] = 0;
+ await waitFor(() => { return element_scrollend_arrived || document_scrollend_arrived; }, target.tagName + "." + attribute + " did not receive scrollend event.");
+ if (target == root_element)
+ assert_false(element_scrollend_arrived);
+ else
+ assert_false(document_scrollend_arrived);
+ assert_equals(target[attribute], 0, target.tagName + "." + attribute + " ");
+ element_scrollend_arrived = false;
+ document_scrollend_arrived = false;
+ }
+ }, "Tests scrollend event for changing scroll attributes.");
+}
+</script>
diff --git a/testing/web-platform/tests/dom/events/scrolling/scrollend-event-fired-for-scrollIntoView.html b/testing/web-platform/tests/dom/events/scrolling/scrollend-event-fired-for-scrollIntoView.html
new file mode 100644
index 0000000000..8782b1dfee
--- /dev/null
+++ b/testing/web-platform/tests/dom/events/scrolling/scrollend-event-fired-for-scrollIntoView.html
@@ -0,0 +1,124 @@
+<!DOCTYPE HTML>
+<meta name="timeout" content="long">
+<meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-actions.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="scroll_support.js"></script>
+<style>
+html {
+ height: 3000px;
+ width: 3000px;
+}
+#targetDiv {
+ width: 200px;
+ height: 200px;
+ overflow: scroll;
+}
+
+#innerDiv {
+ width: 400px;
+ height: 400px;
+}
+</style>
+
+<body style="margin:0" onload=runTest()>
+<div id="targetDiv">
+ <div id="innerDiv">
+ </div>
+</div>
+</body>
+<script>
+var element_scrollend_arrived = false;
+var document_scrollend_arrived = false;
+
+function onElementScrollEnd(event) {
+ assert_false(event.cancelable);
+ assert_false(event.bubbles);
+ element_scrollend_arrived = true;
+}
+
+function onDocumentScrollEnd(event) {
+ assert_false(event.cancelable);
+ // scrollend events are bubbled when the target node is document.
+ assert_true(event.bubbles);
+ document_scrollend_arrived = true;
+}
+
+function callScrollFunction([scrollTarget, scrollFunction, args]) {
+ scrollTarget[scrollFunction](args);
+}
+
+function runTest() {
+ let root_element = document.scrollingElement;
+ let target_div = document.getElementById("targetDiv");
+ let inner_div = document.getElementById("innerDiv");
+
+ // Get expected position for root_element scrollIntoView.
+ root_element.scrollTo(10000, 10000);
+ let max_root_x = root_element.scrollLeft;
+ let max_root_y = root_element.scrollTop;
+ root_element.scrollTo(0, 0);
+
+ target_div.scrollTo(10000, 10000);
+ let max_element_x = target_div.scrollLeft;
+ let max_element_y = target_div.scrollTop;
+ target_div.scrollTo(0, 0);
+
+ promise_test (async (t) => {
+ await waitForCompositorCommit();
+ target_div.addEventListener("scrollend", onElementScrollEnd);
+ document.addEventListener("scrollend", onDocumentScrollEnd);
+
+ let test_cases = [
+ [target_div, max_element_x, max_element_y, [inner_div, "scrollIntoView", { inline: "end", block: "end", behavior: "auto" }]],
+ [target_div, 0, 0, [inner_div, "scrollIntoView", { inline: "start", block: "start", behavior: "smooth" }]],
+ [root_element, max_root_x, max_root_y, [root_element, "scrollIntoView", { inline: "end", block: "end", behavior: "smooth" }]],
+ [root_element, 0, 0, [root_element, "scrollIntoView", { inline: "start", block: "start", behavior: "smooth" }]]
+ ];
+
+ for(i = 0; i < test_cases.length; i++) {
+ let t = test_cases[i];
+ let target = t[0];
+ let expected_x = t[1];
+ let expected_y = t[2];
+ let scroll_datas = t[3];
+
+ callScrollFunction(scroll_datas);
+ await waitFor(() => { return element_scrollend_arrived || document_scrollend_arrived; }, target.tagName + ".scrollIntoView did not receive scrollend event.");
+ if (target == root_element)
+ assert_false(element_scrollend_arrived);
+ else
+ assert_false(document_scrollend_arrived);
+ assert_equals(target.scrollLeft, expected_x, target.tagName + ".scrollIntoView scrollLeft");
+ assert_equals(target.scrollTop, expected_y, target.tagName + ".scrollIntoView scrollTop");
+
+ element_scrollend_arrived = false;
+ document_scrollend_arrived = false;
+ }
+ }, "Tests scrollend event for scrollIntoView.");
+
+ promise_test(async (t) => {
+ document.body.removeChild(target_div);
+ let out_div = document.createElement("div");
+ out_div.style = "width: 100px; height:100px; overflow:scroll; scroll-behavior:smooth;";
+ out_div.appendChild(target_div);
+ document.body.appendChild(out_div);
+ await waitForCompositorCommit();
+
+ element_scrollend_arrived = false;
+ document_scrollend_arrived = false;
+ inner_div.scrollIntoView({ inline: "end", block: "end", behavior: "auto" });
+ await waitFor(() => { return element_scrollend_arrived || document_scrollend_arrived; }, "Nested scrollIntoView did not receive scrollend event.");
+ assert_equals(root_element.scrollLeft, 0, "Nested scrollIntoView root_element scrollLeft");
+ assert_equals(root_element.scrollTop, 0, "Nested scrollIntoView root_element scrollTop");
+ assert_equals(out_div.scrollLeft, 100, "Nested scrollIntoView out_div scrollLeft");
+ assert_equals(out_div.scrollTop, 100, "Nested scrollIntoView out_div scrollTop");
+ assert_equals(target_div.scrollLeft, max_element_x, "Nested scrollIntoView target_div scrollLeft");
+ assert_equals(target_div.scrollTop, max_element_y, "Nested scrollIntoView target_div scrollTop");
+ assert_false(document_scrollend_arrived);
+ }, "Tests scrollend event for nested scrollIntoView.");
+}
+</script>
diff --git a/testing/web-platform/tests/dom/events/scrolling/scrollend-event-fired-to-document.html b/testing/web-platform/tests/dom/events/scrolling/scrollend-event-fired-to-document.html
new file mode 100644
index 0000000000..3090455388
--- /dev/null
+++ b/testing/web-platform/tests/dom/events/scrolling/scrollend-event-fired-to-document.html
@@ -0,0 +1,70 @@
+<!DOCTYPE HTML>
+<meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-actions.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="scroll_support.js"></script>
+<style>
+#targetDiv {
+ width: 200px;
+ height: 200px;
+ overflow: scroll;
+}
+
+#innerDiv {
+ width: 400px;
+ height: 400px;
+}
+</style>
+
+<body style="margin:0" onload=runTest()>
+<div id="targetDiv">
+ <div id="innerDiv">
+ </div>
+</div>
+</body>
+
+<script>
+var target_div = document.getElementById('targetDiv');
+var horizontal_scrollend_arrived = false;
+var vertical_scrollend_arrived = false;
+function onHorizontalScrollEnd(event) {
+ assert_false(event.cancelable);
+ // scrollend events are bubbled when the target node is document.
+ assert_true(event.bubbles);
+ horizontal_scrollend_arrived = true;
+}
+function onVerticalScrollEnd(event) {
+ assert_false(event.cancelable);
+ // scrollend events are bubbled when the target node is document.
+ assert_true(event.bubbles);
+ vertical_scrollend_arrived = true;
+}
+
+function runTest() {
+ promise_test (async (t) => {
+ // Make sure that no scrollend event is sent to target_div.
+ target_div.addEventListener("scrollend",
+ t.unreached_func("target_div got unexpected scrollend event."));
+ await waitForCompositorCommit();
+
+ // Scroll left on target div and wait for the doc to get scrollend event.
+ document.addEventListener("scrollend", onHorizontalScrollEnd);
+ await touchScrollInTarget(300, target_div, 'left');
+ await waitFor(() => { return horizontal_scrollend_arrived; },
+ 'Document did not receive scrollend event after scroll left on target.');
+ assert_equals(target_div.scrollLeft, 0);
+ document.removeEventListener("scrollend", onHorizontalScrollEnd);
+
+ // Scroll up on target div and wait for the doc to get scrollend event.
+ document.addEventListener("scrollend", onVerticalScrollEnd);
+ await touchScrollInTarget(300, target_div, 'up');
+ await waitFor(() => { return vertical_scrollend_arrived; },
+ 'Document did not receive scrollend event after scroll up on target.');
+ assert_equals(target_div.scrollTop, 0);
+ }, 'Tests that the document gets scrollend event when no element scrolls by ' +
+ 'touch.');
+}
+</script>
diff --git a/testing/web-platform/tests/dom/events/scrolling/scrollend-event-fired-to-element-with-overscroll-behavior.html b/testing/web-platform/tests/dom/events/scrolling/scrollend-event-fired-to-element-with-overscroll-behavior.html
new file mode 100644
index 0000000000..acad168e56
--- /dev/null
+++ b/testing/web-platform/tests/dom/events/scrolling/scrollend-event-fired-to-element-with-overscroll-behavior.html
@@ -0,0 +1,102 @@
+<!DOCTYPE HTML>
+<meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-actions.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="scroll_support.js"></script>
+<style>
+#overscrollXDiv {
+ width: 600px;
+ height: 600px;
+ overflow: scroll;
+ overscroll-behavior-x: contain;
+}
+#overscrollYDiv {
+ width: 500px;
+ height: 500px;
+ overflow: scroll;
+ overscroll-behavior-y: none;
+}
+#targetDiv {
+ width: 400px;
+ height: 400px;
+ overflow: scroll;
+}
+.content {
+ width:800px;
+ height:800px;
+}
+</style>
+
+<body style="margin:0" onload=runTest()>
+<div id="overscrollXDiv">
+ <div class=content>
+ <div id="overscrollYDiv">
+ <div class=content>
+ <div id="targetDiv">
+ <div class="content">
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+</div>
+</body>
+
+<script>
+var target_div = document.getElementById('targetDiv');
+var horizontal_scrollend_arrived = false;
+var vertical_scrollend_arrived = false;
+function onHorizontalScrollEnd(event) {
+ assert_false(event.cancelable);
+ assert_false(event.bubbles);
+ horizontal_scrollend_arrived = true;
+}
+function onVerticalScrollEnd(event) {
+ assert_false(event.cancelable);
+ assert_false(event.bubbles);
+ vertical_scrollend_arrived = true;
+}
+// Test that both "onscrollend" and addEventListener("scrollend"... work.
+document.getElementById('overscrollXDiv').onscrollend = onHorizontalScrollEnd;
+document.getElementById('overscrollYDiv').
+ addEventListener("scrollend", onVerticalScrollEnd);
+
+function runTest() {
+ promise_test (async (t) => {
+ // Make sure that no scrollend event is sent to document or target_div.
+ document.addEventListener("scrollend",
+ t.unreached_func("Document got unexpected scrollend event."));
+ target_div.addEventListener("scrollend",
+ t.unreached_func("target_div got unexpected scrollend event."));
+ await waitForCompositorCommit();
+
+ // Scroll left on target div and wait for the element with overscroll-x to
+ // get scrollend event.
+ await touchScrollInTarget(300, target_div, 'left');
+ await waitFor(() => { return horizontal_scrollend_arrived; },
+ 'Expected element did not receive scrollend event after scroll left ' +
+ 'on target.');
+ assert_equals(target_div.scrollLeft, 0);
+
+ let touchEndPromise = new Promise((resolve) => {
+ target_div.addEventListener("touchend", resolve);
+ });
+ await touchScrollInTarget(300, target_div, 'up');
+
+ // The scrollend event should never be fired before the gesture has completed.
+ await touchEndPromise;
+
+ // Ensure we wait at least a tick after the touch end.
+ await waitForCompositorCommit();
+
+ // We should not trigger a scrollend event for a scroll that did not change
+ // the scroll position.
+ assert_equals(vertical_scrollend_arrived, false);
+ assert_equals(target_div.scrollTop, 0);
+ }, 'Tests that the last element in the cut scroll chain gets scrollend ' +
+ 'event when no element scrolls by touch.');
+}
+</script>
diff --git a/testing/web-platform/tests/dom/events/scrolling/scrollend-event-fired-to-scrolled-element.html b/testing/web-platform/tests/dom/events/scrolling/scrollend-event-fired-to-scrolled-element.html
new file mode 100644
index 0000000000..7343396942
--- /dev/null
+++ b/testing/web-platform/tests/dom/events/scrolling/scrollend-event-fired-to-scrolled-element.html
@@ -0,0 +1,68 @@
+<!DOCTYPE HTML>
+<meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-actions.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="scroll_support.js"></script>
+<style>
+#scrollableDiv {
+ width: 200px;
+ height: 200px;
+ overflow: scroll;
+}
+
+#innerDiv {
+ width: 400px;
+ height: 400px;
+}
+</style>
+
+<body style="margin:0" onload=runTest()>
+<div id="scrollableDiv">
+ <div id="innerDiv">
+ </div>
+</div>
+</body>
+
+<script>
+var scrolling_div = document.getElementById('scrollableDiv');
+var horizontal_scrollend_arrived = false;
+var vertical_scrollend_arrived = false;
+function onHorizontalScrollEnd(event) {
+ assert_false(event.cancelable);
+ assert_false(event.bubbles);
+ horizontal_scrollend_arrived = true;
+}
+function onVerticalScrollEnd(event) {
+ assert_false(event.cancelable);
+ assert_false(event.bubbles);
+ vertical_scrollend_arrived = true;
+}
+scrolling_div.addEventListener("scrollend", onHorizontalScrollEnd);
+scrolling_div.addEventListener("scrollend", onVerticalScrollEnd);
+
+function runTest() {
+ promise_test (async (t) => {
+ // Make sure that no scrollend event is sent to document.
+ document.addEventListener("scrollend",
+ t.unreached_func("Document got unexpected scrollend event."));
+ await waitForCompositorCommit();
+
+ // Do a horizontal scroll and wait for scrollend event.
+ await touchScrollInTarget(300, scrolling_div, 'right');
+ await waitFor(() => { return horizontal_scrollend_arrived; },
+ 'Scroller did not receive scrollend event after horizontal scroll.');
+ assert_equals(scrolling_div.scrollWidth - scrolling_div.scrollLeft,
+ scrolling_div.clientWidth);
+
+ // Do a vertical scroll and wait for scrollend event.
+ await touchScrollInTarget(300, scrolling_div, 'down');
+ await waitFor(() => { return vertical_scrollend_arrived; },
+ 'Scroller did not receive scrollend event after vertical scroll.');
+ assert_equals(scrolling_div.scrollHeight - scrolling_div.scrollTop,
+ scrolling_div.clientHeight);
+ }, 'Tests that the scrolled element gets scrollend event at the end of touch scrolling.');
+}
+</script>
diff --git a/testing/web-platform/tests/dom/events/scrolling/scrollend-event-fired-to-window.html b/testing/web-platform/tests/dom/events/scrolling/scrollend-event-fired-to-window.html
new file mode 100644
index 0000000000..ef72f56d2b
--- /dev/null
+++ b/testing/web-platform/tests/dom/events/scrolling/scrollend-event-fired-to-window.html
@@ -0,0 +1,55 @@
+<!DOCTYPE HTML>
+<meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-actions.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="scroll_support.js"></script>
+<style>
+#targetDiv {
+ width: 200px;
+ height: 200px;
+ overflow: scroll;
+}
+
+#innerDiv {
+ width: 400px;
+ height: 400px;
+}
+</style>
+
+<body style="margin:0" onload=runTest()>
+<div id="targetDiv">
+ <div id="innerDiv">
+ </div>
+</div>
+</body>
+
+<script>
+var target_div = document.getElementById('targetDiv');
+var scrollend_arrived = false;
+function onScrollEnd(event) {
+ assert_false(event.cancelable);
+ // scrollend events targetting document are bubbled to the window.
+ assert_true(event.bubbles);
+ scrollend_arrived = true;
+}
+window.addEventListener("scrollend", onScrollEnd);
+
+function runTest() {
+ promise_test (async (t) => {
+ // Make sure that no scrollend event is sent to target_div.
+ target_div.addEventListener("scrollend",
+ t.unreached_func("target_div got unexpected scrollend event."));
+ await waitForCompositorCommit();
+
+ // Scroll up on target div and wait for the doc to get scrollend event.
+ await touchScrollInTarget(300, target_div, 'up');
+ await waitFor(() => { return scrollend_arrived; },
+ 'Window did not receive scrollend event after scroll up on target.');
+ assert_equals(target_div.scrollTop, 0);
+ }, 'Tests that the window gets scrollend event when no element scrolls ' +
+ 'after touch scrolling.');
+}
+</script>
diff --git a/testing/web-platform/tests/dom/events/scrolling/scrollend-event-for-user-scroll.html b/testing/web-platform/tests/dom/events/scrolling/scrollend-event-for-user-scroll.html
new file mode 100644
index 0000000000..5146c5f719
--- /dev/null
+++ b/testing/web-platform/tests/dom/events/scrolling/scrollend-event-for-user-scroll.html
@@ -0,0 +1,199 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-actions.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="scroll_support.js"></script>
+<style>
+ #targetDiv {
+ width: 200px;
+ height: 200px;
+ overflow: scroll;
+ }
+
+ #innerDiv {
+ width: 400px;
+ height: 400px;
+ }
+</style>
+</head>
+<body style="margin:0" onload=runTest()>
+ <div id="targetDiv">
+ <div id="innerDiv">
+ </div>
+ </div>
+</body>
+
+<script>
+var target_div = document.getElementById('targetDiv');
+
+async function resetTargetScrollState(test) {
+ if (target_div.scrollTop != 0 || target_div.scrollLeft != 0) {
+ target_div.scrollTop = 0;
+ target_div.scrollLeft = 0;
+ return waitForScrollendEvent(test, target_div);
+ }
+}
+
+async function verifyScrollStopped(test) {
+ const unscaled_pause_time_in_ms = 100;
+ const x = target_div.scrollLeft;
+ const y = target_div.scrollTop;
+ return new Promise(resolve => {
+ test.step_timeout(() => {
+ assert_equals(x, target_div.scrollLeft);
+ assert_equals(y, target_div.scrollTop);
+ resolve();
+ }, unscaled_pause_time_in_ms);
+ });
+}
+
+async function verifyNoScrollendOnDocument(test) {
+ const callback =
+ test.unreached_func("window got unexpected scrollend event.");
+ window.addEventListener('scrollend', callback);
+}
+
+async function createScrollendPromise(test) {
+ return waitForScrollendEvent(test, target_div).then(evt => {
+ assert_false(evt.cancelable, 'Event is not cancelable');
+ assert_false(evt.bubbles, 'Event targeting element does not bubble');
+ });
+}
+
+function runTest() {
+ promise_test(async (t) => {
+ // Skip the test on a Mac as they do not support touch screens.
+ const isMac = navigator.platform.toUpperCase().indexOf('MAC')>=0;
+ if (isMac)
+ return;
+
+ await resetTargetScrollState(t);
+ await waitForCompositorCommit();
+
+ const targetScrollendPromise = createScrollendPromise(t);
+ verifyNoScrollendOnDocument(t);
+
+ // Perform a touch drag on target div and wait for target_div to get
+ // a scrollend event.
+ await new test_driver.Actions()
+ .addPointer('TestPointer', 'touch')
+ .pointerMove(0, 0, {origin: target_div}) // 0, 0 is center of element.
+ .pointerDown()
+ .addTick()
+ .pointerMove(0, -40, {origin: target_div}) // Drag up to move down.
+ .addTick()
+ .pause(200) // Prevent inertial scroll.
+ .pointerUp()
+ .send();
+
+ await targetScrollendPromise;
+
+ assert_true(target_div.scrollTop > 0);
+ await verifyScrollStopped(t);
+ }, 'Tests that the target_div gets scrollend event when touch dragging.');
+
+ promise_test(async (t) => {
+ // Skip test on platforms that do not have a visible scrollbar (e.g.
+ // overlay scrollbar).
+ const scrollbar_width = target_div.offsetWidth - target_div.clientWidth;
+ if (scrollbar_width == 0)
+ return;
+
+ await resetTargetScrollState(t);
+ await waitForCompositorCommit();
+
+ const targetScrollendPromise = createScrollendPromise(t);
+ verifyNoScrollendOnDocument(t);
+
+ const bounds = target_div.getBoundingClientRect();
+ const x = bounds.right - scrollbar_width / 2;
+ const y = bounds.bottom - 20;
+ await new test_driver.Actions()
+ .addPointer('TestPointer', 'mouse')
+ .pointerMove(x, y, {origin: 'viewport'})
+ .pointerDown()
+ .addTick()
+ .pointerUp()
+ .send();
+
+ await targetScrollendPromise;
+ assert_true(target_div.scrollTop > 0);
+ await verifyScrollStopped(t);
+ }, 'Tests that the target_div gets scrollend event when clicking ' +
+ 'scrollbar.');
+
+ // Same issue as previous test.
+ promise_test(async (t) => {
+ // Skip test on platforms that do not have a visible scrollbar (e.g.
+ // overlay scrollbar).
+ const scrollbar_width = target_div.offsetWidth - target_div.clientWidth;
+ if (scrollbar_width == 0)
+ return;
+
+ resetTargetScrollState(t);
+ const targetScrollendPromise = createScrollendPromise(t);
+ verifyNoScrollendOnDocument(t);
+
+ const bounds = target_div.getBoundingClientRect();
+ const x = bounds.right - scrollbar_width / 2;
+ const y = bounds.top + 30;
+ const dy = 30;
+ await new test_driver.Actions()
+ .addPointer('TestPointer', 'mouse')
+ .pointerMove(x, y, { origin: 'viewport' })
+ .pointerDown()
+ .pointerMove(x, y + dy, { origin: 'viewport' })
+ .addTick()
+ .pointerUp()
+ .send();
+
+ await targetScrollendPromise;
+ assert_true(target_div.scrollTop > 0);
+ await verifyScrollStopped(t);
+ }, 'Tests that the target_div gets scrollend event when dragging the ' +
+ 'scrollbar thumb.');
+
+ promise_test(async (t) => {
+ resetTargetScrollState(t);
+ const targetScrollendPromise = createScrollendPromise(t);
+ verifyNoScrollendOnDocument(t);
+
+ const x = 0;
+ const y = 0;
+ const dx = 0;
+ const dy = 40;
+ const duration_ms = 10;
+ await new test_driver.Actions()
+ .scroll(x, y, dx, dy, { origin: target_div }, duration_ms)
+ .send();
+
+ await targetScrollendPromise;
+ assert_true(target_div.scrollTop > 0);
+ await verifyScrollStopped(t);
+ }, 'Tests that the target_div gets scrollend event when mouse wheel ' +
+ 'scrolling.');
+
+ promise_test(async (t) => {
+ await resetTargetScrollState(t);
+ await waitForCompositorCommit();
+
+ verifyNoScrollendOnDocument(t);
+ const targetScrollendPromise = createScrollendPromise(t);
+
+ target_div.focus();
+ window.test_driver.send_keys(target_div, '\ue015');
+
+ await targetScrollendPromise;
+ assert_true(target_div.scrollTop > 0);
+ await verifyScrollStopped(t);
+ }, 'Tests that the target_div gets scrollend event when sending DOWN key ' +
+ 'to the target.');
+}
+
+</script>
+</html>
diff --git a/testing/web-platform/tests/dom/events/scrolling/scrollend-event-handler-content-attributes.html b/testing/web-platform/tests/dom/events/scrolling/scrollend-event-handler-content-attributes.html
new file mode 100644
index 0000000000..47f563c39b
--- /dev/null
+++ b/testing/web-platform/tests/dom/events/scrolling/scrollend-event-handler-content-attributes.html
@@ -0,0 +1,108 @@
+<!DOCTYPE HTML>
+<meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-actions.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="scroll_support.js"></script>
+<style>
+html, body {
+ margin: 0
+}
+
+body {
+ height: 3000px;
+ width: 3000px;
+}
+
+#targetDiv {
+ width: 200px;
+ height: 200px;
+ overflow: scroll;
+}
+
+#innerDiv {
+ width: 400px;
+ height: 400px;
+}
+</style>
+
+<body onload=runTest() onscrollend="failOnScrollEnd(event)">
+<div id="targetDiv" onscrollend="onElementScrollEnd(event)">
+ <div id="innerDiv">
+ </div>
+</div>
+</body>
+<script>
+let element_scrollend_arrived = false;
+
+function onElementScrollEnd(event) {
+ assert_false(event.cancelable);
+ assert_false(event.bubbles);
+ element_scrollend_arrived = true;
+}
+
+function failOnScrollEnd(event) {
+ assert_true(false, "Scrollend should not be called on: " + event.target);
+}
+
+function runTest() {
+ let target_div = document.getElementById("targetDiv");
+
+ promise_test (async (t) => {
+ await waitForCompositorCommit();
+
+ target_div.scrollTo({top: 200, left: 200});
+ await waitFor(() => { return element_scrollend_arrived; },
+ target_div.tagName + " did not receive scrollend event.");
+ assert_equals(target_div.scrollLeft, 200, target_div.tagName + " scrollLeft");
+ assert_equals(target_div.scrollTop, 200, target_div.tagName + " scrollTop");
+ }, "Tests scrollend event is handled by event handler content attribute.");
+
+ promise_test (async (t) => {
+ await waitForCompositorCommit();
+
+ document.scrollingElement.scrollTo({top: 200, left: 200});
+ // The document body onscrollend event handler content attribute will fail
+ // here, if it is fired.
+ await waitForCompositorCommit();
+ assert_equals(document.scrollingElement.scrollLeft, 200,
+ "Document scrolled on horizontal axis");
+ assert_equals(document.scrollingElement.scrollTop, 200,
+ "Document scrolled on vertical axis");
+ }, "Tests scrollend event is not fired to document body event handler content attribute.");
+
+ promise_test (async (t) => {
+ await waitForCompositorCommit();
+
+ // Reset the scroll position.
+ document.scrollingElement.scrollTo({top: 0, left: 0});
+
+ let scrollend_event = new Promise(resolve => document.onscrollend = resolve);
+ document.scrollingElement.scrollTo({top: 200, left: 200});
+ await scrollend_event;
+
+ assert_equals(document.scrollingElement.scrollTop, 200,
+ "Document scrolled on horizontal axis");
+ assert_equals(document.scrollingElement.scrollLeft, 200,
+ "Document scrolled on vertical axis");
+ }, "Tests scrollend event is fired to document event handler property");
+
+ promise_test (async (t) => {
+ await waitForCompositorCommit();
+
+ // Reset the scroll position.
+ target_div.scrollTo({top: 0, left: 0});
+
+ let scrollend_event = new Promise(resolve => target_div.onscrollend = resolve);
+ target_div.scrollTo({top: 200, left: 200});
+ await scrollend_event;
+
+ assert_equals(target_div.scrollLeft, 200,
+ target_div.tagName + " scrolled on horizontal axis");
+ assert_equals(target_div.scrollLeft, 200,
+ target_div.tagName + " scrolled on vertical axis");
+ }, "Tests scrollend event is fired to element event handler property");
+}
+</script>
diff --git a/testing/web-platform/tests/dom/events/scrolling/scrollend-event-not-fired-after-removing-scroller.tentative.html b/testing/web-platform/tests/dom/events/scrolling/scrollend-event-not-fired-after-removing-scroller.tentative.html
new file mode 100644
index 0000000000..95447fbd12
--- /dev/null
+++ b/testing/web-platform/tests/dom/events/scrolling/scrollend-event-not-fired-after-removing-scroller.tentative.html
@@ -0,0 +1,84 @@
+<!DOCTYPE HTML>
+<meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-actions.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="scroll_support.js"></script>
+<style>
+#rootDiv {
+ width: 500px;
+ height: 500px;
+}
+
+#targetDiv {
+ width: 200px;
+ height: 200px;
+ overflow: scroll;
+}
+
+#innerDiv {
+ width: 500px;
+ height: 4000px;
+}
+</style>
+
+<body style="margin:0" onload=runTest()>
+</body>
+
+<script>
+let scrollend_arrived = false;
+
+async function setupHtmlAndScrollAndRemoveElement(element_to_remove_id) {
+ document.body.innerHTML=`
+ <div id="rootDiv">
+ <div id="targetDiv">
+ <div id="innerDiv">
+ </div>
+ </div>
+ </div>
+ `;
+ await waitForCompositorCommit();
+
+ const target_div = document.getElementById('targetDiv');
+ const element_to_remove = document.getElementById(element_to_remove_id);
+ let reached_half_scroll = false;
+ scrollend_arrived = false;
+
+ target_div.addEventListener("scrollend", () => {
+ scrollend_arrived = true;
+ });
+
+ target_div.onscroll = () => {
+ // Remove the element after reached half of the scroll offset,
+ if(target_div.scrollTop >= 1000) {
+ reached_half_scroll = true;
+ element_to_remove.remove();
+ }
+ };
+
+ target_div.scrollTo({top:2000, left:0, behavior:"smooth"});
+ await waitFor(() => {return reached_half_scroll; },
+ "target_div never reached scroll offset of 1000");
+ await waitForCompositorCommit();
+}
+
+function runTest() {
+ promise_test (async (t) => {
+ await setupHtmlAndScrollAndRemoveElement("rootDiv");
+ await conditionHolds(() => { return !scrollend_arrived; });
+ }, "No scrollend is received after removing parent div");
+
+ promise_test (async (t) => {
+ await setupHtmlAndScrollAndRemoveElement("targetDiv");
+ await conditionHolds(() => { return !scrollend_arrived; });
+ }, "No scrollend is received after removing scrolling element");
+
+ promise_test (async (t) => {
+ await setupHtmlAndScrollAndRemoveElement("innerDiv");
+ await waitFor(() => { return scrollend_arrived; },
+ 'target_div did not receive scrollend event after vertical scroll.');
+ }, "scrollend is received after removing descendant div");
+}
+</script>