summaryrefslogtreecommitdiffstats
path: root/testing/web-platform/tests/web-animations/timing-model/animations
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 01:47:29 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 01:47:29 +0000
commit0ebf5bdf043a27fd3dfb7f92e0cb63d88954c44d (patch)
treea31f07c9bcca9d56ce61e9a1ffd30ef350d513aa /testing/web-platform/tests/web-animations/timing-model/animations
parentInitial commit. (diff)
downloadfirefox-esr-0ebf5bdf043a27fd3dfb7f92e0cb63d88954c44d.tar.xz
firefox-esr-0ebf5bdf043a27fd3dfb7f92e0cb63d88954c44d.zip
Adding upstream version 115.8.0esr.upstream/115.8.0esr
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'testing/web-platform/tests/web-animations/timing-model/animations')
-rw-r--r--testing/web-platform/tests/web-animations/timing-model/animations/canceling-an-animation.html127
-rw-r--r--testing/web-platform/tests/web-animations/timing-model/animations/document-timeline-animation-ref.html19
-rw-r--r--testing/web-platform/tests/web-animations/timing-model/animations/document-timeline-animation.html63
-rw-r--r--testing/web-platform/tests/web-animations/timing-model/animations/finishing-an-animation.html330
-rw-r--r--testing/web-platform/tests/web-animations/timing-model/animations/infinite-duration-animation-ref.html19
-rw-r--r--testing/web-platform/tests/web-animations/timing-model/animations/infinite-duration-animation.html64
-rw-r--r--testing/web-platform/tests/web-animations/timing-model/animations/pausing-an-animation.html119
-rw-r--r--testing/web-platform/tests/web-animations/timing-model/animations/play-states.html185
-rw-r--r--testing/web-platform/tests/web-animations/timing-model/animations/playing-an-animation.html177
-rw-r--r--testing/web-platform/tests/web-animations/timing-model/animations/reverse-running-animation-ref.html17
-rw-r--r--testing/web-platform/tests/web-animations/timing-model/animations/reverse-running-animation.html50
-rw-r--r--testing/web-platform/tests/web-animations/timing-model/animations/reversing-an-animation.html266
-rw-r--r--testing/web-platform/tests/web-animations/timing-model/animations/seamlessly-updating-the-playback-rate-of-an-animation.html171
-rw-r--r--testing/web-platform/tests/web-animations/timing-model/animations/setting-the-current-time-of-an-animation.html167
-rw-r--r--testing/web-platform/tests/web-animations/timing-model/animations/setting-the-playback-rate-of-an-animation.html110
-rw-r--r--testing/web-platform/tests/web-animations/timing-model/animations/setting-the-start-time-of-an-animation.html329
-rw-r--r--testing/web-platform/tests/web-animations/timing-model/animations/setting-the-target-effect-of-an-animation.html129
-rw-r--r--testing/web-platform/tests/web-animations/timing-model/animations/setting-the-timeline-of-an-animation.html255
-rw-r--r--testing/web-platform/tests/web-animations/timing-model/animations/sync-start-times-ref.html20
-rw-r--r--testing/web-platform/tests/web-animations/timing-model/animations/sync-start-times.html72
-rw-r--r--testing/web-platform/tests/web-animations/timing-model/animations/the-current-time-of-an-animation.html75
-rw-r--r--testing/web-platform/tests/web-animations/timing-model/animations/update-playback-rate-fast-ref.html17
-rw-r--r--testing/web-platform/tests/web-animations/timing-model/animations/update-playback-rate-fast.html52
-rw-r--r--testing/web-platform/tests/web-animations/timing-model/animations/update-playback-rate-zero-ref.html16
-rw-r--r--testing/web-platform/tests/web-animations/timing-model/animations/update-playback-rate-zero.html46
-rw-r--r--testing/web-platform/tests/web-animations/timing-model/animations/updating-the-finished-state.html457
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>