scroll_support.js (12803B)
1 async function waitForEvent(eventName, test, target, timeoutMs = 500) { 2 return new Promise((resolve, reject) => { 3 const timeoutCallback = test.step_timeout(() => { 4 reject(`No ${eventName} event received for target ${target}`); 5 }, timeoutMs); 6 target.addEventListener(eventName, (evt) => { 7 clearTimeout(timeoutCallback); 8 resolve(evt); 9 }, { once: true }); 10 }); 11 } 12 13 async function waitForScrollendEvent(test, target, timeoutMs = 500) { 14 return waitForEvent("scrollend", test, target, timeoutMs); 15 } 16 17 async function waitForScrollendEventNoTimeout(target) { 18 return new Promise((resolve) => { 19 target.addEventListener("scrollend", resolve); 20 }); 21 } 22 23 // Waits until a rAF callback with no "scroll" event in the last 200ms. 24 function waitForDelayWithoutScrollEvent(eventTarget) { 25 const TIMEOUT_IN_MS = 200; 26 27 return new Promise(resolve => { 28 let lastScrollEventTime = performance.now(); 29 30 const scrollListener = () => { 31 lastScrollEventTime = performance.now(); 32 }; 33 eventTarget.addEventListener('scroll', scrollListener); 34 35 const tick = () => { 36 if (performance.now() - lastScrollEventTime > TIMEOUT_IN_MS) { 37 eventTarget.removeEventListener('scroll', scrollListener); 38 resolve(); 39 return; 40 } 41 requestAnimationFrame(tick); // wait another frame 42 } 43 requestAnimationFrame(tick); 44 }); 45 } 46 47 // Waits for the end of scrolling. Uses the "scrollend" event if available. 48 // Otherwise, fall backs to waitForDelayWithoutScrollEvent(). 49 function waitForScrollEndFallbackToDelayWithoutScrollEvent(eventTargets) { 50 return new Promise(resolve => { 51 if (!Array.isArray(eventTargets)) { 52 eventTargets = [eventTargets]; 53 } 54 let listeners = []; 55 const cleanup = () => { 56 for (const [eventTarget, eventName, listener] of listeners) { 57 eventTarget.removeEventListener(eventName, listener); 58 } 59 listeners = []; 60 } 61 const addListener = (eventTarget, eventName, listener) => { 62 listeners.push([eventTarget, eventName, listener]); 63 eventTarget.addEventListener(eventName, listener); 64 } 65 if (window.onscrollend !== undefined) { 66 // If scrollend is supported, wait for the first scrollend event. 67 for (const eventTarget of eventTargets) { 68 addListener(eventTarget, 'scrollend', () => { 69 cleanup(); 70 resolve(eventTarget); 71 }); 72 } 73 } else { 74 // Otherwise, wait for the first scroll event, then wait until that 75 // scroller finishes scrolling. 76 for (const eventTarget of eventTargets) { 77 addListener(eventTarget, 'scroll', async () => { 78 cleanup(); 79 await waitForDelayWithoutScrollEvent(eventTarget); 80 resolve(eventTarget); 81 }); 82 } 83 } 84 }); 85 } 86 87 // Waits for the end of scrolling, but resolves after the given timeout if no 88 // scroll event occurs. 89 function waitForScrollEndOrTimeout(eventTarget, timeout) { 90 const rafTimeout = new Promise(resolve => { 91 const startTime = performance.now(); 92 const tick = () => { 93 if (performance.now() - startTime >= timeout) { 94 resolve(); 95 } else { 96 requestAnimationFrame(tick); 97 } 98 }; 99 requestAnimationFrame(tick); 100 }); 101 102 return Promise.race([ 103 waitForScrollEndFallbackToDelayWithoutScrollEvent(eventTarget), 104 rafTimeout 105 ]); 106 } 107 108 async function waitForPointercancelEvent(test, target, timeoutMs = 500) { 109 return waitForEvent("pointercancel", test, target, timeoutMs); 110 } 111 112 // Resets the scroll position to (0,0). If a scroll is required, then the 113 // promise is not resolved until the scrollend event is received. 114 async function waitForScrollReset(test, scroller, x = 0, y = 0) { 115 return new Promise(resolve => { 116 if (scroller.scrollLeft == x && scroller.scrollTop == y) { 117 resolve(); 118 } else { 119 const eventTarget = 120 scroller == document.scrollingElement ? document : scroller; 121 scroller.scrollTo(x, y); 122 waitForScrollendEventNoTimeout(eventTarget).then(resolve); 123 } 124 }); 125 } 126 127 async function createScrollendPromiseForTarget(test, 128 target_div, 129 timeoutMs = 500, 130 targetIsRoot = false) { 131 return waitForScrollendEvent(test, target_div, timeoutMs).then(evt => { 132 assert_false(evt.cancelable, 'Event is not cancelable'); 133 if (targetIsRoot) { 134 assert_true(evt.bubbles, 'Event targeting element does not bubble'); 135 } else { 136 assert_false(evt.bubbles, 'Event targeting element does not bubble'); 137 } 138 }); 139 } 140 141 function verifyNoScrollendOnDocument(test) { 142 const callback = 143 test.unreached_func("window got unexpected scrollend event."); 144 window.addEventListener('scrollend', callback); 145 test.add_cleanup(() => { 146 window.removeEventListener('scrollend', callback); 147 }); 148 } 149 150 async function verifyScrollStopped(test, target_div) { 151 const unscaled_pause_time_in_ms = 100; 152 const x = target_div.scrollLeft; 153 const y = target_div.scrollTop; 154 return new Promise(resolve => { 155 test.step_timeout(() => { 156 assert_equals(target_div.scrollLeft, x); 157 assert_equals(target_div.scrollTop, y); 158 resolve(); 159 }, unscaled_pause_time_in_ms); 160 }); 161 } 162 163 async function resetTargetScrollState(test, target_div) { 164 if (target_div.scrollTop != 0 || target_div.scrollLeft != 0) { 165 target_div.scrollTop = 0; 166 target_div.scrollLeft = 0; 167 return waitForScrollendEvent(test, target_div); 168 } 169 } 170 171 const MAX_FRAME = 700; 172 const MAX_UNCHANGED_FRAMES = 20; 173 174 // Returns a promise that resolves when the given condition is met or rejects 175 // after MAX_FRAME animation frames. 176 // TODO(crbug.com/1400399): deprecate. We should not use frame based waits in 177 // WPT as frame rates may vary greatly in different testing environments. 178 function waitFor(condition, error_message = 'Reaches the maximum frames.') { 179 return new Promise((resolve, reject) => { 180 function tick(frames) { 181 // We requestAnimationFrame either for MAX_FRAM frames or until condition 182 // is met. 183 if (frames >= MAX_FRAME) 184 reject(error_message); 185 else if (condition()) 186 resolve(); 187 else 188 requestAnimationFrame(tick.bind(this, frames + 1)); 189 } 190 tick(0); 191 }); 192 } 193 194 // TODO(crbug.com/1400446): Test driver should defer sending events until the 195 // browser is ready. Also the term compositor-commit is misleading as not all 196 // user-agents use a compositor process. 197 function waitForCompositorCommit() { 198 return new Promise((resolve) => { 199 // rAF twice. 200 window.requestAnimationFrame(() => { 201 window.requestAnimationFrame(resolve); 202 }); 203 }); 204 } 205 206 // Please don't remove this. This is necessary for chromium-based browsers. It 207 // can be a no-op on user-agents that do not have a separate compositor thread. 208 // TODO(crbug.com/1509054): This shouldn't be necessary if the test harness 209 // deferred running the tests until after paint holding. 210 async function waitForCompositorReady() { 211 const animation = 212 document.body.animate({ opacity: [ 0, 1 ] }, {duration: 1 }); 213 return animation.finished; 214 } 215 216 function waitForNextFrame() { 217 const startTime = performance.now(); 218 return new Promise(resolve => { 219 window.requestAnimationFrame((frameTime) => { 220 if (frameTime < startTime) { 221 window.requestAnimationFrame(resolve); 222 } else { 223 resolve(); 224 } 225 }); 226 }); 227 } 228 229 // TODO(crbug.com/1400399): Deprecate as frame rates may vary greatly in 230 // different test environments. 231 function waitForAnimationEnd(getValue) { 232 var last_changed_frame = 0; 233 var last_position = getValue(); 234 return new Promise((resolve, reject) => { 235 function tick(frames) { 236 // We requestAnimationFrame either for MAX_FRAME or until 237 // MAX_UNCHANGED_FRAMES with no change have been observed. 238 if (frames >= MAX_FRAME || frames - last_changed_frame > MAX_UNCHANGED_FRAMES) { 239 resolve(); 240 } else { 241 current_value = getValue(); 242 if (last_position != current_value) { 243 last_changed_frame = frames; 244 last_position = current_value; 245 } 246 requestAnimationFrame(tick.bind(this, frames + 1)); 247 } 248 } 249 tick(0); 250 }) 251 } 252 253 // Scrolls in target according to move_path with pauses in between 254 // The move_path should contains coordinates that are within target boundaries. 255 // Keep in mind that 0,0 is the center of the target element and is also 256 // the pointerDown position. 257 // pointerUp() is fired after sequence of moves. 258 function touchScrollInTargetSequentiallyWithPause(target, move_path, pause_time_in_ms = 100) { 259 const test_driver_actions = new test_driver.Actions() 260 .addPointer("pointer1", "touch") 261 .pointerMove(0, 0, {origin: target}) 262 .pointerDown(); 263 264 const substeps = 5; 265 let x = 0; 266 let y = 0; 267 // Do each move in 5 steps 268 for(let move of move_path) { 269 let step_x = (move.x - x) / substeps; 270 let step_y = (move.y - y) / substeps; 271 for(let step = 0; step < substeps; step++) { 272 x += step_x; 273 y += step_y; 274 test_driver_actions.pointerMove(x, y, {origin: target}); 275 } 276 test_driver_actions.pause(pause_time_in_ms); // To prevent inertial scroll 277 } 278 279 return test_driver_actions.pointerUp().send(); 280 } 281 282 function touchScrollInTarget(pixels_to_scroll, target, direction, pause_time_in_ms = 100) { 283 var x_delta = 0; 284 var y_delta = 0; 285 const num_movs = 5; 286 if (direction == "down") { 287 y_delta = -1 * pixels_to_scroll / num_movs; 288 } else if (direction == "up") { 289 y_delta = pixels_to_scroll / num_movs; 290 } else if (direction == "right") { 291 x_delta = -1 * pixels_to_scroll / num_movs; 292 } else if (direction == "left") { 293 x_delta = pixels_to_scroll / num_movs; 294 } else { 295 throw("scroll direction '" + direction + "' is not expected, direction should be 'down', 'up', 'left' or 'right'"); 296 } 297 return new test_driver.Actions() 298 .addPointer("pointer1", "touch") 299 .pointerMove(0, 0, {origin: target}) 300 .pointerDown() 301 .pointerMove(x_delta, y_delta, {origin: target}) 302 .pointerMove(2 * x_delta, 2 * y_delta, {origin: target}) 303 .pointerMove(3 * x_delta, 3 * y_delta, {origin: target}) 304 .pointerMove(4 * x_delta, 4 * y_delta, {origin: target}) 305 .pointerMove(5 * x_delta, 5 * y_delta, {origin: target}) 306 .pause(pause_time_in_ms) 307 .pointerUp() 308 .send(); 309 } 310 311 // Trigger fling by doing pointerUp right after pointerMoves. 312 function touchFlingInTarget(pixels_to_scroll, target, direction) { 313 return touchScrollInTarget(pixels_to_scroll, target, direction, 0 /* pause_time */); 314 } 315 316 function mouseActionsInTarget(target, origin, delta, pause_time_in_ms = 100) { 317 return new test_driver.Actions() 318 .addPointer("pointer1", "mouse") 319 .pointerMove(origin.x, origin.y, { origin: target }) 320 .pointerDown() 321 .pointerMove(origin.x + delta.x, origin.y + delta.y, { origin: target }) 322 .pointerMove(origin.x + delta.x * 2, origin.y + delta.y * 2, { origin: target }) 323 .pause(pause_time_in_ms) 324 .pointerUp() 325 .send(); 326 } 327 328 // Returns a promise that resolves when the given condition holds for 10 329 // animation frames or rejects if the condition changes to false within 10 330 // animation frames. 331 // TODO(crbug.com/1400399): Deprecate as frame rates may very greatly in 332 // different test environments. 333 function conditionHolds(condition, error_message = 'Condition is not true anymore.') { 334 const MAX_FRAME = 10; 335 return new Promise((resolve, reject) => { 336 function tick(frames) { 337 // We requestAnimationFrame either for 10 frames or until condition is 338 // violated. 339 if (frames >= MAX_FRAME) 340 resolve(); 341 else if (!condition()) 342 reject(error_message); 343 else 344 requestAnimationFrame(tick.bind(this, frames + 1)); 345 } 346 tick(0); 347 }); 348 } 349 350 function scrollElementDown(element, scroll_amount) { 351 let x = 0; 352 let y = 0; 353 let delta_x = 0; 354 let delta_y = scroll_amount; 355 let actions = new test_driver.Actions() 356 .scroll(x, y, delta_x, delta_y, {origin: element}); 357 return actions.send(); 358 } 359 360 function scrollElementLeft(element, scroll_amount) { 361 let x = 0; 362 let y = 0; 363 let delta_x = scroll_amount; 364 let delta_y = 0; 365 let actions = new test_driver.Actions() 366 .scroll(x, y, delta_x, delta_y, {origin: element}); 367 return actions.send(); 368 } 369 370 async function scrollElementByKeyboard(key) { 371 const KEY_CODE_MAP = { 372 'ArrowLeft': '\uE012', 373 'ArrowUp': '\uE013', 374 'ArrowRight': '\uE014', 375 'ArrowDown': '\uE015', 376 }; 377 378 if (!KEY_CODE_MAP.hasOwnProperty(key)) { 379 return Promise.reject(`Invalid key for scrollElementByKeyboard: ${key}`); 380 } 381 const code = KEY_CODE_MAP[key]; 382 for (let i = 0; i < 10; i++) { 383 await new test_driver.Actions() 384 .keyDown(code) 385 .keyUp(code) 386 .send(); 387 } 388 }