diff options
Diffstat (limited to 'testing/web-platform/tests/web-animations/timing-model/animations')
26 files changed, 3352 insertions, 0 deletions
diff --git a/testing/web-platform/tests/web-animations/timing-model/animations/canceling-an-animation.html b/testing/web-platform/tests/web-animations/timing-model/animations/canceling-an-animation.html new file mode 100644 index 0000000000..f296ac4da7 --- /dev/null +++ b/testing/web-platform/tests/web-animations/timing-model/animations/canceling-an-animation.html @@ -0,0 +1,127 @@ +<!DOCTYPE html> +<meta charset=utf-8> +<title>Canceling an animation</title> +<link rel="help" + href="https://drafts.csswg.org/web-animations/#canceling-an-animation-section"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../../testcommon.js"></script> +<body> +<div id="log"></div> +<script> +'use strict'; + +test(t => { + const animation = createDiv(t).animate(null, 100 * MS_PER_SEC); + animation.cancel(); + + assert_equals(animation.startTime, null, + 'The start time of a canceled animation should be unresolved'); + assert_equals(animation.currentTime, null, + 'The hold time of a canceled animation should be unresolved'); +}, 'Canceling an animation should cause its start time and hold time to be' + + ' unresolved'); + +promise_test(t => { + const animation = createDiv(t).animate(null, 100 * MS_PER_SEC); + const retPromise = animation.ready.then(() => { + assert_unreached('ready promise was fulfilled'); + }).catch(err => { + assert_equals(err.name, 'AbortError', + 'ready promise is rejected with AbortError'); + }); + + animation.cancel(); + + return retPromise; +}, 'A play-pending ready promise should be rejected when the animation is' + + ' canceled'); + +promise_test(async t => { + const animation = createDiv(t).animate(null, 100 * MS_PER_SEC); + await animation.ready; + + // Make it pause-pending + animation.pause(); + + // We need to store the original ready promise since cancel() will + // replace it + const originalPromise = animation.ready; + animation.cancel(); + + await promise_rejects_dom(t, 'AbortError', originalPromise, + 'Cancel should abort ready promise'); +}, 'A pause-pending ready promise should be rejected when the animation is' + + ' canceled'); + +promise_test(async t => { + const animation = createDiv(t).animate(null); + animation.cancel(); + const promiseResult = await animation.ready; + assert_equals(promiseResult, animation); +}, 'When an animation is canceled, it should create a resolved Promise'); + +test(t => { + const animation = createDiv(t).animate(null); + const promise = animation.ready; + animation.cancel(); + assert_not_equals(animation.ready, promise); + promise_rejects_dom(t, 'AbortError', promise, 'Cancel should abort ready promise'); +}, 'The ready promise should be replaced when the animation is canceled'); + +promise_test(t => { + const animation = new Animation( + new KeyframeEffect(null, null, { duration: 1000 }), + null + ); + assert_equals(animation.playState, 'idle', + 'The animation should be initially idle'); + + animation.finished.then(t.step_func(() => { + assert_unreached('Finished promise should not resolve'); + }), t.step_func(() => { + assert_unreached('Finished promise should not reject'); + })); + + animation.cancel(); + + return waitForAnimationFrames(3); +}, 'The finished promise should NOT be rejected if the animation is already' + + ' idle'); + +promise_test(t => { + const animation = new Animation( + new KeyframeEffect(null, null, { duration: 1000 }), + null + ); + assert_equals(animation.playState, 'idle', + 'The animation should be initially idle'); + + animation.oncancel = t.step_func(() => { + assert_unreached('Cancel event should not be fired'); + }); + + animation.cancel(); + + return waitForAnimationFrames(3); +}, 'The cancel event should NOT be fired if the animation is already' + + ' idle'); + +promise_test(async t => { + const div = createDiv(t); + const animation = div.animate({}, 100 * MS_PER_SEC); + div.remove(); + + const eventWatcher = new EventWatcher(t, animation, 'cancel'); + + await animation.ready; + animation.cancel(); + + await eventWatcher.wait_for('cancel'); + + assert_equals(animation.effect.target.parentNode, null, + 'cancel event should be fired for the animation on an orphaned element'); +}, 'Canceling an animation should fire cancel event on orphaned element'); + +</script> +</body> diff --git a/testing/web-platform/tests/web-animations/timing-model/animations/document-timeline-animation-ref.html b/testing/web-platform/tests/web-animations/timing-model/animations/document-timeline-animation-ref.html new file mode 100644 index 0000000000..d1ee52a553 --- /dev/null +++ b/testing/web-platform/tests/web-animations/timing-model/animations/document-timeline-animation-ref.html @@ -0,0 +1,19 @@ +<!DOCTYPE html> +<title>Reference for document timeline animation</title> +<style> + #notes { + position: absolute; + left: 0px; + top: 100px; + } + body { + background: white; + } +</style> +<body> + <div id="box"></div> + <p id="notes"> + This test creates a document timeline animation. If any blue pixels appear + in the screenshot, the test fails. + </p> +</body>
\ No newline at end of file diff --git a/testing/web-platform/tests/web-animations/timing-model/animations/document-timeline-animation.html b/testing/web-platform/tests/web-animations/timing-model/animations/document-timeline-animation.html new file mode 100644 index 0000000000..7d4dc76849 --- /dev/null +++ b/testing/web-platform/tests/web-animations/timing-model/animations/document-timeline-animation.html @@ -0,0 +1,63 @@ + +<!DOCTYPE html> +<html class="reftest-wait"> +<meta charset="UTF-8"> +<title>document timeline animation</title> +<link rel="match" href="document-timeline-animation-ref.html"> +<script src="/common/reftest-wait.js"></script> +<script src="../../testcommon.js"></script> +<style> + #box-1, #box-2 { + position: absolute; + top: 0px; + width: 40px; + height: 40px; + } + #box-1 { + background: blue; + z-index: 1; + left: 0px; + } + #box-2 { + background: white; + z-index: 2; + left: 100px; + } + #notes { + position: absolute; + left: 0px; + top: 100px; + } + body { + background: white; + } +</style> + +<body> + <div id="box-1"></div> + <div id="box-2"></div> + <p id="notes"> + This test creates a document timeline animation. If any blue pixels appear + in the screenshot, the test fails. + </p> +</body> +<script> + onload = async function() { + const elem = document.getElementById('box-1'); + const keyframes = [ + { transform: 'none' }, + { transform: 'translateX(100px)' } + ]; + const effect = + new KeyframeEffect(elem, keyframes, + {iterations: 1, duration: 10000, fill: 'forwards'}); + const timeline = new DocumentTimeline(); + const animation = new Animation(effect, timeline); + animation.play(); + await animation.ready; + animation.finish(); + await animation.finished; + await waitForAnimationFrames(2); + takeScreenshot(); + }; +</script> diff --git a/testing/web-platform/tests/web-animations/timing-model/animations/finishing-an-animation.html b/testing/web-platform/tests/web-animations/timing-model/animations/finishing-an-animation.html new file mode 100644 index 0000000000..fbf6558f78 --- /dev/null +++ b/testing/web-platform/tests/web-animations/timing-model/animations/finishing-an-animation.html @@ -0,0 +1,330 @@ +<!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="../../testcommon.js"></script> +<script src="../../resources/timing-override.js"></script> +<body> +<div id="log"></div> +<script> +'use strict'; + +test(t => { + const div = createDiv(t); + const animation = div.animate(null, 100 * MS_PER_SEC); + animation.playbackRate = 0; + + assert_throws_dom('InvalidStateError', () => { + animation.finish(); + }); +}, 'Finishing an animation with a zero playback rate throws'); + +test(t => { + const div = createDiv(t); + const animation = div.animate(null, + { duration : 100 * MS_PER_SEC, + iterations : Infinity }); + + assert_throws_dom('InvalidStateError', () => { + animation.finish(); + }); +}, 'Finishing an infinite animation throws'); + +test(t => { + const div = createDiv(t); + const animation = div.animate(null, 100 * MS_PER_SEC); + animation.finish(); + + assert_time_equals_literal(animation.currentTime, 100 * MS_PER_SEC, + 'After finishing, the currentTime should be set to the end of the' + + ' active duration'); +}, 'Finishing an animation seeks to the end time'); + +test(t => { + const div = createDiv(t); + const animation = div.animate(null, 100 * MS_PER_SEC); + // 1s past effect end + animation.currentTime = + animation.effect.getComputedTiming().endTime + 1 * MS_PER_SEC; + animation.finish(); + + assert_time_equals_literal(animation.currentTime, 100 * MS_PER_SEC, + '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 div = createDiv(t); + const animation = div.animate(null, 100 * MS_PER_SEC); + animation.currentTime = 100 * MS_PER_SEC; + await animation.finished; + + animation.playbackRate = -1; + animation.finish(); + + assert_equals(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 div = createDiv(t); + const animation = div.animate(null, 100 * MS_PER_SEC); + animation.currentTime = 100 * MS_PER_SEC; + await animation.finished; + + animation.playbackRate = -1; + animation.currentTime = -1000; + animation.finish(); + + assert_equals(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 div = createDiv(t); + const animation = div.animate(null, 100 * MS_PER_SEC); + animation.pause(); + await animation.ready; + + animation.finish(); + + assert_equals(animation.playState, 'finished', + 'The play state of a paused animation should become ' + + '"finished"'); + assert_times_equal(animation.startTime, + animation.timeline.currentTime - 100 * MS_PER_SEC, + 'The start time of a paused animation should be set'); +}, 'Finishing a paused animation resolves the start time'); + +test(t => { + const div = createDiv(t); + const animation = div.animate(null, 100 * MS_PER_SEC); + // 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_times_equal(animation.startTime, + animation.timeline.currentTime - 100 * MS_PER_SEC / 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'); + +test(t => { + const div = createDiv(t); + const animation = div.animate(null, 100 * MS_PER_SEC); + 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_times_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'); + +test(t => { + const div = createDiv(t); + const animation = div.animate(null, 100 * MS_PER_SEC); + 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_times_equal(animation.startTime, + animation.timeline.currentTime - 100 * MS_PER_SEC / 0.5, + 'The start time of a play-pending animation should ' + + 'be set'); +}, 'Finishing an animation while play-pending resolves the pending' + + ' task immediately'); + +// FIXME: Add a test for when we are play-pending without an active timeline. +// - In that case even after calling finish() we should still be pending but +// the current time should be updated + +promise_test(async t => { + const div = createDiv(t); + const animation = div.animate(null, 100 * MS_PER_SEC); + 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 div = createDiv(t); + const animation = div.animate(null, 100 * MS_PER_SEC); + 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 effect = new KeyframeEffect(null, null, 100 * MS_PER_SEC); + const animation = new Animation(effect, document.timeline); + 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 without a target resolves the finished promise' + + ' synchronously'); + +promise_test(async t => { + const animation = createDiv(t).animate(null, 100 * MS_PER_SEC); + const promise = animation.ready; + let readyResolved = false; + + animation.finish(); + animation.ready.then(() => { readyResolved = true; }); + + const promiseResult = await animation.finished; + + 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 = createDiv(t).animate(null, 100 * MS_PER_SEC); + await animation.ready; + + animation.updatePlaybackRate(2); + assert_true(animation.pending); + + animation.finish(); + assert_false(animation.pending); + assert_equals(animation.playbackRate, 2); + assert_time_equals_literal(animation.currentTime, 100 * MS_PER_SEC); +}, 'A pending playback rate should be applied immediately when an animation' + + ' is finished'); + +promise_test(async t => { + const animation = createDiv(t).animate(null, 100 * MS_PER_SEC); + await animation.ready; + + animation.updatePlaybackRate(0); + + assert_throws_dom('InvalidStateError', () => { + animation.finish(); + }); +}, 'An exception should be thrown if the effective playback rate is zero'); + +promise_test(async t => { + const animation = createDiv(t).animate(null, { + duration: 100 * MS_PER_SEC, + iterations: Infinity + }); + animation.currentTime = 50 * MS_PER_SEC; + animation.playbackRate = -1; + await animation.ready; + + animation.updatePlaybackRate(1); + + assert_throws_dom('InvalidStateError', () => { + animation.finish(); + }); +}, 'An exception should be thrown when finishing if the effective playback rate' + + ' is positive and the target effect end is infinity'); + +promise_test(async t => { + const animation = createDiv(t).animate(null, { + duration: 100 * MS_PER_SEC, + iterations: Infinity + }); + await animation.ready; + + animation.updatePlaybackRate(-1); + + animation.finish(); + // Should not have thrown +}, 'An exception is NOT thrown when finishing if the effective playback rate' + + ' is negative and the target effect end is infinity'); + +promise_test(async t => { + const div = createDiv(t); + const animation = div.animate({}, 100 * MS_PER_SEC); + div.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 = createDiv(t).animate(null, 100 * MS_PER_SEC); + 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_times_equal(animation.startTime, + animation.timeline.currentTime - 100 * MS_PER_SEC, + 'The start time of a finished animation should be set'); + assert_times_equal(animation.currentTime, 100000, + 'Hold time should be set to end boundary of the animation'); + +}, 'Finishing a canceled animation sets the current and start times'); + +</script> +</body> diff --git a/testing/web-platform/tests/web-animations/timing-model/animations/infinite-duration-animation-ref.html b/testing/web-platform/tests/web-animations/timing-model/animations/infinite-duration-animation-ref.html new file mode 100644 index 0000000000..6b358bd4e7 --- /dev/null +++ b/testing/web-platform/tests/web-animations/timing-model/animations/infinite-duration-animation-ref.html @@ -0,0 +1,19 @@ +<!DOCTYPE html> +<title>Reference for infinite duration animation</title> +<style> + #notes { + position: absolute; + left: 0px; + top: 100px; + } + body { + background: white; + } +</style> +<body> + <p id="notes"> + This test creates an infinite duration animations, which should be stuck at + a progress of 0. If any blue pixels appear in the screenshot, the test + fails. + </p> +</body> diff --git a/testing/web-platform/tests/web-animations/timing-model/animations/infinite-duration-animation.html b/testing/web-platform/tests/web-animations/timing-model/animations/infinite-duration-animation.html new file mode 100644 index 0000000000..c641e5afa2 --- /dev/null +++ b/testing/web-platform/tests/web-animations/timing-model/animations/infinite-duration-animation.html @@ -0,0 +1,64 @@ + +<!DOCTYPE html> +<html class="reftest-wait"> +<meta charset="UTF-8"> +<title>Infinite duration animation</title> +<link rel="match" href="infinite-duration-animation-ref.html"> +<script src="/common/reftest-wait.js"></script> +<script src="../../testcommon.js"></script> +<style> + #box-1, #box-2 { + border: 1px solid white; + height: 40px; + position: absolute; + top: 40px; + width: 40px; + } + #box-1 { + background: blue; + z-index: 1; + } + #box-2 { + background: white; + z-index: 2; + } + #notes { + position: absolute; + left: 0px; + top: 100px; + } + body { + background: white; + } +</style> + +<body> + <div id="box-1"></div> + <div id="box-2"></div> + <p id="notes"> + This test creates an infinite duration animations, which should be stuck at + a progress of 0. If any blue pixels appear in the screenshot, the test + fails. + </p> +</body> +<script> + onload = async function() { + // Double rAF to ensure that we are not bogged down during initialization + // and the compositor is ready. + waitForAnimationFrames(2).then(() => { + const elem = document.getElementById('box-1'); + const keyframes = [ + { transform: 'translateX(0px)' }, + { transform: 'translateX(100px)' } + ]; + const effect = + new KeyframeEffect(elem, keyframes, + {iterations: 3, duration: Infinity}); + const animation = new Animation(effect); + animation.play(); + animation.ready.then(() => { + takeScreenshotDelayed(100); + }); + }); + }; +</script> diff --git a/testing/web-platform/tests/web-animations/timing-model/animations/pausing-an-animation.html b/testing/web-platform/tests/web-animations/timing-model/animations/pausing-an-animation.html new file mode 100644 index 0000000000..dd9522cb35 --- /dev/null +++ b/testing/web-platform/tests/web-animations/timing-model/animations/pausing-an-animation.html @@ -0,0 +1,119 @@ +<!DOCTYPE html> +<meta charset=utf-8> +<title>Pausing an animation</title> +<link rel="help" + href="https://drafts.csswg.org/web-animations/#pausing-an-animation-section"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../../testcommon.js"></script> +<body> +<div id="log"></div> +<script> +'use strict'; + +promise_test(async t => { + const animation = createDiv(t).animate(null, 100 * MS_PER_SEC); + await animation.ready; + + const startTimeBeforePausing = animation.startTime; + + animation.pause(); + assert_equals(animation.startTime, startTimeBeforePausing, + 'The start time does not change when pausing-pending'); + + await animation.ready; + + assert_equals(animation.startTime, null, + 'The start time is unresolved when paused'); +}, 'Pausing clears the start time'); + +promise_test(async t => { + const animation = createDiv(t).animate(null, 100 * MS_PER_SEC); + await animation.ready; + + animation.pause(); + assert_not_equals(animation.startTime, null, + 'The start time is resolved when pause-pending'); + + animation.play(); + assert_not_equals(animation.startTime, null, + 'The start time is preserved when a pause is aborted'); +}, 'Aborting a pause preserves the start time'); + +promise_test(async t => { + const animation = createDiv(t).animate(null, 100 * MS_PER_SEC); + const promise = animation.ready; + animation.pause(); + + const promiseResult = await promise; + + assert_equals(promiseResult, animation); + assert_equals(animation.ready, promise); + assert_false(animation.pending, 'No longer pause-pending'); +}, 'A pending ready promise should be resolved and not replaced when the' + + ' animation is paused'); + +promise_test(async t => { + const animation = createDiv(t).animate(null, 100 * MS_PER_SEC); + // Let animation start roughly half-way through + animation.currentTime = 50 * MS_PER_SEC; + await animation.ready; + + // Go pause-pending and also set a pending playback rate + animation.pause(); + animation.updatePlaybackRate(0.5); + + await animation.ready; + // If the current time was updated using the new playback rate it will jump + // back to 25s but if we correctly used the old playback rate the current time + // will be >= 50s. + assert_greater_than_equal(animation.currentTime, 50 * MS_PER_SEC); +}, 'A pause-pending animation maintains the current time when applying a' + + ' pending playback rate'); + +promise_test(async t => { + // This test does not cover a specific step in the algorithm but serves as a + // high-level sanity check that pausing does, in fact, freeze the current + // time. + const animation = createDiv(t).animate(null, 100 * MS_PER_SEC); + await animation.ready; + + animation.pause(); + await animation.ready; + + const currentTimeAfterPausing = animation.currentTime; + + await waitForNextFrame(); + + assert_equals(animation.currentTime, currentTimeAfterPausing, + 'Animation.currentTime is unchanged after pausing'); +}, 'The animation\'s current time remains fixed after pausing'); + + +promise_test(async t => { + + const animation = createDiv(t).animate(null, 100 * MS_PER_SEC); + + const originalReadyPromise = animation.ready; + animation.cancel(); + assert_equals(animation.startTime, null); + assert_equals(animation.currentTime, null); + + const readyPromise = animation.ready; + assert_not_equals(originalReadyPromise, readyPromise, + 'Canceling an animation should create a new ready promise'); + + animation.pause(); + assert_equals(animation.playState, 'paused', + 'Pausing a canceled animation should update the play state'); + assert_true(animation.pending, 'animation should be pause-pending'); + await animation.ready; + assert_false(animation.pending, + 'animation should no longer be pause-pending'); + assert_equals(animation.startTime, null, 'start time should be unresolved'); + assert_equals(animation.currentTime, 0, 'current time should be set to zero'); + +}, 'Pausing a canceled animation sets the current time'); + +</script> +</body> diff --git a/testing/web-platform/tests/web-animations/timing-model/animations/play-states.html b/testing/web-platform/tests/web-animations/timing-model/animations/play-states.html new file mode 100644 index 0000000000..ec7d8c842f --- /dev/null +++ b/testing/web-platform/tests/web-animations/timing-model/animations/play-states.html @@ -0,0 +1,185 @@ +<!DOCTYPE html> +<meta charset=utf-8> +<title>Play states</title> +<link rel="help" href="https://drafts.csswg.org/web-animations/#play-states"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../../testcommon.js"></script> +<body> +<div id="log"></div> +<script> +'use strict'; + +test(t => { + const animation = new Animation( + new KeyframeEffect(null, {}, 100 * MS_PER_SEC) + ); + assert_equals(animation.currentTime, null, + 'Current time should be initially unresolved'); + + assert_equals(animation.playState, 'idle'); +}, 'reports \'idle\' for an animation with an unresolved current time' + + ' and no pending tasks') + +test(t => { + const animation = createDiv(t).animate({}, 100 * MS_PER_SEC); + + animation.pause(); + + assert_equals(animation.playState, 'paused'); +}, 'reports \'paused\' for an animation with a pending pause task'); + +test(t => { + const animation = new Animation( + new KeyframeEffect(null, {}, 100 * MS_PER_SEC) + ); + + animation.currentTime = 0; + assert_equals(animation.startTime, null, + 'Start time should still be unresolved after setting current' + + ' time'); + + assert_equals(animation.playState, 'paused'); +}, 'reports \'paused\' for an animation with a resolved current time and' + + ' unresolved start time') + +test(t => { + const animation = new Animation( + new KeyframeEffect(null, {}, 100 * MS_PER_SEC) + ); + + animation.startTime = document.timeline.currentTime; + assert_not_equals(animation.currentTime, null, + 'Current time should be resolved after setting start time'); + + assert_equals(animation.playState, 'running'); +}, 'reports \'running\' for an animation with a resolved start time and' + + ' current time'); + +test(t => { + const animation = new Animation( + new KeyframeEffect(null, {}, 100 * MS_PER_SEC) + ); + animation.startTime = document.timeline.currentTime; + + animation.currentTime = 100 * MS_PER_SEC; + + assert_equals(animation.playState, 'finished'); +}, 'reports \'finished\' when playback rate > 0 and' + + ' current time = target effect end'); + +test(t => { + const animation = new Animation( + new KeyframeEffect(null, {}, 100 * MS_PER_SEC) + ); + animation.startTime = document.timeline.currentTime; + + animation.playbackRate = 0; + animation.currentTime = 100 * MS_PER_SEC; + + assert_equals(animation.playState, 'running'); +}, 'reports \'running\' when playback rate = 0 and' + + ' current time = target effect end'); + +test(t => { + const animation = new Animation( + new KeyframeEffect(null, {}, 100 * MS_PER_SEC) + ); + animation.startTime = document.timeline.currentTime; + + animation.playbackRate = -1; + animation.currentTime = 100 * MS_PER_SEC; + + assert_equals(animation.playState, 'running'); +}, 'reports \'running\' when playback rate < 0 and' + + ' current time = target effect end'); + +test(t => { + const animation = new Animation( + new KeyframeEffect(null, {}, 100 * MS_PER_SEC) + ); + animation.startTime = document.timeline.currentTime; + + animation.currentTime = 0; + + assert_equals(animation.playState, 'running'); +}, 'reports \'running\' when playback rate > 0 and' + + ' current time = 0'); + +test(t => { + const animation = new Animation( + new KeyframeEffect(null, {}, 100 * MS_PER_SEC) + ); + animation.startTime = document.timeline.currentTime; + + animation.playbackRate = 0; + animation.currentTime = 0; + + assert_equals(animation.playState, 'running'); +}, 'reports \'running\' when playback rate = 0 and' + + ' current time = 0'); + +test(t => { + const animation = new Animation( + new KeyframeEffect(null, {}, 100 * MS_PER_SEC) + ); + animation.startTime = document.timeline.currentTime; + + animation.playbackRate = -1; + animation.currentTime = 0; + + assert_equals(animation.playState, 'finished'); +}, 'reports \'finished\' when playback rate < 0 and' + + ' current time = 0'); + +test(t => { + const animation = createDiv(t).animate({}, 0); + assert_equals(animation.startTime, null, + 'Sanity check: start time should be unresolved'); + + assert_equals(animation.playState, 'finished'); +}, 'reports \'finished\' when playback rate > 0 and' + + ' current time = target effect end and there is a pending play task'); + +test(t => { + const animation = createDiv(t).animate({}, 100 * MS_PER_SEC); + assert_equals(animation.startTime, null, + 'Sanity check: start time should be unresolved'); + + assert_equals(animation.playState, 'running'); +}, 'reports \'running\' when playback rate > 0 and' + + ' current time < target effect end and there is a pending play task'); + +test(t => { + const animation = createDiv(t).animate({}, 100 * MS_PER_SEC); + assert_equals(animation.playState, 'running'); + assert_true(animation.pending); +}, 'reports \'running\' for a play-pending animation'); + +test(t => { + const animation = createDiv(t).animate({}, 100 * MS_PER_SEC); + animation.pause(); + assert_equals(animation.playState, 'paused'); + assert_true(animation.pending); +}, 'reports \'paused\' for a pause-pending animation'); + +test(t => { + const animation = createDiv(t).animate({}, 0); + assert_equals(animation.playState, 'finished'); + assert_true(animation.pending); +}, 'reports \'finished\' for a finished-pending animation'); + +test(t => { + const animation = createDiv(t).animate({}, 100 * MS_PER_SEC); + // Set up the pending playback rate + animation.updatePlaybackRate(-1); + // Call play again so that we seek to the end while remaining play-pending + animation.play(); + // For a pending animation, the play state should always report what the + // play state _will_ be once we finish pending. + assert_equals(animation.playState, 'running'); + assert_true(animation.pending); +}, 'reports the play state based on the pending playback rate'); + +</script> +</body> diff --git a/testing/web-platform/tests/web-animations/timing-model/animations/playing-an-animation.html b/testing/web-platform/tests/web-animations/timing-model/animations/playing-an-animation.html new file mode 100644 index 0000000000..01e036ae57 --- /dev/null +++ b/testing/web-platform/tests/web-animations/timing-model/animations/playing-an-animation.html @@ -0,0 +1,177 @@ +<!DOCTYPE html> +<meta charset=utf-8> +<title>Playing an animation</title> +<link rel="help" + href="https://drafts.csswg.org/web-animations/#playing-an-animation-section"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../../testcommon.js"></script> +<body> +<div id="log"></div> +<script> +'use strict'; + +test(t => { + const animation = createDiv(t).animate(null, 100 * MS_PER_SEC); + animation.currentTime = 1 * MS_PER_SEC; + assert_time_equals_literal(animation.currentTime, 1 * MS_PER_SEC); + animation.play(); + assert_time_equals_literal(animation.currentTime, 1 * MS_PER_SEC); +}, 'Playing a running animation leaves the current time unchanged'); + +test(t => { + const animation = createDiv(t).animate(null, 100 * MS_PER_SEC); + animation.finish(); + assert_time_equals_literal(animation.currentTime, 100 * MS_PER_SEC); + animation.play(); + assert_time_equals_literal(animation.currentTime, 0); +}, 'Playing a finished animation seeks back to the start'); + +test(t => { + const animation = createDiv(t).animate(null, 100 * MS_PER_SEC); + animation.playbackRate = -1; + animation.currentTime = 0; + assert_time_equals_literal(animation.currentTime, 0); + animation.play(); + assert_time_equals_literal(animation.currentTime, 100 * MS_PER_SEC); +}, 'Playing a finished and reversed animation seeks to end'); + +promise_test(async t => { + const animation = createDiv(t).animate(null, 100 * MS_PER_SEC); + animation.finish(); + + // Initiate a pause then abort it + animation.pause(); + animation.play(); + + // Wait to return to running state + await animation.ready; + + assert_true(animation.currentTime < 100 * 1000, + 'After aborting a pause when finished, the current time should' + + ' jump back to the start of the animation'); +}, 'Playing a pause-pending but previously finished animation seeks back to' + + ' to the start'); + +promise_test(async t => { + const animation = createDiv(t).animate(null, 100 * MS_PER_SEC); + animation.finish(); + await animation.ready; + + animation.play(); + assert_equals(animation.startTime, null, 'start time is unresolved'); +}, 'Playing a finished animation clears the start time'); + +test(t => { + const animation = createDiv(t).animate(null, 100 * MS_PER_SEC); + animation.cancel(); + const promise = animation.ready; + animation.play(); + assert_not_equals(animation.ready, promise); +}, 'The ready promise should be replaced if the animation is not already' + + ' pending'); + +promise_test(async t => { + const animation = createDiv(t).animate(null, 100 * MS_PER_SEC); + const promise = animation.ready; + const promiseResult = await promise; + assert_equals(promiseResult, animation); + assert_equals(animation.ready, promise); +}, 'A pending ready promise should be resolved and not replaced when the' + + ' animation enters the running state'); + +promise_test(async t => { + const animation = createDiv(t).animate(null, 100 * MS_PER_SEC); + animation.currentTime = 50 * MS_PER_SEC; + await animation.ready; + + animation.pause(); + await animation.ready; + + const holdTime = animation.currentTime; + + animation.play(); + await animation.ready; + + assert_less_than_equal( + animation.startTime, + animation.timeline.currentTime - holdTime + TIME_PRECISION + ); +}, 'Resuming an animation from paused calculates start time from hold time'); + +promise_test(async t => { + const animation = createDiv(t).animate(null, 100 * MS_PER_SEC); + await animation.ready; + + // Go to pause-pending state + animation.pause(); + assert_true(animation.pending, 'Animation is pending'); + const pauseReadyPromise = animation.ready; + + // Now play again immediately (abort the pause) + animation.play(); + assert_true(animation.pending, 'Animation is still pending'); + assert_equals(animation.ready, pauseReadyPromise, + 'The pause Promise is re-used when playing while waiting' + + ' to pause'); + + // Sanity check: Animation proceeds to running state + await animation.ready; + assert_true(!animation.pending && animation.playState === 'running', + 'Animation is running after aborting a pause'); +}, 'If a pause operation is interrupted, the ready promise is reused'); + +promise_test(async t => { + // Seek animation beyond target end + const animation = createDiv(t).animate(null, 100 * MS_PER_SEC); + animation.currentTime = -100 * MS_PER_SEC; + await animation.ready; + + // Set pending playback rate to the opposite direction + animation.updatePlaybackRate(-1); + assert_true(animation.pending); + assert_equals(animation.playbackRate, 1); + + // When we play, we should seek to the target end, NOT to zero (which + // is where we would seek to if we used the playbackRate of 1. + animation.play(); + assert_time_equals_literal(animation.currentTime, 100 * MS_PER_SEC); +}, 'A pending playback rate is used when determining auto-rewind behavior'); + +promise_test(async t => { + const animation = createDiv(t).animate(null, 100 * MS_PER_SEC); + animation.cancel(); + assert_equals(animation.startTime, null, + 'Start time should be unresolved'); + + const playTime = animation.timeline.currentTime; + animation.play(); + assert_true(animation.pending, 'Animation should be play-pending'); + + await animation.ready; + + assert_false(animation.pending, 'animation should no longer be pending'); + assert_time_greater_than_equal(animation.startTime, playTime, + 'The start time of the playing animation should be set'); +}, 'Playing a canceled animation sets the start time'); + +promise_test(async t => { + const animation = createDiv(t).animate(null, 100 * MS_PER_SEC); + animation.playbackRate = -1; + animation.cancel(); + assert_equals(animation.startTime, null, + 'Start time should be unresolved'); + + const playTime = animation.timeline.currentTime; + animation.play(); + assert_true(animation.pending, 'Animation should be play-pending'); + + await animation.ready; + + assert_false(animation.pending, 'Animation should no longer be pending'); + assert_time_greater_than_equal(animation.startTime, playTime + 100 * MS_PER_SEC, + 'The start time of the playing animation should be set'); +}, 'Playing a canceled animation backwards sets the start time'); + +</script> +</body> diff --git a/testing/web-platform/tests/web-animations/timing-model/animations/reverse-running-animation-ref.html b/testing/web-platform/tests/web-animations/timing-model/animations/reverse-running-animation-ref.html new file mode 100644 index 0000000000..7bf5b03c1e --- /dev/null +++ b/testing/web-platform/tests/web-animations/timing-model/animations/reverse-running-animation-ref.html @@ -0,0 +1,17 @@ +<!DOCTYPE html> +<title>Reference for reverse running animation</title> +<style> + #box { + background: green; + height: 40px; + width: 40px; + } +</style> +<body> + <div id="box"></div> + <p id="notes"> + This test animates the box color from green to red and reverses the play + direction shortly after the midpoint. If the box remains red, the test + failed. + </p> +</body> diff --git a/testing/web-platform/tests/web-animations/timing-model/animations/reverse-running-animation.html b/testing/web-platform/tests/web-animations/timing-model/animations/reverse-running-animation.html new file mode 100644 index 0000000000..c9eb2a068e --- /dev/null +++ b/testing/web-platform/tests/web-animations/timing-model/animations/reverse-running-animation.html @@ -0,0 +1,50 @@ + +<!DOCTYPE html> +<html class="reftest-wait"> +<meta charset="UTF-8"> +<title>reverse running animation</title> +<link rel="match" href="reverse-running-animation-ref.html"> +<script src="/common/reftest-wait.js"></script> +<script src="../../testcommon.js"></script> +<style> + #box { + background: green; + height: 40px; + width: 40px; + } +</style> +<body> + <div id="box"></div> + <p id="notes"> + This test animates the box color from green to red and reverses the play + direction shortly after the midpoint. If the box remains red, the test + failed. + </p> +</body> +<script> + onload = async function() { + const box = document.getElementById('box'); + const duration = 10000; + const anim = + box.animate({ bacground: [ 'green', 'red' ] }, + { duration: duration, easing: 'steps(2, jump-none)' }); + anim.currentTime = duration / 2; + anim.ready.then(() => { + const startTime = anim.timeline.currentTime; + waitForAnimationFrames(2).then(() => { + anim.reverse(); + anim.ready.then(() => { + const reversalTime = anim.timeline.currentTime; + const forwardPlayingTime = reversalTime - startTime; + const checkIfDone = () => { + if (anim.timeline.currentTime - reversalTime > forwardPlayingTime) + takeScreenshot(); + else + requestAnimationFrame(checkIfDone); + }; + requestAnimationFrame(checkIfDone); + }); + }); + }); + }; +</script> diff --git a/testing/web-platform/tests/web-animations/timing-model/animations/reversing-an-animation.html b/testing/web-platform/tests/web-animations/timing-model/animations/reversing-an-animation.html new file mode 100644 index 0000000000..8d869d72aa --- /dev/null +++ b/testing/web-platform/tests/web-animations/timing-model/animations/reversing-an-animation.html @@ -0,0 +1,266 @@ +<!DOCTYPE html> +<meta charset=utf-8> +<title>Reversing an animation</title> +<link rel="help" + href="https://drafts.csswg.org/web-animations/#reversing-an-animation-section"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../../testcommon.js"></script> +<body> +<div id="log"></div> +<script> +'use strict'; + +promise_test(async t => { + const div = createDiv(t); + const animation = div.animate({}, { duration: 100 * MS_PER_SEC, + iterations: Infinity }); + + await animation.ready; + // Wait a frame because if currentTime is still 0 when we call + // reverse(), it will throw (per spec). + await waitForAnimationFrames(1); + + assert_greater_than_equal(animation.currentTime, 0, + 'currentTime expected to be greater than 0, one frame after starting'); + animation.currentTime = 50 * MS_PER_SEC; + const previousPlaybackRate = animation.playbackRate; + animation.reverse(); + assert_equals(animation.playbackRate, previousPlaybackRate, + 'Playback rate should not have changed'); + await animation.ready; + + assert_equals(animation.playbackRate, -previousPlaybackRate, + 'Playback rate should be inverted'); +}, 'Reversing an animation inverts the playback rate'); + +promise_test(async t => { + const div = createDiv(t); + const animation = div.animate({}, { duration: 100 * MS_PER_SEC, + iterations: Infinity }); + animation.currentTime = 50 * MS_PER_SEC; + animation.pause(); + + await animation.ready; + + animation.reverse(); + await animation.ready; + + assert_equals(animation.playState, 'running', + 'Animation.playState should be "running" after reverse()'); +}, 'Reversing an animation plays a pausing animation'); + +test(t => { + const div = createDiv(t); + const animation = div.animate({}, 100 * MS_PER_SEC); + animation.currentTime = 50 * MS_PER_SEC; + animation.reverse(); + + assert_equals(animation.currentTime, 50 * MS_PER_SEC, + 'The current time should not change it is in the middle of ' + + 'the animation duration'); +}, 'Reversing an animation maintains the same current time'); + +test(t => { + const div = createDiv(t); + const animation = div.animate({}, { duration: 200 * MS_PER_SEC, + delay: -100 * MS_PER_SEC }); + assert_true(animation.pending, + 'The animation is pending before we call reverse'); + + animation.reverse(); + + assert_true(animation.pending, + 'The animation is still pending after calling reverse'); +}, 'Reversing an animation does not cause it to leave the pending state'); + +promise_test(async t => { + const div = createDiv(t); + const animation = div.animate({}, { duration: 200 * MS_PER_SEC, + delay: -100 * MS_PER_SEC }); + let readyResolved = false; + animation.ready.then(() => { readyResolved = true; }); + + animation.reverse(); + + await Promise.resolve(); + assert_false(readyResolved, + 'ready promise should not have been resolved yet'); +}, 'Reversing an animation does not cause it to resolve the ready promise'); + +test(t => { + const div = createDiv(t); + const animation = div.animate({}, 100 * MS_PER_SEC); + animation.currentTime = 200 * MS_PER_SEC; + animation.reverse(); + + assert_equals(animation.currentTime, 100 * MS_PER_SEC, + 'reverse() should start playing from the animation effect end ' + + 'if the playbackRate > 0 and the currentTime > effect end'); +}, 'Reversing an animation when playbackRate > 0 and currentTime > ' + + 'effect end should make it play from the end'); + +test(t => { + const div = createDiv(t); + const animation = div.animate({}, 100 * MS_PER_SEC); + + animation.currentTime = -200 * MS_PER_SEC; + animation.reverse(); + + assert_equals(animation.currentTime, 100 * MS_PER_SEC, + 'reverse() should start playing from the animation effect end ' + + 'if the playbackRate > 0 and the currentTime < 0'); +}, 'Reversing an animation when playbackRate > 0 and currentTime < 0 ' + + 'should make it play from the end'); + +test(t => { + const div = createDiv(t); + const animation = div.animate({}, 100 * MS_PER_SEC); + animation.playbackRate = -1; + animation.currentTime = -200 * MS_PER_SEC; + animation.reverse(); + + assert_equals(animation.currentTime, 0, + 'reverse() should start playing from the start of animation time ' + + 'if the playbackRate < 0 and the currentTime < 0'); +}, 'Reversing an animation when playbackRate < 0 and currentTime < 0 ' + + 'should make it play from the start'); + +test(t => { + const div = createDiv(t); + const animation = div.animate({}, 100 * MS_PER_SEC); + animation.playbackRate = -1; + animation.currentTime = 200 * MS_PER_SEC; + animation.reverse(); + + assert_equals(animation.currentTime, 0, + 'reverse() should start playing from the start of animation time ' + + 'if the playbackRate < 0 and the currentTime > effect end'); +}, 'Reversing an animation when playbackRate < 0 and currentTime > effect ' + + 'end should make it play from the start'); + +test(t => { + const div = createDiv(t); + const animation = div.animate({}, { duration: 100 * MS_PER_SEC, + iterations: Infinity }); + animation.currentTime = -200 * MS_PER_SEC; + + assert_throws_dom('InvalidStateError', + () => { animation.reverse(); }, + 'reverse() should throw InvalidStateError ' + + 'if the playbackRate > 0 and the currentTime < 0 ' + + 'and the target effect is positive infinity'); +}, 'Reversing an animation when playbackRate > 0 and currentTime < 0 ' + + 'and the target effect end is positive infinity should throw an exception'); + +promise_test(async t => { + const animation = createDiv(t).animate({}, { duration: 100 * MS_PER_SEC, + iterations: Infinity }); + animation.currentTime = -200 * MS_PER_SEC; + + try { animation.reverse(); } catch(e) { } + + assert_equals(animation.playbackRate, 1, 'playbackRate is unchanged'); + + await animation.ready; + assert_equals(animation.playbackRate, 1, 'playbackRate remains unchanged'); +}, 'When reversing throws an exception, the playback rate remains unchanged'); + +test(t => { + const div = createDiv(t); + const animation = div.animate({}, { duration: 100 * MS_PER_SEC, + iterations: Infinity }); + animation.currentTime = -200 * MS_PER_SEC; + animation.playbackRate = 0; + + try { + animation.reverse(); + } catch (e) { + assert_unreached(`Unexpected exception when calling reverse(): ${e}`); + } +}, 'Reversing animation when playbackRate = 0 and currentTime < 0 ' + + 'and the target effect end is positive infinity should NOT throw an ' + + 'exception'); + +test(t => { + const div = createDiv(t); + const animation = div.animate({}, { duration: 100 * MS_PER_SEC, + iterations: Infinity }); + animation.playbackRate = -1; + animation.currentTime = -200 * MS_PER_SEC; + animation.reverse(); + + assert_equals(animation.currentTime, 0, + 'reverse() should start playing from the start of animation time ' + + 'if the playbackRate < 0 and the currentTime < 0 ' + + 'and the target effect is positive infinity'); +}, 'Reversing an animation when playbackRate < 0 and currentTime < 0 ' + + 'and the target effect end is positive infinity should make it play ' + + 'from the start'); + +promise_test(async t => { + const div = createDiv(t); + const animation = div.animate({}, 100 * MS_PER_SEC); + animation.playbackRate = 0; + animation.currentTime = 50 * MS_PER_SEC; + animation.reverse(); + + await animation.ready; + assert_equals(animation.playbackRate, 0, + 'reverse() should preserve playbackRate if the playbackRate == 0'); + assert_equals(animation.currentTime, 50 * MS_PER_SEC, + 'reverse() should not affect the currentTime if the playbackRate == 0'); +}, 'Reversing when when playbackRate == 0 should preserve the current ' + + 'time and playback rate'); + +test(t => { + const div = createDiv(t); + const animation = + new Animation(new KeyframeEffect(div, null, 100 * MS_PER_SEC)); + assert_equals(animation.currentTime, null); + + animation.reverse(); + + assert_equals(animation.currentTime, 100 * MS_PER_SEC, + 'animation.currentTime should be at its effect end'); +}, 'Reversing an idle animation from starts playing the animation'); + +test(t => { + const div = createDiv(t); + const animation = + new Animation(new KeyframeEffect(div, null, 100 * MS_PER_SEC), null); + + assert_throws_dom('InvalidStateError', () => { animation.reverse(); }); +}, 'Reversing an animation without an active timeline throws an ' + + 'InvalidStateError'); + +promise_test(async t => { + const animation = createDiv(t).animate(null, 100 * MS_PER_SEC); + await animation.ready; + + animation.updatePlaybackRate(2); + animation.reverse(); + + await animation.ready; + assert_equals(animation.playbackRate, -2); +}, 'Reversing should use the negative pending playback rate'); + +promise_test(async t => { + const animation = createDiv(t).animate(null, { + duration: 100 * MS_PER_SEC, + iterations: Infinity, + }); + animation.currentTime = -200 * MS_PER_SEC; + await animation.ready; + + animation.updatePlaybackRate(2); + assert_throws_dom('InvalidStateError', () => { animation.reverse(); }); + assert_equals(animation.playbackRate, 1); + + await animation.ready; + assert_equals(animation.playbackRate, 2); +}, 'When reversing fails, it should restore any previous pending playback' + + ' rate'); + +</script> +</body> diff --git a/testing/web-platform/tests/web-animations/timing-model/animations/seamlessly-updating-the-playback-rate-of-an-animation.html b/testing/web-platform/tests/web-animations/timing-model/animations/seamlessly-updating-the-playback-rate-of-an-animation.html new file mode 100644 index 0000000000..dffbeabd59 --- /dev/null +++ b/testing/web-platform/tests/web-animations/timing-model/animations/seamlessly-updating-the-playback-rate-of-an-animation.html @@ -0,0 +1,171 @@ +<!doctype html> +<meta charset=utf-8> +<title>Seamlessly updating the playback rate of an animation</title> +<link rel="help" + href="https://drafts.csswg.org/web-animations-1/#seamlessly-updating-the-playback-rate-of-an-animation"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../../testcommon.js"></script> +<body> +<div id="log"></div> +<script> +'use strict'; + +promise_test(async t => { + const animation = createDiv(t).animate(null, 100 * MS_PER_SEC); + await animation.ready; + + animation.currentTime = 50 * MS_PER_SEC; + + animation.updatePlaybackRate(0.5); + await animation.ready; + // Since the animation is in motion (and we want to test it while it is in + // motion!) we can't assert that the current time == 50s but we can check + // that the current time is NOT re-calculated by simply substituting in the + // new playback rate (i.e. without adjusting the start time). If that were + // the case the currentTime would jump to 25s. So we just test the currentTime + // hasn't gone backwards. + assert_greater_than_equal(animation.currentTime, 50 * MS_PER_SEC, + 'Reducing the playback rate should not change the current time ' + + 'of a playing animation'); + + animation.updatePlaybackRate(2); + await animation.ready; + // Likewise, we test here that the current time does not jump to 100s as it + // would if we naively applied a playbackRate of 2 without adjusting the + // startTime. + assert_less_than(animation.currentTime, 100 * MS_PER_SEC, + 'Increasing the playback rate should not change the current time ' + + 'of a playing animation'); +}, 'Updating the playback rate maintains the current time'); + +promise_test(async t => { + const animation = createDiv(t).animate(null, 100 * MS_PER_SEC); + await animation.ready; + + assert_false(animation.pending); + animation.updatePlaybackRate(2); + assert_true(animation.pending); +}, 'Updating the playback rate while running makes the animation pending'); + +promise_test(async t => { + const animation = createDiv(t).animate(null, 100 * MS_PER_SEC); + animation.currentTime = 50 * MS_PER_SEC; + assert_true(animation.pending); + + animation.updatePlaybackRate(0.5); + + // Check that the hold time is updated as expected + assert_time_equals_literal(animation.currentTime, 50 * MS_PER_SEC); + + await animation.ready; + + // As above, check that the currentTime is not calculated by simply + // substituting in the updated playbackRate without updating the startTime. + assert_greater_than_equal(animation.currentTime, 50 * MS_PER_SEC, + 'Reducing the playback rate should not change the current time ' + + 'of a play-pending animation'); +}, 'Updating the playback rate on a play-pending animation maintains' + + ' the current time'); + +promise_test(async t => { + const animation = createDiv(t).animate(null, 100 * MS_PER_SEC); + animation.currentTime = 50 * MS_PER_SEC; + await animation.ready; + + animation.pause(); + animation.updatePlaybackRate(0.5); + + assert_greater_than_equal(animation.currentTime, 50 * MS_PER_SEC); +}, 'Updating the playback rate on a pause-pending animation maintains' + + ' the current time'); + +promise_test(async t => { + const animation = createDiv(t).animate(null, 100 * MS_PER_SEC); + + animation.updatePlaybackRate(2); + animation.updatePlaybackRate(3); + animation.updatePlaybackRate(4); + + assert_equals(animation.playbackRate, 1); + await animation.ready; + + assert_equals(animation.playbackRate, 4); +}, 'If a pending playback rate is set multiple times, the latest wins'); + +test(t => { + const animation = createDiv(t).animate(null, 100 * MS_PER_SEC); + animation.cancel(); + + animation.updatePlaybackRate(2); + assert_equals(animation.playbackRate, 2); + assert_false(animation.pending); +}, 'In the idle state, the playback rate is applied immediately'); + +promise_test(async t => { + const animation = createDiv(t).animate(null, 100 * MS_PER_SEC); + animation.pause(); + await animation.ready; + + animation.updatePlaybackRate(2); + assert_equals(animation.playbackRate, 2); + assert_false(animation.pending); +}, 'In the paused state, the playback rate is applied immediately'); + +promise_test(async t => { + const animation = createDiv(t).animate(null, 100 * MS_PER_SEC); + animation.finish(); + assert_time_equals_literal(animation.currentTime, 100 * MS_PER_SEC); + assert_false(animation.pending); + + animation.updatePlaybackRate(2); + assert_equals(animation.playbackRate, 2); + assert_time_equals_literal(animation.currentTime, 100 * MS_PER_SEC); + assert_false(animation.pending); +}, 'Updating the playback rate on a finished animation maintains' + + ' the current time'); + +promise_test(async t => { + const animation = createDiv(t).animate(null, 100 * MS_PER_SEC); + animation.finish(); + assert_time_equals_literal(animation.currentTime, 100 * MS_PER_SEC); + assert_false(animation.pending); + + animation.updatePlaybackRate(0); + assert_equals(animation.playbackRate, 0); + assert_time_equals_literal(animation.currentTime, 100 * MS_PER_SEC); + assert_false(animation.pending); +}, 'Updating the playback rate to zero on a finished animation maintains' + + ' the current time'); + +promise_test(async t => { + const animation = createDiv(t).animate(null, 100 * MS_PER_SEC); + await animation.ready; + + // Get the animation in a state where it has an unresolved current time, + // a resolved start time (so it is not 'idle') and but no pending play task. + animation.timeline = null; + animation.startTime = 0; + assert_equals(animation.currentTime, null); + assert_equals(animation.playState, 'running'); + + // Make the effect end infinite. + animation.effect.updateTiming({ endDelay: 1e38 }); + + // Now we want to check that when we go to set a negative playback rate we + // don't end up throwing an InvalidStateError (which would happen if we ended + // up applying the auto-rewind behavior). + animation.updatePlaybackRate(-1); + + // Furthermore, we should apply the playback rate immediately since the + // current time is unresolved. + assert_equals(animation.playbackRate, -1, + 'We apply the pending playback rate immediately if the current time is ' + + 'unresolved'); + assert_false(animation.pending); +}, 'Updating the negative playback rate with the unresolved current time and' + + ' a positive infinite associated effect end should not throw an' + + ' exception'); + +</script> +</body> diff --git a/testing/web-platform/tests/web-animations/timing-model/animations/setting-the-current-time-of-an-animation.html b/testing/web-platform/tests/web-animations/timing-model/animations/setting-the-current-time-of-an-animation.html new file mode 100644 index 0000000000..809877345f --- /dev/null +++ b/testing/web-platform/tests/web-animations/timing-model/animations/setting-the-current-time-of-an-animation.html @@ -0,0 +1,167 @@ +<!doctype html> +<meta charset=utf-8> +<title>Setting the current time of an animation</title> +<link rel="help" + href="https://drafts.csswg.org/web-animations-1/#setting-the-current-time-of-an-animation"> +<script src='/resources/testharness.js'></script> +<script src='/resources/testharnessreport.js'></script> +<script src='../../testcommon.js'></script> +<body> +<div id='log'></div> +<script> +'use strict'; + +test(t => { + const anim = new Animation(); + assert_equals(anim.playState, 'idle'); + assert_equals(anim.currentTime, null); + + // This should not throw because the currentTime is already null. + anim.currentTime = null; +}, 'Setting the current time of a pending animation to unresolved does not' + + ' throw a TypeError'); + +promise_test(async t => { + const anim = createDiv(t).animate(null, 100 * MS_PER_SEC); + await anim.ready; + + assert_greater_than_equal(anim.currentTime, 0); + assert_throws_js(TypeError, () => { + anim.currentTime = null; + }); +}, 'Setting the current time of a playing animation to unresolved throws a' + + ' TypeError'); + +promise_test(async t => { + const anim = createDiv(t).animate(null, 100 * MS_PER_SEC); + await anim.ready; + anim.pause(); + + assert_greater_than_equal(anim.currentTime, 0); + assert_throws_js(TypeError, () => { + anim.currentTime = null; + }); +}, 'Setting the current time of a paused animation to unresolved throws a' + + ' TypeError'); + + +promise_test(async t => { + const animation = createDiv(t).animate(null, 100 * MS_PER_SEC); + + assert_throws_js(TypeError, () => { + animation.currentTime = CSSNumericValue.parse("30%"); + }); + assert_throws_js(TypeError, () => { + animation.currentTime = CSSNumericValue.parse("30deg"); + }); + + animation.currentTime = 2000; + assert_equals(animation.currentTime, 2000, "Set current time using double"); + + animation.currentTime = CSSNumericValue.parse("3000"); + assert_equals(animation.currentTime, 3000, "Set current time using " + + "CSSNumericValue number value"); + + animation.currentTime = CSSNumericValue.parse("4000ms"); + assert_equals(animation.currentTime, 4000, "Set current time using " + + "CSSNumericValue milliseconds value"); + + animation.currentTime = CSSNumericValue.parse("50s"); + assert_equals(animation.currentTime, 50000, "Set current time using " + + "CSSNumericValue seconds value"); +}, 'Validate different value types that can be used to set current time'); + +promise_test(async t => { + const anim = createDiv(t).animate(null, 100 * MS_PER_SEC); + await anim.ready; + anim.pause(); + + // We should be pause-pending now + assert_true(anim.pending); + assert_equals(anim.playState, 'paused'); + + // Apply a pending playback rate + anim.updatePlaybackRate(2); + assert_equals(anim.playbackRate, 1); + + // Setting the current time should apply the pending playback rate + anim.currentTime = 50 * MS_PER_SEC; + assert_equals(anim.playbackRate, 2); + assert_false(anim.pending); + + // Sanity check that the current time is preserved + assert_time_equals_literal(anim.currentTime, 50 * MS_PER_SEC); +}, 'Setting the current time of a pausing animation applies a pending playback' + + ' rate'); + + +// The following tests verify that currentTime can be set outside of the normal +// bounds of an animation. + +promise_test(async t => { + const anim = createDiv(t).animate(null, 100 * MS_PER_SEC); + await anim.ready; + + anim.currentTime = 200 * MS_PER_SEC; + assert_equals(anim.playState, 'finished'); + assert_time_equals_literal(anim.currentTime, 200 * MS_PER_SEC); +}, 'Setting the current time after the end with a positive playback rate'); + +promise_test(async t => { + const anim = createDiv(t).animate(null, 100 * MS_PER_SEC); + await anim.ready; + + anim.currentTime = -100 * MS_PER_SEC; + assert_equals(anim.playState, 'running'); + assert_time_equals_literal(anim.currentTime, -100 * MS_PER_SEC); + + await waitForAnimationFrames(2); + assert_greater_than(anim.currentTime, -100 * MS_PER_SEC); +}, 'Setting a negative current time with a positive playback rate'); + +promise_test(async t => { + const anim = createDiv(t).animate(null, 100 * MS_PER_SEC); + anim.updatePlaybackRate(-1); + await anim.ready; + + anim.currentTime = 200 * MS_PER_SEC; + assert_equals(anim.playState, 'running'); + assert_time_equals_literal(anim.currentTime, 200 * MS_PER_SEC); + + await waitForAnimationFrames(2); + assert_less_than(anim.currentTime, 200 * MS_PER_SEC); +}, 'Setting the current time after the end with a negative playback rate'); + +promise_test(async t => { + const anim = createDiv(t).animate(null, 100 * MS_PER_SEC); + anim.updatePlaybackRate(-1); + await anim.ready; + + anim.currentTime = -100 * MS_PER_SEC; + assert_equals(anim.playState, 'finished'); + assert_time_equals_literal(anim.currentTime, -100 * MS_PER_SEC); +}, 'Setting a negative current time with a negative playback rate'); + +promise_test(async t => { + const anim = createDiv(t).animate(null, 100 * MS_PER_SEC); + anim.updatePlaybackRate(0); + await anim.ready; + + // An animation with a playback rate of zero is never in the finished state + // even if currentTime is outside the normal range of [0, effect end]. + anim.currentTime = 200 * MS_PER_SEC; + assert_equals(anim.playState, 'running'); + assert_time_equals_literal(anim.currentTime, 200 * MS_PER_SEC); + await waitForAnimationFrames(2); + assert_time_equals_literal(anim.currentTime, 200 * MS_PER_SEC); + + anim.currentTime = -200 * MS_PER_SEC; + assert_equals(anim.playState, 'running'); + assert_time_equals_literal(anim.currentTime, -200 * MS_PER_SEC); + await waitForAnimationFrames(2); + assert_time_equals_literal(anim.currentTime, -200 * MS_PER_SEC); + +}, 'Setting the current time on an animation with a zero playback rate'); + +</script> +</body> diff --git a/testing/web-platform/tests/web-animations/timing-model/animations/setting-the-playback-rate-of-an-animation.html b/testing/web-platform/tests/web-animations/timing-model/animations/setting-the-playback-rate-of-an-animation.html new file mode 100644 index 0000000000..a1f9e4f3ac --- /dev/null +++ b/testing/web-platform/tests/web-animations/timing-model/animations/setting-the-playback-rate-of-an-animation.html @@ -0,0 +1,110 @@ +<!DOCTYPE html> +<meta charset=utf-8> +<title>Setting the playback rate of an animation</title> +<link rel="help" href="https://drafts.csswg.org/web-animations/#setting-the-playback-rate-of-an-animation"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../../testcommon.js"></script> +<body> +<div id="log"></div> +<script> +'use strict'; + +promise_test(async t => { + const animation = createDiv(t).animate(null, 100 * MS_PER_SEC); + animation.playbackRate = 2; + await animation.ready; + + const previousAnimationCurrentTime = animation.currentTime; + const previousTimelineCurrentTime = animation.timeline.currentTime; + + await waitForAnimationFrames(1); + + const animationCurrentTimeDifference = + animation.currentTime - previousAnimationCurrentTime; + const timelineCurrentTimeDifference = + animation.timeline.currentTime - previousTimelineCurrentTime; + + assert_times_equal( + animationCurrentTimeDifference, + timelineCurrentTimeDifference * animation.playbackRate, + 'The current time should increase two times faster than timeline' + ); +}, 'The playback rate affects the rate of progress of the current time'); + +test(t => { + const animation = createDiv(t).animate(null, 100 * MS_PER_SEC); + animation.currentTime = 50 * MS_PER_SEC; + animation.playbackRate = 2; + assert_time_equals_literal(animation.currentTime, 50 * MS_PER_SEC); +}, 'Setting the playback rate while play-pending preserves the current time'); + +promise_test(async t => { + const animation = createDiv(t).animate(null, 100 * MS_PER_SEC); + animation.currentTime = 50 * MS_PER_SEC; + await animation.ready; + animation.playbackRate = 2; + assert_greater_than_equal(animation.currentTime, 50 * MS_PER_SEC); + assert_less_than(animation.currentTime, 100 * MS_PER_SEC); +}, 'Setting the playback rate while playing preserves the current time'); + +promise_test(async t => { + const animation = createDiv(t).animate(null, 100 * MS_PER_SEC); + animation.currentTime = 50 * MS_PER_SEC; + animation.updatePlaybackRate(2); + animation.playbackRate = 1; + await animation.ready; + assert_equals(animation.playbackRate, 1); +}, 'Setting the playback rate should clear any pending playback rate'); + +promise_test(async t => { + const animation = createDiv(t).animate(null, 100 * MS_PER_SEC); + animation.currentTime = 50 * MS_PER_SEC; + animation.pause(); + await animation.ready; + animation.playbackRate = 2; + // Ensure that the animation remains paused and current time is preserved. + assert_equals(animation.playState, 'paused'); + assert_time_equals_literal(animation.currentTime, 50 * MS_PER_SEC); +}, 'Setting the playback rate while paused preserves the current time and ' + + 'state'); + +promise_test(async t => { + const animation = createDiv(t).animate(null, 100 * MS_PER_SEC); + animation.currentTime = 150 * MS_PER_SEC; + await animation.ready; + animation.playbackRate = 2; + // Ensure that current time is preserved and does not snap to the effect end + // time. + assert_equals(animation.playState, 'finished'); + assert_time_equals_literal(animation.currentTime, 150 * MS_PER_SEC); +}, 'Setting the playback rate while finished preserves the current time'); + +promise_test(async t => { + const animation = createDiv(t).animate(null, 100 * MS_PER_SEC); + animation.currentTime = 150 * MS_PER_SEC; + await animation.ready; + assert_equals(animation.playState, 'finished'); + animation.playbackRate = -1; + // Ensure that current time does not snap to the effect end time and that the + // animation resumes playing. + assert_equals(animation.playState, 'running'); + assert_time_equals_literal(animation.currentTime, 150 * MS_PER_SEC); + await waitForAnimationFrames(2); + assert_less_than(animation.currentTime, 150 * MS_PER_SEC); +}, 'Reversing the playback rate while finished restarts the animation'); + + +promise_test(async t => { + const animation = createDiv(t).animate(null, 100 * MS_PER_SEC); + await animation.ready; + animation.currentTime = 50 * MS_PER_SEC; + animation.playbackRate = 0; + // Ensure that current time does not drift. + assert_equals(animation.playState, 'running'); + await waitForAnimationFrames(2); + assert_time_equals_literal(animation.currentTime, 50 * MS_PER_SEC); +}, 'Setting a zero playback rate while running preserves the current time'); + +</script> +</body> diff --git a/testing/web-platform/tests/web-animations/timing-model/animations/setting-the-start-time-of-an-animation.html b/testing/web-platform/tests/web-animations/timing-model/animations/setting-the-start-time-of-an-animation.html new file mode 100644 index 0000000000..fee3f1e0de --- /dev/null +++ b/testing/web-platform/tests/web-animations/timing-model/animations/setting-the-start-time-of-an-animation.html @@ -0,0 +1,329 @@ +<!DOCTYPE html> +<meta charset=utf-8> +<title>Setting the start time of an animation</title> +<link rel="help" href="https://drafts.csswg.org/web-animations/#setting-the-start-time-of-an-animation"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../../testcommon.js"></script> +<script src="../../resources/timing-override.js"></script> +<body> +<div id="log"></div> +<script> +'use strict'; + +promise_test(async t => { + const animation = + new Animation(new KeyframeEffect(createDiv(t), null, 100 * MS_PER_SEC), + null); + + assert_throws_js(TypeError, () => { + animation.startTime = CSSNumericValue.parse("30%"); + }); + assert_throws_js(TypeError, () => { + animation.startTime = CSSNumericValue.parse("30deg"); + }); + + animation.startTime = 2000; + assert_equals(animation.startTime, 2000, "Set start time using double"); + + animation.startTime = CSSNumericValue.parse("3000"); + assert_equals(animation.startTime, 3000, "Set start time using " + + "CSSNumericValue number value"); + + animation.startTime = CSSNumericValue.parse("4000ms"); + assert_equals(animation.startTime, 4000, "Set start time using " + + "CSSNumericValue milliseconds value"); + + animation.startTime = CSSNumericValue.parse("50s"); + assert_equals(animation.startTime, 50000, "Set start time using " + + "CSSNumericValue seconds value"); +}, 'Validate different value types that can be used to set start time'); + +test(t => { + // It should only be possible to set *either* the start time or the current + // time for an animation that does not have an active timeline. + + const animation = + new Animation(new KeyframeEffect(createDiv(t), null, 100 * MS_PER_SEC), + null); + + assert_equals(animation.currentTime, null, 'Intial current time'); + assert_equals(animation.startTime, null, 'Intial start time'); + + animation.currentTime = 1000; + assert_equals(animation.currentTime, 1000, + 'Setting the current time succeeds'); + assert_equals(animation.startTime, null, + 'Start time remains null after setting current time'); + + animation.startTime = 1000; + assert_equals(animation.startTime, 1000, + 'Setting the start time succeeds'); + assert_equals(animation.currentTime, null, + 'Setting the start time clears the current time'); + + animation.startTime = null; + assert_equals(animation.startTime, null, + 'Setting the start time to an unresolved time succeeds'); + assert_equals(animation.currentTime, null, 'The current time is unaffected'); + +}, 'Setting the start time of an animation without an active timeline'); + +test(t => { + // Setting an unresolved start time on an animation without an active + // timeline should not clear the current time. + + const animation = + new Animation(new KeyframeEffect(createDiv(t), null, 100 * MS_PER_SEC), + null); + + assert_equals(animation.currentTime, null, 'Intial current time'); + assert_equals(animation.startTime, null, 'Intial start time'); + + animation.currentTime = 1000; + assert_equals(animation.currentTime, 1000, + 'Setting the current time succeeds'); + assert_equals(animation.startTime, null, + 'Start time remains null after setting current time'); + + animation.startTime = null; + assert_equals(animation.startTime, null, 'Start time remains unresolved'); + assert_equals(animation.currentTime, 1000, 'Current time is unaffected'); + +}, 'Setting an unresolved start time an animation without an active timeline' + + ' does not clear the current time'); + +test(t => { + const animation = + new Animation(new KeyframeEffect(createDiv(t), null, 100 * MS_PER_SEC), + document.timeline); + + // So long as a hold time is set, querying the current time will return + // the hold time. + + // Since the start time is unresolved at this point, setting the current time + // will set the hold time + animation.currentTime = 1000; + assert_equals(animation.currentTime, 1000, + 'The current time is calculated from the hold time'); + + // If we set the start time, however, we should clear the hold time. + animation.startTime = document.timeline.currentTime - 2000; + assert_time_equals_literal(animation.currentTime, 2000, + 'The current time is calculated from the start' + + ' time, not the hold time'); + + // Sanity check + assert_equals(animation.playState, 'running', + 'Animation reports it is running after setting a resolved' + + ' start time'); +}, 'Setting the start time clears the hold time'); + +test(t => { + const animation = + new Animation(new KeyframeEffect(createDiv(t), null, 100 * MS_PER_SEC), + document.timeline); + + // Set up a running animation (i.e. both start time and current time + // are resolved). + animation.startTime = document.timeline.currentTime - 1000; + assert_equals(animation.playState, 'running'); + assert_time_equals_literal(animation.currentTime, 1000, + 'Current time is resolved for a running animation'); + + // Clear start time + animation.startTime = null; + assert_time_equals_literal(animation.currentTime, 1000, + 'Hold time is set after start time is made' + + ' unresolved'); + assert_equals(animation.playState, 'paused', + 'Animation reports it is paused after setting an unresolved' + + ' start time'); +}, 'Setting an unresolved start time sets the hold time'); + +promise_test(async t => { + const animation = + new Animation(new KeyframeEffect(createDiv(t), null, 100 * MS_PER_SEC), + document.timeline); + + let readyPromiseCallbackCalled = false; + animation.ready.then(() => { readyPromiseCallbackCalled = true; } ); + + // Put the animation in the play-pending state + animation.play(); + + // Sanity check + assert_true(animation.pending && animation.playState === 'running', + 'Animation is in play-pending state'); + + // Setting the start time should resolve the 'ready' promise, i.e. + // it should schedule a microtask to run the promise callbacks. + animation.startTime = document.timeline.currentTime; + assert_false(readyPromiseCallbackCalled, + 'Ready promise callback is not called synchronously'); + + // If we schedule another microtask then it should run immediately after + // the ready promise resolution microtask. + await Promise.resolve(); + assert_true(readyPromiseCallbackCalled, + 'Ready promise callback called after setting startTime'); +}, 'Setting the start time resolves a pending ready promise'); + +promise_test(async t => { + const animation = + new Animation(new KeyframeEffect(createDiv(t), null, 100 * MS_PER_SEC), + document.timeline); + + let readyPromiseCallbackCalled = false; + animation.ready.then(() => { readyPromiseCallbackCalled = true; } ); + + // Put the animation in the pause-pending state + animation.startTime = document.timeline.currentTime; + animation.pause(); + + // Sanity check + assert_true(animation.pending && animation.playState === 'paused', + 'Animation is in pause-pending state'); + + // Setting the start time should resolve the 'ready' promise although + // the resolution callbacks when be run in a separate microtask. + animation.startTime = null; + assert_false(readyPromiseCallbackCalled, + 'Ready promise callback is not called synchronously'); + + await Promise.resolve(); + assert_true(readyPromiseCallbackCalled, + 'Ready promise callback called after setting startTime'); +}, 'Setting the start time resolves a pending pause task'); + +promise_test(async t => { + const animation = + new Animation(new KeyframeEffect(createDiv(t), null, 100 * MS_PER_SEC), + document.timeline); + + // Put the animation in the play-pending state + animation.play(); + + // Sanity check + assert_true(animation.pending, 'Animation is pending'); + assert_equals(animation.playState, 'running', + 'Animation is play-pending'); + assert_equals(animation.startTime, null, 'Start time is null'); + + // Even though the startTime is already null, setting it to the same value + // should still cancel the pending task. + animation.startTime = null; + assert_false(animation.pending, 'Animation is no longer pending'); + assert_equals(animation.playState, 'paused', 'Animation is paused'); +}, 'Setting an unresolved start time on a play-pending animation makes it' + + ' paused'); + +promise_test(async t => { + const animation = + new Animation(new KeyframeEffect(createDiv(t), null, 100 * MS_PER_SEC), + document.timeline); + + // Set start time such that the current time is past the end time + animation.startTime = document.timeline.currentTime + - 110 * MS_PER_SEC; + assert_equals(animation.playState, 'finished', + 'Seeked to finished state using the startTime'); + + // If the 'did seek' flag is true, the current time should be greater than + // the effect end. + assert_greater_than(animation.currentTime, + animation.effect.getComputedTiming().endTime, + 'Setting the start time updated the finished state with' + + ' the \'did seek\' flag set to true'); + + // Furthermore, that time should persist if we have correctly updated + // the hold time + const finishedCurrentTime = animation.currentTime; + await waitForAnimationFrames(1); + assert_equals(animation.currentTime, finishedCurrentTime, + 'Current time does not change after seeking past the effect' + + ' end time by setting the current time'); +}, 'Setting the start time updates the finished state'); + +promise_test(async t => { + const anim = createDiv(t).animate(null, 100 * MS_PER_SEC); + + // We should be play-pending now + assert_true(anim.pending); + assert_equals(anim.playState, 'running'); + + // Apply a pending playback rate + anim.updatePlaybackRate(2); + assert_equals(anim.playbackRate, 1); + assert_true(anim.pending); + + // Setting the start time should apply the pending playback rate + anim.startTime = anim.timeline.currentTime - 25 * MS_PER_SEC; + assert_equals(anim.playbackRate, 2); + assert_false(anim.pending); + + // Sanity check that the start time is preserved and current time is + // calculated using the new playback rate + assert_times_equal(anim.startTime, + anim.timeline.currentTime - 25 * MS_PER_SEC); + assert_time_equals_literal(anim.currentTime, 50 * MS_PER_SEC); +}, 'Setting the start time of a play-pending animation applies a pending playback rate'); + +promise_test(async t => { + const anim = createDiv(t).animate(null, 100 * MS_PER_SEC); + await anim.ready; + + // We should be running now + assert_false(anim.pending); + assert_equals(anim.playState, 'running'); + + // Apply a pending playback rate + anim.updatePlaybackRate(2); + assert_equals(anim.playbackRate, 1); + assert_true(anim.pending); + + // Setting the start time should apply the pending playback rate + anim.startTime = anim.timeline.currentTime - 25 * MS_PER_SEC; + assert_equals(anim.playbackRate, 2); + assert_false(anim.pending); + + // Sanity check that the start time is preserved and current time is + // calculated using the new playback rate + assert_times_equal(anim.startTime, + anim.timeline.currentTime - 25 * MS_PER_SEC); + assert_time_equals_literal(parseInt(anim.currentTime.toPrecision(5), 10), 50 * MS_PER_SEC); +}, 'Setting the start time of a playing animation applies a pending playback rate'); + +promise_test(async t => { + const anim = createDiv(t).animate(null, 100 * MS_PER_SEC); + await anim.ready; + assert_equals(anim.playState, 'running'); + + // Setting the start time updates the finished state. The hold time is not + // constrained by the effect end time. + anim.startTime = -200 * MS_PER_SEC; + assert_equals(anim.playState, 'finished'); + + assert_times_equal(anim.currentTime, + document.timeline.currentTime + 200 * MS_PER_SEC); +}, 'Setting the start time on a running animation updates the play state'); + +promise_test(async t => { + const anim = createDiv(t).animate(null, 100 * MS_PER_SEC); + await anim.ready; + + // Setting the start time updates the finished state. The hold time is not + // constrained by the normal range of the animation time. + anim.currentTime = 100 * MS_PER_SEC; + assert_equals(anim.playState, 'finished'); + anim.playbackRate = -1; + assert_equals(anim.playState, 'running'); + anim.startTime = -200 * MS_PER_SEC; + assert_equals(anim.playState, 'finished'); + assert_times_equal(anim.currentTime, + -document.timeline.currentTime - 200 * MS_PER_SEC); +}, 'Setting the start time on a reverse running animation updates the play ' + + 'state'); + +</script> +</body> diff --git a/testing/web-platform/tests/web-animations/timing-model/animations/setting-the-target-effect-of-an-animation.html b/testing/web-platform/tests/web-animations/timing-model/animations/setting-the-target-effect-of-an-animation.html new file mode 100644 index 0000000000..60ea1850fc --- /dev/null +++ b/testing/web-platform/tests/web-animations/timing-model/animations/setting-the-target-effect-of-an-animation.html @@ -0,0 +1,129 @@ +<!DOCTYPE html> +<meta charset=utf-8> +<title>Setting the target effect of an animation</title> +<link rel='help' href='https://drafts.csswg.org/web-animations/#setting-the-target-effect'> +<script src='/resources/testharness.js'></script> +<script src='/resources/testharnessreport.js'></script> +<script src='../../testcommon.js'></script> +<body> +<div id='log'></div> +<script> +'use strict'; + +promise_test(t => { + const anim = createDiv(t).animate({ marginLeft: [ '0px', '100px' ] }, + 100 * MS_PER_SEC); + assert_true(anim.pending); + + const originalReadyPromise = anim.ready.catch(err => { + assert_unreached('Original ready promise should not be rejected'); + }); + + anim.effect = null; + assert_equals(anim.playState, 'finished'); + assert_true(anim.pending); + + return originalReadyPromise; +}, 'If new effect is null and old effect is not null the animation becomes' + + ' finish-pending'); + +promise_test(async t => { + const anim = new Animation(); + anim.pause(); + assert_true(anim.pending); + + anim.effect = new KeyframeEffect(createDiv(t), + { marginLeft: [ '0px', '100px' ] }, + 100 * MS_PER_SEC); + assert_true(anim.pending); + await anim.ready; + + assert_false(anim.pending); + assert_equals(anim.playState, 'paused'); +}, 'If animation has a pending pause task, reschedule that task to run ' + + 'as soon as animation is ready.'); + +promise_test(async t => { + const anim = new Animation(); + anim.play(); + assert_true(anim.pending); + + anim.effect = new KeyframeEffect(createDiv(t), + { marginLeft: [ '0px', '100px' ] }, + 100 * MS_PER_SEC); + assert_true(anim.pending); + await anim.ready; + + assert_false(anim.pending); + assert_equals(anim.playState, 'running'); +}, 'If animation has a pending play task, reschedule that task to run ' + + 'as soon as animation is ready to play new effect.'); + +promise_test(async t => { + const anim = createDiv(t).animate({ marginLeft: [ '0px', '100px' ] }, + 100 * MS_PER_SEC); + assert_equals(anim.playState, 'running'); + assert_true(anim.pending); + + const originalEffect = anim.effect; + const originalReadyPromise = anim.ready; + + anim.effect = null; + assert_equals(anim.playState, 'finished'); + assert_true(anim.pending); + + anim.effect = originalEffect; + assert_equals(anim.playState, 'running'); + assert_true(anim.pending); + + await originalReadyPromise; + + assert_equals(anim.playState, 'running'); + assert_false(anim.pending); +}, 'The pending play task should be rescheduled even after temporarily setting' + + ' the effect to null'); + +promise_test(async t => { + const animA = createDiv(t).animate({ marginLeft: [ '0px', '100px' ] }, + 100 * MS_PER_SEC); + const animB = new Animation(); + + await animA.ready; + + animB.effect = animA.effect; + assert_equals(animA.effect, null); + assert_equals(animA.playState, 'finished'); +}, 'When setting the effect of an animation to the effect of an existing ' + + 'animation, the existing animation\'s target effect should be set to null.'); + +test(t => { + const animA = createDiv(t).animate({ marginLeft: [ '0px', '100px' ] }, + 100 * MS_PER_SEC); + const animB = new Animation(); + const effect = animA.effect; + animA.currentTime = 50 * MS_PER_SEC; + animB.currentTime = 20 * MS_PER_SEC; + assert_equals(effect.getComputedTiming().progress, 0.5, + 'Original timing comes from first animation'); + animB.effect = effect; + assert_equals(effect.getComputedTiming().progress, 0.2, + 'After setting the effect on a different animation, ' + + 'it uses the new animation\'s timing'); +}, 'After setting the target effect of animation to the target effect of an ' + + 'existing animation, the target effect\'s timing is updated to reflect ' + + 'the current time of the new animation.'); + +promise_test(async t => { + const anim = createDiv(t).animate(null, 100 * MS_PER_SEC); + anim.updatePlaybackRate(2); + assert_equals(anim.playbackRate, 1); + + anim.effect = null; + await anim.ready; + + assert_equals(anim.playbackRate, 2); +}, 'Setting the target effect to null causes a pending playback rate to be' + + ' applied'); + +</script> +</body> diff --git a/testing/web-platform/tests/web-animations/timing-model/animations/setting-the-timeline-of-an-animation.html b/testing/web-platform/tests/web-animations/timing-model/animations/setting-the-timeline-of-an-animation.html new file mode 100644 index 0000000000..d4f802152c --- /dev/null +++ b/testing/web-platform/tests/web-animations/timing-model/animations/setting-the-timeline-of-an-animation.html @@ -0,0 +1,255 @@ +<!DOCTYPE html> +<meta charset=utf-8> +<title>Setting the timeline of an animation</title> +<link rel="help" href="https://drafts.csswg.org/web-animations/#setting-the-timeline"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../../testcommon.js"></script> +<body> +<div id="log"></div> +<script> +'use strict'; + +// --------------------------------------------------------------------- +// +// Tests from no timeline to timeline +// +// --------------------------------------------------------------------- + +test(t => { + const animation = + new Animation(new KeyframeEffect(createDiv(t), null, 100 * MS_PER_SEC), + null); + animation.currentTime = 50 * MS_PER_SEC; + assert_equals(animation.playState, 'paused'); + + animation.timeline = document.timeline; + + assert_equals(animation.playState, 'paused'); + assert_time_equals_literal(animation.currentTime, 50 * MS_PER_SEC); +}, 'After setting timeline on paused animation it is still paused'); + +test(t => { + const animation = + new Animation(new KeyframeEffect(createDiv(t), null, 100 * MS_PER_SEC), + null); + animation.currentTime = 200 * MS_PER_SEC; + assert_equals(animation.playState, 'paused'); + + animation.timeline = document.timeline; + + assert_equals(animation.playState, 'paused'); + assert_time_equals_literal(animation.currentTime, 200 * MS_PER_SEC); +}, 'After setting timeline on animation paused outside active interval' + + ' it is still paused'); + +test(t => { + const animation = + new Animation(new KeyframeEffect(createDiv(t), null, 100 * MS_PER_SEC), + null); + assert_equals(animation.playState, 'idle'); + + animation.timeline = document.timeline; + + assert_equals(animation.playState, 'idle'); +}, 'After setting timeline on an idle animation without a start time' + + ' it is still idle'); + +test(t => { + const animation = + new Animation(new KeyframeEffect(createDiv(t), null, 100 * MS_PER_SEC), + null); + animation.startTime = document.timeline.currentTime; + assert_equals(animation.playState, 'running'); + + animation.timeline = document.timeline; + + assert_equals(animation.playState, 'running'); +}, 'After transitioning from a null timeline on an animation with a start time' + + ' it is still running'); + +test(t => { + const animation = + new Animation(new KeyframeEffect(createDiv(t), null, 100 * MS_PER_SEC), + null); + animation.startTime = document.timeline.currentTime - 200 * MS_PER_SEC; + assert_equals(animation.playState, 'running'); + + animation.timeline = document.timeline; + + assert_equals(animation.playState, 'finished'); +}, 'After transitioning from a null timeline on an animation with a ' + + 'sufficiently ancient start time it is finished'); + +promise_test(async t => { + const animation = + new Animation(new KeyframeEffect(createDiv(t), null, 100 * MS_PER_SEC), + null); + animation.play(); + assert_true(animation.pending && animation.playState === 'running', + 'Animation is initially play-pending'); + + animation.timeline = document.timeline; + + assert_true(animation.pending && animation.playState === 'running', + 'Animation is still play-pending after setting timeline'); + + await animation.ready; + assert_true(!animation.pending && animation.playState === 'running', + 'Animation plays after it finishes pending'); +}, 'After setting timeline on a play-pending animation it begins playing' + + ' after pending'); + +promise_test(async t => { + const animation = + new Animation(new KeyframeEffect(createDiv(t), null, 100 * MS_PER_SEC), + null); + animation.startTime = document.timeline.currentTime; + animation.pause(); + animation.timeline = null; + assert_true(animation.pending && animation.playState === 'paused', + 'Animation is initially pause-pending'); + + animation.timeline = document.timeline; + + assert_true(animation.pending && animation.playState === 'paused', + 'Animation is still pause-pending after setting timeline'); + + await animation.ready; + assert_true(!animation.pending && animation.playState === 'paused', + 'Animation pauses after it finishes pending'); +}, 'After setting timeline on a pause-pending animation it becomes paused' + + ' after pending'); + +// --------------------------------------------------------------------- +// +// Tests from timeline to no timeline +// +// --------------------------------------------------------------------- + +test(t => { + const animation = + new Animation(new KeyframeEffect(createDiv(t), null, 100 * MS_PER_SEC), + document.timeline); + animation.currentTime = 50 * MS_PER_SEC; + assert_false(animation.pending); + assert_equals(animation.playState, 'paused'); + + animation.timeline = null; + + assert_false(animation.pending); + assert_equals(animation.playState, 'paused'); + assert_time_equals_literal(animation.currentTime, 50 * MS_PER_SEC); +}, 'After clearing timeline on paused animation it is still paused'); + +test(t => { + const animation = + new Animation(new KeyframeEffect(createDiv(t), null, 100 * MS_PER_SEC), + document.timeline); + const initialStartTime = document.timeline.currentTime - 200 * MS_PER_SEC; + animation.startTime = initialStartTime; + assert_equals(animation.playState, 'finished'); + + animation.timeline = null; + + assert_equals(animation.playState, 'running'); + assert_times_equal(animation.startTime, initialStartTime); +}, 'After clearing timeline on finished animation it is running'); + +test(t => { + const animation = + new Animation(new KeyframeEffect(createDiv(t), null, 100 * MS_PER_SEC), + document.timeline); + const initialStartTime = document.timeline.currentTime - 50 * MS_PER_SEC; + animation.startTime = initialStartTime; + assert_equals(animation.playState, 'running'); + + animation.timeline = null; + + assert_equals(animation.playState, 'running'); + assert_times_equal(animation.startTime, initialStartTime); +}, 'After clearing timeline on running animation it is still running'); + +test(t => { + const animation = + new Animation(new KeyframeEffect(createDiv(t), null, 100 * MS_PER_SEC), + document.timeline); + assert_equals(animation.playState, 'idle'); + + animation.timeline = null; + + assert_equals(animation.playState, 'idle'); + assert_equals(animation.startTime, null); +}, 'After clearing timeline on idle animation it is still idle'); + +test(t => { + const animation = createDiv(t).animate(null, 100 * MS_PER_SEC); + assert_true(animation.pending && animation.playState === 'running'); + + animation.timeline = null; + + assert_true(animation.pending && animation.playState === 'running'); +}, 'After clearing timeline on play-pending animation it is still pending'); + +promise_test(async t => { + const animation = createDiv(t).animate(null, 100 * MS_PER_SEC); + assert_true(animation.pending && animation.playState === 'running'); + + animation.timeline = null; + animation.timeline = document.timeline; + + assert_true(animation.pending && animation.playState === 'running'); + await animation.ready; + assert_true(!animation.pending && animation.playState === 'running'); +}, 'After clearing and re-setting timeline on play-pending animation it' + + ' begins to play'); + +test(t => { + const animation = + new Animation(new KeyframeEffect(createDiv(t), null, 100 * MS_PER_SEC), + document.timeline); + animation.startTime = document.timeline.currentTime; + animation.pause(); + assert_true(animation.pending && animation.playState === 'paused'); + + animation.timeline = null; + + assert_true(animation.pending && animation.playState === 'paused'); +}, 'After clearing timeline on a pause-pending animation it is still pending'); + +promise_test(async t => { + const animation = + new Animation(new KeyframeEffect(createDiv(t), null, 100 * MS_PER_SEC), + document.timeline); + animation.startTime = document.timeline.currentTime; + animation.pause(); + assert_true(animation.pending && animation.playState === 'paused'); + + animation.timeline = null; + animation.timeline = document.timeline; + + assert_true(animation.pending && animation.playState === 'paused'); + await animation.ready; + assert_true(!animation.pending && animation.playState === 'paused'); +}, 'After clearing and re-setting timeline on a pause-pending animation it' + + ' completes pausing'); + +promise_test(async t => { + const animation = + new Animation(new KeyframeEffect(createDiv(t), null, 100 * MS_PER_SEC), + document.timeline); + const initialStartTime = document.timeline.currentTime - 50 * MS_PER_SEC; + animation.startTime = initialStartTime; + animation.pause(); + animation.play(); + + animation.timeline = null; + animation.timeline = document.timeline; + + await animation.ready; + assert_times_equal(animation.startTime, initialStartTime); +}, 'After clearing and re-setting timeline on an animation in the middle of' + + ' an aborted pause, it continues playing using the same start time'); + +</script> +</body> diff --git a/testing/web-platform/tests/web-animations/timing-model/animations/sync-start-times-ref.html b/testing/web-platform/tests/web-animations/timing-model/animations/sync-start-times-ref.html new file mode 100644 index 0000000000..fc843a132f --- /dev/null +++ b/testing/web-platform/tests/web-animations/timing-model/animations/sync-start-times-ref.html @@ -0,0 +1,20 @@ + +<!DOCTYPE html> +<meta charset="UTF-8"> +<title>Reference for sync start times</title> +<style> + #notes { + position: absolute; + left: 0px; + top: 100px; + } +</style> + +<body> + <p id="notes"> + This test creates a pair of animations, starts the first animation and then + syncs the second animation to align with the first. The test passes if the + box associated with the first animation is completely occluded by the + second. + </p> +</body> diff --git a/testing/web-platform/tests/web-animations/timing-model/animations/sync-start-times.html b/testing/web-platform/tests/web-animations/timing-model/animations/sync-start-times.html new file mode 100644 index 0000000000..e9ef6762ea --- /dev/null +++ b/testing/web-platform/tests/web-animations/timing-model/animations/sync-start-times.html @@ -0,0 +1,72 @@ + +<!DOCTYPE html> +<html class="reftest-wait"> +<meta charset="UTF-8"> +<title>sync start times</title> +<link rel="match" href="sync-start-times-ref.html"> +<script src="/common/reftest-wait.js"></script> +<style> + #box-1, #box-2 { + border: 1px solid white; + height: 40px; + left: 40px; + position: absolute; + top: 40px; + width: 40px; + /* To ensure Chrome to render the two boxes (one actively + animating and the other not) with the same subpixel offset + when there is subpixel translation during animation. */ + will-change: transform; + } + #box-1 { + background: blue; + z-index: 1; + } + #box-2 { + background: white; + z-index: 2; + } + #notes { + position: absolute; + left: 0px; + top: 100px; + } +</style> + +<body> + <div id="box-1"></div> + <div id="box-2"></div> + <p id="notes"> + This test creates a pair of animations, starts the first animation and then + syncs the second animation to align with the first. The test passes if the + box associated with the first animation is completely occluded by the + second. + </p> +</body> +<script> + onload = function() { + function createAnimation(elementId) { + const elem = document.getElementById(elementId); + const keyframes = [ + { transform: 'translateX(0px)' }, + { transform: 'translateX(200px)' } + ]; + const anim = elem.animate(keyframes, { duration: 1000 }); + anim.pause(); + return anim; + }; + + const anim1 = createAnimation('box-1'); + const anim2 = createAnimation('box-2'); + + anim1.currentTime = 500; + anim1.play(); + + anim1.ready.then(() => { + anim2.startTime = anim1.startTime; + requestAnimationFrame(() => { + takeScreenshot(); + }); + }); + }; +</script> diff --git a/testing/web-platform/tests/web-animations/timing-model/animations/the-current-time-of-an-animation.html b/testing/web-platform/tests/web-animations/timing-model/animations/the-current-time-of-an-animation.html new file mode 100644 index 0000000000..77a6b716d2 --- /dev/null +++ b/testing/web-platform/tests/web-animations/timing-model/animations/the-current-time-of-an-animation.html @@ -0,0 +1,75 @@ +<!DOCTYPE html> +<meta charset=utf-8> +<title>The current time of an animation</title> +<link rel="help" href="https://drafts.csswg.org/web-animations/#the-current-time-of-an-animation"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../../testcommon.js"></script> +<script src="../../resources/timing-override.js"></script> +<body> +<div id="log"></div> +<script> +'use strict'; + +test(t => { + const animation = + new Animation(new KeyframeEffect(createDiv(t), null, 100 * MS_PER_SEC), + document.timeline); + + animation.play(); + assert_equals(animation.currentTime, 0, + 'Current time returns the hold time set when entering the play-pending ' + + 'state'); +}, 'The current time returns the hold time when set'); + +promise_test(async t => { + const animation = + new Animation(new KeyframeEffect(createDiv(t), null, 100 * MS_PER_SEC), + null); + + await animation.ready; + assert_equals(animation.currentTime, null); +}, 'The current time is unresolved when there is no associated timeline ' + + '(and no hold time is set)'); + +// FIXME: Test that the current time is unresolved when we have an inactive +// timeline if we find a way of creating an inactive timeline! + +test(t => { + const animation = + new Animation(new KeyframeEffect(createDiv(t), null, 100 * MS_PER_SEC), + document.timeline); + + animation.startTime = null; + assert_equals(animation.currentTime, null); +}, 'The current time is unresolved when the start time is unresolved ' + + '(and no hold time is set)'); + +test(t => { + const animation = + new Animation(new KeyframeEffect(createDiv(t), null, 100 * MS_PER_SEC), + document.timeline); + + animation.playbackRate = 2; + animation.startTime = document.timeline.currentTime - 25 * MS_PER_SEC; + + const timelineTime = document.timeline.currentTime; + const startTime = animation.startTime; + const playbackRate = animation.playbackRate; + assert_times_equal(animation.currentTime, + (timelineTime - startTime) * playbackRate, + 'Animation has a unresolved start time'); +}, 'The current time is calculated from the timeline time, start time and ' + + 'playback rate'); + +promise_test(async t => { + const animation = createDiv(t).animate(null, 100 * MS_PER_SEC); + animation.playbackRate = 0; + + await animation.ready; + await waitForAnimationFrames(1); + assert_time_equals_literal(animation.currentTime, 0); +}, 'The current time does not progress if playback rate is 0'); + +</script> +</body> diff --git a/testing/web-platform/tests/web-animations/timing-model/animations/update-playback-rate-fast-ref.html b/testing/web-platform/tests/web-animations/timing-model/animations/update-playback-rate-fast-ref.html new file mode 100644 index 0000000000..e996815da8 --- /dev/null +++ b/testing/web-platform/tests/web-animations/timing-model/animations/update-playback-rate-fast-ref.html @@ -0,0 +1,17 @@ +<!DOCTYPE html> +<title>Reference for update playback rate zero</title> +<style> + #box { + background: green; + height: 40px; + width: 40px; + } +</style> +<body> + <div id="box"></div> + <p id="notes"> + This test creates a running animation and changes its playback rate + part way through. If the box remains red when the screenshot is captured + the test fails. + </p> +</body> diff --git a/testing/web-platform/tests/web-animations/timing-model/animations/update-playback-rate-fast.html b/testing/web-platform/tests/web-animations/timing-model/animations/update-playback-rate-fast.html new file mode 100644 index 0000000000..c3df1c1bf0 --- /dev/null +++ b/testing/web-platform/tests/web-animations/timing-model/animations/update-playback-rate-fast.html @@ -0,0 +1,52 @@ + +<!DOCTYPE html> +<html class="reftest-wait"> +<meta charset="UTF-8"> +<title>Update playback rate zero</title> +<link rel="match" href="update-playback-rate-fast-ref.html"> +<script src="/common/reftest-wait.js"></script> +<script src="../../testcommon.js"></script> +<style> + #box { + background: green; + height: 40px; + width: 40px; + } +</style> +<body> + <div id="box"></div> + <p id="notes"> + This test creates a running animation and changes its playback rate + part way through. If the box remains red when the screenshot is captured + the test fails. + </p> +</body> +<script> + onload = async function() { + const box = document.getElementById('box'); + const duration = 2000; + const anim = + box.animate({ bacground: [ 'red', 'green' ] }, + { duration: duration, easing: 'steps(2, jump-none)' }); + anim.ready.then(() => { + const startTime = anim.timeline.currentTime; + waitForAnimationFrames(2).then(() => { + anim.updatePlaybackRate(2); + anim.ready.then(() => { + const updateTime = anim.timeline.currentTime; + const baseProgress = (updateTime - startTime) / duration; + const checkIfDone = () => { + const progress = + 2 * (anim.timeline.currentTime - updateTime) / duration + + baseProgress; + if (progress > 0.5) + takeScreenshot(); + else + requestAnimationFrame(checkIfDone); + }; + requestAnimationFrame(checkIfDone); + }); + }); + }); + }; +</script> diff --git a/testing/web-platform/tests/web-animations/timing-model/animations/update-playback-rate-zero-ref.html b/testing/web-platform/tests/web-animations/timing-model/animations/update-playback-rate-zero-ref.html new file mode 100644 index 0000000000..399fd5ce7d --- /dev/null +++ b/testing/web-platform/tests/web-animations/timing-model/animations/update-playback-rate-zero-ref.html @@ -0,0 +1,16 @@ +<!DOCTYPE html> +<title>Reference for update playback rate zero</title> +<style> + #box { + background: green; + height: 40px; + width: 40px; + } +</style> +<body> + <div id="box"></div> + <p id="notes"> + This test creates a running animation and halts its playback rate + part way through. If the box transitions to red the test fails. + </p> +</body> diff --git a/testing/web-platform/tests/web-animations/timing-model/animations/update-playback-rate-zero.html b/testing/web-platform/tests/web-animations/timing-model/animations/update-playback-rate-zero.html new file mode 100644 index 0000000000..db1544ee92 --- /dev/null +++ b/testing/web-platform/tests/web-animations/timing-model/animations/update-playback-rate-zero.html @@ -0,0 +1,46 @@ + +<!DOCTYPE html> +<html class="reftest-wait"> +<meta charset="UTF-8"> +<title>Update playback rate zero</title> +<link rel="match" href="update-playback-rate-zero-ref.html"> +<script src="/common/reftest-wait.js"></script> +<script src="../../testcommon.js"></script> +<style> + #box { + background: green; + height: 40px; + width: 40px; + } +</style> +<body> + <div id="box"></div> + <p id="notes"> + This test creates a running animation and halts its playback rate + part way through. If the box transitions to red the test fails. + </p> +</body> +<script> + onload = async function() { + const box = document.getElementById('box'); + const duration = 2000; + const anim = + box.animate({ bacground: [ 'green', 'red' ] }, + { duration: duration, easing: 'steps(2, jump-none)' }); + anim.ready.then(() => { + const startTime = anim.timeline.currentTime; + waitForAnimationFrames(2).then(() => { + anim.updatePlaybackRate(0); + anim.ready.then(() => { + const checkIfDone = () => { + if (anim.timeline.currentTime - startTime > duration / 2) + takeScreenshot(); + else + requestAnimationFrame(checkIfDone); + }; + requestAnimationFrame(checkIfDone); + }); + }); + }); + }; +</script> diff --git a/testing/web-platform/tests/web-animations/timing-model/animations/updating-the-finished-state.html b/testing/web-platform/tests/web-animations/timing-model/animations/updating-the-finished-state.html new file mode 100644 index 0000000000..4d3cc7950b --- /dev/null +++ b/testing/web-platform/tests/web-animations/timing-model/animations/updating-the-finished-state.html @@ -0,0 +1,457 @@ +<!DOCTYPE html> +<meta charset=utf-8> +<title>Updating the finished state</title> +<meta name="timeout" content="long"> +<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="../../testcommon.js"></script> +<body> +<div id="log"></div> +<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 = createDiv(t).animate(null, 100 * MS_PER_SEC); + + // 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; + + // Seek to 1ms before the target end and then wait 1ms + anim.currentTime = 100 * MS_PER_SEC - 1; + await waitForAnimationFramesWithDelay(1); + + assert_equals(anim.currentTime, 100 * MS_PER_SEC, + '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 = createDiv(t).animate(null, 100 * MS_PER_SEC); + + await anim.ready; + + anim.currentTime = 200 * MS_PER_SEC; + await waitForNextFrame(); + + assert_equals(anim.currentTime, 200 * MS_PER_SEC, + 'Hold time is set so current time should NOT change'); +}, 'Updating the finished state when seeking past end'); + +// Test current time == target end +// +// We can't really write a test for current time == target end with +// did seek = false since that would imply setting up an animation where +// the next animation frame time happens to exactly align with the target end. +// +// Fortunately, we don't need to test that case since even if the implementation +// fails to set the hold time on such a tick, it should be mostly unobservable +// (on the subsequent tick the hold time will be set to the same value anyway). + +// Did seek = true +promise_test(async t => { + const anim = createDiv(t).animate(null, 100 * MS_PER_SEC); + await anim.ready; + + anim.currentTime = 100 * MS_PER_SEC; + await waitForNextFrame(); + + assert_equals(anim.currentTime, 100 * MS_PER_SEC, + '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 = createDiv(t).animate(null, 100 * MS_PER_SEC); + 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; + await waitForAnimationFramesWithDelay(1); + + assert_equals(anim.currentTime, 0 * MS_PER_SEC, + '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 = createDiv(t).animate(null, 100 * MS_PER_SEC); + anim.playbackRate = -1; + anim.play(); + + await anim.ready; + + anim.currentTime = -100 * MS_PER_SEC; + await waitForNextFrame(); + + assert_equals(anim.currentTime, -100 * MS_PER_SEC, + 'Hold time is set so current time should NOT change'); +}, 'Updating the finished state when seeking a reversed animation past zero'); + +// As before, it's difficult to test current time == 0 for did seek = false but +// it doesn't really matter. + +// Did seek = true +promise_test(async t => { + const anim = createDiv(t).animate(null, 100 * MS_PER_SEC); + anim.playbackRate = -1; + anim.play(); + await anim.ready; + + anim.currentTime = 0; + 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 = createDiv(t).animate(null, 100 * MS_PER_SEC); + + // 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, 100 * MS_PER_SEC, + '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, + }); + await waitForNextFrame(); + + assert_greater_than(anim.currentTime, 100 * MS_PER_SEC, + '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 = createDiv(t).animate(null, 100 * MS_PER_SEC); + anim.finish(); + await anim.ready; + + anim.currentTime = 50 * MS_PER_SEC; + // 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, 50 * MS_PER_SEC, 'Start time is updated'); + await waitForNextFrame(); + + assert_greater_than(anim.currentTime, 50 * MS_PER_SEC, + '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 = createDiv(t).animate(null, 100 * MS_PER_SEC); + anim.playbackRate = -1; + await anim.ready; + + anim.currentTime = 50 * MS_PER_SEC; + assert_equals(anim.currentTime, 50 * MS_PER_SEC, 'Start time is updated'); + await waitForNextFrame(); + + assert_less_than(anim.currentTime, 50 * MS_PER_SEC, + '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 = createDiv(t).animate(null, 100 * MS_PER_SEC); + anim.playbackRate = 0; + await anim.ready; + + anim.currentTime = -100 * MS_PER_SEC; + await waitForNextFrame(); + + assert_equals(anim.currentTime, -100 * MS_PER_SEC, + '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 = createDiv(t).animate(null, 100 * MS_PER_SEC); + anim.playbackRate = 0; + await anim.ready; + + anim.currentTime = 50 * MS_PER_SEC; + await waitForNextFrame(); + + assert_equals(anim.currentTime, 50 * MS_PER_SEC, + '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 = createDiv(t).animate(null, 100 * MS_PER_SEC); + anim.playbackRate = 0; + await anim.ready; + + anim.currentTime = 200 * MS_PER_SEC; + await waitForNextFrame(); + + assert_equals(anim.currentTime, 200 * MS_PER_SEC, + '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 = createDiv(t).animate(null, 100 * MS_PER_SEC); + anim.cancel(); + // Trigger a change that will cause the "update the finished state" + // procedure to run. + anim.effect.updateTiming({ duration: 200 * MS_PER_SEC }); + 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 + +test(t => { + const anim = createDiv(t).animate(null, 100 * MS_PER_SEC); + anim.cancel(); + anim.currentTime = 75 * MS_PER_SEC; + 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: 50 * MS_PER_SEC }); + assert_equals(anim.currentTime, 75 * MS_PER_SEC, + '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 = createDiv(t).animate(null, 100 * MS_PER_SEC); + 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 = 150 * MS_PER_SEC; + // Trigger a change that will cause the "update the finished state" + // procedure to run (did seek = false). + anim.effect.updateTiming({ duration: 200 * MS_PER_SEC }); + await waitForAnimationFrames(1); + + assert_equals(anim.currentTime, 150 * MS_PER_SEC, + '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 +test(t => { + const anim = createDiv(t).animate(null, 100 * MS_PER_SEC); + anim.cancel(); + anim.currentTime = 150 * MS_PER_SEC; + // Trigger a change that will cause the "update the finished state" + // procedure to run. + anim.currentTime = 50 * MS_PER_SEC; + assert_equals(anim.currentTime, 50 * MS_PER_SEC, + '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 = createDiv(t).animate(null, 1); + 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 = 1; + animation.currentTime = 0; + animation.pause(); + 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 = createDiv(t).animate(null, 1); + await animation.ready; + + return waitForFinishEventAndPromise(animation); +}, 'Finish notification steps run when the animation completes normally'); + +promise_test(async t => { + const effect = new KeyframeEffect(null, null, 1); + const animation = new Animation(effect, document.timeline); + animation.play(); + await animation.ready; + + return waitForFinishEventAndPromise(animation); +}, 'Finish notification steps run when an animation without a target' + + ' effect completes normally'); + +promise_test(async t => { + const animation = createDiv(t).animate(null, 1); + await animation.ready; + + animation.currentTime = 10; + return waitForFinishEventAndPromise(animation); +}, 'Finish notification steps run when the animation seeks past finish'); + +promise_test(async t => { + const animation = createDiv(t).animate(null, 1); + 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 = createDiv(t).animate(null, 1); + 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 = createDiv(t).animate(null, 1); + const initialFinishedPromise = animation.finished; + await animation.finished; + + animation.play(); + assert_not_equals(initialFinishedPromise, animation.finished); +}, 'Animation finished promise is replaced after replaying from start'); + +async_test(t => { + const animation = createDiv(t).animate(null, 1); + animation.onfinish = event => { + animation.currentTime = 0; + animation.onfinish = event => { + t.done(); + }; + }; +}, 'Animation finish event is fired again after seeking back to start'); + +async_test(t => { + const animation = createDiv(t).animate(null, 1); + animation.onfinish = event => { + animation.play(); + animation.onfinish = event => { + t.done(); + }; + }; +}, 'Animation finish event is fired again after replaying from start'); + +async_test(t => { + const anim = createDiv(t).animate(null, + { duration: 100000, endDelay: 50000 }); + anim.onfinish = t.step_func(event => { + assert_unreached('finish event should not be fired'); + }); + + anim.ready.then(() => { + anim.currentTime = 100000; + return waitForAnimationFrames(2); + }).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 = createDiv(t).animate(null, + { duration: 100000, endDelay: 30000 }); + anim.ready.then(() => { + anim.currentTime = 110000; // 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(); + }); + anim.currentTime = 130000; // after endTime + })); +}, 'finish event is fired after the endDelay has expired'); + +</script> +</body> |