| <!DOCTYPE html> |
| <meta charset=utf-8> |
| <title>Updating the finished state</title> |
| <link rel="help" href="https://drafts.csswg.org/web-animations/#updating-the-finished-state"> |
| <script src="/resources/testharness.js"></script> |
| <script src="/resources/testharnessreport.js"></script> |
| <script src="/web-animations/testcommon.js"></script> |
| <script src="testcommon.js"></script> |
| <style> |
| .scroller { |
| overflow: auto; |
| height: 100px; |
| width: 100px; |
| will-change: transform; |
| } |
| |
| .contents { |
| height: 1000px; |
| width: 100%; |
| } |
| </style> |
| <body> |
| <script> |
| 'use strict'; |
| |
| // -------------------------------------------------------------------- |
| // |
| // TESTS FOR UPDATING THE HOLD TIME |
| // |
| // -------------------------------------------------------------------- |
| |
| // CASE 1: playback rate > 0 and current time >= target effect end |
| // (Also the start time is resolved and there is pending task) |
| |
| // Did seek = false |
| promise_test(async t => { |
| const anim = createScrollLinkedAnimation(t); |
| // Set duration to half of scroll timeline timeRange. |
| anim.effect.updateTiming({ duration: 500 }); |
| const scroller = anim.timeline.scrollSource; |
| const maxScroll = scroller.scrollHeight - scroller.clientHeight; |
| |
| // Wait for new animation frame which allows the timeline to compute new |
| // current time. |
| await waitForNextFrame(); |
| anim.play(); |
| // Here and in the following tests we wait until ready resolves as |
| // otherwise we don't have a resolved start time. We test the case |
| // where the start time is unresolved in a subsequent test. |
| await anim.ready; |
| |
| scroller.scrollTop = 0.7 * maxScroll; |
| await waitForNextFrame(); |
| |
| assert_equals(anim.currentTime, 500, |
| 'Hold time is set to target end clamping current time'); |
| }, 'Updating the finished state when playing past end'); |
| |
| // Did seek = true |
| promise_test(async t => { |
| const anim = createScrollLinkedAnimation(t); |
| const scroller = anim.timeline.scrollSource; |
| const maxScroll = scroller.scrollHeight - scroller.clientHeight; |
| // Wait for new animation frame which allows the timeline to compute new |
| // current time. |
| await waitForNextFrame(); |
| anim.play(); |
| |
| await anim.ready; |
| |
| anim.currentTime = 2000; |
| scroller.scrollTop = 0.7 * maxScroll; |
| await waitForNextFrame(); |
| |
| assert_equals(anim.currentTime, 2000, |
| 'Hold time is set so current time should NOT change'); |
| }, 'Updating the finished state when seeking past end'); |
| |
| // Did seek = false |
| promise_test(async t => { |
| const anim = createScrollLinkedAnimation(t); |
| const scroller = anim.timeline.scrollSource; |
| const maxScroll = scroller.scrollHeight - scroller.clientHeight; |
| |
| // Wait for new animation frame which allows the timeline to compute new |
| // current time. |
| await waitForNextFrame(); |
| anim.play(); |
| await anim.ready; |
| |
| scroller.scrollTop = maxScroll; |
| await waitForNextFrame(); |
| |
| assert_equals(anim.currentTime, 1000, |
| 'Hold time is set to target end clamping current time'); |
| }, 'Updating the finished state when playing exactly to end'); |
| |
| // Did seek = true |
| promise_test(async t => { |
| const anim = createScrollLinkedAnimation(t); |
| // Set duration to half of scroll timeline timeRange. |
| anim.effect.updateTiming({ duration: 500 }); |
| const scroller = anim.timeline.scrollSource; |
| const maxScroll = scroller.scrollHeight - scroller.clientHeight; |
| |
| // Wait for new animation frame which allows the timeline to compute new |
| // current time. |
| await waitForNextFrame(); |
| await anim.ready; |
| |
| anim.currentTime = 500; |
| scroller.scrollTop = 0.7 * maxScroll; |
| await waitForNextFrame(); |
| |
| assert_equals(anim.currentTime, 500, |
| 'Hold time is set so current time should NOT change'); |
| }, 'Updating the finished state when seeking exactly to end'); |
| |
| |
| // CASE 2: playback rate < 0 and current time <= 0 |
| // (Also the start time is resolved and there is pending task) |
| |
| // Did seek = false |
| promise_test(async t => { |
| const anim = createScrollLinkedAnimation(t); |
| // Set duration to half of scroll timeline timeRange. |
| anim.effect.updateTiming({ duration: 500 }); |
| const scroller = anim.timeline.scrollSource; |
| const maxScroll = scroller.scrollHeight - scroller.clientHeight; |
| |
| // Wait for new animation frame which allows the timeline to compute new |
| // current time. |
| await waitForNextFrame(); |
| anim.playbackRate = -1; |
| anim.play(); // Make sure animation is not initially finished |
| |
| await anim.ready; |
| |
| // Seek to 1ms before 0 and then wait 1ms |
| anim.currentTime = 1; |
| scroller.scrollTop = 0.2 * maxScroll; |
| await waitForNextFrame(); |
| |
| assert_equals(anim.currentTime, 0, |
| 'Hold time is set to zero clamping current time'); |
| }, 'Updating the finished state when playing in reverse past zero'); |
| |
| // Did seek = true |
| promise_test(async t => { |
| const anim = createScrollLinkedAnimation(t); |
| const scroller = anim.timeline.scrollSource; |
| const maxScroll = scroller.scrollHeight - scroller.clientHeight; |
| |
| // Wait for new animation frame which allows the timeline to compute new |
| // current time. |
| await waitForNextFrame(); |
| anim.playbackRate = -1; |
| anim.play(); |
| |
| await anim.ready; |
| |
| anim.currentTime = -1000; |
| scroller.scrollTop = 0.2 * maxScroll; |
| await waitForNextFrame(); |
| |
| assert_equals(anim.currentTime, -1000, |
| 'Hold time is set so current time should NOT change'); |
| }, 'Updating the finished state when seeking a reversed animation past zero'); |
| |
| // Did seek = false |
| promise_test(async t => { |
| const anim = createScrollLinkedAnimation(t); |
| const scroller = anim.timeline.scrollSource; |
| const maxScroll = scroller.scrollHeight - scroller.clientHeight; |
| |
| // Wait for new animation frame which allows the timeline to compute new |
| // current time. |
| await waitForNextFrame(); |
| anim.playbackRate = -1; |
| anim.play(); |
| await anim.ready; |
| |
| scroller.scrollTop = maxScroll; |
| await waitForNextFrame(); |
| |
| assert_equals(anim.currentTime, 0, |
| 'Hold time is set to target end clamping current time'); |
| }, 'Updating the finished state when playing a reversed animation exactly ' + |
| 'to zero'); |
| |
| // Did seek = true |
| promise_test(async t => { |
| const anim = createScrollLinkedAnimation(t); |
| const scroller = anim.timeline.scrollSource; |
| const maxScroll = scroller.scrollHeight - scroller.clientHeight; |
| |
| // Wait for new animation frame which allows the timeline to compute new |
| // current time. |
| await waitForNextFrame(); |
| anim.playbackRate = -1; |
| anim.play(); |
| await anim.ready; |
| |
| anim.currentTime = 0; |
| |
| scroller.scrollTop = 0.2 * maxScroll; |
| await waitForNextFrame(); |
| |
| assert_equals(anim.currentTime, 0 * MS_PER_SEC, |
| 'Hold time is set so current time should NOT change'); |
| }, 'Updating the finished state when seeking a reversed animation exactly' |
| + ' to zero'); |
| |
| // CASE 3: playback rate > 0 and current time < target end OR |
| // playback rate < 0 and current time > 0 |
| // (Also the start time is resolved and there is pending task) |
| |
| // Did seek = false; playback rate > 0 |
| promise_test(async t => { |
| const anim = createScrollLinkedAnimation(t); |
| const scroller = anim.timeline.scrollSource; |
| const maxScroll = scroller.scrollHeight - scroller.clientHeight; |
| |
| // Wait for new animation frame which allows the timeline to compute new |
| // current time. |
| await waitForNextFrame(); |
| anim.play(); |
| |
| // We want to test that the hold time is cleared so first we need to |
| // put the animation in a state where the hold time is set. |
| anim.finish(); |
| await anim.ready; |
| |
| assert_equals(anim.currentTime, 1000, |
| 'Hold time is initially set'); |
| |
| // Then extend the duration so that the hold time is cleared and on |
| // the next tick the current time will increase. |
| anim.effect.updateTiming({ |
| duration: anim.effect.getComputedTiming().duration * 2, |
| }); |
| scroller.scrollTop = 0.2 * maxScroll; |
| await waitForNextFrame(); |
| assert_equals(anim.currentTime, 1200, |
| 'Hold time is not set so current time should increase'); |
| }, 'Updating the finished state when playing before end'); |
| |
| |
| // Did seek = true; playback rate > 0 |
| promise_test(async t => { |
| const anim = createScrollLinkedAnimation(t); |
| const scroller = anim.timeline.scrollSource; |
| const maxScroll = scroller.scrollHeight - scroller.clientHeight; |
| |
| // Wait for new animation frame which allows the timeline to compute new |
| // current time. |
| await waitForNextFrame(); |
| anim.play(); |
| anim.finish(); |
| await anim.ready; |
| assert_equals(anim.startTime, -1000); |
| |
| anim.currentTime = 500; |
| // When did seek = true, updating the finished state: (i) updates |
| // the animation's start time and (ii) clears the hold time. |
| // We can test both by checking that the currentTime is initially |
| // updated and then increases. |
| assert_equals(anim.currentTime, 500, 'Start time is updated'); |
| assert_equals(anim.startTime, -500); |
| |
| scroller.scrollTop = 0.2 * maxScroll; |
| await waitForNextFrame(); |
| |
| assert_equals(anim.currentTime, 700, |
| 'Hold time is not set so current time should increase'); |
| }, 'Updating the finished state when seeking before end'); |
| |
| // Did seek = false; playback rate < 0 |
| // |
| // Unfortunately it is not possible to test this case. We need to have |
| // a hold time set, a resolved start time, and then perform some |
| // operation that updates the finished state with did seek set to true. |
| // |
| // However, the only situation where this could arrive is when we |
| // replace the timeline and that procedure is likely to change. For all |
| // other cases we either have an unresolved start time (e.g. when |
| // paused), we don't have a set hold time (e.g. regular playback), or |
| // the current time is zero (and anything that gets us out of that state |
| // will set did seek = true). |
| |
| // Did seek = true; playback rate < 0 |
| promise_test(async t => { |
| const anim = createScrollLinkedAnimation(t); |
| const scroller = anim.timeline.scrollSource; |
| const maxScroll = scroller.scrollHeight - scroller.clientHeight; |
| |
| // Wait for new animation frame which allows the timeline to compute new |
| // current time. |
| await waitForNextFrame(); |
| anim.play(); |
| anim.playbackRate = -1; |
| await anim.ready; |
| |
| anim.currentTime = 500; |
| assert_equals(anim.startTime, 500, 'Start time is updated'); |
| assert_equals(anim.currentTime, 500, 'Current time is updated'); |
| |
| scroller.scrollTop = 0.2 * maxScroll; |
| await waitForNextFrame(); |
| |
| assert_equals(anim.currentTime, 300, |
| 'Hold time is not set so current time should decrease'); |
| }, 'Updating the finished state when seeking a reversed animation before end'); |
| |
| |
| // CASE 4: playback rate == 0 |
| |
| // current time < 0 |
| promise_test(async t => { |
| const anim = createScrollLinkedAnimation(t); |
| const scroller = anim.timeline.scrollSource; |
| const maxScroll = scroller.scrollHeight - scroller.clientHeight; |
| |
| // Wait for new animation frame which allows the timeline to compute new |
| // current time. |
| await waitForNextFrame(); |
| anim.play(); |
| anim.playbackRate = 0; |
| await anim.ready; |
| |
| anim.currentTime = -1000; |
| |
| scroller.scrollTop = 0.2 * maxScroll; |
| await waitForNextFrame(); |
| |
| assert_equals(anim.currentTime, -1000, |
| 'Hold time should not be cleared so current time should' |
| + ' NOT change'); |
| }, 'Updating the finished state when playback rate is zero and the' |
| + ' current time is less than zero'); |
| |
| // current time < target end |
| promise_test(async t => { |
| const anim = createScrollLinkedAnimation(t); |
| const scroller = anim.timeline.scrollSource; |
| const maxScroll = scroller.scrollHeight - scroller.clientHeight; |
| |
| // Wait for new animation frame which allows the timeline to compute new |
| // current time. |
| await waitForNextFrame(); |
| anim.play(); |
| |
| anim.playbackRate = 0; |
| await anim.ready; |
| |
| anim.currentTime = 500; |
| scroller.scrollTop = 0.2 * maxScroll; |
| await waitForNextFrame(); |
| |
| assert_equals(anim.currentTime, 500, |
| 'Hold time should not be cleared so current time should' |
| + ' NOT change'); |
| }, 'Updating the finished state when playback rate is zero and the' |
| + ' current time is less than end'); |
| |
| // current time > target end |
| promise_test(async t => { |
| const anim = createScrollLinkedAnimation(t); |
| const scroller = anim.timeline.scrollSource; |
| const maxScroll = scroller.scrollHeight - scroller.clientHeight; |
| |
| // Wait for new animation frame which allows the timeline to compute new |
| // current time. |
| await waitForNextFrame(); |
| anim.play(); |
| anim.playbackRate = 0; |
| await anim.ready; |
| |
| anim.currentTime = 2000; |
| scroller.scrollTop = 0.2 * maxScroll; |
| await waitForNextFrame(); |
| |
| assert_equals(anim.currentTime, 2000, |
| 'Hold time should not be cleared so current time should' |
| + ' NOT change'); |
| }, 'Updating the finished state when playback rate is zero and the' |
| + ' current time is greater than end'); |
| |
| // CASE 5: current time unresolved |
| |
| promise_test(async t => { |
| const anim = createScrollLinkedAnimation(t); |
| // Wait for new animation frame which allows the timeline to compute new |
| // current time. |
| await waitForNextFrame(); |
| anim.play(); |
| anim.cancel(); |
| // Trigger a change that will cause the "update the finished state" |
| // procedure to run. |
| anim.effect.updateTiming({ duration: 2000 }); |
| assert_equals(anim.currentTime, null, |
| 'The animation hold time / start time should not be updated'); |
| // The "update the finished state" procedure is supposed to run after any |
| // change to timing, but just in case an implementation defers that, let's |
| // wait a frame and check that the hold time / start time has still not been |
| // updated. |
| await waitForAnimationFrames(1); |
| |
| assert_equals(anim.currentTime, null, |
| 'The animation hold time / start time should not be updated'); |
| }, 'Updating the finished state when current time is unresolved'); |
| |
| // CASE 6: has a pending task |
| |
| promise_test(async t => { |
| const anim = createScrollLinkedAnimation(t); |
| // Wait for new animation frame which allows the timeline to compute new |
| // current time. |
| await waitForNextFrame(); |
| anim.play(); |
| anim.cancel(); |
| anim.currentTime = 750; |
| anim.play(); |
| // We now have a pending task and a resolved current time. |
| // |
| // In the next step we will adjust the timing so that the current time |
| // is greater than the target end. At this point the "update the finished |
| // state" procedure should run and if we fail to check for a pending task |
| // we will set the hold time to the target end, i.e. 50ms. |
| anim.effect.updateTiming({ duration: 500 }); |
| assert_equals(anim.currentTime, 750, |
| 'Hold time should not be updated'); |
| }, 'Updating the finished state when there is a pending task'); |
| |
| // CASE 7: start time unresolved |
| |
| // Did seek = false |
| promise_test(async t => { |
| const anim = createScrollLinkedAnimation(t); |
| const scroller = anim.timeline.scrollSource; |
| const maxScroll = scroller.scrollHeight - scroller.clientHeight; |
| // Wait for new animation frame which allows the timeline to compute new |
| // current time. |
| await waitForNextFrame(); |
| anim.cancel(); |
| // Make it so that only the start time is unresolved (to avoid overlapping |
| // with the test case where current time is unresolved) |
| anim.currentTime = 1500; |
| // Trigger a change that will cause the "update the finished state" |
| // procedure to run (did seek = false). |
| anim.effect.updateTiming({ duration: 2000 }); |
| scroller.scrollTop = 0.2 * maxScroll; |
| await waitForNextFrame(); |
| |
| assert_equals(anim.currentTime, 1500, |
| 'The animation hold time should not be updated'); |
| assert_equals(anim.startTime, null, |
| 'The animation start time should not be updated'); |
| }, 'Updating the finished state when start time is unresolved and' |
| + ' did seek = false'); |
| |
| // Did seek = true |
| promise_test(async t => { |
| const anim = createScrollLinkedAnimation(t); |
| // Wait for new animation frame which allows the timeline to compute new |
| // current time. |
| await waitForNextFrame(); |
| anim.cancel(); |
| anim.currentTime = 1500; |
| // Trigger a change that will cause the "update the finished state" |
| // procedure to run. |
| anim.currentTime = 500; |
| assert_equals(anim.currentTime, 500, |
| 'The animation hold time should not be updated'); |
| assert_equals(anim.startTime, null, |
| 'The animation start time should not be updated'); |
| }, 'Updating the finished state when start time is unresolved and' |
| + ' did seek = true'); |
| |
| // -------------------------------------------------------------------- |
| // |
| // TESTS FOR RUNNING FINISH NOTIFICATION STEPS |
| // |
| // -------------------------------------------------------------------- |
| |
| function waitForFinishEventAndPromise(animation) { |
| const eventPromise = new Promise(resolve => { |
| animation.onfinish = resolve; |
| }); |
| return Promise.all([eventPromise, animation.finished]); |
| } |
| |
| promise_test(t => { |
| const animation = createScrollLinkedAnimation(t); |
| const scroller = animation.timeline.scrollSource; |
| const maxScroll = scroller.scrollHeight - scroller.clientHeight; |
| animation.play(); |
| animation.onfinish = |
| t.unreached_func('Seeking to finish should not fire finish event'); |
| animation.finished.then( |
| t.unreached_func('Seeking to finish should not resolve finished promise')); |
| animation.currentTime = 1000; |
| animation.currentTime = 0; |
| animation.pause(); |
| scroller.scrollTop = 0.2 * maxScroll; |
| return waitForAnimationFrames(3); |
| }, 'Finish notification steps don\'t run when the animation seeks to finish' |
| + ' and then seeks back again'); |
| |
| promise_test(async t => { |
| const animation = createScrollLinkedAnimation(t); |
| const scroller = animation.timeline.scrollSource; |
| const maxScroll = scroller.scrollHeight - scroller.clientHeight; |
| animation.play(); |
| await animation.ready; |
| scroller.scrollTop = maxScroll; |
| |
| return waitForFinishEventAndPromise(animation); |
| }, 'Finish notification steps run when the animation completes normally'); |
| |
| promise_test(async t => { |
| const animation = createScrollLinkedAnimation(t); |
| const scroller = animation.timeline.scrollSource; |
| const maxScroll = scroller.scrollHeight - scroller.clientHeight; |
| animation.effect.target = null; |
| |
| animation.play(); |
| await animation.ready; |
| scroller.scrollTop = maxScroll; |
| return waitForFinishEventAndPromise(animation); |
| }, 'Finish notification steps run when an animation without a target' |
| + ' effect completes normally'); |
| |
| promise_test(async t => { |
| const animation = createScrollLinkedAnimation(t); |
| animation.play(); |
| await animation.ready; |
| |
| animation.currentTime = 1010; |
| return waitForFinishEventAndPromise(animation); |
| }, 'Finish notification steps run when the animation seeks past finish'); |
| |
| promise_test(async t => { |
| const animation = createScrollLinkedAnimation(t); |
| animation.play(); |
| await animation.ready; |
| |
| // Register for notifications now since once we seek away from being |
| // finished the 'finished' promise will be replaced. |
| const finishNotificationSteps = waitForFinishEventAndPromise(animation); |
| animation.finish(); |
| animation.currentTime = 0; |
| animation.pause(); |
| return finishNotificationSteps; |
| }, 'Finish notification steps run when the animation completes with .finish(),' |
| + ' even if we then seek away'); |
| |
| promise_test(async t => { |
| const animation = createScrollLinkedAnimation(t); |
| const scroller = animation.timeline.scrollSource; |
| const maxScroll = scroller.scrollHeight - scroller.clientHeight; |
| |
| animation.play(); |
| scroller.scrollTop = maxScroll; |
| const initialFinishedPromise = animation.finished; |
| await animation.finished; |
| |
| animation.currentTime = 0; |
| assert_not_equals(initialFinishedPromise, animation.finished); |
| }, 'Animation finished promise is replaced after seeking back to start'); |
| |
| promise_test(async t => { |
| const animation = createScrollLinkedAnimation(t); |
| const scroller = animation.timeline.scrollSource; |
| const maxScroll = scroller.scrollHeight - scroller.clientHeight; |
| |
| animation.play(); |
| |
| const initialFinishedPromise = animation.finished; |
| scroller.scrollTop = maxScroll; |
| await animation.finished; |
| |
| scroller.scrollTop = 0; |
| await waitForNextFrame(); |
| |
| animation.play(); |
| assert_not_equals(initialFinishedPromise, animation.finished); |
| }, 'Animation finished promise is replaced after replaying from start'); |
| |
| async_test(t => { |
| const animation = createScrollLinkedAnimation(t); |
| const scroller = animation.timeline.scrollSource; |
| const maxScroll = scroller.scrollHeight - scroller.clientHeight; |
| |
| animation.play(); |
| |
| animation.onfinish = event => { |
| scroller.scrollTop = 0; |
| window.requestAnimationFrame(function() { |
| window.requestAnimationFrame(function() { |
| scroller.scrollTop = maxScroll; |
| }); |
| }); |
| animation.onfinish = event => { |
| t.done(); |
| }; |
| }; |
| scroller.scrollTop = maxScroll; |
| }, 'Animation finish event is fired again after seeking back to start'); |
| |
| async_test(t => { |
| const animation = createScrollLinkedAnimation(t); |
| const scroller = animation.timeline.scrollSource; |
| const maxScroll = scroller.scrollHeight - scroller.clientHeight; |
| |
| animation.play(); |
| |
| animation.onfinish = event => { |
| scroller.scrollTop = 0; |
| window.requestAnimationFrame(function() { |
| animation.play(); |
| scroller.scrollTop = maxScroll; |
| animation.onfinish = event => { |
| t.done(); |
| }; |
| }); |
| }; |
| scroller.scrollTop = maxScroll; |
| }, 'Animation finish event is fired again after replaying from start'); |
| |
| async_test(t => { |
| const anim = createScrollLinkedAnimation(t); |
| const scroller = anim.timeline.scrollSource; |
| const maxScroll = scroller.scrollHeight - scroller.clientHeight; |
| |
| anim.effect.updateTiming({ duration: 800, endDelay: 200}); |
| |
| anim.onfinish = t.step_func(event => { |
| assert_unreached('finish event should not be fired'); |
| }); |
| anim.play(); |
| anim.ready.then(() => { |
| scroller.scrollTop = 0.9 * maxScroll; |
| return waitForAnimationFrames(3); |
| }).then(t.step_func(() => { |
| t.done(); |
| })); |
| }, 'finish event is not fired at the end of the active interval when the' |
| + ' endDelay has not expired'); |
| |
| async_test(t => { |
| const anim = createScrollLinkedAnimation(t); |
| const scroller = anim.timeline.scrollSource; |
| const maxScroll = scroller.scrollHeight - scroller.clientHeight; |
| |
| anim.effect.updateTiming({ duration: 800, endDelay: 100}); |
| anim.play(); |
| anim.ready.then(() => { |
| scroller.scrollTop = 0.85 * maxScroll; // during endDelay |
| anim.onfinish = t.step_func(event => { |
| assert_unreached('onfinish event should not be fired during endDelay'); |
| }); |
| return waitForAnimationFrames(2); |
| }).then(t.step_func(() => { |
| anim.onfinish = t.step_func(event => { |
| t.done(); |
| }); |
| scroller.scrollTop = 0.95 * maxScroll; |
| return waitForAnimationFrames(2); |
| })); |
| }, 'finish event is fired after the endDelay has expired'); |
| |
| </script> |
| </body> |