summaryrefslogtreecommitdiffstats
path: root/testing/web-platform/tests/web-animations
diff options
context:
space:
mode:
authorMatt A. Tobin <mattatobin@localhost.localdomain>2018-02-02 04:16:08 -0500
committerMatt A. Tobin <mattatobin@localhost.localdomain>2018-02-02 04:16:08 -0500
commit5f8de423f190bbb79a62f804151bc24824fa32d8 (patch)
tree10027f336435511475e392454359edea8e25895d /testing/web-platform/tests/web-animations
parent49ee0794b5d912db1f95dce6eb52d781dc210db5 (diff)
downloadUXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.gz
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.lz
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.xz
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.zip
Add m-esr52 at 52.6.0
Diffstat (limited to 'testing/web-platform/tests/web-animations')
-rw-r--r--testing/web-platform/tests/web-animations/OWNERS1
-rw-r--r--testing/web-platform/tests/web-animations/README.md107
-rw-r--r--testing/web-platform/tests/web-animations/animation-model/animation-types/discrete-animation.html135
-rw-r--r--testing/web-platform/tests/web-animations/animation-model/animation-types/spacing-keyframes-shapes.html152
-rw-r--r--testing/web-platform/tests/web-animations/animation-model/animation-types/type-per-property.html1198
-rw-r--r--testing/web-platform/tests/web-animations/animation-model/keyframe-effects/effect-value-context.html103
-rw-r--r--testing/web-platform/tests/web-animations/animation-model/keyframe-effects/spacing-keyframes.html391
-rw-r--r--testing/web-platform/tests/web-animations/animation-model/keyframe-effects/the-effect-value-of-a-keyframe-effect.html114
-rw-r--r--testing/web-platform/tests/web-animations/interfaces/Animatable/animate.html180
-rw-r--r--testing/web-platform/tests/web-animations/interfaces/Animation/cancel.html60
-rw-r--r--testing/web-platform/tests/web-animations/interfaces/Animation/constructor.html115
-rw-r--r--testing/web-platform/tests/web-animations/interfaces/Animation/effect.html23
-rw-r--r--testing/web-platform/tests/web-animations/interfaces/Animation/finish.html246
-rw-r--r--testing/web-platform/tests/web-animations/interfaces/Animation/finished.html370
-rw-r--r--testing/web-platform/tests/web-animations/interfaces/Animation/id.html28
-rw-r--r--testing/web-platform/tests/web-animations/interfaces/Animation/oncancel.html33
-rw-r--r--testing/web-platform/tests/web-animations/interfaces/Animation/onfinish.html121
-rw-r--r--testing/web-platform/tests/web-animations/interfaces/Animation/pause.html98
-rw-r--r--testing/web-platform/tests/web-animations/interfaces/Animation/play.html34
-rw-r--r--testing/web-platform/tests/web-animations/interfaces/Animation/playState.html53
-rw-r--r--testing/web-platform/tests/web-animations/interfaces/Animation/playbackRate.html86
-rw-r--r--testing/web-platform/tests/web-animations/interfaces/Animation/ready.html96
-rw-r--r--testing/web-platform/tests/web-animations/interfaces/Animation/reverse.html158
-rw-r--r--testing/web-platform/tests/web-animations/interfaces/Animation/startTime.html55
-rw-r--r--testing/web-platform/tests/web-animations/interfaces/AnimationEffectTiming/delay.html61
-rw-r--r--testing/web-platform/tests/web-animations/interfaces/AnimationEffectTiming/direction.html27
-rw-r--r--testing/web-platform/tests/web-animations/interfaces/AnimationEffectTiming/duration.html148
-rw-r--r--testing/web-platform/tests/web-animations/interfaces/AnimationEffectTiming/easing.html83
-rw-r--r--testing/web-platform/tests/web-animations/interfaces/AnimationEffectTiming/endDelay.html84
-rw-r--r--testing/web-platform/tests/web-animations/interfaces/AnimationEffectTiming/fill.html24
-rw-r--r--testing/web-platform/tests/web-animations/interfaces/AnimationEffectTiming/getAnimations.html91
-rw-r--r--testing/web-platform/tests/web-animations/interfaces/AnimationEffectTiming/getComputedStyle.html172
-rw-r--r--testing/web-platform/tests/web-animations/interfaces/AnimationEffectTiming/iterationStart.html72
-rw-r--r--testing/web-platform/tests/web-animations/interfaces/AnimationEffectTiming/iterations.html56
-rw-r--r--testing/web-platform/tests/web-animations/interfaces/AnimationTimeline/document-timeline.html59
-rw-r--r--testing/web-platform/tests/web-animations/interfaces/AnimationTimeline/idlharness.html36
-rw-r--r--testing/web-platform/tests/web-animations/interfaces/Document/getAnimations.html55
-rw-r--r--testing/web-platform/tests/web-animations/interfaces/DocumentTimeline/constructor.html42
-rw-r--r--testing/web-platform/tests/web-animations/interfaces/KeyframeEffect/constructor.html278
-rw-r--r--testing/web-platform/tests/web-animations/interfaces/KeyframeEffect/copy-contructor.html27
-rw-r--r--testing/web-platform/tests/web-animations/interfaces/KeyframeEffect/effect-easing.html683
-rw-r--r--testing/web-platform/tests/web-animations/interfaces/KeyframeEffect/getComputedTiming.html212
-rw-r--r--testing/web-platform/tests/web-animations/interfaces/KeyframeEffect/iterationComposite.html693
-rw-r--r--testing/web-platform/tests/web-animations/interfaces/KeyframeEffect/processing-a-keyframes-argument.html329
-rw-r--r--testing/web-platform/tests/web-animations/interfaces/KeyframeEffect/setKeyframes.html50
-rw-r--r--testing/web-platform/tests/web-animations/interfaces/KeyframeEffect/setTarget.html160
-rw-r--r--testing/web-platform/tests/web-animations/interfaces/KeyframeEffect/spacing.html60
-rw-r--r--testing/web-platform/tests/web-animations/interfaces/KeyframeEffectReadOnly/copy-contructor.html94
-rw-r--r--testing/web-platform/tests/web-animations/interfaces/KeyframeEffectReadOnly/spacing.html237
-rw-r--r--testing/web-platform/tests/web-animations/resources/effect-easing-tests.js98
-rw-r--r--testing/web-platform/tests/web-animations/resources/keyframe-utils.js563
-rw-r--r--testing/web-platform/tests/web-animations/testcommon.js176
-rw-r--r--testing/web-platform/tests/web-animations/timing-model/animation-effects/active-time.html142
-rw-r--r--testing/web-platform/tests/web-animations/timing-model/animation-effects/current-iteration.html584
-rw-r--r--testing/web-platform/tests/web-animations/timing-model/animation-effects/phases-and-states.html191
-rw-r--r--testing/web-platform/tests/web-animations/timing-model/animation-effects/simple-iteration-progress.html575
-rw-r--r--testing/web-platform/tests/web-animations/timing-model/animations/current-time.html65
-rw-r--r--testing/web-platform/tests/web-animations/timing-model/animations/set-the-animation-start-time.html207
-rw-r--r--testing/web-platform/tests/web-animations/timing-model/animations/set-the-target-effect-of-an-animation.html95
-rw-r--r--testing/web-platform/tests/web-animations/timing-model/animations/set-the-timeline-of-an-animation.html276
-rw-r--r--testing/web-platform/tests/web-animations/timing-model/animations/updating-the-finished-state.html331
61 files changed, 11093 insertions, 0 deletions
diff --git a/testing/web-platform/tests/web-animations/OWNERS b/testing/web-platform/tests/web-animations/OWNERS
new file mode 100644
index 000000000..fd38f5e5b
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/OWNERS
@@ -0,0 +1 @@
+@birtles
diff --git a/testing/web-platform/tests/web-animations/README.md b/testing/web-platform/tests/web-animations/README.md
new file mode 100644
index 000000000..c6d7c72c2
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/README.md
@@ -0,0 +1,107 @@
+Web Animations Test Suite
+=========================
+
+Specification: https://w3c.github.io/web-animations/
+
+
+Guidelines for writing tests
+----------------------------
+
+* Try to follow the spec outline where possible.
+
+ For example, if you want to test setting the start time, you might be
+ tempted to put all the tests in:
+
+ > `/web-animations/interfaces/Animation/startTime.html`
+
+ However, in the spec most of the logic is in the &ldquo;Set the animation
+ start time&ldquo; procedure in the &ldquo;Timing model&rdquo; section.
+
+ Instead, try something like:
+
+ > * `/web-animations/timing-model/animations/set-the-animation-start-time.html`<br>
+ > Tests all the branches and inputs to the procedure as defined in the
+ > spec (using the `Animation.startTime` API).
+ > * `/web-animations/interfaces/Animation/startTime.html`<br>
+ > Tests API-layer specific issues like mapping unresolved values to
+ > null, etc.
+
+ On that note, two levels of subdirectories is enough even if the spec has
+ deeper nesting.
+
+ Note that most of the existing tests in the suite _don't_ do this well yet.
+ That's the direction we're heading, however.
+
+* Test the spec.
+
+ * If the spec defines a timing calculation that is directly
+ reflected in the iteration progress
+ (i.e. `anim.effect.getComputedTiming().progress`), test that instead
+ of calling `getComputedStyle(elem).marginLeft`.
+
+ * Likewise, don't add needless tests for `anim.playbackState`.
+ The playback state is a calculated value based on other values.
+ It's rarely necessary to test directly unless you need, for example,
+ to check that a pending task is scheduled (which isn't observable
+ elsewhere other than waiting for the corresponding promise to
+ complete).
+
+* Try to keep tests as simple and focused as possible.
+
+ e.g.
+
+ ```javascript
+ test(function(t) {
+ var anim = createDiv(t).animate(null);
+ assert_class_string(anim, 'Animation', 'Returned object is an Animation');
+ }, 'Element.animate() creates an Animation object');
+ ```
+
+ ```javascript
+ test(function(t) {
+ assert_throws({ name: 'TypeError' }, function() {
+ createDiv(t).animate(null, -1);
+ });
+ }, 'Setting a negative duration throws a TypeError');
+ ```
+
+ ```javascript
+ promise_test(function(t) {
+ var animation = createDiv(t).animate(null, 100 * MS_PER_SEC);
+ return animation.ready.then(function() {
+ assert_greater_than(animation.startTime, 0, 'startTime when running');
+ });
+ }, 'startTime is resolved when running');
+ ```
+
+ If you're generating complex test loops and factoring out utility functions
+ that affect the logic of the test (other than, say, simple assertion utility
+ functions), you're probably doing it wrong.
+
+ It should be possible to understand exactly what the test is doing at a
+ glance without having to scroll up and down the test file and refer to
+ other files.
+
+ See Justin Searls' presentation, [&ldquo;How to stop hating your
+ tests&rdquo;](http://blog.testdouble.com/posts/2015-11-16-how-to-stop-hating-your-tests.html)
+ for some tips on making your tests simpler.
+
+* Assume tests will run on under-performing hardware where the time between
+ animation frames might run into 10s of seconds.
+ As a result, animations that are expected to still be running during
+ the test should be at least 100s in length.
+
+* Avoid using `GLOBAL_CONSTS` that make the test harder to read.
+ It's fine to repeat the the same parameter values like `100 * MS_PER_SEC`
+ over and over again since it makes it easy to read and debug a test in
+ isolation.
+ Remember, even if we do need to make all tests take, say 200s each, text
+ editors are very good at search and replace.
+
+* Use the `assert_times_equal` assertion for comparing calculated times.
+ It tests times are equal using the precision recommended in the spec whilst
+ allowing implementations to override the function to meet their own
+ precision requirements.
+
+* There are quite a few bad tests in the repository. We're learning as
+ we go. Don't just copy them blindly&mdash;please fix them!
diff --git a/testing/web-platform/tests/web-animations/animation-model/animation-types/discrete-animation.html b/testing/web-platform/tests/web-animations/animation-model/animation-types/discrete-animation.html
new file mode 100644
index 000000000..e43f26d16
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/animation-model/animation-types/discrete-animation.html
@@ -0,0 +1,135 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title>Tests for discrete animation</title>
+<link rel="help" href="http://w3c.github.io/web-animations/#animatable-as-string-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(function(t) {
+ var div = createDiv(t);
+
+ var anim = div.animate({ fontStyle: [ 'normal', 'italic' ] },
+ { duration: 1000, fill: 'forwards' });
+
+ assert_equals(getComputedStyle(div).fontStyle, 'normal',
+ 'Animation produces \'from\' value at start of interval');
+ anim.currentTime = anim.effect.getComputedTiming().duration / 2 - 1;
+ assert_equals(getComputedStyle(div).fontStyle, 'normal',
+ 'Animation produces \'from\' value just before the middle of'
+ + ' the interval');
+ anim.currentTime++;
+ assert_equals(getComputedStyle(div).fontStyle, 'italic',
+ 'Animation produces \'to\' value at exact middle of'
+ + ' the interval');
+ anim.finish();
+ assert_equals(getComputedStyle(div).fontStyle, 'italic',
+ 'Animation produces \'to\' value during forwards fill');
+}, 'Test animating discrete values');
+
+test(function(t) {
+ var div = createDiv(t);
+ var originalHeight = getComputedStyle(div).height;
+
+ var anim = div.animate({ height: [ 'auto', '200px' ] },
+ { duration: 1000, fill: 'forwards' });
+
+ assert_equals(getComputedStyle(div).height, originalHeight,
+ 'Animation produces \'from\' value at start of interval');
+ anim.currentTime = anim.effect.getComputedTiming().duration / 2 - 1;
+ assert_equals(getComputedStyle(div).height, originalHeight,
+ 'Animation produces \'from\' value just before the middle of'
+ + ' the interval');
+ anim.currentTime++;
+ assert_equals(getComputedStyle(div).height, '200px',
+ 'Animation produces \'to\' value at exact middle of'
+ + ' the interval');
+ anim.finish();
+ assert_equals(getComputedStyle(div).height, '200px',
+ 'Animation produces \'to\' value during forwards fill');
+}, 'Test discrete animation is used when interpolation fails');
+
+test(function(t) {
+ var div = createDiv(t);
+ var originalHeight = getComputedStyle(div).height;
+
+ var anim = div.animate({ height: [ 'auto',
+ '200px',
+ '300px',
+ 'auto',
+ '400px' ] },
+ { duration: 1000, fill: 'forwards' });
+
+ // There are five values, so there are four pairs to try to interpolate.
+ // We test at the middle of each pair.
+ assert_equals(getComputedStyle(div).height, originalHeight,
+ 'Animation produces \'from\' value at start of interval');
+ anim.currentTime = 125;
+ assert_equals(getComputedStyle(div).height, '200px',
+ 'First non-interpolable pair uses discrete interpolation');
+ anim.currentTime += 250;
+ assert_equals(getComputedStyle(div).height, '250px',
+ 'Second interpolable pair uses linear interpolation');
+ anim.currentTime += 250;
+ assert_equals(getComputedStyle(div).height, originalHeight,
+ 'Third non-interpolable pair uses discrete interpolation');
+ anim.currentTime += 250;
+ assert_equals(getComputedStyle(div).height, '400px',
+ 'Fourth non-interpolable pair uses discrete interpolation');
+}, 'Test discrete animation is used only for pairs of values that cannot'
+ + ' be interpolated');
+
+test(function(t) {
+ var div = createDiv(t);
+ var originalHeight = getComputedStyle(div).height;
+
+ // Easing: http://cubic-bezier.com/#.68,0,1,.01
+ // With this curve, we don't reach the 50% point until about 95% of
+ // the time has expired.
+ var anim = div.animate({ fontStyle: [ 'italic', 'oblique' ] },
+ { duration: 1000, fill: 'forwards',
+ easing: 'cubic-bezier(0.68,0,1,0.01)' });
+
+ assert_equals(getComputedStyle(div).fontStyle, 'italic',
+ 'Animation produces \'from\' value at start of interval');
+ anim.currentTime = 940;
+ assert_equals(getComputedStyle(div).fontStyle, 'italic',
+ 'Animation produces \'from\' value at 94% of the iteration'
+ + ' time');
+ anim.currentTime = 960;
+ assert_equals(getComputedStyle(div).fontStyle, 'oblique',
+ 'Animation produces \'to\' value at 96% of the iteration'
+ + ' time');
+}, 'Test the 50% switch point for discrete animation is based on the'
+ + ' effect easing');
+
+test(function(t) {
+ var div = createDiv(t);
+ var originalHeight = getComputedStyle(div).height;
+
+ // Easing: http://cubic-bezier.com/#.68,0,1,.01
+ // With this curve, we don't reach the 50% point until about 95% of
+ // the time has expired.
+ var anim = div.animate([ { fontStyle: 'italic',
+ easing: 'cubic-bezier(0.68,0,1,0.01)' },
+ { fontStyle: 'oblique' } ],
+ { duration: 1000, fill: 'forwards' });
+
+ assert_equals(getComputedStyle(div).fontStyle, 'italic',
+ 'Animation produces \'from\' value at start of interval');
+ anim.currentTime = 940;
+ assert_equals(getComputedStyle(div).fontStyle, 'italic',
+ 'Animation produces \'from\' value at 94% of the iteration'
+ + ' time');
+ anim.currentTime = 960;
+ assert_equals(getComputedStyle(div).fontStyle, 'oblique',
+ 'Animation produces \'to\' value at 96% of the iteration'
+ + ' time');
+}, 'Test the 50% switch point for discrete animation is based on the'
+ + ' keyframe easing');
+
+</script>
diff --git a/testing/web-platform/tests/web-animations/animation-model/animation-types/spacing-keyframes-shapes.html b/testing/web-platform/tests/web-animations/animation-model/animation-types/spacing-keyframes-shapes.html
new file mode 100644
index 000000000..9f7cfaea1
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/animation-model/animation-types/spacing-keyframes-shapes.html
@@ -0,0 +1,152 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title>Keyframe spacing tests on shapes</title>
+<link rel="help" href="https://w3c.github.io/web-animations/#spacing-keyframes">
+<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";
+
+// Help function for testing the computed offsets by the distance array.
+function assert_animation_offsets(anim, dist) {
+ const frames = anim.effect.getKeyframes();
+ const cumDist = dist.reduce( (prev, curr) => {
+ prev.push(prev.length == 0 ? curr : curr + prev[prev.length - 1]);
+ return prev;
+ }, []);
+
+ const total = cumDist[cumDist.length - 1];
+ for (var i = 0; i < frames.length; ++i) {
+ assert_equals(frames[i].computedOffset, cumDist[i] / total,
+ 'computedOffset of frame ' + i);
+ }
+}
+
+test(function(t) {
+ var anim =
+ createDiv(t).animate([ { clipPath: 'circle(20px)' },
+ { clipPath: 'ellipse(10px 20px)' },
+ { clipPath: 'polygon(50px 0px, 100px 50px, ' +
+ ' 50px 100px, 0px 50px)' },
+ { clipPath: 'inset(20px round 10px)' } ],
+ { spacing: 'paced(clip-path)' });
+
+ const frames = anim.effect.getKeyframes();
+ const slots = frames.length - 1;
+ for (var i = 0; i < frames.length; ++i) {
+ assert_equals(frames[i].computedOffset, i / slots,
+ 'computedOffset of frame ' + i);
+ }
+}, 'Test falling back to distribute spacing when using different basic shapes');
+
+test(function(t) {
+ var anim =
+ createDiv(t).animate([ { clipPath: 'circle(10px)' },
+ { clipPath: 'circle(20px) content-box' },
+ { clipPath: 'circle(70px)' },
+ { clipPath: 'circle(10px) padding-box' } ],
+ { spacing: 'paced(clip-path)' });
+
+ const frames = anim.effect.getKeyframes();
+ const slots = frames.length - 1;
+ for (var i = 0; i < frames.length; ++i) {
+ assert_equals(frames[i].computedOffset, i / slots,
+ 'computedOffset of frame ' + i);
+ }
+}, 'Test falling back to distribute spacing when using different ' +
+ 'reference boxes');
+
+test(function(t) {
+ // 1st: circle(calc(20px) at calc(20px + 0%) calc(10px + 0%))
+ // 2nd: circle(calc(50px) at calc(10px + 0%) calc(10px + 0%))
+ // 3rd: circle(70px at calc(10px + 0%) calc(50px + 0%))
+ // 4th: circle(10px at calc(50px + 0%) calc(30px + 0%))
+ var anim =
+ createDiv(t).animate([ { clipPath: 'circle(calc(10px + 10px) ' +
+ ' at 20px 10px)' },
+ { clipPath: 'circle(calc(20px + 30px) ' +
+ ' at 10px 10px)' },
+ { clipPath: 'circle(70px at 10px 50px)' },
+ { clipPath: 'circle(10px at 50px 30px)' } ],
+ { spacing: 'paced(clip-path)' });
+
+ var dist = [ 0,
+ Math.sqrt(30 * 30 + 10 * 10),
+ Math.sqrt(20 * 20 + 40 * 40),
+ Math.sqrt(60 * 60 + 40 * 40 + 20 * 20) ];
+ assert_animation_offsets(anim, dist);
+}, 'Test spacing on circle' );
+
+test(function(t) {
+ // 1st: ellipse(20px calc(20px) at calc(0px + 50%) calc(0px + 50%))
+ // 2nd: ellipse(20px calc(50px) at calc(0px + 50%) calc(0px + 50%))
+ // 3rd: ellipse(30px 70px at calc(0px + 50%) calc(0px + 50%))
+ // 4th: ellipse(30px 10px at calc(0px + 50%) calc(0px + 50%))
+ var anim =
+ createDiv(t).animate([ { clipPath: 'ellipse(20px calc(10px + 10px))' },
+ { clipPath: 'ellipse(20px calc(20px + 30px))' },
+ { clipPath: 'ellipse(30px 70px)' },
+ { clipPath: 'ellipse(30px 10px)' } ],
+ { spacing: 'paced(clip-path)' });
+
+ var dist = [ 0,
+ Math.sqrt(30 * 30),
+ Math.sqrt(10 * 10 + 20 * 20),
+ Math.sqrt(60 * 60) ];
+ assert_animation_offsets(anim, dist);
+}, 'Test spacing on ellipse' );
+
+test(function(t) {
+ // 1st: polygon(nonzero, 50px 0px, 100px 50px, 50px 100px, 0px 50px)
+ // 2nd: polygon(nonzero, 40px 0px, 100px 70px, 10px 100px, 0px 70px)
+ // 3rd: polygon(nonzero, 100px 0px, 100px 100px, 10px 80px, 0px 50px)
+ // 4th: polygon(nonzero, 100px 100px, -10px 100px, 20px 80px, 20px 50px)
+ var anim =
+ createDiv(t).animate([ { clipPath: 'polygon(50px 0px, 100px 50px, ' +
+ ' 50px 100px, 0px 50px)' },
+ { clipPath: 'polygon(40px 0px, 100px 70px, ' +
+ ' 10px 100px, 0px 70px)' },
+ { clipPath: 'polygon(100px 0px, 100px 100px, ' +
+ ' 10px 80px, 0px 50px)' },
+ { clipPath: 'polygon(100px 100px, -10px 100px, ' +
+ ' 20px 80px, 20px 50px)' } ],
+ { spacing: 'paced(clip-path)' });
+
+ var dist = [ 0,
+ Math.sqrt(10 * 10 + 20 * 20 + 40 * 40 + 20 * 20),
+ Math.sqrt(60 * 60 + 30 * 30 + 20 * 20 + 20 * 20),
+ Math.sqrt(100 * 100 + 110 * 110 + 10 * 10 + 20 * 20) ];
+ assert_animation_offsets(anim, dist);
+}, 'Test spacing on polygon' );
+
+test(function(t) {
+ // Note: Rounding corners are 4 CSS pair values and
+ // each pair has x & y components.
+ // 1st: inset(5px 5px 5px 5px round 40px 30px 20px 5px / 40px 30px 20px 5px)
+ // 2nd: inset(10px 5px 10px 5px round 50px 60px / 50px 60px)
+ // 3rd: inset(40px 40px 40px 40px round 10px / 10px)
+ // 4th: inset(30px 40px 30px 40px round 20px / 20px)
+ var anim =
+ createDiv(t).animate([ { clipPath: 'inset(5px 5px 5px 5px ' +
+ ' round 40px 30px 20px 5px)' },
+ { clipPath: 'inset(10px 5px round 50px 60px)' },
+ { clipPath: 'inset(40px 40px round 10px)' },
+ { clipPath: 'inset(30px 40px round 20px)' } ],
+ { spacing: 'paced(clip-path)' });
+
+ var dist = [ 0,
+ Math.sqrt(5 * 5 * 2 + (50 - 40) * (50 - 40) * 2 +
+ (60 - 30) * (60 - 30) * 2 +
+ (50 - 20) * (50 - 20) * 2 +
+ (60 - 5) * (60 - 5) * 2),
+ Math.sqrt(30 * 30 * 2 + 35 * 35 * 2 + (50 - 10) * (50 - 10) * 4 +
+ (60 - 10) * (60 - 10) * 4),
+ Math.sqrt(10 * 10 * 2 + (20 - 10) * (20 - 10) * 8) ];
+ assert_animation_offsets(anim, dist);
+}, 'Test spacing on inset' );
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/web-animations/animation-model/animation-types/type-per-property.html b/testing/web-platform/tests/web-animations/animation-model/animation-types/type-per-property.html
new file mode 100644
index 000000000..9e41b753e
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/animation-model/animation-types/type-per-property.html
@@ -0,0 +1,1198 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title>Tests for animation types</title>
+<link rel="help" href="https://w3c.github.io/web-animations/#animation-types">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../../testcommon.js"></script>
+<style>
+html {
+ font-size: 10px;
+}
+</style>
+<body>
+<div id="log"></div>
+<script>
+"use strict";
+
+var gCSSProperties = {
+ "align-content": {
+ // https://drafts.csswg.org/css-align/#propdef-align-content
+ tests: [
+ discrete("flex-start", "flex-end")
+ ]
+ },
+ "align-items": {
+ // https://drafts.csswg.org/css-align/#propdef-align-items
+ tests: [
+ discrete("flex-start", "flex-end")
+ ]
+ },
+ "align-self": {
+ // https://drafts.csswg.org/css-align/#propdef-align-self
+ tests: [
+ discrete("flex-start", "flex-end")
+ ]
+ },
+ "backface-visibility": {
+ // https://drafts.csswg.org/css-transforms/#propdef-backface-visibility
+ "tests": [
+ discrete("visible", "hidden")
+ ]
+ },
+ "background-attachment": {
+ // https://drafts.csswg.org/css-backgrounds-3/#background-attachment
+ "tests": [
+ discrete("fixed", "local")
+ ]
+ },
+ "background-blend-mode": {
+ // https://drafts.fxtf.org/compositing-1/#propdef-background-blend-mode
+ "tests": [
+ discrete("multiply", "screen")
+ ]
+ },
+ "background-clip": {
+ // https://drafts.csswg.org/css-backgrounds-3/#background-clip
+ "tests": [
+ discrete("padding-box", "content-box")
+ ]
+ },
+ "background-image": {
+ // https://drafts.csswg.org/css-backgrounds-3/#background-image
+ "tests": [
+ discrete("url(\"http://localhost/test-1\")", "url(\"http://localhost/test-2\")")
+ ]
+ },
+ "background-origin": {
+ // https://drafts.csswg.org/css-backgrounds-3/#background-origin
+ "tests": [
+ discrete("padding-box", "content-box")
+ ]
+ },
+ "background-repeat": {
+ // https://drafts.csswg.org/css-backgrounds-3/#background-repeat
+ "tests": [
+ discrete("space", "round")
+ ]
+ },
+ "border-bottom-style": {
+ // https://drafts.csswg.org/css-backgrounds-3/#border-bottom-style
+ "tests": [
+ discrete("dotted", "solid")
+ ]
+ },
+ "border-collapse": {
+ // https://drafts.csswg.org/css-tables/#propdef-border-collapse
+ "tests": [
+ discrete("collapse", "separate")
+ ]
+ },
+ "border-image-outset": {
+ // https://drafts.csswg.org/css-backgrounds-3/#border-image-outset
+ "tests": [
+ discrete("1 1 1 1", "5 5 5 5")
+ ]
+ },
+ "border-image-repeat": {
+ // https://drafts.csswg.org/css-backgrounds-3/#border-image-repeat
+ "tests": [
+ discrete("stretch stretch", "repeat repeat")
+ ]
+ },
+ "border-image-slice": {
+ // https://drafts.csswg.org/css-backgrounds-3/#border-image-slice
+ "tests": [
+ discrete("1 1 1 1", "5 5 5 5")
+ ]
+ },
+ "border-image-source": {
+ // https://drafts.csswg.org/css-backgrounds-3/#border-image-source
+ "tests": [
+ discrete("url(\"http://localhost/test-1\")", "url(\"http://localhost/test-2\")")
+ ]
+ },
+ "border-image-width": {
+ // https://drafts.csswg.org/css-backgrounds-3/#border-image-width
+ "tests": [
+ discrete("1 1 1 1", "5 5 5 5")
+ ]
+ },
+ "border-left-style": {
+ // https://drafts.csswg.org/css-backgrounds-3/#border-left-style
+ "tests": [
+ discrete("dotted", "solid")
+ ]
+ },
+ "border-right-style": {
+ // https://drafts.csswg.org/css-backgrounds-3/#border-right-style
+ "tests": [
+ discrete("dotted", "solid")
+ ]
+ },
+ "border-top-style": {
+ // https://drafts.csswg.org/css-backgrounds-3/#border-top-style
+ "tests": [
+ discrete("dotted", "solid")
+ ]
+ },
+ "box-decoration-break": {
+ // https://drafts.csswg.org/css-break/#propdef-box-decoration-break
+ "tests": [
+ discrete("slice", "clone")
+ ]
+ },
+ "box-sizing": {
+ // https://drafts.csswg.org/css-ui-4/#box-sizing
+ "tests": [
+ discrete("content-box", "border-box")
+ ]
+ },
+ "caption-side": {
+ // https://drafts.csswg.org/css-tables/#propdef-caption-side
+ "tests": [
+ discrete("top", "bottom")
+ ]
+ },
+ "clear": {
+ // https://drafts.csswg.org/css-page-floats/#propdef-clear
+ "tests": [
+ discrete("left", "right")
+ ]
+ },
+ "clip-rule": {
+ // https://drafts.fxtf.org/css-masking-1/#propdef-clip-rule
+ tests: [
+ discrete("evenodd", "nonzero")
+ ]
+ },
+ "color-adjust": {
+ // https://drafts.csswg.org/css-color-4/#color-adjust
+ tests: [
+ discrete("economy", "exact")
+ ]
+ },
+ "color-interpolation": {
+ // https://svgwg.org/svg2-draft/painting.html#ColorInterpolationProperty
+ tests: [
+ discrete("linearRGB", "auto")
+ ]
+ },
+ "color-interpolation-filters": {
+ // https://drafts.fxtf.org/filters-1/#propdef-color-interpolation-filters
+ tests: [
+ discrete("sRGB", "linearRGB")
+ ]
+ },
+ "column-fill": {
+ // https://drafts.csswg.org/css-multicol/#propdef-column-fill
+ tests: [
+ discrete("auto", "balance")
+ ]
+ },
+ "column-rule-style": {
+ // https://drafts.csswg.org/css-multicol/#propdef-column-rule-style
+ tests: [
+ discrete("none", "dotted")
+ ]
+ },
+ "contain": {
+ // https://drafts.csswg.org/css-containment/#propdef-contain
+ tests: [
+ discrete("strict", "none")
+ ]
+ },
+ "content": {
+ // https://drafts.csswg.org/css-content-3/#propdef-content
+ tests: [
+ discrete("\"a\"", "\"b\"")
+ ],
+ tagName: "::before"
+ },
+ "counter-increment": {
+ // https://drafts.csswg.org/css-lists-3/#propdef-counter-increment
+ tests: [
+ discrete("ident-1 1", "ident-2 2")
+ ]
+ },
+ "counter-reset": {
+ // https://drafts.csswg.org/css-lists-3/#propdef-counter-reset
+ tests: [
+ discrete("ident-1 1", "ident-2 2")
+ ]
+ },
+ "cursor": {
+ // https://drafts.csswg.org/css2/ui.html#propdef-cursor
+ tests: [
+ discrete("pointer", "wait")
+ ]
+ },
+ "direction": {
+ // https://drafts.csswg.org/css-writing-modes-3/#propdef-direction
+ tests: [
+ discrete("ltr", "rtl")
+ ]
+ },
+ "dominant-baseline": {
+ // https://drafts.csswg.org/css-inline/#propdef-dominant-baseline
+ tests: [
+ discrete("ideographic", "alphabetic")
+ ]
+ },
+ "empty-cells": {
+ // https://drafts.csswg.org/css-tables/#propdef-empty-cells
+ tests: [
+ discrete("show", "hide")
+ ]
+ },
+ "fill-rule": {
+ // https://svgwg.org/svg2-draft/painting.html#FillRuleProperty
+ tests: [
+ discrete("evenodd", "nonzero")
+ ]
+ },
+ "flex-basis": {
+ // https://drafts.csswg.org/css-flexbox/#propdef-flex-basis
+ tests: [
+ lengthPercentageOrCalc(),
+ discrete("auto", "10px")
+ ]
+ },
+ "flex-direction": {
+ // https://drafts.csswg.org/css-flexbox/#propdef-flex-direction
+ tests: [
+ discrete("row", "row-reverse")
+ ]
+ },
+ "flex-grow": {
+ // https://drafts.csswg.org/css-flexbox/#flex-grow-property
+ tests: [
+ positiveNumber()
+ ]
+ },
+ "flex-shrink": {
+ // https://drafts.csswg.org/css-flexbox/#propdef-flex-shrink
+ tests: [
+ positiveNumber()
+ ]
+ },
+ "flex-wrap": {
+ // https://drafts.csswg.org/css-flexbox/#propdef-flex-wrap
+ tests: [
+ discrete("nowrap", "wrap")
+ ]
+ },
+ "font-style": {
+ // https://drafts.csswg.org/css-fonts/#propdef-font-style
+ tests: [
+ discrete("italic", "oblique")
+ ]
+ },
+ "float": {
+ // https://drafts.csswg.org/css-page-floats/#propdef-float
+ tests: [
+ discrete("left", "right")
+ ]
+ },
+ "font-family": {
+ // https://drafts.csswg.org/css-fonts-3/#propdef-font-family
+ tests: [
+ discrete("helvetica", "verdana")
+ ]
+ },
+ "font-feature-settings": {
+ // https://drafts.csswg.org/css-fonts/#descdef-font-feature-settings
+ tests: [
+ discrete("\"liga\" 5", "normal")
+ ]
+ },
+ "font-kerning": {
+ // https://drafts.csswg.org/css-fonts-3/#propdef-font-kerning
+ tests: [
+ discrete("auto", "normal")
+ ]
+ },
+ "font-language-override": {
+ // https://drafts.csswg.org/css-fonts-3/#propdef-font-language-override
+ tests: [
+ discrete("\"eng\"", "normal")
+ ]
+ },
+ "font-style": {
+ // https://drafts.csswg.org/css-fonts-3/#propdef-font-style
+ tests: [
+ discrete("italic", "oblique")
+ ]
+ },
+ "font-synthesis": {
+ // https://drafts.csswg.org/css-fonts-3/#propdef-font-synthesis
+ tests: [
+ discrete("none", "weight style")
+ ]
+ },
+ "font-variant-alternates": {
+ // https://drafts.csswg.org/css-fonts-3/#propdef-font-variant-alternates
+ tests: [
+ discrete("swash(unknown)", "stylistic(unknown)")
+ ]
+ },
+ "font-variant-caps": {
+ // https://drafts.csswg.org/css-fonts-3/#propdef-font-variant-caps
+ tests: [
+ discrete("small-caps", "unicase")
+ ]
+ },
+ "font-variant-east-asian": {
+ // https://drafts.csswg.org/css-fonts-3/#propdef-font-variant-east-asian
+ tests: [
+ discrete("full-width", "proportional-width")
+ ]
+ },
+ "font-variant-ligatures": {
+ // https://drafts.csswg.org/css-fonts-3/#propdef-font-variant-ligatures
+ tests: [
+ discrete("common-ligatures", "no-common-ligatures")
+ ]
+ },
+ "font-variant-numeric": {
+ // https://drafts.csswg.org/css-fonts-3/#propdef-font-variant-numeric
+ tests: [
+ discrete("lining-nums", "oldstyle-nums")
+ ]
+ },
+ "font-variant-position": {
+ // https://drafts.csswg.org/css-fonts-3/#propdef-font-variant-position
+ tests: [
+ discrete("sub", "super")
+ ]
+ },
+ "grid-auto-columns": {
+ // https://drafts.csswg.org/css-grid/#propdef-grid-auto-columns
+ tests: [
+ discrete("1px", "5px")
+ ]
+ },
+ "grid-auto-flow": {
+ // https://drafts.csswg.org/css-grid/#propdef-grid-auto-flow
+ tests: [
+ discrete("row", "column")
+ ]
+ },
+ "grid-auto-rows": {
+ // https://drafts.csswg.org/css-grid/#propdef-grid-auto-rows
+ tests: [
+ discrete("1px", "5px")
+ ]
+ },
+ "grid-column-end": {
+ // https://drafts.csswg.org/css-grid/#propdef-grid-column-end
+ tests: [
+ discrete("1", "5")
+ ]
+ },
+ "grid-column-start": {
+ // https://drafts.csswg.org/css-grid/#propdef-grid-column-start
+ tests: [
+ discrete("1", "5")
+ ]
+ },
+ "grid-row-end": {
+ // https://drafts.csswg.org/css-grid/#propdef-grid-row-end
+ tests: [
+ discrete("1", "5")
+ ]
+ },
+ "grid-row-start": {
+ // https://drafts.csswg.org/css-grid/#propdef-grid-row-start
+ tests: [
+ discrete("1", "5")
+ ]
+ },
+ "grid-template-areas": {
+ // https://drafts.csswg.org/css-template/#grid-template-areas
+ tests: [
+ discrete("\". . a b\" \". .a b\"", "none")
+ ]
+ },
+ "grid-template-columns": {
+ // https://drafts.csswg.org/css-template/#grid-template-columns
+ tests: [
+ discrete("1px", "5px")
+ ]
+ },
+ "grid-template-rows": {
+ // https://drafts.csswg.org/css-template/#grid-template-rows
+ tests: [
+ discrete("1px", "5px")
+ ]
+ },
+ "hyphens": {
+ // https://drafts.csswg.org/css-text-3/#propdef-hyphens
+ tests: [
+ discrete("manual", "auto")
+ ]
+ },
+ "image-orientation": {
+ // https://drafts.csswg.org/css-images-3/#propdef-image-orientation
+ tests: [
+ discrete("0deg", "90deg")
+ ]
+ },
+ "ime-mode": {
+ // https://drafts.csswg.org/css-ui/#input-method-editor
+ tests: [
+ discrete("disabled", "auto")
+ ]
+ },
+ "initial-letter": {
+ // https://drafts.csswg.org/css-inline/#propdef-initial-letter
+ tests: [
+ discrete("1 2", "3 4")
+ ]
+ },
+ "isolation": {
+ // https://drafts.fxtf.org/compositing-1/#propdef-isolation
+ tests: [
+ discrete("auto", "isolate")
+ ]
+ },
+ "justify-content": {
+ // https://drafts.csswg.org/css-align/#propdef-justify-content
+ tests: [
+ discrete("baseline", "last baseline")
+ ]
+ },
+ "justify-items": {
+ // https://drafts.csswg.org/css-align/#propdef-justify-items
+ tests: [
+ discrete("baseline", "last baseline")
+ ]
+ },
+ "justify-self": {
+ // https://drafts.csswg.org/css-align/#propdef-justify-self
+ tests: [
+ discrete("baseline", "last baseline")
+ ]
+ },
+ "list-style-image": {
+ // https://drafts.csswg.org/css-lists-3/#propdef-list-style-image
+ tests: [
+ discrete("url(\"http://localhost/test-1\")", "url(\"http://localhost/test-2\")")
+ ]
+ },
+ "list-style-position": {
+ // https://drafts.csswg.org/css-lists-3/#propdef-list-style-position
+ tests: [
+ discrete("inside", "outside")
+ ]
+ },
+ "list-style-type": {
+ // https://drafts.csswg.org/css-lists-3/#propdef-list-style-type
+ tests: [
+ discrete("circle", "square")
+ ]
+ },
+ "marker-end": {
+ // https://svgwg.org/specs/markers/#MarkerEndProperty
+ tests: [
+ discrete("url(\"http://localhost/test-1\")", "url(\"http://localhost/test-2\")")
+ ]
+ },
+ "marker-mid": {
+ // https://svgwg.org/specs/markers/#MarkerMidProperty
+ tests: [
+ discrete("url(\"http://localhost/test-1\")", "url(\"http://localhost/test-2\")")
+ ]
+ },
+ "marker-start": {
+ // https://svgwg.org/specs/markers/#MarkerStartProperty
+ tests: [
+ discrete("url(\"http://localhost/test-1\")", "url(\"http://localhost/test-2\")")
+ ]
+ },
+ "mask": {
+ // https://drafts.fxtf.org/css-masking-1/#the-mask
+ tests: [
+ discrete("url(\"http://localhost/test-1\")", "url(\"http://localhost/test-2\")")
+ ]
+ },
+ "mask-clip": {
+ // https://drafts.fxtf.org/css-masking-1/#propdef-mask-clip
+ tests: [
+ discrete("content-box", "border-box")
+ ]
+ },
+ "mask-composite": {
+ // https://drafts.fxtf.org/css-masking-1/#propdef-mask-composite
+ tests: [
+ discrete("add", "subtract")
+ ]
+ },
+ "mask-image": {
+ // https://drafts.fxtf.org/css-masking-1/#propdef-mask-image
+ tests: [
+ discrete("url(\"http://localhost/test-1\")", "url(\"http://localhost/test-2\")")
+ ]
+ },
+ "mask-mode": {
+ // https://drafts.fxtf.org/css-masking-1/#propdef-mask-mode
+ tests: [
+ discrete("alpha", "luminance")
+ ]
+ },
+ "mask-origin": {
+ // https://drafts.fxtf.org/css-masking-1/#propdef-mask-origin
+ tests: [
+ discrete("content-box", "border-box")
+ ]
+ },
+ "mask-repeat": {
+ // https://drafts.fxtf.org/css-masking-1/#propdef-mask-repeat
+ tests: [
+ discrete("space", "round")
+ ]
+ },
+ "mask-type": {
+ // https://drafts.fxtf.org/css-masking-1/#propdef-mask-type
+ tests: [
+ discrete("alpha", "luminance")
+ ]
+ },
+ "mix-blend-mode": {
+ // https://drafts.fxtf.org/compositing-1/#propdef-mix-blend-mode
+ tests: [
+ discrete("multiply", "screen")
+ ]
+ },
+ "object-fit": {
+ // https://drafts.csswg.org/css-images-3/#propdef-object-fit
+ tests: [
+ discrete("fill", "contain")
+ ]
+ },
+ "order": {
+ // https://drafts.csswg.org/css-flexbox/#propdef-order
+ tests: [
+ integer()
+ ]
+ },
+ "outline-style": {
+ // https://drafts.csswg.org/css-ui/#propdef-outline-style
+ tests: [
+ discrete("none", "dotted")
+ ]
+ },
+ "overflow-clip-box": {
+ // https://developer.mozilla.org/en/docs/Web/CSS/overflow-clip-box
+ tests: [
+ discrete("padding-box", "content-box")
+ ]
+ },
+ "overflow-wrap": {
+ // https://drafts.csswg.org/css-text-3/#propdef-overflow-wrap
+ tests: [
+ discrete("normal", "break-word")
+ ]
+ },
+ "overflow-x": {
+ // https://drafts.csswg.org/css-overflow-3/#propdef-overflow-x
+ tests: [
+ discrete("visible", "hidden")
+ ]
+ },
+ "overflow-y": {
+ // https://drafts.csswg.org/css-overflow-3/#propdef-overflow-y
+ tests: [
+ discrete("visible", "hidden")
+ ]
+ },
+ "page-break-after": {
+ // https://drafts.csswg.org/css-break-3/#propdef-break-after
+ tests: [
+ discrete("always", "auto")
+ ]
+ },
+ "page-break-before": {
+ // https://drafts.csswg.org/css-break-3/#propdef-break-before
+ tests: [
+ discrete("always", "auto")
+ ]
+ },
+ "page-break-inside": {
+ // https://drafts.csswg.org/css-break-3/#propdef-break-inside
+ tests: [
+ discrete("auto", "avoid")
+ ]
+ },
+ "paint-order": {
+ // https://svgwg.org/svg2-draft/painting.html#PaintOrderProperty
+ tests: [
+ discrete("fill", "stroke")
+ ]
+ },
+ "pointer-events": {
+ // https://svgwg.org/svg2-draft/interact.html#PointerEventsProperty
+ tests: [
+ discrete("fill", "none")
+ ]
+ },
+ "position": {
+ // https://drafts.csswg.org/css-position/#propdef-position
+ tests: [
+ discrete("absolute", "fixed")
+ ]
+ },
+ "quotes": {
+ // https://drafts.csswg.org/css-content-3/#propdef-quotes
+ tests: [
+ discrete("\"“\" \"”\" \"‘\" \"’\"", "\"‘\" \"’\" \"“\" \"”\"")
+ ]
+ },
+ "resize": {
+ // https://drafts.csswg.org/css-ui/#propdef-resize
+ tests: [
+ discrete("both", "horizontal")
+ ]
+ },
+ "ruby-align": {
+ // https://drafts.csswg.org/css-ruby-1/#propdef-ruby-align
+ tests: [
+ discrete("start", "center")
+ ]
+ },
+ "ruby-position": {
+ // https://drafts.csswg.org/css-ruby-1/#propdef-ruby-position
+ tests: [
+ discrete("under", "over")
+ ],
+ tagName: "ruby"
+ },
+ "scroll-behavior": {
+ // https://drafts.csswg.org/cssom-view/#propdef-scroll-behavior
+ tests: [
+ discrete("auto", "smooth")
+ ]
+ },
+ "scroll-snap-type-x": {
+ // https://developer.mozilla.org/en/docs/Web/CSS/scroll-snap-type-x
+ tests: [
+ discrete("mandatory", "proximity")
+ ]
+ },
+ "scroll-snap-type-y": {
+ // https://developer.mozilla.org/en/docs/Web/CSS/scroll-snap-type-y
+ tests: [
+ discrete("mandatory", "proximity")
+ ]
+ },
+ "shape-outside": {
+ // http://dev.w3.org/csswg/css-shapes/#propdef-shape-outside
+ tests: [
+ discrete("url(\"http://localhost/test-1\")", "url(\"http://localhost/test-2\")")
+ ]
+ },
+ "shape-rendering": {
+ // https://svgwg.org/svg2-draft/painting.html#ShapeRenderingProperty
+ tests: [
+ discrete("optimizeSpeed", "crispEdges")
+ ]
+ },
+ "stroke-linecap": {
+ // https://svgwg.org/svg2-draft/painting.html#StrokeLinecapProperty
+ tests: [
+ discrete("round", "square")
+ ]
+ },
+ "stroke-linejoin": {
+ // https://svgwg.org/svg2-draft/painting.html#StrokeLinejoinProperty
+ tests: [
+ discrete("round", "miter")
+ ],
+ tagName: "rect"
+ },
+ "table-layout": {
+ // https://drafts.csswg.org/css-tables/#propdef-table-layout
+ tests: [
+ discrete("auto", "fixed")
+ ]
+ },
+ "text-align": {
+ // https://drafts.csswg.org/css-text-3/#propdef-text-align
+ tests: [
+ discrete("start", "end")
+ ]
+ },
+ "text-align-last": {
+ // https://drafts.csswg.org/css-text-3/#propdef-text-align-last
+ tests: [
+ discrete("start", "end")
+ ]
+ },
+ "text-anchor": {
+ // https://svgwg.org/svg2-draft/text.html#TextAnchorProperty
+ tests: [
+ discrete("middle", "end")
+ ]
+ },
+ "text-combine-upright": {
+ // https://drafts.csswg.org/css-writing-modes-3/#propdef-text-combine-upright
+ tests: [
+ discrete("all", "none")
+ ]
+ },
+ "text-decoration-line": {
+ // https://drafts.csswg.org/css-text-decor-3/#propdef-text-decoration-line
+ tests: [
+ discrete("underline", "overline")
+ ]
+ },
+ "text-decoration-style": {
+ // http://dev.w3.org/csswg/css-text-decor-3/#propdef-text-decoration-style
+ tests: [
+ discrete("solid", "dotted")
+ ]
+ },
+ "text-emphasis-position": {
+ // http://dev.w3.org/csswg/css-text-decor-3/#propdef-text-emphasis-position
+ tests: [
+ discrete("over right", "under left")
+ ]
+ },
+ "text-emphasis-style": {
+ // http://dev.w3.org/csswg/css-text-decor-3/#propdef-text-emphasis-style
+ tests: [
+ discrete("filled circle", "open dot")
+ ]
+ },
+ "text-orientation": {
+ // https://drafts.csswg.org/css-writing-modes-3/#propdef-text-orientation
+ tests: [
+ discrete("upright", "sideways")
+ ]
+ },
+ "text-overflow": {
+ // https://drafts.csswg.org/css-ui/#propdef-text-overflow
+ tests: [
+ discrete("clip", "ellipsis")
+ ]
+ },
+ "text-rendering": {
+ // https://svgwg.org/svg2-draft/painting.html#TextRenderingProperty
+ tests: [
+ discrete("optimizeSpeed", "optimizeLegibility")
+ ]
+ },
+ "text-transform": {
+ // https://drafts.csswg.org/css-text-3/#propdef-text-transform
+ tests: [
+ discrete("capitalize", "uppercase")
+ ]
+ },
+ "touch-action": {
+ // https://w3c.github.io/pointerevents/#the-touch-action-css-property
+ tests: [
+ discrete("auto", "none")
+ ]
+ },
+ "transform-box": {
+ // https://drafts.csswg.org/css-transforms/#propdef-transform-box
+ tests: [
+ discrete("fill-box", "border-box")
+ ]
+ },
+ "transform-style": {
+ // https://drafts.csswg.org/css-transforms/#propdef-transform-style
+ tests: [
+ discrete("flat", "preserve-3d")
+ ]
+ },
+ "unicode-bidi": {
+ // https://drafts.csswg.org/css-writing-modes-3/#propdef-unicode-bidi
+ tests: [
+ discrete("embed", "bidi-override")
+ ]
+ },
+ "vector-effect": {
+ // https://svgwg.org/svg2-draft/coords.html#VectorEffectProperty
+ tests: [
+ discrete("none", "non-scaling-stroke")
+ ]
+ },
+ "visibility": {
+ // https://drafts.csswg.org/css2/visufx.html#propdef-visibility
+ tests: [
+ visibility()
+ ]
+ },
+ "white-space": {
+ // https://drafts.csswg.org/css-text-4/#propdef-white-space
+ tests: [
+ discrete("pre", "nowrap")
+ ]
+ },
+ "word-break": {
+ // https://drafts.csswg.org/css-text-3/#propdef-word-break
+ tests: [
+ discrete("keep-all", "break-all")
+ ]
+ },
+ "will-change": {
+ // http://dev.w3.org/csswg/css-will-change/#propdef-will-change
+ tests: [
+ discrete("scroll-position", "contents")
+ ]
+ },
+ "writing-mode": {
+ // https://drafts.csswg.org/css-writing-modes-3/#propdef-writing-mode
+ tests: [
+ discrete("vertical-rl", "sideways-rl")
+ ]
+ },
+}
+
+for (var property in gCSSProperties) {
+ if (!isSupported(property)) {
+ continue;
+ }
+ var testData = gCSSProperties[property];
+ testData.tests.forEach(function(testFunction) {
+ testFunction(property, testData);
+ });
+}
+
+function discrete(from, to) {
+ return function(property, options) {
+ test(function(t) {
+ var idlName = propertyToIDL(property);
+ var keyframes = {};
+ keyframes[idlName] = [from, to];
+ var target = createTestElement(t, options.tagName);
+ var animation = target.animate(keyframes,
+ { duration: 1000, fill: "both" });
+ testAnimationSamples(animation, idlName,
+ [{ time: 0, expected: from.toLowerCase() },
+ { time: 499, expected: from.toLowerCase() },
+ { time: 500, expected: to.toLowerCase() },
+ { time: 1000, expected: to.toLowerCase() }]);
+ }, property + " uses discrete animation when animating between '"
+ + from + "' and '" + to + "' with linear easing");
+
+ test(function(t) {
+ // Easing: http://cubic-bezier.com/#.68,0,1,.01
+ // With this curve, we don't reach the 50% point until about 95% of
+ // the time has expired.
+ var idlName = propertyToIDL(property);
+ var keyframes = {};
+ keyframes[idlName] = [from, to];
+ var target = createTestElement(t, options.tagName);
+ var animation = target.animate(keyframes,
+ { duration: 1000, fill: "both",
+ easing: "cubic-bezier(0.68,0,1,0.01)" });
+ testAnimationSamples(animation, idlName,
+ [{ time: 0, expected: from.toLowerCase() },
+ { time: 940, expected: from.toLowerCase() },
+ { time: 960, expected: to.toLowerCase() }]);
+ }, property + " uses discrete animation when animating between '"
+ + from + "' and '" + to + "' with effect easing");
+
+ test(function(t) {
+ // Easing: http://cubic-bezier.com/#.68,0,1,.01
+ // With this curve, we don't reach the 50% point until about 95% of
+ // the time has expired.
+ var idlName = propertyToIDL(property);
+ var keyframes = {};
+ keyframes[idlName] = [from, to];
+ keyframes.easing = "cubic-bezier(0.68,0,1,0.01)";
+ var target = createTestElement(t, options.tagName);
+ var animation = target.animate(keyframes,
+ { duration: 1000, fill: "both" });
+ testAnimationSamples(animation, idlName,
+ [{ time: 0, expected: from.toLowerCase() },
+ { time: 940, expected: from.toLowerCase() },
+ { time: 960, expected: to.toLowerCase() }]);
+ }, property + " uses discrete animation when animating between '"
+ + from + "' and '" + to + "' with keyframe easing");
+ }
+}
+
+function length() {
+ return function(property, options) {
+ test(function(t) {
+ var idlName = propertyToIDL(property);
+ var keyframes = {};
+ keyframes[idlName] = ["10px", "50px"];
+ var target = createTestElement(t, options.tagName);
+ var animation = target.animate(keyframes,
+ { duration: 1000, fill: "both" });
+ testAnimationSamples(animation, idlName,
+ [{ time: 0, expected: "10px" },
+ { time: 500, expected: "30px" },
+ { time: 1000, expected: "50px" }]);
+ }, property + " supports animating as a length");
+
+ test(function(t) {
+ var idlName = propertyToIDL(property);
+ var keyframes = {};
+ keyframes[idlName] = ["1rem", "5rem"];
+ var target = createTestElement(t, options.tagName);
+ var animation = target.animate(keyframes,
+ { duration: 1000, fill: "both" });
+ testAnimationSamples(animation, idlName,
+ [{ time: 0, expected: "10px" },
+ { time: 500, expected: "30px" },
+ { time: 1000, expected: "50px" }]);
+ }, property + " supports animating as a length of rem");
+ }
+}
+
+function percentage() {
+ return function(property, options) {
+ test(function(t) {
+ var idlName = propertyToIDL(property);
+ var keyframes = {};
+ keyframes[idlName] = ["10%", "50%"];
+ var target = createTestElement(t, options.tagName);
+ var animation = target.animate(keyframes,
+ { duration: 1000, fill: "both" });
+ testAnimationSamples(animation, idlName,
+ [{ time: 0, expected: "10%" },
+ { time: 500, expected: "30%" },
+ { time: 1000, expected: "50%" }]);
+ }, property + " supports animating as a percentage");
+ }
+}
+
+function integer() {
+ return function(property, options) {
+ test(function(t) {
+ var idlName = propertyToIDL(property);
+ var keyframes = {};
+ keyframes[idlName] = [-2, 2];
+ var target = createTestElement(t, options.tagName);
+ var animation = target.animate(keyframes,
+ { duration: 1000, fill: "both" });
+ testAnimationSamples(animation, idlName,
+ [{ time: 0, expected: "-2" },
+ { time: 500, expected: "0" },
+ { time: 1000, expected: "2" }]);
+ }, property + " supports animating as an integer");
+ }
+}
+
+function positiveNumber() {
+ return function(property, options) {
+ test(function(t) {
+ var idlName = propertyToIDL(property);
+ var keyframes = {};
+ keyframes[idlName] = [1.1, 1.5];
+ var target = createTestElement(t, options.tagName);
+ var animation = target.animate(keyframes,
+ { duration: 1000, fill: "both" });
+ testAnimationSamples(animation, idlName,
+ [{ time: 0, expected: "1.1" },
+ { time: 500, expected: "1.3" },
+ { time: 1000, expected: "1.5" }]);
+ }, property + " supports animating as a positive number");
+ }
+}
+
+function lengthPercentageOrCalc() {
+ return function(property, options) {
+ length()(property, options);
+ percentage()(property, options);
+
+ test(function(t) {
+ var idlName = propertyToIDL(property);
+ var keyframes = {};
+ keyframes[idlName] = ["10px", "20%"];
+ var target = createTestElement(t, options.tagName);
+ var animation = target.animate(keyframes,
+ { duration: 1000, fill: "both" });
+ testAnimationSamples(animation, idlName,
+ [{ time: 0, expected: "10px" },
+ { time: 500, expected: "calc(5px + 10%)" },
+ { time: 1000, expected: "20%" }]);
+ }, property + " supports animating as combination units 'px' and '%'");
+
+ test(function(t) {
+ var idlName = propertyToIDL(property);
+ var keyframes = {};
+ keyframes[idlName] = ["10%", "2em"];
+ var target = createTestElement(t, options.tagName);
+ var animation = target.animate(keyframes,
+ { duration: 1000, fill: "both" });
+ testAnimationSamples(animation, idlName,
+ [{ time: 0, expected: "10%" },
+ { time: 500, expected: "calc(10px + 5%)" },
+ { time: 1000, expected: "20px" }]);
+ }, property + " supports animating as combination units '%' and 'em'");
+
+ test(function(t) {
+ var idlName = propertyToIDL(property);
+ var keyframes = {};
+ keyframes[idlName] = ["1em", "2rem"];
+ var target = createTestElement(t, options.tagName);
+ var animation = target.animate(keyframes,
+ { duration: 1000, fill: "both" });
+ testAnimationSamples(animation, idlName,
+ [{ time: 0, expected: "10px" },
+ { time: 500, expected: "15px" },
+ { time: 1000, expected: "20px" }]);
+ }, property + " supports animating as combination units 'em' and 'rem'");
+
+ test(function(t) {
+ var idlName = propertyToIDL(property);
+ var keyframes = {};
+ keyframes[idlName] = ["10px", "calc(1em + 20%)"];
+ var target = createTestElement(t, options.tagName);
+ var animation = target.animate(keyframes,
+ { duration: 1000, fill: "both" });
+ testAnimationSamples(animation, idlName,
+ [{ time: 0, expected: "10px" },
+ { time: 500, expected: "calc(10px + 10%)" },
+ { time: 1000, expected: "calc(10px + 20%)" }]);
+ }, property + " supports animating as combination units 'px' and 'calc'");
+
+ test(function(t) {
+ var idlName = propertyToIDL(property);
+ var keyframes = {};
+ keyframes[idlName] = ["calc(10px + 10%)", "calc(1em + 1rem + 20%)"];
+ var target = createTestElement(t, options.tagName);
+ var animation = target.animate(keyframes,
+ { duration: 1000, fill: "both" });
+ testAnimationSamples(animation, idlName,
+ [{ time: 0,
+ expected: "calc(10px + 10%)" },
+ { time: 500,
+ expected: "calc(15px + 15%)" },
+ { time: 1000,
+ expected: "calc(20px + 20%)" }]);
+ }, property + " supports animating as a calc");
+ }
+}
+
+function visibility() {
+ return function(property, options) {
+ test(function(t) {
+ var idlName = propertyToIDL(property);
+ var keyframes = {};
+ keyframes[idlName] = ["visible", "hidden"];
+ var target = createTestElement(t, options.tagName);
+ var animation = target.animate(keyframes,
+ { duration: 1000, fill: "both" });
+ testAnimationSamples(animation, idlName,
+ [{ time: 0, expected: "visible" },
+ { time: 999, expected: "visible" },
+ { time: 1000, expected: "hidden" }]);
+ }, property + " uses visibility animation when animating "
+ + "from 'visible' to 'hidden'");
+
+ test(function(t) {
+ var idlName = propertyToIDL(property);
+ var keyframes = {};
+ keyframes[idlName] = ["hidden", "visible"];
+ var target = createTestElement(t, options.tagName);
+ var animation = target.animate(keyframes,
+ { duration: 1000, fill: "both" });
+ testAnimationSamples(animation, idlName,
+ [{ time: 0, expected: "hidden" },
+ { time: 1, expected: "visible" },
+ { time: 1000, expected: "visible" }]);
+ }, property + " uses visibility animation when animating "
+ + "from 'hidden' to 'visible'");
+
+ test(function(t) {
+ var idlName = propertyToIDL(property);
+ var keyframes = {};
+ keyframes[idlName] = ["hidden", "collapse"];
+ var target = createTestElement(t, options.tagName);
+ var animation = target.animate(keyframes,
+ { duration: 1000, fill: "both" });
+ testAnimationSamples(animation, idlName,
+ [{ time: 0, expected: "hidden" },
+ { time: 499, expected: "hidden" },
+ { time: 500, expected: "collapse" },
+ { time: 1000, expected: "collapse" }]);
+ }, property + " uses visibility animation when animating "
+ + "from 'hidden' to 'collapse'");
+
+ test(function(t) {
+ // Easing: http://cubic-bezier.com/#.68,-.55,.26,1.55
+ // With this curve, the value is less than 0 till about 34%
+ // also more than 1 since about 63%
+ var idlName = propertyToIDL(property);
+ var keyframes = {};
+ keyframes[idlName] = ["visible", "hidden"];
+ var target = createTestElement(t, options.tagName);
+ var animation =
+ target.animate(keyframes,
+ { duration: 1000, fill: "both",
+ easing: "cubic-bezier(0.68, -0.55, 0.26, 1.55)" });
+ testAnimationSamples(animation, idlName,
+ [{ time: 0, expected: "visible" },
+ { time: 1, expected: "visible" },
+ { time: 330, expected: "visible" },
+ { time: 340, expected: "visible" },
+ { time: 620, expected: "visible" },
+ { time: 630, expected: "hidden" },
+ { time: 1000, expected: "hidden" }]);
+ }, property + " uses visibility animation when animating "
+ + "from 'visible' to 'hidden' with easeInOutBack easing");
+ }
+}
+
+function testAnimationSamples(animation, idlName, testSamples) {
+ var type = animation.effect.target.type;
+ var target = type
+ ? animation.effect.target.parentElement
+ : animation.effect.target;
+ testSamples.forEach(function(testSample) {
+ animation.currentTime = testSample.time;
+ assert_equals(getComputedStyle(target, type)[idlName],
+ testSample.expected,
+ "The value should be " + testSample.expected +
+ " at " + testSample.time + "ms");
+ });
+}
+
+function createTestElement(t, tagName) {
+ return tagName && tagName.startsWith("::")
+ ? createPseudo(t, tagName.substring(2))
+ : createElement(t, tagName);
+}
+
+function isSupported(property) {
+ var testKeyframe = new TestKeyframe(propertyToIDL(property));
+ try {
+ // Since TestKeyframe returns 'undefined' for |property|,
+ // the KeyframeEffect constructor will throw
+ // if the string "undefined" is not a valid value for the property.
+ new KeyframeEffect(null, testKeyframe);
+ } catch(e) {}
+ return testKeyframe.propAccessCount !== 0;
+}
+
+function TestKeyframe(testProp) {
+ var _propAccessCount = 0;
+
+ Object.defineProperty(this, testProp, {
+ get: function() { _propAccessCount++; },
+ enumerable: true
+ });
+
+ Object.defineProperty(this, 'propAccessCount', {
+ get: function() { return _propAccessCount; }
+ });
+}
+
+function propertyToIDL(property) {
+ // https://w3c.github.io/web-animations/#animation-property-name-to-idl-attribute-name
+ if (property === "float") {
+ return "cssFloat";
+ }
+ return property.replace(/-[a-z]/gi,
+ function (str) {
+ return str.substr(1).toUpperCase(); });
+}
+
+</script>
diff --git a/testing/web-platform/tests/web-animations/animation-model/keyframe-effects/effect-value-context.html b/testing/web-platform/tests/web-animations/animation-model/keyframe-effects/effect-value-context.html
new file mode 100644
index 000000000..07fb6097c
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/animation-model/keyframe-effects/effect-value-context.html
@@ -0,0 +1,103 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title>Tests that property values respond to changes to their context</title>
+<link rel="help" href="https://w3c.github.io/web-animations/#keyframes-section">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../../testcommon.js"></script>
+<body>
+<div id="log"></div>
+<script>
+
+test(function(t) {
+ var div = createDiv(t);
+ div.style.fontSize = '10px';
+ var animation = div.animate([ { marginLeft: '10em' },
+ { marginLeft: '20em' } ], 1000);
+ animation.currentTime = 500;
+ assert_equals(getComputedStyle(div).marginLeft, '150px',
+ 'Effect value before updating font-size');
+ div.style.fontSize = '20px';
+ assert_equals(getComputedStyle(div).marginLeft, '300px',
+ 'Effect value after updating font-size');
+}, 'Effect values reflect changes to font-size on element');
+
+test(function(t) {
+ var parentDiv = createDiv(t);
+ var div = createDiv(t);
+ parentDiv.appendChild(div);
+ parentDiv.style.fontSize = '10px';
+
+ var animation = div.animate([ { marginLeft: '10em' },
+ { marginLeft: '20em' } ], 1000);
+ animation.currentTime = 500;
+ assert_equals(getComputedStyle(div).marginLeft, '150px',
+ 'Effect value before updating font-size on parent element');
+ parentDiv.style.fontSize = '20px';
+ assert_equals(getComputedStyle(div).marginLeft, '300px',
+ 'Effect value after updating font-size on parent element');
+}, 'Effect values reflect changes to font-size on parent element');
+
+promise_test(function(t) {
+ var parentDiv = createDiv(t);
+ var div = createDiv(t);
+ parentDiv.appendChild(div);
+ parentDiv.style.fontSize = '10px';
+ var animation = div.animate([ { marginLeft: '10em' },
+ { marginLeft: '20em' } ], 1000);
+
+ animation.pause();
+ animation.currentTime = 500;
+ parentDiv.style.fontSize = '20px';
+
+ return animation.ready.then(function() {
+ assert_equals(getComputedStyle(div).marginLeft, '300px',
+ 'Effect value after updating font-size on parent element');
+ });
+}, 'Effect values reflect changes to font-size when computed style is not'
+ + ' immediately flushed');
+
+promise_test(function(t) {
+ var divWith10pxFontSize = createDiv(t);
+ divWith10pxFontSize.style.fontSize = '10px';
+ var divWith20pxFontSize = createDiv(t);
+ divWith20pxFontSize.style.fontSize = '20px';
+
+ var div = createDiv(t);
+ div.remove(); // Detach
+ var animation = div.animate([ { marginLeft: '10em' },
+ { marginLeft: '20em' } ], 1000);
+ animation.pause();
+
+ return animation.ready.then(function() {
+ animation.currentTime = 500;
+
+ divWith10pxFontSize.appendChild(div);
+ assert_equals(getComputedStyle(div).marginLeft, '150px',
+ 'Effect value after attaching to font-size:10px parent');
+ divWith20pxFontSize.appendChild(div);
+ assert_equals(getComputedStyle(div).marginLeft, '300px',
+ 'Effect value after attaching to font-size:20px parent');
+ });
+}, 'Effect values reflect changes to font-size from reparenting');
+
+test(function(t) {
+ var divA = createDiv(t);
+ divA.style.fontSize = '10px';
+
+ var divB = createDiv(t);
+ divB.style.fontSize = '20px';
+
+ var animation = divA.animate([ { marginLeft: '10em' },
+ { marginLeft: '20em' } ], 1000);
+ animation.currentTime = 500;
+ assert_equals(getComputedStyle(divA).marginLeft, '150px',
+ 'Effect value before updating target element');
+
+ animation.effect.target = divB;
+ assert_equals(getComputedStyle(divB).marginLeft, '300px',
+ 'Effect value after updating target element');
+}, 'Effect values reflect changes to target element');
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/web-animations/animation-model/keyframe-effects/spacing-keyframes.html b/testing/web-platform/tests/web-animations/animation-model/keyframe-effects/spacing-keyframes.html
new file mode 100644
index 000000000..90e26d276
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/animation-model/keyframe-effects/spacing-keyframes.html
@@ -0,0 +1,391 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title>Keyframe spacing tests</title>
+<link rel="help" href="https://w3c.github.io/web-animations/#spacing-keyframes">
+<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(function(t) {
+ var anim = createDiv(t).animate([ { marginLeft: '0px' },
+ { marginLeft: '-20px' },
+ { marginLeft: '100px' },
+ { marginLeft: '50px' } ],
+ { duration: 100 * MS_PER_SEC });
+
+ var frames = anim.effect.getKeyframes();
+ var slots = frames.length - 1;
+ assert_equals(frames[0].computedOffset, 0.0, '1st frame offset');
+ assert_equals(frames[1].computedOffset, 1.0 / slots, '2nd frame offset');
+ assert_equals(frames[2].computedOffset, 2.0 / slots, '3rd frame offset');
+ assert_equals(frames[3].computedOffset, 1.0, 'last frame offset');
+}, 'Test distribute spacing');
+
+test(function(t) {
+ var anim = createDiv(t).animate([ { marginLeft: '0px' },
+ { marginLeft: '-20px' },
+ { marginLeft: '100px', offset: 0.5 },
+ { marginLeft: '50px' } ],
+ { duration: 100 * MS_PER_SEC,
+ spacing: 'distribute' });
+
+ var frames = anim.effect.getKeyframes();
+ assert_equals(frames[0].computedOffset, 0.0, '1st frame offset');
+ assert_equals(frames[1].computedOffset, 0.5 * 1 / 2, '2nd frame offset');
+ assert_equals(frames[2].computedOffset, 0.5, '3rd frame offset');
+ assert_equals(frames[3].computedOffset, 1.0, 'last frame offset');
+}, 'Test distribute spacing with specific offsets');
+
+test(function(t) {
+ var anim = createDiv(t).animate(null,
+ { duration: 100 * MS_PER_SEC,
+ spacing: 'paced(margin-left)' });
+
+ var frames = anim.effect.getKeyframes();
+ assert_equals(frames.length, 0, "empty keyframe list");
+}, 'Test paced spacing without any keyframe');
+
+
+test(function(t) {
+ var anim = createDiv(t).animate([ { marginLeft: '0px' },
+ { marginLeft: '-20px' },
+ { marginLeft: '100px' },
+ { marginLeft: '50px' } ],
+ { duration: 100 * MS_PER_SEC,
+ spacing: 'paced(margin-left)' });
+
+ var frames = anim.effect.getKeyframes();
+ var cumDist = [0, 20, 140, 190];
+ assert_equals(frames[0].computedOffset, 0.0,
+ '1st frame offset');
+ assert_equals(frames[1].computedOffset, cumDist[1] / cumDist[3],
+ '2nd frame offset');
+ assert_equals(frames[2].computedOffset, cumDist[2] / cumDist[3],
+ '3rd frame offset');
+ assert_equals(frames[3].computedOffset, 1.0,
+ 'last frame offset');
+}, 'Test paced spacing');
+
+test(function(t) {
+ var anim = createDiv(t).animate([ { marginLeft: '0px' },
+ { marginLeft: '-20px' },
+ { marginLeft: '100px', offset: 0.5 },
+ { marginLeft: '120px' },
+ { marginLeft: '50px' } ],
+ { duration: 100 * MS_PER_SEC,
+ spacing: 'paced(margin-left)' });
+
+ var frames = anim.effect.getKeyframes();
+ var cumDist1 = [ 0, 20, 140 ];
+ var cumDist2 = [ 0, 20, 90 ];
+ assert_equals(frames[1].computedOffset, 0.5 * cumDist1[1] / cumDist1[2],
+ '2nd frame offset');
+ assert_equals(frames[2].computedOffset, 0.5,
+ '3rd frame offset');
+ assert_equals(frames[3].computedOffset, 0.5 + 0.5 * cumDist2[1] / cumDist2[2],
+ '4th frame offset');
+}, 'Test paced spacing with specific offsets');
+
+test(function(t) {
+ var anim = createDiv(t).animate([ { marginLeft: '0px' },
+ { marginLeft: '0px' },
+ { marginLeft: '100px' },
+ { marginLeft: '50px' } ],
+ { duration: 100 * MS_PER_SEC,
+ spacing: 'paced(margin-left)' });
+
+ var frames = anim.effect.getKeyframes();
+ var cumDist = [0, 0, 100, 150];
+ assert_equals(frames[1].computedOffset, cumDist[1] / cumDist[3],
+ '2nd frame offset');
+ assert_equals(frames[2].computedOffset, cumDist[2] / cumDist[3],
+ '3rd frame offset');
+}, 'Test paced spacing if some paced property values are equal');
+
+test(function(t) {
+ var anim = createDiv(t).animate([ { marginLeft: '0px' },
+ { marginLeft: '0px' },
+ { marginLeft: '0px' },
+ { marginLeft: '0px' } ],
+ { duration: 100 * MS_PER_SEC,
+ spacing: 'paced(margin-left)' });
+
+ var frames = anim.effect.getKeyframes();
+ var slots = frames.length - 1;
+ assert_equals(frames[1].computedOffset, 1.0 / slots, '2nd frame offset');
+ assert_equals(frames[2].computedOffset, 2.0 / slots, '3rd frame offset');
+}, 'Test falling back to distribute spacing if all paced property value ' +
+ 'are equal');
+
+test(function(t) {
+ var anim = createDiv(t).animate([ { margin: '0px' },
+ { marginTop: '-20px' },
+ { marginLeft: '100px' },
+ { margin: '50px' } ],
+ { duration: 100 * MS_PER_SEC,
+ spacing: 'paced(margin-left)' });
+
+ var frames = anim.effect.getKeyframes();
+ assert_equals(frames[1].computedOffset, frames[2].computedOffset * 1 / 2,
+ '2nd frame offset using distribute spacing');
+ assert_equals(frames[2].computedOffset, 100 / 150,
+ '3rd frame offset using paced spacing');
+}, 'Test paced spacing if there a keyframe without the paced property');
+
+test(function(t) {
+ var anim = createDiv(t).animate([ { margin: '0px' },
+ { marginTop: '40px' },
+ { marginTop: '-20px' },
+ { marginLeft: '40px' },
+ { marginTop: '60px' },
+ { margin: '10px' } ],
+ { duration: 100 * MS_PER_SEC,
+ spacing: 'paced(margin-left)' });
+
+ var frames = anim.effect.getKeyframes();
+ var cumDist = [0, 0, 0, 40, 40, 70];
+ assert_equals(frames[1].computedOffset, frames[3].computedOffset * 1 / 3,
+ '2nd frame offset using distribute spacing');
+ assert_equals(frames[2].computedOffset, frames[3].computedOffset * 2 / 3,
+ '3rd frame offset using distribute spacing');
+ assert_equals(frames[3].computedOffset, cumDist[3] / cumDist[5],
+ '4th frame offset using paced spacing');
+ assert_equals(frames[4].computedOffset,
+ frames[3].computedOffset +
+ (1 - frames[3].computedOffset) * 1 / 2,
+ '5th frame offset using distribute spacing');
+}, 'Test paced spacing if a paced property that appears on only some ' +
+ 'keyframes');
+
+test(function(t) {
+ var anim = createDiv(t).animate([ { margin: '0px' },
+ { marginTop: '-20px', offset: 0.5 },
+ { marginLeft: '40px' },
+ { marginLeft: '100px' },
+ { margin: '50px' } ],
+ { duration: 100 * MS_PER_SEC,
+ spacing: 'paced(margin-left)' });
+
+ var frames = anim.effect.getKeyframes();
+ assert_equals(frames[2].computedOffset, 0.5 + 0.5 * 1 / 3,
+ '3rd frame offset using distribute spacing because it is the ' +
+ 'first paceable keyframe');
+ assert_equals(frames[3].computedOffset,
+ frames[2].computedOffset +
+ (1.0 - frames[2].computedOffset) * 60 / 110,
+ '4th frame offset using paced spacing');
+}, 'Test paced spacing if a paced property that appears on only some ' +
+ 'keyframes and there is a specific offset');
+
+test(function(t) {
+ var anim = createDiv(t).animate([ { margin: '0px' },
+ { marginTop: '20px', offset: 0.2 },
+ { marginTop: '40px' },
+ { marginTop: '-20px' },
+ { marginLeft: '-20px' },
+ { marginLeft: '40px' },
+ { marginTop: '60px' },
+ { marginLeft: '100px' },
+ { marginTop: '50px' },
+ { marginTop: '100px', offset: 0.8 },
+ { margin: '0px' } ],
+ { duration: 100 * MS_PER_SEC,
+ spacing: 'paced(margin-left)' });
+ var frames = anim.effect.getKeyframes();
+ // Test distribute spacing in (A, Paced A] and [Paced B, frame B).
+ var slots = frames.length - 3;
+ var start = 0.2;
+ var diff = 0.8 - start;
+ assert_equals(frames[2].computedOffset, start + diff * 1.0 / slots,
+ '3nd frame offset using distribute spacing');
+ assert_equals(frames[3].computedOffset, start + diff * 2.0 / slots,
+ '4rd frame offset using distribute spacing');
+ assert_equals(frames[4].computedOffset, start + diff * 3.0 / slots,
+ '5th frame offset using distribute spacing because it is ' +
+ 'the first paceable keyframe');
+ assert_equals(frames[7].computedOffset, start + diff * 6.0 / slots,
+ '8th frame offset using distribute spacing because it is ' +
+ 'the last paceable keyframe');
+ assert_equals(frames[8].computedOffset, start + diff * 7.0 / slots,
+ '9th frame offset using distribute spacing');
+ // Test paced spacing and other null computed offsets in (Paced A, Paced B).
+ var cumDist = [0, 60, 60, 120];
+ assert_equals(frames[5].computedOffset,
+ frames[4].computedOffset + cumDist[2] / cumDist[3] *
+ (frames[7].computedOffset - frames[4].computedOffset),
+ '6th frame offset using paced spacing');
+ assert_equals(frames[6].computedOffset,
+ frames[5].computedOffset + 1.0 / 2.0 *
+ (frames[7].computedOffset - frames[5].computedOffset),
+ '7th frame offset using distribute spacing');
+}, 'Test paced spacing where there are some keyframes without offsets and ' +
+ 'without the paced property before the first paceable keyframe and ' +
+ 'after the last paceable keyframe');
+
+test(function(t) {
+ var anim = createDiv(t).animate([ { margin: '0px' },
+ { margin: '-20px' },
+ { margin: '100px' },
+ { margin: '50px' } ],
+ { duration: 100 * MS_PER_SEC,
+ spacing: 'paced(margin)' });
+
+ var frames = anim.effect.getKeyframes();
+ var cumDist = [0, 20, 140, 190];
+ assert_equals(frames[1].computedOffset, cumDist[1] / cumDist[3],
+ '2nd frame offset');
+ assert_equals(frames[2].computedOffset, cumDist[2] / cumDist[3],
+ '3rd frame offset');
+}, 'Test paced spacing for using shorthand property');
+
+test(function(t) {
+ var anim =
+ createDiv(t).animate([ { marginLeft: '0px', marginRight: '0px',
+ marginTop: '10px', marginBottom: '10px' },
+ { marginLeft: '-20px', marginRight: '-20px',
+ marginTop: '0px', marginBottom: '0px' },
+ { marginLeft: '100px', marginRight: '100px',
+ marginTop: '-50px', marginBottom: '-50px' },
+ { marginLeft: '50px', marginRight: '50px',
+ marginTop: '80px', marginBottom: '80px' } ],
+ { duration: 100 * MS_PER_SEC,
+ spacing: 'paced(margin)' });
+
+ var frames = anim.effect.getKeyframes();
+ var dist = [ 0,
+ Math.sqrt(20 * 20 * 2 + 10 * 10 * 2),
+ Math.sqrt(120 * 120 * 2 + 50 * 50 * 2),
+ Math.sqrt(50 * 50 * 2 + 130 * 130 * 2) ];
+ var cumDist = [];
+ dist.reduce(function(prev, curr, i) { return cumDist[i] = prev + curr; }, 0);
+ assert_approx_equals(frames[1].computedOffset, cumDist[1] / cumDist[3],
+ 0.0001, '2nd frame offset');
+ assert_approx_equals(frames[2].computedOffset, cumDist[2] / cumDist[3],
+ 0.0001, '3rd frame offset');
+}, 'Test paced spacing using shorthand property where only the longhand ' +
+ 'components are specified');
+
+test(function(t) {
+ var anim = createDiv(t).animate([ { marginLeft: '0px', marginTop: '0px' },
+ { marginLeft: '-20px', marginTop: '-20px' },
+ { marginLeft: '100px', marginTop: '100px' },
+ { marginLeft: '50px', marginTop: '50px' } ],
+ { duration: 100 * MS_PER_SEC,
+ spacing: 'paced(margin)' });
+
+ var frames = anim.effect.getKeyframes();
+ var slots = frames.length - 1;
+ assert_equals(frames[1].computedOffset, 1 / slots, '2nd frame offset');
+ assert_equals(frames[2].computedOffset, 2 / slots, '3rd frame offset');
+}, 'Test falling back to distribute spacing if all keyframe miss some ' +
+ 'components');
+
+test(function(t) {
+ var anim = createDiv(t).animate([ { margin: '0px' },
+ { marginLeft: '-20px' },
+ { marginTop: '40px' },
+ { margin: '100px' },
+ { margin: '50px' } ],
+ { duration: 100 * MS_PER_SEC,
+ spacing: 'paced(margin)' });
+
+ var frames = anim.effect.getKeyframes();
+ assert_equals(frames[1].computedOffset, frames[3].computedOffset * 1 / 3,
+ '2nd frame offset using distribute spacing');
+ assert_equals(frames[2].computedOffset, frames[3].computedOffset * 2 / 3,
+ '3rd frame offset using distribute spacing');
+ assert_equals(frames[3].computedOffset, 100 / 150,
+ '4th frame offset using paced spacing');
+}, 'Test paced spacing only for keyframes specifying all longhand ' +
+ 'components, and falling back to distribute spacing for the reset');
+
+test(function(t) {
+ var anim = createDiv(t).animate([ { margin: '0px' },
+ { marginLeft: '-20px' },
+ { marginTop: '40px', offset: 0.5 },
+ { margin: '100px' },
+ { margin: '50px' } ],
+ { duration: 100 * MS_PER_SEC,
+ spacing: 'paced(margin)' });
+
+ var frames = anim.effect.getKeyframes();
+ assert_equals(frames[1].computedOffset, 0.5 * 1 / 2,
+ '2nd frame offset using distribute spacing');
+ assert_equals(frames[3].computedOffset, 0.5 + 0.5 * 1 / 2,
+ '4th frame offset using distribute spacing because it is the ' +
+ 'first paceable keyframe from a non-null offset keyframe');
+}, 'Test paced spacing only for keyframes specifying all some components, ' +
+ 'and falling back to distribute spacing for the reset with some specific ' +
+ 'offsets');
+
+// Tests for setting spacing by KeyframeEffect.spacing.
+
+test(function(t) {
+ var anim = createDiv(t).animate([ { marginLeft: '0px' },
+ { marginLeft: '-20px' },
+ { marginLeft: '100px' },
+ { marginLeft: '50px' } ],
+ { duration: 100 * MS_PER_SEC });
+
+ anim.effect.spacing = 'paced(margin-left)';
+
+ var frames = anim.effect.getKeyframes();
+ var cumDist = [0, 20, 140, 190];
+ assert_equals(frames[0].computedOffset, 0.0,
+ '1st frame offset');
+ assert_equals(frames[1].computedOffset, cumDist[1] / cumDist[3],
+ '2nd frame offset');
+ assert_equals(frames[2].computedOffset, cumDist[2] / cumDist[3],
+ '3rd frame offset');
+ assert_equals(frames[3].computedOffset, 1.0,
+ 'last frame offset');
+}, 'Test paced spacing by setter');
+
+test(function(t) {
+ var anim = createDiv(t).animate([ { marginLeft: '0px' },
+ { marginLeft: '-20px' },
+ { marginLeft: '100px' },
+ { marginLeft: '50px' } ],
+ { duration: 100 * MS_PER_SEC,
+ spacing: 'paced(margin-left)' });
+
+ anim.effect.spacing = 'distribute';
+
+ var frames = anim.effect.getKeyframes();
+ var slots = frames.length - 1;
+ assert_equals(frames[0].computedOffset, 0.0, '1st frame offset');
+ assert_equals(frames[1].computedOffset, 1.0 / slots, '2nd frame offset');
+ assert_equals(frames[2].computedOffset, 2.0 / slots, '3rd frame offset');
+ assert_equals(frames[3].computedOffset, 1.0, 'last frame offset');
+}, 'Test distribute spacing by setter');
+
+test(function(t) {
+ var anim =
+ createDiv(t).animate([ { marginLeft: '0px', borderRadius: '0%' },
+ { marginLeft: '-20px', borderRadius: '50%' },
+ { marginLeft: '100px', borderRadius: '25%' },
+ { marginLeft: '50px', borderRadius: '100%' } ],
+ { duration: 100 * MS_PER_SEC,
+ spacing: 'paced(margin-left)' });
+
+ anim.effect.spacing = 'paced(border-radius)';
+
+ var frames = anim.effect.getKeyframes();
+ var cumDist = [0, 50, 50 + 25, 50 + 25 + 75];
+
+ assert_equals(frames[0].computedOffset, 0.0,
+ '1st frame offset');
+ assert_equals(frames[1].computedOffset, cumDist[1] / cumDist[3],
+ '2nd frame offset');
+ assert_equals(frames[2].computedOffset, cumDist[2] / cumDist[3],
+ '3rd frame offset');
+ assert_equals(frames[3].computedOffset, 1.0,
+ 'last frame offset');
+}, 'Test paced spacing by changing the paced property');
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/web-animations/animation-model/keyframe-effects/the-effect-value-of-a-keyframe-effect.html b/testing/web-platform/tests/web-animations/animation-model/keyframe-effects/the-effect-value-of-a-keyframe-effect.html
new file mode 100644
index 000000000..eb67f669a
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/animation-model/keyframe-effects/the-effect-value-of-a-keyframe-effect.html
@@ -0,0 +1,114 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title>Keyframe handling tests</title>
+<link rel="help" href="https://w3c.github.io/web-animations/#the-effect-value-of-a-keyframe-animation-effect">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../../testcommon.js"></script>
+<body>
+<div id="log"></div>
+<div id="target"></div>
+<script>
+'use strict';
+
+test(function(t) {
+ var div = createDiv(t);
+ var anim = div.animate([ { offset: 0, opacity: 0 },
+ { offset: 0, opacity: 0.1 },
+ { offset: 0, opacity: 0.2 },
+ { offset: 1, opacity: 0.8 },
+ { offset: 1, opacity: 0.9 },
+ { offset: 1, opacity: 1 } ],
+ { duration: 1000,
+ easing: 'cubic-bezier(0.5, -0.5, 0.5, 1.5)' });
+ assert_equals(getComputedStyle(div).opacity, '0.2',
+ 'When progress is zero the last keyframe with offset 0 should'
+ + ' be used');
+ // http://cubic-bezier.com/#.5,-0.5,.5,1.5
+ // At t=0.15, the progress should be negative
+ anim.currentTime = 150;
+ assert_equals(getComputedStyle(div).opacity, '0',
+ 'When progress is negative, the first keyframe with a 0 offset'
+ + ' should be used');
+ // At t=0.71, the progress should be just less than 1
+ anim.currentTime = 710;
+ assert_approx_equals(parseFloat(getComputedStyle(div).opacity), 0.8, 0.01,
+ 'When progress is just less than 1, the first keyframe with an'
+ + ' offset of 1 should be used as the interval endpoint');
+ // At t=0.85, the progress should be > 1
+ anim.currentTime = 850;
+ assert_equals(getComputedStyle(div).opacity, '1',
+ 'When progress is greater than 1.0, the last keyframe with a 1'
+ + ' offset should be used');
+ anim.finish();
+ assert_equals(getComputedStyle(div).opacity, '1',
+ 'When progress is equal to 1.0, the last keyframe with a 1'
+ + ' offset should be used');
+}, 'Overlapping keyframes at 0 and 1 use the appropriate value when the'
+ + ' progress is outside the range [0, 1]');
+
+test(function(t) {
+ var div = createDiv(t);
+ var anim = div.animate([ { offset: 0, opacity: 0 },
+ { offset: 0.5, opacity: 0.3 },
+ { offset: 0.5, opacity: 0.5 },
+ { offset: 0.5, opacity: 0.7 },
+ { offset: 1, opacity: 1 } ], 1000);
+ anim.currentTime = 250;
+ assert_equals(getComputedStyle(div).opacity, '0.15',
+ 'Before the overlap point, the first keyframe from the'
+ + ' overlap point should be used as interval endpoint');
+ anim.currentTime = 500;
+ assert_equals(getComputedStyle(div).opacity, '0.7',
+ 'At the overlap point, the last keyframe from the'
+ + ' overlap point should be used as interval startpoint');
+ anim.currentTime = 750;
+ assert_equals(getComputedStyle(div).opacity, '0.85',
+ 'After the overlap point, the last keyframe from the'
+ + ' overlap point should be used as interval startpoint');
+}, 'Overlapping keyframes between 0 and 1 use the appropriate value on each'
+ + ' side of the overlap point');
+
+test(function(t) {
+ var div = createDiv(t);
+ var anim = div.animate({ visibility: ['hidden','visible'] },
+ { duration: 100 * MS_PER_SEC, fill: 'both' });
+
+ anim.currentTime = 0;
+ assert_equals(getComputedStyle(div).visibility, 'hidden',
+ 'Visibility when progress = 0.');
+
+ anim.currentTime = 10 * MS_PER_SEC + 1;
+ assert_equals(getComputedStyle(div).visibility, 'visible',
+ 'Visibility when progress > 0 due to linear easing.');
+
+ anim.finish();
+ assert_equals(getComputedStyle(div).visibility, 'visible',
+ 'Visibility when progress = 1.');
+
+}, "Test visibility clamping behavior.");
+
+test(function(t) {
+ var div = createDiv(t);
+ var anim = div.animate({ visibility: ['hidden', 'visible'] },
+ { duration: 100 * MS_PER_SEC, fill: 'both',
+ easing: 'cubic-bezier(0.25, -0.6, 0, 0.5)' });
+
+ anim.currentTime = 0;
+ assert_equals(getComputedStyle(div).visibility, 'hidden',
+ 'Visibility when progress = 0.');
+
+ // Timing function is below zero. So we expected visibility is hidden.
+ anim.currentTime = 10 * MS_PER_SEC + 1;
+ assert_equals(getComputedStyle(div).visibility, 'hidden',
+ 'Visibility when progress < 0 due to cubic-bezier easing.');
+
+ anim.currentTime = 60 * MS_PER_SEC;
+ assert_equals(getComputedStyle(div).visibility, 'visible',
+ 'Visibility when progress > 0 due to cubic-bezier easing.');
+
+}, "Test visibility clamping behavior with an easing that has a negative component");
+
+done();
+</script>
+</body>
diff --git a/testing/web-platform/tests/web-animations/interfaces/Animatable/animate.html b/testing/web-platform/tests/web-animations/interfaces/Animatable/animate.html
new file mode 100644
index 000000000..a54298aa4
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/interfaces/Animatable/animate.html
@@ -0,0 +1,180 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title>Animatable.animate tests</title>
+<link rel="help" href="https://w3c.github.io/web-animations/#dom-animatable-animate">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../../testcommon.js"></script>
+<script src="../../resources/keyframe-utils.js"></script>
+<body>
+<div id="log"></div>
+<iframe width="10" height="10" id="iframe"></iframe>
+<script>
+'use strict';
+
+// Tests on Element
+
+test(function(t) {
+ var div = createDiv(t);
+ var anim = div.animate(null);
+ assert_class_string(anim, 'Animation', 'Returned object is an Animation');
+}, 'Element.animate() creates an Animation object');
+
+test(function(t) {
+ var iframe = window.frames[0];
+ var div = createDiv(t, iframe.document);
+ var anim = Element.prototype.animate.call(div, null);
+ assert_equals(Object.getPrototypeOf(anim), iframe.Animation.prototype,
+ 'The prototype of the created Animation is that defined on'
+ + ' the relevant global for the target element');
+ assert_not_equals(Object.getPrototypeOf(anim), Animation.prototype,
+ 'The prototype of the created Animation is NOT that of'
+ + ' the current global');
+}, 'Element.animate() creates an Animation object in the relevant realm of'
+ + ' the target element');
+
+test(function(t) {
+ var div = createDiv(t);
+ var anim = Element.prototype.animate.call(div, null);
+ assert_class_string(anim.effect, 'KeyframeEffect',
+ 'Returned Animation has a KeyframeEffect');
+}, 'Element.animate() creates an Animation object with a KeyframeEffect');
+
+test(function(t) {
+ var iframe = window.frames[0];
+ var div = createDiv(t, iframe.document);
+ var anim = Element.prototype.animate.call(div, null);
+ assert_equals(Object.getPrototypeOf(anim.effect),
+ iframe.KeyframeEffect.prototype,
+ 'The prototype of the created KeyframeEffect is that defined on'
+ + ' the relevant global for the target element');
+ assert_not_equals(Object.getPrototypeOf(anim.effect),
+ KeyframeEffect.prototype,
+ 'The prototype of the created KeyframeEffect is NOT that of'
+ + ' the current global');
+}, 'Element.animate() creates an Animation object with a KeyframeEffect'
+ + ' that is created in the relevant realm of the target element');
+
+test(function(t) {
+ var iframe = window.frames[0];
+ var div = createDiv(t, iframe.document);
+ var anim = div.animate(null);
+ assert_equals(Object.getPrototypeOf(anim.effect.timing),
+ iframe.AnimationEffectTiming.prototype,
+ 'The prototype of the created AnimationEffectTiming is that'
+ + ' defined on the relevant global for the target element');
+ assert_not_equals(Object.getPrototypeOf(anim.effect.timing),
+ AnimationEffectTiming.prototype,
+ 'The prototype of the created AnimationEffectTiming is NOT'
+ + ' that of the current global');
+}, 'Element.animate() creates an Animation object with a KeyframeEffect'
+ + ' whose AnimationEffectTiming object is created in the relevant realm'
+ + ' of the target element');
+
+gPropertyIndexedKeyframesTests.forEach(function(subtest) {
+ test(function(t) {
+ var div = createDiv(t);
+ var anim = div.animate(subtest.input, 2000);
+ assert_frame_lists_equal(anim.effect.getKeyframes(), subtest.output);
+ }, 'Element.animate() accepts ' + subtest.desc);
+});
+
+gKeyframeSequenceTests.forEach(function(subtest) {
+ test(function(t) {
+ var div = createDiv(t);
+ var anim = div.animate(subtest.input, 2000);
+ assert_frame_lists_equal(anim.effect.getKeyframes(), subtest.output);
+ }, 'Element.animate() accepts ' + subtest.desc);
+});
+
+gInvalidKeyframesTests.forEach(function(subtest) {
+ test(function(t) {
+ var div = createDiv(t);
+ assert_throws(subtest.expected, function() {
+ div.animate(subtest.input, 2000);
+ });
+ }, 'Element.animate() does not accept ' + subtest.desc);
+});
+
+test(function(t) {
+ var div = createDiv(t);
+ var anim = div.animate({ opacity: [ 0, 1 ] }, 2000);
+ assert_equals(anim.effect.timing.duration, 2000);
+ // Also check that unspecified parameters receive their default values
+ assert_equals(anim.effect.timing.fill, 'auto');
+}, 'Element.animate() accepts a double as an options argument');
+
+test(function(t) {
+ var div = createDiv(t);
+ var anim = div.animate({ opacity: [ 0, 1 ] },
+ { duration: Infinity, fill: 'forwards' });
+ assert_equals(anim.effect.timing.duration, Infinity);
+ assert_equals(anim.effect.timing.fill, 'forwards');
+ // Also check that unspecified parameters receive their default values
+ assert_equals(anim.effect.timing.direction, 'normal');
+}, 'Element.animate() accepts a KeyframeAnimationOptions argument');
+
+test(function(t) {
+ var div = createDiv(t);
+ var anim = div.animate({ opacity: [ 0, 1 ] });
+ assert_equals(anim.effect.timing.duration, 'auto');
+}, 'Element.animate() accepts an absent options argument');
+
+test(function(t) {
+ var div = createDiv(t);
+ var anim = div.animate({ opacity: [ 0, 1 ] }, 2000);
+ assert_equals(anim.id, '');
+}, 'Element.animate() correctly sets the id attribute when no id is specified');
+
+test(function(t) {
+ var div = createDiv(t);
+ var anim = div.animate({ opacity: [ 0, 1 ] }, { id: 'test' });
+ assert_equals(anim.id, 'test');
+}, 'Element.animate() correctly sets the id attribute');
+
+test(function(t) {
+ var div = createDiv(t);
+ var anim = div.animate({ opacity: [ 0, 1 ] }, 2000);
+ assert_equals(anim.timeline, document.timeline);
+}, 'Element.animate() correctly sets the Animation\'s timeline');
+
+async_test(function(t) {
+ var iframe = document.createElement('iframe');
+ iframe.width = 10;
+ iframe.height = 10;
+
+ iframe.addEventListener('load', t.step_func(function() {
+ var div = createDiv(t, iframe.contentDocument);
+ var anim = div.animate({ opacity: [ 0, 1 ] }, 2000);
+ assert_equals(anim.timeline, iframe.contentDocument.timeline);
+ iframe.remove();
+ t.done();
+ }));
+
+ document.body.appendChild(iframe);
+}, 'Element.animate() correctly sets the Animation\'s timeline when ' +
+ 'triggered on an element in a different document');
+
+test(function(t) {
+ var div = createDiv(t);
+ var anim = div.animate({ opacity: [ 0, 1 ] }, 2000);
+ assert_equals(anim.playState, 'pending');
+}, 'Element.animate() calls play on the Animation');
+
+// Tests on CSSPseudoElement
+
+test(function(t) {
+ var pseudoTarget = createPseudo(t, 'before');
+ var anim = pseudoTarget.animate(null);
+ assert_class_string(anim, 'Animation', 'The returned object is an Animation');
+}, 'CSSPseudoElement.animate() creates an Animation object');
+
+test(function(t) {
+ var pseudoTarget = createPseudo(t, 'before');
+ var anim = pseudoTarget.animate(null);
+ assert_equals(anim.effect.target, pseudoTarget,
+ 'The returned Animation targets to the correct object');
+}, 'CSSPseudoElement.animate() creates an Animation object targeting ' +
+ 'to the correct CSSPseudoElement object');
+</script>
+</body>
diff --git a/testing/web-platform/tests/web-animations/interfaces/Animation/cancel.html b/testing/web-platform/tests/web-animations/interfaces/Animation/cancel.html
new file mode 100644
index 000000000..f8f174abd
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/interfaces/Animation/cancel.html
@@ -0,0 +1,60 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title>Animation.cancel()</title>
+<link rel="help" href="https://w3c.github.io/web-animations/#dom-animation-cancel">
+<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(function(t) {
+ var div = createDiv(t);
+ var animation = div.animate({transform: ['translate(100px)', 'translate(100px)']},
+ 100 * MS_PER_SEC);
+ return animation.ready.then(function() {
+ assert_not_equals(getComputedStyle(div).transform, 'none',
+ 'transform style is animated before cancelling');
+ animation.cancel();
+ assert_equals(getComputedStyle(div).transform, 'none',
+ 'transform style is no longer animated after cancelling');
+ });
+}, 'Animated style is cleared after calling Animation.cancel()');
+
+test(function(t) {
+ var div = createDiv(t);
+ var animation = div.animate({marginLeft: ['100px', '200px']},
+ 100 * MS_PER_SEC);
+ animation.effect.timing.easing = 'linear';
+ animation.cancel();
+ assert_equals(getComputedStyle(div).marginLeft, '0px',
+ 'margin-left style is not animated after cancelling');
+
+ animation.currentTime = 50 * MS_PER_SEC;
+ assert_equals(getComputedStyle(div).marginLeft, '150px',
+ 'margin-left style is updated when cancelled animation is'
+ + ' seeked');
+}, 'After cancelling an animation, it can still be seeked');
+
+promise_test(function(t) {
+ var div = createDiv(t);
+ var animation = div.animate({marginLeft:['100px', '200px']},
+ 100 * MS_PER_SEC);
+ return animation.ready.then(function() {
+ animation.cancel();
+ assert_equals(getComputedStyle(div).marginLeft, '0px',
+ 'margin-left style is not animated after cancelling');
+ animation.play();
+ assert_equals(getComputedStyle(div).marginLeft, '100px',
+ 'margin-left style is animated after re-starting animation');
+ return animation.ready;
+ }).then(function() {
+ assert_equals(animation.playState, 'running',
+ 'Animation succeeds in running after being re-started');
+ });
+}, 'After cancelling an animation, it can still be re-used');
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/web-animations/interfaces/Animation/constructor.html b/testing/web-platform/tests/web-animations/interfaces/Animation/constructor.html
new file mode 100644
index 000000000..4f76194b6
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/interfaces/Animation/constructor.html
@@ -0,0 +1,115 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title>Animation constructor tests</title>
+<link rel="help" href="http://w3c.github.io/web-animations/#dom-animation-animation">
+<link rel="author" title="Hiroyuki Ikezoe" href="mailto:hiikezoe@mozilla-japan.org">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../../testcommon.js"></script>
+<body>
+<div id="log"></div>
+<div id="target"></div>
+<script>
+"use strict";
+
+var gTarget = document.getElementById("target");
+
+function createEffect() {
+ return new KeyframeEffectReadOnly(gTarget, { opacity: [0, 1] });
+}
+
+function createNull() {
+ return null;
+}
+
+var gTestArguments = [
+ {
+ createEffect: createNull,
+ timeline: null,
+ expectedTimeline: null,
+ expectedTimelineDescription: "null",
+ description: "with null effect and null timeline"
+ },
+ {
+ createEffect: createNull,
+ timeline: document.timeline,
+ expectedTimeline: document.timeline,
+ expectedTimelineDescription: "document.timeline",
+ description: "with null effect and non-null timeline"
+ },
+ {
+ createEffect: createNull,
+ expectedTimeline: document.timeline,
+ expectedTimelineDescription: "document.timeline",
+ description: "with null effect and no timeline parameter"
+ },
+ {
+ createEffect: createEffect,
+ timeline: null,
+ expectedTimeline: null,
+ expectedTimelineDescription: "null",
+ description: "with non-null effect and null timeline"
+ },
+ {
+ createEffect: createEffect,
+ timeline: document.timeline,
+ expectedTimeline: document.timeline,
+ expectedTimelineDescription: "document.timeline",
+ description: "with non-null effect and non-null timeline"
+ },
+ {
+ createEffect: createEffect,
+ expectedTimeline: document.timeline,
+ expectedTimelineDescription: "document.timeline",
+ description: "with non-null effect and no timeline parameter"
+ },
+];
+
+gTestArguments.forEach(function(args) {
+ test(function(t) {
+ var effect = args.createEffect();
+ var animation = new Animation(effect, args.timeline);
+
+ assert_not_equals(animation, null,
+ "An animation sohuld be created");
+ assert_equals(animation.effect, effect,
+ "Animation returns the same effect passed to " +
+ "the Constructor");
+ assert_equals(animation.timeline, args.expectedTimeline,
+ "Animation timeline should be " + args.expectedTimelineDescription);
+ assert_equals(animation.playState, "idle",
+ "Animation.playState should be initially 'idle'");
+ }, "Animation can be constructed " + args.description);
+});
+
+test(function(t) {
+ var effect = new KeyframeEffectReadOnly(null,
+ { left: ["10px", "20px"] },
+ { duration: 10000,
+ fill: "forwards" });
+ var anim = new Animation(effect, document.timeline);
+ anim.pause();
+ assert_equals(effect.getComputedTiming().progress, 0.0);
+ anim.currentTime += 5000;
+ assert_equals(effect.getComputedTiming().progress, 0.5);
+ anim.finish();
+ assert_equals(effect.getComputedTiming().progress, 1.0);
+}, "Animation constructed by an effect with null target runs normally");
+
+async_test(function(t) {
+ var iframe = document.createElement('iframe');
+
+ iframe.addEventListener('load', t.step_func(function() {
+ var div = createDiv(t, iframe.contentDocument);
+ var effect = new KeyframeEffectReadOnly(div, null, 10000);
+ var anim = new Animation(effect);
+ assert_equals(anim.timeline, document.timeline);
+ iframe.remove();
+ t.done();
+ }));
+
+ document.body.appendChild(iframe);
+}, "Animation constructed with a keyframe that target element is in iframe");
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/web-animations/interfaces/Animation/effect.html b/testing/web-platform/tests/web-animations/interfaces/Animation/effect.html
new file mode 100644
index 000000000..d881a96a7
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/interfaces/Animation/effect.html
@@ -0,0 +1,23 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title>Animation.effect tests</title>
+<link rel="help" href="https://w3c.github.io/web-animations/#dom-animation-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";
+
+test(function(t) {
+ var anim = new Animation();
+ assert_equals(anim.effect, null, "initial effect is null");
+
+ var newEffect = new KeyframeEffectReadOnly(createDiv(t), null);
+ anim.effect = newEffect;
+ assert_equals(anim.effect, newEffect, "new effect is set");
+}, "effect is set correctly.");
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/web-animations/interfaces/Animation/finish.html b/testing/web-platform/tests/web-animations/interfaces/Animation/finish.html
new file mode 100644
index 000000000..9b9c4c710
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/interfaces/Animation/finish.html
@@ -0,0 +1,246 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title>Animation.finish()</title>
+<link rel="help" href="https://w3c.github.io/web-animations/#dom-animation-finish">
+<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';
+
+var gKeyFrames = { 'marginLeft': ['100px', '200px'] };
+
+test(function(t) {
+ var div = createDiv(t);
+ var animation = div.animate(gKeyFrames, 100 * MS_PER_SEC);
+ animation.playbackRate = 0;
+
+ assert_throws({name: 'InvalidStateError'}, function() {
+ animation.finish();
+ });
+}, 'Test exceptions when finishing non-running animation');
+
+test(function(t) {
+ var div = createDiv(t);
+ var animation = div.animate(gKeyFrames,
+ {duration : 100 * MS_PER_SEC,
+ iterations : Infinity});
+
+ assert_throws({name: 'InvalidStateError'}, function() {
+ animation.finish();
+ });
+}, 'Test exceptions when finishing infinite animation');
+
+test(function(t) {
+ var div = createDiv(t);
+ var animation = div.animate(gKeyFrames, 100 * MS_PER_SEC);
+ animation.finish();
+
+ assert_equals(animation.currentTime, 100 * MS_PER_SEC,
+ 'After finishing, the currentTime should be set to the end ' +
+ 'of the active duration');
+}, 'Test finishing of animation');
+
+test(function(t) {
+ var div = createDiv(t);
+ var animation = div.animate(gKeyFrames, 100 * MS_PER_SEC);
+ // 1s past effect end
+ animation.currentTime =
+ animation.effect.getComputedTiming().endTime + 1 * MS_PER_SEC;
+ animation.finish();
+
+ assert_equals(animation.currentTime, 100 * MS_PER_SEC,
+ 'After finishing, the currentTime should be set back to the ' +
+ 'end of the active duration');
+}, 'Test finishing of animation with a current time past the effect end');
+
+promise_test(function(t) {
+ var div = createDiv(t);
+ var animation = div.animate(gKeyFrames, 100 * MS_PER_SEC);
+ animation.currentTime = 100 * MS_PER_SEC;
+ return animation.finished.then(function() {
+ animation.playbackRate = -1;
+ animation.finish();
+
+ assert_equals(animation.currentTime, 0,
+ 'After finishing a reversed animation the currentTime ' +
+ 'should be set to zero');
+ });
+}, 'Test finishing of reversed animation');
+
+promise_test(function(t) {
+ var div = createDiv(t);
+ var animation = div.animate(gKeyFrames, 100 * MS_PER_SEC);
+ animation.currentTime = 100 * MS_PER_SEC;
+ return animation.finished.then(function() {
+ 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');
+ });
+}, 'Test finishing of reversed animation with a current time less than zero');
+
+promise_test(function(t) {
+ var div = createDiv(t);
+ var animation = div.animate(gKeyFrames, 100 * MS_PER_SEC);
+ animation.pause();
+ return animation.ready.then(function() {
+ animation.finish();
+
+ assert_equals(animation.playState, 'finished',
+ 'The play state of a paused animation should become ' +
+ '"finished" after finish() is called');
+ assert_times_equal(animation.startTime,
+ animation.timeline.currentTime - 100 * MS_PER_SEC,
+ 'The start time of a paused animation should be set ' +
+ 'after calling finish()');
+ });
+}, 'Test finish() while paused');
+
+test(function(t) {
+ var div = createDiv(t);
+ var animation = div.animate(gKeyFrames, 100 * MS_PER_SEC);
+ animation.pause();
+ // Update playbackRate so we can test that the calculated startTime
+ // respects it
+ animation.playbackRate = 2;
+ // While animation is still pause-pending call finish()
+ animation.finish();
+
+ assert_equals(animation.playState, 'finished',
+ 'The play state of a pause-pending animation should become ' +
+ '"finished" after finish() is called');
+ assert_times_equal(animation.startTime,
+ animation.timeline.currentTime - 100 * MS_PER_SEC / 2,
+ 'The start time of a pause-pending animation should ' +
+ 'be set after calling finish()');
+}, 'Test finish() while pause-pending with positive playbackRate');
+
+test(function(t) {
+ var div = createDiv(t);
+ var animation = div.animate(gKeyFrames, 100 * MS_PER_SEC);
+ animation.pause();
+ animation.playbackRate = -2;
+ animation.finish();
+
+ assert_equals(animation.playState, 'finished',
+ 'The play state of a pause-pending animation should become ' +
+ '"finished" after finish() is called');
+ assert_equals(animation.startTime, animation.timeline.currentTime,
+ 'The start time of a pause-pending animation should be ' +
+ 'set after calling finish()');
+}, 'Test finish() while pause-pending with negative playbackRate');
+
+test(function(t) {
+ var div = createDiv(t);
+ var animation = div.animate(gKeyFrames, 100 * MS_PER_SEC);
+ animation.playbackRate = 0.5;
+ animation.finish();
+
+ assert_equals(animation.playState, 'finished',
+ 'The play state of a play-pending animation should become ' +
+ '"finished" after finish() is called');
+ 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 after calling finish()');
+}, 'Test finish() while play-pending');
+
+// 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(function(t) {
+ var div = createDiv(t);
+ var animation = div.animate(gKeyFrames, 100 * MS_PER_SEC);
+ return animation.ready.then(function() {
+ 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 calling finish() the play ' +
+ 'state of an animation should become "finished" immediately');
+ });
+}, 'Test finish() during aborted pause');
+
+promise_test(function(t) {
+ var div = createDiv(t);
+ div.style.marginLeft = '10px';
+ var animation = div.animate(gKeyFrames, 100 * MS_PER_SEC);
+ return animation.ready.then(function() {
+ animation.finish();
+ var marginLeft = parseFloat(getComputedStyle(div).marginLeft);
+
+ assert_equals(marginLeft, 10,
+ 'The computed style should be reset when finish() is ' +
+ 'called');
+ });
+}, 'Test resetting of computed style');
+
+promise_test(function(t) {
+ var div = createDiv(t);
+ var animation = div.animate(gKeyFrames, 100 * MS_PER_SEC);
+ var resolvedFinished = false;
+ animation.finished.then(function() {
+ resolvedFinished = true;
+ });
+
+ return animation.ready.then(function() {
+ animation.finish();
+ }).then(function() {
+ assert_true(resolvedFinished,
+ 'Animation.finished should be resolved soon after ' +
+ 'Animation.finish()');
+ });
+}, 'Test finish() resolves finished promise synchronously');
+
+promise_test(function(t) {
+ var effect = new KeyframeEffectReadOnly(null, gKeyFrames, 100 * MS_PER_SEC);
+ var animation = new Animation(effect, document.timeline);
+ var resolvedFinished = false;
+ animation.finished.then(function() {
+ resolvedFinished = true;
+ });
+
+ return animation.ready.then(function() {
+ animation.finish();
+ }).then(function() {
+ assert_true(resolvedFinished,
+ 'Animation.finished should be resolved soon after ' +
+ 'Animation.finish()');
+ });
+}, 'Test finish() resolves finished promise synchronously with an animation ' +
+ 'without a target');
+
+promise_test(function(t) {
+ var effect = new KeyframeEffectReadOnly(null, gKeyFrames, 100 * MS_PER_SEC);
+ var animation = new Animation(effect, document.timeline);
+ animation.play();
+
+ var resolvedFinished = false;
+ animation.finished.then(function() {
+ resolvedFinished = true;
+ });
+
+ return animation.ready.then(function() {
+ animation.currentTime = animation.effect.getComputedTiming().endTime - 1;
+ return waitForAnimationFrames(2);
+ }).then(function() {
+ assert_true(resolvedFinished,
+ 'Animation.finished should be resolved soon after ' +
+ 'Animation finishes normally');
+ });
+}, 'Test normally finished animation resolves finished promise synchronously ' +
+ 'with an animation without a target');
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/web-animations/interfaces/Animation/finished.html b/testing/web-platform/tests/web-animations/interfaces/Animation/finished.html
new file mode 100644
index 000000000..d56de1b03
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/interfaces/Animation/finished.html
@@ -0,0 +1,370 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title>Animation.finished</title>
+<link rel="help" href="https://w3c.github.io/web-animations/#dom-animation-finished">
+<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(function(t) {
+ var div = createDiv(t);
+ var animation = div.animate({}, 100 * MS_PER_SEC);
+ var previousFinishedPromise = animation.finished;
+ return animation.ready.then(function() {
+ assert_equals(animation.finished, previousFinishedPromise,
+ 'Finished promise is the same object when playing starts');
+ animation.pause();
+ assert_equals(animation.finished, previousFinishedPromise,
+ 'Finished promise does not change when pausing');
+ animation.play();
+ assert_equals(animation.finished, previousFinishedPromise,
+ 'Finished promise does not change when play() unpauses');
+
+ animation.currentTime = 100 * MS_PER_SEC;
+
+ return animation.finished;
+ }).then(function() {
+ assert_equals(animation.finished, previousFinishedPromise,
+ 'Finished promise is the same object when playing completes');
+ });
+}, 'Test pausing then playing does not change the finished promise');
+
+promise_test(function(t) {
+ var div = createDiv(t);
+ var animation = div.animate({}, 100 * MS_PER_SEC);
+ var previousFinishedPromise = animation.finished;
+ animation.finish();
+ return animation.finished.then(function() {
+ assert_equals(animation.finished, previousFinishedPromise,
+ 'Finished promise is the same object when playing completes');
+ animation.play();
+ assert_not_equals(animation.finished, previousFinishedPromise,
+ 'Finished promise changes when replaying animation');
+
+ previousFinishedPromise = animation.finished;
+ animation.play();
+ assert_equals(animation.finished, previousFinishedPromise,
+ 'Finished promise is the same after redundant play() call');
+
+ });
+}, 'Test restarting a finished animation');
+
+promise_test(function(t) {
+ var div = createDiv(t);
+ var animation = div.animate({}, 100 * MS_PER_SEC);
+ var previousFinishedPromise;
+ animation.finish();
+ return animation.finished.then(function() {
+ previousFinishedPromise = animation.finished;
+ animation.playbackRate = -1;
+ assert_not_equals(animation.finished, previousFinishedPromise,
+ 'Finished promise should be replaced when reversing a ' +
+ 'finished promise');
+ animation.currentTime = 0;
+ return animation.finished;
+ }).then(function() {
+ previousFinishedPromise = animation.finished;
+ animation.play();
+ assert_not_equals(animation.finished, previousFinishedPromise,
+ 'Finished promise is replaced after play() call on ' +
+ 'finished, reversed animation');
+ });
+}, 'Test restarting a reversed finished animation');
+
+promise_test(function(t) {
+ var div = createDiv(t);
+ var animation = div.animate({}, 100 * MS_PER_SEC);
+ var previousFinishedPromise = animation.finished;
+ animation.finish();
+ return animation.finished.then(function() {
+ animation.currentTime = 100 * MS_PER_SEC + 1000;
+ assert_equals(animation.finished, previousFinishedPromise,
+ 'Finished promise is unchanged jumping past end of ' +
+ 'finished animation');
+ });
+}, 'Test redundant finishing of animation');
+
+promise_test(function(t) {
+ var div = createDiv(t);
+ var animation = div.animate({}, 100 * MS_PER_SEC);
+ // Setup callback to run if finished promise is resolved
+ var finishPromiseResolved = false;
+ animation.finished.then(function() {
+ finishPromiseResolved = true;
+ });
+ return animation.ready.then(function() {
+ // Jump to mid-way in interval and pause
+ animation.currentTime = 100 * MS_PER_SEC / 2;
+ animation.pause();
+ return animation.ready;
+ }).then(function() {
+ // Jump to the end
+ // (But don't use finish() since that should unpause as well)
+ animation.currentTime = 100 * MS_PER_SEC;
+ return waitForAnimationFrames(2);
+ }).then(function() {
+ assert_false(finishPromiseResolved,
+ 'Finished promise should not resolve when paused');
+ });
+}, 'Finished promise does not resolve when paused');
+
+promise_test(function(t) {
+ var div = createDiv(t);
+ var animation = div.animate({}, 100 * MS_PER_SEC);
+ // Setup callback to run if finished promise is resolved
+ var finishPromiseResolved = false;
+ animation.finished.then(function() {
+ finishPromiseResolved = true;
+ });
+ return animation.ready.then(function() {
+ // Jump to mid-way in interval and pause
+ animation.currentTime = 100 * MS_PER_SEC / 2;
+ animation.pause();
+ // Jump to the end
+ animation.currentTime = 100 * MS_PER_SEC;
+ return waitForAnimationFrames(2);
+ }).then(function() {
+ assert_false(finishPromiseResolved,
+ 'Finished promise should not resolve when pause-pending');
+ });
+}, 'Finished promise does not resolve when pause-pending');
+
+promise_test(function(t) {
+ var div = createDiv(t);
+ var animation = div.animate({}, 100 * MS_PER_SEC);
+ animation.finish();
+ return animation.finished.then(function(resolvedAnimation) {
+ assert_equals(resolvedAnimation, animation,
+ 'Object identity of animation passed to Promise callback'
+ + ' matches the animation object owning the Promise');
+ });
+}, 'The finished promise is fulfilled with its Animation');
+
+promise_test(function(t) {
+ var div = createDiv(t);
+ var animation = div.animate({}, 100 * MS_PER_SEC);
+ var previousFinishedPromise = animation.finished;
+
+ // Set up listeners on finished promise
+ var retPromise = animation.finished.then(function() {
+ assert_unreached('finished promise was fulfilled');
+ }).catch(function(err) {
+ assert_equals(err.name, 'AbortError',
+ 'finished promise is rejected with AbortError');
+ assert_not_equals(animation.finished, previousFinishedPromise,
+ 'Finished promise should change after the original is ' +
+ 'rejected');
+ });
+
+ animation.cancel();
+
+ return retPromise;
+}, 'finished promise is rejected when an animation is cancelled by calling ' +
+ 'cancel()');
+
+promise_test(function(t) {
+ var div = createDiv(t);
+ var animation = div.animate({}, 100 * MS_PER_SEC);
+ var previousFinishedPromise = animation.finished;
+ animation.finish();
+ return animation.finished.then(function() {
+ animation.cancel();
+ assert_not_equals(animation.finished, previousFinishedPromise,
+ 'A new finished promise should be created when'
+ + ' cancelling a finished animation');
+ });
+}, 'cancelling an already-finished animation replaces the finished promise');
+
+promise_test(function(t) {
+ var div = createDiv(t);
+ var animation = div.animate({}, 100 * MS_PER_SEC);
+ animation.cancel();
+ // The spec says we still create a new finished promise and reject the old
+ // one even if we're already idle. That behavior might change, but for now
+ // test that we do that.
+ var retPromise = animation.finished.catch(function(err) {
+ assert_equals(err.name, 'AbortError',
+ 'finished promise is rejected with AbortError');
+ });
+
+ // Redundant call to cancel();
+ var previousFinishedPromise = animation.finished;
+ animation.cancel();
+ assert_not_equals(animation.finished, previousFinishedPromise,
+ 'A redundant call to cancel() should still generate a new'
+ + ' finished promise');
+ return retPromise;
+}, 'cancelling an idle animation still replaces the finished promise');
+
+promise_test(function(t) {
+ var div = createDiv(t);
+ var animation = div.animate({}, 100 * MS_PER_SEC);
+ const HALF_DUR = 100 * MS_PER_SEC / 2;
+ const QUARTER_DUR = 100 * MS_PER_SEC / 4;
+ var gotNextFrame = false;
+ var currentTimeBeforeShortening;
+ animation.currentTime = HALF_DUR;
+ return animation.ready.then(function() {
+ currentTimeBeforeShortening = animation.currentTime;
+ animation.effect.timing.duration = QUARTER_DUR;
+ // Below we use gotNextFrame to check that shortening of the animation
+ // duration causes the finished promise to resolve, rather than it just
+ // getting resolved on the next animation frame. This relies on the fact
+ // that the promises are resolved as a micro-task before the next frame
+ // happens.
+ waitForAnimationFrames(1).then(function() {
+ gotNextFrame = true;
+ });
+
+ return animation.finished;
+ }).then(function() {
+ assert_false(gotNextFrame, 'shortening of the animation duration should ' +
+ 'resolve the finished promise');
+ assert_equals(animation.currentTime, currentTimeBeforeShortening,
+ 'currentTime should be unchanged when duration shortened');
+ var previousFinishedPromise = animation.finished;
+ animation.effect.timing.duration = 100 * MS_PER_SEC;
+ assert_not_equals(animation.finished, previousFinishedPromise,
+ 'Finished promise should change after lengthening the ' +
+ 'duration causes the animation to become active');
+ });
+}, 'Test finished promise changes for animation duration changes');
+
+promise_test(function(t) {
+ var div = createDiv(t);
+ var animation = div.animate({}, 100 * MS_PER_SEC);
+ var retPromise = animation.ready.then(function() {
+ animation.playbackRate = 0;
+ animation.currentTime = 100 * MS_PER_SEC + 1000;
+ return waitForAnimationFrames(2);
+ });
+
+ animation.finished.then(t.step_func(function() {
+ assert_unreached('finished promise should not resolve when playbackRate ' +
+ 'is zero');
+ }));
+
+ return retPromise;
+}, 'Test finished promise changes when playbackRate == 0');
+
+promise_test(function(t) {
+ var div = createDiv(t);
+ var animation = div.animate({}, 100 * MS_PER_SEC);
+ return animation.ready.then(function() {
+ animation.playbackRate = -1;
+ return animation.finished;
+ });
+}, 'Test finished promise resolves when reaching to the natural boundary.');
+
+promise_test(function(t) {
+ var div = createDiv(t);
+ var animation = div.animate({}, 100 * MS_PER_SEC);
+ var previousFinishedPromise = animation.finished;
+ animation.finish();
+ return animation.finished.then(function() {
+ animation.currentTime = 0;
+ assert_not_equals(animation.finished, previousFinishedPromise,
+ 'Finished promise should change once a prior ' +
+ 'finished promise resolved and the animation ' +
+ 'falls out finished state');
+ });
+}, 'Test finished promise changes when a prior finished promise resolved ' +
+ 'and the animation falls out finished state');
+
+test(function(t) {
+ var div = createDiv(t);
+ var animation = div.animate({}, 100 * MS_PER_SEC);
+ var previousFinishedPromise = animation.finished;
+ animation.currentTime = 100 * MS_PER_SEC;
+ animation.currentTime = 100 * MS_PER_SEC / 2;
+ assert_equals(animation.finished, previousFinishedPromise,
+ 'No new finished promise generated when finished state ' +
+ 'is checked asynchronously');
+}, 'Test no new finished promise generated when finished state ' +
+ 'is checked asynchronously');
+
+test(function(t) {
+ var div = createDiv(t);
+ var animation = div.animate({}, 100 * MS_PER_SEC);
+ var previousFinishedPromise = animation.finished;
+ animation.finish();
+ animation.currentTime = 100 * MS_PER_SEC / 2;
+ assert_not_equals(animation.finished, previousFinishedPromise,
+ 'New finished promise generated when finished state ' +
+ 'is checked synchronously');
+}, 'Test new finished promise generated when finished state ' +
+ 'is checked synchronously');
+
+promise_test(function(t) {
+ var div = createDiv(t);
+ var animation = div.animate({}, 100 * MS_PER_SEC);
+ var resolvedFinished = false;
+ animation.finished.then(function() {
+ resolvedFinished = true;
+ });
+ return animation.ready.then(function() {
+ animation.finish();
+ animation.currentTime = 100 * MS_PER_SEC / 2;
+ }).then(function() {
+ assert_true(resolvedFinished,
+ 'Animation.finished should be resolved even if ' +
+ 'the finished state is changed soon');
+ });
+
+}, 'Test synchronous finished promise resolved even if finished state ' +
+ 'is changed soon');
+
+promise_test(function(t) {
+ var div = createDiv(t);
+ var animation = div.animate({}, 100 * MS_PER_SEC);
+ var resolvedFinished = false;
+ animation.finished.then(function() {
+ resolvedFinished = true;
+ });
+
+ return animation.ready.then(function() {
+ animation.currentTime = 100 * MS_PER_SEC;
+ animation.finish();
+ }).then(function() {
+ assert_true(resolvedFinished,
+ 'Animation.finished should be resolved soon after finish() is ' +
+ 'called even if there are other asynchronous promises just before it');
+ });
+}, 'Test synchronous finished promise resolved even if asynchronous ' +
+ 'finished promise happens just before synchronous promise');
+
+promise_test(function(t) {
+ var div = createDiv(t);
+ var animation = div.animate({}, 100 * MS_PER_SEC);
+ animation.finished.then(t.step_func(function() {
+ assert_unreached('Animation.finished should not be resolved');
+ }));
+
+ return animation.ready.then(function() {
+ animation.currentTime = 100 * MS_PER_SEC;
+ animation.currentTime = 100 * MS_PER_SEC / 2;
+ });
+}, 'Test finished promise is not resolved when the animation ' +
+ 'falls out finished state immediately');
+
+promise_test(function(t) {
+ var div = createDiv(t);
+ var animation = div.animate({}, 100 * MS_PER_SEC);
+ return animation.ready.then(function() {
+ animation.currentTime = 100 * MS_PER_SEC;
+ animation.finished.then(t.step_func(function() {
+ assert_unreached('Animation.finished should not be resolved');
+ }));
+ animation.currentTime = 0;
+ });
+
+}, 'Test finished promise is not resolved once the animation ' +
+ 'falls out finished state even though the current finished ' +
+ 'promise is generated soon after animation state became finished');
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/web-animations/interfaces/Animation/id.html b/testing/web-platform/tests/web-animations/interfaces/Animation/id.html
new file mode 100644
index 000000000..2fadd5623
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/interfaces/Animation/id.html
@@ -0,0 +1,28 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title>Animation.id</title>
+<link rel="help" href="https://w3c.github.io/web-animations/#dom-animation-id">
+<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(function(t) {
+ var div = createDiv(t);
+ var animation = div.animate({}, 100 * MS_PER_SEC);
+ assert_equals(animation.id, '', 'id for Animation is initially empty');
+}, 'Animation.id initial value');
+
+test(function(t) {
+ var div = createDiv(t);
+ var animation = div.animate({}, 100 * MS_PER_SEC);
+ animation.id = 'anim';
+
+ assert_equals(animation.id, 'anim', 'animation.id reflects the value set');
+}, 'Animation.id setter');
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/web-animations/interfaces/Animation/oncancel.html b/testing/web-platform/tests/web-animations/interfaces/Animation/oncancel.html
new file mode 100644
index 000000000..b8d9ced61
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/interfaces/Animation/oncancel.html
@@ -0,0 +1,33 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title>Animation.oncancel</title>
+<link rel="help" href="https://w3c.github.io/web-animations/#dom-animation-oncancel">
+<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";
+
+async_test(function(t) {
+ var div = createDiv(t);
+ var animation = div.animate({}, 100 * MS_PER_SEC);
+ var finishedTimelineTime;
+ animation.finished.then().catch(function() {
+ finishedTimelineTime = animation.timeline.currentTime;
+ });
+
+ animation.oncancel = t.step_func_done(function(event) {
+ assert_equals(event.currentTime, null,
+ 'event.currentTime should be null');
+ assert_equals(event.timelineTime, finishedTimelineTime,
+ 'event.timelineTime should equal to the animation timeline ' +
+ 'when finished promise is rejected');
+ });
+
+ animation.cancel();
+}, 'oncancel event is fired when animation.cancel() is called.');
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/web-animations/interfaces/Animation/onfinish.html b/testing/web-platform/tests/web-animations/interfaces/Animation/onfinish.html
new file mode 100644
index 000000000..50e5bed6b
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/interfaces/Animation/onfinish.html
@@ -0,0 +1,121 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title>Animation.onfinish</title>
+<link rel="help" href="https://w3c.github.io/web-animations/#dom-animation-onfinish">
+<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";
+
+async_test(function(t) {
+ var div = createDiv(t);
+ var animation = div.animate({}, 100 * MS_PER_SEC);
+ var finishedTimelineTime;
+ animation.finished.then(function() {
+ finishedTimelineTime = animation.timeline.currentTime;
+ });
+
+ animation.onfinish = t.step_func_done(function(event) {
+ assert_equals(event.currentTime, 0,
+ 'event.currentTime should be zero');
+ assert_equals(event.timelineTime, finishedTimelineTime,
+ 'event.timelineTime should equal to the animation timeline ' +
+ 'when finished promise is resolved');
+ });
+
+ animation.playbackRate = -1;
+}, 'onfinish event is fired when the currentTime < 0 and ' +
+ 'the playbackRate < 0');
+
+async_test(function(t) {
+ var div = createDiv(t);
+ var animation = div.animate({}, 100 * MS_PER_SEC);
+
+ var finishedTimelineTime;
+ animation.finished.then(function() {
+ finishedTimelineTime = animation.timeline.currentTime;
+ });
+
+ animation.onfinish = t.step_func_done(function(event) {
+ assert_equals(event.currentTime, 100 * MS_PER_SEC,
+ 'event.currentTime should be the effect end');
+ assert_equals(event.timelineTime, finishedTimelineTime,
+ 'event.timelineTime should equal to the animation timeline ' +
+ 'when finished promise is resolved');
+ });
+
+ animation.currentTime = 100 * MS_PER_SEC;
+}, 'onfinish event is fired when the currentTime > 0 and ' +
+ 'the playbackRate > 0');
+
+async_test(function(t) {
+ var div = createDiv(t);
+ var animation = div.animate({}, 100 * MS_PER_SEC);
+
+ var finishedTimelineTime;
+ animation.finished.then(function() {
+ finishedTimelineTime = animation.timeline.currentTime;
+ });
+
+ animation.onfinish = t.step_func_done(function(event) {
+ assert_equals(event.currentTime, 100 * MS_PER_SEC,
+ 'event.currentTime should be the effect end');
+ assert_equals(event.timelineTime, finishedTimelineTime,
+ 'event.timelineTime should equal to the animation timeline ' +
+ 'when finished promise is resolved');
+ });
+
+ animation.finish();
+}, 'onfinish event is fired when animation.finish() is called');
+
+promise_test(function(t) {
+ var div = createDiv(t);
+ var animation = div.animate({}, 100 * MS_PER_SEC);
+
+ animation.onfinish = function(event) {
+ assert_unreached('onfinish event should not be fired');
+ };
+
+ animation.currentTime = 100 * MS_PER_SEC / 2;
+ animation.pause();
+
+ return animation.ready.then(function() {
+ animation.currentTime = 100 * MS_PER_SEC;
+ return waitForAnimationFrames(2);
+ });
+}, 'onfinish event is not fired when paused');
+
+promise_test(function(t) {
+ var div = createDiv(t);
+ var animation = div.animate({}, 100 * MS_PER_SEC);
+ animation.onfinish = function(event) {
+ assert_unreached('onfinish event should not be fired');
+ };
+
+ return animation.ready.then(function() {
+ animation.playbackRate = 0;
+ animation.currentTime = 100 * MS_PER_SEC;
+ return waitForAnimationFrames(2);
+ });
+}, 'onfinish event is not fired when the playbackRate is zero');
+
+promise_test(function(t) {
+ var div = createDiv(t);
+ var animation = div.animate({}, 100 * MS_PER_SEC);
+ animation.onfinish = function(event) {
+ assert_unreached('onfinish event should not be fired');
+ };
+
+ return animation.ready.then(function() {
+ animation.currentTime = 100 * MS_PER_SEC;
+ animation.currentTime = 100 * MS_PER_SEC / 2;
+ return waitForAnimationFrames(2);
+ });
+}, 'onfinish event is not fired when the animation falls out ' +
+ 'finished state immediately');
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/web-animations/interfaces/Animation/pause.html b/testing/web-platform/tests/web-animations/interfaces/Animation/pause.html
new file mode 100644
index 000000000..bcfaf4a9b
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/interfaces/Animation/pause.html
@@ -0,0 +1,98 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title>Animation.pause()</title>
+<link rel="help" href="https://w3c.github.io/web-animations/#dom-animation-pause">
+<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(function(t) {
+ var div = createDiv(t);
+ var animation = div.animate({}, 1000 * MS_PER_SEC);
+ var previousCurrentTime = animation.currentTime;
+
+ return animation.ready.then(waitForAnimationFrames(1)).then(function() {
+ assert_true(animation.currentTime >= previousCurrentTime,
+ 'currentTime is initially increasing');
+ animation.pause();
+ return animation.ready;
+ }).then(function() {
+ previousCurrentTime = animation.currentTime;
+ return waitForAnimationFrames(1);
+ }).then(function() {
+ assert_equals(animation.currentTime, previousCurrentTime,
+ 'currentTime does not increase after calling pause()');
+ });
+}, 'pause() a running animation');
+
+promise_test(function(t) {
+ var div = createDiv(t);
+ var animation = div.animate({}, 1000 * MS_PER_SEC);
+
+ // Go to idle state then pause
+ animation.cancel();
+ animation.pause();
+
+ assert_equals(animation.currentTime, 0, 'currentTime is set to 0');
+ assert_equals(animation.startTime, null, 'startTime is not set');
+ assert_equals(animation.playState, 'pending', 'initially pause-pending');
+
+ // Check it still resolves as expected
+ return animation.ready.then(function() {
+ assert_equals(animation.playState, 'paused',
+ 'resolves to paused state asynchronously');
+ assert_equals(animation.currentTime, 0,
+ 'keeps the initially set currentTime');
+ });
+}, 'pause() from idle');
+
+promise_test(function(t) {
+ var div = createDiv(t);
+ var animation = div.animate({}, 1000 * MS_PER_SEC);
+ animation.cancel();
+ animation.playbackRate = -1;
+ animation.pause();
+
+ assert_equals(animation.currentTime, 1000 * MS_PER_SEC,
+ 'currentTime is set to the effect end');
+
+ return animation.ready.then(function() {
+ assert_equals(animation.currentTime, 1000 * MS_PER_SEC,
+ 'keeps the initially set currentTime');
+ });
+}, 'pause() from idle with a negative playbackRate');
+
+test(function(t) {
+ var div = createDiv(t);
+ var animation = div.animate({}, {duration: 1000 * MS_PER_SEC,
+ iterations: Infinity});
+ animation.cancel();
+ animation.playbackRate = -1;
+
+ assert_throws('InvalidStateError',
+ function () { animation.pause(); },
+ 'Expect InvalidStateError exception on calling pause() ' +
+ 'from idle with a negative playbackRate and ' +
+ 'infinite-duration animation');
+}, 'pause() from idle with a negative playbackRate and endless effect');
+
+promise_test(function(t) {
+ var div = createDiv(t);
+ var animation = div.animate({}, 1000 * MS_PER_SEC);
+ return animation.ready
+ .then(function(animation) {
+ animation.finish();
+ animation.pause();
+ return animation.ready;
+ }).then(function(animation) {
+ assert_equals(animation.currentTime, 1000 * MS_PER_SEC,
+ 'currentTime after pausing finished animation');
+ });
+}, 'pause() on a finished animation');
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/web-animations/interfaces/Animation/play.html b/testing/web-platform/tests/web-animations/interfaces/Animation/play.html
new file mode 100644
index 000000000..767d8df17
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/interfaces/Animation/play.html
@@ -0,0 +1,34 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title>Animation.play()</title>
+<link rel="help" href="https://w3c.github.io/web-animations/#dom-animation-play">
+<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(function(t) {
+ var div = createDiv(t);
+ var animation = div.animate({ transform: ['none', 'translate(10px)']},
+ { duration : 100 * MS_PER_SEC,
+ iterations : Infinity});
+ return animation.ready.then(function() {
+ // Seek to a time outside the active range so that play() will have to
+ // snap back to the start
+ animation.currentTime = -5 * MS_PER_SEC;
+ animation.playbackRate = -1;
+
+ assert_throws('InvalidStateError',
+ function () { animation.play(); },
+ 'Expected InvalidStateError exception on calling play() ' +
+ 'with a negative playbackRate and infinite-duration ' +
+ 'animation');
+ });
+}, 'play() throws when seeking an infinite-duration animation played in ' +
+ 'reverse');
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/web-animations/interfaces/Animation/playState.html b/testing/web-platform/tests/web-animations/interfaces/Animation/playState.html
new file mode 100644
index 000000000..15af526cd
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/interfaces/Animation/playState.html
@@ -0,0 +1,53 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title>Animation.playState</title>
+<link rel="help" href="https://w3c.github.io/web-animations/#dom-animation-playstate">
+<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(function(t) {
+ var div = createDiv(t);
+ var animation = div.animate({}, 100 * MS_PER_SEC);
+
+ assert_equals(animation.playState, 'pending');
+ return animation.ready.then(function() {
+ assert_equals(animation.playState, 'running');
+ });
+}, 'Animation.playState reports \'pending\'->\'running\' when initially ' +
+ 'played');
+
+promise_test(function(t) {
+ var div = createDiv(t);
+ var animation = div.animate({}, 100 * MS_PER_SEC);
+ animation.pause();
+
+ assert_equals(animation.playState, 'pending');
+ return animation.ready.then(function() {
+ assert_equals(animation.playState, 'paused');
+ });
+}, 'Animation.playState reports \'pending\'->\'paused\' when pausing');
+
+test(function(t) {
+ var div = createDiv(t);
+ var animation = div.animate({}, 100 * MS_PER_SEC);
+ animation.cancel();
+ assert_equals(animation.playState, 'idle');
+}, 'Animation.playState is \'idle\' when canceled.');
+
+test(function(t) {
+ var div = createDiv(t);
+ var animation = div.animate({}, 100 * MS_PER_SEC);
+ animation.cancel();
+ animation.currentTime = 50 * MS_PER_SEC;
+ assert_equals(animation.playState, 'paused',
+ 'After seeking an idle animation, it is effectively paused');
+}, 'Animation.playState is \'paused\' after cancelling an animation, ' +
+ 'seeking it makes it paused');
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/web-animations/interfaces/Animation/playbackRate.html b/testing/web-platform/tests/web-animations/interfaces/Animation/playbackRate.html
new file mode 100644
index 000000000..c923df6b4
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/interfaces/Animation/playbackRate.html
@@ -0,0 +1,86 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title>Animation.playbackRate</title>
+<link rel="help" href="https://w3c.github.io/web-animations/#dom-animation-playbackrate">
+<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";
+
+function assert_playbackrate(animation,
+ previousAnimationCurrentTime,
+ previousTimelineCurrentTime,
+ description) {
+ var accuracy = 0.001; /* accuracy of DOMHighResTimeStamp */
+ var animationCurrentTimeDifference =
+ animation.currentTime - previousAnimationCurrentTime;
+ var timelineCurrentTimeDifference =
+ animation.timeline.currentTime - previousTimelineCurrentTime;
+
+ assert_approx_equals(animationCurrentTimeDifference,
+ timelineCurrentTimeDifference * animation.playbackRate,
+ accuracy,
+ description);
+}
+
+promise_test(function(t) {
+ var div = createDiv(t);
+ var animation = div.animate(null, 100 * MS_PER_SEC);
+ return animation.ready.then(function() {
+ animation.currentTime = 7 * MS_PER_SEC; // ms
+ animation.playbackRate = 0.5;
+
+ assert_equals(animation.currentTime, 7 * MS_PER_SEC,
+ 'Reducing Animation.playbackRate should not change the currentTime ' +
+ 'of a playing animation');
+ animation.playbackRate = 2;
+ assert_equals(animation.currentTime, 7 * MS_PER_SEC,
+ 'Increasing Animation.playbackRate should not change the currentTime ' +
+ 'of a playing animation');
+ });
+}, 'Test the initial effect of setting playbackRate on currentTime');
+
+promise_test(function(t) {
+ var div = createDiv(t);
+ var animation = div.animate(null, 100 * MS_PER_SEC);
+ animation.playbackRate = 2;
+ var previousTimelineCurrentTime;
+ var previousAnimationCurrentTime;
+ return animation.ready.then(function() {
+ previousAnimationCurrentTime = animation.currentTime;
+ previousTimelineCurrentTime = animation.timeline.currentTime;
+ return waitForAnimationFrames(1);
+ }).then(function() {
+ assert_playbackrate(animation,
+ previousAnimationCurrentTime,
+ previousTimelineCurrentTime,
+ 'animation.currentTime should be 2 times faster than timeline.');
+ });
+}, 'Test the effect of setting playbackRate on currentTime');
+
+promise_test(function(t) {
+ var div = createDiv(t);
+ var animation = div.animate(null, 100 * MS_PER_SEC);
+ animation.playbackRate = 2;
+ var previousTimelineCurrentTime;
+ var previousAnimationCurrentTime;
+ return animation.ready.then(function() {
+ previousAnimationCurrentTime = animation.currentTime;
+ previousTimelineCurrentTime = animation.timeline.currentTime;
+ animation.playbackRate = 1;
+ return waitForAnimationFrames(1);
+ }).then(function() {
+ assert_equals(animation.playbackRate, 1,
+ 'sanity check: animation.playbackRate is still 1.');
+ assert_playbackrate(animation,
+ previousAnimationCurrentTime,
+ previousTimelineCurrentTime,
+ 'animation.currentTime should be the same speed as timeline now.');
+ });
+}, 'Test the effect of setting playbackRate while playing animation');
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/web-animations/interfaces/Animation/ready.html b/testing/web-platform/tests/web-animations/interfaces/Animation/ready.html
new file mode 100644
index 000000000..815fe3da7
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/interfaces/Animation/ready.html
@@ -0,0 +1,96 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title>Animation.ready</title>
+<link rel="help" href="https://w3c.github.io/web-animations/#dom-animation-ready">
+<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(function(t) {
+ var div = createDiv(t);
+ var animation = div.animate({}, 100 * MS_PER_SEC);
+ var originalReadyPromise = animation.ready;
+ var pauseReadyPromise;
+
+ return animation.ready.then(function() {
+ assert_equals(animation.ready, originalReadyPromise,
+ 'Ready promise is the same object when playing completes');
+ animation.pause();
+ assert_not_equals(animation.ready, originalReadyPromise,
+ 'A new ready promise is created when pausing');
+ pauseReadyPromise = animation.ready;
+ // Wait for the promise to fulfill since if we abort the pause the ready
+ // promise object is reused.
+ return animation.ready;
+ }).then(function() {
+ animation.play();
+ assert_not_equals(animation.ready, pauseReadyPromise,
+ 'A new ready promise is created when playing');
+ });
+}, 'A new ready promise is created when play()/pause() is called');
+
+promise_test(function(t) {
+ var div = createDiv(t);
+ var animation = div.animate({}, 100 * MS_PER_SEC);
+
+ return animation.ready.then(function() {
+ var promiseBeforeCallingPlay = animation.ready;
+ animation.play();
+ assert_equals(animation.ready, promiseBeforeCallingPlay,
+ 'Ready promise has same object identity after redundant call'
+ + ' to play()');
+ });
+}, 'Redundant calls to play() do not generate new ready promise objects');
+
+promise_test(function(t) {
+ var div = createDiv(t);
+ var animation = div.animate({}, 100 * MS_PER_SEC);
+
+ return animation.ready.then(function(resolvedAnimation) {
+ assert_equals(resolvedAnimation, animation,
+ 'Object identity of Animation passed to Promise callback'
+ + ' matches the Animation object owning the Promise');
+ });
+}, 'The ready promise is fulfilled with its Animation');
+
+promise_test(function(t) {
+ var div = createDiv(t);
+ var animation = div.animate({}, 100 * MS_PER_SEC);
+
+ var retPromise = animation.ready.then(function() {
+ assert_unreached('ready promise was fulfilled');
+ }).catch(function(err) {
+ assert_equals(err.name, 'AbortError',
+ 'ready promise is rejected with AbortError');
+ });
+
+ animation.cancel();
+
+ return retPromise;
+}, 'ready promise is rejected when a pause-pending animation is cancelled by'
+ + ' calling cancel()');
+
+promise_test(function(t) {
+ var div = createDiv(t);
+ var animation = div.animate({}, 100 * MS_PER_SEC);
+ return animation.ready.then(function() {
+ animation.pause();
+ // Set up listeners on pause-pending ready promise
+ var retPromise = animation.ready.then(function() {
+ assert_unreached('ready promise was fulfilled');
+ }).catch(function(err) {
+ assert_equals(err.name, 'AbortError',
+ 'ready promise is rejected with AbortError');
+ });
+ animation.cancel();
+ return retPromise;
+ });
+}, 'ready promise is rejected when a pause-pending animation is cancelled by'
+ + ' calling cancel()');
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/web-animations/interfaces/Animation/reverse.html b/testing/web-platform/tests/web-animations/interfaces/Animation/reverse.html
new file mode 100644
index 000000000..555226111
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/interfaces/Animation/reverse.html
@@ -0,0 +1,158 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title>Animation.reverse()</title>
+<link rel="help" href="https://w3c.github.io/web-animations/#dom-animation-reverse">
+<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(function(t) {
+ var div = createDiv(t);
+ var animation = div.animate({}, {duration: 100 * MS_PER_SEC,
+ iterations: Infinity});
+
+ // Wait a frame because if currentTime is still 0 when we call
+ // reverse(), it will throw (per spec).
+ return animation.ready.then(waitForAnimationFrames(1)).then(function() {
+ assert_greater_than_equal(animation.currentTime, 0,
+ 'currentTime expected to be greater than 0, one frame after starting');
+ animation.currentTime = 50 * MS_PER_SEC;
+ var previousPlaybackRate = animation.playbackRate;
+ animation.reverse();
+ assert_equals(animation.playbackRate, -previousPlaybackRate,
+ 'playbackRate should be inverted');
+ });
+}, 'reverse() inverts playbackRate');
+
+promise_test(function(t) {
+ var div = createDiv(t);
+ var animation = div.animate({}, {duration: 100 * MS_PER_SEC,
+ iterations: Infinity});
+ animation.currentTime = 50 * MS_PER_SEC;
+ animation.pause();
+
+ return animation.ready.then(function() {
+ animation.reverse();
+ return animation.ready;
+ }).then(function() {
+ assert_equals(animation.playState, 'running',
+ 'Animation.playState should be "running" after reverse()');
+ });
+}, 'reverse() starts to play when pausing animation');
+
+test(function(t) {
+ var div = createDiv(t);
+ var animation = div.animate({}, 100 * MS_PER_SEC);
+ animation.currentTime = 50 * MS_PER_SEC;
+ animation.reverse();
+
+ assert_equals(animation.currentTime, 50 * MS_PER_SEC,
+ 'reverse() should not change the currentTime ' +
+ 'if the currentTime is in the middle of animation duration');
+}, 'reverse() maintains the same currentTime');
+
+test(function(t) {
+ var div = createDiv(t);
+ var 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');
+}, 'reverse() when playbackRate > 0 and currentTime > effect end');
+
+test(function(t) {
+ var div = createDiv(t);
+ var 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');
+}, 'reverse() when playbackRate > 0 and currentTime < 0');
+
+test(function(t) {
+ var div = createDiv(t);
+ var 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');
+}, 'reverse() when playbackRate < 0 and currentTime < 0');
+
+test(function(t) {
+ var div = createDiv(t);
+ var 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');
+}, 'reverse() when playbackRate < 0 and currentTime > effect end');
+
+test(function(t) {
+ var div = createDiv(t);
+ var animation = div.animate({}, {duration: 100 * MS_PER_SEC,
+ iterations: Infinity});
+ animation.currentTime = -200 * MS_PER_SEC;
+
+ assert_throws('InvalidStateError',
+ function () { animation.reverse(); },
+ 'reverse() should throw InvalidStateError ' +
+ 'if the playbackRate > 0 and the currentTime < 0 ' +
+ 'and the target effect is positive infinity');
+}, 'reverse() when playbackRate > 0 and currentTime < 0 ' +
+ 'and the target effect end is positive infinity');
+
+test(function(t) {
+ var div = createDiv(t);
+ var 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');
+}, 'reverse() when playbackRate < 0 and currentTime < 0 ' +
+ 'and the target effect end is positive infinity');
+
+test(function(t) {
+ var div = createDiv(t);
+ var animation = div.animate({}, 100 * MS_PER_SEC);
+ animation.playbackRate = 0;
+ animation.currentTime = 50 * MS_PER_SEC;
+ animation.reverse();
+
+ 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');
+ t.done();
+}, 'reverse() when playbackRate == 0');
+
+test(function(t) {
+ var div = createDiv(t);
+ var animation =
+ new Animation(new KeyframeEffect(div, null, 100 * MS_PER_SEC), null);
+
+ assert_throws('InvalidStateError', function() { animation.reverse(); });
+}, 'Reversing an animation without an active timeline throws an ' +
+ 'InvalidStateError');
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/web-animations/interfaces/Animation/startTime.html b/testing/web-platform/tests/web-animations/interfaces/Animation/startTime.html
new file mode 100644
index 000000000..6dfd7cf29
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/interfaces/Animation/startTime.html
@@ -0,0 +1,55 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title>Animation.startTime tests</title>
+<link rel="help"
+href="https://w3c.github.io/web-animations/#dom-animation-starttime">
+<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(function(t) {
+ var animation = new Animation(new KeyframeEffect(createDiv(t), null),
+ document.timeline);
+ assert_equals(animation.startTime, null, 'startTime is unresolved');
+}, 'startTime of a newly created (idle) animation is unresolved');
+
+test(function(t) {
+ var animation = new Animation(new KeyframeEffect(createDiv(t), null),
+ document.timeline);
+ animation.play();
+ assert_equals(animation.startTime, null, 'startTime is unresolved');
+}, 'startTime of a play-pending animation is unresolved');
+
+test(function(t) {
+ var animation = new Animation(new KeyframeEffect(createDiv(t), null),
+ document.timeline);
+ animation.pause();
+ assert_equals(animation.startTime, null, 'startTime is unresolved');
+}, 'startTime of a pause-pending animation is unresolved');
+
+test(function(t) {
+ var animation = createDiv(t).animate(null);
+ assert_equals(animation.startTime, null, 'startTime is unresolved');
+}, 'startTime of a play-pending animation created using Element.animate'
+ + ' shortcut is unresolved');
+
+promise_test(function(t) {
+ var animation = createDiv(t).animate(null, 100 * MS_PER_SEC);
+ return animation.ready.then(function() {
+ assert_greater_than(animation.startTime, 0, 'startTime when running');
+ });
+}, 'startTime is resolved when running');
+
+test(function(t) {
+ var animation = createDiv(t).animate(null, 100 * MS_PER_SEC);
+ animation.cancel();
+ assert_equals(animation.startTime, null);
+ assert_equals(animation.currentTime, null);
+}, 'startTime and currentTime are unresolved when animation is cancelled');
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/web-animations/interfaces/AnimationEffectTiming/delay.html b/testing/web-platform/tests/web-animations/interfaces/AnimationEffectTiming/delay.html
new file mode 100644
index 000000000..d6e1cd904
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/interfaces/AnimationEffectTiming/delay.html
@@ -0,0 +1,61 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title>delay tests</title>
+<link rel="help" href="https://w3c.github.io/web-animations/#dom-animationeffecttiming-delay">
+<link rel="author" title="Daisuke Akatsuka" href="mailto:daisuke@mozilla-japan.org">
+<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(function(t) {
+ var div = createDiv(t);
+ var anim = div.animate({ opacity: [ 0, 1 ] }, 100);
+ anim.effect.timing.delay = 100;
+ assert_equals(anim.effect.timing.delay, 100, 'set delay 100');
+ assert_equals(anim.effect.getComputedTiming().delay, 100,
+ 'getComputedTiming() after set delay 100');
+}, 'set delay 100');
+
+test(function(t) {
+ var div = createDiv(t);
+ var anim = div.animate({ opacity: [ 0, 1 ] }, 100);
+ anim.effect.timing.delay = -100;
+ assert_equals(anim.effect.timing.delay, -100, 'set delay -100');
+ assert_equals(anim.effect.getComputedTiming().delay, -100,
+ 'getComputedTiming() after set delay -100');
+}, 'set delay -100');
+
+test(function(t) {
+ var div = createDiv(t);
+ var anim = div.animate({ opacity: [ 0, 1 ] }, 100);
+ anim.effect.timing.delay = 100;
+ assert_equals(anim.effect.getComputedTiming().progress, null);
+ assert_equals(anim.effect.getComputedTiming().currentIteration, null);
+}, 'Test adding a positive delay to an animation without a backwards fill ' +
+ 'makes it no longer active');
+
+test(function(t) {
+ var div = createDiv(t);
+ var anim = div.animate({ opacity: [ 0, 1 ] },
+ { fill: 'both',
+ duration: 100 });
+ anim.effect.timing.delay = -50;
+ assert_equals(anim.effect.getComputedTiming().progress, 0.5);
+}, 'Test seeking an animation by setting a negative delay');
+
+test(function(t) {
+ var div = createDiv(t);
+ var anim = div.animate({ opacity: [ 0, 1 ] },
+ { fill: 'both',
+ duration: 100 });
+ anim.effect.timing.delay = -100;
+ assert_equals(anim.effect.getComputedTiming().progress, 1);
+ assert_equals(anim.effect.getComputedTiming().currentIteration, 0);
+}, 'Test finishing an animation using a large negative delay');
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/web-animations/interfaces/AnimationEffectTiming/direction.html b/testing/web-platform/tests/web-animations/interfaces/AnimationEffectTiming/direction.html
new file mode 100644
index 000000000..c3657f3a0
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/interfaces/AnimationEffectTiming/direction.html
@@ -0,0 +1,27 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title>direction tests</title>
+<link rel="help" href="https://w3c.github.io/web-animations/#dom-animationeffecttiming-direction">
+<link rel="author" title="Ryo Kato" href="mailto:foobar094@gmail.com">
+<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(function(t) {
+ var div = createDiv(t);
+ var anim = div.animate({ opacity: [ 0, 1 ] }, 2000);
+
+ var directions = ['normal', 'reverse', 'alternate', 'alternate-reverse'];
+ directions.forEach(function(direction) {
+ anim.effect.timing.direction = direction;
+ assert_equals(anim.effect.timing.direction, direction,
+ 'set direction to ' + direction);
+ });
+}, 'set direction to a valid keyword');
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/web-animations/interfaces/AnimationEffectTiming/duration.html b/testing/web-platform/tests/web-animations/interfaces/AnimationEffectTiming/duration.html
new file mode 100644
index 000000000..8e2a1a1b9
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/interfaces/AnimationEffectTiming/duration.html
@@ -0,0 +1,148 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title>duration tests</title>
+<link rel="help" href="http://w3c.github.io/web-animations/#dom-animationeffecttiming-duration">
+<link rel="author" title="Ryo Motozawa" href="mailto:motozawa@mozilla-japan.org">
+<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(function(t) {
+ var div = createDiv(t);
+ var anim = div.animate({ opacity: [ 0, 1 ] }, 2000);
+ anim.effect.timing.duration = 123.45;
+ assert_times_equal(anim.effect.timing.duration, 123.45,
+ 'set duration 123.45');
+ assert_times_equal(anim.effect.getComputedTiming().duration, 123.45,
+ 'getComputedTiming() after set duration 123.45');
+}, 'set duration 123.45');
+
+test(function(t) {
+ var div = createDiv(t);
+ var anim = div.animate({ opacity: [ 0, 1 ] }, 2000);
+ anim.effect.timing.duration = 'auto';
+ assert_equals(anim.effect.timing.duration, 'auto', 'set duration \'auto\'');
+ assert_equals(anim.effect.getComputedTiming().duration, 0,
+ 'getComputedTiming() after set duration \'auto\'');
+}, 'set duration auto');
+
+test(function(t) {
+ var div = createDiv(t);
+ var anim = div.animate({ opacity: [ 0, 1 ] }, { duration: 'auto' });
+ assert_equals(anim.effect.timing.duration, 'auto', 'set duration \'auto\'');
+ assert_equals(anim.effect.getComputedTiming().duration, 0,
+ 'getComputedTiming() after set duration \'auto\'');
+}, 'set auto duration in animate as object');
+
+test(function(t) {
+ var div = createDiv(t);
+ var anim = div.animate({ opacity: [ 0, 1 ] }, 2000);
+ anim.effect.timing.duration = Infinity;
+ assert_equals(anim.effect.timing.duration, Infinity, 'set duration Infinity');
+ assert_equals(anim.effect.getComputedTiming().duration, Infinity,
+ 'getComputedTiming() after set duration Infinity');
+}, 'set duration Infinity');
+
+test(function(t) {
+ var div = createDiv(t);
+ assert_throws({ name: 'TypeError' }, function() {
+ div.animate({ opacity: [ 0, 1 ] }, -1);
+ });
+}, 'set negative duration in animate using a duration parameter');
+
+test(function(t) {
+ var div = createDiv(t);
+ assert_throws({ name: 'TypeError' }, function() {
+ div.animate({ opacity: [ 0, 1 ] }, -Infinity);
+ });
+}, 'set negative Infinity duration in animate using a duration parameter');
+
+test(function(t) {
+ var div = createDiv(t);
+ assert_throws({ name: 'TypeError' }, function() {
+ div.animate({ opacity: [ 0, 1 ] }, NaN);
+ });
+}, 'set NaN duration in animate using a duration parameter');
+
+test(function(t) {
+ var div = createDiv(t);
+ assert_throws({ name: 'TypeError' }, function() {
+ div.animate({ opacity: [ 0, 1 ] }, { duration: -1 });
+ });
+}, 'set negative duration in animate using an options object');
+
+test(function(t) {
+ var div = createDiv(t);
+ assert_throws({ name: 'TypeError' }, function() {
+ div.animate({ opacity: [ 0, 1 ] }, { duration: -Infinity });
+ });
+}, 'set negative Infinity duration in animate using an options object');
+
+test(function(t) {
+ var div = createDiv(t);
+ assert_throws({ name: 'TypeError' }, function() {
+ div.animate({ opacity: [ 0, 1 ] }, { duration: NaN });
+ });
+}, 'set NaN duration in animate using an options object');
+
+test(function(t) {
+ var div = createDiv(t);
+ assert_throws({ name: 'TypeError' }, function() {
+ div.animate({ opacity: [ 0, 1 ] }, { duration: 'abc' });
+ });
+}, 'set abc string duration in animate using an options object');
+
+test(function(t) {
+ var div = createDiv(t);
+ assert_throws({ name: 'TypeError' }, function() {
+ div.animate({ opacity: [ 0, 1 ] }, { duration: '100' });
+ });
+}, 'set 100 string duration in animate using an options object');
+
+test(function(t) {
+ var div = createDiv(t);
+ var anim = div.animate({ opacity: [ 0, 1 ] }, 2000);
+ assert_throws({ name: 'TypeError' }, function() {
+ anim.effect.timing.duration = -1;
+ });
+}, 'set negative duration');
+
+test(function(t) {
+ var div = createDiv(t);
+ var anim = div.animate({ opacity: [ 0, 1 ] }, 2000);
+ assert_throws({ name: 'TypeError' }, function() {
+ anim.effect.timing.duration = -Infinity;
+ });
+}, 'set negative Infinity duration');
+
+test(function(t) {
+ var div = createDiv(t);
+ var anim = div.animate({ opacity: [ 0, 1 ] }, 2000);
+ assert_throws({ name: 'TypeError' }, function() {
+ anim.effect.timing.duration = NaN;
+ });
+}, 'set NaN duration');
+
+test(function(t) {
+ var div = createDiv(t);
+ var anim = div.animate({ opacity: [ 0, 1 ] }, 2000);
+ assert_throws({ name: 'TypeError' }, function() {
+ anim.effect.timing.duration = 'abc';
+ });
+}, 'set duration abc');
+
+test(function(t) {
+ var div = createDiv(t);
+ var anim = div.animate({ opacity: [ 0, 1 ] }, 2000);
+ assert_throws({ name: 'TypeError' }, function() {
+ anim.effect.timing.duration = '100';
+ });
+}, 'set duration string 100');
+
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/web-animations/interfaces/AnimationEffectTiming/easing.html b/testing/web-platform/tests/web-animations/interfaces/AnimationEffectTiming/easing.html
new file mode 100644
index 000000000..cedd7e68b
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/interfaces/AnimationEffectTiming/easing.html
@@ -0,0 +1,83 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title>easing tests</title>
+<link rel="help" href="https://w3c.github.io/web-animations/#dom-animationeffecttiming-easing">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../../testcommon.js"></script>
+<script src="../../resources/effect-easing-tests.js"></script>
+<body>
+<div id="log"></div>
+<script>
+'use strict';
+
+function assert_progress(animation, currentTime, easingFunction) {
+ animation.currentTime = currentTime;
+ var portion = currentTime / animation.effect.timing.duration;
+ assert_approx_equals(animation.effect.getComputedTiming().progress,
+ easingFunction(portion),
+ 0.01,
+ 'The progress of the animation should be approximately ' +
+ easingFunction(portion) + ' at ' + currentTime + 'ms');
+}
+
+gEffectEasingTests.forEach(function(options) {
+ test(function(t) {
+ var target = createDiv(t);
+ var anim = target.animate([ { opacity: 0 }, { opacity: 1 } ],
+ { duration: 1000 * MS_PER_SEC,
+ fill: 'forwards' });
+ anim.effect.timing.easing = options.easing;
+ assert_equals(anim.effect.timing.easing,
+ options.serialization || options.easing);
+
+ var easing = options.easingFunction;
+ assert_progress(anim, 0, easing);
+ assert_progress(anim, 250 * MS_PER_SEC, easing);
+ assert_progress(anim, 500 * MS_PER_SEC, easing);
+ assert_progress(anim, 750 * MS_PER_SEC, easing);
+ assert_progress(anim, 1000 * MS_PER_SEC, easing);
+ }, options.desc);
+});
+
+gInvalidEasingTests.forEach(function(options) {
+ test(function(t) {
+ var div = createDiv(t);
+ var anim = div.animate({ opacity: [ 0, 1 ] }, 100 * MS_PER_SEC);
+ assert_throws({ name: 'TypeError' },
+ function() {
+ anim.effect.timing.easing = options.easing;
+ });
+ }, 'Invalid effect easing value test: \'' + options.easing + '\'');
+});
+
+test(function(t) {
+ var delay = 1000 * MS_PER_SEC;
+
+ var target = createDiv(t);
+ var anim = target.animate([ { opacity: 0 }, { opacity: 1 } ],
+ { duration: 1000 * MS_PER_SEC,
+ fill: 'both',
+ delay: delay,
+ easing: 'steps(2, start)' });
+
+ anim.effect.timing.easing = 'steps(2, end)';
+ assert_equals(anim.effect.getComputedTiming().progress, 0,
+ 'easing replace to steps(2, end) at before phase');
+
+ anim.currentTime = delay + 750 * MS_PER_SEC;
+ assert_equals(anim.effect.getComputedTiming().progress, 0.5,
+ 'change currentTime to active phase');
+
+ anim.effect.timing.easing = 'steps(2, start)';
+ assert_equals(anim.effect.getComputedTiming().progress, 1,
+ 'easing replace to steps(2, start) at active phase');
+
+ anim.currentTime = delay + 1500 * MS_PER_SEC;
+ anim.effect.timing.easing = 'steps(2, end)';
+ assert_equals(anim.effect.getComputedTiming().progress, 1,
+ 'easing replace to steps(2, end) again at after phase');
+}, 'Change the easing while the animation is running');
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/web-animations/interfaces/AnimationEffectTiming/endDelay.html b/testing/web-platform/tests/web-animations/interfaces/AnimationEffectTiming/endDelay.html
new file mode 100644
index 000000000..40136b45c
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/interfaces/AnimationEffectTiming/endDelay.html
@@ -0,0 +1,84 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title>endDelay tests</title>
+<link rel="help" href="http://w3c.github.io/web-animations/#dom-animationeffecttiming-enddelay">
+<link rel="author" title="Ryo Motozawa" href="mailto:motozawa@mozilla-japan.org">
+<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(function(t) {
+ var div = createDiv(t);
+ var anim = div.animate({ opacity: [ 0, 1 ] }, 2000);
+ anim.effect.timing.endDelay = 123.45;
+ assert_times_equal(anim.effect.timing.endDelay, 123.45,
+ 'set endDelay 123.45');
+ assert_times_equal(anim.effect.getComputedTiming().endDelay, 123.45,
+ 'getComputedTiming() after set endDelay 123.45');
+}, 'set endDelay 123.45');
+
+test(function(t) {
+ var div = createDiv(t);
+ var anim = div.animate({ opacity: [ 0, 1 ] }, 2000);
+ anim.effect.timing.endDelay = -1000;
+ assert_equals(anim.effect.timing.endDelay, -1000, 'set endDelay -1000');
+ assert_equals(anim.effect.getComputedTiming().endDelay, -1000,
+ 'getComputedTiming() after set endDelay -1000');
+}, 'set endDelay -1000');
+
+test(function(t) {
+ var div = createDiv(t);
+ var anim = div.animate({ opacity: [ 0, 1 ] }, 2000);
+ assert_throws({name: "TypeError"}, function() {
+ anim.effect.timing.endDelay = Infinity;
+ }, 'we can not assign Infinity to timing.endDelay');
+}, 'set endDelay Infinity');
+
+test(function(t) {
+ var div = createDiv(t);
+ var anim = div.animate({ opacity: [ 0, 1 ] }, 2000);
+ assert_throws({name: "TypeError"}, function() {
+ anim.effect.timing.endDelay = -Infinity;
+ }, 'we can not assign negative Infinity to timing.endDelay');
+}, 'set endDelay negative Infinity');
+
+async_test(function(t) {
+ var div = createDiv(t);
+ var anim = div.animate({ opacity: [ 0, 1 ] },
+ { duration: 100000, endDelay: 50000 });
+ anim.onfinish = t.step_func(function(event) {
+ assert_unreached('onfinish event should not be fired');
+ });
+
+ anim.ready.then(function() {
+ anim.currentTime = 100000;
+ return waitForAnimationFrames(2);
+ }).then(t.step_func(function() {
+ t.done();
+ }));
+}, 'onfinish event is not fired duration endDelay');
+
+async_test(function(t) {
+ var div = createDiv(t);
+ var anim = div.animate({ opacity: [ 0, 1 ] },
+ { duration: 100000, endDelay: 30000 });
+ anim.ready.then(function() {
+ anim.currentTime = 110000; // during endDelay
+ anim.onfinish = t.step_func(function(event) {
+ assert_unreached('onfinish event should not be fired during endDelay');
+ });
+ return waitForAnimationFrames(2);
+ }).then(t.step_func(function() {
+ anim.onfinish = t.step_func(function(event) {
+ t.done();
+ });
+ anim.currentTime = 130000; // after endTime
+ }));
+}, 'onfinish event is fired currentTime is after endTime');
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/web-animations/interfaces/AnimationEffectTiming/fill.html b/testing/web-platform/tests/web-animations/interfaces/AnimationEffectTiming/fill.html
new file mode 100644
index 000000000..21e523b73
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/interfaces/AnimationEffectTiming/fill.html
@@ -0,0 +1,24 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title>fill tests</title>
+<link rel="help" href="https://w3c.github.io/web-animations/#dom-animationeffecttiming-fill">
+<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';
+
+["none", "forwards", "backwards", "both", ].forEach(function(fill){
+ test(function(t) {
+ var div = createDiv(t);
+ var anim = div.animate({ opacity: [ 0, 1 ] }, 100);
+ anim.effect.timing.fill = fill;
+ assert_equals(anim.effect.timing.fill, fill, 'set fill ' + fill);
+ assert_equals(anim.effect.getComputedTiming().fill, fill, 'getComputedTiming() after set fill ' + fill);
+ }, 'set fill ' + fill);
+});
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/web-animations/interfaces/AnimationEffectTiming/getAnimations.html b/testing/web-platform/tests/web-animations/interfaces/AnimationEffectTiming/getAnimations.html
new file mode 100644
index 000000000..d96192c9d
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/interfaces/AnimationEffectTiming/getAnimations.html
@@ -0,0 +1,91 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title>Element.getAnimations tests</title>
+<link rel="help" href="http://w3c.github.io/web-animations/#animationeffecttiming">
+<link rel="author" title="Ryo Motozawa" href="mailto:motozawa@mozilla-japan.org">
+<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(function(t) {
+ var div = createDiv(t);
+ var anim = div.animate({ opacity: [ 0, 1 ] }, 2000);
+ anim.finish();
+ assert_equals(div.getAnimations().length, 0, 'animation finished');
+ anim.effect.timing.duration += 100000;
+ assert_equals(div.getAnimations()[0], anim, 'set duration 102000');
+ anim.effect.timing.duration = 0;
+ assert_equals(div.getAnimations().length, 0, 'set duration 0');
+ anim.effect.timing.duration = 'auto';
+ assert_equals(div.getAnimations().length, 0, 'set duration \'auto\'');
+}, 'when duration is changed');
+
+test(function(t) {
+ var div = createDiv(t);
+ var anim = div.animate({ opacity: [ 0, 1 ] }, 2000);
+
+ anim.effect.timing.endDelay = -3000;
+ assert_equals(div.getAnimations().length, 0,
+ 'set negative endDelay so as endTime is less than currentTime');
+ anim.effect.timing.endDelay = 1000;
+ assert_equals(div.getAnimations()[0], anim,
+ 'set positive endDelay so as endTime is more than currentTime');
+
+ anim.effect.timing.duration = 1000;
+ anim.currentTime = 1500;
+ assert_equals(div.getAnimations().length, 0,
+ 'set currentTime less than endTime');
+ anim.effect.timing.endDelay = -500;
+ anim.currentTime = 400;
+ assert_equals(div.getAnimations()[0], anim,
+ 'set currentTime less than endTime when endDelay is negative value');
+ anim.currentTime = 500;
+ assert_equals(div.getAnimations().length, 0,
+ 'set currentTime same as endTime when endDelay is negative value');
+ anim.currentTime = 1000;
+ assert_equals(div.getAnimations().length, 0,
+ 'set currentTime same as duration when endDelay is negative value');
+}, 'when endDelay is changed');
+
+test(function(t) {
+ var div = createDiv(t);
+ var anim = div.animate({ opacity: [ 0, 1 ] }, 2000);
+ anim.finish();
+ assert_equals(div.getAnimations().length, 0, 'animation finished');
+ anim.effect.timing.iterations = 10;
+ assert_equals(div.getAnimations()[0], anim, 'set iterations 10');
+ anim.effect.timing.iterations = 0;
+ assert_equals(div.getAnimations().length, 0, 'set iterations 0');
+ anim.effect.timing.iterations = Infinity;
+ assert_equals(div.getAnimations().length, 1, 'set iterations Infinity');
+}, 'when iterations is changed');
+
+test(function(t) {
+ var div = createDiv(t);
+ var anim = div.animate({ opacity: [ 0, 1 ] },
+ { duration: 1000, delay: 500, endDelay: -500 });
+ assert_equals(div.getAnimations()[0], anim, 'when currentTime 0');
+ anim.currentTime = 500;
+ assert_equals(div.getAnimations()[0], anim, 'set currentTime 500');
+ anim.currentTime = 1000;
+ assert_equals(div.getAnimations().length, 0, 'set currentTime 1000');
+}, 'when currentTime changed in duration:1000, delay: 500, endDelay: -500');
+
+test(function(t) {
+ var div = createDiv(t);
+ var anim = div.animate({ opacity: [ 0, 1 ] },
+ { duration: 1000, delay: -500, endDelay: -500 });
+ assert_equals(div.getAnimations().length, 0, 'when currentTime 0');
+ anim.currentTime = 500;
+ assert_equals(div.getAnimations().length, 0, 'set currentTime 500');
+ anim.currentTime = 1000;
+ assert_equals(div.getAnimations().length, 0, 'set currentTime 1000');
+}, 'when currentTime changed in duration:1000, delay: -500, endDelay: -500');
+
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/web-animations/interfaces/AnimationEffectTiming/getComputedStyle.html b/testing/web-platform/tests/web-animations/interfaces/AnimationEffectTiming/getComputedStyle.html
new file mode 100644
index 000000000..d6503a431
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/interfaces/AnimationEffectTiming/getComputedStyle.html
@@ -0,0 +1,172 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title>getComputedStyle tests</title>
+<link rel="help" href="http://w3c.github.io/web-animations/#animationeffecttiming">
+<link rel="author" title="Ryo Motozawa" href="mailto:motozawa@mozilla-japan.org">
+<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(function(t) {
+ var div = createDiv(t);
+ var anim = div.animate({ opacity: [ 0, 1 ] }, 100000);
+ anim.finish();
+ assert_equals(getComputedStyle(div).opacity, '1', 'animation finished');
+ anim.effect.timing.duration *= 2;
+ assert_equals(getComputedStyle(div).opacity, '0.5', 'set double duration');
+ anim.effect.timing.duration = 0;
+ assert_equals(getComputedStyle(div).opacity, '1', 'set duration 0');
+ anim.effect.timing.duration = 'auto';
+ assert_equals(getComputedStyle(div).opacity, '1', 'set duration \'auto\'');
+}, 'changed duration immediately updates its computed styles');
+
+test(function(t) {
+ var div = createDiv(t);
+ var anim = div.animate({ opacity: [ 0, 1 ] }, 100000);
+ anim.finish();
+ assert_equals(getComputedStyle(div).opacity, '1', 'animation finished');
+ anim.effect.timing.iterations = 2;
+ assert_equals(getComputedStyle(div).opacity, '0', 'set 2 iterations');
+ anim.effect.timing.iterations = 0;
+ assert_equals(getComputedStyle(div).opacity, '1', 'set iterations 0');
+ anim.effect.timing.iterations = Infinity;
+ assert_equals(getComputedStyle(div).opacity, '0', 'set iterations Infinity');
+}, 'changed iterations immediately updates its computed styles');
+
+test(function(t) {
+ var div = createDiv(t);
+ var anim = div.animate({ opacity: [ 1, 0 ] },
+ { duration: 10000, endDelay: 1000, fill: 'none' });
+
+ anim.currentTime = 9000;
+ assert_equals(getComputedStyle(div).opacity, '0.1',
+ 'set currentTime during duration');
+
+ anim.currentTime = 10900;
+ assert_equals(getComputedStyle(div).opacity, '1',
+ 'set currentTime during endDelay');
+
+ anim.currentTime = 11100;
+ assert_equals(getComputedStyle(div).opacity, '1',
+ 'set currentTime after endDelay');
+}, 'change currentTime when fill is none and endDelay is positive');
+
+test(function(t) {
+ var div = createDiv(t);
+ var anim = div.animate({ opacity: [ 1, 0 ] },
+ { duration: 10000,
+ endDelay: 1000,
+ fill: 'forwards' });
+ anim.currentTime = 5000;
+ assert_equals(getComputedStyle(div).opacity, '0.5',
+ 'set currentTime during duration');
+
+ anim.currentTime = 9999;
+ assert_equals(getComputedStyle(div).opacity, '0.0001',
+ 'set currentTime just a little before duration');
+
+ anim.currentTime = 10900;
+ assert_equals(getComputedStyle(div).opacity, '0',
+ 'set currentTime during endDelay');
+
+ anim.currentTime = 11100;
+ assert_equals(getComputedStyle(div).opacity, '0',
+ 'set currentTime after endDelay');
+}, 'change currentTime when fill forwards and endDelay is positive');
+
+test(function(t) {
+ var div = createDiv(t);
+ var anim = div.animate({ opacity: [ 1, 0 ] },
+ { duration: 10000, endDelay: -5000, fill: 'none' });
+
+ anim.currentTime = 1000;
+ assert_equals(getComputedStyle(div).opacity, '0.9',
+ 'set currentTime before endTime');
+
+ anim.currentTime = 10000;
+ assert_equals(getComputedStyle(div).opacity, '1',
+ 'set currentTime after endTime');
+}, 'change currentTime when fill none and endDelay is negative');
+
+test(function(t) {
+ var div = createDiv(t);
+ var anim = div.animate({ opacity: [ 1, 0 ] },
+ { duration: 10000,
+ endDelay: -5000,
+ fill: 'forwards' });
+
+ anim.currentTime = 1000;
+ assert_equals(getComputedStyle(div).opacity, '0.9',
+ 'set currentTime before endTime');
+
+ anim.currentTime = 5000;
+ assert_equals(getComputedStyle(div).opacity, '0.5',
+ 'set currentTime same as endTime');
+
+ anim.currentTime = 10000;
+ assert_equals(getComputedStyle(div).opacity, '0',
+ 'set currentTime after endTime');
+}, 'change currentTime when fill forwards and endDelay is negative');
+
+test(function(t) {
+ var div = createDiv(t);
+ var anim = div.animate({ opacity: [ 0, 1 ] },
+ { duration: 10000,
+ direction: 'normal' });
+
+ anim.currentTime = 7000;
+ anim.effect.timing.direction = 'reverse';
+
+ assert_equals(getComputedStyle(div).opacity, '0.3',
+ 'change direction from "normal" to "reverse"');
+}, 'change direction from "normal" to "reverse"');
+
+test(function(t) {
+ var div = createDiv(t);
+ var anim = div.animate({ opacity: [ 0, 1 ] },
+ { iterations: 2,
+ duration: 10000,
+ direction: 'normal' });
+
+ anim.currentTime = 17000;
+ anim.effect.timing.direction = 'alternate';
+
+ assert_equals(getComputedStyle(div).opacity, '0.3',
+ 'change direction from "normal" to "alternate"');
+ }, 'change direction from "normal" to "alternate"');
+
+test(function(t) {
+ var div = createDiv(t);
+ var anim = div.animate({ opacity: [ 0, 1 ] },
+ { iterations: 2,
+ duration: 10000,
+ direction: 'normal' });
+
+ anim.currentTime = 17000;
+ anim.effect.timing.direction = 'alternate-reverse';
+
+ assert_equals(getComputedStyle(div).opacity, '0.7',
+ 'change direction from "normal" to "alternate-reverse"');
+}, 'change direction from "normal" to "alternate-reverse"');
+
+test(function(t) {
+ var div = createDiv(t);
+ var anim = div.animate({ opacity: [ 0, 1 ] },
+ { fill: 'backwards',
+ duration: 10000,
+ direction: 'normal' });
+
+ // test for a flip of value at the currentTime = 0
+ anim.effect.timing.direction = 'reverse';
+
+ assert_equals(getComputedStyle(div).opacity, '1',
+ 'change direction from "normal" to "reverse" ' +
+ 'at the starting point');
+}, 'change direction from "normal" to "reverse"');
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/web-animations/interfaces/AnimationEffectTiming/iterationStart.html b/testing/web-platform/tests/web-animations/interfaces/AnimationEffectTiming/iterationStart.html
new file mode 100644
index 000000000..0273fd12b
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/interfaces/AnimationEffectTiming/iterationStart.html
@@ -0,0 +1,72 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title>iterationStart tests</title>
+<link rel="help" href="https://w3c.github.io/web-animations/#dom-animationeffecttiming-iterationstart">
+<link rel="author" title="Daisuke Akatsuka" href="mailto:daisuke@mozilla-japan.org">
+<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(function(t) {
+ var div = createDiv(t);
+ var anim = div.animate({ opacity: [ 0, 1 ] },
+ { iterationStart: 0.2,
+ iterations: 1,
+ fill: 'both',
+ duration: 100,
+ delay: 1 });
+ anim.effect.timing.iterationStart = 2.5;
+ assert_equals(anim.effect.getComputedTiming().progress, 0.5);
+ assert_equals(anim.effect.getComputedTiming().currentIteration, 2);
+}, 'Test that changing the iterationStart affects computed timing ' +
+ 'when backwards-filling');
+
+test(function(t) {
+ var div = createDiv(t);
+ var anim = div.animate({ opacity: [ 0, 1 ] },
+ { iterationStart: 0.2,
+ iterations: 1,
+ fill: 'both',
+ duration: 100,
+ delay: 0 });
+ anim.effect.timing.iterationStart = 2.5;
+ assert_equals(anim.effect.getComputedTiming().progress, 0.5);
+ assert_equals(anim.effect.getComputedTiming().currentIteration, 2);
+}, 'Test that changing the iterationStart affects computed timing ' +
+ 'during the active phase');
+
+test(function(t) {
+ var div = createDiv(t);
+ var anim = div.animate({ opacity: [ 0, 1 ] },
+ { iterationStart: 0.2,
+ iterations: 1,
+ fill: 'both',
+ duration: 100,
+ delay: 0 });
+ anim.finish();
+ anim.effect.timing.iterationStart = 2.5;
+ assert_equals(anim.effect.getComputedTiming().progress, 0.5);
+ assert_equals(anim.effect.getComputedTiming().currentIteration, 3);
+}, 'Test that changing the iterationStart affects computed timing ' +
+ 'when forwards-filling');
+
+test(function(t) {
+ var div = createDiv(t);
+ var anim = div.animate({ opacity: [ 0, 1 ] }, 100);
+ assert_throws({ name: 'TypeError' },
+ function() {
+ anim.effect.timing.iterationStart = -1;
+ });
+ assert_throws({ name: 'TypeError' },
+ function() {
+ div.animate({ opacity: [ 0, 1 ] },
+ { iterationStart: -1 });
+ });
+}, 'Test invalid iterationStart value');
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/web-animations/interfaces/AnimationEffectTiming/iterations.html b/testing/web-platform/tests/web-animations/interfaces/AnimationEffectTiming/iterations.html
new file mode 100644
index 000000000..1ba41028b
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/interfaces/AnimationEffectTiming/iterations.html
@@ -0,0 +1,56 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title>iterations tests</title>
+<link rel="help" href="https://w3c.github.io/web-animations/#dom-animationeffecttiming-iterations">
+<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(function(t) {
+ var div = createDiv(t);
+ var anim = div.animate({ opacity: [ 0, 1 ] }, 2000);
+ anim.effect.timing.iterations = 2;
+ assert_equals(anim.effect.timing.iterations, 2, 'set duration 2');
+ assert_equals(anim.effect.getComputedTiming().iterations, 2,
+ 'getComputedTiming() after set iterations 2');
+}, 'set iterations 2');
+
+test(function(t) {
+ var div = createDiv(t);
+ var anim = div.animate({ opacity: [ 0, 1 ] }, 2000);
+ anim.effect.timing.iterations = Infinity;
+ assert_equals(anim.effect.timing.iterations, Infinity, 'set duration Infinity');
+ assert_equals(anim.effect.getComputedTiming().iterations, Infinity,
+ 'getComputedTiming() after set iterations Infinity');
+}, 'set iterations Infinity');
+
+test(function(t) {
+ var div = createDiv(t);
+ var anim = div.animate({ opacity: [ 0, 1 ] }, 2000);
+ assert_throws({ name: 'TypeError' }, function() {
+ anim.effect.timing.iterations = -1;
+ });
+}, 'set negative iterations');
+
+test(function(t) {
+ var div = createDiv(t);
+ var anim = div.animate({ opacity: [ 0, 1 ] }, 2000);
+ assert_throws({ name: 'TypeError' }, function() {
+ anim.effect.timing.iterations = -Infinity;
+ });
+}, 'set negative infinity iterations ');
+
+test(function(t) {
+ var div = createDiv(t);
+ var anim = div.animate({ opacity: [ 0, 1 ] }, 2000);
+ assert_throws({ name: 'TypeError' }, function() {
+ anim.effect.timing.iterations = NaN;
+ });
+}, 'set NaN iterations');
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/web-animations/interfaces/AnimationTimeline/document-timeline.html b/testing/web-platform/tests/web-animations/interfaces/AnimationTimeline/document-timeline.html
new file mode 100644
index 000000000..0f782f50a
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/interfaces/AnimationTimeline/document-timeline.html
@@ -0,0 +1,59 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>Default document timeline tests</title>
+<link rel="help" href="https://w3c.github.io/web-animations/#the-documents-default-timeline">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<iframe width="10" height="10" id="iframe"></iframe>
+<script>
+'use strict';
+
+test(function() {
+ assert_equals(document.timeline, document.timeline,
+ 'document.timeline returns the same object every time');
+ var iframe = document.getElementById('iframe');
+ assert_not_equals(document.timeline, iframe.contentDocument.timeline,
+ 'document.timeline returns a different object for each document');
+ assert_not_equals(iframe.contentDocument.timeline, null,
+ 'document.timeline on an iframe is not null');
+}, 'document.timeline identity tests');
+
+promise_test(function(t) {
+ assert_true(document.timeline.currentTime > 0,
+ 'document.timeline.currentTime is positive');
+ // 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_true(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.
+ return window.requestAnimationFrame(t.step_func(function(rafTime) {
+ assert_equals(document.timeline.currentTime, rafTime,
+ 'document.timeline.currentTime matches' +
+ ' requestAnimationFrame time');
+ }));
+}, 'document.timeline.currentTime value tests');
+
+promise_test(function(t) {
+ var valueAtStart = document.timeline.currentTime;
+ var timeAtStart = window.performance.now();
+ while (window.performance.now() - timeAtStart < 100) {
+ // Wait 100ms
+ }
+ assert_equals(document.timeline.currentTime, valueAtStart,
+ 'document.timeline.currentTime does not change within a script block');
+ return window.requestAnimationFrame(t.step_func(function() {
+ assert_true(document.timeline.currentTime > valueAtStart,
+ 'document.timeline.currentTime increases between script blocks');
+ }));
+}, 'document.timeline.currentTime liveness tests');
+
+</script>
diff --git a/testing/web-platform/tests/web-animations/interfaces/AnimationTimeline/idlharness.html b/testing/web-platform/tests/web-animations/interfaces/AnimationTimeline/idlharness.html
new file mode 100644
index 000000000..29c891407
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/interfaces/AnimationTimeline/idlharness.html
@@ -0,0 +1,36 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>Web Animations API: DocumentTimeline tests</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/WebIDLParser.js"></script>
+<script src="/resources/idlharness.js"></script>
+<div id="log"></div>
+<script type="text/plain" id="AnimationTimeline-IDL">
+interface AnimationTimeline {
+ readonly attribute double? currentTime;
+};
+</script>
+<script type="text/plain" id="DocumentTimeline-IDL">
+dictionary DocumentTimelineOptions {
+ DOMHighResTimeStamp originTime = 0;
+};
+[Constructor (optional DocumentTimelineOptions options)]
+interface DocumentTimeline : AnimationTimeline {
+};
+</script>
+<script>
+'use strict';
+
+var idlArray;
+test(function() {
+ idlArray = new IdlArray();
+ idlArray.add_untested_idls(
+ document.getElementById('AnimationTimeline-IDL').textContent);
+ idlArray.add_idls(
+ document.getElementById('DocumentTimeline-IDL').textContent);
+ idlArray.add_objects( { DocumentTimeline: ['document.timeline'] } );
+});
+idlArray.test();
+
+</script>
diff --git a/testing/web-platform/tests/web-animations/interfaces/Document/getAnimations.html b/testing/web-platform/tests/web-animations/interfaces/Document/getAnimations.html
new file mode 100644
index 000000000..e87751c04
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/interfaces/Document/getAnimations.html
@@ -0,0 +1,55 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title>document.getAnimations tests</title>
+<link rel="help" href="https://w3c.github.io/web-animations/#dom-document-getanimations">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../../testcommon.js"></script>
+<body>
+<div id="log"></div>
+<div id="target"></div>
+<script>
+"use strict";
+
+var gKeyFrames = { 'marginLeft': ['100px', '200px'] };
+
+test(function(t) {
+ assert_equals(document.getAnimations().length, 0,
+ 'getAnimations returns an empty sequence for a document ' +
+ 'with no animations');
+}, 'Test document.getAnimations for non-animated content');
+
+test(function(t) {
+ var div = createDiv(t);
+ var anim1 = div.animate(gKeyFrames, 100 * MS_PER_SEC);
+ var anim2 = div.animate(gKeyFrames, 100 * MS_PER_SEC);
+ assert_equals(document.getAnimations().length, 2,
+ 'getAnimation returns running animations');
+
+ anim1.finish();
+ anim2.finish();
+ assert_equals(document.getAnimations().length, 0,
+ 'getAnimation only returns running animations');
+}, 'Test document.getAnimations for script-generated animations')
+
+test(function(t) {
+ var div = createDiv(t);
+ var anim1 = div.animate(gKeyFrames, 100 * MS_PER_SEC);
+ var anim2 = div.animate(gKeyFrames, 100 * MS_PER_SEC);
+ assert_array_equals(document.getAnimations(),
+ [ anim1, anim2 ],
+ 'getAnimations() returns running animations');
+}, 'Test the order of document.getAnimations with script generated animations')
+
+test(function(t) {
+ var effect = new KeyframeEffectReadOnly(null, gKeyFrames, 100 * MS_PER_SEC);
+ var anim = new Animation(effect, document.timeline);
+ anim.play();
+
+ assert_equals(document.getAnimations().length, 0,
+ 'document.getAnimations() only returns animations targeting ' +
+ 'elements in this document');
+}, 'Test document.getAnimations with null target');
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/web-animations/interfaces/DocumentTimeline/constructor.html b/testing/web-platform/tests/web-animations/interfaces/DocumentTimeline/constructor.html
new file mode 100644
index 000000000..8968ff4ce
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/interfaces/DocumentTimeline/constructor.html
@@ -0,0 +1,42 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title>DocumentTimeline constructor tests</title>
+<link rel="help" href="https://w3c.github.io/web-animations/#the-documenttimeline-interface">
+<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(function(t) {
+ var timeline = new DocumentTimeline();
+
+ assert_times_equal(timeline.currentTime, document.timeline.currentTime);
+}, 'An origin time of zero is used when none is supplied');
+
+test(function(t) {
+ var timeline = new DocumentTimeline({ originTime: 0 });
+ assert_times_equal(timeline.currentTime, document.timeline.currentTime);
+}, 'A zero origin time produces a document timeline with a current time ' +
+ 'identical to the default document timeline');
+
+test(function(t) {
+ var timeline = new DocumentTimeline({ originTime: 10 * MS_PER_SEC });
+
+ assert_times_equal(timeline.currentTime,
+ (document.timeline.currentTime - 10 * MS_PER_SEC));
+}, 'A positive origin time makes the document timeline\'s current time lag ' +
+ 'behind the default document timeline');
+
+test(function(t) {
+ var timeline = new DocumentTimeline({ originTime: -10 * MS_PER_SEC });
+
+ assert_times_equal(timeline.currentTime,
+ (document.timeline.currentTime + 10 * MS_PER_SEC));
+}, 'A negative origin time makes the document timeline\'s current time run ' +
+ 'ahead of the default document timeline');
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/web-animations/interfaces/KeyframeEffect/constructor.html b/testing/web-platform/tests/web-animations/interfaces/KeyframeEffect/constructor.html
new file mode 100644
index 000000000..97c7613f8
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/interfaces/KeyframeEffect/constructor.html
@@ -0,0 +1,278 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title>KeyframeEffectReadOnly constructor tests</title>
+<link rel="help" href="https://w3c.github.io/web-animations/#the-keyframeeffect-interfaces">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../../testcommon.js"></script>
+<script src="../../resources/keyframe-utils.js"></script>
+<body>
+<div id="log"></div>
+<div id="target"></div>
+<style>
+#target {
+ border-style: solid; /* so border-*-width values don't compute to 0 */
+}
+</style>
+<script>
+"use strict";
+
+var target = document.getElementById("target");
+
+test(function(t) {
+ gEmptyKeyframeListTests.forEach(function(frames) {
+ assert_equals(new KeyframeEffectReadOnly(target, frames)
+ .getKeyframes().length,
+ 0, "number of frames for " + JSON.stringify(frames));
+ });
+}, "a KeyframeEffectReadOnly can be constructed with no frames");
+
+test(function(t) {
+ gEasingValueTests.forEach(function(subtest) {
+ var easing = subtest[0];
+ var expected = subtest[1];
+ var effect = new KeyframeEffectReadOnly(target, {
+ left: ["10px", "20px"],
+ easing: easing
+ });
+ assert_equals(effect.getKeyframes()[0].easing, expected,
+ "resulting easing for '" + easing + "'");
+ });
+}, "easing values are parsed correctly when passed to the " +
+ "KeyframeEffectReadOnly constructor in a property-indexed keyframe");
+
+test(function(t) {
+ gEasingValueTests.forEach(function(subtest) {
+ var easing = subtest[0];
+ var expected = subtest[1];
+ var effect = new KeyframeEffectReadOnly(target, [
+ { offset: 0, left: "10px", easing: easing },
+ { offset: 1, left: "20px" }
+ ]);
+ assert_equals(effect.getKeyframes()[0].easing, expected,
+ "resulting easing for '" + easing + "'");
+ });
+}, "easing values are parsed correctly when passed to the " +
+ "KeyframeEffectReadOnly constructor in regular keyframes");
+
+test(function(t) {
+ gEasingValueTests.forEach(function(subtest) {
+ var easing = subtest[0];
+ var expected = subtest[1];
+ var effect = new KeyframeEffectReadOnly(target, {
+ left: ["10px", "20px"]
+ }, { easing: easing });
+ assert_equals(effect.timing.easing, expected,
+ "resulting easing for '" + easing + "'");
+ });
+}, "easing values are parsed correctly when passed to the " +
+ "KeyframeEffectReadOnly constructor in KeyframeTimingOptions");
+
+test(function(t) {
+ var getKeyframe = function(composite) {
+ return { left: [ "10px", "20px" ], composite: composite };
+ };
+ gGoodKeyframeCompositeValueTests.forEach(function(composite) {
+ var effect = new KeyframeEffectReadOnly(target, getKeyframe(composite));
+ assert_equals(effect.getKeyframes()[0].composite, composite,
+ "resulting composite for '" + composite + "'");
+ });
+ gBadCompositeValueTests.forEach(function(composite) {
+ assert_throws(new TypeError, function() {
+ new KeyframeEffectReadOnly(target, getKeyframe(composite));
+ });
+ });
+}, "composite values are parsed correctly when passed to the " +
+ "KeyframeEffectReadOnly constructor in property-indexed keyframes");
+
+test(function(t) {
+ var getKeyframes = function(composite) {
+ return [
+ { offset: 0, left: "10px", composite: composite },
+ { offset: 1, left: "20px" }
+ ];
+ };
+ gGoodKeyframeCompositeValueTests.forEach(function(composite) {
+ var effect = new KeyframeEffectReadOnly(target, getKeyframes(composite));
+ assert_equals(effect.getKeyframes()[0].composite, composite,
+ "resulting composite for '" + composite + "'");
+ });
+ gBadCompositeValueTests.forEach(function(composite) {
+ assert_throws(new TypeError, function() {
+ new KeyframeEffectReadOnly(target, getKeyframes(composite));
+ });
+ });
+}, "composite values are parsed correctly when passed to the " +
+ "KeyframeEffectReadOnly constructor in regular keyframes");
+
+test(function(t) {
+ gGoodOptionsCompositeValueTests.forEach(function(composite) {
+ var effect = new KeyframeEffectReadOnly(target, {
+ left: ["10px", "20px"]
+ }, { composite: composite });
+ assert_equals(effect.getKeyframes()[0].composite, composite,
+ "resulting composite for '" + composite + "'");
+ });
+ gBadCompositeValueTests.forEach(function(composite) {
+ assert_throws(new TypeError, function() {
+ new KeyframeEffectReadOnly(target, {
+ left: ["10px", "20px"]
+ }, { composite: composite });
+ });
+ });
+}, "composite values are parsed correctly when passed to the " +
+ "KeyframeEffectReadOnly constructor in KeyframeTimingOptions");
+
+gPropertyIndexedKeyframesTests.forEach(function(subtest) {
+ test(function(t) {
+ var effect = new KeyframeEffectReadOnly(target, subtest.input);
+ assert_frame_lists_equal(effect.getKeyframes(), subtest.output);
+ }, "a KeyframeEffectReadOnly can be constructed with " + subtest.desc);
+
+ test(function(t) {
+ var effect = new KeyframeEffectReadOnly(target, subtest.input);
+ var secondEffect =
+ new KeyframeEffectReadOnly(target, effect.getKeyframes());
+ assert_frame_lists_equal(secondEffect.getKeyframes(),
+ effect.getKeyframes());
+ }, "a KeyframeEffectReadOnly constructed with " + subtest.desc +
+ " roundtrips");
+});
+
+test(function(t) {
+ var expectedOrder = ["composite", "easing", "offset", "left", "marginLeft"];
+ var actualOrder = [];
+ var kf1 = {};
+ var kf2 = { marginLeft: "10px", left: "20px", offset: 1 };
+ [{ p: "marginLeft", v: "10px" },
+ { p: "left", v: "20px" },
+ { p: "offset", v: "0" },
+ { p: "easing", v: "linear" },
+ { p: "composite", v: "replace" }].forEach(function(e) {
+ Object.defineProperty(kf1, e.p, {
+ enumerable: true,
+ get: function() { actualOrder.push(e.p); return e.v; }
+ });
+ });
+ new KeyframeEffectReadOnly(target, [kf1, kf2]);
+ assert_array_equals(actualOrder, expectedOrder, "property access order");
+}, "the KeyframeEffectReadOnly constructor reads keyframe properties in the " +
+ "expected order");
+
+gKeyframeSequenceTests.forEach(function(subtest) {
+ test(function(t) {
+ var effect = new KeyframeEffectReadOnly(target, subtest.input);
+ assert_frame_lists_equal(effect.getKeyframes(), subtest.output);
+ }, "a KeyframeEffectReadOnly can be constructed with " + subtest.desc);
+
+ test(function(t) {
+ var effect = new KeyframeEffectReadOnly(target, subtest.input);
+ var secondEffect =
+ new KeyframeEffectReadOnly(target, effect.getKeyframes());
+ assert_frame_lists_equal(secondEffect.getKeyframes(),
+ effect.getKeyframes());
+ }, "a KeyframeEffectReadOnly constructed with " + subtest.desc +
+ " roundtrips");
+});
+
+gInvalidKeyframesTests.forEach(function(subtest) {
+ test(function(t) {
+ assert_throws(subtest.expected, function() {
+ new KeyframeEffectReadOnly(target, subtest.input);
+ });
+ }, "KeyframeEffectReadOnly constructor throws with " + subtest.desc);
+});
+
+gInvalidEasingInKeyframeSequenceTests.forEach(function(subtest) {
+ test(function(t) {
+ assert_throws(new TypeError, function() {
+ new KeyframeEffectReadOnly(target, subtest.input);
+ });
+ }, "Invalid easing [" + subtest.desc + "] in keyframe sequence " +
+ "should be thrown");
+});
+
+test(function(t) {
+ var effect = new KeyframeEffectReadOnly(target,
+ { left: ["10px", "20px"] });
+
+ var timing = effect.timing;
+ assert_equals(timing.delay, 0, "default delay");
+ assert_equals(timing.endDelay, 0, "default endDelay");
+ assert_equals(timing.fill, "auto", "default fill");
+ assert_equals(timing.iterations, 1.0, "default iterations");
+ assert_equals(timing.iterationStart, 0.0, "default iterationStart");
+ assert_equals(timing.duration, "auto", "default duration");
+ assert_equals(timing.direction, "normal", "default direction");
+ assert_equals(timing.easing, "linear", "default easing");
+
+ assert_equals(effect.composite, "replace", "default composite");
+ assert_equals(effect.iterationComposite, "replace",
+ "default iterationComposite");
+ assert_equals(effect.spacing, "distribute",
+ "default spacing");
+}, "a KeyframeEffectReadOnly constructed without any " +
+ "KeyframeEffectOptions object");
+
+gKeyframeEffectOptionTests.forEach(function(stest) {
+ test(function(t) {
+ var effect = new KeyframeEffectReadOnly(target,
+ { left: ["10px", "20px"] },
+ stest.input);
+
+ // Helper function to provide default expected values when the test does
+ // not supply them.
+ var expected = function(field, defaultValue) {
+ return field in stest.expected ? stest.expected[field] : defaultValue;
+ };
+
+ var timing = effect.timing;
+ assert_equals(timing.delay, expected("delay", 0),
+ "timing delay");
+ assert_equals(timing.fill, expected("fill", "auto"),
+ "timing fill");
+ assert_equals(timing.iterations, expected("iterations", 1),
+ "timing iterations");
+ assert_equals(timing.duration, expected("duration", "auto"),
+ "timing duration");
+ assert_equals(timing.direction, expected("direction", "normal"),
+ "timing direction");
+
+ }, "a KeyframeEffectReadOnly constructed by " + stest.desc);
+});
+
+gInvalidKeyframeEffectOptionTests.forEach(function(stest) {
+ test(function(t) {
+ assert_throws(stest.expected, function() {
+ new KeyframeEffectReadOnly(target,
+ { left: ["10px", "20px"] },
+ stest.input);
+ });
+ }, "Invalid KeyframeEffectReadOnly option by " + stest.desc);
+});
+
+test(function(t) {
+ var effect = new KeyframeEffectReadOnly(null,
+ { left: ["10px", "20px"] },
+ { duration: 100 * MS_PER_SEC,
+ fill: "forwards" });
+ assert_equals(effect.target, null,
+ "Effect created with null target has correct target");
+}, "a KeyframeEffectReadOnly constructed with null target");
+
+test(function(t) {
+ var effect = new KeyframeEffect(target, null);
+ assert_class_string(effect, "KeyframeEffect");
+ assert_class_string(effect.timing, "AnimationEffectTiming");
+}, "KeyframeEffect constructor creates an AnimationEffectTiming timing object");
+
+test(function(t) {
+ var test_error = { name: "test" };
+
+ assert_throws(test_error, function() {
+ new KeyframeEffect(target, { get left() { throw test_error }})
+ });
+}, "KeyframeEffect constructor propagates exceptions generated by accessing"
+ + " the options object");
+</script>
+</body>
diff --git a/testing/web-platform/tests/web-animations/interfaces/KeyframeEffect/copy-contructor.html b/testing/web-platform/tests/web-animations/interfaces/KeyframeEffect/copy-contructor.html
new file mode 100644
index 000000000..bc278389c
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/interfaces/KeyframeEffect/copy-contructor.html
@@ -0,0 +1,27 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title>KeyframeEffect copy constructor tests</title>
+<link rel="help"
+href="https://w3c.github.io/web-animations/#dom-keyframeeffect-keyframeeffect-source">
+<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(function(t) {
+ var effect = new KeyframeEffectReadOnly(createDiv(t), null);
+ assert_equals(effect.constructor.name, 'KeyframeEffectReadOnly');
+ assert_equals(effect.timing.constructor.name,
+ 'AnimationEffectTimingReadOnly');
+
+ // Make a mutable copy
+ var copiedEffect = new KeyframeEffect(effect);
+ assert_equals(copiedEffect.constructor.name, 'KeyframeEffect');
+ assert_equals(copiedEffect.timing.constructor.name, 'AnimationEffectTiming');
+}, 'Test mutable copy from a KeyframeEffectReadOnly source');
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/web-animations/interfaces/KeyframeEffect/effect-easing.html b/testing/web-platform/tests/web-animations/interfaces/KeyframeEffect/effect-easing.html
new file mode 100644
index 000000000..05019cdf3
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/interfaces/KeyframeEffect/effect-easing.html
@@ -0,0 +1,683 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title>Effect-level easing tests</title>
+<link rel="help" href="http://w3c.github.io/web-animations/#calculating-the-transformed-time">
+<link rel="author" title="Hiroyuki Ikezoe" href="mailto:hiikezoe@mozilla-japan.org">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../../testcommon.js"></script>
+<script src="../../resources/effect-easing-tests.js"></script>
+<body>
+<div id="log"></div>
+<div id="target"></div>
+<script>
+"use strict";
+
+function assert_style_left_at(animation, time, easingFunction) {
+ animation.currentTime = time;
+ var portion = time / animation.effect.timing.duration;
+ assert_approx_equals(pxToNum(getComputedStyle(animation.effect.target).left),
+ easingFunction(portion) * 100,
+ 0.01,
+ 'The left of the animation should be approximately ' +
+ easingFunction(portion) * 100 + ' at ' + time + 'ms');
+}
+
+gEffectEasingTests.forEach(function(options) {
+ test(function(t) {
+ var target = createDiv(t);
+ target.style.position = 'absolute';
+ var anim = target.animate([ { left: '0px' }, { left: '100px' } ],
+ { duration: 1000,
+ fill: 'forwards',
+ easing: options.easing });
+ var easing = options.easingFunction;
+
+ anim.pause();
+
+ assert_style_left_at(anim, 0, easing);
+ assert_style_left_at(anim, 250, easing);
+ assert_style_left_at(anim, 500, easing);
+ assert_style_left_at(anim, 750, easing);
+ assert_style_left_at(anim, 1000, easing);
+ }, options.desc);
+});
+
+var gEffectEasingTestsWithKeyframeEasing = [
+ {
+ desc: 'effect easing produces values greater than 1 with keyframe ' +
+ 'easing cubic-bezier(0, 0, 0, 0)',
+ easing: 'cubic-bezier(0, 1.5, 1, 1.5)',
+ keyframeEasing: 'cubic-bezier(0, 0, 0, 0)', // linear
+ easingFunction: cubicBezier(0, 1.5, 1, 1.5)
+ },
+ {
+ desc: 'effect easing produces values greater than 1 with keyframe ' +
+ 'easing cubic-bezier(1, 1, 1, 1)',
+ easing: 'cubic-bezier(0, 1.5, 1, 1.5)',
+ keyframeEasing: 'cubic-bezier(1, 1, 1, 1)', // linear
+ easingFunction: cubicBezier(0, 1.5, 1, 1.5)
+ },
+ {
+ desc: 'effect easing produces negative values 1 with keyframe ' +
+ 'easing cubic-bezier(0, 0, 0, 0)',
+ easing: 'cubic-bezier(0, -0.5, 1, -0.5)',
+ keyframeEasing: 'cubic-bezier(0, 0, 0, 0)', // linear
+ easingFunction: cubicBezier(0, -0.5, 1, -0.5)
+ },
+ {
+ desc: 'effect easing produces negative values 1 with keyframe ' +
+ 'easing cubic-bezier(1, 1, 1, 1)',
+ easing: 'cubic-bezier(0, -0.5, 1, -0.5)',
+ keyframeEasing: 'cubic-bezier(1, 1, 1, 1)', // linear
+ easingFunction: cubicBezier(0, -0.5, 1, -0.5)
+ },
+];
+
+gEffectEasingTestsWithKeyframeEasing.forEach(function(options) {
+ test(function(t) {
+ var target = createDiv(t);
+ target.style.position = 'absolute';
+ var anim = target.animate(
+ [ { left: '0px', easing: options.keyframeEasing },
+ { left: '100px' } ],
+ { duration: 1000,
+ fill: 'forwards',
+ easing: options.easing });
+ var easing = options.easingFunction;
+
+ anim.pause();
+
+ assert_style_left_at(anim, 0, easing);
+ assert_style_left_at(anim, 250, easing);
+ assert_style_left_at(anim, 500, easing);
+ assert_style_left_at(anim, 750, easing);
+ assert_style_left_at(anim, 1000, easing);
+ }, options.desc);
+});
+
+// Other test cases that effect easing produces values outside of [0,1].
+test(function(t) {
+ var target = createDiv(t);
+ target.style.position = 'absolute';
+ var anim = target.animate([ { left: '0px', easing: 'step-start' },
+ { left: '100px' } ],
+ { duration: 1000,
+ fill: 'forwards',
+ easing: 'cubic-bezier(0, 1.5, 1, 1.5)' });
+ anim.pause();
+
+ // The bezier function produces values greater than 1 in (0.23368794, 1)
+ anim.currentTime = 0;
+ assert_equals(getComputedStyle(target).left, '100px');
+ anim.currentTime = 230;
+ assert_equals(getComputedStyle(target).left, '100px');
+ anim.currentTime = 250;
+ assert_equals(getComputedStyle(target).left, '100px');
+ anim.currentTime = 1000;
+ assert_equals(getComputedStyle(target).left, '100px');
+}, 'effect easing produces values greater than 1 with step-start keyframe');
+
+test(function(t) {
+ var target = createDiv(t);
+ target.style.position = 'absolute';
+ var anim = target.animate([ { left: '0px', easing: 'step-end' },
+ { left: '100px' } ],
+ { duration: 1000,
+ fill: 'forwards',
+ easing: 'cubic-bezier(0, 1.5, 1, 1.5)' });
+ anim.pause();
+
+ // The bezier function produces values greater than 1 in (0.23368794, 1)
+ anim.currentTime = 0;
+ assert_equals(getComputedStyle(target).left, '0px');
+ anim.currentTime = 230;
+ assert_equals(getComputedStyle(target).left, '0px');
+ anim.currentTime = 250;
+ assert_equals(getComputedStyle(target).left, '100px');
+ anim.currentTime = 1000;
+ assert_equals(getComputedStyle(target).left, '100px');
+}, 'effect easing produces values greater than 1 with step-end keyframe');
+
+test(function(t) {
+ var target = createDiv(t);
+ target.style.position = 'absolute';
+ var anim = target.animate([ { left: '0px', easing: 'step-start' },
+ { left: '100px' } ],
+ { duration: 1000,
+ fill: 'forwards',
+ easing: 'cubic-bezier(0, -0.5, 1, -0.5)' });
+ anim.pause();
+
+ // The bezier function produces negative values in (0, 0.766312060)
+ anim.currentTime = 0;
+ assert_equals(getComputedStyle(target).left, '100px');
+ anim.currentTime = 750;
+ assert_equals(getComputedStyle(target).left, '0px');
+ anim.currentTime = 800;
+ assert_equals(getComputedStyle(target).left, '100px');
+ anim.currentTime = 1000;
+ assert_equals(getComputedStyle(target).left, '100px');
+}, 'effect easing produces negative values with step-start keyframe');
+
+test(function(t) {
+ var target = createDiv(t);
+ target.style.position = 'absolute';
+ var anim = target.animate([ { left: '0px', easing: 'step-end' },
+ { left: '100px' } ],
+ { duration: 1000,
+ fill: 'forwards',
+ easing: 'cubic-bezier(0, -0.5, 1, -0.5)' });
+ anim.pause();
+
+ // The bezier function produces negative values in (0, 0.766312060)
+ anim.currentTime = 0;
+ assert_equals(getComputedStyle(target).left, '0px');
+ anim.currentTime = 750;
+ assert_equals(getComputedStyle(target).left, '0px');
+ anim.currentTime = 800;
+ assert_equals(getComputedStyle(target).left, '0px');
+ anim.currentTime = 1000;
+ assert_equals(getComputedStyle(target).left, '100px');
+}, 'effect easing produces negative values with step-end keyframe');
+
+test(function(t) {
+ var target = createDiv(t);
+ target.style.position = 'absolute';
+ var anim = target.animate(
+ // http://cubic-bezier.com/#.5,1,.5,0
+ [ { left: '0px', easing: 'cubic-bezier(0.5, 1, 0.5, 0)' },
+ { left: '100px' } ],
+ { duration: 1000,
+ fill: 'forwards',
+ easing: 'cubic-bezier(0, 1.5, 1, 1.5)' });
+ var keyframeEasing = function(x) {
+ assert_greater_than_equal(x, 0.0,
+ 'This function should be called in [0, 1.0] range');
+ assert_less_than_equal(x, 1.0,
+ 'This function should be called in [0, 1.0] range');
+ return cubicBezier(0.5, 1, 0.5, 0)(x);
+ }
+ var keyframeEasingExtrapolated = function(x) {
+ assert_greater_than(x, 1.0,
+ 'This function should be called in (1.0, infinity) range');
+ // p3x + (p2y - p3y) / (p2x - p3x) * (x - p3x)
+ return 1.0 + (0 - 1) / (0.5 - 1) * (x - 1.0);
+ }
+ var effectEasing = function(x) {
+ return cubicBezier(0, 1.5, 1, 1.5)(x);
+ }
+
+ anim.pause();
+
+ // The effect-easing produces values greater than 1 in (0.23368794, 1)
+ assert_style_left_at(anim, 0, function(x) {
+ return keyframeEasing(effectEasing(x));
+ });
+ assert_style_left_at(anim, 230, function(x) {
+ return keyframeEasing(effectEasing(x));
+ });
+ assert_style_left_at(anim, 240, function(x) {
+ return keyframeEasingExtrapolated(effectEasing(x));
+ });
+ // Near the extreme point of the effect-easing function
+ assert_style_left_at(anim, 700, function(x) {
+ return keyframeEasingExtrapolated(effectEasing(x));
+ });
+ assert_style_left_at(anim, 990, function(x) {
+ return keyframeEasingExtrapolated(effectEasing(x));
+ });
+ assert_style_left_at(anim, 1000, function(x) {
+ return keyframeEasing(effectEasing(x));
+ });
+}, 'effect easing produces values greater than 1 with keyframe easing ' +
+ 'producing values greater than 1');
+
+test(function(t) {
+ var target = createDiv(t);
+ target.style.position = 'absolute';
+ var anim = target.animate(
+ // http://cubic-bezier.com/#0,1.5,1,1.5
+ [ { left: '0px', easing: 'cubic-bezier(0, 1.5, 1, 1.5)' },
+ { left: '100px' } ],
+ { duration: 1000,
+ fill: 'forwards',
+ easing: 'cubic-bezier(0, 1.5, 1, 1.5)' });
+ var easing = function(x) {
+ assert_greater_than_equal(x, 0.0,
+ 'This function should be called in [0, 1.0] range');
+ assert_less_than_equal(x, 1.0,
+ 'This function should be called in [0, 1.0] range');
+ return cubicBezier(0, 1.5, 1, 1.5)(x);
+ }
+ var easingExtrapolated = function(x) {
+ assert_greater_than(x, 1.0,
+ 'This function should be called in negative range');
+ // For cubic-bezier(0, 1.5, 1, 1.5), the tangent at the
+ // endpoint (x = 1.0) is infinity so we should just return 1.0.
+ return 1.0;
+ }
+
+ anim.pause();
+
+ // The effect-easing produces values greater than 1 in (0.23368794, 1)
+ assert_style_left_at(anim, 0, function(x) {
+ return easing(easing(x))
+ });
+ assert_style_left_at(anim, 230, function(x) {
+ return easing(easing(x))
+ });
+ assert_style_left_at(anim, 240, function(x) {
+ return easingExtrapolated(easing(x));
+ });
+ // Near the extreme point of the effect-easing function
+ assert_style_left_at(anim, 700, function(x) {
+ return easingExtrapolated(easing(x));
+ });
+ assert_style_left_at(anim, 990, function(x) {
+ return easingExtrapolated(easing(x));
+ });
+ assert_style_left_at(anim, 1000, function(x) {
+ return easing(easing(x))
+ });
+}, 'effect easing which produces values greater than 1 and the tangent on ' +
+ 'the upper boundary is infinity with keyframe easing producing values ' +
+ 'greater than 1');
+
+test(function(t) {
+ var target = createDiv(t);
+ target.style.position = 'absolute';
+ var anim = target.animate(
+ // http://cubic-bezier.com/#.5,1,.5,0
+ [ { left: '0px', easing: 'cubic-bezier(0.5, 1, 0.5, 0)' },
+ { left: '100px' } ],
+ { duration: 1000,
+ fill: 'forwards',
+ easing: 'cubic-bezier(0, -0.5, 1, -0.5)' });
+ var keyframeEasing = function(x) {
+ assert_greater_than_equal(x, 0.0,
+ 'This function should be called in [0, 1.0] range');
+ assert_less_than_equal(x, 1.0,
+ 'This function should be called in [0, 1.0] range');
+ return cubicBezier(0.5, 1, 0.5, 0)(x);
+ }
+ var keyframeEasingExtrapolated = function(x) {
+ assert_less_than(x, 0.0,
+ 'This function should be called in negative range');
+ // p0x + (p1y - p0y) / (p1x - p0x) * (x - p0x)
+ return (1 / 0.5) * x;
+ }
+ var effectEasing = function(x) {
+ return cubicBezier(0, -0.5, 1, -0.5)(x);
+ }
+
+ anim.pause();
+
+ // The effect-easing produces negative values in (0, 0.766312060)
+ assert_style_left_at(anim, 0, function(x) {
+ return keyframeEasing(effectEasing(x));
+ });
+ assert_style_left_at(anim, 10, function(x) {
+ return keyframeEasingExtrapolated(effectEasing(x));
+ });
+ // Near the extreme point of the effect-easing function
+ assert_style_left_at(anim, 300, function(x) {
+ return keyframeEasingExtrapolated(effectEasing(x));
+ });
+ assert_style_left_at(anim, 750, function(x) {
+ return keyframeEasingExtrapolated(effectEasing(x));
+ });
+ assert_style_left_at(anim, 770, function(x) {
+ return keyframeEasing(effectEasing(x));
+ });
+ assert_style_left_at(anim, 1000, function(x) {
+ return keyframeEasing(effectEasing(x));
+ });
+}, 'effect easing produces negative values with keyframe easing ' +
+ 'producing negative values');
+
+test(function(t) {
+ var target = createDiv(t);
+ target.style.position = 'absolute';
+ var anim = target.animate(
+ // http://cubic-bezier.com/#0,-0.5,1,-0.5
+ [ { left: '0px', easing: 'cubic-bezier(0, -0.5, 1, -0.5)' },
+ { left: '100px' } ],
+ { duration: 1000,
+ fill: 'forwards',
+ easing: 'cubic-bezier(0, -0.5, 1, -0.5)' });
+ var easing = function(x) {
+ assert_greater_than_equal(x, 0.0,
+ 'This function should be called in [0, 1.0] range');
+ assert_less_than_equal(x, 1.0,
+ 'This function should be called in [0, 1.0] range');
+ return cubicBezier(0, -0.5, 1, -0.5)(x);
+ }
+ var easingExtrapolated = function(x) {
+ assert_less_than(x, 0.0,
+ 'This function should be called in negative range');
+ // For cubic-bezier(0, -0.5, 1, -0.5), the tangent at the
+ // endpoint (x = 0.0) is infinity so we should just return 0.0.
+ return 0.0;
+ }
+
+ anim.pause();
+
+ // The effect-easing produces negative values in (0, 0.766312060)
+ assert_style_left_at(anim, 0, function(x) {
+ return easing(easing(x))
+ });
+ assert_style_left_at(anim, 10, function(x) {
+ return easingExtrapolated(easing(x));
+ });
+ // Near the extreme point of the effect-easing function
+ assert_style_left_at(anim, 300, function(x) {
+ return easingExtrapolated(easing(x));
+ });
+ assert_style_left_at(anim, 750, function(x) {
+ return easingExtrapolated(easing(x));
+ });
+ assert_style_left_at(anim, 770, function(x) {
+ return easing(easing(x))
+ });
+ assert_style_left_at(anim, 1000, function(x) {
+ return easing(easing(x))
+ });
+}, 'effect easing which produces negative values and the tangent on ' +
+ 'the lower boundary is infinity with keyframe easing producing ' +
+ 'negative values');
+
+var gStepTimingFunctionTests = [
+ {
+ description: 'Test bounds point of step-start easing',
+ keyframe: [ { width: '0px' },
+ { width: '100px' } ],
+ 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 compositor',
+ keyframe: [ { opacity: 0 },
+ { opacity: 1 } ],
+ 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',
+ keyframe: [ { width: '0px' },
+ { width: '100px' } ],
+ 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',
+ keyframe: [ { width: '0px' },
+ { width: '100px' } ],
+ 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',
+ keyframe: [ { width: '0px' },
+ { width: '100px' } ],
+ 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',
+ keyframe: [ { width: '0px' },
+ { width: '100px' } ],
+ 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',
+ keyframe: [ { width: '0px' },
+ { width: '100px' } ],
+ 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',
+ keyframe: [ { width: '0px' },
+ { width: '100px' } ],
+ 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',
+ keyframe: [ { width: '0px' },
+ { width: '100px' } ],
+ 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-start easing in keyframe',
+ keyframe: [ { width: '0px', easing: 'steps(2, start)' },
+ { width: '100px' } ],
+ effect: {
+ delay: 1000,
+ duration: 1000,
+ fill: 'both',
+ },
+ conditions: [
+ { currentTime: 0, progress: 0, width: '0px' },
+ { currentTime: 999, progress: 0, width: '0px' },
+ { currentTime: 1000, progress: 0, width: '50px' },
+ { currentTime: 1499, progress: 0.499, width: '50px' },
+ { currentTime: 1500, progress: 0.5, width: '100px' },
+ { currentTime: 2000, progress: 1, width: '100px' },
+ { currentTime: 2500, progress: 1, width: '100px' }
+ ]
+ },
+ {
+ description: 'Test bounds point of step-end easing ' +
+ 'with iterationStart and delay',
+ keyframe: [ { width: '0px' },
+ { width: '100px' } ],
+ 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',
+ keyframe: [ { width: '0px' },
+ { width: '100px' } ],
+ 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 },
+ ]
+ }
+];
+
+gStepTimingFunctionTests.forEach(function(options) {
+ test(function(t) {
+ var target = createDiv(t);
+ var animation = target.animate(options.keyframe, options.effect);
+ options.conditions.forEach(function(condition) {
+ animation.currentTime = condition.currentTime;
+ if (typeof condition.progress !== 'undefined') {
+ assert_equals(animation.effect.getComputedTiming().progress,
+ condition.progress,
+ 'Progress at ' + animation.currentTime + 'ms');
+ }
+ if (typeof condition.width !== 'undefined') {
+ assert_equals(getComputedStyle(target).width,
+ condition.width,
+ 'Progress at ' + animation.currentTime + 'ms');
+ }
+ });
+ }, options.description);
+});
+
+gInvalidEasingTests.forEach(function(options) {
+ test(function(t) {
+ var div = createDiv(t);
+ assert_throws({ name: 'TypeError' },
+ function() {
+ div.animate({ easing: options.easing }, 100 * MS_PER_SEC);
+ });
+ }, 'Invalid keyframe easing value: \'' + options.easing + '\'');
+});
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/web-animations/interfaces/KeyframeEffect/getComputedTiming.html b/testing/web-platform/tests/web-animations/interfaces/KeyframeEffect/getComputedTiming.html
new file mode 100644
index 000000000..4d799272b
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/interfaces/KeyframeEffect/getComputedTiming.html
@@ -0,0 +1,212 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title>KeyframeEffectReadOnly getComputedTiming() tests</title>
+<link rel="help" href="https://w3c.github.io/web-animations/#dom-animationeffectreadonly-getcomputedtiming">
+<link rel="author" title="Boris Chiou" href="mailto:boris.chiou@gmail.com">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../../testcommon.js"></script>
+<body>
+<div id="log"></div>
+<div id="target"></div>
+<script>
+"use strict";
+
+var target = document.getElementById("target");
+
+test(function(t) {
+ var effect = new KeyframeEffectReadOnly(target,
+ { left: ["10px", "20px"] });
+
+ var ct = effect.getComputedTiming();
+ assert_equals(ct.delay, 0, "computed delay");
+ assert_equals(ct.fill, "none", "computed fill");
+ assert_equals(ct.iterations, 1.0, "computed iterations");
+ assert_equals(ct.duration, 0, "computed duration");
+ assert_equals(ct.direction, "normal", "computed direction");
+}, "values of getComputedTiming() when a KeyframeEffectReadOnly is " +
+ "constructed without any KeyframeEffectOptions object");
+
+var gGetComputedTimingTests = [
+ { desc: "an empty KeyframeEffectOptions object",
+ input: { },
+ expected: { } },
+ { desc: "a normal KeyframeEffectOptions object",
+ input: { delay: 1000,
+ fill: "auto",
+ iterations: 5.5,
+ duration: "auto",
+ direction: "alternate" },
+ expected: { delay: 1000,
+ fill: "none",
+ iterations: 5.5,
+ duration: 0,
+ direction: "alternate" } },
+ { desc: "a double value",
+ input: 3000,
+ timing: { duration: 3000 },
+ expected: { delay: 0,
+ fill: "none",
+ iterations: 1,
+ duration: 3000,
+ direction: "normal" } },
+ { desc: "+Infinity",
+ input: Infinity,
+ expected: { duration: Infinity } },
+ { desc: "an Infinity duration",
+ input: { duration: Infinity },
+ expected: { duration: Infinity } },
+ { desc: "an auto duration",
+ input: { duration: "auto" },
+ expected: { duration: 0 } },
+ { desc: "an Infinity iterations",
+ input: { iterations: Infinity },
+ expected: { iterations: Infinity } },
+ { desc: "an auto fill",
+ input: { fill: "auto" },
+ expected: { fill: "none" } },
+ { desc: "a forwards fill",
+ input: { fill: "forwards" },
+ expected: { fill: "forwards" } }
+];
+
+gGetComputedTimingTests.forEach(function(stest) {
+ test(function(t) {
+ var effect = new KeyframeEffectReadOnly(target,
+ { left: ["10px", "20px"] },
+ stest.input);
+
+ // Helper function to provide default expected values when the test does
+ // not supply them.
+ var expected = function(field, defaultValue) {
+ return field in stest.expected ? stest.expected[field] : defaultValue;
+ };
+
+ var ct = effect.getComputedTiming();
+ assert_equals(ct.delay, expected("delay", 0),
+ "computed delay");
+ assert_equals(ct.fill, expected("fill", "none"),
+ "computed fill");
+ assert_equals(ct.iterations, expected("iterations", 1),
+ "computed iterations");
+ assert_equals(ct.duration, expected("duration", 0),
+ "computed duration");
+ assert_equals(ct.direction, expected("direction", "normal"),
+ "computed direction");
+
+ }, "values of getComputedTiming() when a KeyframeEffectReadOnly is " +
+ "constructed by " + stest.desc);
+});
+
+var gActiveDurationTests = [
+ { desc: "an empty KeyframeEffectOptions object",
+ input: { },
+ expected: 0 },
+ { desc: "a non-zero duration and default iteration count",
+ input: { duration: 1000 },
+ expected: 1000 },
+ { desc: "a non-zero duration and integral iteration count",
+ input: { duration: 1000, iterations: 7 },
+ expected: 7000 },
+ { desc: "a non-zero duration and fractional iteration count",
+ input: { duration: 1000, iterations: 2.5 },
+ expected: 2500 },
+ { desc: "an non-zero duration and infinite iteration count",
+ input: { duration: 1000, iterations: Infinity },
+ expected: Infinity },
+ { desc: "an non-zero duration and zero iteration count",
+ input: { duration: 1000, iterations: 0 },
+ expected: 0 },
+ { desc: "a zero duration and default iteration count",
+ input: { duration: 0 },
+ expected: 0 },
+ { desc: "a zero duration and fractional iteration count",
+ input: { duration: 0, iterations: 2.5 },
+ expected: 0 },
+ { desc: "a zero duration and infinite iteration count",
+ input: { duration: 0, iterations: Infinity },
+ expected: 0 },
+ { desc: "a zero duration and zero iteration count",
+ input: { duration: 0, iterations: 0 },
+ expected: 0 },
+ { desc: "an infinite duration and default iteration count",
+ input: { duration: Infinity },
+ expected: Infinity },
+ { desc: "an infinite duration and zero iteration count",
+ input: { duration: Infinity, iterations: 0 },
+ expected: 0 },
+ { desc: "an infinite duration and fractional iteration count",
+ input: { duration: Infinity, iterations: 2.5 },
+ expected: Infinity },
+ { desc: "an infinite duration and infinite iteration count",
+ input: { duration: Infinity, iterations: Infinity },
+ expected: Infinity },
+];
+
+gActiveDurationTests.forEach(function(stest) {
+ test(function(t) {
+ var effect = new KeyframeEffectReadOnly(target,
+ { left: ["10px", "20px"] },
+ stest.input);
+
+ assert_equals(effect.getComputedTiming().activeDuration,
+ stest.expected);
+
+ }, "getComputedTiming().activeDuration for " + stest.desc);
+});
+
+var gEndTimeTests = [
+ { desc: "an empty KeyframeEffectOptions object",
+ input: { },
+ expected: 0 },
+ { desc: "a non-zero duration and default iteration count",
+ input: { duration: 1000 },
+ expected: 1000 },
+ { desc: "a non-zero duration and non-default iteration count",
+ input: { duration: 1000, iterations: 2.5 },
+ expected: 2500 },
+ { desc: "a non-zero duration and non-zero delay",
+ input: { duration: 1000, delay: 1500 },
+ expected: 2500 },
+ { desc: "a non-zero duration, non-zero delay and non-default iteration",
+ input: { duration: 1000, delay: 1500, iterations: 2 },
+ expected: 3500 },
+ { desc: "an infinite iteration count",
+ input: { duration: 1000, iterations: Infinity },
+ expected: Infinity },
+ { desc: "an infinite duration",
+ input: { duration: Infinity, iterations: 10 },
+ expected: Infinity },
+ { desc: "an infinite duration and delay",
+ input: { duration: Infinity, iterations: 10, delay: 1000 },
+ expected: Infinity },
+ { desc: "an infinite duration and negative delay",
+ input: { duration: Infinity, iterations: 10, delay: -1000 },
+ expected: Infinity },
+ { desc: "an non-zero duration and negative delay",
+ input: { duration: 1000, iterations: 2, delay: -1000 },
+ expected: 1000 },
+ { desc: "an non-zero duration and negative delay greater than active " +
+ "duration",
+ input: { duration: 1000, iterations: 2, delay: -3000 },
+ expected: 0 },
+ { desc: "a zero duration and negative delay",
+ input: { duration: 0, iterations: 2, delay: -1000 },
+ expected: 0 }
+];
+
+gEndTimeTests.forEach(function(stest) {
+ test(function(t) {
+ var effect = new KeyframeEffectReadOnly(target,
+ { left: ["10px", "20px"] },
+ stest.input);
+
+ assert_equals(effect.getComputedTiming().endTime,
+ stest.expected);
+
+ }, "getComputedTiming().endTime for " + stest.desc);
+});
+
+done();
+</script>
+</body>
diff --git a/testing/web-platform/tests/web-animations/interfaces/KeyframeEffect/iterationComposite.html b/testing/web-platform/tests/web-animations/interfaces/KeyframeEffect/iterationComposite.html
new file mode 100644
index 000000000..743d10887
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/interfaces/KeyframeEffect/iterationComposite.html
@@ -0,0 +1,693 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>KeyframeEffect.iterationComposite tests</title>
+<link rel="help" href="https://w3c.github.io/web-animations/#effect-accumulation-section">
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src="../../testcommon.js"></script>
+<div id="log"></div>
+<script>
+'use strict';
+
+test(function(t) {
+ var div = createDiv(t);
+ var anim =
+ div.animate({ marginLeft: ['0px', '10px'] },
+ { duration: 100 * MS_PER_SEC,
+ easing: 'linear',
+ iterations: 10,
+ iterationComposite: 'accumulate' });
+ anim.pause();
+
+ anim.currentTime = anim.effect.timing.duration / 2;
+ assert_equals(getComputedStyle(div).marginLeft, '5px',
+ 'Animated margin-left style at 50s of the first iteration');
+ anim.currentTime = anim.effect.timing.duration * 2;
+ assert_equals(getComputedStyle(div).marginLeft, '20px',
+ 'Animated margin-left style at 0s of the third iteration');
+ anim.currentTime += anim.effect.timing.duration / 2;
+ assert_equals(getComputedStyle(div).marginLeft, '25px',
+ 'Animated margin-left style at 50s of the third iteration');
+}, 'iterationComposite of <length> type animation');
+
+test(function(t) {
+ var parent = createDiv(t);
+ parent.style.width = '100px';
+ var div = createDiv(t);
+ parent.appendChild(div);
+
+ var anim =
+ div.animate({ width: ['0%', '50%'] },
+ { duration: 100 * MS_PER_SEC,
+ easing: 'linear',
+ iterations: 10,
+ iterationComposite: 'accumulate' });
+ anim.pause();
+
+ anim.currentTime = anim.effect.timing.duration / 2;
+ assert_equals(getComputedStyle(div).width, '25px',
+ 'Animated width style at 50s of the first iteration');
+ anim.currentTime = anim.effect.timing.duration * 2;
+ assert_equals(getComputedStyle(div).width, '100px',
+ 'Animated width style at 0s of the third iteration');
+ anim.currentTime += anim.effect.timing.duration / 2;
+ assert_equals(getComputedStyle(div).width, '125px',
+ 'Animated width style at 50s of the third iteration');
+}, 'iterationComposite of <percentage> type animation');
+
+test(function(t) {
+ var div = createDiv(t);
+ var anim =
+ div.animate({ color: ['rgb(0, 0, 0)', 'rgb(120, 120, 120)'] },
+ { duration: 100 * MS_PER_SEC,
+ easing: 'linear',
+ iterations: 10,
+ iterationComposite: 'accumulate' });
+ anim.pause();
+
+ anim.currentTime = anim.effect.timing.duration / 2;
+ assert_equals(getComputedStyle(div).color, 'rgb(60, 60, 60)',
+ 'Animated color style at 50s of the first iteration');
+ anim.currentTime = anim.effect.timing.duration * 2;
+ assert_equals(getComputedStyle(div).color, 'rgb(240, 240, 240)',
+ 'Animated color style at 0s of the third iteration');
+ anim.currentTime += anim.effect.timing.duration / 2;
+ assert_equals(getComputedStyle(div).color, 'rgb(255, 255, 255)',
+ 'Animated color style at 50s of the third iteration');
+}, 'iterationComposite of <color> type animation');
+
+test(function(t) {
+ var div = createDiv(t);
+ var anim =
+ div.animate({ color: ['rgb(0, 120, 0)', 'rgb(60, 60, 60)'] },
+ { duration: 100 * MS_PER_SEC,
+ easing: 'linear',
+ iterations: 10,
+ iterationComposite: 'accumulate' });
+ anim.pause();
+
+ anim.currentTime = anim.effect.timing.duration / 2;
+ assert_equals(getComputedStyle(div).color, 'rgb(30, 90, 30)',
+ 'Animated color style at 50s of the first iteration');
+ anim.currentTime = anim.effect.timing.duration * 2;
+ assert_equals(getComputedStyle(div).color, 'rgb(120, 240, 120)',
+ 'Animated color style at 0s of the third iteration');
+ anim.currentTime += anim.effect.timing.duration / 2;
+ // The green color is (240 + 180) / 2 = 210
+ assert_equals(getComputedStyle(div).color, 'rgb(150, 210, 150)',
+ 'Animated color style at 50s of the third iteration');
+}, 'iterationComposite of <color> type animation that green component is ' +
+ 'decreasing');
+
+test(function(t) {
+ var div = createDiv(t);
+ var anim =
+ div.animate({ flexGrow: [0, 10] },
+ { duration: 100 * MS_PER_SEC,
+ easing: 'linear',
+ iterations: 10,
+ iterationComposite: 'accumulate' });
+ anim.pause();
+
+ anim.currentTime = anim.effect.timing.duration / 2;
+ assert_equals(getComputedStyle(div).flexGrow, '5',
+ 'Animated flex-grow style at 50s of the first iteration');
+ anim.currentTime = anim.effect.timing.duration * 2;
+ assert_equals(getComputedStyle(div).flexGrow, '20',
+ 'Animated flex-grow style at 0s of the third iteration');
+ anim.currentTime += anim.effect.timing.duration / 2;
+ assert_equals(getComputedStyle(div).flexGrow, '25',
+ 'Animated flex-grow style at 50s of the third iteration');
+}, 'iterationComposite of <number> type animation');
+
+test(function(t) {
+ var div = createDiv(t);
+ div.style.position = 'absolute';
+ var anim =
+ div.animate({ clip: ['rect(0px, 0px, 0px, 0px)',
+ 'rect(10px, 10px, 10px, 10px)'] },
+ { duration: 100 * MS_PER_SEC,
+ easing: 'linear',
+ iterations: 10,
+ iterationComposite: 'accumulate' });
+ anim.pause();
+
+ anim.currentTime = anim.effect.timing.duration / 2;
+ assert_equals(getComputedStyle(div).clip, 'rect(5px, 5px, 5px, 5px)',
+ 'Animated clip style at 50s of the first iteration');
+ anim.currentTime = anim.effect.timing.duration * 2;
+ assert_equals(getComputedStyle(div).clip, 'rect(20px, 20px, 20px, 20px)',
+ 'Animated clip style at 0s of the third iteration');
+ anim.currentTime += anim.effect.timing.duration / 2;
+ assert_equals(getComputedStyle(div).clip, 'rect(25px, 25px, 25px, 25px)',
+ 'Animated clip style at 50s of the third iteration');
+}, 'iterationComposite of <shape> type animation');
+
+test(function(t) {
+ var div = createDiv(t);
+ var anim =
+ div.animate({ width: ['calc(0vw + 0px)', 'calc(0vw + 10px)'] },
+ { duration: 100 * MS_PER_SEC,
+ easing: 'linear',
+ iterations: 10,
+ iterationComposite: 'accumulate' });
+ anim.pause();
+
+ anim.currentTime = anim.effect.timing.duration / 2;
+ assert_equals(getComputedStyle(div).width, '5px',
+ 'Animated calc width style at 50s of the first iteration');
+ anim.currentTime = anim.effect.timing.duration * 2;
+ assert_equals(getComputedStyle(div).width, '20px',
+ 'Animated calc width style at 0s of the third iteration');
+ anim.currentTime += anim.effect.timing.duration / 2;
+ assert_equals(getComputedStyle(div).width, '25px',
+ 'Animated calc width style at 50s of the third iteration');
+}, 'iterationComposite of <calc()> value animation');
+
+test(function(t) {
+ var parent = createDiv(t);
+ parent.style.width = '100px';
+ var div = createDiv(t);
+ parent.appendChild(div);
+
+ var anim =
+ div.animate({ width: ['calc(0% + 0px)', 'calc(10% + 10px)'] },
+ { duration: 100 * MS_PER_SEC,
+ easing: 'linear',
+ iterations: 10,
+ iterationComposite: 'accumulate' });
+ anim.pause();
+
+ anim.currentTime = anim.effect.timing.duration / 2;
+ assert_equals(getComputedStyle(div).width, '10px',
+ // 100px * 5% + 5px
+ 'Animated calc width style at 50s of the first iteration');
+ anim.currentTime = anim.effect.timing.duration * 2;
+ assert_equals(getComputedStyle(div).width,
+ '40px', // 100px * (10% + 10%) + (10px + 10px)
+ 'Animated calc width style at 0s of the third iteration');
+ anim.currentTime += anim.effect.timing.duration / 2;
+ assert_equals(getComputedStyle(div).width,
+ '50px', // (40px + 60px) / 2
+ 'Animated calc width style at 50s of the third iteration');
+}, 'iterationComposite of <calc()> value animation that the values can\'t' +
+ 'be reduced');
+
+test(function(t) {
+ var div = createDiv(t);
+ var anim =
+ div.animate({ opacity: [0, 0.4] },
+ { duration: 100 * MS_PER_SEC,
+ easing: 'linear',
+ iterations: 10,
+ iterationComposite: 'accumulate' });
+ anim.pause();
+
+ anim.currentTime = anim.effect.timing.duration / 2;
+ assert_equals(getComputedStyle(div).opacity, '0.2',
+ 'Animated opacity style at 50s of the first iteration');
+ anim.currentTime = anim.effect.timing.duration * 2;
+ assert_equals(getComputedStyle(div).opacity, '0.8',
+ 'Animated opacity style at 0s of the third iteration');
+ anim.currentTime += anim.effect.timing.duration / 2;
+ assert_equals(getComputedStyle(div).opacity, '1', // (0.8 + 1.2) * 0.5
+ 'Animated opacity style at 50s of the third iteration');
+}, 'iterationComposite of opacity animation');
+
+test(function(t) {
+ var div = createDiv(t);
+ var anim =
+ div.animate({ boxShadow: ['rgb(0, 0, 0) 0px 0px 0px 0px',
+ 'rgb(120, 120, 120) 10px 10px 10px 0px'] },
+ { duration: 100 * MS_PER_SEC,
+ easing: 'linear',
+ iterations: 10,
+ iterationComposite: 'accumulate' });
+ anim.pause();
+
+ anim.currentTime = anim.effect.timing.duration / 2;
+ assert_equals(getComputedStyle(div).boxShadow,
+ 'rgb(60, 60, 60) 5px 5px 5px 0px',
+ 'Animated box-shadow style at 50s of the first iteration');
+ anim.currentTime = anim.effect.timing.duration * 2;
+ assert_equals(getComputedStyle(div).boxShadow,
+ 'rgb(240, 240, 240) 20px 20px 20px 0px',
+ 'Animated box-shadow style at 0s of the third iteration');
+ anim.currentTime += anim.effect.timing.duration / 2;
+ assert_equals(getComputedStyle(div).boxShadow,
+ 'rgb(255, 255, 255) 25px 25px 25px 0px',
+ 'Animated box-shadow style at 50s of the third iteration');
+}, 'iterationComposite of box-shadow animation');
+
+test(function(t) {
+ var div = createDiv(t);
+ var anim =
+ div.animate({ filter: ['blur(0px)', 'blur(10px)'] },
+ { duration: 100 * MS_PER_SEC,
+ easing: 'linear',
+ iterations: 10,
+ iterationComposite: 'accumulate' });
+ anim.pause();
+
+ anim.currentTime = anim.effect.timing.duration / 2;
+ assert_equals(getComputedStyle(div).filter, 'blur(5px)',
+ 'Animated filter blur style at 50s of the first iteration');
+ anim.currentTime = anim.effect.timing.duration * 2;
+ assert_equals(getComputedStyle(div).filter, 'blur(20px)',
+ 'Animated filter blur style at 0s of the third iteration');
+ anim.currentTime += anim.effect.timing.duration / 2;
+ assert_equals(getComputedStyle(div).filter, 'blur(25px)',
+ 'Animated filter blur style at 50s of the third iteration');
+}, 'iterationComposite of filter blur animation');
+
+test(function(t) {
+ var div = createDiv(t);
+ var anim =
+ div.animate({ filter: ['brightness(1)',
+ 'brightness(180%)'] },
+ { duration: 100 * MS_PER_SEC,
+ easing: 'linear',
+ iterations: 10,
+ iterationComposite: 'accumulate' });
+ anim.pause();
+
+ anim.currentTime = anim.effect.timing.duration / 2;
+ assert_equals(getComputedStyle(div).filter,
+ 'brightness(1.4)',
+ 'Animated filter brightness style at 50s of the first iteration');
+ anim.currentTime = anim.effect.timing.duration * 2;
+ assert_equals(getComputedStyle(div).filter,
+ 'brightness(2.6)', // brightness(1) + brightness(0.8) + brightness(0.8)
+ 'Animated filter brightness style at 0s of the third iteration');
+ anim.currentTime += anim.effect.timing.duration / 2;
+ assert_equals(getComputedStyle(div).filter,
+ 'brightness(3)', // (brightness(2.6) + brightness(3.4)) * 0.5
+ 'Animated filter brightness style at 50s of the third iteration');
+}, 'iterationComposite of filter brightness for different unit animation');
+
+test(function(t) {
+ var div = createDiv(t);
+ var anim =
+ div.animate({ filter: ['brightness(0)',
+ 'brightness(1)'] },
+ { duration: 100 * MS_PER_SEC,
+ easing: 'linear',
+ iterations: 10,
+ iterationComposite: 'accumulate' });
+ anim.pause();
+
+ anim.currentTime = anim.effect.timing.duration / 2;
+ assert_equals(getComputedStyle(div).filter,
+ 'brightness(0.5)',
+ 'Animated filter brightness style at 50s of the first iteration');
+ anim.currentTime = anim.effect.timing.duration * 2;
+ assert_equals(getComputedStyle(div).filter,
+ 'brightness(0)', // brightness(1) is an identity element, not accumulated.
+ 'Animated filter brightness style at 0s of the third iteration');
+ anim.currentTime += anim.effect.timing.duration / 2;
+ assert_equals(getComputedStyle(div).filter,
+ 'brightness(0.5)', // brightness(1) is an identity element, not accumulated.
+ 'Animated filter brightness style at 50s of the third iteration');
+}, 'iterationComposite of filter brightness animation');
+
+test(function(t) {
+ var div = createDiv(t);
+ var anim =
+ div.animate({ filter: ['drop-shadow(rgb(0, 0, 0) 0px 0px 0px)',
+ 'drop-shadow(rgb(120, 120, 120) 10px 10px 10px)'] },
+ { duration: 100 * MS_PER_SEC,
+ easing: 'linear',
+ iterations: 10,
+ iterationComposite: 'accumulate' });
+ anim.pause();
+
+ anim.currentTime = anim.effect.timing.duration / 2;
+ assert_equals(getComputedStyle(div).filter,
+ 'drop-shadow(rgb(60, 60, 60) 5px 5px 5px)',
+ 'Animated filter drop-shadow style at 50s of the first iteration');
+ anim.currentTime = anim.effect.timing.duration * 2;
+ assert_equals(getComputedStyle(div).filter,
+ 'drop-shadow(rgb(240, 240, 240) 20px 20px 20px)',
+ 'Animated filter drop-shadow style at 0s of the third iteration');
+ anim.currentTime += anim.effect.timing.duration / 2;
+ assert_equals(getComputedStyle(div).filter,
+ 'drop-shadow(rgb(255, 255, 255) 25px 25px 25px)',
+ 'Animated filter drop-shadow style at 50s of the third iteration');
+}, 'iterationComposite of filter drop-shadow animation');
+
+test(function(t) {
+ var div = createDiv(t);
+ var anim =
+ div.animate({ filter: ['brightness(1) contrast(1)',
+ 'brightness(2) contrast(2)'] },
+ { duration: 100 * MS_PER_SEC,
+ easing: 'linear',
+ iterations: 10,
+ iterationComposite: 'accumulate' });
+ anim.pause();
+
+ anim.currentTime = anim.effect.timing.duration / 2;
+ assert_equals(getComputedStyle(div).filter,
+ 'brightness(1.5) contrast(1.5)',
+ 'Animated filter list at 50s of the first iteration');
+ anim.currentTime = anim.effect.timing.duration * 2;
+ assert_equals(getComputedStyle(div).filter,
+ 'brightness(3) contrast(3)',
+ 'Animated filter list at 0s of the third iteration');
+ anim.currentTime += anim.effect.timing.duration / 2;
+ assert_equals(getComputedStyle(div).filter,
+ 'brightness(3.5) contrast(3.5)',
+ 'Animated filter list at 50s of the third iteration');
+}, 'iterationComposite of same filter list animation');
+
+test(function(t) {
+ var div = createDiv(t);
+ var anim =
+ div.animate({ filter: ['brightness(1) contrast(1)',
+ 'contrast(2) brightness(2)'] },
+ { duration: 100 * MS_PER_SEC,
+ easing: 'linear',
+ iterations: 10,
+ iterationComposite: 'accumulate' });
+ anim.pause();
+
+ anim.currentTime = anim.effect.timing.duration / 2;
+ assert_equals(getComputedStyle(div).filter,
+ 'contrast(2) brightness(2)', // discrete
+ 'Animated filter list at 50s of the first iteration');
+ anim.currentTime = anim.effect.timing.duration * 2;
+ assert_equals(getComputedStyle(div).filter,
+ // We can't accumulate 'contrast(2) brightness(2)' onto
+ // the first list 'brightness(1) contrast(1)' because of
+ // mismatch of the order.
+ 'brightness(1) contrast(1)',
+ 'Animated filter list at 0s of the third iteration');
+ anim.currentTime += anim.effect.timing.duration / 2;
+ assert_equals(getComputedStyle(div).filter,
+ // We *can* accumulate 'contrast(2) brightness(2)' onto
+ // the same list 'contrast(2) brightness(2)' here.
+ 'contrast(4) brightness(4)', // discrete
+ 'Animated filter list at 50s of the third iteration');
+}, 'iterationComposite of discrete filter list because of mismatch ' +
+ 'of the order');
+
+test(function(t) {
+ var div = createDiv(t);
+ var anim =
+ div.animate({ filter: ['sepia(0)',
+ 'sepia(1) contrast(2)'] },
+ { duration: 100 * MS_PER_SEC,
+ easing: 'linear',
+ iterations: 10,
+ iterationComposite: 'accumulate' });
+ anim.pause();
+
+ anim.currentTime = anim.effect.timing.duration / 2;
+ assert_equals(getComputedStyle(div).filter,
+ 'sepia(0.5) contrast(1.5)',
+ 'Animated filter list at 50s of the first iteration');
+ anim.currentTime = anim.effect.timing.duration * 2;
+ assert_equals(getComputedStyle(div).filter,
+ 'sepia(2) contrast(3)',
+ 'Animated filter list at 0s of the third iteration');
+ anim.currentTime += anim.effect.timing.duration / 2;
+ assert_equals(getComputedStyle(div).filter,
+ 'sepia(2.5) contrast(3.5)',
+ 'Animated filter list at 50s of the third iteration');
+}, 'iterationComposite of different length filter list animation');
+
+test(function(t) {
+ var div = createDiv(t);
+ var anim =
+ div.animate({ transform: ['rotate(0deg)', 'rotate(180deg)'] },
+ { duration: 100 * MS_PER_SEC,
+ easing: 'linear',
+ iterations: 10,
+ iterationComposite: 'accumulate' });
+ anim.pause();
+
+ anim.currentTime = anim.effect.timing.duration / 2;
+ assert_equals(getComputedStyle(div).transform,
+ 'matrix(0, 1, -1, 0, 0, 0)', // rotate(90deg)
+ 'Animated transform(rotate) style at 50s of the first iteration');
+ anim.currentTime = anim.effect.timing.duration * 2;
+ assert_equals(getComputedStyle(div).transform,
+ 'matrix(1, 0, 0, 1, 0, 0)', // rotate(360deg)
+ 'Animated transform(rotate) style at 0s of the third iteration');
+ anim.currentTime += anim.effect.timing.duration / 2;
+ assert_equals(getComputedStyle(div).transform,
+ 'matrix(0, 1, -1, 0, 0, 0)', // rotate(450deg)
+ 'Animated transform(rotate) style at 50s of the third iteration');
+}, 'iterationComposite of transform(rotate) animation');
+
+test(function(t) {
+ var div = createDiv(t);
+ var anim =
+ div.animate({ transform: ['scale(0)', 'scale(1)'] },
+ { duration: 100 * MS_PER_SEC,
+ easing: 'linear',
+ iterations: 10,
+ iterationComposite: 'accumulate' });
+ anim.pause();
+
+ anim.currentTime = anim.effect.timing.duration / 2;
+ assert_equals(getComputedStyle(div).transform,
+ 'matrix(0.5, 0, 0, 0.5, 0, 0)', // scale(0.5)
+ 'Animated transform(scale) style at 50s of the first iteration');
+ anim.currentTime = anim.effect.timing.duration * 2;
+ assert_equals(getComputedStyle(div).transform,
+ 'matrix(0, 0, 0, 0, 0, 0)', // scale(0); scale(1) is an identity element,
+ // not accumulated.
+ 'Animated transform(scale) style at 0s of the third iteration');
+ anim.currentTime += anim.effect.timing.duration / 2;
+ assert_equals(getComputedStyle(div).transform,
+ 'matrix(0.5, 0, 0, 0.5, 0, 0)', // scale(0.5); scale(1) an identity
+ // element, not accumulated.
+ 'Animated transform(scale) style at 50s of the third iteration');
+}, 'iterationComposite of transform: [ scale(0), scale(1) ] animation');
+
+test(function(t) {
+ var div = createDiv(t);
+ var anim =
+ div.animate({ transform: ['scale(1)', 'scale(2)'] },
+ { duration: 100 * MS_PER_SEC,
+ easing: 'linear',
+ iterations: 10,
+ iterationComposite: 'accumulate' });
+ anim.pause();
+
+ anim.currentTime = anim.effect.timing.duration / 2;
+ assert_equals(getComputedStyle(div).transform,
+ 'matrix(1.5, 0, 0, 1.5, 0, 0)', // scale(1.5)
+ 'Animated transform(scale) style at 50s of the first iteration');
+ anim.currentTime = anim.effect.timing.duration * 2;
+ assert_equals(getComputedStyle(div).transform,
+ 'matrix(3, 0, 0, 3, 0, 0)', // scale(1 + (2 -1) + (2 -1))
+ 'Animated transform(scale) style at 0s of the third iteration');
+ anim.currentTime += anim.effect.timing.duration / 2;
+ assert_equals(getComputedStyle(div).transform,
+ 'matrix(3.5, 0, 0, 3.5, 0, 0)', // (scale(3) + scale(4)) * 0.5
+ 'Animated transform(scale) style at 50s of the third iteration');
+}, 'iterationComposite of transform: [ scale(1), scale(2) ] animation');
+
+test(function(t) {
+ var div = createDiv(t);
+ var anim =
+ div.animate({ transform: ['scale(0)', 'scale(2)'] },
+ { duration: 100 * MS_PER_SEC,
+ easing: 'linear',
+ iterations: 10,
+ iterationComposite: 'accumulate' });
+ anim.pause();
+
+ anim.currentTime = anim.effect.timing.duration / 2;
+ assert_equals(getComputedStyle(div).transform,
+ 'matrix(1, 0, 0, 1, 0, 0)', // scale(1)
+ 'Animated transform(scale) style at 50s of the first iteration');
+ anim.currentTime = anim.effect.timing.duration * 2;
+ assert_equals(getComputedStyle(div).transform,
+ 'matrix(2, 0, 0, 2, 0, 0)', // (scale(0) + scale(2-1)*2)
+ 'Animated transform(scale) style at 0s of the third iteration');
+ anim.currentTime += anim.effect.timing.duration / 2;
+ assert_equals(getComputedStyle(div).transform,
+ 'matrix(3, 0, 0, 3, 0, 0)', // (scale(2) + scale(4)) * 0.5
+ 'Animated transform(scale) style at 50s of the third iteration');
+}, 'iterationComposite of transform: scale(2) animation');
+
+test(function(t) {
+ var div = createDiv(t);
+ var anim =
+ div.animate({ transform: ['rotate(0deg) translateX(0px)',
+ 'rotate(180deg) translateX(10px)'] },
+ { duration: 100 * MS_PER_SEC,
+ easing: 'linear',
+ iterations: 10,
+ iterationComposite: 'accumulate' });
+ anim.pause();
+
+ anim.currentTime = anim.effect.timing.duration / 2;
+ assert_equals(getComputedStyle(div).transform,
+ 'matrix(0, 1, -1, 0, 0, 5)', // rotate(90deg) translateX(5px)
+ 'Animated transform list at 50s of the first iteration');
+ anim.currentTime = anim.effect.timing.duration * 2;
+ assert_equals(getComputedStyle(div).transform,
+ 'matrix(1, 0, 0, 1, 20, 0)', // rotate(360deg) translateX(20px)
+ 'Animated transform list at 0s of the third iteration');
+ anim.currentTime += anim.effect.timing.duration / 2;
+ assert_equals(getComputedStyle(div).transform,
+ 'matrix(0, 1, -1, 0, 0, 25)', // rotate(450deg) translateX(25px)
+ 'Animated transform list at 50s of the third iteration');
+}, 'iterationComposite of transform list animation');
+
+test(function(t) {
+ var div = createDiv(t);
+ // The transform list whose order is mismatched is compounded,
+ // so below animation is the same as;
+ // from matrix(2, 0, 0, 2, 0, 0) to matrix(3, 0, 0, 3, 30, 0)
+ var anim =
+ div.animate({ transform: ['translateX(0px) scale(2)',
+ 'scale(3) translateX(10px)'] },
+ { duration: 100 * MS_PER_SEC,
+ easing: 'linear',
+ iterations: 10,
+ iterationComposite: 'accumulate' });
+ anim.pause();
+
+ anim.currentTime = anim.effect.timing.duration / 2;
+ assert_equals(getComputedStyle(div).transform,
+ 'matrix(2.5, 0, 0, 2.5, 15, 0)', // scale(2.5) (0px + 30px*2) / 2
+ 'Animated transform list at 50s of the first iteration');
+ anim.currentTime = anim.effect.timing.duration * 2;
+ assert_equals(getComputedStyle(div).transform,
+ 'matrix(4, 0, 0, 4, 60, 0)', // scale(2+(3-2)*2) (0px + 30px*2)
+ 'Animated transform list at 0s of the third iteration');
+ anim.currentTime += anim.effect.timing.duration / 2;
+ assert_equals(getComputedStyle(div).transform,
+ 'matrix(5.5, 0, 0, 5.5, 135, 0)', // scale(4+7)/2 (60px + 210px)
+ 'Animated transform list at 50s of the third iteration');
+}, 'iterationComposite of transform list animation whose order is mismatched');
+
+test(function(t) {
+ var div = createDiv(t);
+ // Even if each transform list does not have functions which exist in
+ // other pair of the list, we don't fill any missing functions at all,
+ // it's just computed as compounded matrices
+ // Below animation is the same as;
+ // from matrix(1, 0, 0, 1, 0, 0) to matrix(2, 0, 0, 2, 20, 0)
+ var anim =
+ div.animate({ transform: ['translateX(0px)',
+ 'scale(2) translateX(10px)'] },
+ { duration: 100 * MS_PER_SEC,
+ easing: 'linear',
+ iterations: 10,
+ iterationComposite: 'accumulate' });
+ anim.pause();
+
+ anim.currentTime = anim.effect.timing.duration / 2;
+ assert_equals(getComputedStyle(div).transform,
+ 'matrix(1.5, 0, 0, 1.5, 10, 0)', // scale(1.5) (0px + 10px*2) / 2
+ 'Animated transform list at 50s of the first iteration');
+ anim.currentTime = anim.effect.timing.duration * 2;
+ assert_equals(getComputedStyle(div).transform,
+ 'matrix(3, 0, 0, 3, 40, 0)', // scale(1+(2-1)*2) (0px + 20px*2)
+ 'Animated transform list at 0s of the third iteration');
+ anim.currentTime += anim.effect.timing.duration / 2;
+ assert_equals(getComputedStyle(div).transform,
+ 'matrix(3.5, 0, 0, 3.5, 80, 0)', // scale(3+4)/2 (40px + 20px)
+ 'Animated transform list at 50s of the third iteration');
+}, 'iterationComposite of transform list animation whose order is mismatched');
+
+test(function(t) {
+ var div = createDiv(t);
+ var anim =
+ div.animate({ marginLeft: ['10px', '20px'] },
+ { duration: 100 * MS_PER_SEC,
+ easing: 'linear',
+ iterations: 10,
+ iterationComposite: 'accumulate' });
+ anim.pause();
+
+ anim.currentTime = anim.effect.timing.duration / 2;
+ assert_equals(getComputedStyle(div).marginLeft, '15px',
+ 'Animated margin-left style at 50s of the first iteration');
+ anim.currentTime = anim.effect.timing.duration * 2;
+ assert_equals(getComputedStyle(div).marginLeft, '50px', // 10px + 20px + 20px
+ 'Animated margin-left style at 0s of the third iteration');
+ anim.currentTime += anim.effect.timing.duration / 2;
+ assert_equals(getComputedStyle(div).marginLeft, '55px', // (50px + 60px) * 0.5
+ 'Animated margin-left style at 50s of the third iteration');
+}, 'iterationComposite starts with non-zero value animation');
+
+test(function(t) {
+ var div = createDiv(t);
+ var anim =
+ div.animate({ marginLeft: ['10px', '-10px'] },
+ { duration: 100 * MS_PER_SEC,
+ easing: 'linear',
+ iterations: 10,
+ iterationComposite: 'accumulate' });
+ anim.pause();
+
+ anim.currentTime = anim.effect.timing.duration / 2;
+ assert_equals(getComputedStyle(div).marginLeft,
+ '0px',
+ 'Animated margin-left style at 50s of the first iteration');
+ anim.currentTime = anim.effect.timing.duration * 2;
+ assert_equals(getComputedStyle(div).marginLeft,
+ '-10px', // 10px + -10px + -10px
+ 'Animated margin-left style at 0s of the third iteration');
+ anim.currentTime += anim.effect.timing.duration / 2;
+ assert_equals(getComputedStyle(div).marginLeft,
+ '-20px', // (-10px + -30px) * 0.5
+ 'Animated margin-left style at 50s of the third iteration');
+}, 'iterationComposite with negative final value animation');
+
+test(function(t) {
+ var div = createDiv(t);
+ var anim = div.animate({ marginLeft: ['0px', '10px'] },
+ { duration: 100 * MS_PER_SEC,
+ easing: 'linear',
+ iterations: 10,
+ iterationComposite: 'accumulate' });
+ anim.pause();
+
+ anim.currentTime =
+ anim.effect.timing.duration * 2 + anim.effect.timing.duration / 2;
+ assert_equals(getComputedStyle(div).marginLeft, '25px',
+ 'Animated style at 50s of the third iteration');
+
+ anim.effect.iterationComposite = 'replace';
+ assert_equals(getComputedStyle(div).marginLeft, '5px',
+ 'Animated style at 50s of the third iteration');
+
+ anim.effect.iterationComposite = 'accumulate';
+ assert_equals(getComputedStyle(div).marginLeft, '25px',
+ 'Animated style at 50s of the third iteration');
+}, 'interationComposite changes');
+
+test(function(t) {
+ var div = createDiv(t);
+ var anim = div.animate({ marginLeft: ['0px', '10px'] },
+ { duration: 100 * MS_PER_SEC,
+ easing: 'linear',
+ iterations: 10,
+ iterationComposite: 'accumulate' });
+ anim.pause();
+
+ anim.currentTime =
+ anim.effect.timing.duration * 2 + anim.effect.timing.duration / 2;
+ assert_equals(getComputedStyle(div).marginLeft, '25px',
+ 'Animated style at 50s of the third iteration');
+
+ // double its duration.
+ anim.effect.timing.duration = anim.effect.timing.duration * 2;
+ assert_equals(getComputedStyle(div).marginLeft, '12.5px',
+ 'Animated style at 25s of the first iteration');
+
+ // half of original.
+ anim.effect.timing.duration = anim.effect.timing.duration / 4;
+ assert_equals(getComputedStyle(div).marginLeft, '50px',
+ 'Animated style at 50s of the fourth iteration');
+}, 'duration changes with iterationComposite(accumulate)');
+
+</script>
diff --git a/testing/web-platform/tests/web-animations/interfaces/KeyframeEffect/processing-a-keyframes-argument.html b/testing/web-platform/tests/web-animations/interfaces/KeyframeEffect/processing-a-keyframes-argument.html
new file mode 100644
index 000000000..d16831281
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/interfaces/KeyframeEffect/processing-a-keyframes-argument.html
@@ -0,0 +1,329 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title>KeyframeEffectReadOnly constructor tests</title>
+<link rel="help" href="https://w3c.github.io/web-animations/#processing-a-keyframes-argument">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../../testcommon.js"></script>
+<script src="../../resources/keyframe-utils.js"></script>
+<body>
+<div id="log"></div>
+<div id="target"></div>
+<script>
+'use strict';
+
+// Test the "process a keyframe-like object" procedure.
+//
+// This file only tests the KeyframeEffectReadOnly constructor since it is
+// assumed that the implementation of the KeyframeEffect constructor,
+// Animatable.animate() method, and KeyframeEffect.setKeyframes() method will
+// all share common machinery and it is not necessary to test each method.
+
+// Test that only animatable properties are accessed
+
+var gNonAnimatableProps = [
+ 'animation', // Shorthands where all the longhand sub-properties are not
+ // animatable, are also not animatable.
+ 'animationDelay',
+ 'animationDirection',
+ 'animationDuration',
+ 'animationFillMode',
+ 'animationIterationCount',
+ 'animationName',
+ 'animationPlayState',
+ 'animationTimingFunction',
+ 'transition',
+ 'transitionDelay',
+ 'transitionDuration',
+ 'transitionProperty',
+ 'transitionTimingFunction',
+ 'display',
+ 'unsupportedProperty',
+];
+
+function TestKeyframe(testProp) {
+ var _propAccessCount = 0;
+
+ Object.defineProperty(this, testProp, {
+ get: function() { _propAccessCount++; },
+ enumerable: true
+ });
+
+ Object.defineProperty(this, 'propAccessCount', {
+ get: function() { return _propAccessCount; }
+ });
+}
+
+function GetTestKeyframeSequence(testProp) {
+ return [ new TestKeyframe(testProp) ]
+}
+
+gNonAnimatableProps.forEach(function(prop) {
+ test(function(t) {
+ var testKeyframe = new TestKeyframe(prop);
+
+ new KeyframeEffectReadOnly(null, testKeyframe);
+
+ assert_equals(testKeyframe.propAccessCount, 0, 'Accessor not called');
+ }, 'non-animatable property \'' + prop + '\' is not accessed when using'
+ + ' a property-indexed keyframe object');
+});
+
+gNonAnimatableProps.forEach(function(prop) {
+ test(function(t) {
+ var testKeyframes = GetTestKeyframeSequence(prop);
+
+ new KeyframeEffectReadOnly(null, testKeyframes);
+
+ assert_equals(testKeyframes[0].propAccessCount, 0, 'Accessor not called');
+ }, 'non-animatable property \'' + prop + '\' is not accessed when using'
+ + ' a keyframe sequence');
+});
+
+// Test equivalent forms of property indexed and sequenced keyframe syntax
+
+function assertEquivalentKeyframeSyntax(keyframesA, keyframesB) {
+ var processedKeyframesA = new KeyframeEffectReadOnly(null, keyframesA).getKeyframes();
+ var processedKeyframesB = new KeyframeEffectReadOnly(null, keyframesB).getKeyframes();
+ assert_frame_lists_equal(processedKeyframesA, processedKeyframesB);
+}
+
+var gEquivalentSyntaxTests = [
+ {
+ description: 'two properties with one value',
+ indexedKeyframes: {
+ left: '100px',
+ opacity: ['1'],
+ },
+ sequencedKeyframes: [
+ {left: '100px', opacity: '1'},
+ ],
+ },
+ {
+ description: 'two properties with three values',
+ indexedKeyframes: {
+ left: ['10px', '100px', '150px'],
+ opacity: ['1', '0', '1'],
+ },
+ sequencedKeyframes: [
+ {left: '10px', opacity: '1'},
+ {left: '100px', opacity: '0'},
+ {left: '150px', opacity: '1'},
+ ],
+ },
+ {
+ description: 'two properties with different numbers of values',
+ indexedKeyframes: {
+ left: ['0px', '100px', '200px'],
+ opacity: ['0', '1']
+ },
+ sequencedKeyframes: [
+ {left: '0px', opacity: '0'},
+ {left: '100px'},
+ {left: '200px', opacity: '1'},
+ ],
+ },
+ {
+ description: 'same offset applied to all keyframes',
+ indexedKeyframes: {
+ left: ['0px', '100px'],
+ offset: 0.5,
+ },
+ sequencedKeyframes: [
+ {left: '0px', offset: 0.5},
+ {left: '100px', offset: 0.5},
+ ],
+ },
+ {
+ description: 'same easing applied to all keyframes',
+ indexedKeyframes: {
+ left: ['10px', '100px', '150px'],
+ opacity: ['1', '0', '1'],
+ easing: 'ease',
+ },
+ sequencedKeyframes: [
+ {left: '10px', opacity: '1', easing: 'ease'},
+ {left: '100px', opacity: '0', easing: 'ease'},
+ {left: '150px', opacity: '1', easing: 'ease'},
+ ],
+ },
+ {
+ description: 'same composite applied to all keyframes',
+ indexedKeyframes: {
+ left: ['0px', '100px'],
+ composite: 'add',
+ },
+ sequencedKeyframes: [
+ {left: '0px', composite: 'add'},
+ {left: '100px', composite: 'add'},
+ ],
+ },
+];
+
+gEquivalentSyntaxTests.forEach(function({description, indexedKeyframes, sequencedKeyframes}) {
+ test(function(t) {
+ assertEquivalentKeyframeSyntax(indexedKeyframes, sequencedKeyframes);
+ }, 'Equivalent property indexed and sequenced keyframes: ' + description);
+});
+
+// Test handling of custom iterable objects.
+
+function createIterable(iterations) {
+ return {
+ [Symbol.iterator]() {
+ var i = 0;
+ return {
+ next() {
+ return iterations[i++];
+ },
+ };
+ },
+ };
+}
+
+test(() => {
+ var effect = new KeyframeEffect(null, createIterable([
+ {done: false, value: {left: '100px'}},
+ {done: false, value: {left: '300px'}},
+ {done: false, value: {left: '200px'}},
+ {done: true},
+ ]));
+ assert_frame_lists_equal(effect.getKeyframes(), [
+ {offset: null, computedOffset: 0, easing: 'linear', left: '100px'},
+ {offset: null, computedOffset: 0.5, easing: 'linear', left: '300px'},
+ {offset: null, computedOffset: 1, easing: 'linear', left: '200px'},
+ ]);
+}, 'Custom iterator with basic keyframes.');
+
+test(() => {
+ var keyframes = createIterable([
+ {done: false, value: {left: '100px'}},
+ {done: false, value: {left: '300px'}},
+ {done: false, value: {left: '200px'}},
+ {done: true},
+ ]);
+ keyframes.easing = 'ease-in-out';
+ keyframes.offset = '0.1';
+ var effect = new KeyframeEffect(null, keyframes);
+ assert_frame_lists_equal(effect.getKeyframes(), [
+ {offset: null, computedOffset: 0, easing: 'linear', left: '100px'},
+ {offset: null, computedOffset: 0.5, easing: 'linear', left: '300px'},
+ {offset: null, computedOffset: 1, easing: 'linear', left: '200px'},
+ ]);
+}, 'easing and offset are ignored on iterable objects.');
+
+test(() => {
+ var effect = new KeyframeEffect(null, createIterable([
+ {done: false, value: {left: '100px', top: '200px'}},
+ {done: false, value: {left: '300px'}},
+ {done: false, value: {left: '200px', top: '100px'}},
+ {done: true},
+ ]));
+ assert_frame_lists_equal(effect.getKeyframes(), [
+ {offset: null, computedOffset: 0, easing: 'linear', left: '100px', top: '200px'},
+ {offset: null, computedOffset: 0.5, easing: 'linear', left: '300px'},
+ {offset: null, computedOffset: 1, easing: 'linear', left: '200px', top: '100px'},
+ ]);
+}, 'Custom iterator with multiple properties specified.');
+
+test(() => {
+ var effect = new KeyframeEffect(null, createIterable([
+ {done: false, value: {left: '100px'}},
+ {done: false, value: {left: '250px', offset: 0.75}},
+ {done: false, value: {left: '200px'}},
+ {done: true},
+ ]));
+ assert_frame_lists_equal(effect.getKeyframes(), [
+ {offset: null, computedOffset: 0, easing: 'linear', left: '100px'},
+ {offset: 0.75, computedOffset: 0.75, easing: 'linear', left: '250px'},
+ {offset: null, computedOffset: 1, easing: 'linear', left: '200px'},
+ ]);
+}, 'Custom iterator with offset specified.');
+
+test(() => {
+ assert_throws({name: 'TypeError'}, function() {
+ new KeyframeEffect(null, createIterable([
+ {done: false, value: {left: '100px'}},
+ {done: false, value: 1234},
+ {done: false, value: {left: '200px'}},
+ {done: true},
+ ]));
+ });
+}, 'Custom iterator with non object keyframe should throw.');
+
+test(() => {
+ var effect = new KeyframeEffect(null, createIterable([
+ {done: false, value: {left: ['100px', '200px']}},
+ {done: true},
+ ]));
+ assert_frame_lists_equal(effect.getKeyframes(), [
+ {offset: null, computedOffset: 1, easing: 'linear', left: '100px,200px'}
+ ]);
+}, 'Custom iterator with value list in keyframe should give bizarre string representation of list.');
+
+test(function(t) {
+ var keyframe = {};
+ Object.defineProperty(keyframe, 'width', {value: '200px'});
+ Object.defineProperty(keyframe, 'height', {
+ value: '100px',
+ enumerable: true});
+ assert_equals(keyframe.width, '200px', 'width of keyframe is readable');
+ assert_equals(keyframe.height, '100px', 'height of keyframe is readable');
+ var anim = createDiv(t).animate([keyframe, {height: '200px'}], 1);
+ assert_frame_lists_equal(anim.effect.getKeyframes(), [
+ {offset: null, computedOffset: 0, easing: 'linear', height: '100px'},
+ {offset: null, computedOffset: 1, easing: 'linear', height: '200px'},
+ ]);
+}, 'Only enumerable properties on keyframes are considered');
+
+test(function(t) {
+ var KeyframeParent = function() { this.width = '100px'; };
+ KeyframeParent.prototype = { height: '100px' };
+ var Keyframe = function() { this.top = '100px'; };
+ Keyframe.prototype = Object.create(KeyframeParent.prototype);
+ Object.defineProperty(Keyframe.prototype, 'left', {
+ value: '100px',
+ enumerable: 'true'});
+ var keyframe = new Keyframe();
+ var anim = createDiv(t).animate([keyframe, {top: '200px'}], 1);
+ assert_frame_lists_equal(anim.effect.getKeyframes(), [
+ {offset: null, computedOffset: 0, easing: 'linear', top: '100px'},
+ {offset: null, computedOffset: 1, easing: 'linear', top: '200px'},
+ ]);
+}, 'Only properties defined directly on keyframes are considered');
+
+test(function(t) {
+ var keyframes = {};
+ Object.defineProperty(keyframes, 'width', ['100px', '200px']);
+ Object.defineProperty(keyframes, 'height', {
+ value: ['100px', '200px'],
+ enumerable: true});
+ var anim = createDiv(t).animate(keyframes, 1);
+ assert_frame_lists_equal(anim.effect.getKeyframes(), [
+ {offset: null, computedOffset: 0, easing: 'linear', height: '100px'},
+ {offset: null, computedOffset: 1, easing: 'linear', height: '200px'},
+ ]);
+}, 'Only enumerable properties on property indexed keyframes are considered');
+
+test(function(t) {
+ var KeyframesParent = function() { this.width = '100px'; };
+ KeyframesParent.prototype = { height: '100px' };
+ var Keyframes = function() { this.top = ['100px', '200px']; };
+ Keyframes.prototype = Object.create(KeyframesParent.prototype);
+ Object.defineProperty(Keyframes.prototype, 'left', {
+ value: ['100px', '200px'],
+ enumerable: 'true'});
+ var keyframes = new Keyframes();
+ var anim = createDiv(t).animate(keyframes, 1);
+ assert_frame_lists_equal(anim.effect.getKeyframes(), [
+ {offset: null, computedOffset: 0, easing: 'linear', top: '100px'},
+ {offset: null, computedOffset: 1, easing: 'linear', top: '200px'},
+ ]);
+}, 'Only properties defined directly on property indexed keyframes are considered');
+
+// FIXME: Test that properties are accessed in ascending order by Unicode
+// codepoint
+// (There is an existing test for this in
+// keyframe-effect/constructor.html that should be moved here.)
+
+</script>
diff --git a/testing/web-platform/tests/web-animations/interfaces/KeyframeEffect/setKeyframes.html b/testing/web-platform/tests/web-animations/interfaces/KeyframeEffect/setKeyframes.html
new file mode 100644
index 000000000..c951411de
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/interfaces/KeyframeEffect/setKeyframes.html
@@ -0,0 +1,50 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title>KeyframeEffect setKeyframes() tests</title>
+<link rel="help" href="https://w3c.github.io/web-animations/#dom-keyframeeffect-setkeyframes">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../../testcommon.js"></script>
+<script src="../../resources/keyframe-utils.js"></script>
+<body>
+<div id="log"></div>
+<div id="target"></div>
+<script>
+'use strict';
+
+var target = document.getElementById('target');
+
+test(function(t) {
+ gEmptyKeyframeListTests.forEach(function(frame) {
+ var effect = new KeyframeEffect(target, {});
+ effect.setKeyframes(frame);
+ assert_frame_lists_equal(effect.getKeyframes(), []);
+ });
+}, 'Keyframes can be replaced with an empty keyframe');
+
+gPropertyIndexedKeyframesTests.forEach(function(subtest) {
+ test(function(t) {
+ var effect = new KeyframeEffect(target, {});
+ effect.setKeyframes(subtest.input);
+ assert_frame_lists_equal(effect.getKeyframes(), subtest.output);
+ }, 'Keyframes can be replaced with ' + subtest.desc);
+});
+
+gKeyframeSequenceTests.forEach(function(subtest) {
+ test(function(t) {
+ var effect = new KeyframeEffect(target, {});
+ effect.setKeyframes(subtest.input);
+ assert_frame_lists_equal(effect.getKeyframes(), subtest.output);
+ }, 'Keyframes can be replaced with ' + subtest.desc);
+});
+
+gInvalidKeyframesTests.forEach(function(subtest) {
+ test(function(t) {
+ var effect = new KeyframeEffect(target, {});
+ assert_throws(subtest.expected, function() {
+ effect.setKeyframes(subtest.input);
+ });
+ }, 'KeyframeEffect constructor throws with ' + subtest.desc);
+});
+</script>
+</body>
diff --git a/testing/web-platform/tests/web-animations/interfaces/KeyframeEffect/setTarget.html b/testing/web-platform/tests/web-animations/interfaces/KeyframeEffect/setTarget.html
new file mode 100644
index 000000000..2b07d3de6
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/interfaces/KeyframeEffect/setTarget.html
@@ -0,0 +1,160 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title>Writable effect.target tests</title>
+<link rel="help"
+ href="https://w3c.github.io/web-animations/#dom-keyframeeffect-target">
+<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";
+
+var gKeyFrames = { 'marginLeft': ['0px', '100px'] };
+
+test(function(t) {
+ var div = createDiv(t);
+ var effect = new KeyframeEffect(null, gKeyFrames, 100 * MS_PER_SEC);
+ effect.target = div;
+
+ var anim = new Animation(effect, document.timeline);
+ anim.play();
+
+ anim.currentTime = 50 * MS_PER_SEC;
+ assert_equals(getComputedStyle(div).marginLeft, '50px',
+ 'Value at 50% progress');
+}, 'Test setting target before constructing the associated animation');
+
+test(function(t) {
+ var div = createDiv(t);
+ div.style.marginLeft = '10px';
+ var effect = new KeyframeEffect(null, gKeyFrames, 100 * MS_PER_SEC);
+ var anim = new Animation(effect, document.timeline);
+ anim.play();
+
+ anim.currentTime = 50 * MS_PER_SEC;
+ assert_equals(getComputedStyle(div).marginLeft, '10px',
+ 'Value at 50% progress before setting new target');
+ effect.target = div;
+ assert_equals(getComputedStyle(div).marginLeft, '50px',
+ 'Value at 50% progress after setting new target');
+}, 'Test setting target from null to a valid target');
+
+test(function(t) {
+ var div = createDiv(t);
+ div.style.marginLeft = '10px';
+ var anim = div.animate(gKeyFrames, 100 * MS_PER_SEC);
+
+ anim.currentTime = 50 * MS_PER_SEC;
+ assert_equals(getComputedStyle(div).marginLeft, '50px',
+ 'Value at 50% progress before clearing the target')
+
+ anim.effect.target = null;
+ assert_equals(getComputedStyle(div).marginLeft, '10px',
+ 'Value after clearing the target')
+}, 'Test setting target from a valid target to null');
+
+test(function(t) {
+ var a = createDiv(t);
+ var b = createDiv(t);
+ a.style.marginLeft = '10px';
+ b.style.marginLeft = '20px';
+ var anim = a.animate(gKeyFrames, 100 * MS_PER_SEC);
+
+ anim.currentTime = 50 * MS_PER_SEC;
+ assert_equals(getComputedStyle(a).marginLeft, '50px',
+ 'Value of 1st element (currently targeted) before ' +
+ 'changing the effect target');
+ assert_equals(getComputedStyle(b).marginLeft, '20px',
+ 'Value of 2nd element (currently not targeted) before ' +
+ 'changing the effect target');
+ anim.effect.target = b;
+ assert_equals(getComputedStyle(a).marginLeft, '10px',
+ 'Value of 1st element (currently not targeted) after ' +
+ 'changing the effect target');
+ assert_equals(getComputedStyle(b).marginLeft, '50px',
+ 'Value of 2nd element (currently targeted) after ' +
+ 'changing the effect target');
+
+ // This makes sure the animation property is changed correctly on new
+ // targeted element.
+ anim.currentTime = 75 * MS_PER_SEC;
+ assert_equals(getComputedStyle(b).marginLeft, '75px',
+ 'Value of 2nd target (currently targeted) after ' +
+ 'changing the animation current time.');
+}, 'Test setting target from a valid target to another target');
+
+test(function(t) {
+ var anim = createDiv(t).animate([ { marginLeft: '0px' },
+ { marginLeft: '-20px' },
+ { marginLeft: '100px' },
+ { marginLeft: '50px' } ],
+ { duration: 100 * MS_PER_SEC,
+ spacing: 'paced(margin-left)' });
+
+ anim.effect.target = null;
+
+ var frames = anim.effect.getKeyframes();
+ var slots = frames.length - 1;
+ assert_equals(frames[0].computedOffset, 0.0, '1st frame offset');
+ assert_equals(frames[1].computedOffset, 1.0 / slots, '2nd frame offset');
+ assert_equals(frames[2].computedOffset, 2.0 / slots, '3rd frame offset');
+ assert_equals(frames[3].computedOffset, 1.0, 'last frame offset');
+}, 'Test falling back to distribute spacing mode after setting null target');
+
+test(function(t) {
+ var effect = new KeyframeEffect(null,
+ [ { marginLeft: '0px' },
+ { marginLeft: '-20px' },
+ { marginLeft: '100px' },
+ { marginLeft: '50px' } ],
+ { duration: 100 * MS_PER_SEC,
+ spacing: 'paced(margin-left)' });
+ var frames = effect.getKeyframes();
+ var slots = frames.length - 1;
+ assert_equals(frames[1].computedOffset, 1.0 / slots, '2nd frame offset');
+ assert_equals(frames[2].computedOffset, 2.0 / slots, '3rd frame offset');
+}, 'Test falling back to distribute spacing mode if there is no context ' +
+ 'element');
+
+test(function(t) {
+ var div1 = createDiv(t);
+ var div2 = createDiv(t);
+ div1.style.marginLeft = '-20px';
+ div2.style.marginLeft = '-50px';
+ var child1 = document.createElement('div');
+ var child2 = document.createElement('div');
+ div1.appendChild(child1);
+ div2.appendChild(child2);
+ // body
+ // / \
+ // div1 div2
+ // (-20px) (-50px)
+ // | |
+ // child1 child2
+ var anim = child1.animate([ { marginLeft: '0px' },
+ { marginLeft: 'inherit' },
+ { marginLeft: '100px' },
+ { marginLeft: '50px' } ],
+ { duration: 100 * MS_PER_SEC,
+ spacing: 'paced(margin-left)' });
+
+ var frames = anim.effect.getKeyframes();
+ var cumDist = [0, 20, 140, 190];
+ assert_equals(frames[1].computedOffset, cumDist[1] / cumDist[3],
+ '2nd frame offset');
+ assert_equals(frames[2].computedOffset, cumDist[2] / cumDist[3],
+ '3rd frame offset');
+
+ anim.effect.target = child2;
+ frames = anim.effect.getKeyframes();
+ cumDist = [0, 50, 200, 250];
+ assert_equals(frames[1].computedOffset, cumDist[1] / cumDist[3],
+ '2nd frame offset after setting a new target');
+ assert_equals(frames[2].computedOffset, cumDist[2] / cumDist[3],
+ '3rd frame offset after setting a new target');
+}, 'Test paced spacing mode after setting a new target');
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/web-animations/interfaces/KeyframeEffect/spacing.html b/testing/web-platform/tests/web-animations/interfaces/KeyframeEffect/spacing.html
new file mode 100644
index 000000000..612a3af8d
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/interfaces/KeyframeEffect/spacing.html
@@ -0,0 +1,60 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title>KeyframeEffect spacing attribute tests</title>
+<link rel="help"
+ href="https://w3c.github.io/web-animations/#dom-keyframeeffect-spacing">
+<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(function(t) {
+ var anim = createDiv(t).animate(null);
+ assert_throws(new TypeError, function() {
+ anim.effect.spacing = '';
+ });
+}, 'Test throwing TypeError if using empty string');
+
+test(function(t) {
+ var anim = createDiv(t).animate(null);
+ assert_throws(new TypeError, function() {
+ anim.effect.spacing = 'dist';
+ });
+}, 'Test throwing TypeError if not using the correct keyword');
+
+test(function(t) {
+ var anim = createDiv(t).animate(null);
+ anim.effect.spacing = 'paced(A)';
+ assert_equals(anim.effect.spacing, 'distribute', 'spacing mode');
+}, 'Test falling back to distribute spacing if using a unrecognized property');
+
+test(function(t) {
+ var anim = createDiv(t).animate(null);
+ anim.effect.spacing = 'paced(--bg-color)';
+ assert_equals(anim.effect.spacing, 'distribute', 'spacing mode');
+}, 'Test falling back to distribute spacing if using CSS variables');
+
+test(function(t) {
+ var anim = createDiv(t).animate(null);
+ anim.effect.spacing = 'paced(animation-duration)';
+ assert_equals(anim.effect.spacing, 'distribute', 'spacing mode');
+}, 'Test falling back to distribute spacing if using a non-animatable ' +
+ 'property');
+
+test(function(t) {
+ var anim = createDiv(t).animate(null);
+ anim.effect.spacing = 'distribute';
+ assert_equals(anim.effect.spacing, 'distribute', 'spacing mode');
+}, 'Test spacing value if setting distribute');
+
+test(function(t) {
+ var anim = createDiv(t).animate(null);
+ anim.effect.spacing = 'paced(transform)';
+ assert_equals(anim.effect.spacing, 'paced(transform)', 'spacing mode');
+}, 'Test spacing value if setting paced');
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/web-animations/interfaces/KeyframeEffectReadOnly/copy-contructor.html b/testing/web-platform/tests/web-animations/interfaces/KeyframeEffectReadOnly/copy-contructor.html
new file mode 100644
index 000000000..287ffe114
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/interfaces/KeyframeEffectReadOnly/copy-contructor.html
@@ -0,0 +1,94 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title>KeyframeEffectReadOnly copy constructor tests</title>
+<link rel="help"
+href="https://w3c.github.io/web-animations/#dom-keyframeeffectreadonly-keyframeeffectreadonly-source">
+<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(function(t) {
+ var effect = new KeyframeEffectReadOnly(createDiv(t), null);
+ var copiedEffect = new KeyframeEffectReadOnly(effect);
+ assert_equals(copiedEffect.target, effect.target, 'same target');
+}, 'Test copied keyframeEffectReadOnly has the same target');
+
+test(function(t) {
+ var effect =
+ new KeyframeEffectReadOnly(null,
+ [ { marginLeft: '0px' },
+ { marginLeft: '-20px', easing: 'ease-in',
+ offset: 0.1 },
+ { marginLeft: '100px', easing: 'ease-out' },
+ { marginLeft: '50px' } ],
+ { spacing: 'paced(margin-left)' });
+
+ var copiedEffect = new KeyframeEffectReadOnly(effect);
+ var KeyframesA = effect.getKeyframes();
+ var KeyframesB = copiedEffect.getKeyframes();
+ assert_equals(KeyframesA.length, KeyframesB.length, 'same keyframes length');
+
+ for (var i = 0; i < KeyframesA.length; ++i) {
+ assert_equals(KeyframesA[i].offset, KeyframesB[i].offset,
+ 'Keyframe ' + i + ' has the same offset');
+ assert_equals(KeyframesA[i].computedOffset, KeyframesB[i].computedOffset,
+ 'keyframe ' + i + ' has the same computedOffset');
+ assert_equals(KeyframesA[i].easing, KeyframesB[i].easing,
+ 'keyframe ' + i + ' has the same easing');
+ assert_equals(KeyframesA[i].composite, KeyframesB[i].composite,
+ 'keyframe ' + i + ' has the same composite');
+
+ assert_true(!!KeyframesA[i].marginLeft,
+ 'original keyframe ' + i + ' has the valid property value');
+ assert_true(!!KeyframesB[i].marginLeft,
+ 'new keyframe ' + i + ' has the valid property value');
+ assert_equals(KeyframesA[i].marginLeft, KeyframesB[i].marginLeft,
+ 'keyframe ' + i + ' has the same property value pair');
+ }
+}, 'Test copied keyframeEffectReadOnly has the same keyframes');
+
+test(function(t) {
+ var effect = new KeyframeEffectReadOnly(null, null,
+ { spacing: 'paced(margin-left)',
+ iterationComposite: 'accumulate' });
+
+ var copiedEffect = new KeyframeEffectReadOnly(effect);
+ assert_equals(copiedEffect.spacing, effect.spacing, 'same spacing');
+ assert_equals(copiedEffect.iterationComposite, effect.iterationComposite,
+ 'same iterationCompositeOperation');
+ assert_equals(copiedEffect.composite, effect.composite,
+ 'same compositeOperation');
+}, 'Test copied keyframeEffectReadOnly has the same keyframeEffectOptions');
+
+test(function(t) {
+ var effect = new KeyframeEffectReadOnly(null, null,
+ { duration: 100 * MS_PER_SEC,
+ delay: -1 * MS_PER_SEC,
+ endDelay: 2 * MS_PER_SEC,
+ fill: 'forwards',
+ iterationStart: 2,
+ iterations: 20,
+ easing: 'ease-out',
+ direction: 'alternate' } );
+
+ var copiedEffect = new KeyframeEffectReadOnly(effect);
+ var timingA = effect.timing;
+ var timingB = copiedEffect.timing;
+ assert_not_equals(timingA, timingB, 'different timing objects');
+ assert_equals(timingA.delay, timingB.delay, 'same delay');
+ assert_equals(timingA.endDelay, timingB.endDelay, 'same endDelay');
+ assert_equals(timingA.fill, timingB.fill, 'same fill');
+ assert_equals(timingA.iterationStart, timingB.iterationStart,
+ 'same iterationStart');
+ assert_equals(timingA.iterations, timingB.iterations, 'same iterations');
+ assert_equals(timingA.duration, timingB.duration, 'same duration');
+ assert_equals(timingA.direction, timingB.direction, 'same direction');
+ assert_equals(timingA.easing, timingB.easing, 'same easing');
+}, 'Test copied keyframeEffectReadOnly has the same timing content');
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/web-animations/interfaces/KeyframeEffectReadOnly/spacing.html b/testing/web-platform/tests/web-animations/interfaces/KeyframeEffectReadOnly/spacing.html
new file mode 100644
index 000000000..c83d1ebcb
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/interfaces/KeyframeEffectReadOnly/spacing.html
@@ -0,0 +1,237 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title>KeyframeEffectReadOnly spacing attribute tests</title>
+<link rel="help"
+href="https://w3c.github.io/web-animations/#dom-keyframeeffectreadonly-spacing">
+<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(function(t) {
+ assert_throws(new TypeError, function() {
+ createDiv(t).animate(null, { spacing: '' });
+ });
+}, 'Test throwing TypeError if using empty string');
+
+test(function(t) {
+ assert_throws(new TypeError, function() {
+ createDiv(t).animate(null, { spacing: 'dist' });
+ });
+}, 'Test throwing TypeError if not using the correct keyword');
+
+test(function(t) {
+ assert_throws(new TypeError, function() {
+ createDiv(t).animate(null, { spacing: ' paced(margin-left)' });
+ });
+}, 'Test throwing TypeError if adding leading spaces');
+
+test(function(t) {
+ assert_throws(new TypeError, function() {
+ createDiv(t).animate(null, { spacing: 'paced(margin-left) ' });
+ });
+}, 'Test throwing TypeError if adding trailing spaces');
+
+test(function(t) {
+ assert_throws(new TypeError, function() {
+ createDiv(t).animate(null, { spacing: 'paced( margin-left)' });
+ });
+}, 'Test throwing TypeError if adding leading spaces before the ' +
+ 'paced property');
+
+test(function(t) {
+ assert_throws(new TypeError, function() {
+ createDiv(t).animate(null, { spacing: 'paced(margin-left )' });
+ });
+}, 'Test throwing TypeError if adding trailing spaces after the ' +
+ 'paced property');
+
+test(function(t) {
+ assert_throws(new TypeError, function() {
+ createDiv(t).animate(null, { spacing: 'paced()' });
+ });
+}, 'Test throwing TypeError if these is no paced property');
+
+test(function(t) {
+ assert_throws(new TypeError, function() {
+ createDiv(t).animate(null, { spacing: 'paced(.margin)' });
+ });
+}, 'Test throwing TypeError if using a non-ident started string');
+
+test(function(t) {
+ assert_throws(new TypeError, function() {
+ createDiv(t).animate(null, { spacing: 'paced(1margin)' });
+ });
+}, 'Test throwing TypeError if using a non-ident started string');
+
+test(function(t) {
+ assert_throws(new TypeError, function() {
+ createDiv(t).animate(null, { spacing: 'paced(\\)' });
+ });
+}, 'Test throwing TypeError if using a non-ident started string with ' +
+ 'an invalid escape');
+
+test(function(t) {
+ assert_throws(new TypeError, function() {
+ createDiv(t).animate(null, { spacing: 'paced(\\\fmargin)' });
+ });
+}, 'Test throwing TypeError if using a non-ident started string with ' +
+ 'an invalid escape (FF)');
+
+test(function(t) {
+ assert_throws(new TypeError, function() {
+ createDiv(t).animate(null, { spacing: 'paced(\\\rmargin)' });
+ });
+}, 'Test throwing TypeError if using a non-ident started string with ' +
+ 'an invalid escape (CR)');
+
+test(function(t) {
+ assert_throws(new TypeError, function() {
+ createDiv(t).animate(null, { spacing: 'paced(\\\nmargin)' });
+ });
+}, 'Test throwing TypeError if using a non-ident started string with ' +
+ 'an invalid escape (LF)');
+
+test(function(t) {
+ assert_throws(new TypeError, function() {
+ createDiv(t).animate(null, { spacing: 'paced(- )' });
+ });
+}, 'Test throwing TypeError if using a non-ident started string with ' +
+ 'a leading minus and an invalid name-start code point');
+
+test(function(t) {
+ assert_throws(new TypeError, function() {
+ createDiv(t).animate(null, { spacing: 'paced(-\\)' });
+ });
+}, 'Test throwing TypeError if using a non-ident started string with ' +
+ 'a leading minus and an invalid escape');
+
+test(function(t) {
+ assert_throws(new TypeError, function() {
+ createDiv(t).animate(null, { spacing: 'paced(-\\\fmargin)' });
+ });
+}, 'Test throwing TypeError if using a non-ident started string with ' +
+ 'a leading minus and an invalid escape (FF)');
+
+test(function(t) {
+ assert_throws(new TypeError, function() {
+ createDiv(t).animate(null, { spacing: 'paced(-\\\rmargin)' });
+ });
+}, 'Test throwing TypeError if using a non-ident started string with ' +
+ 'a leading minus and an invalid escape (CR)');
+
+test(function(t) {
+ assert_throws(new TypeError, function() {
+ createDiv(t).animate(null, { spacing: 'paced(-\\\nmargin)' });
+ });
+}, 'Test throwing TypeError if using a non-ident started string with ' +
+ 'a leading minus and an invalid escape (LF)');
+
+test(function(t) {
+ assert_throws(new TypeError, function() {
+ createDiv(t).animate(null, { spacing: 'paced(--\\)' });
+ });
+}, 'Test throwing TypeError if using a non-ident string with an invalid ' +
+ 'escape');
+
+test(function(t) {
+ assert_throws(new TypeError, function() {
+ createDiv(t).animate(null, { spacing: 'paced(--\\\fmargin)' });
+ });
+}, 'Test throwing TypeError if using a non-ident string with an invalid ' +
+ 'escape (FF)');
+
+test(function(t) {
+ assert_throws(new TypeError, function() {
+ createDiv(t).animate(null, { spacing: 'paced(--\\\rmargin)' });
+ });
+}, 'Test throwing TypeError if using a non-ident string with an invalid ' +
+ 'escape (CR)');
+
+test(function(t) {
+ assert_throws(new TypeError, function() {
+ createDiv(t).animate(null, { spacing: 'paced(--\\\nmargin)' });
+ });
+}, 'Test throwing TypeError if using a non-ident string with an invalid ' +
+ 'escape (LF)');
+
+test(function(t) {
+ assert_throws(new TypeError, function() {
+ createDiv(t).animate(null, { spacing: 'paced(margin.left)' });
+ });
+}, 'Test throwing TypeError if using a non-ident string with an invalid name ' +
+ 'code point');
+
+test(function(t) {
+ var anim = createDiv(t).animate(null, { spacing: 'paced(A)' });
+ assert_equals(anim.effect.spacing, 'distribute', 'spacing mode');
+}, 'Test falling back to distribute spacing if using a unrecognized property');
+
+test(function(t) {
+ var anim = createDiv(t).animate(null, { spacing: 'paced(\\.margin)' });
+ assert_equals(anim.effect.spacing, 'distribute', 'spacing mode');
+}, 'Test falling back to distribute spacing if using a unrecognized property ' +
+ 'which starts with a valid escape (Full stop)');
+
+test(function(t) {
+ var anim = createDiv(t).animate(null, { spacing: 'paced(\\ margin)' });
+ assert_equals(anim.effect.spacing, 'distribute', 'spacing mode');
+}, 'Test falling back to distribute spacing if using a unrecognized property ' +
+ 'which starts with a valid escape (white space)');
+
+test(function(t) {
+ var anim = createDiv(t).animate(null, { spacing: 'paced(_margin)' });
+ assert_equals(anim.effect.spacing, 'distribute', 'spacing mode');
+}, 'Test falling back to distribute spacing if using a unrecognized property ' +
+ 'which starts with a valid escape (low line)');
+
+test(function(t) {
+ var anim = createDiv(t).animate(null, { spacing: 'paced(-_margin)' });
+ assert_equals(anim.effect.spacing, 'distribute', 'spacing mode');
+}, 'Test falling back to distribute spacing if using a unrecognized property ' +
+ 'which starts with a minus and a low line');
+
+test(function(t) {
+ var anim = createDiv(t).animate(null, { spacing: 'paced(-\\.margin)' });
+ assert_equals(anim.effect.spacing, 'distribute', 'spacing mode');
+}, 'Test falling back to distribute spacing if using a unrecognized property ' +
+ 'which starts with a minus and a valid escape');
+
+test(function(t) {
+ var anim = createDiv(t).animate(null, { spacing: 'paced(--bg-color)' });
+ assert_equals(anim.effect.spacing, 'distribute', 'spacing mode');
+}, 'Test falling back to distribute spacing if using CSS variables');
+
+test(function(t) {
+ var anim = createDiv(t).animate(null, { spacing: 'paced(animation)' });
+ assert_equals(anim.effect.spacing, 'distribute', 'spacing mode');
+}, 'Test falling back to distribute spacing if using a non-animatable ' +
+ 'shorthand property');
+
+test(function(t) {
+ var anim = createDiv(t).animate(null,
+ { spacing: 'paced(animation-duration)' });
+ assert_equals(anim.effect.spacing, 'distribute', 'spacing mode');
+}, 'Test falling back to distribute spacing if using a non-animatable ' +
+ 'property');
+
+test(function(t) {
+ var anim = createDiv(t).animate(null);
+ assert_equals(anim.effect.spacing, 'distribute', 'spacing mode');
+}, 'Test default value of spacing');
+
+test(function(t) {
+ var anim = createDiv(t).animate(null, { spacing: 'distribute' });
+ assert_equals(anim.effect.spacing, 'distribute', 'spacing mode');
+}, 'Test spacing value if setting distribute');
+
+test(function(t) {
+ var anim = createDiv(t).animate(null, { spacing: 'paced(margin-left)' });
+ assert_equals(anim.effect.spacing, 'paced(margin-left)', 'spacing mode');
+}, 'Test spacing value if setting paced');
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/web-animations/resources/effect-easing-tests.js b/testing/web-platform/tests/web-animations/resources/effect-easing-tests.js
new file mode 100644
index 000000000..49c4ff5b8
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/resources/effect-easing-tests.js
@@ -0,0 +1,98 @@
+var gEffectEasingTests = [
+ {
+ desc: 'step-start function',
+ easing: 'step-start',
+ easingFunction: stepStart(1),
+ serialization: 'steps(1, start)'
+ },
+ {
+ desc: 'steps(1, start) function',
+ easing: 'steps(1, start)',
+ easingFunction: stepStart(1)
+ },
+ {
+ desc: 'steps(2, start) function',
+ easing: 'steps(2, start)',
+ easingFunction: stepStart(2)
+ },
+ {
+ desc: 'step-end function',
+ easing: 'step-end',
+ easingFunction: stepEnd(1),
+ serialization: 'steps(1)'
+ },
+ {
+ desc: 'steps(1) function',
+ easing: 'steps(1)',
+ easingFunction: stepEnd(1)
+ },
+ {
+ desc: 'steps(1, end) function',
+ easing: 'steps(1, end)',
+ easingFunction: stepEnd(1),
+ serialization: 'steps(1)'
+ },
+ {
+ desc: 'steps(2, end) function',
+ easing: 'steps(2, end)',
+ easingFunction: stepEnd(2),
+ serialization: 'steps(2)'
+ },
+ {
+ desc: 'linear function',
+ easing: 'linear', // cubic-bezier(0, 0, 1.0, 1.0)
+ easingFunction: cubicBezier(0, 0, 1.0, 1.0)
+ },
+ {
+ desc: 'ease function',
+ easing: 'ease', // cubic-bezier(0.25, 0.1, 0.25, 1.0)
+ easingFunction: cubicBezier(0.25, 0.1, 0.25, 1.0)
+ },
+ {
+ desc: 'ease-in function',
+ easing: 'ease-in', // cubic-bezier(0.42, 0, 1.0, 1.0)
+ easingFunction: cubicBezier(0.42, 0, 1.0, 1.0)
+ },
+ {
+ desc: 'ease-in-out function',
+ easing: 'ease-in-out', // cubic-bezier(0.42, 0, 0.58, 1.0)
+ easingFunction: cubicBezier(0.42, 0, 0.58, 1.0)
+ },
+ {
+ desc: 'ease-out function',
+ easing: 'ease-out', // cubic-bezier(0, 0, 0.58, 1.0)
+ easingFunction: cubicBezier(0, 0, 0.58, 1.0)
+ },
+ {
+ desc: 'easing function which produces values greater than 1',
+ easing: 'cubic-bezier(0, 1.5, 1, 1.5)',
+ easingFunction: cubicBezier(0, 1.5, 1, 1.5)
+ }
+];
+
+var gInvalidEasingTests = [
+ {
+ easing: ''
+ },
+ {
+ easing: 'test'
+ },
+ {
+ easing: 'cubic-bezier(1.1, 0, 1, 1)'
+ },
+ {
+ easing: 'cubic-bezier(0, 0, 1.1, 1)'
+ },
+ {
+ easing: 'cubic-bezier(-0.1, 0, 1, 1)'
+ },
+ {
+ easing: 'cubic-bezier(0, 0, -0.1, 1)'
+ },
+ {
+ easing: 'steps(-1, start)'
+ },
+ {
+ easing: 'steps(0.1, start)'
+ },
+];
diff --git a/testing/web-platform/tests/web-animations/resources/keyframe-utils.js b/testing/web-platform/tests/web-animations/resources/keyframe-utils.js
new file mode 100644
index 000000000..626f0bffb
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/resources/keyframe-utils.js
@@ -0,0 +1,563 @@
+"use strict";
+
+// Utility functions and common keyframe test data.
+
+// ------------------------------
+// Helper functions
+// ------------------------------
+
+/**
+ * Test equality between two lists of computed keyframes
+ * @param {Array.<ComputedKeyframe>} a - actual computed keyframes
+ * @param {Array.<ComputedKeyframe>} b - expected computed keyframes
+ */
+function assert_frame_lists_equal(a, b) {
+ assert_equals(a.length, b.length, "number of frames");
+ for (var i = 0; i < Math.min(a.length, b.length); i++) {
+ assert_frames_equal(a[i], b[i], "ComputedKeyframe #" + i);
+ }
+}
+
+/** Helper */
+function assert_frames_equal(a, b, name) {
+ assert_equals(Object.keys(a).sort().toString(),
+ Object.keys(b).sort().toString(),
+ "properties on " + name);
+ for (var p in a) {
+ assert_equals(a[p], b[p], "value for '" + p + "' on " + name);
+ }
+}
+
+// ------------------------------
+// Easing values
+// ------------------------------
+
+// [specified easing value, expected easing value]
+var gEasingValueTests = [
+ ["linear", "linear"],
+ ["ease-in-out", "ease-in-out"],
+ ["Ease\\2d in-out", "ease-in-out"],
+ ["ease /**/", "ease"],
+];
+
+var gInvalidEasingInKeyframeSequenceTests = [
+ { desc: "a blank easing",
+ input: [{ easing: "" }] },
+ { desc: "an unrecognized easing",
+ input: [{ easing: "unrecognized" }] },
+ { desc: "an 'initial' easing",
+ input: [{ easing: "initial" }] },
+ { desc: "an 'inherit' easing",
+ input: [{ easing: "inherit" }] },
+ { desc: "a variable easing",
+ input: [{ easing: "var(--x)" }] },
+ { desc: "a multi-value easing",
+ input: [{ easing: "ease-in-out, ease-out" }] }
+];
+
+// ------------------------------
+// Composite values
+// ------------------------------
+
+var gGoodKeyframeCompositeValueTests = [
+ "replace", "add", "accumulate", undefined
+];
+
+var gGoodOptionsCompositeValueTests = [
+ "replace", "add", "accumulate"
+];
+
+var gBadCompositeValueTests = [
+ "unrecognised", "replace ", "Replace", null
+];
+
+// ------------------------------
+// Keyframes
+// ------------------------------
+
+var gEmptyKeyframeListTests = [
+ [],
+ null,
+ undefined,
+];
+
+var gPropertyIndexedKeyframesTests = [
+ { desc: "a one property two value property-indexed keyframes specification",
+ input: { left: ["10px", "20px"] },
+ output: [{ offset: null, computedOffset: 0, easing: "linear",
+ left: "10px" },
+ { offset: null, computedOffset: 1, easing: "linear",
+ left: "20px" }] },
+ { desc: "a one shorthand property two value property-indexed keyframes"
+ + " specification",
+ input: { margin: ["10px", "10px 20px 30px 40px"] },
+ output: [{ offset: null, computedOffset: 0, easing: "linear",
+ margin: "10px" },
+ { offset: null, computedOffset: 1, easing: "linear",
+ margin: "10px 20px 30px 40px" }] },
+ { desc: "a two property (one shorthand and one of its longhand components)"
+ + " two value property-indexed keyframes specification",
+ input: { marginTop: ["50px", "60px"],
+ margin: ["10px", "10px 20px 30px 40px"] },
+ output: [{ offset: null, computedOffset: 0, easing: "linear",
+ marginTop: "50px", margin: "10px" },
+ { offset: null, computedOffset: 1, easing: "linear",
+ marginTop: "60px", margin: "10px 20px 30px 40px" }] },
+ { desc: "a two property two value property-indexed keyframes specification",
+ input: { left: ["10px", "20px"],
+ top: ["30px", "40px"] },
+ output: [{ offset: null, computedOffset: 0, easing: "linear",
+ left: "10px", top: "30px" },
+ { offset: null, computedOffset: 1, easing: "linear",
+ left: "20px", top: "40px" }] },
+ { desc: "a two property property-indexed keyframes specification with"
+ + " different numbers of values",
+ input: { left: ["10px", "20px", "30px"],
+ top: ["40px", "50px"] },
+ output: [{ offset: null, computedOffset: 0.0, easing: "linear",
+ left: "10px", top: "40px" },
+ { offset: null, computedOffset: 0.5, easing: "linear",
+ left: "20px" },
+ { offset: null, computedOffset: 1.0, easing: "linear",
+ left: "30px", top: "50px" }] },
+ { desc: "a property-indexed keyframes specification with an invalid value",
+ input: { left: ["10px", "20px", "30px", "40px", "50px"],
+ top: ["15px", "25px", "invalid", "45px", "55px"] },
+ output: [{ offset: null, computedOffset: 0.00, easing: "linear",
+ left: "10px", top: "15px" },
+ { offset: null, computedOffset: 0.25, easing: "linear",
+ left: "20px", top: "25px" },
+ { offset: null, computedOffset: 0.50, easing: "linear",
+ left: "30px", top: "invalid" },
+ { offset: null, computedOffset: 0.75, easing: "linear",
+ left: "40px", top: "45px" },
+ { offset: null, computedOffset: 1.00, easing: "linear",
+ left: "50px", top: "55px" }] },
+ { desc: "a one property two value property-indexed keyframes specification"
+ + " that needs to stringify its values",
+ input: { opacity: [0, 1] },
+ output: [{ offset: null, computedOffset: 0, easing: "linear",
+ opacity: "0" },
+ { offset: null, computedOffset: 1, easing: "linear",
+ opacity: "1" }] },
+ { desc: "a property-indexed keyframes specification with a CSS variable"
+ + " reference",
+ input: { left: [ "var(--dist)", "calc(var(--dist) + 100px)" ] },
+ output: [{ offset: null, computedOffset: 0.0, easing: "linear",
+ left: "var(--dist)" },
+ { offset: null, computedOffset: 1.0, easing: "linear",
+ left: "calc(var(--dist) + 100px)" }] },
+ { desc: "a property-indexed keyframes specification with a CSS variable"
+ + " reference in a shorthand property",
+ input: { margin: [ "var(--dist)", "calc(var(--dist) + 100px)" ] },
+ output: [{ offset: null, computedOffset: 0.0, easing: "linear",
+ margin: "var(--dist)" },
+ { offset: null, computedOffset: 1.0, easing: "linear",
+ margin: "calc(var(--dist) + 100px)" }] },
+ { desc: "a one property one value property-indexed keyframes specification",
+ input: { left: ["10px"] },
+ output: [{ offset: null, computedOffset: 1, easing: "linear",
+ left: "10px" }] },
+ { desc: "a one property one non-array value property-indexed keyframes"
+ + " specification",
+ input: { left: "10px" },
+ output: [{ offset: null, computedOffset: 1, easing: "linear",
+ left: "10px" }] },
+ { desc: "a one property two value property-indexed keyframes specification"
+ + " where the first value is invalid",
+ input: { left: ["invalid", "10px"] },
+ output: [{ offset: null, computedOffset: 0, easing: "linear",
+ left: "invalid" },
+ { offset: null, computedOffset: 1, easing: "linear",
+ left: "10px" }] },
+ { desc: "a one property two value property-indexed keyframes specification"
+ + " where the second value is invalid",
+ input: { left: ["10px", "invalid"] },
+ output: [{ offset: null, computedOffset: 0, easing: "linear",
+ left: "10px" },
+ { offset: null, computedOffset: 1, easing: "linear",
+ left: "invalid" }] },
+];
+
+var gKeyframeSequenceTests = [
+ { desc: "a one property one keyframe sequence",
+ input: [{ offset: 1, left: "10px" }],
+ output: [{ offset: null, computedOffset: 1, easing: "linear",
+ left: "10px" }] },
+ { desc: "a one property two keyframe sequence",
+ input: [{ offset: 0, left: "10px" },
+ { offset: 1, left: "20px" }],
+ output: [{ offset: 0, computedOffset: 0, easing: "linear", left: "10px" },
+ { offset: 1, computedOffset: 1, easing: "linear", left: "20px" }]
+ },
+ { desc: "a two property two keyframe sequence",
+ input: [{ offset: 0, left: "10px", top: "30px" },
+ { offset: 1, left: "20px", top: "40px" }],
+ output: [{ offset: 0, computedOffset: 0, easing: "linear",
+ left: "10px", top: "30px" },
+ { offset: 1, computedOffset: 1, easing: "linear",
+ left: "20px", top: "40px" }] },
+ { desc: "a one shorthand property two keyframe sequence",
+ input: [{ offset: 0, margin: "10px" },
+ { offset: 1, margin: "20px 30px 40px 50px" }],
+ output: [{ offset: 0, computedOffset: 0, easing: "linear",
+ margin: "10px" },
+ { offset: 1, computedOffset: 1, easing: "linear",
+ margin: "20px 30px 40px 50px" }] },
+ { desc: "a two property (a shorthand and one of its component longhands)"
+ + " two keyframe sequence",
+ input: [{ offset: 0, margin: "10px", marginTop: "20px" },
+ { offset: 1, marginTop: "70px", margin: "30px 40px 50px 60px" }],
+ output: [{ offset: 0, computedOffset: 0, easing: "linear",
+ margin: "10px", marginTop: "20px" },
+ { offset: 1, computedOffset: 1, easing: "linear",
+ marginTop: "70px", margin: "30px 40px 50px 60px" }] },
+ { desc: "a keyframe sequence with duplicate values for a given interior"
+ + " offset",
+ input: [{ offset: 0.0, left: "10px" },
+ { offset: 0.5, left: "20px" },
+ { offset: 0.5, left: "30px" },
+ { offset: 0.5, left: "40px" },
+ { offset: 1.0, left: "50px" }],
+ output: [{ offset: 0.0, computedOffset: 0.0, easing: "linear",
+ left: "10px" },
+ { offset: 0.5, computedOffset: 0.5, easing: "linear",
+ left: "20px" },
+ { offset: 0.5, computedOffset: 0.5, easing: "linear",
+ left: "30px" },
+ { offset: 0.5, computedOffset: 0.5, easing: "linear",
+ left: "40px" },
+ { offset: 1.0, computedOffset: 1.0, easing: "linear",
+ left: "50px" }] },
+ { desc: "a keyframe sequence with duplicate values for offsets 0 and 1",
+ input: [{ offset: 0, left: "10px" },
+ { offset: 0, left: "20px" },
+ { offset: 0, left: "30px" },
+ { offset: 1, left: "40px" },
+ { offset: 1, left: "50px" },
+ { offset: 1, left: "60px" }],
+ output: [{ offset: 0, computedOffset: 0, easing: "linear", left: "10px" },
+ { offset: 0, computedOffset: 0, easing: "linear", left: "20px" },
+ { offset: 0, computedOffset: 0, easing: "linear", left: "30px" },
+ { offset: 1, computedOffset: 1, easing: "linear", left: "40px" },
+ { offset: 1, computedOffset: 1, easing: "linear", left: "50px" },
+ { offset: 1, computedOffset: 1, easing: "linear", left: "60px" }]
+ },
+ { desc: "a two property four keyframe sequence",
+ input: [{ offset: 0, left: "10px" },
+ { offset: 0, top: "20px" },
+ { offset: 1, top: "30px" },
+ { offset: 1, left: "40px" }],
+ output: [{ offset: 0, computedOffset: 0, easing: "linear", left: "10px" },
+ { offset: 0, computedOffset: 0, easing: "linear", top: "20px" },
+ { offset: 1, computedOffset: 1, easing: "linear", top: "30px" },
+ { offset: 1, computedOffset: 1, easing: "linear", left: "40px" }]
+ },
+ { desc: "a single keyframe sequence with omitted offsets",
+ input: [{ left: "10px" }],
+ output: [{ offset: null, computedOffset: 1, easing: "linear",
+ left: "10px" }] },
+ { desc: "a single keyframe sequence with string offset",
+ input: [{ offset: '0.5', left: "10px" }],
+ output: [{ offset: 0.5, computedOffset: 1, easing: "linear",
+ left: "10px" }] },
+ { desc: "a one property keyframe sequence with some omitted offsets",
+ input: [{ offset: 0.00, left: "10px" },
+ { offset: 0.25, left: "20px" },
+ { left: "30px" },
+ { left: "40px" },
+ { offset: 1.00, left: "50px" }],
+ output: [{ offset: 0.00, computedOffset: 0.00, easing: "linear",
+ left: "10px" },
+ { offset: 0.25, computedOffset: 0.25, easing: "linear",
+ left: "20px" },
+ { offset: null, computedOffset: 0.50, easing: "linear",
+ left: "30px" },
+ { offset: null, computedOffset: 0.75, easing: "linear",
+ left: "40px" },
+ { offset: 1.00, computedOffset: 1.00, easing: "linear",
+ left: "50px" }] },
+ { desc: "a two property keyframe sequence with some omitted offsets",
+ input: [{ offset: 0.00, left: "10px", top: "20px" },
+ { offset: 0.25, left: "30px" },
+ { left: "40px" },
+ { left: "50px", top: "60px" },
+ { offset: 1.00, left: "70px", top: "80px" }],
+ output: [{ offset: 0.00, computedOffset: 0.00, easing: "linear",
+ left: "10px", top: "20px" },
+ { offset: 0.25, computedOffset: 0.25, easing: "linear",
+ left: "30px" },
+ { offset: null, computedOffset: 0.50, easing: "linear",
+ left: "40px" },
+ { offset: null, computedOffset: 0.75, easing: "linear",
+ left: "50px", top: "60px" },
+ { offset: 1.00, computedOffset: 1.00, easing: "linear",
+ left: "70px", top: "80px" }] },
+ { desc: "a one property keyframe sequence with all omitted offsets",
+ input: [{ left: "10px" },
+ { left: "20px" },
+ { left: "30px" },
+ { left: "40px" },
+ { left: "50px" }],
+ output: [{ offset: null, computedOffset: 0.00, easing: "linear",
+ left: "10px" },
+ { offset: null, computedOffset: 0.25, easing: "linear",
+ left: "20px" },
+ { offset: null, computedOffset: 0.50, easing: "linear",
+ left: "30px" },
+ { offset: null, computedOffset: 0.75, easing: "linear",
+ left: "40px" },
+ { offset: null, computedOffset: 1.00, easing: "linear",
+ left: "50px" }] },
+ { desc: "a keyframe sequence with different easing values, but the same"
+ + " easing value for a given offset",
+ input: [{ offset: 0.0, easing: "ease", left: "10px"},
+ { offset: 0.0, easing: "ease", top: "20px"},
+ { offset: 0.5, easing: "linear", left: "30px" },
+ { offset: 0.5, easing: "linear", top: "40px" },
+ { offset: 1.0, easing: "step-end", left: "50px" },
+ { offset: 1.0, easing: "step-end", top: "60px" }],
+ output: [{ offset: 0.0, computedOffset: 0.0, easing: "ease",
+ left: "10px" },
+ { offset: 0.0, computedOffset: 0.0, easing: "ease",
+ top: "20px" },
+ { offset: 0.5, computedOffset: 0.5, easing: "linear",
+ left: "30px" },
+ { offset: 0.5, computedOffset: 0.5, easing: "linear",
+ top: "40px" },
+ { offset: 1.0, computedOffset: 1.0, easing: "steps(1)",
+ left: "50px" },
+ { offset: 1.0, computedOffset: 1.0, easing: "steps(1)",
+ top: "60px" }] },
+ { desc: "a keyframe sequence with different composite values, but the"
+ + " same composite value for a given offset",
+ input: [{ offset: 0.0, composite: "replace", left: "10px" },
+ { offset: 0.0, composite: "replace", top: "20px" },
+ { offset: 0.5, composite: "add", left: "30px" },
+ { offset: 0.5, composite: "add", top: "40px" },
+ { offset: 1.0, composite: "replace", left: "50px" },
+ { offset: 1.0, composite: "replace", top: "60px" }],
+ output: [{ offset: 0.0, computedOffset: 0.0, easing: "linear",
+ composite: "replace", left: "10px" },
+ { offset: 0.0, computedOffset: 0.0, easing: "linear",
+ composite: "replace", top: "20px" },
+ { offset: 0.5, computedOffset: 0.0, easing: "linear",
+ composite: "add", left: "30px" },
+ { offset: 0.5, computedOffset: 0.0, easing: "linear",
+ composite: "add", top: "40px" },
+ { offset: 1.0, computedOffset: 1.0, easing: "linear",
+ composite: "replace", left: "50px" },
+ { offset: 1.0, computedOffset: 1.0, easing: "linear",
+ composite: "replace", top: "60px" }] },
+ { desc: "a one property two keyframe sequence that needs to stringify"
+ + " its values",
+ input: [{ offset: 0, opacity: 0 },
+ { offset: 1, opacity: 1 }],
+ output: [{ offset: 0, computedOffset: 0, easing: "linear", opacity: "0" },
+ { offset: 1, computedOffset: 1, easing: "linear", opacity: "1" }]
+ },
+ { desc: "a keyframe sequence with a CSS variable reference",
+ input: [{ left: "var(--dist)" },
+ { left: "calc(var(--dist) + 100px)" }],
+ output: [{ offset: null, computedOffset: 0.0, easing: "linear",
+ left: "var(--dist)" },
+ { offset: null, computedOffset: 1.0, easing: "linear",
+ left: "calc(var(--dist) + 100px)" }] },
+ { desc: "a keyframe sequence with a CSS variable reference in a shorthand"
+ + " property",
+ input: [{ margin: "var(--dist)" },
+ { margin: "calc(var(--dist) + 100px)" }],
+ output: [{ offset: null, computedOffset: 0.0, easing: "linear",
+ margin: "var(--dist)" },
+ { offset: null, computedOffset: 1.0, easing: "linear",
+ margin: "calc(var(--dist) + 100px)" }] },
+ { desc: "a keyframe sequence where shorthand precedes longhand",
+ input: [{ offset: 0, margin: "10px", marginRight: "20px" },
+ { offset: 1, margin: "30px" }],
+ output: [{ offset: 0, computedOffset: 0, easing: "linear",
+ margin: "10px", marginRight: "20px" },
+ { offset: 1, computedOffset: 1, easing: "linear",
+ margin: "30px" }] },
+ { desc: "a keyframe sequence where longhand precedes shorthand",
+ input: [{ offset: 0, marginRight: "20px", margin: "10px" },
+ { offset: 1, margin: "30px" }],
+ output: [{ offset: 0, computedOffset: 0, easing: "linear",
+ marginRight: "20px", margin: "10px" },
+ { offset: 1, computedOffset: 1, easing: "linear",
+ margin: "30px" }] },
+ { desc: "a keyframe sequence where lesser shorthand precedes greater"
+ + " shorthand",
+ input: [{ offset: 0,
+ borderLeft: "1px solid rgb(1, 2, 3)",
+ border: "2px dotted rgb(4, 5, 6)" },
+ { offset: 1, border: "3px dashed rgb(7, 8, 9)" }],
+ output: [{ offset: 0, computedOffset: 0, easing: "linear",
+ borderLeft: "1px solid rgb(1, 2, 3)",
+ border: "2px dotted rgb(4, 5, 6)" },
+ { offset: 1, computedOffset: 1, easing: "linear",
+ border: "3px dashed rgb(7, 8, 9)" }] },
+ { desc: "a keyframe sequence where greater shorthand precedes lesser"
+ + " shorthand",
+ input: [{ offset: 0, border: "2px dotted rgb(4, 5, 6)",
+ borderLeft: "1px solid rgb(1, 2, 3)" },
+ { offset: 1, border: "3px dashed rgb(7, 8, 9)" }],
+ output: [{ offset: 0, computedOffset: 0, easing: "linear",
+ border: "2px dotted rgb(4, 5, 6)",
+ borderLeft: "1px solid rgb(1, 2, 3)" },
+ { offset: 1, computedOffset: 1, easing: "linear",
+ border: "3px dashed rgb(7, 8, 9)" }] },
+ { desc: "a two property keyframe sequence where one property is missing"
+ + " from the first keyframe",
+ input: [{ offset: 0, left: "10px" },
+ { offset: 1, left: "20px", top: "30px" }],
+ output: [{ offset: 0, computedOffset: 0, easing: "linear", left: "10px" },
+ { offset: 1, computedOffset: 1, easing: "linear",
+ left: "20px", top: "30px" }] },
+ { desc: "a two property keyframe sequence where one property is missing"
+ + " from the last keyframe",
+ input: [{ offset: 0, left: "10px", top: "20px" },
+ { offset: 1, left: "30px" }],
+ output: [{ offset: 0, computedOffset: 0, easing: "linear",
+ left: "10px" , top: "20px" },
+ { offset: 1, computedOffset: 1, easing: "linear",
+ left: "30px" }] },
+ { desc: "a keyframe sequence with repeated values at offset 1 with"
+ + " different easings",
+ input: [{ offset: 0.0, left: "100px", easing: "ease" },
+ { offset: 0.0, left: "200px", easing: "ease" },
+ { offset: 0.5, left: "300px", easing: "linear" },
+ { offset: 1.0, left: "400px", easing: "ease-out" },
+ { offset: 1.0, left: "500px", easing: "step-end" }],
+ output: [{ offset: 0.0, computedOffset: 0.0, easing: "ease",
+ left: "100px" },
+ { offset: 0.0, computedOffset: 0.0, easing: "ease",
+ left: "200px" },
+ { offset: 0.5, computedOffset: 0.5, easing: "linear",
+ left: "300px" },
+ { offset: 1.0, computedOffset: 1.0, easing: "ease-out",
+ left: "400px" },
+ { offset: 1.0, computedOffset: 1.0, easing: "steps(1)",
+ left: "500px" }] },
+];
+
+var gInvalidKeyframesTests = [
+ { desc: "keyframes with an out-of-bounded positive offset",
+ input: [ { opacity: 0 },
+ { opacity: 0.5, offset: 2 },
+ { opacity: 1 } ],
+ expected: { name: "TypeError" } },
+ { desc: "keyframes with an out-of-bounded negative offset",
+ input: [ { opacity: 0 },
+ { opacity: 0.5, offset: -1 },
+ { opacity: 1 } ],
+ expected: { name: "TypeError" } },
+ { desc: "keyframes not loosely sorted by offset",
+ input: [ { opacity: 0, offset: 1 },
+ { opacity: 1, offset: 0 } ],
+ expected: { name: "TypeError" } },
+ { desc: "property-indexed keyframes with an invalid easing value",
+ input: { opacity: [ 0, 0.5, 1 ],
+ easing: "inherit" },
+ expected: { name: "TypeError" } },
+ { desc: "a keyframe sequence with an invalid easing value",
+ input: [ { opacity: 0, easing: "jumpy" },
+ { opacity: 1 } ],
+ expected: { name: "TypeError" } },
+ { desc: "keyframes with an invalid composite value",
+ input: [ { opacity: 0, composite: "alternate" },
+ { opacity: 1 } ],
+ expected: { name: "TypeError" } }
+];
+
+// ------------------------------
+// KeyframeEffectOptions
+// ------------------------------
+
+var gKeyframeEffectOptionTests = [
+ { desc: "an empty KeyframeEffectOptions object",
+ input: { },
+ expected: { } },
+ { desc: "a normal KeyframeEffectOptions object",
+ input: { delay: 1000,
+ fill: "auto",
+ iterations: 5.5,
+ duration: "auto",
+ direction: "alternate" },
+ expected: { delay: 1000,
+ fill: "auto",
+ iterations: 5.5,
+ duration: "auto",
+ direction: "alternate" } },
+ { desc: "a double value",
+ input: 3000,
+ expected: { duration: 3000 } },
+ { desc: "+Infinity",
+ input: Infinity,
+ expected: { duration: Infinity } },
+ { desc: "an Infinity duration",
+ input: { duration: Infinity },
+ expected: { duration: Infinity } },
+ { desc: "an auto duration",
+ input: { duration: "auto" },
+ expected: { duration: "auto" } },
+ { desc: "an Infinity iterations",
+ input: { iterations: Infinity },
+ expected: { iterations: Infinity } },
+ { desc: "an auto fill",
+ input: { fill: "auto" },
+ expected: { fill: "auto" } },
+ { desc: "a forwards fill",
+ input: { fill: "forwards" },
+ expected: { fill: "forwards" } }
+];
+
+var gInvalidKeyframeEffectOptionTests = [
+ { desc: "-Infinity",
+ input: -Infinity,
+ expected: { name: "TypeError" } },
+ { desc: "NaN",
+ input: NaN,
+ expected: { name: "TypeError" } },
+ { desc: "a negative value",
+ input: -1,
+ expected: { name: "TypeError" } },
+ { desc: "a negative Infinity duration",
+ input: { duration: -Infinity },
+ expected: { name: "TypeError" } },
+ { desc: "a NaN duration",
+ input: { duration: NaN },
+ expected: { name: "TypeError" } },
+ { desc: "a negative duration",
+ input: { duration: -1 },
+ expected: { name: "TypeError" } },
+ { desc: "a string duration",
+ input: { duration: "merrychristmas" },
+ expected: { name: "TypeError" } },
+ { desc: "a negative Infinity iterations",
+ input: { iterations: -Infinity},
+ expected: { name: "TypeError" } },
+ { desc: "a NaN iterations",
+ input: { iterations: NaN },
+ expected: { name: "TypeError" } },
+ { desc: "a negative iterations",
+ input: { iterations: -1 },
+ expected: { name: "TypeError" } },
+ { desc: "a blank easing",
+ input: { easing: "" },
+ expected: { name: "TypeError" } },
+ { desc: "an unrecognized easing",
+ input: { easing: "unrecognised" },
+ expected: { name: "TypeError" } },
+ { desc: "an 'initial' easing",
+ input: { easing: "initial" },
+ expected: { name: "TypeError" } },
+ { desc: "an 'inherit' easing",
+ input: { easing: "inherit" },
+ expected: { name: "TypeError" } },
+ { desc: "a variable easing",
+ input: { easing: "var(--x)" },
+ expected: { name: "TypeError" } },
+ { desc: "a multi-value easing",
+ input: { easing: "ease-in-out, ease-out" },
+ expected: { name: "TypeError" } }
+];
diff --git a/testing/web-platform/tests/web-animations/testcommon.js b/testing/web-platform/tests/web-animations/testcommon.js
new file mode 100644
index 000000000..31ebdfaf2
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/testcommon.js
@@ -0,0 +1,176 @@
+/*
+Distributed under both the W3C Test Suite License [1] and the W3C
+3-clause BSD License [2]. To contribute to a W3C Test Suite, see the
+policies and contribution forms [3].
+
+[1] http://www.w3.org/Consortium/Legal/2008/04-testsuite-license
+[2] http://www.w3.org/Consortium/Legal/2008/03-bsd-license
+[3] http://www.w3.org/2004/10/27-testcases
+ */
+
+'use strict';
+
+var MS_PER_SEC = 1000;
+
+// The recommended minimum precision to use for time values[1].
+//
+// [1] https://w3c.github.io/web-animations/#precision-of-time-values
+var TIME_PRECISION = 0.0005; // ms
+
+// Allow implementations to substitute an alternative method for comparing
+// times based on their precision requirements.
+if (!window.assert_times_equal) {
+ window.assert_times_equal = function(actual, expected, description) {
+ assert_approx_equals(actual, expected, TIME_PRECISION, description);
+ }
+}
+
+// creates div element, appends it to the document body and
+// removes the created element during test cleanup
+function createDiv(test, doc) {
+ return createElement(test, 'div', doc);
+}
+
+// creates element of given tagName, appends it to the document body and
+// removes the created element during test cleanup
+// if tagName is null or undefined, returns div element
+function createElement(test, tagName, doc) {
+ if (!doc) {
+ doc = document;
+ }
+ var element = doc.createElement(tagName || 'div');
+ doc.body.appendChild(element);
+ test.add_cleanup(function() {
+ element.remove();
+ });
+ return element;
+}
+
+// Creates a style element with the specified rules, appends it to the document
+// head and removes the created element during test cleanup.
+// |rules| is an object. For example:
+// { '@keyframes anim': '' ,
+// '.className': 'animation: anim 100s;' };
+// or
+// { '.className1::before': 'content: ""; width: 0px; transition: all 10s;',
+// '.className2::before': 'width: 100px;' };
+// The object property name could be a keyframes name, or a selector.
+// The object property value is declarations which are property:value pairs
+// split by a space.
+function createStyle(test, rules, doc) {
+ if (!doc) {
+ doc = document;
+ }
+ var extraStyle = doc.createElement('style');
+ doc.head.appendChild(extraStyle);
+ if (rules) {
+ var sheet = extraStyle.sheet;
+ for (var selector in rules) {
+ sheet.insertRule(selector + '{' + rules[selector] + '}',
+ sheet.cssRules.length);
+ }
+ }
+ test.add_cleanup(function() {
+ extraStyle.remove();
+ });
+}
+
+// Create a pseudo element
+function createPseudo(test, type) {
+ createStyle(test, { '@keyframes anim': '',
+ ['.pseudo::' + type]: 'animation: anim 10s;' });
+ var div = createDiv(test);
+ div.classList.add('pseudo');
+ var anims = document.getAnimations();
+ assert_true(anims.length >= 1);
+ var anim = anims[anims.length - 1];
+ assert_equals(anim.effect.target.parentElement, div);
+ assert_equals(anim.effect.target.type, '::' + type);
+ anim.cancel();
+ return anim.effect.target;
+}
+
+// Convert px unit value to a Number
+function pxToNum(str) {
+ return Number(String(str).match(/^(-?[\d.]+)px$/)[1]);
+}
+
+// Cubic bezier with control points (0, 0), (x1, y1), (x2, y2), and (1, 1).
+function cubicBezier(x1, y1, x2, y2) {
+ function xForT(t) {
+ var omt = 1-t;
+ return 3 * omt * omt * t * x1 + 3 * omt * t * t * x2 + t * t * t;
+ }
+
+ function yForT(t) {
+ var omt = 1-t;
+ return 3 * omt * omt * t * y1 + 3 * omt * t * t * y2 + t * t * t;
+ }
+
+ function tForX(x) {
+ // Binary subdivision.
+ var mint = 0, maxt = 1;
+ for (var i = 0; i < 30; ++i) {
+ var guesst = (mint + maxt) / 2;
+ var guessx = xForT(guesst);
+ if (x < guessx) {
+ maxt = guesst;
+ } else {
+ mint = guesst;
+ }
+ }
+ return (mint + maxt) / 2;
+ }
+
+ return function bezierClosure(x) {
+ if (x == 0) {
+ return 0;
+ }
+ if (x == 1) {
+ return 1;
+ }
+ return yForT(tForX(x));
+ }
+}
+
+function stepEnd(nsteps) {
+ return function stepEndClosure(x) {
+ return Math.floor(x * nsteps) / nsteps;
+ }
+}
+
+function stepStart(nsteps) {
+ return function stepStartClosure(x) {
+ var result = Math.floor(x * nsteps + 1.0) / nsteps;
+ return (result > 1.0) ? 1.0 : result;
+ }
+}
+
+function waitForAnimationFrames(frameCount) {
+ return new Promise(function(resolve, reject) {
+ function handleFrame() {
+ if (--frameCount <= 0) {
+ resolve();
+ } else {
+ window.requestAnimationFrame(handleFrame); // wait another frame
+ }
+ }
+ window.requestAnimationFrame(handleFrame);
+ });
+}
+
+// Continually calls requestAnimationFrame until |minDelay| has elapsed
+// as recorded using document.timeline.currentTime (i.e. frame time not
+// wall-clock time).
+function waitForAnimationFramesWithDelay(minDelay) {
+ var startTime = document.timeline.currentTime;
+ return new Promise(function(resolve) {
+ (function handleFrame() {
+ if (document.timeline.currentTime - startTime >= minDelay) {
+ resolve();
+ } else {
+ window.requestAnimationFrame(handleFrame);
+ }
+ }());
+ });
+}
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 000000000..bdaad08ed
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/timing-model/animation-effects/active-time.html
@@ -0,0 +1,142 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title>Active time tests</title>
+<link rel="help" href="https://w3c.github.io/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(function(t) {
+ var tests = [ { fill: 'none', progress: null },
+ { fill: 'backwards', progress: 0 },
+ { fill: 'forwards', progress: null },
+ { fill: 'both', progress: 0 } ];
+ tests.forEach(function(test) {
+ var 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(function(t) {
+ var anim = createDiv(t).animate(null, 1000);
+ anim.currentTime = 500;
+ assert_times_equal(anim.effect.getComputedTiming().progress, 0.5);
+}, 'Active time in active phase and no start delay is the local time');
+
+test(function(t) {
+ var anim = createDiv(t).animate(null, { duration: 1000, delay: 500 });
+ anim.currentTime = 1000;
+ assert_times_equal(anim.effect.getComputedTiming().progress, 0.5);
+}, 'Active time in active phase and positive start delay is the local time'
+ + ' minus the start delay');
+
+test(function(t) {
+ var anim = createDiv(t).animate(null, { duration: 1000, delay: -500 });
+ assert_times_equal(anim.effect.getComputedTiming().progress, 0.5);
+}, 'Active time in active phase and negative start delay is the local time'
+ + ' minus the start delay');
+
+test(function(t) {
+ var anim = createDiv(t).animate(null);
+ assert_equals(anim.effect.getComputedTiming().progress, null);
+}, 'Active time in after phase with no fill is unresolved');
+
+test(function(t) {
+ var 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(function(t) {
+ var 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_times_equal(anim.effect.getComputedTiming().progress, 0.3);
+}, 'Active time in after phase with forwards fill is the active duration');
+
+test(function(t) {
+ var 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(function(t) {
+ var 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_times_equal(anim.effect.getComputedTiming().progress, 0.3);
+}, 'Active time in after phase with forwards fill and positive end delay'
+ + ' is the active duration');
+
+test(function(t) {
+ var 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_times_equal(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(function(t) {
+ var 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(function(t) {
+ var 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(function(t) {
+ var anim = createDiv(t).animate(null, { duration: 1000,
+ iterations: 2.3,
+ delay: 500,
+ fill: 'both' });
+ anim.finish();
+ assert_equals(anim.effect.getComputedTiming().currentIteration, 2);
+ assert_times_equal(anim.effect.getComputedTiming().progress, 0.3);
+}, 'Active time in after phase with \'both\' fill is the active duration');
+
+test(function(t) {
+ // Create an effect with a non-zero duration so we ensure we're not just
+ // testing the after-phase behavior.
+ var 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 000000000..d24908628
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/timing-model/animation-effects/current-iteration.html
@@ -0,0 +1,584 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title>Current iteration tests</title>
+<link rel="help" href="https://w3c.github.io/web-animations/#current-iteration">
+<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';
+
+function runTests(tests, description) {
+ tests.forEach(function(currentTest) {
+ var testParams = '';
+ for (var attr in currentTest.input) {
+ testParams += ' ' + attr + ':' + currentTest.input[attr];
+ }
+ test(function(t) {
+ var div = createDiv(t);
+ var anim = div.animate({ opacity: [ 0, 1 ] }, currentTest.input);
+ assert_equals(anim.effect.getComputedTiming().currentIteration,
+ currentTest.before);
+ anim.currentTime = currentTest.input.delay || 0;
+ assert_equals(anim.effect.getComputedTiming().currentIteration,
+ currentTest.active);
+ if (typeof currentTest.after !== 'undefined') {
+ anim.finish();
+ assert_equals(anim.effect.getComputedTiming().currentIteration,
+ currentTest.after);
+ }
+ }, description + ':' + testParams);
+ });
+}
+
+async_test(function(t) {
+ var div = createDiv(t);
+ var anim = div.animate({ opacity: [ 0, 1 ] }, { delay: 1 });
+ assert_equals(anim.effect.getComputedTiming().currentIteration, null);
+ anim.finished.then(t.step_func(function() {
+ 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,
+ active: 0,
+ after: 0
+ },
+
+ {
+ input: { iterations: 0,
+ iterationStart: 0,
+ duration: 100,
+ delay: 1,
+ fill: 'both' },
+ before: 0,
+ active: 0,
+ after: 0
+ },
+
+ {
+ input: { iterations: 0,
+ iterationStart: 0,
+ duration: Infinity,
+ delay: 1,
+ fill: 'both' },
+ before: 0,
+ active: 0,
+ after: 0
+ },
+
+ {
+ input: { iterations: 0,
+ iterationStart: 2.5,
+ duration: 0,
+ delay: 1,
+ fill: 'both' },
+ before: 2,
+ active: 2,
+ after: 2
+ },
+
+ {
+ input: { iterations: 0,
+ iterationStart: 2.5,
+ duration: 100,
+ delay: 1,
+ fill: 'both' },
+ before: 2,
+ active: 2,
+ after: 2
+ },
+
+ {
+ input: { iterations: 0,
+ iterationStart: 2.5,
+ duration: Infinity,
+ delay: 1,
+ fill: 'both' },
+ before: 2,
+ active: 2,
+ after: 2
+ },
+
+ {
+ input: { iterations: 0,
+ iterationStart: 3,
+ duration: 0,
+ delay: 1,
+ fill: 'both' },
+ before: 3,
+ active: 3,
+ after: 3
+ },
+
+ {
+ input: { iterations: 0,
+ iterationStart: 3,
+ duration: 100,
+ delay: 1,
+ fill: 'both' },
+ before: 3,
+ active: 3,
+ after: 3
+ },
+
+ {
+ input: { iterations: 0,
+ iterationStart: 3,
+ duration: Infinity,
+ delay: 1,
+ fill: 'both' },
+ before: 3,
+ active: 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,
+ active: 2,
+ 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,
+ active: 5,
+ 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,
+ active: 5,
+ 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,
+ active: 3,
+ 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,
+ active: 5,
+ 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,
+ active: 6,
+ 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,
+ active: Infinity,
+ 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,
+ active: Infinity,
+ 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,
+ active: Infinity,
+ 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: 0
+ },
+
+ {
+ 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: 0
+ },
+
+ {
+ 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');
+
+</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 000000000..5dc32066f
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/timing-model/animation-effects/phases-and-states.html
@@ -0,0 +1,191 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>Tests for phases and states</title>
+<link rel="help" href="https://w3c.github.io/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
+//
+// --------------------------------------------------------------------
+
+function assert_phase_at_time(animation, phase, currentTime) {
+ animation.currentTime = currentTime;
+
+ if (phase === 'active') {
+ // If the fill mode is 'none', then progress will only be non-null if we
+ // are in the active phase.
+ animation.effect.timing.fill = 'none';
+ assert_not_equals(animation.effect.getComputedTiming().progress, null,
+ 'Animation effect is in active phase when current time'
+ + ' is ' + currentTime + 'ms');
+ } else {
+ // The easiest way to distinguish between the 'before' phase and the 'after'
+ // phase is to toggle the fill mode. For example, if the progress is null
+ // will the fill node is 'none' but non-null when the fill mode is
+ // 'backwards' then we are in the before phase.
+ animation.effect.timing.fill = 'none';
+ assert_equals(animation.effect.getComputedTiming().progress, null,
+ 'Animation effect is in ' + phase + ' phase when current time'
+ + ' is ' + currentTime + 'ms'
+ + ' (progress is null with \'none\' fill mode)');
+
+ animation.effect.timing.fill = phase === 'before'
+ ? 'backwards'
+ : 'forwards';
+ assert_not_equals(animation.effect.getComputedTiming().progress, null,
+ 'Animation effect is in ' + phase + ' phase when current'
+ + ' time is ' + currentTime + 'ms'
+ + ' (progress is non-null with appropriate fill mode)');
+ }
+}
+
+test(function(t) {
+ var animation = createDiv(t).animate(null, 1);
+
+ [ { currentTime: -1, phase: 'before' },
+ { currentTime: 0, phase: 'active' },
+ { currentTime: 1, phase: 'after' } ]
+ .forEach(function(test) {
+ assert_phase_at_time(animation, test.phase, test.currentTime);
+ });
+}, 'Phase calculation for a simple animation effect');
+
+test(function(t) {
+ var animation = createDiv(t).animate(null, { duration: 1, delay: 1 });
+
+ [ { currentTime: 0, phase: 'before' },
+ { currentTime: 1, phase: 'active' },
+ { currentTime: 2, phase: 'after' } ]
+ .forEach(function(test) {
+ assert_phase_at_time(animation, test.phase, test.currentTime);
+ });
+}, 'Phase calculation for an animation effect with a positive start delay');
+
+test(function(t) {
+ var animation = createDiv(t).animate(null, { duration: 1, delay: -1 });
+
+ [ { currentTime: -2, phase: 'before' },
+ { currentTime: -1, phase: 'before' },
+ { currentTime: 0, phase: 'after' } ]
+ .forEach(function(test) {
+ assert_phase_at_time(animation, test.phase, test.currentTime);
+ });
+}, 'Phase calculation for an animation effect with a negative start delay');
+
+test(function(t) {
+ var animation = createDiv(t).animate(null, { duration: 1, endDelay: 1 });
+
+ [ { currentTime: -1, phase: 'before' },
+ { currentTime: 0, phase: 'active' },
+ { currentTime: 1, phase: 'after' },
+ { currentTime: 2, phase: 'after' } ]
+ .forEach(function(test) {
+ assert_phase_at_time(animation, test.phase, test.currentTime);
+ });
+}, 'Phase calculation for an animation effect with a positive end delay');
+
+test(function(t) {
+ var animation = createDiv(t).animate(null, { duration: 2, endDelay: -1 });
+
+ [ { currentTime: -1, phase: 'before' },
+ { currentTime: 0, phase: 'active' },
+ { currentTime: 0.9, phase: 'active' },
+ { currentTime: 1, phase: 'after' } ]
+ .forEach(function(test) {
+ 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(function(t) {
+ var animation = createDiv(t).animate(null, { duration: 1, endDelay: -1 });
+
+ [ { currentTime: -1, phase: 'before' },
+ { currentTime: 0, phase: 'after' },
+ { currentTime: 1, phase: 'after' } ]
+ .forEach(function(test) {
+ 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(function(t) {
+ var animation = createDiv(t).animate(null, { duration: 1, endDelay: -2 });
+
+ [ { currentTime: -2, phase: 'before' },
+ { currentTime: -1, phase: 'before' },
+ { currentTime: 0, phase: 'after' } ]
+ .forEach(function(test) {
+ 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(function(t) {
+ var animation = createDiv(t).animate(null, { duration: 2,
+ delay: 1,
+ endDelay: -1 });
+
+ [ { currentTime: 0, phase: 'before' },
+ { currentTime: 1, phase: 'active' },
+ { currentTime: 2, phase: 'after' } ]
+ .forEach(function(test) {
+ 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(function(t) {
+ var animation = createDiv(t).animate(null, { duration: 1,
+ delay: -1,
+ endDelay: -1 });
+
+ [ { currentTime: -2, phase: 'before' },
+ { currentTime: -1, phase: 'before' },
+ { currentTime: 0, phase: 'after' } ]
+ .forEach(function(test) {
+ 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(function(t) {
+ var animation = createDiv(t).animate(null, { duration: 1,
+ delay: -1,
+ endDelay: -2 });
+
+ [ { currentTime: -3, phase: 'before' },
+ { currentTime: -2, phase: 'before' },
+ { currentTime: -1, phase: 'before' },
+ { currentTime: 0, phase: 'after' } ]
+ .forEach(function(test) {
+ 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(function(t) {
+ var animation = createDiv(t).animate(null, 1);
+ animation.playbackRate = -1;
+
+ [ { currentTime: -1, phase: 'before' },
+ { currentTime: 0, phase: 'before' },
+ { currentTime: 1, phase: 'active' },
+ { currentTime: 2, phase: 'after' } ]
+ .forEach(function(test) {
+ 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 000000000..f6a3a51bd
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/timing-model/animation-effects/simple-iteration-progress.html
@@ -0,0 +1,575 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title>Simple iteration progress tests</title>
+<link rel="help"
+ href="https://w3c.github.io/web-animations/#simple-iteration-progress">
+<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';
+
+function runTests(tests, description) {
+ tests.forEach(function(currentTest) {
+ var testParams = '';
+ for (var attr in currentTest.input) {
+ testParams += ' ' + attr + ':' + currentTest.input[attr];
+ }
+ test(function(t) {
+ var div = createDiv(t);
+ var anim = div.animate({ opacity: [ 0, 1 ] }, currentTest.input);
+ assert_equals(anim.effect.getComputedTiming().progress,
+ currentTest.before);
+ anim.currentTime = currentTest.input.delay || 0;
+ assert_equals(anim.effect.getComputedTiming().progress,
+ currentTest.active);
+ if (typeof currentTest.after !== 'undefined') {
+ anim.finish();
+ assert_equals(anim.effect.getComputedTiming().progress,
+ currentTest.after);
+ }
+ }, description + ':' + testParams);
+ });
+}
+
+
+// --------------------------------------------------------------------
+//
+// Zero iteration duration tests
+//
+// --------------------------------------------------------------------
+
+runTests([
+ {
+ input: { iterations: 0,
+ iterationStart: 0,
+ duration: 0,
+ delay: 1,
+ fill: 'both' },
+ before: 0,
+ active: 0,
+ after: 0
+ },
+
+ {
+ input: { iterations: 0,
+ iterationStart: 0,
+ duration: 100,
+ delay: 1,
+ fill: 'both' },
+ before: 0,
+ active: 0,
+ after: 0
+ },
+
+ {
+ input: { iterations: 0,
+ iterationStart: 0,
+ duration: Infinity,
+ delay: 1,
+ fill: 'both' },
+ before: 0,
+ active: 0,
+ after: 0
+ },
+
+ {
+ input: { iterations: 0,
+ iterationStart: 2.5,
+ duration: 0,
+ delay: 1,
+ fill: 'both' },
+ before: 0.5,
+ active: 0.5,
+ after: 0.5
+ },
+
+ {
+ input: { iterations: 0,
+ iterationStart: 2.5,
+ duration: 100,
+ delay: 1,
+ fill: 'both' },
+ before: 0.5,
+ active: 0.5,
+ after: 0.5
+ },
+
+ {
+ input: { iterations: 0,
+ iterationStart: 2.5,
+ duration: Infinity,
+ delay: 1,
+ fill: 'both' },
+ before: 0.5,
+ active: 0.5,
+ after: 0.5
+ },
+
+ {
+ input: { iterations: 0,
+ iterationStart: 3,
+ duration: 0,
+ delay: 1,
+ fill: 'both' },
+ before: 0,
+ active: 0,
+ after: 0
+ },
+
+ {
+ input: { iterations: 0,
+ iterationStart: 3,
+ duration: 100,
+ delay: 1,
+ fill: 'both' },
+ before: 0,
+ active: 0,
+ after: 0
+ },
+
+ {
+ input: { iterations: 0,
+ iterationStart: 3,
+ duration: Infinity,
+ delay: 1,
+ fill: 'both' },
+ before: 0,
+ active: 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,
+ active: 1,
+ 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,
+ active: 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,
+ active: 1,
+ 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,
+ active: 0.5,
+ 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,
+ active: 1,
+ 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,
+ active: 0.5,
+ 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,
+ active: 1,
+ 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,
+ active: 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,
+ active: 1,
+ 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: 1
+ },
+
+ {
+ 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: 1
+ },
+
+ {
+ 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');
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/web-animations/timing-model/animations/current-time.html b/testing/web-platform/tests/web-animations/timing-model/animations/current-time.html
new file mode 100644
index 000000000..efc7ba78b
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/timing-model/animations/current-time.html
@@ -0,0 +1,65 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title>Tests for current time</title>
+<link rel="help" href="https://w3c.github.io/web-animations/#current-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(function(t) {
+ var 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(function(t) {
+ var animation =
+ new Animation(new KeyframeEffect(createDiv(t), null, 100 * MS_PER_SEC),
+ null);
+
+ return animation.ready.then(function() {
+ 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(function(t) {
+ var 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)');
+
+promise_test(function(t) {
+ var 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;
+
+ var timelineTime = document.timeline.currentTime;
+ var startTime = animation.startTime;
+ var 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');
+</script>
+</body>
diff --git a/testing/web-platform/tests/web-animations/timing-model/animations/set-the-animation-start-time.html b/testing/web-platform/tests/web-animations/timing-model/animations/set-the-animation-start-time.html
new file mode 100644
index 000000000..8b74c92a4
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/timing-model/animations/set-the-animation-start-time.html
@@ -0,0 +1,207 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title>Setting the start time tests</title>
+<link rel="help" href="https://w3c.github.io/web-animations/#set-the-animation-start-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(function(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.
+
+ var 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(function(t)
+{
+ // Setting an unresolved start time on an animation without an active
+ // timeline should not clear the current time.
+
+ var 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(function(t)
+{
+ var 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_times_equal(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(function(t)
+{
+ var 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_times_equal(animation.currentTime, 1000,
+ 'Current time is resolved for a running animation')
+
+ // Clear start time
+ animation.startTime = null;
+ assert_times_equal(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(function(t)
+{
+ var animation =
+ new Animation(new KeyframeEffect(createDiv(t), null, 100 * MS_PER_SEC),
+ document.timeline);
+
+ var readyPromiseCallbackCalled = false;
+ animation.ready.then(function() { readyPromiseCallbackCalled = true; } );
+
+ // Put the animation in the play-pending state
+ animation.play();
+
+ // Sanity check
+ assert_equals(animation.playState, 'pending',
+ '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.
+ return Promise.resolve().then(function() {
+ assert_true(readyPromiseCallbackCalled,
+ 'Ready promise callback called after setting startTime');
+ });
+}, 'Setting the start time resolves a pending ready promise');
+
+promise_test(function(t)
+{
+ var animation =
+ new Animation(new KeyframeEffect(createDiv(t), null, 100 * MS_PER_SEC),
+ document.timeline);
+
+ var readyPromiseCallbackCalled = false;
+ animation.ready.then(function() { readyPromiseCallbackCalled = true; } );
+
+ // Put the animation in the pause-pending state
+ animation.startTime = document.timeline.currentTime;
+ animation.pause();
+
+ // Sanity check
+ assert_equals(animation.playState, 'pending',
+ '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');
+
+ return Promise.resolve().then(function() {
+ assert_true(readyPromiseCallbackCalled,
+ 'Ready promise callback called after setting startTime');
+ });
+}, 'Setting the start time resolves a pending pause task');
+
+promise_test(function(t)
+{
+ var 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
+ var finishedCurrentTime = animation.currentTime;
+ return waitForAnimationFrames(1).then(function() {
+ 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');
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/web-animations/timing-model/animations/set-the-target-effect-of-an-animation.html b/testing/web-platform/tests/web-animations/timing-model/animations/set-the-target-effect-of-an-animation.html
new file mode 100644
index 000000000..4c51f0141
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/timing-model/animations/set-the-target-effect-of-an-animation.html
@@ -0,0 +1,95 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title>Setting the target effect tests</title>
+<link rel='help' href='https://w3c.github.io/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(function(t) {
+ var anim = createDiv(t).animate({ marginLeft: [ '0px', '100px' ] },
+ 100 * MS_PER_SEC);
+ assert_equals(anim.playState, 'pending');
+
+ var retPromise = anim.ready.then(function() {
+ assert_unreached('ready promise is fulfilled');
+ }).catch(function(err) {
+ assert_equals(err.name, 'AbortError',
+ 'ready promise is rejected with AbortError');
+ });
+
+ anim.effect = null;
+ assert_equals(anim.playState, 'paused');
+
+ return retPromise;
+}, 'If new effect is null and old effect is not null, we reset the pending ' +
+ 'tasks and ready promise is rejected');
+
+promise_test(function(t) {
+ var anim = new Animation();
+ anim.pause();
+ assert_equals(anim.playState, 'pending');
+
+ anim.effect = new KeyframeEffectReadOnly(createDiv(t),
+ { marginLeft: [ '0px', '100px' ] },
+ 100 * MS_PER_SEC);
+ assert_equals(anim.playState, 'pending');
+
+ return anim.ready.then(function() {
+ 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(function(t) {
+ var anim = new Animation();
+ anim.play();
+ assert_equals(anim.playState, 'pending');
+
+ anim.effect = new KeyframeEffectReadOnly(createDiv(t),
+ { marginLeft: [ '0px', '100px' ] },
+ 100 * MS_PER_SEC);
+ assert_equals(anim.playState, 'pending');
+
+ return anim.ready.then(function() {
+ 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(function(t) {
+ var animA = createDiv(t).animate({ marginLeft: [ '0px', '100px' ] },
+ 100 * MS_PER_SEC);
+ var animB = new Animation();
+
+ return animA.ready.then(function() {
+ 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(function(t) {
+ var animA = createDiv(t).animate({ marginLeft: [ '0px', '100px' ] },
+ 100 * MS_PER_SEC);
+ var animB = new Animation();
+ var 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.');
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/web-animations/timing-model/animations/set-the-timeline-of-an-animation.html b/testing/web-platform/tests/web-animations/timing-model/animations/set-the-timeline-of-an-animation.html
new file mode 100644
index 000000000..c540fe2ca
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/timing-model/animations/set-the-timeline-of-an-animation.html
@@ -0,0 +1,276 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title>Setting the timeline tests</title>
+<link rel="help" href="https://w3c.github.io/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(function(t) {
+ var 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_times_equal(animation.currentTime, 50 * MS_PER_SEC);
+}, 'After setting timeline on paused animation it is still paused');
+
+test(function(t) {
+ var 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_times_equal(animation.currentTime, 200 * MS_PER_SEC);
+}, 'After setting timeline on animation paused outside active interval'
+ + ' it is still paused');
+
+test(function(t) {
+ var 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(function(t) {
+ var animation =
+ new Animation(new KeyframeEffect(createDiv(t), null, 100 * MS_PER_SEC),
+ null);
+ animation.startTime = document.timeline.currentTime;
+ assert_equals(animation.playState, 'idle');
+
+ animation.timeline = document.timeline;
+
+ assert_equals(animation.playState, 'running');
+}, 'After setting timeline on an idle animation with a start time'
+ + ' it is running');
+
+test(function(t) {
+ var 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, 'idle');
+
+ animation.timeline = document.timeline;
+
+ assert_equals(animation.playState, 'finished');
+}, 'After setting timeline on an idle animation with a sufficiently ancient'
+ + ' start time it is finished');
+
+test(function(t) {
+ var animation =
+ new Animation(new KeyframeEffect(createDiv(t), null, 100 * MS_PER_SEC),
+ null);
+ animation.play();
+ assert_equals(animation.playState, 'pending');
+
+ animation.timeline = document.timeline;
+
+ assert_equals(animation.playState, 'pending');
+}, 'After setting timeline on a play-pending animation it is still pending');
+
+promise_test(function(t) {
+ var animation =
+ new Animation(new KeyframeEffect(createDiv(t), null, 100 * MS_PER_SEC),
+ null);
+ animation.play();
+ assert_equals(animation.playState, 'pending');
+
+ animation.timeline = document.timeline;
+
+ return animation.ready.then(function() {
+ assert_equals(animation.playState, 'running');
+ });
+}, 'After setting timeline on a play-pending animation it begins playing'
+ + ' after pending');
+
+test(function(t) {
+ var animation =
+ new Animation(new KeyframeEffect(createDiv(t), null, 100 * MS_PER_SEC),
+ null);
+ animation.startTime = document.timeline.currentTime;
+ animation.pause();
+ animation.timeline = null;
+ assert_equals(animation.playState, 'pending');
+
+ animation.timeline = document.timeline;
+
+ assert_equals(animation.playState, 'pending');
+}, 'After setting timeline on a pause-pending animation it is still pending');
+
+promise_test(function(t) {
+ var animation =
+ new Animation(new KeyframeEffect(createDiv(t), null, 100 * MS_PER_SEC),
+ null);
+ animation.startTime = document.timeline.currentTime;
+ animation.pause();
+ animation.timeline = null;
+ assert_equals(animation.playState, 'pending');
+
+ animation.timeline = document.timeline;
+
+ return animation.ready.then(function() {
+ assert_equals(animation.playState, 'paused');
+ });
+}, 'After setting timeline on a pause-pending animation it becomes paused'
+ + ' after pending');
+
+// ---------------------------------------------------------------------
+//
+// Tests from timeline to no timeline
+//
+// ---------------------------------------------------------------------
+
+test(function(t) {
+ var animation =
+ new Animation(new KeyframeEffect(createDiv(t), null, 100 * MS_PER_SEC),
+ document.timeline);
+ animation.currentTime = 50 * MS_PER_SEC;
+ assert_equals(animation.playState, 'paused');
+
+ animation.timeline = null;
+
+ assert_equals(animation.playState, 'paused');
+ assert_times_equal(animation.currentTime, 50 * MS_PER_SEC);
+}, 'After clearing timeline on paused animation it is still paused');
+
+test(function(t) {
+ var animation =
+ new Animation(new KeyframeEffect(createDiv(t), null, 100 * MS_PER_SEC),
+ document.timeline);
+ var initialStartTime = document.timeline.currentTime - 200 * MS_PER_SEC;
+ animation.startTime = initialStartTime;
+ assert_equals(animation.playState, 'finished');
+
+ animation.timeline = null;
+
+ assert_equals(animation.playState, 'idle');
+ assert_times_equal(animation.startTime, initialStartTime);
+}, 'After clearing timeline on finished animation it is idle');
+
+test(function(t) {
+ var animation =
+ new Animation(new KeyframeEffect(createDiv(t), null, 100 * MS_PER_SEC),
+ document.timeline);
+ var initialStartTime = document.timeline.currentTime - 50 * MS_PER_SEC;
+ animation.startTime = initialStartTime;
+ assert_equals(animation.playState, 'running');
+
+ animation.timeline = null;
+
+ assert_equals(animation.playState, 'idle');
+ assert_times_equal(animation.startTime, initialStartTime);
+}, 'After clearing timeline on running animation it is idle');
+
+test(function(t) {
+ var 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(function(t) {
+ var animation = createDiv(t).animate(null, 100 * MS_PER_SEC);
+ assert_equals(animation.playState, 'pending');
+
+ animation.timeline = null;
+
+ assert_equals(animation.playState, 'pending');
+}, 'After clearing timeline on play-pending animation it is still pending');
+
+promise_test(function(t) {
+ var animation = createDiv(t).animate(null, 100 * MS_PER_SEC);
+ assert_equals(animation.playState, 'pending');
+
+ animation.timeline = null;
+ animation.timeline = document.timeline;
+
+ assert_equals(animation.playState, 'pending');
+ return animation.ready.then(function() {
+ assert_equals(animation.playState, 'running');
+ });
+}, 'After clearing and re-setting timeline on play-pending animation it'
+ + ' begins to play');
+
+test(function(t) {
+ var animation =
+ new Animation(new KeyframeEffect(createDiv(t), null, 100 * MS_PER_SEC),
+ document.timeline);
+ animation.startTime = document.timeline.currentTime;
+ animation.pause();
+ assert_equals(animation.playState, 'pending');
+
+ animation.timeline = null;
+
+ assert_equals(animation.playState, 'pending');
+}, 'After clearing timeline on a pause-pending animation it is still pending');
+
+promise_test(function(t) {
+ var animation =
+ new Animation(new KeyframeEffect(createDiv(t), null, 100 * MS_PER_SEC),
+ document.timeline);
+ animation.startTime = document.timeline.currentTime;
+ animation.pause();
+ assert_equals(animation.playState, 'pending');
+
+ animation.timeline = null;
+ animation.timeline = document.timeline;
+
+ assert_equals(animation.playState, 'pending');
+ return animation.ready.then(function() {
+ assert_equals(animation.playState, 'paused');
+ });
+}, 'After clearing and re-setting timeline on a pause-pending animation it'
+ + ' becomes paused');
+
+promise_test(function(t) {
+ var animation =
+ new Animation(new KeyframeEffect(createDiv(t), null, 100 * MS_PER_SEC),
+ document.timeline);
+ var initialStartTime = document.timeline.currentTime - 50 * MS_PER_SEC;
+ animation.startTime = initialStartTime;
+ animation.pause();
+ animation.play();
+
+ animation.timeline = null;
+ animation.timeline = document.timeline;
+ assert_equals(animation.playState, 'pending');
+
+ return animation.ready.then(function() {
+ assert_equals(animation.playState, 'running');
+ 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/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 000000000..0b77443f2
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/timing-model/animations/updating-the-finished-state.html
@@ -0,0 +1,331 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title>Tests for updating the finished state of an animation</title>
+<link rel="help" href="https://w3c.github.io/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';
+
+//
+// NOTE TO THE POOR PERSON WHO HAS TO MERGE THIS WITH THE TEST OF THE SAME
+// NAME FROM BLINK
+//
+// There is a pull request from Blink at:
+//
+// https://github.com/w3c/web-platform-tests/pull/3328
+//
+// which this file will surely conflict with.
+//
+// However, those tests cover a different part of the same algorithm. They
+// are mostly concerned with testing events and promises rather than the
+// timing part of the algorithm.
+//
+// The tests below cover the first part of the algorithm. So, please keep both
+// sets of tests and delete this comment. Preferably put the tests in this
+// file first.
+//
+// Thank you!
+//
+
+
+// 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(function(t) {
+ var 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.
+ return anim.ready.then(function() {
+ // Seek to 1ms before the target end and then wait 1ms
+ anim.currentTime = 100 * MS_PER_SEC - 1;
+ return waitForAnimationFramesWithDelay(1);
+ }).then(function() {
+ 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(function(t) {
+ var anim = createDiv(t).animate(null, 100 * MS_PER_SEC);
+ return anim.ready.then(function() {
+ anim.currentTime = 200 * MS_PER_SEC;
+ return waitForAnimationFrames(1);
+ }).then(function() {
+ 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(function(t) {
+ var anim = createDiv(t).animate(null, 100 * MS_PER_SEC);
+ return anim.ready.then(function() {
+ anim.currentTime = 100 * MS_PER_SEC;
+ return waitForAnimationFrames(1);
+ }).then(function() {
+ 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(function(t) {
+ var anim = createDiv(t).animate(null, 100 * MS_PER_SEC);
+ anim.playbackRate = -1;
+ anim.play(); // Make sure animation is not initially finished
+ return anim.ready.then(function() {
+ // Seek to 1ms before 0 and then wait 1ms
+ anim.currentTime = 1;
+ return waitForAnimationFramesWithDelay(1);
+ }).then(function() {
+ 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(function(t) {
+ var anim = createDiv(t).animate(null, 100 * MS_PER_SEC);
+ anim.playbackRate = -1;
+ anim.play();
+ return anim.ready.then(function() {
+ anim.currentTime = -100 * MS_PER_SEC;
+ return waitForAnimationFrames(1);
+ }).then(function() {
+ 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(function(t) {
+ var anim = createDiv(t).animate(null, 100 * MS_PER_SEC);
+ anim.playbackRate = -1;
+ anim.play();
+ return anim.ready.then(function() {
+ anim.currentTime = 0;
+ return waitForAnimationFrames(1);
+ }).then(function() {
+ 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(function(t) {
+ var 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();
+ return anim.ready.then(function() {
+ 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.timing.duration *= 2;
+ return waitForAnimationFrames(1);
+ }).then(function() {
+ 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(function(t) {
+ var anim = createDiv(t).animate(null, 100 * MS_PER_SEC);
+ anim.finish();
+ return anim.ready.then(function() {
+ 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');
+ return waitForAnimationFrames(1);
+ }).then(function() {
+ 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(function(t) {
+ var anim = createDiv(t).animate(null, 100 * MS_PER_SEC);
+ anim.playbackRate = -1;
+ return anim.ready.then(function() {
+ anim.currentTime = 50 * MS_PER_SEC;
+ assert_equals(anim.currentTime, 50 * MS_PER_SEC, 'Start time is updated');
+ return waitForAnimationFrames(1);
+ }).then(function() {
+ 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(function(t) {
+ var anim = createDiv(t).animate(null, 100 * MS_PER_SEC);
+ anim.playbackRate = 0;
+ return anim.ready.then(function() {
+ anim.currentTime = -100 * MS_PER_SEC;
+ return waitForAnimationFrames(1);
+ }).then(function() {
+ 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(function(t) {
+ var anim = createDiv(t).animate(null, 100 * MS_PER_SEC);
+ anim.playbackRate = 0;
+ return anim.ready.then(function() {
+ anim.currentTime = 50 * MS_PER_SEC;
+ return waitForAnimationFrames(1);
+ }).then(function() {
+ 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(function(t) {
+ var anim = createDiv(t).animate(null, 100 * MS_PER_SEC);
+ anim.playbackRate = 0;
+ return anim.ready.then(function() {
+ anim.currentTime = 200 * MS_PER_SEC;
+ return waitForAnimationFrames(1);
+ }).then(function() {
+ 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(function(t) {
+ var 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.timing.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.
+ return waitForAnimationFrames(1).then(function() {
+ 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(function(t) {
+ var 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.timing.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(function(t) {
+ var 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.timing.duration = 200 * MS_PER_SEC;
+ return waitForAnimationFrames(1).then(function() {
+ 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(function(t) {
+ var 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');
+
+</script>
+</body>