updating-the-finished-state.html (18703B)
1 <!DOCTYPE html> 2 <meta charset=utf-8> 3 <title>Updating the finished state</title> 4 <link rel="help" href="https://drafts.csswg.org/web-animations/#updating-the-finished-state"> 5 <script src="/resources/testharness.js"></script> 6 <script src="/resources/testharnessreport.js"></script> 7 <script src="/web-animations/testcommon.js"></script> 8 <script src="testcommon.js"></script> 9 <style> 10 .scroller { 11 overflow: auto; 12 height: 100px; 13 width: 100px; 14 will-change: transform; 15 } 16 17 .contents { 18 height: 1000px; 19 width: 100%; 20 } 21 </style> 22 <body> 23 <script> 24 'use strict'; 25 26 // -------------------------------------------------------------------- 27 // 28 // TESTS FOR UPDATING THE HOLD TIME 29 // 30 // -------------------------------------------------------------------- 31 32 // CASE 1: playback rate > 0 and current time >= target effect end 33 // (Also the start time is resolved and there is pending task) 34 // Did seek = true 35 promise_test(async t => { 36 const anim = createScrollLinkedAnimation(t); 37 const scroller = anim.timeline.source; 38 const maxScroll = scroller.scrollHeight - scroller.clientHeight; 39 // Wait for new animation frame which allows the timeline to compute new 40 // current time. 41 await waitForNextFrame(); 42 anim.play(); 43 44 await anim.ready; 45 46 anim.currentTime = CSS.percent(200); 47 scroller.scrollTop = 0.7 * maxScroll; 48 await waitForNextFrame(); 49 50 assert_percents_equal(anim.currentTime, 200, 51 'Hold time is set so current time should NOT change'); 52 }, 'Updating the finished state when seeking past end'); 53 54 // Did seek = false 55 promise_test(async t => { 56 const anim = createScrollLinkedAnimation(t); 57 const scroller = anim.timeline.source; 58 const maxScroll = scroller.scrollHeight - scroller.clientHeight; 59 60 // Wait for new animation frame which allows the timeline to compute new 61 // current time. 62 await waitForNextFrame(); 63 anim.play(); 64 await anim.ready; 65 66 scroller.scrollTop = maxScroll; 67 await waitForNextFrame(); 68 69 assert_percents_equal(anim.currentTime, 100, 70 'Hold time is set to target end clamping current time'); 71 }, 'Updating the finished state when playing exactly to end'); 72 73 // Did seek = true 74 promise_test(async t => { 75 const anim = createScrollLinkedAnimation(t); 76 const scroller = anim.timeline.source; 77 const maxScroll = scroller.scrollHeight - scroller.clientHeight; 78 79 // Wait for new animation frame which allows the timeline to compute new 80 // current time. 81 await waitForNextFrame(); 82 await anim.ready; 83 84 anim.currentTime = CSS.percent(100); 85 scroller.scrollTop = 0.7 * maxScroll; 86 await waitForNextFrame(); 87 88 assert_percents_equal(anim.currentTime, 100, 89 'Hold time is set so current time should NOT change'); 90 }, 'Updating the finished state when seeking exactly to end'); 91 92 93 // CASE 2: playback rate < 0 and current time <= 0 94 // (Also the start time is resolved and there is pending task) 95 96 // Did seek = false 97 promise_test(async t => { 98 const anim = createScrollLinkedAnimation(t); 99 const scroller = anim.timeline.source; 100 const maxScroll = scroller.scrollHeight - scroller.clientHeight; 101 102 // Wait for new animation frame which allows the timeline to compute new 103 // current time. 104 await waitForNextFrame(); 105 anim.playbackRate = -1; 106 anim.play(); // Make sure animation is not initially finished 107 108 await anim.ready; 109 110 // Seek to 1ms before 0 and then wait 1ms 111 anim.currentTime = CSS.percent(1); 112 scroller.scrollTop = 0.2 * maxScroll; 113 await waitForNextFrame(); 114 115 assert_percents_equal(anim.currentTime, 0, 116 'Hold time is set to zero clamping current time'); 117 }, 'Updating the finished state when playing in reverse past zero'); 118 119 // Did seek = true 120 promise_test(async t => { 121 const anim = createScrollLinkedAnimation(t); 122 const scroller = anim.timeline.source; 123 const maxScroll = scroller.scrollHeight - scroller.clientHeight; 124 125 // Wait for new animation frame which allows the timeline to compute new 126 // current time. 127 await waitForNextFrame(); 128 anim.playbackRate = -1; 129 anim.play(); 130 131 await anim.ready; 132 133 anim.currentTime = CSS.percent(-100); 134 scroller.scrollTop = 0.2 * maxScroll; 135 await waitForNextFrame(); 136 137 assert_percents_equal(anim.currentTime, -100, 138 'Hold time is set so current time should NOT change'); 139 }, 'Updating the finished state when seeking a reversed animation past zero'); 140 141 // Did seek = false 142 promise_test(async t => { 143 const anim = createScrollLinkedAnimation(t); 144 const scroller = anim.timeline.source; 145 const maxScroll = scroller.scrollHeight - scroller.clientHeight; 146 147 // Wait for new animation frame which allows the timeline to compute new 148 // current time. 149 await waitForNextFrame(); 150 anim.playbackRate = -1; 151 anim.play(); 152 await anim.ready; 153 154 scroller.scrollTop = maxScroll; 155 await waitForNextFrame(); 156 157 assert_percents_equal(anim.currentTime, 0, 158 'Hold time is set to target end clamping current time'); 159 }, 'Updating the finished state when playing a reversed animation exactly ' + 160 'to zero'); 161 162 // Did seek = true 163 promise_test(async t => { 164 const anim = createScrollLinkedAnimation(t); 165 const scroller = anim.timeline.source; 166 const maxScroll = scroller.scrollHeight - scroller.clientHeight; 167 168 // Wait for new animation frame which allows the timeline to compute new 169 // current time. 170 await waitForNextFrame(); 171 anim.playbackRate = -1; 172 anim.play(); 173 await anim.ready; 174 175 anim.currentTime = CSS.percent(0); 176 177 scroller.scrollTop = 0.2 * maxScroll; 178 await waitForNextFrame(); 179 180 assert_percents_equal(anim.currentTime, 0, 181 'Hold time is set so current time should NOT change'); 182 }, 'Updating the finished state when seeking a reversed animation exactly ' + 183 'to zero'); 184 185 // CASE 3: playback rate > 0 and current time < target end OR 186 // playback rate < 0 and current time > 0 187 // (Also the start time is resolved and there is pending task) 188 189 // Did seek = true; playback rate > 0 190 promise_test(async t => { 191 const anim = createScrollLinkedAnimation(t); 192 const scroller = anim.timeline.source; 193 const maxScroll = scroller.scrollHeight - scroller.clientHeight; 194 195 // Wait for new animation frame which allows the timeline to compute new 196 // current time. 197 await waitForNextFrame(); 198 anim.play(); 199 anim.finish(); 200 await anim.ready; 201 assert_percents_equal(anim.startTime, -100); 202 203 anim.currentTime = CSS.percent(50); 204 // When did seek = true, updating the finished state: (i) updates 205 // the animation's start time and (ii) clears the hold time. 206 // We can test both by checking that the currentTime is initially 207 // updated and then increases. 208 assert_percents_equal(anim.currentTime, 50, 'Start time is updated'); 209 assert_percents_equal(anim.startTime, -50); 210 211 scroller.scrollTop = 0.2 * maxScroll; 212 await waitForNextFrame(); 213 214 assert_percents_equal(anim.currentTime, 70, 215 'Hold time is not set so current time should increase'); 216 }, 'Updating the finished state when seeking before end'); 217 218 // Did seek = false; playback rate < 0 219 // 220 // Unfortunately it is not possible to test this case. We need to have 221 // a hold time set, a resolved start time, and then perform some 222 // operation that updates the finished state with did seek set to true. 223 // 224 // However, the only situation where this could arrive is when we 225 // replace the timeline and that procedure is likely to change. For all 226 // other cases we either have an unresolved start time (e.g. when 227 // paused), we don't have a set hold time (e.g. regular playback), or 228 // the current time is zero (and anything that gets us out of that state 229 // will set did seek = true). 230 231 // Did seek = true; playback rate < 0 232 promise_test(async t => { 233 const anim = createScrollLinkedAnimation(t); 234 const scroller = anim.timeline.source; 235 const maxScroll = scroller.scrollHeight - scroller.clientHeight; 236 237 // Wait for new animation frame which allows the timeline to compute new 238 // current time. 239 await waitForNextFrame(); 240 anim.play(); 241 anim.playbackRate = -1; 242 await anim.ready; 243 244 anim.currentTime = CSS.percent(50); 245 assert_percents_equal(anim.startTime, 50, 'Start time is updated'); 246 assert_percents_equal(anim.currentTime, 50, 'Current time is updated'); 247 248 scroller.scrollTop = 0.2 * maxScroll; 249 await waitForNextFrame(); 250 251 assert_percents_equal(anim.currentTime, 30, 252 'Hold time is not set so current time should decrease'); 253 }, 'Updating the finished state when seeking a reversed animation before end'); 254 255 256 // CASE 4: playback rate == 0 257 258 // current time < 0 259 promise_test(async t => { 260 const anim = createScrollLinkedAnimation(t); 261 const scroller = anim.timeline.source; 262 const maxScroll = scroller.scrollHeight - scroller.clientHeight; 263 264 // Wait for new animation frame which allows the timeline to compute new 265 // current time. 266 await waitForNextFrame(); 267 anim.play(); 268 anim.playbackRate = 0; 269 await anim.ready; 270 271 anim.currentTime = CSS.percent(-100); 272 273 scroller.scrollTop = 0.2 * maxScroll; 274 await waitForNextFrame(); 275 276 assert_percents_equal(anim.currentTime, -100, 277 'Hold time should not be cleared so current time should NOT change'); 278 }, 'Updating the finished state when playback rate is zero and the current ' + 279 'time is less than zero'); 280 281 // current time < target end 282 promise_test(async t => { 283 const anim = createScrollLinkedAnimation(t); 284 const scroller = anim.timeline.source; 285 const maxScroll = scroller.scrollHeight - scroller.clientHeight; 286 287 // Wait for new animation frame which allows the timeline to compute new 288 // current time. 289 await waitForNextFrame(); 290 anim.play(); 291 292 anim.playbackRate = 0; 293 await anim.ready; 294 295 anim.currentTime = CSS.percent(50); 296 scroller.scrollTop = 0.2 * maxScroll; 297 await waitForNextFrame(); 298 299 assert_percents_equal(anim.currentTime, 50, 300 'Hold time should not be cleared so current time should NOT change'); 301 }, 'Updating the finished state when playback rate is zero and the current ' + 302 'time is less than end'); 303 304 // current time > target end 305 promise_test(async t => { 306 const anim = createScrollLinkedAnimation(t); 307 const scroller = anim.timeline.source; 308 const maxScroll = scroller.scrollHeight - scroller.clientHeight; 309 310 // Wait for new animation frame which allows the timeline to compute new 311 // current time. 312 await waitForNextFrame(); 313 anim.play(); 314 anim.playbackRate = 0; 315 await anim.ready; 316 317 anim.currentTime = CSS.percent(200); 318 scroller.scrollTop = 0.2 * maxScroll; 319 await waitForNextFrame(); 320 321 assert_percents_equal(anim.currentTime, 200, 322 'Hold time should not be cleared so current time should NOT change'); 323 }, 'Updating the finished state when playback rate is zero and the current' + 324 'time is greater than end'); 325 326 // CASE 5: current time unresolved 327 328 promise_test(async t => { 329 const anim = createScrollLinkedAnimation(t); 330 // Wait for new animation frame which allows the timeline to compute new 331 // current time. 332 await waitForNextFrame(); 333 anim.play(); 334 anim.cancel(); 335 // Trigger a change that will cause the "update the finished state" 336 // procedure to run. 337 anim.effect.updateTiming({ duration: 2000 }); 338 assert_equals(anim.currentTime, null, 339 'The animation hold time / start time should not be updated'); 340 // The "update the finished state" procedure is supposed to run after any 341 // change to timing, but just in case an implementation defers that, let's 342 // wait a frame and check that the hold time / start time has still not been 343 // updated. 344 await waitForAnimationFrames(1); 345 346 assert_equals(anim.currentTime, null, 347 'The animation hold time / start time should not be updated'); 348 }, 'Updating the finished state when current time is unresolved'); 349 350 // CASE 7: start time unresolved 351 352 // Did seek = true 353 promise_test(async t => { 354 const anim = createScrollLinkedAnimation(t); 355 // Wait for new animation frame which allows the timeline to compute new 356 // current time. 357 await waitForNextFrame(); 358 anim.cancel(); 359 anim.currentTime = CSS.percent(150); 360 // Trigger a change that will cause the "update the finished state" 361 // procedure to run. 362 anim.currentTime = CSS.percent(50); 363 assert_percents_equal(anim.currentTime, 50, 364 'The animation hold time should not be updated'); 365 assert_equals(anim.startTime, null, 366 'The animation start time should not be updated'); 367 }, 'Updating the finished state when start time is unresolved and did seek = ' + 368 'true'); 369 370 // -------------------------------------------------------------------- 371 // 372 // TESTS FOR RUNNING FINISH NOTIFICATION STEPS 373 // 374 // -------------------------------------------------------------------- 375 376 function waitForFinishEventAndPromise(animation) { 377 const eventPromise = new Promise(resolve => { 378 animation.onfinish = resolve; 379 }); 380 return Promise.all([eventPromise, animation.finished]); 381 } 382 383 promise_test(t => { 384 const animation = createScrollLinkedAnimation(t); 385 const scroller = animation.timeline.source; 386 const maxScroll = scroller.scrollHeight - scroller.clientHeight; 387 animation.play(); 388 animation.onfinish = 389 t.unreached_func('Seeking to finish should not fire finish event'); 390 animation.finished.then( 391 t.unreached_func( 392 'Seeking to finish should not resolve finished promise')); 393 animation.currentTime = CSS.percent(100); 394 animation.currentTime = CSS.percent(0); 395 animation.pause(); 396 scroller.scrollTop = 0.2 * maxScroll; 397 return waitForAnimationFrames(3); 398 }, 'Finish notification steps don\'t run when the animation seeks to finish ' + 399 'and then seeks back again'); 400 401 promise_test(async t => { 402 const animation = createScrollLinkedAnimation(t); 403 const scroller = animation.timeline.source; 404 const maxScroll = scroller.scrollHeight - scroller.clientHeight; 405 animation.play(); 406 await animation.ready; 407 scroller.scrollTop = maxScroll; 408 409 return waitForFinishEventAndPromise(animation); 410 }, 'Finish notification steps run when the animation completes normally'); 411 412 promise_test(async t => { 413 const animation = createScrollLinkedAnimation(t); 414 const scroller = animation.timeline.source; 415 const maxScroll = scroller.scrollHeight - scroller.clientHeight; 416 animation.effect.target = null; 417 418 animation.play(); 419 await animation.ready; 420 scroller.scrollTop = maxScroll; 421 return waitForFinishEventAndPromise(animation); 422 }, 'Finish notification steps run when an animation without a target effect ' + 423 'completes normally'); 424 425 promise_test(async t => { 426 const animation = createScrollLinkedAnimation(t); 427 animation.play(); 428 await animation.ready; 429 430 animation.currentTime = CSS.percent(101); 431 return waitForFinishEventAndPromise(animation); 432 }, 'Finish notification steps run when the animation seeks past finish'); 433 434 promise_test(async t => { 435 const animation = createScrollLinkedAnimation(t); 436 animation.play(); 437 await animation.ready; 438 439 // Register for notifications now since once we seek away from being 440 // finished the 'finished' promise will be replaced. 441 const finishNotificationSteps = waitForFinishEventAndPromise(animation); 442 animation.finish(); 443 animation.currentTime = CSS.percent(0); 444 animation.pause(); 445 return finishNotificationSteps; 446 }, 'Finish notification steps run when the animation completes with ' + 447 '.finish(), even if we then seek away'); 448 449 promise_test(async t => { 450 const animation = createScrollLinkedAnimation(t); 451 const scroller = animation.timeline.source; 452 const maxScroll = scroller.scrollHeight - scroller.clientHeight; 453 454 animation.play(); 455 scroller.scrollTop = maxScroll; 456 const initialFinishedPromise = animation.finished; 457 await animation.finished; 458 459 animation.currentTime = CSS.percent(0); 460 assert_not_equals(initialFinishedPromise, animation.finished); 461 }, 'Animation finished promise is replaced after seeking back to start'); 462 463 promise_test(async t => { 464 const animation = createScrollLinkedAnimation(t); 465 const scroller = animation.timeline.source; 466 const maxScroll = scroller.scrollHeight - scroller.clientHeight; 467 468 animation.play(); 469 470 const initialFinishedPromise = animation.finished; 471 scroller.scrollTop = maxScroll; 472 await animation.finished; 473 474 scroller.scrollTop = 0; 475 await waitForNextFrame(); 476 477 animation.play(); 478 assert_not_equals(initialFinishedPromise, animation.finished); 479 }, 'Animation finished promise is replaced after replaying from start'); 480 481 async_test(t => { 482 const animation = createScrollLinkedAnimation(t); 483 const scroller = animation.timeline.source; 484 const maxScroll = scroller.scrollHeight - scroller.clientHeight; 485 486 animation.play(); 487 488 animation.onfinish = event => { 489 scroller.scrollTop = 0; 490 window.requestAnimationFrame(function() { 491 window.requestAnimationFrame(function() { 492 scroller.scrollTop = maxScroll; 493 }); 494 }); 495 animation.onfinish = event => { 496 t.done(); 497 }; 498 }; 499 scroller.scrollTop = maxScroll; 500 }, 'Animation finish event is fired again after seeking back to start'); 501 502 async_test(t => { 503 const animation = createScrollLinkedAnimation(t); 504 const scroller = animation.timeline.source; 505 const maxScroll = scroller.scrollHeight - scroller.clientHeight; 506 507 animation.play(); 508 509 animation.onfinish = event => { 510 scroller.scrollTop = 0; 511 window.requestAnimationFrame(function() { 512 animation.play(); 513 scroller.scrollTop = maxScroll; 514 animation.onfinish = event => { 515 t.done(); 516 }; 517 }); 518 }; 519 scroller.scrollTop = maxScroll; 520 }, 'Animation finish event is fired again after replaying from start'); 521 522 async_test(t => { 523 const anim = createScrollLinkedAnimation(t); 524 const scroller = anim.timeline.source; 525 const maxScroll = scroller.scrollHeight - scroller.clientHeight; 526 527 anim.effect.updateTiming({ duration: 800, endDelay: 200}); 528 529 anim.onfinish = t.step_func(event => { 530 assert_unreached('finish event should not be fired'); 531 }); 532 anim.play(); 533 anim.ready.then(() => { 534 scroller.scrollTop = 0.9 * maxScroll; 535 return waitForAnimationFrames(3); 536 }).then(t.step_func(() => { 537 t.done(); 538 })); 539 }, 'finish event is not fired at the end of the active interval when the ' + 540 'endDelay has not expired'); 541 542 async_test(t => { 543 const anim = createScrollLinkedAnimation(t); 544 const scroller = anim.timeline.source; 545 const maxScroll = scroller.scrollHeight - scroller.clientHeight; 546 547 anim.effect.updateTiming({ duration: 800, endDelay: 100}); 548 anim.play(); 549 anim.ready.then(() => { 550 scroller.scrollTop = 0.95 * maxScroll; // during endDelay 551 anim.onfinish = t.step_func(event => { 552 assert_unreached('onfinish event should not be fired during endDelay'); 553 }); 554 return waitForAnimationFrames(2); 555 }).then(t.step_func(() => { 556 anim.onfinish = t.step_func(event => { 557 t.done(); 558 }); 559 scroller.scrollTop = maxScroll; 560 return waitForAnimationFrames(2); 561 })); 562 }, 'finish event is fired after the endDelay has expired'); 563 564 </script> 565 </body>