updating-the-finished-state.html (16333B)
1 <!DOCTYPE html> 2 <meta charset=utf-8> 3 <title>Updating the finished state</title> 4 <meta name="timeout" content="long"> 5 <link rel="help" href="https://drafts.csswg.org/web-animations/#updating-the-finished-state"> 6 <script src="/resources/testharness.js"></script> 7 <script src="/resources/testharnessreport.js"></script> 8 <script src="../../testcommon.js"></script> 9 <body> 10 <div id="log"></div> 11 <script> 12 'use strict'; 13 14 // -------------------------------------------------------------------- 15 // 16 // TESTS FOR UPDATING THE HOLD TIME 17 // 18 // -------------------------------------------------------------------- 19 20 // CASE 1: playback rate > 0 and current time >= target effect end 21 // (Also the start time is resolved and there is pending task) 22 23 // Did seek = false 24 promise_test(async t => { 25 const anim = createDiv(t).animate(null, 100 * MS_PER_SEC); 26 27 // Here and in the following tests we wait until ready resolves as 28 // otherwise we don't have a resolved start time. We test the case 29 // where the start time is unresolved in a subsequent test. 30 await anim.ready; 31 32 // Seek to 1ms before the target end and then wait 1ms 33 anim.currentTime = 100 * MS_PER_SEC - 1; 34 await waitForAnimationFramesWithDelay(1); 35 36 assert_equals(anim.currentTime, 100 * MS_PER_SEC, 37 'Hold time is set to target end clamping current time'); 38 }, 'Updating the finished state when playing past end'); 39 40 // Did seek = true 41 promise_test(async t => { 42 const anim = createDiv(t).animate(null, 100 * MS_PER_SEC); 43 44 await anim.ready; 45 46 anim.currentTime = 200 * MS_PER_SEC; 47 await waitForNextFrame(); 48 49 assert_equals(anim.currentTime, 200 * MS_PER_SEC, 50 'Hold time is set so current time should NOT change'); 51 }, 'Updating the finished state when seeking past end'); 52 53 // Test current time == target end 54 // 55 // We can't really write a test for current time == target end with 56 // did seek = false since that would imply setting up an animation where 57 // the next animation frame time happens to exactly align with the target end. 58 // 59 // Fortunately, we don't need to test that case since even if the implementation 60 // fails to set the hold time on such a tick, it should be mostly unobservable 61 // (on the subsequent tick the hold time will be set to the same value anyway). 62 63 // Did seek = true 64 promise_test(async t => { 65 const anim = createDiv(t).animate(null, 100 * MS_PER_SEC); 66 await anim.ready; 67 68 anim.currentTime = 100 * MS_PER_SEC; 69 await waitForNextFrame(); 70 71 assert_equals(anim.currentTime, 100 * MS_PER_SEC, 72 'Hold time is set so current time should NOT change'); 73 }, 'Updating the finished state when seeking exactly to end'); 74 75 76 // CASE 2: playback rate < 0 and current time <= 0 77 // (Also the start time is resolved and there is pending task) 78 79 // Did seek = false 80 promise_test(async t => { 81 const anim = createDiv(t).animate(null, 100 * MS_PER_SEC); 82 anim.playbackRate = -1; 83 anim.play(); // Make sure animation is not initially finished 84 85 await anim.ready; 86 87 // Seek to 1ms before 0 and then wait 1ms 88 anim.currentTime = 1; 89 await waitForAnimationFramesWithDelay(1); 90 91 assert_equals(anim.currentTime, 0 * MS_PER_SEC, 92 'Hold time is set to zero clamping current time'); 93 }, 'Updating the finished state when playing in reverse past zero'); 94 95 // Did seek = true 96 promise_test(async t => { 97 const anim = createDiv(t).animate(null, 100 * MS_PER_SEC); 98 anim.playbackRate = -1; 99 anim.play(); 100 101 await anim.ready; 102 103 anim.currentTime = -100 * MS_PER_SEC; 104 await waitForNextFrame(); 105 106 assert_equals(anim.currentTime, -100 * MS_PER_SEC, 107 'Hold time is set so current time should NOT change'); 108 }, 'Updating the finished state when seeking a reversed animation past zero'); 109 110 // As before, it's difficult to test current time == 0 for did seek = false but 111 // it doesn't really matter. 112 113 // Did seek = true 114 promise_test(async t => { 115 const anim = createDiv(t).animate(null, 100 * MS_PER_SEC); 116 anim.playbackRate = -1; 117 anim.play(); 118 await anim.ready; 119 120 anim.currentTime = 0; 121 await waitForNextFrame(); 122 123 assert_equals(anim.currentTime, 0 * MS_PER_SEC, 124 'Hold time is set so current time should NOT change'); 125 }, 'Updating the finished state when seeking a reversed animation exactly' 126 + ' to zero'); 127 128 // CASE 3: playback rate > 0 and current time < target end OR 129 // playback rate < 0 and current time > 0 130 // (Also the start time is resolved and there is pending task) 131 132 // Did seek = false; playback rate > 0 133 promise_test(async t => { 134 const anim = createDiv(t).animate(null, 100 * MS_PER_SEC); 135 136 // We want to test that the hold time is cleared so first we need to 137 // put the animation in a state where the hold time is set. 138 anim.finish(); 139 await anim.ready; 140 141 assert_equals(anim.currentTime, 100 * MS_PER_SEC, 142 'Hold time is initially set'); 143 // Then extend the duration so that the hold time is cleared and on 144 // the next tick the current time will increase. 145 anim.effect.updateTiming({ 146 duration: anim.effect.getComputedTiming().duration * 2, 147 }); 148 await waitForNextFrame(); 149 150 assert_greater_than(anim.currentTime, 100 * MS_PER_SEC, 151 'Hold time is not set so current time should increase'); 152 }, 'Updating the finished state when playing before end'); 153 154 // Did seek = true; playback rate > 0 155 promise_test(async t => { 156 const anim = createDiv(t).animate(null, 100 * MS_PER_SEC); 157 anim.finish(); 158 await anim.ready; 159 160 anim.currentTime = 50 * MS_PER_SEC; 161 // When did seek = true, updating the finished state: (i) updates 162 // the animation's start time and (ii) clears the hold time. 163 // We can test both by checking that the currentTime is initially 164 // updated and then increases. 165 assert_equals(anim.currentTime, 50 * MS_PER_SEC, 'Start time is updated'); 166 await waitForNextFrame(); 167 168 assert_greater_than(anim.currentTime, 50 * MS_PER_SEC, 169 'Hold time is not set so current time should increase'); 170 }, 'Updating the finished state when seeking before end'); 171 172 // Did seek = false; playback rate < 0 173 // 174 // Unfortunately it is not possible to test this case. We need to have 175 // a hold time set, a resolved start time, and then perform some 176 // operation that updates the finished state with did seek set to true. 177 // 178 // However, the only situation where this could arrive is when we 179 // replace the timeline and that procedure is likely to change. For all 180 // other cases we either have an unresolved start time (e.g. when 181 // paused), we don't have a set hold time (e.g. regular playback), or 182 // the current time is zero (and anything that gets us out of that state 183 // will set did seek = true). 184 185 // Did seek = true; playback rate < 0 186 promise_test(async t => { 187 const anim = createDiv(t).animate(null, 100 * MS_PER_SEC); 188 anim.playbackRate = -1; 189 await anim.ready; 190 191 anim.currentTime = 50 * MS_PER_SEC; 192 assert_equals(anim.currentTime, 50 * MS_PER_SEC, 'Start time is updated'); 193 await waitForNextFrame(); 194 195 assert_less_than(anim.currentTime, 50 * MS_PER_SEC, 196 'Hold time is not set so current time should decrease'); 197 }, 'Updating the finished state when seeking a reversed animation before end'); 198 199 // CASE 4: playback rate == 0 200 201 // current time < 0 202 promise_test(async t => { 203 const anim = createDiv(t).animate(null, 100 * MS_PER_SEC); 204 anim.playbackRate = 0; 205 await anim.ready; 206 207 anim.currentTime = -100 * MS_PER_SEC; 208 await waitForNextFrame(); 209 210 assert_equals(anim.currentTime, -100 * MS_PER_SEC, 211 'Hold time should not be cleared so current time should' 212 + ' NOT change'); 213 }, 'Updating the finished state when playback rate is zero and the' 214 + ' current time is less than zero'); 215 216 // current time < target end 217 promise_test(async t => { 218 const anim = createDiv(t).animate(null, 100 * MS_PER_SEC); 219 anim.playbackRate = 0; 220 await anim.ready; 221 222 anim.currentTime = 50 * MS_PER_SEC; 223 await waitForNextFrame(); 224 225 assert_equals(anim.currentTime, 50 * MS_PER_SEC, 226 'Hold time should not be cleared so current time should' 227 + ' NOT change'); 228 }, 'Updating the finished state when playback rate is zero and the' 229 + ' current time is less than end'); 230 231 // current time > target end 232 promise_test(async t => { 233 const anim = createDiv(t).animate(null, 100 * MS_PER_SEC); 234 anim.playbackRate = 0; 235 await anim.ready; 236 237 anim.currentTime = 200 * MS_PER_SEC; 238 await waitForNextFrame(); 239 240 assert_equals(anim.currentTime, 200 * MS_PER_SEC, 241 'Hold time should not be cleared so current time should' 242 + ' NOT change'); 243 }, 'Updating the finished state when playback rate is zero and the' 244 + ' current time is greater than end'); 245 246 // CASE 5: current time unresolved 247 248 promise_test(async t => { 249 const anim = createDiv(t).animate(null, 100 * MS_PER_SEC); 250 anim.cancel(); 251 // Trigger a change that will cause the "update the finished state" 252 // procedure to run. 253 anim.effect.updateTiming({ duration: 200 * MS_PER_SEC }); 254 assert_equals(anim.currentTime, null, 255 'The animation hold time / start time should not be updated'); 256 // The "update the finished state" procedure is supposed to run after any 257 // change to timing, but just in case an implementation defers that, let's 258 // wait a frame and check that the hold time / start time has still not been 259 // updated. 260 await waitForAnimationFrames(1); 261 262 assert_equals(anim.currentTime, null, 263 'The animation hold time / start time should not be updated'); 264 }, 'Updating the finished state when current time is unresolved'); 265 266 // CASE 6: has a pending task 267 268 test(t => { 269 const anim = createDiv(t).animate(null, 100 * MS_PER_SEC); 270 anim.cancel(); 271 anim.currentTime = 75 * MS_PER_SEC; 272 anim.play(); 273 // We now have a pending task and a resolved current time. 274 // 275 // In the next step we will adjust the timing so that the current time 276 // is greater than the target end. At this point the "update the finished 277 // state" procedure should run and if we fail to check for a pending task 278 // we will set the hold time to the target end, i.e. 50ms. 279 anim.effect.updateTiming({ duration: 50 * MS_PER_SEC }); 280 assert_equals(anim.currentTime, 75 * MS_PER_SEC, 281 'Hold time should not be updated'); 282 }, 'Updating the finished state when there is a pending task'); 283 284 // CASE 7: start time unresolved 285 286 // Did seek = false 287 promise_test(async t => { 288 const anim = createDiv(t).animate(null, 100 * MS_PER_SEC); 289 anim.cancel(); 290 // Make it so that only the start time is unresolved (to avoid overlapping 291 // with the test case where current time is unresolved) 292 anim.currentTime = 150 * MS_PER_SEC; 293 // Trigger a change that will cause the "update the finished state" 294 // procedure to run (did seek = false). 295 anim.effect.updateTiming({ duration: 200 * MS_PER_SEC }); 296 await waitForAnimationFrames(1); 297 298 assert_equals(anim.currentTime, 150 * MS_PER_SEC, 299 'The animation hold time should not be updated'); 300 assert_equals(anim.startTime, null, 301 'The animation start time should not be updated'); 302 }, 'Updating the finished state when start time is unresolved and' 303 + ' did seek = false'); 304 305 // Did seek = true 306 test(t => { 307 const anim = createDiv(t).animate(null, 100 * MS_PER_SEC); 308 anim.cancel(); 309 anim.currentTime = 150 * MS_PER_SEC; 310 // Trigger a change that will cause the "update the finished state" 311 // procedure to run. 312 anim.currentTime = 50 * MS_PER_SEC; 313 assert_equals(anim.currentTime, 50 * MS_PER_SEC, 314 'The animation hold time should not be updated'); 315 assert_equals(anim.startTime, null, 316 'The animation start time should not be updated'); 317 }, 'Updating the finished state when start time is unresolved and' 318 + ' did seek = true'); 319 320 // -------------------------------------------------------------------- 321 // 322 // TESTS FOR RUNNING FINISH NOTIFICATION STEPS 323 // 324 // -------------------------------------------------------------------- 325 326 function waitForFinishEventAndPromise(animation) { 327 const eventPromise = new Promise(resolve => { 328 animation.onfinish = resolve; 329 }); 330 return Promise.all([eventPromise, animation.finished]); 331 } 332 333 promise_test(t => { 334 const animation = createDiv(t).animate(null, 1); 335 animation.onfinish = 336 t.unreached_func('Seeking to finish should not fire finish event'); 337 animation.finished.then( 338 t.unreached_func('Seeking to finish should not resolve finished promise')); 339 animation.currentTime = 1; 340 animation.currentTime = 0; 341 animation.pause(); 342 return waitForAnimationFrames(3); 343 }, 'Finish notification steps don\'t run when the animation seeks to finish' 344 + ' and then seeks back again'); 345 346 promise_test(async t => { 347 const animation = createDiv(t).animate(null, 1); 348 await animation.ready; 349 350 return waitForFinishEventAndPromise(animation); 351 }, 'Finish notification steps run when the animation completes normally'); 352 353 promise_test(async t => { 354 const effect = new KeyframeEffect(null, null, 1); 355 const animation = new Animation(effect, document.timeline); 356 animation.play(); 357 await animation.ready; 358 359 return waitForFinishEventAndPromise(animation); 360 }, 'Finish notification steps run when an animation without a target' 361 + ' effect completes normally'); 362 363 promise_test(async t => { 364 const animation = createDiv(t).animate(null, 1); 365 await animation.ready; 366 367 animation.currentTime = 10; 368 return waitForFinishEventAndPromise(animation); 369 }, 'Finish notification steps run when the animation seeks past finish'); 370 371 promise_test(async t => { 372 const animation = createDiv(t).animate(null, 1); 373 await animation.ready; 374 375 // Register for notifications now since once we seek away from being 376 // finished the 'finished' promise will be replaced. 377 const finishNotificationSteps = waitForFinishEventAndPromise(animation); 378 animation.finish(); 379 animation.currentTime = 0; 380 animation.pause(); 381 return finishNotificationSteps; 382 }, 'Finish notification steps run when the animation completes with .finish(),' 383 + ' even if we then seek away'); 384 385 promise_test(async t => { 386 const animation = createDiv(t).animate(null, 1); 387 const initialFinishedPromise = animation.finished; 388 await animation.finished; 389 390 animation.currentTime = 0; 391 assert_not_equals(initialFinishedPromise, animation.finished); 392 }, 'Animation finished promise is replaced after seeking back to start'); 393 394 promise_test(async t => { 395 const animation = createDiv(t).animate(null, 1); 396 const initialFinishedPromise = animation.finished; 397 await animation.finished; 398 399 animation.play(); 400 assert_not_equals(initialFinishedPromise, animation.finished); 401 }, 'Animation finished promise is replaced after replaying from start'); 402 403 async_test(t => { 404 const animation = createDiv(t).animate(null, 1); 405 animation.onfinish = event => { 406 animation.currentTime = 0; 407 animation.onfinish = event => { 408 t.done(); 409 }; 410 }; 411 }, 'Animation finish event is fired again after seeking back to start'); 412 413 async_test(t => { 414 const animation = createDiv(t).animate(null, 1); 415 animation.onfinish = event => { 416 animation.play(); 417 animation.onfinish = event => { 418 t.done(); 419 }; 420 }; 421 }, 'Animation finish event is fired again after replaying from start'); 422 423 async_test(t => { 424 const anim = createDiv(t).animate(null, 425 { duration: 100000, endDelay: 50000 }); 426 anim.onfinish = t.step_func(event => { 427 assert_unreached('finish event should not be fired'); 428 }); 429 430 anim.ready.then(() => { 431 anim.currentTime = 100000; 432 return waitForAnimationFrames(2); 433 }).then(t.step_func(() => { 434 t.done(); 435 })); 436 }, 'finish event is not fired at the end of the active interval when the' 437 + ' endDelay has not expired'); 438 439 async_test(t => { 440 const anim = createDiv(t).animate(null, 441 { duration: 100000, endDelay: 30000 }); 442 anim.ready.then(() => { 443 anim.currentTime = 110000; // during endDelay 444 anim.onfinish = t.step_func(event => { 445 assert_unreached('onfinish event should not be fired during endDelay'); 446 }); 447 return waitForAnimationFrames(2); 448 }).then(t.step_func(() => { 449 anim.onfinish = t.step_func(event => { 450 t.done(); 451 }); 452 anim.currentTime = 130000; // after endTime 453 })); 454 }, 'finish event is fired after the endDelay has expired'); 455 456 </script> 457 </body>