<!DOCTYPE html>
<meta charset=utf-8>
<title>Finishing an animation</title>
<link rel="help"
href="https://drafts.csswg.org/web-animations/#finishing-an-animation-section">
<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: 200px;
width: 100px;
will-change: transform;
}
.contents {
height: 1000px;
width: 100%;
}
</style>
<body>
<div id="log"></div>
<script>
'use strict';
promise_test(async t => {
const animation = createScrollLinkedAnimation(t);
animation.play();
animation.playbackRate = 0;
assert_throws_dom('InvalidStateError', () => {
animation.finish();
});
}, 'Finishing an animation with a zero playback rate throws');
promise_test(async t => {
const animation = createScrollLinkedAnimation(t);
animation.play();
animation.finish();
assert_percents_equal(animation.currentTime, 100,
'After finishing, the currentTime should be set to the end of the'
+ ' active duration');
}, 'Finishing an animation seeks to the end time');
promise_test(async t => {
const animation = createScrollLinkedAnimation(t);
animation.play();
// 1% past effect end
animation.currentTime = CSSNumericValue.parse("101%");
animation.finish();
assert_percents_equal(animation.currentTime, 100,
'After finishing, the currentTime should be set back to the end of the'
+ ' active duration');
}, 'Finishing an animation with a current time past the effect end jumps'
+ ' back to the end');
promise_test(async t => {
const animation = createScrollLinkedAnimation(t);
const scroller = animation.timeline.source;
const maxScroll = scroller.scrollHeight - scroller.clientHeight;
animation.play();
scroller.scrollTop = maxScroll;
await animation.finished;
animation.playbackRate = -1;
animation.finish();
assert_percents_equal(animation.currentTime, 0,
'After finishing a reversed animation the ' +
'currentTime should be set to zero');
}, 'Finishing a reversed animation jumps to zero time');
promise_test(async t => {
const animation = createScrollLinkedAnimation(t);
animation.play();
animation.currentTime = CSSNumericValue.parse("100%");
await animation.finished;
animation.playbackRate = -1;
animation.currentTime = CSSNumericValue.parse("-1000%");
animation.finish();
assert_percents_equal(animation.currentTime, 0,
'After finishing a reversed animation the ' +
'currentTime should be set back to zero');
}, 'Finishing a reversed animation with a current time less than zero'
+ ' makes it jump back to zero');
promise_test(async t => {
const animation = createScrollLinkedAnimation(t);
animation.play();
animation.playbackRate = 0.5;
animation.finish();
assert_false(animation.pending);
assert_equals(animation.playState, 'finished',
'The play state of a play-pending animation should become ' +
'"finished"');
assert_percents_equal(animation.startTime,
animation.timeline.currentTime.value - 100 / 0.5,
'The start time of a play-pending animation should ' +
'be set');
}, 'Finishing an animation while play-pending resolves the pending'
+ ' task immediately');
promise_test(async t => {
const animation = createScrollLinkedAnimation(t);
const scroller = animation.timeline.source;
await runAndWaitForFrameUpdate(() => {
// Make the scroll timeline inactive.
scroller.style.overflow = 'visible';
});
animation.play();
animation.finish();
await animation.finished;
assert_true(animation.pending);
assert_equals(animation.playState, 'finished',
'The play state of a play-pending animation should become ' +
'"finished"');
assert_percents_equal(animation.currentTime, 100,
'The current time of a play-pending animation should ' +
'be set to the end of the active duration');
assert_equals(animation.startTime, null,
'The start time of a finished play-pending animation should ' +
'be unresolved');
}, 'Finishing an animation attached to inactive timeline while play-pending '
+ 'doesn\'t resolves the pending task');
promise_test(async t => {
const animation = createScrollLinkedAnimation(t);
animation.play();
let resolvedFinished = false;
animation.finished.then(() => {
resolvedFinished = true;
});
await animation.ready;
animation.finish();
await Promise.resolve();
assert_true(resolvedFinished, 'finished promise should be resolved');
}, 'Finishing an animation resolves the finished promise synchronously');
promise_test(async t => {
const animation = createScrollLinkedAnimation(t);
animation.play();
const promise = animation.ready;
let readyResolved = false;
animation.finish();
animation.ready.then(() => { readyResolved = true; });
const promiseResult = await animation.finished;
await animation.ready;
assert_equals(promiseResult, animation);
assert_equals(animation.ready, promise);
assert_true(readyResolved);
}, 'A pending ready promise is resolved and not replaced when the animation'
+ ' is finished');
promise_test(async t => {
const animation = createScrollLinkedAnimation(t);
animation.play();
animation.effect.target.remove();
const eventWatcher = new EventWatcher(t, animation, 'finish');
await animation.ready;
animation.finish();
await eventWatcher.wait_for('finish');
assert_equals(animation.effect.target.parentNode, null,
'finish event should be fired for the animation on an orphaned element');
}, 'Finishing an animation fires finish event on orphaned element');
promise_test(async t => {
const animation = createScrollLinkedAnimation(t);
animation.play();
await animation.ready;
const originalFinishPromise = animation.finished;
animation.cancel();
assert_equals(animation.startTime, null);
assert_equals(animation.currentTime, null);
const resolvedFinishPromise = animation.finished;
assert_not_equals(originalFinishPromise, resolvedFinishPromise,
'Canceling an animation should create a new finished promise');
animation.finish();
assert_equals(animation.playState, 'finished',
'The play state of a canceled animation should become ' +
'"finished"');
assert_percents_equal(animation.startTime,
animation.timeline.currentTime.value - 100,
'The start time of a finished animation should be set');
assert_percents_equal(animation.currentTime, 100,
'Hold time should be set to end boundary of the ' +
'animation');
}, 'Finishing a canceled animation sets the current and start times');
promise_test(async t => {
const animation = createScrollLinkedAnimation(t);
const scroller = animation.timeline.source;
const maxScroll = scroller.scrollHeight - scroller.clientHeight;
scroller.scrollTop = 0.25 * maxScroll;
// Wait for new animation frame which allows the timeline to compute new
// current time.
await waitForNextFrame();
const eventWatcher = new EventWatcher(t, animation, 'finish');
animation.finish();
await animation.finished;
const finishEvent = await eventWatcher.wait_for('finish');
assert_equals(animation.playState, 'finished',
'Animation is finished.');
assert_percents_equal(animation.currentTime, 100,
'The current time is the end of the active duration in finished state.');
assert_percents_equal(animation.startTime, -75,
'The start time is calculated to match the current time.');
assert_percents_equal(finishEvent.currentTime, 100,
'event.currentTime is the animation current time.');
assert_percents_equal(finishEvent.timelineTime, 25,
'event.timelineTime is timeline.currentTime');
}, 'Finishing idle animation produces correct state and fires finish event.');
promise_test(async t => {
const animation = createScrollLinkedAnimation(t);
const scroller = animation.timeline.source;
// Make the scroll timeline inactive.
scroller.style.overflow = 'visible';
await waitForNextFrame();
assert_equals(animation.timeline.currentTime, null,
'Sanity check the timeline is inactive.');
animation.finish();
assert_equals(animation.playState, 'paused', 'Animation is paused.');
}, 'Finishing idle animation attached to inactive timeline pauses the ' +
'animation.');
promise_test(async t => {
const animation = createScrollLinkedAnimation(t);
const scroller = animation.timeline.source;
const maxScroll = scroller.scrollHeight - scroller.clientHeight;
scroller.scrollTop = 0.25 * maxScroll;
animation.play();
await animation.ready;
const eventWatcher = new EventWatcher(t, animation, 'finish');
animation.finish();
await animation.finished;
const finishEvent = await eventWatcher.wait_for('finish');
assert_equals(animation.playState, 'finished',
'Animation is finished.');
assert_percents_equal(animation.currentTime, 100,
'The current time is the end of active duration in finished state.');
assert_percents_equal(animation.startTime, -75,
'The start time is calculated to match animation current time.');
assert_percents_equal(finishEvent.currentTime, 100,
'event.currentTime is the animation current time.');
assert_percents_equal(finishEvent.timelineTime, 25,
'event.timelineTime is timeline.currentTime');
}, 'Finishing running animation produces correct state and fires finish event.');
promise_test(async t => {
const animation = createScrollLinkedAnimation(t);
const scroller = animation.timeline.source;
animation.play();
await animation.ready;
// Make the scroll timeline inactive.
scroller.style.overflow = 'visible';
scroller.scrollTop;
await waitForNextFrame();
assert_equals(animation.timeline.currentTime, null,
'Sanity check the timeline is inactive.');
assert_equals(animation.playState, 'running',
'Sanity check the animation is running.');
animation.finish();
assert_equals(animation.playState, 'paused', 'Animation is paused.');
}, 'Finishing running animation attached to inactive timeline pauses the ' +
'animation.');
promise_test(async t => {
const animation = createScrollLinkedAnimation(t);
animation.pause();
await animation.ready;
animation.finish();
assert_equals(animation.playState, 'finished',
'The play state of a paused animation should become ' +
'"finished"');
assert_percents_equal(animation.startTime,
animation.timeline.currentTime.value - 100,
'The start time of a paused animation should be set');
}, 'Finishing a paused animation resolves the start time');
promise_test(async t => {
const animation = createScrollLinkedAnimation(t);
animation.play();
// Update playbackRate so we can test that the calculated startTime
// respects it
animation.playbackRate = 2;
animation.pause();
// While animation is still pause-pending call finish()
animation.finish();
assert_false(animation.pending);
assert_equals(animation.playState, 'finished',
'The play state of a pause-pending animation should become ' +
'"finished"');
assert_percents_equal(animation.startTime,
animation.timeline.currentTime.value - 100 / 2,
'The start time of a pause-pending animation should ' +
'be set');
}, 'Finishing a pause-pending animation resolves the pending task'
+ ' immediately and update the start time');
promise_test(async t => {
const animation = createScrollLinkedAnimation(t);
animation.play();
animation.playbackRate = -2;
animation.pause();
animation.finish();
assert_false(animation.pending);
assert_equals(animation.playState, 'finished',
'The play state of a pause-pending animation should become ' +
'"finished"');
assert_percents_equal(animation.startTime,
animation.timeline.currentTime,
'The start time of a pause-pending animation should ' +
'be set');
}, 'Finishing a pause-pending animation with negative playback rate'
+ ' resolves the pending task immediately');
promise_test(async t => {
const animation = createScrollLinkedAnimation(t);
animation.play();
await animation.ready;
animation.pause();
animation.play();
// We are now in the unusual situation of being play-pending whilst having
// a resolved start time. Check that finish() still triggers a transition
// to the finished state immediately.
animation.finish();
assert_equals(animation.playState, 'finished',
'After aborting a pause then finishing an animation its play ' +
'state should become "finished" immediately');
}, 'Finishing an animation during an aborted pause makes it finished'
+ ' immediately');
promise_test(async t => {
const animation = createScrollLinkedAnimation(t);
animation.play();
await animation.ready;
animation.updatePlaybackRate(2);
assert_true(animation.pending);
animation.finish();
assert_false(animation.pending);
assert_equals(animation.playbackRate, 2);
assert_percents_equal(animation.currentTime, 100);
}, 'A pending playback rate should be applied immediately when an animation'
+ ' is finished');
promise_test(async t => {
const animation = createScrollLinkedAnimation(t);
animation.play();
await animation.ready;
animation.updatePlaybackRate(0);
assert_throws_dom('InvalidStateError', () => {
animation.finish();
});
}, 'An exception should be thrown if the effective playback rate is zero');
</script>
</body>