async function waitForEvent(eventName, test, target, timeoutMs = 500) { return new Promise((resolve, reject) => { const timeoutCallback = test.step_timeout(() => { reject(`No ${eventName} event received for target ${target}`); }, timeoutMs); target.addEventListener(eventName, (evt) => { clearTimeout(timeoutCallback); resolve(evt); }, { once: true }); }); } async function waitForScrollendEvent(test, target, timeoutMs = 500) { return waitForEvent("scrollend", test, target, timeoutMs); } async function waitForScrollendEventNoTimeout(target) { return new Promise((resolve) => { target.addEventListener("scrollend", resolve); }); } // Waits until a rAF callback with no "scroll" event in the last 200ms. function waitForDelayWithoutScrollEvent(eventTarget) { const TIMEOUT_IN_MS = 200; return new Promise(resolve => { let lastScrollEventTime = performance.now(); const scrollListener = () => { lastScrollEventTime = performance.now(); }; eventTarget.addEventListener('scroll', scrollListener); const tick = () => { if (performance.now() - lastScrollEventTime > TIMEOUT_IN_MS) { eventTarget.removeEventListener('scroll', scrollListener); resolve(); return; } requestAnimationFrame(tick); // wait another frame } requestAnimationFrame(tick); }); } // Waits for the end of scrolling. Uses the "scrollend" event if available. // Otherwise, fall backs to waitForDelayWithoutScrollEvent(). function waitForScrollEndFallbackToDelayWithoutScrollEvent(eventTargets) { return new Promise(resolve => { if (!Array.isArray(eventTargets)) { eventTargets = [eventTargets]; } let listeners = []; const cleanup = () => { for (const [eventTarget, eventName, listener] of listeners) { eventTarget.removeEventListener(eventName, listener); } listeners = []; } const addListener = (eventTarget, eventName, listener) => { listeners.push([eventTarget, eventName, listener]); eventTarget.addEventListener(eventName, listener); } if (window.onscrollend !== undefined) { // If scrollend is supported, wait for the first scrollend event. for (const eventTarget of eventTargets) { addListener(eventTarget, 'scrollend', () => { cleanup(); resolve(eventTarget); }); } } else { // Otherwise, wait for the first scroll event, then wait until that // scroller finishes scrolling. for (const eventTarget of eventTargets) { addListener(eventTarget, 'scroll', async () => { cleanup(); await waitForDelayWithoutScrollEvent(eventTarget); resolve(eventTarget); }); } } }); } async function waitForPointercancelEvent(test, target, timeoutMs = 500) { return waitForEvent("pointercancel", test, target, timeoutMs); } // Resets the scroll position to (0,0). If a scroll is required, then the // promise is not resolved until the scrollend event is received. async function waitForScrollReset(test, scroller, x = 0, y = 0) { return new Promise(resolve => { if (scroller.scrollLeft == x && scroller.scrollTop == y) { resolve(); } else { const eventTarget = scroller == document.scrollingElement ? document : scroller; scroller.scrollTo(x, y); waitForScrollendEventNoTimeout(eventTarget).then(resolve); } }); } async function createScrollendPromiseForTarget(test, target_div, timeoutMs = 500, targetIsRoot = false) { return waitForScrollendEvent(test, target_div, timeoutMs).then(evt => { assert_false(evt.cancelable, 'Event is not cancelable'); if (targetIsRoot) { assert_true(evt.bubbles, 'Event targeting element does not bubble'); } else { assert_false(evt.bubbles, 'Event targeting element does not bubble'); } }); } function verifyNoScrollendOnDocument(test) { const callback = test.unreached_func("window got unexpected scrollend event."); window.addEventListener('scrollend', callback); test.add_cleanup(() => { window.removeEventListener('scrollend', callback); }); } async function verifyScrollStopped(test, target_div) { 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(target_div.scrollLeft, x); assert_equals(target_div.scrollTop, y); resolve(); }, unscaled_pause_time_in_ms); }); } async function resetTargetScrollState(test, target_div) { if (target_div.scrollTop != 0 || target_div.scrollLeft != 0) { target_div.scrollTop = 0; target_div.scrollLeft = 0; return waitForScrollendEvent(test, target_div); } } 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); }); }); } // Please don't remove this. This is necessary for chromium-based browsers. It // can be a no-op on user-agents that do not have a separate compositor thread. // TODO(crbug.com/1509054): This shouldn't be necessary if the test harness // deferred running the tests until after paint holding. async function waitForCompositorReady() { const animation = document.body.animate({ opacity: [ 0, 1 ] }, {duration: 1 }); return animation.finished; } function waitForNextFrame() { const startTime = performance.now(); return new Promise(resolve => { window.requestAnimationFrame((frameTime) => { if (frameTime < startTime) { window.requestAnimationFrame(resolve); } else { 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 // The move_path should contains coordinates that are within target boundaries. // Keep in mind that 0,0 is the center of the target element and is also // the pointerDown position. // pointerUp() is fired after sequence of moves. 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); // To prevent inertial scroll } 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) { return 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); }); } function scrollElementDown(element, scroll_amount) { let x = 0; let y = 0; let delta_x = 0; let delta_y = scroll_amount; let actions = new test_driver.Actions() .scroll(x, y, delta_x, delta_y, {origin: element}); return actions.send(); } function scrollElementLeft(element, scroll_amount) { let x = 0; let y = 0; let delta_x = scroll_amount; let delta_y = 0; let actions = new test_driver.Actions() .scroll(x, y, delta_x, delta_y, {origin: element}); return actions.send(); }