summaryrefslogtreecommitdiffstats
path: root/testing/web-platform/tests/web-animations/timing-model
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
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')
-rw-r--r--testing/web-platform/tests/web-animations/timing-model/animation-effects/active-time.html141
-rw-r--r--testing/web-platform/tests/web-animations/timing-model/animation-effects/current-iteration.html620
-rw-r--r--testing/web-platform/tests/web-animations/timing-model/animation-effects/local-time.html29
-rw-r--r--testing/web-platform/tests/web-animations/timing-model/animation-effects/phases-and-states.html149
-rw-r--r--testing/web-platform/tests/web-animations/timing-model/animation-effects/simple-iteration-progress.html600
-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
-rw-r--r--testing/web-platform/tests/web-animations/timing-model/time-transformations/transformed-progress.html391
-rw-r--r--testing/web-platform/tests/web-animations/timing-model/timelines/document-timelines.html50
-rw-r--r--testing/web-platform/tests/web-animations/timing-model/timelines/resources/target-frame.html76
-rw-r--r--testing/web-platform/tests/web-animations/timing-model/timelines/resources/timeline-frame.html20
-rw-r--r--testing/web-platform/tests/web-animations/timing-model/timelines/sibling-iframe-timeline.html58
-rw-r--r--testing/web-platform/tests/web-animations/timing-model/timelines/timelines.html112
-rw-r--r--testing/web-platform/tests/web-animations/timing-model/timelines/update-and-send-events-replacement.html1017
-rw-r--r--testing/web-platform/tests/web-animations/timing-model/timelines/update-and-send-events.html257
39 files changed, 6872 insertions, 0 deletions
diff --git a/testing/web-platform/tests/web-animations/timing-model/animation-effects/active-time.html b/testing/web-platform/tests/web-animations/timing-model/animation-effects/active-time.html
new file mode 100644
index 0000000000..a2feb2323c
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/timing-model/animation-effects/active-time.html
@@ -0,0 +1,141 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title>Active time</title>
+<link rel="help" href="https://drafts.csswg.org/web-animations/#calculating-the-active-time">
+<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 tests = [ { fill: 'none', progress: null },
+ { fill: 'backwards', progress: 0 },
+ { fill: 'forwards', progress: null },
+ { fill: 'both', progress: 0 } ];
+ for (const test of tests) {
+ const anim = createDiv(t).animate(null, { delay: 1, fill: test.fill });
+ assert_equals(anim.effect.getComputedTiming().progress, test.progress,
+ `Progress in before phase when using '${test.fill}' fill`);
+ }
+}, 'Active time in before phase');
+
+test(t => {
+ const anim = createDiv(t).animate(null, 1000);
+ anim.currentTime = 500;
+ assert_time_equals_literal(anim.effect.getComputedTiming().progress, 0.5);
+}, 'Active time in active phase and no start delay is the local time');
+
+test(t => {
+ const anim = createDiv(t).animate(null, { duration: 1000, delay: 500 });
+ anim.currentTime = 1000;
+ assert_time_equals_literal(anim.effect.getComputedTiming().progress, 0.5);
+}, 'Active time in active phase and positive start delay is the local time'
+ + ' minus the start delay');
+
+test(t => {
+ const anim = createDiv(t).animate(null, { duration: 1000, delay: -500 });
+ assert_time_equals_literal(anim.effect.getComputedTiming().progress, 0.5);
+}, 'Active time in active phase and negative start delay is the local time'
+ + ' minus the start delay');
+
+test(t => {
+ const anim = createDiv(t).animate(null);
+ assert_equals(anim.effect.getComputedTiming().progress, null);
+}, 'Active time in after phase with no fill is unresolved');
+
+test(t => {
+ const anim = createDiv(t).animate(null, { fill: 'backwards' });
+ assert_equals(anim.effect.getComputedTiming().progress, null);
+}, 'Active time in after phase with backwards-only fill is unresolved');
+
+test(t => {
+ const anim = createDiv(t).animate(null, { duration: 1000,
+ iterations: 2.3,
+ delay: 500, // Should have no effect
+ fill: 'forwards' });
+ anim.finish();
+ assert_equals(anim.effect.getComputedTiming().currentIteration, 2);
+ assert_time_equals_literal(anim.effect.getComputedTiming().progress, 0.3);
+}, 'Active time in after phase with forwards fill is the active duration');
+
+test(t => {
+ const anim = createDiv(t).animate(null, { duration: 0,
+ iterations: Infinity,
+ fill: 'forwards' });
+ anim.finish();
+ assert_equals(anim.effect.getComputedTiming().currentIteration, Infinity);
+ assert_equals(anim.effect.getComputedTiming().progress, 1);
+}, 'Active time in after phase with forwards fill, zero-duration, and '
+ + ' infinite iteration count is the active duration');
+
+test(t => {
+ const anim = createDiv(t).animate(null, { duration: 1000,
+ iterations: 2.3,
+ delay: 500,
+ endDelay: 4000,
+ fill: 'forwards' });
+ anim.finish();
+ assert_equals(anim.effect.getComputedTiming().currentIteration, 2);
+ assert_time_equals_literal(anim.effect.getComputedTiming().progress, 0.3);
+}, 'Active time in after phase with forwards fill and positive end delay'
+ + ' is the active duration');
+
+test(t => {
+ const anim = createDiv(t).animate(null, { duration: 1000,
+ iterations: 2.3,
+ delay: 500,
+ endDelay: -800,
+ fill: 'forwards' });
+ anim.finish();
+ assert_equals(anim.effect.getComputedTiming().currentIteration, 1);
+ assert_time_equals_literal(anim.effect.getComputedTiming().progress, 0.5);
+}, 'Active time in after phase with forwards fill and negative end delay'
+ + ' is the active duration + end delay');
+
+test(t => {
+ const anim = createDiv(t).animate(null, { duration: 1000,
+ iterations: 2.3,
+ delay: 500,
+ endDelay: -2500,
+ fill: 'forwards' });
+ anim.finish();
+ assert_equals(anim.effect.getComputedTiming().currentIteration, 0);
+ assert_equals(anim.effect.getComputedTiming().progress, 0);
+}, 'Active time in after phase with forwards fill and negative end delay'
+ + ' greater in magnitude than the active duration is zero');
+
+test(t => {
+ const anim = createDiv(t).animate(null, { duration: 1000,
+ iterations: 2.3,
+ delay: 500,
+ endDelay: -4000,
+ fill: 'forwards' });
+ anim.finish();
+ assert_equals(anim.effect.getComputedTiming().currentIteration, 0);
+ assert_equals(anim.effect.getComputedTiming().progress, 0);
+}, 'Active time in after phase with forwards fill and negative end delay'
+ + ' greater in magnitude than the sum of the active duration and start delay'
+ + ' is zero');
+
+test(t => {
+ const anim = createDiv(t).animate(null, { duration: 1000,
+ iterations: 2.3,
+ delay: 500,
+ fill: 'both' });
+ anim.finish();
+ assert_equals(anim.effect.getComputedTiming().currentIteration, 2);
+ assert_time_equals_literal(anim.effect.getComputedTiming().progress, 0.3);
+}, 'Active time in after phase with \'both\' fill is the active duration');
+
+test(t => {
+ // Create an effect with a non-zero duration so we ensure we're not just
+ // testing the after-phase behavior.
+ const effect = new KeyframeEffect(null, null, 1);
+ assert_equals(effect.getComputedTiming().progress, null);
+}, 'Active time when the local time is unresolved, is unresolved');
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/web-animations/timing-model/animation-effects/current-iteration.html b/testing/web-platform/tests/web-animations/timing-model/animation-effects/current-iteration.html
new file mode 100644
index 0000000000..24464ce05f
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/timing-model/animation-effects/current-iteration.html
@@ -0,0 +1,620 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title>Current iteration</title>
+<link rel="help" href="https://drafts.csswg.org/web-animations/#current-iteration">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../../testcommon.js"></script>
+<script src="../../resources/effect-tests.js"></script>
+<body>
+<div id="log"></div>
+<script>
+'use strict';
+
+function runTests(tests, description) {
+ for (const currentTest of tests) {
+ let testParams = Object.entries(currentTest.input)
+ .map(([attr, value]) => `${attr}:${value}`)
+ .join(' ');
+ if (currentTest.playbackRate !== undefined) {
+ testParams += ` playbackRate:${currentTest.playbackRate}`;
+ }
+
+ test(t => {
+ const div = createDiv(t);
+ const anim = div.animate({}, currentTest.input);
+ if (currentTest.playbackRate !== undefined) {
+ anim.playbackRate = currentTest.playbackRate;
+ }
+
+ assert_computed_timing_for_each_phase(
+ anim,
+ 'currentIteration',
+ { before: currentTest.before,
+ activeBoundary: currentTest.active,
+ after: currentTest.after },
+ );
+ }, `${description}: ${testParams}`);
+ }
+}
+
+async_test(t => {
+ const div = createDiv(t);
+ const anim = div.animate({ opacity: [ 0, 1 ] }, { delay: 1 });
+ assert_equals(anim.effect.getComputedTiming().currentIteration, null);
+ anim.finished.then(t.step_func(() => {
+ assert_equals(anim.effect.getComputedTiming().currentIteration, null);
+ t.done();
+ }));
+}, 'Test currentIteration during before and after phase when fill is none');
+
+
+// --------------------------------------------------------------------
+//
+// Zero iteration duration tests
+//
+// --------------------------------------------------------------------
+
+runTests([
+ {
+ input: { iterations: 0,
+ iterationStart: 0,
+ duration: 0,
+ delay: 1,
+ fill: 'both' },
+ before: 0,
+ after: 0
+ },
+
+ {
+ input: { iterations: 0,
+ iterationStart: 0,
+ duration: 100,
+ delay: 1,
+ fill: 'both' },
+ before: 0,
+ after: 0
+ },
+
+ {
+ input: { iterations: 0,
+ iterationStart: 0,
+ duration: Infinity,
+ delay: 1,
+ fill: 'both' },
+ before: 0,
+ after: 0
+ },
+
+ {
+ input: { iterations: 0,
+ iterationStart: 2.5,
+ duration: 0,
+ delay: 1,
+ fill: 'both' },
+ before: 2,
+ after: 2
+ },
+
+ {
+ input: { iterations: 0,
+ iterationStart: 2.5,
+ duration: 100,
+ delay: 1,
+ fill: 'both' },
+ before: 2,
+ after: 2
+ },
+
+ {
+ input: { iterations: 0,
+ iterationStart: 2.5,
+ duration: Infinity,
+ delay: 1,
+ fill: 'both' },
+ before: 2,
+ after: 2
+ },
+
+ {
+ input: { iterations: 0,
+ iterationStart: 3,
+ duration: 0,
+ delay: 1,
+ fill: 'both' },
+ before: 3,
+ after: 3
+ },
+
+ {
+ input: { iterations: 0,
+ iterationStart: 3,
+ duration: 100,
+ delay: 1,
+ fill: 'both' },
+ before: 3,
+ after: 3
+ },
+
+ {
+ input: { iterations: 0,
+ iterationStart: 3,
+ duration: Infinity,
+ delay: 1,
+ fill: 'both' },
+ before: 3,
+ after: 3
+ }
+], 'Test zero iterations');
+
+
+// --------------------------------------------------------------------
+//
+// Tests where the iteration count is an integer
+//
+// --------------------------------------------------------------------
+
+runTests([
+ {
+ input: { iterations: 3,
+ iterationStart: 0,
+ duration: 0,
+ delay: 1,
+ fill: 'both' },
+ before: 0,
+ after: 2
+ },
+
+ {
+ input: { iterations: 3,
+ iterationStart: 0,
+ duration: 100,
+ delay: 1,
+ fill: 'both' },
+ before: 0,
+ active: 0,
+ after: 2
+ },
+
+ {
+ input: { iterations: 3,
+ iterationStart: 0,
+ duration: Infinity,
+ delay: 1,
+ fill: 'both' },
+ before: 0,
+ active: 0
+ },
+
+ {
+ input: { iterations: 3,
+ iterationStart: 2.5,
+ duration: 0,
+ delay: 1,
+ fill: 'both' },
+ before: 2,
+ after: 5
+ },
+
+ {
+ input: { iterations: 3,
+ iterationStart: 2.5,
+ duration: 100,
+ delay: 1,
+ fill: 'both' },
+ before: 2,
+ active: 2,
+ after: 5
+ },
+
+ {
+ input: { iterations: 3,
+ iterationStart: 2.5,
+ duration: Infinity,
+ delay: 1,
+ fill: 'both' },
+ before: 2,
+ active: 2
+ },
+
+ {
+ input: { iterations: 3,
+ iterationStart: 3,
+ duration: 0,
+ delay: 1,
+ fill: 'both' },
+ before: 3,
+ after: 5
+ },
+
+ {
+ input: { iterations: 3,
+ iterationStart: 3,
+ duration: 100,
+ delay: 1,
+ fill: 'both' },
+ before: 3,
+ active: 3,
+ after: 5
+ },
+
+ {
+ input: { iterations: 3,
+ iterationStart: 3,
+ duration: Infinity,
+ delay: 1,
+ fill: 'both' },
+ before: 3,
+ active: 3
+ }
+], 'Test integer iterations');
+
+
+// --------------------------------------------------------------------
+//
+// Tests where the iteration count is a fraction
+//
+// --------------------------------------------------------------------
+
+runTests([
+ {
+ input: { iterations: 3.5,
+ iterationStart: 0,
+ duration: 0,
+ delay: 1,
+ fill: 'both' },
+ before: 0,
+ after: 3
+ },
+
+ {
+ input: { iterations: 3.5,
+ iterationStart: 0,
+ duration: 100,
+ delay: 1,
+ fill: 'both' },
+ before: 0,
+ active: 0,
+ after: 3
+ },
+
+ {
+ input: { iterations: 3.5,
+ iterationStart: 0,
+ duration: Infinity,
+ delay: 1,
+ fill: 'both' },
+ before: 0,
+ active: 0
+ },
+
+ {
+ input: { iterations: 3.5,
+ iterationStart: 2.5,
+ duration: 0,
+ delay: 1,
+ fill: 'both' },
+ before: 2,
+ after: 5
+ },
+
+ {
+ input: { iterations: 3.5,
+ iterationStart: 2.5,
+ duration: 100,
+ delay: 1,
+ fill: 'both' },
+ before: 2,
+ active: 2,
+ after: 5
+ },
+
+ {
+ input: { iterations: 3.5,
+ iterationStart: 2.5,
+ duration: Infinity,
+ delay: 1,
+ fill: 'both' },
+ before: 2,
+ active: 2
+ },
+
+ {
+ input: { iterations: 3.5,
+ iterationStart: 3,
+ duration: 0,
+ delay: 1,
+ fill: 'both' },
+ before: 3,
+ after: 6
+ },
+
+ {
+ input: { iterations: 3.5,
+ iterationStart: 3,
+ duration: 100,
+ delay: 1,
+ fill: 'both' },
+ before: 3,
+ active: 3,
+ after: 6
+ },
+
+ {
+ input: { iterations: 3.5,
+ iterationStart: 3,
+ duration: Infinity,
+ delay: 1,
+ fill: 'both' },
+ before: 3,
+ active: 3
+ }
+], 'Test fractional iterations');
+
+
+// --------------------------------------------------------------------
+//
+// Tests where the iteration count is Infinity
+//
+// --------------------------------------------------------------------
+
+runTests([
+ {
+ input: { iterations: Infinity,
+ iterationStart: 0,
+ duration: 0,
+ delay: 1,
+ fill: 'both' },
+ before: 0,
+ after: Infinity
+ },
+
+ {
+ input: { iterations: Infinity,
+ iterationStart: 0,
+ duration: 100,
+ delay: 1,
+ fill: 'both' },
+ before: 0,
+ active: 0
+ },
+
+ {
+ input: { iterations: Infinity,
+ iterationStart: 0,
+ duration: Infinity,
+ delay: 1,
+ fill: 'both' },
+ before: 0,
+ active: 0
+ },
+
+ {
+ input: { iterations: Infinity,
+ iterationStart: 2.5,
+ duration: 0,
+ delay: 1,
+ fill: 'both' },
+ before: 2,
+ after: Infinity
+ },
+
+ {
+ input: { iterations: Infinity,
+ iterationStart: 2.5,
+ duration: 100,
+ delay: 1,
+ fill: 'both' },
+ before: 2,
+ active: 2
+ },
+
+ {
+ input: { iterations: Infinity,
+ iterationStart: 2.5,
+ duration: Infinity,
+ delay: 1,
+ fill: 'both' },
+ before: 2,
+ active: 2
+ },
+
+ {
+ input: { iterations: Infinity,
+ iterationStart: 3,
+ duration: 0,
+ delay: 1,
+ fill: 'both' },
+ before: 3,
+ after: Infinity
+ },
+
+ {
+ input: { iterations: Infinity,
+ iterationStart: 3,
+ duration: 100,
+ delay: 1,
+ fill: 'both' },
+ before: 3,
+ active: 3
+ },
+
+ {
+ input: { iterations: Infinity,
+ iterationStart: 3,
+ duration: Infinity,
+ delay: 1,
+ fill: 'both' },
+ before: 3,
+ active: 3
+ }
+], 'Test infinity iterations');
+
+
+// --------------------------------------------------------------------
+//
+// End delay tests
+//
+// --------------------------------------------------------------------
+
+runTests([
+ {
+ input: { duration: 100,
+ delay: 1,
+ fill: 'both',
+ endDelay: 50 },
+ before: 0,
+ active: 0,
+ after: 0
+ },
+
+ {
+ input: { duration: 100,
+ delay: 1,
+ fill: 'both',
+ endDelay: -50 },
+ before: 0,
+ active: 0,
+ after: 0
+ },
+
+ {
+ input: { duration: 100,
+ delay: 1,
+ fill: 'both',
+ endDelay: -100 },
+ before: 0,
+ active: 0,
+ after: 0
+ },
+
+ {
+ input: { duration: 100,
+ delay: 1,
+ fill: 'both',
+ endDelay: -200 },
+ before: 0,
+ active: 0,
+ after: 0
+ },
+
+ {
+ input: { iterationStart: 0.5,
+ duration: 100,
+ delay: 1,
+ fill: 'both',
+ endDelay: 50 },
+ before: 0,
+ active: 0,
+ after: 1
+ },
+
+ {
+ input: { iterationStart: 0.5,
+ duration: 100,
+ delay: 1,
+ fill: 'both',
+ endDelay: -50 },
+ before: 0,
+ active: 0,
+ after: 1
+ },
+
+ {
+ input: { iterationStart: 0.5,
+ duration: 100,
+ delay: 1,
+ fill: 'both',
+ endDelay: -100 },
+ before: 0,
+ active: 0,
+ after: 0
+ },
+
+ {
+ input: { iterations: 2,
+ duration: 100,
+ delay: 1,
+ fill: 'both',
+ endDelay: -100 },
+ before: 0,
+ active: 0,
+ after: 1
+ },
+
+ {
+ input: { iterations: 1,
+ iterationStart: 2,
+ duration: 100,
+ delay: 1,
+ fill: 'both',
+ endDelay: -50 },
+ before: 2,
+ active: 2,
+ after: 2
+ },
+
+ {
+ input: { iterations: 1,
+ iterationStart: 2,
+ duration: 100,
+ delay: 1,
+ fill: 'both',
+ endDelay: -100 },
+ before: 2,
+ active: 2,
+ after: 2
+ },
+], 'Test end delay');
+
+
+// --------------------------------------------------------------------
+//
+// Negative playback rate tests
+//
+// --------------------------------------------------------------------
+
+runTests([
+ {
+ input: { duration: 1,
+ delay: 1,
+ fill: 'both' },
+ playbackRate: -1,
+ before: 0,
+ active: 0,
+ after: 0
+ },
+
+ {
+ input: { duration: 1,
+ delay: 1,
+ iterations: 2,
+ fill: 'both' },
+ playbackRate: -1,
+ before: 0,
+ active: 1,
+ after: 1
+ },
+
+ {
+ input: { duration: 0,
+ delay: 1,
+ fill: 'both' },
+ playbackRate: -1,
+ before: 0,
+ after: 0
+ },
+
+ {
+ input: { duration: 0,
+ iterations: 0,
+ delay: 1,
+ fill: 'both' },
+ playbackRate: -1,
+ before: 0,
+ after: 0
+ },
+], 'Test negative playback rate');
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/web-animations/timing-model/animation-effects/local-time.html b/testing/web-platform/tests/web-animations/timing-model/animation-effects/local-time.html
new file mode 100644
index 0000000000..79437d9f54
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/timing-model/animation-effects/local-time.html
@@ -0,0 +1,29 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title>Local time</title>
+<link rel="help" href="https://drafts.csswg.org/web-animations/#local-time">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../../testcommon.js"></script>
+<body>
+<script>
+'use strict';
+
+test(t => {
+ const anim = createDiv(t).animate(null, 10 * MS_PER_SEC);
+ for (const seconds of [-1, 0, 5, 10, 20]) {
+ anim.currentTime = seconds * MS_PER_SEC;
+ assert_equals(
+ anim.effect.getComputedTiming().localTime,
+ seconds * MS_PER_SEC
+ );
+ }
+}, 'Local time is current time for animation effects associated with an animation');
+
+test(t => {
+ const effect = new KeyframeEffect(createDiv(t), null, 10 * MS_PER_SEC);
+ assert_equals(effect.getComputedTiming().localTime, null);
+}, 'Local time is unresolved for animation effects not associated with an animation');
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/web-animations/timing-model/animation-effects/phases-and-states.html b/testing/web-platform/tests/web-animations/timing-model/animation-effects/phases-and-states.html
new file mode 100644
index 0000000000..a33dbf517e
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/timing-model/animation-effects/phases-and-states.html
@@ -0,0 +1,149 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>Phases and states</title>
+<link rel="help" href="https://drafts.csswg.org/web-animations/#animation-effect-phases-and-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';
+
+// --------------------------------------------------------------------
+//
+// Phases
+//
+// --------------------------------------------------------------------
+
+test(t => {
+ const animation = createDiv(t).animate(null, 1);
+
+ for (const test of [{ currentTime: -1, phase: 'before' },
+ { currentTime: 0, phase: 'active' },
+ { currentTime: 1, phase: 'after' }]) {
+ assert_phase_at_time(animation, test.phase, test.currentTime);
+ }
+}, 'Phase calculation for a simple animation effect');
+
+test(t => {
+ const animation = createDiv(t).animate(null, { duration: 1, delay: 1 });
+
+ for (const test of [{ currentTime: 0, phase: 'before' },
+ { currentTime: 1, phase: 'active' },
+ { currentTime: 2, phase: 'after' }]) {
+ assert_phase_at_time(animation, test.phase, test.currentTime);
+ }
+}, 'Phase calculation for an animation effect with a positive start delay');
+
+test(t => {
+ const animation = createDiv(t).animate(null, { duration: 1, delay: -1 });
+
+ for (const test of [{ currentTime: -2, phase: 'before' },
+ { currentTime: -1, phase: 'before' },
+ { currentTime: 0, phase: 'after' }]) {
+ assert_phase_at_time(animation, test.phase, test.currentTime);
+ }
+}, 'Phase calculation for an animation effect with a negative start delay');
+
+test(t => {
+ const animation = createDiv(t).animate(null, { duration: 1, endDelay: 1 });
+
+ for (const test of [{ currentTime: -1, phase: 'before' },
+ { currentTime: 0, phase: 'active' },
+ { currentTime: 1, phase: 'after' },
+ { currentTime: 2, phase: 'after' }]) {
+ assert_phase_at_time(animation, test.phase, test.currentTime);
+ }
+}, 'Phase calculation for an animation effect with a positive end delay');
+
+test(t => {
+ const animation = createDiv(t).animate(null, { duration: 2, endDelay: -1 });
+
+ for (const test of [{ currentTime: -1, phase: 'before' },
+ { currentTime: 0, phase: 'active' },
+ { currentTime: 0.9, phase: 'active' },
+ { currentTime: 1, phase: 'after' }]) {
+ assert_phase_at_time(animation, test.phase, test.currentTime);
+ }
+}, 'Phase calculation for an animation effect with a negative end delay lesser'
+ + ' in magnitude than the active duration');
+
+test(t => {
+ const animation = createDiv(t).animate(null, { duration: 1, endDelay: -1 });
+
+ for (const test of [{ currentTime: -1, phase: 'before' },
+ { currentTime: 0, phase: 'after' },
+ { currentTime: 1, phase: 'after' }]) {
+ assert_phase_at_time(animation, test.phase, test.currentTime);
+ }
+}, 'Phase calculation for an animation effect with a negative end delay equal'
+ + ' in magnitude to the active duration');
+
+test(t => {
+ const animation = createDiv(t).animate(null, { duration: 1, endDelay: -2 });
+
+ for (const test of [{ currentTime: -2, phase: 'before' },
+ { currentTime: -1, phase: 'before' },
+ { currentTime: 0, phase: 'after' }]) {
+ assert_phase_at_time(animation, test.phase, test.currentTime);
+ }
+}, 'Phase calculation for an animation effect with a negative end delay'
+ + ' greater in magnitude than the active duration');
+
+test(t => {
+ const animation = createDiv(t).animate(null, { duration: 2,
+ delay: 1,
+ endDelay: -1 });
+
+ for (const test of [{ currentTime: 0, phase: 'before' },
+ { currentTime: 1, phase: 'active' },
+ { currentTime: 2, phase: 'after' }]) {
+ assert_phase_at_time(animation, test.phase, test.currentTime);
+ }
+}, 'Phase calculation for an animation effect with a positive start delay'
+ + ' and a negative end delay lesser in magnitude than the active duration');
+
+test(t => {
+ const animation = createDiv(t).animate(null, { duration: 1,
+ delay: -1,
+ endDelay: -1 });
+
+ for (const test of [{ currentTime: -2, phase: 'before' },
+ { currentTime: -1, phase: 'before' },
+ { currentTime: 0, phase: 'after' }]) {
+ assert_phase_at_time(animation, test.phase, test.currentTime);
+ }
+}, 'Phase calculation for an animation effect with a negative start delay'
+ + ' and a negative end delay equal in magnitude to the active duration');
+
+test(t => {
+ const animation = createDiv(t).animate(null, { duration: 1,
+ delay: -1,
+ endDelay: -2 });
+
+ for (const test of [{ currentTime: -3, phase: 'before' },
+ { currentTime: -2, phase: 'before' },
+ { currentTime: -1, phase: 'before' },
+ { currentTime: 0, phase: 'after' }]) {
+ assert_phase_at_time(animation, test.phase, test.currentTime);
+ }
+}, 'Phase calculation for an animation effect with a negative start delay'
+ + ' and a negative end delay equal greater in magnitude than the active'
+ + ' duration');
+
+test(t => {
+ const animation = createDiv(t).animate(null, 1);
+ animation.playbackRate = -1;
+
+ for (const test of [{ currentTime: -1, phase: 'before' },
+ { currentTime: 0, phase: 'before' },
+ { currentTime: 1, phase: 'active' },
+ { currentTime: 2, phase: 'after' }]) {
+ assert_phase_at_time(animation, test.phase, test.currentTime);
+ }
+}, 'Phase calculation for a simple animation effect with negative playback'
+ + ' rate');
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/web-animations/timing-model/animation-effects/simple-iteration-progress.html b/testing/web-platform/tests/web-animations/timing-model/animation-effects/simple-iteration-progress.html
new file mode 100644
index 0000000000..3c42f79a71
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/timing-model/animation-effects/simple-iteration-progress.html
@@ -0,0 +1,600 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title>Simple iteration progress</title>
+<link rel="help"
+ href="https://drafts.csswg.org/web-animations/#simple-iteration-progress">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../../testcommon.js"></script>
+<script src="../../resources/effect-tests.js"></script>
+<body>
+<div id="log"></div>
+<script>
+'use strict';
+
+function runTests(tests, description) {
+ for (const currentTest of tests) {
+ let testParams = Object.entries(currentTest.input)
+ .map(([attr, value]) => `${attr}:${value}`)
+ .join(' ');
+ if (currentTest.playbackRate !== undefined) {
+ testParams += ` playbackRate:${currentTest.playbackRate}`;
+ }
+
+ test(t => {
+ const div = createDiv(t);
+ const anim = div.animate({}, currentTest.input);
+ if (currentTest.playbackRate !== undefined) {
+ anim.playbackRate = currentTest.playbackRate;
+ }
+
+ assert_computed_timing_for_each_phase(
+ anim,
+ 'progress',
+ { before: currentTest.before,
+ activeBoundary: currentTest.active,
+ after: currentTest.after },
+ );
+ }, `${description}: ${testParams}`);
+ }
+}
+
+
+// --------------------------------------------------------------------
+//
+// Zero iteration duration tests
+//
+// --------------------------------------------------------------------
+
+runTests([
+ {
+ input: { iterations: 0,
+ iterationStart: 0,
+ duration: 0,
+ delay: 1,
+ fill: 'both' },
+ before: 0,
+ after: 0
+ },
+
+ {
+ input: { iterations: 0,
+ iterationStart: 0,
+ duration: 100,
+ delay: 1,
+ fill: 'both' },
+ before: 0,
+ after: 0
+ },
+
+ {
+ input: { iterations: 0,
+ iterationStart: 0,
+ duration: Infinity,
+ delay: 1,
+ fill: 'both' },
+ before: 0,
+ after: 0
+ },
+
+ {
+ input: { iterations: 0,
+ iterationStart: 2.5,
+ duration: 0,
+ delay: 1,
+ fill: 'both' },
+ before: 0.5,
+ after: 0.5
+ },
+
+ {
+ input: { iterations: 0,
+ iterationStart: 2.5,
+ duration: 100,
+ delay: 1,
+ fill: 'both' },
+ before: 0.5,
+ after: 0.5
+ },
+
+ {
+ input: { iterations: 0,
+ iterationStart: 2.5,
+ duration: Infinity,
+ delay: 1,
+ fill: 'both' },
+ before: 0.5,
+ after: 0.5
+ },
+
+ {
+ input: { iterations: 0,
+ iterationStart: 3,
+ duration: 0,
+ delay: 1,
+ fill: 'both' },
+ before: 0,
+ after: 0
+ },
+
+ {
+ input: { iterations: 0,
+ iterationStart: 3,
+ duration: 100,
+ delay: 1,
+ fill: 'both' },
+ before: 0,
+ after: 0
+ },
+
+ {
+ input: { iterations: 0,
+ iterationStart: 3,
+ duration: Infinity,
+ delay: 1,
+ fill: 'both' },
+ before: 0,
+ after: 0
+ }
+], 'Test zero iterations');
+
+
+// --------------------------------------------------------------------
+//
+// Tests where the iteration count is an integer
+//
+// --------------------------------------------------------------------
+
+runTests([
+ {
+ input: { iterations: 3,
+ iterationStart: 0,
+ duration: 0,
+ delay: 1,
+ fill: 'both' },
+ before: 0,
+ after: 1
+ },
+
+ {
+ input: { iterations: 3,
+ iterationStart: 0,
+ duration: 100,
+ delay: 1,
+ fill: 'both' },
+ before: 0,
+ active: 0,
+ after: 1
+ },
+
+ {
+ input: { iterations: 3,
+ iterationStart: 0,
+ duration: Infinity,
+ delay: 1,
+ fill: 'both' },
+ before: 0,
+ active: 0
+ },
+
+ {
+ input: { iterations: 3,
+ iterationStart: 2.5,
+ duration: 0,
+ delay: 1,
+ fill: 'both' },
+ before: 0.5,
+ after: 0.5
+ },
+
+ {
+ input: { iterations: 3,
+ iterationStart: 2.5,
+ duration: 100,
+ delay: 1,
+ fill: 'both' },
+ before: 0.5,
+ active: 0.5,
+ after: 0.5
+ },
+
+ {
+ input: { iterations: 3,
+ iterationStart: 2.5,
+ duration: Infinity,
+ delay: 1,
+ fill: 'both' },
+ before: 0.5,
+ active: 0.5
+ },
+
+ {
+ input: { iterations: 3,
+ iterationStart: 3,
+ duration: 0,
+ delay: 1,
+ fill: 'both' },
+ before: 0,
+ after: 1
+ },
+
+ {
+ input: { iterations: 3,
+ iterationStart: 3,
+ duration: 100,
+ delay: 1,
+ fill: 'both' },
+ before: 0,
+ active: 0,
+ after: 1
+ },
+
+ {
+ input: { iterations: 3,
+ iterationStart: 3,
+ duration: Infinity,
+ delay: 1,
+ fill: 'both' },
+ before: 0,
+ active: 0
+ }
+], 'Test integer iterations');
+
+
+// --------------------------------------------------------------------
+//
+// Tests where the iteration count is a fraction
+//
+// --------------------------------------------------------------------
+
+runTests([
+ {
+ input: { iterations: 3.5,
+ iterationStart: 0,
+ duration: 0,
+ delay: 1,
+ fill: 'both' },
+ before: 0,
+ after: 0.5
+ },
+
+ {
+ input: { iterations: 3.5,
+ iterationStart: 0,
+ duration: 100,
+ delay: 1,
+ fill: 'both' },
+ before: 0,
+ active: 0,
+ after: 0.5
+ },
+
+ {
+ input: { iterations: 3.5,
+ iterationStart: 0,
+ duration: Infinity,
+ delay: 1,
+ fill: 'both' },
+ before: 0,
+ active: 0
+ },
+
+ {
+ input: { iterations: 3.5,
+ iterationStart: 2.5,
+ duration: 0,
+ delay: 1,
+ fill: 'both' },
+ before: 0.5,
+ after: 1
+ },
+
+ {
+ input: { iterations: 3.5,
+ iterationStart: 2.5,
+ duration: 100,
+ delay: 1,
+ fill: 'both' },
+ before: 0.5,
+ active: 0.5,
+ after: 1
+ },
+
+ {
+ input: { iterations: 3.5,
+ iterationStart: 2.5,
+ duration: Infinity,
+ delay: 1,
+ fill: 'both' },
+ before: 0.5,
+ active: 0.5
+ },
+
+ {
+ input: { iterations: 3.5,
+ iterationStart: 3,
+ duration: 0,
+ delay: 1,
+ fill: 'both' },
+ before: 0,
+ after: 0.5
+ },
+
+ {
+ input: { iterations: 3.5,
+ iterationStart: 3,
+ duration: 100,
+ delay: 1,
+ fill: 'both' },
+ before: 0,
+ active: 0,
+ after: 0.5
+ },
+
+ {
+ input: { iterations: 3.5,
+ iterationStart: 3,
+ duration: Infinity,
+ delay: 1,
+ fill: 'both' },
+ before: 0,
+ active: 0
+ }
+], 'Test fractional iterations');
+
+
+// --------------------------------------------------------------------
+//
+// Tests where the iteration count is Infinity
+//
+// --------------------------------------------------------------------
+
+runTests([
+ {
+ input: { iterations: Infinity,
+ iterationStart: 0,
+ duration: 0,
+ delay: 1,
+ fill: 'both' },
+ before: 0,
+ after: 1
+ },
+
+ {
+ input: { iterations: Infinity,
+ iterationStart: 0,
+ duration: 100,
+ delay: 1,
+ fill: 'both' },
+ before: 0,
+ active: 0
+ },
+
+ {
+ input: { iterations: Infinity,
+ iterationStart: 0,
+ duration: Infinity,
+ delay: 1,
+ fill: 'both' },
+ before: 0,
+ active: 0
+ },
+
+ {
+ input: { iterations: Infinity,
+ iterationStart: 2.5,
+ duration: 0,
+ delay: 1,
+ fill: 'both' },
+ before: 0.5,
+ after: 0.5
+ },
+
+ {
+ input: { iterations: Infinity,
+ iterationStart: 2.5,
+ duration: 100,
+ delay: 1,
+ fill: 'both' },
+ before: 0.5,
+ active: 0.5
+ },
+
+ {
+ input: { iterations: Infinity,
+ iterationStart: 2.5,
+ duration: Infinity,
+ delay: 1,
+ fill: 'both' },
+ before: 0.5,
+ active: 0.5
+ },
+
+ {
+ input: { iterations: Infinity,
+ iterationStart: 3,
+ duration: 0,
+ delay: 1,
+ fill: 'both' },
+ before: 0,
+ after: 1
+ },
+
+ {
+ input: { iterations: Infinity,
+ iterationStart: 3,
+ duration: 100,
+ delay: 1,
+ fill: 'both' },
+ before: 0,
+ active: 0
+ },
+
+ {
+ input: { iterations: Infinity,
+ iterationStart: 3,
+ duration: Infinity,
+ delay: 1,
+ fill: 'both' },
+ before: 0,
+ active: 0
+ }
+], 'Test infinity iterations');
+
+
+// --------------------------------------------------------------------
+//
+// End delay tests
+//
+// --------------------------------------------------------------------
+
+runTests([
+ {
+ input: { duration: 100,
+ delay: 1,
+ fill: 'both',
+ endDelay: 50 },
+ before: 0,
+ active: 0,
+ after: 1
+ },
+
+ {
+ input: { duration: 100,
+ delay: 1,
+ fill: 'both',
+ endDelay: -50 },
+ before: 0,
+ active: 0,
+ after: 0.5
+ },
+
+ {
+ input: { duration: 100,
+ delay: 1,
+ fill: 'both',
+ endDelay: -100 },
+ before: 0,
+ active: 0,
+ after: 0
+ },
+
+ {
+ input: { duration: 100,
+ delay: 1,
+ fill: 'both',
+ endDelay: -200 },
+ before: 0,
+ active: 0,
+ after: 0
+ },
+
+ {
+ input: { iterationStart: 0.5,
+ duration: 100,
+ delay: 1,
+ fill: 'both',
+ endDelay: 50 },
+ before: 0.5,
+ active: 0.5,
+ after: 0.5
+ },
+
+ {
+ input: { iterationStart: 0.5,
+ duration: 100,
+ delay: 1,
+ fill: 'both',
+ endDelay: -50 },
+ before: 0.5,
+ active: 0.5,
+ after: 0
+ },
+
+ {
+ input: { iterationStart: 0.5,
+ duration: 100,
+ delay: 1,
+ fill: 'both',
+ endDelay: -100 },
+ before: 0.5,
+ active: 0.5,
+ after: 0.5
+ },
+
+ {
+ input: { iterations: 2,
+ duration: 100,
+ delay: 1,
+ fill: 'both',
+ endDelay: -100 },
+ before: 0,
+ active: 0,
+ after: 0
+ },
+
+ {
+ input: { iterations: 1,
+ iterationStart: 2,
+ duration: 100,
+ delay: 1,
+ fill: 'both',
+ endDelay: -50 },
+ before: 0,
+ active: 0,
+ after: 0.5
+ },
+
+ {
+ input: { iterations: 1,
+ iterationStart: 2,
+ duration: 100,
+ delay: 1,
+ fill: 'both',
+ endDelay: -100 },
+ before: 0,
+ active: 0,
+ after: 0
+ },
+], 'Test end delay');
+
+
+// --------------------------------------------------------------------
+//
+// Negative playback rate tests
+//
+// --------------------------------------------------------------------
+
+runTests([
+ {
+ input: { duration: 1,
+ delay: 1,
+ fill: 'both' },
+ playbackRate: -1,
+ before: 0,
+ active: 1,
+ after: 1
+ },
+
+ {
+ input: { duration: 0,
+ delay: 1,
+ fill: 'both' },
+ playbackRate: -1,
+ before: 0,
+ after: 1
+ },
+
+ {
+ input: { duration: 0,
+ iterations: 0,
+ delay: 1,
+ fill: 'both' },
+ playbackRate: -1,
+ before: 0,
+ after: 0
+ },
+], 'Test negative playback rate');
+
+</script>
+</body>
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>
diff --git a/testing/web-platform/tests/web-animations/timing-model/time-transformations/transformed-progress.html b/testing/web-platform/tests/web-animations/timing-model/time-transformations/transformed-progress.html
new file mode 100644
index 0000000000..960e333c09
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/timing-model/time-transformations/transformed-progress.html
@@ -0,0 +1,391 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title>Transformed progress</title>
+<link rel="help" href="https://drafts.csswg.org/web-animations/#calculating-the-transformed-progress">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../../testcommon.js"></script>
+<script src="../../resources/easing-tests.js"></script>
+<body>
+<div id="log"></div>
+<div id="target"></div>
+<script>
+'use strict';
+
+for (const params of gEasingTests) {
+ test(t => {
+ const target = createDiv(t);
+ const anim = target.animate(null, { duration: 1000,
+ fill: 'forwards',
+ easing: params.easing });
+
+ for (const sampleTime of [0, 250, 500, 750, 1000]) {
+ anim.currentTime = sampleTime;
+ const portion = sampleTime / anim.effect.getComputedTiming().duration;
+ const expectedProgress = params.easingFunction(portion);
+ assert_approx_equals(anim.effect.getComputedTiming().progress,
+ expectedProgress,
+ 0.01,
+ 'The progress should be approximately ' +
+ `${expectedProgress} at ${sampleTime}ms`);
+ }
+ }, `Transformed progress for ${params.desc}`);
+}
+
+// Additional tests for various boundary conditions of step timing functions.
+
+const gStepTimingFunctionTests = [
+ {
+ description: 'Test bounds point of step-start easing',
+ effect: {
+ delay: 1000,
+ duration: 1000,
+ fill: 'both',
+ easing: 'steps(2, start)'
+ },
+ conditions: [
+ { currentTime: 0, progress: 0 },
+ { currentTime: 999, progress: 0 },
+ { currentTime: 1000, progress: 0.5 },
+ { currentTime: 1499, progress: 0.5 },
+ { currentTime: 1500, progress: 1 },
+ { currentTime: 2000, progress: 1 }
+ ]
+ },
+ {
+ description: 'Test bounds point of step-start easing with reverse direction',
+ effect: {
+ delay: 1000,
+ duration: 1000,
+ fill: 'both',
+ direction: 'reverse',
+ easing: 'steps(2, start)'
+ },
+ conditions: [
+ { currentTime: 0, progress: 1 },
+ { currentTime: 1001, progress: 1 },
+ { currentTime: 1500, progress: 1 },
+ { currentTime: 1501, progress: 0.5 },
+ { currentTime: 2000, progress: 0 },
+ { currentTime: 2500, progress: 0 }
+ ]
+ },
+ {
+ description: 'Test bounds point of step-start easing ' +
+ 'with iterationStart not at a transition point',
+ effect: {
+ delay: 1000,
+ duration: 1000,
+ fill: 'both',
+ iterationStart: 0.25,
+ easing: 'steps(2, start)'
+ },
+ conditions: [
+ { currentTime: 0, progress: 0.5 },
+ { currentTime: 999, progress: 0.5 },
+ { currentTime: 1000, progress: 0.5 },
+ { currentTime: 1249, progress: 0.5 },
+ { currentTime: 1250, progress: 1 },
+ { currentTime: 1749, progress: 1 },
+ { currentTime: 1750, progress: 0.5 },
+ { currentTime: 2000, progress: 0.5 },
+ { currentTime: 2500, progress: 0.5 },
+ ]
+ },
+ {
+ description: 'Test bounds point of step-start easing ' +
+ 'with iterationStart and delay',
+ effect: {
+ delay: 1000,
+ duration: 1000,
+ fill: 'both',
+ iterationStart: 0.5,
+ easing: 'steps(2, start)'
+ },
+ conditions: [
+ { currentTime: 0, progress: 0.5 },
+ { currentTime: 999, progress: 0.5 },
+ { currentTime: 1000, progress: 1 },
+ { currentTime: 1499, progress: 1 },
+ { currentTime: 1500, progress: 0.5 },
+ { currentTime: 2000, progress: 1 }
+ ]
+ },
+ {
+ description: 'Test bounds point of step-start easing ' +
+ 'with iterationStart and reverse direction',
+ effect: {
+ delay: 1000,
+ duration: 1000,
+ fill: 'both',
+ iterationStart: 0.5,
+ direction: 'reverse',
+ easing: 'steps(2, start)'
+ },
+ conditions: [
+ { currentTime: 0, progress: 1 },
+ { currentTime: 1000, progress: 1 },
+ { currentTime: 1001, progress: 0.5 },
+ { currentTime: 1499, progress: 0.5 },
+ { currentTime: 1500, progress: 1 },
+ { currentTime: 1999, progress: 1 },
+ { currentTime: 2000, progress: 0.5 },
+ { currentTime: 2500, progress: 0.5 }
+ ]
+ },
+ {
+ description: 'Test bounds point of step(4, start) easing ' +
+ 'with iterationStart 0.75 and delay',
+ effect: {
+ duration: 1000,
+ fill: 'both',
+ delay: 1000,
+ iterationStart: 0.75,
+ easing: 'steps(4, start)'
+ },
+ conditions: [
+ { currentTime: 0, progress: 0.75 },
+ { currentTime: 999, progress: 0.75 },
+ { currentTime: 1000, progress: 1 },
+ { currentTime: 2000, progress: 1 },
+ { currentTime: 2500, progress: 1 }
+ ]
+ },
+ {
+ description: 'Test bounds point of step-start easing ' +
+ 'with alternate direction',
+ effect: {
+ duration: 1000,
+ fill: 'both',
+ delay: 1000,
+ iterations: 2,
+ iterationStart: 1.5,
+ direction: 'alternate',
+ easing: 'steps(2, start)'
+ },
+ conditions: [
+ { currentTime: 0, progress: 1 },
+ { currentTime: 1000, progress: 1 },
+ { currentTime: 1001, progress: 0.5 },
+ { currentTime: 2999, progress: 1 },
+ { currentTime: 3000, progress: 0.5 },
+ { currentTime: 3500, progress: 0.5 }
+ ]
+ },
+ {
+ description: 'Test bounds point of step-start easing ' +
+ 'with alternate-reverse direction',
+ effect: {
+ duration: 1000,
+ fill: 'both',
+ delay: 1000,
+ iterations: 2,
+ iterationStart: 0.5,
+ direction: 'alternate-reverse',
+ easing: 'steps(2, start)'
+ },
+ conditions: [
+ { currentTime: 0, progress: 1 },
+ { currentTime: 1000, progress: 1 },
+ { currentTime: 1001, progress: 0.5 },
+ { currentTime: 2999, progress: 1 },
+ { currentTime: 3000, progress: 0.5 },
+ { currentTime: 3500, progress: 0.5 }
+ ]
+ },
+ {
+ description: 'Test bounds point of step-end easing',
+ effect: {
+ delay: 1000,
+ duration: 1000,
+ fill: 'both',
+ easing: 'steps(2, end)'
+ },
+ conditions: [
+ { currentTime: 0, progress: 0 },
+ { currentTime: 999, progress: 0 },
+ { currentTime: 1000, progress: 0 },
+ { currentTime: 1499, progress: 0 },
+ { currentTime: 1500, progress: 0.5 },
+ { currentTime: 2000, progress: 1 }
+ ]
+ },
+ {
+ description: 'Test bounds point of step-end easing ' +
+ 'with iterationStart and delay',
+ effect: {
+ duration: 1000,
+ fill: 'both',
+ delay: 1000,
+ iterationStart: 0.5,
+ easing: 'steps(2, end)'
+ },
+ conditions: [
+ { currentTime: 0, progress: 0 },
+ { currentTime: 999, progress: 0 },
+ { currentTime: 1000, progress: 0.5 },
+ { currentTime: 1499, progress: 0.5 },
+ { currentTime: 1500, progress: 0 },
+ { currentTime: 1999, progress: 0 },
+ { currentTime: 2000, progress: 0.5 },
+ { currentTime: 2500, progress: 0.5 }
+ ]
+ },
+ {
+ description: 'Test bounds point of step-end easing ' +
+ 'with iterationStart not at a transition point',
+ effect: {
+ delay: 1000,
+ duration: 1000,
+ fill: 'both',
+ iterationStart: 0.75,
+ easing: 'steps(2, end)'
+ },
+ conditions: [
+ { currentTime: 0, progress: 0.5 },
+ { currentTime: 999, progress: 0.5 },
+ { currentTime: 1000, progress: 0.5 },
+ { currentTime: 1249, progress: 0.5 },
+ { currentTime: 1250, progress: 0 },
+ { currentTime: 1749, progress: 0 },
+ { currentTime: 1750, progress: 0.5 },
+ { currentTime: 2000, progress: 0.5 },
+ { currentTime: 2500, progress: 0.5 },
+ ]
+ },
+ {
+ description: 'Test bounds point of steps(jump-both) easing',
+ effect: {
+ delay: 1000,
+ duration: 1000,
+ fill: 'both',
+ easing: 'steps(2, jump-both)'
+ },
+ conditions: [
+ { currentTime: 0, progress: 0 },
+ { currentTime: 999, progress: 0 },
+ { currentTime: 1000, progress: 1/3 },
+ { currentTime: 1499, progress: 1/3 },
+ { currentTime: 1500, progress: 2/3 },
+ { currentTime: 2000, progress: 1 }
+ ]
+ },
+ {
+ description: 'Test bounds point of steps(jump-both) easing ' +
+ 'with iterationStart and delay',
+ effect: {
+ duration: 1000,
+ fill: 'both',
+ delay: 1000,
+ iterationStart: 0.5,
+ easing: 'steps(2, jump-both)'
+ },
+ conditions: [
+ { currentTime: 0, progress: 1/3 },
+ { currentTime: 999, progress: 1/3 },
+ { currentTime: 1000, progress: 2/3 },
+ { currentTime: 1499, progress: 2/3 },
+ { currentTime: 1500, progress: 1/3 },
+ { currentTime: 1999, progress: 1/3 },
+ { currentTime: 2000, progress: 2/3 },
+ { currentTime: 2500, progress: 2/3 }
+ ]
+ },
+ {
+ description: 'Test bounds point of steps(jump-both) easing ' +
+ 'with iterationStart not at a transition point',
+ effect: {
+ delay: 1000,
+ duration: 1000,
+ fill: 'both',
+ iterationStart: 0.75,
+ easing: 'steps(2, jump-both)'
+ },
+ conditions: [
+ { currentTime: 0, progress: 2/3 },
+ { currentTime: 999, progress: 2/3 },
+ { currentTime: 1000, progress: 2/3 },
+ { currentTime: 1249, progress: 2/3 },
+ { currentTime: 1250, progress: 1/3 },
+ { currentTime: 1749, progress: 1/3 },
+ { currentTime: 1750, progress: 2/3 },
+ { currentTime: 2000, progress: 2/3 },
+ { currentTime: 2500, progress: 2/3 }
+ ]
+ },
+ {
+ description: 'Test bounds point of steps(jump-none) easing',
+ effect: {
+ delay: 1000,
+ duration: 1000,
+ fill: 'both',
+ easing: 'steps(2, jump-none)'
+ },
+ conditions: [
+ { currentTime: 0, progress: 0 },
+ { currentTime: 1000, progress: 0 },
+ { currentTime: 1499, progress: 0 },
+ { currentTime: 1500, progress: 1 },
+ { currentTime: 2000, progress: 1 }
+ ]
+ },
+ {
+ description: 'Test bounds point of steps(jump-none) easing ' +
+ 'with iterationStart and delay',
+ effect: {
+ duration: 1000,
+ fill: 'both',
+ delay: 1000,
+ iterationStart: 0.5,
+ easing: 'steps(2, jump-none)'
+ },
+ conditions: [
+ { currentTime: 0, progress: 0 },
+ { currentTime: 999, progress: 0 },
+ { currentTime: 1000, progress: 1 },
+ { currentTime: 1499, progress: 1 },
+ { currentTime: 1500, progress: 0 },
+ { currentTime: 1999, progress: 0 },
+ { currentTime: 2000, progress: 1 },
+ { currentTime: 2500, progress: 1 }
+ ]
+ },
+ {
+ description: 'Test bounds point of steps(jump-none) easing ' +
+ 'with iterationStart not at a transition point',
+ effect: {
+ delay: 1000,
+ duration: 1000,
+ fill: 'both',
+ iterationStart: 0.75,
+ easing: 'steps(2, jump-none)'
+ },
+ conditions: [
+ { currentTime: 0, progress: 1 },
+ { currentTime: 999, progress: 1 },
+ { currentTime: 1000, progress: 1 },
+ { currentTime: 1249, progress: 1 },
+ { currentTime: 1250, progress: 0 },
+ { currentTime: 1749, progress: 0 },
+ { currentTime: 1750, progress: 1 },
+ { currentTime: 2000, progress: 1 },
+ { currentTime: 2500, progress: 1 }
+ ]
+ },
+];
+
+for (const options of gStepTimingFunctionTests) {
+ test(t => {
+ const target = createDiv(t);
+ const animation = target.animate(null, options.effect);
+ for (const condition of options.conditions) {
+ animation.currentTime = condition.currentTime;
+ assert_equals(animation.effect.getComputedTiming().progress,
+ condition.progress,
+ `Progress at ${animation.currentTime}ms`);
+ }
+ }, options.description);
+}
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/web-animations/timing-model/timelines/document-timelines.html b/testing/web-platform/tests/web-animations/timing-model/timelines/document-timelines.html
new file mode 100644
index 0000000000..c71d73331c
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/timing-model/timelines/document-timelines.html
@@ -0,0 +1,50 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>Document timelines</title>
+<link rel="help" href="https://drafts.csswg.org/web-animations/#document-timelines">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../../testcommon.js"></script>
+<div id="log"></div>
+<script>
+'use strict';
+
+async_test(t => {
+ assert_greater_than_equal(document.timeline.currentTime, 0,
+ 'The current time is initially is positive or zero');
+
+ // document.timeline.currentTime should be set even before document
+ // load fires. We expect this code to be run before document load and hence
+ // the above assertion is sufficient.
+ // If the following assertion fails, this test needs to be redesigned.
+ assert_not_equals(document.readyState, 'complete',
+ 'Test is running prior to document load');
+
+ // Test that the document timeline's current time is measured from
+ // navigationStart.
+ //
+ // We can't just compare document.timeline.currentTime to
+ // window.performance.now() because currentTime is only updated on a sample
+ // so we use requestAnimationFrame instead.
+ window.requestAnimationFrame(rafTime => {
+ t.step(() => {
+ assert_equals(document.timeline.currentTime, rafTime,
+ 'The current time matches requestAnimationFrame time');
+ });
+ t.done();
+ });
+}, 'Document timelines report current time relative to navigationStart');
+
+async_test(t => {
+ window.requestAnimationFrame(rafTime => {
+ t.step(() => {
+ const iframe = document.createElement('iframe');
+ document.body.appendChild(iframe);
+ assert_greater_than_equal(iframe.contentDocument.timeline.currentTime, 0,
+ 'The current time of a new iframe is initially is positive or zero');
+ });
+ t.done();
+ });
+}, 'Child frames do not report negative initial times');
+
+</script>
diff --git a/testing/web-platform/tests/web-animations/timing-model/timelines/resources/target-frame.html b/testing/web-platform/tests/web-animations/timing-model/timelines/resources/target-frame.html
new file mode 100644
index 0000000000..18ee4fd8a2
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/timing-model/timelines/resources/target-frame.html
@@ -0,0 +1,76 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <style type="text/css">
+ #target {
+ background: green;
+ height: 50px;
+ width: 50px;
+ }
+ </style>
+</head>
+<body>
+ <div id="target"></div>
+</body>
+<script src="../../../testcommon.js"></script>
+<script type="text/javascript">
+ function sendResult(message) {
+ top.postMessage(message, '*');
+ }
+
+ function waitForAnimationReady(anim) {
+ // Resolution of the ready promise, though UA dependent, is expected to
+ // happen within a few frames. Throttling rendering of the frame owning
+ // the animation's timeline may delay resolution of the ready promise,
+ // resulting in a test failure.
+ let frameTimeout = 10;
+ let resolved = false;
+ return new Promise((resolve, reject) => {
+ anim.ready.then(() => {
+ resolved = true;
+ resolve('PASS');
+ });
+ const tick = () => {
+ requestAnimationFrame(() => {
+ if (!resolved) {
+ if (--frameTimeout == 0)
+ resolve('FAIL: Animation is still pending');
+ else
+ tick();
+ }
+ });
+ };
+ tick();
+ });
+ }
+
+ function verifyAnimationIsUpdating() {
+ return new Promise(resolve => {
+ waitForAnimationFrames(3).then(() => {
+ const opacity = getComputedStyle(target).opacity;
+ const result =
+ (opacity == 1) ? 'FAIL: opacity remained unchanged' : 'PASS';
+ resolve(result);
+ });
+ });
+ }
+
+ async function runTest() {
+ const anim = document.getAnimations()[0];
+ if (!anim) {
+ setResult('FAIL: Failed to create animation');
+ return;
+ }
+ waitForAnimationReady(anim).then(result => {
+ if (result != 'PASS') {
+ sendResult(result);
+ return;
+ }
+ verifyAnimationIsUpdating().then(result => {
+ sendResult(result);
+ });
+ });
+ }
+</script>
+</html>
diff --git a/testing/web-platform/tests/web-animations/timing-model/timelines/resources/timeline-frame.html b/testing/web-platform/tests/web-animations/timing-model/timelines/resources/timeline-frame.html
new file mode 100644
index 0000000000..9c8cdabc9d
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/timing-model/timelines/resources/timeline-frame.html
@@ -0,0 +1,20 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+</head>
+<script type="text/javascript">
+ const targetWindow = window.top.a;
+ const element = targetWindow.document.getElementById('target');
+ const keyframes = { opacity: [1, 0.2] };
+ const options = {
+ duration: 1000,
+ // Use this document's timeline rather then the timeline of the
+ // element's document.
+ timeline: document.timeline,
+ fill: 'forwards'
+ };
+ element.animate(keyframes, options);
+ targetWindow.runTest();
+</script>
+</html>
diff --git a/testing/web-platform/tests/web-animations/timing-model/timelines/sibling-iframe-timeline.html b/testing/web-platform/tests/web-animations/timing-model/timelines/sibling-iframe-timeline.html
new file mode 100644
index 0000000000..8a611c8579
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/timing-model/timelines/sibling-iframe-timeline.html
@@ -0,0 +1,58 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Animate using sibling iframe's timeline</title>
+</head>
+<body></body>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script type="text/javascript">
+ 'use strict';
+
+ function crossSiteUrl(filename) {
+ const url =
+ get_host_info().HTTP_REMOTE_ORIGIN +
+ '/web-animations/timing-model/timelines/resources/' +
+ filename;
+ return url;
+ }
+
+ function loadFrame(name, path, hidden) {
+ return new Promise(resolve => {
+ const frame = document.createElement('iframe');
+ if (hidden)
+ frame.style = 'visibility: hidden;';
+ frame.name = name;
+ document.body.appendChild(frame);
+ frame.onload = () => {
+ resolve();
+ }
+ frame.src = crossSiteUrl(path);
+ });
+ }
+
+ function waitForTestResults() {
+ return new Promise(resolve => {
+ const listener = (evt) => {
+ window.removeEventListener('message', listener);
+ resolve(evt.data);
+ };
+ window.addEventListener('message', listener);
+ });
+ }
+
+ promise_test(async t => {
+ const promise = waitForTestResults().then((data) => {
+ assert_equals(data, 'PASS');
+ });
+ // Animate an element in frame A.
+ await loadFrame('a', 'target-frame.html', false);
+ // Animation's timeline is in hidden frame B.
+ await loadFrame('b', 'timeline-frame.html', true);
+
+ return promise;
+ }, 'animation tied to another frame\'s timeline runs properly');
+</script>
+</html>
diff --git a/testing/web-platform/tests/web-animations/timing-model/timelines/timelines.html b/testing/web-platform/tests/web-animations/timing-model/timelines/timelines.html
new file mode 100644
index 0000000000..d570eed5c2
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/timing-model/timelines/timelines.html
@@ -0,0 +1,112 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>Timelines</title>
+<link rel="help" href="https://drafts.csswg.org/web-animations/#timelines">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../../testcommon.js"></script>
+<style>
+@keyframes opacity-animation {
+ from { opacity: 1; }
+ to { opacity: 0; }
+}
+</style>
+<div id="log"></div>
+<script>
+'use strict';
+
+promise_test(t => {
+ const valueAtStart = document.timeline.currentTime;
+ const timeAtStart = window.performance.now();
+ while (window.performance.now() - timeAtStart < 50) {
+ // Wait 50ms
+ }
+ assert_equals(document.timeline.currentTime, valueAtStart,
+ 'Timeline time does not change within an animation frame');
+ return waitForAnimationFrames(1).then(() => {
+ assert_greater_than(document.timeline.currentTime, valueAtStart,
+ 'Timeline time increases between animation frames');
+ });
+}, 'Timeline time increases once per animation frame');
+
+async_test(t => {
+ const iframe = document.createElement('iframe');
+ iframe.width = 10;
+ iframe.height = 10;
+
+ iframe.addEventListener('load', t.step_func(() => {
+ const iframeTimeline = iframe.contentDocument.timeline;
+ const valueAtStart = iframeTimeline.currentTime;
+ const timeAtStart = window.performance.now();
+ while (iframe.contentWindow.performance.now() - timeAtStart < 50) {
+ // Wait 50ms
+ }
+ assert_equals(iframeTimeline.currentTime, valueAtStart,
+ 'Timeline time within an iframe does not change within an '
+ + ' animation frame');
+
+ iframe.contentWindow.requestAnimationFrame(t.step_func_done(() => {
+ assert_greater_than(iframeTimeline.currentTime, valueAtStart,
+ 'Timeline time within an iframe increases between animation frames');
+ iframe.remove();
+ }));
+ }));
+
+ document.body.appendChild(iframe);
+}, 'Timeline time increases once per animation frame in an iframe');
+
+async_test(t => {
+ const startTime = document.timeline.currentTime;
+ let firstRafTime;
+
+ requestAnimationFrame(() => {
+ t.step(() => {
+ assert_greater_than_equal(document.timeline.currentTime, startTime,
+ 'Timeline time should have progressed');
+ firstRafTime = document.timeline.currentTime;
+ });
+ });
+
+ requestAnimationFrame(() => {
+ t.step(() => {
+ assert_equals(document.timeline.currentTime, firstRafTime,
+ 'Timeline time should be the same');
+ });
+ t.done();
+ });
+}, 'Timeline time should be the same for all RAF callbacks in an animation'
+ + ' frame');
+
+async_test(t => {
+ const div = createDiv(t);
+ const animation = div.animate(null, 100 * MS_PER_SEC);
+
+ animation.ready.then(t.step_func(() => {
+ const readyTimelineTime = document.timeline.currentTime;
+ requestAnimationFrame(t.step_func_done(() => {
+ assert_equals(readyTimelineTime, document.timeline.currentTime,
+ 'There should be a microtask checkpoint');
+ }));
+ }));
+}, 'Performs a microtask checkpoint after updating timelins');
+
+async_test(t => {
+ const div = createDiv(t);
+ let readyPromiseRan = false;
+ let finishedPromiseRan = false;
+ div.style.animation = 'opacity-animation 1ms';
+ let anim = div.getAnimations()[0];
+ anim.ready.then(t.step_func(() => {
+ readyPromiseRan = true;
+ }));
+ div.addEventListener('animationstart', t.step_func(() => {
+ assert_true(readyPromiseRan, 'It should run ready promise before animationstart event');
+ }));
+ anim.finished.then(t.step_func(() => {
+ finishedPromiseRan = true;
+ }));
+ div.addEventListener('animationend', t.step_func_done(() => {
+ assert_true(finishedPromiseRan, 'It should run finished promise before animationend event');
+ }));
+}, 'Runs finished promise before animation events');
+</script>
diff --git a/testing/web-platform/tests/web-animations/timing-model/timelines/update-and-send-events-replacement.html b/testing/web-platform/tests/web-animations/timing-model/timelines/update-and-send-events-replacement.html
new file mode 100644
index 0000000000..d6ed734831
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/timing-model/timelines/update-and-send-events-replacement.html
@@ -0,0 +1,1017 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>Update animations and send events (replacement)</title>
+<link rel="help" href="https://drafts.csswg.org/web-animations/#update-animations-and-send-events">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../../testcommon.js"></script>
+<style>
+@keyframes opacity-animation {
+ to { opacity: 1 }
+}
+</style>
+<div id="log"></div>
+<script>
+'use strict';
+
+promise_test(async t => {
+ const div = createDiv(t);
+
+ const animA = div.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' });
+ const animB = div.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' });
+ await animA.finished;
+
+ assert_equals(animA.replaceState, 'removed');
+ assert_equals(animB.replaceState, 'active');
+}, 'Removes an animation when another covers the same properties');
+
+promise_test(async t => {
+ const div = createDiv(t);
+
+ const animA = div.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' });
+ await animA.finished;
+
+ assert_equals(animA.replaceState, 'active');
+
+ const animB = div.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' });
+ await animB.finished;
+
+ assert_equals(animA.replaceState, 'removed');
+ assert_equals(animB.replaceState, 'active');
+}, 'Removes an animation after another animation finishes');
+
+promise_test(async t => {
+ const div = createDiv(t);
+
+ const animA = div.animate(
+ { opacity: 1, width: '100px' },
+ { duration: 1, fill: 'forwards' }
+ );
+ await animA.finished;
+
+ assert_equals(animA.replaceState, 'active');
+
+ const animB = div.animate(
+ { width: '200px' },
+ { duration: 1, fill: 'forwards' }
+ );
+ await animB.finished;
+
+ assert_equals(animA.replaceState, 'active');
+ assert_equals(animB.replaceState, 'active');
+
+ const animC = div.animate(
+ { opacity: 0.5 },
+ { duration: 1, fill: 'forwards' }
+ );
+ await animC.finished;
+
+ assert_equals(animA.replaceState, 'removed');
+ assert_equals(animB.replaceState, 'active');
+ assert_equals(animC.replaceState, 'active');
+}, 'Removes an animation after multiple other animations finish');
+
+promise_test(async t => {
+ const div = createDiv(t);
+
+ const animA = div.animate(
+ { opacity: 1 },
+ { duration: 100 * MS_PER_SEC, fill: 'forwards' }
+ );
+ const animB = div.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' });
+ await animB.finished;
+
+ assert_equals(animB.replaceState, 'active');
+ assert_equals(animB.replaceState, 'active');
+
+ // Seek animA to just before it finishes since we want to test the behavior
+ // when the animation finishes by the ticking of the timeline, not by seeking
+ // (that is covered in a separate test).
+
+ animA.currentTime = 99.99 * MS_PER_SEC;
+ await animA.finished;
+
+ assert_equals(animA.replaceState, 'removed');
+ assert_equals(animB.replaceState, 'active');
+}, 'Removes an animation after it finishes');
+
+promise_test(async t => {
+ const div = createDiv(t);
+
+ const animA = div.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' });
+ const animB = div.animate(
+ { opacity: 1 },
+ { duration: 100 * MS_PER_SEC, fill: 'forwards' }
+ );
+ await animA.finished;
+
+ assert_equals(animA.replaceState, 'active');
+ assert_equals(animB.replaceState, 'active');
+
+ animB.finish();
+
+ // Replacement should not happen until the next time the "update animations
+ // and send events" procedure runs.
+
+ assert_equals(animA.replaceState, 'active');
+ assert_equals(animB.replaceState, 'active');
+
+ await waitForNextFrame();
+
+ assert_equals(animA.replaceState, 'removed');
+ assert_equals(animB.replaceState, 'active');
+}, 'Removes an animation after seeking another animation');
+
+promise_test(async t => {
+ const div = createDiv(t);
+
+ const animA = div.animate(
+ { opacity: 1 },
+ { duration: 100 * MS_PER_SEC, fill: 'forwards' }
+ );
+ const animB = div.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' });
+ await animB.finished;
+
+ assert_equals(animA.replaceState, 'active');
+ assert_equals(animB.replaceState, 'active');
+
+ animA.finish();
+
+ // Replacement should not happen until the next time the "update animations
+ // and send events" procedure runs.
+
+ assert_equals(animA.replaceState, 'active');
+ assert_equals(animB.replaceState, 'active');
+
+ await waitForNextFrame();
+
+ assert_equals(animA.replaceState, 'removed');
+ assert_equals(animB.replaceState, 'active');
+}, 'Removes an animation after seeking it');
+
+promise_test(async t => {
+ const div = createDiv(t);
+
+ const animA = div.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' });
+ const animB = div.animate({ opacity: 1 }, 1);
+ await animA.finished;
+
+ assert_equals(animA.replaceState, 'active');
+ assert_equals(animB.replaceState, 'active');
+
+ animB.effect.updateTiming({ fill: 'forwards' });
+
+ // Replacement should not happen until the next time the "update animations
+ // and send events" procedure runs.
+
+ assert_equals(animA.replaceState, 'active');
+ assert_equals(animB.replaceState, 'active');
+
+ await waitForNextFrame();
+
+ assert_equals(animA.replaceState, 'removed');
+ assert_equals(animB.replaceState, 'active');
+}, 'Removes an animation after updating the fill mode of another animation');
+
+promise_test(async t => {
+ const div = createDiv(t);
+
+ const animA = div.animate({ opacity: 1 }, 1);
+ const animB = div.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' });
+ await animA.finished;
+
+ assert_equals(animA.replaceState, 'active');
+ assert_equals(animB.replaceState, 'active');
+
+ animA.effect.updateTiming({ fill: 'forwards' });
+
+ // Replacement should not happen until the next time the "update animations
+ // and send events" procedure runs.
+
+ assert_equals(animA.replaceState, 'active');
+ assert_equals(animB.replaceState, 'active');
+
+ await waitForNextFrame();
+
+ assert_equals(animA.replaceState, 'removed');
+ assert_equals(animB.replaceState, 'active');
+}, 'Removes an animation after updating its fill mode');
+
+promise_test(async t => {
+ const div = createDiv(t);
+
+ const animA = div.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' });
+ const animB = div.animate({ opacity: 1 }, 1);
+ await animA.finished;
+
+ assert_equals(animA.replaceState, 'active');
+ assert_equals(animB.replaceState, 'active');
+
+ animB.effect = new KeyframeEffect(
+ div,
+ { opacity: 1 },
+ {
+ duration: 1,
+ fill: 'forwards',
+ }
+ );
+
+ assert_equals(animA.replaceState, 'active');
+ assert_equals(animB.replaceState, 'active');
+
+ await waitForNextFrame();
+
+ assert_equals(animA.replaceState, 'removed');
+ assert_equals(animB.replaceState, 'active');
+}, "Removes an animation after updating another animation's effect to one with different timing");
+
+promise_test(async t => {
+ const div = createDiv(t);
+
+ const animA = div.animate({ opacity: 1 }, 1);
+ const animB = div.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' });
+ await animB.finished;
+
+ assert_equals(animA.replaceState, 'active');
+ assert_equals(animB.replaceState, 'active');
+
+ animA.effect = new KeyframeEffect(
+ div,
+ { opacity: 1 },
+ {
+ duration: 1,
+ fill: 'forwards',
+ }
+ );
+
+ assert_equals(animA.replaceState, 'active');
+ assert_equals(animB.replaceState, 'active');
+
+ await waitForNextFrame();
+
+ assert_equals(animA.replaceState, 'removed');
+ assert_equals(animB.replaceState, 'active');
+}, 'Removes an animation after updating its effect to one with different timing');
+
+promise_test(async t => {
+ const div = createDiv(t);
+
+ const animA = div.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' });
+ const animB = div.animate(
+ { opacity: 1 },
+ { duration: 100 * MS_PER_SEC, fill: 'forwards' }
+ );
+
+ await animA.finished;
+
+ // Set up a timeline that makes animB finished
+ animB.timeline = new DocumentTimeline({
+ originTime:
+ document.timeline.currentTime - 100 * MS_PER_SEC - animB.startTime,
+ });
+
+ assert_equals(animA.replaceState, 'active');
+ assert_equals(animB.replaceState, 'active');
+
+ await waitForNextFrame();
+
+ assert_equals(animA.replaceState, 'removed');
+ assert_equals(animB.replaceState, 'active');
+}, "Removes an animation after updating another animation's timeline");
+
+promise_test(async t => {
+ const div = createDiv(t);
+
+ const animA = div.animate(
+ { opacity: 1 },
+ { duration: 100 * MS_PER_SEC, fill: 'forwards' }
+ );
+ const animB = div.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' });
+
+ await animB.finished;
+
+ // Set up a timeline that makes animA finished
+ animA.timeline = new DocumentTimeline({
+ originTime:
+ document.timeline.currentTime - 100 * MS_PER_SEC - animA.startTime,
+ });
+
+ assert_equals(animA.replaceState, 'active');
+ assert_equals(animB.replaceState, 'active');
+
+ await waitForNextFrame();
+
+ assert_equals(animA.replaceState, 'removed');
+ assert_equals(animB.replaceState, 'active');
+}, 'Removes an animation after updating its timeline');
+
+promise_test(async t => {
+ const div = createDiv(t);
+
+ const animA = div.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' });
+ const animB = div.animate(
+ { width: '100px' },
+ { duration: 1, fill: 'forwards' }
+ );
+ await animA.finished;
+
+ assert_equals(animA.replaceState, 'active');
+ assert_equals(animB.replaceState, 'active');
+
+ animB.effect.setKeyframes({ width: '100px', opacity: 1 });
+
+ assert_equals(animA.replaceState, 'active');
+ assert_equals(animB.replaceState, 'active');
+
+ await waitForNextFrame();
+
+ assert_equals(animA.replaceState, 'removed');
+ assert_equals(animB.replaceState, 'active');
+}, "Removes an animation after updating another animation's effect's properties");
+
+promise_test(async t => {
+ const div = createDiv(t);
+
+ const animA = div.animate(
+ { opacity: 1, width: '100px' },
+ { duration: 1, fill: 'forwards' }
+ );
+ const animB = div.animate(
+ { width: '200px' },
+ { duration: 1, fill: 'forwards' }
+ );
+ await animA.finished;
+
+ assert_equals(animA.replaceState, 'active');
+ assert_equals(animB.replaceState, 'active');
+
+ animA.effect.setKeyframes({ width: '100px' });
+
+ assert_equals(animA.replaceState, 'active');
+ assert_equals(animB.replaceState, 'active');
+
+ await waitForNextFrame();
+
+ assert_equals(animA.replaceState, 'removed');
+ assert_equals(animB.replaceState, 'active');
+}, "Removes an animation after updating its effect's properties");
+
+promise_test(async t => {
+ const div = createDiv(t);
+
+ const animA = div.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' });
+ const animB = div.animate(
+ { width: '100px' },
+ { duration: 1, fill: 'forwards' }
+ );
+ await animA.finished;
+
+ assert_equals(animA.replaceState, 'active');
+ assert_equals(animB.replaceState, 'active');
+
+ animB.effect = new KeyframeEffect(
+ div,
+ { width: '100px', opacity: 1 },
+ { duration: 1, fill: 'forwards' }
+ );
+
+ assert_equals(animA.replaceState, 'active');
+ assert_equals(animB.replaceState, 'active');
+
+ await waitForNextFrame();
+
+ assert_equals(animA.replaceState, 'removed');
+ assert_equals(animB.replaceState, 'active');
+}, "Removes an animation after updating another animation's effect to one with different properties");
+
+promise_test(async t => {
+ const div = createDiv(t);
+
+ const animA = div.animate(
+ { opacity: 1, width: '100px' },
+ { duration: 1, fill: 'forwards' }
+ );
+ const animB = div.animate(
+ { width: '200px' },
+ { duration: 1, fill: 'forwards' }
+ );
+ await animA.finished;
+
+ assert_equals(animA.replaceState, 'active');
+ assert_equals(animB.replaceState, 'active');
+
+ animA.effect = new KeyframeEffect(
+ div,
+ { width: '100px' },
+ {
+ duration: 1,
+ fill: 'forwards',
+ }
+ );
+
+ assert_equals(animA.replaceState, 'active');
+ assert_equals(animB.replaceState, 'active');
+
+ await waitForNextFrame();
+
+ assert_equals(animA.replaceState, 'removed');
+ assert_equals(animB.replaceState, 'active');
+}, 'Removes an animation after updating its effect to one with different properties');
+
+promise_test(async t => {
+ const div = createDiv(t);
+
+ const animA = div.animate(
+ { marginLeft: '10px' },
+ { duration: 1, fill: 'forwards' }
+ );
+ const animB = div.animate(
+ { margin: '20px' },
+ { duration: 1, fill: 'forwards' }
+ );
+ await animA.finished;
+
+ assert_equals(animA.replaceState, 'removed');
+ assert_equals(animB.replaceState, 'active');
+}, 'Removes an animation when another animation uses a shorthand');
+
+promise_test(async t => {
+ const div = createDiv(t);
+
+ const animA = div.animate(
+ { margin: '10px' },
+ { duration: 1, fill: 'forwards' }
+ );
+ const animB = div.animate(
+ {
+ marginLeft: '10px',
+ marginTop: '20px',
+ marginRight: '30px',
+ marginBottom: '40px',
+ },
+ { duration: 1, fill: 'forwards' }
+ );
+ await animA.finished;
+
+ assert_equals(animA.replaceState, 'removed');
+ assert_equals(animB.replaceState, 'active');
+}, 'Removes an animation that uses a shorthand');
+
+promise_test(async t => {
+ const div = createDiv(t);
+
+ const animA = div.animate(
+ { marginLeft: '10px' },
+ { duration: 1, fill: 'forwards' }
+ );
+ const animB = div.animate(
+ { marginInlineStart: '20px' },
+ { duration: 1, fill: 'forwards' }
+ );
+ await animA.finished;
+
+ assert_equals(animA.replaceState, 'removed');
+ assert_equals(animB.replaceState, 'active');
+}, 'Removes an animation by another animation using logical properties');
+
+promise_test(async t => {
+ const div = createDiv(t);
+
+ const animA = div.animate(
+ { marginInlineStart: '10px' },
+ { duration: 1, fill: 'forwards' }
+ );
+ const animB = div.animate(
+ { marginLeft: '20px' },
+ { duration: 1, fill: 'forwards' }
+ );
+ await animA.finished;
+
+ assert_equals(animA.replaceState, 'removed');
+ assert_equals(animB.replaceState, 'active');
+}, 'Removes an animation using logical properties');
+
+promise_test(async t => {
+ const div = createDiv(t);
+
+ const animA = div.animate(
+ { marginTop: '10px' },
+ { duration: 1, fill: 'forwards' }
+ );
+ const animB = div.animate(
+ { marginInlineStart: '20px' },
+ { duration: 1, fill: 'forwards' }
+ );
+ await animA.finished;
+
+ assert_equals(animA.replaceState, 'active');
+ assert_equals(animB.replaceState, 'active');
+
+ div.style.writingMode = 'vertical-rl';
+
+ assert_equals(animA.replaceState, 'active');
+ assert_equals(animB.replaceState, 'active');
+
+ await waitForNextFrame();
+
+ assert_equals(animA.replaceState, 'removed');
+ assert_equals(animB.replaceState, 'active');
+}, 'Removes an animation by another animation using logical properties after updating the context');
+
+promise_test(async t => {
+ const divA = createDiv(t);
+ const divB = createDiv(t);
+
+ const animA = divA.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' });
+ const animB = divB.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' });
+ await animA.finished;
+
+ assert_equals(animA.replaceState, 'active');
+ assert_equals(animB.replaceState, 'active');
+
+ animB.effect.target = divA;
+
+ assert_equals(animA.replaceState, 'active');
+ assert_equals(animB.replaceState, 'active');
+
+ await waitForNextFrame();
+
+ assert_equals(animA.replaceState, 'removed');
+ assert_equals(animB.replaceState, 'active');
+}, "Removes an animation after updating another animation's effect's target");
+
+promise_test(async t => {
+ const divA = createDiv(t);
+ const divB = createDiv(t);
+
+ const animA = divA.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' });
+ const animB = divB.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' });
+ await animA.finished;
+
+ assert_equals(animA.replaceState, 'active');
+ assert_equals(animB.replaceState, 'active');
+
+ animA.effect.target = divB;
+
+ assert_equals(animA.replaceState, 'active');
+ assert_equals(animB.replaceState, 'active');
+
+ await waitForNextFrame();
+
+ assert_equals(animA.replaceState, 'removed');
+ assert_equals(animB.replaceState, 'active');
+}, "Removes an animation after updating its effect's target");
+
+promise_test(async t => {
+ const divA = createDiv(t);
+ const divB = createDiv(t);
+
+ const animA = divA.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' });
+ const animB = divB.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' });
+ await animA.finished;
+
+ assert_equals(animA.replaceState, 'active');
+ assert_equals(animB.replaceState, 'active');
+
+ animB.effect = new KeyframeEffect(
+ divA,
+ { opacity: 1 },
+ {
+ duration: 1,
+ fill: 'forwards',
+ }
+ );
+
+ assert_equals(animA.replaceState, 'active');
+ assert_equals(animB.replaceState, 'active');
+
+ await waitForNextFrame();
+
+ assert_equals(animA.replaceState, 'removed');
+ assert_equals(animB.replaceState, 'active');
+}, "Removes an animation after updating another animation's effect to one with a different target");
+
+promise_test(async t => {
+ const divA = createDiv(t);
+ const divB = createDiv(t);
+
+ const animA = divA.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' });
+ const animB = divB.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' });
+ await animA.finished;
+
+ assert_equals(animA.replaceState, 'active');
+ assert_equals(animB.replaceState, 'active');
+
+ animA.effect = new KeyframeEffect(
+ divB,
+ { opacity: 1 },
+ {
+ duration: 1,
+ fill: 'forwards',
+ }
+ );
+
+ assert_equals(animA.replaceState, 'active');
+ assert_equals(animB.replaceState, 'active');
+
+ await waitForNextFrame();
+
+ assert_equals(animA.replaceState, 'removed');
+ assert_equals(animB.replaceState, 'active');
+}, 'Removes an animation after updating its effect to one with a different target');
+
+promise_test(async t => {
+ const div = createDiv(t);
+ div.style.animation = 'opacity-animation 1ms forwards';
+ const cssAnimation = div.getAnimations()[0];
+
+ const scriptAnimation = div.animate(
+ { opacity: 1 },
+ {
+ duration: 1,
+ fill: 'forwards',
+ }
+ );
+ await scriptAnimation.finished;
+
+ assert_equals(cssAnimation.replaceState, 'active');
+ assert_equals(scriptAnimation.replaceState, 'active');
+}, 'Does NOT remove a CSS animation tied to markup');
+
+promise_test(async t => {
+ const div = createDiv(t);
+ div.style.animation = 'opacity-animation 1ms forwards';
+ const cssAnimation = div.getAnimations()[0];
+
+ // Break tie to markup
+ div.style.animationName = 'none';
+ assert_equals(cssAnimation.playState, 'idle');
+
+ // Restart animation
+ cssAnimation.play();
+
+ const scriptAnimation = div.animate(
+ { opacity: 1 },
+ {
+ duration: 1,
+ fill: 'forwards',
+ }
+ );
+ await scriptAnimation.finished;
+
+ assert_equals(cssAnimation.replaceState, 'removed');
+ assert_equals(scriptAnimation.replaceState, 'active');
+}, 'Removes a CSS animation no longer tied to markup');
+
+promise_test(async t => {
+ // Setup transition
+ const div = createDiv(t);
+ div.style.opacity = '0';
+ div.style.transition = 'opacity 1ms';
+ getComputedStyle(div).opacity;
+ div.style.opacity = '1';
+ const cssTransition = div.getAnimations()[0];
+ cssTransition.effect.updateTiming({ fill: 'forwards' });
+
+ const scriptAnimation = div.animate(
+ { opacity: 1 },
+ {
+ duration: 1,
+ fill: 'forwards',
+ }
+ );
+ await scriptAnimation.finished;
+
+ assert_equals(cssTransition.replaceState, 'active');
+ assert_equals(scriptAnimation.replaceState, 'active');
+}, 'Does NOT remove a CSS transition tied to markup');
+
+promise_test(async t => {
+ // Setup transition
+ const div = createDiv(t);
+ div.style.opacity = '0';
+ div.style.transition = 'opacity 1ms';
+ getComputedStyle(div).opacity;
+ div.style.opacity = '1';
+ const cssTransition = div.getAnimations()[0];
+ cssTransition.effect.updateTiming({ fill: 'forwards' });
+
+ // Break tie to markup
+ div.style.transitionProperty = 'none';
+ assert_equals(cssTransition.playState, 'idle');
+
+ // Restart transition
+ cssTransition.play();
+
+ const scriptAnimation = div.animate(
+ { opacity: 1 },
+ {
+ duration: 1,
+ fill: 'forwards',
+ }
+ );
+ await scriptAnimation.finished;
+
+ assert_equals(cssTransition.replaceState, 'removed');
+ assert_equals(scriptAnimation.replaceState, 'active');
+}, 'Removes a CSS transition no longer tied to markup');
+
+promise_test(async t => {
+ const div = createDiv(t);
+
+ const animA = div.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' });
+ const animB = div.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' });
+ const eventWatcher = new EventWatcher(t, animA, 'remove');
+
+ const event = await eventWatcher.wait_for('remove');
+
+ assert_times_equal(event.timelineTime, document.timeline.currentTime);
+ assert_times_equal(event.currentTime, 1);
+}, 'Dispatches an event when removing');
+
+promise_test(async t => {
+ const div = createDiv(t);
+
+ const animA = div.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' });
+ const animB = div.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' });
+ const eventWatcher = new EventWatcher(t, animA, 'remove');
+
+ await eventWatcher.wait_for('remove');
+
+ // Check we don't get another event
+ animA.addEventListener(
+ 'remove',
+ t.step_func(() => {
+ assert_unreached('remove event should not be fired a second time');
+ })
+ );
+
+ // Restart animation
+ animA.play();
+
+ await waitForNextFrame();
+
+ // Finish animation
+ animA.finish();
+
+ await waitForNextFrame();
+}, 'Does NOT dispatch a remove event twice');
+
+promise_test(async t => {
+ const div = createDiv(t);
+
+ const animA = div.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' });
+ const animB = div.animate(
+ { opacity: 1 },
+ { duration: 100 * MS_PER_SEC, fill: 'forwards' }
+ );
+ await animA.finished;
+
+ assert_equals(animA.replaceState, 'active');
+
+ animB.finish();
+ animB.currentTime = 0;
+
+ await waitForNextFrame();
+
+ assert_equals(animA.replaceState, 'active');
+}, "Does NOT remove an animation after making a redundant change to another animation's current time");
+
+promise_test(async t => {
+ const div = createDiv(t);
+
+ const animA = div.animate(
+ { opacity: 1 },
+ { duration: 100 * MS_PER_SEC, fill: 'forwards' }
+ );
+ const animB = div.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' });
+ await animB.finished;
+
+ assert_equals(animA.replaceState, 'active');
+
+ animA.finish();
+ animA.currentTime = 0;
+
+ await waitForNextFrame();
+
+ assert_equals(animA.replaceState, 'active');
+}, 'Does NOT remove an animation after making a redundant change to its current time');
+
+promise_test(async t => {
+ const div = createDiv(t);
+
+ const animA = div.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' });
+ const animB = div.animate(
+ { opacity: 1 },
+ { duration: 100 * MS_PER_SEC, fill: 'forwards' }
+ );
+ await animA.finished;
+
+ assert_equals(animA.replaceState, 'active');
+
+ // Set up a timeline that makes animB finished but then restore it
+ animB.timeline = new DocumentTimeline({
+ originTime:
+ document.timeline.currentTime - 100 * MS_PER_SEC - animB.startTime,
+ });
+ animB.timeline = document.timeline;
+
+ await waitForNextFrame();
+
+ assert_equals(animA.replaceState, 'active');
+}, "Does NOT remove an animation after making a redundant change to another animation's timeline");
+
+promise_test(async t => {
+ const div = createDiv(t);
+
+ const animA = div.animate(
+ { opacity: 1 },
+ { duration: 100 * MS_PER_SEC, fill: 'forwards' }
+ );
+ const animB = div.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' });
+ await animB.finished;
+
+ assert_equals(animA.replaceState, 'active');
+
+ // Set up a timeline that makes animA finished but then restore it
+ animA.timeline = new DocumentTimeline({
+ originTime:
+ document.timeline.currentTime - 100 * MS_PER_SEC - animA.startTime,
+ });
+ animA.timeline = document.timeline;
+
+ await waitForNextFrame();
+
+ assert_equals(animA.replaceState, 'active');
+}, 'Does NOT remove an animation after making a redundant change to its timeline');
+
+promise_test(async t => {
+ const div = createDiv(t);
+ const animA = div.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' });
+ const animB = div.animate(
+ { marginLeft: '100px' },
+ {
+ duration: 1,
+ fill: 'forwards',
+ }
+ );
+ await animA.finished;
+
+ assert_equals(animA.replaceState, 'active');
+
+ // Redundant change
+ animB.effect.setKeyframes({ marginLeft: '100px', opacity: 1 });
+ animB.effect.setKeyframes({ marginLeft: '100px' });
+
+ await waitForNextFrame();
+
+ assert_equals(animA.replaceState, 'active');
+}, "Does NOT remove an animation after making a redundant change to another animation's effect's properties");
+
+promise_test(async t => {
+ const div = createDiv(t);
+ const animA = div.animate(
+ { marginLeft: '100px' },
+ {
+ duration: 1,
+ fill: 'forwards',
+ }
+ );
+ const animB = div.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' });
+ await animA.finished;
+
+ assert_equals(animA.replaceState, 'active');
+
+ // Redundant change
+ animA.effect.setKeyframes({ opacity: 1 });
+ animA.effect.setKeyframes({ marginLeft: '100px' });
+
+ await waitForNextFrame();
+
+ assert_equals(animA.replaceState, 'active');
+}, "Does NOT remove an animation after making a redundant change to its effect's properties");
+
+promise_test(async t => {
+ const div = createDiv(t);
+
+ const animA = div.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' });
+ const animB = div.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' });
+ animB.timeline = new DocumentTimeline();
+
+ await animA.finished;
+
+ // If, for example, we only update the timeline for animA before checking
+ // replacement state, then animB will not be finished and animA will not be
+ // replaced.
+
+ assert_equals(animA.replaceState, 'removed');
+ assert_equals(animB.replaceState, 'active');
+}, 'Updates ALL timelines before checking for replacement');
+
+promise_test(async t => {
+ const div = createDiv(t);
+ const animA = div.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' });
+ const animB = div.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' });
+
+ const events = [];
+ const logEvent = (targetName, eventType) => {
+ events.push(`${targetName}:${eventType}`);
+ };
+
+ animA.addEventListener('finish', () => logEvent('animA', 'finish'));
+ animA.addEventListener('remove', () => logEvent('animA', 'remove'));
+ animB.addEventListener('finish', () => logEvent('animB', 'finish'));
+ animB.addEventListener('remove', () => logEvent('animB', 'remove'));
+
+ await animA.finished;
+
+ // Allow all events to be dispatched
+
+ await waitForNextFrame();
+
+ assert_array_equals(events, [
+ 'animA:finish',
+ 'animB:finish',
+ 'animA:remove',
+ ]);
+}, 'Dispatches remove events after finish events');
+
+promise_test(async t => {
+ const div = createDiv(t);
+ const animA = div.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' });
+ const animB = div.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' });
+
+ const eventWatcher = new EventWatcher(t, animA, 'remove');
+
+ await animA.finished;
+
+ let rAFReceived = false;
+ requestAnimationFrame(() => (rAFReceived = true));
+
+ await eventWatcher.wait_for('remove');
+
+ assert_false(
+ rAFReceived,
+ 'remove event should be fired before requestAnimationFrame'
+ );
+}, 'Fires remove event before requestAnimationFrame');
+
+promise_test(async t => {
+ const div = createDiv(t);
+ const animA = div.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' });
+ const animB = div.animate(
+ { width: '100px' },
+ { duration: 1, fill: 'forwards' }
+ );
+ const animC = div.animate(
+ { opacity: 0.5, width: '200px' },
+ { duration: 1, fill: 'forwards' }
+ );
+
+ // In the event handler for animA (which should be fired before that of animB)
+ // we make a change to animC so that it no longer covers animB.
+ //
+ // If the remove event for animB is not already queued by this point, it will
+ // fail to fire.
+ animA.addEventListener('remove', () => {
+ animC.effect.setKeyframes({
+ opacity: 0.5,
+ });
+ });
+
+ const eventWatcher = new EventWatcher(t, animB, 'remove');
+ await eventWatcher.wait_for('remove');
+
+ assert_equals(animA.replaceState, 'removed');
+ assert_equals(animB.replaceState, 'removed');
+ assert_equals(animC.replaceState, 'active');
+}, 'Queues all remove events before running them');
+
+promise_test(async t => {
+ const outerIframe = document.createElement('iframe');
+ outerIframe.width = 10;
+ outerIframe.height = 10;
+ await insertFrameAndAwaitLoad(t, outerIframe, document);
+
+ const innerIframe = document.createElement('iframe');
+ innerIframe.width = 10;
+ innerIframe.height = 10;
+ await insertFrameAndAwaitLoad(t, innerIframe, outerIframe.contentDocument);
+
+ const div = createDiv(t, innerIframe.contentDocument);
+
+ const animA = div.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' });
+ const animB = div.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' });
+
+ // Sanity check: The timeline for these animations should be the default
+ // document timeline for div.
+ assert_equals(animA.timeline, innerIframe.contentDocument.timeline);
+ assert_equals(animB.timeline, innerIframe.contentDocument.timeline);
+
+ await animA.finished;
+
+ assert_equals(animA.replaceState, 'removed');
+ assert_equals(animB.replaceState, 'active');
+}, 'Performs removal in deeply nested iframes');
+
+</script>
diff --git a/testing/web-platform/tests/web-animations/timing-model/timelines/update-and-send-events.html b/testing/web-platform/tests/web-animations/timing-model/timelines/update-and-send-events.html
new file mode 100644
index 0000000000..255e013f27
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/timing-model/timelines/update-and-send-events.html
@@ -0,0 +1,257 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>Update animations and send events</title>
+<meta name="timeout" content="long">
+<link rel="help" href="https://drafts.csswg.org/web-animations/#update-animations-and-send-events">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../../testcommon.js"></script>
+<div id="log"></div>
+<script>
+'use strict';
+
+promise_test(async t => {
+ const div = createDiv(t);
+ const animation = div.animate(null, 100 * MS_PER_SEC);
+
+ // The ready promise should be resolved as part of micro-task checkpoint
+ // after updating the current time of all timeslines in the procedure to
+ // "update animations and send events".
+ await animation.ready;
+
+ let rAFReceived = false;
+ requestAnimationFrame(() => rAFReceived = true);
+
+ const eventWatcher = new EventWatcher(t, animation, 'cancel');
+ animation.cancel();
+
+ await eventWatcher.wait_for('cancel');
+
+ assert_false(rAFReceived,
+ 'cancel event should be fired before requestAnimationFrame');
+}, 'Fires cancel event before requestAnimationFrame');
+
+promise_test(async t => {
+ const div = createDiv(t);
+ const animation = div.animate(null, 100 * MS_PER_SEC);
+
+ // Like the above test, the ready promise should be resolved micro-task
+ // checkpoint after updating the current time of all timeslines in the
+ // procedure to "update animations and send events".
+ await animation.ready;
+
+ let rAFReceived = false;
+ requestAnimationFrame(() => rAFReceived = true);
+
+ const eventWatcher = new EventWatcher(t, animation, 'finish');
+ animation.finish();
+
+ await eventWatcher.wait_for('finish');
+
+ assert_false(rAFReceived,
+ 'finish event should be fired before requestAnimationFrame');
+}, 'Fires finish event before requestAnimationFrame');
+
+function animationType(anim) {
+ if (anim instanceof CSSAnimation) {
+ return 'CSSAnimation';
+ } else if (anim instanceof CSSTransition) {
+ return 'CSSTransition';
+ } else {
+ return 'ScriptAnimation';
+ }
+}
+
+promise_test(async t => {
+ createStyle(t, { '@keyframes anim': '' });
+ const div = createDiv(t);
+
+ getComputedStyle(div).marginLeft;
+ div.style = 'animation: anim 100s; ' +
+ 'transition: margin-left 100s; ' +
+ 'margin-left: 100px;';
+ div.animate(null, 100 * MS_PER_SEC);
+ const animations = div.getAnimations();
+
+ let receivedEvents = [];
+ animations.forEach(anim => {
+ anim.onfinish = event => {
+ receivedEvents.push({
+ type: animationType(anim) + ':' + event.type,
+ timeStamp: event.timeStamp
+ });
+ };
+ });
+
+ await Promise.all(animations.map(anim => anim.ready));
+
+ // Setting current time to the time just before the effect end.
+ animations.forEach(anim => anim.currentTime = 100 * MS_PER_SEC - 1);
+
+ await waitForNextFrame();
+
+ assert_array_equals(receivedEvents.map(event => event.type),
+ [ 'CSSTransition:finish', 'CSSAnimation:finish',
+ 'ScriptAnimation:finish' ],
+ 'finish events for various animation type should be sorted by composite ' +
+ 'order');
+}, 'Sorts finish events by composite order');
+
+promise_test(async t => {
+ createStyle(t, { '@keyframes anim': '' });
+ const div = createDiv(t);
+
+ let receivedEvents = [];
+ function receiveEvent(type, timeStamp) {
+ receivedEvents.push({ type, timeStamp });
+ }
+
+ div.onanimationcancel = event => receiveEvent(event.type, event.timeStamp);
+ div.ontransitioncancel = event => receiveEvent(event.type, event.timeStamp);
+
+ getComputedStyle(div).marginLeft;
+ div.style = 'animation: anim 100s; ' +
+ 'transition: margin-left 100s; ' +
+ 'margin-left: 100px;';
+ div.animate(null, 100 * MS_PER_SEC);
+ const animations = div.getAnimations();
+
+ animations.forEach(anim => {
+ anim.oncancel = event => {
+ receiveEvent(animationType(anim) + ':' + event.type, event.timeStamp);
+ };
+ });
+
+ await Promise.all(animations.map(anim => anim.ready));
+
+ const timeInAnimationReady = document.timeline.currentTime;
+
+ // Call cancel() in reverse composite order. I.e. canceling for script
+ // animation happen first, then for CSS animation and CSS transition.
+ // 'cancel' events for these animations should be sorted by composite
+ // order.
+ animations.reverse().forEach(anim => anim.cancel());
+
+ // requestAnimationFrame callback which is actually the _same_ frame since we
+ // are currently operating in the `ready` callbac of the animations which
+ // happens as part of the "Update animations and send events" procedure
+ // _before_ we run animation frame callbacks.
+ await waitForAnimationFrames(1);
+
+ assert_times_equal(timeInAnimationReady, document.timeline.currentTime,
+ 'A rAF callback should happen in the same frame');
+
+ assert_array_equals(receivedEvents.map(event => event.type),
+ // This ordering needs more clarification in the spec, but the intention is
+ // that the cancel playback event fires before the equivalent CSS cancel
+ // event in each case.
+ [ 'CSSTransition:cancel', 'CSSAnimation:cancel', 'ScriptAnimation:cancel',
+ 'transitioncancel', 'animationcancel' ],
+ 'cancel events should be sorted by composite order');
+}, 'Sorts cancel events by composite order');
+
+promise_test(async t => {
+ const div = createDiv(t);
+ getComputedStyle(div).marginLeft;
+ div.style = 'transition: margin-left 100s; margin-left: 100px;';
+ const anim = div.getAnimations()[0];
+
+ let receivedEvents = [];
+ anim.oncancel = event => receivedEvents.push(event);
+
+ const eventWatcher = new EventWatcher(t, div, 'transitionstart');
+ await eventWatcher.wait_for('transitionstart');
+
+ const timeInEventCallback = document.timeline.currentTime;
+
+ // Calling cancel() queues a cancel event
+ anim.cancel();
+
+ await waitForAnimationFrames(1);
+ assert_times_equal(timeInEventCallback, document.timeline.currentTime,
+ 'A rAF callback should happen in the same frame');
+
+ assert_array_equals(receivedEvents, [],
+ 'The queued cancel event shouldn\'t be dispatched in the same frame');
+
+ await waitForAnimationFrames(1);
+ assert_array_equals(receivedEvents.map(event => event.type), ['cancel'],
+ 'The cancel event should be dispatched in a later frame');
+}, 'Queues a cancel event in transitionstart event callback');
+
+promise_test(async t => {
+ const div = createDiv(t);
+ getComputedStyle(div).marginLeft;
+ div.style = 'transition: margin-left 100s; margin-left: 100px;';
+ const anim = div.getAnimations()[0];
+
+ let receivedEvents = [];
+ anim.oncancel = event => receivedEvents.push(event);
+ div.ontransitioncancel = event => receivedEvents.push(event);
+
+ await anim.ready;
+
+ anim.cancel();
+
+ await waitForAnimationFrames(1);
+
+ assert_array_equals(receivedEvents.map(event => event.type),
+ [ 'cancel', 'transitioncancel' ],
+ 'Playback and CSS events for the same transition should be sorted by ' +
+ 'schedule event time and composite order');
+}, 'Sorts events for the same transition');
+
+promise_test(async t => {
+ const div = createDiv(t);
+ const anim = div.animate(null, 100 * MS_PER_SEC);
+
+ let receivedEvents = [];
+ anim.oncancel = event => receivedEvents.push(event);
+ anim.onfinish = event => receivedEvents.push(event);
+
+ await anim.ready;
+
+ anim.finish();
+ anim.cancel();
+
+ await waitForAnimationFrames(1);
+
+ assert_array_equals(receivedEvents.map(event => event.type),
+ [ 'finish', 'cancel' ],
+ 'Calling finish() synchronously queues a finish event when updating the ' +
+ 'finish state so it should appear before the cancel event');
+}, 'Playback events with the same timeline retain the order in which they are' +
+ 'queued');
+
+promise_test(async t => {
+ const div = createDiv(t);
+
+ // Create two animations with separate timelines
+
+ const timelineA = document.timeline;
+ const animA = div.animate(null, 100 * MS_PER_SEC);
+
+ const timelineB = new DocumentTimeline();
+ const animB = new Animation(
+ new KeyframeEffect(div, null, 100 * MS_PER_SEC),
+ timelineB
+ );
+ animB.play();
+
+ animA.currentTime = 99.9 * MS_PER_SEC;
+ animB.currentTime = 99.9 * MS_PER_SEC;
+
+ // When the next tick happens both animations should be updated, and we will
+ // notice that they are now finished. As a result their finished promise
+ // callbacks should be queued. All of that should happen before we run the
+ // next microtask checkpoint and actually run the promise callbacks and
+ // hence the calls to cancel should not stop the existing callbacks from
+ // being run.
+
+ animA.finished.then(() => { animB.cancel() });
+ animB.finished.then(() => { animA.cancel() });
+
+ await Promise.all([animA.finished, animB.finished]);
+}, 'All timelines are updated before running microtasks');
+
+</script>