summaryrefslogtreecommitdiffstats
path: root/testing/web-platform/tests/web-animations
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
commit26a029d407be480d791972afb5975cf62c9360a6 (patch)
treef435a8308119effd964b339f76abb83a57c29483 /testing/web-platform/tests/web-animations
parentInitial commit. (diff)
downloadfirefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz
firefox-26a029d407be480d791972afb5975cf62c9360a6.zip
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'testing/web-platform/tests/web-animations')
-rw-r--r--testing/web-platform/tests/web-animations/META.yml5
-rw-r--r--testing/web-platform/tests/web-animations/README.md116
-rw-r--r--testing/web-platform/tests/web-animations/WEB_FEATURES.yml3
-rw-r--r--testing/web-platform/tests/web-animations/animation-model/animation-types/accumulation-per-property-001.html24
-rw-r--r--testing/web-platform/tests/web-animations/animation-model/animation-types/accumulation-per-property-002.html24
-rw-r--r--testing/web-platform/tests/web-animations/animation-model/animation-types/addition-per-property-001.html24
-rw-r--r--testing/web-platform/tests/web-animations/animation-model/animation-types/addition-per-property-002.html24
-rw-r--r--testing/web-platform/tests/web-animations/animation-model/animation-types/discrete.html135
-rw-r--r--testing/web-platform/tests/web-animations/animation-model/animation-types/display.tentative.html34
-rw-r--r--testing/web-platform/tests/web-animations/animation-model/animation-types/interpolation-per-property-001.html24
-rw-r--r--testing/web-platform/tests/web-animations/animation-model/animation-types/interpolation-per-property-002.html24
-rw-r--r--testing/web-platform/tests/web-animations/animation-model/animation-types/property-list.js1618
-rw-r--r--testing/web-platform/tests/web-animations/animation-model/animation-types/property-types.js2936
-rw-r--r--testing/web-platform/tests/web-animations/animation-model/animation-types/property-utils.js38
-rw-r--r--testing/web-platform/tests/web-animations/animation-model/animation-types/scrollbar-interpolation.html91
-rw-r--r--testing/web-platform/tests/web-animations/animation-model/animation-types/visibility.html57
-rw-r--r--testing/web-platform/tests/web-animations/animation-model/combining-effects/applying-interpolated-transform.html87
-rw-r--r--testing/web-platform/tests/web-animations/animation-model/combining-effects/applying-the-composited-result.html29
-rw-r--r--testing/web-platform/tests/web-animations/animation-model/combining-effects/effect-composition.html154
-rw-r--r--testing/web-platform/tests/web-animations/animation-model/keyframe-effects/computed-keyframes-shorthands.html30
-rw-r--r--testing/web-platform/tests/web-animations/animation-model/keyframe-effects/effect-in-removed-iframe-crash.html22
-rw-r--r--testing/web-platform/tests/web-animations/animation-model/keyframe-effects/effect-on-marquee-parent-crash.html21
-rw-r--r--testing/web-platform/tests/web-animations/animation-model/keyframe-effects/effect-value-context-filling.html377
-rw-r--r--testing/web-platform/tests/web-animations/animation-model/keyframe-effects/effect-value-context.html105
-rw-r--r--testing/web-platform/tests/web-animations/animation-model/keyframe-effects/effect-value-interval-distance.html36
-rw-r--r--testing/web-platform/tests/web-animations/animation-model/keyframe-effects/effect-value-iteration-composite-operation.html824
-rw-r--r--testing/web-platform/tests/web-animations/animation-model/keyframe-effects/effect-value-opacity-replaced-effect-in-shadow-root-ref.html9
-rw-r--r--testing/web-platform/tests/web-animations/animation-model/keyframe-effects/effect-value-opacity-replaced-effect-in-shadow-root.html26
-rw-r--r--testing/web-platform/tests/web-animations/animation-model/keyframe-effects/effect-value-opacity-replaced-effect-ref.html9
-rw-r--r--testing/web-platform/tests/web-animations/animation-model/keyframe-effects/effect-value-opacity-replaced-effect.html32
-rw-r--r--testing/web-platform/tests/web-animations/animation-model/keyframe-effects/effect-value-overlapping-keyframes.html77
-rw-r--r--testing/web-platform/tests/web-animations/animation-model/keyframe-effects/effect-value-replaced-animations.html161
-rw-r--r--testing/web-platform/tests/web-animations/animation-model/keyframe-effects/effect-value-transformed-distance.html84
-rw-r--r--testing/web-platform/tests/web-animations/animation-model/keyframe-effects/keyframe-exceptions.html39
-rw-r--r--testing/web-platform/tests/web-animations/animation-model/keyframe-effects/transform-and-opacity-on-inline-001-ref.html14
-rw-r--r--testing/web-platform/tests/web-animations/animation-model/keyframe-effects/transform-and-opacity-on-inline-001.html27
-rw-r--r--testing/web-platform/tests/web-animations/animation-model/resources/side-effects-common.js81
-rw-r--r--testing/web-platform/tests/web-animations/animation-model/side-effects-of-animations-current-ref.html109
-rw-r--r--testing/web-platform/tests/web-animations/animation-model/side-effects-of-animations-current.html174
-rw-r--r--testing/web-platform/tests/web-animations/animation-model/side-effects-of-animations-in-effect-ref.html109
-rw-r--r--testing/web-platform/tests/web-animations/animation-model/side-effects-of-animations-in-effect.html174
-rw-r--r--testing/web-platform/tests/web-animations/animation-model/side-effects-of-animations-none-ref.html48
-rw-r--r--testing/web-platform/tests/web-animations/animation-model/side-effects-of-animations-none.html79
-rw-r--r--testing/web-platform/tests/web-animations/crashtests/color-mix-crashtest.html35
-rw-r--r--testing/web-platform/tests/web-animations/crashtests/get-computed-timing-crash.html20
-rw-r--r--testing/web-platform/tests/web-animations/crashtests/infinite-active-duration.html68
-rw-r--r--testing/web-platform/tests/web-animations/crashtests/partially-overlapping-animations-one-not-current-001.html17
-rw-r--r--testing/web-platform/tests/web-animations/crashtests/reparent-animating-element-001.html40
-rw-r--r--testing/web-platform/tests/web-animations/crashtests/reparent-animating-element-002.html44
-rw-r--r--testing/web-platform/tests/web-animations/idlharness.window.js21
-rw-r--r--testing/web-platform/tests/web-animations/interfaces/Animatable/animate-no-browsing-context.html107
-rw-r--r--testing/web-platform/tests/web-animations/interfaces/Animatable/animate.html363
-rw-r--r--testing/web-platform/tests/web-animations/interfaces/Animatable/getAnimations-iframe.html51
-rw-r--r--testing/web-platform/tests/web-animations/interfaces/Animatable/getAnimations.html355
-rw-r--r--testing/web-platform/tests/web-animations/interfaces/Animation/cancel.html133
-rw-r--r--testing/web-platform/tests/web-animations/interfaces/Animation/commitStyles-crash.html26
-rw-r--r--testing/web-platform/tests/web-animations/interfaces/Animation/commitStyles-svg-crash.html12
-rw-r--r--testing/web-platform/tests/web-animations/interfaces/Animation/commitStyles.html577
-rw-r--r--testing/web-platform/tests/web-animations/interfaces/Animation/constructor.html124
-rw-r--r--testing/web-platform/tests/web-animations/interfaces/Animation/effect.html42
-rw-r--r--testing/web-platform/tests/web-animations/interfaces/Animation/finished.html416
-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.html119
-rw-r--r--testing/web-platform/tests/web-animations/interfaces/Animation/onremove.html58
-rw-r--r--testing/web-platform/tests/web-animations/interfaces/Animation/pause.html98
-rw-r--r--testing/web-platform/tests/web-animations/interfaces/Animation/pending.html55
-rw-r--r--testing/web-platform/tests/web-animations/interfaces/Animation/persist.html40
-rw-r--r--testing/web-platform/tests/web-animations/interfaces/Animation/play.html34
-rw-r--r--testing/web-platform/tests/web-animations/interfaces/Animation/ready.html78
-rw-r--r--testing/web-platform/tests/web-animations/interfaces/Animation/startTime.html55
-rw-r--r--testing/web-platform/tests/web-animations/interfaces/Animation/style-change-events.html376
-rw-r--r--testing/web-platform/tests/web-animations/interfaces/AnimationEffect/getComputedTiming.html214
-rw-r--r--testing/web-platform/tests/web-animations/interfaces/AnimationEffect/updateTiming.html475
-rw-r--r--testing/web-platform/tests/web-animations/interfaces/AnimationPlaybackEvent/constructor.html30
-rw-r--r--testing/web-platform/tests/web-animations/interfaces/Document/timeline.html23
-rw-r--r--testing/web-platform/tests/web-animations/interfaces/DocumentOrShadowRoot/getAnimations.html234
-rw-r--r--testing/web-platform/tests/web-animations/interfaces/DocumentTimeline/constructor.html43
-rw-r--r--testing/web-platform/tests/web-animations/interfaces/DocumentTimeline/style-change-events.html92
-rw-r--r--testing/web-platform/tests/web-animations/interfaces/KeyframeEffect/composite.html47
-rw-r--r--testing/web-platform/tests/web-animations/interfaces/KeyframeEffect/constructor.html195
-rw-r--r--testing/web-platform/tests/web-animations/interfaces/KeyframeEffect/copy-constructor.html93
-rw-r--r--testing/web-platform/tests/web-animations/interfaces/KeyframeEffect/getKeyframes.html25
-rw-r--r--testing/web-platform/tests/web-animations/interfaces/KeyframeEffect/iterationComposite.html36
-rw-r--r--testing/web-platform/tests/web-animations/interfaces/KeyframeEffect/processing-a-keyframes-argument-001.html602
-rw-r--r--testing/web-platform/tests/web-animations/interfaces/KeyframeEffect/processing-a-keyframes-argument-002.html125
-rw-r--r--testing/web-platform/tests/web-animations/interfaces/KeyframeEffect/setKeyframes.html55
-rw-r--r--testing/web-platform/tests/web-animations/interfaces/KeyframeEffect/style-change-events.html242
-rw-r--r--testing/web-platform/tests/web-animations/interfaces/KeyframeEffect/target.html271
-rw-r--r--testing/web-platform/tests/web-animations/resources/easing-tests.js121
-rw-r--r--testing/web-platform/tests/web-animations/resources/effect-tests.js75
-rw-r--r--testing/web-platform/tests/web-animations/resources/keyframe-tests.js827
-rw-r--r--testing/web-platform/tests/web-animations/resources/keyframe-utils.js51
-rw-r--r--testing/web-platform/tests/web-animations/resources/timing-override.js18
-rw-r--r--testing/web-platform/tests/web-animations/resources/timing-tests.js46
-rw-r--r--testing/web-platform/tests/web-animations/resources/timing-utils.js52
-rw-r--r--testing/web-platform/tests/web-animations/resources/xhr-doc.py5
-rw-r--r--testing/web-platform/tests/web-animations/responsive/assorted-lengths.html102
-rw-r--r--testing/web-platform/tests/web-animations/responsive/backgroundPosition.html25
-rw-r--r--testing/web-platform/tests/web-animations/responsive/backgroundSize.html22
-rw-r--r--testing/web-platform/tests/web-animations/responsive/baselineShift.html43
-rw-r--r--testing/web-platform/tests/web-animations/responsive/borderImageWidth.html22
-rw-r--r--testing/web-platform/tests/web-animations/responsive/borderRadius.html39
-rw-r--r--testing/web-platform/tests/web-animations/responsive/borderWidth.html33
-rw-r--r--testing/web-platform/tests/web-animations/responsive/boxShadow.html30
-rw-r--r--testing/web-platform/tests/web-animations/responsive/clip.html45
-rw-r--r--testing/web-platform/tests/web-animations/responsive/columnCount.html32
-rw-r--r--testing/web-platform/tests/web-animations/responsive/columnGap.html49
-rw-r--r--testing/web-platform/tests/web-animations/responsive/d.html27
-rw-r--r--testing/web-platform/tests/web-animations/responsive/font-size-adjust.html32
-rw-r--r--testing/web-platform/tests/web-animations/responsive/fontSize.html72
-rw-r--r--testing/web-platform/tests/web-animations/responsive/fontWeight.html42
-rw-r--r--testing/web-platform/tests/web-animations/responsive/lineHeight.html80
-rw-r--r--testing/web-platform/tests/web-animations/responsive/minHeight.html35
-rw-r--r--testing/web-platform/tests/web-animations/responsive/offset-path.html27
-rw-r--r--testing/web-platform/tests/web-animations/responsive/offsetDistance.html57
-rw-r--r--testing/web-platform/tests/web-animations/responsive/offsetRotate.html43
-rw-r--r--testing/web-platform/tests/web-animations/responsive/opacity.html48
-rw-r--r--testing/web-platform/tests/web-animations/responsive/perspective.html49
-rw-r--r--testing/web-platform/tests/web-animations/responsive/resources/block.html10
-rw-r--r--testing/web-platform/tests/web-animations/responsive/rotate.html22
-rw-r--r--testing/web-platform/tests/web-animations/responsive/rowGap.html49
-rw-r--r--testing/web-platform/tests/web-animations/responsive/shapeMargin.html24
-rw-r--r--testing/web-platform/tests/web-animations/responsive/shapeOutside.html59
-rw-r--r--testing/web-platform/tests/web-animations/responsive/strokeDasharray.html28
-rw-r--r--testing/web-platform/tests/web-animations/responsive/text-size-adjust.html29
-rw-r--r--testing/web-platform/tests/web-animations/responsive/textIndent.html59
-rw-r--r--testing/web-platform/tests/web-animations/responsive/to-color-change.html253
-rw-r--r--testing/web-platform/tests/web-animations/responsive/to-inherited-change.html56
-rw-r--r--testing/web-platform/tests/web-animations/responsive/to-style-change.html44
-rw-r--r--testing/web-platform/tests/web-animations/responsive/toggle-animated-iframe-visibility-ref.html10
-rw-r--r--testing/web-platform/tests/web-animations/responsive/toggle-animated-iframe-visibility.html55
-rw-r--r--testing/web-platform/tests/web-animations/responsive/transform.html22
-rw-r--r--testing/web-platform/tests/web-animations/responsive/translate.html22
-rw-r--r--testing/web-platform/tests/web-animations/responsive/verticalAlign.html22
-rw-r--r--testing/web-platform/tests/web-animations/testcommon.js342
-rw-r--r--testing/web-platform/tests/web-animations/timing-model/animation-effects/active-time.html141
-rw-r--r--testing/web-platform/tests/web-animations/timing-model/animation-effects/current-iteration.html620
-rw-r--r--testing/web-platform/tests/web-animations/timing-model/animation-effects/local-time.html29
-rw-r--r--testing/web-platform/tests/web-animations/timing-model/animation-effects/phases-and-states.html149
-rw-r--r--testing/web-platform/tests/web-animations/timing-model/animation-effects/simple-iteration-progress.html600
-rw-r--r--testing/web-platform/tests/web-animations/timing-model/animations/canceling-an-animation.html127
-rw-r--r--testing/web-platform/tests/web-animations/timing-model/animations/document-timeline-animation-ref.html19
-rw-r--r--testing/web-platform/tests/web-animations/timing-model/animations/document-timeline-animation.html63
-rw-r--r--testing/web-platform/tests/web-animations/timing-model/animations/finishing-an-animation.html330
-rw-r--r--testing/web-platform/tests/web-animations/timing-model/animations/infinite-duration-animation-ref.html19
-rw-r--r--testing/web-platform/tests/web-animations/timing-model/animations/infinite-duration-animation.html64
-rw-r--r--testing/web-platform/tests/web-animations/timing-model/animations/invalidating-animation-before-start-time-synced.html71
-rw-r--r--testing/web-platform/tests/web-animations/timing-model/animations/pausing-an-animation.html119
-rw-r--r--testing/web-platform/tests/web-animations/timing-model/animations/play-states.html185
-rw-r--r--testing/web-platform/tests/web-animations/timing-model/animations/playing-an-animation.html177
-rw-r--r--testing/web-platform/tests/web-animations/timing-model/animations/reverse-running-animation-ref.html17
-rw-r--r--testing/web-platform/tests/web-animations/timing-model/animations/reverse-running-animation.html50
-rw-r--r--testing/web-platform/tests/web-animations/timing-model/animations/reversing-an-animation.html266
-rw-r--r--testing/web-platform/tests/web-animations/timing-model/animations/seamlessly-updating-the-playback-rate-of-an-animation.html171
-rw-r--r--testing/web-platform/tests/web-animations/timing-model/animations/setting-the-current-time-of-an-animation.html167
-rw-r--r--testing/web-platform/tests/web-animations/timing-model/animations/setting-the-playback-rate-of-an-animation.html110
-rw-r--r--testing/web-platform/tests/web-animations/timing-model/animations/setting-the-start-time-of-an-animation.html329
-rw-r--r--testing/web-platform/tests/web-animations/timing-model/animations/setting-the-target-effect-of-an-animation.html129
-rw-r--r--testing/web-platform/tests/web-animations/timing-model/animations/setting-the-timeline-of-an-animation.html255
-rw-r--r--testing/web-platform/tests/web-animations/timing-model/animations/sync-start-times-ref.html20
-rw-r--r--testing/web-platform/tests/web-animations/timing-model/animations/sync-start-times.html72
-rw-r--r--testing/web-platform/tests/web-animations/timing-model/animations/the-current-time-of-an-animation.html75
-rw-r--r--testing/web-platform/tests/web-animations/timing-model/animations/update-playback-rate-fast-ref.html17
-rw-r--r--testing/web-platform/tests/web-animations/timing-model/animations/update-playback-rate-fast.html52
-rw-r--r--testing/web-platform/tests/web-animations/timing-model/animations/update-playback-rate-zero-ref.html16
-rw-r--r--testing/web-platform/tests/web-animations/timing-model/animations/update-playback-rate-zero.html46
-rw-r--r--testing/web-platform/tests/web-animations/timing-model/animations/updating-the-finished-state.html457
-rw-r--r--testing/web-platform/tests/web-animations/timing-model/time-transformations/transformed-progress.html391
-rw-r--r--testing/web-platform/tests/web-animations/timing-model/timelines/document-timelines.html50
-rw-r--r--testing/web-platform/tests/web-animations/timing-model/timelines/resources/target-frame.html76
-rw-r--r--testing/web-platform/tests/web-animations/timing-model/timelines/resources/timeline-frame.html20
-rw-r--r--testing/web-platform/tests/web-animations/timing-model/timelines/sibling-iframe-timeline.html58
-rw-r--r--testing/web-platform/tests/web-animations/timing-model/timelines/timelines.html144
-rw-r--r--testing/web-platform/tests/web-animations/timing-model/timelines/update-and-send-events-replacement.html1017
-rw-r--r--testing/web-platform/tests/web-animations/timing-model/timelines/update-and-send-events.html257
176 files changed, 24571 insertions, 0 deletions
diff --git a/testing/web-platform/tests/web-animations/META.yml b/testing/web-platform/tests/web-animations/META.yml
new file mode 100644
index 0000000000..9ba1245bde
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/META.yml
@@ -0,0 +1,5 @@
+spec: https://drafts.csswg.org/web-animations/
+suggested_reviewers:
+ - birtles
+ - flackr
+ - graouts
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 0000000000..c41e0e048e
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/README.md
@@ -0,0 +1,116 @@
+Web Animations Test Suite
+=========================
+
+Specification: https://drafts.csswg.org/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/setting-the-start-time-of-an-animation.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(t => {
+ const animation = createDiv(t).animate(null);
+ assert_class_string(animation, 'Animation', 'Returned object is an Animation');
+ }, 'Element.animate() creates an Animation object');
+ ```
+
+ ```javascript
+ test(t => {
+ assert_throws_js(TypeError, () => {
+ createDiv(t).animate(null, -1);
+ });
+ }, 'Setting a negative duration throws a TypeError');
+ ```
+
+ ```javascript
+ promise_test(t => {
+ const animation = createDiv(t).animate(null, 100 * MS_PER_SEC);
+ return animation.ready.then(() => {
+ 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 times returned from
+ the API. This asserts that the time values are equal using a tolerance
+ based on the precision recommended in the spec. This tolerance is applied
+ to *both* of the values being compared. That is, it effectively allows
+ double the epsilon that is used when comparing with an absolute value.
+
+ For comparing a time value returned from the API to an absolute value, use
+ `assert_time_equals_literal`. This tests that the actual value is equal to
+ the expected value within the precision recommended in the spec.
+
+ Both `assert_times_equal` and `assert_time_equals_literal` are defined in a
+ way that implementations can override them 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/WEB_FEATURES.yml b/testing/web-platform/tests/web-animations/WEB_FEATURES.yml
new file mode 100644
index 0000000000..be78a70b81
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/WEB_FEATURES.yml
@@ -0,0 +1,3 @@
+features:
+- name: web-animations
+ files: "**"
diff --git a/testing/web-platform/tests/web-animations/animation-model/animation-types/accumulation-per-property-001.html b/testing/web-platform/tests/web-animations/animation-model/animation-types/accumulation-per-property-001.html
new file mode 100644
index 0000000000..a3fd115563
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/animation-model/animation-types/accumulation-per-property-001.html
@@ -0,0 +1,24 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>Accumulation for each property</title>
+<link rel="help" href="https://drafts.csswg.org/web-animations/#animation-types">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../../testcommon.js"></script>
+<script src="property-list.js"></script>
+<script src="property-types.js"></script>
+<script src="property-utils.js"></script>
+<style>
+html {
+ font-size: 10px;
+}
+</style>
+<body>
+<div id="log"></div>
+<script>
+'use strict';
+
+test(function() {
+ runAnimationTypeTest(gCSSProperties1, 'testAccumulation');
+}, 'Setup');
+</script>
diff --git a/testing/web-platform/tests/web-animations/animation-model/animation-types/accumulation-per-property-002.html b/testing/web-platform/tests/web-animations/animation-model/animation-types/accumulation-per-property-002.html
new file mode 100644
index 0000000000..5cf411edf6
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/animation-model/animation-types/accumulation-per-property-002.html
@@ -0,0 +1,24 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>Accumulation for each property</title>
+<link rel="help" href="https://drafts.csswg.org/web-animations/#animation-types">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../../testcommon.js"></script>
+<script src="property-list.js"></script>
+<script src="property-types.js"></script>
+<script src="property-utils.js"></script>
+<style>
+html {
+ font-size: 10px;
+}
+</style>
+<body>
+<div id="log"></div>
+<script>
+'use strict';
+
+test(function() {
+ runAnimationTypeTest(gCSSProperties2, 'testAccumulation');
+}, 'Setup');
+</script>
diff --git a/testing/web-platform/tests/web-animations/animation-model/animation-types/addition-per-property-001.html b/testing/web-platform/tests/web-animations/animation-model/animation-types/addition-per-property-001.html
new file mode 100644
index 0000000000..2fbec2c4cd
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/animation-model/animation-types/addition-per-property-001.html
@@ -0,0 +1,24 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>Addition for each property</title>
+<link rel="help" href="https://drafts.csswg.org/web-animations/#animation-types">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../../testcommon.js"></script>
+<script src="property-list.js"></script>
+<script src="property-types.js"></script>
+<script src="property-utils.js"></script>
+<style>
+html {
+ font-size: 10px;
+}
+</style>
+<body>
+<div id="log"></div>
+<script>
+'use strict';
+
+test(function() {
+ runAnimationTypeTest(gCSSProperties1, 'testAddition');
+}, "Setup");
+</script>
diff --git a/testing/web-platform/tests/web-animations/animation-model/animation-types/addition-per-property-002.html b/testing/web-platform/tests/web-animations/animation-model/animation-types/addition-per-property-002.html
new file mode 100644
index 0000000000..3b1c40e3c7
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/animation-model/animation-types/addition-per-property-002.html
@@ -0,0 +1,24 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>Addition for each property</title>
+<link rel="help" href="https://drafts.csswg.org/web-animations/#animation-types">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../../testcommon.js"></script>
+<script src="property-list.js"></script>
+<script src="property-types.js"></script>
+<script src="property-utils.js"></script>
+<style>
+html {
+ font-size: 10px;
+}
+</style>
+<body>
+<div id="log"></div>
+<script>
+'use strict';
+
+test(function() {
+ runAnimationTypeTest(gCSSProperties2, 'testAddition');
+}, "Setup");
+</script>
diff --git a/testing/web-platform/tests/web-animations/animation-model/animation-types/discrete.html b/testing/web-platform/tests/web-animations/animation-model/animation-types/discrete.html
new file mode 100644
index 0000000000..76f42bc7a4
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/animation-model/animation-types/discrete.html
@@ -0,0 +1,135 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title>Discrete animation type</title>
+<link rel="help" href="https://drafts.csswg.org/web-animations/#discrete-animation-type">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../../testcommon.js"></script>
+<body>
+<div id="log"></div>
+<script>
+'use strict';
+
+test(t => {
+ const div = createDiv(t);
+
+ const 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(t => {
+ const div = createDiv(t);
+ const originalHeight = getComputedStyle(div).height;
+
+ const 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(t => {
+ const div = createDiv(t);
+ const originalHeight = getComputedStyle(div).height;
+
+ const 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(t => {
+ const div = createDiv(t);
+ const 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.
+ const anim = div.animate({ fontStyle: [ 'normal', 'italic' ] },
+ { duration: 1000, fill: 'forwards',
+ easing: 'cubic-bezier(0.68,0,1,0.01)' });
+
+ assert_equals(getComputedStyle(div).fontStyle, 'normal',
+ 'Animation produces \'from\' value at start of interval');
+ anim.currentTime = 940;
+ assert_equals(getComputedStyle(div).fontStyle, 'normal',
+ 'Animation produces \'from\' value at 94% of the iteration'
+ + ' time');
+ anim.currentTime = 960;
+ assert_equals(getComputedStyle(div).fontStyle, 'italic',
+ '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(t => {
+ const div = createDiv(t);
+ const 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.
+ const anim = div.animate([ { fontStyle: 'normal',
+ easing: 'cubic-bezier(0.68,0,1,0.01)' },
+ { fontStyle: 'italic' } ],
+ { duration: 1000, fill: 'forwards' });
+
+ assert_equals(getComputedStyle(div).fontStyle, 'normal',
+ 'Animation produces \'from\' value at start of interval');
+ anim.currentTime = 940;
+ assert_equals(getComputedStyle(div).fontStyle, 'normal',
+ 'Animation produces \'from\' value at 94% of the iteration'
+ + ' time');
+ anim.currentTime = 960;
+ assert_equals(getComputedStyle(div).fontStyle, 'italic',
+ '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/display.tentative.html b/testing/web-platform/tests/web-animations/animation-model/animation-types/display.tentative.html
new file mode 100644
index 0000000000..6b28dcd964
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/animation-model/animation-types/display.tentative.html
@@ -0,0 +1,34 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title>Animation type for the 'display' property</title>
+<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(t => {
+ const div = createDiv(t);
+ div.style.display = 'none';
+ const anim = div.animate({ display: ['block', 'block'] },
+ { duration: 100 * MS_PER_SEC });
+
+ anim.currentTime = 0;
+ assert_equals(getComputedStyle(div).display, 'block',
+ 'Display when progress = 0');
+
+ anim.currentTime = 10 * MS_PER_SEC + 1;
+ assert_equals(getComputedStyle(div).display, 'block',
+ 'Display when progress > 0');
+
+ anim.finish();
+ assert_equals(getComputedStyle(div).display, 'none',
+ 'Display when progress = 1');
+
+ }, 'Display can be held by animation');
+ </script>
+</body>
diff --git a/testing/web-platform/tests/web-animations/animation-model/animation-types/interpolation-per-property-001.html b/testing/web-platform/tests/web-animations/animation-model/animation-types/interpolation-per-property-001.html
new file mode 100644
index 0000000000..97f2822473
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/animation-model/animation-types/interpolation-per-property-001.html
@@ -0,0 +1,24 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title>Interpolation for each property</title>
+<link rel="help" href="https://drafts.csswg.org/web-animations/#animation-types">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../../testcommon.js"></script>
+<script src="property-list.js"></script>
+<script src="property-types.js"></script>
+<script src="property-utils.js"></script>
+<style>
+html {
+ font-size: 10px;
+}
+</style>
+<body>
+<div id="log"></div>
+<script>
+'use strict';
+
+test(function() {
+ runAnimationTypeTest(gCSSProperties1, 'testInterpolation');
+}, 'Setup');
+</script>
diff --git a/testing/web-platform/tests/web-animations/animation-model/animation-types/interpolation-per-property-002.html b/testing/web-platform/tests/web-animations/animation-model/animation-types/interpolation-per-property-002.html
new file mode 100644
index 0000000000..9ccc613cfc
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/animation-model/animation-types/interpolation-per-property-002.html
@@ -0,0 +1,24 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title>Interpolation for each property</title>
+<link rel="help" href="https://drafts.csswg.org/web-animations/#animation-types">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../../testcommon.js"></script>
+<script src="property-list.js"></script>
+<script src="property-types.js"></script>
+<script src="property-utils.js"></script>
+<style>
+html {
+ font-size: 10px;
+}
+</style>
+<body>
+<div id="log"></div>
+<script>
+'use strict';
+
+test(function() {
+ runAnimationTypeTest(gCSSProperties2, 'testInterpolation');
+}, 'Setup');
+</script>
diff --git a/testing/web-platform/tests/web-animations/animation-model/animation-types/property-list.js b/testing/web-platform/tests/web-animations/animation-model/animation-types/property-list.js
new file mode 100644
index 0000000000..98ab02a122
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/animation-model/animation-types/property-list.js
@@ -0,0 +1,1618 @@
+'use strict';
+
+const gCSSProperties1 = {
+ 'align-content': {
+ // https://drafts.csswg.org/css-align/#propdef-align-content
+ types: [
+ { type: 'discrete' , options: [ [ 'flex-start', 'flex-end' ] ] }
+ ]
+ },
+ 'align-items': {
+ // https://drafts.csswg.org/css-align/#propdef-align-items
+ types: [
+ { type: 'discrete', options: [ [ 'flex-start', 'flex-end' ] ] }
+ ]
+ },
+ 'align-self': {
+ // https://drafts.csswg.org/css-align/#propdef-align-self
+ types: [
+ { type: 'discrete', options: [ [ 'flex-start', 'flex-end' ] ] }
+ ]
+ },
+ 'backface-visibility': {
+ // https://drafts.csswg.org/css-transforms/#propdef-backface-visibility
+ types: [
+ { type: 'discrete', options: [ [ 'visible', 'hidden' ] ] }
+ ]
+ },
+ 'background-attachment': {
+ // https://drafts.csswg.org/css-backgrounds-3/#background-attachment
+ types: [
+ { type: 'discrete', options: [ [ 'fixed', 'local' ] ] }
+ ]
+ },
+ 'background-color': {
+ // https://drafts.csswg.org/css-backgrounds-3/#background-color
+ types: [ 'color' ]
+ },
+ 'background-blend-mode': {
+ // https://drafts.fxtf.org/compositing-1/#propdef-background-blend-mode
+ types: [
+ { type: 'discrete', options: [ [ 'multiply', 'screen' ] ] }
+ ]
+ },
+ 'background-clip': {
+ // https://drafts.csswg.org/css-backgrounds-3/#background-clip
+ types: [
+ { type: 'discrete', options: [ [ 'padding-box', 'content-box' ] ] }
+ ]
+ },
+ 'background-image': {
+ // https://drafts.csswg.org/css-backgrounds-3/#background-image
+ types: [
+ { type: 'discrete',
+ options: [ [ 'url("http://localhost/test-1")',
+ 'url("http://localhost/test-2")' ] ] }
+ ]
+ },
+ 'background-origin': {
+ // https://drafts.csswg.org/css-backgrounds-3/#background-origin
+ types: [
+ { type: 'discrete', options: [ [ 'padding-box', 'content-box' ] ] }
+ ]
+ },
+ 'background-position': {
+ // https://drafts.csswg.org/css-backgrounds-3/#background-position
+ types: [
+ ]
+ },
+ 'background-position-x': {
+ // https://drafts.csswg.org/css-backgrounds-4/#propdef-background-position-x
+ types: [
+ ]
+ },
+ 'background-position-y': {
+ // https://drafts.csswg.org/css-backgrounds-4/#propdef-background-position-y
+ types: [
+ ]
+ },
+ 'background-repeat': {
+ // https://drafts.csswg.org/css-backgrounds-3/#background-repeat
+ types: [
+ { type: 'discrete', options: [ [ 'space', 'round' ] ] }
+ ]
+ },
+ 'background-size': {
+ // https://drafts.csswg.org/css-backgrounds-3/#background-size
+ types: [
+ ]
+ },
+ 'block-size': {
+ // https://drafts.csswg.org/css-logical-props/#propdef-block-size
+ types: [
+ ]
+ },
+ 'border-block-end-color': {
+ // https://drafts.csswg.org/css-logical-props/#propdef-border-block-end-color
+ types: [
+ ]
+ },
+ 'border-block-end-style': {
+ // https://drafts.csswg.org/css-logical-props/#propdef-border-block-end-style
+ types: [
+ ]
+ },
+ 'border-block-end-width': {
+ // https://drafts.csswg.org/css-logical-props/#propdef-border-block-end-width
+ types: [
+ ]
+ },
+ 'border-block-start-color': {
+ // https://drafts.csswg.org/css-logical-props/#propdef-border-block-start-color
+ types: [
+ ]
+ },
+ 'border-block-start-style': {
+ // https://drafts.csswg.org/css-logical-props/#propdef-border-block-start-style
+ types: [
+ ]
+ },
+ 'border-block-start-width': {
+ // https://drafts.csswg.org/css-logical-props/#propdef-border-block-start-width
+ types: [
+ ]
+ },
+ 'border-bottom-color': {
+ // https://drafts.csswg.org/css-backgrounds-3/#border-bottom-color
+ types: [ 'color' ]
+ },
+ 'border-bottom-left-radius': {
+ // https://drafts.csswg.org/css-backgrounds-3/#border-bottom-left-radius
+ types: [
+ ]
+ },
+ 'border-bottom-right-radius': {
+ // https://drafts.csswg.org/css-backgrounds-3/#border-bottom-right-radius
+ types: [
+ ]
+ },
+ 'border-bottom-style': {
+ // https://drafts.csswg.org/css-backgrounds-3/#border-bottom-style
+ types: [
+ { type: 'discrete', options: [ [ 'dotted', 'solid' ] ] }
+ ]
+ },
+ 'border-bottom-width': {
+ // https://drafts.csswg.org/css-backgrounds-3/#border-bottom-width
+ types: [ 'length' ],
+ setup: t => {
+ const element = createElement(t);
+ element.style.borderBottomStyle = 'solid';
+ return element;
+ }
+ },
+ 'border-collapse': {
+ // https://drafts.csswg.org/css-tables/#propdef-border-collapse
+ types: [
+ { type: 'discrete', options: [ [ 'collapse', 'separate' ] ] }
+ ]
+ },
+ 'border-inline-end-color': {
+ // https://drafts.csswg.org/css-logical-props/#propdef-border-inline-end-color
+ types: [
+ ]
+ },
+ 'border-inline-end-style': {
+ // https://drafts.csswg.org/css-logical-props/#propdef-border-inline-end-style
+ types: [
+ ]
+ },
+ 'border-inline-end-width': {
+ // https://drafts.csswg.org/css-logical-props/#propdef-border-inline-end-width
+ types: [
+ ]
+ },
+ 'border-inline-start-color': {
+ // https://drafts.csswg.org/css-logical-props/#propdef-border-inline-start-color
+ types: [
+ ]
+ },
+ 'border-inline-start-style': {
+ // https://drafts.csswg.org/css-logical-props/#propdef-border-block-start-style
+ types: [
+ ]
+ },
+ 'border-inline-start-width': {
+ // https://drafts.csswg.org/css-logical-props/#propdef-border-inline-start-width
+ types: [
+ ]
+ },
+ 'border-image-outset': {
+ // https://drafts.csswg.org/css-backgrounds-3/#border-image-outset
+ types: [
+ ]
+ },
+ 'border-image-repeat': {
+ // https://drafts.csswg.org/css-backgrounds-3/#border-image-repeat
+ types: [
+ { type: 'discrete', options: [ [ 'stretch repeat', 'round space' ] ] }
+ ]
+ },
+ 'border-image-slice': {
+ // https://drafts.csswg.org/css-backgrounds-3/#border-image-slice
+ types: [
+ ]
+ },
+ 'border-image-source': {
+ // https://drafts.csswg.org/css-backgrounds-3/#border-image-source
+ types: [
+ { type: 'discrete',
+ options: [ [ 'url("http://localhost/test-1")',
+ 'url("http://localhost/test-2")' ] ] }
+ ]
+ },
+ 'border-image-width': {
+ // https://drafts.csswg.org/css-backgrounds-3/#border-image-width
+ types: [
+ ]
+ },
+ 'border-left-color': {
+ // https://drafts.csswg.org/css-backgrounds-3/#border-left-color
+ types: [ 'color' ]
+ },
+ 'border-left-style': {
+ // https://drafts.csswg.org/css-backgrounds-3/#border-left-style
+ types: [
+ { type: 'discrete', options: [ [ 'dotted', 'solid' ] ] }
+ ]
+ },
+ 'border-left-width': {
+ // https://drafts.csswg.org/css-backgrounds-3/#border-left-width
+ types: [ 'length' ],
+ setup: t => {
+ const element = createElement(t);
+ element.style.borderLeftStyle = 'solid';
+ return element;
+ }
+ },
+ 'border-right-color': {
+ // https://drafts.csswg.org/css-backgrounds-3/#border-right-color
+ types: [ 'color' ]
+ },
+ 'border-right-style': {
+ // https://drafts.csswg.org/css-backgrounds-3/#border-right-style
+ types: [
+ { type: 'discrete', options: [ [ 'dotted', 'solid' ] ] }
+ ]
+ },
+ 'border-right-width': {
+ // https://drafts.csswg.org/css-backgrounds-3/#border-right-width
+ types: [ 'length' ],
+ setup: t => {
+ const element = createElement(t);
+ element.style.borderRightStyle = 'solid';
+ return element;
+ }
+ },
+ 'border-spacing': {
+ // https://drafts.csswg.org/css-tables/#propdef-border-spacing
+ types: [ 'lengthPair' ]
+ },
+ 'border-top-color': {
+ // https://drafts.csswg.org/css-backgrounds-3/#border-top-color
+ types: [ 'color' ]
+ },
+ 'border-top-left-radius': {
+ // https://drafts.csswg.org/css-backgrounds-3/#border-top-left-radius
+ types: [
+ ]
+ },
+ 'border-top-right-radius': {
+ // https://drafts.csswg.org/css-backgrounds-3/#border-top-right-radius
+ types: [
+ ]
+ },
+ 'border-top-style': {
+ // https://drafts.csswg.org/css-backgrounds-3/#border-top-style
+ types: [
+ { type: 'discrete', options: [ [ 'dotted', 'solid' ] ] }
+ ]
+ },
+ 'border-top-width': {
+ // https://drafts.csswg.org/css-backgrounds-3/#border-top-width
+ types: [ 'length' ],
+ setup: t => {
+ const element = createElement(t);
+ element.style.borderTopStyle = 'solid';
+ return element;
+ }
+ },
+ 'bottom': {
+ // https://drafts.csswg.org/css-position/#propdef-bottom
+ types: [
+ ]
+ },
+ 'box-decoration-break': {
+ // https://drafts.csswg.org/css-break/#propdef-box-decoration-break
+ types: [
+ { type: 'discrete', options: [ [ 'slice', 'clone' ] ] }
+ ]
+ },
+ 'box-shadow': {
+ // https://drafts.csswg.org/css-backgrounds/#box-shadow
+ types: [ 'boxShadowList' ],
+ },
+ 'box-sizing': {
+ // https://drafts.csswg.org/css-ui-4/#box-sizing
+ types: [
+ { type: 'discrete', options: [ [ 'content-box', 'border-box' ] ] }
+ ]
+ },
+ 'caption-side': {
+ // https://drafts.csswg.org/css-tables/#propdef-caption-side
+ types: [
+ { type: 'discrete', options: [ [ 'top', 'bottom' ] ] }
+ ]
+ },
+ 'caret-color': {
+ // https://drafts.csswg.org/css-ui/#propdef-caret-color
+ types: [ 'color' ]
+ },
+ 'clear': {
+ // https://drafts.csswg.org/css-page-floats/#propdef-clear
+ types: [
+ { type: 'discrete', options: [ [ 'left', 'right' ] ] }
+ ]
+ },
+ 'clip': {
+ // https://drafts.fxtf.org/css-masking-1/#propdef-clip
+ types: [
+ 'rect',
+ { type: 'discrete', options: [ [ 'rect(10px, 10px, 10px, 10px)',
+ 'auto' ],
+ [ 'rect(10px, 10px, 10px, 10px)',
+ 'rect(10px, 10px, 10px, auto)'] ] }
+ ]
+ },
+ 'clip-path': {
+ // https://drafts.fxtf.org/css-masking-1/#propdef-clip-path
+ types: [
+ ]
+ },
+ 'clip-rule': {
+ // https://drafts.fxtf.org/css-masking-1/#propdef-clip-rule
+ types: [
+ { type: 'discrete', options: [ [ 'evenodd', 'nonzero' ] ] }
+ ]
+ },
+ 'color': {
+ // https://drafts.csswg.org/css-color/#propdef-color
+ types: [ 'color' ]
+ },
+ 'color-adjust': {
+ // https://drafts.csswg.org/css-color-4/#color-adjust
+ types: [
+ { type: 'discrete', options: [ [ 'economy', 'exact' ] ] }
+ ]
+ },
+ 'color-interpolation': {
+ // https://svgwg.org/svg2-draft/painting.html#ColorInterpolationProperty
+ types: [
+ { type: 'discrete', options: [ [ 'linearrgb', 'auto' ] ] }
+ ]
+ },
+ 'color-interpolation-filters': {
+ // https://drafts.fxtf.org/filters-1/#propdef-color-interpolation-filters
+ types: [
+ { type: 'discrete', options: [ [ 'srgb', 'linearrgb' ] ] }
+ ]
+ },
+ 'column-count': {
+ // https://drafts.csswg.org/css-multicol/#propdef-column-count
+ types: [ 'positiveInteger',
+ { type: 'discrete', options: [ [ 'auto', '10' ] ] }
+ ]
+ },
+ 'column-gap': {
+ // https://drafts.csswg.org/css-multicol/#propdef-column-gap
+ types: [ 'length',
+ { type: 'discrete', options: [ [ 'normal', '200px' ] ] }
+ ]
+ },
+ 'column-rule-color': {
+ // https://drafts.csswg.org/css-multicol/#propdef-column-rule-color
+ types: [ 'color' ]
+ },
+ 'column-fill': {
+ // https://drafts.csswg.org/css-multicol/#propdef-column-fill
+ types: [
+ { type: 'discrete', options: [ [ 'auto', 'balance' ] ] }
+ ]
+ },
+ 'column-rule-style': {
+ // https://drafts.csswg.org/css-multicol/#propdef-column-rule-style
+ types: [
+ { type: 'discrete', options: [ [ 'none', 'dotted' ] ] }
+ ]
+ },
+ 'column-rule-width': {
+ // https://drafts.csswg.org/css-multicol/#propdef-column-rule-width
+ types: [ 'length' ],
+ setup: t => {
+ const element = createElement(t);
+ element.style.columnRuleStyle = 'solid';
+ return element;
+ }
+ },
+ 'column-width': {
+ // https://drafts.csswg.org/css-multicol/#propdef-column-width
+ types: [ 'length',
+ { type: 'discrete', options: [ [ 'auto', '1px' ] ] }
+ ]
+ },
+ 'counter-increment': {
+ // https://drafts.csswg.org/css-lists-3/#propdef-counter-increment
+ types: [
+ { type: 'discrete', options: [ [ 'ident-1 1', 'ident-2 2' ] ] }
+ ]
+ },
+ 'counter-reset': {
+ // https://drafts.csswg.org/css-lists-3/#propdef-counter-reset
+ types: [
+ { type: 'discrete', options: [ [ 'ident-1 1', 'ident-2 2' ] ] }
+ ]
+ },
+ 'cursor': {
+ // https://drafts.csswg.org/css2/ui.html#propdef-cursor
+ types: [
+ { type: 'discrete', options: [ [ 'pointer', 'wait' ] ] }
+ ]
+ },
+ 'dominant-baseline': {
+ // https://drafts.csswg.org/css-inline/#propdef-dominant-baseline
+ types: [
+ { type: 'discrete', options: [ [ 'ideographic', 'alphabetic' ] ] }
+ ]
+ },
+ 'empty-cells': {
+ // https://drafts.csswg.org/css-tables/#propdef-empty-cells
+ types: [
+ { type: 'discrete', options: [ [ 'show', 'hide' ] ] }
+ ]
+ },
+ 'field-sizing': {
+ // https://drafts.csswg.org/css-ui/#field-sizing
+ types: [
+ { type: 'discrete', options: [ [ 'fixed', 'content' ] ] }
+ ]
+ },
+ 'fill': {
+ // https://svgwg.org/svg2-draft/painting.html#FillProperty
+ types: [
+ ]
+ },
+ 'fill-opacity': {
+ // https://svgwg.org/svg2-draft/painting.html#FillOpacityProperty
+ types: [ 'opacity' ]
+ },
+ 'fill-rule': {
+ // https://svgwg.org/svg2-draft/painting.html#FillRuleProperty
+ types: [
+ { type: 'discrete', options: [ [ 'evenodd', 'nonzero' ] ] }
+ ]
+ },
+ 'filter': {
+ // https://drafts.fxtf.org/filters/#propdef-filter
+ types: [ 'filterList' ]
+ },
+ 'flex-basis': {
+ // https://drafts.csswg.org/css-flexbox/#propdef-flex-basis
+ types: [
+ 'lengthPercentageOrCalc',
+ { type: 'discrete', options: [ [ 'auto', '10px' ] ] }
+ ]
+ },
+ 'flex-direction': {
+ // https://drafts.csswg.org/css-flexbox/#propdef-flex-direction
+ types: [
+ { type: 'discrete', options: [ [ 'row', 'row-reverse' ] ] }
+ ]
+ },
+ 'flex-grow': {
+ // https://drafts.csswg.org/css-flexbox/#flex-grow-property
+ types: [ 'positiveNumber' ]
+ },
+ 'flex-shrink': {
+ // https://drafts.csswg.org/css-flexbox/#propdef-flex-shrink
+ types: [ 'positiveNumber' ]
+ },
+ 'flex-wrap': {
+ // https://drafts.csswg.org/css-flexbox/#propdef-flex-wrap
+ types: [
+ { type: 'discrete', options: [ [ 'nowrap', 'wrap' ] ] }
+ ]
+ },
+ 'flood-color': {
+ // https://drafts.fxtf.org/filters/#FloodColorProperty
+ types: [ 'color' ]
+ },
+ 'flood-opacity': {
+ // https://drafts.fxtf.org/filters/#propdef-flood-opacity
+ types: [ 'opacity' ]
+ },
+ 'font-size': {
+ // https://drafts.csswg.org/css-fonts-3/#propdef-font-size
+ types: [
+ ]
+ },
+ 'font-size-adjust': {
+ // https://drafts.csswg.org/css-fonts-3/#propdef-font-size-adjust
+ types: [
+ ]
+ },
+ 'font-stretch': {
+ // https://drafts.csswg.org/css-fonts-4/#propdef-font-stretch
+ types: [ 'percentage' ]
+ },
+ 'font-style': {
+ // https://drafts.csswg.org/css-fonts/#propdef-font-style
+ types: [
+ { type: 'discrete', options: [ [ 'italic', 'oblique' ] ] }
+ ]
+ },
+ 'float': {
+ // https://drafts.csswg.org/css-page-floats/#propdef-float
+ types: [
+ { type: 'discrete', options: [ [ 'left', 'right' ] ] }
+ ]
+ },
+ 'font-family': {
+ // https://drafts.csswg.org/css-fonts-3/#propdef-font-family
+ types: [
+ { type: 'discrete', options: [ [ 'helvetica', 'verdana' ] ] }
+ ]
+ },
+ 'font-feature-settings': {
+ // https://drafts.csswg.org/css-fonts/#descdef-font-feature-settings
+ types: [
+ { type: 'discrete', options: [ [ '"liga" 5', 'normal' ] ] }
+ ]
+ },
+ 'font-kerning': {
+ // https://drafts.csswg.org/css-fonts-3/#propdef-font-kerning
+ types: [
+ { type: 'discrete', options: [ [ 'auto', 'normal' ] ] }
+ ]
+ },
+ 'font-language-override': {
+ // https://drafts.csswg.org/css-fonts-3/#propdef-font-language-override
+ types: [
+ { type: 'discrete', options: [ [ '"eng"', 'normal' ] ] }
+ ]
+ },
+ 'font-style': {
+ // https://drafts.csswg.org/css-fonts-3/#propdef-font-style
+ types: [
+ { type: 'discrete', options: [ [ 'italic', 'oblique' ] ] }
+ ]
+ },
+ 'font-synthesis': {
+ // https://drafts.csswg.org/css-fonts-3/#propdef-font-synthesis
+ types: [
+ { type: 'discrete', options: [ [ 'none', 'weight style' ] ] }
+ ]
+ },
+ 'font-variant-alternates': {
+ // https://drafts.csswg.org/css-fonts-3/#propdef-font-variant-alternates
+ types: [
+ { type: 'discrete',
+ options: [ [ 'swash(unknown)', 'stylistic(unknown)' ] ] }
+ ]
+ },
+ 'font-variant-caps': {
+ // https://drafts.csswg.org/css-fonts-3/#propdef-font-variant-caps
+ types: [
+ { type: 'discrete', options: [ [ 'small-caps', 'unicase' ] ] }
+ ]
+ },
+ 'font-variant-east-asian': {
+ // https://drafts.csswg.org/css-fonts-3/#propdef-font-variant-east-asian
+ types: [
+ { type: 'discrete', options: [ [ 'full-width', 'proportional-width' ] ] }
+ ]
+ },
+ 'font-variant-emoji': {
+ // https://drafts.csswg.org/css-fonts/#propdef-font-variant-emoji
+ types: [
+ { type: 'discrete', options: [ [ 'text', 'emoji' ] ] }
+ ]
+ },
+ 'font-variant-ligatures': {
+ // https://drafts.csswg.org/css-fonts-3/#propdef-font-variant-ligatures
+ types: [
+ { type: 'discrete',
+ options: [ [ 'common-ligatures', 'no-common-ligatures' ] ] }
+ ]
+ },
+ 'font-variant-numeric': {
+ // https://drafts.csswg.org/css-fonts-3/#propdef-font-variant-numeric
+ types: [
+ { type: 'discrete', options: [ [ 'lining-nums', 'oldstyle-nums' ] ] }
+ ]
+ },
+ 'font-variant-position': {
+ // https://drafts.csswg.org/css-fonts-3/#propdef-font-variant-position
+ types: [
+ { type: 'discrete', options: [ [ 'sub', 'super' ] ] }
+ ]
+ },
+ 'font-variation-settings': {
+ // https://drafts.csswg.org/css-fonts-4/#descdef-font-face-font-variation-settings
+ types: [
+ 'fontVariationSettings',
+ { type: 'discrete',
+ options: [ ['"wdth" 1, "wght" 1.1', '"wdth" 5'],
+ ['"wdth" 5', 'normal']
+ ] },
+ ]
+ },
+ 'font-weight': {
+ // https://drafts.csswg.org/css-fonts-3/#propdef-font-weight
+ types: [
+ ]
+ },
+ 'grid-auto-columns': {
+ // https://drafts.csswg.org/css-grid/#propdef-grid-auto-columns
+ types: [
+ { type: 'discrete', options: [ [ '1px', '5px' ] ] }
+ ]
+ },
+ 'grid-auto-flow': {
+ // https://drafts.csswg.org/css-grid/#propdef-grid-auto-flow
+ types: [
+ { type: 'discrete', options: [ [ 'row', 'column' ] ] }
+ ]
+ },
+ 'grid-auto-rows': {
+ // https://drafts.csswg.org/css-grid/#propdef-grid-auto-rows
+ types: [
+ { type: 'discrete', options: [ [ '1px', '5px' ] ] }
+ ]
+ },
+ 'grid-column-end': {
+ // https://drafts.csswg.org/css-grid/#propdef-grid-column-end
+ types: [
+ { type: 'discrete', options: [ [ '1', '5' ] ] }
+ ]
+ },
+ 'grid-column-gap': {
+ // https://drafts.csswg.org/css-grid/#propdef-grid-column-gap
+ types: [
+ ]
+ },
+ 'grid-column-start': {
+ // https://drafts.csswg.org/css-grid/#propdef-grid-column-start
+ types: [
+ { type: 'discrete', options: [ [ '1', '5' ] ] }
+ ]
+ },
+ 'grid-row-end': {
+ // https://drafts.csswg.org/css-grid/#propdef-grid-row-end
+ types: [
+ { type: 'discrete', options: [ [ '1', '5' ] ] }
+ ]
+ },
+ 'grid-row-gap': {
+ // https://drafts.csswg.org/css-grid/#propdef-grid-row-gap
+ types: [
+ ]
+ },
+ 'grid-row-start': {
+ // https://drafts.csswg.org/css-grid/#propdef-grid-row-start
+ types: [
+ { type: 'discrete', options: [ [ '1', '5' ] ] }
+ ]
+ },
+ 'grid-template-areas': {
+ // https://drafts.csswg.org/css-template/#grid-template-areas
+ types: [
+ { type: 'discrete', options: [ [ '". . a b" ". . a b"', 'none' ] ] }
+ ]
+ },
+ 'height': {
+ // https://drafts.csswg.org/css21/visudet.html#propdef-height
+ types: [
+ ]
+ },
+ 'hyphens': {
+ // https://drafts.csswg.org/css-text-3/#propdef-hyphens
+ types: [
+ { type: 'discrete', options: [ [ 'manual', 'none' ] ] }
+ ]
+ },
+ 'image-orientation': {
+ // https://drafts.csswg.org/css-images-3/#propdef-image-orientation
+ types: [
+ { type: 'discrete', options: [ [ 'none', 'from-image' ] ] }
+ ]
+ },
+ 'image-rendering': {
+ // https://drafts.csswg.org/css-images-3/#propdef-image-rendering
+ types: [
+ ]
+ },
+ 'ime-mode': {
+ // https://drafts.csswg.org/css-ui/#input-method-editor
+ types: [
+ { type: 'discrete', options: [ [ 'disabled', 'auto' ] ] }
+ ]
+ },
+ 'initial-letter': {
+ // https://drafts.csswg.org/css-inline/#propdef-initial-letter
+ types: [
+ { type: 'discrete', options: [ [ '1 2', '3 4' ] ] }
+ ]
+ },
+};
+
+const gCSSProperties2 = {
+ 'inline-size': {
+ // https://drafts.csswg.org/css-logical-props/#propdef-inline-size
+ types: [
+ ]
+ },
+ 'isolation': {
+ // https://drafts.fxtf.org/compositing-1/#propdef-isolation
+ types: [
+ { type: 'discrete', options: [ [ 'auto', 'isolate' ] ] }
+ ]
+ },
+ 'justify-content': {
+ // https://drafts.csswg.org/css-align/#propdef-justify-content
+ types: [
+ { type: 'discrete', options: [ [ 'start', 'end' ] ] }
+ ]
+ },
+ 'justify-items': {
+ // https://drafts.csswg.org/css-align/#propdef-justify-items
+ types: [
+ { type: 'discrete', options: [ [ 'start', 'end' ] ] }
+ ]
+ },
+ 'justify-self': {
+ // https://drafts.csswg.org/css-align/#propdef-justify-self
+ types: [
+ { type: 'discrete', options: [ [ 'start', 'end' ] ] }
+ ]
+ },
+ 'left': {
+ // https://drafts.csswg.org/css-position/#propdef-left
+ types: [
+ ]
+ },
+ 'letter-spacing': {
+ // https://drafts.csswg.org/css-text-3/#propdef-letter-spacing
+ types: [ 'length' ]
+ },
+ 'lighting-color': {
+ // https://drafts.fxtf.org/filters/#LightingColorProperty
+ types: [ 'color' ]
+ },
+ 'line-height': {
+ // https://drafts.csswg.org/css-inline/#line-height-property
+ types: [
+ { type: 'discrete', options: [ [ 'normal', '10px' ],
+ [ 'normal', '10', 'normal', '100px' ] ] }
+ ]
+ },
+ 'list-style-image': {
+ // https://drafts.csswg.org/css-lists-3/#propdef-list-style-image
+ types: [
+ { type: 'discrete',
+ options: [ [ 'url("http://localhost/test-1")',
+ 'url("http://localhost/test-2")' ] ] }
+ ]
+ },
+ 'list-style-position': {
+ // https://drafts.csswg.org/css-lists-3/#propdef-list-style-position
+ types: [
+ { type: 'discrete', options: [ [ 'inside', 'outside' ] ] }
+ ]
+ },
+ 'list-style-type': {
+ // https://drafts.csswg.org/css-lists-3/#propdef-list-style-type
+ types: [
+ { type: 'discrete', options: [ [ 'circle', 'square' ] ] }
+ ]
+ },
+ 'margin-block-end': {
+ // https://drafts.csswg.org/css-logical-props/#propdef-margin-block-end
+ types: [
+ ]
+ },
+ 'margin-block-start': {
+ // https://drafts.csswg.org/css-logical-props/#propdef-margin-block-start
+ types: [
+ ]
+ },
+ 'margin-bottom': {
+ // https://drafts.csswg.org/css-box/#propdef-margin-bottom
+ types: [
+ ]
+ },
+ 'margin-inline-end': {
+ // https://drafts.csswg.org/css-logical-props/#propdef-margin-inline-end
+ types: [
+ ]
+ },
+ 'margin-inline-start': {
+ // https://drafts.csswg.org/css-logical-props/#propdef-margin-inline-start
+ types: [
+ ]
+ },
+ 'margin-left': {
+ // https://drafts.csswg.org/css-box/#propdef-margin-left
+ types: [
+ ]
+ },
+ 'margin-right': {
+ // https://drafts.csswg.org/css-box/#propdef-margin-right
+ types: [
+ ]
+ },
+ 'margin-top': {
+ // https://drafts.csswg.org/css-box/#propdef-margin-top
+ types: [
+ ]
+ },
+ 'marker-end': {
+ // https://svgwg.org/specs/markers/#MarkerEndProperty
+ types: [
+ { type: 'discrete',
+ options: [ [ 'url("http://localhost/test-1")',
+ 'url("http://localhost/test-2")' ] ] }
+ ]
+ },
+ 'marker-mid': {
+ // https://svgwg.org/specs/markers/#MarkerMidProperty
+ types: [
+ { type: 'discrete',
+ options: [ [ 'url("http://localhost/test-1")',
+ 'url("http://localhost/test-2")' ] ] }
+ ]
+ },
+ 'marker-start': {
+ // https://svgwg.org/specs/markers/#MarkerStartProperty
+ types: [
+ { type: 'discrete',
+ options: [ [ 'url("http://localhost/test-1")',
+ 'url("http://localhost/test-2")' ] ] }
+ ]
+ },
+ 'mask': {
+ // https://drafts.fxtf.org/css-masking-1/#the-mask
+ types: [
+ { type: 'discrete',
+ options: [ [ 'url("http://localhost/test-1")',
+ 'url("http://localhost/test-2")' ] ] }
+ ]
+ },
+ 'mask-clip': {
+ // https://drafts.fxtf.org/css-masking-1/#propdef-mask-clip
+ types: [
+ { type: 'discrete', options: [ [ 'content-box', 'border-box' ] ] }
+ ]
+ },
+ 'mask-composite': {
+ // https://drafts.fxtf.org/css-masking-1/#propdef-mask-composite
+ types: [
+ { type: 'discrete', options: [ [ 'add', 'subtract' ] ] }
+ ]
+ },
+ 'mask-image': {
+ // https://drafts.fxtf.org/css-masking-1/#propdef-mask-image
+ types: [
+ { type: 'discrete',
+ options: [ [ 'url("http://localhost/test-1")',
+ 'url("http://localhost/test-2")' ] ] }
+ ]
+ },
+ 'mask-mode': {
+ // https://drafts.fxtf.org/css-masking-1/#propdef-mask-mode
+ types: [
+ { type: 'discrete', options: [ [ 'alpha', 'luminance' ] ] }
+ ]
+ },
+ 'mask-origin': {
+ // https://drafts.fxtf.org/css-masking-1/#propdef-mask-origin
+ types: [
+ { type: 'discrete', options: [ [ 'content-box', 'border-box' ] ] }
+ ]
+ },
+ 'mask-position': {
+ // https://drafts.fxtf.org/css-masking-1/#propdef-mask-position
+ types: [
+ ]
+ },
+ 'mask-position-x': {
+ // https://lists.w3.org/Archives/Public/www-style/2014Jun/0166.html
+ types: [
+ ]
+ },
+ 'mask-position-y': {
+ // https://lists.w3.org/Archives/Public/www-style/2014Jun/0166.html
+ types: [
+ ]
+ },
+ 'mask-repeat': {
+ // https://drafts.fxtf.org/css-masking-1/#propdef-mask-repeat
+ types: [
+ { type: 'discrete', options: [ [ 'space', 'round' ] ] }
+ ]
+ },
+ 'mask-size': {
+ // https://drafts.fxtf.org/css-masking-1/#propdef-mask-size
+ types: [
+ ]
+ },
+ 'mask-type': {
+ // https://drafts.fxtf.org/css-masking-1/#propdef-mask-type
+ types: [
+ { type: 'discrete', options: [ [ 'alpha', 'luminance' ] ] }
+ ]
+ },
+ 'max-block-size': {
+ // https://drafts.csswg.org/css-logical-props/#propdef-max-block-size
+ types: [
+ ]
+ },
+ 'max-height': {
+ // https://drafts.csswg.org/css21/visudet.html#propdef-max-height
+ types: [
+ ]
+ },
+ 'max-inline-size': {
+ // https://drafts.csswg.org/css-logical-props/#propdef-max-inline-size
+ types: [
+ ]
+ },
+ 'max-width': {
+ // https://drafts.csswg.org/css21/visudet.html#propdef-max-width
+ types: [
+ ]
+ },
+ 'min-block-size': {
+ // https://drafts.csswg.org/css-logical-props/#propdef-min-block-size
+ types: [
+ ]
+ },
+ 'min-height': {
+ // https://drafts.csswg.org/css21/visudet.html#propdef-min-height
+ types: [
+ ]
+ },
+ 'min-inline-size': {
+ // https://drafts.csswg.org/css-logical-props/#propdef-min-inline-size
+ types: [
+ ]
+ },
+ 'min-width': {
+ // https://drafts.csswg.org/css21/visudet.html#propdef-min-width
+ types: [
+ ]
+ },
+ 'mix-blend-mode': {
+ // https://drafts.fxtf.org/compositing-1/#propdef-mix-blend-mode
+ types: [
+ { type: 'discrete', options: [ [ 'multiply', 'screen' ] ] }
+ ]
+ },
+ 'object-fit': {
+ // https://drafts.csswg.org/css-images-3/#propdef-object-fit
+ types: [
+ { type: 'discrete', options: [ [ 'fill', 'contain' ] ] }
+ ]
+ },
+ 'object-position': {
+ // https://drafts.csswg.org/css-images-3/#propdef-object-position
+ types: [
+ ]
+ },
+ 'inset-block-end': {
+ // https://drafts.csswg.org/css-logical-props/#propdef-inset-block-end
+ types: [
+ ]
+ },
+ 'inset-block-start': {
+ // https://drafts.csswg.org/css-logical-props/#propdef-inset-block-start
+ types: [
+ ]
+ },
+ 'inset-inline-end': {
+ // https://drafts.csswg.org/css-logical-props/#propdef-inset-inline-end
+ types: [
+ ]
+ },
+ 'inset-inline-start': {
+ // https://drafts.csswg.org/css-logical-props/#propdef-inset-inline-start
+ types: [
+ ]
+ },
+ 'offset-distance': {
+ // https://drafts.fxtf.org/motion-1/#offset-distance-property
+ types: [ 'lengthPercentageOrCalc' ]
+ },
+ 'offset-path': {
+ // https://drafts.fxtf.org/motion-1/#offset-path-property
+ types: [
+ ]
+ },
+ 'opacity': {
+ // https://drafts.csswg.org/css-color/#propdef-opacity
+ types: [
+ ]
+ },
+ 'order': {
+ // https://drafts.csswg.org/css-flexbox/#propdef-order
+ types: [ 'integer' ]
+ },
+ 'outline-color': {
+ // https://drafts.csswg.org/css-ui-3/#propdef-outline-color
+ types: [ 'color' ]
+ },
+ 'outline-offset': {
+ // https://drafts.csswg.org/css-ui-3/#propdef-outline-offset
+ types: [ 'length' ]
+ },
+ 'outline-style': {
+ // https://drafts.csswg.org/css-ui/#propdef-outline-style
+ types: [
+ { type: 'discrete', options: [ [ 'none', 'dotted' ] ] }
+ ]
+ },
+ 'outline-width': {
+ // https://drafts.csswg.org/css-ui-3/#propdef-outline-width
+ types: [ 'length' ],
+ setup: t => {
+ const element = createElement(t);
+ element.style.outlineStyle = 'solid';
+ return element;
+ }
+ },
+ 'overflow': {
+ // https://drafts.csswg.org/css-overflow/#propdef-overflow
+ types: [
+ ]
+ },
+ 'overflow-wrap': {
+ // https://drafts.csswg.org/css-text-3/#propdef-overflow-wrap
+ types: [
+ { type: 'discrete', options: [ [ 'normal', 'break-word' ] ] }
+ ]
+ },
+ 'overflow-x': {
+ // https://drafts.csswg.org/css-overflow-3/#propdef-overflow-x
+ types: [
+ { type: 'discrete', options: [ [ 'visible', 'hidden' ] ] }
+ ]
+ },
+ 'overflow-y': {
+ // https://drafts.csswg.org/css-overflow-3/#propdef-overflow-y
+ types: [
+ { type: 'discrete', options: [ [ 'visible', 'hidden' ] ] }
+ ]
+ },
+ 'padding-block-end': {
+ // https://drafts.csswg.org/css-logical-props/#propdef-padding-block-end
+ types: [
+ ]
+ },
+ 'padding-block-start': {
+ // https://drafts.csswg.org/css-logical-props/#propdef-padding-block-start
+ types: [
+ ]
+ },
+ 'padding-bottom': {
+ // https://drafts.csswg.org/css-box/#propdef-padding-bottom
+ types: [
+ ]
+ },
+ 'padding-inline-end': {
+ // https://drafts.csswg.org/css-logical-props/#propdef-padding-inline-end
+ types: [
+ ]
+ },
+ 'padding-inline-start': {
+ // https://drafts.csswg.org/css-logical-props/#propdef-padding-inline-start
+ types: [
+ ]
+ },
+ 'padding-left': {
+ // https://drafts.csswg.org/css-box/#propdef-padding-left
+ types: [
+ ]
+ },
+ 'padding-right': {
+ // https://drafts.csswg.org/css-box/#propdef-padding-right
+ types: [
+ ]
+ },
+ 'padding-top': {
+ // https://drafts.csswg.org/css-box/#propdef-padding-top
+ types: [
+ ]
+ },
+ 'page-break-after': {
+ // https://drafts.csswg.org/css-break-3/#propdef-break-after
+ types: [
+ { type: 'discrete', options: [ [ 'always', 'auto' ] ] }
+ ]
+ },
+ 'page-break-before': {
+ // https://drafts.csswg.org/css-break-3/#propdef-break-before
+ types: [
+ { type: 'discrete', options: [ [ 'always', 'auto' ] ] }
+ ]
+ },
+ 'page-break-inside': {
+ // https://drafts.csswg.org/css-break-3/#propdef-break-inside
+ types: [
+ { type: 'discrete', options: [ [ 'auto', 'avoid' ] ] }
+ ]
+ },
+ 'paint-order': {
+ // https://svgwg.org/svg2-draft/painting.html#PaintOrderProperty
+ types: [
+ { type: 'discrete', options: [ [ 'fill', 'stroke' ] ] }
+ ]
+ },
+ 'perspective': {
+ // https://drafts.csswg.org/css-transforms-1/#propdef-perspective
+ types: [ 'length' ]
+ },
+ 'perspective-origin': {
+ // https://drafts.csswg.org/css-transforms-1/#propdef-perspective-origin
+ types: [ 'position' ]
+ },
+ 'pointer-events': {
+ // https://svgwg.org/svg2-draft/interact.html#PointerEventsProperty
+ types: [
+ { type: 'discrete', options: [ [ 'fill', 'none' ] ] }
+ ]
+ },
+ 'position': {
+ // https://drafts.csswg.org/css-position/#propdef-position
+ types: [
+ { type: 'discrete', options: [ [ 'absolute', 'fixed' ] ] }
+ ]
+ },
+ 'quotes': {
+ // https://drafts.csswg.org/css-content-3/#propdef-quotes
+ types: [
+ { type: 'discrete', options: [ [ '"“" "”" "‘" "’"', '"‘" "’" "“" "”"' ] ] }
+ ]
+ },
+ 'resize': {
+ // https://drafts.csswg.org/css-ui/#propdef-resize
+ types: [
+ { type: 'discrete', options: [ [ 'both', 'horizontal' ] ] }
+ ]
+ },
+ 'right': {
+ // https://drafts.csswg.org/css-position/#propdef-right
+ types: [
+ ]
+ },
+ 'ruby-align': {
+ // https://drafts.csswg.org/css-ruby-1/#propdef-ruby-align
+ types: [
+ { type: 'discrete', options: [ [ 'start', 'center' ] ] }
+ ]
+ },
+ 'ruby-position': {
+ // https://drafts.csswg.org/css-ruby-1/#propdef-ruby-position
+ types: [
+ { type: 'discrete', options: [ [ 'under', 'over' ] ] }
+ ],
+ setup: t => {
+ return createElement(t, 'ruby');
+ }
+ },
+ 'scroll-behavior': {
+ // https://drafts.csswg.org/cssom-view/#propdef-scroll-behavior
+ types: [
+ { type: 'discrete', options: [ [ 'auto', 'smooth' ] ] }
+ ]
+ },
+ 'scrollbar-color': {
+ // https://drafts.csswg.org/css-scrollbars/#propdef-scrollbar-color
+ types: [ 'colorPair' ]
+ },
+ 'scrollbar-gutter': {
+ // https://drafts.csswg.org/css-overflow/#propdef-scrollbar-gutter
+ types: [
+ { type: 'discrete', options: [ [ 'auto', 'stable' ], [ 'auto', 'stable both-edges' ], [ 'stable', 'stable both-edges' ] ] }
+ ]
+ },
+ 'scrollbar-width': {
+ // https://drafts.csswg.org/css-scrollbars/#propdef-scrollbar-width
+ types: [
+ { type: 'discrete', options: [ [ 'auto', 'thin' ], [ 'auto', 'none' ], [ 'thin', 'none' ] ] }
+ ]
+ },
+ 'shape-outside': {
+ // http://dev.w3.org/csswg/css-shapes/#propdef-shape-outside
+ types: [
+ { type: 'discrete',
+ options: [ [ 'url("http://localhost/test-1")',
+ 'url("http://localhost/test-2")' ] ] }
+ ]
+ },
+ 'shape-rendering': {
+ // https://svgwg.org/svg2-draft/painting.html#ShapeRenderingProperty
+ types: [
+ { type: 'discrete', options: [ [ 'optimizeSpeed', 'crispEdges' ] ] }
+ ]
+ },
+ 'stop-color': {
+ // https://svgwg.org/svg2-draft/pservers.html#StopColorProperty
+ types: [ 'color' ]
+ },
+ 'stop-opacity': {
+ // https://svgwg.org/svg2-draft/pservers.html#StopOpacityProperty
+ types: [ 'opacity' ]
+ },
+ 'stroke': {
+ // https://svgwg.org/svg2-draft/painting.html#StrokeProperty
+ types: [
+ ]
+ },
+ 'stroke-dasharray': {
+ // https://svgwg.org/svg2-draft/painting.html#StrokeDasharrayProperty
+ types: [
+ 'dasharray',
+ { type: 'discrete', options: [ [ 'none', '10px, 20px' ] ] }
+ ]
+ },
+ 'stroke-dashoffset': {
+ // https://svgwg.org/svg2-draft/painting.html#StrokeDashoffsetProperty
+ types: [
+ ]
+ },
+ 'stroke-linecap': {
+ // https://svgwg.org/svg2-draft/painting.html#StrokeLinecapProperty
+ types: [
+ { type: 'discrete', options: [ [ 'round', 'square' ] ] }
+ ]
+ },
+ 'stroke-linejoin': {
+ // https://svgwg.org/svg2-draft/painting.html#StrokeLinejoinProperty
+ types: [
+ { type: 'discrete', options: [ [ 'round', 'miter' ] ] }
+ ],
+ setup: t => {
+ return createElement(t, 'rect');
+ }
+ },
+ 'stroke-miterlimit': {
+ // https://svgwg.org/svg2-draft/painting.html#StrokeMiterlimitProperty
+ types: [ 'positiveNumber' ]
+ },
+ 'stroke-opacity': {
+ // https://svgwg.org/svg2-draft/painting.html#StrokeOpacityProperty
+ types: [ 'opacity' ]
+ },
+ 'stroke-width': {
+ // https://svgwg.org/svg2-draft/painting.html#StrokeWidthProperty
+ types: [
+ ]
+ },
+ 'table-layout': {
+ // https://drafts.csswg.org/css-tables/#propdef-table-layout
+ types: [
+ { type: 'discrete', options: [ [ 'auto', 'fixed' ] ] }
+ ]
+ },
+ 'text-align': {
+ // https://drafts.csswg.org/css-text-3/#propdef-text-align
+ types: [
+ { type: 'discrete', options: [ [ 'start', 'end' ] ] }
+ ]
+ },
+ 'text-align-last': {
+ // https://drafts.csswg.org/css-text-3/#propdef-text-align-last
+ types: [
+ { type: 'discrete', options: [ [ 'start', 'end' ] ] }
+ ]
+ },
+ 'text-anchor': {
+ // https://svgwg.org/svg2-draft/text.html#TextAnchorProperty
+ types: [
+ { type: 'discrete', options: [ [ 'middle', 'end' ] ] }
+ ]
+ },
+ 'text-autospace': {
+ // https://drafts.csswg.org/css-text-4/#text-autospace-property
+ types: [
+ { type: 'discrete', options: [ [ 'normal', 'no-autospace' ] ] }
+ ]
+ },
+ 'text-decoration-color': {
+ // https://drafts.csswg.org/css-text-decor-3/#propdef-text-decoration-color
+ types: [ 'color' ]
+ },
+ 'text-decoration-line': {
+ // https://drafts.csswg.org/css-text-decor-3/#propdef-text-decoration-line
+ types: [
+ { type: 'discrete', options: [ [ 'underline', 'overline' ] ] }
+ ]
+ },
+ 'text-decoration-style': {
+ // http://dev.w3.org/csswg/css-text-decor-3/#propdef-text-decoration-style
+ types: [
+ { type: 'discrete', options: [ [ 'solid', 'dotted' ] ] }
+ ]
+ },
+ 'text-emphasis-color': {
+ // https://drafts.csswg.org/css-text-decor-3/#propdef-text-emphasis-color
+ types: [ 'color' ]
+ },
+ 'text-emphasis-position': {
+ // http://dev.w3.org/csswg/css-text-decor-3/#propdef-text-emphasis-position
+ types: [
+ { type: 'discrete', options: [ [ 'over', 'under left' ] ] }
+ ]
+ },
+ 'text-emphasis-style': {
+ // http://dev.w3.org/csswg/css-text-decor-3/#propdef-text-emphasis-style
+ types: [
+ { type: 'discrete', options: [ [ 'circle', 'open dot' ] ] }
+ ]
+ },
+ 'text-group-align': {
+ // https://drafts.csswg.org/css-text-4/#propdef-text-group-align
+ types: [
+ { type: 'discrete', options: [ [ 'none', 'center' ] ] }
+ ]
+ },
+ 'text-indent': {
+ // https://drafts.csswg.org/css-text-3/#propdef-text-indent
+ types: [
+ ]
+ },
+ 'text-overflow': {
+ // https://drafts.csswg.org/css-ui/#propdef-text-overflow
+ types: [
+ { type: 'discrete', options: [ [ 'clip', 'ellipsis' ] ] }
+ ]
+ },
+ 'text-rendering': {
+ // https://svgwg.org/svg2-draft/painting.html#TextRenderingProperty
+ types: [
+ { type: 'discrete', options: [ [ 'optimizeSpeed', 'optimizeLegibility' ] ] }
+ ]
+ },
+ 'text-shadow': {
+ // https://drafts.csswg.org/css-text-decor-3/#propdef-text-shadow
+ types: [ 'textShadowList' ],
+ setup: t => {
+ const element = createElement(t);
+ element.style.color = 'green';
+ return element;
+ }
+ },
+ 'text-spacing-trim': {
+ // https://drafts.csswg.org/css-text-4/#text-spacing-trim-property
+ types: [
+ { type: 'discrete', options: [ [ 'normal', 'space-all' ] ] }
+ ]
+ },
+ 'text-transform': {
+ // https://drafts.csswg.org/css-text-3/#propdef-text-transform
+ types: [
+ { type: 'discrete', options: [ [ 'capitalize', 'uppercase' ] ] }
+ ]
+ },
+ 'text-wrap': {
+ // https://drafts.csswg.org/css-text-4/#propdef-text-wrap
+ types: [
+ { type: 'discrete', options: [ [ 'wrap', 'nowrap' ] ] }
+ ]
+ },
+ 'touch-action': {
+ // https://w3c.github.io/pointerevents/#the-touch-action-css-property
+ types: [
+ { type: 'discrete', options: [ [ 'auto', 'none' ] ] }
+ ]
+ },
+ 'top': {
+ // https://drafts.csswg.org/css-position/#propdef-top
+ types: [
+ ]
+ },
+ 'transform': {
+ // https://drafts.csswg.org/css-transforms/#propdef-transform
+ types: [ 'transformList' ]
+ },
+ 'transform-box': {
+ // https://drafts.csswg.org/css-transforms/#propdef-transform-box
+ types: [
+ { type: 'discrete', options: [ [ 'fill-box', 'border-box' ] ] }
+ ]
+ },
+ 'transform-origin': {
+ // https://drafts.csswg.org/css-transforms/#propdef-transform-origin
+ types: [
+ ]
+ },
+ 'transform-style': {
+ // https://drafts.csswg.org/css-transforms/#propdef-transform-style
+ types: [
+ { type: 'discrete', options: [ [ 'flat', 'preserve-3d' ] ] }
+ ]
+ },
+ 'rotate': {
+ // https://drafts.csswg.org/css-transforms-2/#individual-transforms
+ types: [ 'rotateList' ]
+ },
+ 'translate': {
+ // https://drafts.csswg.org/css-transforms-2/#individual-transforms
+ types: [ 'translateList' ],
+ setup: t => {
+ // We need to set a width/height for resolving percentages against.
+ const element = createElement(t);
+ element.style.width = '100px';
+ element.style.height = '100px';
+ return element;
+ }
+ },
+ 'scale': {
+ // https://drafts.csswg.org/css-transforms-2/#individual-transforms
+ types: [ 'scaleList' ]
+ },
+ 'vector-effect': {
+ // https://svgwg.org/svg2-draft/coords.html#VectorEffectProperty
+ types: [
+ { type: 'discrete', options: [ [ 'none', 'non-scaling-stroke' ] ] },
+ ]
+ },
+ 'vertical-align': {
+ // https://drafts.csswg.org/css21/visudet.html#propdef-vertical-align
+ types: [
+ ]
+ },
+ 'view-transition-name': {
+ // https://drafts.csswg.org/css-view-transitions/#propdef-view-transition-name
+ types: [
+ { type: 'discrete', options: [ [ 'none', 'header' ] ] },
+ ]
+ },
+ 'visibility': {
+ // https://drafts.csswg.org/css2/visufx.html#propdef-visibility
+ types: [ 'visibility' ]
+ },
+ 'white-space': {
+ // https://drafts.csswg.org/css-text-4/#propdef-white-space
+ types: [
+ { type: 'discrete', options: [ [ 'pre', 'nowrap' ] ] }
+ ]
+ },
+ 'white-space-collapse': {
+ // https://drafts.csswg.org/css-text-4/#propdef-white-space-collapse
+ types: [
+ { type: 'discrete', options: [ [ 'collapse', 'preserve' ] ] }
+ ]
+ },
+ 'width': {
+ // https://drafts.csswg.org/css21/visudet.html#propdef-width
+ types: [
+ ]
+ },
+ 'word-break': {
+ // https://drafts.csswg.org/css-text-3/#propdef-word-break
+ types: [
+ { type: 'discrete', options: [ [ 'keep-all', 'break-all' ] ] }
+ ]
+ },
+ 'word-spacing': {
+ // https://drafts.csswg.org/css-text-3/#propdef-word-spacing
+ types: [ 'lengthPercentageOrCalc' ]
+ },
+ 'z-index': {
+ // https://drafts.csswg.org/css-position/#propdef-z-index
+ types: [
+ ]
+ },
+};
+
+function testAnimationSamples(animation, idlName, testSamples) {
+ const pseudoType = animation.effect.pseudoElement;
+ const target = animation.effect.target;
+ for (const testSample of testSamples) {
+ animation.currentTime = testSample.time;
+ assert_equals(getComputedStyle(target, pseudoType)[idlName].toLowerCase(),
+ testSample.expected,
+ `The value should be ${testSample.expected}` +
+ ` at ${testSample.time}ms`);
+ }
+}
+
+function toOrderedArray(string) {
+ return string.split(/\s*,\s/).sort();
+}
+
+// This test is for some list-based CSS properties such as font-variant-settings
+// don't specify an order for serializing computed values.
+// This test is for such the property.
+function testAnimationSamplesWithAnyOrder(animation, idlName, testSamples) {
+ const type = animation.effect.pseudoElement;
+ const target = animation.effect.target;
+ for (const testSample of testSamples) {
+ animation.currentTime = testSample.time;
+
+ // Convert to array and sort the expected and actual value lists first
+ // before comparing them.
+ const computedValues =
+ toOrderedArray(getComputedStyle(target, type)[idlName]);
+ const expectedValues = toOrderedArray(testSample.expected);
+
+ assert_array_equals(computedValues, expectedValues,
+ `The computed values should be ${expectedValues}` +
+ ` at ${testSample.time}ms`);
+ }
+}
+
+function RoundMatrix(style) {
+ var matrixMatch = style.match(/^(matrix(3d)?)\(.+\)$/);
+ if (!!matrixMatch) {
+ var matrixType = matrixMatch[1];
+ var matrixArgs = style.substr(matrixType.length);
+ var extractmatrix = function(matrixStr) {
+ var list = [];
+ var regex = /[+\-]?[0-9]+[.]?[0-9]*(e[+/-][0-9]+)?/g;
+ var match = undefined;
+ do {
+ match = regex.exec(matrixStr);
+ if (match) {
+ list.push(parseFloat(parseFloat(match[0]).toFixed(6)));
+ }
+ } while (match);
+ return list;
+ }
+ return matrixType + '(' + extractmatrix(matrixArgs).join(', ') + ')';
+ }
+ return style;
+}
+
+function testAnimationSampleMatrices(animation, idlName, testSamples) {
+ const target = animation.effect.target;
+ for (const testSample of testSamples) {
+ animation.currentTime = testSample.time;
+ const actual = RoundMatrix(getComputedStyle(target)[idlName]);
+ const expected = RoundMatrix(createMatrixFromArray(testSample.expected));
+ assert_matrix_equals(actual, expected,
+ `The value should be ${expected} at`
+ + ` ${testSample.time}ms but got ${actual}`);
+ }
+}
+
+function testAnimationSampleRotate3d(animation, idlName, testSamples) {
+ const target = animation.effect.target;
+ for (const testSample of testSamples) {
+ animation.currentTime = testSample.time;
+ const actual = getComputedStyle(target)[idlName];
+ const expected = testSample.expected;
+ assert_rotate3d_equals(actual, expected,
+ `The value should be ${expected} at`
+ + ` ${testSample.time}ms but got ${actual}`);
+ }
+}
+
+function createTestElement(t, setup) {
+ return setup ? setup(t) : createElement(t);
+}
+
+function isSupported(property) {
+ const testKeyframe = new TestKeyframe(propertyToIDL(property));
+ assert_not_equals(window.KeyframeEffect, undefined, 'window.KeyframeEffect');
+ 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) {
+ let _propAccessCount = 0;
+
+ Object.defineProperty(this, testProp, {
+ get: function() { _propAccessCount++; },
+ enumerable: true
+ });
+
+ Object.defineProperty(this, 'propAccessCount', {
+ get: function() { return _propAccessCount; }
+ });
+}
+
+function propertyToIDL(property) {
+ // https://drafts.csswg.org/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(); });
+}
+function calcFromPercentage(idlName, percentageValue) {
+ const examElem = document.createElement('div');
+ document.body.appendChild(examElem);
+ examElem.style[idlName] = percentageValue;
+
+ const calcValue = getComputedStyle(examElem)[idlName];
+ document.body.removeChild(examElem);
+
+ return calcValue;
+}
diff --git a/testing/web-platform/tests/web-animations/animation-model/animation-types/property-types.js b/testing/web-platform/tests/web-animations/animation-model/animation-types/property-types.js
new file mode 100644
index 0000000000..4d693561ce
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/animation-model/animation-types/property-types.js
@@ -0,0 +1,2936 @@
+'use strict';
+
+const expected = values => {
+ // Some properties, such as line-height, report computed values which differ
+ // from the keyframe values. To support this, we allow optional values to specify
+ // explicit "from" and "to" values as additional keyframe values.
+ const [ from, to ] = values;
+ return [ values[2] ?? from, values[3] ?? to ];
+};
+
+const discreteType = {
+ testInterpolation: (property, setup, options) => {
+ for (const keyframes of options) {
+ const [ from, to ] = keyframes;
+ const [ expectedFrom, expectedTo ] = expected(keyframes);
+ test(t => {
+ const idlName = propertyToIDL(property);
+ const target = createTestElement(t, setup);
+ const animation = target.animate({ [idlName]: [from, to] },
+ { duration: 1000, fill: 'both' });
+ testAnimationSamples(animation, idlName,
+ [{ time: 0, expected: expectedFrom.toLowerCase() },
+ { time: 499, expected: expectedFrom.toLowerCase() },
+ { time: 500, expected: expectedTo.toLowerCase() },
+ { time: 1000, expected: expectedTo.toLowerCase() }]);
+ }, `${property} uses discrete animation when animating between`
+ + ` "${from}" and "${to}" with linear easing`);
+
+ test(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.
+ const idlName = propertyToIDL(property);
+ const keyframes = {};
+ const target = createTestElement(t, setup);
+ const animation = target.animate(
+ { [idlName]: [from, to] },
+ {
+ duration: 1000,
+ fill: 'both',
+ easing: 'cubic-bezier(0.68,0,1,0.01)',
+ }
+ );
+ testAnimationSamples(animation, idlName,
+ [{ time: 0, expected: expectedFrom.toLowerCase() },
+ { time: 940, expected: expectedFrom.toLowerCase() },
+ { time: 960, expected: expectedTo.toLowerCase() }]);
+ }, `${property} uses discrete animation when animating between`
+ + ` "${from}" and "${to}" with effect easing`);
+
+ test(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.
+ const idlName = propertyToIDL(property);
+ const target = createTestElement(t, setup);
+ const animation = target.animate(
+ {
+ [idlName]: [from, to],
+ easing: 'cubic-bezier(0.68,0,1,0.01)',
+ },
+ { duration: 1000, fill: 'both' }
+ );
+ testAnimationSamples(animation, idlName,
+ [{ time: 0, expected: expectedFrom.toLowerCase() },
+ { time: 940, expected: expectedFrom.toLowerCase() },
+ { time: 960, expected: expectedTo.toLowerCase() }]);
+ }, `${property} uses discrete animation when animating between`
+ + ` "${from}" and "${to}" with keyframe easing`);
+ }
+ },
+
+ testAdditionOrAccumulation: (property, setup, options, composite) => {
+ for (const keyframes of options) {
+ const [ from, to ] = keyframes;
+ const [ expectedFrom, expectedTo ] = expected(keyframes);
+ test(t => {
+ const idlName = propertyToIDL(property);
+ const target = createTestElement(t, setup);
+ target.animate({ [idlName]: [from, from] }, 1000);
+ const animation = target.animate(
+ { [idlName]: [to, to] },
+ { duration: 1000, composite }
+ );
+ testAnimationSamples(animation, idlName,
+ [{ time: 0, expected: expectedTo.toLowerCase() }]);
+ }, `${property}: "${to}" onto "${from}"`);
+
+ test(t => {
+ const idlName = propertyToIDL(property);
+ const target = createTestElement(t, setup);
+ target.animate({ [idlName]: [to, to] }, 1000);
+ const animation = target.animate(
+ { [idlName]: [from, from] },
+ { duration: 1000, composite }
+ );
+ testAnimationSamples(animation, idlName,
+ [{ time: 0, expected: expectedFrom.toLowerCase() }]);
+ }, `${property}: "${from}" onto "${to}"`);
+ }
+ },
+
+ testAddition: function(property, setup, options) {
+ this.testAdditionOrAccumulation(property, setup, options, 'add');
+ },
+
+ testAccumulation: function(property, setup, options) {
+ this.testAdditionOrAccumulation(property, setup, options, 'accumulate');
+ },
+};
+
+const lengthType = {
+ testInterpolation: (property, setup) => {
+ test(t => {
+ const idlName = propertyToIDL(property);
+ const target = createTestElement(t, setup);
+ const animation = target.animate({ [idlName]: ['10px', '50px'] },
+ { duration: 1000, fill: 'both' });
+ testAnimationSamples(animation, idlName,
+ [{ time: 500, expected: '30px' }]);
+ }, `${property} supports animating as a length`);
+
+ test(t => {
+ const idlName = propertyToIDL(property);
+ const target = createTestElement(t, setup);
+ const animation = target.animate({ [idlName]: ['1rem', '5rem'] },
+ { duration: 1000, fill: 'both' });
+ testAnimationSamples(animation, idlName,
+ [{ time: 500, expected: '30px' }]);
+ }, `${property} supports animating as a length of rem`);
+ },
+
+ testAdditionOrAccumulation: (property, setup, composite) => {
+ test(t => {
+ const idlName = propertyToIDL(property);
+ const target = createTestElement(t, setup);
+ target.style[idlName] = '10px';
+ const animation = target.animate(
+ { [idlName]: ['10px', '50px'] },
+ { duration: 1000, composite }
+ );
+ testAnimationSamples(animation, idlName, [{ time: 0, expected: '20px' }]);
+ }, `${property}: length`);
+
+ test(t => {
+ const idlName = propertyToIDL(property);
+ const target = createTestElement(t, setup);
+ target.style[idlName] = '1rem';
+ const animation = target.animate(
+ { [idlName]: ['1rem', '5rem'] },
+ { duration: 1000, composite }
+ );
+ testAnimationSamples(animation, idlName, [{ time: 0, expected: '20px' }]);
+ }, `${property}: length of rem`);
+ },
+
+ testAddition: function(property, setup) {
+ this.testAdditionOrAccumulation(property, setup, 'add');
+ },
+
+ testAccumulation: function(property, setup) {
+ this.testAdditionOrAccumulation(property, setup, 'accumulate');
+ },
+};
+
+const lengthPairType = {
+ testInterpolation: (property, setup) => {
+ test(t => {
+ const idlName = propertyToIDL(property);
+ const target = createTestElement(t, setup);
+ const animation = target.animate(
+ { [idlName]: ['10px 10px', '50px 50px'] },
+ { duration: 1000, fill: 'both' }
+ );
+ testAnimationSamples(animation, idlName,
+ [{ time: 500, expected: '30px 30px' }]);
+ }, `${property} supports animating as a length pair`);
+
+ test(t => {
+ const idlName = propertyToIDL(property);
+ const target = createTestElement(t, setup);
+ const animation = target.animate(
+ { [idlName]: ['1rem 1rem', '5rem 5rem'] },
+ { duration: 1000, fill: 'both' }
+ );
+ testAnimationSamples(animation, idlName,
+ [{ time: 500, expected: '30px 30px' }]);
+ }, `${property} supports animating as a length pair of rem`);
+ },
+
+ testAdditionOrAccumulation: (property, setup, composite) => {
+ test(t => {
+ const idlName = propertyToIDL(property);
+ const target = createTestElement(t, setup);
+ target.style[idlName] = '10px 10px';
+ const animation = target.animate(
+ { [idlName]: ['10px 10px', '50px 50px'] },
+ { duration: 1000, composite }
+ );
+ testAnimationSamples(
+ animation,
+ idlName,
+ [{ time: 0, expected: '20px 20px' }]
+ );
+ }, `${property}: length pair`);
+
+ test(t => {
+ const idlName = propertyToIDL(property);
+ const target = createTestElement(t, setup);
+ target.style[idlName] = '1rem 1rem';
+ const animation = target.animate(
+ { [idlName]: ['1rem 1rem', '5rem 5rem'] },
+ { duration: 1000, composite }
+ );
+ testAnimationSamples(
+ animation,
+ idlName,
+ [{ time: 0, expected: '20px 20px' }]
+ );
+ }, `${property}: length pair of rem`);
+ },
+
+ testAddition: function(property, setup) {
+ this.testAdditionOrAccumulation(property, setup, 'add');
+ },
+
+ testAccumulation: function(property, setup) {
+ this.testAdditionOrAccumulation(property, setup, 'accumulate');
+ },
+};
+
+const percentageType = {
+ testInterpolation: (property, setup) => {
+ test(t => {
+ const idlName = propertyToIDL(property);
+ const target = createTestElement(t, setup);
+ const animation = target.animate({ [idlName]: ['10%', '50%'] },
+ { duration: 1000, fill: 'both' });
+ testAnimationSamples(animation, idlName,
+ [{ time: 500, expected: '30%' }]);
+ }, `${property} supports animating as a percentage`);
+ },
+
+ testAdditionOrAccumulation: (property, setup, composite) => {
+ test(t => {
+ const idlName = propertyToIDL(property);
+ const target = createTestElement(t, setup);
+ target.style[idlName] = '60%';
+ const animation = target.animate(
+ { [idlName]: ['70%', '100%'] },
+ { duration: 1000, composite }
+ );
+ testAnimationSamples(animation, idlName, [{ time: 0, expected: '130%' }]);
+ }, `${property}: percentage`);
+ },
+
+ testAddition: function(property, setup) {
+ this.testAdditionOrAccumulation(property, setup, 'add');
+ },
+
+ testAccumulation: function(property, setup) {
+ this.testAdditionOrAccumulation(property, setup, 'accumulate');
+ },
+};
+
+const integerType = {
+ testInterpolation: (property, setup) => {
+ test(t => {
+ const idlName = propertyToIDL(property);
+ const target = createTestElement(t, setup);
+ const animation = target.animate({ [idlName]: [-2, 2] },
+ { duration: 1000, fill: 'both' });
+ testAnimationSamples(animation, idlName,
+ [{ time: 500, expected: '0' }]);
+ }, `${property} supports animating as an integer`);
+ },
+
+ testAdditionOrAccumulation: (property, setup, composite) => {
+ test(t => {
+ const idlName = propertyToIDL(property);
+ const target = createTestElement(t, setup);
+ target.style[idlName] = -1;
+ const animation = target.animate(
+ { [idlName]: [-2, 2] },
+ { duration: 1000, composite }
+ );
+ testAnimationSamples(animation, idlName,
+ [{ time: 0, expected: '-3' }]);
+ }, `${property}: integer`);
+ },
+
+ testAddition: function(property, setup) {
+ this.testAdditionOrAccumulation(property, setup, 'add');
+ },
+
+ testAccumulation: function(property, setup) {
+ this.testAdditionOrAccumulation(property, setup, 'accumulate');
+ },
+};
+
+const positiveIntegerType = {
+ testInterpolation: (property, setup) => {
+ test(t => {
+ const idlName = propertyToIDL(property);
+ const target = createTestElement(t, setup);
+ const animation = target.animate({ [idlName]: [1, 3] },
+ { duration: 1000, fill: 'both' });
+ testAnimationSamples(animation, idlName,
+ [ { time: 500, expected: '2' } ]);
+ }, `${property} supports animating as a positive integer`);
+ },
+
+ testAdditionOrAccumulation: (property, setup, composite) => {
+ test(t => {
+ const idlName = propertyToIDL(property);
+ const target = createTestElement(t, setup);
+ target.style[idlName] = 1;
+ const animation = target.animate(
+ { [idlName]: [2, 5] },
+ { duration: 1000, composite }
+ );
+ testAnimationSamples(animation, idlName,
+ [{ time: 0, expected: '3' }]);
+ }, `${property}: positive integer`);
+ },
+
+ testAddition: function(property, setup) {
+ this.testAdditionOrAccumulation(property, setup, 'add');
+ },
+
+ testAccumulation: function(property, setup) {
+ this.testAdditionOrAccumulation(property, setup, 'accumulate');
+ },
+};
+
+const lengthPercentageOrCalcType = {
+ testInterpolation: (property, setup) => {
+ lengthType.testInterpolation(property, setup);
+ percentageType.testInterpolation(property, setup);
+
+ test(t => {
+ const idlName = propertyToIDL(property);
+ const target = createTestElement(t, setup);
+ const animation = target.animate({ [idlName]: ['10px', '20%'] },
+ { duration: 1000, fill: 'both' });
+ testAnimationSamples(animation, idlName,
+ [{ time: 500, expected: 'calc(10% + 5px)' }]);
+ }, `${property} supports animating as combination units "px" and "%"`);
+
+ test(t => {
+ const idlName = propertyToIDL(property);
+ const target = createTestElement(t, setup);
+ const animation = target.animate({ [idlName]: ['10%', '2em'] },
+ { duration: 1000, fill: 'both' });
+ testAnimationSamples(animation, idlName,
+ [{ time: 500, expected: 'calc(5% + 10px)' }]);
+ }, `${property} supports animating as combination units "%" and "em"`);
+
+ test(t => {
+ const idlName = propertyToIDL(property);
+ const target = createTestElement(t, setup);
+ const animation = target.animate({ [idlName]: ['1em', '2rem'] },
+ { duration: 1000, fill: 'both' });
+ testAnimationSamples(animation, idlName,
+ [{ time: 500, expected: '15px' }]);
+ }, `${property} supports animating as combination units "em" and "rem"`);
+
+ test(t => {
+ const idlName = propertyToIDL(property);
+ const target = createTestElement(t, setup);
+ const animation = target.animate(
+ { [idlName]: ['10px', 'calc(1em + 20%)'] },
+ { duration: 1000, fill: 'both' }
+ );
+ testAnimationSamples(animation, idlName,
+ [{ time: 500, expected: 'calc(10% + 10px)' }]);
+ }, `${property} supports animating as combination units "px" and "calc"`);
+
+ test(t => {
+ const idlName = propertyToIDL(property);
+ const target = createTestElement(t, setup);
+ const animation = target.animate(
+ { [idlName]: ['calc(10px + 10%)', 'calc(1em + 1rem + 20%)'] },
+ { duration: 1000, fill: 'both' });
+ testAnimationSamples(animation, idlName,
+ [{ time: 500,
+ expected: 'calc(15% + 15px)' }]);
+ }, `${property} supports animating as a calc`);
+ },
+
+ testAdditionOrAccumulation: (property, setup, composite) => {
+ lengthType.testAddition(property, setup);
+ percentageType.testAddition(property, setup);
+
+ test(t => {
+ const idlName = propertyToIDL(property);
+ const target = createTestElement(t, setup);
+ target.style[idlName] = '10px';
+ const animation = target.animate({ [idlName]: ['10%', '50%'] },
+ { duration: 1000, composite });
+ testAnimationSamples(animation, idlName,
+ [{ time: 0, expected: 'calc(10% + 10px)' }]);
+ }, `${property}: units "%" onto "px"`);
+
+ test(t => {
+ const idlName = propertyToIDL(property);
+ const target = createTestElement(t, setup);
+ target.style[idlName] = '10%';
+ const animation = target.animate({ [idlName]: ['10px', '50px'] },
+ { duration: 1000, composite });
+ testAnimationSamples(animation, idlName,
+ [{ time: 0, expected: 'calc(10% + 10px)' }]);
+ }, `${property}: units "px" onto "%"`);
+
+ test(t => {
+ const idlName = propertyToIDL(property);
+ const target = createTestElement(t, setup);
+ target.style[idlName] = '10%';
+ const animation = target.animate({ [idlName]: ['2rem', '5rem'] },
+ { duration: 1000, composite });
+ testAnimationSamples(animation, idlName,
+ [{ time: 0, expected: 'calc(10% + 20px)' }]);
+ }, `${property}: units "rem" onto "%"`);
+
+ test(t => {
+ const idlName = propertyToIDL(property);
+ const target = createTestElement(t, setup);
+ target.style[idlName] = '2rem';
+ const animation = target.animate({ [idlName]: ['10%', '50%'] },
+ { duration: 1000, composite });
+ testAnimationSamples(animation, idlName,
+ [{ time: 0, expected: 'calc(10% + 20px)' }]);
+ }, `${property}: units "%" onto "rem"`);
+
+ test(t => {
+ const idlName = propertyToIDL(property);
+ const target = createTestElement(t, setup);
+ target.style[idlName] = '2em';
+ const animation = target.animate({ [idlName]: ['2rem', '5rem'] },
+ { duration: 1000, composite });
+ testAnimationSamples(animation, idlName, [{ time: 0, expected: '40px' }]);
+ }, `${property}: units "rem" onto "em"`);
+
+ test(t => {
+ const idlName = propertyToIDL(property);
+ const target = createTestElement(t, setup);
+ target.style[idlName] = '2rem';
+ const animation = target.animate({ [idlName]: ['2em', '5em'] },
+ { duration: 1000, composite });
+ testAnimationSamples(animation, idlName, [{ time: 0, expected: '40px' }]);
+ }, `${property}: units "em" onto "rem"`);
+
+ test(t => {
+ const idlName = propertyToIDL(property);
+ const target = createTestElement(t, setup);
+ target.style[idlName] = '10px';
+ const animation = target.animate({ [idlName]: ['calc(2em + 20%)',
+ 'calc(5rem + 50%)'] },
+ { duration: 1000, composite });
+ testAnimationSamples(animation, idlName,
+ [{ time: 0, expected: 'calc(20% + 30px)' }]);
+ }, `${property}: units "calc" onto "px"`);
+
+ test(t => {
+ const idlName = propertyToIDL(property);
+ const target = createTestElement(t, setup);
+ target.style[idlName] = 'calc(10px + 10%)';
+ const animation = target.animate({ [idlName]: ['calc(20px + 20%)',
+ 'calc(2em + 3rem + 40%)'] },
+ { duration: 1000, composite });
+ testAnimationSamples(animation, idlName,
+ [{ time: 0, expected: 'calc(30% + 30px)' }]);
+ }, `${property}: calc`);
+ },
+
+ testAddition: function(property, setup) {
+ this.testAdditionOrAccumulation(property, setup, 'add');
+ },
+
+ testAccumulation: function(property, setup) {
+ this.testAdditionOrAccumulation(property, setup, 'accumulate');
+ },
+};
+
+const positiveNumberType = {
+ testInterpolation: (property, setup, expectedUnit='') => {
+ test(t => {
+ const idlName = propertyToIDL(property);
+ const target = createTestElement(t, setup);
+ const animation = target.animate({ [idlName]: [1.1, 1.5] },
+ { duration: 1000, fill: 'both' });
+ testAnimationSamples(animation, idlName,
+ [{ time: 500, expected: '1.3' + expectedUnit }]);
+ }, `${property} supports animating as a positive number`);
+ },
+
+ testAdditionOrAccumulation: (property, setup, composite) => {
+ test(t => {
+ const idlName = propertyToIDL(property);
+ const target = createTestElement(t, setup);
+ target.style[idlName] = 1.1;
+ const animation = target.animate({ [idlName]: [1.1, 1.5] },
+ { duration: 1000, composite });
+ testAnimationSamples(animation, idlName, [{ time: 0, expected: '2.2' }]);
+ }, `${property}: positive number`);
+ },
+
+ testAddition: function(property, setup) {
+ this.testAdditionOrAccumulation(property, setup, 'add');
+ },
+
+ testAccumulation: function(property, setup) {
+ this.testAdditionOrAccumulation(property, setup, 'accumulate');
+ },
+};
+
+// Test using float values in the range [0, 1]
+const opacityType = {
+ testInterpolation: (property, setup) => {
+ test(t => {
+ const idlName = propertyToIDL(property);
+ const target = createTestElement(t, setup);
+ const animation = target.animate({ [idlName]: [0.3, 0.8] },
+ { duration: 1000, fill: 'both' });
+ testAnimationSamples(animation, idlName,
+ [{ time: 500, expected: '0.55' }]);
+ }, `${property} supports animating as a [0, 1] number`);
+ },
+
+ testAdditionOrAccumulation: (property, setup, composite) => {
+ test(t => {
+ const idlName = propertyToIDL(property);
+ const target = createTestElement(t, setup);
+ target.style[idlName] = 0.3;
+ const animation = target.animate({ [idlName]: [0.3, 0.8] },
+ { duration: 1000, composite });
+ testAnimationSamples(animation, idlName, [{ time: 0, expected: '0.6' }]);
+ }, `${property}: [0, 1] number`);
+
+ test(t => {
+ const idlName = propertyToIDL(property);
+ const target = createTestElement(t, setup);
+ target.style[idlName] = 0.8;
+ const animation = target.animate({ [idlName]: [0.3, 0.8] },
+ { duration: 1000, composite });
+ testAnimationSamples(animation, idlName, [{ time: 0, expected: '1' }]);
+ }, `${property}: [0, 1] number (clamped)`);
+ },
+
+ testAddition: function(property, setup) {
+ this.testAdditionOrAccumulation(property, setup, 'add');
+ },
+
+ testAccumulation: function(property, setup) {
+ this.testAdditionOrAccumulation(property, setup, 'accumulate');
+ },
+};
+
+const visibilityType = {
+ testInterpolation: (property, setup) => {
+ test(t => {
+ const idlName = propertyToIDL(property);
+ const target = createTestElement(t, setup);
+ const animation = target.animate({ [idlName]: ['visible', 'hidden'] },
+ { 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(t => {
+ const idlName = propertyToIDL(property);
+ const target = createTestElement(t, setup);
+ const animation = target.animate({ [idlName]: ['hidden', 'visible'] },
+ { 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(t => {
+ const idlName = propertyToIDL(property);
+ const target = createTestElement(t, setup);
+ const animation = target.animate({ [idlName]: ['hidden', 'collapse'] },
+ { 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(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%
+ const idlName = propertyToIDL(property);
+ const target = createTestElement(t, setup);
+ const animation =
+ target.animate({ [idlName]: ['visible', 'hidden'] },
+ { 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');
+ },
+
+ testAdditionOrAccumulation: (property, setup, composite) => {
+ test(t => {
+ const idlName = propertyToIDL(property);
+ const target = createTestElement(t, setup);
+ target.style[idlName] = 'visible';
+ const animation = target.animate({ [idlName]: ['visible', 'hidden'] },
+ { duration: 1000, fill: 'both',
+ composite });
+ testAnimationSamples(animation, idlName,
+ [{ time: 0, expected: 'visible' },
+ { time: 1000, expected: 'hidden' }]);
+ }, `${property}: onto "visible"`);
+
+ test(t => {
+ const idlName = propertyToIDL(property);
+ const target = createTestElement(t, setup);
+ target.style[idlName] = 'hidden';
+ const animation = target.animate({ [idlName]: ['hidden', 'visible'] },
+ { duration: 1000, fill: 'both',
+ composite });
+ testAnimationSamples(animation, idlName,
+ [{ time: 0, expected: 'hidden' },
+ { time: 1000, expected: 'visible' }]);
+ }, `${property}: onto "hidden"`);
+ },
+
+ testAddition: function(property, setup) {
+ this.testAdditionOrAccumulation(property, setup, 'add');
+ },
+
+ testAccumulation: function(property, setup) {
+ this.testAdditionOrAccumulation(property, setup, 'accumulate');
+ },
+};
+
+const colorType = {
+ testInterpolation: (property, setup) => {
+ test(t => {
+ const idlName = propertyToIDL(property);
+ const target = createTestElement(t, setup);
+ const animation = target.animate({ [idlName]: ['rgb(255, 0, 0)',
+ 'rgb(0, 0, 255)'] },
+ 1000);
+ testAnimationSamples(animation, idlName,
+ [{ time: 500, expected: 'rgb(128, 0, 128)' }]);
+ }, `${property} supports animating as color of rgb()`);
+
+ test(t => {
+ const idlName = propertyToIDL(property);
+ const target = createTestElement(t, setup);
+ const animation = target.animate({ [idlName]: ['#ff0000', '#0000ff'] },
+ 1000);
+ testAnimationSamples(animation, idlName,
+ [{ time: 500, expected: 'rgb(128, 0, 128)' }]);
+ }, `${property} supports animating as color of #RGB`);
+
+ test(t => {
+ const idlName = propertyToIDL(property);
+ const target = createTestElement(t, setup);
+ const animation = target.animate({ [idlName]: ['hsl(0, 100%, 50%)',
+ 'hsl(240, 100%, 50%)'] },
+ 1000);
+ testAnimationSamples(animation, idlName,
+ [{ time: 500, expected: 'rgb(128, 0, 128)' }]);
+ }, `${property} supports animating as color of hsl()`);
+
+ test(t => {
+ const idlName = propertyToIDL(property);
+ const target = createTestElement(t, setup);
+ const animation = target.animate(
+ { [idlName]: ['#ff000066', '#0000ffcc'] },
+ 1000
+ );
+ // R: 255 * (0.4 * 0.5) / 0.6 = 85
+ // B: 255 * (0.8 * 0.5) / 0.6 = 170
+ testAnimationSamples(animation, idlName,
+ [{ time: 500, expected: 'rgba(85, 0, 170, 0.6)' }]);
+ }, `${property} supports animating as color of #RGBa`);
+
+ test(t => {
+ const idlName = propertyToIDL(property);
+ const target = createTestElement(t, setup);
+ const animation = target.animate(
+ {
+ [idlName]: ['rgba(255, 0, 0, 0.4)', 'rgba(0, 0, 255, 0.8)'],
+ },
+ 1000
+ );
+ testAnimationSamples(animation, idlName, // Same as above.
+ [{ time: 500, expected: 'rgba(85, 0, 170, 0.6)' }]);
+ }, `${property} supports animating as color of rgba()`);
+
+ test(t => {
+ const idlName = propertyToIDL(property);
+ const target = createTestElement(t, setup);
+ const animation = target.animate(
+ {
+ [idlName]: ['hsla(0, 100%, 50%, 0.4)', 'hsla(240, 100%, 50%, 0.8)'],
+ },
+ 1000
+ );
+ testAnimationSamples(animation, idlName, // Same as above.
+ [{ time: 500, expected: 'rgba(85, 0, 170, 0.6)' }]);
+ }, `${property} supports animating as color of hsla()`);
+ },
+
+ testAdditionOrAccumulation: (property, setup, composite) => {
+ test(t => {
+ const idlName = propertyToIDL(property);
+ const target = createTestElement(t, setup);
+ target.style[idlName] = 'rgb(128, 128, 128)';
+ const animation = target.animate(
+ {
+ [idlName]: ['rgb(255, 0, 0)', 'rgb(0, 0, 255)'],
+ },
+ { duration: 1000, composite }
+ );
+ testAnimationSamples(animation, idlName,
+ [{ time: 0, expected: 'rgb(255, 128, 128)' },
+ // The value at 50% is interpolated
+ // from 'rgb(128+255, 128, 128)'
+ // to 'rgb(128, 128, 128+255)'.
+ { time: 500, expected: 'rgb(255, 128, 255)' }]);
+ }, `${property} supports animating as color of rgb() with overflowed `
+ + ' from and to values');
+
+ test(t => {
+ const idlName = propertyToIDL(property);
+ const target = createTestElement(t, setup);
+ target.style[idlName] = 'rgb(128, 128, 128)';
+ const animation = target.animate({ [idlName]: ['#ff0000', '#0000ff'] },
+ { duration: 1000, composite });
+ testAnimationSamples(animation, idlName,
+ [{ time: 0, expected: 'rgb(255, 128, 128)' }]);
+ }, `${property} supports animating as color of #RGB`);
+
+ test(t => {
+ const idlName = propertyToIDL(property);
+ const target = createTestElement(t, setup);
+ target.style[idlName] = 'rgb(128, 128, 128)';
+ const animation = target.animate({ [idlName]: ['hsl(0, 100%, 50%)',
+ 'hsl(240, 100%, 50%)'] },
+ { duration: 1000, composite });
+ testAnimationSamples(animation, idlName,
+ [{ time: 0, expected: 'rgb(255, 128, 128)' }]);
+ }, `${property} supports animating as color of hsl()`);
+
+ test(t => {
+ const idlName = propertyToIDL(property);
+ const target = createTestElement(t, setup);
+ target.style[idlName] = 'rgb(128, 128, 128)';
+ const animation = target.animate(
+ { [idlName]: ['#ff000066', '#0000ffcc'] },
+ { duration: 1000, composite }
+ );
+ testAnimationSamples(animation, idlName,
+ [{ time: 0, expected: 'rgb(230, 128, 128)' }]);
+ }, `${property} supports animating as color of #RGBa`);
+
+ test(t => {
+ const idlName = propertyToIDL(property);
+ const target = createTestElement(t, setup);
+ target.style[idlName] = 'rgb(128, 128, 128)';
+ const animation = target.animate({ [idlName]: ['rgba(255, 0, 0, 0.4)',
+ 'rgba(0, 0, 255, 0.8)'] },
+ { duration: 1000, composite });
+ testAnimationSamples(animation, idlName, // Same as above.
+ [{ time: 0, expected: 'rgb(230, 128, 128)' }]);
+ }, `${property} supports animating as color of rgba()`);
+
+ test(t => {
+ const idlName = propertyToIDL(property);
+ const target = createTestElement(t, setup);
+ target.style[idlName] = 'rgb(128, 128, 128)';
+ const animation = target.animate(
+ {
+ [idlName]: ['hsla(0, 100%, 50%, 0.4)', 'hsla(240, 100%, 50%, 0.8)'],
+ },
+ { duration: 1000, composite }
+ );
+ testAnimationSamples(animation, idlName, // Same as above.
+ [{ time: 0, expected: 'rgb(230, 128, 128)' }]);
+ }, `${property} supports animating as color of hsla()`);
+ },
+
+ testAddition: function(property, setup) {
+ this.testAdditionOrAccumulation(property, setup, 'add');
+ },
+
+ testAccumulation: function(property, setup) {
+ this.testAdditionOrAccumulation(property, setup, 'accumulate');
+ },
+};
+
+const colorPairType = {
+ testInterpolation: (property, setup) => {
+ test(t => {
+ const idlName = propertyToIDL(property);
+ const target = createTestElement(t, setup);
+ const animation = target.animate({ [idlName]: ['rgb(255, 0, 0) rgb(0, 0, 255)',
+ 'rgb(0, 0, 255) rgb(0, 255, 0)'] },
+ 1000);
+ testAnimationSamples(animation, idlName,
+ [{ time: 500, expected: 'rgb(128, 0, 128) rgb(0, 128, 128)' }]);
+ }, `${property} supports animating as color pair of rgb()`);
+
+ test(t => {
+ const idlName = propertyToIDL(property);
+ const target = createTestElement(t, setup);
+ const animation = target.animate({ [idlName]: ['#ff0000 #00ff00', '#0000ff #ff0000'] },
+ 1000);
+ testAnimationSamples(animation, idlName,
+ [{ time: 500, expected: 'rgb(128, 0, 128) rgb(128, 128, 0)' }]);
+ }, `${property} supports animating as color pair of #RGB`);
+
+ test(t => {
+ const idlName = propertyToIDL(property);
+ const target = createTestElement(t, setup);
+ const animation = target.animate({ [idlName]: ['hsl(0, 100%, 50%) hsl(0, 100%, 50%)',
+ 'hsl(240, 100%, 50%) hsl(360, 100%, 50%)'] },
+ 1000);
+ testAnimationSamples(animation, idlName,
+ [{ time: 500, expected: 'rgb(128, 0, 128) rgb(255, 0, 0)' }]);
+ }, `${property} supports animating as color pair of hsl()`);
+
+ test(t => {
+ const idlName = propertyToIDL(property);
+ const target = createTestElement(t, setup);
+ const animation = target.animate(
+ { [idlName]: ['#ff000066 #ff000066', '#0000ffcc #00ff00cc'] },
+ 1000
+ );
+ // R: 255 * (0.4 * 0.5) / 0.6 = 85
+ // B: 255 * (0.8 * 0.5) / 0.6 = 170
+ testAnimationSamples(animation, idlName,
+ [{ time: 500, expected: 'rgba(85, 0, 170, 0.6) rgba(85, 170, 0, 0.6)' }]);
+ }, `${property} supports animating as color pair of #RGBa`);
+
+ test(t => {
+ const idlName = propertyToIDL(property);
+ const target = createTestElement(t, setup);
+ const animation = target.animate(
+ {
+ [idlName]: ['rgba(255, 0, 0, 0.4) rgba(255, 0, 0, 0.4)', 'rgba(0, 0, 255, 0.8) rgba(0, 255, 0, 0.8)'],
+ },
+ 1000
+ );
+ testAnimationSamples(animation, idlName, // Same as above.
+ [{ time: 500, expected: 'rgba(85, 0, 170, 0.6) rgba(85, 170, 0, 0.6)' }]);
+ }, `${property} supports animating as color pair of rgba()`);
+
+ test(t => {
+ const idlName = propertyToIDL(property);
+ const target = createTestElement(t, setup);
+ const animation = target.animate(
+ {
+ [idlName]: ['hsla(0, 100%, 50%, 0.4) hsla(0, 100%, 50%, 0.4)', 'hsla(240, 100%, 50%, 0.8) hsla(360, 100%, 50%, 0.8)'],
+ },
+ 1000
+ );
+ testAnimationSamples(animation, idlName, // Same as above.
+ [{ time: 500, expected: 'rgba(85, 0, 170, 0.6) rgba(255, 0, 0, 0.6)' }]);
+ }, `${property} supports animating as color pair of hsla()`);
+ },
+
+ testAdditionOrAccumulation: (property, setup, composite) => {
+ test(t => {
+ const idlName = propertyToIDL(property);
+ const target = createTestElement(t, setup);
+ target.style[idlName] = 'rgb(128, 128, 128) rgb(0, 0, 0)';
+ const animation = target.animate(
+ {
+ [idlName]: ['rgb(255, 0, 0) rgb(0, 0, 255)', 'rgb(0, 0, 255) rgb(255, 0, 0)']
+ },
+ { duration: 1000, composite }
+ );
+ testAnimationSamples(animation, idlName,
+ [{ time: 0, expected: 'rgb(255, 128, 128) rgb(0, 0, 255)' },
+ { time: 500, expected: 'rgb(255, 128, 255) rgb(128, 0, 128)' }]);
+ }, `${property} supports animating as color pair of rgb() with overflowed `
+ + ' from and to values');
+
+ test(t => {
+ const idlName = propertyToIDL(property);
+ const target = createTestElement(t, setup);
+ target.style[idlName] = 'rgb(128, 128, 128) rgb(0, 0, 0)';
+ const animation = target.animate({ [idlName]: ['#ff0000 #0000ff', '#0000ff #ff0000'] },
+ { duration: 1000, composite });
+ testAnimationSamples(animation, idlName,
+ [{ time: 0, expected: 'rgb(255, 128, 128) rgb(0, 0, 255)' }]);
+ }, `${property} supports animating as color pair of #RGB`);
+
+ test(t => {
+ const idlName = propertyToIDL(property);
+ const target = createTestElement(t, setup);
+ target.style[idlName] = 'rgb(128, 128, 128) rgb(0, 0, 0)';
+ const animation = target.animate({ [idlName]: ['hsl(0, 100%, 50%) hsl(0, 100%, 50%)',
+ 'hsl(240, 100%, 50%) hsl(360, 100%, 50%)'] },
+ { duration: 1000, composite });
+ testAnimationSamples(animation, idlName,
+ [{ time: 0, expected: 'rgb(255, 128, 128) rgb(255, 0, 0)' }]);
+ }, `${property} supports animating as color pair of hsl()`);
+
+ test(t => {
+ const idlName = propertyToIDL(property);
+ const target = createTestElement(t, setup);
+ target.style[idlName] = 'rgb(128, 128, 128) rgb(0, 0, 0)';
+ const animation = target.animate(
+ { [idlName]: ['#ff000066 #ff000066', '#0000ffcc #00ff00cc'] },
+ { duration: 1000, composite }
+ );
+ testAnimationSamples(animation, idlName,
+ [{ time: 0, expected: 'rgb(230, 128, 128) rgb(102, 0, 0)' }]);
+ }, `${property} supports animating as color pair of #RGBa`);
+
+ test(t => {
+ const idlName = propertyToIDL(property);
+ const target = createTestElement(t, setup);
+ target.style[idlName] = 'rgb(128, 128, 128) rgb(0, 0, 0)';
+ const animation = target.animate({ [idlName]: ['rgba(255, 0, 0, 0.4) rgba(0, 255, 0, 0.4)',
+ 'rgba(0, 0, 255, 0.8) rgba(255, 0, 0, 0.8)'] },
+ { duration: 1000, composite });
+ testAnimationSamples(animation, idlName, // Same as above.
+ [{ time: 0, expected: 'rgb(230, 128, 128) rgb(0, 102, 0)' }]);
+ }, `${property} supports animating as color pair of rgba()`);
+
+ test(t => {
+ const idlName = propertyToIDL(property);
+ const target = createTestElement(t, setup);
+ target.style[idlName] = 'rgb(128, 128, 128) rgb(0, 0, 0)';
+ const animation = target.animate(
+ {
+ [idlName]: ['hsla(0, 100%, 50%, 0.4) hsla(0, 100%, 50%, 0.4)', 'hsla(240, 100%, 50%, 0.8) hsla(360, 100%, 50%, 0.8)'],
+ },
+ { duration: 1000, composite }
+ );
+ testAnimationSamples(animation, idlName, // Same as above.
+ [{ time: 0, expected: 'rgb(230, 128, 128) rgb(102, 0, 0)' }]);
+ }, `${property} supports animating as color pair of hsla()`);
+ },
+
+ testAddition: function(property, setup) {
+ this.testAdditionOrAccumulation(property, setup, 'add');
+ },
+
+ testAccumulation: function(property, setup) {
+ this.testAdditionOrAccumulation(property, setup, 'accumulate');
+ },
+};
+
+const transformListType = {
+ testInterpolation: (property, setup) => {
+ test(t => {
+ const idlName = propertyToIDL(property);
+ const target = createTestElement(t, setup);
+ const animation = target.animate(
+ {
+ [idlName]: ['translate(200px, -200px)', 'translate(400px, 400px)'],
+ },
+ 1000
+ );
+ testAnimationSampleMatrices(animation, idlName,
+ [{ time: 500, expected: [ 1, 0, 0, 1, 300, 100 ] }]);
+ }, `${property}: translate`);
+
+ test(t => {
+ const idlName = propertyToIDL(property);
+ const target = createTestElement(t, setup);
+ const animation = target.animate(
+ {
+ [idlName]: ['rotate(45deg)', 'rotate(135deg)'],
+ },
+ 1000
+ );
+
+ testAnimationSampleMatrices(animation, idlName,
+ [{ time: 500, expected: [ Math.cos(Math.PI / 2),
+ Math.sin(Math.PI / 2),
+ -Math.sin(Math.PI / 2),
+ Math.cos(Math.PI / 2),
+ 0, 0] }]);
+ }, `${property}: rotate`);
+
+ test(t => {
+ const idlName = propertyToIDL(property);
+ const target = createTestElement(t, setup);
+ const animation = target.animate({ [idlName]: ['scale(3)', 'scale(5)'] },
+ 1000);
+
+ testAnimationSampleMatrices(animation, idlName,
+ [{ time: 500, expected: [ 4, 0, 0, 4, 0, 0 ] }]);
+ }, `${property}: scale`);
+
+ test(t => {
+ const idlName = propertyToIDL(property);
+ const target = createTestElement(t, setup);
+ const animation = target.animate({ [idlName]: ['skew(30deg, 60deg)',
+ 'skew(60deg, 30deg)'] },
+ 1000);
+
+ testAnimationSampleMatrices(animation, idlName,
+ [{ time: 500, expected: [ 1, Math.tan(Math.PI / 4),
+ Math.tan(Math.PI / 4), 1,
+ 0, 0] }]);
+ }, `${property}: skew`);
+
+ test(t => {
+ const idlName = propertyToIDL(property);
+ const target = createTestElement(t, setup);
+ const animation =
+ target.animate({ [idlName]: ['translateX(100px) rotate(45deg)',
+ 'translateX(200px) rotate(135deg)'] },
+ 1000);
+
+ testAnimationSampleMatrices(animation, idlName,
+ [{ time: 500, expected: [ Math.cos(Math.PI / 2),
+ Math.sin(Math.PI / 2),
+ -Math.sin(Math.PI / 2),
+ Math.cos(Math.PI / 2),
+ 150, 0 ] }]);
+ }, `${property}: rotate and translate`);
+
+ test(t => {
+ const idlName = propertyToIDL(property);
+ const target = createTestElement(t, setup);
+ const animation =
+ target.animate({ [idlName]: ['rotate(45deg) translateX(100px)',
+ 'rotate(135deg) translateX(200px)'] },
+ 1000);
+
+ testAnimationSampleMatrices(animation, idlName,
+ [{ time: 500, expected: [ Math.cos(Math.PI / 2),
+ Math.sin(Math.PI / 2),
+ -Math.sin(Math.PI / 2),
+ Math.cos(Math.PI / 2),
+ 150 * Math.cos(Math.PI / 2),
+ 150 * Math.sin(Math.PI / 2) ] }]);
+ }, `${property}: translate and rotate`);
+
+ test(t => {
+ const idlName = propertyToIDL(property);
+ const target = createTestElement(t, setup);
+ const animation =
+ target.animate({ [idlName]: ['rotate(0deg)',
+ 'rotate(1080deg) translateX(100px)'] },
+ 1000);
+
+ testAnimationSampleMatrices(animation, idlName,
+ [{ time: 500, expected: [ -1, 0, 0, -1, -50, 0 ] }]);
+ }, `${property}: extend shorter list (from)`);
+
+ test(t => {
+ const idlName = propertyToIDL(property);
+ const target = createTestElement(t, setup);
+ const animation =
+ target.animate({ [idlName]: ['rotate(0deg) translateX(100px)',
+ 'rotate(1080deg)'] },
+ 1000);
+
+ testAnimationSampleMatrices(animation, idlName,
+ [{ time: 500, expected: [ -1, 0, 0, -1, -50, 0 ] }]);
+ }, `${property}: extend shorter list (to)`);
+
+ test(t => {
+ const idlName = propertyToIDL(property);
+ const target = createTestElement(t, setup);
+ const animation = // matrix(0, 1, -1, 0, 0, 100)
+ target.animate({ [idlName]: ['rotate(90deg) translateX(100px)',
+ // matrix(-1, 0, 0, -1, 200, 0)
+ 'translateX(200px) rotate(180deg)'] },
+ 1000);
+
+ testAnimationSampleMatrices(animation, idlName,
+ [{ time: 500, expected: [ Math.cos(Math.PI * 3 / 4),
+ Math.sin(Math.PI * 3 / 4),
+ -Math.sin(Math.PI * 3 / 4),
+ Math.cos(Math.PI * 3 / 4),
+ 100, 50 ] }]);
+ }, `${property}: mismatch order of translate and rotate`);
+
+ test(t => {
+ const idlName = propertyToIDL(property);
+ const target = createTestElement(t, setup);
+ const animation = // Same matrices as above.
+ target.animate({ [idlName]: [ 'matrix(0, 1, -1, 0, 0, 100)',
+ 'matrix(-1, 0, 0, -1, 200, 0)' ] },
+ 1000);
+
+ testAnimationSampleMatrices(animation, idlName,
+ [{ time: 500, expected: [ Math.cos(Math.PI * 3 / 4),
+ Math.sin(Math.PI * 3 / 4),
+ -Math.sin(Math.PI * 3 / 4),
+ Math.cos(Math.PI * 3 / 4),
+ 100, 50 ] }]);
+ }, `${property}: matrix`);
+
+ test(t => {
+ const idlName = propertyToIDL(property);
+ const target = createTestElement(t, setup);
+ const animation =
+ target.animate({ [idlName]: [ 'rotate3d(1, 1, 0, 0deg)',
+ 'rotate3d(1, 1, 0, 90deg)'] },
+ 1000);
+
+ testAnimationSampleMatrices(animation, idlName,
+ [{ time: 500, expected: rotate3dToMatrix(1, 1, 0, Math.PI / 4) }]);
+ }, `${property}: rotate3d`);
+
+ test(t => {
+ const idlName = propertyToIDL(property);
+ const target = createTestElement(t, setup);
+ // To calculate expected matrices easily, generate input matrices from
+ // rotate3d.
+ const from = rotate3dToMatrix3d(1, 1, 0, Math.PI / 4);
+ const to = rotate3dToMatrix3d(1, 1, 0, Math.PI * 3 / 4);
+ const animation = target.animate({ [idlName]: [ from, to ] }, 1000);
+
+ testAnimationSampleMatrices(animation, idlName,
+ [{ time: 500, expected: rotate3dToMatrix(1, 1, 0, Math.PI * 2 / 4) }]);
+ }, `${property}: matrix3d`);
+
+ // This test aims for forcing the two mismatched transforms to be
+ // decomposed into matrix3d before interpolation. Therefore, we not only
+ // test the interpolation, but also test the 3D matrix decomposition.
+ test(t => {
+ const idlName = propertyToIDL(property);
+ const target = createTestElement(t, setup);
+ const animation = target.animate(
+ {
+ [idlName]: [
+ 'scale(0.3)',
+ // scale(0.5) translateZ(1px)
+ 'matrix3d(0.5, 0, 0, 0, 0, 0.5, 0, 0, 0, 0, 1, 0, 0, 0, 1, 1)',
+ ],
+ },
+ 1000
+ );
+
+ testAnimationSampleMatrices(animation, idlName,
+ [{ time: 500, expected: [ 0.4, 0, 0, 0,
+ 0, 0.4, 0, 0,
+ 0, 0, 1, 0,
+ 0, 0, 0.5, 1] }]);
+ }, `${property}: mismatched 3D transforms`);
+
+ test(t => {
+ const idlName = propertyToIDL(property);
+ const target = createTestElement(t, setup);
+ const animation =
+ target.animate({ [idlName]: ['rotateY(60deg)', 'none' ] }, 1000);
+
+ testAnimationSampleMatrices(animation, idlName,
+ // rotateY(30deg) == rotate3D(0, 1, 0, 30deg)
+ [{ time: 500, expected: rotate3dToMatrix(0, 1, 0, Math.PI / 6) }]);
+ }, `${property}: rotateY`);
+
+ // Following tests aim for test the fallback discrete interpolation behavior
+ // for non-invertible matrices. The non-invertible matrix that we use is the
+ // singular matrix, matrix(1, 1, 0, 0, 0, 100).
+ test(t => {
+ const idlName = propertyToIDL(property);
+ const target = createTestElement(t, setup);
+ const animation =
+ target.animate({ [idlName]: ['matrix(-1, 0, 0, -1, 200, 0)',
+ 'matrix( 1, 1, 0, 0, 0, 100)'] },
+ { duration: 1000, fill: 'both' });
+
+ testAnimationSampleMatrices(animation, idlName,
+ [ { time: 0, expected: [ -1, 0, 0, -1, 200, 0 ] },
+ { time: 499, expected: [ -1, 0, 0, -1, 200, 0 ] },
+ { time: 500, expected: [ 1, 1, 0, 0, 0, 100 ] },
+ { time: 1000, expected: [ 1, 1, 0, 0, 0, 100 ] }]);
+ }, `${property}: non-invertible matrices`);
+
+ test(t => {
+ const idlName = propertyToIDL(property);
+ const target = createTestElement(t, setup);
+ const animation = target.animate(
+ {
+ [idlName]: [
+ // matrix(0, -1, 1, 0, 250, 0)
+ 'translate(50px) matrix(-1, 0, 0, -1, 200, 0) rotate(90deg)',
+ // matrix(-1, -1, 0, 0, 100, 100)
+ 'translate(100px) matrix( 1, 1, 0, 0, 0, 100) rotate(180deg)',
+ ],
+ },
+ { duration: 1000, fill: 'both' }
+ );
+
+ testAnimationSampleMatrices(animation, idlName,
+ [ { time: 0, expected: [ 0, -1, 1, 0, 250, 0 ] },
+ { time: 499, expected: [ 0, -1, 1, 0, 250, 0 ] },
+ { time: 500, expected: [ -1, -1, 0, 0, 100, 100 ] },
+ { time: 1000, expected: [ -1, -1, 0, 0, 100, 100 ] }]);
+ }, `${property}: non-invertible matrices in matched transform lists`);
+
+ test(t => {
+ const idlName = propertyToIDL(property);
+ const target = createTestElement(t, setup);
+ const animation = target.animate(
+ {
+ [idlName]: [
+ // matrix(-2, 0, 0, -2, 250, 0)
+ 'translate(50px) matrix(-1, 0, 0, -1, 200, 0) scale(2)',
+ // matrix(1, 1, 1, 1, 100, 100)
+ 'translate(100px) matrix( 1, 1, 0, 0, 0, 100) skew(45deg)',
+ ],
+ },
+ { duration: 1000, fill: 'both' }
+ );
+
+ testAnimationSampleMatrices(animation, idlName,
+ [ { time: 0, expected: [ -2, 0, 0, -2, 250, 0 ] },
+ { time: 499, expected: [ -2, 0, 0, -2, 250, 0 ] },
+ { time: 500, expected: [ 1, 1, 1, 1, 100, 100 ] },
+ { time: 1000, expected: [ 1, 1, 1, 1, 100, 100 ] }]);
+ }, `${property}: non-invertible matrices in mismatched transform lists`);
+
+ test(t => {
+ const idlName = propertyToIDL(property);
+ const target = createTestElement(t, setup);
+ const animation = target.animate(
+ {
+ // perspective(0) is treated as perspective(1px)
+ [idlName]: ['perspective(0)', 'perspective(10px)'],
+ },
+ 1000
+ );
+ testAnimationSampleMatrices(animation, idlName,
+ [{ time: 500, expected: [ 1, 0, 0, 0,
+ 0, 1, 0, 0,
+ 0, 0, 1, -0.55,
+ 0, 0, 0, 1 ] }]);
+ }, `${property}: perspective`);
+
+ },
+
+ testAddition: function(property, setup) {
+ test(t => {
+ const idlName = propertyToIDL(property);
+ const target = createTestElement(t, setup);
+ target.style[idlName] = 'translateX(100px)';
+ const animation = target.animate({ [idlName]: ['translateX(-200px)',
+ 'translateX(500px)'] },
+ { duration: 1000, fill: 'both',
+ composite: 'add' });
+ testAnimationSampleMatrices(animation, idlName,
+ [ { time: 0, expected: [ 1, 0, 0, 1, -100, 0 ] },
+ { time: 1000, expected: [ 1, 0, 0, 1, 600, 0 ] }]);
+ }, `${property}: translate`);
+
+ test(t => {
+ const idlName = propertyToIDL(property);
+ const target = createTestElement(t, setup);
+ target.style[idlName] = 'rotate(45deg)';
+ const animation = target.animate({ [idlName]: ['rotate(-90deg)',
+ 'rotate(90deg)'] },
+ { duration: 1000, fill: 'both',
+ composite: 'add' });
+
+ testAnimationSampleMatrices(animation, idlName,
+ [{ time: 0, expected: [ Math.cos(-Math.PI / 4),
+ Math.sin(-Math.PI / 4),
+ -Math.sin(-Math.PI / 4),
+ Math.cos(-Math.PI / 4),
+ 0, 0] },
+ { time: 1000, expected: [ Math.cos(Math.PI * 3 / 4),
+ Math.sin(Math.PI * 3 / 4),
+ -Math.sin(Math.PI * 3 / 4),
+ Math.cos(Math.PI * 3 / 4),
+ 0, 0] }]);
+ }, `${property}: rotate`);
+
+ test(t => {
+ const idlName = propertyToIDL(property);
+ const target = createTestElement(t, setup);
+ target.style[idlName] = 'scale(2)';
+ const animation = target.animate({ [idlName]: ['scale(-3)', 'scale(5)'] },
+ { duration: 1000, fill: 'both',
+ composite: 'add' });
+
+ testAnimationSampleMatrices(animation, idlName,
+ [{ time: 0, expected: [ -6, 0, 0, -6, 0, 0 ] }, // scale(-3) scale(2)
+ { time: 1000, expected: [ 10, 0, 0, 10, 0, 0 ] }]); // scale(5) scale(2)
+ }, `${property}: scale`);
+
+ test(t => {
+ const idlName = propertyToIDL(property);
+ const target = createTestElement(t, setup);
+ // matrix(1, tan(10deg), tan(10deg), 1)
+ target.style[idlName] = 'skew(10deg, 10deg)';
+ const animation = // matrix(1, tan(20deg), tan(-30deg), 1)
+ target.animate({ [idlName]: ['skew(-30deg, 20deg)',
+ // matrix(1, tan(-30deg), tan(20deg), 1)
+ 'skew(20deg, -30deg)'] },
+ { duration: 1000, fill: 'both', composite: 'add' });
+
+ // matrix at 0%.
+ // [ 1 tan(10deg) ] [ 1 tan(-30deg) ]
+ // [ tan(10deg) 1 ] [ tan(20deg) 1 ] =
+ //
+ // [ 1 + tan(10deg) * tan(20deg) tan(-30deg) + tan(10deg) ]
+ // [ tan(10deg) + tan(20deg) tan(10deg) * tan(-30deg) + 1 ]
+
+ // matrix at 100%.
+ // [ 1 tan(10deg) ] [ 1 tan(20deg) ]
+ // [ tan(10deg) 1 ] [ tan(-30deg) 1 ] =
+ //
+ // [ 1 + tan(10deg) * tan(-30deg) tan(20deg) + tan(10deg) ]
+ // [ tan(10deg) + tan(-30deg) tan(10deg) * tan(20deg) + 1 ]
+
+ testAnimationSampleMatrices(animation, idlName,
+ [{ time: 0, expected: [ 1 + Math.tan(Math.PI/18) * Math.tan(Math.PI/9),
+ Math.tan(Math.PI/18) + Math.tan(Math.PI/9),
+ Math.tan(-Math.PI/6) + Math.tan(Math.PI/18),
+ 1 + Math.tan(Math.PI/18) * Math.tan(-Math.PI/6),
+ 0, 0] },
+ { time: 1000, expected: [ 1 + Math.tan(Math.PI/18) * Math.tan(-Math.PI/6),
+ Math.tan(Math.PI/18) + Math.tan(-Math.PI/6),
+ Math.tan(Math.PI/9) + Math.tan(Math.PI/18),
+ 1 + Math.tan(Math.PI/18) * Math.tan(Math.PI/9),
+ 0, 0] }]);
+ }, `${property}: skew`);
+
+ test(t => {
+ const idlName = propertyToIDL(property);
+ const target = createTestElement(t, setup);
+ // matrix(1, 0, 0, 1, 100, 0)
+ target.style[idlName] = 'translateX(100px)';
+ const animation = // matrix(0, 1, -1, 0, 0, 0)
+ target.animate({ [idlName]: ['rotate(90deg)',
+ // matrix(-1, 0, 0, -1, 0, 0)
+ 'rotate(180deg)'] },
+ { duration: 1000, fill: 'both', composite: 'add' });
+
+ testAnimationSampleMatrices(animation, idlName,
+ [{ time: 0, expected: [ 0, 1, -1, 0, 100, 0 ] },
+ { time: 1000, expected: [ -1, 0, 0, -1, 100, 0 ] }]);
+ }, `${property}: rotate on translate`);
+
+ test(t => {
+ const idlName = propertyToIDL(property);
+ const target = createTestElement(t, setup);
+ // matrix(0, 1, -1, 0, 0, 0)
+ target.style[idlName] = 'rotate(90deg)';
+ const animation = // matrix(1, 0, 0, 1, 100, 0)
+ target.animate({ [idlName]: ['translateX(100px)',
+ // matrix(1, 0, 0, 1, 200, 0)
+ 'translateX(200px)'] },
+ { duration: 1000, fill: 'both', composite: 'add' });
+
+ testAnimationSampleMatrices(animation, idlName,
+ [{ time: 0, expected: [ 0, 1, -1, 0, 0, 100 ] },
+ { time: 1000, expected: [ 0, 1, -1, 0, 0, 200 ] }]);
+ }, `${property}: translate on rotate`);
+
+ test(t => {
+ const idlName = propertyToIDL(property);
+ const target = createTestElement(t, setup);
+ target.style[idlName] = 'rotate(45deg) translateX(100px)';
+ const animation = target.animate({ [idlName]: ['rotate(-90deg)',
+ 'rotate(90deg)'] },
+ { duration: 1000, fill: 'both',
+ composite: 'add' });
+
+ testAnimationSampleMatrices(animation, idlName,
+ [{ time: 0, expected: [ Math.cos(-Math.PI / 4),
+ Math.sin(-Math.PI / 4),
+ -Math.sin(-Math.PI / 4),
+ Math.cos(-Math.PI / 4),
+ 100 * Math.cos(Math.PI / 4),
+ 100 * Math.sin(Math.PI / 4) ] },
+ { time: 1000, expected: [ Math.cos(Math.PI * 3 / 4),
+ Math.sin(Math.PI * 3 / 4),
+ -Math.sin(Math.PI * 3 / 4),
+ Math.cos(Math.PI * 3 / 4),
+ 100 * Math.cos(Math.PI / 4),
+ 100 * Math.sin(Math.PI / 4) ] }]);
+ }, `${property}: rotate on rotate and translate`);
+
+ test(t => {
+ const idlName = propertyToIDL(property);
+ const target = createTestElement(t, setup);
+ target.style[idlName] = 'matrix(0, 1, -1, 0, 0, 0)';
+ const animation = // Same matrices as above.
+ target.animate({ [idlName]: [ 'matrix(1, 0, 0, 1, 100, 0)',
+ 'matrix(1, 0, 0, 1, 200, 0)' ] },
+ { duration: 1000, fill: 'both', composite: 'add' });
+
+ testAnimationSampleMatrices(animation, idlName,
+ [{ time: 0, expected: [ 0, 1, -1, 0, 0, 100 ] },
+ { time: 1000, expected: [ 0, 1, -1, 0, 0, 200 ] }]);
+ }, `${property}: matrix`);
+
+ test(t => {
+ const idlName = propertyToIDL(property);
+ const target = createTestElement(t, setup);
+ target.style[idlName] = 'rotate3d(1, 1, 0, 45deg)';
+ const animation =
+ target.animate({ [idlName]: [ 'rotate3d(1, 1, 0, -90deg)',
+ 'rotate3d(1, 1, 0, 90deg)'] },
+ { duration: 1000, fill: 'both', composite: 'add' });
+
+ testAnimationSampleMatrices(animation, idlName,
+ [{ time: 0, expected: rotate3dToMatrix(1, 1, 0, -Math.PI / 4) },
+ { time: 1000, expected: rotate3dToMatrix(1, 1, 0, 3 * Math.PI / 4) }]);
+ }, `${property}: rotate3d`);
+
+ test(t => {
+ const idlName = propertyToIDL(property);
+ const target = createTestElement(t, setup);
+ // To calculate expected matrices easily, generate input matrices from
+ // rotate3d.
+ target.style[idlName] = rotate3dToMatrix3d(1, 1, 0, Math.PI / 4);
+ const from = rotate3dToMatrix3d(1, 1, 0, -Math.PI / 2);
+ const to = rotate3dToMatrix3d(1, 1, 0, Math.PI / 2);
+ const animation =
+ target.animate({ [idlName]: [ from, to ] },
+ { duration: 1000, fill: 'both', composite: 'add' });
+
+ testAnimationSampleMatrices(animation, idlName,
+ [{ time: 0, expected: rotate3dToMatrix(1, 1, 0, -Math.PI / 4) },
+ { time: 1000, expected: rotate3dToMatrix(1, 1, 0, 3 * Math.PI / 4) }]);
+ }, `${property}: matrix3d`);
+
+ // Following tests aim for test the addition behavior for non-invertible
+ // matrices. Note that the addition for non-invertible matrices should be
+ // the same, just like addition for invertible matrices. With these tests,
+ // we can assure that addition never behaves as discrete. The non-invertible
+ // matrix that we use is the singular matrix, matrix(1, 1, 0, 0, 0, 100).
+ test(t => {
+ const idlName = propertyToIDL(property);
+ const target = createTestElement(t, setup);
+ target.style[idlName] = 'translateX(50px)';
+ const animation =
+ target.animate({ [idlName]: ['matrix(-1, 0, 0, -1, 200, 0)',
+ 'matrix( 1, 1, 0, 0, 0, 100)'] },
+ { duration: 1000, fill: 'both', composite: 'add' });
+
+ testAnimationSampleMatrices(animation, idlName,
+ [ { time: 0, expected: [ -1, 0, 0, -1, 250, 0 ] },
+ { time: 1000, expected: [ 1, 1, 0, 0, 50, 100 ] }]);
+ }, `${property}: non-invertible matrices`);
+
+ test(t => {
+ const idlName = propertyToIDL(property);
+ const target = createTestElement(t, setup);
+ target.style[idlName] = 'translateX(50px)';
+ const animation = // matrix(0, -1, 1, 0, 200, 0)
+ target.animate({ [idlName]: ['matrix(-1, 0, 0, -1, 200, 0) rotate(90deg)',
+ // matrix(-1, -1, 0, 0, 0, 100)
+ 'matrix( 1, 1, 0, 0, 0, 100) rotate(180deg)'] },
+ { duration: 1000, fill: 'both', composite: 'add' });
+
+ testAnimationSampleMatrices(animation, idlName,
+ [ { time: 0, expected: [ 0, -1, 1, 0, 250, 0 ] },
+ { time: 1000, expected: [ -1, -1, 0, 0, 50, 100 ] }]);
+ }, `${property}: non-invertible matrices in matched transform lists`);
+
+ test(t => {
+ const idlName = propertyToIDL(property);
+ const target = createTestElement(t, setup);
+ target.style[idlName] = 'translateX(50px)';
+ const animation = // matrix(-2, 0, 0, -2, 200, 0)
+ target.animate({ [idlName]: ['matrix(-1, 0, 0, -1, 200, 0) scale(2)',
+ // matrix(1, 1, 1, 1, 0, 100)
+ 'matrix( 1, 1, 0, 0, 0, 100) skew(45deg)'] },
+ { duration: 1000, fill: 'both', composite: 'add' });
+
+ testAnimationSampleMatrices(animation, idlName,
+ [ { time: 0, expected: [ -2, 0, 0, -2, 250, 0 ] },
+ { time: 1000, expected: [ 1, 1, 1, 1, 50, 100 ] }]);
+ }, `${property}: non-invertible matrices in mismatched transform lists`);
+ },
+
+ testAccumulation: function(property, setup) {
+ test(t => {
+ const idlName = propertyToIDL(property);
+ const target = createTestElement(t, setup);
+ target.style[idlName] = 'translateX(100px)';
+ const animation = target.animate({ [idlName]: ['translateX(-200px)',
+ 'translateX(500px)'] },
+ { duration: 1000, fill: 'both',
+ composite: 'accumulate' });
+ testAnimationSampleMatrices(animation, idlName,
+ [ { time: 0, expected: [ 1, 0, 0, 1, -100, 0 ] },
+ { time: 1000, expected: [ 1, 0, 0, 1, 600, 0 ] }]);
+ }, `${property}: translate`);
+
+ test(t => {
+ const idlName = propertyToIDL(property);
+ const target = createTestElement(t, setup);
+ target.style[idlName] = 'rotate(45deg)';
+ const animation = target.animate({ [idlName]: ['rotate(-90deg)',
+ 'rotate(90deg)'] },
+ { duration: 1000, fill: 'both',
+ composite: 'accumulate' });
+
+ testAnimationSampleMatrices(animation, idlName,
+ [{ time: 0, expected: [ Math.cos(-Math.PI / 4),
+ Math.sin(-Math.PI / 4),
+ -Math.sin(-Math.PI / 4),
+ Math.cos(-Math.PI / 4),
+ 0, 0] },
+ { time: 1000, expected: [ Math.cos(Math.PI * 3 / 4),
+ Math.sin(Math.PI * 3 / 4),
+ -Math.sin(Math.PI * 3 / 4),
+ Math.cos(Math.PI * 3 / 4),
+ 0, 0] }]);
+ }, `${property}: rotate`);
+
+ test(t => {
+ const idlName = propertyToIDL(property);
+ const target = createTestElement(t, setup);
+ target.style[idlName] = 'scale(2)';
+ const animation = target.animate({ [idlName]: ['scale(-3)', 'scale(5)'] },
+ { duration: 1000, fill: 'both',
+ composite: 'accumulate' });
+
+ testAnimationSampleMatrices(animation, idlName,
+ // scale((2 - 1) + (-3 - 1) + 1)
+ [{ time: 0, expected: [ -2, 0, 0, -2, 0, 0 ] },
+ // scale((2 - 1) + (5 - 1) + 1)
+ { time: 1000, expected: [ 6, 0, 0, 6, 0, 0 ] }]);
+ }, `${property}: scale`);
+
+ test(t => {
+ const idlName = propertyToIDL(property);
+ const target = createTestElement(t, setup);
+ // matrix(1, tan(10deg), tan(10deg), 1)
+ target.style[idlName] = 'skew(10deg, 10deg)';
+ const animation = // matrix(1, tan(20deg), tan(-30deg), 1)
+ target.animate({ [idlName]: ['skew(-30deg, 20deg)',
+ // matrix(1, tan(-30deg), tan(20deg), 1)
+ 'skew(20deg, -30deg)'] },
+ { duration: 1000, fill: 'both', composite: 'accumulate' });
+
+ testAnimationSampleMatrices(animation, idlName,
+ [{ time: 0, expected: [ 1, Math.tan(Math.PI/6),
+ Math.tan(-Math.PI/9), 1,
+ 0, 0] },
+ { time: 1000, expected: [ 1, Math.tan(-Math.PI/9),
+ Math.tan(Math.PI/6), 1,
+ 0, 0] }]);
+ }, `${property}: skew`);
+
+ test(t => {
+ const idlName = propertyToIDL(property);
+ const target = createTestElement(t, setup);
+ // matrix(1, 0, 0, 1, 100, 0)
+ target.style[idlName] = 'translateX(100px)';
+ const animation = // matrix(0, 1, -1, 0, 0, 0)
+ target.animate({ [idlName]: ['rotate(90deg)',
+ // matrix(-1, 0, 0, -1, 0, 0)
+ 'rotate(180deg)'] },
+ { duration: 1000, fill: 'both', composite: 'accumulate' });
+
+ testAnimationSampleMatrices(animation, idlName,
+ [{ time: 0, expected: [ 0, 1, -1, 0, 100, 0 ] },
+ { time: 1000, expected: [ -1, 0, 0, -1, 100, 0 ] }]);
+ }, `${property}: rotate on translate`);
+
+ test(t => {
+ const idlName = propertyToIDL(property);
+ const target = createTestElement(t, setup);
+ // matrix(0, 1, -1, 0, 0, 0)
+ target.style[idlName] = 'rotate(90deg)';
+ const animation = // matrix(1, 0, 0, 1, 100, 0)
+ target.animate({ [idlName]: ['translateX(100px)',
+ // matrix(1, 0, 0, 1, 200, 0)
+ 'translateX(200px)'] },
+ { duration: 1000, fill: 'both', composite: 'accumulate' });
+
+ testAnimationSampleMatrices(animation, idlName,
+ [{ time: 0, expected: [ 0, 1, -1, 0, 100, 0 ] },
+ { time: 1000, expected: [ 0, 1, -1, 0, 200, 0 ] }]);
+ }, `${property}: translate on rotate`);
+
+ test(t => {
+ const idlName = propertyToIDL(property);
+ const target = createTestElement(t, setup);
+ target.style[idlName] = 'rotate(45deg)';
+ const animation =
+ target.animate({ [idlName]: ['rotate(45deg) translateX(0px)',
+ 'rotate(45deg) translateX(100px)'] },
+ { duration: 1000, fill: 'both', composite: 'accumulate' });
+
+ testAnimationSampleMatrices(animation, idlName,
+ [{ time: 0, expected: [ Math.cos(Math.PI / 2),
+ Math.sin(Math.PI / 2),
+ -Math.sin(Math.PI / 2),
+ Math.cos(Math.PI / 2),
+ 0 * Math.cos(Math.PI / 2),
+ 0 * Math.sin(Math.PI / 2) ] },
+ { time: 1000, expected: [ Math.cos(Math.PI / 2),
+ Math.sin(Math.PI / 2),
+ -Math.sin(Math.PI / 2),
+ Math.cos(Math.PI / 2),
+ 100 * Math.cos(Math.PI / 2),
+ 100 * Math.sin(Math.PI / 2) ] }]);
+ }, `${property}: rotate and translate on rotate`);
+
+ test(t => {
+ const idlName = propertyToIDL(property);
+ const target = createTestElement(t, setup);
+ target.style[idlName] = 'rotate(45deg) translateX(100px)';
+ const animation =
+ target.animate({ [idlName]: ['rotate(45deg)', 'rotate(45deg)'] },
+ { duration: 1000, fill: 'both', composite: 'accumulate' });
+
+ testAnimationSampleMatrices(animation, idlName,
+ [{ time: 0, expected: [ Math.cos(Math.PI / 2),
+ Math.sin(Math.PI / 2),
+ -Math.sin(Math.PI / 2),
+ Math.cos(Math.PI / 2),
+ 100 * Math.cos(Math.PI / 2),
+ 100 * Math.sin(Math.PI / 2) ] },
+ { time: 1000, expected: [ Math.cos(Math.PI / 2),
+ Math.sin(Math.PI / 2),
+ -Math.sin(Math.PI / 2),
+ Math.cos(Math.PI / 2),
+ 100 * Math.cos(Math.PI / 2),
+ 100 * Math.sin(Math.PI / 2) ] }]);
+ }, `${property}: rotate on rotate and translate`);
+
+ test(t => {
+ const idlName = propertyToIDL(property);
+ const target = createTestElement(t, setup);
+ target.style[idlName] = 'matrix(0, 1, -1, 0, 0, 0)';
+ const animation = // Same matrices as above.
+ target.animate({ [idlName]: [ 'matrix(1, 0, 0, 1, 100, 0)',
+ 'matrix(1, 0, 0, 1, 200, 0)' ] },
+ { duration: 1000, fill: 'both', composite: 'accumulate' });
+
+ testAnimationSampleMatrices(animation, idlName,
+ [{ time: 0, expected: [ 0, 1, -1, 0, 100, 0 ] },
+ { time: 1000, expected: [ 0, 1, -1, 0, 200, 0 ] }]);
+ }, `${property}: matrix`);
+
+ test(t => {
+ const idlName = propertyToIDL(property);
+ const target = createTestElement(t, setup);
+ target.style[idlName] = 'rotate3d(1, 1, 0, 45deg)';
+ const animation =
+ target.animate({ [idlName]: [ 'rotate3d(1, 1, 0, -90deg)',
+ 'rotate3d(1, 1, 0, 90deg)'] },
+ { duration: 1000, fill: 'both', composite: 'accumulate' });
+
+ testAnimationSampleMatrices(animation, idlName,
+ [{ time: 0, expected: rotate3dToMatrix(1, 1, 0, -Math.PI / 4) },
+ { time: 1000, expected: rotate3dToMatrix(1, 1, 0, 3 * Math.PI / 4) }]);
+ }, `${property}: rotate3d`);
+
+ test(t => {
+ const idlName = propertyToIDL(property);
+ const target = createTestElement(t, setup);
+ // To calculate expected matrices easily, generate input matrices from
+ // rotate3d.
+ target.style[idlName] = rotate3dToMatrix3d(1, 1, 0, Math.PI / 4);
+ const from = rotate3dToMatrix3d(1, 1, 0, -Math.PI / 2);
+ const to = rotate3dToMatrix3d(1, 1, 0, Math.PI / 2);
+ const animation =
+ target.animate({ [idlName]: [ from, to ] },
+ { duration: 1000, fill: 'both', composite: 'accumulate' });
+
+ testAnimationSampleMatrices(animation, idlName,
+ [{ time: 0, expected: rotate3dToMatrix(1, 1, 0, -Math.PI / 4) },
+ { time: 1000, expected: rotate3dToMatrix(1, 1, 0, 3 * Math.PI / 4) }]);
+ }, `${property}: matrix3d`);
+
+ test(t => {
+ const idlName = propertyToIDL(property);
+ const target = createTestElement(t, setup);
+ const matrixArray = [ 1, 0, 0, 0,
+ 0, 1, 0, 0,
+ 0, 0, 1, 0,
+ 0, 0, 1, 1 ];
+
+ target.style[idlName] = createMatrixFromArray(matrixArray);
+ const animation =
+ target.animate({ [idlName]: [ 'none', 'none' ] },
+ { duration: 1000, fill: 'both', composite: 'accumulate' });
+
+ testAnimationSampleMatrices(animation, idlName,
+ [{ time: 0, expected: matrixArray },
+ { time: 1000, expected: matrixArray }]);
+ }, `${property}: none`);
+
+ // Following tests aim for test the fallback discrete accumulation behavior
+ // for non-invertible matrices. The non-invertible matrix that we use is the
+ // singular matrix, matrix(1, 1, 0, 0, 0, 100).
+ test(t => {
+ const idlName = propertyToIDL(property);
+ const target = createTestElement(t, setup);
+ target.animate({ [idlName]: ['matrix(-1, 0, 0, -1, 200, 0)',
+ 'matrix(-1, 0, 0, -1, 200, 0)'] }, 1000);
+ const animation = target.animate(
+ {
+ [idlName]: [
+ 'matrix( 1, 1, 0, 0, 0, 100)',
+ 'matrix( 1, 1, 0, 0, 0, 100)',
+ ],
+ },
+ { duration: 1000, composite: 'accumulate' }
+ );
+ testAnimationSampleMatrices(animation, idlName, [
+ { time: 0, expected: [1, 1, 0, 0, 0, 100] },
+ ]);
+ }, `${property}: non-invertible matrices (non-invertible onto invertible)`);
+
+ test(t => {
+ const idlName = propertyToIDL(property);
+ const target = createTestElement(t, setup);
+ target.animate({ [idlName]: ['matrix( 1, 1, 0, 0, 0, 100)',
+ 'matrix( 1, 1, 0, 0, 0, 100)'] }, 1000);
+ const animation = target.animate(
+ {
+ [idlName]: [
+ 'matrix(-1, 0, 0, -1, 200, 0)',
+ 'matrix(-1, 0, 0, -1, 200, 0)',
+ ],
+ },
+ { duration: 1000, composite: 'accumulate' }
+ );
+ testAnimationSampleMatrices(animation, idlName, [
+ { time: 0, expected: [-1, 0, 0, -1, 200, 0] },
+ ]);
+ }, `${property}: non-invertible matrices (invertible onto non-invertible)`);
+
+ test(t => {
+ const idlName = propertyToIDL(property);
+ const target = createTestElement(t, setup);
+ // matrix(0, -1, 1, 0, 250, 0)
+ target.animate(
+ {
+ [idlName]: [
+ 'translate(50px) matrix(-1, 0, 0, -1, 200, 0) rotate(90deg)',
+ 'translate(50px) matrix(-1, 0, 0, -1, 200, 0) rotate(90deg)',
+ ],
+ },
+ 1000
+ );
+ // matrix(-1, -1, 0, 0, 100, 100)
+ const animation = target.animate(
+ {
+ [idlName]: [
+ 'translate(100px) matrix( 1, 1, 0, 0, 0, 100) rotate(180deg)',
+ 'translate(100px) matrix( 1, 1, 0, 0, 0, 100) rotate(180deg)',
+ ],
+ },
+ { duration: 1000, composite: 'accumulate' }
+ );
+ testAnimationSampleMatrices(animation, idlName, [
+ { time: 0, expected: [-1, -1, 0, 0, 100, 100] },
+ ]);
+ }, `${property}: non-invertible matrices in matched transform lists (non-invertible onto invertible)`);
+
+ test(t => {
+ const idlName = propertyToIDL(property);
+ const target = createTestElement(t, setup);
+ // matrix(-1, -1, 0, 0, 100, 100)
+ target.animate(
+ {
+ [idlName]: [
+ 'translate(100px) matrix(1, 1, 0, 0, 0, 100) rotate(180deg)',
+ 'translate(100px) matrix(1, 1, 0, 0, 0, 100) rotate(180deg)',
+ ],
+ },
+ 1000
+ );
+ // matrix(0, -1, 1, 0, 250, 0)
+ const animation = target.animate(
+ {
+ [idlName]: [
+ 'translate(50px) matrix(-1, 0, 0, -1, 200, 0) rotate(90deg)',
+ 'translate(50px) matrix(-1, 0, 0, -1, 200, 0) rotate(90deg)',
+ ],
+ },
+ { duration: 1000, composite: 'accumulate' }
+ );
+ testAnimationSampleMatrices(animation, idlName, [
+ { time: 0, expected: [0, -1, 1, 0, 250, 0] },
+ ]);
+ }, `${property}: non-invertible matrices in matched transform lists (invertible onto non-invertible)`);
+
+ test(t => {
+ const idlName = propertyToIDL(property);
+ const target = createTestElement(t, setup);
+ // matrix(-2, 0, 0, -2, 250, 0)
+ target.animate(
+ {
+ [idlName]: [
+ 'translate(50px) matrix(-1, 0, 0, -1, 200, 0) scale(2)',
+ 'translate(50px) matrix(-1, 0, 0, -1, 200, 0) scale(2)',
+ ],
+ },
+ 1000
+ );
+ // matrix(1, 1, 1, 1, 100, 100)
+ const animation = target.animate(
+ {
+ [idlName]: [
+ 'translate(100px) matrix(1, 1, 0, 0, 0, 100) skew(45deg)',
+ 'translate(100px) matrix(1, 1, 0, 0, 0, 100) skew(45deg)',
+ ],
+ },
+ { duration: 1000, composite: 'accumulate' }
+ );
+ testAnimationSampleMatrices(animation, idlName, [
+ { time: 0, expected: [1, 1, 1, 1, 100, 100] },
+ ]);
+ }, `${property}: non-invertible matrices in mismatched transform lists`
+ + ' (non-invertible onto invertible)');
+
+ test(t => {
+ const idlName = propertyToIDL(property);
+ const target = createTestElement(t, setup);
+ // matrix(1, 1, 1, 1, 100, 100)
+ target.animate(
+ {
+ [idlName]: [
+ 'translate(100px) matrix(1, 1, 0, 0, 0, 100) skew(45deg)',
+ 'translate(100px) matrix(1, 1, 0, 0, 0, 100) skew(45deg)',
+ ],
+ },
+ 1000
+ );
+ // matrix(-2, 0, 0, -2, 250, 0)
+ const animation = target.animate(
+ {
+ [idlName]: [
+ 'translate(50px) matrix(-1, 0, 0, -1, 200, 0) scale(2)',
+ 'translate(50px) matrix(-1, 0, 0, -1, 200, 0) scale(2)',
+ ],
+ },
+ { duration: 1000, composite: 'accumulate' }
+ );
+ testAnimationSampleMatrices(animation, idlName, [
+ { time: 0, expected: [-2, 0, 0, -2, 250, 0] },
+ ]);
+ }, `${property}: non-invertible matrices in mismatched transform lists`
+ + ' (invertible onto non-invertible)');
+ },
+};
+
+const rotateListType = {
+ testInterpolation: (property, setup) => {
+ test(t => {
+ const idlName = propertyToIDL(property);
+ const target = createTestElement(t, setup);
+ const animation = target.animate(
+ {
+ [idlName]: ['45deg', '135deg'],
+ },
+ 1000
+ );
+
+ testAnimationSamples(animation, idlName,
+ [{ time: 500, expected: '90deg' }]);
+ }, `${property} without rotation axes`);
+
+ test(t => {
+ const idlName = propertyToIDL(property);
+ const target = createTestElement(t, setup);
+ const animation =
+ target.animate({ [idlName]: [ '0 1 0 0deg',
+ '0 1 0 90deg'] },
+ 1000);
+
+ testAnimationSamples(animation, idlName,
+ [{ time: 500, expected: 'y 45deg' }]);
+ }, `${property} with rotation axes`);
+
+ test(t => {
+ const idlName = propertyToIDL(property);
+ const target = createTestElement(t, setup);
+
+ const animation =
+ target.animate({ [idlName]: [ '0 1 0 0deg',
+ '0 1 0 720deg'] },
+ 1000);
+
+ testAnimationSamples(animation, idlName,
+ [{ time: 250, expected: 'y 180deg' }]);
+ }, `${property} with rotation axes and range over 360 degrees`);
+
+ test(t => {
+ const idlName = propertyToIDL(property);
+ const target = createTestElement(t, setup);
+ const animation =
+ target.animate({ [idlName]: [ '0 1 0 0deg',
+ '0 1 1 90deg'] },
+ 1000);
+
+ testAnimationSampleRotate3d(animation, idlName,
+ [{ time: 500, expected: '0 0.707107 0.707107 45deg' }]);
+ }, `${property} with different rotation axes`);
+ },
+ testAddition: function(property, setup) {
+ test(t => {
+ const idlName = propertyToIDL(property);
+ const target = createTestElement(t, setup);
+ target.style[idlName] = '45deg';
+ const animation = target.animate({ [idlName]: ['-90deg', '90deg'] },
+ { duration: 1000, fill: 'both',
+ composite: 'add' });
+
+ testAnimationSamples(animation, idlName,
+ [{ time: 0, expected: '-45deg' },
+ { time: 1000, expected: '135deg' }]);
+ }, `${property} without rotation axes`);
+
+ test(t => {
+ const idlName = propertyToIDL(property);
+ const target = createTestElement(t, setup);
+ // Rotation specified in transform property should not affect the computed
+ // value of |property|.
+ target.style.transform = 'rotate(20deg)';
+ target.style[idlName] = 'y -45deg';
+ const animation =
+ target.animate({ [idlName]: ['0 1 0 90deg',
+ '0 1 0 180deg'] },
+ { duration: 1000, fill: 'both', composite: 'add' });
+
+ testAnimationSamples(animation, idlName,
+ [{ time: 0, expected: 'y 45deg' },
+ { time: 1000, expected: 'y 135deg' }]);
+ }, `${property} with underlying transform`);
+
+ test(t => {
+ const idlName = propertyToIDL(property);
+ const target = createTestElement(t, setup);
+ const animation =
+ target.animate({ [idlName]: [ '0 1 0 0deg',
+ '1 1 1 270deg'] },
+ { duration: 1000, fill: 'both', composite: 'add' });
+
+ testAnimationSampleRotate3d(animation, idlName,
+ [{ time: 500, expected: '0.57735 0.57735 0.57735 135deg' }]);
+ }, `${property} with different rotation axes`);
+ },
+ testAccumulation: function(property, setup) {
+ test(t => {
+ const idlName = propertyToIDL(property);
+ const target = createTestElement(t, setup);
+ target.style[idlName] = '45deg';
+ const animation = target.animate({ [idlName]: ['-90deg', '90deg'] },
+ { duration: 1000, fill: 'both',
+ composite: 'accumulate' });
+
+ testAnimationSamples(animation, idlName,
+ [{ time: 0, expected: '-45deg' },
+ { time: 1000, expected: '135deg' }]);
+ }, `${property} without rotation axes`);
+
+ test(t => {
+ const idlName = propertyToIDL(property);
+ const target = createTestElement(t, setup);
+ target.style.transform = 'translateX(100px)';
+ target.style[idlName] = '1 0 0 -45deg';
+ const animation =
+ target.animate({ [idlName]: ['1 0 0 90deg',
+ '1 0 0 180deg'] },
+ { duration: 1000, fill: 'both', composite: 'accumulate' });
+
+ testAnimationSamples(animation, idlName,
+ [{ time: 0, expected: 'x 45deg' },
+ { time: 1000, expected: 'x 135deg' }]);
+ }, `${property} with underlying transform`);
+
+ test(t => {
+ const idlName = propertyToIDL(property);
+ const target = createTestElement(t, setup);
+ const animation =
+ target.animate({ [idlName]: [ '0 1 0 0deg',
+ '1 0 1 180deg'] },
+ { duration: 1000, fill: 'both', composite: 'accumulate' });
+
+ testAnimationSampleRotate3d(animation, idlName,
+ [{ time: 500, expected: '0.707107 0 0.707107 90deg' }]);
+ }, `${property} with different rotation axes`);
+ },
+};
+
+const translateListType = {
+ testInterpolation: (property, setup) => {
+ test(t => {
+ const idlName = propertyToIDL(property);
+ const target = createTestElement(t, setup);
+ const animation = target.animate(
+ {
+ [idlName]: ['200px', '400px'],
+ },
+ 1000
+ );
+ testAnimationSamples(animation, idlName,
+ [{ time: 500, expected: '300px' }]);
+ }, `${property} with two unspecified values`);
+
+ test(t => {
+ const idlName = propertyToIDL(property);
+ const target = createTestElement(t, setup);
+ const animation = target.animate(
+ {
+ [idlName]: ['200px -200px', '400px 400px'],
+ },
+ 1000
+ );
+ testAnimationSamples(animation, idlName,
+ [{ time: 500, expected: '300px 100px' }]);
+ }, `${property} with one unspecified value`);
+
+ test(t => {
+ const idlName = propertyToIDL(property);
+ const target = createTestElement(t, setup);
+ const animation = target.animate(
+ {
+ [idlName]: ['200px -200px 600px', '400px 400px -200px'],
+ },
+ 1000
+ );
+ testAnimationSamples(animation, idlName,
+ [{ time: 500, expected: '300px 100px 200px' }]);
+ }, `${property} with all three values specified`);
+
+ test(t => {
+ const idlName = propertyToIDL(property);
+ const target = createTestElement(t, setup);
+ const animation = target.animate(
+ {
+ [idlName]: ['0% -101px 600px', '400px 50% -200px'],
+ },
+ 1000
+ );
+ testAnimationSamples(animation, idlName,
+ [{ time: 500, expected: 'calc(0% + 200px) calc(25% - 50.5px) 200px' }]);
+ }, `${property} with combination of percentages and lengths`);
+ },
+ testAddition: function(property, setup) {
+ test(t => {
+ const idlName = propertyToIDL(property);
+ const target = createTestElement(t, setup);
+ target.style[idlName] = '100px';
+ const animation = target.animate({ [idlName]: ['-200px', '500px'] },
+ { duration: 1000, fill: 'both',
+ composite: 'add' });
+ testAnimationSamples(animation, idlName,
+ [ { time: 0, expected: '-100px' },
+ { time: 1000, expected: '600px' }]);
+
+ }, `${property}`);
+
+ test(t => {
+ const idlName = propertyToIDL(property);
+ const target = createTestElement(t, setup);
+ target.transform = 'translateY(100px)';
+ target.style[idlName] = '100px';
+ const animation = target.animate({ [idlName]: ['-200px', '500px'] },
+ { duration: 1000, fill: 'both',
+ composite: 'add' });
+ testAnimationSamples(animation, idlName,
+ [ { time: 0, expected: '-100px' },
+ { time: 1000, expected: '600px' }]);
+
+ }, `${property} with underlying transform`);
+
+ test(t => {
+ const idlName = propertyToIDL(property);
+ const target = createTestElement(t, setup);
+ target.style[idlName] = '50%';
+ const animation = target.animate({ [idlName]: ['-200px', '500px'] },
+ { duration: 1000, fill: 'both',
+ composite: 'add' });
+ testAnimationSamples(animation, idlName,
+ [ { time: 0, expected: 'calc(50% - 200px)' },
+ { time: 1000, expected: 'calc(50% + 500px)' }]);
+
+ }, `${property} with underlying percentage value`);
+ },
+ testAccumulation: function(property, setup) {
+ test(t => {
+ const idlName = propertyToIDL(property);
+ const target = createTestElement(t, setup);
+ target.style[idlName] = '100px';
+ const animation = target.animate({ [idlName]: ['-200px', '500px'] },
+ { duration: 1000, fill: 'both',
+ composite: 'accumulate' });
+ testAnimationSamples(animation, idlName,
+ [ { time: 0, expected: '-100px' },
+ { time: 1000, expected: '600px' }]);
+ }, `${property}`);
+
+ test(t => {
+ const idlName = propertyToIDL(property);
+ const target = createTestElement(t, setup);
+ target.transform = 'translateY(100px)';
+ target.style[idlName] = '100px';
+ const animation = target.animate({ [idlName]: ['-200px', '500px'] },
+ { duration: 1000, fill: 'both',
+ composite: 'accumulate' });
+ testAnimationSamples(animation, idlName,
+ [ { time: 0, expected: '-100px' },
+ { time: 1000, expected: '600px' }]);
+ }, `${property} with transform`);
+ },
+};
+
+const scaleListType = {
+ testInterpolation: (property, setup) => {
+ test(t => {
+ const idlName = propertyToIDL(property);
+ const target = createTestElement(t, setup);
+ const animation = target.animate({ [idlName]: ['3', '5'] },
+ 1000);
+
+ testAnimationSamples(animation, idlName,
+ [{ time: 500, expected: '4' }]);
+ }, `${property} with two unspecified values`);
+
+ test(t => {
+ const idlName = propertyToIDL(property);
+ const target = createTestElement(t, setup);
+ const animation = target.animate({ [idlName]: ['3 3', '5 5'] },
+ 1000);
+
+ testAnimationSamples(animation, idlName,
+ [{ time: 500, expected: '4' }]);
+ }, `${property} with one unspecified value`);
+
+ test(t => {
+ const idlName = propertyToIDL(property);
+ const target = createTestElement(t, setup);
+ const animation = target.animate({ [idlName]: ['3 3 3', '5 5 5'] },
+ 1000);
+
+ testAnimationSamples(animation, idlName,
+ [{ time: 500, expected: '4 4 4' }]);
+ }, `${property}`);
+ },
+ testAddition: function(property, setup) {
+ test(t => {
+ const idlName = propertyToIDL(property);
+ const target = createTestElement(t, setup);
+ target.style[idlName] = '2';
+ const animation = target.animate({ [idlName]: ['-3', '5'] },
+ { duration: 1000, fill: 'both',
+ composite: 'add' });
+
+ testAnimationSamples(animation, idlName,
+ [{ time: 0, expected: '-6' },
+ { time: 1000, expected: '10' }]);
+ }, `${property} with two unspecified values`);
+
+ test(t => {
+ const idlName = propertyToIDL(property);
+ const target = createTestElement(t, setup);
+ target.style[idlName] = '2 2';
+ const animation = target.animate({ [idlName]: ['-3 -3', '5 5'] },
+ { duration: 1000, fill: 'both',
+ composite: 'add' });
+
+ testAnimationSamples(animation, idlName,
+ [{ time: 0, expected: '-6' },
+ { time: 1000, expected: '10' }]);
+ }, `${property} with one unspecified value`);
+
+ test(t => {
+ const idlName = propertyToIDL(property);
+ const target = createTestElement(t, setup);
+ target.style[idlName] = '2 2 2';
+ const animation = target.animate({ [idlName]: ['-1 -2 -3', '4 5 6'] },
+ { duration: 1000, fill: 'both',
+ composite: 'add' });
+
+ testAnimationSamples(animation, idlName,
+ [{ time: 0, expected: '-2 -4 -6' },
+ { time: 1000, expected: '8 10 12' }]);
+ }, `${property}`);
+ },
+ testAccumulation: function(property, setup) {
+ test(t => {
+ const idlName = propertyToIDL(property);
+ const target = createTestElement(t, setup);
+ target.style[idlName] = '2';
+ const animation = target.animate({ [idlName]: ['-3', '5'] },
+ { duration: 1000, fill: 'both',
+ composite: 'accumulate' });
+
+ testAnimationSamples(animation, idlName,
+ [{ time: 0, expected: '-2' },
+ { time: 1000, expected: '6' }]);
+ }, `${property} with two unspecified values`);
+
+ test(t => {
+ const idlName = propertyToIDL(property);
+ const target = createTestElement(t, setup);
+ target.style[idlName] = '2 2';
+ const animation = target.animate({ [idlName]: ['-3 -3', '5 5'] },
+ { duration: 1000, fill: 'both',
+ composite: 'accumulate' });
+
+ testAnimationSamples(animation, idlName,
+ [{ time: 0, expected: '-2' },
+ { time: 1000, expected: '6' }]);
+ }, `${property} with one unspecified value`);
+
+ test(t => {
+ const idlName = propertyToIDL(property);
+ const target = createTestElement(t, setup);
+ target.style[idlName] = '2 2 2';
+ const animation = target.animate({ [idlName]: ['-1 -2 -3', '4 5 6'] },
+ { duration: 1000, fill: 'both',
+ composite: 'accumulate' });
+
+ testAnimationSamples(animation, idlName,
+ [{ time: 0, expected: '0 -1 -2' },
+ { time: 1000, expected: '5 6 7' }]);
+ }, `${property}`);
+ },
+};
+
+const filterListType = {
+ testInterpolation: (property, setup) => {
+ test(t => {
+ const idlName = propertyToIDL(property);
+ const target = createTestElement(t, setup);
+ const animation = target.animate({ [idlName]:
+ ['blur(10px)', 'blur(50px)'] },
+ 1000);
+
+ testAnimationSamples(animation, idlName,
+ [{ time: 500, expected: 'blur(30px)' }]);
+ }, `${property}: blur function`);
+
+ test(t => {
+ const idlName = propertyToIDL(property);
+ const target = createTestElement(t, setup);
+ const animation = target.animate({ [idlName]: ['hue-rotate(0deg)',
+ 'hue-rotate(100deg)'] },
+ 1000);
+
+ testAnimationSamples(animation, idlName,
+ [{ time: 500, expected: 'hue-rotate(50deg)' }]);
+ }, `${property}: hue-rotate function with same unit(deg)`);
+
+ test(t => {
+ const idlName = propertyToIDL(property);
+ const target = createTestElement(t, setup);
+ const animation = target.animate({ [idlName]: ['hue-rotate(10deg)',
+ 'hue-rotate(100rad)'] },
+ 1000);
+
+ // 10deg = 0.1745rad.
+ testAnimationSamples(animation, idlName,
+ [{ time: 500, expected: 'hue-rotate(2869.79deg)' }]);
+ }, `${property}: hue-rotate function with different unit(deg -> rad)`);
+
+ test(t => {
+ const idlName = propertyToIDL(property);
+ const target = createTestElement(t, setup);
+ const animation = target.animate(
+ { [idlName]:
+ ['drop-shadow(10px 10px 10px rgba(255, 0, 0, 0.4))',
+ 'drop-shadow(50px 50px 50px rgba(0, 0, 255, 0.8))'] },
+ 1000);
+
+ testAnimationSamples(
+ animation, idlName,
+ [{ time: 500,
+ expected: 'drop-shadow(rgba(85, 0, 170, 0.6) 30px 30px 30px)' }]);
+ }, `${property}: drop-shadow function`);
+
+ test(t => {
+ const idlName = propertyToIDL(property);
+ const target = createTestElement(t, setup);
+ const animation = target.animate(
+ { [idlName]:
+ ['brightness(0.1) contrast(0.1) grayscale(0.1) invert(0.1) ' +
+ 'opacity(0.1) saturate(0.1) sepia(0.1)',
+ 'brightness(0.5) contrast(0.5) grayscale(0.5) invert(0.5) ' +
+ 'opacity(0.5) saturate(0.5) sepia(0.5)'] },
+ 1000);
+
+ testAnimationSamples(animation, idlName,
+ [{ time: 500,
+ expected: 'brightness(0.3) contrast(0.3) grayscale(0.3) ' +
+ 'invert(0.3) opacity(0.3) saturate(0.3) sepia(0.3)' }]);
+ }, `${property}: percentage or numeric-specifiable functions`
+ + ' (number value)');
+
+ test(t => {
+ const idlName = propertyToIDL(property);
+ const target = createTestElement(t, setup);
+ const animation = target.animate(
+ { [idlName]:
+ ['brightness(10%) contrast(10%) grayscale(10%) invert(10%) ' +
+ 'opacity(10%) saturate(10%) sepia(10%)',
+ 'brightness(50%) contrast(50%) grayscale(50%) invert(50%) ' +
+ 'opacity(50%) saturate(50%) sepia(50%)'] },
+ 1000);
+
+ testAnimationSamples(animation, idlName,
+ [{ time: 500,
+ expected: 'brightness(0.3) contrast(0.3) grayscale(0.3) ' +
+ 'invert(0.3) opacity(0.3) saturate(0.3) sepia(0.3)' }]);
+ }, `${property}: percentage or numeric-specifiable functions`
+ + ' (percentage value)');
+
+ test(t => {
+ const idlName = propertyToIDL(property);
+ const target = createTestElement(t, setup);
+ const animation = target.animate(
+ { [idlName]:
+ // To make missing filter-function-lists, specified the grayscale.
+ ['grayscale(0)',
+ 'grayscale(1) brightness(0) contrast(0) opacity(0) saturate(0)' ]},
+ 1000);
+
+ testAnimationSamples(animation, idlName,
+ [{ time: 500,
+ expected: 'grayscale(0.5) brightness(0.5) contrast(0.5) ' +
+ 'opacity(0.5) saturate(0.5)' }]);
+ }, `${property}: interpolate different length of filter-function-list`
+ + ' with function which lacuna value is 1');
+
+ test(t => {
+ const idlName = propertyToIDL(property);
+ const target = createTestElement(t, setup);
+ const animation = target.animate(
+ { [idlName]:
+ // To make missing filter-function-lists, specified the opacity.
+ ['opacity(1)',
+ 'opacity(0) grayscale(1) invert(1) sepia(1) blur(10px)'] },
+ 1000);
+
+ testAnimationSamples(animation, idlName,
+ [{ time: 500,
+ expected:
+ 'opacity(0.5) grayscale(0.5) invert(0.5) sepia(0.5) blur(5px)' }]);
+ }, `${property}: interpolate different length of filter-function-list`
+ + ' with function which lacuna value is 0');
+
+ test(t => {
+ const idlName = propertyToIDL(property);
+ const target = createTestElement(t, setup);
+ const animation = target.animate(
+ { [idlName]:
+ ['blur(0px)',
+ 'blur(10px) drop-shadow(10px 10px 10px rgba(0, 0, 255, 0.8))'] },
+ 1000);
+
+ testAnimationSamples(animation, idlName,
+ [{ time: 500,
+ // Per the spec: The initial value for interpolation is all length values
+ // set to 0 and the used color set to transparent.
+ expected: 'blur(5px) drop-shadow(rgba(0, 0, 255, 0.4) 5px 5px 5px)' }]);
+ }, `${property}: interpolate different length of filter-function-list`
+ + ' with drop-shadow function');
+
+ test(t => {
+ const idlName = propertyToIDL(property);
+ const target = createTestElement(t, setup);
+ const animation = target.animate({ [idlName]: ['none', 'blur(10px)'] },
+ 1000);
+
+ testAnimationSamples(animation, idlName,
+ [{ time: 500, expected: 'blur(5px)' }]);
+ }, `${property}: interpolate from none`);
+
+ test(t => {
+ const idlName = propertyToIDL(property);
+ const target = createTestElement(t, setup);
+ const animation = target.animate(
+ { [idlName]:
+ ['blur(0px) url(\"#f1\")',
+ 'blur(10px) url(\"#f2\")']},
+ 1000);
+ testAnimationSamples(animation, idlName,
+ [{ time: 499, expected: 'blur(0px) url(\"#f1\")' },
+ { time: 500, expected: 'blur(10px) url(\"#f2\")' }]);
+ }, `${property}: url function (interpoalte as discrete)`);
+ },
+
+ testAddition: function(property, setup) {
+ test(t => {
+ const idlName = propertyToIDL(property);
+ const target = createTestElement(t, setup);
+ target.style[idlName] = 'blur(10px)';
+ const animation = target.animate({ [idlName]: ['blur(20px)',
+ 'blur(50px)'] },
+ { duration: 1000, composite: 'add' });
+ testAnimationSamples(animation, idlName,
+ [ { time: 0, expected: 'blur(10px) blur(20px)' }]);
+ }, `${property}: blur on blur`);
+
+ test(t => {
+ const idlName = propertyToIDL(property);
+ const target = createTestElement(t, setup);
+ target.style[idlName] = 'blur(10px)';
+ const animation = target.animate({ [idlName]: ['brightness(80%)',
+ 'brightness(40%)'] },
+ { duration: 1000, composite: 'add' });
+ testAnimationSamples(animation, idlName,
+ [ { time: 0, expected: 'blur(10px) brightness(0.8)' }]);
+ }, `${property}: different filter functions`);
+ },
+
+ testAccumulation: function(property, setup) {
+ test(t => {
+ const idlName = propertyToIDL(property);
+ const target = createTestElement(t, setup);
+ target.style[idlName] = 'blur(10px) brightness(0.3)';
+ const animation = target.animate(
+ {
+ [idlName]: [
+ 'blur(20px) brightness(0.1)',
+ 'blur(20px) brightness(0.1)',
+ ],
+ },
+ { duration: 1000, composite: 'accumulate' }
+ );
+ // brightness(0.1) onto brightness(0.3) means
+ // brightness((0.1 - 1.0) + (0.3 - 1.0) + 1.0). The result of this formula
+ // is brightness(-0.6) that means brightness(0.0).
+ testAnimationSamples(animation, idlName,
+ [ { time: 0, expected: 'blur(30px) brightness(0)' }]);
+ }, `${property}: same ordered filter functions`);
+
+ test(t => {
+ const idlName = propertyToIDL(property);
+ const target = createTestElement(t, setup);
+ target.style[idlName] = 'blur(10px) brightness(1.3)';
+ const animation = target.animate(
+ {
+ [idlName]: [
+ 'brightness(1.2) blur(20px)',
+ 'brightness(1.2) blur(20px)',
+ ],
+ },
+ { duration: 1000, composite: 'accumulate' }
+ );
+ // Mismatched ordered functions can't be accumulated.
+ testAnimationSamples(animation, idlName,
+ [ { time: 0, expected: 'brightness(1.2) blur(20px)' }]);
+ }, `${property}: mismatched ordered filter functions`);
+ },
+};
+
+const textShadowListType = {
+ testInterpolation: (property, setup) => {
+ test(t => {
+ const idlName = propertyToIDL(property);
+ const target = createTestElement(t, setup);
+ const animation =
+ target.animate({ [idlName]: [ 'none',
+ 'rgb(100, 100, 100) 10px 10px 10px'] },
+ { duration: 1000, fill: 'both' });
+ testAnimationSamples(animation, idlName,
+ // Premultiplied
+ [{ time: 500, expected: 'rgba(100, 100, 100, 0.5) 5px 5px 5px' }]);
+ }, `${property}: from none to other`);
+
+ test(t => {
+ const idlName = propertyToIDL(property);
+ const target = createTestElement(t, setup);
+ const animation =
+ target.animate({ [idlName]: [ 'rgb(100, 100, 100) 10px 10px 10px',
+ 'none' ] },
+ { duration: 1000, fill: 'both' });
+ testAnimationSamples(animation, idlName,
+ // Premultiplied
+ [{ time: 500, expected: 'rgba(100, 100, 100, 0.5) 5px 5px 5px' }]);
+ }, `${property}: from other to none`);
+
+ test(t => {
+ const idlName = propertyToIDL(property);
+ const target = createTestElement(t, setup);
+ const animation =
+ target.animate({ [idlName]: [ 'rgb(0, 0, 0) 0px 0px 0px',
+ 'rgb(100, 100, 100) 10px 10px 10px'] },
+ { duration: 1000, fill: 'both' });
+ testAnimationSamples(animation, idlName,
+ [{ time: 500, expected: 'rgb(50, 50, 50) 5px 5px 5px' }]);
+ }, `${property}: single shadow`);
+
+ test(t => {
+ const idlName = propertyToIDL(property);
+ const target = createTestElement(t, setup);
+ const animation =
+ target.animate({ [idlName]: [ 'rgb(0, 0, 0) 0px 0px 0px, '
+ + 'rgb(200, 200, 200) 20px 20px 20px',
+ 'rgb(100, 100, 100) 10px 10px 10px, '
+ + 'rgb(100, 100, 100) 10px 10px 10px'] },
+ { duration: 1000, fill: 'both' });
+ testAnimationSamples(animation, idlName,
+ [{ time: 500, expected: 'rgb(50, 50, 50) 5px 5px 5px, '
+ + 'rgb(150, 150, 150) 15px 15px 15px' }]);
+ }, `${property}: shadow list`);
+
+ test(t => {
+ const idlName = propertyToIDL(property);
+ const target = createTestElement(t, setup);
+ const animation =
+ target.animate({ [idlName]: [ 'rgb(200, 200, 200) 20px 20px 20px',
+ 'rgb(100, 100, 100) 10px 10px 10px, '
+ + 'rgb(100, 100, 100) 10px 10px 10px'] },
+ { duration: 1000, fill: 'both' });
+ testAnimationSamples(animation, idlName,
+ [{ time: 500, expected: 'rgb(150, 150, 150) 15px 15px 15px, '
+ + 'rgba(100, 100, 100, 0.5) 5px 5px 5px' }]);
+ }, `${property}: mismatched list length (from longer to shorter)`);
+
+ test(t => {
+ const idlName = propertyToIDL(property);
+ const target = createTestElement(t, setup);
+ const animation =
+ target.animate({ [idlName]: [ 'rgb(100, 100, 100) 10px 10px 10px, '
+ + 'rgb(100, 100, 100) 10px 10px 10px',
+ 'rgb(200, 200, 200) 20px 20px 20px'] },
+ { duration: 1000, fill: 'both' });
+ testAnimationSamples(animation, idlName,
+ [{ time: 500, expected: 'rgb(150, 150, 150) 15px 15px 15px, '
+ + 'rgba(100, 100, 100, 0.5) 5px 5px 5px' }]);
+ }, `${property}: mismatched list length (from shorter to longer)`);
+
+ test(t => {
+ const idlName = propertyToIDL(property);
+ const target = createTestElement(t, setup);
+ target.style.color = 'rgb(0, 255, 0)';
+ const animation =
+ target.animate({ [idlName]: [ 'currentcolor 0px 0px 0px',
+ 'currentcolor 10px 10px 10px'] },
+ { duration: 1000, fill: 'both' });
+ testAnimationSamples(animation, idlName,
+ [{ time: 500, expected: 'rgb(0, 255, 0) 5px 5px 5px' }]);
+ }, `${property}: with currentcolor`);
+ },
+
+ testAddition: function(property, setup) {
+ test(t => {
+ const idlName = propertyToIDL(property);
+ const target = createTestElement(t, setup);
+ target.style[idlName] = 'rgb(0, 0, 0) 0px 0px 0px';
+ const animation =
+ target.animate({ [idlName]: [ 'rgb(120, 120, 120) 10px 10px 10px',
+ 'rgb(120, 120, 120) 10px 10px 10px'] },
+ { duration: 1000, composite: 'add' });
+ testAnimationSamples(animation, idlName,
+ [ { time: 0, expected: 'rgb(0, 0, 0) 0px 0px 0px, ' +
+ 'rgb(120, 120, 120) 10px 10px 10px' }]);
+ }, `${property}: shadow`);
+ },
+
+ testAccumulation: function(property, setup) {
+ test(t => {
+ const idlName = propertyToIDL(property);
+ const target = createTestElement(t, setup);
+ target.style[idlName] = 'rgb(120, 120, 120) 10px 10px 10px';
+ const animation =
+ target.animate({ [idlName]: [ 'rgb(120, 120, 120) 10px 10px 10px',
+ 'rgb(120, 120, 120) 10px 10px 10px'] },
+ { duration: 1000, composite: 'accumulate' });
+ testAnimationSamples(animation, idlName,
+ [ { time: 0, expected: 'rgb(240, 240, 240) 20px 20px 20px' }]);
+ }, `${property}: shadow`);
+ },
+};
+
+
+const boxShadowListType = {
+ testInterpolation: (property, setup) => {
+ test(t => {
+ const idlName = propertyToIDL(property);
+ const target = createTestElement(t, setup);
+ const animation =
+ target.animate({ [idlName]: [ 'none',
+ 'rgb(100, 100, 100) 10px 10px 10px 0px'] },
+ { duration: 1000, fill: 'both' });
+ testAnimationSamples(animation, idlName,
+ // Premultiplied
+ [{ time: 500, expected: 'rgba(100, 100, 100, 0.5) 5px 5px 5px 0px' }]);
+ }, `${property}: from none to other`);
+
+ test(t => {
+ const idlName = propertyToIDL(property);
+ const target = createTestElement(t, setup);
+ const animation =
+ target.animate({ [idlName]: [ 'rgb(100, 100, 100) 10px 10px 10px 0px',
+ 'none' ] },
+ { duration: 1000, fill: 'both' });
+ testAnimationSamples(animation, idlName,
+ // Premultiplied
+ [{ time: 500, expected: 'rgba(100, 100, 100, 0.5) 5px 5px 5px 0px' }]);
+ }, `${property}: from other to none`);
+
+ test(t => {
+ const idlName = propertyToIDL(property);
+ const target = createTestElement(t, setup);
+ const animation =
+ target.animate({ [idlName]: [ 'rgb(0, 0, 0) 0px 0px 0px 0px',
+ 'rgb(100, 100, 100) 10px 10px 10px 0px'] },
+ { duration: 1000, fill: 'both' });
+ testAnimationSamples(animation, idlName,
+ [{ time: 500, expected: 'rgb(50, 50, 50) 5px 5px 5px 0px' }]);
+ }, `${property}: single shadow`);
+
+ test(t => {
+ const idlName = propertyToIDL(property);
+ const target = createTestElement(t, setup);
+ const animation =
+ target.animate({ [idlName]: [ 'rgb(0, 0, 0) 0px 0px 0px 0px, '
+ + 'rgb(200, 200, 200) 20px 20px 20px 20px',
+ 'rgb(100, 100, 100) 10px 10px 10px 0px, '
+ + 'rgb(100, 100, 100) 10px 10px 10px 0px'] },
+ { duration: 1000, fill: 'both' });
+ testAnimationSamples(animation, idlName,
+ [{ time: 500, expected: 'rgb(50, 50, 50) 5px 5px 5px 0px, '
+ + 'rgb(150, 150, 150) 15px 15px 15px 10px' }]);
+ }, `${property}: shadow list`);
+
+ test(t => {
+ const idlName = propertyToIDL(property);
+ const target = createTestElement(t, setup);
+ const animation =
+ target.animate({ [idlName]: [ 'rgb(200, 200, 200) 20px 20px 20px 20px',
+ 'rgb(100, 100, 100) 10px 10px 10px 0px, '
+ + 'rgb(100, 100, 100) 10px 10px 10px 0px'] },
+ { duration: 1000, fill: 'both' });
+ testAnimationSamples(animation, idlName,
+ [{ time: 500, expected: 'rgb(150, 150, 150) 15px 15px 15px 10px, '
+ + 'rgba(100, 100, 100, 0.5) 5px 5px 5px 0px' }]);
+ }, `${property}: mismatched list length (from shorter to longer)`);
+
+ test(t => {
+ const idlName = propertyToIDL(property);
+ const target = createTestElement(t, setup);
+ const animation =
+ target.animate({ [idlName]: [ 'rgb(100, 100, 100) 10px 10px 10px 0px, '
+ + 'rgb(100, 100, 100) 10px 10px 10px 0px',
+ 'rgb(200, 200, 200) 20px 20px 20px 20px']},
+ { duration: 1000, fill: 'both' });
+ testAnimationSamples(animation, idlName,
+ [{ time: 500, expected: 'rgb(150, 150, 150) 15px 15px 15px 10px, '
+ + 'rgba(100, 100, 100, 0.5) 5px 5px 5px 0px' }]);
+ }, `${property}: mismatched list length (from longer to shorter)`);
+
+ test(t => {
+ const idlName = propertyToIDL(property);
+ const target = createTestElement(t, setup);
+ target.style.color = 'rgb(0, 255, 0)';
+ const animation =
+ target.animate({ [idlName]: [ 'currentcolor 0px 0px 0px 0px',
+ 'currentcolor 10px 10px 10px 10px'] },
+ { duration: 1000, fill: 'both' });
+ testAnimationSamples(animation, idlName,
+ [{ time: 500, expected: 'rgb(0, 255, 0) 5px 5px 5px 5px' }]);
+ }, `${property}: with currentcolor`);
+ },
+
+ testAddition: function(property, setup) {
+ test(t => {
+ const idlName = propertyToIDL(property);
+ const target = createTestElement(t, setup);
+ target.style[idlName] = 'rgb(0, 0, 0) 0px 0px 0px 0px';
+ const animation =
+ target.animate({ [idlName]: [ 'rgb(120, 120, 120) 10px 10px 10px 0px',
+ 'rgb(120, 120, 120) 10px 10px 10px 0px'] },
+ { duration: 1000, composite: 'add' });
+ testAnimationSamples(animation, idlName,
+ [ { time: 0, expected: 'rgb(0, 0, 0) 0px 0px 0px 0px, ' +
+ 'rgb(120, 120, 120) 10px 10px 10px 0px' }]);
+ }, `${property}: shadow`);
+ },
+
+ testAccumulation: function(property, setup) {
+ test(t => {
+ const idlName = propertyToIDL(property);
+ const target = createTestElement(t, setup);
+ target.style[idlName] = 'rgb(120, 120, 120) 10px 10px 10px 10px';
+ const animation =
+ target.animate({ [idlName]: [ 'rgb(120, 120, 120) 10px 10px 10px 10px',
+ 'rgb(120, 120, 120) 10px 10px 10px 10px'] },
+ { duration: 1000, composite: 'accumulate' });
+ testAnimationSamples(animation, idlName,
+ [ { time: 0, expected: 'rgb(240, 240, 240) 20px 20px 20px 20px' }]);
+ }, `${property}: shadow`);
+ },
+};
+
+const positionType = {
+ testInterpolation: (property, setup) => {
+ lengthPairType.testInterpolation(property, setup);
+
+ test(t => {
+ const idlName = propertyToIDL(property);
+ const target = createTestElement(t, setup);
+ const animation = target.animate({ [idlName]: ['10% 10%', '50% 50%'] },
+ { duration: 1000, fill: 'both' });
+ testAnimationSamples(
+ animation, idlName,
+ [{ time: 500, expected: calcFromPercentage(idlName, '30% 30%') }]);
+ }, `${property} supports animating as a position of percent`);
+ },
+
+ testAdditionOrAccumulation: (property, setup, composite) => {
+ lengthPairType.testAddition(property, setup);
+
+ test(t => {
+ const idlName = propertyToIDL(property);
+ const target = createTestElement(t, setup);
+ target.style[idlName] = '60% 60%';
+ const animation = target.animate({ [idlName]: ['70% 70%', '100% 100%'] },
+ { duration: 1000, composite });
+ testAnimationSamples(
+ animation, idlName,
+ [{ time: 0, expected: calcFromPercentage(idlName, '130% 130%') }]);
+ }, `${property}: position of percentage`);
+ },
+
+ testAddition: function(property, setup) {
+ this.testAdditionOrAccumulation(property, setup, 'add');
+ },
+
+ testAccumulation: function(property, setup) {
+ this.testAdditionOrAccumulation(property, setup, 'accumulate');
+ },
+};
+
+const rectType = {
+ testInterpolation: (property, setup) => {
+ test(t => {
+ const idlName = propertyToIDL(property);
+ const target = createTestElement(t, setup);
+ const animation = target.animate({ [idlName]:
+ ['rect(10px, 10px, 10px, 10px)',
+ 'rect(50px, 50px, 50px, 50px)'] },
+ { duration: 1000, fill: 'both' });
+ testAnimationSamples(
+ animation, idlName,
+ [{ time: 500, expected: 'rect(30px, 30px, 30px, 30px)' }]);
+ }, `${property} supports animating as a rect`);
+ },
+
+ testAdditionOrAccumulation: (property, setup, composite) => {
+ test(t => {
+ const idlName = propertyToIDL(property);
+ const target = createTestElement(t, setup);
+ target.style[idlName] = 'rect(100px, 100px, 100px, 100px)';
+ const animation = target.animate({ [idlName]:
+ ['rect(10px, 10px, 10px, 10px)',
+ 'rect(10px, 10px, 10px, 10px)'] },
+ { duration: 1000, composite });
+ testAnimationSamples(
+ animation, idlName,
+ [{ time: 0, expected: 'rect(110px, 110px, 110px, 110px)' }]);
+ }, `${property}: rect`);
+ },
+
+ testAddition: function(property, setup) {
+ this.testAdditionOrAccumulation(property, setup, 'add');
+ },
+
+ testAccumulation: function(property, setup) {
+ this.testAdditionOrAccumulation(property, setup, 'accumulate');
+ },
+}
+
+// stroke-dasharray: none | [ <length> | <percentage> | <number> ]*
+const dasharrayType = {
+ testInterpolation: (property, setup) => {
+ percentageType.testInterpolation(property, setup);
+ positiveNumberType.testInterpolation(property, setup, 'px');
+
+ test(t => {
+ const idlName = propertyToIDL(property);
+ const target = createTestElement(t, setup);
+ const animation = target.animate({ [idlName]:
+ ['8, 16, 4',
+ '4, 8, 12, 16'] },
+ { duration: 1000, fill: 'both' });
+ testAnimationSamples(
+ animation, idlName,
+ [{ time: 500, expected: '6px, 12px, 8px, 12px, 10px, 6px, 10px, 16px, 4px, 8px, 14px, 10px' }]);
+ }, `${property} supports animating as a dasharray (mismatched length)`);
+
+ test(t => {
+ const idlName = propertyToIDL(property);
+ const target = createTestElement(t, setup);
+ const animation = target.animate({ [idlName]:
+ ['2, 50%, 6, 10',
+ '6, 30%, 2, 2'] },
+ { duration: 1000, fill: 'both' });
+ testAnimationSamples(
+ animation, idlName,
+ [{ time: 500, expected: '4px, 40%, 4px, 6px' }]);
+ }, `${property} supports animating as a dasharray (mixed lengths and percentages)`);
+
+ },
+
+ // Note that stroke-dasharray is neither additive nor cumulative, so we should
+ // write this additive test case that animating value replaces underlying
+ // values.
+ // See https://www.w3.org/TR/SVG2/painting.html#StrokeDashing.
+ testAdditionOrAccumulation: (property, setup, composite) => {
+ test(t => {
+ const idlName = propertyToIDL(property);
+ const target = createTestElement(t, setup);
+ target.style[idlName] = '6, 30%, 2px';
+ const animation = target.animate({ [idlName]:
+ ['1, 2, 3, 4, 5',
+ '6, 7, 8, 9, 10'] },
+ { duration: 1000, composite });
+ testAnimationSamples(
+ animation, idlName,
+ [{ time: 0, expected: '1px, 2px, 3px, 4px, 5px' }]);
+ }, `${property}: dasharray`);
+ },
+
+ testAddition: function(property, setup) {
+ this.testAdditionOrAccumulation(property, setup, 'add');
+ },
+
+ testAccumulation: function(property, setup) {
+ this.testAdditionOrAccumulation(property, setup, 'accumulate');
+ },
+}
+
+const fontVariationSettingsType = {
+ testInterpolation: (property, setup) => {
+ test(t => {
+ const idlName = propertyToIDL(property);
+ const target = createTestElement(t, setup);
+ const animation =
+ target.animate({ [idlName]: ['"wght" 1.1', '"wght" 1.5'] },
+ { duration: 1000, fill: 'both' });
+ testAnimationSamples(animation, idlName,
+ [{ time: 0, expected: '"wght" 1.1' },
+ { time: 250, expected: '"wght" 1.2' },
+ { time: 750, expected: '"wght" 1.4' } ]);
+ }, `${property} supports animation as float`);
+
+ test(t => {
+ const idlName = propertyToIDL(property);
+ const target = createTestElement(t, setup);
+ const animation =
+ target.animate({ [idlName]: ['"wdth" 1, "wght" 1.1',
+ '"wght" 1.5, "wdth" 5'] },
+ { duration: 1000, fill: 'both' });
+ testAnimationSamplesWithAnyOrder(
+ animation, idlName,
+ [{ time: 0, expected: '"wdth" 1, "wght" 1.1' },
+ { time: 250, expected: '"wdth" 2, "wght" 1.2' },
+ { time: 750, expected: '"wdth" 4, "wght" 1.4' } ]);
+ }, `${property} supports animation as float with multiple tags`);
+
+ test(t => {
+ const idlName = propertyToIDL(property);
+ const target = createTestElement(t, setup);
+ const animation =
+ target.animate({ [idlName]: ['"wdth" 1, "wght" 1.1',
+ '"wght" 10, "wdth" 5, "wght" 1.5'] },
+ { duration: 1000, fill: 'both' });
+ testAnimationSamplesWithAnyOrder(
+ animation, idlName,
+ [{ time: 250, expected: '"wdth" 2, "wght" 1.2' },
+ { time: 750, expected: '"wdth" 4, "wght" 1.4' } ]);
+ }, `${property} supports animation as float with multiple duplicate tags`);
+ },
+
+ testAdditionOrAccumulation: (property, setup, composite) => {
+ test(t => {
+ const idlName = propertyToIDL(property);
+ const target = createTestElement(t, setup);
+ target.style[idlName] = '"wght" 1';
+ const animation =
+ target.animate({ [idlName]: ['"wght" 1.1', '"wght" 1.5'] },
+ { duration: 1000, composite });
+ testAnimationSamples(animation, idlName,
+ [{ time: 250, expected: '"wght" 2.2' },
+ { time: 750, expected: '"wght" 2.4' } ]);
+ }, `${property} with composite type ${composite}`);
+ },
+
+ testAddition: function(property, setup) {
+ this.testAdditionOrAccumulation(property, setup, 'add');
+ },
+
+ testAccumulation: function(property, setup) {
+ this.testAdditionOrAccumulation(property, setup, 'accumulate');
+ },
+}
+
+const types = {
+ color: colorType,
+ colorPair: colorPairType,
+ discrete: discreteType,
+ filterList: filterListType,
+ integer: integerType,
+ positiveInteger: positiveIntegerType,
+ length: lengthType,
+ percentage: percentageType,
+ lengthPercentageOrCalc: lengthPercentageOrCalcType,
+ lengthPair: lengthPairType,
+ positiveNumber: positiveNumberType,
+ opacity: opacityType,
+ transformList: transformListType,
+ rotateList: rotateListType,
+ translateList: translateListType,
+ scaleList: scaleListType,
+ visibility: visibilityType,
+ boxShadowList: boxShadowListType,
+ textShadowList: textShadowListType,
+ rect: rectType,
+ position: positionType,
+ dasharray: dasharrayType,
+ fontVariationSettings: fontVariationSettingsType,
+};
diff --git a/testing/web-platform/tests/web-animations/animation-model/animation-types/property-utils.js b/testing/web-platform/tests/web-animations/animation-model/animation-types/property-utils.js
new file mode 100644
index 0000000000..d3a7b12a61
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/animation-model/animation-types/property-utils.js
@@ -0,0 +1,38 @@
+'use strict';
+
+function runAnimationTypeTest(gCSSProperties, testType) {
+ for (const property in gCSSProperties) {
+ if (!isSupported(property)) {
+ continue;
+ }
+
+ const setupFunction = gCSSProperties[property].setup;
+ for (const animationType of gCSSProperties[property].types) {
+ let typeObject;
+ let animationTypeString;
+ if (typeof animationType === 'string') {
+ typeObject = types[animationType];
+ animationTypeString = animationType;
+ } else if (typeof animationType === 'object' &&
+ animationType.type && typeof animationType.type === 'string') {
+ typeObject = types[animationType.type];
+ animationTypeString = animationType.type;
+ }
+
+ // First, test that the animation type object has 'testAccumulation', or
+ // 'testAddition', or 'testInterpolation'.
+ // We use test() function here so that we can continue the remainder tests
+ // even if this test fails.
+ test(t => {
+ assert_own_property(typeObject, testType, animationTypeString +
+ ` should have ${testType} property`);
+ assert_equals(typeof typeObject[testType], 'function',
+ `${testType} method should be a function`);
+ }, `${property} (type: ${animationTypeString}) has ${testType} function`);
+
+ if (typeObject[testType] && typeof typeObject[testType] === 'function') {
+ typeObject[testType](property, setupFunction, animationType.options);
+ }
+ }
+ }
+}
diff --git a/testing/web-platform/tests/web-animations/animation-model/animation-types/scrollbar-interpolation.html b/testing/web-platform/tests/web-animations/animation-model/animation-types/scrollbar-interpolation.html
new file mode 100644
index 0000000000..bc02dd00b9
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/animation-model/animation-types/scrollbar-interpolation.html
@@ -0,0 +1,91 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width, initial-scale=1">
+<link rel="help" href="https://drafts.csswg.org/web-animations/#animation-types">
+<title>Scrollbar interpolation</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../../testcommon.js"></script>
+</head>
+<style type="text/css">
+ #container {
+ height: 100px;
+ width: 100px;
+ overflow: scroll;
+ }
+ #contents {
+ height: 200px;
+ width: 200px;
+ }
+</style>
+<body>
+ <div id="container">
+ <div id="contents"></div>
+ </div>
+</body>
+<!-- Extend coverage in interpolation-per-property-002 to include testing
+ if named and system colors
+-->
+<script type="text/javascript">
+ function interpolate(keyframes, property) {
+ const anim = container.animate(keyframes, {
+ duration: 1000,
+ easing: 'linear'
+ });
+ anim.pause();
+ anim.currentTime = 500;
+ const result = getComputedStyle(container)[property];
+ anim.cancel();
+ return result;
+ }
+
+ function test_scrollbar_interpolation(from, to) {
+ let fromKeyframe =`${from.thumb} ${from.track}`;
+ let toKeyframe = `${to.thumb} ${to.track}`;
+ let keyframes = { scrollbarColor: [ fromKeyframe, toKeyframe ] };
+ const scrollbarColors = interpolate(keyframes, 'scrollbarColor');
+
+ // As the colors may be system dependent, we use the container as a
+ // color swatch and resolve the color blend via background-color.
+ // The scrollbar colors are expected to match the blend on the thumb
+ // and track colors respectively.
+ keyframes = {
+ backgroundColor: [`${from.thumb}`, `${to.thumb}` ]
+ }
+ const expectedThumbColor = interpolate(keyframes, 'backgroundColor');
+ keyframes = {
+ backgroundColor: [ `${from.track}`, `${to.track}` ]
+ }
+ const expectedTrackColor = interpolate(keyframes, 'backgroundColor');
+ assert_equals(scrollbarColors,
+ `${expectedThumbColor} ${expectedTrackColor}`,
+ `${fromKeyframe} to ${toKeyframe}`);
+ }
+
+ test(() => {
+ const data = [
+ {
+ from: { thumb: 'black', track: 'white' },
+ to: { thumb: 'lime', track: 'darkgreen' }
+ },
+ {
+ from: { thumb: '#000', track: 'pink' },
+ to: { thumb: 'canvas', track: 'buttontext' }
+ },
+ {
+ from: { thumb: 'rgba(255,255,255,0)', track: 'rgba(128,128,128,0)'},
+ to: { thumb: 'rgba(255,255,255,1)', track: 'rgba(128,128,128,1)'}
+ },
+ {
+ from: { thumb: 'transparent', track: 'transparent' },
+ to: { thumb: 'rgba(255,255,255,1)', track: 'rgba(128,128,128,1)'}
+ }
+ ];
+ data.forEach((test) => {
+ test_scrollbar_interpolation(test.from, test.to);
+ });
+ }, 'Verify scrollbar-color-interpolation');
+</script>
+</html>
diff --git a/testing/web-platform/tests/web-animations/animation-model/animation-types/visibility.html b/testing/web-platform/tests/web-animations/animation-model/animation-types/visibility.html
new file mode 100644
index 0000000000..f5a60b4e2c
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/animation-model/animation-types/visibility.html
@@ -0,0 +1,57 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title>Animation type for the 'visibility' property</title>
+<!-- FIXME: The following spec link should be updated once this definition has
+ been moved to CSS Values & Units. -->
+<link rel="help" href="https://drafts.csswg.org/css-transitions/#animtype-visibility">
+<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(t => {
+ const div = createDiv(t);
+ const 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');
+
+}, 'Visibility clamping behavior');
+
+test(t => {
+ const div = createDiv(t);
+ const 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');
+
+}, 'Visibility clamping behavior with an easing that has a negative component');
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/web-animations/animation-model/combining-effects/applying-interpolated-transform.html b/testing/web-platform/tests/web-animations/animation-model/combining-effects/applying-interpolated-transform.html
new file mode 100644
index 0000000000..c101b5f0cf
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/animation-model/combining-effects/applying-interpolated-transform.html
@@ -0,0 +1,87 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title> apply interpolated transform on multiple keyframes</title>
+<link rel="help" href="https://drafts.csswg.org/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>
+<div id="target"></div>
+<script>
+'use strict';
+// This test tests the correctness if animation behavior under
+// box-size-denpendent and non-box-size-dependent transformation.
+test(t => {
+ var div_1 = createDiv(t);
+ div_1.style.width = "100px";
+ // Non-pairwise compatible transforms that are effectively no-ops with a
+ // matrix fallback. Both rotate(360deg) and scale(1) are identity matrix,
+ // making it easy to compute.
+ const keyframe1 = [
+ {"transform":"translateX( 200px ) rotate( 360deg )"},
+ {"transform":"translateX( 100% ) scale( 1 )"},
+ ];
+ const keyframe2 = [
+ {"transform":"translateX( 200px ) rotate( 360deg )"},
+ {"transform":"translateX( 100% ) scale( 1 )"},
+ {"transform":"none"},
+ {}
+ ];
+
+ const animation1 = div_1.animate(keyframe1, {
+ "duration":3000,
+ "easing":"linear",
+ });
+ const animation2 = div_1.animate(keyframe2, {
+ "duration":3000,
+ "easing":"linear",
+ });
+ // new animation on the second div, using px value instead of % as a reference
+
+ var div_2 = createDiv(t);
+ div_2.style.width = "100px";
+ const keyframe3 = [
+ {"transform":"translateX( 200px ) rotate( 360deg )"},
+ {"transform":"translateX( 100px ) scale( 1 )"},
+ ];
+ const keyframe4 = [
+ {"transform":"translateX( 200px ) rotate( 360deg )"},
+ {"transform":"translateX( 100px ) scale( 1 )"},
+ {"transform":"none"},
+ {}
+ ];
+
+ const animation3 = div_2.animate(keyframe1, {
+ "duration":3000,
+ "easing":"linear",
+ });
+ const animation4 = div_2.animate(keyframe2, {
+ "duration":3000,
+ "easing":"linear",
+ "composite": 'replace',
+ });
+ animation1.pause();
+ animation2.pause();
+ animation3.pause();
+ animation4.pause();
+ var i;
+ for (i = 0; i <= 30; i++) {
+ animation2.currentTime = 100 * i;
+ animation4.currentTime = 100 * i;
+ var box_size_dependent_transform = getComputedStyle(div_1)['transform'];
+ var reference_transform = getComputedStyle(div_2)['transform']
+ var progress = i / 30;
+ // The second animation replaces the first animation. As the rotate and
+ // scale perations are effectively no-ops when the matrix fallback is
+ // applied. The expected behavior is to go from x-postion 200px to 0 in the
+ // first 2000ms and go back to x-position 200px in the last 1000ms.
+ var expected_transform = 'matrix(1, 0, 0, 1, $1, 0)'
+ .replace('$1', Math.max(200 - 300 * progress, 0)
+ + Math.max(0, -400 + 600 * progress));
+ assert_matrix_equals(box_size_dependent_transform, reference_transform);
+ assert_matrix_equals(reference_transform, expected_transform);
+ }
+})
+</script>
+</body>
diff --git a/testing/web-platform/tests/web-animations/animation-model/combining-effects/applying-the-composited-result.html b/testing/web-platform/tests/web-animations/animation-model/combining-effects/applying-the-composited-result.html
new file mode 100644
index 0000000000..336115e577
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/animation-model/combining-effects/applying-the-composited-result.html
@@ -0,0 +1,29 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>Applying the composited result</title>
+<link rel="help"
+ href="https://drafts.csswg.org/web-animations-1/#applying-the-composited-result">
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src="../../testcommon.js"></script>
+<div id="log"></div>
+<script>
+'use strict';
+
+promise_test(async t => {
+ const div = createDiv(t);
+ div.style.marginLeft = '10px';
+ const animation = div.animate(
+ { marginLeft: ['100px', '200px'] },
+ 100 * MS_PER_SEC
+ );
+ await animation.ready;
+
+ animation.finish();
+
+ const marginLeft = parseFloat(getComputedStyle(div).marginLeft);
+ assert_equals(marginLeft, 10, 'The computed style should be reset');
+}, 'Finishing an animation that does not fill forwards causes its animation'
+ + ' style to be cleared');
+
+</script>
diff --git a/testing/web-platform/tests/web-animations/animation-model/combining-effects/effect-composition.html b/testing/web-platform/tests/web-animations/animation-model/combining-effects/effect-composition.html
new file mode 100644
index 0000000000..70a8cdd92c
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/animation-model/combining-effects/effect-composition.html
@@ -0,0 +1,154 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>Effect composition</title>
+<link rel="help" href="https://drafts.csswg.org/web-animations/#effect-composition">
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src="../../testcommon.js"></script>
+<div id="log"></div>
+<script>
+'use strict';
+
+for (const composite of ['accumulate', 'add']) {
+ test(t => {
+ const div = createDiv(t);
+ div.style.marginLeft = '10px';
+ const anim =
+ div.animate({ marginLeft: ['0px', '10px'], composite }, 100);
+
+ anim.currentTime = 50;
+ assert_equals(getComputedStyle(div).marginLeft, '15px',
+ 'Animated margin-left style at 50%');
+ }, `${composite} onto the base value`);
+
+ test(t => {
+ const div = createDiv(t);
+ div.style.marginLeft = '10px';
+ const anim =
+ div.animate([{ marginLeft: '20px', offset: 0.25 }, { marginLeft: '30px', offset: 0.75 }],
+ { duration: 100, composite });
+
+ anim.currentTime = 50;
+ assert_equals(getComputedStyle(div).marginLeft, '35px',
+ 'Animated margin-left style at 50%');
+ }, `${composite} onto the base value when the interval does not include the 0 or 1 keyframe`);
+
+ test(t => {
+ const div = createDiv(t);
+ const anims = [];
+ anims.push(div.animate({ marginLeft: ['10px', '20px'],
+ composite: 'replace' },
+ 100));
+ anims.push(div.animate({ marginLeft: ['0px', '10px'],
+ composite },
+ 100));
+
+ for (const anim of anims) {
+ anim.currentTime = 50;
+ }
+
+ assert_equals(getComputedStyle(div).marginLeft, '20px',
+ 'Animated style at 50%');
+ }, `${composite} onto an underlying animation value`);
+
+ test(t => {
+ const div = createDiv(t);
+ const anims = [];
+ anims.push(div.animate({ transform: 'translateX(100px)' }, { duration: 100, composite: 'replace' }));
+ anims.push(div.animate({ transform: 'translateY(100px)' }, { duration: 100, composite }));
+
+ for (const anim of anims) {
+ anim.currentTime = 50;
+ }
+
+ assert_equals(getComputedStyle(div).transform, 'matrix(1, 0, 0, 1, 50, 50)',
+ 'Animated style at 50%');
+ }, `${composite} onto an underlying animation value with implicit from values`);
+
+ test(t => {
+ const div = createDiv(t);
+ const anims = [];
+ anims.push(div.animate([{ offset: 1, transform: 'translateX(100px)' }], { duration: 100, composite: 'replace' }));
+ anims.push(div.animate([{ offset: 1, transform: 'translateY(100px)' }], { duration: 100, composite }));
+
+ for (const anim of anims) {
+ anim.currentTime = 50;
+ }
+
+ assert_equals(getComputedStyle(div).transform, 'matrix(1, 0, 0, 1, 50, 50)',
+ 'Animated style at 50%');
+ }, `${composite} onto an underlying animation value with implicit to values`);
+
+ test(t => {
+ const div = createDiv(t);
+ div.style.marginLeft = '10px';
+ const anim =
+ div.animate([{ marginLeft: '10px', composite },
+ { marginLeft: '30px', composite: 'replace' }],
+ 100);
+
+ anim.currentTime = 50;
+ assert_equals(getComputedStyle(div).marginLeft, '25px',
+ 'Animated style at 50%');
+ }, `Composite when mixing ${composite} and replace`);
+
+ test(t => {
+ const div = createDiv(t);
+ div.style.marginLeft = '10px';
+ const anim =
+ div.animate([{ marginLeft: '10px', composite: 'replace' },
+ { marginLeft: '20px' }],
+ { duration: 100 , composite });
+
+ anim.currentTime = 50;
+ assert_equals(getComputedStyle(div).marginLeft, '20px',
+ 'Animated style at 50%');
+ }, `${composite} specified on a keyframe overrides the composite mode of`
+ + ' the effect');
+
+ test(t => {
+ const div = createDiv(t);
+ div.style.marginLeft = '10px';
+ const anim =
+ div.animate([{ marginLeft: '10px', composite: 'replace' },
+ { marginLeft: '20px' }],
+ 100);
+
+ anim.effect.composite = composite;
+ anim.currentTime = 50; // (10 + (10 + 20)) * 0.5
+ assert_equals(getComputedStyle(div).marginLeft, '20px',
+ 'Animated style at 50%');
+ }, 'unspecified composite mode on a keyframe is overriden by setting'
+ + ` ${composite} of the effect`);
+}
+
+test(t => {
+ const div = createDiv(t);
+ const anims = [];
+ anims.push(div.animate({ marginLeft: ['10px', '20px'],
+ composite: 'replace' },
+ 100));
+ anims.push(div.animate({ marginLeft: ['0px', '10px'],
+ composite: 'add' },
+ 100));
+ // This should fully replace the previous effects.
+ anims.push(div.animate({ marginLeft: ['20px', '30px'],
+ composite: 'replace' },
+ 100));
+ anims.push(div.animate({ marginLeft: ['30px', '40px'],
+ composite: 'add' },
+ 100));
+
+ for (const anim of anims) {
+ anim.currentTime = 50;
+ }
+
+ // The result of applying the above effect stack is:
+ // underlying = 0.5 * 20 + 0.5 * 30 = 25
+ // result = 0.5 * (underlying + 30px) + 0.5 * (underlying + 40px)
+ // = 60
+ assert_equals(getComputedStyle(div).marginLeft, '60px',
+ 'Animated style at 50%');
+}, 'Composite replace fully replaces the underlying animation value');
+
+</script>
diff --git a/testing/web-platform/tests/web-animations/animation-model/keyframe-effects/computed-keyframes-shorthands.html b/testing/web-platform/tests/web-animations/animation-model/keyframe-effects/computed-keyframes-shorthands.html
new file mode 100644
index 0000000000..ff62a23bce
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/animation-model/keyframe-effects/computed-keyframes-shorthands.html
@@ -0,0 +1,30 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>Calculating computed keyframes: Shorthand properties</title>
+<link rel="help" href="https://drafts.csswg.org/web-animations-1/#calculating-computed-keyframes">
+<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(t => {
+ const div = createDiv(t);
+ div.style.opacity = '0';
+
+ const animation = div.animate({ all: 'initial' }, 100 * MS_PER_SEC);
+ animation.currentTime = 50 * MS_PER_SEC;
+
+ assert_approx_equals(
+ parseFloat(getComputedStyle(div).opacity),
+ 0.5,
+ 0.0001,
+ 'Should be half way through an opacity animation'
+ );
+}, 'It should be possible to animate the all shorthand');
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/web-animations/animation-model/keyframe-effects/effect-in-removed-iframe-crash.html b/testing/web-platform/tests/web-animations/animation-model/keyframe-effects/effect-in-removed-iframe-crash.html
new file mode 100644
index 0000000000..209ede786f
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/animation-model/keyframe-effects/effect-in-removed-iframe-crash.html
@@ -0,0 +1,22 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1">
+ <title>Querying keyframes for an effect in a removed iframe should not crash
+ </title>
+<link rel="help" href="https://www.w3.org/TR/web-animations-1/#dom-keyframeeffect-getkeyframes">
+</head>
+<body>
+ <div id="target"></div>
+ <iframe id="iframe"></iframe>
+</body>
+<script type="text/javascript">
+ const target = document.getElementById('target');
+ const iframe = document.getElementById('iframe');
+ const keyframes = [{ background: 'green' }, { background: 'blue' }, ];
+ const effect = new iframe.contentWindow.KeyframeEffect(target, keyframes);
+ iframe.parentNode.removeChild(iframe);
+ const result = effect.getKeyframes();
+</script>
+</html>
diff --git a/testing/web-platform/tests/web-animations/animation-model/keyframe-effects/effect-on-marquee-parent-crash.html b/testing/web-platform/tests/web-animations/animation-model/keyframe-effects/effect-on-marquee-parent-crash.html
new file mode 100644
index 0000000000..2948d6b66e
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/animation-model/keyframe-effects/effect-on-marquee-parent-crash.html
@@ -0,0 +1,21 @@
+<!DOCTYPE html>
+<title>Animating marquee's parent inside display:none</title>
+<link rel="help" href="https://drafts.csswg.org/web-animations-1/">
+<link rel="help" href="https://crbug.com/1290016">
+<div id=container></div>
+<script>
+ let outer = document.createElement('div');
+ outer.style.display = 'none';
+ let inner = document.createElement('div');
+ let marquee = document.createElement('marquee');
+
+ let effect = new KeyframeEffect(inner, [{'width': '1px'}], 1);
+ let anim = new Animation(effect, null);
+ anim.pause();
+
+ document.body.append(outer);
+ outer.append(inner);
+ inner.append(marquee);
+
+ // Don't crash.
+</script>
diff --git a/testing/web-platform/tests/web-animations/animation-model/keyframe-effects/effect-value-context-filling.html b/testing/web-platform/tests/web-animations/animation-model/keyframe-effects/effect-value-context-filling.html
new file mode 100644
index 0000000000..fcb7f13126
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/animation-model/keyframe-effects/effect-value-context-filling.html
@@ -0,0 +1,377 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>The effect value of a keyframe effect: Forwards-filling animations whose
+ values depend on their context (target element)</title>
+<link rel="help" href="https://drafts.csswg.org/web-animations/#calculating-computed-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(t => {
+ const div = createDiv(t);
+ div.style.fontSize = '10px';
+ const animation = div.animate(
+ [{ marginLeft: '10em' }, { marginLeft: '20em' }],
+ { duration: 1000, fill: 'forwards' }
+ );
+ animation.finish();
+ assert_equals(
+ getComputedStyle(div).marginLeft,
+ '200px',
+ 'Effect value before updating font-size'
+ );
+
+ div.style.fontSize = '20px';
+
+ assert_equals(
+ getComputedStyle(div).marginLeft,
+ '400px',
+ 'Effect value after updating font-size'
+ );
+}, 'Filling effect values reflect changes to font-size on element');
+
+test(t => {
+ const parentDiv = createDiv(t);
+ const div = createDiv(t);
+ parentDiv.appendChild(div);
+ parentDiv.style.fontSize = '10px';
+
+ const animation = div.animate(
+ [{ marginLeft: '10em' }, { marginLeft: '20em' }],
+ { duration: 1000, fill: 'forwards' }
+ );
+ animation.finish();
+ assert_equals(
+ getComputedStyle(div).marginLeft,
+ '200px',
+ 'Effect value before updating font-size on parent element'
+ );
+
+ parentDiv.style.fontSize = '20px';
+
+ assert_equals(
+ getComputedStyle(div).marginLeft,
+ '400px',
+ 'Effect value after updating font-size on parent element'
+ );
+}, 'Filling effect values reflect changes to font-size on parent element');
+
+test(t => {
+ const div = createDiv(t);
+ div.style.setProperty('--target', '100px');
+ const animation = div.animate(
+ [{ marginLeft: '0px' }, { marginLeft: 'var(--target)' }],
+ { duration: 1000, fill: 'forwards' }
+ );
+ animation.finish();
+ assert_equals(
+ getComputedStyle(div).marginLeft,
+ '100px',
+ 'Effect value before updating variable'
+ );
+
+ div.style.setProperty('--target', '200px');
+
+ assert_equals(
+ getComputedStyle(div).marginLeft,
+ '200px',
+ 'Effect value after updating variable'
+ );
+}, 'Filling effect values reflect changes to variables on element');
+
+test(t => {
+ const parentDiv = createDiv(t);
+ const div = createDiv(t);
+ parentDiv.appendChild(div);
+
+ parentDiv.style.setProperty('--target', '10em');
+ parentDiv.style.fontSize = '10px';
+
+ const animation = div.animate(
+ [{ marginLeft: '0px' }, { marginLeft: 'calc(var(--target) * 2)' }],
+ { duration: 1000, fill: 'forwards' }
+ );
+ animation.finish();
+ assert_equals(
+ getComputedStyle(div).marginLeft,
+ '200px',
+ 'Effect value before updating variable'
+ );
+
+ parentDiv.style.setProperty('--target', '20em');
+
+ assert_equals(
+ getComputedStyle(div).marginLeft,
+ '400px',
+ 'Effect value after updating variable'
+ );
+}, 'Filling effect values reflect changes to variables on parent element');
+
+test(t => {
+ const div = createDiv(t);
+ const animation = div.animate(
+ [{ marginLeft: '100px' }, { marginLeft: '200px' }],
+ { duration: 1000, fill: 'forwards' }
+ );
+ animation.finish();
+ assert_equals(
+ getComputedStyle(div).marginLeft,
+ '200px',
+ 'Effect value before updating the animation'
+ );
+
+ animation.effect.setKeyframes([
+ { marginLeft: '100px' },
+ { marginLeft: '300px' },
+ ]);
+
+ assert_equals(
+ getComputedStyle(div).marginLeft,
+ '300px',
+ 'Effect value after updating the animation'
+ );
+}, 'Filling effect values reflect changes to the the animation\'s keyframes');
+
+test(t => {
+ const div = createDiv(t);
+ div.style.marginLeft = '100px';
+ const animation = div.animate(
+ [{ marginLeft: '100px' }, { marginLeft: '200px' }],
+ { duration: 1000, fill: 'forwards' }
+ );
+ animation.finish();
+ assert_equals(
+ getComputedStyle(div).marginLeft,
+ '200px',
+ 'Effect value before updating the animation'
+ );
+
+ animation.effect.composite = 'add';
+
+ assert_equals(
+ getComputedStyle(div).marginLeft,
+ '300px',
+ 'Effect value after updating the composite mode'
+ );
+}, 'Filling effect values reflect changes to the the animation\'s composite mode');
+
+test(t => {
+ const div = createDiv(t);
+ const animation = div.animate(
+ [{ marginLeft: '0px' }, { marginLeft: '100px' }],
+ { duration: 1000, iterations: 2, fill: 'forwards' }
+ );
+ animation.finish();
+ assert_equals(
+ getComputedStyle(div).marginLeft,
+ '100px',
+ 'Effect value before updating the animation'
+ );
+
+ animation.effect.iterationComposite = 'accumulate';
+
+ assert_equals(
+ getComputedStyle(div).marginLeft,
+ '200px',
+ 'Effect value after updating the iteration composite mode'
+ );
+}, 'Filling effect values reflect changes to the the animation\'s iteration composite mode');
+
+test(t => {
+ const div = createDiv(t);
+ div.style.marginLeft = '100px';
+ const animation = div.animate(
+ [{ marginLeft: '100px' }, { marginLeft: '200px' }],
+ { duration: 1000, fill: 'forwards', composite: 'add' }
+ );
+ animation.finish();
+ assert_equals(
+ getComputedStyle(div).marginLeft,
+ '300px',
+ 'Effect value before updating underlying value'
+ );
+
+ div.style.marginLeft = '200px';
+
+ assert_equals(
+ getComputedStyle(div).marginLeft,
+ '400px',
+ 'Effect value after updating underlying value'
+ );
+}, 'Filling effect values reflect changes to the base value when using'
+ + ' additive animation');
+
+test(t => {
+ const div = createDiv(t);
+ div.style.marginLeft = '100px';
+ const animation = div.animate(
+ [{ marginLeft: '100px' }, { marginLeft: '200px', composite: 'add' }],
+ { duration: 1000, fill: 'forwards' }
+ );
+ animation.finish();
+ assert_equals(
+ getComputedStyle(div).marginLeft,
+ '300px',
+ 'Effect value before updating underlying value'
+ );
+
+ div.style.marginLeft = '200px';
+
+ assert_equals(
+ getComputedStyle(div).marginLeft,
+ '400px',
+ 'Effect value after updating underlying value'
+ );
+}, 'Filling effect values reflect changes to the base value when using'
+ + ' additive animation on a single keyframe');
+
+test(t => {
+ const div = createDiv(t);
+ div.style.marginLeft = '0px';
+ const animation = div.animate([{ marginLeft: '100px', offset: 0 }], {
+ duration: 1000,
+ fill: 'forwards',
+ });
+ animation.finish();
+ assert_equals(
+ getComputedStyle(div).marginLeft,
+ '0px',
+ 'Effect value before updating underlying value'
+ );
+
+ div.style.marginLeft = '200px';
+
+ assert_equals(
+ getComputedStyle(div).marginLeft,
+ '200px',
+ 'Effect value after updating underlying value'
+ );
+}, 'Filling effect values reflect changes to the base value when using'
+ + ' the fill value is an implicit keyframe');
+
+test(t => {
+ const parentDiv = createDiv(t);
+ const div = createDiv(t);
+ parentDiv.appendChild(div);
+ parentDiv.style.fontSize = '10px';
+ div.style.marginLeft = '10em';
+ // Computed underlying margin-left is 100px
+
+ const animation = div.animate(
+ [{ marginLeft: '100px' }, { marginLeft: '200px' }],
+ { duration: 1000, fill: 'forwards', composite: 'add' }
+ );
+ animation.finish();
+ assert_equals(
+ getComputedStyle(div).marginLeft,
+ '300px',
+ 'Effect value before updating font-size on parent'
+ );
+
+ parentDiv.style.fontSize = '20px';
+ // Computed underlying margin-left is now 200px
+
+ assert_equals(
+ getComputedStyle(div).marginLeft,
+ '400px',
+ 'Effect value after updating font-size on parent'
+ );
+}, 'Filling effect values reflect changes to the base value via a'
+ + ' parent element');
+
+test(t => {
+ const div = createDiv(t);
+ const animationA = div.animate(
+ [{ marginLeft: '0px' }, { marginLeft: '100px' }],
+ { duration: 2000, fill: 'forwards', easing: 'step-end' }
+ );
+ const animationB = div.animate(
+ [{ marginLeft: '100px' }, { marginLeft: '200px' }],
+ { duration: 1000, fill: 'forwards', composite: 'add' }
+ );
+ animationB.finish();
+ assert_equals(
+ getComputedStyle(div).marginLeft,
+ '200px',
+ 'Effect value before updating underyling animation'
+ );
+
+ // Go to end of the underlying animation so that it jumps to 100px
+ animationA.finish();
+
+ assert_equals(
+ getComputedStyle(div).marginLeft,
+ '300px',
+ 'Effect value after updating underlying animation'
+ );
+}, 'Filling effect values reflect changes to underlying animations');
+
+test(t => {
+ const parentDiv = createDiv(t);
+ const div = createDiv(t);
+ parentDiv.appendChild(div);
+
+ parentDiv.style.fontSize = '10px';
+
+ const animationA = div.animate(
+ [{ marginLeft: '0px' }, { marginLeft: '10em' }],
+ { duration: 2000, fill: 'forwards', easing: 'step-start' }
+ );
+ const animationB = div.animate(
+ [{ marginLeft: '100px' }, { marginLeft: '200px' }],
+ { duration: 1000, fill: 'forwards', composite: 'add' }
+ );
+ animationB.finish();
+ assert_equals(
+ getComputedStyle(div).marginLeft,
+ '300px',
+ 'Effect value before updating parent font-size'
+ );
+
+ parentDiv.style.fontSize = '20px';
+ // At this point the underlying animation's output should jump to 200px.
+
+ assert_equals(
+ getComputedStyle(div).marginLeft,
+ '400px',
+ 'Effect value after updating parent font-size'
+ );
+}, 'Filling effect values reflect changes to underlying animations via a'
+ + ' a parent element');
+
+test(t => {
+ const div = createDiv(t);
+ const animationA = div.animate(
+ [{ marginLeft: '0px' }, { marginLeft: '0px' }],
+ { duration: 2000, fill: 'forwards' }
+ );
+ const animationB = div.animate(
+ [{ marginLeft: '100px' }, { marginLeft: '200px' }],
+ { duration: 1000, fill: 'forwards', composite: 'add' }
+ );
+ animationB.finish();
+ assert_equals(
+ getComputedStyle(div).marginLeft,
+ '200px',
+ 'Effect value before updating underyling animation'
+ );
+
+ animationA.effect.setKeyframes([
+ { marginLeft: '100px' },
+ { marginLeft: '100px' },
+ ]);
+
+ assert_equals(
+ getComputedStyle(div).marginLeft,
+ '300px',
+ 'Effect value after updating underlying animation'
+ );
+}, 'Filling effect values reflect changes to underlying animations made by'
+ + ' directly changing the keyframes');
+
+</script>
+</body>
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 0000000000..3730a02098
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/animation-model/keyframe-effects/effect-value-context.html
@@ -0,0 +1,105 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title>The effect value of a keyframe effect: Property values that depend on
+ their context (target element)</title>
+<link rel="help" href="https://drafts.csswg.org/web-animations/#calculating-computed-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(t => {
+ const div = createDiv(t);
+ div.style.fontSize = '10px';
+ const 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(t => {
+ const parentDiv = createDiv(t);
+ const div = createDiv(t);
+ parentDiv.appendChild(div);
+ parentDiv.style.fontSize = '10px';
+
+ const 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(t => {
+ const parentDiv = createDiv(t);
+ const div = createDiv(t);
+ parentDiv.appendChild(div);
+ parentDiv.style.fontSize = '10px';
+ const animation = div.animate([ { marginLeft: '10em' },
+ { marginLeft: '20em' } ], 1000);
+
+ animation.pause();
+ animation.currentTime = 500;
+ parentDiv.style.fontSize = '20px';
+
+ return animation.ready.then(() => {
+ 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(t => {
+ const divWith10pxFontSize = createDiv(t);
+ divWith10pxFontSize.style.fontSize = '10px';
+ const divWith20pxFontSize = createDiv(t);
+ divWith20pxFontSize.style.fontSize = '20px';
+
+ const div = createDiv(t);
+ div.remove(); // Detach
+ const animation = div.animate([ { marginLeft: '10em' },
+ { marginLeft: '20em' } ], 1000);
+ animation.pause();
+
+ return animation.ready.then(() => {
+ 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(t => {
+ const divA = createDiv(t);
+ divA.style.fontSize = '10px';
+
+ const divB = createDiv(t);
+ divB.style.fontSize = '20px';
+
+ const 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/effect-value-interval-distance.html b/testing/web-platform/tests/web-animations/animation-model/keyframe-effects/effect-value-interval-distance.html
new file mode 100644
index 0000000000..6bf5d8cd46
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/animation-model/keyframe-effects/effect-value-interval-distance.html
@@ -0,0 +1,36 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>The effect value of a keyframe effect: Calculating the interval
+ distance between keyframes</title>
+<link rel="help" href="https://drafts.csswg.org/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>
+<script>
+'use strict';
+
+test(t => {
+ // In Firefox there was a floating precision bug in the calculation of the
+ // progress at the end of the 0.2<->1.0 interval. This test exercises that
+ // calculation in case other UAs suffer from the same problem.
+ const target = createDiv(t);
+ const anim = target.animate(
+ [
+ { opacity: 0 },
+ { offset: 0.2, opacity: 1, easing: 'step-end' },
+ { opacity: 0 },
+ ],
+ {
+ duration: 1000,
+ fill: 'forwards',
+ }
+ );
+
+ anim.currentTime = 1000;
+ assert_equals(getComputedStyle(target).opacity, '0');
+}, 'Interval distance is calculated correctly (precision test)');
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/web-animations/animation-model/keyframe-effects/effect-value-iteration-composite-operation.html b/testing/web-platform/tests/web-animations/animation-model/keyframe-effects/effect-value-iteration-composite-operation.html
new file mode 100644
index 0000000000..abfda86f4d
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/animation-model/keyframe-effects/effect-value-iteration-composite-operation.html
@@ -0,0 +1,824 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>The effect value of a keyframe effect: Applying the iteration composite
+ operation</title>
+<link rel="help" href="https://drafts.csswg.org/web-animations/#the-effect-value-of-a-keyframe-animation-effect">
+<link rel="help" href="https://drafts.csswg.org/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(t => {
+ const div = createDiv(t);
+ const anim =
+ div.animate({ alignContent: ['flex-start', 'flex-end'] },
+ { duration: 100 * MS_PER_SEC,
+ easing: 'linear',
+ iterations: 10,
+ iterationComposite: 'accumulate' });
+
+ anim.currentTime = anim.effect.getComputedTiming().duration / 2;
+ assert_equals(getComputedStyle(div).alignContent, 'flex-end',
+ 'Animated align-content style at 50s of the first iteration');
+ anim.currentTime = anim.effect.getComputedTiming().duration * 2;
+ assert_equals(getComputedStyle(div).alignContent, 'flex-start',
+ 'Animated align-content style at 0s of the third iteration');
+ anim.currentTime += anim.effect.getComputedTiming().duration / 2;
+ assert_equals(getComputedStyle(div).alignContent, 'flex-end',
+ 'Animated align-content style at 50s of the third iteration');
+}, 'iteration composition of discrete type animation (align-content)');
+
+test(t => {
+ const div = createDiv(t);
+ const anim =
+ div.animate({ marginLeft: ['0px', '10px'] },
+ { duration: 100 * MS_PER_SEC,
+ easing: 'linear',
+ iterations: 10,
+ iterationComposite: 'accumulate' });
+ anim.pause();
+
+ anim.currentTime = anim.effect.getComputedTiming().duration / 2;
+ assert_equals(getComputedStyle(div).marginLeft, '5px',
+ 'Animated margin-left style at 50s of the first iteration');
+ anim.currentTime = anim.effect.getComputedTiming().duration * 2;
+ assert_equals(getComputedStyle(div).marginLeft, '20px',
+ 'Animated margin-left style at 0s of the third iteration');
+ anim.currentTime += anim.effect.getComputedTiming().duration / 2;
+ assert_equals(getComputedStyle(div).marginLeft, '25px',
+ 'Animated margin-left style at 50s of the third iteration');
+}, 'iteration composition of <length> type animation');
+
+test(t => {
+ const parent = createDiv(t);
+ parent.style.width = '100px';
+ const div = createDiv(t);
+ parent.appendChild(div);
+
+ const anim =
+ div.animate({ width: ['0%', '50%'] },
+ { duration: 100 * MS_PER_SEC,
+ easing: 'linear',
+ iterations: 10,
+ iterationComposite: 'accumulate' });
+ anim.pause();
+
+ anim.currentTime = anim.effect.getComputedTiming().duration / 2;
+ assert_equals(getComputedStyle(div).width, '25px',
+ 'Animated width style at 50s of the first iteration');
+ anim.currentTime = anim.effect.getComputedTiming().duration * 2;
+ assert_equals(getComputedStyle(div).width, '100px',
+ 'Animated width style at 0s of the third iteration');
+ anim.currentTime += anim.effect.getComputedTiming().duration / 2;
+ assert_equals(getComputedStyle(div).width, '125px',
+ 'Animated width style at 50s of the third iteration');
+}, 'iteration composition of <percentage> type animation');
+
+test(t => {
+ const div = createDiv(t);
+ const 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.getComputedTiming().duration / 2;
+ assert_equals(getComputedStyle(div).color, 'rgb(60, 60, 60)',
+ 'Animated color style at 50s of the first iteration');
+ anim.currentTime = anim.effect.getComputedTiming().duration * 2;
+ assert_equals(getComputedStyle(div).color, 'rgb(240, 240, 240)',
+ 'Animated color style at 0s of the third iteration');
+ anim.currentTime += anim.effect.getComputedTiming().duration / 2;
+ assert_equals(getComputedStyle(div).color, 'rgb(255, 255, 255)',
+ 'Animated color style at 50s of the third iteration');
+}, 'iteration composition of <color> type animation');
+
+test(t => {
+ const div = createDiv(t);
+ const 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.getComputedTiming().duration / 2;
+ assert_equals(getComputedStyle(div).color, 'rgb(30, 90, 30)',
+ 'Animated color style at 50s of the first iteration');
+ anim.currentTime = anim.effect.getComputedTiming().duration * 2;
+ assert_equals(getComputedStyle(div).color, 'rgb(120, 240, 120)',
+ 'Animated color style at 0s of the third iteration');
+ anim.currentTime += anim.effect.getComputedTiming().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');
+}, 'iteration composition of <color> type animation that green component is ' +
+ 'decreasing');
+
+ test(t => {
+ const div = createDiv(t);
+ const anim =
+ div.animate({ flexGrow: [0, 10] },
+ { duration: 100 * MS_PER_SEC,
+ easing: 'linear',
+ iterations: 10,
+ iterationComposite: 'accumulate' });
+ anim.pause();
+
+ anim.currentTime = anim.effect.getComputedTiming().duration / 2;
+ assert_equals(getComputedStyle(div).flexGrow, '5',
+ 'Animated flex-grow style at 50s of the first iteration');
+ anim.currentTime = anim.effect.getComputedTiming().duration * 2;
+ assert_equals(getComputedStyle(div).flexGrow, '20',
+ 'Animated flex-grow style at 0s of the third iteration');
+ anim.currentTime += anim.effect.getComputedTiming().duration / 2;
+ assert_equals(getComputedStyle(div).flexGrow, '25',
+ 'Animated flex-grow style at 50s of the third iteration');
+}, 'iteration composition of <number> type animation');
+
+test(t => {
+ const div = createDiv(t);
+ div.style.position = 'absolute';
+ const 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.getComputedTiming().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.getComputedTiming().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.getComputedTiming().duration / 2;
+ assert_equals(getComputedStyle(div).clip, 'rect(25px, 25px, 25px, 25px)',
+ 'Animated clip style at 50s of the third iteration');
+}, 'iteration composition of <shape> type animation');
+
+test(t => {
+ const div = createDiv(t);
+ const 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.getComputedTiming().duration / 2;
+ assert_equals(getComputedStyle(div).width, '5px',
+ 'Animated calc width style at 50s of the first iteration');
+ anim.currentTime = anim.effect.getComputedTiming().duration * 2;
+ assert_equals(getComputedStyle(div).width, '20px',
+ 'Animated calc width style at 0s of the third iteration');
+ anim.currentTime += anim.effect.getComputedTiming().duration / 2;
+ assert_equals(getComputedStyle(div).width, '25px',
+ 'Animated calc width style at 50s of the third iteration');
+}, 'iteration composition of <calc()> value animation');
+
+test(t => {
+ const parent = createDiv(t);
+ parent.style.width = '100px';
+ const div = createDiv(t);
+ parent.appendChild(div);
+
+ const 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.getComputedTiming().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.getComputedTiming().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.getComputedTiming().duration / 2;
+ assert_equals(getComputedStyle(div).width,
+ '50px', // (40px + 60px) / 2
+ 'Animated calc width style at 50s of the third iteration');
+}, 'iteration composition of <calc()> value animation that the values can\'t' +
+ 'be reduced');
+
+test(t => {
+ const div = createDiv(t);
+ const anim =
+ div.animate({ opacity: [0, 0.4] },
+ { duration: 100 * MS_PER_SEC,
+ easing: 'linear',
+ iterations: 10,
+ iterationComposite: 'accumulate' });
+ anim.pause();
+
+ anim.currentTime = anim.effect.getComputedTiming().duration / 2;
+ assert_equals(getComputedStyle(div).opacity, '0.2',
+ 'Animated opacity style at 50s of the first iteration');
+ anim.currentTime = anim.effect.getComputedTiming().duration * 2;
+ assert_equals(getComputedStyle(div).opacity, '0.8',
+ 'Animated opacity style at 0s of the third iteration');
+ anim.currentTime += anim.effect.getComputedTiming().duration / 2;
+ assert_equals(getComputedStyle(div).opacity, '1', // (0.8 + 1.2) * 0.5
+ 'Animated opacity style at 50s of the third iteration');
+}, 'iteration composition of opacity animation');
+
+test(t => {
+ const div = createDiv(t);
+ const 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.getComputedTiming().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.getComputedTiming().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.getComputedTiming().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');
+}, 'iteration composition of box-shadow animation');
+
+test(t => {
+ const div = createDiv(t);
+ const 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.getComputedTiming().duration / 2;
+ assert_equals(getComputedStyle(div).filter, 'blur(5px)',
+ 'Animated filter blur style at 50s of the first iteration');
+ anim.currentTime = anim.effect.getComputedTiming().duration * 2;
+ assert_equals(getComputedStyle(div).filter, 'blur(20px)',
+ 'Animated filter blur style at 0s of the third iteration');
+ anim.currentTime += anim.effect.getComputedTiming().duration / 2;
+ assert_equals(getComputedStyle(div).filter, 'blur(25px)',
+ 'Animated filter blur style at 50s of the third iteration');
+}, 'iteration composition of filter blur animation');
+
+test(t => {
+ const div = createDiv(t);
+ const 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.getComputedTiming().duration / 2;
+ assert_equals(getComputedStyle(div).filter,
+ 'brightness(1.4)',
+ 'Animated filter brightness style at 50s of the first iteration');
+ anim.currentTime = anim.effect.getComputedTiming().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.getComputedTiming().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');
+}, 'iteration composition of filter brightness for different unit animation');
+
+test(t => {
+ const div = createDiv(t);
+ const 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.getComputedTiming().duration / 2;
+ assert_equals(getComputedStyle(div).filter,
+ 'brightness(0.5)',
+ 'Animated filter brightness style at 50s of the first iteration');
+ anim.currentTime = anim.effect.getComputedTiming().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.getComputedTiming().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');
+}, 'iteration composition of filter brightness animation');
+
+test(t => {
+ const div = createDiv(t);
+ const 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.getComputedTiming().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.getComputedTiming().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.getComputedTiming().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');
+}, 'iteration composition of filter drop-shadow animation');
+
+test(t => {
+ const div = createDiv(t);
+ const 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.getComputedTiming().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.getComputedTiming().duration * 2;
+ assert_equals(getComputedStyle(div).filter,
+ 'brightness(3) contrast(3)',
+ 'Animated filter list at 0s of the third iteration');
+ anim.currentTime += anim.effect.getComputedTiming().duration / 2;
+ assert_equals(getComputedStyle(div).filter,
+ 'brightness(3.5) contrast(3.5)',
+ 'Animated filter list at 50s of the third iteration');
+}, 'iteration composition of same filter list animation');
+
+test(t => {
+ const div = createDiv(t);
+ const 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.getComputedTiming().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.getComputedTiming().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.getComputedTiming().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');
+}, 'iteration composition of discrete filter list because of mismatch ' +
+ 'of the order');
+
+ test(t => {
+ const div = createDiv(t);
+ const 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.getComputedTiming().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.getComputedTiming().duration * 2;
+ assert_equals(getComputedStyle(div).filter,
+ 'sepia(1) contrast(3)',
+ 'Animated filter list at 0s of the third iteration');
+ anim.currentTime += anim.effect.getComputedTiming().duration / 2;
+ assert_equals(getComputedStyle(div).filter,
+ 'sepia(1) contrast(3.5)',
+ 'Animated filter list at 50s of the third iteration');
+}, 'iteration composition of different length filter list animation');
+
+test(t => {
+ const div = createDiv(t);
+ const 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.getComputedTiming().duration / 2;
+ assert_matrix_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.getComputedTiming().duration * 2;
+ assert_matrix_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.getComputedTiming().duration / 2;
+ assert_matrix_equals(getComputedStyle(div).transform,
+ 'matrix(0, 1, -1, 0, 0, 0)', // rotate(450deg)
+ 'Animated transform(rotate) style at 50s of the third iteration');
+}, 'iteration composition of transform(rotate) animation');
+
+test(t => {
+ const div = createDiv(t);
+ const 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.getComputedTiming().duration / 2;
+ assert_matrix_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.getComputedTiming().duration * 2;
+ assert_matrix_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.getComputedTiming().duration / 2;
+ assert_matrix_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');
+}, 'iteration composition of transform: [ scale(0), scale(1) ] animation');
+
+test(t => {
+ const div = createDiv(t);
+ const 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.getComputedTiming().duration / 2;
+ assert_matrix_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.getComputedTiming().duration * 2;
+ assert_matrix_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.getComputedTiming().duration / 2;
+ assert_matrix_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');
+}, 'iteration composition of transform: [ scale(1), scale(2) ] animation');
+
+test(t => {
+ const div = createDiv(t);
+ const 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.getComputedTiming().duration / 2;
+ assert_matrix_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.getComputedTiming().duration * 2;
+ assert_matrix_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.getComputedTiming().duration / 2;
+ assert_matrix_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');
+}, 'iteration composition of transform: scale(2) animation');
+
+test(t => {
+ const div = createDiv(t);
+ const 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.getComputedTiming().duration / 2;
+ assert_matrix_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.getComputedTiming().duration * 2;
+ assert_matrix_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.getComputedTiming().duration / 2;
+ assert_matrix_equals(getComputedStyle(div).transform,
+ 'matrix(0, 1, -1, 0, 0, 25)', // rotate(450deg) translateX(25px)
+ 'Animated transform list at 50s of the third iteration');
+}, 'iteration composition of transform list animation');
+
+test(t => {
+ const div = createDiv(t);
+ const anim =
+ div.animate({ transform: ['matrix(2, 0, 0, 2, 0, 0)',
+ 'matrix(3, 0, 0, 3, 30, 0)'] },
+ { duration: 100 * MS_PER_SEC,
+ easing: 'linear',
+ iterations: 10,
+ iterationComposite: 'accumulate' });
+ anim.pause();
+
+ anim.currentTime = anim.effect.getComputedTiming().duration / 2;
+ assert_matrix_equals(getComputedStyle(div).transform,
+ 'matrix(2.5, 0, 0, 2.5, 15, 0)',
+ 'Animated transform of matrix function at 50s of the first iteration');
+ anim.currentTime = anim.effect.getComputedTiming().duration * 2;
+ assert_matrix_equals(getComputedStyle(div).transform,
+ // scale(2) + (scale(3-1)*2) + translateX(30px)*2
+ 'matrix(6, 0, 0, 6, 60, 0)',
+ 'Animated transform of matrix function at 0s of the third iteration');
+ anim.currentTime += anim.effect.getComputedTiming().duration / 2;
+ assert_matrix_equals(getComputedStyle(div).transform,
+ // from: matrix(6, 0, 0, 6, 60, 0)
+ // to: matrix(7, 0, 0, 7, 90, 0)
+ // = scale(3) + (scale(3-1)*2) + translateX(30px)*3
+ 'matrix(6.5, 0, 0, 6.5, 75, 0)',
+ 'Animated transform of matrix function at 50s of the third iteration');
+}, 'iteration composition of transform of matrix function');
+
+test(t => {
+ const div = createDiv(t);
+ const 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.getComputedTiming().duration / 2;
+ assert_matrix_equals(getComputedStyle(div).transform,
+ // Interpolate between matrix(2, 0, 0, 2, 0, 0) = translateX(0px) scale(2)
+ // and matrix(3, 0, 0, 3, 30, 0) = scale(3) translateX(10px)
+ 'matrix(2.5, 0, 0, 2.5, 15, 0)',
+ 'Animated transform list at 50s of the first iteration');
+ anim.currentTime = anim.effect.getComputedTiming().duration * 2;
+ assert_matrix_equals(getComputedStyle(div).transform,
+ // 'from' and 'to' value are mismatched, so accumulate
+ // matrix(2, 0, 0, 2, 0, 0) onto matrix(3, 0, 0, 3, 30, 0) * 2
+ // = scale(2) + (scale(3-1)*2) + translateX(30px)*2
+ 'matrix(6, 0, 0, 6, 60, 0)',
+ 'Animated transform list at 0s of the third iteration');
+ anim.currentTime += anim.effect.getComputedTiming().duration / 2;
+ assert_matrix_equals(getComputedStyle(div).transform,
+ // Interpolate between matrix(6, 0, 0, 6, 60, 0)
+ // and matrix(7, 0, 0, 7, 210, 0) = scale(7) translate(30px)
+ 'matrix(6.5, 0, 0, 6.5, 135, 0)',
+ 'Animated transform list at 50s of the third iteration');
+}, 'iteration composition of transform list animation whose order is'
+ + ' mismatched');
+
+test(t => {
+ const 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.
+ const 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.getComputedTiming().duration / 2;
+ assert_matrix_equals(getComputedStyle(div).transform,
+ // Interpolate between matrix(1, 0, 0, 1, 0, 0) = translateX(0px)
+ // and matrix(2, 0, 0, 2, 20, 0) = scale(2) translateX(10px)
+ 'matrix(1.5, 0, 0, 1.5, 10, 0)',
+ 'Animated transform list at 50s of the first iteration');
+ anim.currentTime = anim.effect.getComputedTiming().duration * 2;
+ assert_matrix_equals(getComputedStyle(div).transform,
+ // 'from' and 'to' value are mismatched, so accumulate
+ // matrix(1, 0, 0, 1, 0, 0) onto matrix(2, 0, 0, 2, 20, 0) * 2
+ // = scale(1) + (scale(2-1)*2) + translateX(20px)*2
+ 'matrix(3, 0, 0, 3, 40, 0)',
+ 'Animated transform list at 0s of the third iteration');
+ anim.currentTime += anim.effect.getComputedTiming().duration / 2;
+ assert_matrix_equals(getComputedStyle(div).transform,
+ // Interpolate between matrix(3, 0, 0, 3, 40, 0)
+ // and matrix(4, 0, 0, 4, 120, 0) =
+ // scale(2 + (2-1)*2) translate(10px * 3)
+ 'matrix(3.5, 0, 0, 3.5, 80, 0)',
+ 'Animated transform list at 50s of the third iteration');
+}, 'iteration composition of transform list animation whose order is'
+ + ' mismatched because of missing functions');
+
+ test(t => {
+ const div = createDiv(t);
+ const anim =
+ div.animate({ transform: ['none',
+ 'translateX(10px)'] },
+ { duration: 100 * MS_PER_SEC,
+ easing: 'linear',
+ iterations: 10,
+ iterationComposite: 'accumulate' });
+ anim.pause();
+
+ anim.currentTime = anim.effect.getComputedTiming().duration / 2;
+ assert_matrix_equals(getComputedStyle(div).transform,
+ // translateX(none) -> translateX(10px) @ 50%
+ 'matrix(1, 0, 0, 1, 5, 0)',
+ 'Animated transform list at 50s of the first iteration');
+ anim.currentTime = anim.effect.getComputedTiming().duration * 2;
+ assert_matrix_equals(getComputedStyle(div).transform,
+ // translateX(10px * 2 + none) -> translateX(10px * 2 + 10px) @ 0%
+ 'matrix(1, 0, 0, 1, 20, 0)',
+ 'Animated transform list at 0s of the third iteration');
+ anim.currentTime += anim.effect.getComputedTiming().duration / 2;
+ assert_matrix_equals(getComputedStyle(div).transform,
+ // translateX(10px * 2 + none) -> translateX(10px * 2 + 10px) @ 50%
+ 'matrix(1, 0, 0, 1, 25, 0)',
+ 'Animated transform list at 50s of the third iteration');
+}, 'iteration composition of transform from none to translate');
+
+test(t => {
+ const div = createDiv(t);
+ const anim =
+ div.animate({ transform: ['matrix3d(1, 0, 0, 0, ' +
+ '0, 1, 0, 0, ' +
+ '0, 0, 1, 0, ' +
+ '0, 0, 30, 1)',
+ 'matrix3d(1, 0, 0, 0, ' +
+ '0, 1, 0, 0, ' +
+ '0, 0, 1, 0, ' +
+ '0, 0, 50, 1)'] },
+ { duration: 100 * MS_PER_SEC,
+ easing: 'linear',
+ iterations: 10,
+ iterationComposite: 'accumulate' });
+ anim.pause();
+
+ anim.currentTime = anim.effect.getComputedTiming().duration / 2;
+ assert_matrix_equals(getComputedStyle(div).transform,
+ 'matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 40, 1)',
+ 'Animated transform of matrix3d function at 50s of the first iteration');
+ anim.currentTime = anim.effect.getComputedTiming().duration * 2;
+ assert_matrix_equals(getComputedStyle(div).transform,
+ // translateZ(30px) + (translateZ(50px)*2)
+ 'matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 130, 1)',
+ 'Animated transform of matrix3d function at 0s of the third iteration');
+ anim.currentTime += anim.effect.getComputedTiming().duration / 2;
+ assert_matrix_equals(getComputedStyle(div).transform,
+ // from: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 130, 1)
+ // to: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 150, 1)
+ 'matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 140, 1)',
+ 'Animated transform of matrix3d function at 50s of the third iteration');
+}, 'iteration composition of transform of matrix3d function');
+
+test(t => {
+ const div = createDiv(t);
+ const anim =
+ div.animate({ transform: ['rotate3d(1, 1, 0, 0deg)',
+ 'rotate3d(1, 1, 0, 90deg)'] },
+ { duration: 100 * MS_PER_SEC,
+ easing: 'linear',
+ iterations: 10,
+ iterationComposite: 'accumulate' });
+ anim.pause();
+
+ anim.currentTime = 0;
+ assert_matrix_equals(getComputedStyle(div).transform,
+ 'matrix(1, 0, 0, 1, 0, 0)', // Actually not rotated at all.
+ 'Animated transform of rotate3d function at 50s of the first iteration');
+ anim.currentTime = anim.effect.getComputedTiming().duration * 2;
+ assert_matrix_equals(getComputedStyle(div).transform,
+ rotate3dToMatrix3d(1, 1, 0, Math.PI), // 180deg
+ 'Animated transform of rotate3d function at 0s of the third iteration');
+ anim.currentTime += anim.effect.getComputedTiming().duration / 2;
+ assert_matrix_equals(getComputedStyle(div).transform,
+ rotate3dToMatrix3d(1, 1, 0, 225 * Math.PI / 180), //((270 + 180) * 0.5)deg
+ 'Animated transform of rotate3d function at 50s of the third iteration');
+}, 'iteration composition of transform of rotate3d function');
+
+test(t => {
+ const div = createDiv(t);
+ const anim =
+ div.animate({ marginLeft: ['10px', '20px'] },
+ { duration: 100 * MS_PER_SEC,
+ easing: 'linear',
+ iterations: 10,
+ iterationComposite: 'accumulate' });
+ anim.pause();
+
+ anim.currentTime = anim.effect.getComputedTiming().duration / 2;
+ assert_equals(getComputedStyle(div).marginLeft, '15px',
+ 'Animated margin-left style at 50s of the first iteration');
+ anim.currentTime = anim.effect.getComputedTiming().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.getComputedTiming().duration / 2;
+ assert_equals(getComputedStyle(div).marginLeft, '55px', // (50px + 60px) * 0.5
+ 'Animated margin-left style at 50s of the third iteration');
+}, 'iteration composition starts with non-zero value animation');
+
+test(t => {
+ const div = createDiv(t);
+ const anim =
+ div.animate({ marginLeft: ['10px', '-10px'] },
+ { duration: 100 * MS_PER_SEC,
+ easing: 'linear',
+ iterations: 10,
+ iterationComposite: 'accumulate' });
+ anim.pause();
+
+ anim.currentTime = anim.effect.getComputedTiming().duration / 2;
+ assert_equals(getComputedStyle(div).marginLeft,
+ '0px',
+ 'Animated margin-left style at 50s of the first iteration');
+ anim.currentTime = anim.effect.getComputedTiming().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.getComputedTiming().duration / 2;
+ assert_equals(getComputedStyle(div).marginLeft,
+ '-20px', // (-10px + -30px) * 0.5
+ 'Animated margin-left style at 50s of the third iteration');
+}, 'iteration composition with negative final value animation');
+
+test(t => {
+ const div = createDiv(t);
+ const anim = div.animate({ marginLeft: ['0px', '10px'] },
+ { duration: 100 * MS_PER_SEC,
+ easing: 'linear',
+ iterations: 10,
+ iterationComposite: 'accumulate' });
+ anim.pause();
+
+ anim.currentTime =
+ anim.effect.getComputedTiming().duration * 2 +
+ anim.effect.getComputedTiming().duration / 2;
+ assert_equals(getComputedStyle(div).marginLeft, '25px',
+ 'Animated style at 50s of the third iteration');
+
+ // double its duration.
+ anim.effect.updateTiming({
+ duration: anim.effect.getComputedTiming().duration * 2
+ });
+ assert_equals(getComputedStyle(div).marginLeft, '12.5px',
+ 'Animated style at 25s of the first iteration');
+
+ // half of original.
+ anim.effect.updateTiming({
+ duration: anim.effect.getComputedTiming().duration / 4
+ });
+ assert_equals(getComputedStyle(div).marginLeft, '50px',
+ 'Animated style at 50s of the fourth iteration');
+}, 'duration changes with an iteration composition operation of accumulate');
+
+</script>
diff --git a/testing/web-platform/tests/web-animations/animation-model/keyframe-effects/effect-value-opacity-replaced-effect-in-shadow-root-ref.html b/testing/web-platform/tests/web-animations/animation-model/keyframe-effects/effect-value-opacity-replaced-effect-in-shadow-root-ref.html
new file mode 100644
index 0000000000..792ca214cf
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/animation-model/keyframe-effects/effect-value-opacity-replaced-effect-in-shadow-root-ref.html
@@ -0,0 +1,9 @@
+<!doctype html>
+<style>
+div {
+ width: 100px;
+ height: 100px;
+ background: green;
+}
+</style>
+<div></div>
diff --git a/testing/web-platform/tests/web-animations/animation-model/keyframe-effects/effect-value-opacity-replaced-effect-in-shadow-root.html b/testing/web-platform/tests/web-animations/animation-model/keyframe-effects/effect-value-opacity-replaced-effect-in-shadow-root.html
new file mode 100644
index 0000000000..43e229856b
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/animation-model/keyframe-effects/effect-value-opacity-replaced-effect-in-shadow-root.html
@@ -0,0 +1,26 @@
+<!doctype html>
+<html class="reftest-wait">
+<head>
+<meta charset=utf-8>
+<title>The effect value of consecutive animations targeting 'opacity' in a shadow root</title>
+<link rel="help" href="https://drafts.csswg.org/web-animations/">
+<link rel="match" href="effect-value-opacity-replaced-effect-in-shadow-root-ref.html">
+</head>
+<body>
+<custom-element></custom-element>
+<script>
+'use strict';
+
+(async function () {
+ const customElement = document.querySelector('custom-element');
+ const shadowRoot = customElement.attachShadow({ mode: 'open' });
+ shadowRoot.innerHTML = `<div style="width: 100px; height: 100px; background-color: green"></div>`;
+ const target = shadowRoot.firstElementChild;
+
+ await target.animate({ opacity: [1, 0] }, { duration: 10, fill: 'both' }).finished;
+ await target.animate({ opacity: [0, 1] }, { duration: 10, fill: 'both' }).finished;
+ document.documentElement.classList.remove("reftest-wait");
+})();
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/web-animations/animation-model/keyframe-effects/effect-value-opacity-replaced-effect-ref.html b/testing/web-platform/tests/web-animations/animation-model/keyframe-effects/effect-value-opacity-replaced-effect-ref.html
new file mode 100644
index 0000000000..792ca214cf
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/animation-model/keyframe-effects/effect-value-opacity-replaced-effect-ref.html
@@ -0,0 +1,9 @@
+<!doctype html>
+<style>
+div {
+ width: 100px;
+ height: 100px;
+ background: green;
+}
+</style>
+<div></div>
diff --git a/testing/web-platform/tests/web-animations/animation-model/keyframe-effects/effect-value-opacity-replaced-effect.html b/testing/web-platform/tests/web-animations/animation-model/keyframe-effects/effect-value-opacity-replaced-effect.html
new file mode 100644
index 0000000000..bef3bd3e8d
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/animation-model/keyframe-effects/effect-value-opacity-replaced-effect.html
@@ -0,0 +1,32 @@
+<!doctype html>
+<html class="reftest-wait">
+<head>
+<meta charset=utf-8>
+<title>The effect value of consecutive animations targeting 'opacity'</title>
+<link rel="help" href="https://drafts.csswg.org/web-animations/">
+<link rel="match" href="effect-value-opacity-replaced-effect-ref.html">
+<style>
+div {
+ width: 100px;
+ height: 100px;
+ background: green;
+ will-change: opacity;
+}
+</style>
+</head>
+<body>
+<div></div>
+<script>
+'use strict';
+
+(async function () {
+ const div = document.querySelector('div');
+
+ await div.animate({ opacity: [1, 0] }, { duration: 10, fill: 'both' }).finished;
+ await div.animate({ opacity: [0, 1] }, { duration: 10, fill: 'both' }).finished;
+
+ document.documentElement.classList.remove("reftest-wait");
+})();
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/web-animations/animation-model/keyframe-effects/effect-value-overlapping-keyframes.html b/testing/web-platform/tests/web-animations/animation-model/keyframe-effects/effect-value-overlapping-keyframes.html
new file mode 100644
index 0000000000..a2a0683921
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/animation-model/keyframe-effects/effect-value-overlapping-keyframes.html
@@ -0,0 +1,77 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title>The effect value of a keyframe effect: Overlapping keyframes</title>
+<link rel="help" href="https://drafts.csswg.org/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';
+
+function assert_opacity_value(opacity, expected, description) {
+ return assert_approx_equals(parseFloat(opacity), expected, 0.0001, description);
+}
+
+test(t => {
+ const div = createDiv(t);
+ const 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_opacity_value(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(t => {
+ const div = createDiv(t);
+ const 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_opacity_value(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_opacity_value(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_opacity_value(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');
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/web-animations/animation-model/keyframe-effects/effect-value-replaced-animations.html b/testing/web-platform/tests/web-animations/animation-model/keyframe-effects/effect-value-replaced-animations.html
new file mode 100644
index 0000000000..d40a01fdd2
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/animation-model/keyframe-effects/effect-value-replaced-animations.html
@@ -0,0 +1,161 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>The effect value of a keyframe effect: Overlapping keyframes</title>
+<link rel="help" href="https://drafts.csswg.org/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>
+<script>
+'use strict';
+
+function assert_opacity_value(opacity, expected, description) {
+ return assert_approx_equals(
+ parseFloat(opacity),
+ expected,
+ 0.0001,
+ description
+ );
+}
+
+promise_test(async t => {
+ const div = createDiv(t);
+ div.style.opacity = '0.1';
+
+ const animA = div.animate(
+ { opacity: 0.2 },
+ { duration: 1, fill: 'forwards' }
+ );
+ const animB = div.animate(
+ { opacity: 0.3 },
+ { duration: 1, fill: 'forwards' }
+ );
+ await animA.finished;
+
+ // Sanity check
+ assert_equals(animA.replaceState, 'removed');
+ assert_equals(animB.replaceState, 'active');
+
+ // animA is now removed so if we cancel animB, we should go back to the
+ // underlying value
+ animB.cancel();
+ assert_opacity_value(
+ getComputedStyle(div).opacity,
+ 0.1,
+ 'Opacity should be the un-animated value'
+ );
+}, 'Removed animations do not contribute to animated style');
+
+promise_test(async t => {
+ const div = createDiv(t);
+ div.style.opacity = '0.1';
+
+ const animA = div.animate(
+ { opacity: 0.2 },
+ { duration: 1, fill: 'forwards' }
+ );
+ const animB = div.animate(
+ { opacity: 0.3, composite: 'add' },
+ { duration: 1, fill: 'forwards' }
+ );
+ await animA.finished;
+
+ // Sanity check
+ assert_equals(animA.replaceState, 'removed');
+ assert_equals(animB.replaceState, 'active');
+
+ // animA has been removed so the final result should be 0.1 + 0.3 = 0.4.
+ // (If animA were not removed it would be 0.2 + 0.3 = 0.5.)
+ assert_opacity_value(
+ getComputedStyle(div).opacity,
+ 0.4,
+ 'Opacity value should not include the removed animation'
+ );
+}, 'Removed animations do not contribute to the effect stack');
+
+promise_test(async t => {
+ const div = createDiv(t);
+ div.style.opacity = '0.1';
+
+ const animA = div.animate(
+ { opacity: 0.2 },
+ { duration: 1, fill: 'forwards' }
+ );
+ const animB = div.animate(
+ { opacity: 0.3 },
+ { duration: 1, fill: 'forwards' }
+ );
+
+ await animA.finished;
+
+ animA.persist();
+
+ animB.cancel();
+ assert_opacity_value(
+ getComputedStyle(div).opacity,
+ 0.2,
+ "Opacity should be the persisted animation's value"
+ );
+}, 'Persisted animations contribute to animated style');
+
+promise_test(async t => {
+ const div = createDiv(t);
+ div.style.opacity = '0.1';
+
+ const animA = div.animate(
+ { opacity: 0.2 },
+ { duration: 1, fill: 'forwards' }
+ );
+ const animB = div.animate(
+ { opacity: 0.3, composite: 'add' },
+ { duration: 1, fill: 'forwards' }
+ );
+
+ await animA.finished;
+
+ assert_opacity_value(
+ getComputedStyle(div).opacity,
+ 0.4,
+ 'Opacity value should NOT include the contribution of the removed animation'
+ );
+
+ animA.persist();
+
+ assert_opacity_value(
+ getComputedStyle(div).opacity,
+ 0.5,
+ 'Opacity value should include the contribution of the persisted animation'
+ );
+}, 'Persisted animations contribute to the effect stack');
+
+promise_test(async t => {
+ const div = createDiv(t);
+ div.style.opacity = '0.1';
+
+ const animA = div.animate(
+ { opacity: 0.2 },
+ { duration: 1, fill: 'forwards' }
+ );
+
+ // Persist the animation before it finishes (and before it would otherwise be
+ // removed).
+ animA.persist();
+
+ const animB = div.animate(
+ { opacity: 0.3, composite: 'add' },
+ { duration: 1, fill: 'forwards' }
+ );
+
+ await animA.finished;
+
+ assert_opacity_value(
+ getComputedStyle(div).opacity,
+ 0.5,
+ 'Opacity value should include the contribution of the persisted animation'
+ );
+}, 'Animations persisted before they would be removed contribute to the'
+ + ' effect stack');
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/web-animations/animation-model/keyframe-effects/effect-value-transformed-distance.html b/testing/web-platform/tests/web-animations/animation-model/keyframe-effects/effect-value-transformed-distance.html
new file mode 100644
index 0000000000..a33d6d4f24
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/animation-model/keyframe-effects/effect-value-transformed-distance.html
@@ -0,0 +1,84 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title>The effect value of a keyframe effect: Calculating the transformed
+ distance between keyframes</title>
+<link rel="help" href="https://drafts.csswg.org/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>
+<script src="../../resources/easing-tests.js"></script>
+<body>
+<div id="log"></div>
+<script>
+'use strict';
+
+// Test that applying easing to keyframes is applied as expected
+
+for (const params of gEasingTests) {
+ test(t => {
+ const target = createDiv(t);
+ const anim = target.animate([ { width: '0px' },
+ // We put the easing on the second keyframe
+ // so we can test that it is only applied
+ // to the specified keyframe.
+ { width: '100px', easing: params.easing },
+ { width: '200px' } ],
+ { duration: 2000,
+ fill: 'forwards' });
+
+ for (const sampleTime of [0, 999, 1000, 1100, 1500, 2000]) {
+ anim.currentTime = sampleTime;
+
+ const portion = (sampleTime - 1000) / 1000;
+ const expectedWidth = sampleTime < 1000
+ ? sampleTime / 10 // first segment is linear
+ : 100 + params.easingFunction(portion) * 100;
+ assert_approx_equals(parseFloat(getComputedStyle(target).width),
+ expectedWidth,
+ 0.02,
+ 'The width should be approximately ' +
+ `${expectedWidth} at ${sampleTime}ms`);
+ }
+ }, `A ${params.desc} on a keyframe affects the resulting style`);
+}
+
+// Test that a linear-equivalent cubic-bezier easing applied to a keyframe does
+// not alter (including clamping) the result.
+
+for (const params of gEasingTests) {
+ const linearEquivalentEasings = [ 'cubic-bezier(0, 0, 0, 0)',
+ 'cubic-bezier(1, 1, 1, 1)' ];
+ test(t => {
+ for (const linearEquivalentEasing of linearEquivalentEasings) {
+ const timing = { duration: 1000,
+ fill: 'forwards',
+ easing: params.easing };
+
+ const linearTarget = createDiv(t);
+ const linearAnim = linearTarget.animate([ { width: '0px' },
+ { width: '100px' } ],
+ timing);
+
+ const equivalentTarget = createDiv(t);
+ const equivalentAnim =
+ equivalentTarget.animate([ { width: '0px',
+ easing: linearEquivalentEasing },
+ { width: '100px' } ],
+ timing);
+
+ for (const sampleTime of [0, 250, 500, 750, 1000]) {
+ linearAnim.currentTime = sampleTime;
+ equivalentAnim.currentTime = sampleTime;
+
+ assert_equals(getComputedStyle(linearTarget).width,
+ getComputedStyle(equivalentTarget).width,
+ `The 'width' of the animated elements should be equal ` +
+ `at ${sampleTime}ms`);
+ }
+ }
+ }, 'Linear-equivalent cubic-bezier keyframe easing applied to an effect ' +
+ `with a ${params.desc} does not alter the result`);
+}
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/web-animations/animation-model/keyframe-effects/keyframe-exceptions.html b/testing/web-platform/tests/web-animations/animation-model/keyframe-effects/keyframe-exceptions.html
new file mode 100644
index 0000000000..4cb4be76a8
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/animation-model/keyframe-effects/keyframe-exceptions.html
@@ -0,0 +1,39 @@
+<!DOCTYPE html>
+<head>
+<meta charset="utf-8">
+<link rel="help" src="https://www.w3.org/TR/web-animations-1/#processing-a-keyframes-argument">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/web-animations/testcommon.js"></script>
+<script src="support/testcommon.js"></script>
+<title>Keyframes with invalid offsets</title>
+</head>
+<script>
+ test(() => {
+ assert_throws_js(TypeError,
+ function() {
+ document.documentElement.animate([
+ {offset: 0.6},
+ {offset: 0.4}
+ ]);
+ });
+ }, 'Offsets must be loosely sorted');
+
+ test(() => {
+ assert_throws_js(TypeError,
+ function() {
+ document.documentElement.animate([
+ {offset: 'whatever'}
+ ]);
+ });
+ }, 'Invalid offset');
+
+ test(() => {
+ assert_throws_js(TypeError,
+ function() {
+ document.documentElement.animate([
+ {offset: -1}
+ ]);
+ });
+ }, 'Offsets must be null or in the range [0,1]');
+</script>
diff --git a/testing/web-platform/tests/web-animations/animation-model/keyframe-effects/transform-and-opacity-on-inline-001-ref.html b/testing/web-platform/tests/web-animations/animation-model/keyframe-effects/transform-and-opacity-on-inline-001-ref.html
new file mode 100644
index 0000000000..1e7f250c48
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/animation-model/keyframe-effects/transform-and-opacity-on-inline-001-ref.html
@@ -0,0 +1,14 @@
+<!DOCTYPE html>
+<html>
+<title>CSS Test (Animations): Element.animate() animating both transform and opacity on an inline</title>
+<link rel="author" title="L. David Baron" href="https://dbaron.org/">
+<link rel="author" title="Google" href="http://www.google.com/">
+
+<style>
+#target {
+ opacity: 0.4;
+ will-change: opacity;
+}
+</style>
+
+<body><span id="target">x</span></body>
diff --git a/testing/web-platform/tests/web-animations/animation-model/keyframe-effects/transform-and-opacity-on-inline-001.html b/testing/web-platform/tests/web-animations/animation-model/keyframe-effects/transform-and-opacity-on-inline-001.html
new file mode 100644
index 0000000000..f76b53cd4c
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/animation-model/keyframe-effects/transform-and-opacity-on-inline-001.html
@@ -0,0 +1,27 @@
+<!DOCTYPE html>
+<html>
+<title>CSS Test (Animations): Element.animate() animating both transform and opacity on an inline</title>
+<link rel="author" title="L. David Baron" href="https://dbaron.org/">
+<link rel="author" title="Google" href="http://www.google.com/">
+<link rel="help" href="https://bugs.chromium.org/p/chromium/issues/detail?id=1316688">
+<link rel="match" href="transform-and-opacity-on-inline-001-ref.html">
+<meta name="assert" content="This should not crash, and should render as opacity 0.5.">
+
+<script>
+// The transform animation should be ignored; the opacity animation should work.
+window.onload = function() {
+ document.getElementById("target").animate(
+ [
+ {
+ "transform": "translateX(0px)",
+ "opacity": "0.8",
+ },
+ {
+ "transform": "translateX(300px)",
+ "opacity": "0.0",
+ }
+ ],
+ { duration:1000000, delay: -500000, easing: "steps(3, jump-both)" });
+}
+</script>
+<body><span id="target">x</span></body>
diff --git a/testing/web-platform/tests/web-animations/animation-model/resources/side-effects-common.js b/testing/web-platform/tests/web-animations/animation-model/resources/side-effects-common.js
new file mode 100644
index 0000000000..2f6531d4fc
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/animation-model/resources/side-effects-common.js
@@ -0,0 +1,81 @@
+'use strict';
+
+const PROPERTY_OPACITY = 0;
+const PROPERTY_TRANSFORM = 1;
+const PROPERTY_BGCOLOR = 2;
+
+const PHASE_BEFORE = 10;
+const PHASE_ACTIVE = 11;
+const PHASE_AFTER = 12;
+
+const STATE_CURRENT = 100;
+const STATE_IN_EFFECT = 101;
+const STATE_NONE = 102;
+
+// Creates an animation in the given state/page used to test side-effects. See:
+// https://drafts.csswg.org/web-animations-1/#animation-effect-phases-and-states
+//
+// testcase - A query string for the test case root. Must have a descendant
+// with the 'target' class that will be animated.
+// state - One of the STATE_ constants above. Configures the animation to be
+// either "current", "in effect" or neither.
+// property - One of the PROPERTY_ constants above, the property the animation
+// will target.
+// phase - One of the PHASE_ constants above. Configures the animation to be in
+// the before/active/after phase.
+function setupAnimation(testcase, state, property, phase) {
+ const root = document.querySelector(testcase);
+ const effect_target = root.querySelector('.target');
+
+ let keyframe;
+ if (property == PROPERTY_OPACITY)
+ keyframe = { opacity: 1};
+ else if (property == PROPERTY_TRANSFORM)
+ keyframe = { transform: 'translateX(0px)' };
+ else if (property == PROPERTY_BGCOLOR)
+ keyframe = { backgroundColor: 'red' };
+ else
+ throw new Error('Unexpected property');
+
+ const kPhaseDuration = 1000000;
+ const kBeforePhaseTime = kPhaseDuration / 2;
+ const kActivePhaseTime = kPhaseDuration + kPhaseDuration / 2;
+ const kAfterPhaseTime = 2 * kPhaseDuration + kPhaseDuration / 2;
+
+ const options = {
+ duration: kPhaseDuration,
+ delay: kPhaseDuration,
+ endDelay: kPhaseDuration,
+
+ easing: 'steps(1, jump-both)',
+
+ fill: (state == STATE_IN_EFFECT ? 'both' : 'none'),
+ };
+
+ const animation = effect_target.animate(
+ [ keyframe, keyframe ], options);
+
+ switch(phase) {
+ case PHASE_BEFORE:
+ animation.currentTime = kBeforePhaseTime;
+ if (state == STATE_IN_EFFECT || state == STATE_NONE)
+ animation.playbackRate = -1;
+ break;
+
+ case PHASE_ACTIVE:
+ if (state == STATE_NONE)
+ throw new Error("Cannot have state[NONE] in the active phase");
+
+ animation.currentTime = kActivePhaseTime;
+ break;
+
+ case PHASE_AFTER:
+ animation.currentTime = kAfterPhaseTime;
+ if (state == STATE_CURRENT)
+ animation.playbackRate = -1;
+ break;
+
+ default:
+ throw new Error('Unexpected phase');
+ }
+}
diff --git a/testing/web-platform/tests/web-animations/animation-model/side-effects-of-animations-current-ref.html b/testing/web-platform/tests/web-animations/animation-model/side-effects-of-animations-current-ref.html
new file mode 100644
index 0000000000..acc0465b07
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/animation-model/side-effects-of-animations-current-ref.html
@@ -0,0 +1,109 @@
+<!DOCTYPE html>
+<title>
+ Current animations should effectively apply will-change (i.e. force a
+ stacking context) (ref)
+</title>
+<link rel="help" href="https://drafts.csswg.org/web-animations-1/#side-effects-section">
+<link rel="author" href="mailto:bokan@chromium.org">
+<style>
+ .testcase {
+ position: relative;
+ margin: 2px;
+ border: 1px solid black;
+ width: 250px;
+ height: 25px;
+ }
+ .label {
+ flex-grow: 1;
+ }
+ .container {
+ background-color: lightgrey;
+ margin: 4px;
+ display: flex;
+ width: 600px;
+ flex-direction: row;
+ justify-content: flex-end;
+ align-items: center;
+ }
+ .pass {
+ width: 100%;
+ height: 100%;
+ background-color: limegreen;
+ }
+</style>
+
+<!-- OPACITY -->
+
+<!-- Is current - before phase playing forwards -->
+<div class="container">
+ <div class="label">Opacity - before phase</div>
+ <div class="testcase" id="opacity-before">
+ <div class="pass"></div>
+ </div>
+</div>
+
+<!-- Is current - in play-->
+<div class="container">
+ <div class="label">Opacity - active phase</div>
+ <div class="testcase" id="opacity-active">
+ <div class="pass"></div>
+ </div>
+</div>
+
+<!-- Is current - after phase playing backwards-->
+<div class="container">
+ <div class="label">Opacity - after phase</div>
+ <div class="testcase" id="opacity-after">
+ <div class="pass"></div>
+ </div>
+</div>
+
+<!-- TRANSFORM -->
+
+<!-- Is current - before phase playing forwards -->
+<div class="container">
+ <div class="label">Transform - before phase</div>
+ <div class="testcase" id="transform-before">
+ <div class="pass"></div>
+ </div>
+</div>
+
+<!-- Is current - in play-->
+<div class="container">
+ <div class="label">Transform - active phase</div>
+ <div class="testcase" id="transform-active">
+ <div class="pass"></div>
+ </div>
+</div>
+
+<!-- Is current - after phase playing backwards-->
+<div class="container">
+ <div class="label">Transform - after phase</div>
+ <div class="testcase" id="transform-after">
+ <div class="pass"></div>
+ </div>
+</div>
+
+<!-- BACKGROUND COLOR -->
+<div class="container">
+ <div class="label">Background-color - before phase</div>
+ <div class="testcase" id="bgcolor-before">
+ <div class="pass"></div>
+ </div>
+</div>
+
+<!-- Is current - in play-->
+<div class="container">
+ <div class="label">Background-color - active phase</div>
+ <div class="testcase" id="bgcolor-active">
+ <div class="pass"></div>
+ </div>
+</div>
+
+<!-- Is current - after phase playing backwards-->
+<div class="container">
+ <div class="label">Background-color - after phase</div>
+ <div class="testcase" id="bgcolor-after">
+ <div class="pass"></div>
+ </div>
+</div>
diff --git a/testing/web-platform/tests/web-animations/animation-model/side-effects-of-animations-current.html b/testing/web-platform/tests/web-animations/animation-model/side-effects-of-animations-current.html
new file mode 100644
index 0000000000..18b13be9fa
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/animation-model/side-effects-of-animations-current.html
@@ -0,0 +1,174 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+<title>
+ Current animations should effectively apply will-change (i.e. force a
+ stacking context)
+</title>
+<link rel="help" href="https://drafts.csswg.org/web-animations-1/#side-effects-section">
+<link rel="author" href="mailto:bokan@chromium.org">
+<link rel="match" href="side-effects-of-animations-current-ref.html">
+<script src="/common/reftest-wait.js"></script>
+<script src="../testcommon.js"></script>
+<script src="resources/side-effects-common.js"></script>
+<style>
+ .testcase {
+ position: relative;
+ margin: 2px;
+ border: 1px solid black;
+ width: 250px;
+ height: 25px;
+ }
+ .container {
+ background-color: lightgrey;
+ margin: 4px;
+ display: flex;
+ width: 600px;
+ flex-direction: row;
+ justify-content: flex-end;
+ align-items: center;
+ }
+ .label {
+ flex-grow: 1;
+ }
+ .target {
+ width: 100%;
+ height: 100%;
+ }
+ .absolutebox {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ }
+ .pass {
+ background-color: limegreen;
+ }
+ .fail {
+ background-color: red;
+ }
+</style>
+
+<script>
+onload = takeScreenshotOnAnimationsReady;
+</script>
+
+<!-- OPACITY -->
+
+<!-- Is current - before phase playing forwards -->
+<div class="container">
+ <div class="label">Opacity - before phase</div>
+ <div class="testcase" id="opacity-before">
+ <div class="absolutebox fail"></div>
+ <div class="target pass"></div>
+ </div>
+</div>
+<script>
+ setupAnimation('#opacity-before', STATE_CURRENT, PROPERTY_OPACITY, PHASE_BEFORE);
+</script>
+
+<!-- Is current - in play-->
+<div class="container">
+ <div class="label">Opacity - active phase</div>
+ <div class="testcase" id="opacity-active">
+ <div class="absolutebox fail"></div>
+ <div class="target pass"></div>
+ </div>
+</div>
+<script>
+ setupAnimation('#opacity-active', STATE_CURRENT, PROPERTY_OPACITY, PHASE_ACTIVE);
+</script>
+
+<!-- Is current - after phase playing backwards-->
+<div class="container">
+ <div class="label">Opacity - after phase</div>
+ <div class="testcase" id="opacity-after">
+ <div class="absolutebox fail"></div>
+ <div class="target pass"></div>
+ </div>
+</div>
+<script>
+ setupAnimation('#opacity-after', STATE_CURRENT, PROPERTY_OPACITY, PHASE_AFTER);
+</script>
+
+<!-- TRANSFORM -->
+
+<!-- Is current - before phase playing forwards -->
+<div class="container">
+ <div class="label">Transform - before phase</div>
+ <div class="testcase" id="transform-before">
+ <div class="absolutebox fail"></div>
+ <div class="target pass"></div>
+ </div>
+</div>
+<script>
+ setupAnimation('#transform-before', STATE_CURRENT, PROPERTY_TRANSFORM, PHASE_BEFORE);
+</script>
+
+<!-- Is current - in play-->
+<div class="container">
+ <div class="label">Transform - active phase</div>
+ <div class="testcase" id="transform-active">
+ <div class="absolutebox fail"></div>
+ <div class="target pass"></div>
+ </div>
+</div>
+<script>
+ setupAnimation('#transform-active', STATE_CURRENT, PROPERTY_TRANSFORM, PHASE_ACTIVE);
+</script>
+
+<!-- Is current - after phase playing backwards-->
+<div class="container">
+ <div class="label">Transform - after phase</div>
+ <div class="testcase" id="transform-after">
+ <div class="absolutebox fail"></div>
+ <div class="target pass"></div>
+ </div>
+</div>
+<script>
+ setupAnimation('#transform-after', STATE_CURRENT, PROPERTY_TRANSFORM, PHASE_AFTER);
+</script>
+
+<!-- BACKGROUND COLOR -->
+<!--
+ unlike opacity and transform, background color shouldn't create a stacking
+ context.
+-->
+
+<!-- Is current - before phase playing forwards -->
+<div class="container">
+ <div class="label">Background-color - before phase</div>
+ <div class="testcase" id="bgcolor-before">
+ <div class="absolutebox pass"></div>
+ <div class="target fail"></div>
+ </div>
+</div>
+<script>
+ setupAnimation('#bgcolor-before', STATE_CURRENT, PROPERTY_BGCOLOR, PHASE_BEFORE);
+</script>
+
+<!-- Is current - in play-->
+<div class="container">
+ <div class="label">Background-color - active phase</div>
+ <div class="testcase" id="bgcolor-active">
+ <div class="absolutebox pass"></div>
+ <div class="target fail"></div>
+ </div>
+</div>
+<script>
+ setupAnimation('#bgcolor-active', STATE_CURRENT, PROPERTY_BGCOLOR, PHASE_ACTIVE);
+</script>
+
+<!-- Is current - after phase playing backwards-->
+<div class="container">
+ <div class="label">Background-color - after phase</div>
+ <div class="testcase" id="bgcolor-after">
+ <div class="absolutebox pass"></div>
+ <div class="target fail"></div>
+ </div>
+</div>
+<script>
+ setupAnimation('#bgcolor-after', STATE_CURRENT, PROPERTY_BGCOLOR, PHASE_AFTER);
+</script>
+
+</html>
diff --git a/testing/web-platform/tests/web-animations/animation-model/side-effects-of-animations-in-effect-ref.html b/testing/web-platform/tests/web-animations/animation-model/side-effects-of-animations-in-effect-ref.html
new file mode 100644
index 0000000000..24160cb267
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/animation-model/side-effects-of-animations-in-effect-ref.html
@@ -0,0 +1,109 @@
+<!DOCTYPE html>
+<title>
+ In effect animations should effectively apply will-change (i.e. force a
+ stacking context) (ref)
+</title>
+<link rel="help" href="https://drafts.csswg.org/web-animations-1/#side-effects-section">
+<link rel="author" href="mailto:bokan@chromium.org">
+<style>
+ .testcase {
+ position: relative;
+ margin: 2px;
+ border: 1px solid black;
+ width: 250px;
+ height: 25px;
+ }
+ .label {
+ flex-grow: 1;
+ }
+ .container {
+ background-color: lightgrey;
+ margin: 4px;
+ display: flex;
+ width: 600px;
+ flex-direction: row;
+ justify-content: flex-end;
+ align-items: center;
+ }
+ .pass {
+ width: 100%;
+ height: 100%;
+ background-color: limegreen;
+ }
+</style>
+
+<!-- OPACITY -->
+
+<!-- Is current - before phase playing forwards -->
+<div class="container">
+ <div class="label">Opacity - before phase</div>
+ <div class="testcase" id="opacity-before">
+ <div class="pass"></div>
+ </div>
+</div>
+
+<!-- Is current - in play-->
+<div class="container">
+ <div class="label">Opacity - active phase</div>
+ <div class="testcase" id="opacity-active">
+ <div class="pass"></div>
+ </div>
+</div>
+
+<!-- Is current - after phase playing backwards-->
+<div class="container">
+ <div class="label">Opacity - after phase</div>
+ <div class="testcase" id="opacity-after">
+ <div class="pass"></div>
+ </div>
+</div>
+
+<!-- TRANSFORM -->
+
+<!-- Is current - before phase playing forwards -->
+<div class="container">
+ <div class="label">Transform - before phase</div>
+ <div class="testcase" id="transform-before">
+ <div class="pass"></div>
+ </div>
+</div>
+
+<!-- Is current - in play-->
+<div class="container">
+ <div class="label">Transform - active phase</div>
+ <div class="testcase" id="transform-active">
+ <div class="pass"></div>
+ </div>
+</div>
+
+<!-- Is current - after phase playing backwards-->
+<div class="container">
+ <div class="label">Transform - after phase</div>
+ <div class="testcase" id="transform-after">
+ <div class="pass"></div>
+ </div>
+</div>
+
+<!-- BACKGROUND COLOR -->
+<div class="container">
+ <div class="label">Background-color - before phase</div>
+ <div class="testcase" id="bgcolor-before">
+ <div class="pass"></div>
+ </div>
+</div>
+
+<!-- Is current - in play-->
+<div class="container">
+ <div class="label">Background-color - active phase</div>
+ <div class="testcase" id="bgcolor-active">
+ <div class="pass"></div>
+ </div>
+</div>
+
+<!-- Is current - after phase playing backwards-->
+<div class="container">
+ <div class="label">Background-color - after phase</div>
+ <div class="testcase" id="bgcolor-after">
+ <div class="pass"></div>
+ </div>
+</div>
diff --git a/testing/web-platform/tests/web-animations/animation-model/side-effects-of-animations-in-effect.html b/testing/web-platform/tests/web-animations/animation-model/side-effects-of-animations-in-effect.html
new file mode 100644
index 0000000000..90ee055082
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/animation-model/side-effects-of-animations-in-effect.html
@@ -0,0 +1,174 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+<title>
+ In effect animations should effectively apply will-change (i.e. force a
+ stacking context)
+</title>
+<link rel="help" href="https://drafts.csswg.org/web-animations-1/#side-effects-section">
+<link rel="author" href="mailto:bokan@chromium.org">
+<link rel="match" href="side-effects-of-animations-in-effect-ref.html">
+<script src="/common/reftest-wait.js"></script>
+<script src="../testcommon.js"></script>
+<script src="resources/side-effects-common.js"></script>
+<style>
+ .testcase {
+ position: relative;
+ margin: 2px;
+ border: 1px solid black;
+ width: 250px;
+ height: 25px;
+ }
+ .container {
+ background-color: lightgrey;
+ margin: 4px;
+ display: flex;
+ width: 600px;
+ flex-direction: row;
+ justify-content: flex-end;
+ align-items: center;
+ }
+ .label {
+ flex-grow: 1;
+ }
+ .target {
+ width: 100%;
+ height: 100%;
+ }
+ .absolutebox {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ }
+ .pass {
+ background-color: limegreen;
+ }
+ .fail {
+ background-color: red;
+ }
+</style>
+
+<script>
+onload = takeScreenshotOnAnimationsReady;
+</script>
+
+<!-- OPACITY -->
+
+<!-- Is current - before phase playing forwards -->
+<div class="container">
+ <div class="label">Opacity - before phase</div>
+ <div class="testcase" id="opacity-before">
+ <div class="absolutebox fail"></div>
+ <div class="target pass"></div>
+ </div>
+</div>
+<script>
+ setupAnimation('#opacity-before', STATE_IN_EFFECT, PROPERTY_OPACITY, PHASE_BEFORE);
+</script>
+
+<!-- Is current - in play-->
+<div class="container">
+ <div class="label">Opacity - active phase</div>
+ <div class="testcase" id="opacity-active">
+ <div class="absolutebox fail"></div>
+ <div class="target pass"></div>
+ </div>
+</div>
+<script>
+ setupAnimation('#opacity-active', STATE_IN_EFFECT, PROPERTY_OPACITY, PHASE_ACTIVE);
+</script>
+
+<!-- Is current - after phase playing backwards-->
+<div class="container">
+ <div class="label">Opacity - after phase</div>
+ <div class="testcase" id="opacity-after">
+ <div class="absolutebox fail"></div>
+ <div class="target pass"></div>
+ </div>
+</div>
+<script>
+ setupAnimation('#opacity-after', STATE_IN_EFFECT, PROPERTY_OPACITY, PHASE_AFTER);
+</script>
+
+<!-- TRANSFORM -->
+
+<!-- Is current - before phase playing forwards -->
+<div class="container">
+ <div class="label">Transform - before phase</div>
+ <div class="testcase" id="transform-before">
+ <div class="absolutebox fail"></div>
+ <div class="target pass"></div>
+ </div>
+</div>
+<script>
+ setupAnimation('#transform-before', STATE_IN_EFFECT, PROPERTY_TRANSFORM, PHASE_BEFORE);
+</script>
+
+<!-- Is current - in play-->
+<div class="container">
+ <div class="label">Transform - active phase</div>
+ <div class="testcase" id="transform-active">
+ <div class="absolutebox fail"></div>
+ <div class="target pass"></div>
+ </div>
+</div>
+<script>
+ setupAnimation('#transform-active', STATE_IN_EFFECT, PROPERTY_TRANSFORM, PHASE_ACTIVE);
+</script>
+
+<!-- Is current - after phase playing backwards-->
+<div class="container">
+ <div class="label">Transform - after phase</div>
+ <div class="testcase" id="transform-after">
+ <div class="absolutebox fail"></div>
+ <div class="target pass"></div>
+ </div>
+</div>
+<script>
+ setupAnimation('#transform-after', STATE_IN_EFFECT, PROPERTY_TRANSFORM, PHASE_AFTER);
+</script>
+
+<!-- BACKGROUND COLOR -->
+<!--
+ unlike opacity and transform, background color shouldn't create a stacking
+ context.
+-->
+
+<!-- Is current - before phase playing forwards -->
+<div class="container">
+ <div class="label">Background-color - before phase</div>
+ <div class="testcase" id="bgcolor-before">
+ <div class="absolutebox pass"></div>
+ <div class="target fail"></div>
+ </div>
+</div>
+<script>
+ setupAnimation('#bgcolor-before', STATE_IN_EFFECT, PROPERTY_BGCOLOR, PHASE_BEFORE);
+</script>
+
+<!-- Is current - in play-->
+<div class="container">
+ <div class="label">Background-color - active phase</div>
+ <div class="testcase" id="bgcolor-active">
+ <div class="absolutebox pass"></div>
+ <div class="target fail"></div>
+ </div>
+</div>
+<script>
+ setupAnimation('#bgcolor-active', STATE_IN_EFFECT, PROPERTY_BGCOLOR, PHASE_ACTIVE);
+</script>
+
+<!-- Is current - after phase playing backwards-->
+<div class="container">
+ <div class="label">Background-color - after phase</div>
+ <div class="testcase" id="bgcolor-after">
+ <div class="absolutebox pass"></div>
+ <div class="target fail"></div>
+ </div>
+</div>
+<script>
+ setupAnimation('#bgcolor-after', STATE_IN_EFFECT, PROPERTY_BGCOLOR, PHASE_AFTER);
+</script>
+
+</html>
diff --git a/testing/web-platform/tests/web-animations/animation-model/side-effects-of-animations-none-ref.html b/testing/web-platform/tests/web-animations/animation-model/side-effects-of-animations-none-ref.html
new file mode 100644
index 0000000000..4d1ea997f4
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/animation-model/side-effects-of-animations-none-ref.html
@@ -0,0 +1,48 @@
+<!DOCTYPE html>
+<title>
+ Animations not in effect nor current shouldn't have side-effects (ref).
+</title>
+<link rel="help" href="https://drafts.csswg.org/web-animations-1/#side-effects-section">
+<link rel="author" href="mailto:bokan@chromium.org">
+<style>
+ .testcase {
+ position: relative;
+ margin: 2px;
+ border: 1px solid black;
+ width: 250px;
+ height: 25px;
+ }
+ .label {
+ flex-grow: 1;
+ }
+ .container {
+ background-color: lightgrey;
+ margin: 4px;
+ display: flex;
+ width: 600px;
+ flex-direction: row;
+ justify-content: flex-end;
+ align-items: center;
+ }
+ .pass {
+ width: 100%;
+ height: 100%;
+ background-color: limegreen;
+ }
+</style>
+
+<!-- before phase playing backwards -->
+<div class="container">
+ <div class="label">Before phase</div>
+ <div class="testcase" id="before">
+ <div class="pass"></div>
+ </div>
+</div>
+
+<!-- after phase playing forwards -->
+<div class="container">
+ <div class="label">After phase</div>
+ <div class="testcase" id="after">
+ <div class="pass"></div>
+ </div>
+</div>
diff --git a/testing/web-platform/tests/web-animations/animation-model/side-effects-of-animations-none.html b/testing/web-platform/tests/web-animations/animation-model/side-effects-of-animations-none.html
new file mode 100644
index 0000000000..d200409e64
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/animation-model/side-effects-of-animations-none.html
@@ -0,0 +1,79 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+<title>
+ Animations not in effect nor current shouldn't have side-effects.
+</title>
+<link rel="help" href="https://drafts.csswg.org/web-animations-1/#side-effects-section">
+<link rel="author" href="mailto:bokan@chromium.org">
+<link rel="match" href="side-effects-of-animations-none-ref.html">
+<script src="/common/reftest-wait.js"></script>
+<script src="../testcommon.js"></script>
+<script src="resources/side-effects-common.js"></script>
+<style>
+ .testcase {
+ position: relative;
+ margin: 2px;
+ border: 1px solid black;
+ width: 250px;
+ height: 25px;
+ }
+ .container {
+ background-color: lightgrey;
+ margin: 4px;
+ display: flex;
+ width: 600px;
+ flex-direction: row;
+ justify-content: flex-end;
+ align-items: center;
+ }
+ .label {
+ flex-grow: 1;
+ }
+ .target {
+ width: 100%;
+ height: 100%;
+ }
+ .absolutebox {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ }
+ .pass {
+ background-color: limegreen;
+ }
+ .fail {
+ background-color: red;
+ }
+</style>
+
+<script>
+onload = takeScreenshotOnAnimationsReady;
+</script>
+
+<!-- before phase playing backwards -->
+<div class="container">
+ <div class="label">Before phase</div>
+ <div class="testcase" id="before">
+ <div class="absolutebox pass"></div>
+ <div class="target fail"></div>
+ </div>
+</div>
+<script>
+ setupAnimation('#before', STATE_NONE, PROPERTY_OPACITY, PHASE_BEFORE);
+</script>
+
+<!-- after phase playing forwards-->
+<div class="container">
+ <div class="label">After phase</div>
+ <div class="testcase" id="after">
+ <div class="absolutebox pass"></div>
+ <div class="target fail"></div>
+ </div>
+</div>
+<script>
+ setupAnimation('#after', STATE_NONE, PROPERTY_OPACITY, PHASE_AFTER);
+</script>
+
+</html>
diff --git a/testing/web-platform/tests/web-animations/crashtests/color-mix-crashtest.html b/testing/web-platform/tests/web-animations/crashtests/color-mix-crashtest.html
new file mode 100644
index 0000000000..91d29464f5
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/crashtests/color-mix-crashtest.html
@@ -0,0 +1,35 @@
+<!DOCTYPE html>
+<html class="test-wait">
+<title>Interpolation of a color-mix function with currentcolor should not crash</title>
+<!-- crbug.com/1493430 -->
+<style type="text/css">
+ @keyframes colorize {
+ from { color: forestgreen; }
+ to { color: plum; }
+ }
+ #target {
+ animation: colorize 1s Infinite alternate;
+ border: 1px solid transparent;
+ transition: border-color 1s ease-in-out;
+ }
+ #target.update {
+ border-color:
+ color-mix(in hsl longer hue, hsl(120 100% 50%) 20%,
+ currentcolor);
+ }
+</style>
+<body>
+ <div id="target">Hello world</div>
+</body>
+<script src="../testcommon.js"></script>
+<script>
+ window.onload = async () => {
+ await waitForNextFrame();
+ target.classList.add('update');
+ await Promise.all(document.getAnimations().map(a => a.resolve));
+ await waitForNextFrame();
+ await waitForNextFrame();
+ document.documentElement.classList.remove('test-wait');
+ };
+</script>
+</html>
diff --git a/testing/web-platform/tests/web-animations/crashtests/get-computed-timing-crash.html b/testing/web-platform/tests/web-animations/crashtests/get-computed-timing-crash.html
new file mode 100644
index 0000000000..b666eea91f
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/crashtests/get-computed-timing-crash.html
@@ -0,0 +1,20 @@
+<!DOCTYPE html>
+<html>
+<title>GetComputedTiming on an animation without an execution context,
+timeline or playback rate</title>
+<link rel="author" title="Google" href="http://www.google.com/">
+<link rel="help" href="https://bugs.chromium.org/p/chromium/issues/detail?id=1318012">
+<meta name="assert" content="This should not crash.">
+<body>
+ <div id="target"></div>
+ <iframe id="iframe"></iframe>
+</body>
+<script type="text/javascript">
+ const keyframeEffect = new KeyframeEffect(target, { opacity: [1, 1] });
+ const anim = new iframe.contentWindow.Animation(keyframeEffect, null);
+ anim.play();
+ anim.playbackRate = 0;
+ document.body.removeChild(iframe);
+ anim.effect.getComputedTiming();
+</script>
+</html>
diff --git a/testing/web-platform/tests/web-animations/crashtests/infinite-active-duration.html b/testing/web-platform/tests/web-animations/crashtests/infinite-active-duration.html
new file mode 100644
index 0000000000..f92cd13942
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/crashtests/infinite-active-duration.html
@@ -0,0 +1,68 @@
+<!DOCTYPE html>
+<title>Various test cases producing infinite active duration</title>
+<link rel="help" href="https://drafts.csswg.org/css-animations-1/#animation-iteration-count" />
+<script>
+ let effect = new KeyframeEffect(null,
+ { opacity: [0, 1] },
+ { duration: 1, delay: -17592186044416, iterations: Infinity });
+ effect.getComputedTiming();
+
+ // Infinity delay + Infinity active duration
+ effect = new KeyframeEffect(null,
+ { opacity: [0, 1] },
+ { duration: 1, delay: Number.MAX_VALUE, iterations: Infinity });
+ effect.getComputedTiming();
+
+ // Infinity end delay + Infinity active duration
+ effect = new KeyframeEffect(null,
+ { opacity: [0, 1] },
+ { duration: 1, endDelay: Number.MAX_VALUE, iterations: Infinity });
+ effect.getComputedTiming();
+
+ // Infinity delay + Infinity active duration + Infinity end delay
+ effect = new KeyframeEffect(null,
+ { opacity: [0, 1] },
+ { duration: 1,
+ delay: Number.MAX_VALUE, endDelay: Number.MAX_VALUE,
+ iterations: Infinity });
+ effect.getComputedTiming();
+
+ // -Infinity delay + Infinity active duration
+ effect = new KeyframeEffect(null,
+ { opacity: [0, 1] },
+ { duration: 1, delay: -Number.MAX_VALUE, iterations: Infinity });
+ effect.getComputedTiming();
+
+ // -Infinity end delay + Infinity active duration
+ effect = new KeyframeEffect(null,
+ { opacity: [0, 1] },
+ { duration: 1, endDelay: -Number.MAX_VALUE, iterations: Infinity });
+ effect.getComputedTiming();
+
+ // -Infinity delay + Infinity active duration + -Infinity end delay
+ effect = new KeyframeEffect(null,
+ { opacity: [0, 1] },
+ { duration: 1,
+ delay: -Number.MAX_VALUE, endDelay: -Number.MAX_VALUE,
+ iterations: Infinity });
+ effect.getComputedTiming();
+
+ // -Infinity delay + finite active duration
+ effect = new KeyframeEffect(null,
+ { opacity: [0, 1] },
+ { duration: 1, delay: -Number.MAX_VALUE, iterations: 1 });
+ effect.getComputedTiming();
+
+ // -Infinity end delay + finite active duration
+ effect = new KeyframeEffect(null,
+ { opacity: [0, 1] },
+ { duration: 1, endDelay: -Number.MAX_VALUE, iterations: 1 });
+ effect.getComputedTiming();
+
+ // very large iterations
+ effect = new KeyframeEffect(null,
+ { opacity: [0, 1] },
+ { duration: 1, delay: 281474976710655, iterations: 18014398509481984 });
+ effect.getComputedTiming();
+
+</script>
diff --git a/testing/web-platform/tests/web-animations/crashtests/partially-overlapping-animations-one-not-current-001.html b/testing/web-platform/tests/web-animations/crashtests/partially-overlapping-animations-one-not-current-001.html
new file mode 100644
index 0000000000..b943514f42
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/crashtests/partially-overlapping-animations-one-not-current-001.html
@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<html>
+<title>CSS Test (Animations): Reparenting an element with a web animation on the compositor</title>
+<link rel="author" title="L. David Baron" href="https://dbaron.org/">
+<link rel="author" title="Google" href="http://www.google.com/">
+<link rel="help" href="https://bugs.chromium.org/p/chromium/issues/detail?id=1319304">
+<meta name="assert" content="This should not crash.">
+
+<script>
+window.onload = function(){
+ let div = document.querySelector("div");
+ let a1 = div.animate([{"transform": "translateX(10px)", "opacity": "0.4"}], { duration: 1000 });
+ a1.reverse();
+ let a2 = div.animate([{"transform": "translateY(10px)"}], { duration: 1000 });
+}
+</script>
+<div>X</div>
diff --git a/testing/web-platform/tests/web-animations/crashtests/reparent-animating-element-001.html b/testing/web-platform/tests/web-animations/crashtests/reparent-animating-element-001.html
new file mode 100644
index 0000000000..49ee9c433c
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/crashtests/reparent-animating-element-001.html
@@ -0,0 +1,40 @@
+<!DOCTYPE html>
+<html class="test-wait">
+<title>CSS Test (Animations): Reparenting an element with a web animation on the compositor</title>
+<link rel="author" title="L. David Baron" href="https://dbaron.org/">
+<link rel="author" title="Google" href="http://www.google.com/">
+<link rel="help" href="https://bugs.chromium.org/p/chromium/issues/detail?id=1301838">
+<meta name="assert" content="This should not crash.">
+
+<style>
+#animate {
+ width: 100px;
+ height: 100px;
+ background: blue;
+}
+</style>
+<div id="animate"></div>
+<div id="newparent"></div>
+<script>
+
+document.getElementById("animate").animate(
+ [
+ { transform: "rotate(0deg)" },
+ { transform: "rotate(360deg)" }
+ ],
+ {
+ duration: 5000,
+ iterations: Infinity
+ }
+);
+
+requestAnimationFrame(function() {
+ requestAnimationFrame(function() {
+ document.getElementById("newparent").appendChild(document.getElementById("animate"));
+ requestAnimationFrame(function() {
+ document.documentElement.classList.remove("test-wait");
+ });
+ });
+});
+
+</script>
diff --git a/testing/web-platform/tests/web-animations/crashtests/reparent-animating-element-002.html b/testing/web-platform/tests/web-animations/crashtests/reparent-animating-element-002.html
new file mode 100644
index 0000000000..0d3549fc33
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/crashtests/reparent-animating-element-002.html
@@ -0,0 +1,44 @@
+<!DOCTYPE html>
+<html class="test-wait">
+<title>CSS Test (Animations): Reparenting an element with a web animation on the compositor</title>
+<link rel="author" title="L. David Baron" href="https://dbaron.org/">
+<link rel="author" title="Google" href="http://www.google.com/">
+<link rel="help" href="https://bugs.chromium.org/p/chromium/issues/detail?id=1305487">
+<meta name="assert" content="This should not crash.">
+<!--
+
+The Chromium implementation of <marquee> essentially uses web animations
+underneath. However, I was unable to make a testcase for this crash
+that uses web animations directly. Despite that, it still seems worth
+adding this testcase here in WPT.
+
+-->
+
+<style>
+#animate {
+ width: 100px;
+ height: 100px;
+}
+#newparent {
+ display: none;
+}
+</style>
+<marquee id="animate">X</marquee>
+<div id="newparent"></div>
+<script>
+
+let a = document.getElementById("animate");
+
+requestAnimationFrame(function() {
+ // use setTimeout because the crash doesn't happen if we do this inside
+ // a requestAnimationFrame callback
+ setTimeout(function() {
+ a.remove();
+ document.getElementById("newparent").appendChild(a);
+ requestAnimationFrame(function() {
+ document.documentElement.classList.remove("test-wait");
+ });
+ }, 0);
+});
+
+</script>
diff --git a/testing/web-platform/tests/web-animations/idlharness.window.js b/testing/web-platform/tests/web-animations/idlharness.window.js
new file mode 100644
index 0000000000..aaf6f58e7b
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/idlharness.window.js
@@ -0,0 +1,21 @@
+// META: script=/resources/WebIDLParser.js
+// META: script=/resources/idlharness.js
+// META: timeout=long
+
+'use strict';
+
+idl_test(
+ ['web-animations', 'web-animations-2'],
+ ['dom', 'html', 'scroll-animations'],
+ idl_array => {
+ idl_array.add_objects({
+ Animation: ['new Animation()'],
+ AnimationPlaybackEvent: ['new AnimationPlaybackEvent("cancel")'],
+ Document: ['document'],
+ DocumentTimeline: ['document.timeline'],
+ KeyframeEffect: ['new KeyframeEffect(null, null)'],
+ ShadowRoot: ['shadowRoot'],
+ });
+ self.shadowRoot = document.createElement("div").attachShadow({mode: "open"});
+ }
+);
diff --git a/testing/web-platform/tests/web-animations/interfaces/Animatable/animate-no-browsing-context.html b/testing/web-platform/tests/web-animations/interfaces/Animatable/animate-no-browsing-context.html
new file mode 100644
index 0000000000..61a7502a98
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/interfaces/Animatable/animate-no-browsing-context.html
@@ -0,0 +1,107 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>Animatable.animate in combination with elements in documents
+ without a browsing context</title>
+<link rel="help" href="https://drafts.csswg.org/web-animations/#dom-animatable-animate">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../../testcommon.js"></script>
+<div id="log"></div>
+<script>
+'use strict';
+
+//
+// The following tests relate to animations on elements in documents without
+// a browsing context. This is NOT the same as documents that are not bound to
+// a document tree.
+//
+
+function getXHRDoc(t) {
+ return new Promise(resolve => {
+ const xhr = new XMLHttpRequest();
+ xhr.open('GET', '../../resources/xhr-doc.py');
+ xhr.responseType = 'document';
+ xhr.onload = t.step_func(() => {
+ assert_equals(xhr.readyState, xhr.DONE,
+ 'Request should complete successfully');
+ assert_equals(xhr.status, 200,
+ 'Response should be OK');
+ resolve(xhr.responseXML);
+ });
+ xhr.send();
+ });
+}
+
+promise_test(t => {
+ return getXHRDoc(t).then(xhrdoc => {
+ const div = xhrdoc.getElementById('test');
+ const anim = div.animate(null);
+ assert_class_string(anim.timeline, 'DocumentTimeline',
+ 'Animation should have a timeline');
+ assert_equals(anim.timeline, xhrdoc.timeline,
+ 'Animation timeline should be the default document timeline'
+ + ' of the XHR doc');
+ assert_not_equals(anim.timeline, document.timeline,
+ 'Animation timeline should NOT be the same timeline as'
+ + ' the default document timeline for the current'
+ + ' document');
+
+ });
+}, 'Element.animate() creates an animation with the correct timeline'
+ + ' when called on an element in a document without a browsing context');
+
+//
+// The following tests are cross-cutting tests that are not specific to the
+// Animatable.animate() interface. Instead, they draw on assertions from
+// various parts of the spec. These assertions are tested in other tests
+// but are repeated here to confirm that user agents are not doing anything
+// different in the particular case of document without a browsing context.
+//
+
+promise_test(t => {
+ return getXHRDoc(t).then(xhrdoc => {
+ const div = xhrdoc.getElementById('test');
+ const anim = div.animate(null);
+ // Since a document from XHR will not be active by itself, its document
+ // timeline will also be inactive.
+ assert_equals(anim.timeline.currentTime, null,
+ 'Document timeline time should be null');
+ });
+}, 'The timeline associated with an animation trigger on an element in'
+ + ' a document without a browsing context is inactive');
+
+promise_test(t => {
+ let anim;
+ return getXHRDoc(t).then(xhrdoc => {
+ const div = xhrdoc.getElementById('test');
+ anim = div.animate(null);
+ anim.timeline = document.timeline;
+ assert_true(anim.pending, 'The animation should be initially pending');
+ return waitForAnimationFrames(2);
+ }).then(() => {
+ // Because the element is in a document without a browsing context, it will
+ // not be rendered and hence the user agent will never deem it ready to
+ // animate.
+ assert_true(anim.pending,
+ 'The animation should still be pending after replacing'
+ + ' the document timeline');
+ });
+}, 'Replacing the timeline of an animation targetting an element in a'
+ + ' document without a browsing context leaves it in the pending state');
+
+promise_test(t => {
+ let anim;
+ return getXHRDoc(t).then(xhrdoc => {
+ const div = xhrdoc.getElementById('test');
+ anim = div.animate({ opacity: [ 0, 1 ] }, 1000);
+ anim.timeline = document.timeline;
+ document.body.appendChild(div);
+ assert_equals(getComputedStyle(div).opacity, '0',
+ 'Style should be updated');
+ });
+}, 'Replacing the timeline of an animation targetting an element in a'
+ + ' document without a browsing context and then adopting that element'
+ + ' causes it to start updating style');
+
+</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 0000000000..28e803d690
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/interfaces/Animatable/animate.html
@@ -0,0 +1,363 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title>Animatable.animate</title>
+<link rel="help" href="https://drafts.csswg.org/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/easing-tests.js"></script>
+<script src="../../resources/keyframe-utils.js"></script>
+<script src="../../resources/keyframe-tests.js"></script>
+<script src="../../resources/timing-utils.js"></script>
+<script src="../../resources/timing-tests.js"></script>
+<style>
+.pseudo::before {content: '';}
+.pseudo::after {content: '';}
+.pseudo::marker {content: '';}
+</style>
+<body>
+<div id="log"></div>
+<iframe width="10" height="10" id="iframe"></iframe>
+<script>
+'use strict';
+
+// Tests on Element
+
+test(t => {
+ const anim = createDiv(t).animate(null);
+ assert_class_string(anim, 'Animation', 'Returned object is an Animation');
+}, 'Element.animate() creates an Animation object');
+
+test(t => {
+ const iframe = window.frames[0];
+ const div = createDiv(t, iframe.document);
+ const 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(t => {
+ const div = createDiv(t);
+ const 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(t => {
+ const iframe = window.frames[0];
+ const div = createDiv(t, iframe.document);
+ const 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');
+
+for (const subtest of gEmptyKeyframeListTests) {
+ test(t => {
+ const anim = createDiv(t).animate(subtest, 2000);
+ assert_not_equals(anim, null);
+ }, 'Element.animate() accepts empty keyframe lists ' +
+ `(input: ${JSON.stringify(subtest)})`);
+}
+
+for (const subtest of gKeyframesTests) {
+ test(t => {
+ const anim = createDiv(t).animate(subtest.input, 2000);
+ assert_frame_lists_equal(anim.effect.getKeyframes(), subtest.output);
+ }, `Element.animate() accepts ${subtest.desc}`);
+}
+
+for (const subtest of gInvalidKeyframesTests) {
+ test(t => {
+ const div = createDiv(t);
+ assert_throws_js(TypeError, () => {
+ div.animate(subtest.input, 2000);
+ });
+ }, `Element.animate() does not accept ${subtest.desc}`);
+}
+
+test(t => {
+ const anim = createDiv(t).animate(null, 2000);
+ assert_equals(anim.effect.getTiming().duration, 2000);
+ assert_default_timing_except(anim.effect, ['duration']);
+}, 'Element.animate() accepts a double as an options argument');
+
+test(t => {
+ const anim = createDiv(t).animate(null,
+ { duration: Infinity, fill: 'forwards' });
+ assert_equals(anim.effect.getTiming().duration, Infinity);
+ assert_equals(anim.effect.getTiming().fill, 'forwards');
+ assert_default_timing_except(anim.effect, ['duration', 'fill']);
+}, 'Element.animate() accepts a KeyframeAnimationOptions argument');
+
+test(t => {
+ const anim = createDiv(t).animate(null);
+ assert_default_timing_except(anim.effect, []);
+}, 'Element.animate() accepts an absent options argument');
+
+for (const invalid of gBadDelayValues) {
+ test(t => {
+ assert_throws_js(TypeError, () => {
+ createDiv(t).animate(null, { delay: invalid });
+ });
+ }, `Element.animate() does not accept invalid delay value: ${invalid}`);
+}
+
+test(t => {
+ const anim = createDiv(t).animate(null, { duration: 'auto' });
+ assert_equals(anim.effect.getTiming().duration, 'auto', 'set duration \'auto\'');
+ assert_equals(anim.effect.getComputedTiming().duration, 0,
+ 'getComputedTiming() after set duration \'auto\'');
+}, 'Element.animate() accepts a duration of \'auto\' using a dictionary'
+ + ' object');
+
+for (const invalid of gBadDurationValues) {
+ if (typeof invalid === 'string' && !isNaN(parseFloat(invalid))) {
+ continue;
+ }
+ test(t => {
+ assert_throws_js(TypeError, () => {
+ createDiv(t).animate(null, invalid);
+ });
+ }, 'Element.animate() does not accept invalid duration value: '
+ + (typeof invalid === 'string' ? `"${invalid}"` : invalid));
+}
+
+for (const invalid of gBadDurationValues) {
+ test(t => {
+ assert_throws_js(TypeError, () => {
+ createDiv(t).animate(null, { duration: invalid });
+ });
+ }, 'Element.animate() does not accept invalid duration value: '
+ + (typeof invalid === 'string' ? `"${invalid}"` : invalid)
+ + ' using a dictionary object');
+}
+
+for (const invalidEasing of gInvalidEasings) {
+ test(t => {
+ assert_throws_js(TypeError, () => {
+ createDiv(t).animate({ easing: invalidEasing }, 2000);
+ });
+ }, `Element.animate() does not accept invalid easing: '${invalidEasing}'`);
+}
+
+for (const invalid of gBadIterationStartValues) {
+ test(t => {
+ assert_throws_js(TypeError, () => {
+ createDiv(t).animate(null, { iterationStart: invalid });
+ });
+ }, 'Element.animate() does not accept invalid iterationStart value: ' +
+ invalid);
+}
+
+for (const invalid of gBadIterationsValues) {
+ test(t => {
+ assert_throws_js(TypeError, () => {
+ createDiv(t).animate(null, { iterations: invalid });
+ });
+ }, 'Element.animate() does not accept invalid iterations value: ' +
+ invalid);
+}
+
+test(t => {
+ const anim = createDiv(t).animate(null, 2000);
+ assert_equals(anim.id, '');
+}, 'Element.animate() correctly sets the id attribute when no id is specified');
+
+test(t => {
+ const anim = createDiv(t).animate(null, { id: 'test' });
+ assert_equals(anim.id, 'test');
+}, 'Element.animate() correctly sets the id attribute');
+
+test(t => {
+ const anim = createDiv(t).animate(null, 2000);
+ assert_equals(anim.timeline, document.timeline);
+}, 'Element.animate() correctly sets the Animation\'s timeline');
+
+async_test(t => {
+ const iframe = document.createElement('iframe');
+ iframe.width = 10;
+ iframe.height = 10;
+
+ iframe.addEventListener('load', t.step_func(() => {
+ const div = createDiv(t, iframe.contentDocument);
+ const anim = div.animate(null, 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');
+
+for (const subtest of gAnimationTimelineTests) {
+ test(t => {
+ const anim = createDiv(t).animate(null, { timeline: subtest.timeline });
+ assert_not_equals(anim, null,
+ 'An animation sohuld be created');
+ assert_equals(anim.timeline, subtest.expectedTimeline,
+ 'Animation timeline should be '+
+ subtest.expectedTimelineDescription);
+ }, 'Element.animate() correctly sets the Animation\'s timeline '
+ + subtest.description + ' in KeyframeAnimationOptions.');
+}
+
+test(t => {
+ const anim = createDiv(t).animate(null, 2000);
+ assert_equals(anim.playState, 'running');
+}, 'Element.animate() calls play on the Animation');
+
+promise_test(async t => {
+ const div = createDiv(t);
+
+ let gotTransition = false;
+ div.addEventListener('transitionrun', () => {
+ gotTransition = true;
+ });
+
+ // Setup transition start point.
+ div.style.transition = 'opacity 100s';
+ getComputedStyle(div).opacity;
+
+ // Update specified style but don't flush style.
+ div.style.opacity = '0.5';
+
+ // Trigger a new animation at the same time.
+ const anim = div.animate({ opacity: [0, 1] }, 100 * MS_PER_SEC);
+
+ // If Element.animate() produces a style change event it will have triggered
+ // a transition.
+ //
+ // If it does NOT produce a style change event, the animation will override
+ // the before-change style and after-change style such that a transition is
+ // never triggered.
+
+ // Wait for the animation to start and then for one more animation
+ // frame to give the transitionrun event a chance to be dispatched.
+ await anim.ready;
+ await waitForAnimationFrames(1);
+
+ assert_false(gotTransition, 'A transition should NOT have been triggered');
+}, 'Element.animate() does NOT trigger a style change event');
+
+// Tests on pseudo-elements
+// Some tests occur twice (on pseudo-elements with and without content)
+// in order to test both code paths for tree-abiding pseudo-elements in blink.
+
+test(t => {
+ const div = createDiv(t);
+ div.classList.add('pseudo');
+ const anim = div.animate(null, {pseudoElement: '::before'});
+ assert_class_string(anim, 'Animation', 'The returned object is an Animation');
+}, 'animate() with pseudoElement parameter creates an Animation object');
+
+test(t => {
+ const div = createDiv(t);
+ const anim = div.animate(null, {pseudoElement: '::before'});
+ assert_class_string(anim, 'Animation', 'The returned object is an Animation');
+}, 'animate() with pseudoElement parameter without content creates an Animation object');
+
+test(t => {
+ const div = createDiv(t);
+ div.classList.add('pseudo');
+ div.style.display = 'list-item';
+ const anim = div.animate(null, {pseudoElement: '::marker'});
+ assert_class_string(anim, 'Animation', 'The returned object is an Animation for ::marker');
+}, 'animate() with pseudoElement parameter creates an Animation object for ::marker');
+
+test(t => {
+ const div = createDiv(t);
+ div.classList.add('pseudo');
+ div.textContent = 'foo';
+ const anim = div.animate(null, {pseudoElement: '::first-line'});
+ assert_class_string(anim, 'Animation', 'The returned object is an Animation for ::first-line');
+}, 'animate() with pseudoElement parameter creates an Animation object for ::first-line');
+
+test(t => {
+ const div = createDiv(t);
+ div.classList.add('pseudo');
+ const anim = div.animate(null, {pseudoElement: '::before'});
+ assert_equals(anim.effect.target, div, 'The returned element has the correct target element');
+ assert_equals(anim.effect.pseudoElement, '::before',
+ 'The returned Animation targets the correct selector');
+}, 'animate() with pseudoElement an Animation object targeting ' +
+ 'the correct pseudo-element');
+
+test(t => {
+ const div = createDiv(t);
+ const anim = div.animate(null, {pseudoElement: '::before'});
+ assert_equals(anim.effect.target, div, 'The returned element has the correct target element');
+ assert_equals(anim.effect.pseudoElement, '::before',
+ 'The returned Animation targets the correct selector');
+}, 'animate() with pseudoElement without content creates an Animation object targeting ' +
+ 'the correct pseudo-element');
+
+test(t => {
+ const div = createDiv(t);
+ div.classList.add('pseudo');
+ div.style.display = 'list-item';
+ const anim = div.animate(null, {pseudoElement: '::marker'});
+ assert_equals(anim.effect.target, div, 'The returned element has the correct target element');
+ assert_equals(anim.effect.pseudoElement, '::marker',
+ 'The returned Animation targets the correct selector');
+}, 'animate() with pseudoElement an Animation object targeting ' +
+ 'the correct pseudo-element for ::marker');
+
+test(t => {
+ const div = createDiv(t);
+ div.classList.add('pseudo');
+ div.textContent = 'foo';
+ const anim = div.animate(null, {pseudoElement: '::first-line'});
+ assert_equals(anim.effect.target, div, 'The returned element has the correct target element');
+ assert_equals(anim.effect.pseudoElement, '::first-line',
+ 'The returned Animation targets the correct selector');
+}, 'animate() with pseudoElement an Animation object targeting ' +
+ 'the correct pseudo-element for ::first-line');
+
+for (const pseudo of [
+ '',
+ 'before',
+ ':abc',
+ '::abc',
+]) {
+ test(t => {
+ const div = createDiv(t);
+ assert_throws_dom("SyntaxError", () => {
+ div.animate(null, {pseudoElement: pseudo});
+ });
+ }, `animate() with a non-null invalid pseudoElement '${pseudo}' throws a ` +
+ `SyntaxError`);
+}
+
+test(t => {
+ const div = createDiv(t);
+ div.animate(null, { pseudoElement: '::placeHOLDER' });
+}, `animate() with pseudoElement ::placeholder does not throw`);
+
+promise_test(async t => {
+ const div = createDiv(t);
+ div.classList.add('pseudo');
+ let animBefore = div.animate({opacity: [1, 0]}, {duration: 1, pseudoElement: '::before', fill: 'both'});
+ let animAfter = div.animate({opacity: [1, 0]}, {duration: 1, pseudoElement: '::after', fill: 'both'});
+ await animBefore.finished;
+ await animAfter.finished;
+ // The animation on ::before should not be replaced as it targets a different
+ // pseudo-element.
+ assert_equals(animBefore.replaceState, 'active');
+ assert_equals(animAfter.replaceState, 'active');
+}, 'Finished fill animation doesn\'t replace animation on a different pseudoElement');
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/web-animations/interfaces/Animatable/getAnimations-iframe.html b/testing/web-platform/tests/web-animations/interfaces/Animatable/getAnimations-iframe.html
new file mode 100644
index 0000000000..1851878c41
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/interfaces/Animatable/getAnimations-iframe.html
@@ -0,0 +1,51 @@
+<!DOCTYPE html>
+<title>getAnimations in dirty iframe</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../../testcommon.js"></script>
+<style>
+ iframe {
+ width: 200px;
+ height: 40px;
+ }
+</style>
+<body>
+<script>
+
+ const createFrame = async test => {
+ const iframe = createElement(test, "iframe");
+ const contents = "" +
+ "<style>" +
+ " div { color: red; }" +
+ " @keyframes test {" +
+ " from { color: green; }" +
+ " to { color: green; }" +
+ " }" +
+ " @media (min-width: 300px) {" +
+ " div { animation: test 1s linear forwards; }" +
+ " }" +
+ "</style>" +
+ "<div id=div>Green</div>";
+ iframe.setAttribute("srcdoc", contents);
+ await new Promise(resolve => iframe.addEventListener("load", resolve));
+ return iframe;
+ };
+
+ const iframeTest = (getAnimations, interfaceName) => {
+ promise_test(async test => {
+ const frame = await createFrame(test);
+ const inner_div = frame.contentDocument.getElementById('div');
+ assert_equals(getComputedStyle(inner_div).color, 'rgb(255, 0, 0)');
+
+ frame.style.width = '400px';
+ const animations = getAnimations(inner_div);
+ assert_equals(animations.length, 1);
+ assert_equals(getComputedStyle(inner_div).color, 'rgb(0, 128, 0)');
+ }, `Calling ${interfaceName}.getAnimations updates layout of parent frame if needed`);
+ }
+
+ iframeTest(element => element.getAnimations(), 'Element');
+ iframeTest(element => element.ownerDocument.getAnimations(), 'Document');
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/web-animations/interfaces/Animatable/getAnimations.html b/testing/web-platform/tests/web-animations/interfaces/Animatable/getAnimations.html
new file mode 100644
index 0000000000..fd8719299d
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/interfaces/Animatable/getAnimations.html
@@ -0,0 +1,355 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title>Animatable.getAnimations</title>
+<link rel="help" href="https://drafts.csswg.org/web-animations/#dom-animatable-getanimations">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../../testcommon.js"></script>
+<body>
+<script>
+'use strict';
+
+test(t => {
+ const div = createDiv(t);
+ assert_array_equals(div.getAnimations(), []);
+}, 'Returns an empty array for an element with no animations');
+
+test(t => {
+ const div = createDiv(t);
+ const animationA = div.animate(null, 100 * MS_PER_SEC);
+ const animationB = div.animate(null, 100 * MS_PER_SEC);
+ assert_array_equals(div.getAnimations(), [animationA, animationB]);
+}, 'Returns both animations for an element with two animations');
+
+test(t => {
+ const divA = createDiv(t);
+ const divB = createDiv(t);
+ const animationA = divA.animate(null, 100 * MS_PER_SEC);
+ const animationB = divB.animate(null, 100 * MS_PER_SEC);
+ assert_array_equals(divA.getAnimations(), [animationA], 'divA');
+ assert_array_equals(divB.getAnimations(), [animationB], 'divB');
+}, 'Returns only the animations specific to each sibling element');
+
+test(t => {
+ const divParent = createDiv(t);
+ const divChild = createDiv(t);
+ divParent.appendChild(divChild);
+ const animationParent = divParent.animate(null, 100 * MS_PER_SEC);
+ const animationChild = divChild.animate(null, 100 * MS_PER_SEC);
+ assert_array_equals(divParent.getAnimations(), [animationParent],
+ 'divParent');
+ assert_array_equals(divChild.getAnimations(), [animationChild], 'divChild');
+}, 'Returns only the animations specific to each parent/child element');
+
+test(t => {
+ const divParent = createDiv(t);
+ const divChild = createDiv(t);
+ divParent.appendChild(divChild);
+ const divGrandChildA = createDiv(t);
+ const divGrandChildB = createDiv(t);
+ divChild.appendChild(divGrandChildA);
+ divChild.appendChild(divGrandChildB);
+
+ // Trigger the animations in a somewhat random order
+ const animGrandChildB = divGrandChildB.animate(null, 100 * MS_PER_SEC);
+ const animChild = divChild.animate(null, 100 * MS_PER_SEC);
+ const animGrandChildA = divGrandChildA.animate(null, 100 * MS_PER_SEC);
+
+ assert_array_equals(
+ divParent.getAnimations({ subtree: true }),
+ [animGrandChildB, animChild, animGrandChildA],
+ 'Returns expected animations from parent'
+ );
+ assert_array_equals(
+ divChild.getAnimations({ subtree: true }),
+ [animGrandChildB, animChild, animGrandChildA],
+ 'Returns expected animations from child'
+ );
+ assert_array_equals(
+ divGrandChildA.getAnimations({ subtree: true }),
+ [animGrandChildA],
+ 'Returns expected animations from grandchild A'
+ );
+}, 'Returns animations on descendants when subtree: true is specified');
+
+test(t => {
+ createStyle(t, {
+ '@keyframes anim': '',
+ [`.pseudo::before`]: 'animation: anim 100s; ' + "content: '';",
+ });
+ const div = createDiv(t);
+ div.classList.add('pseudo');
+
+ assert_equals(
+ div.getAnimations().length,
+ 0,
+ 'Returns no animations when subtree is false'
+ );
+ assert_equals(
+ div.getAnimations({ subtree: true }).length,
+ 1,
+ 'Returns one animation when subtree is true'
+ );
+}, 'Returns animations on pseudo-elements when subtree: true is specified');
+
+test(t => {
+ const host = createDiv(t);
+ const shadow = host.attachShadow({ mode: 'open' });
+
+ const elem = createDiv(t);
+ shadow.appendChild(elem);
+
+ const elemChild = createDiv(t);
+ elem.appendChild(elemChild);
+
+ elemChild.animate(null, 100 * MS_PER_SEC);
+
+ assert_equals(
+ host.getAnimations({ subtree: true }).length,
+ 0,
+ 'Returns no animations with subtree:true when called on the host'
+ );
+ assert_equals(
+ elem.getAnimations({ subtree: true }).length,
+ 1,
+ 'Returns one animation when called on a parent in the shadow tree'
+ );
+}, 'Does NOT cross shadow-tree boundaries when subtree: true is specified');
+
+test(t => {
+ const foreignElement
+ = document.createElementNS('http://example.org/test', 'test');
+ document.body.appendChild(foreignElement);
+ t.add_cleanup(() => {
+ foreignElement.remove();
+ });
+
+ const animation = foreignElement.animate(null, 100 * MS_PER_SEC);
+ assert_array_equals(foreignElement.getAnimations(), [animation]);
+}, 'Returns animations for a foreign element');
+
+test(t => {
+ const div = createDiv(t);
+ const animation = div.animate(null, 100 * MS_PER_SEC);
+ animation.finish();
+ assert_array_equals(div.getAnimations(), []);
+}, 'Does not return finished animations that do not fill forwards');
+
+test(t => {
+ const div = createDiv(t);
+ const animation = div.animate(null, {
+ duration: 100 * MS_PER_SEC,
+ fill: 'forwards',
+ });
+ animation.finish();
+ assert_array_equals(div.getAnimations(), [animation]);
+}, 'Returns finished animations that fill forwards');
+
+test(t => {
+ const div = createDiv(t);
+ const animation = div.animate(null, {
+ duration: 100 * MS_PER_SEC,
+ delay: 100 * MS_PER_SEC,
+ });
+ assert_array_equals(div.getAnimations(), [animation]);
+}, 'Returns animations yet to reach their active phase');
+
+test(t => {
+ const div = createDiv(t);
+ const animation = div.animate(null, 100 * MS_PER_SEC);
+ animation.playbackRate = -1;
+ assert_array_equals(div.getAnimations(), []);
+}, 'Does not return reversed finished animations that do not fill backwards');
+
+test(t => {
+ const div = createDiv(t);
+ const animation = div.animate(null, {
+ duration: 100 * MS_PER_SEC,
+ fill: 'backwards',
+ });
+ animation.playbackRate = -1;
+ assert_array_equals(div.getAnimations(), [animation]);
+}, 'Returns reversed finished animations that fill backwards');
+
+test(t => {
+ const div = createDiv(t);
+ const animation = div.animate(null, 100 * MS_PER_SEC);
+ animation.playbackRate = -1;
+ animation.currentTime = 200 * MS_PER_SEC;
+ assert_array_equals(div.getAnimations(), [animation]);
+}, 'Returns reversed animations yet to reach their active phase');
+
+test(t => {
+ const div = createDiv(t);
+ const animation = div.animate(null, {
+ duration: 100 * MS_PER_SEC,
+ delay: 100 * MS_PER_SEC,
+ });
+ animation.playbackRate = 0;
+ assert_array_equals(div.getAnimations(), []);
+}, 'Does not return animations with zero playback rate in before phase');
+
+test(t => {
+ const div = createDiv(t);
+ const animation = div.animate(null, 100 * MS_PER_SEC);
+ animation.finish();
+ animation.playbackRate = 0;
+ animation.currentTime = 200 * MS_PER_SEC;
+ assert_array_equals(div.getAnimations(), []);
+}, 'Does not return animations with zero playback rate in after phase');
+
+test(t => {
+ const div = createDiv(t);
+ const effect = new KeyframeEffect(div, {}, 225);
+ const animation = new Animation(effect, new DocumentTimeline());
+ animation.reverse();
+ animation.pause();
+ animation.playbackRate = -1;;
+ animation.updatePlaybackRate(1);
+ assert_array_equals(div.getAnimations(), []);
+}, 'Does not return an animation that has recently been made not current by setting the playback rate');
+
+test(t => {
+ const div = createDiv(t);
+ const animation = div.animate(null, 100 * MS_PER_SEC);
+
+ animation.finish();
+ assert_array_equals(div.getAnimations(), [],
+ 'Animation should not be returned when it is finished');
+
+ animation.effect.updateTiming({
+ duration: animation.effect.getTiming().duration + 100 * MS_PER_SEC,
+ });
+ assert_array_equals(div.getAnimations(), [animation],
+ 'Animation should be returned after extending the'
+ + ' duration');
+
+ animation.effect.updateTiming({ duration: 0 });
+ assert_array_equals(div.getAnimations(), [],
+ 'Animation should not be returned after setting the'
+ + ' duration to zero');
+}, 'Returns animations based on dynamic changes to individual'
+ + ' animations\' duration');
+
+test(t => {
+ const div = createDiv(t);
+ const animation = div.animate(null, 100 * MS_PER_SEC);
+
+ animation.effect.updateTiming({ endDelay: -200 * MS_PER_SEC });
+ assert_array_equals(div.getAnimations(), [],
+ 'Animation should not be returned after setting a'
+ + ' negative end delay such that the end time is less'
+ + ' than the current time');
+
+ animation.effect.updateTiming({ endDelay: 100 * MS_PER_SEC });
+ assert_array_equals(div.getAnimations(), [animation],
+ 'Animation should be returned after setting a positive'
+ + ' end delay such that the end time is more than the'
+ + ' current time');
+}, 'Returns animations based on dynamic changes to individual'
+ + ' animations\' end delay');
+
+test(t => {
+ const div = createDiv(t);
+ const animation = div.animate(null, 100 * MS_PER_SEC);
+
+ animation.finish();
+ assert_array_equals(div.getAnimations(), [],
+ 'Animation should not be returned when it is finished');
+
+ animation.effect.updateTiming({ iterations: 10 });
+ assert_array_equals(div.getAnimations(), [animation],
+ 'Animation should be returned after inreasing the'
+ + ' number of iterations');
+
+ animation.effect.updateTiming({ iterations: 0 });
+ assert_array_equals(div.getAnimations(), [],
+ 'Animations should not be returned after setting the'
+ + ' iteration count to zero');
+
+ animation.effect.updateTiming({ iterations: Infinity });
+ assert_array_equals(div.getAnimations(), [animation],
+ 'Animation should be returned after inreasing the'
+ + ' number of iterations to infinity');
+}, 'Returns animations based on dynamic changes to individual'
+ + ' animations\' iteration count');
+
+test(t => {
+ const div = createDiv(t);
+ const animation = div.animate(null,
+ { duration: 100 * MS_PER_SEC,
+ delay: 50 * MS_PER_SEC,
+ endDelay: -50 * MS_PER_SEC });
+
+ assert_array_equals(div.getAnimations(), [animation],
+ 'Animation should be returned at during delay phase');
+
+ animation.currentTime = 50 * MS_PER_SEC;
+ assert_array_equals(div.getAnimations(), [animation],
+ 'Animation should be returned after seeking to the start'
+ + ' of the active interval');
+
+ animation.currentTime = 100 * MS_PER_SEC;
+ assert_array_equals(div.getAnimations(), [],
+ 'Animation should not be returned after seeking to the'
+ + ' clipped end of the active interval');
+}, 'Returns animations based on dynamic changes to individual'
+ + ' animations\' current time');
+
+promise_test(async t => {
+ const div = createDiv(t);
+
+ const animA = div.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' });
+ const animB = div.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' });
+ await animA.finished;
+ // It is not guaranteed that the mircrotask PerformCheckpoint() happens before
+ // the animation finish promised got resolved, because the microtask
+ // checkpoint could also be triggered from other source such as the event_loop
+ // Thus we wait for one animation frame to make sure the finished animation is
+ // properly removed.
+ await waitForNextFrame(1);
+ assert_array_equals(div.getAnimations(), [animB]);
+}, 'Does not return an animation that has been removed');
+
+promise_test(async t => {
+ const div = createDiv(t);
+
+ const animA = div.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' });
+ const animB = div.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' });
+ await animA.finished;
+
+ animA.persist();
+
+ assert_array_equals(div.getAnimations(), [animA, animB]);
+}, 'Returns an animation that has been persisted');
+
+promise_test(async t => {
+ const div = createDiv(t);
+ const watcher = EventWatcher(t, div, 'transitionrun');
+
+ // Create a covering animation to prevent transitions from firing after
+ // calling getAnimations().
+ const coveringAnimation = new Animation(
+ new KeyframeEffect(div, { opacity: [0, 1] }, 100 * MS_PER_SEC)
+ );
+
+ // Setup transition start point.
+ div.style.transition = 'opacity 100s';
+ getComputedStyle(div).opacity;
+
+ // Update specified style but don't flush style.
+ div.style.opacity = '0.5';
+
+ // Fetch animations
+ div.getAnimations();
+
+ // Play the covering animation to ensure that only the call to
+ // getAnimations() has a chance to trigger transitions.
+ coveringAnimation.play();
+
+ // If getAnimations() flushed style, we should get a transitionrun event.
+ await watcher.wait_for('transitionrun');
+}, 'Triggers a style change event');
+
+</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 0000000000..a7da9755dd
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/interfaces/Animation/cancel.html
@@ -0,0 +1,133 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title>Animation.cancel</title>
+<link rel="help" href="https://drafts.csswg.org/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(t => {
+ const div = createDiv(t);
+ const animation = div.animate(
+ { transform: ['translate(100px)', 'translate(100px)'] },
+ 100 * MS_PER_SEC
+ );
+ return animation.ready.then(() => {
+ 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(t => {
+ const div = createDiv(t);
+ const animation = div.animate({ marginLeft: ['100px', '200px'] },
+ 100 * MS_PER_SEC);
+ animation.effect.updateTiming({ 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(t => {
+ const div = createDiv(t);
+ const animation = div.animate({ marginLeft:['100px', '200px'] },
+ 100 * MS_PER_SEC);
+ return animation.ready.then(() => {
+ 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(() => {
+ assert_equals(animation.playState, 'running',
+ 'Animation succeeds in running after being re-started');
+ });
+}, 'After cancelling an animation, it can still be re-used');
+
+promise_test(async t => {
+ for (const type of ["resolve", "reject"]) {
+ const anim = new Animation();
+
+ let isThenGet = false;
+ let isThenCalled = false;
+ let resolveFinished;
+ let rejectFinished;
+ const thenCalledPromise = new Promise(resolveThenCalledPromise => {
+ // Make `anim` thenable.
+ Object.defineProperty(anim, "then", {
+ get() {
+ isThenGet = true;
+ return function(resolve, reject) {
+ isThenCalled = true;
+ resolveThenCalledPromise(true);
+ resolveFinished = resolve;
+ rejectFinished = reject;
+ };
+ },
+ });
+ });
+
+ // Lazily create finished promise.
+ const finishedPromise = anim.finished;
+
+ assert_false(isThenGet, "then property shouldn't be accessed yet");
+
+ // Resolve finished promise with `anim`, that gets `then`, and
+ // calls in the thenable job.
+ anim.finish();
+
+ assert_true(isThenGet, "then property should be accessed");
+ assert_false(isThenCalled, "then property shouldn't be called yet");
+
+ // Reject finished promise.
+ // This should be ignored.
+ anim.cancel();
+
+ // Wait for the thenable job.
+ await thenCalledPromise;
+
+ assert_true(isThenCalled, "then property should be called");
+
+ const dummyPromise = new Promise(resolve => {
+ step_timeout(() => {
+ resolve("dummy");
+ }, 100);
+ });
+ const dummy = await Promise.race([finishedPromise, dummyPromise]);
+ assert_equals(dummy, "dummy", "finishedPromise shouldn't be settled yet");
+
+ if (type === "resolve") {
+ resolveFinished("hello");
+ const finished = await finishedPromise;
+ assert_equals(finished, "hello",
+ "finishedPromise should be resolved with given value");
+ } else {
+ rejectFinished("hello");
+ try {
+ await finishedPromise;
+ assert_unreached("finishedPromise should be rejected")
+ } catch (e) {
+ assert_equals(e, "hello",
+ "finishedPromise should be rejected with given value");
+ }
+ }
+ }
+}, "Animation.finished promise should not be rejected by cancel method once "
+ + "it is resolved with inside finish method");
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/web-animations/interfaces/Animation/commitStyles-crash.html b/testing/web-platform/tests/web-animations/interfaces/Animation/commitStyles-crash.html
new file mode 100644
index 0000000000..063fe5a4eb
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/interfaces/Animation/commitStyles-crash.html
@@ -0,0 +1,26 @@
+<!doctype html>
+<link rel=help href="https://bugzilla.mozilla.org/show_bug.cgi?id=1741491">
+<script>
+ class CustomElement0 extends HTMLElement {
+ constructor () {
+ super()
+ }
+
+ static get observedAttributes () { return ["style"] }
+
+ async attributeChangedCallback () {
+ const animation = this.animate([{
+ "boxShadow": "none",
+ "visibility": "collapse"
+ }], 1957)
+ animation.commitStyles()
+ }
+ }
+
+ customElements.define("custom-element-0", CustomElement0)
+ window.addEventListener("load", () => {
+ const custom = document.createElement("custom-element-0")
+ document.documentElement.appendChild(custom)
+ custom.style.fontFamily = "family_name_0"
+ })
+</script>
diff --git a/testing/web-platform/tests/web-animations/interfaces/Animation/commitStyles-svg-crash.html b/testing/web-platform/tests/web-animations/interfaces/Animation/commitStyles-svg-crash.html
new file mode 100644
index 0000000000..7fc1fef9ce
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/interfaces/Animation/commitStyles-svg-crash.html
@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<html class=test-wait>
+<link rel=help href="https://crbug.com/1385691">
+<svg id=svg></svg>
+<script>
+ let anim = svg.animate({'svg-viewBox': '1 1 1 1'}, 1);
+ anim.ready.then(() => {
+ anim.commitStyles();
+ document.documentElement.classList.remove('test-wait');
+ });
+</script>
+</html>
diff --git a/testing/web-platform/tests/web-animations/interfaces/Animation/commitStyles.html b/testing/web-platform/tests/web-animations/interfaces/Animation/commitStyles.html
new file mode 100644
index 0000000000..9a7dbea8b8
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/interfaces/Animation/commitStyles.html
@@ -0,0 +1,577 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>Animation.commitStyles</title>
+<link rel="help" href="https://drafts.csswg.org/web-animations/#dom-animation-commitstyles">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../../testcommon.js"></script>
+<style>
+.pseudo::before {content: '';}
+.pseudo::after {content: '';}
+.pseudo::marker {content: '';}
+</style>
+<body>
+<div id="log"></div>
+<script>
+'use strict';
+
+function assert_numeric_style_equals(opacity, expected, description) {
+ return assert_approx_equals(
+ parseFloat(opacity),
+ expected,
+ 0.0001,
+ description
+ );
+}
+
+test(t => {
+ const div = createDiv(t);
+ div.style.opacity = '0.1';
+
+ const animation = div.animate(
+ { opacity: 0.2 },
+ { duration: 1, fill: 'forwards' }
+ );
+ animation.finish();
+
+ animation.commitStyles();
+
+ // Cancel the animation so we can inspect the underlying style
+ animation.cancel();
+
+ assert_numeric_style_equals(getComputedStyle(div).opacity, 0.2);
+}, 'Commits styles');
+
+test(t => {
+ const div = createDiv(t);
+ div.style.translate = '100px';
+ div.style.rotate = '45deg';
+ div.style.scale = '2';
+
+ const animation = div.animate(
+ { translate: '200px',
+ rotate: '90deg',
+ scale: 3 },
+ { duration: 1, fill: 'forwards' }
+ );
+ animation.finish();
+
+ animation.commitStyles();
+
+ // Cancel the animation so we can inspect the underlying style
+ animation.cancel();
+
+ assert_equals(getComputedStyle(div).translate, '200px');
+ assert_equals(getComputedStyle(div).rotate, '90deg');
+ assert_numeric_style_equals(getComputedStyle(div).scale, 3);
+}, 'Commits styles for individual transform properties');
+
+promise_test(async t => {
+ const div = createDiv(t);
+ div.style.opacity = '0.1';
+
+ const animA = div.animate(
+ { opacity: 0.2 },
+ { duration: 1, fill: 'forwards' }
+ );
+ const animB = div.animate(
+ { opacity: 0.3 },
+ { duration: 1, fill: 'forwards' }
+ );
+
+ await animA.finished;
+
+ animB.cancel();
+
+ animA.commitStyles();
+
+ assert_numeric_style_equals(getComputedStyle(div).opacity, 0.2);
+}, 'Commits styles for an animation that has been removed');
+
+test(t => {
+ const div = createDiv(t);
+ div.style.margin = '10px';
+
+ const animation = div.animate(
+ { margin: '20px' },
+ { duration: 1, fill: 'forwards' }
+ );
+ animation.finish();
+
+ animation.commitStyles();
+
+ animation.cancel();
+
+ assert_equals(div.style.marginLeft, '20px');
+}, 'Commits shorthand styles');
+
+test(t => {
+ const div = createDiv(t);
+ div.style.marginLeft = '10px';
+
+ const animation = div.animate(
+ { marginInlineStart: '20px' },
+ { duration: 1, fill: 'forwards' }
+ );
+ animation.finish();
+
+ animation.commitStyles();
+
+ animation.cancel();
+
+ assert_equals(getComputedStyle(div).marginLeft, '20px');
+}, 'Commits logical properties');
+
+test(t => {
+ const div = createDiv(t);
+ div.style.marginLeft = '10px';
+
+ const animation = div.animate(
+ { marginInlineStart: '20px' },
+ { duration: 1, fill: 'forwards' }
+ );
+ animation.finish();
+
+ animation.commitStyles();
+
+ animation.cancel();
+
+ assert_equals(div.style.marginLeft, '20px');
+}, 'Commits logical properties as physical properties');
+
+test(t => {
+ const div = createDiv(t);
+ div.style.marginLeft = '10px';
+
+ const animation = div.animate({ opacity: [0.2, 0.7] }, 1000);
+ animation.currentTime = 500;
+ animation.commitStyles();
+ animation.cancel();
+
+ assert_numeric_style_equals(getComputedStyle(div).opacity, 0.45);
+}, 'Commits values calculated mid-interval');
+
+test(t => {
+ const div = createDiv(t);
+ div.style.setProperty('--target', '0.5');
+
+ const animation = div.animate(
+ { opacity: 'var(--target)' },
+ { duration: 1, fill: 'forwards' }
+ );
+ animation.finish();
+ animation.commitStyles();
+ animation.cancel();
+
+ assert_numeric_style_equals(getComputedStyle(div).opacity, 0.5);
+
+ // Changes to the variable should have no effect
+ div.style.setProperty('--target', '1');
+
+ assert_numeric_style_equals(getComputedStyle(div).opacity, 0.5);
+}, 'Commits variable references as their computed values');
+
+
+test(t => {
+ const div = createDiv(t);
+ div.style.setProperty('--target', '0.5');
+ div.style.opacity = 'var(--target)';
+ const animation = div.animate(
+ { '--target': 0.8 },
+ { duration: 1, fill: 'forwards' }
+ );
+ animation.finish();
+ animation.commitStyles();
+ animation.cancel();
+
+ assert_numeric_style_equals(getComputedStyle(div).opacity, 0.8);
+}, 'Commits custom variables');
+
+test(t => {
+ const div = createDiv(t);
+ div.style.fontSize = '10px';
+
+ const animation = div.animate(
+ { width: '10em' },
+ { duration: 1, fill: 'forwards' }
+ );
+ animation.finish();
+ animation.commitStyles();
+ animation.cancel();
+
+ assert_numeric_style_equals(getComputedStyle(div).width, 100);
+
+ div.style.fontSize = '20px';
+ assert_numeric_style_equals(getComputedStyle(div).width, 100,
+ "Changes to the font-size should have no effect");
+}, 'Commits em units as pixel values');
+
+test(t => {
+ const div = createDiv(t);
+ div.style.fontSize = '10px';
+
+ const animation = div.animate(
+ { lineHeight: '1.5' },
+ { duration: 1, fill: 'forwards' }
+ );
+ animation.finish();
+ animation.commitStyles();
+ animation.cancel();
+
+ assert_numeric_style_equals(getComputedStyle(div).lineHeight, 15);
+ assert_equals(div.style.lineHeight, "1.5", "line-height is committed as a relative value");
+
+ div.style.fontSize = '20px';
+ assert_numeric_style_equals(getComputedStyle(div).lineHeight, 30,
+ "Changes to the font-size should affect the committed line-height");
+
+}, 'Commits relative line-height');
+
+test(t => {
+ const div = createDiv(t);
+ const animation = div.animate(
+ { transform: 'translate(20px, 20px)' },
+ { duration: 1, fill: 'forwards' }
+ );
+ animation.finish();
+ animation.commitStyles();
+ animation.cancel();
+ assert_equals(getComputedStyle(div).transform, 'matrix(1, 0, 0, 1, 20, 20)');
+}, 'Commits transforms');
+
+test(t => {
+ const div = createDiv(t);
+ const animation = div.animate(
+ { transform: 'translate(20px, 20px)' },
+ { duration: 1, fill: 'forwards' }
+ );
+ animation.finish();
+ animation.commitStyles();
+ animation.cancel();
+ assert_equals(div.style.transform, 'translate(20px, 20px)');
+}, 'Commits transforms as a transform list');
+
+test(t => {
+ const div = createDiv(t);
+ div.style.width = '200px';
+ div.style.height = '200px';
+
+ const animation = div.animate({ transform: ["translate(100%, 0%)", "scale(3)"] }, 1000);
+ animation.currentTime = 500;
+ animation.commitStyles();
+ animation.cancel();
+
+ // TODO(https://github.com/w3c/csswg-drafts/issues/2854):
+ // We can't check the committed value directly since it is not specced yet in this case,
+ // but it should still produce the correct resolved value.
+ assert_equals(getComputedStyle(div).transform, "matrix(2, 0, 0, 2, 100, 0)",
+ "Resolved transform is correct after commit.");
+}, 'Commits matrix-interpolated relative transforms');
+
+test(t => {
+ const div = createDiv(t);
+ div.style.width = '200px';
+ div.style.height = '200px';
+
+ const animation = div.animate({ transform: ["none", "none"] }, 1000);
+ animation.currentTime = 500;
+ animation.commitStyles();
+ animation.cancel();
+
+ assert_equals(div.style.transform, "none",
+ "Resolved transform is correct after commit.");
+}, 'Commits "none" transform');
+
+promise_test(async t => {
+ const div = createDiv(t);
+ div.style.opacity = '0.1';
+
+ const animA = div.animate(
+ { opacity: '0.2' },
+ { duration: 1, fill: 'forwards' }
+ );
+ const animB = div.animate(
+ { opacity: '0.2', composite: 'add' },
+ { duration: 1, fill: 'forwards' }
+ );
+ const animC = div.animate(
+ { opacity: '0.3', composite: 'add' },
+ { duration: 1, fill: 'forwards' }
+ );
+
+ animA.persist();
+ animB.persist();
+
+ await animB.finished;
+
+ // The values above have been chosen such that various error conditions
+ // produce results that all differ from the desired result:
+ //
+ // Expected result:
+ //
+ // animA + animB = 0.4
+ //
+ // Likely error results:
+ //
+ // <underlying> = 0.1
+ // (Commit didn't work at all)
+ //
+ // animB = 0.2
+ // (Didn't add at all when resolving)
+ //
+ // <underlying> + animB = 0.3
+ // (Added to the underlying value instead of lower-priority animations when
+ // resolving)
+ //
+ // <underlying> + animA + animB = 0.5
+ // (Didn't respect the composite mode of lower-priority animations)
+ //
+ // animA + animB + animC = 0.7
+ // (Resolved the whole stack, not just up to the target effect)
+ //
+
+ animB.commitStyles();
+
+ animA.cancel();
+ animB.cancel();
+ animC.cancel();
+
+ assert_numeric_style_equals(getComputedStyle(div).opacity, 0.4);
+}, 'Commits the intermediate value of an animation in the middle of stack');
+
+promise_test(async t => {
+ const div = createDiv(t);
+ div.style.opacity = '0.1';
+
+ const animA = div.animate(
+ { opacity: '0.2', composite: 'add' },
+ { duration: 1, fill: 'forwards' }
+ );
+ const animB = div.animate(
+ { opacity: '0.2', composite: 'add' },
+ { duration: 1, fill: 'forwards' }
+ );
+ const animC = div.animate(
+ { opacity: '0.3', composite: 'add' },
+ { duration: 1, fill: 'forwards' }
+ );
+
+ animA.persist();
+ animB.persist();
+ await animB.finished;
+
+ // The error cases are similar to the above test with one additional case;
+ // verifying that the animations composite on top of the correct underlying
+ // base style.
+ //
+ // Expected result:
+ //
+ // <underlying> + animA + animB = 0.5
+ //
+ // Additional error results:
+ //
+ // <underlying> + animA + animB + animC + animA + animB = 1.0 (saturates)
+ // (Added to the computed value instead of underlying value when
+ // resolving)
+ //
+ // animA + animB = 0.4
+ // Failed to composite on top of underlying value.
+ //
+
+ animB.commitStyles();
+
+ animA.cancel();
+ animB.cancel();
+ animC.cancel();
+
+ assert_numeric_style_equals(getComputedStyle(div).opacity, 0.5);
+}, 'Commit composites on top of the underlying value');
+
+promise_test(async t => {
+ const div = createDiv(t);
+ div.style.opacity = '0.1';
+
+ // Setup animation
+ const animation = div.animate(
+ { opacity: 0.2 },
+ { duration: 1, fill: 'forwards' }
+ );
+ animation.finish();
+
+ // Setup observer
+ const mutationRecords = [];
+ const observer = new MutationObserver(mutations => {
+ mutationRecords.push(...mutations);
+ });
+ observer.observe(div, { attributes: true, attributeOldValue: true });
+
+ animation.commitStyles();
+
+ // Wait for mutation records to be dispatched
+ await Promise.resolve();
+
+ assert_equals(mutationRecords.length, 1, 'Should have one mutation record');
+
+ const mutation = mutationRecords[0];
+ assert_equals(mutation.type, 'attributes');
+ assert_equals(mutation.oldValue, 'opacity: 0.1;');
+
+ observer.disconnect();
+}, 'Triggers mutation observers when updating style');
+
+promise_test(async t => {
+ const div = createDiv(t);
+ div.style.opacity = '0.2';
+
+ // Setup animation
+ const animation = div.animate(
+ { opacity: 0.2 },
+ { duration: 1, fill: 'forwards' }
+ );
+ animation.finish();
+
+ // Setup observer
+ const mutationRecords = [];
+ const observer = new MutationObserver(mutations => {
+ mutationRecords.push(...mutations);
+ });
+ observer.observe(div, { attributes: true });
+
+ animation.commitStyles();
+
+ // Wait for mutation records to be dispatched
+ await Promise.resolve();
+
+ assert_equals(mutationRecords.length, 0, 'Should have no mutation records');
+
+ observer.disconnect();
+}, 'Does NOT trigger mutation observers when the change to style is redundant');
+
+test(t => {
+
+ const div = createDiv(t);
+ div.classList.add('pseudo');
+ const animation = div.animate(
+ { opacity: 0 },
+ { duration: 1, fill: 'forwards', pseudoElement: '::before' }
+ );
+
+ assert_throws_dom('NoModificationAllowedError', () => {
+ animation.commitStyles();
+ });
+}, 'Throws if the target element is a pseudo element');
+
+test(t => {
+ const animation = createDiv(t).animate(
+ { opacity: 0 },
+ { duration: 1, fill: 'forwards' }
+ );
+
+ const nonStyleElement
+ = document.createElementNS('http://example.org/test', 'test');
+ document.body.appendChild(nonStyleElement);
+ animation.effect.target = nonStyleElement;
+
+ assert_throws_dom('NoModificationAllowedError', () => {
+ animation.commitStyles();
+ });
+
+ nonStyleElement.remove();
+}, 'Throws if the target element is not something with a style attribute');
+
+test(t => {
+ const div = createDiv(t);
+ const animation = div.animate(
+ { opacity: 0 },
+ { duration: 1, fill: 'forwards' }
+ );
+
+ div.style.display = 'none';
+
+ assert_throws_dom('InvalidStateError', () => {
+ animation.commitStyles();
+ });
+}, 'Throws if the target effect is display:none');
+
+test(t => {
+ const container = createDiv(t);
+ const div = createDiv(t);
+ container.append(div);
+
+ const animation = div.animate(
+ { opacity: 0 },
+ { duration: 1, fill: 'forwards' }
+ );
+
+ container.style.display = 'none';
+
+ assert_throws_dom('InvalidStateError', () => {
+ animation.commitStyles();
+ });
+}, "Throws if the target effect's ancestor is display:none");
+
+test(t => {
+ const container = createDiv(t);
+ const div = createDiv(t);
+ container.append(div);
+
+ const animation = div.animate(
+ { opacity: 0 },
+ { duration: 1, fill: 'forwards' }
+ );
+
+ container.style.display = 'contents';
+
+ // Should NOT throw
+ animation.commitStyles();
+}, 'Treats display:contents as rendered');
+
+test(t => {
+ const container = createDiv(t);
+ const div = createDiv(t);
+ container.append(div);
+
+ const animation = div.animate(
+ { opacity: 0 },
+ { duration: 1, fill: 'forwards' }
+ );
+
+ div.style.display = 'contents';
+ container.style.display = 'none';
+
+ assert_throws_dom('InvalidStateError', () => {
+ animation.commitStyles();
+ });
+}, 'Treats display:contents in a display:none subtree as not rendered');
+
+test(t => {
+ const div = createDiv(t);
+ const animation = div.animate(
+ { opacity: 0 },
+ { duration: 1, fill: 'forwards' }
+ );
+
+ div.remove();
+
+ assert_throws_dom('InvalidStateError', () => {
+ animation.commitStyles();
+ });
+}, 'Throws if the target effect is disconnected');
+
+test(t => {
+ const div = createDiv(t);
+ div.classList.add('pseudo');
+ const animation = div.animate(
+ { opacity: 0 },
+ { duration: 1, fill: 'forwards', pseudoElement: '::before' }
+ );
+
+ div.remove();
+
+ assert_throws_dom('NoModificationAllowedError', () => {
+ animation.commitStyles();
+ });
+}, 'Checks the pseudo element condition before the not rendered condition');
+
+</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 0000000000..e964c10ee4
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/interfaces/Animation/constructor.html
@@ -0,0 +1,124 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title>Animation constructor</title>
+<link rel="help" href="https://drafts.csswg.org/web-animations/#dom-animation-animation">
+<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';
+
+const gTarget = document.getElementById('target');
+
+function createEffect() {
+ return new KeyframeEffect(gTarget, { opacity: [0, 1] });
+}
+
+function createNull() {
+ return null;
+}
+
+const 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'
+ },
+];
+
+for (const args of gTestArguments) {
+ test(t => {
+ const effect = args.createEffect();
+ const 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(t => {
+ const effect = new KeyframeEffect(null,
+ { left: ['10px', '20px'] },
+ { duration: 10000, fill: 'forwards' });
+ const 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(t => {
+ const iframe = document.createElement('iframe');
+
+ iframe.addEventListener('load', t.step_func(() => {
+ const div = createDiv(t, iframe.contentDocument);
+ const effect = new KeyframeEffect(div, null, 10000);
+ const 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');
+
+test(t => {
+ const div = createDiv(t);
+ const effect = new KeyframeEffect(div, { opacity: [0, 1]}, 10000);
+ const anim1 = new Animation(effect);
+ anim1.play();
+ const anim2 = new Animation(effect);
+ anim2.play();
+ assert_equals(anim1.effect, null, 'Effect is reset on first animation');
+ assert_equals(anim1.playState, 'finished',
+ 'Resetting the affect does not cancel the animation');
+}, 'Animation created with an affect associated with another animation.');
+</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 0000000000..cb8bc09c36
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/interfaces/Animation/effect.html
@@ -0,0 +1,42 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title>Animation.effect</title>
+<link rel="help" href="https://drafts.csswg.org/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(t => {
+ const anim = new Animation();
+ assert_equals(anim.effect, null, 'initial effect is null');
+
+ const newEffect = new KeyframeEffect(createDiv(t), null);
+ anim.effect = newEffect;
+ assert_equals(anim.effect, newEffect, 'new effect is set');
+}, 'effect is set correctly.');
+
+test(t => {
+ const div = createDiv(t);
+ const animation = div.animate({ left: ['100px', '100px'] },
+ { fill: 'forwards' });
+ const effect = animation.effect;
+
+ assert_equals(getComputedStyle(div).left, '100px',
+ 'animation is initially having an effect');
+
+ animation.effect = null;
+ assert_equals(getComputedStyle(div).left, 'auto',
+ 'animation no longer has an effect');
+
+ animation.effect = effect;
+ assert_equals(getComputedStyle(div).left, '100px',
+ 'animation has an effect again');
+}, 'Clearing and setting Animation.effect should update the computed style'
+ + ' of the target element');
+
+</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 0000000000..bee4fd8fb7
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/interfaces/Animation/finished.html
@@ -0,0 +1,416 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title>Animation.finished</title>
+<link rel="help" href="https://drafts.csswg.org/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(t => {
+ const div = createDiv(t);
+ const animation = div.animate({}, 100 * MS_PER_SEC);
+ const previousFinishedPromise = animation.finished;
+ return animation.ready.then(() => {
+ 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(() => {
+ 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(t => {
+ const div = createDiv(t);
+ const animation = div.animate({}, 100 * MS_PER_SEC);
+ let previousFinishedPromise = animation.finished;
+ animation.finish();
+ return animation.finished.then(() => {
+ 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(t => {
+ const div = createDiv(t);
+ const animation = div.animate({}, 100 * MS_PER_SEC);
+ let previousFinishedPromise;
+ animation.finish();
+ return animation.finished.then(() => {
+ 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(() => {
+ 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(t => {
+ const div = createDiv(t);
+ const animation = div.animate({}, 100 * MS_PER_SEC);
+ const previousFinishedPromise = animation.finished;
+ animation.finish();
+ return animation.finished.then(() => {
+ 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(t => {
+ const div = createDiv(t);
+ const animation = div.animate({}, 100 * MS_PER_SEC);
+ // Setup callback to run if finished promise is resolved
+ let finishPromiseResolved = false;
+ animation.finished.then(() => {
+ finishPromiseResolved = true;
+ });
+ return animation.ready.then(() => {
+ // Jump to mid-way in interval and pause
+ animation.currentTime = 100 * MS_PER_SEC / 2;
+ animation.pause();
+ return animation.ready;
+ }).then(() => {
+ // 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(() => {
+ assert_false(finishPromiseResolved,
+ 'Finished promise should not resolve when paused');
+ });
+}, 'Finished promise does not resolve when paused');
+
+promise_test(t => {
+ const div = createDiv(t);
+ const animation = div.animate({}, 100 * MS_PER_SEC);
+ // Setup callback to run if finished promise is resolved
+ let finishPromiseResolved = false;
+ animation.finished.then(() => {
+ finishPromiseResolved = true;
+ });
+ return animation.ready.then(() => {
+ // 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(() => {
+ assert_false(finishPromiseResolved,
+ 'Finished promise should not resolve when pause-pending');
+ });
+}, 'Finished promise does not resolve when pause-pending');
+
+promise_test(t => {
+ const div = createDiv(t);
+ const animation = div.animate({}, 100 * MS_PER_SEC);
+ animation.finish();
+ return animation.finished.then(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(t => {
+ const div = createDiv(t);
+ const animation = div.animate({}, 100 * MS_PER_SEC);
+ const previousFinishedPromise = animation.finished;
+
+ // Set up listeners on finished promise
+ const retPromise = animation.finished.then(() => {
+ assert_unreached('finished promise was fulfilled');
+ }).catch(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 canceled by calling ' +
+ 'cancel()');
+
+promise_test(t => {
+ const div = createDiv(t);
+ const animation = div.animate({}, 100 * MS_PER_SEC);
+ const previousFinishedPromise = animation.finished;
+ animation.finish();
+ return animation.finished.then(() => {
+ animation.cancel();
+ assert_not_equals(animation.finished, previousFinishedPromise,
+ 'A new finished promise should be created when'
+ + ' canceling a finished animation');
+ });
+}, 'canceling an already-finished animation replaces the finished promise');
+
+promise_test(t => {
+ const div = createDiv(t);
+ const animation = div.animate({}, 100 * MS_PER_SEC);
+ const HALF_DUR = 100 * MS_PER_SEC / 2;
+ const QUARTER_DUR = 100 * MS_PER_SEC / 4;
+ let gotNextFrame = false;
+ let currentTimeBeforeShortening;
+ animation.currentTime = HALF_DUR;
+ return animation.ready.then(() => {
+ currentTimeBeforeShortening = animation.currentTime;
+ animation.effect.updateTiming({ 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(() => {
+ gotNextFrame = true;
+ });
+
+ return animation.finished;
+ }).then(() => {
+ 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');
+ const previousFinishedPromise = animation.finished;
+ animation.effect.updateTiming({ 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(t => {
+ const div = createDiv(t);
+ const animation = div.animate({}, 100 * MS_PER_SEC);
+ const retPromise = animation.ready.then(() => {
+ animation.playbackRate = 0;
+ animation.currentTime = 100 * MS_PER_SEC + 1000;
+ return waitForAnimationFrames(2);
+ });
+
+ animation.finished.then(t.step_func(() => {
+ assert_unreached('finished promise should not resolve when playbackRate ' +
+ 'is zero');
+ }));
+
+ return retPromise;
+}, 'Test finished promise changes when playbackRate == 0');
+
+promise_test(t => {
+ const div = createDiv(t);
+ const animation = div.animate({}, 100 * MS_PER_SEC);
+ return animation.ready.then(() => {
+ animation.playbackRate = -1;
+ return animation.finished;
+ });
+}, 'Test finished promise resolves when reaching to the natural boundary.');
+
+promise_test(t => {
+ const div = createDiv(t);
+ const animation = div.animate({}, 100 * MS_PER_SEC);
+ const previousFinishedPromise = animation.finished;
+ animation.finish();
+ return animation.finished.then(() => {
+ 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(t => {
+ const div = createDiv(t);
+ const animation = div.animate({}, 100 * MS_PER_SEC);
+ const 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(t => {
+ const div = createDiv(t);
+ const animation = div.animate({}, 100 * MS_PER_SEC);
+ const 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(t => {
+ const div = createDiv(t);
+ const animation = div.animate({}, 100 * MS_PER_SEC);
+ let resolvedFinished = false;
+ animation.finished.then(() => {
+ resolvedFinished = true;
+ });
+ return animation.ready.then(() => {
+ animation.finish();
+ animation.currentTime = 100 * MS_PER_SEC / 2;
+ }).then(() => {
+ 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(t => {
+ const div = createDiv(t);
+ const animation = div.animate({}, 100 * MS_PER_SEC);
+ let resolvedFinished = false;
+ animation.finished.then(() => {
+ resolvedFinished = true;
+ });
+
+ return animation.ready.then(() => {
+ animation.currentTime = 100 * MS_PER_SEC;
+ animation.finish();
+ }).then(() => {
+ 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(t => {
+ const div = createDiv(t);
+ const animation = div.animate({}, 100 * MS_PER_SEC);
+ animation.finished.then(t.step_func(() => {
+ assert_unreached('Animation.finished should not be resolved');
+ }));
+
+ return animation.ready.then(() => {
+ 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(t => {
+ const div = createDiv(t);
+ const animation = div.animate({}, 100 * MS_PER_SEC);
+ return animation.ready.then(() => {
+ animation.currentTime = 100 * MS_PER_SEC;
+ animation.finished.then(t.step_func(() => {
+ 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');
+
+promise_test(t => {
+ const animation = createDiv(t).animate(null, 100 * MS_PER_SEC);
+ let ready = false;
+ animation.ready.then(
+ t.step_func(() => {
+ ready = true;
+ }),
+ t.unreached_func('Ready promise must not be rejected')
+ );
+
+ const testSuccess = animation.finished.then(
+ t.step_func(() => {
+ assert_true(ready, 'Ready promise has resolved');
+ }),
+ t.unreached_func('Finished promise must not be rejected')
+ );
+
+ const timeout = waitForAnimationFrames(3).then(() => {
+ return Promise.reject('Finished promise did not arrive in time');
+ });
+
+ animation.finish();
+ return Promise.race([timeout, testSuccess]);
+}, 'Finished promise should be resolved after the ready promise is resolved');
+
+promise_test(t => {
+ const animation = createDiv(t).animate(null, 100 * MS_PER_SEC);
+ let caught = false;
+ animation.ready.then(
+ t.unreached_func('Ready promise must not be resolved'),
+ t.step_func(() => {
+ caught = true;
+ })
+ );
+
+ const testSuccess = animation.finished.then(
+ t.unreached_func('Finished promise must not be resolved'),
+ t.step_func(() => {
+ assert_true(caught, 'Ready promise has been rejected');
+ })
+ );
+
+ const timeout = waitForAnimationFrames(3).then(() => {
+ return Promise.reject('Finished promise was not rejected in time');
+ });
+
+ animation.cancel();
+ return Promise.race([timeout, testSuccess]);
+}, 'Finished promise should be rejected after the ready promise is rejected');
+
+promise_test(async t => {
+ const animation = createDiv(t).animate(null, 100 * MS_PER_SEC);
+
+ // Ensure the finished promise is created
+ const finished = animation.finished;
+
+ window.addEventListener(
+ 'unhandledrejection',
+ t.unreached_func('Should not get an unhandled rejection')
+ );
+
+ animation.cancel();
+
+ // Wait a moment to allow a chance for the event to be dispatched.
+ await waitForAnimationFrames(2);
+}, 'Finished promise does not report an unhandledrejection when rejected');
+
+</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 0000000000..5b9586bfaf
--- /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://drafts.csswg.org/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(t => {
+ const div = createDiv(t);
+ const animation = div.animate({}, 100 * MS_PER_SEC);
+ assert_equals(animation.id, '', 'id for Animation is initially empty');
+}, 'Animation.id initial value');
+
+test(t => {
+ const div = createDiv(t);
+ const 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 0000000000..d539119609
--- /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://drafts.csswg.org/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(t => {
+ const div = createDiv(t);
+ const animation = div.animate({}, 100 * MS_PER_SEC);
+ let finishedTimelineTime;
+ animation.finished.then().catch(() => {
+ finishedTimelineTime = animation.timeline.currentTime;
+ });
+
+ animation.oncancel = t.step_func_done(event => {
+ assert_equals(event.currentTime, null,
+ 'event.currentTime should be null');
+ assert_times_equal(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 0000000000..b58fea0362
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/interfaces/Animation/onfinish.html
@@ -0,0 +1,119 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title>Animation.onfinish</title>
+<link rel="help" href="https://drafts.csswg.org/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(t => {
+ const div = createDiv(t);
+ const animation = div.animate({}, 100 * MS_PER_SEC);
+ let finishedTimelineTime;
+ animation.finished.then(() => {
+ finishedTimelineTime = animation.timeline.currentTime;
+ });
+
+ animation.onfinish = t.step_func_done(event => {
+ assert_equals(event.currentTime, 0,
+ 'event.currentTime should be zero');
+ assert_times_equal(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(t => {
+ const div = createDiv(t);
+ const animation = div.animate({}, 100 * MS_PER_SEC);
+
+ let finishedTimelineTime;
+ animation.finished.then(() => {
+ finishedTimelineTime = animation.timeline.currentTime;
+ });
+
+ animation.onfinish = t.step_func_done(event => {
+ assert_times_equal(event.currentTime, 100 * MS_PER_SEC,
+ 'event.currentTime should be the effect end');
+ assert_times_equal(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(t => {
+ const div = createDiv(t);
+ const animation = div.animate({}, 100 * MS_PER_SEC);
+
+ let finishedTimelineTime;
+ animation.finished.then(() => {
+ finishedTimelineTime = animation.timeline.currentTime;
+ });
+
+ animation.onfinish = t.step_func_done(event => {
+ assert_times_equal(event.currentTime, 100 * MS_PER_SEC,
+ 'event.currentTime should be the effect end');
+ assert_times_equal(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(async t => {
+ const div = createDiv(t);
+ const animation = div.animate({}, 100 * MS_PER_SEC);
+
+ animation.onfinish = event => {
+ assert_unreached('onfinish event should not be fired');
+ };
+
+ animation.currentTime = 100 * MS_PER_SEC / 2;
+ animation.pause();
+
+ await animation.ready;
+ animation.currentTime = 100 * MS_PER_SEC;
+ await waitForAnimationFrames(2);
+}, 'onfinish event is not fired when paused');
+
+promise_test(async t => {
+ const div = createDiv(t);
+ const animation = div.animate({}, 100 * MS_PER_SEC);
+ animation.onfinish = event => {
+ assert_unreached('onfinish event should not be fired');
+ };
+
+ await animation.ready;
+ animation.playbackRate = 0;
+ animation.currentTime = 100 * MS_PER_SEC;
+ await waitForAnimationFrames(2);
+}, 'onfinish event is not fired when the playbackRate is zero');
+
+promise_test(async t => {
+ const div = createDiv(t);
+ const animation = div.animate({}, 100 * MS_PER_SEC);
+
+ animation.onfinish = event => {
+ assert_unreached('onfinish event should not be fired');
+ };
+
+ await animation.ready;
+ animation.currentTime = 100 * MS_PER_SEC;
+ animation.currentTime = 100 * MS_PER_SEC / 2;
+ await 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/onremove.html b/testing/web-platform/tests/web-animations/interfaces/Animation/onremove.html
new file mode 100644
index 0000000000..1a41a3d21c
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/interfaces/Animation/onremove.html
@@ -0,0 +1,58 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title>Animation.onremove</title>
+<link rel="help" href="https://drafts.csswg.org/web-animations/#dom-animation-onremove">
+<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(t => {
+ const div = createDiv(t);
+ const animA = div.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' });
+ const animB = div.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' });
+
+ let finishedTimelineTime = null;
+ animB.onfinish = event => {
+ finishedTimelineTime = event.timelineTime;
+ };
+
+ animA.onremove = t.step_func_done(event => {
+ assert_equals(animA.replaceState, 'removed');
+ assert_equals(event.currentTime, 1);
+ assert_true(finishedTimelineTime != null, 'finished event fired');
+ assert_equals(event.timelineTime, finishedTimelineTime,
+ 'timeline time is set');
+ });
+
+}, 'onremove event is fired when replaced animation is removed.');
+
+promise_test(async t => {
+ const div = createDiv(t);
+ const animA = div.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' });
+ const animB = div.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' });
+ const animC = div.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' });
+ const animD = div.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' });
+
+ const removed = [];
+
+ animA.onremove = () => { removed.push('A'); };
+ animB.onremove = () => { removed.push('B'); };
+ animC.onremove = () => { removed.push('C'); };
+
+ animD.onremove = event => {
+ assert_unreached('onremove event should not be fired');
+ };
+
+ await waitForAnimationFrames(2);
+
+ assert_equals(removed.join(''), 'ABC');
+
+}, 'onremove events are fired in the correct order');
+
+</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 0000000000..1d1bd5fd89
--- /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://drafts.csswg.org/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(t => {
+ const div = createDiv(t);
+ const animation = div.animate({}, 1000 * MS_PER_SEC);
+ let previousCurrentTime = animation.currentTime;
+
+ return animation.ready.then(waitForAnimationFrames(1)).then(() => {
+ assert_true(animation.currentTime >= previousCurrentTime,
+ 'currentTime is initially increasing');
+ animation.pause();
+ return animation.ready;
+ }).then(() => {
+ previousCurrentTime = animation.currentTime;
+ return waitForAnimationFrames(1);
+ }).then(() => {
+ assert_equals(animation.currentTime, previousCurrentTime,
+ 'currentTime does not increase after calling pause()');
+ });
+}, 'pause() a running animation');
+
+promise_test(t => {
+ const div = createDiv(t);
+ const 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, 'paused', 'in paused play state');
+ assert_true(animation.pending, 'initially pause-pending');
+
+ // Check it still resolves as expected
+ return animation.ready.then(() => {
+ assert_false(animation.pending, 'no longer pending');
+ assert_equals(animation.currentTime, 0,
+ 'keeps the initially set currentTime');
+ });
+}, 'pause() from idle');
+
+promise_test(t => {
+ const div = createDiv(t);
+ const 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(() => {
+ assert_equals(animation.currentTime, 1000 * MS_PER_SEC,
+ 'keeps the initially set currentTime');
+ });
+}, 'pause() from idle with a negative playbackRate');
+
+test(t => {
+ const div = createDiv(t);
+ const animation = div.animate({}, {duration: 1000 * MS_PER_SEC,
+ iterations: Infinity});
+ animation.cancel();
+ animation.playbackRate = -1;
+
+ assert_throws_dom('InvalidStateError',
+ () => { 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(t => {
+ const div = createDiv(t);
+ const animation = div.animate({}, 1000 * MS_PER_SEC);
+ return animation.ready
+ .then(animation => {
+ animation.finish();
+ animation.pause();
+ return animation.ready;
+ }).then(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/pending.html b/testing/web-platform/tests/web-animations/interfaces/Animation/pending.html
new file mode 100644
index 0000000000..c200f9e977
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/interfaces/Animation/pending.html
@@ -0,0 +1,55 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title>Animation.pending</title>
+<link rel="help" href="https://drafts.csswg.org/web-animations/#dom-animation-pending">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../../testcommon.js"></script>
+<body>
+<div id="log"></div>
+<script>
+'use strict';
+
+promise_test(t => {
+ const div = createDiv(t);
+ const animation = div.animate({}, 100 * MS_PER_SEC);
+
+ assert_true(animation.pending);
+ return animation.ready.then(() => {
+ assert_false(animation.pending);
+ });
+}, 'reports true -> false when initially played');
+
+promise_test(t => {
+ const div = createDiv(t);
+ const animation = div.animate({}, 100 * MS_PER_SEC);
+ animation.pause();
+
+ assert_true(animation.pending);
+ return animation.ready.then(() => {
+ assert_false(animation.pending);
+ });
+}, 'reports true -> false when paused');
+
+promise_test(async t => {
+ const animation =
+ new Animation(new KeyframeEffect(createDiv(t), null, 100 * MS_PER_SEC),
+ null);
+ animation.play();
+ assert_true(animation.pending);
+ await waitForAnimationFrames(2);
+ assert_true(animation.pending);
+}, 'reports true -> true when played without a timeline');
+
+promise_test(async t => {
+ const animation =
+ new Animation(new KeyframeEffect(createDiv(t), null, 100 * MS_PER_SEC),
+ null);
+ animation.pause();
+ assert_true(animation.pending);
+ await waitForAnimationFrames(2);
+ assert_true(animation.pending);
+}, 'reports true -> true when paused without a timeline');
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/web-animations/interfaces/Animation/persist.html b/testing/web-platform/tests/web-animations/interfaces/Animation/persist.html
new file mode 100644
index 0000000000..c18993cbc4
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/interfaces/Animation/persist.html
@@ -0,0 +1,40 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>Animation.persist</title>
+<link rel="help" href="https://drafts.csswg.org/web-animations/#dom-animation-persist">
+<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(t => {
+ const div = createDiv(t);
+
+ const animA = div.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' });
+ const animB = div.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' });
+
+ animA.onremove = t.step_func_done(() => {
+ assert_equals(animA.replaceState, 'removed');
+ animA.persist();
+ assert_equals(animA.replaceState, 'persisted');
+ });
+}, 'Allows an animation to be persisted after being removed');
+
+promise_test(async t => {
+ const div = createDiv(t);
+
+ const animA = div.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' });
+ const animB = div.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' });
+
+ animA.persist();
+
+ await animA.finished;
+
+ assert_equals(animA.replaceState, 'persisted');
+}, 'Allows an animation to be persisted before being removed');
+
+</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 0000000000..6c5d604b1e
--- /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://drafts.csswg.org/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(t => {
+ const div = createDiv(t);
+ const animation = div.animate({ transform: ['none', 'translate(10px)']},
+ { duration: 100 * MS_PER_SEC,
+ iterations: Infinity });
+ return animation.ready.then(() => {
+ // 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_dom('InvalidStateError',
+ () => { 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/ready.html b/testing/web-platform/tests/web-animations/interfaces/Animation/ready.html
new file mode 100644
index 0000000000..462e2a0484
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/interfaces/Animation/ready.html
@@ -0,0 +1,78 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title>Animation.ready</title>
+<link rel="help" href="https://drafts.csswg.org/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(t => {
+ const div = createDiv(t);
+ const animation = div.animate(null, 100 * MS_PER_SEC);
+ const originalReadyPromise = animation.ready;
+ let pauseReadyPromise;
+
+ return animation.ready.then(() => {
+ 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(() => {
+ 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(t => {
+ const div = createDiv(t);
+ const animation = div.animate(null, 100 * MS_PER_SEC);
+
+ return animation.ready.then(() => {
+ const 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(t => {
+ const div = createDiv(t);
+ const animation = div.animate(null, 100 * MS_PER_SEC);
+
+ return animation.ready.then(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(async t => {
+ const animation = createDiv(t).animate(null, 100 * MS_PER_SEC);
+
+ // Ensure the ready promise is created
+ const ready = animation.ready;
+
+ window.addEventListener(
+ 'unhandledrejection',
+ t.unreached_func('Should not get an unhandled rejection')
+ );
+
+ animation.cancel();
+
+ // Wait a moment to allow a chance for the event to be dispatched.
+ await waitForAnimationFrames(2);
+}, 'The ready promise does not report an unhandledrejection when rejected');
+
+</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 0000000000..61f76955a3
--- /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</title>
+<link rel="help"
+href="https://drafts.csswg.org/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(t => {
+ const 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(t => {
+ const 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(t => {
+ const 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(t => {
+ const 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(t => {
+ const animation = createDiv(t).animate(null, 100 * MS_PER_SEC);
+ return animation.ready.then(() => {
+ assert_greater_than(animation.startTime, 0, 'startTime when running');
+ });
+}, 'startTime is resolved when running');
+
+test(t => {
+ const 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/Animation/style-change-events.html b/testing/web-platform/tests/web-animations/interfaces/Animation/style-change-events.html
new file mode 100644
index 0000000000..0ec21657e3
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/interfaces/Animation/style-change-events.html
@@ -0,0 +1,376 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>Animation interface: style change events</title>
+<link rel="help"
+ href="https://drafts.csswg.org/web-animations-1/#model-liveness">
+<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 that each property defined in the Animation interface behaves as
+// expected with regards to whether or not it produces style change events.
+//
+// There are two types of tests:
+//
+// PlayAnimationTest
+//
+// For properties that are able to cause the Animation to start affecting
+// the target CSS property.
+//
+// This function takes either:
+//
+// (a) A function that simply "plays" that passed-in Animation (i.e. makes
+// it start affecting the target CSS property.
+//
+// (b) An object with the following format:
+//
+// {
+// setup: elem => { /* return Animation */ },
+// test: animation => { /* play |animation| */ },
+// shouldFlush: boolean /* optional, defaults to false */
+// }
+//
+// If the latter form is used, the setup function should return an Animation
+// that does NOT (yet) have an in-effect AnimationEffect that affects the
+// 'opacity' property. Otherwise, the transition we use to detect if a style
+// change event has occurred will never have a chance to be triggered (since
+// the animated style will clobber both before-change and after-change
+// style).
+//
+// Examples of valid animations:
+//
+// - An animation that is idle, or finished but without a fill mode.
+// - An animation with an effect that that does not affect opacity.
+//
+// UsePropertyTest
+//
+// For properties that cannot cause the Animation to start affecting the
+// target CSS property.
+//
+// The shape of the parameter to the UsePropertyTest is identical to the
+// PlayAnimationTest. The only difference is that the function (or 'test'
+// function of the object format is used) does not need to play the
+// animation, but simply needs to get/set the property under test.
+
+const PlayAnimationTest = testFuncOrObj => {
+ let test, setup, shouldFlush;
+
+ if (typeof testFuncOrObj === 'function') {
+ test = testFuncOrObj;
+ shouldFlush = false;
+ } else {
+ test = testFuncOrObj.test;
+ if (typeof testFuncOrObj.setup === 'function') {
+ setup = testFuncOrObj.setup;
+ }
+ shouldFlush = !!testFuncOrObj.shouldFlush;
+ }
+
+ if (!setup) {
+ setup = elem =>
+ new Animation(
+ new KeyframeEffect(elem, { opacity: [0, 1] }, 100 * MS_PER_SEC)
+ );
+ }
+
+ return { test, setup, shouldFlush };
+};
+
+const UsePropertyTest = testFuncOrObj => {
+ const { setup, test, shouldFlush } = PlayAnimationTest(testFuncOrObj);
+
+ let coveringAnimation;
+ return {
+ setup: elem => {
+ coveringAnimation = new Animation(
+ new KeyframeEffect(elem, { opacity: [0, 1] }, 100 * MS_PER_SEC)
+ );
+
+ return setup(elem);
+ },
+ test: animation => {
+ test(animation);
+ coveringAnimation.play();
+ },
+ shouldFlush,
+ };
+};
+
+const tests = {
+ id: UsePropertyTest(animation => (animation.id = 'yer')),
+ get effect() {
+ let effect;
+ return PlayAnimationTest({
+ setup: elem => {
+ // Create a new effect and animation but don't associate them yet
+ effect = new KeyframeEffect(
+ elem,
+ { opacity: [0.5, 1] },
+ 100 * MS_PER_SEC
+ );
+ return elem.animate(null, 100 * MS_PER_SEC);
+ },
+ test: animation => {
+ // Read the effect
+ animation.effect;
+
+ // Assign the effect
+ animation.effect = effect;
+ },
+ });
+ },
+ timeline: PlayAnimationTest({
+ setup: elem => {
+ // Create a new animation with no timeline
+ const animation = new Animation(
+ new KeyframeEffect(elem, { opacity: [0.5, 1] }, 100 * MS_PER_SEC),
+ null
+ );
+ // Set the hold time so that once we assign a timeline it will begin to
+ // play.
+ animation.currentTime = 0;
+
+ return animation;
+ },
+ test: animation => {
+ // Get the timeline
+ animation.timeline;
+
+ // Play the animation by setting the timeline
+ animation.timeline = document.timeline;
+ },
+ }),
+ startTime: PlayAnimationTest(animation => {
+ // Get the startTime
+ animation.startTime;
+
+ // Play the animation by setting the startTime
+ animation.startTime = document.timeline.currentTime;
+ }),
+ currentTime: PlayAnimationTest(animation => {
+ // Get the currentTime
+ animation.currentTime;
+
+ // Play the animation by setting the currentTime
+ animation.currentTime = 0;
+ }),
+ playbackRate: UsePropertyTest(animation => {
+ // Get and set the playbackRate
+ animation.playbackRate = animation.playbackRate * 1.1;
+ }),
+ playState: UsePropertyTest(animation => animation.playState),
+ pending: UsePropertyTest(animation => animation.pending),
+ // Strictly speaking, rangeStart and rangeEnd can change whether the effect
+ // is active, but only if the animation has a view timeline. Otherwise, it has
+ // no effect.
+ rangeStart: UsePropertyTest(animation => animation.rangeStart),
+ rangeEnd: UsePropertyTest(animation => animation.rangeEnd),
+ replaceState: UsePropertyTest(animation => animation.replaceState),
+ ready: UsePropertyTest(animation => animation.ready),
+ finished: UsePropertyTest(animation => {
+ // Get the finished Promise
+ animation.finished;
+ }),
+ onfinish: UsePropertyTest(animation => {
+ // Get the onfinish member
+ animation.onfinish;
+
+ // Set the onfinish menber
+ animation.onfinish = () => {};
+ }),
+ onremove: UsePropertyTest(animation => {
+ // Get the onremove member
+ animation.onremove;
+
+ // Set the onremove menber
+ animation.onremove = () => {};
+ }),
+ oncancel: UsePropertyTest(animation => {
+ // Get the oncancel member
+ animation.oncancel;
+
+ // Set the oncancel menber
+ animation.oncancel = () => {};
+ }),
+ cancel: UsePropertyTest({
+ // Animate _something_ just to make the test more interesting
+ setup: elem => elem.animate({ color: ['green', 'blue'] }, 100 * MS_PER_SEC),
+ test: animation => {
+ animation.cancel();
+ },
+ }),
+ finish: PlayAnimationTest({
+ setup: elem =>
+ new Animation(
+ new KeyframeEffect(
+ elem,
+ { opacity: [0.5, 1] },
+ {
+ duration: 100 * MS_PER_SEC,
+ fill: 'both',
+ }
+ )
+ ),
+ test: animation => {
+ animation.finish();
+ },
+ }),
+ play: PlayAnimationTest(animation => animation.play()),
+ pause: PlayAnimationTest(animation => {
+ // Pause animation -- this will cause the animation to transition from the
+ // 'idle' state to the 'paused' (but pending) state with hold time zero.
+ animation.pause();
+ }),
+ updatePlaybackRate: UsePropertyTest(animation => {
+ animation.updatePlaybackRate(1.1);
+ }),
+ // We would like to use a PlayAnimationTest here but reverse() is async and
+ // doesn't start applying its result until the animation is ready.
+ reverse: UsePropertyTest({
+ setup: elem => {
+ // Create a new animation and seek it to the end so that it no longer
+ // affects style (since it has no fill mode).
+ const animation = elem.animate({ opacity: [0.5, 1] }, 100 * MS_PER_SEC);
+ animation.finish();
+ return animation;
+ },
+ test: animation => {
+ animation.reverse();
+ },
+ }),
+ persist: PlayAnimationTest({
+ setup: async elem => {
+ // Create an animation whose replaceState is 'removed'.
+ const animA = elem.animate(
+ { opacity: 1 },
+ { duration: 1, fill: 'forwards' }
+ );
+ const animB = elem.animate(
+ { opacity: 1 },
+ { duration: 1, fill: 'forwards' }
+ );
+ await animA.finished;
+ animB.cancel();
+
+ return animA;
+ },
+ test: animation => {
+ animation.persist();
+ },
+ }),
+ commitStyles: PlayAnimationTest({
+ setup: async elem => {
+ // Create an animation whose replaceState is 'removed'.
+ const animA = elem.animate(
+ // It's important to use opacity of '1' here otherwise we'll create a
+ // transition due to updating the specified style whereas the transition
+ // we want to detect is the one from flushing due to calling
+ // commitStyles.
+ { opacity: 1 },
+ { duration: 1, fill: 'forwards' }
+ );
+ const animB = elem.animate(
+ { opacity: 1 },
+ { duration: 1, fill: 'forwards' }
+ );
+ await animA.finished;
+ animB.cancel();
+
+ return animA;
+ },
+ test: animation => {
+ animation.commitStyles();
+ },
+ shouldFlush: true,
+ }),
+ get ['Animation constructor']() {
+ let originalElem;
+ return UsePropertyTest({
+ setup: elem => {
+ originalElem = elem;
+ // Return a dummy animation so the caller has something to wait on
+ return elem.animate(null);
+ },
+ test: () =>
+ new Animation(
+ new KeyframeEffect(
+ originalElem,
+ { opacity: [0.5, 1] },
+ 100 * MS_PER_SEC
+ )
+ ),
+ });
+ },
+};
+
+// Check that each enumerable property and the constructor follow the
+// expected behavior with regards to triggering style change events.
+const properties = [
+ ...Object.keys(Animation.prototype),
+ 'Animation constructor',
+];
+
+test(() => {
+ for (const property of Object.keys(tests)) {
+ assert_in_array(
+ property,
+ properties,
+ `Test property '${property}' should be one of the properties on ` +
+ ' Animation'
+ );
+ }
+}, 'All property keys are recognized');
+
+for (const key of properties) {
+ promise_test(async t => {
+ assert_own_property(tests, key, `Should have a test for '${key}' property`);
+ const { setup, test, shouldFlush } = tests[key];
+
+ // Setup target element
+ const div = createDiv(t);
+ let gotTransition = false;
+ div.addEventListener('transitionrun', () => {
+ gotTransition = true;
+ });
+
+ // Setup animation
+ const animation = await setup(div);
+
+ // Setup transition start point
+ div.style.transition = 'opacity 100s';
+ getComputedStyle(div).opacity;
+
+ // Update specified style but don't flush
+ div.style.opacity = '0.5';
+
+ // Trigger the property
+ test(animation);
+
+ // If the test function produced a style change event it will have triggered
+ // a transition.
+
+ // Wait for the animation to start and then for at least two animation
+ // frames to give the transitionrun event a chance to be dispatched.
+ assert_true(
+ typeof animation.ready !== 'undefined',
+ 'Should have a valid animation to wait on'
+ );
+ await animation.ready;
+ await waitForAnimationFrames(2);
+
+ if (shouldFlush) {
+ assert_true(gotTransition, 'A transition should have been triggered');
+ } else {
+ assert_false(
+ gotTransition,
+ 'A transition should NOT have been triggered'
+ );
+ }
+ }, `Animation.${key} produces expected style change events`);
+}
+</script>
+</body>
diff --git a/testing/web-platform/tests/web-animations/interfaces/AnimationEffect/getComputedTiming.html b/testing/web-platform/tests/web-animations/interfaces/AnimationEffect/getComputedTiming.html
new file mode 100644
index 0000000000..10bd193361
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/interfaces/AnimationEffect/getComputedTiming.html
@@ -0,0 +1,214 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title>AnimationEffect.getComputedTiming</title>
+<link rel="help" href="https://drafts.csswg.org/web-animations/#dom-animationeffect-getcomputedtiming">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../../testcommon.js"></script>
+<body>
+<div id="log"></div>
+<script>
+'use strict';
+
+test(t => {
+ const effect = new KeyframeEffect(null, null);
+
+ const ct = effect.getComputedTiming();
+ assert_equals(ct.delay, 0, 'computed delay');
+ assert_equals(ct.endDelay, 0, 'computed endDelay');
+ assert_equals(ct.fill, 'none', 'computed fill');
+ assert_equals(ct.iterationStart, 0.0, 'computed iterationStart');
+ assert_equals(ct.iterations, 1.0, 'computed iterations');
+ assert_equals(ct.duration, 0, 'computed duration');
+ assert_equals(ct.direction, 'normal', 'computed direction');
+ assert_equals(ct.easing, 'linear', 'computed easing');
+}, 'values of getComputedTiming() when a KeyframeEffect is ' +
+ 'constructed without any KeyframeEffectOptions object');
+
+const gGetComputedTimingTests = [
+ { desc: 'an empty KeyframeEffectOptions object',
+ input: { },
+ expected: { } },
+ { desc: 'a normal KeyframeEffectOptions object',
+ input: { delay: 1000,
+ endDelay: 2000,
+ fill: 'auto',
+ iterationStart: 0.5,
+ iterations: 5.5,
+ duration: 'auto',
+ direction: 'alternate',
+ easing: 'steps(2)' },
+ expected: { delay: 1000,
+ endDelay: 2000,
+ fill: 'none',
+ iterationStart: 0.5,
+ iterations: 5.5,
+ duration: 0,
+ direction: 'alternate',
+ easing: 'steps(2)' } },
+ { 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' } }
+];
+
+for (const stest of gGetComputedTimingTests) {
+ test(t => {
+ const effect = new KeyframeEffect(null, null, stest.input);
+
+ // Helper function to provide default expected values when the test does
+ // not supply them.
+ const expected = (field, defaultValue) => {
+ return field in stest.expected ? stest.expected[field] : defaultValue;
+ };
+
+ const ct = effect.getComputedTiming();
+ assert_equals(ct.delay, expected('delay', 0),
+ 'computed delay');
+ assert_equals(ct.endDelay, expected('endDelay', 0),
+ 'computed endDelay');
+ assert_equals(ct.fill, expected('fill', 'none'),
+ 'computed fill');
+ assert_equals(ct.iterationStart, expected('iterationStart', 0),
+ 'computed iterations');
+ 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');
+ assert_equals(ct.easing, expected('easing', 'linear'),
+ 'computed easing');
+
+ }, 'values of getComputedTiming() when a KeyframeEffect is'
+ + ` constructed by ${stest.desc}`);
+}
+
+const 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 },
+];
+
+for (const stest of gActiveDurationTests) {
+ test(t => {
+ const effect = new KeyframeEffect(null, null, stest.input);
+
+ assert_equals(effect.getComputedTiming().activeDuration,
+ stest.expected);
+
+ }, `getComputedTiming().activeDuration for ${stest.desc}`);
+}
+
+const 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 }
+];
+
+for (const stest of gEndTimeTests) {
+ test(t => {
+ const effect = new KeyframeEffect(null, null, stest.input);
+
+ assert_equals(effect.getComputedTiming().endTime,
+ stest.expected);
+
+ }, `getComputedTiming().endTime for ${stest.desc}`);
+}
+</script>
+</body>
diff --git a/testing/web-platform/tests/web-animations/interfaces/AnimationEffect/updateTiming.html b/testing/web-platform/tests/web-animations/interfaces/AnimationEffect/updateTiming.html
new file mode 100644
index 0000000000..6a340c0bf4
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/interfaces/AnimationEffect/updateTiming.html
@@ -0,0 +1,475 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>AnimationEffect.updateTiming</title>
+<link rel="help" href="https://drafts.csswg.org/web-animations-1/#dom-animationeffect-updatetiming">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../../testcommon.js"></script>
+<script src="../../resources/easing-tests.js"></script>
+<script src="../../resources/timing-tests.js"></script>
+<body>
+<div id="log"></div>
+<script>
+'use strict';
+
+// ------------------------------
+// delay
+// ------------------------------
+
+test(t => {
+ const anim = createDiv(t).animate(null, 100);
+ anim.effect.updateTiming({ delay: 100 });
+ assert_equals(anim.effect.getTiming().delay, 100, 'set delay 100');
+ assert_equals(anim.effect.getComputedTiming().delay, 100,
+ 'getComputedTiming() after set delay 100');
+}, 'Allows setting the delay to a positive number');
+
+test(t => {
+ const anim = createDiv(t).animate(null, 100);
+ anim.effect.updateTiming({ delay: -100 });
+ assert_equals(anim.effect.getTiming().delay, -100, 'set delay -100');
+ assert_equals(anim.effect.getComputedTiming().delay, -100,
+ 'getComputedTiming() after set delay -100');
+}, 'Allows setting the delay to a negative number');
+
+test(t => {
+ const anim = createDiv(t).animate(null, 100);
+ anim.effect.updateTiming({ delay: 100 });
+ assert_equals(anim.effect.getComputedTiming().progress, null);
+ assert_equals(anim.effect.getComputedTiming().currentIteration, null);
+}, 'Allows setting the delay of an animation in progress: positive delay that'
+ + ' causes the animation to be no longer in-effect');
+
+test(t => {
+ const anim = createDiv(t).animate(null, { fill: 'both', duration: 100 });
+ anim.effect.updateTiming({ delay: -50 });
+ assert_equals(anim.effect.getComputedTiming().progress, 0.5);
+}, 'Allows setting the delay of an animation in progress: negative delay that'
+ + ' seeks into the active interval');
+
+test(t => {
+ const anim = createDiv(t).animate(null, { fill: 'both', duration: 100 });
+ anim.effect.updateTiming({ delay: -100 });
+ assert_equals(anim.effect.getComputedTiming().progress, 1);
+ assert_equals(anim.effect.getComputedTiming().currentIteration, 0);
+}, 'Allows setting the delay of an animation in progress: large negative delay'
+ + ' that causes the animation to be finished');
+
+for (const invalid of gBadDelayValues) {
+ test(t => {
+ const anim = createDiv(t).animate(null);
+ assert_throws_js(TypeError, () => {
+ anim.effect.updateTiming({ delay: invalid });
+ });
+ }, `Throws when setting invalid delay value: ${invalid}`);
+}
+
+
+// ------------------------------
+// endDelay
+// ------------------------------
+
+test(t => {
+ const anim = createDiv(t).animate(null, 2000);
+ anim.effect.updateTiming({ endDelay: 123.45 });
+ assert_time_equals_literal(anim.effect.getTiming().endDelay, 123.45,
+ 'set endDelay 123.45');
+ assert_time_equals_literal(anim.effect.getComputedTiming().endDelay, 123.45,
+ 'getComputedTiming() after set endDelay 123.45');
+}, 'Allows setting the endDelay to a positive number');
+
+test(t => {
+ const anim = createDiv(t).animate(null, 2000);
+ anim.effect.updateTiming({ endDelay: -1000 });
+ assert_equals(anim.effect.getTiming().endDelay, -1000, 'set endDelay -1000');
+ assert_equals(anim.effect.getComputedTiming().endDelay, -1000,
+ 'getComputedTiming() after set endDelay -1000');
+}, 'Allows setting the endDelay to a negative number');
+
+test(t => {
+ const anim = createDiv(t).animate(null, 2000);
+ assert_throws_js(TypeError, () => {
+ anim.effect.updateTiming({ endDelay: Infinity });
+ });
+}, 'Throws when setting the endDelay to infinity');
+
+test(t => {
+ const anim = createDiv(t).animate(null, 2000);
+ assert_throws_js(TypeError, () => {
+ anim.effect.updateTiming({ endDelay: -Infinity });
+ });
+}, 'Throws when setting the endDelay to negative infinity');
+
+
+// ------------------------------
+// fill
+// ------------------------------
+
+for (const fill of ['none', 'forwards', 'backwards', 'both']) {
+ test(t => {
+ const anim = createDiv(t).animate(null, 100);
+ anim.effect.updateTiming({ fill });
+ assert_equals(anim.effect.getTiming().fill, fill, 'set fill ' + fill);
+ assert_equals(anim.effect.getComputedTiming().fill, fill,
+ 'getComputedTiming() after set fill ' + fill);
+ }, `Allows setting the fill to '${fill}'`);
+}
+
+
+// ------------------------------
+// iterationStart
+// ------------------------------
+
+test(t => {
+ const anim = createDiv(t).animate(null,
+ { iterationStart: 0.2,
+ iterations: 1,
+ fill: 'both',
+ duration: 100,
+ delay: 1 });
+ anim.effect.updateTiming({ iterationStart: 2.5 });
+ assert_time_equals_literal(anim.effect.getComputedTiming().progress, 0.5);
+ assert_equals(anim.effect.getComputedTiming().currentIteration, 2);
+}, 'Allows setting the iterationStart of an animation in progress:'
+ + ' backwards-filling');
+
+test(t => {
+ const anim = createDiv(t).animate(null,
+ { iterationStart: 0.2,
+ iterations: 1,
+ fill: 'both',
+ duration: 100,
+ delay: 0 });
+ anim.effect.updateTiming({ iterationStart: 2.5 });
+ assert_time_equals_literal(anim.effect.getComputedTiming().progress, 0.5);
+ assert_equals(anim.effect.getComputedTiming().currentIteration, 2);
+}, 'Allows setting the iterationStart of an animation in progress:'
+ + ' active phase');
+
+test(t => {
+ const anim = createDiv(t).animate(null,
+ { iterationStart: 0.2,
+ iterations: 1,
+ fill: 'both',
+ duration: 100,
+ delay: 0 });
+ anim.finish();
+ anim.effect.updateTiming({ iterationStart: 2.5 });
+ assert_time_equals_literal(anim.effect.getComputedTiming().progress, 0.5);
+ assert_equals(anim.effect.getComputedTiming().currentIteration, 3);
+}, 'Allows setting the iterationStart of an animation in progress:'
+ + ' forwards-filling');
+
+for (const invalid of gBadIterationStartValues) {
+ test(t => {
+ const anim = createDiv(t).animate(null);
+ assert_throws_js(TypeError, () => {
+ anim.effect.updateTiming({ iterationStart: invalid });
+ }, `setting ${invalid}`);
+ }, `Throws when setting invalid iterationStart value: ${invalid}`);
+}
+
+// ------------------------------
+// iterations
+// ------------------------------
+
+test(t => {
+ const anim = createDiv(t).animate(null, 2000);
+ anim.effect.updateTiming({ iterations: 2 });
+ assert_equals(anim.effect.getTiming().iterations, 2, 'set duration 2');
+ assert_equals(anim.effect.getComputedTiming().iterations, 2,
+ 'getComputedTiming() after set iterations 2');
+}, 'Allows setting iterations to a double value');
+
+test(t => {
+ const anim = createDiv(t).animate(null, 2000);
+ anim.effect.updateTiming({ iterations: Infinity });
+ assert_equals(anim.effect.getTiming().iterations, Infinity,
+ 'set duration Infinity');
+ assert_equals(anim.effect.getComputedTiming().iterations, Infinity,
+ 'getComputedTiming() after set iterations Infinity');
+}, 'Allows setting iterations to infinity');
+
+for (const invalid of gBadIterationsValues) {
+ test(t => {
+ const anim = createDiv(t).animate(null);
+ assert_throws_js(TypeError, () => {
+ anim.effect.updateTiming({ iterations: invalid });
+ });
+ }, `Throws when setting invalid iterations value: ${invalid}`);
+}
+
+test(t => {
+ const anim = createDiv(t).animate(null, { duration: 100000, fill: 'both' });
+
+ anim.finish();
+
+ assert_equals(anim.effect.getComputedTiming().progress, 1,
+ 'progress when animation is finished');
+ assert_equals(anim.effect.getComputedTiming().currentIteration, 0,
+ 'current iteration when animation is finished');
+
+ anim.effect.updateTiming({ iterations: 2 });
+
+ assert_time_equals_literal(anim.effect.getComputedTiming().progress,
+ 0,
+ 'progress after adding an iteration');
+ assert_time_equals_literal(anim.effect.getComputedTiming().currentIteration,
+ 1,
+ 'current iteration after adding an iteration');
+
+ anim.effect.updateTiming({ iterations: 0 });
+
+ assert_equals(anim.effect.getComputedTiming().progress, 0,
+ 'progress after setting iterations to zero');
+ assert_equals(anim.effect.getComputedTiming().currentIteration, 0,
+ 'current iteration after setting iterations to zero');
+
+ anim.effect.updateTiming({ iterations: Infinity });
+
+ assert_equals(anim.effect.getComputedTiming().progress, 0,
+ 'progress after setting iterations to Infinity');
+ assert_equals(anim.effect.getComputedTiming().currentIteration, 1,
+ 'current iteration after setting iterations to Infinity');
+}, 'Allows setting the iterations of an animation in progress');
+
+
+// ------------------------------
+// duration
+// ------------------------------
+
+for (const duration of gGoodDurationValues) {
+ test(t => {
+ const anim = createDiv(t).animate(null, 2000);
+ anim.effect.updateTiming({ duration: duration.specified });
+ if (typeof duration.specified === 'number') {
+ assert_time_equals_literal(anim.effect.getTiming().duration,
+ duration.specified,
+ 'Updates specified duration');
+ } else {
+ assert_equals(anim.effect.getTiming().duration, duration.specified,
+ 'Updates specified duration');
+ }
+ assert_time_equals_literal(anim.effect.getComputedTiming().duration,
+ duration.computed,
+ 'Updates computed duration');
+ }, `Allows setting the duration to ${duration.specified}`);
+}
+
+for (const invalid of gBadDurationValues) {
+ test(t => {
+ assert_throws_js(TypeError, () => {
+ createDiv(t).animate(null, { duration: invalid });
+ });
+ }, 'Throws when setting invalid duration: '
+ + (typeof invalid === 'string' ? `"${invalid}"` : invalid));
+}
+
+test(t => {
+ const anim = createDiv(t).animate(null, { duration: 100000, fill: 'both' });
+ anim.finish();
+ assert_equals(anim.effect.getComputedTiming().progress, 1,
+ 'progress when animation is finished');
+ anim.effect.updateTiming({ duration: anim.effect.getTiming().duration * 2 });
+ assert_time_equals_literal(anim.effect.getComputedTiming().progress, 0.5,
+ 'progress after doubling the duration');
+ anim.effect.updateTiming({ duration: 0 });
+ assert_equals(anim.effect.getComputedTiming().progress, 1,
+ 'progress after setting duration to zero');
+ anim.effect.updateTiming({ duration: 'auto' });
+ assert_equals(anim.effect.getComputedTiming().progress, 1,
+ 'progress after setting duration to \'auto\'');
+}, 'Allows setting the duration of an animation in progress');
+
+promise_test(t => {
+ const anim = createDiv(t).animate(null, 100 * MS_PER_SEC);
+ return anim.ready.then(() => {
+ const originalStartTime = anim.startTime;
+ const originalCurrentTime = anim.currentTime;
+ assert_time_equals_literal(
+ anim.effect.getComputedTiming().duration,
+ 100 * MS_PER_SEC,
+ 'Initial duration should be as set on KeyframeEffect'
+ );
+
+ anim.effect.updateTiming({ duration: 200 * MS_PER_SEC });
+ assert_time_equals_literal(
+ anim.effect.getComputedTiming().duration,
+ 200 * MS_PER_SEC,
+ 'Effect duration should have been updated'
+ );
+ assert_times_equal(anim.startTime, originalStartTime,
+ 'startTime should be unaffected by changing effect ' +
+ 'duration');
+ assert_times_equal(anim.currentTime, originalCurrentTime,
+ 'currentTime should be unaffected by changing effect ' +
+ 'duration');
+ });
+}, 'Allows setting the duration of an animation in progress such that the' +
+ ' the start and current time do not change');
+
+
+// ------------------------------
+// direction
+// ------------------------------
+
+test(t => {
+ const anim = createDiv(t).animate(null, 2000);
+
+ const directions = ['normal', 'reverse', 'alternate', 'alternate-reverse'];
+ for (const direction of directions) {
+ anim.effect.updateTiming({ direction: direction });
+ assert_equals(anim.effect.getTiming().direction, direction,
+ `set direction to ${direction}`);
+ }
+}, 'Allows setting the direction to each of the possible keywords');
+
+test(t => {
+ const anim = createDiv(t).animate(null, {
+ duration: 10000,
+ direction: 'normal',
+ });
+ anim.currentTime = 7000;
+ assert_time_equals_literal(anim.effect.getComputedTiming().progress, 0.7,
+ 'progress before updating direction');
+
+ anim.effect.updateTiming({ direction: 'reverse' });
+
+ assert_time_equals_literal(anim.effect.getComputedTiming().progress, 0.3,
+ 'progress after updating direction');
+}, 'Allows setting the direction of an animation in progress from \'normal\' to'
+ + ' \'reverse\'');
+
+test(t => {
+ const anim = createDiv(t).animate(null,
+ { duration: 10000, direction: 'normal' });
+ assert_equals(anim.effect.getComputedTiming().progress, 0,
+ 'progress before updating direction');
+
+ anim.effect.updateTiming({ direction: 'reverse' });
+
+ assert_equals(anim.effect.getComputedTiming().progress, 1,
+ 'progress after updating direction');
+}, 'Allows setting the direction of an animation in progress from \'normal\' to'
+ + ' \'reverse\' while at start of active interval');
+
+test(t => {
+ const anim = createDiv(t).animate(null,
+ { fill: 'backwards',
+ duration: 10000,
+ delay: 10000,
+ direction: 'normal' });
+ assert_equals(anim.effect.getComputedTiming().progress, 0,
+ 'progress before updating direction');
+
+ anim.effect.updateTiming({ direction: 'reverse' });
+
+ assert_equals(anim.effect.getComputedTiming().progress, 1,
+ 'progress after updating direction');
+}, 'Allows setting the direction of an animation in progress from \'normal\' to'
+ + ' \'reverse\' while filling backwards');
+
+test(t => {
+ const anim = createDiv(t).animate(null,
+ { iterations: 2,
+ duration: 10000,
+ direction: 'normal' });
+ anim.currentTime = 17000;
+ assert_time_equals_literal(anim.effect.getComputedTiming().progress, 0.7,
+ 'progress before updating direction');
+
+ anim.effect.updateTiming({ direction: 'alternate' });
+
+ assert_time_equals_literal(anim.effect.getComputedTiming().progress, 0.3,
+ 'progress after updating direction');
+}, 'Allows setting the direction of an animation in progress from \'normal\' to'
+ + ' \'alternate\'');
+
+test(t => {
+ const anim = createDiv(t).animate(null,
+ { iterations: 2,
+ duration: 10000,
+ direction: 'alternate' });
+ anim.currentTime = 17000;
+ assert_time_equals_literal(anim.effect.getComputedTiming().progress, 0.3,
+ 'progress before updating direction');
+
+ anim.effect.updateTiming({ direction: 'alternate-reverse' });
+
+ assert_time_equals_literal(anim.effect.getComputedTiming().progress, 0.7,
+ 'progress after updating direction');
+}, 'Allows setting the direction of an animation in progress from \'alternate\''
+ + ' to \'alternate-reverse\'');
+
+
+// ------------------------------
+// easing
+// ------------------------------
+
+function assert_progress(animation, currentTime, easingFunction) {
+ animation.currentTime = currentTime;
+ const portion = currentTime / animation.effect.getTiming().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`);
+}
+
+for (const options of gEasingTests) {
+ test(t => {
+ const target = createDiv(t);
+ const anim = target.animate(null,
+ { duration: 1000 * MS_PER_SEC,
+ fill: 'forwards' });
+ anim.effect.updateTiming({ easing: options.easing });
+ assert_equals(anim.effect.getTiming().easing,
+ options.serialization || options.easing);
+
+ const 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);
+ }, `Allows setting the easing to a ${options.desc}`);
+}
+
+for (const easing of gRoundtripEasings) {
+ test(t => {
+ const anim = createDiv(t).animate(null);
+ anim.effect.updateTiming({ easing: easing });
+ assert_equals(anim.effect.getTiming().easing, easing);
+ }, `Updates the specified value when setting the easing to '${easing}'`);
+}
+
+test(t => {
+ const delay = 1000 * MS_PER_SEC;
+
+ const target = createDiv(t);
+ const anim = target.animate(null,
+ { duration: 1000 * MS_PER_SEC,
+ fill: 'both',
+ delay: delay,
+ easing: 'steps(2, start)' });
+
+ anim.effect.updateTiming({ 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.updateTiming({ 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.updateTiming({ easing: 'steps(2, end)' });
+ assert_equals(anim.effect.getComputedTiming().progress, 1,
+ 'easing replace to steps(2, end) again at after phase');
+}, 'Allows setting the easing of an animation in progress');
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/web-animations/interfaces/AnimationPlaybackEvent/constructor.html b/testing/web-platform/tests/web-animations/interfaces/AnimationPlaybackEvent/constructor.html
new file mode 100644
index 0000000000..1c40a3fb21
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/interfaces/AnimationPlaybackEvent/constructor.html
@@ -0,0 +1,30 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>AnimationPlaybackEvent constructor</title>
+<link rel="help"
+ href="https://drafts.csswg.org/web-animations/#dom-animationplaybackevent-animationplaybackevent">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+'use strict';
+
+test(t => {
+ const evt = new AnimationPlaybackEvent('finish');
+ assert_equals(evt.type, 'finish');
+ assert_equals(evt.currentTime, null);
+ assert_equals(evt.timelineTime, null);
+}, 'Event created without an event parameter has null time values');
+
+test(t => {
+ const evt =
+ new AnimationPlaybackEvent('cancel', {
+ currentTime: -100,
+ timelineTime: 100,
+ });
+ assert_equals(evt.type, 'cancel');
+ assert_equals(evt.currentTime, -100);
+ assert_equals(evt.timelineTime, 100);
+}, 'Created event reflects times specified in constructor');
+
+</script>
diff --git a/testing/web-platform/tests/web-animations/interfaces/Document/timeline.html b/testing/web-platform/tests/web-animations/interfaces/Document/timeline.html
new file mode 100644
index 0000000000..b8b4d74d5e
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/interfaces/Document/timeline.html
@@ -0,0 +1,23 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>Document.timeline</title>
+<link rel="help" href="https://drafts.csswg.org/web-animations/#dom-document-timeline">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../../testcommon.js"></script>
+<div id="log"></div>
+<iframe width="10" height="10" id="iframe"></iframe>
+<script>
+'use strict';
+
+test(() => {
+ assert_equals(document.timeline, document.timeline,
+ 'Document.timeline returns the same object every time');
+ const 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 returns the default document timeline');
+
+</script>
diff --git a/testing/web-platform/tests/web-animations/interfaces/DocumentOrShadowRoot/getAnimations.html b/testing/web-platform/tests/web-animations/interfaces/DocumentOrShadowRoot/getAnimations.html
new file mode 100644
index 0000000000..9bcc042a8f
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/interfaces/DocumentOrShadowRoot/getAnimations.html
@@ -0,0 +1,234 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title>DocumentOrShadowRoot.getAnimations</title>
+<link rel="help" href="https://drafts.csswg.org/web-animations-1/#dom-documentorshadowroot-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';
+
+const gKeyFrames = { 'marginLeft': ['100px', '200px'] };
+
+test(t => {
+ assert_equals(document.getAnimations().length, 0,
+ 'getAnimations returns an empty sequence for a document ' +
+ 'with no animations');
+}, 'Document.getAnimations() returns an empty sequence for non-animated'
+ + ' content');
+
+test(t => {
+ const div = createDiv(t);
+ const anim1 = div.animate(gKeyFrames, 100 * MS_PER_SEC);
+ const 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');
+}, 'Document.getAnimations() returns script-generated animations')
+
+test(t => {
+ const div = createDiv(t);
+ const anim1 = div.animate(gKeyFrames, 100 * MS_PER_SEC);
+ const anim2 = div.animate(gKeyFrames, 100 * MS_PER_SEC);
+ assert_array_equals(document.getAnimations(),
+ [ anim1, anim2 ],
+ 'getAnimations() returns running animations');
+}, 'Document.getAnimations() returns script-generated animations in the order'
+ + ' they were created')
+
+test(t => {
+ // This element exists but is not a descendent of any document, so isn't
+ // picked up by getAnimations.
+ const div = document.createElement('div');
+ const anim = div.animate(gKeyFrames, 100 * MS_PER_SEC);
+ assert_equals(document.getAnimations().length, 0);
+
+ // Now connect the div; it should appear in the list of animations.
+ document.body.appendChild(div);
+ t.add_cleanup(() => { div.remove(); });
+ assert_equals(document.getAnimations().length, 1);
+}, 'Document.getAnimations() does not return a disconnected node');
+
+test(t => {
+ const effect = new KeyframeEffect(null, gKeyFrames, 100 * MS_PER_SEC);
+ const anim = new Animation(effect, document.timeline);
+ anim.play();
+
+ assert_equals(document.getAnimations().length, 0,
+ 'document.getAnimations() only returns animations targeting ' +
+ 'elements in this document');
+}, 'Document.getAnimations() does not return an animation with a null target');
+
+promise_test(async t => {
+ const iframe = document.createElement('iframe');
+ await insertFrameAndAwaitLoad(t, iframe, document)
+
+ const div = createDiv(t, iframe.contentDocument)
+ const effect = new KeyframeEffect(div, null, 100 * MS_PER_SEC);
+ const anim = new Animation(effect, document.timeline);
+ anim.play();
+
+ // The animation's timeline is from the main document, but the effect's
+ // target element is part of the iframe document and that is what matters
+ // for getAnimations.
+ assert_equals(document.getAnimations().length, 0);
+ assert_equals(iframe.contentDocument.getAnimations().length, 1);
+ anim.finish();
+}, 'Document.getAnimations() returns animations on elements inside same-origin'
+ + ' iframes');
+
+promise_test(async t => {
+ const iframe1 = document.createElement('iframe');
+ const iframe2 = document.createElement('iframe');
+
+ await insertFrameAndAwaitLoad(t, iframe1, document);
+ await insertFrameAndAwaitLoad(t, iframe2, document);
+
+ const div_frame1 = createDiv(t, iframe1.contentDocument)
+ const div_main_frame = createDiv(t)
+ const effect1 = new KeyframeEffect(div_frame1, null, 100 * MS_PER_SEC);
+ const anim1 = new Animation(effect1, document.timeline);
+ anim1.play();
+ // Animation of div_frame1 is in iframe with main timeline.
+ // The animation's timeline is from the iframe, but the effect's target
+ // element is part of the iframe's document.
+ assert_equals(document.getAnimations().length, 0);
+ assert_equals(iframe1.contentDocument.getAnimations().length, 1);
+ anim1.finish();
+
+ // animation of div_frame1 in iframe1 with iframe timeline
+ const effect2 = new KeyframeEffect(div_frame1, null, 100 * MS_PER_SEC);
+ const anim2 = new Animation(effect2, iframe1.contentDocument.timeline);
+ anim2.play();
+ assert_equals(document.getAnimations().length, 0);
+ assert_equals(iframe1.contentDocument.getAnimations().length, 1);
+ anim2.finish();
+
+ //animation of div_main_frame in main frame with iframe timeline
+ const effect3 = new KeyframeEffect(div_main_frame, null, 100 * MS_PER_SEC);
+ const anim3 = new Animation(effect3, iframe1.contentDocument.timeline);
+ anim3.play();
+ assert_equals(document.getAnimations().length, 1);
+ assert_equals(iframe1.contentDocument.getAnimations().length, 0);
+ anim3.finish();
+
+ //animation of div_frame1 in iframe1 with another iframe's timeline
+ const effect4 = new KeyframeEffect(div_frame1, null, 100 * MS_PER_SEC);
+ const anim4 = new Animation(effect4, iframe2.contentDocument.timeline);
+ anim4.play();
+ assert_equals(document.getAnimations().length, 0);
+ assert_equals(iframe1.contentDocument.getAnimations().length, 1);
+ assert_equals(iframe2.contentDocument.getAnimations().length, 0);
+ anim4.finish();
+}, 'iframe.contentDocument.getAnimations() returns animations on elements '
+ + 'inside same-origin Document');
+
+test(t => {
+ const div = createDiv(t);
+ const shadow = div.attachShadow({ mode: 'open' });
+
+ // Create a tree with the following structure
+ //
+ // div
+ // |
+ // (ShadowRoot)
+ // / \
+ // childA childB
+ // (*anim2) |
+ // grandChild
+ // (*anim1)
+ //
+ // This lets us test that:
+ //
+ // a) All children of the ShadowRoot are included
+ // b) Descendants of the children are included
+ // c) The result is sorted by composite order (since we fire anim1 before
+ // anim2 despite childA appearing first in tree order)
+
+ const childA = createDiv(t);
+ shadow.append(childA);
+
+ const childB = createDiv(t);
+ shadow.append(childB);
+
+ const grandChild = createDiv(t);
+ childB.append(grandChild);
+
+ const anim1 = grandChild.animate(gKeyFrames, 100 * MS_PER_SEC)
+ const anim2 = childA.animate(gKeyFrames, 100 * MS_PER_SEC)
+
+ assert_array_equals(
+ div.shadowRoot.getAnimations(),
+ [ anim1, anim2 ],
+ 'getAnimations() called on ShadowRoot returns expected animations'
+ );
+}, 'ShadowRoot.getAnimations() return all animations in the shadow tree');
+
+test(t => {
+ const div = createDiv(t);
+ const shadow = div.attachShadow({ mode: 'open' });
+
+ const child = createDiv(t);
+ shadow.append(child);
+
+ child.animate(gKeyFrames, 100 * MS_PER_SEC)
+
+ assert_array_equals(
+ document.getAnimations(),
+ [],
+ 'getAnimations() called on Document does not return animations from shadow'
+ + ' trees'
+ );
+}, 'Document.getAnimations() does NOT return animations in shadow trees');
+
+test(t => {
+ const div = createDiv(t);
+ const shadow = div.attachShadow({ mode: 'open' });
+
+ div.animate(gKeyFrames, 100 * MS_PER_SEC)
+
+ assert_array_equals(
+ div.shadowRoot.getAnimations(),
+ [],
+ 'getAnimations() called on ShadowRoot does not return animations from'
+ + ' Document'
+ );
+}, 'ShadowRoot.getAnimations() does NOT return animations in parent document');
+
+promise_test(async t => {
+ const div = createDiv(t);
+ const watcher = EventWatcher(t, div, 'transitionrun');
+
+ // Create a covering animation to prevent transitions from firing after
+ // calling getAnimations().
+ const coveringAnimation = new Animation(
+ new KeyframeEffect(div, { opacity: [0, 1] }, 100 * MS_PER_SEC)
+ );
+
+ // Setup transition start point.
+ div.style.transition = 'opacity 100s';
+ getComputedStyle(div).opacity;
+
+ // Update specified style but don't flush style.
+ div.style.opacity = '0.5';
+
+ // Fetch animations
+ document.getAnimations();
+
+ // Play the covering animation to ensure that only the call to
+ // getAnimations() has a chance to trigger transitions.
+ coveringAnimation.play();
+
+ // If getAnimations() flushed style, we should get a transitionrun event.
+ await watcher.wait_for('transitionrun');
+}, 'Document.getAnimations() triggers a style change event');
+
+</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 0000000000..ca0997ac8f
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/interfaces/DocumentTimeline/constructor.html
@@ -0,0 +1,43 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title>DocumentTimeline constructor tests</title>
+<link rel="help" href="https://drafts.csswg.org/web-animations/#the-documenttimeline-interface">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../../testcommon.js"></script>
+<script src="../../resources/timing-override.js"></script>
+<body>
+<div id="log"></div>
+<script>
+'use strict';
+
+test(t => {
+ const timeline = new DocumentTimeline();
+
+ assert_times_equal(timeline.currentTime, document.timeline.currentTime);
+}, 'An origin time of zero is used when none is supplied');
+
+test(t => {
+ const 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(t => {
+ const 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(t => {
+ const 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/DocumentTimeline/style-change-events.html b/testing/web-platform/tests/web-animations/interfaces/DocumentTimeline/style-change-events.html
new file mode 100644
index 0000000000..c1607e6fb9
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/interfaces/DocumentTimeline/style-change-events.html
@@ -0,0 +1,92 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>DocumentTimeline interface: style change events</title>
+<link rel="help"
+ href="https://drafts.csswg.org/web-animations-1/#model-liveness">
+<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: If more members are added to the DocumentTimeline interface it might be
+// better to rewrite these test in the same style as:
+//
+// web-animations/interfaces/Animation/style-change-events.html
+// web-animations/interfaces/KeyframeEffect/style-change-events.html
+
+promise_test(async t => {
+ const div = createDiv(t);
+
+ let gotTransition = false;
+ div.addEventListener('transitionrun', () => {
+ gotTransition = true;
+ });
+
+ // Create a covering animation but don't play it yet.
+ const coveringAnimation = new Animation(
+ new KeyframeEffect(div, { opacity: [0, 1] }, 100 * MS_PER_SEC)
+ );
+
+ // Setup transition start point.
+ div.style.transition = 'opacity 100s';
+ getComputedStyle(div).opacity;
+
+ // Update specified style but don't flush style.
+ div.style.opacity = '0.5';
+
+ // Get the currentTime
+ document.timeline.currentTime;
+
+ // Run the covering animation
+ coveringAnimation.play();
+
+ // If getting DocumentTimeline.currentTime produced a style change event it
+ // will trigger a transition. Otherwise, the covering animation will cause
+ // the before-change and after-change styles to be the same such that no
+ // transition is triggered on the next restyle.
+
+ // Wait for a couple of animation frames to give the transitionrun event
+ // a chance to be dispatched.
+ await waitForAnimationFrames(2);
+
+ assert_false(gotTransition, 'A transition should NOT have been triggered');
+}, 'DocumentTimeline.currentTime does NOT trigger a style change event');
+
+promise_test(async t => {
+ const div = createDiv(t);
+
+ let gotTransition = false;
+ div.addEventListener('transitionrun', () => {
+ gotTransition = true;
+ });
+
+ // Create a covering animation but don't play it yet.
+ const coveringAnimation = new Animation(
+ new KeyframeEffect(div, { opacity: [0, 1] }, 100 * MS_PER_SEC)
+ );
+
+ // Setup transition start point.
+ div.style.transition = 'opacity 100s';
+ getComputedStyle(div).opacity;
+
+ // Update specified style but don't flush style.
+ div.style.opacity = '0.5';
+
+ // Create a new DocumentTimeline
+ new DocumentTimeline();
+
+ // Run the covering animation
+ coveringAnimation.play();
+
+ // Wait for a couple of animation frames to give the transitionrun event
+ // a chance to be dispatched.
+ await waitForAnimationFrames(2);
+
+ assert_false(gotTransition, 'A transition should NOT have been triggered');
+}, 'DocumentTimeline constructor does NOT trigger a style change event');
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/web-animations/interfaces/KeyframeEffect/composite.html b/testing/web-platform/tests/web-animations/interfaces/KeyframeEffect/composite.html
new file mode 100644
index 0000000000..bcca2cad24
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/interfaces/KeyframeEffect/composite.html
@@ -0,0 +1,47 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>KeyframeEffect.composite</title>
+<link rel="help"
+ href="https://drafts.csswg.org/web-animations/#dom-keyframeeffect-composite">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../../testcommon.js"></script>
+<body>
+<div id="log"></div>
+<script>
+'use strict';
+
+test(t => {
+ const anim = createDiv(t).animate(null);
+ assert_equals(anim.effect.composite, 'replace',
+ 'The default value should be replace');
+}, 'Default value');
+
+test(t => {
+ const anim = createDiv(t).animate(null);
+ anim.effect.composite = 'add';
+ assert_equals(anim.effect.composite, 'add',
+ 'The effect composite value should be replaced');
+}, 'Change composite value');
+
+test(t => {
+ const anim = createDiv(t).animate({ left: '10px' });
+
+ anim.effect.composite = 'add';
+ const keyframes = anim.effect.getKeyframes();
+ assert_equals(keyframes[0].composite, 'auto',
+ 'unspecified keyframe composite value should be auto even ' +
+ 'if effect composite is set');
+}, 'Unspecified keyframe composite value when setting effect composite');
+
+test(t => {
+ const anim = createDiv(t).animate({ left: '10px', composite: 'replace' });
+
+ anim.effect.composite = 'add';
+ const keyframes = anim.effect.getKeyframes();
+ assert_equals(keyframes[0].composite, 'replace',
+ 'specified keyframe composite value should not be overridden ' +
+ 'by setting effect composite');
+}, 'Specified keyframe composite value when setting effect composite');
+
+</script>
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 0000000000..f9d552e63e
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/interfaces/KeyframeEffect/constructor.html
@@ -0,0 +1,195 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title>KeyframeEffect constructor</title>
+<link rel="help"
+ href="https://drafts.csswg.org/web-animations/#dom-keyframeeffect-keyframeeffect">
+<link rel="help"
+ href="https://drafts.csswg.org/web-animations/#dom-keyframeeffectreadonly-keyframeeffectreadonly">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../../testcommon.js"></script>
+<script src="../../resources/easing-tests.js"></script>
+<script src="../../resources/keyframe-utils.js"></script>
+<script src="../../resources/keyframe-tests.js"></script>
+<body>
+<div id="log"></div>
+<div id="target"></div>
+<script>
+'use strict';
+
+const target = document.getElementById('target');
+
+test(t => {
+ for (const frames of gEmptyKeyframeListTests) {
+ assert_equals(new KeyframeEffect(target, frames).getKeyframes().length,
+ 0, `number of frames for ${JSON.stringify(frames)}`);
+ }
+}, 'A KeyframeEffect can be constructed with no frames');
+
+test(t => {
+ for (const subtest of gEasingParsingTests) {
+ const easing = subtest[0];
+ const expected = subtest[1];
+ const effect = new KeyframeEffect(target, {
+ left: ['10px', '20px']
+ }, { easing: easing });
+ assert_equals(effect.getTiming().easing, expected,
+ `resulting easing for '${easing}'`);
+ }
+}, 'easing values are parsed correctly when passed to the ' +
+ 'KeyframeEffect constructor in KeyframeEffectOptions');
+
+test(t => {
+ for (const invalidEasing of gInvalidEasings) {
+ assert_throws_js(TypeError, () => {
+ new KeyframeEffect(target, null, { easing: invalidEasing });
+ }, `TypeError is thrown for easing '${invalidEasing}'`);
+ }
+}, 'Invalid easing values are correctly rejected when passed to the ' +
+ 'KeyframeEffect constructor in KeyframeEffectOptions');
+
+test(t => {
+ const getKeyframe =
+ composite => ({ left: [ '10px', '20px' ], composite: composite });
+ for (const composite of gGoodKeyframeCompositeValueTests) {
+ const effect = new KeyframeEffect(target, getKeyframe(composite));
+ assert_equals(effect.getKeyframes()[0].composite, composite,
+ `resulting composite for '${composite}'`);
+ }
+ for (const composite of gBadKeyframeCompositeValueTests) {
+ assert_throws_js(TypeError, () => {
+ new KeyframeEffect(target, getKeyframe(composite));
+ });
+ }
+}, 'composite values are parsed correctly when passed to the ' +
+ 'KeyframeEffect constructor in property-indexed keyframes');
+
+test(t => {
+ const getKeyframes = composite =>
+ [
+ { offset: 0, left: '10px', composite: composite },
+ { offset: 1, left: '20px' }
+ ];
+ for (const composite of gGoodKeyframeCompositeValueTests) {
+ const effect = new KeyframeEffect(target, getKeyframes(composite));
+ assert_equals(effect.getKeyframes()[0].composite, composite,
+ `resulting composite for '${composite}'`);
+ }
+ for (const composite of gBadKeyframeCompositeValueTests) {
+ assert_throws_js(TypeError, () => {
+ new KeyframeEffect(target, getKeyframes(composite));
+ });
+ }
+}, 'composite values are parsed correctly when passed to the ' +
+ 'KeyframeEffect constructor in regular keyframes');
+
+test(t => {
+ for (const composite of gGoodOptionsCompositeValueTests) {
+ const effect = new KeyframeEffect(target, {
+ left: ['10px', '20px']
+ }, { composite });
+ assert_equals(effect.getKeyframes()[0].composite, 'auto',
+ `resulting composite for '${composite}'`);
+ }
+ for (const composite of gBadOptionsCompositeValueTests) {
+ assert_throws_js(TypeError, () => {
+ new KeyframeEffect(target, {
+ left: ['10px', '20px']
+ }, { composite: composite });
+ });
+ }
+}, 'composite value is auto if the composite operation specified on the ' +
+ 'keyframe effect is being used');
+
+for (const subtest of gKeyframesTests) {
+ test(t => {
+ const effect = new KeyframeEffect(target, subtest.input);
+ assert_frame_lists_equal(effect.getKeyframes(), subtest.output);
+ }, `A KeyframeEffect can be constructed with ${subtest.desc}`);
+
+ test(t => {
+ const effect = new KeyframeEffect(target, subtest.input);
+ const secondEffect = new KeyframeEffect(target, effect.getKeyframes());
+ assert_frame_lists_equal(secondEffect.getKeyframes(),
+ effect.getKeyframes());
+ }, `A KeyframeEffect constructed with ${subtest.desc} roundtrips`);
+}
+
+for (const subtest of gInvalidKeyframesTests) {
+ test(t => {
+ assert_throws_js(TypeError, () => {
+ new KeyframeEffect(target, subtest.input);
+ });
+ }, `KeyframeEffect constructor throws with ${subtest.desc}`);
+}
+
+test(t => {
+ const effect = new KeyframeEffect(target, { left: ['10px', '20px'] });
+
+ const timing = effect.getTiming();
+ 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');
+}, 'A KeyframeEffect constructed without any KeyframeEffectOptions object');
+
+for (const subtest of gKeyframeEffectOptionTests) {
+ test(t => {
+ const effect = new KeyframeEffect(target, { left: ['10px', '20px'] },
+ subtest.input);
+
+ // Helper function to provide default expected values when the test does
+ // not supply them.
+ const expected = (field, defaultValue) => {
+ return field in subtest.expected ? subtest.expected[field] : defaultValue;
+ };
+
+ const timing = effect.getTiming();
+ 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 KeyframeEffect constructed by ${subtest.desc}`);
+}
+
+for (const subtest of gInvalidKeyframeEffectOptionTests) {
+ test(t => {
+ assert_throws_js(TypeError, () => {
+ new KeyframeEffect(target, { left: ['10px', '20px'] }, subtest.input);
+ });
+ }, `Invalid KeyframeEffect option by ${subtest.desc}`);
+}
+
+test(t => {
+ const effect = new KeyframeEffect(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 KeyframeEffect constructed with null target');
+
+test(t => {
+ const test_error = { name: 'test' };
+
+ assert_throws_exactly(test_error, () => {
+ 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-constructor.html b/testing/web-platform/tests/web-animations/interfaces/KeyframeEffect/copy-constructor.html
new file mode 100644
index 0000000000..e3bc0db00a
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/interfaces/KeyframeEffect/copy-constructor.html
@@ -0,0 +1,93 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title>KeyframeEffect copy constructor</title>
+<link rel="help"
+ href="https://drafts.csswg.org/web-animations/#dom-keyframeeffect-keyframeeffect-source">
+<link rel="help"
+ href="https://drafts.csswg.org/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(t => {
+ const effect = new KeyframeEffect(createDiv(t), null);
+ const copiedEffect = new KeyframeEffect(effect);
+ assert_equals(copiedEffect.target, effect.target, 'same target');
+}, 'Copied KeyframeEffect has the same target');
+
+test(t => {
+ const effect =
+ new KeyframeEffect(null,
+ [ { marginLeft: '0px' },
+ { marginLeft: '-20px', easing: 'ease-in',
+ offset: 0.1 },
+ { marginLeft: '100px', easing: 'ease-out' },
+ { marginLeft: '50px' } ]);
+
+ const copiedEffect = new KeyframeEffect(effect);
+ const keyframesA = effect.getKeyframes();
+ const keyframesB = copiedEffect.getKeyframes();
+ assert_equals(keyframesA.length, keyframesB.length, 'same keyframes length');
+
+ for (let 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 a valid property value`);
+ assert_true(!!keyframesB[i].marginLeft,
+ `New keyframe ${i} has a valid property value`);
+ assert_equals(keyframesA[i].marginLeft, keyframesB[i].marginLeft,
+ `Keyframe ${i} has the same property value pair`);
+ }
+}, 'Copied KeyframeEffect has the same keyframes');
+
+test(t => {
+ const effect =
+ new KeyframeEffect(null, null, { iterationComposite: 'accumulate' });
+
+ const copiedEffect = new KeyframeEffect(effect);
+ assert_equals(copiedEffect.iterationComposite, effect.iterationComposite,
+ 'same iterationCompositeOperation');
+ assert_equals(copiedEffect.composite, effect.composite,
+ 'same compositeOperation');
+}, 'Copied KeyframeEffect has the same KeyframeEffectOptions');
+
+test(t => {
+ const effect = new KeyframeEffect(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' } );
+
+ const copiedEffect = new KeyframeEffect(effect);
+ const timingA = effect.getTiming();
+ const timingB = copiedEffect.getTiming();
+ 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');
+}, 'Copied KeyframeEffect has the same timing content');
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/web-animations/interfaces/KeyframeEffect/getKeyframes.html b/testing/web-platform/tests/web-animations/interfaces/KeyframeEffect/getKeyframes.html
new file mode 100644
index 0000000000..1f8d267e4a
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/interfaces/KeyframeEffect/getKeyframes.html
@@ -0,0 +1,25 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title>KeyframeEffect getKeyframes()</title>
+<link rel="help"
+ href="https://drafts.csswg.org/web-animations/#dom-keyframeeffect-getkeyframes">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../../resources/keyframe-utils.js"></script>
+<script src="../../resources/keyframe-tests.js"></script>
+<body>
+<div id="log"></div>
+<div id="target"></div>
+<script>
+'use strict';
+
+const target = document.getElementById('target');
+
+
+for (const subtest of gKeyframeSerializationTests) {
+ test(t => {
+ const effect = new KeyframeEffect(target, subtest.input);
+ assert_frame_lists_equal(effect.getKeyframes(), subtest.output);
+ }, `getKeyframes() should serialize its css values with ${subtest.desc}`);
+}
+</script> \ No newline at end of file
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 0000000000..bbb8ee2a32
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/interfaces/KeyframeEffect/iterationComposite.html
@@ -0,0 +1,36 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>KeyframeEffect.iterationComposite</title>
+<link rel="help" href="https://drafts.csswg.org/web-animations/#dom-keyframeeffect-iterationcomposite">
+<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(t => {
+ const div = createDiv(t);
+ const anim = div.animate({ marginLeft: ['0px', '10px'] },
+ { duration: 100 * MS_PER_SEC,
+ easing: 'linear',
+ iterations: 10,
+ iterationComposite: 'accumulate' });
+ anim.pause();
+
+ anim.currentTime =
+ anim.effect.getComputedTiming().duration * 2 +
+ anim.effect.getComputedTiming().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');
+}, 'iterationComposite can be updated while an animation is in progress');
+
+</script>
diff --git a/testing/web-platform/tests/web-animations/interfaces/KeyframeEffect/processing-a-keyframes-argument-001.html b/testing/web-platform/tests/web-animations/interfaces/KeyframeEffect/processing-a-keyframes-argument-001.html
new file mode 100644
index 0000000000..271a47b301
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/interfaces/KeyframeEffect/processing-a-keyframes-argument-001.html
@@ -0,0 +1,602 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title>Processing a keyframes argument (property access)</title>
+<link rel="help" href="https://drafts.csswg.org/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';
+
+// This file only tests the KeyframeEffect 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
+
+const 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',
+ 'contain',
+ 'direction',
+ 'display',
+ 'textCombineUpright',
+ 'textOrientation',
+ 'unicodeBidi',
+ 'willChange',
+ 'writingMode',
+
+ 'unsupportedProperty',
+
+ 'float', // We use the string "cssFloat" to represent "float" property, and
+ // so reject "float" in the keyframe-like object.
+ 'font-size', // Supported property that uses dashes
+];
+
+function TestKeyframe(testProp) {
+ let _propAccessCount = 0;
+
+ Object.defineProperty(this, testProp, {
+ get: () => { _propAccessCount++; },
+ enumerable: true,
+ });
+
+ Object.defineProperty(this, 'propAccessCount', {
+ get: () => _propAccessCount
+ });
+}
+
+function GetTestKeyframeSequence(testProp) {
+ return [ new TestKeyframe(testProp) ]
+}
+
+for (const prop of gNonAnimatableProps) {
+ test(() => {
+ const testKeyframe = new TestKeyframe(prop);
+
+ new KeyframeEffect(null, testKeyframe);
+
+ assert_equals(testKeyframe.propAccessCount, 0, 'Accessor not called');
+ }, `non-animatable property '${prop}' is not accessed when using`
+ + ' a property-indexed keyframe object');
+}
+
+for (const prop of gNonAnimatableProps) {
+ test(() => {
+ const testKeyframes = GetTestKeyframeSequence(prop);
+
+ new KeyframeEffect(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) {
+ const processedKeyframesA =
+ new KeyframeEffect(null, keyframesA).getKeyframes();
+ const processedKeyframesB =
+ new KeyframeEffect(null, keyframesB).getKeyframes();
+ assert_frame_lists_equal(processedKeyframesA, processedKeyframesB);
+}
+
+const 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 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' },
+ ],
+ },
+];
+
+for (const {description, indexedKeyframes, sequencedKeyframes} of
+ gEquivalentSyntaxTests) {
+ test(() => {
+ assertEquivalentKeyframeSyntax(indexedKeyframes, sequencedKeyframes);
+ }, `Equivalent property-indexed and sequenced keyframes: ${description}`);
+}
+
+// Test handling of custom iterable objects.
+
+function createIterable(iterations) {
+ return {
+ [Symbol.iterator]() {
+ let i = 0;
+ return {
+ next() {
+ return iterations[i++];
+ },
+ };
+ },
+ };
+}
+
+test(() => {
+ const 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',
+ composite: 'auto',
+ },
+ {
+ offset: null,
+ computedOffset: 0.5,
+ easing: 'linear',
+ left: '300px',
+ composite: 'auto',
+ },
+ {
+ offset: null,
+ computedOffset: 1,
+ easing: 'linear',
+ left: '200px',
+ composite: 'auto',
+ },
+ ]);
+}, 'Keyframes are read from a custom iterator');
+
+test(() => {
+ const 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';
+ const effect = new KeyframeEffect(null, keyframes);
+ assert_frame_lists_equal(effect.getKeyframes(), [
+ {
+ offset: null,
+ computedOffset: 0,
+ easing: 'linear',
+ left: '100px',
+ composite: 'auto',
+ },
+ {
+ offset: null,
+ computedOffset: 0.5,
+ easing: 'linear',
+ left: '300px',
+ composite: 'auto',
+ },
+ {
+ offset: null,
+ computedOffset: 1,
+ easing: 'linear',
+ left: '200px',
+ composite: 'auto',
+ },
+ ]);
+}, '\'easing\' and \'offset\' are ignored on iterable objects');
+
+test(() => {
+ const 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',
+ composite: 'auto',
+ },
+ {
+ offset: null,
+ computedOffset: 0.5,
+ easing: 'linear',
+ left: '300px',
+ composite: 'auto',
+ },
+ {
+ offset: null,
+ computedOffset: 1,
+ easing: 'linear',
+ left: '200px',
+ top: '100px',
+ composite: 'auto',
+ },
+ ]);
+}, 'Keyframes are read from a custom iterator with multiple properties'
+ + ' specified');
+
+test(() => {
+ const 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',
+ composite: 'auto',
+ },
+ {
+ offset: 0.75,
+ computedOffset: 0.75,
+ easing: 'linear',
+ left: '250px',
+ composite: 'auto',
+ },
+ {
+ offset: null,
+ computedOffset: 1,
+ easing: 'linear',
+ left: '200px',
+ composite: 'auto',
+ },
+ ]);
+}, 'Keyframes are read from a custom iterator with where an offset is'
+ + ' specified');
+
+test(() => {
+ const test_error = { name: 'test' };
+ const bad_keyframe = { get left() { throw test_error; } };
+ assert_throws_exactly(test_error, () => {
+ new KeyframeEffect(null, createIterable([
+ { done: false, value: { left: '100px' } },
+ { done: false, value: bad_keyframe },
+ { done: false, value: { left: '200px' } },
+ { done: true },
+ ]));
+ });
+}, 'If a keyframe throws for an animatable property, that exception should be'
+ + ' propagated');
+
+test(() => {
+ assert_throws_js(TypeError, () => {
+ new KeyframeEffect(null, createIterable([
+ { done: false, value: { left: '100px' } },
+ { done: false, value: 1234 },
+ { done: false, value: { left: '200px' } },
+ { done: true },
+ ]));
+ });
+}, 'Reading from a custom iterator that returns a non-object keyframe'
+ + ' should throw');
+
+test(() => {
+ assert_throws_js(TypeError, () => {
+ new KeyframeEffect(null, createIterable([
+ { done: false, value: { left: '100px', easing: '' } },
+ { done: false, value: 1234 },
+ { done: false, value: { left: '200px' } },
+ { done: true },
+ ]));
+ });
+}, 'Reading from a custom iterator that returns a non-object keyframe'
+ + ' and an invalid easing should throw');
+
+test(() => {
+ assert_throws_js(TypeError, () => {
+ new KeyframeEffect(null, createIterable([
+ { done: false, value: { left: '100px' } },
+ { done: false, value: { left: '150px', offset: 'o' } },
+ { done: false, value: { left: '200px' } },
+ { done: true },
+ ]));
+ });
+}, 'Reading from a custom iterator that returns a keyframe with a non finite'
+ + ' floating-point offset value should throw');
+
+test(() => {
+ assert_throws_js(TypeError, () => {
+ new KeyframeEffect(null, createIterable([
+ { done: false, value: { left: '100px', easing: '' } },
+ { done: false, value: { left: '150px', offset: 'o' } },
+ { done: false, value: { left: '200px' } },
+ { done: true },
+ ]));
+ });
+}, 'Reading from a custom iterator that returns a keyframe with a non finite'
+ + ' floating-point offset value and an invalid easing should throw');
+
+test(() => {
+ const effect = new KeyframeEffect(null, createIterable([
+ { done: false, value: { left: '100px' } },
+ { done: false }, // No value member; keyframe is undefined.
+ { done: false, value: { left: '200px' } },
+ { done: true },
+ ]));
+ assert_frame_lists_equal(effect.getKeyframes(), [
+ { left: '100px', offset: null, computedOffset: 0, easing: 'linear', composite: 'auto' },
+ { offset: null, computedOffset: 0.5, easing: 'linear', composite: 'auto' },
+ { left: '200px', offset: null, computedOffset: 1, easing: 'linear', composite: 'auto' },
+ ]);
+}, 'An undefined keyframe returned from a custom iterator should be treated as a'
+ + ' default keyframe');
+
+test(() => {
+ const effect = new KeyframeEffect(null, createIterable([
+ { done: false, value: { left: '100px' } },
+ { done: false, value: null },
+ { done: false, value: { left: '200px' } },
+ { done: true },
+ ]));
+ assert_frame_lists_equal(effect.getKeyframes(), [
+ { left: '100px', offset: null, computedOffset: 0, easing: 'linear', composite: 'auto' },
+ { offset: null, computedOffset: 0.5, easing: 'linear', composite: 'auto' },
+ { left: '200px', offset: null, computedOffset: 1, easing: 'linear', composite: 'auto' },
+ ]);
+}, 'A null keyframe returned from a custom iterator should be treated as a'
+ + ' default keyframe');
+
+test(() => {
+ const 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', composite: 'auto' }
+ ]);
+}, 'A list of values returned from a custom iterator should be ignored');
+
+test(() => {
+ const test_error = { name: 'test' };
+ const keyframe_obj = {
+ [Symbol.iterator]() {
+ return { next() { throw test_error; } };
+ },
+ };
+ assert_throws_exactly(test_error, () => {
+ new KeyframeEffect(null, keyframe_obj);
+ });
+}, 'If a custom iterator throws from next(), the exception should be rethrown');
+
+// Test handling of invalid Symbol.iterator
+
+test(() => {
+ const test_error = { name: 'test' };
+ const keyframe_obj = {
+ [Symbol.iterator]() {
+ throw test_error;
+ },
+ };
+ assert_throws_exactly(test_error, () => {
+ new KeyframeEffect(null, keyframe_obj);
+ });
+}, 'Accessing a Symbol.iterator property that throws should rethrow');
+
+test(() => {
+ const keyframe_obj = {
+ [Symbol.iterator]() {
+ return 42; // Not an object.
+ },
+ };
+ assert_throws_js(TypeError, () => {
+ new KeyframeEffect(null, keyframe_obj);
+ });
+}, 'A non-object returned from the Symbol.iterator property should cause a'
+ + ' TypeError to be thrown');
+
+test(() => {
+ const 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');
+
+ const effect = new KeyframeEffect(null, [keyframe, { height: '200px' }]);
+
+ assert_frame_lists_equal(effect.getKeyframes(), [
+ {
+ offset: null,
+ computedOffset: 0,
+ easing: 'linear',
+ height: '100px',
+ composite: 'auto',
+ },
+ {
+ offset: null,
+ computedOffset: 1,
+ easing: 'linear',
+ height: '200px',
+ composite: 'auto',
+ },
+ ]);
+}, 'Only enumerable properties on keyframes are read');
+
+test(() => {
+ const KeyframeParent = function() { this.width = '100px'; };
+ KeyframeParent.prototype = { height: '100px' };
+ const Keyframe = function() { this.top = '100px'; };
+ Keyframe.prototype = Object.create(KeyframeParent.prototype);
+ Object.defineProperty(Keyframe.prototype, 'left', {
+ value: '100px',
+ enumerable: true,
+ });
+ const keyframe = new Keyframe();
+
+ const effect = new KeyframeEffect(null, [keyframe, { top: '200px' }]);
+
+ assert_frame_lists_equal(effect.getKeyframes(), [
+ {
+ offset: null,
+ computedOffset: 0,
+ easing: 'linear',
+ top: '100px',
+ composite: 'auto',
+ },
+ {
+ offset: null,
+ computedOffset: 1,
+ easing: 'linear',
+ top: '200px',
+ composite: 'auto',
+ },
+ ]);
+}, 'Only properties defined directly on keyframes are read');
+
+test(() => {
+ const keyframes = {};
+ Object.defineProperty(keyframes, 'width', ['100px', '200px']);
+ Object.defineProperty(keyframes, 'height', {
+ value: ['100px', '200px'],
+ enumerable: true,
+ });
+
+ const effect = new KeyframeEffect(null, keyframes);
+
+ assert_frame_lists_equal(effect.getKeyframes(), [
+ {
+ offset: null,
+ computedOffset: 0,
+ easing: 'linear',
+ height: '100px',
+ composite: 'auto',
+ },
+ {
+ offset: null,
+ computedOffset: 1,
+ easing: 'linear',
+ height: '200px',
+ composite: 'auto',
+ },
+ ]);
+}, 'Only enumerable properties on property-indexed keyframes are read');
+
+test(() => {
+ const KeyframesParent = function() { this.width = '100px'; };
+ KeyframesParent.prototype = { height: '100px' };
+ const Keyframes = function() { this.top = ['100px', '200px']; };
+ Keyframes.prototype = Object.create(KeyframesParent.prototype);
+ Object.defineProperty(Keyframes.prototype, 'left', {
+ value: ['100px', '200px'],
+ enumerable: true,
+ });
+ const keyframes = new Keyframes();
+
+ const effect = new KeyframeEffect(null, keyframes);
+
+ assert_frame_lists_equal(effect.getKeyframes(), [
+ {
+ offset: null,
+ computedOffset: 0,
+ easing: 'linear',
+ top: '100px',
+ composite: 'auto',
+ },
+ {
+ offset: null,
+ computedOffset: 1,
+ easing: 'linear',
+ top: '200px',
+ composite: 'auto',
+ },
+ ]);
+}, 'Only properties defined directly on property-indexed keyframes are read');
+
+test(() => {
+ const expectedOrder = ['composite', 'easing', 'offset', 'left', 'marginLeft'];
+ const actualOrder = [];
+ const kf1 = {};
+ for (const {prop, value} of [{ prop: 'marginLeft', value: '10px' },
+ { prop: 'left', value: '20px' },
+ { prop: 'offset', value: '0' },
+ { prop: 'easing', value: 'linear' },
+ { prop: 'composite', value: 'replace' }]) {
+ Object.defineProperty(kf1, prop, {
+ enumerable: true,
+ get: () => { actualOrder.push(prop); return value; }
+ });
+ }
+ const kf2 = { marginLeft: '10px', left: '20px', offset: 1 };
+
+ new KeyframeEffect(target, [kf1, kf2]);
+
+ assert_array_equals(actualOrder, expectedOrder, 'property access order');
+}, 'Properties are read in ascending order by Unicode codepoint');
+
+</script>
diff --git a/testing/web-platform/tests/web-animations/interfaces/KeyframeEffect/processing-a-keyframes-argument-002.html b/testing/web-platform/tests/web-animations/interfaces/KeyframeEffect/processing-a-keyframes-argument-002.html
new file mode 100644
index 0000000000..8620f883f9
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/interfaces/KeyframeEffect/processing-a-keyframes-argument-002.html
@@ -0,0 +1,125 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title>Processing a keyframes argument (easing)</title>
+<link rel="help" href="https://drafts.csswg.org/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/easing-tests.js"></script>
+<body>
+<div id="log"></div>
+<div id="target"></div>
+<script>
+'use strict';
+
+test(() => {
+ for (const [easing, expected] of gEasingParsingTests) {
+ const effect = new KeyframeEffect(target, {
+ left: ['10px', '20px'],
+ easing: easing
+ });
+ assert_equals(effect.getKeyframes()[0].easing, expected,
+ `resulting easing for '${easing}'`);
+ }
+}, 'easing values are parsed correctly when set on a property-indexed'
+ + ' keyframe');
+
+test(() => {
+ for (const [easing, expected] of gEasingParsingTests) {
+ const effect = new KeyframeEffect(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 using a keyframe sequence');
+
+test(() => {
+ for (const invalidEasing of gInvalidEasings) {
+ assert_throws_js(TypeError, () => {
+ new KeyframeEffect(target, { easing: invalidEasing });
+ }, `TypeError is thrown for easing '${invalidEasing}'`);
+ }
+}, 'Invalid easing values are correctly rejected when set on a property-'
+ + 'indexed keyframe');
+
+test(() => {
+ for (const invalidEasing of gInvalidEasings) {
+ assert_throws_js(TypeError, () => {
+ new KeyframeEffect(target, [{ easing: invalidEasing }]);
+ }, `TypeError is thrown for easing '${invalidEasing}'`);
+ }
+}, 'Invalid easing values are correctly rejected when using a keyframe'
+ + ' sequence');
+
+test(() => {
+ let readToEnd = false;
+ const keyframe_obj = {
+ *[Symbol.iterator]() {
+ yield { left: '100px', easing: '' };
+ yield { left: '200px' };
+ readToEnd = true;
+ },
+ };
+ assert_throws_js(
+ TypeError,
+ () => {
+ new KeyframeEffect(null, keyframe_obj);
+ },
+ 'TypeError is thrown for an invalid easing'
+ );
+ assert_true(
+ readToEnd,
+ 'Read all the keyframe properties before reporting invalid easing'
+ );
+}, 'Invalid easing values are correctly rejected after doing all the'
+ + ' iterating');
+
+test(() => {
+ let propAccessCount = 0;
+ const keyframe = {};
+ const addProp = prop => {
+ Object.defineProperty(keyframe, prop, {
+ get: () => { propAccessCount++; },
+ enumerable: true
+ });
+ }
+ addProp('height');
+ addProp('width');
+ keyframe.easing = 'easy-peasy';
+
+ assert_throws_js(TypeError, () => {
+ new KeyframeEffect(target, keyframe);
+ });
+ assert_equals(propAccessCount, 2,
+ 'All properties were read before throwing the easing error');
+}, 'Errors from invalid easings on a property-indexed keyframe are thrown after reading all properties');
+
+test(() => {
+ let propAccessCount = 0;
+
+ const addProp = (keyframe, prop) => {
+ Object.defineProperty(keyframe, prop, {
+ get: () => { propAccessCount++; },
+ enumerable: true
+ });
+ }
+
+ const kf1 = {};
+ addProp(kf1, 'height');
+ addProp(kf1, 'width');
+ kf1.easing = 'easy-peasy';
+
+ const kf2 = {};
+ addProp(kf2, 'height');
+ addProp(kf2, 'width');
+
+ assert_throws_js(TypeError, () => {
+ new KeyframeEffect(target, [ kf1, kf2 ]);
+ });
+ assert_equals(propAccessCount, 4,
+ 'All properties were read before throwing the easing error');
+}, 'Errors from invalid easings on a keyframe sequence are thrown after reading all properties');
+
+</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 0000000000..a5c81a29bd
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/interfaces/KeyframeEffect/setKeyframes.html
@@ -0,0 +1,55 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title>KeyframeEffect.setKeyframes</title>
+<link rel="help" href="https://drafts.csswg.org/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>
+<script src="../../resources/keyframe-tests.js"></script>
+<body>
+<div id="log"></div>
+<div id="target"></div>
+<script>
+'use strict';
+
+const target = document.getElementById('target');
+
+test(t => {
+ for (const frame of gEmptyKeyframeListTests) {
+ const effect = new KeyframeEffect(target, {});
+ effect.setKeyframes(frame);
+ assert_frame_lists_equal(effect.getKeyframes(), []);
+ }
+}, 'Keyframes can be replaced with an empty keyframe');
+
+for (const subtest of gKeyframesTests) {
+ test(t => {
+ const effect = new KeyframeEffect(target, {});
+ effect.setKeyframes(subtest.input);
+ assert_frame_lists_equal(effect.getKeyframes(), subtest.output);
+ }, `Keyframes can be replaced with ${subtest.desc}`);
+}
+
+for (const subtest of gInvalidKeyframesTests) {
+ test(t => {
+ const effect = new KeyframeEffect(target, {});
+ assert_throws_js(TypeError, () => {
+ effect.setKeyframes(subtest.input);
+ });
+ }, `KeyframeEffect constructor throws with ${subtest.desc}`);
+}
+
+test(t => {
+ const frames1 = [ { left: '100px' }, { left: '200px' } ];
+ const frames2 = [ { left: '200px' }, { left: '300px' } ];
+
+ const animation = target.animate(frames1, 1000);
+ animation.currentTime = 500;
+ assert_equals(getComputedStyle(target).left, "150px");
+
+ animation.effect.setKeyframes(frames2);
+ assert_equals(getComputedStyle(target).left, "250px");
+}, 'Changes made via setKeyframes should be immediately visible in style');
+</script>
+</body>
diff --git a/testing/web-platform/tests/web-animations/interfaces/KeyframeEffect/style-change-events.html b/testing/web-platform/tests/web-animations/interfaces/KeyframeEffect/style-change-events.html
new file mode 100644
index 0000000000..eecf170cd9
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/interfaces/KeyframeEffect/style-change-events.html
@@ -0,0 +1,242 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>KeyframeEffect interface: style change events</title>
+<link rel="help"
+ href="https://drafts.csswg.org/web-animations-1/#model-liveness">
+<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 that each property defined in the KeyframeEffect interface does not
+// produce style change events.
+//
+// There are two types of tests:
+//
+// MakeInEffectTest
+//
+// For properties that are able to cause the KeyframeEffect to start
+// affecting the CSS 'opacity' property.
+//
+// This function takes either:
+//
+// (a) A function that makes the passed-in KeyframeEffect affect the
+// 'opacity' property.
+//
+// (b) An object with the following format:
+//
+// {
+// setup: elem => { /* return Animation */ }
+// test: effect => { /* make |effect| affect 'opacity' */ }
+// }
+//
+// If the latter form is used, the setup function should return an Animation
+// whose KeyframeEffect does NOT (yet) affect the 'opacity' property (or is
+// NOT yet in-effect). Otherwise, the transition we use to detect if a style
+// change event has occurred will never have a chance to be triggered (since
+// the animated style will clobber both before-change and after-change
+// style).
+//
+// UsePropertyTest
+//
+// For properties that cannot cause the KeyframeEffect to start affecting the
+// CSS 'opacity' property.
+//
+// The shape of the parameter to the UsePropertyTest is identical to the
+// MakeInEffectTest. The only difference is that the function (or 'test'
+// function of the object format is used) does not need to make the
+// KeyframeEffect affect the CSS 'opacity' property, but simply needs to
+// get/set the property under test.
+
+const MakeInEffectTest = testFuncOrObj => {
+ let test, setup;
+
+ if (typeof testFuncOrObj === 'function') {
+ test = testFuncOrObj;
+ } else {
+ test = testFuncOrObj.test;
+ if (typeof testFuncOrObj.setup === 'function') {
+ setup = testFuncOrObj.setup;
+ }
+ }
+
+ if (!setup) {
+ setup = elem =>
+ elem.animate({ color: ['blue', 'green'] }, 100 * MS_PER_SEC);
+ }
+
+ return { test, setup };
+};
+
+const UsePropertyTest = testFuncOrObj => {
+ const { test, setup } = MakeInEffectTest(testFuncOrObj);
+
+ let coveringAnimation;
+ return {
+ setup: elem => {
+ coveringAnimation = new Animation(
+ new KeyframeEffect(elem, { opacity: [0, 1] }, 100 * MS_PER_SEC)
+ );
+
+ return setup(elem);
+ },
+ test: effect => {
+ test(effect);
+ coveringAnimation.play();
+ },
+ };
+};
+
+const tests = {
+ getTiming: UsePropertyTest(effect => effect.getTiming()),
+ getComputedTiming: UsePropertyTest(effect => effect.getComputedTiming()),
+ updateTiming: MakeInEffectTest({
+ // Initially put the effect in its before phase (with no fill mode)...
+ setup: elem =>
+ elem.animate(
+ { opacity: [0.5, 1] },
+ {
+ duration: 100 * MS_PER_SEC,
+ delay: 100 * MS_PER_SEC,
+ }
+ ),
+ // ... so that when the delay is removed, it begins to affect the opacity.
+ test: effect => {
+ effect.updateTiming({ delay: 0 });
+ },
+ }),
+ get target() {
+ let targetElem;
+ return MakeInEffectTest({
+ setup: (elem, t) => {
+ targetElem = elem;
+ const targetB = createDiv(t);
+ return targetB.animate({ opacity: [0.5, 1] }, 100 * MS_PER_SEC);
+ },
+ test: effect => {
+ effect.target = targetElem;
+ },
+ });
+ },
+ pseudoElement: MakeInEffectTest({
+ setup: elem => elem.animate(
+ {opacity: [0.5, 1]},
+ {duration: 100 * MS_PER_SEC, pseudoElement: '::before'}
+ ),
+ test: effect => {
+ effect.pseudoElement = null;
+ },
+ }),
+ iterationComposite: UsePropertyTest(effect => {
+ // Get iterationComposite
+ effect.iterationComposite;
+
+ // Set iterationComposite
+ effect.iterationComposite = 'accumulate';
+ }),
+ composite: UsePropertyTest(effect => {
+ // Get composite
+ effect.composite;
+
+ // Set composite
+ effect.composite = 'add';
+ }),
+ getKeyframes: UsePropertyTest(effect => effect.getKeyframes()),
+ setKeyframes: MakeInEffectTest(effect =>
+ effect.setKeyframes({ opacity: [0.5, 1] })
+ ),
+ get ['KeyframeEffect constructor']() {
+ let originalElem;
+ let animation;
+ return UsePropertyTest({
+ setup: elem => {
+ originalElem = elem;
+ // Return a dummy animation so the caller has something to wait on
+ return elem.animate(null);
+ },
+ test: () =>
+ new KeyframeEffect(
+ originalElem,
+ { opacity: [0.5, 1] },
+ 100 * MS_PER_SEC
+ ),
+ });
+ },
+ get ['KeyframeEffect copy constructor']() {
+ let effectToClone;
+ return UsePropertyTest({
+ setup: elem => {
+ effectToClone = new KeyframeEffect(
+ elem,
+ { opacity: [0.5, 1] },
+ 100 * MS_PER_SEC
+ );
+ // Return a dummy animation so the caller has something to wait on
+ return elem.animate(null);
+ },
+ test: () => new KeyframeEffect(effectToClone),
+ });
+ },
+};
+
+// Check that each enumerable property and the constructors follow the
+// expected behavior with regards to triggering style change events.
+const properties = [
+ ...Object.keys(AnimationEffect.prototype),
+ ...Object.keys(KeyframeEffect.prototype),
+ 'KeyframeEffect constructor',
+ 'KeyframeEffect copy constructor',
+];
+
+test(() => {
+ for (const property of Object.keys(tests)) {
+ assert_in_array(
+ property,
+ properties,
+ `Test property '${property}' should be one of the properties on ` +
+ ' KeyframeEffect'
+ );
+ }
+}, 'All property keys are recognized');
+
+for (const key of properties) {
+ promise_test(async t => {
+ assert_own_property(tests, key, `Should have a test for '${key}' property`);
+ const { setup, test } = tests[key];
+
+ // Setup target element
+ const div = createDiv(t);
+ let gotTransition = false;
+ div.addEventListener('transitionrun', () => {
+ gotTransition = true;
+ });
+
+ // Setup animation
+ const animation = setup(div, t);
+
+ // Setup transition start point
+ div.style.transition = 'opacity 100s';
+ getComputedStyle(div).opacity;
+
+ // Update specified style but don't flush
+ div.style.opacity = '0.5';
+
+ // Trigger the property
+ test(animation.effect);
+
+ // If the test function produced a style change event it will have triggered
+ // a transition.
+
+ // Wait for the animation to start and then for at least one animation
+ // frame to give the transitionrun event a chance to be dispatched.
+ await animation.ready;
+ await waitForAnimationFrames(1);
+
+ assert_false(gotTransition, 'A transition should NOT have been triggered');
+ }, `KeyframeEffect.${key} does NOT trigger a style change event`);
+}
+</script>
+</body>
diff --git a/testing/web-platform/tests/web-animations/interfaces/KeyframeEffect/target.html b/testing/web-platform/tests/web-animations/interfaces/KeyframeEffect/target.html
new file mode 100644
index 0000000000..58d17b98d5
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/interfaces/KeyframeEffect/target.html
@@ -0,0 +1,271 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title>KeyframeEffect.target and .pseudoElement</title>
+<link rel="help"
+ href="https://drafts.csswg.org/web-animations/#dom-keyframeeffect-target">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../../testcommon.js"></script>
+<style>
+ .before::before {content: 'foo'; display: inline-block;}
+ .after::after {content: 'bar'; display: inline-block;}
+ .pseudoa::before, .pseudoc::before {margin-left: 10px;}
+ .pseudob::before, .pseudoc::after {margin-left: 20px;}
+</style>
+<body>
+<div id="log"></div>
+<script>
+'use strict';
+
+const gKeyFrames = { 'marginLeft': ['0px', '100px'] };
+
+test(t => {
+ const div = createDiv(t);
+ const effect = new KeyframeEffect(null, gKeyFrames, 100 * MS_PER_SEC);
+ effect.target = div;
+
+ const 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(t => {
+ const div = createDiv(t);
+ div.style.marginLeft = '10px';
+ const effect = new KeyframeEffect(null, gKeyFrames, 100 * MS_PER_SEC);
+ const 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(t => {
+ const div = createDiv(t);
+ div.style.marginLeft = '10px';
+ const 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(t => {
+ const a = createDiv(t);
+ const b = createDiv(t);
+ a.style.marginLeft = '10px';
+ b.style.marginLeft = '20px';
+ const 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');
+
+promise_test(async t => {
+ const animation = createDiv(t).animate(
+ { opacity: 0 },
+ { duration: 1, fill: 'forwards' }
+ );
+
+ const foreignElement
+ = document.createElementNS('http://example.org/test', 'test');
+ document.body.appendChild(foreignElement);
+ t.add_cleanup(() => {
+ foreignElement.remove();
+ });
+
+ animation.effect.target = foreignElement;
+
+ // Wait a frame to make sure nothing bad happens when the UA tries to update
+ // style.
+ await waitForNextFrame();
+}, 'Target element can be set to a foreign element');
+
+// Pseudo-element tests
+// (testing target and pseudoElement in these cases)
+// Since blink uses separate code paths for handling pseudo-element styles
+// depending on whether content is set (putting the pseudo-element in the layout),
+// we run tests on both cases.
+for (const hasContent of [true, false]){
+ test(t => {
+ const d = createDiv(t);
+ d.classList.add('pseudoa');
+ if (hasContent) {
+ d.classList.add('before');
+ }
+
+ const effect = new KeyframeEffect(null, gKeyFrames, 100 * MS_PER_SEC);
+ const anim = new Animation(effect, document.timeline);
+ anim.play();
+
+ anim.currentTime = 50 * MS_PER_SEC;
+ assert_equals(getComputedStyle(d, '::before').marginLeft, '10px',
+ 'Value at 50% progress before setting new target');
+ effect.target = d;
+ effect.pseudoElement = '::before';
+
+ assert_equals(effect.target, d, "Target element is set correctly");
+ assert_equals(effect.pseudoElement, '::before', "Target pseudo-element set correctly");
+ assert_equals(getComputedStyle(d, '::before').marginLeft, '50px',
+ 'Value at 50% progress after setting new target');
+ }, "Change target from null to " + (hasContent ? "an existing" : "a non-existing") +
+ " pseudoElement setting target first.");
+
+ test(t => {
+ const d = createDiv(t);
+ d.classList.add('pseudoa');
+ if (hasContent) {
+ d.classList.add('before');
+ }
+
+ const effect = new KeyframeEffect(null, gKeyFrames, 100 * MS_PER_SEC);
+ const anim = new Animation(effect, document.timeline);
+ anim.play();
+
+ anim.currentTime = 50 * MS_PER_SEC;
+ assert_equals(getComputedStyle(d, '::before').marginLeft, '10px',
+ 'Value at 50% progress before setting new target');
+ effect.pseudoElement = '::before';
+ effect.target = d;
+
+ assert_equals(effect.target, d, "Target element is set correctly");
+ assert_equals(effect.pseudoElement, '::before', "Target pseudo-element set correctly");
+ assert_equals(getComputedStyle(d, '::before').marginLeft, '50px',
+ 'Value at 50% progress after setting new target');
+ }, "Change target from null to " + (hasContent ? "an existing" : "a non-existing") +
+ " pseudoElement setting pseudoElement first.");
+
+ test(t => {
+ const d = createDiv(t);
+ d.classList.add('pseudoa');
+ if (hasContent) {
+ d.classList.add('before');
+ }
+ const anim = d.animate(gKeyFrames, {duration: 100 * MS_PER_SEC, pseudoElement: '::before'});
+
+ anim.currentTime = 50 * MS_PER_SEC;
+ anim.effect.pseudoElement = null;
+ assert_equals(anim.effect.target, d,
+ "Animation targets specified element (target element)");
+ assert_equals(anim.effect.pseudoElement, null,
+ "Animation targets specified element (null pseudo-selector)");
+ assert_equals(getComputedStyle(d, '::before').marginLeft, '10px',
+ 'Value of 1st element (currently not targeted) after ' +
+ 'changing the effect target');
+ assert_equals(getComputedStyle(d).marginLeft, '50px',
+ 'Value of 2nd element (currently targeted) after ' +
+ 'changing the effect target');
+ }, "Change target from " + (hasContent ? "an existing" : "a non-existing") + " pseudo-element to the originating element.");
+
+ for (const prevHasContent of [true, false]) {
+ test(t => {
+ const a = createDiv(t);
+ a.classList.add('pseudoa');
+ const b = createDiv(t);
+ b.classList.add('pseudob');
+ if (prevHasContent) {
+ a.classList.add('before');
+ }
+ if (hasContent) {
+ b.classList.add('before');
+ }
+
+ const anim = a.animate(gKeyFrames, {duration: 100 * MS_PER_SEC, pseudoElement: '::before'});
+
+ anim.currentTime = 50 * MS_PER_SEC;
+ anim.effect.target = b;
+ assert_equals(anim.effect.target, b,
+ "Animation targets specified pseudo-element (target element)");
+ assert_equals(anim.effect.pseudoElement, '::before',
+ "Animation targets specified pseudo-element (pseudo-selector)");
+ assert_equals(getComputedStyle(a, '::before').marginLeft, '10px',
+ 'Value of 1st element (currently not targeted) after ' +
+ 'changing the effect target');
+ assert_equals(getComputedStyle(b, '::before').marginLeft, '50px',
+ 'Value of 2nd element (currently targeted) after ' +
+ 'changing the effect target');
+ }, "Change target from " + (prevHasContent ? "an existing" : "a non-existing") +
+ " to a different " + (hasContent ? "existing" : "non-existing") +
+ " pseudo-element by setting target.");
+
+ test(t => {
+ const d = createDiv(t);
+ d.classList.add('pseudoc');
+ if (prevHasContent) {
+ d.classList.add('before');
+ }
+ if (hasContent) {
+ d.classList.add('after');
+ }
+
+ const anim = d.animate(gKeyFrames, {duration: 100 * MS_PER_SEC, pseudoElement: '::before'});
+
+ anim.currentTime = 50 * MS_PER_SEC;
+ anim.effect.pseudoElement = '::after';
+ assert_equals(anim.effect.target, d,
+ "Animation targets specified pseudo-element (target element)");
+ assert_equals(anim.effect.pseudoElement, '::after',
+ "Animation targets specified pseudo-element (pseudo-selector)");
+ assert_equals(getComputedStyle(d, '::before').marginLeft, '10px',
+ 'Value of 1st element (currently not targeted) after ' +
+ 'changing the effect target');
+ assert_equals(getComputedStyle(d, '::after').marginLeft, '50px',
+ 'Value of 2nd element (currently targeted) after ' +
+ 'changing the effect target');
+ }, "Change target from " + (prevHasContent ? "an existing" : "a non-existing") +
+ " to a different " + (hasContent ? "existing" : "non-existing") +
+ " pseudo-element by setting pseudoElement.");
+ }
+}
+
+for (const pseudo of [
+ '',
+ 'before',
+ ':abc',
+ '::abc',
+]) {
+ test(t => {
+ const effect = new KeyframeEffect(null, gKeyFrames, 100 * MS_PER_SEC);
+ assert_throws_dom("SyntaxError", () => effect.pseudoElement = pseudo );
+ }, `Changing pseudoElement to a non-null invalid pseudo-selector ` +
+ `'${pseudo}' throws a SyntaxError`);
+}
+
+test(t => {
+ const effect = new KeyframeEffect(null, gKeyFrames, 100 * MS_PER_SEC);
+ effect.pseudoElement = '::placeHOLDER';
+ assert_equals(effect.pseudoElement, '::placeholder');
+}, `Changing pseudoElement to ::placeHOLDER works`);
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/web-animations/resources/easing-tests.js b/testing/web-platform/tests/web-animations/resources/easing-tests.js
new file mode 100644
index 0000000000..a05264b0f5
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/resources/easing-tests.js
@@ -0,0 +1,121 @@
+'use strict';
+
+const gEasingTests = [
+ {
+ 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)
+ },
+ {
+ desc: 'easing function which produces values less than 1',
+ easing: 'cubic-bezier(0, -0.5, 1, -0.5)',
+ easingFunction: cubicBezier(0, -0.5, 1, -0.5)
+ }
+];
+
+const gEasingParsingTests = [
+ ['linear', 'linear'],
+ ['ease-in-out', 'ease-in-out'],
+ ['Ease\\2d in-out', 'ease-in-out'],
+ ['ease /**/', 'ease'],
+];
+
+const gInvalidEasings = [
+ '',
+ '7',
+ 'test',
+ 'initial',
+ 'inherit',
+ 'unset',
+ 'unrecognized',
+ 'var(--x)',
+ 'ease-in-out, ease-out',
+ 'cubic-bezier(1.1, 0, 1, 1)',
+ 'cubic-bezier(0, 0, 1.1, 1)',
+ 'cubic-bezier(-0.1, 0, 1, 1)',
+ 'cubic-bezier(0, 0, -0.1, 1)',
+ 'cubic-bezier(0.1, 0, 4, 0.4)',
+ 'steps(-1, start)',
+ 'steps(0.1, start)',
+ 'steps(3, nowhere)',
+ 'steps(-3, end)',
+ 'function (a){return a}',
+ 'function (x){return x}',
+ 'function(x, y){return 0.3}',
+];
+
+// Easings that should serialize to the same string
+const gRoundtripEasings = [
+ 'ease',
+ 'linear',
+ 'ease-in',
+ 'ease-out',
+ 'ease-in-out',
+ 'cubic-bezier(0.1, 5, 0.23, 0)',
+ 'steps(3, start)',
+ 'steps(3)',
+];
diff --git a/testing/web-platform/tests/web-animations/resources/effect-tests.js b/testing/web-platform/tests/web-animations/resources/effect-tests.js
new file mode 100644
index 0000000000..8a18ab13c6
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/resources/effect-tests.js
@@ -0,0 +1,75 @@
+'use strict';
+
+// Common utility methods for testing animation effects
+
+// Tests the |property| member of |animation's| target effect's computed timing
+// at the various points indicated by |values|.
+//
+// |values| has the format:
+//
+// {
+// before, // value to test during before phase
+// activeBoundary, // value to test at the very beginning of the active
+// // phase when playing forwards, or the very end of
+// // the active phase when playing backwards.
+// // This should be undefined if the active duration of
+// // the effect is zero.
+// after, // value to test during the after phase or undefined if the
+// // active duration is infinite
+// }
+//
+function assert_computed_timing_for_each_phase(animation, property, values) {
+ // Some computed timing properties (e.g. 'progress') require floating-point
+ // comparison, whilst exact equality suffices for others.
+ const assert_property_equals =
+ (property === 'progress') ? assert_times_equal : assert_equals;
+
+ const effect = animation.effect;
+ const timing = effect.getComputedTiming();
+
+ // The following calculations are based on the definitions here:
+ // https://drafts.csswg.org/web-animations/#animation-effect-phases-and-states
+ const beforeActive = Math.max(Math.min(timing.delay, timing.endTime), 0);
+ const activeAfter =
+ Math.max(Math.min(timing.delay + timing.activeDuration, timing.endTime), 0);
+ const direction = animation.playbackRate < 0 ? 'backwards' : 'forwards';
+
+ // Before phase
+ if (direction === 'forwards') {
+ animation.currentTime = beforeActive - 1;
+ } else {
+ animation.currentTime = beforeActive;
+ }
+ assert_property_equals(effect.getComputedTiming()[property], values.before,
+ `Value of ${property} in the before phase`);
+
+ // Active phase
+ if (effect.getComputedTiming().activeDuration > 0) {
+ if (direction === 'forwards') {
+ animation.currentTime = beforeActive;
+ } else {
+ animation.currentTime = activeAfter;
+ }
+ assert_property_equals(effect.getComputedTiming()[property], values.activeBoundary,
+ `Value of ${property} at the boundary of the active phase`);
+ } else {
+ assert_equals(values.activeBoundary, undefined,
+ 'Test specifies a value to check during the active phase but'
+ + ' the animation has a zero duration');
+ }
+
+ // After phase
+ if (effect.getComputedTiming().activeDuration !== Infinity) {
+ if (direction === 'forwards') {
+ animation.currentTime = activeAfter;
+ } else {
+ animation.currentTime = activeAfter + 1;
+ }
+ assert_property_equals(effect.getComputedTiming()[property], values.after,
+ `Value of ${property} in the after phase`);
+ } else {
+ assert_equals(values.after, undefined,
+ 'Test specifies a value to check during the after phase but'
+ + ' the animation has an infinite duration');
+ }
+}
diff --git a/testing/web-platform/tests/web-animations/resources/keyframe-tests.js b/testing/web-platform/tests/web-animations/resources/keyframe-tests.js
new file mode 100644
index 0000000000..43e0d7575f
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/resources/keyframe-tests.js
@@ -0,0 +1,827 @@
+'use strict';
+
+// ==============================
+//
+// Common keyframe test data
+//
+// ==============================
+
+
+// ------------------------------
+// Composite values
+// ------------------------------
+
+const gGoodKeyframeCompositeValueTests = [
+ 'replace', 'add', 'accumulate', 'auto'
+];
+
+const gBadKeyframeCompositeValueTests = [
+ 'unrecognised', 'replace ', 'Replace', null
+];
+
+const gGoodOptionsCompositeValueTests = [
+ 'replace', 'add', 'accumulate'
+];
+
+const gBadOptionsCompositeValueTests = [
+ 'unrecognised', 'replace ', 'Replace', null
+];
+
+// ------------------------------
+// Keyframes
+// ------------------------------
+
+const gEmptyKeyframeListTests = [
+ [],
+ null,
+ undefined,
+];
+
+// Helper methods to make defining computed keyframes more readable.
+
+const offset = offset => ({
+ offset,
+ computedOffset: offset,
+});
+
+const computedOffset = computedOffset => ({
+ offset: null,
+ computedOffset,
+});
+
+const keyframe = (offset, props, easing='linear', composite) => {
+ // The object spread operator is not yet available in all browsers so we use
+ // Object.assign instead.
+ const result = {};
+ Object.assign(result, offset, props, { easing });
+ result.composite = composite || 'auto';
+ return result;
+};
+
+const gKeyframesTests = [
+
+ // ----------- Property-indexed keyframes: property handling -----------
+
+ {
+ desc: 'a one property two value property-indexed keyframes specification',
+ input: { left: ['10px', '20px'] },
+ output: [keyframe(computedOffset(0), { left: '10px' }),
+ keyframe(computedOffset(1), { left: '20px' })],
+ },
+ {
+ desc: 'a one shorthand property two value property-indexed keyframes'
+ + ' specification',
+ input: { margin: ['10px', '10px 20px 30px 40px'] },
+ output: [keyframe(computedOffset(0), { margin: '10px' }),
+ keyframe(computedOffset(1), { 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: [keyframe(computedOffset(0),
+ { marginTop: '50px', margin: '10px' }),
+ keyframe(computedOffset(1),
+ { marginTop: '60px', margin: '10px 20px 30px 40px' })],
+ },
+ {
+ desc: 'a two property (one shorthand and one of its shorthand components)'
+ + ' two value property-indexed keyframes specification',
+ input: { border: ['pink', '2px'],
+ borderColor: ['green', 'blue'] },
+ output: [keyframe(computedOffset(0),
+ { border: 'pink', borderColor: 'green' }),
+ keyframe(computedOffset(1),
+ { border: '2px', borderColor: 'blue' })],
+ },
+ {
+ desc: 'a two property two value property-indexed keyframes specification',
+ input: { left: ['10px', '20px'],
+ top: ['30px', '40px'] },
+ output: [keyframe(computedOffset(0), { left: '10px', top: '30px' }),
+ keyframe(computedOffset(1), { 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: [keyframe(computedOffset(0), { left: '10px', top: '40px' }),
+ keyframe(computedOffset(0.5), { left: '20px' }),
+ keyframe(computedOffset(1), { 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: [keyframe(computedOffset(0), { left: '10px', top: '15px' }),
+ keyframe(computedOffset(0.25), { left: '20px', top: '25px' }),
+ keyframe(computedOffset(0.5), { left: '30px' }),
+ keyframe(computedOffset(0.75), { left: '40px', top: '45px' }),
+ keyframe(computedOffset(1), { 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: [keyframe(computedOffset(0), { opacity: '0' }),
+ keyframe(computedOffset(1), { opacity: '1' })],
+ },
+ {
+ desc: 'a property-indexed keyframes specification with a CSS variable'
+ + ' reference',
+ input: { left: [ 'var(--dist)', 'calc(var(--dist) + 100px)' ] },
+ output: [keyframe(computedOffset(0), { left: 'var(--dist)' }),
+ keyframe(computedOffset(1), { 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: [keyframe(computedOffset(0),
+ { margin: 'var(--dist)' }),
+ keyframe(computedOffset(1),
+ { margin: 'calc(var(--dist) + 100px)' })],
+ },
+ {
+ desc: 'a one property one value property-indexed keyframes specification',
+ input: { left: ['10px'] },
+ output: [keyframe(computedOffset(1), { left: '10px' })],
+ },
+ {
+ desc: 'a one property one non-array value property-indexed keyframes'
+ + ' specification',
+ input: { left: '10px' },
+ output: [keyframe(computedOffset(1), { left: '10px' })],
+ },
+ {
+ desc: 'a one property two value property-indexed keyframes specification'
+ + ' where the first value is invalid',
+ input: { left: ['invalid', '10px'] },
+ output: [keyframe(computedOffset(0), {}),
+ keyframe(computedOffset(1), { left: '10px' })]
+ },
+ {
+ desc: 'a one property two value property-indexed keyframes specification'
+ + ' where the second value is invalid',
+ input: { left: ['10px', 'invalid'] },
+ output: [keyframe(computedOffset(0), { left: '10px' }),
+ keyframe(computedOffset(1), {})]
+ },
+ {
+ desc: 'a property-indexed keyframes specification with a CSS variable as'
+ + ' the property',
+ input: { '--custom': ['1', '2'] },
+ output: [keyframe(computedOffset(0), { '--custom': '1' }),
+ keyframe(computedOffset(1), { '--custom': '2' })]
+ },
+
+ // ----------- Property-indexed keyframes: offset handling -----------
+
+ {
+ desc: 'a property-indexed keyframe with a single offset',
+ input: { left: ['10px', '20px', '30px'], offset: 0.5 },
+ output: [keyframe(offset(0.5), { left: '10px' }),
+ keyframe(computedOffset(0.75), { left: '20px' }),
+ keyframe(computedOffset(1), { left: '30px' })],
+ },
+ {
+ desc: 'a property-indexed keyframe with an array of offsets',
+ input: { left: ['10px', '20px', '30px'], offset: [ 0.1, 0.25, 0.8 ] },
+ output: [keyframe(offset(0.1), { left: '10px' }),
+ keyframe(offset(0.25), { left: '20px' }),
+ keyframe(offset(0.8), { left: '30px' })],
+ },
+ {
+ desc: 'a property-indexed keyframe with an array of offsets that is too'
+ + ' short',
+ input: { left: ['10px', '20px', '30px'], offset: [ 0, 0.25 ] },
+ output: [keyframe(offset(0), { left: '10px' }),
+ keyframe(offset(0.25), { left: '20px' }),
+ keyframe(computedOffset(1), { left: '30px' })],
+ },
+ {
+ desc: 'a property-indexed keyframe with an array of offsets that is too'
+ + ' long',
+ input: { left: ['10px', '20px', '30px'],
+ offset: [ 0, 0.25, 0.5, 0.75, 1 ] },
+ output: [keyframe(offset(0), { left: '10px' }),
+ keyframe(offset(0.25), { left: '20px' }),
+ keyframe(offset(0.5), { left: '30px' })],
+ },
+ {
+ desc: 'a property-indexed keyframe with an empty array of offsets',
+ input: { left: ['10px', '20px', '30px'], offset: [] },
+ output: [keyframe(computedOffset(0), { left: '10px' }),
+ keyframe(computedOffset(0.5), { left: '20px' }),
+ keyframe(computedOffset(1), { left: '30px' })],
+ },
+ {
+ desc: 'a property-indexed keyframe with an array of offsets with an'
+ + ' embedded null value',
+ input: { left: ['10px', '20px', '30px'],
+ offset: [ 0, null, 0.5 ] },
+ output: [keyframe(offset(0), { left: '10px' }),
+ keyframe(computedOffset(0.25), { left: '20px' }),
+ keyframe(offset(0.5), { left: '30px' })],
+ },
+ {
+ desc: 'a property-indexed keyframe with an array of offsets with a'
+ + ' trailing null value',
+ input: { left: ['10px', '20px', '30px'],
+ offset: [ 0, 0.25, null ] },
+ output: [keyframe(offset(0), { left: '10px' }),
+ keyframe(offset(0.25), { left: '20px' }),
+ keyframe(computedOffset(1), { left: '30px' })],
+ },
+ {
+ desc: 'a property-indexed keyframe with an array of offsets with leading'
+ + ' and trailing null values',
+ input: { left: ['10px', '20px', '30px'],
+ offset: [ null, 0.25, null ] },
+ output: [keyframe(computedOffset(0), { left: '10px' }),
+ keyframe(offset(0.25), { left: '20px' }),
+ keyframe(computedOffset(1), { left: '30px' })],
+ },
+ {
+ desc: 'a property-indexed keyframe with an array of offsets with'
+ + ' adjacent null values',
+ input: { left: ['10px', '20px', '30px'],
+ offset: [ null, null, 0.5 ] },
+ output: [keyframe(computedOffset(0), { left: '10px' }),
+ keyframe(computedOffset(0.25), { left: '20px' }),
+ keyframe(offset(0.5), { left: '30px' })],
+ },
+ {
+ desc: 'a property-indexed keyframe with an array of offsets with'
+ + ' all null values (and too many at that)',
+ input: { left: ['10px', '20px', '30px'],
+ offset: [ null, null, null, null, null ] },
+ output: [keyframe(computedOffset(0), { left: '10px' }),
+ keyframe(computedOffset(0.5), { left: '20px' }),
+ keyframe(computedOffset(1), { left: '30px' })],
+ },
+ {
+ desc: 'a property-indexed keyframe with a single null offset',
+ input: { left: ['10px', '20px', '30px'], offset: null },
+ output: [keyframe(computedOffset(0), { left: '10px' }),
+ keyframe(computedOffset(0.5), { left: '20px' }),
+ keyframe(computedOffset(1), { left: '30px' })],
+ },
+ {
+ desc: 'a property-indexed keyframe with an array of offsets that is not'
+ + ' strictly ascending in the unused part of the array',
+ input: { left: ['10px', '20px', '30px'],
+ offset: [ 0, 0.2, 0.8, 0.6 ] },
+ output: [keyframe(offset(0), { left: '10px' }),
+ keyframe(offset(0.2), { left: '20px' }),
+ keyframe(offset(0.8), { left: '30px' })],
+ },
+
+ // ----------- Property-indexed keyframes: easing handling -----------
+
+ {
+ desc: 'a property-indexed keyframe without any specified easing',
+ input: { left: ['10px', '20px', '30px'] },
+ output: [keyframe(computedOffset(0), { left: '10px' }, 'linear'),
+ keyframe(computedOffset(0.5), { left: '20px' }, 'linear'),
+ keyframe(computedOffset(1), { left: '30px' }, 'linear')],
+ },
+ {
+ desc: 'a property-indexed keyframe with a single easing',
+ input: { left: ['10px', '20px', '30px'], easing: 'ease-in' },
+ output: [keyframe(computedOffset(0), { left: '10px' }, 'ease-in'),
+ keyframe(computedOffset(0.5), { left: '20px' }, 'ease-in'),
+ keyframe(computedOffset(1), { left: '30px' }, 'ease-in')],
+ },
+ {
+ desc: 'a property-indexed keyframe with an array of easings',
+ input: { left: ['10px', '20px', '30px'],
+ easing: ['ease-in', 'ease-out', 'ease-in-out'] },
+ output: [keyframe(computedOffset(0), { left: '10px' }, 'ease-in'),
+ keyframe(computedOffset(0.5), { left: '20px' }, 'ease-out'),
+ keyframe(computedOffset(1), { left: '30px' }, 'ease-in-out')],
+ },
+ {
+ desc: 'a property-indexed keyframe with an array of easings that is too'
+ + ' short',
+ input: { left: ['10px', '20px', '30px'],
+ easing: ['ease-in', 'ease-out'] },
+ output: [keyframe(computedOffset(0), { left: '10px' }, 'ease-in'),
+ keyframe(computedOffset(0.5), { left: '20px' }, 'ease-out'),
+ keyframe(computedOffset(1), { left: '30px' }, 'ease-in')],
+ },
+ {
+ desc: 'a property-indexed keyframe with a single-element array of'
+ + ' easings',
+ input: { left: ['10px', '20px', '30px'], easing: ['ease-in'] },
+ output: [keyframe(computedOffset(0), { left: '10px' }, 'ease-in'),
+ keyframe(computedOffset(0.5), { left: '20px' }, 'ease-in'),
+ keyframe(computedOffset(1), { left: '30px' }, 'ease-in')],
+ },
+ {
+ desc: 'a property-indexed keyframe with an empty array of easings',
+ input: { left: ['10px', '20px', '30px'], easing: [] },
+ output: [keyframe(computedOffset(0), { left: '10px' }, 'linear'),
+ keyframe(computedOffset(0.5), { left: '20px' }, 'linear'),
+ keyframe(computedOffset(1), { left: '30px' }, 'linear')],
+ },
+ {
+ desc: 'a property-indexed keyframe with an array of easings that is too'
+ + ' long',
+ input: { left: ['10px', '20px', '30px'],
+ easing: ['steps(1)', 'steps(2)', 'steps(3)', 'steps(4)'] },
+ output: [keyframe(computedOffset(0), { left: '10px' }, 'steps(1)'),
+ keyframe(computedOffset(0.5), { left: '20px' }, 'steps(2)'),
+ keyframe(computedOffset(1), { left: '30px' }, 'steps(3)')],
+ },
+
+ // ----------- Property-indexed keyframes: composite handling -----------
+
+ {
+ desc: 'a property-indexed keyframe with a single composite operation',
+ input: { left: ['10px', '20px', '30px'], composite: 'add' },
+ output: [keyframe(computedOffset(0), { left: '10px' }, 'linear', 'add'),
+ keyframe(computedOffset(0.5), { left: '20px' }, 'linear', 'add'),
+ keyframe(computedOffset(1), { left: '30px' }, 'linear', 'add')],
+ },
+ {
+ desc: 'a property-indexed keyframe with a composite array',
+ input: { left: ['10px', '20px', '30px'],
+ composite: ['add', 'replace', 'accumulate'] },
+ output: [keyframe(computedOffset(0), { left: '10px' },
+ 'linear', 'add'),
+ keyframe(computedOffset(0.5), { left: '20px' },
+ 'linear', 'replace'),
+ keyframe(computedOffset(1), { left: '30px' },
+ 'linear', 'accumulate')],
+ },
+ {
+ desc: 'a property-indexed keyframe with a composite array that is too'
+ + ' short',
+ input: { left: ['10px', '20px', '30px', '40px', '50px'],
+ composite: ['add', 'replace'] },
+ output: [keyframe(computedOffset(0), { left: '10px' },
+ 'linear', 'add'),
+ keyframe(computedOffset(0.25), { left: '20px' },
+ 'linear', 'replace'),
+ keyframe(computedOffset(0.5), { left: '30px' },
+ 'linear', 'add'),
+ keyframe(computedOffset(0.75), { left: '40px' },
+ 'linear', 'replace'),
+ keyframe(computedOffset(1), { left: '50px' },
+ 'linear', 'add')],
+ },
+ {
+ desc: 'a property-indexed keyframe with a composite array that is too'
+ + ' long',
+ input: { left: ['10px', '20px'],
+ composite: ['add', 'replace', 'accumulate'] },
+ output: [keyframe(computedOffset(0), { left: '10px' },
+ 'linear', 'add'),
+ keyframe(computedOffset(1), { left: '20px' },
+ 'linear', 'replace')],
+ },
+ {
+ desc: 'a property-indexed keyframe with a single-element composite array',
+ input: { left: ['10px', '20px', '30px'],
+ composite: ['add'] },
+ output: [keyframe(computedOffset(0), { left: '10px' }, 'linear', 'add'),
+ keyframe(computedOffset(0.5), { left: '20px' }, 'linear', 'add'),
+ keyframe(computedOffset(1), { left: '30px' }, 'linear', 'add')],
+ },
+
+ // ----------- Keyframe sequence: property handling -----------
+
+ {
+ desc: 'a one property one keyframe sequence',
+ input: [{ offset: 1, left: '10px' }],
+ output: [keyframe(offset(1), { left: '10px' })],
+ },
+ {
+ desc: 'a one property two keyframe sequence',
+ input: [{ offset: 0, left: '10px' },
+ { offset: 1, left: '20px' }],
+ output: [keyframe(offset(0), { left: '10px' }),
+ keyframe(offset(1), { left: '20px' })],
+ },
+ {
+ desc: 'a two property two keyframe sequence',
+ input: [{ offset: 0, left: '10px', top: '30px' },
+ { offset: 1, left: '20px', top: '40px' }],
+ output: [keyframe(offset(0), { left: '10px', top: '30px' }),
+ keyframe(offset(1), { 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: [keyframe(offset(0), { margin: '10px' }),
+ keyframe(offset(1), { 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: [keyframe(offset(0), { margin: '10px', marginTop: '20px' }),
+ keyframe(offset(1), { marginTop: '70px',
+ margin: '30px 40px 50px 60px' })],
+ },
+ {
+ 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: [keyframe(offset(0), { left: '10px' }),
+ keyframe(offset(1), { 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: [keyframe(offset(0), { left: '10px', top: '20px' }),
+ keyframe(offset(1), { left: '30px' })],
+ },
+ {
+ desc: 'a one property two keyframe sequence that needs to stringify'
+ + ' its values',
+ input: [{ offset: 0, opacity: 0 },
+ { offset: 1, opacity: 1 }],
+ output: [keyframe(offset(0), { opacity: '0' }),
+ keyframe(offset(1), { opacity: '1' })],
+ },
+ {
+ desc: 'a keyframe sequence with a CSS variable reference',
+ input: [{ left: 'var(--dist)' },
+ { left: 'calc(var(--dist) + 100px)' }],
+ output: [keyframe(computedOffset(0), { left: 'var(--dist)' }),
+ keyframe(computedOffset(1), { 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: [keyframe(computedOffset(0),
+ { margin: 'var(--dist)' }),
+ keyframe(computedOffset(1),
+ { margin: 'calc(var(--dist) + 100px)' })],
+ },
+ {
+ desc: 'a keyframe sequence with a CSS variable as its property',
+ input: [{ '--custom': 'a' },
+ { '--custom': 'b' }],
+ output: [keyframe(computedOffset(0), { '--custom': 'a' }),
+ keyframe(computedOffset(1), { '--custom': 'b' })]
+ },
+
+ // ----------- Keyframe sequence: offset handling -----------
+
+ {
+ 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: [keyframe(offset(0), { left: '10px' }),
+ keyframe(offset(0.5), { left: '20px' }),
+ keyframe(offset(0.5), { left: '30px' }),
+ keyframe(offset(0.5), { left: '40px' }),
+ keyframe(offset(1), { 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: [keyframe(offset(0), { left: '10px' }),
+ keyframe(offset(0), { left: '20px' }),
+ keyframe(offset(0), { left: '30px' }),
+ keyframe(offset(1), { left: '40px' }),
+ keyframe(offset(1), { left: '50px' }),
+ keyframe(offset(1), { 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: [keyframe(offset(0), { left: '10px' }),
+ keyframe(offset(0), { top: '20px' }),
+ keyframe(offset(1), { top: '30px' }),
+ keyframe(offset(1), { left: '40px' })],
+ },
+ {
+ desc: 'a single keyframe sequence with omitted offset',
+ input: [{ left: '10px' }],
+ output: [keyframe(computedOffset(1), { left: '10px' })],
+ },
+ {
+ desc: 'a single keyframe sequence with null offset',
+ input: [{ offset: null, left: '10px' }],
+ output: [keyframe(computedOffset(1), { left: '10px' })],
+ },
+ {
+ desc: 'a single keyframe sequence with string offset',
+ input: [{ offset: '0.5', left: '10px' }],
+ output: [keyframe(offset(0.5), { 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: [keyframe(offset(0), { left: '10px' }),
+ keyframe(offset(0.25), { left: '20px' }),
+ keyframe(computedOffset(0.5), { left: '30px' }),
+ keyframe(computedOffset(0.75), { left: '40px' }),
+ keyframe(offset(1), { left: '50px' })],
+ },
+ {
+ desc: 'a one property keyframe sequence with some null offsets',
+ input: [{ offset: 0.00, left: '10px' },
+ { offset: 0.25, left: '20px' },
+ { offset: null, left: '30px' },
+ { offset: null, left: '40px' },
+ { offset: 1.00, left: '50px' }],
+ output: [keyframe(offset(0), { left: '10px' }),
+ keyframe(offset(0.25), { left: '20px' }),
+ keyframe(computedOffset(0.5), { left: '30px' }),
+ keyframe(computedOffset(0.75), { left: '40px' }),
+ keyframe(offset(1), { 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: [keyframe(offset(0), { left: '10px', top: '20px' }),
+ keyframe(offset(0.25), { left: '30px' }),
+ keyframe(computedOffset(0.5), { left: '40px' }),
+ keyframe(computedOffset(0.75), { left: '50px', top: '60px' }),
+ keyframe(offset(1), { 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: [keyframe(computedOffset(0), { left: '10px' }),
+ keyframe(computedOffset(0.25), { left: '20px' }),
+ keyframe(computedOffset(0.5), { left: '30px' }),
+ keyframe(computedOffset(0.75), { left: '40px' }),
+ keyframe(computedOffset(1), { left: '50px' })],
+ },
+
+ // ----------- Keyframe sequence: easing handling -----------
+
+ {
+ 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: [keyframe(offset(0), { left: '10px' }, 'ease'),
+ keyframe(offset(0), { top: '20px' }, 'ease'),
+ keyframe(offset(0.5), { left: '30px' }, 'linear'),
+ keyframe(offset(0.5), { top: '40px' }, 'linear'),
+ keyframe(offset(1), { left: '50px' }, 'steps(1)'),
+ keyframe(offset(1), { top: '60px' }, 'steps(1)')],
+ },
+
+ // ----------- Keyframe sequence: composite handling -----------
+
+ {
+ 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: [keyframe(offset(0), { left: '10px' }, 'linear', 'replace'),
+ keyframe(offset(0), { top: '20px' }, 'linear', 'replace'),
+ keyframe(offset(0.5), { left: '30px' }, 'linear', 'add'),
+ keyframe(offset(0.5), { top: '40px' }, 'linear', 'add'),
+ keyframe(offset(1), { left: '50px' }, 'linear', 'replace'),
+ keyframe(offset(1), { top: '60px' }, 'linear', 'replace')],
+ },
+];
+
+const gInvalidKeyframesTests = [
+ {
+ desc: 'keyframes with an out-of-bounded positive offset',
+ input: [ { opacity: 0 },
+ { opacity: 0.5, offset: 2 },
+ { opacity: 1 } ],
+ },
+ {
+ desc: 'keyframes with an out-of-bounded negative offset',
+ input: [ { opacity: 0 },
+ { opacity: 0.5, offset: -1 },
+ { opacity: 1 } ],
+ },
+ {
+ desc: 'property-indexed keyframes not loosely sorted by offset',
+ input: { opacity: [ 0, 1 ], offset: [ 1, 0 ] },
+ },
+ {
+ desc: 'property-indexed keyframes not loosely sorted by offset even'
+ + ' though not all offsets are specified',
+ input: { opacity: [ 0, 0.5, 1 ], offset: [ 0.5, 0 ] },
+ },
+ {
+ desc: 'property-indexed keyframes with offsets out of range',
+ input: { opacity: [ 0, 0.5, 1 ], offset: [ 0, 1.1 ] },
+ },
+ {
+ desc: 'keyframes not loosely sorted by offset',
+ input: [ { opacity: 0, offset: 1 },
+ { opacity: 1, offset: 0 } ],
+ },
+ {
+ desc: 'property-indexed keyframes with an invalid easing value',
+ input: { opacity: [ 0, 0.5, 1 ],
+ easing: 'inherit' },
+ },
+ {
+ desc: 'property-indexed keyframes with an invalid easing value as one of'
+ + ' the array values',
+ input: { opacity: [ 0, 0.5, 1 ],
+ easing: [ 'ease-in', 'inherit' ] },
+ },
+ {
+ desc: 'property-indexed keyframe with an invalid easing in the unused'
+ + ' part of the array of easings',
+ input: { left: ['10px', '20px', '30px'],
+ easing: ['steps(1)', 'steps(2)', 'steps(3)', 'invalid'] },
+ },
+ {
+ desc: 'empty property-indexed keyframe with an invalid easing',
+ input: { easing: 'invalid' },
+ },
+ {
+ desc: 'empty property-indexed keyframe with an invalid easings array',
+ input: { easing: ['invalid'] },
+ },
+ {
+ desc: 'a keyframe sequence with an invalid easing value',
+ input: [ { opacity: 0, easing: 'jumpy' },
+ { opacity: 1 } ],
+ },
+ {
+ desc: 'property-indexed keyframes with an invalid composite value',
+ input: { opacity: [ 0, 0.5, 1 ],
+ composite: 'alternate' },
+ },
+ {
+ desc: 'property-indexed keyframes with an invalid composite value as one'
+ + ' of the array values',
+ input: { opacity: [ 0, 0.5, 1 ],
+ composite: [ 'add', 'alternate' ] },
+ },
+ {
+ desc: 'keyframes with an invalid composite value',
+ input: [ { opacity: 0, composite: 'alternate' },
+ { opacity: 1 } ],
+ },
+];
+
+
+const gKeyframeSerializationTests = [
+ {
+ desc: 'a on keyframe sequence which requires value serilaization of its'
+ + ' values',
+ input: [{offset: 0, backgroundColor: 'rgb(1,2,3)' }],
+ output: [keyframe(offset(0), { backgroundColor: 'rgb(1, 2, 3)' })],
+ },
+];
+
+
+
+// ------------------------------
+// KeyframeEffectOptions
+// ------------------------------
+
+const 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' },
+ }
+];
+
+const gInvalidKeyframeEffectOptionTests = [
+ { desc: '-Infinity', input: -Infinity },
+ { desc: 'NaN', input: NaN },
+ { desc: 'a negative value', input: -1 },
+ { desc: 'a negative Infinity duration', input: { duration: -Infinity } },
+ { desc: 'a NaN duration', input: { duration: NaN } },
+ { desc: 'a negative duration', input: { duration: -1 } },
+ { desc: 'a string duration', input: { duration: 'merrychristmas' } },
+ { desc: 'a negative Infinity iterations', input: { iterations: -Infinity} },
+ { desc: 'a NaN iterations', input: { iterations: NaN } },
+ { desc: 'a negative iterations', input: { iterations: -1 } },
+ { desc: 'a blank easing', input: { easing: '' } },
+ { desc: 'an unrecognized easing', input: { easing: 'unrecognised' } },
+ { 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' } },
+];
+
+// There is currently only ScrollTimeline that can be constructed and used here
+// beyond document timeline. Given that ScrollTimeline is not stable as of yet
+// it's tested in scroll-animations/animation-with-animatable-interface.html.
+const gAnimationTimelineTests = [
+ {
+ expectedTimeline: document.timeline,
+ expectedTimelineDescription: 'document.timeline',
+ description: 'with no timeline parameter'
+ },
+ {
+ timeline: undefined,
+ expectedTimeline: document.timeline,
+ expectedTimelineDescription: 'document.timeline',
+ description: 'with undefined timeline'
+ },
+ {
+ timeline: null,
+ expectedTimeline: null,
+ expectedTimelineDescription: 'null',
+ description: 'with null timeline'
+ },
+ {
+ timeline: document.timeline,
+ expectedTimeline: document.timeline,
+ expectedTimelineDescription: 'document.timeline',
+ description: 'with DocumentTimeline'
+ },
+]; \ No newline at end of file
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 0000000000..60fb9781a0
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/resources/keyframe-utils.js
@@ -0,0 +1,51 @@
+'use strict';
+
+// =======================================
+//
+// Utility functions for testing keyframes
+//
+// =======================================
+
+
+// ------------------------------
+// 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, message) {
+ assert_equals(a.length, b.length, `number of frames: ${(message || '')}`);
+ for (let i = 0; i < Math.min(a.length, b.length); i++) {
+ assert_frames_equal(a[i], b[i],
+ `ComputedKeyframe #${i}: ${(message || '')}`);
+ }
+}
+
+/** Helper for assert_frame_lists_equal */
+function assert_frames_equal(a, b, name) {
+ assert_equals(Object.keys(a).sort().toString(),
+ Object.keys(b).sort().toString(),
+ `properties on ${name} should match`);
+ // Iterates sorted keys to ensure stable failures.
+ for (const p of Object.keys(a).sort()) {
+ if (typeof b[p] == 'number')
+ assert_approx_equals(a[p], b[p], 1e-6, `value for '${p}' on ${name}`);
+ else if (typeof b[p] == 'object') {
+ for (const key in b[p]) {
+ if (typeof b[p][key] == 'number') {
+ assert_approx_equals(a[p][key], b[p][key], 1e-6,
+ `value for '${p}.${key}' on ${name}`);
+ } else {
+ assert_equals((a[p][key] || 'undefined').toString(),
+ b[p][key].toString(),
+ `value for '${p}.${key}' on ${name}`);
+ }
+ }
+ }
+ else
+ assert_equals(a[p], b[p], `value for '${p}' on ${name}`);
+ }
+}
diff --git a/testing/web-platform/tests/web-animations/resources/timing-override.js b/testing/web-platform/tests/web-animations/resources/timing-override.js
new file mode 100644
index 0000000000..a1d65030f0
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/resources/timing-override.js
@@ -0,0 +1,18 @@
+// Firefox implements unconditional clamping of 20 usec; and for certain web-animation tests,
+// we hit some test failures because the Time Precision is too small. We override these functions
+// on a per-test basis for Firefox only.
+if(navigator.userAgent.toLowerCase().indexOf('firefox') > -1){
+ window.assert_times_equal = (actual, expected, description) => {
+ let TIME_PRECISION = 0.02;
+ assert_approx_equals(actual, expected, TIME_PRECISION * 2, description);
+ };
+
+ window.assert_time_equals_literal = (actual, expected, description) => {
+ let TIME_PRECISION = 0.02;
+ if (Math.abs(expected) === Infinity) {
+ assert_equals(actual, expected, description);
+ } else {
+ assert_approx_equals(actual, expected, TIME_PRECISION, description);
+ }
+ }
+}
diff --git a/testing/web-platform/tests/web-animations/resources/timing-tests.js b/testing/web-platform/tests/web-animations/resources/timing-tests.js
new file mode 100644
index 0000000000..4b0f021f74
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/resources/timing-tests.js
@@ -0,0 +1,46 @@
+'use strict';
+
+// =================================
+//
+// Common timing parameter test data
+//
+// =================================
+
+
+// ------------------------------
+// Delay values
+// ------------------------------
+
+const gBadDelayValues = [
+ NaN, Infinity, -Infinity
+];
+
+// ------------------------------
+// Duration values
+// ------------------------------
+
+const gGoodDurationValues = [
+ { specified: 123.45, computed: 123.45 },
+ { specified: 'auto', computed: 0 },
+ { specified: Infinity, computed: Infinity },
+];
+
+const gBadDurationValues = [
+ -1, NaN, -Infinity, 'abc', '100'
+];
+
+// ------------------------------
+// iterationStart values
+// ------------------------------
+
+const gBadIterationStartValues = [
+ -1, NaN, Infinity, -Infinity
+];
+
+// ------------------------------
+// iterations values
+// ------------------------------
+
+const gBadIterationsValues = [
+ -1, -Infinity, NaN
+];
diff --git a/testing/web-platform/tests/web-animations/resources/timing-utils.js b/testing/web-platform/tests/web-animations/resources/timing-utils.js
new file mode 100644
index 0000000000..fba62a1f6d
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/resources/timing-utils.js
@@ -0,0 +1,52 @@
+'use strict';
+
+// =======================================
+//
+// Utility functions for testing timing
+//
+// =======================================
+
+
+// ------------------------------
+// Helper functions
+// ------------------------------
+
+// Utility function to check that a subset of timing properties have their
+// default values.
+function assert_default_timing_except(effect, propertiesToSkip) {
+ const defaults = {
+ delay: 0,
+ endDelay: 0,
+ fill: 'auto',
+ iterationStart: 0,
+ iterations: 1,
+ duration: 'auto',
+ direction: 'normal',
+ easing: 'linear',
+ };
+
+ for (const prop of Object.keys(defaults)) {
+ if (propertiesToSkip.includes(prop)) {
+ continue;
+ }
+
+ assert_equals(
+ effect.getTiming()[prop],
+ defaults[prop],
+ `${prop} parameter has default value:`
+ );
+ }
+}
+
+function waitForAnimationTime(animation, time) {
+ return new Promise((resolve) => {
+ function raf() {
+ if (animation.currentTime < time) {
+ requestAnimationFrame(raf);
+ } else {
+ resolve();
+ }
+ }
+ requestAnimationFrame(raf);
+ });
+}
diff --git a/testing/web-platform/tests/web-animations/resources/xhr-doc.py b/testing/web-platform/tests/web-animations/resources/xhr-doc.py
new file mode 100644
index 0000000000..0023a44e22
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/resources/xhr-doc.py
@@ -0,0 +1,5 @@
+def main(request, response):
+ headers = [(b"Content-type", b"text/html;charset=utf-8")]
+ content = u"<!doctype html><div id=test></div>"
+
+ return headers, content
diff --git a/testing/web-platform/tests/web-animations/responsive/assorted-lengths.html b/testing/web-platform/tests/web-animations/responsive/assorted-lengths.html
new file mode 100644
index 0000000000..50e01f766c
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/responsive/assorted-lengths.html
@@ -0,0 +1,102 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<div id='container'>
+<div id='element'></div>
+</div>
+
+<script>
+var element = document.getElementById('element');
+var container = document.getElementById('container');
+
+test(function() {
+ element.style.fontSize = '10px';
+ var player = element.animate([{bottom: '3em'}, {bottom: '5em'}], 10);
+ player.pause();
+ player.currentTime = 5;
+ element.style.fontSize = '20px';
+ assert_equals(getComputedStyle(element).bottom, '80px');
+}, 'bottom responsive to style changes');
+
+test(function() {
+ element.style.fontSize = '10px';
+ var player = element.animate([{height: '3em'}, {height: '5em'}], 10);
+ player.pause();
+ player.currentTime = 5;
+ element.style.fontSize = '20px';
+ assert_equals(getComputedStyle(element).height, '80px');
+}, 'height responsive to style changes');
+
+test(function() {
+ element.style.fontSize = '10px';
+ var player = element.animate([{letterSpacing: '3em'}, {letterSpacing: '5em'}], 10);
+ player.pause();
+ player.currentTime = 5;
+ element.style.fontSize = '20px';
+ assert_equals(getComputedStyle(element).letterSpacing, '80px');
+}, 'letterSpacing responsive to style changes');
+
+test(function() {
+ var player = element.animate([{letterSpacing: 'normal'}, {letterSpacing: 'normal'}], 10);
+ player.pause();
+ player.currentTime = 5;
+ assert_equals(getComputedStyle(element).letterSpacing, 'normal');
+}, 'letterSpacing can be normal');
+
+test(function() {
+ element.style.fontSize = '10px';
+ var player = element.animate([{marginRight: '3em'}, {marginRight: '5em'}], 10);
+ player.pause();
+ player.currentTime = 5;
+ element.style.fontSize = '20px';
+ assert_equals(getComputedStyle(element).marginRight, '80px');
+}, 'marginRight responsive to style changes');
+
+test(function() {
+ element.style.fontSize = '10px';
+ container.style.width = '300px';
+ var player = element.animate([{marginRight: '3em'}, {marginRight: '50%'}], 10);
+ player.pause();
+ player.currentTime = 5;
+ element.style.fontSize = '20px';
+ container.style.width = '600px';
+ assert_equals(getComputedStyle(element).marginRight, '180px');
+}, 'marginRight allows percentages');
+
+test(function() {
+ element.style.fontSize = '10px';
+ var player = element.animate([{outlineOffset: '3em'}, {outlineOffset: '5em'}], 10);
+ player.pause();
+ player.currentTime = 5;
+ element.style.outline = 'dashed thin';
+ element.style.fontSize = '20px';
+ assert_equals(getComputedStyle(element).outlineOffset, '80px');
+}, 'outlineOffset responsive to style changes');
+
+test(function() {
+ container.style.fontSize = '10px';
+ var player = container.animate([{perspective: '3em'}, {perspective: '5em'}], 10);
+ player.pause();
+ player.currentTime = 5;
+ container.style.fontSize = '20px';
+ assert_equals(getComputedStyle(container).perspective, '80px');
+}, 'perspective responsive to style changes');
+
+test(function() {
+ var player = element.animate([{perspective: 'none'}, {perspective: 'none'}], 10);
+ player.pause();
+ player.currentTime = 10;
+ assert_equals(getComputedStyle(element).perspective, 'none');
+}, 'perspective can be none');
+
+test(function() {
+ element.style.fontSize = '10px';
+ var player = element.animate([{wordSpacing: '3em'}, {wordSpacing: '5em'}], 10);
+ player.pause();
+ player.currentTime = 5;
+ element.style.fontSize = '20px';
+ assert_equals(getComputedStyle(element).wordSpacing, '80px');
+}, 'wordSpacing responsive to style changes');
+
+</script>
diff --git a/testing/web-platform/tests/web-animations/responsive/backgroundPosition.html b/testing/web-platform/tests/web-animations/responsive/backgroundPosition.html
new file mode 100644
index 0000000000..cd6c6991a3
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/responsive/backgroundPosition.html
@@ -0,0 +1,25 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<div id='container'>
+<div id='element'></div>
+</div>
+
+<script>
+var element = document.getElementById('element');
+var container = document.getElementById('container');
+
+test(function() {
+ element.style.fontSize = '10px';
+ var player = element.animate([
+ {backgroundPosition: '10% 1em'},
+ {backgroundPosition: '20% 5em'}],
+ 10);
+ player.pause();
+ player.currentTime = 5;
+ element.style.fontSize = '20px';
+ assert_equals(getComputedStyle(element).backgroundPosition, '15% 60px');
+}, 'Background position responsive to style changes');
+
+</script>
diff --git a/testing/web-platform/tests/web-animations/responsive/backgroundSize.html b/testing/web-platform/tests/web-animations/responsive/backgroundSize.html
new file mode 100644
index 0000000000..1e9ccf96a1
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/responsive/backgroundSize.html
@@ -0,0 +1,22 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<div id='container'>
+<div id='element'></div>
+</div>
+
+<script>
+var element = document.getElementById('element');
+var container = document.getElementById('container');
+
+test(function() {
+ container.style.fontSize = '50px';
+ var player = element.animate([{backgroundSize: '300px 30px'}, {backgroundSize: '10em 1em'}], 10);
+ player.pause();
+ player.currentTime = 5;
+ container.style.fontSize = '10px';
+ assert_equals(getComputedStyle(element).backgroundSize, '200px 20px');
+}, 'Border image width responsive to font size changes');
+
+</script>
diff --git a/testing/web-platform/tests/web-animations/responsive/baselineShift.html b/testing/web-platform/tests/web-animations/responsive/baselineShift.html
new file mode 100644
index 0000000000..4ccaaf33ac
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/responsive/baselineShift.html
@@ -0,0 +1,43 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<svg>
+ <text>
+ <tspan id='container'>
+ <tspan id='element'></tspan>
+ </tspan>
+ </text>
+</svg>
+
+<script>
+var element = document.getElementById('element');
+var container = document.getElementById('container');
+
+test(function() {
+ element.style.fontSize = '10px';
+ var player = element.animate([{baselineShift: '3em'}, {baselineShift: '5em'}], 10);
+ player.pause();
+ player.currentTime = 5;
+ element.style.fontSize = '20px';
+ assert_equals(getComputedStyle(element).baselineShift, '80px');
+}, 'baselineShift responsive to style changes');
+
+test(function() {
+ container.style.baselineShift = 'sub';
+ var player = element.animate([{baselineShift: 'inherit'}, {baselineShift: '20px'}], 4000);
+ player.pause();
+
+ player.currentTime = 1000;
+ assert_equals(getComputedStyle(element).baselineShift, 'sub');
+
+ container.style.baselineShift = 'super';
+ assert_equals(getComputedStyle(element).baselineShift, 'super');
+
+ container.style.baselineShift = '100px';
+ assert_equals(getComputedStyle(element).baselineShift, '80px');
+
+ container.style.baselineShift = 'sub';
+ assert_equals(getComputedStyle(element).baselineShift, 'sub');
+}, 'baselineShift responsive to inherited changes');
+</script>
diff --git a/testing/web-platform/tests/web-animations/responsive/borderImageWidth.html b/testing/web-platform/tests/web-animations/responsive/borderImageWidth.html
new file mode 100644
index 0000000000..4b832889bd
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/responsive/borderImageWidth.html
@@ -0,0 +1,22 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<div id='container'>
+<div id='element'></div>
+</div>
+
+<script>
+var element = document.getElementById('element');
+var container = document.getElementById('container');
+
+test(function() {
+ container.style.fontSize = '50px';
+ var player = element.animate([{borderImageWidth: '300px 30px'}, {borderImageWidth: '10em 1em'}], 10);
+ player.pause();
+ player.currentTime = 5;
+ container.style.fontSize = '10px';
+ assert_equals(getComputedStyle(element).borderImageWidth, '200px 20px');
+}, 'Border image width responsive to font size changes');
+
+</script>
diff --git a/testing/web-platform/tests/web-animations/responsive/borderRadius.html b/testing/web-platform/tests/web-animations/responsive/borderRadius.html
new file mode 100644
index 0000000000..c59696ec90
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/responsive/borderRadius.html
@@ -0,0 +1,39 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id='element'></div>
+<script>
+
+var element = document.getElementById('element');
+
+test(function() {
+ var keyframes = [
+ {borderTopLeftRadius: '8% 16%'},
+ {borderTopLeftRadius: '12% 24%'}
+ ];
+
+ element.style.width = '100';
+ element.style.height = '200';
+ var player = element.animate(keyframes, 10);
+ player.pause();
+ player.currentTime = 5;
+ element.style.width = '300';
+ element.style.height = '600';
+ assert_equals(getComputedStyle(element).borderTopLeftRadius, '10% 20%');
+}, 'Border radius percentages are supported');
+
+test(function() {
+ var keyframes = [
+ {borderTopLeftRadius: '8em 16em'},
+ {borderTopLeftRadius: '12em 24em'}
+ ];
+
+ element.style.fontSize = '10px';
+ var player = element.animate(keyframes, 10);
+ player.pause();
+ player.currentTime = 5;
+ element.style.fontSize = '20px';
+ assert_equals(getComputedStyle(element).borderTopLeftRadius, '200px 400px');
+}, 'Border radius lengths respond to style changes');
+
+</script>
diff --git a/testing/web-platform/tests/web-animations/responsive/borderWidth.html b/testing/web-platform/tests/web-animations/responsive/borderWidth.html
new file mode 100644
index 0000000000..090d9a185a
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/responsive/borderWidth.html
@@ -0,0 +1,33 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<div id='container'>
+<div id='element'></div>
+</div>
+
+<script>
+var element = document.getElementById('element');
+var container = document.getElementById('container');
+
+test(function() {
+ element.style.fontSize = '10px';
+ element.style.border = 'none';
+ var player = element.animate([{borderBottomWidth: '300px'}, {borderBottomWidth: '10em'}], 10);
+ player.pause();
+ player.currentTime = 5;
+ element.style.border = 'solid';
+ assert_equals(getComputedStyle(element).borderBottomWidth, '200px');
+}, 'Border width responsive to border style changes');
+
+test(function() {
+ element.style.fontSize = '50px';
+ element.style.border = 'solid';
+ var player = element.animate([{borderBottomWidth: '300px'}, {borderBottomWidth: '10em'}], 10);
+ player.pause();
+ player.currentTime = 5;
+ element.style.fontSize = '10px';
+ assert_equals(getComputedStyle(element).borderBottomWidth, '200px');
+}, 'Border width responsive to font size changes');
+
+</script>
diff --git a/testing/web-platform/tests/web-animations/responsive/boxShadow.html b/testing/web-platform/tests/web-animations/responsive/boxShadow.html
new file mode 100644
index 0000000000..bd1911132d
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/responsive/boxShadow.html
@@ -0,0 +1,30 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id='container'>
+ <div id='element'></div>
+</div>
+<script>
+
+var container = document.getElementById('container');
+var element = document.getElementById('element');
+
+test(function() {
+ container.style.fontSize = '10px';
+ container.style.color = 'red';
+
+ var keyframes = [
+ {boxShadow: '10em 10em currentColor'},
+ {boxShadow: '10em 10em currentColor'}
+ ];
+ var player = element.animate(keyframes, 10);
+ player.pause();
+ player.currentTime = 5;
+
+ var boxShadow = getComputedStyle(element).boxShadow;
+ container.style.fontSize = '20px';
+ container.style.color = 'green';
+ assert_not_equals(getComputedStyle(element).boxShadow, boxShadow);
+}, 'boxShadow responsive to style changes');
+
+</script>
diff --git a/testing/web-platform/tests/web-animations/responsive/clip.html b/testing/web-platform/tests/web-animations/responsive/clip.html
new file mode 100644
index 0000000000..316e977576
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/responsive/clip.html
@@ -0,0 +1,45 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id='container'>
+ <div id='element'></div>
+</div>
+<script>
+
+var container = document.getElementById('container');
+var element = document.getElementById('element');
+
+test(function() {
+ var keyframes = [
+ {clip: 'inherit'},
+ {clip: 'rect(10px, 20px, 30px, 40px)'}
+ ];
+
+ container.style.clip = 'rect(10px, 20px, 30px, 40px)';
+ var player = element.animate(keyframes, 20);
+
+ player.pause();
+ player.currentTime = 5;
+ assert_equals(getComputedStyle(element).clip, 'rect(10px, 20px, 30px, 40px)');
+
+ container.style.clip = 'rect(10px, 20px, 430px, 440px)';
+ assert_equals(getComputedStyle(element).clip, 'rect(10px, 20px, 330px, 340px)');
+}, 'clip responsive to inherited clip changes');
+
+test(function() {
+ var keyframes = [
+ {clip: 'inherit'},
+ {clip: 'rect(10px, 20px, 30px, auto)'}
+ ];
+
+ container.style.clip = 'auto';
+ var player = element.animate(keyframes, 20);
+
+ player.pause();
+ player.currentTime = 5;
+ assert_equals(getComputedStyle(element).clip, 'auto');
+
+ container.style.clip = 'rect(410px, 420px, 30px, auto)';
+ assert_equals(getComputedStyle(element).clip, 'rect(310px, 320px, 30px, auto)');
+}, 'clip responsive to inherited clip changes from auto');
+</script>
diff --git a/testing/web-platform/tests/web-animations/responsive/columnCount.html b/testing/web-platform/tests/web-animations/responsive/columnCount.html
new file mode 100644
index 0000000000..c92d5cbf8f
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/responsive/columnCount.html
@@ -0,0 +1,32 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id='container'>
+ <div id='element'></div>
+</div>
+<script>
+
+var container = document.getElementById('container');
+var element = document.getElementById('element');
+
+test(function() {
+ var keyframes = [
+ {columnCount: 'inherit'},
+ {columnCount: '6'}
+ ];
+
+ container.style.columnCount = 'auto';
+ var player = element.animate(keyframes, 20);
+
+ player.pause();
+ player.currentTime = 5;
+ assert_equals(getComputedStyle(element).columnCount, 'auto');
+
+ container.style.columnCount = '2';
+ assert_equals(getComputedStyle(element).columnCount, '3');
+
+ player.currentTime = 10;
+ container.style.columnCount = '8';
+ assert_equals(getComputedStyle(element).columnCount, '7');
+}, 'column-count responsive to inherited column-count changes');
+</script>
diff --git a/testing/web-platform/tests/web-animations/responsive/columnGap.html b/testing/web-platform/tests/web-animations/responsive/columnGap.html
new file mode 100644
index 0000000000..43d44152cb
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/responsive/columnGap.html
@@ -0,0 +1,49 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<div id='container'>
+<div id='element'></div>
+</div>
+
+<script>
+var element = document.getElementById('element');
+var container = document.getElementById('container');
+
+test(function() {
+ element.style.fontSize = '10px';
+ var player = element.animate([{columnGap: '3em'}, {columnGap: '5em'}], 10);
+ player.pause();
+ player.currentTime = 5;
+ element.style.fontSize = '20px';
+ assert_equals(getComputedStyle(element).columnGap, '80px');
+}, 'column-gap responsive to style changes');
+
+test(function() {
+ element.style.fontSize = '10px';
+ var player = element.animate([{columnGap: '40px'}, {columnGap: 'calc(40px - 2em)'}], 10);
+ player.pause();
+
+ player.currentTime = 5;
+ element.style.fontSize = '40px';
+ assert_equals(getComputedStyle(element).columnGap, '20px');
+
+ player.currentTime = 7.5;
+ assert_equals(getComputedStyle(element).columnGap, '10px');
+}, 'column-gap clamped to 0px on keyframes');
+
+test(function() {
+ container.style.columnGap = 'normal';
+ var player = element.animate([{columnGap: 'inherit'}, {columnGap: '20px'}], 4000);
+ player.pause();
+
+ player.currentTime = 1000;
+ assert_equals(getComputedStyle(element).columnGap, 'normal');
+
+ container.style.columnGap = '100px';
+ assert_equals(getComputedStyle(element).columnGap, '80px');
+
+ container.style.columnGap = 'normal';
+ assert_equals(getComputedStyle(element).columnGap, 'normal');
+}, 'column-gap responsive to inherited changes');
+</script>
diff --git a/testing/web-platform/tests/web-animations/responsive/d.html b/testing/web-platform/tests/web-animations/responsive/d.html
new file mode 100644
index 0000000000..55c6c23a0e
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/responsive/d.html
@@ -0,0 +1,27 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id='container'>
+ <div id='element'></div>
+</div>
+<script>
+
+var container = document.getElementById('container');
+var element = document.getElementById('element');
+
+test(function() {
+ var keyframes = [
+ {d: 'inherit'},
+ {d: 'path("M 0 0 H 200")'}
+ ];
+
+ container.style.d = 'path("M 0 0 H 100")';
+ var player = element.animate(keyframes, 10);
+
+ player.pause();
+ player.currentTime = 5;
+ container.style.d = 'path("M 0 0 H 400")';
+ assert_equals(getComputedStyle(element).d, 'path("M 0 0 H 300")');
+}, 'd responsive to inherited d changes');
+
+</script>
diff --git a/testing/web-platform/tests/web-animations/responsive/font-size-adjust.html b/testing/web-platform/tests/web-animations/responsive/font-size-adjust.html
new file mode 100644
index 0000000000..282ec7fede
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/responsive/font-size-adjust.html
@@ -0,0 +1,32 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id='container'>
+ <div id='element'></div>
+</div>
+<script>
+
+var container = document.getElementById('container');
+var element = document.getElementById('element');
+
+test(function() {
+ var keyframes = [
+ {fontSizeAdjust: 'inherit'},
+ {fontSizeAdjust: '6'}
+ ];
+
+ container.style.fontSizeAdjust = 'none';
+ var player = element.animate(keyframes, 20);
+
+ player.pause();
+ player.currentTime = 5;
+ assert_equals(getComputedStyle(element).fontSizeAdjust, 'none');
+
+ container.style.fontSizeAdjust = '2';
+ assert_equals(getComputedStyle(element).fontSizeAdjust, '3');
+
+ player.currentTime = 10;
+ container.style.fontSizeAdjust = '8';
+ assert_equals(getComputedStyle(element).fontSizeAdjust, '7');
+}, 'font-size-adjust responsive to inherited font-size-adjust changes');
+</script>
diff --git a/testing/web-platform/tests/web-animations/responsive/fontSize.html b/testing/web-platform/tests/web-animations/responsive/fontSize.html
new file mode 100644
index 0000000000..d65aeacd46
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/responsive/fontSize.html
@@ -0,0 +1,72 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id='container'>
+ <div id='element'></div>
+</div>
+<script>
+
+var container = document.getElementById('container');
+var element = document.getElementById('element');
+
+test(function() {
+ var keyframes = [
+ {fontSize: 'larger'},
+ {fontSize: 'larger'}
+ ];
+
+ container.style.fontSize = 'small';
+ var player = element.animate(keyframes, 10);
+ player.pause();
+ player.currentTime = 5;
+ fontSize = getComputedStyle(element).fontSize;
+ container.style.fontSize = 'medium';
+ assert_not_equals(getComputedStyle(element).fontSize, fontSize);
+}, 'Relative font size larger responsive to style changes');
+
+test(function() {
+ var keyframes = [
+ {fontSize: 'smaller'},
+ {fontSize: 'smaller'}
+ ];
+
+ container.style.fontSize = 'large';
+ var player = element.animate(keyframes, 10);
+ player.pause();
+ player.currentTime = 5;
+ fontSize = getComputedStyle(element).fontSize;
+ container.style.fontSize = 'medium';
+ assert_not_equals(getComputedStyle(element).fontSize, fontSize);
+}, 'Relative font size smaller responsive to style changes');
+
+test(function() {
+ var keyframes = [
+ {fontSize: 'medium'},
+ {fontSize: 'medium'}
+ ];
+
+ container.style.fontFamily = 'cursive';
+ var player = element.animate(keyframes, 10);
+ player.pause();
+ player.currentTime = 5;
+ fontSize = getComputedStyle(element).fontSize;
+ container.style.fontFamily = 'monospace';
+ assert_not_equals(getComputedStyle(element).fontSize, fontSize);
+}, 'Font size medium responsive to style changes');
+
+test(function() {
+ var keyframes = [
+ {fontSize: 'initial'},
+ {fontSize: 'initial'}
+ ];
+
+ container.style.fontFamily = 'monospace';
+ var player = element.animate(keyframes, 10);
+ player.pause();
+ player.currentTime = 5;
+ fontSize = getComputedStyle(element).fontSize;
+ container.style.fontFamily = 'serif';
+ assert_not_equals(getComputedStyle(element).fontSize, fontSize);
+}, 'Font size initial responsive to style changes');
+
+</script>
diff --git a/testing/web-platform/tests/web-animations/responsive/fontWeight.html b/testing/web-platform/tests/web-animations/responsive/fontWeight.html
new file mode 100644
index 0000000000..e8fbbae0e7
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/responsive/fontWeight.html
@@ -0,0 +1,42 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id='container'>
+ <div id='element'></div>
+</div>
+<script>
+
+var container = document.getElementById('container');
+var element = document.getElementById('element');
+
+test(function() {
+ var keyframes = [
+ {fontWeight: 'bolder'},
+ {fontWeight: 'bolder'}
+ ];
+
+ container.style.fontWeight = '100';
+ var player = element.animate(keyframes, 10);
+ player.pause();
+ player.currentTime = 5;
+ fontWeight = getComputedStyle(element).fontWeight;
+ container.style.fontWeight = '800';
+ assert_not_equals(getComputedStyle(element).fontWeight, fontWeight);
+}, 'Relative font weight bolder responsive to style changes');
+
+test(function() {
+ var keyframes = [
+ {fontWeight: 'lighter'},
+ {fontWeight: 'lighter'}
+ ];
+
+ container.style.fontWeight = '900';
+ var player = element.animate(keyframes, 10);
+ player.pause();
+ player.currentTime = 5;
+ fontWeight = getComputedStyle(element).fontWeight;
+ container.style.fontWeight = '200';
+ assert_not_equals(getComputedStyle(element).fontWeight, fontWeight);
+}, 'Relative font weight lighter responsive to style changes');
+
+</script>
diff --git a/testing/web-platform/tests/web-animations/responsive/lineHeight.html b/testing/web-platform/tests/web-animations/responsive/lineHeight.html
new file mode 100644
index 0000000000..03d45e54c1
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/responsive/lineHeight.html
@@ -0,0 +1,80 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<div id='container'>
+<div id='element'></div>
+</div>
+
+<script>
+var element = document.getElementById('element');
+var container = document.getElementById('container');
+
+test(function() {
+ element.style.fontSize = '10px';
+ var player = element.animate([{lineHeight: '3em'}, {lineHeight: '5em'}], 10);
+ player.pause();
+ player.currentTime = 5;
+ element.style.fontSize = '20px';
+ assert_equals(getComputedStyle(element).lineHeight, '80px');
+}, 'lineHeight responsive to style changes');
+
+test(function() {
+ element.style.fontSize = '10px';
+ var player = element.animate([{lineHeight: '40px'}, {lineHeight: 'calc(40px - 2em)'}], 10);
+ player.pause();
+
+ player.currentTime = 5;
+ element.style.fontSize = '40px';
+ assert_equals(getComputedStyle(element).lineHeight, '20px');
+
+ player.currentTime = 7.5;
+ assert_equals(getComputedStyle(element).lineHeight, '10px');
+}, 'lineHeight clamped to 0px on keyframes');
+
+test(function() {
+ container.style.lineHeight = 'normal';
+ var player = element.animate([{lineHeight: 'inherit'}, {lineHeight: '20px'}], 4000);
+ player.pause();
+
+ player.currentTime = 1000;
+ assert_equals(getComputedStyle(element).lineHeight, 'normal');
+
+ container.style.lineHeight = '100px';
+ assert_equals(getComputedStyle(element).lineHeight, '80px');
+
+ container.style.lineHeight = 'normal';
+ assert_equals(getComputedStyle(element).lineHeight, 'normal');
+}, 'lineHeight responsive to inherited changes from keyword');
+
+test(function() {
+ container.style.fontSize = '10px';
+ container.style.lineHeight = '1.0';
+ const expected = getComputedStyle(container).lineHeight;
+ var player = element.animate([{lineHeight: 'inherit'}, {lineHeight: '20px'}], 4000);
+ player.pause();
+
+ player.currentTime = 1000;
+ getComputedStyle(element).lineHeight;
+
+ container.style.lineHeight = '100px';
+ assert_equals(getComputedStyle(element).lineHeight, '80px');
+}, 'lineHeight responsive to inherited changes from number');
+
+test(function() {
+ container.style.fontSize = '10px';
+ container.style.lineHeight = '1';
+ var player = element.animate([{lineHeight: 'inherit'}, {lineHeight: '2'}], 4000);
+ player.pause();
+
+ player.currentTime = 1000;
+ const expected = getComputedStyle(element).lineHeight;
+
+ container.style.lineHeight = '97px';
+ assert_equals(getComputedStyle(element).lineHeight, '97px');
+
+ container.style.lineHeight = '1';
+ assert_equals(getComputedStyle(element).lineHeight, expected);
+}, 'lineHeight responsive to inherited changes from length');
+
+</script>
diff --git a/testing/web-platform/tests/web-animations/responsive/minHeight.html b/testing/web-platform/tests/web-animations/responsive/minHeight.html
new file mode 100644
index 0000000000..07474bc3f4
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/responsive/minHeight.html
@@ -0,0 +1,35 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<div id='container'>
+<div id='element'></div>
+</div>
+
+<script>
+var element = document.getElementById('element');
+var container = document.getElementById('container');
+
+test(function() {
+ element.style.fontSize = '10px';
+ var player = element.animate([{minHeight: '3em'}, {minHeight: '5em'}], 10);
+ player.pause();
+ player.currentTime = 5;
+ element.style.fontSize = '20px';
+ assert_equals(getComputedStyle(element).minHeight, '80px');
+}, 'minHeight responsive to style changes');
+
+test(function() {
+ element.style.fontSize = '10px';
+ var player = element.animate([{minHeight: '40px'}, {minHeight: 'calc(40px - 2em)'}], 10);
+ player.pause();
+
+ player.currentTime = 5;
+ element.style.fontSize = '40px';
+ assert_equals(getComputedStyle(element).minHeight, '20px');
+
+ player.currentTime = 7.5;
+ assert_equals(getComputedStyle(element).minHeight, '10px');
+}, 'minHeight clamped to 0px on keyframes');
+
+</script>
diff --git a/testing/web-platform/tests/web-animations/responsive/offset-path.html b/testing/web-platform/tests/web-animations/responsive/offset-path.html
new file mode 100644
index 0000000000..eff8ede60f
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/responsive/offset-path.html
@@ -0,0 +1,27 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id='container'>
+ <div id='element'></div>
+</div>
+<script>
+
+var container = document.getElementById('container');
+var element = document.getElementById('element');
+
+test(function() {
+ var keyframes = [
+ {offsetPath: 'inherit'},
+ {offsetPath: 'path("M 0 0 H 200")'}
+ ];
+
+ container.style.offsetPath = 'path("M 0 0 H 100")';
+ var player = element.animate(keyframes, 10);
+
+ player.pause();
+ player.currentTime = 5;
+ container.style.offsetPath = 'path("M 0 0 H 400")';
+ assert_equals(getComputedStyle(element).offsetPath, 'path("M 0 0 H 300")');
+}, 'offset-path responsive to inherited offset-path changes');
+
+</script>
diff --git a/testing/web-platform/tests/web-animations/responsive/offsetDistance.html b/testing/web-platform/tests/web-animations/responsive/offsetDistance.html
new file mode 100644
index 0000000000..d56da9c767
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/responsive/offsetDistance.html
@@ -0,0 +1,57 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id='container'>
+ <div id='element'></div>
+</div>
+<script>
+
+var container = document.getElementById('container');
+var element = document.getElementById('element');
+
+test(function() {
+ var keyframes = [
+ {offsetDistance: '4%'},
+ {offsetDistance: '12%'}
+ ];
+
+ element.style.width = '100';
+ element.style.height = '200';
+ var player = element.animate(keyframes, 20);
+ player.pause();
+ player.currentTime = 15;
+ element.style.width = '300';
+ element.style.height = '600';
+ assert_equals(getComputedStyle(element).offsetDistance, '10%');
+}, 'offsetDistance percentages are supported');
+
+test(function() {
+ var keyframes = [
+ {offsetDistance: '8em'},
+ {offsetDistance: '16em'}
+ ];
+
+ element.style.fontSize = '10px';
+ var player = element.animate(keyframes, 20);
+ player.pause();
+ player.currentTime = 5;
+ element.style.fontSize = '20px';
+ assert_equals(getComputedStyle(element).offsetDistance, '200px');
+}, 'offsetDistance lengths respond to style changes');
+
+test(function() {
+ var keyframes = [
+ {offsetDistance: 'inherit'},
+ {offsetDistance: '200px'}
+ ];
+
+ container.style.offsetDistance = '100px';
+ var player = element.animate(keyframes, 10);
+
+ player.pause();
+ player.currentTime = 5;
+
+ container.style.offsetDistance = '400px';
+ assert_equals(getComputedStyle(element).offsetDistance, '300px');
+}, 'offsetDistance responsive to inherited offsetDistance changes');
+</script>
diff --git a/testing/web-platform/tests/web-animations/responsive/offsetRotate.html b/testing/web-platform/tests/web-animations/responsive/offsetRotate.html
new file mode 100644
index 0000000000..f0957e15c5
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/responsive/offsetRotate.html
@@ -0,0 +1,43 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id='container'>
+ <div id='element'></div>
+</div>
+<script>
+
+var container = document.getElementById('container');
+var element = document.getElementById('element');
+
+test(function() {
+ var keyframes = [
+ {offsetRotate: 'inherit'},
+ {offsetRotate: 'auto 200deg'}
+ ];
+
+ container.style.offsetRotate = 'auto 100deg';
+ var player = element.animate(keyframes, 10);
+
+ player.pause();
+ player.currentTime = 5;
+ assert_equals(getComputedStyle(element).offsetRotate, 'auto 150deg');
+
+ container.style.offsetRotate = '400deg';
+ assert_equals(getComputedStyle(element).offsetRotate, 'auto 200deg');
+
+ container.style.offsetRotate = '400deg auto';
+ assert_equals(getComputedStyle(element).offsetRotate, 'auto 300deg');
+
+ container.style.offsetRotate = '800deg auto';
+ assert_equals(getComputedStyle(element).offsetRotate, 'auto 500deg');
+
+ container.style.offsetRotate = '400deg';
+ assert_equals(getComputedStyle(element).offsetRotate, 'auto 200deg');
+
+ container.style.offsetRotate = '800deg auto';
+ assert_equals(getComputedStyle(element).offsetRotate, 'auto 500deg');
+
+ container.style.offsetRotate = '400deg auto';
+ assert_equals(getComputedStyle(element).offsetRotate, 'auto 300deg');
+}, 'offsetRotate responsive to inherited offsetRotate changes');
+</script>
diff --git a/testing/web-platform/tests/web-animations/responsive/opacity.html b/testing/web-platform/tests/web-animations/responsive/opacity.html
new file mode 100644
index 0000000000..0bedfc879a
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/responsive/opacity.html
@@ -0,0 +1,48 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id='container'>
+ <div id='element'></div>
+</div>
+<script>
+
+'use strict';
+var container = document.getElementById('container');
+var element = document.getElementById('element');
+
+var properties = [
+ 'fillOpacity',
+ 'floodOpacity',
+ 'opacity',
+ 'shapeImageThreshold',
+ 'stopOpacity',
+ 'strokeOpacity',
+];
+
+for (var property of properties) {
+ test(function() {
+ var initialKeyframe = {};
+ initialKeyframe[property] = 'inherit';
+ var finalKeyframe = {};
+ finalKeyframe[property] = '0.5';
+ var keyframes = [ initialKeyframe, finalKeyframe ];
+
+ container.style[property] = 1;
+ var player = element.animate(keyframes, 10);
+
+ player.pause();
+ player.currentTime = 5;
+ assert_equals(getComputedStyle(element)[property], '0.75');
+
+ container.style[property] = 0.25;
+ assert_equals(getComputedStyle(element)[property], '0.375');
+
+ container.style[property] = -0.5; // clamps to 0
+ assert_equals(getComputedStyle(element)[property], '0.25');
+
+ container.style[property] = 2; // clamps to 1
+ assert_equals(getComputedStyle(element)[property], '0.75');
+ }, property + ' responsive to inherited changes');
+}
+
+</script>
diff --git a/testing/web-platform/tests/web-animations/responsive/perspective.html b/testing/web-platform/tests/web-animations/responsive/perspective.html
new file mode 100644
index 0000000000..f32b4dbad3
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/responsive/perspective.html
@@ -0,0 +1,49 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<div id='container'>
+<div id='element'></div>
+</div>
+
+<script>
+var element = document.getElementById('element');
+var container = document.getElementById('container');
+
+test(function() {
+ element.style.fontSize = '10px';
+ var player = element.animate([{perspective: '3em'}, {perspective: '5em'}], 10);
+ player.pause();
+ player.currentTime = 5;
+ element.style.fontSize = '20px';
+ assert_equals(getComputedStyle(element).perspective, '80px');
+}, 'perspective responsive to style changes');
+
+test(function() {
+ element.style.fontSize = '10px';
+ var player = element.animate([{perspective: '40px'}, {perspective: 'calc(40px - 2em)'}], 10);
+ player.pause();
+
+ player.currentTime = 5;
+ element.style.fontSize = '40px';
+ assert_equals(getComputedStyle(element).perspective, '20px');
+
+ player.currentTime = 7.5;
+ assert_equals(getComputedStyle(element).perspective, '10px');
+}, 'perspective clamped to 0px on keyframes');
+
+test(function() {
+ container.style.perspective = 'none';
+ var player = element.animate([{perspective: 'inherit'}, {perspective: '20px'}], 4000);
+ player.pause();
+
+ player.currentTime = 1000;
+ assert_equals(getComputedStyle(element).perspective, 'none');
+
+ container.style.perspective = '100px';
+ assert_equals(getComputedStyle(element).perspective, '80px');
+
+ container.style.perspective = 'none';
+ assert_equals(getComputedStyle(element).perspective, 'none');
+}, 'perspective responsive to inherited changes');
+</script>
diff --git a/testing/web-platform/tests/web-animations/responsive/resources/block.html b/testing/web-platform/tests/web-animations/responsive/resources/block.html
new file mode 100644
index 0000000000..8284055969
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/responsive/resources/block.html
@@ -0,0 +1,10 @@
+<!DOCTYPE html>
+<style>
+ .testBlock {
+ width: 100px;
+ height: 100px;
+ background: #00cc66;
+ border-top: 50px solid #ffff00;
+ }
+</style>
+<div class="testBlock" id="testBlock"></div> \ No newline at end of file
diff --git a/testing/web-platform/tests/web-animations/responsive/rotate.html b/testing/web-platform/tests/web-animations/responsive/rotate.html
new file mode 100644
index 0000000000..b0e4213200
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/responsive/rotate.html
@@ -0,0 +1,22 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<div id='container'>
+<div id='element'></div>
+</div>
+
+<script>
+var element = document.getElementById('element');
+var container = document.getElementById('container');
+
+test(function() {
+ container.style.rotate = 'none';
+ var player = element.animate([{rotate: 'inherit'}, {rotate: '100deg'}], 10);
+ player.pause();
+ player.currentTime = 2;
+ container.style.rotate = '200deg';
+ assert_equals(getComputedStyle(element).rotate, '180deg');
+}, 'Rotate responsive to inherited rotate changes');
+
+</script>
diff --git a/testing/web-platform/tests/web-animations/responsive/rowGap.html b/testing/web-platform/tests/web-animations/responsive/rowGap.html
new file mode 100644
index 0000000000..b7dae62f23
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/responsive/rowGap.html
@@ -0,0 +1,49 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<div id='container'>
+<div id='element'></div>
+</div>
+
+<script>
+var element = document.getElementById('element');
+var container = document.getElementById('container');
+
+test(function() {
+ element.style.fontSize = '10px';
+ var player = element.animate([{rowGap: '3em'}, {rowGap: '5em'}], 10);
+ player.pause();
+ player.currentTime = 5;
+ element.style.fontSize = '20px';
+ assert_equals(getComputedStyle(element).rowGap, '80px');
+}, 'row-gap responsive to style changes');
+
+test(function() {
+ element.style.fontSize = '10px';
+ var player = element.animate([{rowGap: '40px'}, {rowGap: 'calc(40px - 2em)'}], 10);
+ player.pause();
+
+ player.currentTime = 5;
+ element.style.fontSize = '40px';
+ assert_equals(getComputedStyle(element).rowGap, '20px');
+
+ player.currentTime = 7.5;
+ assert_equals(getComputedStyle(element).rowGap, '10px');
+}, 'row-gap clamped to 0px on keyframes');
+
+test(function() {
+ container.style.rowGap = 'normal';
+ var player = element.animate([{rowGap: 'inherit'}, {rowGap: '20px'}], 4000);
+ player.pause();
+
+ player.currentTime = 1000;
+ assert_equals(getComputedStyle(element).rowGap, 'normal');
+
+ container.style.rowGap = '100px';
+ assert_equals(getComputedStyle(element).rowGap, '80px');
+
+ container.style.rowGap = 'normal';
+ assert_equals(getComputedStyle(element).rowGap, 'normal');
+}, 'row-gap responsive to inherited changes');
+</script>
diff --git a/testing/web-platform/tests/web-animations/responsive/shapeMargin.html b/testing/web-platform/tests/web-animations/responsive/shapeMargin.html
new file mode 100644
index 0000000000..02d6151ad0
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/responsive/shapeMargin.html
@@ -0,0 +1,24 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<div id='container'>
+<div id='element'></div>
+</div>
+
+<script>
+var element = document.getElementById('element');
+var container = document.getElementById('container');
+
+test(function() {
+ element.style.fontSize = '10px';
+ element.style.width = '200px';
+ var player = element.animate([{shapeMargin: '3em'}, {shapeMargin: '80%'}], 10);
+ player.pause();
+ player.currentTime = 5;
+ element.style.fontSize = '20px';
+ element.style.width = '100px';
+ assert_equals(getComputedStyle(element).shapeMargin, 'calc(40% + 30px)');
+}, 'shapeMargin responsive to style changes');
+
+</script>
diff --git a/testing/web-platform/tests/web-animations/responsive/shapeOutside.html b/testing/web-platform/tests/web-animations/responsive/shapeOutside.html
new file mode 100644
index 0000000000..2a54576a33
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/responsive/shapeOutside.html
@@ -0,0 +1,59 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id='container'>
+ <div id='element'></div>
+</div>
+<script>
+
+var container = document.getElementById('container');
+var element = document.getElementById('element');
+
+test(function() {
+ container.style.fontSize = '10px';
+
+ var keyframes = [
+ {shapeOutside: 'circle(10em at 50% 50%)'},
+ {shapeOutside: 'circle(10em at 50% 50%)'}
+ ];
+ var player = element.animate(keyframes, 10);
+ player.pause();
+ player.currentTime = 5;
+
+ var shapeOutside = getComputedStyle(element).shapeOutside;
+ container.style.fontSize = '20px';
+ assert_not_equals(getComputedStyle(element).shapeOutside, shapeOutside);
+}, 'shapeOutside responsive to style changes');
+
+test(function() {
+ var keyframes = [
+ {shapeOutside: 'inherit'},
+ {shapeOutside: 'circle(200px at 50% 50%)'}
+ ];
+
+ container.style.shapeOutside = 'circle(100px at 50% 50%)';
+ var player = element.animate(keyframes, 10);
+
+ player.pause();
+ player.currentTime = 5;
+ assert_equals(getComputedStyle(element).shapeOutside, 'circle(150px at 50% 50%)');
+
+ container.style.shapeOutside = 'inset(100%)';
+ assert_equals(getComputedStyle(element).shapeOutside, 'circle(200px at 50% 50%)');
+
+ container.style.shapeOutside = 'circle(400px at 50% 50%)';
+ assert_equals(getComputedStyle(element).shapeOutside, 'circle(300px at 50% 50%)');
+
+ container.style.shapeOutside = 'circle(800px at 50% 50%)';
+ assert_equals(getComputedStyle(element).shapeOutside, 'circle(500px at 50% 50%)');
+
+ container.style.shapeOutside = 'inset(100%)';
+ assert_equals(getComputedStyle(element).shapeOutside, 'circle(200px at 50% 50%)');
+
+ container.style.shapeOutside = 'circle(800px at 50% 50%)';
+ assert_equals(getComputedStyle(element).shapeOutside, 'circle(500px at 50% 50%)');
+
+ container.style.shapeOutside = 'circle(400px at 50% 50%)';
+ assert_equals(getComputedStyle(element).shapeOutside, 'circle(300px at 50% 50%)');
+}, 'shapeOutside responsive to inherited shapeOutside changes');
+</script>
diff --git a/testing/web-platform/tests/web-animations/responsive/strokeDasharray.html b/testing/web-platform/tests/web-animations/responsive/strokeDasharray.html
new file mode 100644
index 0000000000..a1ccb30e90
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/responsive/strokeDasharray.html
@@ -0,0 +1,28 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id='container'>
+ <div id='element'></div>
+</div>
+<script>
+
+var container = document.getElementById('container');
+var element = document.getElementById('element');
+
+test(function() {
+ container.style.fontSize = '10px';
+
+ var keyframes = [
+ {strokeDasharray: '10em 10em'},
+ {strokeDasharray: '10em 10em'}
+ ];
+ var player = element.animate(keyframes, 10);
+ player.pause();
+ player.currentTime = 5;
+
+ var strokeDasharray = getComputedStyle(element).strokeDasharray;
+ container.style.fontSize = '20px';
+ assert_not_equals(getComputedStyle(element).strokeDasharray, strokeDasharray);
+}, 'strokeDasharray responsive to style changes');
+
+</script>
diff --git a/testing/web-platform/tests/web-animations/responsive/text-size-adjust.html b/testing/web-platform/tests/web-animations/responsive/text-size-adjust.html
new file mode 100644
index 0000000000..203b067a77
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/responsive/text-size-adjust.html
@@ -0,0 +1,29 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id='container'>
+ <div id='element'></div>
+</div>
+<script>
+
+var container = document.getElementById('container');
+var element = document.getElementById('element');
+
+test(function() {
+ var keyframes = [
+ {textSizeAdjust: 'inherit'},
+ {textSizeAdjust: '60%'}
+ ];
+
+ container.style.textSizeAdjust = '100%';
+ var player = element.animate(keyframes, 20);
+
+ player.pause();
+ player.currentTime = 5;
+ assert_equals(getComputedStyle(element).textSizeAdjust, '90%');
+
+ player.currentTime = 10;
+ container.style.textSizeAdjust = '50%';
+ assert_equals(getComputedStyle(element).textSizeAdjust, '55%');
+}, 'text-size-adjust responsive to inherited text-size-adjust changes');
+</script>
diff --git a/testing/web-platform/tests/web-animations/responsive/textIndent.html b/testing/web-platform/tests/web-animations/responsive/textIndent.html
new file mode 100644
index 0000000000..430417d28b
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/responsive/textIndent.html
@@ -0,0 +1,59 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id='container'>
+ <div id='element'></div>
+</div>
+<script>
+
+var container = document.getElementById('container');
+var element = document.getElementById('element');
+
+test(function() {
+ container.style.fontSize = '10px';
+
+ var keyframes = [
+ {textIndent: '10em hanging'},
+ {textIndent: '10em hanging'}
+ ];
+ var player = element.animate(keyframes, 10);
+ player.pause();
+ player.currentTime = 5;
+
+ var textIndent = getComputedStyle(element).textIndent;
+ container.style.fontSize = '20px';
+ assert_not_equals(getComputedStyle(element).textIndent, textIndent);
+}, 'textIndent responsive to style changes');
+
+test(function() {
+ var keyframes = [
+ {textIndent: 'inherit'},
+ {textIndent: '200px hanging each-line'}
+ ];
+
+ container.style.textIndent = '100px hanging each-line';
+ var player = element.animate(keyframes, 10);
+
+ player.pause();
+ player.currentTime = 5;
+ assert_equals(getComputedStyle(element).textIndent, '150px hanging each-line');
+
+ container.style.textIndent = '400px hanging';
+ assert_equals(getComputedStyle(element).textIndent, '200px hanging each-line');
+
+ container.style.textIndent = '400px hanging each-line';
+ assert_equals(getComputedStyle(element).textIndent, '300px hanging each-line');
+
+ container.style.textIndent = '800px hanging each-line';
+ assert_equals(getComputedStyle(element).textIndent, '500px hanging each-line');
+
+ container.style.textIndent = '400px each-line';
+ assert_equals(getComputedStyle(element).textIndent, '200px hanging each-line');
+
+ container.style.textIndent = '800px hanging each-line';
+ assert_equals(getComputedStyle(element).textIndent, '500px hanging each-line');
+
+ container.style.textIndent = '400px hanging each-line';
+ assert_equals(getComputedStyle(element).textIndent, '300px hanging each-line');
+}, 'textIndent responsive to inherited textIndent changes');
+</script>
diff --git a/testing/web-platform/tests/web-animations/responsive/to-color-change.html b/testing/web-platform/tests/web-animations/responsive/to-color-change.html
new file mode 100644
index 0000000000..6c3fcccf62
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/responsive/to-color-change.html
@@ -0,0 +1,253 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<style>
+a { visibility: hidden; }
+</style>
+
+<div id='container'>
+ <div id='child'></div>
+</div>
+<div id='element'></div>
+<svg>
+ <rect id='svgElement'></rect>
+</svg>
+
+<a href='example.com/unvisited' id='unvisited'><div id='unvisitedchild'>Unvisited</div></a>
+<a href='' id='visited'><div id='visitedchild'>Visited</div></a>
+
+<script>
+'use strict';
+var container = document.getElementById('container');
+var child = document.getElementById('child');
+var element = document.getElementById('element');
+var unvisited = document.getElementById('unvisited');
+var visited = document.getElementById('visited');
+var unvisitedChild = document.getElementById('unvisitedchild');
+var visitedChild = document.getElementById('visitedchild');
+
+test(function() {
+ var keyframes = [
+ {backgroundColor: 'currentColor'},
+ {backgroundColor: 'rgb(0, 68, 0)'}
+ ];
+
+ element.style.color = 'rgb(204, 0, 0)';
+ var player = element.animate(keyframes, 10);
+ player.pause();
+ player.currentTime = 5;
+ element.style.color = 'rgb(0, 0, 204)';
+ assert_equals(getComputedStyle(element).backgroundColor, 'rgb(0, 34, 102)');
+}, 'Background color responsive to currentColor changes');
+
+test(function() {
+ var keyframes = [
+ {fill: 'currentColor'},
+ {fill: 'rgb(0, 68, 0)'}
+ ];
+
+ svgElement.style.color = 'rgb(204, 0, 0)';
+ var player = svgElement.animate(keyframes, 10);
+ player.pause();
+ player.currentTime = 5;
+ svgElement.style.color = 'rgb(0, 0, 204)';
+ assert_equals(getComputedStyle(svgElement).fill, 'rgb(0, 34, 102)');
+}, 'Fill color responsive to currentColor changes');
+
+test(function() {
+ var keyframes = [
+ {backgroundColor: 'rgba(250, 240, 220, 0.431372549)'},
+ {backgroundColor: 'currentColor'}
+ ];
+
+ element.style.color = 'rgb(204, 0, 0)';
+ var player = element.animate(keyframes, 10);
+ player.pause();
+ player.currentTime = 3;
+ element.style.color = 'rgba(160, 190, 180, 0.980392157)';
+ assert_equals(getComputedStyle(element).backgroundColor, 'rgba(206, 215, 200, 0.596)');
+}, 'Color interpolation uses pre-multiplied colors');
+
+test(function() {
+ var keyframes = [
+ {
+ borderBottomColor: 'currentColor',
+ borderLeftColor: 'currentColor',
+ borderRightColor: 'currentColor',
+ borderTopColor: 'currentColor',
+ offset: 0
+ },
+ {
+ borderBottomColor: 'rgb(0, 68, 0)',
+ borderLeftColor: 'rgb(0, 70, 0)',
+ borderRightColor: 'rgb(0, 72, 0)',
+ borderTopColor: 'rgb(0, 74, 0)',
+ offset: 1
+ }
+ ];
+
+ element.style.color = 'rgb(204, 0, 0)';
+ var player = element.animate(keyframes, 10);
+ player.pause();
+ player.currentTime = 5;
+ element.style.color = 'rgb(0, 0, 204)';
+ assert_equals(getComputedStyle(element).borderBottomColor, 'rgb(0, 34, 102)');
+ assert_equals(getComputedStyle(element).borderLeftColor, 'rgb(0, 35, 102)');
+ assert_equals(getComputedStyle(element).borderRightColor, 'rgb(0, 36, 102)');
+ assert_equals(getComputedStyle(element).borderTopColor, 'rgb(0, 37, 102)');
+}, 'Border color responsive to currentColor changes');
+
+test(function() {
+ var keyframes = [
+ {
+ borderBottomColor: 'currentColor',
+ borderLeftColor: 'currentColor',
+ borderRightColor: 'currentColor',
+ borderTopColor: 'currentColor',
+ offset: 0
+ },
+ {
+ borderBottomColor: 'rgb(0, 68, 0)',
+ borderLeftColor: 'rgb(0, 70, 0)',
+ borderRightColor: 'rgb(0, 72, 0)',
+ borderTopColor: 'rgb(0, 74, 0)',
+ offset: 1
+ }
+ ];
+
+ element.style.color = 'rgb(204, 0, 0)';
+ var player = element.animate(keyframes, 10);
+ player.pause();
+ player.currentTime = 5;
+ element.style.color = 'rgb(0, 0, 200)';
+ assert_equals(getComputedStyle(element).borderBottomColor, 'rgb(0, 34, 100)');
+ assert_equals(getComputedStyle(element).borderLeftColor, 'rgb(0, 35, 100)');
+ assert_equals(getComputedStyle(element).borderRightColor, 'rgb(0, 36, 100)');
+ assert_equals(getComputedStyle(element).borderTopColor, 'rgb(0, 37, 100)');
+}, 'Border color responsive to currentColor changes again');
+
+test(function() {
+ var keyframes = [
+ {outlineColor: 'currentColor'},
+ {outlineColor: 'rgb(0, 68, 0)'}
+ ];
+
+ element.style.color = 'rgb(204, 0, 0)';
+ var player = element.animate(keyframes, 10);
+ player.pause();
+ player.currentTime = 5;
+ element.style.color = 'rgb(0, 0, 204)';
+ assert_equals(getComputedStyle(element).outlineColor, 'rgb(0, 34, 102)');
+}, 'Outline color responsive to currentColor changes');
+
+test(function() {
+ var keyframes = [
+ {stroke: 'rgb(0, 68, 0)'},
+ {stroke: 'currentColor'}
+ ];
+
+ svgElement.style.color = 'rgb(204, 0, 0)';
+ var player = svgElement.animate(keyframes, 10);
+ player.pause();
+ player.currentTime = 5;
+ svgElement.style.color = 'rgb(0, 0, 204)';
+ assert_equals(getComputedStyle(svgElement).stroke, 'rgb(0, 34, 102)');
+}, 'Stroke color responsive to currentColor changes');
+
+test(function() {
+ var keyframes = [
+ {textDecorationColor: 'rgb(0, 68, 0)'},
+ {textDecorationColor: 'currentColor'}
+ ];
+
+ element.style.color = 'rgb(204, 0, 0)';
+ var player = element.animate(keyframes, 10);
+ player.pause();
+ player.currentTime = 5;
+ element.style.color = 'rgb(0, 0, 204)';
+ assert_equals(getComputedStyle(element).textDecorationColor, 'rgb(0, 34, 102)');
+}, 'Text decoration color responsive to currentColor changes');
+
+test(function() {
+ var keyframes = [
+ {color: 'currentColor'},
+ {color: 'rgb(0, 68, 0)'}
+ ];
+
+ child.style.color = 'rgb(10, 10, 10)'; // Should be ignored
+ container.style.color = 'rgb(204, 0, 0)';
+ var player = child.animate(keyframes, 10);
+ player.pause();
+ player.currentTime = 5;
+ container.style.color = 'rgb(0, 0, 204)';
+ assert_equals(getComputedStyle(child).color, 'rgb(0, 34, 102)');
+ player.currentTime = 7.5;
+ container.style.color = 'rgb(136, 0, 136)';
+ assert_equals(getComputedStyle(child).color, 'rgb(34, 51, 34)');
+}, 'Color responsive to parent currentColor changes');
+
+test(function() {
+ var keyframes = [
+ {color: 'rgb(0, 68, 0)'},
+ {color: 'currentColor'}
+ ];
+
+ child.style.color = 'rgb(10, 10, 10)'; // Should be ignored
+ container.style.color = 'rgb(204, 0, 0)';
+ var player = child.animate(keyframes, 10);
+ player.pause();
+ player.currentTime = 5;
+ container.style.color = 'rgb(0, 0, 204)';
+ assert_equals(getComputedStyle(child).color, 'rgb(0, 34, 102)');
+ player.currentTime = 7.5;
+ container.style.color = 'rgb(136, 0, 68)';
+ assert_equals(getComputedStyle(child).color, 'rgb(102, 17, 51)');
+}, 'Color responsive to repeated parent currentColor changes');
+
+test(function() {
+ var keyframes = [
+ {backgroundColor: 'currentColor'},
+ {backgroundColor: 'rgb(100, 150, 200)'}
+ ];
+
+ var player1 = unvisited.animate(keyframes, 10);
+ var player2 = visited.animate(keyframes, 10);
+ player1.pause();
+ player2.pause();
+ player1.currentTime = 5;
+ player2.currentTime = 5;
+ assert_equals(getComputedStyle(unvisited).backgroundColor, getComputedStyle(visited).backgroundColor);
+}, 'Color animations do not expose visited status');
+
+test(function() {
+ var keyframes = [
+ {color: 'rgb(100, 150, 200)'},
+ {color: 'currentColor'}
+ ];
+
+ var player1 = unvisitedChild.animate(keyframes, 10);
+ var player2 = visitedChild.animate(keyframes, 10);
+ player1.pause();
+ player2.pause();
+ player1.currentTime = 5;
+ player2.currentTime = 5;
+ assert_equals(getComputedStyle(unvisitedChild).color, getComputedStyle(visitedChild).color);
+}, 'Color animations do not expose parent visited status');
+
+test(function() {
+ var keyframes = [
+ {columnRuleColor: 'inherit'},
+ {columnRuleColor: 'rgb(70, 70, 170)'}
+ ];
+
+ container.style.columnRuleColor = 'rgb(170, 70, 70)';
+ var player = child.animate(keyframes, 10);
+
+ player.pause();
+ player.currentTime = 5;
+ assert_equals(getComputedStyle(child).columnRuleColor, 'rgb(120, 70, 120)');
+
+ container.style.columnRuleColor = 'rgb(70, 170, 70)';
+ assert_equals(getComputedStyle(child).columnRuleColor, 'rgb(70, 120, 120)');
+}, 'Color animations respond to inherited changes');
+</script>
diff --git a/testing/web-platform/tests/web-animations/responsive/to-inherited-change.html b/testing/web-platform/tests/web-animations/responsive/to-inherited-change.html
new file mode 100644
index 0000000000..96ef5d24e1
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/responsive/to-inherited-change.html
@@ -0,0 +1,56 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<div id='container'>
+ <div id='element'></div>
+</div>
+
+<script>
+
+var container = document.getElementById('container');
+var element = document.getElementById('element');
+
+test(function() {
+ var keyframes = [
+ {fontSize: 'inherit'},
+ {fontSize: '20px'}
+ ];
+
+ container.style.fontSize = '10px';
+ var player = element.animate(keyframes, 10);
+ player.pause();
+ player.currentTime = 5;
+ container.style.fontSize = '30px';
+ assert_equals(getComputedStyle(element).fontSize, '25px');
+}, 'Font size responsive to inherited changes at start');
+
+test(function() {
+ var keyframes = [
+ {fontSize: '50px'},
+ {fontSize: 'inherit'}
+ ];
+
+ container.style.fontSize = '40px';
+ var player = element.animate(keyframes, 10);
+ player.pause();
+ player.currentTime = 5;
+ container.style.fontSize = '60px';
+ assert_equals(getComputedStyle(element).fontSize, '55px');
+}, 'Font size responsive to inherited changes at end');
+
+test(function() {
+ var keyframes = [
+ {fontSize: 'inherit'},
+ {fontSize: 'inherit'}
+ ];
+
+ container.style.fontSize = '70px';
+ var player = element.animate(keyframes, 10);
+ player.pause();
+ player.currentTime = 5;
+ container.style.fontSize = '80px';
+ assert_equals(getComputedStyle(element).fontSize, '80px');
+}, 'Font size responsive to inherited changes');
+
+</script>
diff --git a/testing/web-platform/tests/web-animations/responsive/to-style-change.html b/testing/web-platform/tests/web-animations/responsive/to-style-change.html
new file mode 100644
index 0000000000..8f4bf4c8b5
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/responsive/to-style-change.html
@@ -0,0 +1,44 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<div id='container'>
+<div id='element'></div>
+</div>
+
+<script>
+var element = document.getElementById('element');
+var container = document.getElementById('container');
+
+test(function() {
+ element.style.fontSize = '10px';
+ container.style.width = '1000px';
+ var player = element.animate([{left: 'calc(100px + 80%)'}, {left: '10em'}], 10);
+ player.pause();
+ player.currentTime = 5;
+ assert_equals(getComputedStyle(element).left, 'calc(40% + 100px)');
+ element.style.fontSize = '20px';
+ assert_equals(getComputedStyle(element).left, 'calc(40% + 150px)');
+ container.style.width = '500px';
+ assert_equals(getComputedStyle(element).left, 'calc(40% + 150px)');
+}, 'Lengths responsive to style changes');
+
+test(function() {
+ container.style.width = '1000px';
+ var player = element.animate([{paddingTop: '30%'}, {paddingTop: '50%'}], 10);
+ player.pause();
+ player.currentTime = 5;
+ container.style.width = '700px';
+ assert_equals(getComputedStyle(element).paddingTop, '280px');
+}, 'Percentages responsive to width style changes');
+
+test(function() {
+ element.style.fontSize = '1px';
+ var player = element.animate([{lineHeight: '9'}, {lineHeight: '13'}], 10);
+ player.pause();
+ player.currentTime = 2.5;
+ element.style.fontSize = '7px';
+ assert_equals(getComputedStyle(element).lineHeight, '70px');
+}, 'Numbers responsive to style changes');
+
+</script>
diff --git a/testing/web-platform/tests/web-animations/responsive/toggle-animated-iframe-visibility-ref.html b/testing/web-platform/tests/web-animations/responsive/toggle-animated-iframe-visibility-ref.html
new file mode 100644
index 0000000000..dab5bed7c8
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/responsive/toggle-animated-iframe-visibility-ref.html
@@ -0,0 +1,10 @@
+<!DOCTYPE html>
+<style>
+ .rotated {
+ transform: rotate(90deg);
+ }
+</style>
+<div id="container">
+ <iframe class="rotated" src="resources/block.html">
+ </iframe>
+</div> \ No newline at end of file
diff --git a/testing/web-platform/tests/web-animations/responsive/toggle-animated-iframe-visibility.html b/testing/web-platform/tests/web-animations/responsive/toggle-animated-iframe-visibility.html
new file mode 100644
index 0000000000..f50ffaad34
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/responsive/toggle-animated-iframe-visibility.html
@@ -0,0 +1,55 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+<meta name="assert" content="This should resume the animation after unhiding the iframe.">
+<title>CSS Test (Animations): Unhiding iframe visibility should restart animation. </title>
+<link rel="help" href="https://bugs.chromium.org/p/chromium/issues/detail?id=616270">
+<link rel="match" href="toggle-animated-iframe-visibility-ref.html">
+<script src="/common/reftest-wait.js"></script>
+
+<div id="container"></div>
+
+<div id="log"></div>
+
+<script>
+ var container;
+ var block;
+ var logDiv;
+
+ function verifyVisibility(expected_visibility, message) {
+ if (getComputedStyle(block).visibility !== expected_visibility)
+ logDiv.innerHTML = `FAIL: ${message}`;
+ }
+
+ async function runTest() {
+ var animation = block.animate(
+ { transform: [ 'rotate(0deg)', 'rotate(180deg)' ] },
+ {
+ duration: 10000000,
+ delay: -5000000,
+ easing: 'cubic-bezier(0, 1, 1, 0)'
+ });
+
+ await animation.ready;
+
+ container.style.visibility = 'hidden';
+ requestAnimationFrame(() => {
+ verifyVisibility('hidden', 'style.visibility should be hidden');
+ container.style.visibility = 'visible';
+
+ requestAnimationFrame(() => {
+ verifyVisibility('visible', 'style.visiblity should be visible');
+ takeScreenshot();
+ });
+ });
+ }
+
+ window.onload = function () {
+ logDiv = document.getElementById('log');
+ container = document.getElementById('container');
+ block = document.createElement('iframe');
+
+ container.appendChild(block);
+ block.onload = runTest;
+ block.src = 'resources/block.html';
+ };
+</script>
diff --git a/testing/web-platform/tests/web-animations/responsive/transform.html b/testing/web-platform/tests/web-animations/responsive/transform.html
new file mode 100644
index 0000000000..d57f8b136a
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/responsive/transform.html
@@ -0,0 +1,22 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<div id='container'>
+<div id='element'></div>
+</div>
+
+<script>
+var element = document.getElementById('element');
+var container = document.getElementById('container');
+
+test(function() {
+ container.style.fontSize = '10px';
+ var player = element.animate([{transform: 'translateX(10em)'}, {transform: 'translateX(20em)'}], 10);
+ player.pause();
+ player.currentTime = 2;
+ container.style.fontSize = '20px';
+ assert_equals(getComputedStyle(element).transform, 'matrix(1, 0, 0, 1, 240, 0)');
+}, 'Transform responsive to font size changes');
+
+</script>
diff --git a/testing/web-platform/tests/web-animations/responsive/translate.html b/testing/web-platform/tests/web-animations/responsive/translate.html
new file mode 100644
index 0000000000..8df4d8577d
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/responsive/translate.html
@@ -0,0 +1,22 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<div id='container'>
+<div id='element'></div>
+</div>
+
+<script>
+var element = document.getElementById('element');
+var container = document.getElementById('container');
+
+test(function() {
+ container.style.fontSize = '10px';
+ var player = element.animate([{translate: '1em 2em'}, {translate: '10em 20em'}], 10);
+ player.pause();
+ player.currentTime = 2;
+ container.style.fontSize = '20px';
+ assert_equals(getComputedStyle(element).translate, '56px 112px');
+}, 'Translate responsive to font size changes');
+
+</script>
diff --git a/testing/web-platform/tests/web-animations/responsive/verticalAlign.html b/testing/web-platform/tests/web-animations/responsive/verticalAlign.html
new file mode 100644
index 0000000000..af81fa481b
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/responsive/verticalAlign.html
@@ -0,0 +1,22 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<div id='container'>
+<div id='element'></div>
+</div>
+
+<script>
+var element = document.getElementById('element');
+var container = document.getElementById('container');
+
+test(function() {
+ element.style.fontSize = '10px';
+ var player = element.animate([{verticalAlign: '3em'}, {verticalAlign: '5em'}], 10);
+ player.pause();
+ player.currentTime = 5;
+ element.style.fontSize = '20px';
+ assert_equals(getComputedStyle(element).verticalAlign, '80px');
+}, 'verticalAlign responsive to style changes');
+
+</script>
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 0000000000..5b22c658d5
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/testcommon.js
@@ -0,0 +1,342 @@
+'use strict';
+
+const MS_PER_SEC = 1000;
+
+// The recommended minimum precision to use for time values[1].
+//
+// [1] https://drafts.csswg.org/web-animations/#precision-of-time-values
+const 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 = (actual, expected, description) => {
+ assert_approx_equals(actual, expected, TIME_PRECISION * 2, description);
+ };
+}
+
+// Allow implementations to substitute an alternative method for comparing
+// times based on their precision requirements.
+if (!window.assert_time_greater_than_equal) {
+ window.assert_time_greater_than_equal = (actual, expected, description) => {
+ assert_greater_than_equal(actual, expected - 2 * TIME_PRECISION,
+ description);
+ };
+}
+
+// Allow implementations to substitute an alternative method for comparing
+// a time value based on its precision requirements with a fixed value.
+if (!window.assert_time_equals_literal) {
+ window.assert_time_equals_literal = (actual, expected, description) => {
+ if (Math.abs(expected) === Infinity) {
+ assert_equals(actual, expected, description);
+ } else {
+ 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;
+ }
+ const element = doc.createElement(tagName || 'div');
+ doc.body.appendChild(element);
+ test.add_cleanup(() => {
+ 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;
+ }
+ const extraStyle = doc.createElement('style');
+ doc.head.appendChild(extraStyle);
+ if (rules) {
+ const sheet = extraStyle.sheet;
+ for (const selector in rules) {
+ sheet.insertRule(`${selector}{${rules[selector]}}`,
+ sheet.cssRules.length);
+ }
+ }
+ test.add_cleanup(() => {
+ extraStyle.remove();
+ });
+}
+
+// Cubic bezier with control points (0, 0), (x1, y1), (x2, y2), and (1, 1).
+function cubicBezier(x1, y1, x2, y2) {
+ const xForT = t => {
+ const omt = 1-t;
+ return 3 * omt * omt * t * x1 + 3 * omt * t * t * x2 + t * t * t;
+ };
+
+ const yForT = t => {
+ const omt = 1-t;
+ return 3 * omt * omt * t * y1 + 3 * omt * t * t * y2 + t * t * t;
+ };
+
+ const tForX = x => {
+ // Binary subdivision.
+ let mint = 0, maxt = 1;
+ for (let i = 0; i < 30; ++i) {
+ const guesst = (mint + maxt) / 2;
+ const guessx = xForT(guesst);
+ if (x < guessx) {
+ maxt = guesst;
+ } else {
+ mint = guesst;
+ }
+ }
+ return (mint + maxt) / 2;
+ };
+
+ return x => {
+ if (x == 0) {
+ return 0;
+ }
+ if (x == 1) {
+ return 1;
+ }
+ return yForT(tForX(x));
+ };
+}
+
+function stepEnd(nsteps) {
+ return x => Math.floor(x * nsteps) / nsteps;
+}
+
+function stepStart(nsteps) {
+ return x => {
+ const result = Math.floor(x * nsteps + 1.0) / nsteps;
+ return (result > 1.0) ? 1.0 : result;
+ };
+}
+
+function waitForAnimationFrames(frameCount) {
+ return new Promise(resolve => {
+ 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) {
+ const startTime = document.timeline.currentTime;
+ return new Promise(resolve => {
+ (function handleFrame() {
+ if (document.timeline.currentTime - startTime >= minDelay) {
+ resolve();
+ } else {
+ window.requestAnimationFrame(handleFrame);
+ }
+ }());
+ });
+}
+
+function runAndWaitForFrameUpdate(callback) {
+ return new Promise(resolve => {
+ window.requestAnimationFrame(() => {
+ callback();
+ window.requestAnimationFrame(resolve);
+ });
+ });
+}
+
+// Waits for a requestAnimationFrame callback in the next refresh driver tick.
+function waitForNextFrame() {
+ const timeAtStart = document.timeline.currentTime;
+ return new Promise(resolve => {
+ (function handleFrame() {
+ if (timeAtStart === document.timeline.currentTime) {
+ window.requestAnimationFrame(handleFrame);
+ } else {
+ resolve();
+ }
+ }());
+ });
+}
+
+async function insertFrameAndAwaitLoad(test, iframe, doc) {
+ const eventWatcher = new EventWatcher(test, iframe, ['load']);
+ const event_promise = eventWatcher.wait_for('load');
+
+ doc.body.appendChild(iframe);
+ test.add_cleanup(() => { doc.body.removeChild(iframe); });
+
+ await event_promise;
+}
+
+// Returns 'matrix()' or 'matrix3d()' function string generated from an array.
+function createMatrixFromArray(array) {
+ return (array.length == 16 ? 'matrix3d' : 'matrix') + `(${array.join()})`;
+}
+
+// Returns 'matrix3d()' function string equivalent to
+// 'rotate3d(x, y, z, radian)'.
+function rotate3dToMatrix3d(x, y, z, radian) {
+ return createMatrixFromArray(rotate3dToMatrix(x, y, z, radian));
+}
+
+// Returns an array of the 4x4 matrix equivalent to 'rotate3d(x, y, z, radian)'.
+// https://drafts.csswg.org/css-transforms-2/#Rotate3dDefined
+function rotate3dToMatrix(x, y, z, radian) {
+ const sc = Math.sin(radian / 2) * Math.cos(radian / 2);
+ const sq = Math.sin(radian / 2) * Math.sin(radian / 2);
+
+ // Normalize the vector.
+ const length = Math.sqrt(x*x + y*y + z*z);
+ x /= length;
+ y /= length;
+ z /= length;
+
+ return [
+ 1 - 2 * (y*y + z*z) * sq,
+ 2 * (x * y * sq + z * sc),
+ 2 * (x * z * sq - y * sc),
+ 0,
+ 2 * (x * y * sq - z * sc),
+ 1 - 2 * (x*x + z*z) * sq,
+ 2 * (y * z * sq + x * sc),
+ 0,
+ 2 * (x * z * sq + y * sc),
+ 2 * (y * z * sq - x * sc),
+ 1 - 2 * (x*x + y*y) * sq,
+ 0,
+ 0,
+ 0,
+ 0,
+ 1
+ ];
+}
+
+// Compare matrix string like 'matrix(1, 0, 0, 1, 100, 0)' with tolerances.
+function assert_matrix_equals(actual, expected, description) {
+ const matrixRegExp = /^matrix(?:3d)*\((.+)\)/;
+ assert_regexp_match(actual, matrixRegExp,
+ 'Actual value is not a matrix')
+ assert_regexp_match(expected, matrixRegExp,
+ 'Expected value is not a matrix');
+
+ const actualMatrixArray =
+ actual.match(matrixRegExp)[1].split(',').map(Number);
+ const expectedMatrixArray =
+ expected.match(matrixRegExp)[1].split(',').map(Number);
+
+ assert_equals(actualMatrixArray.length, expectedMatrixArray.length,
+ `dimension of the matrix: ${description}`);
+ for (let i = 0; i < actualMatrixArray.length; i++) {
+ assert_approx_equals(actualMatrixArray[i], expectedMatrixArray[i], 0.0001,
+ `expected ${expected} but got ${actual}: ${description}`);
+ }
+}
+
+// Compare rotate3d vector like '0 1 0 45deg' with tolerances.
+function assert_rotate3d_equals(actual, expected, description) {
+ const rotationRegExp =/^((([+-]?\d+(\.+\d+)?\s){3})?\d+(\.+\d+)?)deg/;
+
+ assert_regexp_match(actual, rotationRegExp,
+ 'Actual value is not a rotate3d vector')
+ assert_regexp_match(expected, rotationRegExp,
+ 'Expected value is not a rotate3d vector');
+
+ const actualRotationVector =
+ actual.match(rotationRegExp)[1].split(' ').map(Number);
+ const expectedRotationVector =
+ expected.match(rotationRegExp)[1].split(' ').map(Number);
+
+ assert_equals(actualRotationVector.length, expectedRotationVector.length,
+ `dimension of the matrix: ${description}`);
+ for (let i = 0; i < actualRotationVector.length; i++) {
+ assert_approx_equals(
+ actualRotationVector[i],
+ expectedRotationVector[i],
+ 0.0001,
+ `expected ${expected} but got ${actual}: ${description}`);
+ }
+}
+
+function assert_phase_at_time(animation, phase, currentTime) {
+ animation.currentTime = currentTime;
+ assert_phase(animation, phase);
+}
+
+function assert_phase(animation, phase) {
+ const fillMode = animation.effect.getTiming().fill;
+ const currentTime = animation.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.updateTiming({ fill: 'none' });
+ assert_not_equals(animation.effect.getComputedTiming().progress, null,
+ 'Animation effect is in active phase when current time ' +
+ `is ${currentTime}.`);
+ } 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
+ // when the fill mode is 'none' but non-null when the fill mode is
+ // 'backwards' then we are in the before phase.
+ animation.effect.updateTiming({ fill: 'none' });
+ assert_equals(animation.effect.getComputedTiming().progress, null,
+ `Animation effect is in ${phase} phase when current time ` +
+ `is ${currentTime} (progress is null with 'none' fill mode)`);
+
+ animation.effect.updateTiming({
+ fill: phase === 'before' ? 'backwards' : 'forwards',
+ });
+ assert_not_equals(animation.effect.getComputedTiming().progress, null,
+ `Animation effect is in ${phase} phase when current ` +
+ `time is ${currentTime} (progress is non-null with ` +
+ `appropriate fill mode)`);
+ }
+
+ // Reset fill mode to avoid side-effects.
+ animation.effect.updateTiming({ fill: fillMode });
+}
+
+
+// Use with reftest-wait to wait until compositor commits are no longer deferred
+// before taking the screenshot.
+// crbug.com/1378671
+async function waitForCompositorReady() {
+ const animation =
+ document.body.animate({ opacity: [ 1, 1 ] }, {duration: 1 });
+ return animation.finished;
+}
+
+async function takeScreenshotOnAnimationsReady() {
+ await Promise.all(document.getAnimations().map(a => a.ready));
+ requestAnimationFrame(() => requestAnimationFrame(takeScreenshot));
+}
+
diff --git a/testing/web-platform/tests/web-animations/timing-model/animation-effects/active-time.html b/testing/web-platform/tests/web-animations/timing-model/animation-effects/active-time.html
new file mode 100644
index 0000000000..a2feb2323c
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/timing-model/animation-effects/active-time.html
@@ -0,0 +1,141 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title>Active time</title>
+<link rel="help" href="https://drafts.csswg.org/web-animations/#calculating-the-active-time">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../../testcommon.js"></script>
+<body>
+<div id="log"></div>
+<script>
+'use strict';
+
+test(t => {
+ const tests = [ { fill: 'none', progress: null },
+ { fill: 'backwards', progress: 0 },
+ { fill: 'forwards', progress: null },
+ { fill: 'both', progress: 0 } ];
+ for (const test of tests) {
+ const anim = createDiv(t).animate(null, { delay: 1, fill: test.fill });
+ assert_equals(anim.effect.getComputedTiming().progress, test.progress,
+ `Progress in before phase when using '${test.fill}' fill`);
+ }
+}, 'Active time in before phase');
+
+test(t => {
+ const anim = createDiv(t).animate(null, 1000);
+ anim.currentTime = 500;
+ assert_time_equals_literal(anim.effect.getComputedTiming().progress, 0.5);
+}, 'Active time in active phase and no start delay is the local time');
+
+test(t => {
+ const anim = createDiv(t).animate(null, { duration: 1000, delay: 500 });
+ anim.currentTime = 1000;
+ assert_time_equals_literal(anim.effect.getComputedTiming().progress, 0.5);
+}, 'Active time in active phase and positive start delay is the local time'
+ + ' minus the start delay');
+
+test(t => {
+ const anim = createDiv(t).animate(null, { duration: 1000, delay: -500 });
+ assert_time_equals_literal(anim.effect.getComputedTiming().progress, 0.5);
+}, 'Active time in active phase and negative start delay is the local time'
+ + ' minus the start delay');
+
+test(t => {
+ const anim = createDiv(t).animate(null);
+ assert_equals(anim.effect.getComputedTiming().progress, null);
+}, 'Active time in after phase with no fill is unresolved');
+
+test(t => {
+ const anim = createDiv(t).animate(null, { fill: 'backwards' });
+ assert_equals(anim.effect.getComputedTiming().progress, null);
+}, 'Active time in after phase with backwards-only fill is unresolved');
+
+test(t => {
+ const anim = createDiv(t).animate(null, { duration: 1000,
+ iterations: 2.3,
+ delay: 500, // Should have no effect
+ fill: 'forwards' });
+ anim.finish();
+ assert_equals(anim.effect.getComputedTiming().currentIteration, 2);
+ assert_time_equals_literal(anim.effect.getComputedTiming().progress, 0.3);
+}, 'Active time in after phase with forwards fill is the active duration');
+
+test(t => {
+ const anim = createDiv(t).animate(null, { duration: 0,
+ iterations: Infinity,
+ fill: 'forwards' });
+ anim.finish();
+ assert_equals(anim.effect.getComputedTiming().currentIteration, Infinity);
+ assert_equals(anim.effect.getComputedTiming().progress, 1);
+}, 'Active time in after phase with forwards fill, zero-duration, and '
+ + ' infinite iteration count is the active duration');
+
+test(t => {
+ const anim = createDiv(t).animate(null, { duration: 1000,
+ iterations: 2.3,
+ delay: 500,
+ endDelay: 4000,
+ fill: 'forwards' });
+ anim.finish();
+ assert_equals(anim.effect.getComputedTiming().currentIteration, 2);
+ assert_time_equals_literal(anim.effect.getComputedTiming().progress, 0.3);
+}, 'Active time in after phase with forwards fill and positive end delay'
+ + ' is the active duration');
+
+test(t => {
+ const anim = createDiv(t).animate(null, { duration: 1000,
+ iterations: 2.3,
+ delay: 500,
+ endDelay: -800,
+ fill: 'forwards' });
+ anim.finish();
+ assert_equals(anim.effect.getComputedTiming().currentIteration, 1);
+ assert_time_equals_literal(anim.effect.getComputedTiming().progress, 0.5);
+}, 'Active time in after phase with forwards fill and negative end delay'
+ + ' is the active duration + end delay');
+
+test(t => {
+ const anim = createDiv(t).animate(null, { duration: 1000,
+ iterations: 2.3,
+ delay: 500,
+ endDelay: -2500,
+ fill: 'forwards' });
+ anim.finish();
+ assert_equals(anim.effect.getComputedTiming().currentIteration, 0);
+ assert_equals(anim.effect.getComputedTiming().progress, 0);
+}, 'Active time in after phase with forwards fill and negative end delay'
+ + ' greater in magnitude than the active duration is zero');
+
+test(t => {
+ const anim = createDiv(t).animate(null, { duration: 1000,
+ iterations: 2.3,
+ delay: 500,
+ endDelay: -4000,
+ fill: 'forwards' });
+ anim.finish();
+ assert_equals(anim.effect.getComputedTiming().currentIteration, 0);
+ assert_equals(anim.effect.getComputedTiming().progress, 0);
+}, 'Active time in after phase with forwards fill and negative end delay'
+ + ' greater in magnitude than the sum of the active duration and start delay'
+ + ' is zero');
+
+test(t => {
+ const anim = createDiv(t).animate(null, { duration: 1000,
+ iterations: 2.3,
+ delay: 500,
+ fill: 'both' });
+ anim.finish();
+ assert_equals(anim.effect.getComputedTiming().currentIteration, 2);
+ assert_time_equals_literal(anim.effect.getComputedTiming().progress, 0.3);
+}, 'Active time in after phase with \'both\' fill is the active duration');
+
+test(t => {
+ // Create an effect with a non-zero duration so we ensure we're not just
+ // testing the after-phase behavior.
+ const effect = new KeyframeEffect(null, null, 1);
+ assert_equals(effect.getComputedTiming().progress, null);
+}, 'Active time when the local time is unresolved, is unresolved');
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/web-animations/timing-model/animation-effects/current-iteration.html b/testing/web-platform/tests/web-animations/timing-model/animation-effects/current-iteration.html
new file mode 100644
index 0000000000..24464ce05f
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/timing-model/animation-effects/current-iteration.html
@@ -0,0 +1,620 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title>Current iteration</title>
+<link rel="help" href="https://drafts.csswg.org/web-animations/#current-iteration">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../../testcommon.js"></script>
+<script src="../../resources/effect-tests.js"></script>
+<body>
+<div id="log"></div>
+<script>
+'use strict';
+
+function runTests(tests, description) {
+ for (const currentTest of tests) {
+ let testParams = Object.entries(currentTest.input)
+ .map(([attr, value]) => `${attr}:${value}`)
+ .join(' ');
+ if (currentTest.playbackRate !== undefined) {
+ testParams += ` playbackRate:${currentTest.playbackRate}`;
+ }
+
+ test(t => {
+ const div = createDiv(t);
+ const anim = div.animate({}, currentTest.input);
+ if (currentTest.playbackRate !== undefined) {
+ anim.playbackRate = currentTest.playbackRate;
+ }
+
+ assert_computed_timing_for_each_phase(
+ anim,
+ 'currentIteration',
+ { before: currentTest.before,
+ activeBoundary: currentTest.active,
+ after: currentTest.after },
+ );
+ }, `${description}: ${testParams}`);
+ }
+}
+
+async_test(t => {
+ const div = createDiv(t);
+ const anim = div.animate({ opacity: [ 0, 1 ] }, { delay: 1 });
+ assert_equals(anim.effect.getComputedTiming().currentIteration, null);
+ anim.finished.then(t.step_func(() => {
+ assert_equals(anim.effect.getComputedTiming().currentIteration, null);
+ t.done();
+ }));
+}, 'Test currentIteration during before and after phase when fill is none');
+
+
+// --------------------------------------------------------------------
+//
+// Zero iteration duration tests
+//
+// --------------------------------------------------------------------
+
+runTests([
+ {
+ input: { iterations: 0,
+ iterationStart: 0,
+ duration: 0,
+ delay: 1,
+ fill: 'both' },
+ before: 0,
+ after: 0
+ },
+
+ {
+ input: { iterations: 0,
+ iterationStart: 0,
+ duration: 100,
+ delay: 1,
+ fill: 'both' },
+ before: 0,
+ after: 0
+ },
+
+ {
+ input: { iterations: 0,
+ iterationStart: 0,
+ duration: Infinity,
+ delay: 1,
+ fill: 'both' },
+ before: 0,
+ after: 0
+ },
+
+ {
+ input: { iterations: 0,
+ iterationStart: 2.5,
+ duration: 0,
+ delay: 1,
+ fill: 'both' },
+ before: 2,
+ after: 2
+ },
+
+ {
+ input: { iterations: 0,
+ iterationStart: 2.5,
+ duration: 100,
+ delay: 1,
+ fill: 'both' },
+ before: 2,
+ after: 2
+ },
+
+ {
+ input: { iterations: 0,
+ iterationStart: 2.5,
+ duration: Infinity,
+ delay: 1,
+ fill: 'both' },
+ before: 2,
+ after: 2
+ },
+
+ {
+ input: { iterations: 0,
+ iterationStart: 3,
+ duration: 0,
+ delay: 1,
+ fill: 'both' },
+ before: 3,
+ after: 3
+ },
+
+ {
+ input: { iterations: 0,
+ iterationStart: 3,
+ duration: 100,
+ delay: 1,
+ fill: 'both' },
+ before: 3,
+ after: 3
+ },
+
+ {
+ input: { iterations: 0,
+ iterationStart: 3,
+ duration: Infinity,
+ delay: 1,
+ fill: 'both' },
+ before: 3,
+ after: 3
+ }
+], 'Test zero iterations');
+
+
+// --------------------------------------------------------------------
+//
+// Tests where the iteration count is an integer
+//
+// --------------------------------------------------------------------
+
+runTests([
+ {
+ input: { iterations: 3,
+ iterationStart: 0,
+ duration: 0,
+ delay: 1,
+ fill: 'both' },
+ before: 0,
+ after: 2
+ },
+
+ {
+ input: { iterations: 3,
+ iterationStart: 0,
+ duration: 100,
+ delay: 1,
+ fill: 'both' },
+ before: 0,
+ active: 0,
+ after: 2
+ },
+
+ {
+ input: { iterations: 3,
+ iterationStart: 0,
+ duration: Infinity,
+ delay: 1,
+ fill: 'both' },
+ before: 0,
+ active: 0
+ },
+
+ {
+ input: { iterations: 3,
+ iterationStart: 2.5,
+ duration: 0,
+ delay: 1,
+ fill: 'both' },
+ before: 2,
+ after: 5
+ },
+
+ {
+ input: { iterations: 3,
+ iterationStart: 2.5,
+ duration: 100,
+ delay: 1,
+ fill: 'both' },
+ before: 2,
+ active: 2,
+ after: 5
+ },
+
+ {
+ input: { iterations: 3,
+ iterationStart: 2.5,
+ duration: Infinity,
+ delay: 1,
+ fill: 'both' },
+ before: 2,
+ active: 2
+ },
+
+ {
+ input: { iterations: 3,
+ iterationStart: 3,
+ duration: 0,
+ delay: 1,
+ fill: 'both' },
+ before: 3,
+ after: 5
+ },
+
+ {
+ input: { iterations: 3,
+ iterationStart: 3,
+ duration: 100,
+ delay: 1,
+ fill: 'both' },
+ before: 3,
+ active: 3,
+ after: 5
+ },
+
+ {
+ input: { iterations: 3,
+ iterationStart: 3,
+ duration: Infinity,
+ delay: 1,
+ fill: 'both' },
+ before: 3,
+ active: 3
+ }
+], 'Test integer iterations');
+
+
+// --------------------------------------------------------------------
+//
+// Tests where the iteration count is a fraction
+//
+// --------------------------------------------------------------------
+
+runTests([
+ {
+ input: { iterations: 3.5,
+ iterationStart: 0,
+ duration: 0,
+ delay: 1,
+ fill: 'both' },
+ before: 0,
+ after: 3
+ },
+
+ {
+ input: { iterations: 3.5,
+ iterationStart: 0,
+ duration: 100,
+ delay: 1,
+ fill: 'both' },
+ before: 0,
+ active: 0,
+ after: 3
+ },
+
+ {
+ input: { iterations: 3.5,
+ iterationStart: 0,
+ duration: Infinity,
+ delay: 1,
+ fill: 'both' },
+ before: 0,
+ active: 0
+ },
+
+ {
+ input: { iterations: 3.5,
+ iterationStart: 2.5,
+ duration: 0,
+ delay: 1,
+ fill: 'both' },
+ before: 2,
+ after: 5
+ },
+
+ {
+ input: { iterations: 3.5,
+ iterationStart: 2.5,
+ duration: 100,
+ delay: 1,
+ fill: 'both' },
+ before: 2,
+ active: 2,
+ after: 5
+ },
+
+ {
+ input: { iterations: 3.5,
+ iterationStart: 2.5,
+ duration: Infinity,
+ delay: 1,
+ fill: 'both' },
+ before: 2,
+ active: 2
+ },
+
+ {
+ input: { iterations: 3.5,
+ iterationStart: 3,
+ duration: 0,
+ delay: 1,
+ fill: 'both' },
+ before: 3,
+ after: 6
+ },
+
+ {
+ input: { iterations: 3.5,
+ iterationStart: 3,
+ duration: 100,
+ delay: 1,
+ fill: 'both' },
+ before: 3,
+ active: 3,
+ after: 6
+ },
+
+ {
+ input: { iterations: 3.5,
+ iterationStart: 3,
+ duration: Infinity,
+ delay: 1,
+ fill: 'both' },
+ before: 3,
+ active: 3
+ }
+], 'Test fractional iterations');
+
+
+// --------------------------------------------------------------------
+//
+// Tests where the iteration count is Infinity
+//
+// --------------------------------------------------------------------
+
+runTests([
+ {
+ input: { iterations: Infinity,
+ iterationStart: 0,
+ duration: 0,
+ delay: 1,
+ fill: 'both' },
+ before: 0,
+ after: Infinity
+ },
+
+ {
+ input: { iterations: Infinity,
+ iterationStart: 0,
+ duration: 100,
+ delay: 1,
+ fill: 'both' },
+ before: 0,
+ active: 0
+ },
+
+ {
+ input: { iterations: Infinity,
+ iterationStart: 0,
+ duration: Infinity,
+ delay: 1,
+ fill: 'both' },
+ before: 0,
+ active: 0
+ },
+
+ {
+ input: { iterations: Infinity,
+ iterationStart: 2.5,
+ duration: 0,
+ delay: 1,
+ fill: 'both' },
+ before: 2,
+ after: Infinity
+ },
+
+ {
+ input: { iterations: Infinity,
+ iterationStart: 2.5,
+ duration: 100,
+ delay: 1,
+ fill: 'both' },
+ before: 2,
+ active: 2
+ },
+
+ {
+ input: { iterations: Infinity,
+ iterationStart: 2.5,
+ duration: Infinity,
+ delay: 1,
+ fill: 'both' },
+ before: 2,
+ active: 2
+ },
+
+ {
+ input: { iterations: Infinity,
+ iterationStart: 3,
+ duration: 0,
+ delay: 1,
+ fill: 'both' },
+ before: 3,
+ after: Infinity
+ },
+
+ {
+ input: { iterations: Infinity,
+ iterationStart: 3,
+ duration: 100,
+ delay: 1,
+ fill: 'both' },
+ before: 3,
+ active: 3
+ },
+
+ {
+ input: { iterations: Infinity,
+ iterationStart: 3,
+ duration: Infinity,
+ delay: 1,
+ fill: 'both' },
+ before: 3,
+ active: 3
+ }
+], 'Test infinity iterations');
+
+
+// --------------------------------------------------------------------
+//
+// End delay tests
+//
+// --------------------------------------------------------------------
+
+runTests([
+ {
+ input: { duration: 100,
+ delay: 1,
+ fill: 'both',
+ endDelay: 50 },
+ before: 0,
+ active: 0,
+ after: 0
+ },
+
+ {
+ input: { duration: 100,
+ delay: 1,
+ fill: 'both',
+ endDelay: -50 },
+ before: 0,
+ active: 0,
+ after: 0
+ },
+
+ {
+ input: { duration: 100,
+ delay: 1,
+ fill: 'both',
+ endDelay: -100 },
+ before: 0,
+ active: 0,
+ after: 0
+ },
+
+ {
+ input: { duration: 100,
+ delay: 1,
+ fill: 'both',
+ endDelay: -200 },
+ before: 0,
+ active: 0,
+ after: 0
+ },
+
+ {
+ input: { iterationStart: 0.5,
+ duration: 100,
+ delay: 1,
+ fill: 'both',
+ endDelay: 50 },
+ before: 0,
+ active: 0,
+ after: 1
+ },
+
+ {
+ input: { iterationStart: 0.5,
+ duration: 100,
+ delay: 1,
+ fill: 'both',
+ endDelay: -50 },
+ before: 0,
+ active: 0,
+ after: 1
+ },
+
+ {
+ input: { iterationStart: 0.5,
+ duration: 100,
+ delay: 1,
+ fill: 'both',
+ endDelay: -100 },
+ before: 0,
+ active: 0,
+ after: 0
+ },
+
+ {
+ input: { iterations: 2,
+ duration: 100,
+ delay: 1,
+ fill: 'both',
+ endDelay: -100 },
+ before: 0,
+ active: 0,
+ after: 1
+ },
+
+ {
+ input: { iterations: 1,
+ iterationStart: 2,
+ duration: 100,
+ delay: 1,
+ fill: 'both',
+ endDelay: -50 },
+ before: 2,
+ active: 2,
+ after: 2
+ },
+
+ {
+ input: { iterations: 1,
+ iterationStart: 2,
+ duration: 100,
+ delay: 1,
+ fill: 'both',
+ endDelay: -100 },
+ before: 2,
+ active: 2,
+ after: 2
+ },
+], 'Test end delay');
+
+
+// --------------------------------------------------------------------
+//
+// Negative playback rate tests
+//
+// --------------------------------------------------------------------
+
+runTests([
+ {
+ input: { duration: 1,
+ delay: 1,
+ fill: 'both' },
+ playbackRate: -1,
+ before: 0,
+ active: 0,
+ after: 0
+ },
+
+ {
+ input: { duration: 1,
+ delay: 1,
+ iterations: 2,
+ fill: 'both' },
+ playbackRate: -1,
+ before: 0,
+ active: 1,
+ after: 1
+ },
+
+ {
+ input: { duration: 0,
+ delay: 1,
+ fill: 'both' },
+ playbackRate: -1,
+ before: 0,
+ after: 0
+ },
+
+ {
+ input: { duration: 0,
+ iterations: 0,
+ delay: 1,
+ fill: 'both' },
+ playbackRate: -1,
+ before: 0,
+ after: 0
+ },
+], 'Test negative playback rate');
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/web-animations/timing-model/animation-effects/local-time.html b/testing/web-platform/tests/web-animations/timing-model/animation-effects/local-time.html
new file mode 100644
index 0000000000..79437d9f54
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/timing-model/animation-effects/local-time.html
@@ -0,0 +1,29 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title>Local time</title>
+<link rel="help" href="https://drafts.csswg.org/web-animations/#local-time">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../../testcommon.js"></script>
+<body>
+<script>
+'use strict';
+
+test(t => {
+ const anim = createDiv(t).animate(null, 10 * MS_PER_SEC);
+ for (const seconds of [-1, 0, 5, 10, 20]) {
+ anim.currentTime = seconds * MS_PER_SEC;
+ assert_equals(
+ anim.effect.getComputedTiming().localTime,
+ seconds * MS_PER_SEC
+ );
+ }
+}, 'Local time is current time for animation effects associated with an animation');
+
+test(t => {
+ const effect = new KeyframeEffect(createDiv(t), null, 10 * MS_PER_SEC);
+ assert_equals(effect.getComputedTiming().localTime, null);
+}, 'Local time is unresolved for animation effects not associated with an animation');
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/web-animations/timing-model/animation-effects/phases-and-states.html b/testing/web-platform/tests/web-animations/timing-model/animation-effects/phases-and-states.html
new file mode 100644
index 0000000000..a33dbf517e
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/timing-model/animation-effects/phases-and-states.html
@@ -0,0 +1,149 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>Phases and states</title>
+<link rel="help" href="https://drafts.csswg.org/web-animations/#animation-effect-phases-and-states">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../../testcommon.js"></script>
+<body>
+<div id="log"></div>
+<script>
+'use strict';
+
+// --------------------------------------------------------------------
+//
+// Phases
+//
+// --------------------------------------------------------------------
+
+test(t => {
+ const animation = createDiv(t).animate(null, 1);
+
+ for (const test of [{ currentTime: -1, phase: 'before' },
+ { currentTime: 0, phase: 'active' },
+ { currentTime: 1, phase: 'after' }]) {
+ assert_phase_at_time(animation, test.phase, test.currentTime);
+ }
+}, 'Phase calculation for a simple animation effect');
+
+test(t => {
+ const animation = createDiv(t).animate(null, { duration: 1, delay: 1 });
+
+ for (const test of [{ currentTime: 0, phase: 'before' },
+ { currentTime: 1, phase: 'active' },
+ { currentTime: 2, phase: 'after' }]) {
+ assert_phase_at_time(animation, test.phase, test.currentTime);
+ }
+}, 'Phase calculation for an animation effect with a positive start delay');
+
+test(t => {
+ const animation = createDiv(t).animate(null, { duration: 1, delay: -1 });
+
+ for (const test of [{ currentTime: -2, phase: 'before' },
+ { currentTime: -1, phase: 'before' },
+ { currentTime: 0, phase: 'after' }]) {
+ assert_phase_at_time(animation, test.phase, test.currentTime);
+ }
+}, 'Phase calculation for an animation effect with a negative start delay');
+
+test(t => {
+ const animation = createDiv(t).animate(null, { duration: 1, endDelay: 1 });
+
+ for (const test of [{ currentTime: -1, phase: 'before' },
+ { currentTime: 0, phase: 'active' },
+ { currentTime: 1, phase: 'after' },
+ { currentTime: 2, phase: 'after' }]) {
+ assert_phase_at_time(animation, test.phase, test.currentTime);
+ }
+}, 'Phase calculation for an animation effect with a positive end delay');
+
+test(t => {
+ const animation = createDiv(t).animate(null, { duration: 2, endDelay: -1 });
+
+ for (const test of [{ currentTime: -1, phase: 'before' },
+ { currentTime: 0, phase: 'active' },
+ { currentTime: 0.9, phase: 'active' },
+ { currentTime: 1, phase: 'after' }]) {
+ assert_phase_at_time(animation, test.phase, test.currentTime);
+ }
+}, 'Phase calculation for an animation effect with a negative end delay lesser'
+ + ' in magnitude than the active duration');
+
+test(t => {
+ const animation = createDiv(t).animate(null, { duration: 1, endDelay: -1 });
+
+ for (const test of [{ currentTime: -1, phase: 'before' },
+ { currentTime: 0, phase: 'after' },
+ { currentTime: 1, phase: 'after' }]) {
+ assert_phase_at_time(animation, test.phase, test.currentTime);
+ }
+}, 'Phase calculation for an animation effect with a negative end delay equal'
+ + ' in magnitude to the active duration');
+
+test(t => {
+ const animation = createDiv(t).animate(null, { duration: 1, endDelay: -2 });
+
+ for (const test of [{ currentTime: -2, phase: 'before' },
+ { currentTime: -1, phase: 'before' },
+ { currentTime: 0, phase: 'after' }]) {
+ assert_phase_at_time(animation, test.phase, test.currentTime);
+ }
+}, 'Phase calculation for an animation effect with a negative end delay'
+ + ' greater in magnitude than the active duration');
+
+test(t => {
+ const animation = createDiv(t).animate(null, { duration: 2,
+ delay: 1,
+ endDelay: -1 });
+
+ for (const test of [{ currentTime: 0, phase: 'before' },
+ { currentTime: 1, phase: 'active' },
+ { currentTime: 2, phase: 'after' }]) {
+ assert_phase_at_time(animation, test.phase, test.currentTime);
+ }
+}, 'Phase calculation for an animation effect with a positive start delay'
+ + ' and a negative end delay lesser in magnitude than the active duration');
+
+test(t => {
+ const animation = createDiv(t).animate(null, { duration: 1,
+ delay: -1,
+ endDelay: -1 });
+
+ for (const test of [{ currentTime: -2, phase: 'before' },
+ { currentTime: -1, phase: 'before' },
+ { currentTime: 0, phase: 'after' }]) {
+ assert_phase_at_time(animation, test.phase, test.currentTime);
+ }
+}, 'Phase calculation for an animation effect with a negative start delay'
+ + ' and a negative end delay equal in magnitude to the active duration');
+
+test(t => {
+ const animation = createDiv(t).animate(null, { duration: 1,
+ delay: -1,
+ endDelay: -2 });
+
+ for (const test of [{ currentTime: -3, phase: 'before' },
+ { currentTime: -2, phase: 'before' },
+ { currentTime: -1, phase: 'before' },
+ { currentTime: 0, phase: 'after' }]) {
+ assert_phase_at_time(animation, test.phase, test.currentTime);
+ }
+}, 'Phase calculation for an animation effect with a negative start delay'
+ + ' and a negative end delay equal greater in magnitude than the active'
+ + ' duration');
+
+test(t => {
+ const animation = createDiv(t).animate(null, 1);
+ animation.playbackRate = -1;
+
+ for (const test of [{ currentTime: -1, phase: 'before' },
+ { currentTime: 0, phase: 'before' },
+ { currentTime: 1, phase: 'active' },
+ { currentTime: 2, phase: 'after' }]) {
+ assert_phase_at_time(animation, test.phase, test.currentTime);
+ }
+}, 'Phase calculation for a simple animation effect with negative playback'
+ + ' rate');
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/web-animations/timing-model/animation-effects/simple-iteration-progress.html b/testing/web-platform/tests/web-animations/timing-model/animation-effects/simple-iteration-progress.html
new file mode 100644
index 0000000000..3c42f79a71
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/timing-model/animation-effects/simple-iteration-progress.html
@@ -0,0 +1,600 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title>Simple iteration progress</title>
+<link rel="help"
+ href="https://drafts.csswg.org/web-animations/#simple-iteration-progress">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../../testcommon.js"></script>
+<script src="../../resources/effect-tests.js"></script>
+<body>
+<div id="log"></div>
+<script>
+'use strict';
+
+function runTests(tests, description) {
+ for (const currentTest of tests) {
+ let testParams = Object.entries(currentTest.input)
+ .map(([attr, value]) => `${attr}:${value}`)
+ .join(' ');
+ if (currentTest.playbackRate !== undefined) {
+ testParams += ` playbackRate:${currentTest.playbackRate}`;
+ }
+
+ test(t => {
+ const div = createDiv(t);
+ const anim = div.animate({}, currentTest.input);
+ if (currentTest.playbackRate !== undefined) {
+ anim.playbackRate = currentTest.playbackRate;
+ }
+
+ assert_computed_timing_for_each_phase(
+ anim,
+ 'progress',
+ { before: currentTest.before,
+ activeBoundary: currentTest.active,
+ after: currentTest.after },
+ );
+ }, `${description}: ${testParams}`);
+ }
+}
+
+
+// --------------------------------------------------------------------
+//
+// Zero iteration duration tests
+//
+// --------------------------------------------------------------------
+
+runTests([
+ {
+ input: { iterations: 0,
+ iterationStart: 0,
+ duration: 0,
+ delay: 1,
+ fill: 'both' },
+ before: 0,
+ after: 0
+ },
+
+ {
+ input: { iterations: 0,
+ iterationStart: 0,
+ duration: 100,
+ delay: 1,
+ fill: 'both' },
+ before: 0,
+ after: 0
+ },
+
+ {
+ input: { iterations: 0,
+ iterationStart: 0,
+ duration: Infinity,
+ delay: 1,
+ fill: 'both' },
+ before: 0,
+ after: 0
+ },
+
+ {
+ input: { iterations: 0,
+ iterationStart: 2.5,
+ duration: 0,
+ delay: 1,
+ fill: 'both' },
+ before: 0.5,
+ after: 0.5
+ },
+
+ {
+ input: { iterations: 0,
+ iterationStart: 2.5,
+ duration: 100,
+ delay: 1,
+ fill: 'both' },
+ before: 0.5,
+ after: 0.5
+ },
+
+ {
+ input: { iterations: 0,
+ iterationStart: 2.5,
+ duration: Infinity,
+ delay: 1,
+ fill: 'both' },
+ before: 0.5,
+ after: 0.5
+ },
+
+ {
+ input: { iterations: 0,
+ iterationStart: 3,
+ duration: 0,
+ delay: 1,
+ fill: 'both' },
+ before: 0,
+ after: 0
+ },
+
+ {
+ input: { iterations: 0,
+ iterationStart: 3,
+ duration: 100,
+ delay: 1,
+ fill: 'both' },
+ before: 0,
+ after: 0
+ },
+
+ {
+ input: { iterations: 0,
+ iterationStart: 3,
+ duration: Infinity,
+ delay: 1,
+ fill: 'both' },
+ before: 0,
+ after: 0
+ }
+], 'Test zero iterations');
+
+
+// --------------------------------------------------------------------
+//
+// Tests where the iteration count is an integer
+//
+// --------------------------------------------------------------------
+
+runTests([
+ {
+ input: { iterations: 3,
+ iterationStart: 0,
+ duration: 0,
+ delay: 1,
+ fill: 'both' },
+ before: 0,
+ after: 1
+ },
+
+ {
+ input: { iterations: 3,
+ iterationStart: 0,
+ duration: 100,
+ delay: 1,
+ fill: 'both' },
+ before: 0,
+ active: 0,
+ after: 1
+ },
+
+ {
+ input: { iterations: 3,
+ iterationStart: 0,
+ duration: Infinity,
+ delay: 1,
+ fill: 'both' },
+ before: 0,
+ active: 0
+ },
+
+ {
+ input: { iterations: 3,
+ iterationStart: 2.5,
+ duration: 0,
+ delay: 1,
+ fill: 'both' },
+ before: 0.5,
+ after: 0.5
+ },
+
+ {
+ input: { iterations: 3,
+ iterationStart: 2.5,
+ duration: 100,
+ delay: 1,
+ fill: 'both' },
+ before: 0.5,
+ active: 0.5,
+ after: 0.5
+ },
+
+ {
+ input: { iterations: 3,
+ iterationStart: 2.5,
+ duration: Infinity,
+ delay: 1,
+ fill: 'both' },
+ before: 0.5,
+ active: 0.5
+ },
+
+ {
+ input: { iterations: 3,
+ iterationStart: 3,
+ duration: 0,
+ delay: 1,
+ fill: 'both' },
+ before: 0,
+ after: 1
+ },
+
+ {
+ input: { iterations: 3,
+ iterationStart: 3,
+ duration: 100,
+ delay: 1,
+ fill: 'both' },
+ before: 0,
+ active: 0,
+ after: 1
+ },
+
+ {
+ input: { iterations: 3,
+ iterationStart: 3,
+ duration: Infinity,
+ delay: 1,
+ fill: 'both' },
+ before: 0,
+ active: 0
+ }
+], 'Test integer iterations');
+
+
+// --------------------------------------------------------------------
+//
+// Tests where the iteration count is a fraction
+//
+// --------------------------------------------------------------------
+
+runTests([
+ {
+ input: { iterations: 3.5,
+ iterationStart: 0,
+ duration: 0,
+ delay: 1,
+ fill: 'both' },
+ before: 0,
+ after: 0.5
+ },
+
+ {
+ input: { iterations: 3.5,
+ iterationStart: 0,
+ duration: 100,
+ delay: 1,
+ fill: 'both' },
+ before: 0,
+ active: 0,
+ after: 0.5
+ },
+
+ {
+ input: { iterations: 3.5,
+ iterationStart: 0,
+ duration: Infinity,
+ delay: 1,
+ fill: 'both' },
+ before: 0,
+ active: 0
+ },
+
+ {
+ input: { iterations: 3.5,
+ iterationStart: 2.5,
+ duration: 0,
+ delay: 1,
+ fill: 'both' },
+ before: 0.5,
+ after: 1
+ },
+
+ {
+ input: { iterations: 3.5,
+ iterationStart: 2.5,
+ duration: 100,
+ delay: 1,
+ fill: 'both' },
+ before: 0.5,
+ active: 0.5,
+ after: 1
+ },
+
+ {
+ input: { iterations: 3.5,
+ iterationStart: 2.5,
+ duration: Infinity,
+ delay: 1,
+ fill: 'both' },
+ before: 0.5,
+ active: 0.5
+ },
+
+ {
+ input: { iterations: 3.5,
+ iterationStart: 3,
+ duration: 0,
+ delay: 1,
+ fill: 'both' },
+ before: 0,
+ after: 0.5
+ },
+
+ {
+ input: { iterations: 3.5,
+ iterationStart: 3,
+ duration: 100,
+ delay: 1,
+ fill: 'both' },
+ before: 0,
+ active: 0,
+ after: 0.5
+ },
+
+ {
+ input: { iterations: 3.5,
+ iterationStart: 3,
+ duration: Infinity,
+ delay: 1,
+ fill: 'both' },
+ before: 0,
+ active: 0
+ }
+], 'Test fractional iterations');
+
+
+// --------------------------------------------------------------------
+//
+// Tests where the iteration count is Infinity
+//
+// --------------------------------------------------------------------
+
+runTests([
+ {
+ input: { iterations: Infinity,
+ iterationStart: 0,
+ duration: 0,
+ delay: 1,
+ fill: 'both' },
+ before: 0,
+ after: 1
+ },
+
+ {
+ input: { iterations: Infinity,
+ iterationStart: 0,
+ duration: 100,
+ delay: 1,
+ fill: 'both' },
+ before: 0,
+ active: 0
+ },
+
+ {
+ input: { iterations: Infinity,
+ iterationStart: 0,
+ duration: Infinity,
+ delay: 1,
+ fill: 'both' },
+ before: 0,
+ active: 0
+ },
+
+ {
+ input: { iterations: Infinity,
+ iterationStart: 2.5,
+ duration: 0,
+ delay: 1,
+ fill: 'both' },
+ before: 0.5,
+ after: 0.5
+ },
+
+ {
+ input: { iterations: Infinity,
+ iterationStart: 2.5,
+ duration: 100,
+ delay: 1,
+ fill: 'both' },
+ before: 0.5,
+ active: 0.5
+ },
+
+ {
+ input: { iterations: Infinity,
+ iterationStart: 2.5,
+ duration: Infinity,
+ delay: 1,
+ fill: 'both' },
+ before: 0.5,
+ active: 0.5
+ },
+
+ {
+ input: { iterations: Infinity,
+ iterationStart: 3,
+ duration: 0,
+ delay: 1,
+ fill: 'both' },
+ before: 0,
+ after: 1
+ },
+
+ {
+ input: { iterations: Infinity,
+ iterationStart: 3,
+ duration: 100,
+ delay: 1,
+ fill: 'both' },
+ before: 0,
+ active: 0
+ },
+
+ {
+ input: { iterations: Infinity,
+ iterationStart: 3,
+ duration: Infinity,
+ delay: 1,
+ fill: 'both' },
+ before: 0,
+ active: 0
+ }
+], 'Test infinity iterations');
+
+
+// --------------------------------------------------------------------
+//
+// End delay tests
+//
+// --------------------------------------------------------------------
+
+runTests([
+ {
+ input: { duration: 100,
+ delay: 1,
+ fill: 'both',
+ endDelay: 50 },
+ before: 0,
+ active: 0,
+ after: 1
+ },
+
+ {
+ input: { duration: 100,
+ delay: 1,
+ fill: 'both',
+ endDelay: -50 },
+ before: 0,
+ active: 0,
+ after: 0.5
+ },
+
+ {
+ input: { duration: 100,
+ delay: 1,
+ fill: 'both',
+ endDelay: -100 },
+ before: 0,
+ active: 0,
+ after: 0
+ },
+
+ {
+ input: { duration: 100,
+ delay: 1,
+ fill: 'both',
+ endDelay: -200 },
+ before: 0,
+ active: 0,
+ after: 0
+ },
+
+ {
+ input: { iterationStart: 0.5,
+ duration: 100,
+ delay: 1,
+ fill: 'both',
+ endDelay: 50 },
+ before: 0.5,
+ active: 0.5,
+ after: 0.5
+ },
+
+ {
+ input: { iterationStart: 0.5,
+ duration: 100,
+ delay: 1,
+ fill: 'both',
+ endDelay: -50 },
+ before: 0.5,
+ active: 0.5,
+ after: 0
+ },
+
+ {
+ input: { iterationStart: 0.5,
+ duration: 100,
+ delay: 1,
+ fill: 'both',
+ endDelay: -100 },
+ before: 0.5,
+ active: 0.5,
+ after: 0.5
+ },
+
+ {
+ input: { iterations: 2,
+ duration: 100,
+ delay: 1,
+ fill: 'both',
+ endDelay: -100 },
+ before: 0,
+ active: 0,
+ after: 0
+ },
+
+ {
+ input: { iterations: 1,
+ iterationStart: 2,
+ duration: 100,
+ delay: 1,
+ fill: 'both',
+ endDelay: -50 },
+ before: 0,
+ active: 0,
+ after: 0.5
+ },
+
+ {
+ input: { iterations: 1,
+ iterationStart: 2,
+ duration: 100,
+ delay: 1,
+ fill: 'both',
+ endDelay: -100 },
+ before: 0,
+ active: 0,
+ after: 0
+ },
+], 'Test end delay');
+
+
+// --------------------------------------------------------------------
+//
+// Negative playback rate tests
+//
+// --------------------------------------------------------------------
+
+runTests([
+ {
+ input: { duration: 1,
+ delay: 1,
+ fill: 'both' },
+ playbackRate: -1,
+ before: 0,
+ active: 1,
+ after: 1
+ },
+
+ {
+ input: { duration: 0,
+ delay: 1,
+ fill: 'both' },
+ playbackRate: -1,
+ before: 0,
+ after: 1
+ },
+
+ {
+ input: { duration: 0,
+ iterations: 0,
+ delay: 1,
+ fill: 'both' },
+ playbackRate: -1,
+ before: 0,
+ after: 0
+ },
+], 'Test negative playback rate');
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/web-animations/timing-model/animations/canceling-an-animation.html b/testing/web-platform/tests/web-animations/timing-model/animations/canceling-an-animation.html
new file mode 100644
index 0000000000..f296ac4da7
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/timing-model/animations/canceling-an-animation.html
@@ -0,0 +1,127 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title>Canceling an animation</title>
+<link rel="help"
+ href="https://drafts.csswg.org/web-animations/#canceling-an-animation-section">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../../testcommon.js"></script>
+<body>
+<div id="log"></div>
+<script>
+'use strict';
+
+test(t => {
+ const animation = createDiv(t).animate(null, 100 * MS_PER_SEC);
+ animation.cancel();
+
+ assert_equals(animation.startTime, null,
+ 'The start time of a canceled animation should be unresolved');
+ assert_equals(animation.currentTime, null,
+ 'The hold time of a canceled animation should be unresolved');
+}, 'Canceling an animation should cause its start time and hold time to be'
+ + ' unresolved');
+
+promise_test(t => {
+ const animation = createDiv(t).animate(null, 100 * MS_PER_SEC);
+ const retPromise = animation.ready.then(() => {
+ assert_unreached('ready promise was fulfilled');
+ }).catch(err => {
+ assert_equals(err.name, 'AbortError',
+ 'ready promise is rejected with AbortError');
+ });
+
+ animation.cancel();
+
+ return retPromise;
+}, 'A play-pending ready promise should be rejected when the animation is'
+ + ' canceled');
+
+promise_test(async t => {
+ const animation = createDiv(t).animate(null, 100 * MS_PER_SEC);
+ await animation.ready;
+
+ // Make it pause-pending
+ animation.pause();
+
+ // We need to store the original ready promise since cancel() will
+ // replace it
+ const originalPromise = animation.ready;
+ animation.cancel();
+
+ await promise_rejects_dom(t, 'AbortError', originalPromise,
+ 'Cancel should abort ready promise');
+}, 'A pause-pending ready promise should be rejected when the animation is'
+ + ' canceled');
+
+promise_test(async t => {
+ const animation = createDiv(t).animate(null);
+ animation.cancel();
+ const promiseResult = await animation.ready;
+ assert_equals(promiseResult, animation);
+}, 'When an animation is canceled, it should create a resolved Promise');
+
+test(t => {
+ const animation = createDiv(t).animate(null);
+ const promise = animation.ready;
+ animation.cancel();
+ assert_not_equals(animation.ready, promise);
+ promise_rejects_dom(t, 'AbortError', promise, 'Cancel should abort ready promise');
+}, 'The ready promise should be replaced when the animation is canceled');
+
+promise_test(t => {
+ const animation = new Animation(
+ new KeyframeEffect(null, null, { duration: 1000 }),
+ null
+ );
+ assert_equals(animation.playState, 'idle',
+ 'The animation should be initially idle');
+
+ animation.finished.then(t.step_func(() => {
+ assert_unreached('Finished promise should not resolve');
+ }), t.step_func(() => {
+ assert_unreached('Finished promise should not reject');
+ }));
+
+ animation.cancel();
+
+ return waitForAnimationFrames(3);
+}, 'The finished promise should NOT be rejected if the animation is already'
+ + ' idle');
+
+promise_test(t => {
+ const animation = new Animation(
+ new KeyframeEffect(null, null, { duration: 1000 }),
+ null
+ );
+ assert_equals(animation.playState, 'idle',
+ 'The animation should be initially idle');
+
+ animation.oncancel = t.step_func(() => {
+ assert_unreached('Cancel event should not be fired');
+ });
+
+ animation.cancel();
+
+ return waitForAnimationFrames(3);
+}, 'The cancel event should NOT be fired if the animation is already'
+ + ' idle');
+
+promise_test(async t => {
+ const div = createDiv(t);
+ const animation = div.animate({}, 100 * MS_PER_SEC);
+ div.remove();
+
+ const eventWatcher = new EventWatcher(t, animation, 'cancel');
+
+ await animation.ready;
+ animation.cancel();
+
+ await eventWatcher.wait_for('cancel');
+
+ assert_equals(animation.effect.target.parentNode, null,
+ 'cancel event should be fired for the animation on an orphaned element');
+}, 'Canceling an animation should fire cancel event on orphaned element');
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/web-animations/timing-model/animations/document-timeline-animation-ref.html b/testing/web-platform/tests/web-animations/timing-model/animations/document-timeline-animation-ref.html
new file mode 100644
index 0000000000..d1ee52a553
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/timing-model/animations/document-timeline-animation-ref.html
@@ -0,0 +1,19 @@
+<!DOCTYPE html>
+<title>Reference for document timeline animation</title>
+<style>
+ #notes {
+ position: absolute;
+ left: 0px;
+ top: 100px;
+ }
+ body {
+ background: white;
+ }
+</style>
+<body>
+ <div id="box"></div>
+ <p id="notes">
+ This test creates a document timeline animation. If any blue pixels appear
+ in the screenshot, the test fails.
+ </p>
+</body> \ No newline at end of file
diff --git a/testing/web-platform/tests/web-animations/timing-model/animations/document-timeline-animation.html b/testing/web-platform/tests/web-animations/timing-model/animations/document-timeline-animation.html
new file mode 100644
index 0000000000..7d4dc76849
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/timing-model/animations/document-timeline-animation.html
@@ -0,0 +1,63 @@
+
+<!DOCTYPE html>
+<html class="reftest-wait">
+<meta charset="UTF-8">
+<title>document timeline animation</title>
+<link rel="match" href="document-timeline-animation-ref.html">
+<script src="/common/reftest-wait.js"></script>
+<script src="../../testcommon.js"></script>
+<style>
+ #box-1, #box-2 {
+ position: absolute;
+ top: 0px;
+ width: 40px;
+ height: 40px;
+ }
+ #box-1 {
+ background: blue;
+ z-index: 1;
+ left: 0px;
+ }
+ #box-2 {
+ background: white;
+ z-index: 2;
+ left: 100px;
+ }
+ #notes {
+ position: absolute;
+ left: 0px;
+ top: 100px;
+ }
+ body {
+ background: white;
+ }
+</style>
+
+<body>
+ <div id="box-1"></div>
+ <div id="box-2"></div>
+ <p id="notes">
+ This test creates a document timeline animation. If any blue pixels appear
+ in the screenshot, the test fails.
+ </p>
+</body>
+<script>
+ onload = async function() {
+ const elem = document.getElementById('box-1');
+ const keyframes = [
+ { transform: 'none' },
+ { transform: 'translateX(100px)' }
+ ];
+ const effect =
+ new KeyframeEffect(elem, keyframes,
+ {iterations: 1, duration: 10000, fill: 'forwards'});
+ const timeline = new DocumentTimeline();
+ const animation = new Animation(effect, timeline);
+ animation.play();
+ await animation.ready;
+ animation.finish();
+ await animation.finished;
+ await waitForAnimationFrames(2);
+ takeScreenshot();
+ };
+</script>
diff --git a/testing/web-platform/tests/web-animations/timing-model/animations/finishing-an-animation.html b/testing/web-platform/tests/web-animations/timing-model/animations/finishing-an-animation.html
new file mode 100644
index 0000000000..fbf6558f78
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/timing-model/animations/finishing-an-animation.html
@@ -0,0 +1,330 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title>Finishing an animation</title>
+<link rel="help"
+ href="https://drafts.csswg.org/web-animations/#finishing-an-animation-section">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../../testcommon.js"></script>
+<script src="../../resources/timing-override.js"></script>
+<body>
+<div id="log"></div>
+<script>
+'use strict';
+
+test(t => {
+ const div = createDiv(t);
+ const animation = div.animate(null, 100 * MS_PER_SEC);
+ animation.playbackRate = 0;
+
+ assert_throws_dom('InvalidStateError', () => {
+ animation.finish();
+ });
+}, 'Finishing an animation with a zero playback rate throws');
+
+test(t => {
+ const div = createDiv(t);
+ const animation = div.animate(null,
+ { duration : 100 * MS_PER_SEC,
+ iterations : Infinity });
+
+ assert_throws_dom('InvalidStateError', () => {
+ animation.finish();
+ });
+}, 'Finishing an infinite animation throws');
+
+test(t => {
+ const div = createDiv(t);
+ const animation = div.animate(null, 100 * MS_PER_SEC);
+ animation.finish();
+
+ assert_time_equals_literal(animation.currentTime, 100 * MS_PER_SEC,
+ 'After finishing, the currentTime should be set to the end of the'
+ + ' active duration');
+}, 'Finishing an animation seeks to the end time');
+
+test(t => {
+ const div = createDiv(t);
+ const animation = div.animate(null, 100 * MS_PER_SEC);
+ // 1s past effect end
+ animation.currentTime =
+ animation.effect.getComputedTiming().endTime + 1 * MS_PER_SEC;
+ animation.finish();
+
+ assert_time_equals_literal(animation.currentTime, 100 * MS_PER_SEC,
+ 'After finishing, the currentTime should be set back to the end of the'
+ + ' active duration');
+}, 'Finishing an animation with a current time past the effect end jumps'
+ + ' back to the end');
+
+promise_test(async t => {
+ const div = createDiv(t);
+ const animation = div.animate(null, 100 * MS_PER_SEC);
+ animation.currentTime = 100 * MS_PER_SEC;
+ await animation.finished;
+
+ animation.playbackRate = -1;
+ animation.finish();
+
+ assert_equals(animation.currentTime, 0,
+ 'After finishing a reversed animation the currentTime ' +
+ 'should be set to zero');
+}, 'Finishing a reversed animation jumps to zero time');
+
+promise_test(async t => {
+ const div = createDiv(t);
+ const animation = div.animate(null, 100 * MS_PER_SEC);
+ animation.currentTime = 100 * MS_PER_SEC;
+ await animation.finished;
+
+ animation.playbackRate = -1;
+ animation.currentTime = -1000;
+ animation.finish();
+
+ assert_equals(animation.currentTime, 0,
+ 'After finishing a reversed animation the currentTime ' +
+ 'should be set back to zero');
+}, 'Finishing a reversed animation with a current time less than zero'
+ + ' makes it jump back to zero');
+
+promise_test(async t => {
+ const div = createDiv(t);
+ const animation = div.animate(null, 100 * MS_PER_SEC);
+ animation.pause();
+ await animation.ready;
+
+ animation.finish();
+
+ assert_equals(animation.playState, 'finished',
+ 'The play state of a paused animation should become ' +
+ '"finished"');
+ assert_times_equal(animation.startTime,
+ animation.timeline.currentTime - 100 * MS_PER_SEC,
+ 'The start time of a paused animation should be set');
+}, 'Finishing a paused animation resolves the start time');
+
+test(t => {
+ const div = createDiv(t);
+ const animation = div.animate(null, 100 * MS_PER_SEC);
+ // Update playbackRate so we can test that the calculated startTime
+ // respects it
+ animation.playbackRate = 2;
+ animation.pause();
+ // While animation is still pause-pending call finish()
+ animation.finish();
+
+ assert_false(animation.pending);
+ assert_equals(animation.playState, 'finished',
+ 'The play state of a pause-pending animation should become ' +
+ '"finished"');
+ assert_times_equal(animation.startTime,
+ animation.timeline.currentTime - 100 * MS_PER_SEC / 2,
+ 'The start time of a pause-pending animation should ' +
+ 'be set');
+}, 'Finishing a pause-pending animation resolves the pending task'
+ + ' immediately and update the start time');
+
+test(t => {
+ const div = createDiv(t);
+ const animation = div.animate(null, 100 * MS_PER_SEC);
+ animation.playbackRate = -2;
+ animation.pause();
+ animation.finish();
+
+ assert_false(animation.pending);
+ assert_equals(animation.playState, 'finished',
+ 'The play state of a pause-pending animation should become ' +
+ '"finished"');
+ assert_times_equal(animation.startTime, animation.timeline.currentTime,
+ 'The start time of a pause-pending animation should be ' +
+ 'set');
+}, 'Finishing a pause-pending animation with negative playback rate'
+ + ' resolves the pending task immediately');
+
+test(t => {
+ const div = createDiv(t);
+ const animation = div.animate(null, 100 * MS_PER_SEC);
+ animation.playbackRate = 0.5;
+ animation.finish();
+
+ assert_false(animation.pending);
+ assert_equals(animation.playState, 'finished',
+ 'The play state of a play-pending animation should become ' +
+ '"finished"');
+ assert_times_equal(animation.startTime,
+ animation.timeline.currentTime - 100 * MS_PER_SEC / 0.5,
+ 'The start time of a play-pending animation should ' +
+ 'be set');
+}, 'Finishing an animation while play-pending resolves the pending'
+ + ' task immediately');
+
+// FIXME: Add a test for when we are play-pending without an active timeline.
+// - In that case even after calling finish() we should still be pending but
+// the current time should be updated
+
+promise_test(async t => {
+ const div = createDiv(t);
+ const animation = div.animate(null, 100 * MS_PER_SEC);
+ await animation.ready;
+
+ animation.pause();
+ animation.play();
+ // We are now in the unusual situation of being play-pending whilst having
+ // a resolved start time. Check that finish() still triggers a transition
+ // to the finished state immediately.
+ animation.finish();
+
+ assert_equals(animation.playState, 'finished',
+ 'After aborting a pause then finishing an animation its play ' +
+ 'state should become "finished" immediately');
+}, 'Finishing an animation during an aborted pause makes it finished'
+ + ' immediately');
+
+promise_test(async t => {
+ const div = createDiv(t);
+ const animation = div.animate(null, 100 * MS_PER_SEC);
+ let resolvedFinished = false;
+ animation.finished.then(() => {
+ resolvedFinished = true;
+ });
+
+ await animation.ready;
+
+ animation.finish();
+ await Promise.resolve();
+
+ assert_true(resolvedFinished, 'finished promise should be resolved');
+}, 'Finishing an animation resolves the finished promise synchronously');
+
+promise_test(async t => {
+ const effect = new KeyframeEffect(null, null, 100 * MS_PER_SEC);
+ const animation = new Animation(effect, document.timeline);
+ let resolvedFinished = false;
+ animation.finished.then(() => {
+ resolvedFinished = true;
+ });
+
+ await animation.ready;
+
+ animation.finish();
+ await Promise.resolve();
+
+ assert_true(resolvedFinished, 'finished promise should be resolved');
+}, 'Finishing an animation without a target resolves the finished promise'
+ + ' synchronously');
+
+promise_test(async t => {
+ const animation = createDiv(t).animate(null, 100 * MS_PER_SEC);
+ const promise = animation.ready;
+ let readyResolved = false;
+
+ animation.finish();
+ animation.ready.then(() => { readyResolved = true; });
+
+ const promiseResult = await animation.finished;
+
+ assert_equals(promiseResult, animation);
+ assert_equals(animation.ready, promise);
+ assert_true(readyResolved);
+}, 'A pending ready promise is resolved and not replaced when the animation'
+ + ' is finished');
+
+promise_test(async t => {
+ const animation = createDiv(t).animate(null, 100 * MS_PER_SEC);
+ await animation.ready;
+
+ animation.updatePlaybackRate(2);
+ assert_true(animation.pending);
+
+ animation.finish();
+ assert_false(animation.pending);
+ assert_equals(animation.playbackRate, 2);
+ assert_time_equals_literal(animation.currentTime, 100 * MS_PER_SEC);
+}, 'A pending playback rate should be applied immediately when an animation'
+ + ' is finished');
+
+promise_test(async t => {
+ const animation = createDiv(t).animate(null, 100 * MS_PER_SEC);
+ await animation.ready;
+
+ animation.updatePlaybackRate(0);
+
+ assert_throws_dom('InvalidStateError', () => {
+ animation.finish();
+ });
+}, 'An exception should be thrown if the effective playback rate is zero');
+
+promise_test(async t => {
+ const animation = createDiv(t).animate(null, {
+ duration: 100 * MS_PER_SEC,
+ iterations: Infinity
+ });
+ animation.currentTime = 50 * MS_PER_SEC;
+ animation.playbackRate = -1;
+ await animation.ready;
+
+ animation.updatePlaybackRate(1);
+
+ assert_throws_dom('InvalidStateError', () => {
+ animation.finish();
+ });
+}, 'An exception should be thrown when finishing if the effective playback rate'
+ + ' is positive and the target effect end is infinity');
+
+promise_test(async t => {
+ const animation = createDiv(t).animate(null, {
+ duration: 100 * MS_PER_SEC,
+ iterations: Infinity
+ });
+ await animation.ready;
+
+ animation.updatePlaybackRate(-1);
+
+ animation.finish();
+ // Should not have thrown
+}, 'An exception is NOT thrown when finishing if the effective playback rate'
+ + ' is negative and the target effect end is infinity');
+
+promise_test(async t => {
+ const div = createDiv(t);
+ const animation = div.animate({}, 100 * MS_PER_SEC);
+ div.remove();
+
+ const eventWatcher = new EventWatcher(t, animation, 'finish');
+
+ await animation.ready;
+ animation.finish();
+
+ await eventWatcher.wait_for('finish');
+ assert_equals(animation.effect.target.parentNode, null,
+ 'finish event should be fired for the animation on an orphaned element');
+}, 'Finishing an animation fires finish event on orphaned element');
+
+promise_test(async t => {
+ const animation = createDiv(t).animate(null, 100 * MS_PER_SEC);
+ await animation.ready;
+
+ const originalFinishPromise = animation.finished;
+
+ animation.cancel();
+ assert_equals(animation.startTime, null);
+ assert_equals(animation.currentTime, null);
+
+ const resolvedFinishPromise = animation.finished;
+ assert_not_equals(originalFinishPromise, resolvedFinishPromise,
+ 'Canceling an animation should create a new finished promise');
+
+ animation.finish();
+ assert_equals(animation.playState, 'finished',
+ 'The play state of a canceled animation should become ' +
+ '"finished"');
+ assert_times_equal(animation.startTime,
+ animation.timeline.currentTime - 100 * MS_PER_SEC,
+ 'The start time of a finished animation should be set');
+ assert_times_equal(animation.currentTime, 100000,
+ 'Hold time should be set to end boundary of the animation');
+
+}, 'Finishing a canceled animation sets the current and start times');
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/web-animations/timing-model/animations/infinite-duration-animation-ref.html b/testing/web-platform/tests/web-animations/timing-model/animations/infinite-duration-animation-ref.html
new file mode 100644
index 0000000000..6b358bd4e7
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/timing-model/animations/infinite-duration-animation-ref.html
@@ -0,0 +1,19 @@
+<!DOCTYPE html>
+<title>Reference for infinite duration animation</title>
+<style>
+ #notes {
+ position: absolute;
+ left: 0px;
+ top: 100px;
+ }
+ body {
+ background: white;
+ }
+</style>
+<body>
+ <p id="notes">
+ This test creates an infinite duration animations, which should be stuck at
+ a progress of 0. If any blue pixels appear in the screenshot, the test
+ fails.
+ </p>
+</body>
diff --git a/testing/web-platform/tests/web-animations/timing-model/animations/infinite-duration-animation.html b/testing/web-platform/tests/web-animations/timing-model/animations/infinite-duration-animation.html
new file mode 100644
index 0000000000..c641e5afa2
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/timing-model/animations/infinite-duration-animation.html
@@ -0,0 +1,64 @@
+
+<!DOCTYPE html>
+<html class="reftest-wait">
+<meta charset="UTF-8">
+<title>Infinite duration animation</title>
+<link rel="match" href="infinite-duration-animation-ref.html">
+<script src="/common/reftest-wait.js"></script>
+<script src="../../testcommon.js"></script>
+<style>
+ #box-1, #box-2 {
+ border: 1px solid white;
+ height: 40px;
+ position: absolute;
+ top: 40px;
+ width: 40px;
+ }
+ #box-1 {
+ background: blue;
+ z-index: 1;
+ }
+ #box-2 {
+ background: white;
+ z-index: 2;
+ }
+ #notes {
+ position: absolute;
+ left: 0px;
+ top: 100px;
+ }
+ body {
+ background: white;
+ }
+</style>
+
+<body>
+ <div id="box-1"></div>
+ <div id="box-2"></div>
+ <p id="notes">
+ This test creates an infinite duration animations, which should be stuck at
+ a progress of 0. If any blue pixels appear in the screenshot, the test
+ fails.
+ </p>
+</body>
+<script>
+ onload = async function() {
+ // Double rAF to ensure that we are not bogged down during initialization
+ // and the compositor is ready.
+ waitForAnimationFrames(2).then(() => {
+ const elem = document.getElementById('box-1');
+ const keyframes = [
+ { transform: 'translateX(0px)' },
+ { transform: 'translateX(100px)' }
+ ];
+ const effect =
+ new KeyframeEffect(elem, keyframes,
+ {iterations: 3, duration: Infinity});
+ const animation = new Animation(effect);
+ animation.play();
+ animation.ready.then(() => {
+ takeScreenshotDelayed(100);
+ });
+ });
+ };
+</script>
diff --git a/testing/web-platform/tests/web-animations/timing-model/animations/invalidating-animation-before-start-time-synced.html b/testing/web-platform/tests/web-animations/timing-model/animations/invalidating-animation-before-start-time-synced.html
new file mode 100644
index 0000000000..653e46906a
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/timing-model/animations/invalidating-animation-before-start-time-synced.html
@@ -0,0 +1,71 @@
+<!DOCTYPE html>
+<link rel="author" href="mailto:bokan@chromium.org">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<style>
+.target {
+ width: 100px;
+ height: 100px;
+ background-color: dodgerblue;
+}
+</style>
+<script>
+function jank() {
+ let start = performance.now();
+ let x = 0;
+ while(performance.now() - start < 100) {
+ x++;
+ }
+}
+
+function target1() { return document.querySelector('#target1'); }
+function target2() { return document.querySelector('#target2'); }
+function spinner() { return document.querySelector('#spinner'); }
+
+function firstFrame() {
+ target1().animate([{transform: 'translateX(400px)'}], {id: "target1", duration: 10000});
+ target2().animate([{transform: 'translateX(400px)'}], {id: "target2", duration: 10000});
+ requestAnimationFrame(secondFrame);
+
+ // Simulate some jank so that, if the above animations are started
+ // asynchronously, the next rendering opportunity is likely to start
+ // immediately after this one and without the animations having started yet.
+ jank();
+}
+
+function secondFrame() {
+ // Modify the style to invalidate the starting keyframe on the first target
+ // only.
+ target1().style.transform = `translateY(-1px)`;
+
+ // The spinner is used to avoid a specific Chrome behavior (bug?). It ensures
+ // a new animation is pushed to the compositor in this frame and prevents the
+ // #target1 animation being started from the main thread in
+ // PendingAnimations::Update when `started_synchronized_on_compositor` is
+ // false.
+ spinner().animate([{transform: 'rotateZ(90deg)'}], {id: 'spinner', duration: 1000});
+
+ requestAnimationFrame(finishTestCb);
+}
+
+let finishTestCb = null;
+const finishTest = new Promise(resolve => { finishTestCb = resolve; });
+
+promise_test(async (t) => {
+ onload = () => requestAnimationFrame(firstFrame);
+ await finishTest;
+
+ anim1 = target1().getAnimations()[0];
+ anim2 = target2().getAnimations()[0];
+
+ await Promise.all([anim1.ready, anim2.ready]);
+ assert_not_equals(anim1.startTime, 0);
+ assert_equals(anim1.startTime, anim2.startTime);
+}, "Animation invalidated before startTime is set doesn't affect startTime");
+</script>
+<!-- This text is necessary in Chrome in order to trigger a first contentful
+paint which unblocks compositor commits. -->
+The blue boxes below should stay aligned in the x-axis.
+<div id="target1" class="target"></div>
+<div id="target2" class="target"></div>
+<div id="spinner" class="target" style="background:limegreen"></div>
diff --git a/testing/web-platform/tests/web-animations/timing-model/animations/pausing-an-animation.html b/testing/web-platform/tests/web-animations/timing-model/animations/pausing-an-animation.html
new file mode 100644
index 0000000000..dd9522cb35
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/timing-model/animations/pausing-an-animation.html
@@ -0,0 +1,119 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title>Pausing an animation</title>
+<link rel="help"
+ href="https://drafts.csswg.org/web-animations/#pausing-an-animation-section">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../../testcommon.js"></script>
+<body>
+<div id="log"></div>
+<script>
+'use strict';
+
+promise_test(async t => {
+ const animation = createDiv(t).animate(null, 100 * MS_PER_SEC);
+ await animation.ready;
+
+ const startTimeBeforePausing = animation.startTime;
+
+ animation.pause();
+ assert_equals(animation.startTime, startTimeBeforePausing,
+ 'The start time does not change when pausing-pending');
+
+ await animation.ready;
+
+ assert_equals(animation.startTime, null,
+ 'The start time is unresolved when paused');
+}, 'Pausing clears the start time');
+
+promise_test(async t => {
+ const animation = createDiv(t).animate(null, 100 * MS_PER_SEC);
+ await animation.ready;
+
+ animation.pause();
+ assert_not_equals(animation.startTime, null,
+ 'The start time is resolved when pause-pending');
+
+ animation.play();
+ assert_not_equals(animation.startTime, null,
+ 'The start time is preserved when a pause is aborted');
+}, 'Aborting a pause preserves the start time');
+
+promise_test(async t => {
+ const animation = createDiv(t).animate(null, 100 * MS_PER_SEC);
+ const promise = animation.ready;
+ animation.pause();
+
+ const promiseResult = await promise;
+
+ assert_equals(promiseResult, animation);
+ assert_equals(animation.ready, promise);
+ assert_false(animation.pending, 'No longer pause-pending');
+}, 'A pending ready promise should be resolved and not replaced when the'
+ + ' animation is paused');
+
+promise_test(async t => {
+ const animation = createDiv(t).animate(null, 100 * MS_PER_SEC);
+ // Let animation start roughly half-way through
+ animation.currentTime = 50 * MS_PER_SEC;
+ await animation.ready;
+
+ // Go pause-pending and also set a pending playback rate
+ animation.pause();
+ animation.updatePlaybackRate(0.5);
+
+ await animation.ready;
+ // If the current time was updated using the new playback rate it will jump
+ // back to 25s but if we correctly used the old playback rate the current time
+ // will be >= 50s.
+ assert_greater_than_equal(animation.currentTime, 50 * MS_PER_SEC);
+}, 'A pause-pending animation maintains the current time when applying a'
+ + ' pending playback rate');
+
+promise_test(async t => {
+ // This test does not cover a specific step in the algorithm but serves as a
+ // high-level sanity check that pausing does, in fact, freeze the current
+ // time.
+ const animation = createDiv(t).animate(null, 100 * MS_PER_SEC);
+ await animation.ready;
+
+ animation.pause();
+ await animation.ready;
+
+ const currentTimeAfterPausing = animation.currentTime;
+
+ await waitForNextFrame();
+
+ assert_equals(animation.currentTime, currentTimeAfterPausing,
+ 'Animation.currentTime is unchanged after pausing');
+}, 'The animation\'s current time remains fixed after pausing');
+
+
+promise_test(async t => {
+
+ const animation = createDiv(t).animate(null, 100 * MS_PER_SEC);
+
+ const originalReadyPromise = animation.ready;
+ animation.cancel();
+ assert_equals(animation.startTime, null);
+ assert_equals(animation.currentTime, null);
+
+ const readyPromise = animation.ready;
+ assert_not_equals(originalReadyPromise, readyPromise,
+ 'Canceling an animation should create a new ready promise');
+
+ animation.pause();
+ assert_equals(animation.playState, 'paused',
+ 'Pausing a canceled animation should update the play state');
+ assert_true(animation.pending, 'animation should be pause-pending');
+ await animation.ready;
+ assert_false(animation.pending,
+ 'animation should no longer be pause-pending');
+ assert_equals(animation.startTime, null, 'start time should be unresolved');
+ assert_equals(animation.currentTime, 0, 'current time should be set to zero');
+
+}, 'Pausing a canceled animation sets the current time');
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/web-animations/timing-model/animations/play-states.html b/testing/web-platform/tests/web-animations/timing-model/animations/play-states.html
new file mode 100644
index 0000000000..ec7d8c842f
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/timing-model/animations/play-states.html
@@ -0,0 +1,185 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title>Play states</title>
+<link rel="help" href="https://drafts.csswg.org/web-animations/#play-states">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../../testcommon.js"></script>
+<body>
+<div id="log"></div>
+<script>
+'use strict';
+
+test(t => {
+ const animation = new Animation(
+ new KeyframeEffect(null, {}, 100 * MS_PER_SEC)
+ );
+ assert_equals(animation.currentTime, null,
+ 'Current time should be initially unresolved');
+
+ assert_equals(animation.playState, 'idle');
+}, 'reports \'idle\' for an animation with an unresolved current time'
+ + ' and no pending tasks')
+
+test(t => {
+ const animation = createDiv(t).animate({}, 100 * MS_PER_SEC);
+
+ animation.pause();
+
+ assert_equals(animation.playState, 'paused');
+}, 'reports \'paused\' for an animation with a pending pause task');
+
+test(t => {
+ const animation = new Animation(
+ new KeyframeEffect(null, {}, 100 * MS_PER_SEC)
+ );
+
+ animation.currentTime = 0;
+ assert_equals(animation.startTime, null,
+ 'Start time should still be unresolved after setting current'
+ + ' time');
+
+ assert_equals(animation.playState, 'paused');
+}, 'reports \'paused\' for an animation with a resolved current time and'
+ + ' unresolved start time')
+
+test(t => {
+ const animation = new Animation(
+ new KeyframeEffect(null, {}, 100 * MS_PER_SEC)
+ );
+
+ animation.startTime = document.timeline.currentTime;
+ assert_not_equals(animation.currentTime, null,
+ 'Current time should be resolved after setting start time');
+
+ assert_equals(animation.playState, 'running');
+}, 'reports \'running\' for an animation with a resolved start time and'
+ + ' current time');
+
+test(t => {
+ const animation = new Animation(
+ new KeyframeEffect(null, {}, 100 * MS_PER_SEC)
+ );
+ animation.startTime = document.timeline.currentTime;
+
+ animation.currentTime = 100 * MS_PER_SEC;
+
+ assert_equals(animation.playState, 'finished');
+}, 'reports \'finished\' when playback rate > 0 and'
+ + ' current time = target effect end');
+
+test(t => {
+ const animation = new Animation(
+ new KeyframeEffect(null, {}, 100 * MS_PER_SEC)
+ );
+ animation.startTime = document.timeline.currentTime;
+
+ animation.playbackRate = 0;
+ animation.currentTime = 100 * MS_PER_SEC;
+
+ assert_equals(animation.playState, 'running');
+}, 'reports \'running\' when playback rate = 0 and'
+ + ' current time = target effect end');
+
+test(t => {
+ const animation = new Animation(
+ new KeyframeEffect(null, {}, 100 * MS_PER_SEC)
+ );
+ animation.startTime = document.timeline.currentTime;
+
+ animation.playbackRate = -1;
+ animation.currentTime = 100 * MS_PER_SEC;
+
+ assert_equals(animation.playState, 'running');
+}, 'reports \'running\' when playback rate < 0 and'
+ + ' current time = target effect end');
+
+test(t => {
+ const animation = new Animation(
+ new KeyframeEffect(null, {}, 100 * MS_PER_SEC)
+ );
+ animation.startTime = document.timeline.currentTime;
+
+ animation.currentTime = 0;
+
+ assert_equals(animation.playState, 'running');
+}, 'reports \'running\' when playback rate > 0 and'
+ + ' current time = 0');
+
+test(t => {
+ const animation = new Animation(
+ new KeyframeEffect(null, {}, 100 * MS_PER_SEC)
+ );
+ animation.startTime = document.timeline.currentTime;
+
+ animation.playbackRate = 0;
+ animation.currentTime = 0;
+
+ assert_equals(animation.playState, 'running');
+}, 'reports \'running\' when playback rate = 0 and'
+ + ' current time = 0');
+
+test(t => {
+ const animation = new Animation(
+ new KeyframeEffect(null, {}, 100 * MS_PER_SEC)
+ );
+ animation.startTime = document.timeline.currentTime;
+
+ animation.playbackRate = -1;
+ animation.currentTime = 0;
+
+ assert_equals(animation.playState, 'finished');
+}, 'reports \'finished\' when playback rate < 0 and'
+ + ' current time = 0');
+
+test(t => {
+ const animation = createDiv(t).animate({}, 0);
+ assert_equals(animation.startTime, null,
+ 'Sanity check: start time should be unresolved');
+
+ assert_equals(animation.playState, 'finished');
+}, 'reports \'finished\' when playback rate > 0 and'
+ + ' current time = target effect end and there is a pending play task');
+
+test(t => {
+ const animation = createDiv(t).animate({}, 100 * MS_PER_SEC);
+ assert_equals(animation.startTime, null,
+ 'Sanity check: start time should be unresolved');
+
+ assert_equals(animation.playState, 'running');
+}, 'reports \'running\' when playback rate > 0 and'
+ + ' current time < target effect end and there is a pending play task');
+
+test(t => {
+ const animation = createDiv(t).animate({}, 100 * MS_PER_SEC);
+ assert_equals(animation.playState, 'running');
+ assert_true(animation.pending);
+}, 'reports \'running\' for a play-pending animation');
+
+test(t => {
+ const animation = createDiv(t).animate({}, 100 * MS_PER_SEC);
+ animation.pause();
+ assert_equals(animation.playState, 'paused');
+ assert_true(animation.pending);
+}, 'reports \'paused\' for a pause-pending animation');
+
+test(t => {
+ const animation = createDiv(t).animate({}, 0);
+ assert_equals(animation.playState, 'finished');
+ assert_true(animation.pending);
+}, 'reports \'finished\' for a finished-pending animation');
+
+test(t => {
+ const animation = createDiv(t).animate({}, 100 * MS_PER_SEC);
+ // Set up the pending playback rate
+ animation.updatePlaybackRate(-1);
+ // Call play again so that we seek to the end while remaining play-pending
+ animation.play();
+ // For a pending animation, the play state should always report what the
+ // play state _will_ be once we finish pending.
+ assert_equals(animation.playState, 'running');
+ assert_true(animation.pending);
+}, 'reports the play state based on the pending playback rate');
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/web-animations/timing-model/animations/playing-an-animation.html b/testing/web-platform/tests/web-animations/timing-model/animations/playing-an-animation.html
new file mode 100644
index 0000000000..01e036ae57
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/timing-model/animations/playing-an-animation.html
@@ -0,0 +1,177 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title>Playing an animation</title>
+<link rel="help"
+ href="https://drafts.csswg.org/web-animations/#playing-an-animation-section">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../../testcommon.js"></script>
+<body>
+<div id="log"></div>
+<script>
+'use strict';
+
+test(t => {
+ const animation = createDiv(t).animate(null, 100 * MS_PER_SEC);
+ animation.currentTime = 1 * MS_PER_SEC;
+ assert_time_equals_literal(animation.currentTime, 1 * MS_PER_SEC);
+ animation.play();
+ assert_time_equals_literal(animation.currentTime, 1 * MS_PER_SEC);
+}, 'Playing a running animation leaves the current time unchanged');
+
+test(t => {
+ const animation = createDiv(t).animate(null, 100 * MS_PER_SEC);
+ animation.finish();
+ assert_time_equals_literal(animation.currentTime, 100 * MS_PER_SEC);
+ animation.play();
+ assert_time_equals_literal(animation.currentTime, 0);
+}, 'Playing a finished animation seeks back to the start');
+
+test(t => {
+ const animation = createDiv(t).animate(null, 100 * MS_PER_SEC);
+ animation.playbackRate = -1;
+ animation.currentTime = 0;
+ assert_time_equals_literal(animation.currentTime, 0);
+ animation.play();
+ assert_time_equals_literal(animation.currentTime, 100 * MS_PER_SEC);
+}, 'Playing a finished and reversed animation seeks to end');
+
+promise_test(async t => {
+ const animation = createDiv(t).animate(null, 100 * MS_PER_SEC);
+ animation.finish();
+
+ // Initiate a pause then abort it
+ animation.pause();
+ animation.play();
+
+ // Wait to return to running state
+ await animation.ready;
+
+ assert_true(animation.currentTime < 100 * 1000,
+ 'After aborting a pause when finished, the current time should'
+ + ' jump back to the start of the animation');
+}, 'Playing a pause-pending but previously finished animation seeks back to'
+ + ' to the start');
+
+promise_test(async t => {
+ const animation = createDiv(t).animate(null, 100 * MS_PER_SEC);
+ animation.finish();
+ await animation.ready;
+
+ animation.play();
+ assert_equals(animation.startTime, null, 'start time is unresolved');
+}, 'Playing a finished animation clears the start time');
+
+test(t => {
+ const animation = createDiv(t).animate(null, 100 * MS_PER_SEC);
+ animation.cancel();
+ const promise = animation.ready;
+ animation.play();
+ assert_not_equals(animation.ready, promise);
+}, 'The ready promise should be replaced if the animation is not already'
+ + ' pending');
+
+promise_test(async t => {
+ const animation = createDiv(t).animate(null, 100 * MS_PER_SEC);
+ const promise = animation.ready;
+ const promiseResult = await promise;
+ assert_equals(promiseResult, animation);
+ assert_equals(animation.ready, promise);
+}, 'A pending ready promise should be resolved and not replaced when the'
+ + ' animation enters the running state');
+
+promise_test(async t => {
+ const animation = createDiv(t).animate(null, 100 * MS_PER_SEC);
+ animation.currentTime = 50 * MS_PER_SEC;
+ await animation.ready;
+
+ animation.pause();
+ await animation.ready;
+
+ const holdTime = animation.currentTime;
+
+ animation.play();
+ await animation.ready;
+
+ assert_less_than_equal(
+ animation.startTime,
+ animation.timeline.currentTime - holdTime + TIME_PRECISION
+ );
+}, 'Resuming an animation from paused calculates start time from hold time');
+
+promise_test(async t => {
+ const animation = createDiv(t).animate(null, 100 * MS_PER_SEC);
+ await animation.ready;
+
+ // Go to pause-pending state
+ animation.pause();
+ assert_true(animation.pending, 'Animation is pending');
+ const pauseReadyPromise = animation.ready;
+
+ // Now play again immediately (abort the pause)
+ animation.play();
+ assert_true(animation.pending, 'Animation is still pending');
+ assert_equals(animation.ready, pauseReadyPromise,
+ 'The pause Promise is re-used when playing while waiting'
+ + ' to pause');
+
+ // Sanity check: Animation proceeds to running state
+ await animation.ready;
+ assert_true(!animation.pending && animation.playState === 'running',
+ 'Animation is running after aborting a pause');
+}, 'If a pause operation is interrupted, the ready promise is reused');
+
+promise_test(async t => {
+ // Seek animation beyond target end
+ const animation = createDiv(t).animate(null, 100 * MS_PER_SEC);
+ animation.currentTime = -100 * MS_PER_SEC;
+ await animation.ready;
+
+ // Set pending playback rate to the opposite direction
+ animation.updatePlaybackRate(-1);
+ assert_true(animation.pending);
+ assert_equals(animation.playbackRate, 1);
+
+ // When we play, we should seek to the target end, NOT to zero (which
+ // is where we would seek to if we used the playbackRate of 1.
+ animation.play();
+ assert_time_equals_literal(animation.currentTime, 100 * MS_PER_SEC);
+}, 'A pending playback rate is used when determining auto-rewind behavior');
+
+promise_test(async t => {
+ const animation = createDiv(t).animate(null, 100 * MS_PER_SEC);
+ animation.cancel();
+ assert_equals(animation.startTime, null,
+ 'Start time should be unresolved');
+
+ const playTime = animation.timeline.currentTime;
+ animation.play();
+ assert_true(animation.pending, 'Animation should be play-pending');
+
+ await animation.ready;
+
+ assert_false(animation.pending, 'animation should no longer be pending');
+ assert_time_greater_than_equal(animation.startTime, playTime,
+ 'The start time of the playing animation should be set');
+}, 'Playing a canceled animation sets the start time');
+
+promise_test(async t => {
+ const animation = createDiv(t).animate(null, 100 * MS_PER_SEC);
+ animation.playbackRate = -1;
+ animation.cancel();
+ assert_equals(animation.startTime, null,
+ 'Start time should be unresolved');
+
+ const playTime = animation.timeline.currentTime;
+ animation.play();
+ assert_true(animation.pending, 'Animation should be play-pending');
+
+ await animation.ready;
+
+ assert_false(animation.pending, 'Animation should no longer be pending');
+ assert_time_greater_than_equal(animation.startTime, playTime + 100 * MS_PER_SEC,
+ 'The start time of the playing animation should be set');
+}, 'Playing a canceled animation backwards sets the start time');
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/web-animations/timing-model/animations/reverse-running-animation-ref.html b/testing/web-platform/tests/web-animations/timing-model/animations/reverse-running-animation-ref.html
new file mode 100644
index 0000000000..7bf5b03c1e
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/timing-model/animations/reverse-running-animation-ref.html
@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<title>Reference for reverse running animation</title>
+<style>
+ #box {
+ background: green;
+ height: 40px;
+ width: 40px;
+ }
+</style>
+<body>
+ <div id="box"></div>
+ <p id="notes">
+ This test animates the box color from green to red and reverses the play
+ direction shortly after the midpoint. If the box remains red, the test
+ failed.
+ </p>
+</body>
diff --git a/testing/web-platform/tests/web-animations/timing-model/animations/reverse-running-animation.html b/testing/web-platform/tests/web-animations/timing-model/animations/reverse-running-animation.html
new file mode 100644
index 0000000000..c9eb2a068e
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/timing-model/animations/reverse-running-animation.html
@@ -0,0 +1,50 @@
+
+<!DOCTYPE html>
+<html class="reftest-wait">
+<meta charset="UTF-8">
+<title>reverse running animation</title>
+<link rel="match" href="reverse-running-animation-ref.html">
+<script src="/common/reftest-wait.js"></script>
+<script src="../../testcommon.js"></script>
+<style>
+ #box {
+ background: green;
+ height: 40px;
+ width: 40px;
+ }
+</style>
+<body>
+ <div id="box"></div>
+ <p id="notes">
+ This test animates the box color from green to red and reverses the play
+ direction shortly after the midpoint. If the box remains red, the test
+ failed.
+ </p>
+</body>
+<script>
+ onload = async function() {
+ const box = document.getElementById('box');
+ const duration = 10000;
+ const anim =
+ box.animate({ bacground: [ 'green', 'red' ] },
+ { duration: duration, easing: 'steps(2, jump-none)' });
+ anim.currentTime = duration / 2;
+ anim.ready.then(() => {
+ const startTime = anim.timeline.currentTime;
+ waitForAnimationFrames(2).then(() => {
+ anim.reverse();
+ anim.ready.then(() => {
+ const reversalTime = anim.timeline.currentTime;
+ const forwardPlayingTime = reversalTime - startTime;
+ const checkIfDone = () => {
+ if (anim.timeline.currentTime - reversalTime > forwardPlayingTime)
+ takeScreenshot();
+ else
+ requestAnimationFrame(checkIfDone);
+ };
+ requestAnimationFrame(checkIfDone);
+ });
+ });
+ });
+ };
+</script>
diff --git a/testing/web-platform/tests/web-animations/timing-model/animations/reversing-an-animation.html b/testing/web-platform/tests/web-animations/timing-model/animations/reversing-an-animation.html
new file mode 100644
index 0000000000..8d869d72aa
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/timing-model/animations/reversing-an-animation.html
@@ -0,0 +1,266 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title>Reversing an animation</title>
+<link rel="help"
+ href="https://drafts.csswg.org/web-animations/#reversing-an-animation-section">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../../testcommon.js"></script>
+<body>
+<div id="log"></div>
+<script>
+'use strict';
+
+promise_test(async t => {
+ const div = createDiv(t);
+ const animation = div.animate({}, { duration: 100 * MS_PER_SEC,
+ iterations: Infinity });
+
+ await animation.ready;
+ // Wait a frame because if currentTime is still 0 when we call
+ // reverse(), it will throw (per spec).
+ await waitForAnimationFrames(1);
+
+ assert_greater_than_equal(animation.currentTime, 0,
+ 'currentTime expected to be greater than 0, one frame after starting');
+ animation.currentTime = 50 * MS_PER_SEC;
+ const previousPlaybackRate = animation.playbackRate;
+ animation.reverse();
+ assert_equals(animation.playbackRate, previousPlaybackRate,
+ 'Playback rate should not have changed');
+ await animation.ready;
+
+ assert_equals(animation.playbackRate, -previousPlaybackRate,
+ 'Playback rate should be inverted');
+}, 'Reversing an animation inverts the playback rate');
+
+promise_test(async t => {
+ const div = createDiv(t);
+ const animation = div.animate({}, { duration: 100 * MS_PER_SEC,
+ iterations: Infinity });
+ animation.currentTime = 50 * MS_PER_SEC;
+ animation.pause();
+
+ await animation.ready;
+
+ animation.reverse();
+ await animation.ready;
+
+ assert_equals(animation.playState, 'running',
+ 'Animation.playState should be "running" after reverse()');
+}, 'Reversing an animation plays a pausing animation');
+
+test(t => {
+ const div = createDiv(t);
+ const animation = div.animate({}, 100 * MS_PER_SEC);
+ animation.currentTime = 50 * MS_PER_SEC;
+ animation.reverse();
+
+ assert_equals(animation.currentTime, 50 * MS_PER_SEC,
+ 'The current time should not change it is in the middle of ' +
+ 'the animation duration');
+}, 'Reversing an animation maintains the same current time');
+
+test(t => {
+ const div = createDiv(t);
+ const animation = div.animate({}, { duration: 200 * MS_PER_SEC,
+ delay: -100 * MS_PER_SEC });
+ assert_true(animation.pending,
+ 'The animation is pending before we call reverse');
+
+ animation.reverse();
+
+ assert_true(animation.pending,
+ 'The animation is still pending after calling reverse');
+}, 'Reversing an animation does not cause it to leave the pending state');
+
+promise_test(async t => {
+ const div = createDiv(t);
+ const animation = div.animate({}, { duration: 200 * MS_PER_SEC,
+ delay: -100 * MS_PER_SEC });
+ let readyResolved = false;
+ animation.ready.then(() => { readyResolved = true; });
+
+ animation.reverse();
+
+ await Promise.resolve();
+ assert_false(readyResolved,
+ 'ready promise should not have been resolved yet');
+}, 'Reversing an animation does not cause it to resolve the ready promise');
+
+test(t => {
+ const div = createDiv(t);
+ const animation = div.animate({}, 100 * MS_PER_SEC);
+ animation.currentTime = 200 * MS_PER_SEC;
+ animation.reverse();
+
+ assert_equals(animation.currentTime, 100 * MS_PER_SEC,
+ 'reverse() should start playing from the animation effect end ' +
+ 'if the playbackRate > 0 and the currentTime > effect end');
+}, 'Reversing an animation when playbackRate > 0 and currentTime > ' +
+ 'effect end should make it play from the end');
+
+test(t => {
+ const div = createDiv(t);
+ const animation = div.animate({}, 100 * MS_PER_SEC);
+
+ animation.currentTime = -200 * MS_PER_SEC;
+ animation.reverse();
+
+ assert_equals(animation.currentTime, 100 * MS_PER_SEC,
+ 'reverse() should start playing from the animation effect end ' +
+ 'if the playbackRate > 0 and the currentTime < 0');
+}, 'Reversing an animation when playbackRate > 0 and currentTime < 0 ' +
+ 'should make it play from the end');
+
+test(t => {
+ const div = createDiv(t);
+ const animation = div.animate({}, 100 * MS_PER_SEC);
+ animation.playbackRate = -1;
+ animation.currentTime = -200 * MS_PER_SEC;
+ animation.reverse();
+
+ assert_equals(animation.currentTime, 0,
+ 'reverse() should start playing from the start of animation time ' +
+ 'if the playbackRate < 0 and the currentTime < 0');
+}, 'Reversing an animation when playbackRate < 0 and currentTime < 0 ' +
+ 'should make it play from the start');
+
+test(t => {
+ const div = createDiv(t);
+ const animation = div.animate({}, 100 * MS_PER_SEC);
+ animation.playbackRate = -1;
+ animation.currentTime = 200 * MS_PER_SEC;
+ animation.reverse();
+
+ assert_equals(animation.currentTime, 0,
+ 'reverse() should start playing from the start of animation time ' +
+ 'if the playbackRate < 0 and the currentTime > effect end');
+}, 'Reversing an animation when playbackRate < 0 and currentTime > effect ' +
+ 'end should make it play from the start');
+
+test(t => {
+ const div = createDiv(t);
+ const animation = div.animate({}, { duration: 100 * MS_PER_SEC,
+ iterations: Infinity });
+ animation.currentTime = -200 * MS_PER_SEC;
+
+ assert_throws_dom('InvalidStateError',
+ () => { animation.reverse(); },
+ 'reverse() should throw InvalidStateError ' +
+ 'if the playbackRate > 0 and the currentTime < 0 ' +
+ 'and the target effect is positive infinity');
+}, 'Reversing an animation when playbackRate > 0 and currentTime < 0 ' +
+ 'and the target effect end is positive infinity should throw an exception');
+
+promise_test(async t => {
+ const animation = createDiv(t).animate({}, { duration: 100 * MS_PER_SEC,
+ iterations: Infinity });
+ animation.currentTime = -200 * MS_PER_SEC;
+
+ try { animation.reverse(); } catch(e) { }
+
+ assert_equals(animation.playbackRate, 1, 'playbackRate is unchanged');
+
+ await animation.ready;
+ assert_equals(animation.playbackRate, 1, 'playbackRate remains unchanged');
+}, 'When reversing throws an exception, the playback rate remains unchanged');
+
+test(t => {
+ const div = createDiv(t);
+ const animation = div.animate({}, { duration: 100 * MS_PER_SEC,
+ iterations: Infinity });
+ animation.currentTime = -200 * MS_PER_SEC;
+ animation.playbackRate = 0;
+
+ try {
+ animation.reverse();
+ } catch (e) {
+ assert_unreached(`Unexpected exception when calling reverse(): ${e}`);
+ }
+}, 'Reversing animation when playbackRate = 0 and currentTime < 0 ' +
+ 'and the target effect end is positive infinity should NOT throw an ' +
+ 'exception');
+
+test(t => {
+ const div = createDiv(t);
+ const animation = div.animate({}, { duration: 100 * MS_PER_SEC,
+ iterations: Infinity });
+ animation.playbackRate = -1;
+ animation.currentTime = -200 * MS_PER_SEC;
+ animation.reverse();
+
+ assert_equals(animation.currentTime, 0,
+ 'reverse() should start playing from the start of animation time ' +
+ 'if the playbackRate < 0 and the currentTime < 0 ' +
+ 'and the target effect is positive infinity');
+}, 'Reversing an animation when playbackRate < 0 and currentTime < 0 ' +
+ 'and the target effect end is positive infinity should make it play ' +
+ 'from the start');
+
+promise_test(async t => {
+ const div = createDiv(t);
+ const animation = div.animate({}, 100 * MS_PER_SEC);
+ animation.playbackRate = 0;
+ animation.currentTime = 50 * MS_PER_SEC;
+ animation.reverse();
+
+ await animation.ready;
+ assert_equals(animation.playbackRate, 0,
+ 'reverse() should preserve playbackRate if the playbackRate == 0');
+ assert_equals(animation.currentTime, 50 * MS_PER_SEC,
+ 'reverse() should not affect the currentTime if the playbackRate == 0');
+}, 'Reversing when when playbackRate == 0 should preserve the current ' +
+ 'time and playback rate');
+
+test(t => {
+ const div = createDiv(t);
+ const animation =
+ new Animation(new KeyframeEffect(div, null, 100 * MS_PER_SEC));
+ assert_equals(animation.currentTime, null);
+
+ animation.reverse();
+
+ assert_equals(animation.currentTime, 100 * MS_PER_SEC,
+ 'animation.currentTime should be at its effect end');
+}, 'Reversing an idle animation from starts playing the animation');
+
+test(t => {
+ const div = createDiv(t);
+ const animation =
+ new Animation(new KeyframeEffect(div, null, 100 * MS_PER_SEC), null);
+
+ assert_throws_dom('InvalidStateError', () => { animation.reverse(); });
+}, 'Reversing an animation without an active timeline throws an ' +
+ 'InvalidStateError');
+
+promise_test(async t => {
+ const animation = createDiv(t).animate(null, 100 * MS_PER_SEC);
+ await animation.ready;
+
+ animation.updatePlaybackRate(2);
+ animation.reverse();
+
+ await animation.ready;
+ assert_equals(animation.playbackRate, -2);
+}, 'Reversing should use the negative pending playback rate');
+
+promise_test(async t => {
+ const animation = createDiv(t).animate(null, {
+ duration: 100 * MS_PER_SEC,
+ iterations: Infinity,
+ });
+ animation.currentTime = -200 * MS_PER_SEC;
+ await animation.ready;
+
+ animation.updatePlaybackRate(2);
+ assert_throws_dom('InvalidStateError', () => { animation.reverse(); });
+ assert_equals(animation.playbackRate, 1);
+
+ await animation.ready;
+ assert_equals(animation.playbackRate, 2);
+}, 'When reversing fails, it should restore any previous pending playback'
+ + ' rate');
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/web-animations/timing-model/animations/seamlessly-updating-the-playback-rate-of-an-animation.html b/testing/web-platform/tests/web-animations/timing-model/animations/seamlessly-updating-the-playback-rate-of-an-animation.html
new file mode 100644
index 0000000000..dffbeabd59
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/timing-model/animations/seamlessly-updating-the-playback-rate-of-an-animation.html
@@ -0,0 +1,171 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>Seamlessly updating the playback rate of an animation</title>
+<link rel="help"
+ href="https://drafts.csswg.org/web-animations-1/#seamlessly-updating-the-playback-rate-of-an-animation">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../../testcommon.js"></script>
+<body>
+<div id="log"></div>
+<script>
+'use strict';
+
+promise_test(async t => {
+ const animation = createDiv(t).animate(null, 100 * MS_PER_SEC);
+ await animation.ready;
+
+ animation.currentTime = 50 * MS_PER_SEC;
+
+ animation.updatePlaybackRate(0.5);
+ await animation.ready;
+ // Since the animation is in motion (and we want to test it while it is in
+ // motion!) we can't assert that the current time == 50s but we can check
+ // that the current time is NOT re-calculated by simply substituting in the
+ // new playback rate (i.e. without adjusting the start time). If that were
+ // the case the currentTime would jump to 25s. So we just test the currentTime
+ // hasn't gone backwards.
+ assert_greater_than_equal(animation.currentTime, 50 * MS_PER_SEC,
+ 'Reducing the playback rate should not change the current time ' +
+ 'of a playing animation');
+
+ animation.updatePlaybackRate(2);
+ await animation.ready;
+ // Likewise, we test here that the current time does not jump to 100s as it
+ // would if we naively applied a playbackRate of 2 without adjusting the
+ // startTime.
+ assert_less_than(animation.currentTime, 100 * MS_PER_SEC,
+ 'Increasing the playback rate should not change the current time ' +
+ 'of a playing animation');
+}, 'Updating the playback rate maintains the current time');
+
+promise_test(async t => {
+ const animation = createDiv(t).animate(null, 100 * MS_PER_SEC);
+ await animation.ready;
+
+ assert_false(animation.pending);
+ animation.updatePlaybackRate(2);
+ assert_true(animation.pending);
+}, 'Updating the playback rate while running makes the animation pending');
+
+promise_test(async t => {
+ const animation = createDiv(t).animate(null, 100 * MS_PER_SEC);
+ animation.currentTime = 50 * MS_PER_SEC;
+ assert_true(animation.pending);
+
+ animation.updatePlaybackRate(0.5);
+
+ // Check that the hold time is updated as expected
+ assert_time_equals_literal(animation.currentTime, 50 * MS_PER_SEC);
+
+ await animation.ready;
+
+ // As above, check that the currentTime is not calculated by simply
+ // substituting in the updated playbackRate without updating the startTime.
+ assert_greater_than_equal(animation.currentTime, 50 * MS_PER_SEC,
+ 'Reducing the playback rate should not change the current time ' +
+ 'of a play-pending animation');
+}, 'Updating the playback rate on a play-pending animation maintains'
+ + ' the current time');
+
+promise_test(async t => {
+ const animation = createDiv(t).animate(null, 100 * MS_PER_SEC);
+ animation.currentTime = 50 * MS_PER_SEC;
+ await animation.ready;
+
+ animation.pause();
+ animation.updatePlaybackRate(0.5);
+
+ assert_greater_than_equal(animation.currentTime, 50 * MS_PER_SEC);
+}, 'Updating the playback rate on a pause-pending animation maintains'
+ + ' the current time');
+
+promise_test(async t => {
+ const animation = createDiv(t).animate(null, 100 * MS_PER_SEC);
+
+ animation.updatePlaybackRate(2);
+ animation.updatePlaybackRate(3);
+ animation.updatePlaybackRate(4);
+
+ assert_equals(animation.playbackRate, 1);
+ await animation.ready;
+
+ assert_equals(animation.playbackRate, 4);
+}, 'If a pending playback rate is set multiple times, the latest wins');
+
+test(t => {
+ const animation = createDiv(t).animate(null, 100 * MS_PER_SEC);
+ animation.cancel();
+
+ animation.updatePlaybackRate(2);
+ assert_equals(animation.playbackRate, 2);
+ assert_false(animation.pending);
+}, 'In the idle state, the playback rate is applied immediately');
+
+promise_test(async t => {
+ const animation = createDiv(t).animate(null, 100 * MS_PER_SEC);
+ animation.pause();
+ await animation.ready;
+
+ animation.updatePlaybackRate(2);
+ assert_equals(animation.playbackRate, 2);
+ assert_false(animation.pending);
+}, 'In the paused state, the playback rate is applied immediately');
+
+promise_test(async t => {
+ const animation = createDiv(t).animate(null, 100 * MS_PER_SEC);
+ animation.finish();
+ assert_time_equals_literal(animation.currentTime, 100 * MS_PER_SEC);
+ assert_false(animation.pending);
+
+ animation.updatePlaybackRate(2);
+ assert_equals(animation.playbackRate, 2);
+ assert_time_equals_literal(animation.currentTime, 100 * MS_PER_SEC);
+ assert_false(animation.pending);
+}, 'Updating the playback rate on a finished animation maintains'
+ + ' the current time');
+
+promise_test(async t => {
+ const animation = createDiv(t).animate(null, 100 * MS_PER_SEC);
+ animation.finish();
+ assert_time_equals_literal(animation.currentTime, 100 * MS_PER_SEC);
+ assert_false(animation.pending);
+
+ animation.updatePlaybackRate(0);
+ assert_equals(animation.playbackRate, 0);
+ assert_time_equals_literal(animation.currentTime, 100 * MS_PER_SEC);
+ assert_false(animation.pending);
+}, 'Updating the playback rate to zero on a finished animation maintains'
+ + ' the current time');
+
+promise_test(async t => {
+ const animation = createDiv(t).animate(null, 100 * MS_PER_SEC);
+ await animation.ready;
+
+ // Get the animation in a state where it has an unresolved current time,
+ // a resolved start time (so it is not 'idle') and but no pending play task.
+ animation.timeline = null;
+ animation.startTime = 0;
+ assert_equals(animation.currentTime, null);
+ assert_equals(animation.playState, 'running');
+
+ // Make the effect end infinite.
+ animation.effect.updateTiming({ endDelay: 1e38 });
+
+ // Now we want to check that when we go to set a negative playback rate we
+ // don't end up throwing an InvalidStateError (which would happen if we ended
+ // up applying the auto-rewind behavior).
+ animation.updatePlaybackRate(-1);
+
+ // Furthermore, we should apply the playback rate immediately since the
+ // current time is unresolved.
+ assert_equals(animation.playbackRate, -1,
+ 'We apply the pending playback rate immediately if the current time is ' +
+ 'unresolved');
+ assert_false(animation.pending);
+}, 'Updating the negative playback rate with the unresolved current time and'
+ + ' a positive infinite associated effect end should not throw an'
+ + ' exception');
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/web-animations/timing-model/animations/setting-the-current-time-of-an-animation.html b/testing/web-platform/tests/web-animations/timing-model/animations/setting-the-current-time-of-an-animation.html
new file mode 100644
index 0000000000..809877345f
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/timing-model/animations/setting-the-current-time-of-an-animation.html
@@ -0,0 +1,167 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>Setting the current time of an animation</title>
+<link rel="help"
+ href="https://drafts.csswg.org/web-animations-1/#setting-the-current-time-of-an-animation">
+<script src='/resources/testharness.js'></script>
+<script src='/resources/testharnessreport.js'></script>
+<script src='../../testcommon.js'></script>
+<body>
+<div id='log'></div>
+<script>
+'use strict';
+
+test(t => {
+ const anim = new Animation();
+ assert_equals(anim.playState, 'idle');
+ assert_equals(anim.currentTime, null);
+
+ // This should not throw because the currentTime is already null.
+ anim.currentTime = null;
+}, 'Setting the current time of a pending animation to unresolved does not'
+ + ' throw a TypeError');
+
+promise_test(async t => {
+ const anim = createDiv(t).animate(null, 100 * MS_PER_SEC);
+ await anim.ready;
+
+ assert_greater_than_equal(anim.currentTime, 0);
+ assert_throws_js(TypeError, () => {
+ anim.currentTime = null;
+ });
+}, 'Setting the current time of a playing animation to unresolved throws a'
+ + ' TypeError');
+
+promise_test(async t => {
+ const anim = createDiv(t).animate(null, 100 * MS_PER_SEC);
+ await anim.ready;
+ anim.pause();
+
+ assert_greater_than_equal(anim.currentTime, 0);
+ assert_throws_js(TypeError, () => {
+ anim.currentTime = null;
+ });
+}, 'Setting the current time of a paused animation to unresolved throws a'
+ + ' TypeError');
+
+
+promise_test(async t => {
+ const animation = createDiv(t).animate(null, 100 * MS_PER_SEC);
+
+ assert_throws_js(TypeError, () => {
+ animation.currentTime = CSSNumericValue.parse("30%");
+ });
+ assert_throws_js(TypeError, () => {
+ animation.currentTime = CSSNumericValue.parse("30deg");
+ });
+
+ animation.currentTime = 2000;
+ assert_equals(animation.currentTime, 2000, "Set current time using double");
+
+ animation.currentTime = CSSNumericValue.parse("3000");
+ assert_equals(animation.currentTime, 3000, "Set current time using " +
+ "CSSNumericValue number value");
+
+ animation.currentTime = CSSNumericValue.parse("4000ms");
+ assert_equals(animation.currentTime, 4000, "Set current time using " +
+ "CSSNumericValue milliseconds value");
+
+ animation.currentTime = CSSNumericValue.parse("50s");
+ assert_equals(animation.currentTime, 50000, "Set current time using " +
+ "CSSNumericValue seconds value");
+}, 'Validate different value types that can be used to set current time');
+
+promise_test(async t => {
+ const anim = createDiv(t).animate(null, 100 * MS_PER_SEC);
+ await anim.ready;
+ anim.pause();
+
+ // We should be pause-pending now
+ assert_true(anim.pending);
+ assert_equals(anim.playState, 'paused');
+
+ // Apply a pending playback rate
+ anim.updatePlaybackRate(2);
+ assert_equals(anim.playbackRate, 1);
+
+ // Setting the current time should apply the pending playback rate
+ anim.currentTime = 50 * MS_PER_SEC;
+ assert_equals(anim.playbackRate, 2);
+ assert_false(anim.pending);
+
+ // Sanity check that the current time is preserved
+ assert_time_equals_literal(anim.currentTime, 50 * MS_PER_SEC);
+}, 'Setting the current time of a pausing animation applies a pending playback'
+ + ' rate');
+
+
+// The following tests verify that currentTime can be set outside of the normal
+// bounds of an animation.
+
+promise_test(async t => {
+ const anim = createDiv(t).animate(null, 100 * MS_PER_SEC);
+ await anim.ready;
+
+ anim.currentTime = 200 * MS_PER_SEC;
+ assert_equals(anim.playState, 'finished');
+ assert_time_equals_literal(anim.currentTime, 200 * MS_PER_SEC);
+}, 'Setting the current time after the end with a positive playback rate');
+
+promise_test(async t => {
+ const anim = createDiv(t).animate(null, 100 * MS_PER_SEC);
+ await anim.ready;
+
+ anim.currentTime = -100 * MS_PER_SEC;
+ assert_equals(anim.playState, 'running');
+ assert_time_equals_literal(anim.currentTime, -100 * MS_PER_SEC);
+
+ await waitForAnimationFrames(2);
+ assert_greater_than(anim.currentTime, -100 * MS_PER_SEC);
+}, 'Setting a negative current time with a positive playback rate');
+
+promise_test(async t => {
+ const anim = createDiv(t).animate(null, 100 * MS_PER_SEC);
+ anim.updatePlaybackRate(-1);
+ await anim.ready;
+
+ anim.currentTime = 200 * MS_PER_SEC;
+ assert_equals(anim.playState, 'running');
+ assert_time_equals_literal(anim.currentTime, 200 * MS_PER_SEC);
+
+ await waitForAnimationFrames(2);
+ assert_less_than(anim.currentTime, 200 * MS_PER_SEC);
+}, 'Setting the current time after the end with a negative playback rate');
+
+promise_test(async t => {
+ const anim = createDiv(t).animate(null, 100 * MS_PER_SEC);
+ anim.updatePlaybackRate(-1);
+ await anim.ready;
+
+ anim.currentTime = -100 * MS_PER_SEC;
+ assert_equals(anim.playState, 'finished');
+ assert_time_equals_literal(anim.currentTime, -100 * MS_PER_SEC);
+}, 'Setting a negative current time with a negative playback rate');
+
+promise_test(async t => {
+ const anim = createDiv(t).animate(null, 100 * MS_PER_SEC);
+ anim.updatePlaybackRate(0);
+ await anim.ready;
+
+ // An animation with a playback rate of zero is never in the finished state
+ // even if currentTime is outside the normal range of [0, effect end].
+ anim.currentTime = 200 * MS_PER_SEC;
+ assert_equals(anim.playState, 'running');
+ assert_time_equals_literal(anim.currentTime, 200 * MS_PER_SEC);
+ await waitForAnimationFrames(2);
+ assert_time_equals_literal(anim.currentTime, 200 * MS_PER_SEC);
+
+ anim.currentTime = -200 * MS_PER_SEC;
+ assert_equals(anim.playState, 'running');
+ assert_time_equals_literal(anim.currentTime, -200 * MS_PER_SEC);
+ await waitForAnimationFrames(2);
+ assert_time_equals_literal(anim.currentTime, -200 * MS_PER_SEC);
+
+}, 'Setting the current time on an animation with a zero playback rate');
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/web-animations/timing-model/animations/setting-the-playback-rate-of-an-animation.html b/testing/web-platform/tests/web-animations/timing-model/animations/setting-the-playback-rate-of-an-animation.html
new file mode 100644
index 0000000000..a1f9e4f3ac
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/timing-model/animations/setting-the-playback-rate-of-an-animation.html
@@ -0,0 +1,110 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title>Setting the playback rate of an animation</title>
+<link rel="help" href="https://drafts.csswg.org/web-animations/#setting-the-playback-rate-of-an-animation">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../../testcommon.js"></script>
+<body>
+<div id="log"></div>
+<script>
+'use strict';
+
+promise_test(async t => {
+ const animation = createDiv(t).animate(null, 100 * MS_PER_SEC);
+ animation.playbackRate = 2;
+ await animation.ready;
+
+ const previousAnimationCurrentTime = animation.currentTime;
+ const previousTimelineCurrentTime = animation.timeline.currentTime;
+
+ await waitForAnimationFrames(1);
+
+ const animationCurrentTimeDifference =
+ animation.currentTime - previousAnimationCurrentTime;
+ const timelineCurrentTimeDifference =
+ animation.timeline.currentTime - previousTimelineCurrentTime;
+
+ assert_times_equal(
+ animationCurrentTimeDifference,
+ timelineCurrentTimeDifference * animation.playbackRate,
+ 'The current time should increase two times faster than timeline'
+ );
+}, 'The playback rate affects the rate of progress of the current time');
+
+test(t => {
+ const animation = createDiv(t).animate(null, 100 * MS_PER_SEC);
+ animation.currentTime = 50 * MS_PER_SEC;
+ animation.playbackRate = 2;
+ assert_time_equals_literal(animation.currentTime, 50 * MS_PER_SEC);
+}, 'Setting the playback rate while play-pending preserves the current time');
+
+promise_test(async t => {
+ const animation = createDiv(t).animate(null, 100 * MS_PER_SEC);
+ animation.currentTime = 50 * MS_PER_SEC;
+ await animation.ready;
+ animation.playbackRate = 2;
+ assert_greater_than_equal(animation.currentTime, 50 * MS_PER_SEC);
+ assert_less_than(animation.currentTime, 100 * MS_PER_SEC);
+}, 'Setting the playback rate while playing preserves the current time');
+
+promise_test(async t => {
+ const animation = createDiv(t).animate(null, 100 * MS_PER_SEC);
+ animation.currentTime = 50 * MS_PER_SEC;
+ animation.updatePlaybackRate(2);
+ animation.playbackRate = 1;
+ await animation.ready;
+ assert_equals(animation.playbackRate, 1);
+}, 'Setting the playback rate should clear any pending playback rate');
+
+promise_test(async t => {
+ const animation = createDiv(t).animate(null, 100 * MS_PER_SEC);
+ animation.currentTime = 50 * MS_PER_SEC;
+ animation.pause();
+ await animation.ready;
+ animation.playbackRate = 2;
+ // Ensure that the animation remains paused and current time is preserved.
+ assert_equals(animation.playState, 'paused');
+ assert_time_equals_literal(animation.currentTime, 50 * MS_PER_SEC);
+}, 'Setting the playback rate while paused preserves the current time and '
+ + 'state');
+
+promise_test(async t => {
+ const animation = createDiv(t).animate(null, 100 * MS_PER_SEC);
+ animation.currentTime = 150 * MS_PER_SEC;
+ await animation.ready;
+ animation.playbackRate = 2;
+ // Ensure that current time is preserved and does not snap to the effect end
+ // time.
+ assert_equals(animation.playState, 'finished');
+ assert_time_equals_literal(animation.currentTime, 150 * MS_PER_SEC);
+}, 'Setting the playback rate while finished preserves the current time');
+
+promise_test(async t => {
+ const animation = createDiv(t).animate(null, 100 * MS_PER_SEC);
+ animation.currentTime = 150 * MS_PER_SEC;
+ await animation.ready;
+ assert_equals(animation.playState, 'finished');
+ animation.playbackRate = -1;
+ // Ensure that current time does not snap to the effect end time and that the
+ // animation resumes playing.
+ assert_equals(animation.playState, 'running');
+ assert_time_equals_literal(animation.currentTime, 150 * MS_PER_SEC);
+ await waitForAnimationFrames(2);
+ assert_less_than(animation.currentTime, 150 * MS_PER_SEC);
+}, 'Reversing the playback rate while finished restarts the animation');
+
+
+promise_test(async t => {
+ const animation = createDiv(t).animate(null, 100 * MS_PER_SEC);
+ await animation.ready;
+ animation.currentTime = 50 * MS_PER_SEC;
+ animation.playbackRate = 0;
+ // Ensure that current time does not drift.
+ assert_equals(animation.playState, 'running');
+ await waitForAnimationFrames(2);
+ assert_time_equals_literal(animation.currentTime, 50 * MS_PER_SEC);
+}, 'Setting a zero playback rate while running preserves the current time');
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/web-animations/timing-model/animations/setting-the-start-time-of-an-animation.html b/testing/web-platform/tests/web-animations/timing-model/animations/setting-the-start-time-of-an-animation.html
new file mode 100644
index 0000000000..fee3f1e0de
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/timing-model/animations/setting-the-start-time-of-an-animation.html
@@ -0,0 +1,329 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title>Setting the start time of an animation</title>
+<link rel="help" href="https://drafts.csswg.org/web-animations/#setting-the-start-time-of-an-animation">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../../testcommon.js"></script>
+<script src="../../resources/timing-override.js"></script>
+<body>
+<div id="log"></div>
+<script>
+'use strict';
+
+promise_test(async t => {
+ const animation =
+ new Animation(new KeyframeEffect(createDiv(t), null, 100 * MS_PER_SEC),
+ null);
+
+ assert_throws_js(TypeError, () => {
+ animation.startTime = CSSNumericValue.parse("30%");
+ });
+ assert_throws_js(TypeError, () => {
+ animation.startTime = CSSNumericValue.parse("30deg");
+ });
+
+ animation.startTime = 2000;
+ assert_equals(animation.startTime, 2000, "Set start time using double");
+
+ animation.startTime = CSSNumericValue.parse("3000");
+ assert_equals(animation.startTime, 3000, "Set start time using " +
+ "CSSNumericValue number value");
+
+ animation.startTime = CSSNumericValue.parse("4000ms");
+ assert_equals(animation.startTime, 4000, "Set start time using " +
+ "CSSNumericValue milliseconds value");
+
+ animation.startTime = CSSNumericValue.parse("50s");
+ assert_equals(animation.startTime, 50000, "Set start time using " +
+ "CSSNumericValue seconds value");
+}, 'Validate different value types that can be used to set start time');
+
+test(t => {
+ // It should only be possible to set *either* the start time or the current
+ // time for an animation that does not have an active timeline.
+
+ const animation =
+ new Animation(new KeyframeEffect(createDiv(t), null, 100 * MS_PER_SEC),
+ null);
+
+ assert_equals(animation.currentTime, null, 'Intial current time');
+ assert_equals(animation.startTime, null, 'Intial start time');
+
+ animation.currentTime = 1000;
+ assert_equals(animation.currentTime, 1000,
+ 'Setting the current time succeeds');
+ assert_equals(animation.startTime, null,
+ 'Start time remains null after setting current time');
+
+ animation.startTime = 1000;
+ assert_equals(animation.startTime, 1000,
+ 'Setting the start time succeeds');
+ assert_equals(animation.currentTime, null,
+ 'Setting the start time clears the current time');
+
+ animation.startTime = null;
+ assert_equals(animation.startTime, null,
+ 'Setting the start time to an unresolved time succeeds');
+ assert_equals(animation.currentTime, null, 'The current time is unaffected');
+
+}, 'Setting the start time of an animation without an active timeline');
+
+test(t => {
+ // Setting an unresolved start time on an animation without an active
+ // timeline should not clear the current time.
+
+ const animation =
+ new Animation(new KeyframeEffect(createDiv(t), null, 100 * MS_PER_SEC),
+ null);
+
+ assert_equals(animation.currentTime, null, 'Intial current time');
+ assert_equals(animation.startTime, null, 'Intial start time');
+
+ animation.currentTime = 1000;
+ assert_equals(animation.currentTime, 1000,
+ 'Setting the current time succeeds');
+ assert_equals(animation.startTime, null,
+ 'Start time remains null after setting current time');
+
+ animation.startTime = null;
+ assert_equals(animation.startTime, null, 'Start time remains unresolved');
+ assert_equals(animation.currentTime, 1000, 'Current time is unaffected');
+
+}, 'Setting an unresolved start time an animation without an active timeline'
+ + ' does not clear the current time');
+
+test(t => {
+ const animation =
+ new Animation(new KeyframeEffect(createDiv(t), null, 100 * MS_PER_SEC),
+ document.timeline);
+
+ // So long as a hold time is set, querying the current time will return
+ // the hold time.
+
+ // Since the start time is unresolved at this point, setting the current time
+ // will set the hold time
+ animation.currentTime = 1000;
+ assert_equals(animation.currentTime, 1000,
+ 'The current time is calculated from the hold time');
+
+ // If we set the start time, however, we should clear the hold time.
+ animation.startTime = document.timeline.currentTime - 2000;
+ assert_time_equals_literal(animation.currentTime, 2000,
+ 'The current time is calculated from the start'
+ + ' time, not the hold time');
+
+ // Sanity check
+ assert_equals(animation.playState, 'running',
+ 'Animation reports it is running after setting a resolved'
+ + ' start time');
+}, 'Setting the start time clears the hold time');
+
+test(t => {
+ const animation =
+ new Animation(new KeyframeEffect(createDiv(t), null, 100 * MS_PER_SEC),
+ document.timeline);
+
+ // Set up a running animation (i.e. both start time and current time
+ // are resolved).
+ animation.startTime = document.timeline.currentTime - 1000;
+ assert_equals(animation.playState, 'running');
+ assert_time_equals_literal(animation.currentTime, 1000,
+ 'Current time is resolved for a running animation');
+
+ // Clear start time
+ animation.startTime = null;
+ assert_time_equals_literal(animation.currentTime, 1000,
+ 'Hold time is set after start time is made'
+ + ' unresolved');
+ assert_equals(animation.playState, 'paused',
+ 'Animation reports it is paused after setting an unresolved'
+ + ' start time');
+}, 'Setting an unresolved start time sets the hold time');
+
+promise_test(async t => {
+ const animation =
+ new Animation(new KeyframeEffect(createDiv(t), null, 100 * MS_PER_SEC),
+ document.timeline);
+
+ let readyPromiseCallbackCalled = false;
+ animation.ready.then(() => { readyPromiseCallbackCalled = true; } );
+
+ // Put the animation in the play-pending state
+ animation.play();
+
+ // Sanity check
+ assert_true(animation.pending && animation.playState === 'running',
+ 'Animation is in play-pending state');
+
+ // Setting the start time should resolve the 'ready' promise, i.e.
+ // it should schedule a microtask to run the promise callbacks.
+ animation.startTime = document.timeline.currentTime;
+ assert_false(readyPromiseCallbackCalled,
+ 'Ready promise callback is not called synchronously');
+
+ // If we schedule another microtask then it should run immediately after
+ // the ready promise resolution microtask.
+ await Promise.resolve();
+ assert_true(readyPromiseCallbackCalled,
+ 'Ready promise callback called after setting startTime');
+}, 'Setting the start time resolves a pending ready promise');
+
+promise_test(async t => {
+ const animation =
+ new Animation(new KeyframeEffect(createDiv(t), null, 100 * MS_PER_SEC),
+ document.timeline);
+
+ let readyPromiseCallbackCalled = false;
+ animation.ready.then(() => { readyPromiseCallbackCalled = true; } );
+
+ // Put the animation in the pause-pending state
+ animation.startTime = document.timeline.currentTime;
+ animation.pause();
+
+ // Sanity check
+ assert_true(animation.pending && animation.playState === 'paused',
+ 'Animation is in pause-pending state');
+
+ // Setting the start time should resolve the 'ready' promise although
+ // the resolution callbacks when be run in a separate microtask.
+ animation.startTime = null;
+ assert_false(readyPromiseCallbackCalled,
+ 'Ready promise callback is not called synchronously');
+
+ await Promise.resolve();
+ assert_true(readyPromiseCallbackCalled,
+ 'Ready promise callback called after setting startTime');
+}, 'Setting the start time resolves a pending pause task');
+
+promise_test(async t => {
+ const animation =
+ new Animation(new KeyframeEffect(createDiv(t), null, 100 * MS_PER_SEC),
+ document.timeline);
+
+ // Put the animation in the play-pending state
+ animation.play();
+
+ // Sanity check
+ assert_true(animation.pending, 'Animation is pending');
+ assert_equals(animation.playState, 'running',
+ 'Animation is play-pending');
+ assert_equals(animation.startTime, null, 'Start time is null');
+
+ // Even though the startTime is already null, setting it to the same value
+ // should still cancel the pending task.
+ animation.startTime = null;
+ assert_false(animation.pending, 'Animation is no longer pending');
+ assert_equals(animation.playState, 'paused', 'Animation is paused');
+}, 'Setting an unresolved start time on a play-pending animation makes it'
+ + ' paused');
+
+promise_test(async t => {
+ const animation =
+ new Animation(new KeyframeEffect(createDiv(t), null, 100 * MS_PER_SEC),
+ document.timeline);
+
+ // Set start time such that the current time is past the end time
+ animation.startTime = document.timeline.currentTime
+ - 110 * MS_PER_SEC;
+ assert_equals(animation.playState, 'finished',
+ 'Seeked to finished state using the startTime');
+
+ // If the 'did seek' flag is true, the current time should be greater than
+ // the effect end.
+ assert_greater_than(animation.currentTime,
+ animation.effect.getComputedTiming().endTime,
+ 'Setting the start time updated the finished state with'
+ + ' the \'did seek\' flag set to true');
+
+ // Furthermore, that time should persist if we have correctly updated
+ // the hold time
+ const finishedCurrentTime = animation.currentTime;
+ await waitForAnimationFrames(1);
+ assert_equals(animation.currentTime, finishedCurrentTime,
+ 'Current time does not change after seeking past the effect'
+ + ' end time by setting the current time');
+}, 'Setting the start time updates the finished state');
+
+promise_test(async t => {
+ const anim = createDiv(t).animate(null, 100 * MS_PER_SEC);
+
+ // We should be play-pending now
+ assert_true(anim.pending);
+ assert_equals(anim.playState, 'running');
+
+ // Apply a pending playback rate
+ anim.updatePlaybackRate(2);
+ assert_equals(anim.playbackRate, 1);
+ assert_true(anim.pending);
+
+ // Setting the start time should apply the pending playback rate
+ anim.startTime = anim.timeline.currentTime - 25 * MS_PER_SEC;
+ assert_equals(anim.playbackRate, 2);
+ assert_false(anim.pending);
+
+ // Sanity check that the start time is preserved and current time is
+ // calculated using the new playback rate
+ assert_times_equal(anim.startTime,
+ anim.timeline.currentTime - 25 * MS_PER_SEC);
+ assert_time_equals_literal(anim.currentTime, 50 * MS_PER_SEC);
+}, 'Setting the start time of a play-pending animation applies a pending playback rate');
+
+promise_test(async t => {
+ const anim = createDiv(t).animate(null, 100 * MS_PER_SEC);
+ await anim.ready;
+
+ // We should be running now
+ assert_false(anim.pending);
+ assert_equals(anim.playState, 'running');
+
+ // Apply a pending playback rate
+ anim.updatePlaybackRate(2);
+ assert_equals(anim.playbackRate, 1);
+ assert_true(anim.pending);
+
+ // Setting the start time should apply the pending playback rate
+ anim.startTime = anim.timeline.currentTime - 25 * MS_PER_SEC;
+ assert_equals(anim.playbackRate, 2);
+ assert_false(anim.pending);
+
+ // Sanity check that the start time is preserved and current time is
+ // calculated using the new playback rate
+ assert_times_equal(anim.startTime,
+ anim.timeline.currentTime - 25 * MS_PER_SEC);
+ assert_time_equals_literal(parseInt(anim.currentTime.toPrecision(5), 10), 50 * MS_PER_SEC);
+}, 'Setting the start time of a playing animation applies a pending playback rate');
+
+promise_test(async t => {
+ const anim = createDiv(t).animate(null, 100 * MS_PER_SEC);
+ await anim.ready;
+ assert_equals(anim.playState, 'running');
+
+ // Setting the start time updates the finished state. The hold time is not
+ // constrained by the effect end time.
+ anim.startTime = -200 * MS_PER_SEC;
+ assert_equals(anim.playState, 'finished');
+
+ assert_times_equal(anim.currentTime,
+ document.timeline.currentTime + 200 * MS_PER_SEC);
+}, 'Setting the start time on a running animation updates the play state');
+
+promise_test(async t => {
+ const anim = createDiv(t).animate(null, 100 * MS_PER_SEC);
+ await anim.ready;
+
+ // Setting the start time updates the finished state. The hold time is not
+ // constrained by the normal range of the animation time.
+ anim.currentTime = 100 * MS_PER_SEC;
+ assert_equals(anim.playState, 'finished');
+ anim.playbackRate = -1;
+ assert_equals(anim.playState, 'running');
+ anim.startTime = -200 * MS_PER_SEC;
+ assert_equals(anim.playState, 'finished');
+ assert_times_equal(anim.currentTime,
+ -document.timeline.currentTime - 200 * MS_PER_SEC);
+}, 'Setting the start time on a reverse running animation updates the play '
+ + 'state');
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/web-animations/timing-model/animations/setting-the-target-effect-of-an-animation.html b/testing/web-platform/tests/web-animations/timing-model/animations/setting-the-target-effect-of-an-animation.html
new file mode 100644
index 0000000000..60ea1850fc
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/timing-model/animations/setting-the-target-effect-of-an-animation.html
@@ -0,0 +1,129 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title>Setting the target effect of an animation</title>
+<link rel='help' href='https://drafts.csswg.org/web-animations/#setting-the-target-effect'>
+<script src='/resources/testharness.js'></script>
+<script src='/resources/testharnessreport.js'></script>
+<script src='../../testcommon.js'></script>
+<body>
+<div id='log'></div>
+<script>
+'use strict';
+
+promise_test(t => {
+ const anim = createDiv(t).animate({ marginLeft: [ '0px', '100px' ] },
+ 100 * MS_PER_SEC);
+ assert_true(anim.pending);
+
+ const originalReadyPromise = anim.ready.catch(err => {
+ assert_unreached('Original ready promise should not be rejected');
+ });
+
+ anim.effect = null;
+ assert_equals(anim.playState, 'finished');
+ assert_true(anim.pending);
+
+ return originalReadyPromise;
+}, 'If new effect is null and old effect is not null the animation becomes'
+ + ' finish-pending');
+
+promise_test(async t => {
+ const anim = new Animation();
+ anim.pause();
+ assert_true(anim.pending);
+
+ anim.effect = new KeyframeEffect(createDiv(t),
+ { marginLeft: [ '0px', '100px' ] },
+ 100 * MS_PER_SEC);
+ assert_true(anim.pending);
+ await anim.ready;
+
+ assert_false(anim.pending);
+ assert_equals(anim.playState, 'paused');
+}, 'If animation has a pending pause task, reschedule that task to run ' +
+ 'as soon as animation is ready.');
+
+promise_test(async t => {
+ const anim = new Animation();
+ anim.play();
+ assert_true(anim.pending);
+
+ anim.effect = new KeyframeEffect(createDiv(t),
+ { marginLeft: [ '0px', '100px' ] },
+ 100 * MS_PER_SEC);
+ assert_true(anim.pending);
+ await anim.ready;
+
+ assert_false(anim.pending);
+ assert_equals(anim.playState, 'running');
+}, 'If animation has a pending play task, reschedule that task to run ' +
+ 'as soon as animation is ready to play new effect.');
+
+promise_test(async t => {
+ const anim = createDiv(t).animate({ marginLeft: [ '0px', '100px' ] },
+ 100 * MS_PER_SEC);
+ assert_equals(anim.playState, 'running');
+ assert_true(anim.pending);
+
+ const originalEffect = anim.effect;
+ const originalReadyPromise = anim.ready;
+
+ anim.effect = null;
+ assert_equals(anim.playState, 'finished');
+ assert_true(anim.pending);
+
+ anim.effect = originalEffect;
+ assert_equals(anim.playState, 'running');
+ assert_true(anim.pending);
+
+ await originalReadyPromise;
+
+ assert_equals(anim.playState, 'running');
+ assert_false(anim.pending);
+}, 'The pending play task should be rescheduled even after temporarily setting'
+ + ' the effect to null');
+
+promise_test(async t => {
+ const animA = createDiv(t).animate({ marginLeft: [ '0px', '100px' ] },
+ 100 * MS_PER_SEC);
+ const animB = new Animation();
+
+ await animA.ready;
+
+ animB.effect = animA.effect;
+ assert_equals(animA.effect, null);
+ assert_equals(animA.playState, 'finished');
+}, 'When setting the effect of an animation to the effect of an existing ' +
+ 'animation, the existing animation\'s target effect should be set to null.');
+
+test(t => {
+ const animA = createDiv(t).animate({ marginLeft: [ '0px', '100px' ] },
+ 100 * MS_PER_SEC);
+ const animB = new Animation();
+ const effect = animA.effect;
+ animA.currentTime = 50 * MS_PER_SEC;
+ animB.currentTime = 20 * MS_PER_SEC;
+ assert_equals(effect.getComputedTiming().progress, 0.5,
+ 'Original timing comes from first animation');
+ animB.effect = effect;
+ assert_equals(effect.getComputedTiming().progress, 0.2,
+ 'After setting the effect on a different animation, ' +
+ 'it uses the new animation\'s timing');
+}, 'After setting the target effect of animation to the target effect of an ' +
+ 'existing animation, the target effect\'s timing is updated to reflect ' +
+ 'the current time of the new animation.');
+
+promise_test(async t => {
+ const anim = createDiv(t).animate(null, 100 * MS_PER_SEC);
+ anim.updatePlaybackRate(2);
+ assert_equals(anim.playbackRate, 1);
+
+ anim.effect = null;
+ await anim.ready;
+
+ assert_equals(anim.playbackRate, 2);
+}, 'Setting the target effect to null causes a pending playback rate to be'
+ + ' applied');
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/web-animations/timing-model/animations/setting-the-timeline-of-an-animation.html b/testing/web-platform/tests/web-animations/timing-model/animations/setting-the-timeline-of-an-animation.html
new file mode 100644
index 0000000000..d4f802152c
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/timing-model/animations/setting-the-timeline-of-an-animation.html
@@ -0,0 +1,255 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title>Setting the timeline of an animation</title>
+<link rel="help" href="https://drafts.csswg.org/web-animations/#setting-the-timeline">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../../testcommon.js"></script>
+<body>
+<div id="log"></div>
+<script>
+'use strict';
+
+// ---------------------------------------------------------------------
+//
+// Tests from no timeline to timeline
+//
+// ---------------------------------------------------------------------
+
+test(t => {
+ const animation =
+ new Animation(new KeyframeEffect(createDiv(t), null, 100 * MS_PER_SEC),
+ null);
+ animation.currentTime = 50 * MS_PER_SEC;
+ assert_equals(animation.playState, 'paused');
+
+ animation.timeline = document.timeline;
+
+ assert_equals(animation.playState, 'paused');
+ assert_time_equals_literal(animation.currentTime, 50 * MS_PER_SEC);
+}, 'After setting timeline on paused animation it is still paused');
+
+test(t => {
+ const animation =
+ new Animation(new KeyframeEffect(createDiv(t), null, 100 * MS_PER_SEC),
+ null);
+ animation.currentTime = 200 * MS_PER_SEC;
+ assert_equals(animation.playState, 'paused');
+
+ animation.timeline = document.timeline;
+
+ assert_equals(animation.playState, 'paused');
+ assert_time_equals_literal(animation.currentTime, 200 * MS_PER_SEC);
+}, 'After setting timeline on animation paused outside active interval'
+ + ' it is still paused');
+
+test(t => {
+ const animation =
+ new Animation(new KeyframeEffect(createDiv(t), null, 100 * MS_PER_SEC),
+ null);
+ assert_equals(animation.playState, 'idle');
+
+ animation.timeline = document.timeline;
+
+ assert_equals(animation.playState, 'idle');
+}, 'After setting timeline on an idle animation without a start time'
+ + ' it is still idle');
+
+test(t => {
+ const animation =
+ new Animation(new KeyframeEffect(createDiv(t), null, 100 * MS_PER_SEC),
+ null);
+ animation.startTime = document.timeline.currentTime;
+ assert_equals(animation.playState, 'running');
+
+ animation.timeline = document.timeline;
+
+ assert_equals(animation.playState, 'running');
+}, 'After transitioning from a null timeline on an animation with a start time'
+ + ' it is still running');
+
+test(t => {
+ const animation =
+ new Animation(new KeyframeEffect(createDiv(t), null, 100 * MS_PER_SEC),
+ null);
+ animation.startTime = document.timeline.currentTime - 200 * MS_PER_SEC;
+ assert_equals(animation.playState, 'running');
+
+ animation.timeline = document.timeline;
+
+ assert_equals(animation.playState, 'finished');
+}, 'After transitioning from a null timeline on an animation with a ' +
+ 'sufficiently ancient start time it is finished');
+
+promise_test(async t => {
+ const animation =
+ new Animation(new KeyframeEffect(createDiv(t), null, 100 * MS_PER_SEC),
+ null);
+ animation.play();
+ assert_true(animation.pending && animation.playState === 'running',
+ 'Animation is initially play-pending');
+
+ animation.timeline = document.timeline;
+
+ assert_true(animation.pending && animation.playState === 'running',
+ 'Animation is still play-pending after setting timeline');
+
+ await animation.ready;
+ assert_true(!animation.pending && animation.playState === 'running',
+ 'Animation plays after it finishes pending');
+}, 'After setting timeline on a play-pending animation it begins playing'
+ + ' after pending');
+
+promise_test(async t => {
+ const animation =
+ new Animation(new KeyframeEffect(createDiv(t), null, 100 * MS_PER_SEC),
+ null);
+ animation.startTime = document.timeline.currentTime;
+ animation.pause();
+ animation.timeline = null;
+ assert_true(animation.pending && animation.playState === 'paused',
+ 'Animation is initially pause-pending');
+
+ animation.timeline = document.timeline;
+
+ assert_true(animation.pending && animation.playState === 'paused',
+ 'Animation is still pause-pending after setting timeline');
+
+ await animation.ready;
+ assert_true(!animation.pending && animation.playState === 'paused',
+ 'Animation pauses after it finishes pending');
+}, 'After setting timeline on a pause-pending animation it becomes paused'
+ + ' after pending');
+
+// ---------------------------------------------------------------------
+//
+// Tests from timeline to no timeline
+//
+// ---------------------------------------------------------------------
+
+test(t => {
+ const animation =
+ new Animation(new KeyframeEffect(createDiv(t), null, 100 * MS_PER_SEC),
+ document.timeline);
+ animation.currentTime = 50 * MS_PER_SEC;
+ assert_false(animation.pending);
+ assert_equals(animation.playState, 'paused');
+
+ animation.timeline = null;
+
+ assert_false(animation.pending);
+ assert_equals(animation.playState, 'paused');
+ assert_time_equals_literal(animation.currentTime, 50 * MS_PER_SEC);
+}, 'After clearing timeline on paused animation it is still paused');
+
+test(t => {
+ const animation =
+ new Animation(new KeyframeEffect(createDiv(t), null, 100 * MS_PER_SEC),
+ document.timeline);
+ const initialStartTime = document.timeline.currentTime - 200 * MS_PER_SEC;
+ animation.startTime = initialStartTime;
+ assert_equals(animation.playState, 'finished');
+
+ animation.timeline = null;
+
+ assert_equals(animation.playState, 'running');
+ assert_times_equal(animation.startTime, initialStartTime);
+}, 'After clearing timeline on finished animation it is running');
+
+test(t => {
+ const animation =
+ new Animation(new KeyframeEffect(createDiv(t), null, 100 * MS_PER_SEC),
+ document.timeline);
+ const initialStartTime = document.timeline.currentTime - 50 * MS_PER_SEC;
+ animation.startTime = initialStartTime;
+ assert_equals(animation.playState, 'running');
+
+ animation.timeline = null;
+
+ assert_equals(animation.playState, 'running');
+ assert_times_equal(animation.startTime, initialStartTime);
+}, 'After clearing timeline on running animation it is still running');
+
+test(t => {
+ const animation =
+ new Animation(new KeyframeEffect(createDiv(t), null, 100 * MS_PER_SEC),
+ document.timeline);
+ assert_equals(animation.playState, 'idle');
+
+ animation.timeline = null;
+
+ assert_equals(animation.playState, 'idle');
+ assert_equals(animation.startTime, null);
+}, 'After clearing timeline on idle animation it is still idle');
+
+test(t => {
+ const animation = createDiv(t).animate(null, 100 * MS_PER_SEC);
+ assert_true(animation.pending && animation.playState === 'running');
+
+ animation.timeline = null;
+
+ assert_true(animation.pending && animation.playState === 'running');
+}, 'After clearing timeline on play-pending animation it is still pending');
+
+promise_test(async t => {
+ const animation = createDiv(t).animate(null, 100 * MS_PER_SEC);
+ assert_true(animation.pending && animation.playState === 'running');
+
+ animation.timeline = null;
+ animation.timeline = document.timeline;
+
+ assert_true(animation.pending && animation.playState === 'running');
+ await animation.ready;
+ assert_true(!animation.pending && animation.playState === 'running');
+}, 'After clearing and re-setting timeline on play-pending animation it'
+ + ' begins to play');
+
+test(t => {
+ const animation =
+ new Animation(new KeyframeEffect(createDiv(t), null, 100 * MS_PER_SEC),
+ document.timeline);
+ animation.startTime = document.timeline.currentTime;
+ animation.pause();
+ assert_true(animation.pending && animation.playState === 'paused');
+
+ animation.timeline = null;
+
+ assert_true(animation.pending && animation.playState === 'paused');
+}, 'After clearing timeline on a pause-pending animation it is still pending');
+
+promise_test(async t => {
+ const animation =
+ new Animation(new KeyframeEffect(createDiv(t), null, 100 * MS_PER_SEC),
+ document.timeline);
+ animation.startTime = document.timeline.currentTime;
+ animation.pause();
+ assert_true(animation.pending && animation.playState === 'paused');
+
+ animation.timeline = null;
+ animation.timeline = document.timeline;
+
+ assert_true(animation.pending && animation.playState === 'paused');
+ await animation.ready;
+ assert_true(!animation.pending && animation.playState === 'paused');
+}, 'After clearing and re-setting timeline on a pause-pending animation it'
+ + ' completes pausing');
+
+promise_test(async t => {
+ const animation =
+ new Animation(new KeyframeEffect(createDiv(t), null, 100 * MS_PER_SEC),
+ document.timeline);
+ const initialStartTime = document.timeline.currentTime - 50 * MS_PER_SEC;
+ animation.startTime = initialStartTime;
+ animation.pause();
+ animation.play();
+
+ animation.timeline = null;
+ animation.timeline = document.timeline;
+
+ await animation.ready;
+ assert_times_equal(animation.startTime, initialStartTime);
+}, 'After clearing and re-setting timeline on an animation in the middle of'
+ + ' an aborted pause, it continues playing using the same start time');
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/web-animations/timing-model/animations/sync-start-times-ref.html b/testing/web-platform/tests/web-animations/timing-model/animations/sync-start-times-ref.html
new file mode 100644
index 0000000000..fc843a132f
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/timing-model/animations/sync-start-times-ref.html
@@ -0,0 +1,20 @@
+
+<!DOCTYPE html>
+<meta charset="UTF-8">
+<title>Reference for sync start times</title>
+<style>
+ #notes {
+ position: absolute;
+ left: 0px;
+ top: 100px;
+ }
+</style>
+
+<body>
+ <p id="notes">
+ This test creates a pair of animations, starts the first animation and then
+ syncs the second animation to align with the first. The test passes if the
+ box associated with the first animation is completely occluded by the
+ second.
+ </p>
+</body>
diff --git a/testing/web-platform/tests/web-animations/timing-model/animations/sync-start-times.html b/testing/web-platform/tests/web-animations/timing-model/animations/sync-start-times.html
new file mode 100644
index 0000000000..e9ef6762ea
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/timing-model/animations/sync-start-times.html
@@ -0,0 +1,72 @@
+
+<!DOCTYPE html>
+<html class="reftest-wait">
+<meta charset="UTF-8">
+<title>sync start times</title>
+<link rel="match" href="sync-start-times-ref.html">
+<script src="/common/reftest-wait.js"></script>
+<style>
+ #box-1, #box-2 {
+ border: 1px solid white;
+ height: 40px;
+ left: 40px;
+ position: absolute;
+ top: 40px;
+ width: 40px;
+ /* To ensure Chrome to render the two boxes (one actively
+ animating and the other not) with the same subpixel offset
+ when there is subpixel translation during animation. */
+ will-change: transform;
+ }
+ #box-1 {
+ background: blue;
+ z-index: 1;
+ }
+ #box-2 {
+ background: white;
+ z-index: 2;
+ }
+ #notes {
+ position: absolute;
+ left: 0px;
+ top: 100px;
+ }
+</style>
+
+<body>
+ <div id="box-1"></div>
+ <div id="box-2"></div>
+ <p id="notes">
+ This test creates a pair of animations, starts the first animation and then
+ syncs the second animation to align with the first. The test passes if the
+ box associated with the first animation is completely occluded by the
+ second.
+ </p>
+</body>
+<script>
+ onload = function() {
+ function createAnimation(elementId) {
+ const elem = document.getElementById(elementId);
+ const keyframes = [
+ { transform: 'translateX(0px)' },
+ { transform: 'translateX(200px)' }
+ ];
+ const anim = elem.animate(keyframes, { duration: 1000 });
+ anim.pause();
+ return anim;
+ };
+
+ const anim1 = createAnimation('box-1');
+ const anim2 = createAnimation('box-2');
+
+ anim1.currentTime = 500;
+ anim1.play();
+
+ anim1.ready.then(() => {
+ anim2.startTime = anim1.startTime;
+ requestAnimationFrame(() => {
+ takeScreenshot();
+ });
+ });
+ };
+</script>
diff --git a/testing/web-platform/tests/web-animations/timing-model/animations/the-current-time-of-an-animation.html b/testing/web-platform/tests/web-animations/timing-model/animations/the-current-time-of-an-animation.html
new file mode 100644
index 0000000000..77a6b716d2
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/timing-model/animations/the-current-time-of-an-animation.html
@@ -0,0 +1,75 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title>The current time of an animation</title>
+<link rel="help" href="https://drafts.csswg.org/web-animations/#the-current-time-of-an-animation">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../../testcommon.js"></script>
+<script src="../../resources/timing-override.js"></script>
+<body>
+<div id="log"></div>
+<script>
+'use strict';
+
+test(t => {
+ const animation =
+ new Animation(new KeyframeEffect(createDiv(t), null, 100 * MS_PER_SEC),
+ document.timeline);
+
+ animation.play();
+ assert_equals(animation.currentTime, 0,
+ 'Current time returns the hold time set when entering the play-pending ' +
+ 'state');
+}, 'The current time returns the hold time when set');
+
+promise_test(async t => {
+ const animation =
+ new Animation(new KeyframeEffect(createDiv(t), null, 100 * MS_PER_SEC),
+ null);
+
+ await animation.ready;
+ assert_equals(animation.currentTime, null);
+}, 'The current time is unresolved when there is no associated timeline ' +
+ '(and no hold time is set)');
+
+// FIXME: Test that the current time is unresolved when we have an inactive
+// timeline if we find a way of creating an inactive timeline!
+
+test(t => {
+ const animation =
+ new Animation(new KeyframeEffect(createDiv(t), null, 100 * MS_PER_SEC),
+ document.timeline);
+
+ animation.startTime = null;
+ assert_equals(animation.currentTime, null);
+}, 'The current time is unresolved when the start time is unresolved ' +
+ '(and no hold time is set)');
+
+test(t => {
+ const animation =
+ new Animation(new KeyframeEffect(createDiv(t), null, 100 * MS_PER_SEC),
+ document.timeline);
+
+ animation.playbackRate = 2;
+ animation.startTime = document.timeline.currentTime - 25 * MS_PER_SEC;
+
+ const timelineTime = document.timeline.currentTime;
+ const startTime = animation.startTime;
+ const playbackRate = animation.playbackRate;
+ assert_times_equal(animation.currentTime,
+ (timelineTime - startTime) * playbackRate,
+ 'Animation has a unresolved start time');
+}, 'The current time is calculated from the timeline time, start time and ' +
+ 'playback rate');
+
+promise_test(async t => {
+ const animation = createDiv(t).animate(null, 100 * MS_PER_SEC);
+ animation.playbackRate = 0;
+
+ await animation.ready;
+ await waitForAnimationFrames(1);
+ assert_time_equals_literal(animation.currentTime, 0);
+}, 'The current time does not progress if playback rate is 0');
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/web-animations/timing-model/animations/update-playback-rate-fast-ref.html b/testing/web-platform/tests/web-animations/timing-model/animations/update-playback-rate-fast-ref.html
new file mode 100644
index 0000000000..e996815da8
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/timing-model/animations/update-playback-rate-fast-ref.html
@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<title>Reference for update playback rate zero</title>
+<style>
+ #box {
+ background: green;
+ height: 40px;
+ width: 40px;
+ }
+</style>
+<body>
+ <div id="box"></div>
+ <p id="notes">
+ This test creates a running animation and changes its playback rate
+ part way through. If the box remains red when the screenshot is captured
+ the test fails.
+ </p>
+</body>
diff --git a/testing/web-platform/tests/web-animations/timing-model/animations/update-playback-rate-fast.html b/testing/web-platform/tests/web-animations/timing-model/animations/update-playback-rate-fast.html
new file mode 100644
index 0000000000..c3df1c1bf0
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/timing-model/animations/update-playback-rate-fast.html
@@ -0,0 +1,52 @@
+
+<!DOCTYPE html>
+<html class="reftest-wait">
+<meta charset="UTF-8">
+<title>Update playback rate zero</title>
+<link rel="match" href="update-playback-rate-fast-ref.html">
+<script src="/common/reftest-wait.js"></script>
+<script src="../../testcommon.js"></script>
+<style>
+ #box {
+ background: green;
+ height: 40px;
+ width: 40px;
+ }
+</style>
+<body>
+ <div id="box"></div>
+ <p id="notes">
+ This test creates a running animation and changes its playback rate
+ part way through. If the box remains red when the screenshot is captured
+ the test fails.
+ </p>
+</body>
+<script>
+ onload = async function() {
+ const box = document.getElementById('box');
+ const duration = 2000;
+ const anim =
+ box.animate({ bacground: [ 'red', 'green' ] },
+ { duration: duration, easing: 'steps(2, jump-none)' });
+ anim.ready.then(() => {
+ const startTime = anim.timeline.currentTime;
+ waitForAnimationFrames(2).then(() => {
+ anim.updatePlaybackRate(2);
+ anim.ready.then(() => {
+ const updateTime = anim.timeline.currentTime;
+ const baseProgress = (updateTime - startTime) / duration;
+ const checkIfDone = () => {
+ const progress =
+ 2 * (anim.timeline.currentTime - updateTime) / duration +
+ baseProgress;
+ if (progress > 0.5)
+ takeScreenshot();
+ else
+ requestAnimationFrame(checkIfDone);
+ };
+ requestAnimationFrame(checkIfDone);
+ });
+ });
+ });
+ };
+</script>
diff --git a/testing/web-platform/tests/web-animations/timing-model/animations/update-playback-rate-zero-ref.html b/testing/web-platform/tests/web-animations/timing-model/animations/update-playback-rate-zero-ref.html
new file mode 100644
index 0000000000..399fd5ce7d
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/timing-model/animations/update-playback-rate-zero-ref.html
@@ -0,0 +1,16 @@
+<!DOCTYPE html>
+<title>Reference for update playback rate zero</title>
+<style>
+ #box {
+ background: green;
+ height: 40px;
+ width: 40px;
+ }
+</style>
+<body>
+ <div id="box"></div>
+ <p id="notes">
+ This test creates a running animation and halts its playback rate
+ part way through. If the box transitions to red the test fails.
+ </p>
+</body>
diff --git a/testing/web-platform/tests/web-animations/timing-model/animations/update-playback-rate-zero.html b/testing/web-platform/tests/web-animations/timing-model/animations/update-playback-rate-zero.html
new file mode 100644
index 0000000000..db1544ee92
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/timing-model/animations/update-playback-rate-zero.html
@@ -0,0 +1,46 @@
+
+<!DOCTYPE html>
+<html class="reftest-wait">
+<meta charset="UTF-8">
+<title>Update playback rate zero</title>
+<link rel="match" href="update-playback-rate-zero-ref.html">
+<script src="/common/reftest-wait.js"></script>
+<script src="../../testcommon.js"></script>
+<style>
+ #box {
+ background: green;
+ height: 40px;
+ width: 40px;
+ }
+</style>
+<body>
+ <div id="box"></div>
+ <p id="notes">
+ This test creates a running animation and halts its playback rate
+ part way through. If the box transitions to red the test fails.
+ </p>
+</body>
+<script>
+ onload = async function() {
+ const box = document.getElementById('box');
+ const duration = 2000;
+ const anim =
+ box.animate({ bacground: [ 'green', 'red' ] },
+ { duration: duration, easing: 'steps(2, jump-none)' });
+ anim.ready.then(() => {
+ const startTime = anim.timeline.currentTime;
+ waitForAnimationFrames(2).then(() => {
+ anim.updatePlaybackRate(0);
+ anim.ready.then(() => {
+ const checkIfDone = () => {
+ if (anim.timeline.currentTime - startTime > duration / 2)
+ takeScreenshot();
+ else
+ requestAnimationFrame(checkIfDone);
+ };
+ requestAnimationFrame(checkIfDone);
+ });
+ });
+ });
+ };
+</script>
diff --git a/testing/web-platform/tests/web-animations/timing-model/animations/updating-the-finished-state.html b/testing/web-platform/tests/web-animations/timing-model/animations/updating-the-finished-state.html
new file mode 100644
index 0000000000..4d3cc7950b
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/timing-model/animations/updating-the-finished-state.html
@@ -0,0 +1,457 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title>Updating the finished state</title>
+<meta name="timeout" content="long">
+<link rel="help" href="https://drafts.csswg.org/web-animations/#updating-the-finished-state">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../../testcommon.js"></script>
+<body>
+<div id="log"></div>
+<script>
+'use strict';
+
+// --------------------------------------------------------------------
+//
+// TESTS FOR UPDATING THE HOLD TIME
+//
+// --------------------------------------------------------------------
+
+// CASE 1: playback rate > 0 and current time >= target effect end
+// (Also the start time is resolved and there is pending task)
+
+// Did seek = false
+promise_test(async t => {
+ const anim = createDiv(t).animate(null, 100 * MS_PER_SEC);
+
+ // Here and in the following tests we wait until ready resolves as
+ // otherwise we don't have a resolved start time. We test the case
+ // where the start time is unresolved in a subsequent test.
+ await anim.ready;
+
+ // Seek to 1ms before the target end and then wait 1ms
+ anim.currentTime = 100 * MS_PER_SEC - 1;
+ await waitForAnimationFramesWithDelay(1);
+
+ assert_equals(anim.currentTime, 100 * MS_PER_SEC,
+ 'Hold time is set to target end clamping current time');
+}, 'Updating the finished state when playing past end');
+
+// Did seek = true
+promise_test(async t => {
+ const anim = createDiv(t).animate(null, 100 * MS_PER_SEC);
+
+ await anim.ready;
+
+ anim.currentTime = 200 * MS_PER_SEC;
+ await waitForNextFrame();
+
+ assert_equals(anim.currentTime, 200 * MS_PER_SEC,
+ 'Hold time is set so current time should NOT change');
+}, 'Updating the finished state when seeking past end');
+
+// Test current time == target end
+//
+// We can't really write a test for current time == target end with
+// did seek = false since that would imply setting up an animation where
+// the next animation frame time happens to exactly align with the target end.
+//
+// Fortunately, we don't need to test that case since even if the implementation
+// fails to set the hold time on such a tick, it should be mostly unobservable
+// (on the subsequent tick the hold time will be set to the same value anyway).
+
+// Did seek = true
+promise_test(async t => {
+ const anim = createDiv(t).animate(null, 100 * MS_PER_SEC);
+ await anim.ready;
+
+ anim.currentTime = 100 * MS_PER_SEC;
+ await waitForNextFrame();
+
+ assert_equals(anim.currentTime, 100 * MS_PER_SEC,
+ 'Hold time is set so current time should NOT change');
+}, 'Updating the finished state when seeking exactly to end');
+
+
+// CASE 2: playback rate < 0 and current time <= 0
+// (Also the start time is resolved and there is pending task)
+
+// Did seek = false
+promise_test(async t => {
+ const anim = createDiv(t).animate(null, 100 * MS_PER_SEC);
+ anim.playbackRate = -1;
+ anim.play(); // Make sure animation is not initially finished
+
+ await anim.ready;
+
+ // Seek to 1ms before 0 and then wait 1ms
+ anim.currentTime = 1;
+ await waitForAnimationFramesWithDelay(1);
+
+ assert_equals(anim.currentTime, 0 * MS_PER_SEC,
+ 'Hold time is set to zero clamping current time');
+}, 'Updating the finished state when playing in reverse past zero');
+
+// Did seek = true
+promise_test(async t => {
+ const anim = createDiv(t).animate(null, 100 * MS_PER_SEC);
+ anim.playbackRate = -1;
+ anim.play();
+
+ await anim.ready;
+
+ anim.currentTime = -100 * MS_PER_SEC;
+ await waitForNextFrame();
+
+ assert_equals(anim.currentTime, -100 * MS_PER_SEC,
+ 'Hold time is set so current time should NOT change');
+}, 'Updating the finished state when seeking a reversed animation past zero');
+
+// As before, it's difficult to test current time == 0 for did seek = false but
+// it doesn't really matter.
+
+// Did seek = true
+promise_test(async t => {
+ const anim = createDiv(t).animate(null, 100 * MS_PER_SEC);
+ anim.playbackRate = -1;
+ anim.play();
+ await anim.ready;
+
+ anim.currentTime = 0;
+ await waitForNextFrame();
+
+ assert_equals(anim.currentTime, 0 * MS_PER_SEC,
+ 'Hold time is set so current time should NOT change');
+}, 'Updating the finished state when seeking a reversed animation exactly'
+ + ' to zero');
+
+// CASE 3: playback rate > 0 and current time < target end OR
+// playback rate < 0 and current time > 0
+// (Also the start time is resolved and there is pending task)
+
+// Did seek = false; playback rate > 0
+promise_test(async t => {
+ const anim = createDiv(t).animate(null, 100 * MS_PER_SEC);
+
+ // We want to test that the hold time is cleared so first we need to
+ // put the animation in a state where the hold time is set.
+ anim.finish();
+ await anim.ready;
+
+ assert_equals(anim.currentTime, 100 * MS_PER_SEC,
+ 'Hold time is initially set');
+ // Then extend the duration so that the hold time is cleared and on
+ // the next tick the current time will increase.
+ anim.effect.updateTiming({
+ duration: anim.effect.getComputedTiming().duration * 2,
+ });
+ await waitForNextFrame();
+
+ assert_greater_than(anim.currentTime, 100 * MS_PER_SEC,
+ 'Hold time is not set so current time should increase');
+}, 'Updating the finished state when playing before end');
+
+// Did seek = true; playback rate > 0
+promise_test(async t => {
+ const anim = createDiv(t).animate(null, 100 * MS_PER_SEC);
+ anim.finish();
+ await anim.ready;
+
+ anim.currentTime = 50 * MS_PER_SEC;
+ // When did seek = true, updating the finished state: (i) updates
+ // the animation's start time and (ii) clears the hold time.
+ // We can test both by checking that the currentTime is initially
+ // updated and then increases.
+ assert_equals(anim.currentTime, 50 * MS_PER_SEC, 'Start time is updated');
+ await waitForNextFrame();
+
+ assert_greater_than(anim.currentTime, 50 * MS_PER_SEC,
+ 'Hold time is not set so current time should increase');
+}, 'Updating the finished state when seeking before end');
+
+// Did seek = false; playback rate < 0
+//
+// Unfortunately it is not possible to test this case. We need to have
+// a hold time set, a resolved start time, and then perform some
+// operation that updates the finished state with did seek set to true.
+//
+// However, the only situation where this could arrive is when we
+// replace the timeline and that procedure is likely to change. For all
+// other cases we either have an unresolved start time (e.g. when
+// paused), we don't have a set hold time (e.g. regular playback), or
+// the current time is zero (and anything that gets us out of that state
+// will set did seek = true).
+
+// Did seek = true; playback rate < 0
+promise_test(async t => {
+ const anim = createDiv(t).animate(null, 100 * MS_PER_SEC);
+ anim.playbackRate = -1;
+ await anim.ready;
+
+ anim.currentTime = 50 * MS_PER_SEC;
+ assert_equals(anim.currentTime, 50 * MS_PER_SEC, 'Start time is updated');
+ await waitForNextFrame();
+
+ assert_less_than(anim.currentTime, 50 * MS_PER_SEC,
+ 'Hold time is not set so current time should decrease');
+}, 'Updating the finished state when seeking a reversed animation before end');
+
+// CASE 4: playback rate == 0
+
+// current time < 0
+promise_test(async t => {
+ const anim = createDiv(t).animate(null, 100 * MS_PER_SEC);
+ anim.playbackRate = 0;
+ await anim.ready;
+
+ anim.currentTime = -100 * MS_PER_SEC;
+ await waitForNextFrame();
+
+ assert_equals(anim.currentTime, -100 * MS_PER_SEC,
+ 'Hold time should not be cleared so current time should'
+ + ' NOT change');
+}, 'Updating the finished state when playback rate is zero and the'
+ + ' current time is less than zero');
+
+// current time < target end
+promise_test(async t => {
+ const anim = createDiv(t).animate(null, 100 * MS_PER_SEC);
+ anim.playbackRate = 0;
+ await anim.ready;
+
+ anim.currentTime = 50 * MS_PER_SEC;
+ await waitForNextFrame();
+
+ assert_equals(anim.currentTime, 50 * MS_PER_SEC,
+ 'Hold time should not be cleared so current time should'
+ + ' NOT change');
+}, 'Updating the finished state when playback rate is zero and the'
+ + ' current time is less than end');
+
+// current time > target end
+promise_test(async t => {
+ const anim = createDiv(t).animate(null, 100 * MS_PER_SEC);
+ anim.playbackRate = 0;
+ await anim.ready;
+
+ anim.currentTime = 200 * MS_PER_SEC;
+ await waitForNextFrame();
+
+ assert_equals(anim.currentTime, 200 * MS_PER_SEC,
+ 'Hold time should not be cleared so current time should'
+ + ' NOT change');
+}, 'Updating the finished state when playback rate is zero and the'
+ + ' current time is greater than end');
+
+// CASE 5: current time unresolved
+
+promise_test(async t => {
+ const anim = createDiv(t).animate(null, 100 * MS_PER_SEC);
+ anim.cancel();
+ // Trigger a change that will cause the "update the finished state"
+ // procedure to run.
+ anim.effect.updateTiming({ duration: 200 * MS_PER_SEC });
+ assert_equals(anim.currentTime, null,
+ 'The animation hold time / start time should not be updated');
+ // The "update the finished state" procedure is supposed to run after any
+ // change to timing, but just in case an implementation defers that, let's
+ // wait a frame and check that the hold time / start time has still not been
+ // updated.
+ await waitForAnimationFrames(1);
+
+ assert_equals(anim.currentTime, null,
+ 'The animation hold time / start time should not be updated');
+}, 'Updating the finished state when current time is unresolved');
+
+// CASE 6: has a pending task
+
+test(t => {
+ const anim = createDiv(t).animate(null, 100 * MS_PER_SEC);
+ anim.cancel();
+ anim.currentTime = 75 * MS_PER_SEC;
+ anim.play();
+ // We now have a pending task and a resolved current time.
+ //
+ // In the next step we will adjust the timing so that the current time
+ // is greater than the target end. At this point the "update the finished
+ // state" procedure should run and if we fail to check for a pending task
+ // we will set the hold time to the target end, i.e. 50ms.
+ anim.effect.updateTiming({ duration: 50 * MS_PER_SEC });
+ assert_equals(anim.currentTime, 75 * MS_PER_SEC,
+ 'Hold time should not be updated');
+}, 'Updating the finished state when there is a pending task');
+
+// CASE 7: start time unresolved
+
+// Did seek = false
+promise_test(async t => {
+ const anim = createDiv(t).animate(null, 100 * MS_PER_SEC);
+ anim.cancel();
+ // Make it so that only the start time is unresolved (to avoid overlapping
+ // with the test case where current time is unresolved)
+ anim.currentTime = 150 * MS_PER_SEC;
+ // Trigger a change that will cause the "update the finished state"
+ // procedure to run (did seek = false).
+ anim.effect.updateTiming({ duration: 200 * MS_PER_SEC });
+ await waitForAnimationFrames(1);
+
+ assert_equals(anim.currentTime, 150 * MS_PER_SEC,
+ 'The animation hold time should not be updated');
+ assert_equals(anim.startTime, null,
+ 'The animation start time should not be updated');
+}, 'Updating the finished state when start time is unresolved and'
+ + ' did seek = false');
+
+// Did seek = true
+test(t => {
+ const anim = createDiv(t).animate(null, 100 * MS_PER_SEC);
+ anim.cancel();
+ anim.currentTime = 150 * MS_PER_SEC;
+ // Trigger a change that will cause the "update the finished state"
+ // procedure to run.
+ anim.currentTime = 50 * MS_PER_SEC;
+ assert_equals(anim.currentTime, 50 * MS_PER_SEC,
+ 'The animation hold time should not be updated');
+ assert_equals(anim.startTime, null,
+ 'The animation start time should not be updated');
+}, 'Updating the finished state when start time is unresolved and'
+ + ' did seek = true');
+
+// --------------------------------------------------------------------
+//
+// TESTS FOR RUNNING FINISH NOTIFICATION STEPS
+//
+// --------------------------------------------------------------------
+
+function waitForFinishEventAndPromise(animation) {
+ const eventPromise = new Promise(resolve => {
+ animation.onfinish = resolve;
+ });
+ return Promise.all([eventPromise, animation.finished]);
+}
+
+promise_test(t => {
+ const animation = createDiv(t).animate(null, 1);
+ animation.onfinish =
+ t.unreached_func('Seeking to finish should not fire finish event');
+ animation.finished.then(
+ t.unreached_func('Seeking to finish should not resolve finished promise'));
+ animation.currentTime = 1;
+ animation.currentTime = 0;
+ animation.pause();
+ return waitForAnimationFrames(3);
+}, 'Finish notification steps don\'t run when the animation seeks to finish'
+ + ' and then seeks back again');
+
+promise_test(async t => {
+ const animation = createDiv(t).animate(null, 1);
+ await animation.ready;
+
+ return waitForFinishEventAndPromise(animation);
+}, 'Finish notification steps run when the animation completes normally');
+
+promise_test(async t => {
+ const effect = new KeyframeEffect(null, null, 1);
+ const animation = new Animation(effect, document.timeline);
+ animation.play();
+ await animation.ready;
+
+ return waitForFinishEventAndPromise(animation);
+}, 'Finish notification steps run when an animation without a target'
+ + ' effect completes normally');
+
+promise_test(async t => {
+ const animation = createDiv(t).animate(null, 1);
+ await animation.ready;
+
+ animation.currentTime = 10;
+ return waitForFinishEventAndPromise(animation);
+}, 'Finish notification steps run when the animation seeks past finish');
+
+promise_test(async t => {
+ const animation = createDiv(t).animate(null, 1);
+ await animation.ready;
+
+ // Register for notifications now since once we seek away from being
+ // finished the 'finished' promise will be replaced.
+ const finishNotificationSteps = waitForFinishEventAndPromise(animation);
+ animation.finish();
+ animation.currentTime = 0;
+ animation.pause();
+ return finishNotificationSteps;
+}, 'Finish notification steps run when the animation completes with .finish(),'
+ + ' even if we then seek away');
+
+promise_test(async t => {
+ const animation = createDiv(t).animate(null, 1);
+ const initialFinishedPromise = animation.finished;
+ await animation.finished;
+
+ animation.currentTime = 0;
+ assert_not_equals(initialFinishedPromise, animation.finished);
+}, 'Animation finished promise is replaced after seeking back to start');
+
+promise_test(async t => {
+ const animation = createDiv(t).animate(null, 1);
+ const initialFinishedPromise = animation.finished;
+ await animation.finished;
+
+ animation.play();
+ assert_not_equals(initialFinishedPromise, animation.finished);
+}, 'Animation finished promise is replaced after replaying from start');
+
+async_test(t => {
+ const animation = createDiv(t).animate(null, 1);
+ animation.onfinish = event => {
+ animation.currentTime = 0;
+ animation.onfinish = event => {
+ t.done();
+ };
+ };
+}, 'Animation finish event is fired again after seeking back to start');
+
+async_test(t => {
+ const animation = createDiv(t).animate(null, 1);
+ animation.onfinish = event => {
+ animation.play();
+ animation.onfinish = event => {
+ t.done();
+ };
+ };
+}, 'Animation finish event is fired again after replaying from start');
+
+async_test(t => {
+ const anim = createDiv(t).animate(null,
+ { duration: 100000, endDelay: 50000 });
+ anim.onfinish = t.step_func(event => {
+ assert_unreached('finish event should not be fired');
+ });
+
+ anim.ready.then(() => {
+ anim.currentTime = 100000;
+ return waitForAnimationFrames(2);
+ }).then(t.step_func(() => {
+ t.done();
+ }));
+}, 'finish event is not fired at the end of the active interval when the'
+ + ' endDelay has not expired');
+
+async_test(t => {
+ const anim = createDiv(t).animate(null,
+ { duration: 100000, endDelay: 30000 });
+ anim.ready.then(() => {
+ anim.currentTime = 110000; // during endDelay
+ anim.onfinish = t.step_func(event => {
+ assert_unreached('onfinish event should not be fired during endDelay');
+ });
+ return waitForAnimationFrames(2);
+ }).then(t.step_func(() => {
+ anim.onfinish = t.step_func(event => {
+ t.done();
+ });
+ anim.currentTime = 130000; // after endTime
+ }));
+}, 'finish event is fired after the endDelay has expired');
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/web-animations/timing-model/time-transformations/transformed-progress.html b/testing/web-platform/tests/web-animations/timing-model/time-transformations/transformed-progress.html
new file mode 100644
index 0000000000..960e333c09
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/timing-model/time-transformations/transformed-progress.html
@@ -0,0 +1,391 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title>Transformed progress</title>
+<link rel="help" href="https://drafts.csswg.org/web-animations/#calculating-the-transformed-progress">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../../testcommon.js"></script>
+<script src="../../resources/easing-tests.js"></script>
+<body>
+<div id="log"></div>
+<div id="target"></div>
+<script>
+'use strict';
+
+for (const params of gEasingTests) {
+ test(t => {
+ const target = createDiv(t);
+ const anim = target.animate(null, { duration: 1000,
+ fill: 'forwards',
+ easing: params.easing });
+
+ for (const sampleTime of [0, 250, 500, 750, 1000]) {
+ anim.currentTime = sampleTime;
+ const portion = sampleTime / anim.effect.getComputedTiming().duration;
+ const expectedProgress = params.easingFunction(portion);
+ assert_approx_equals(anim.effect.getComputedTiming().progress,
+ expectedProgress,
+ 0.01,
+ 'The progress should be approximately ' +
+ `${expectedProgress} at ${sampleTime}ms`);
+ }
+ }, `Transformed progress for ${params.desc}`);
+}
+
+// Additional tests for various boundary conditions of step timing functions.
+
+const gStepTimingFunctionTests = [
+ {
+ description: 'Test bounds point of step-start easing',
+ effect: {
+ delay: 1000,
+ duration: 1000,
+ fill: 'both',
+ easing: 'steps(2, start)'
+ },
+ conditions: [
+ { currentTime: 0, progress: 0 },
+ { currentTime: 999, progress: 0 },
+ { currentTime: 1000, progress: 0.5 },
+ { currentTime: 1499, progress: 0.5 },
+ { currentTime: 1500, progress: 1 },
+ { currentTime: 2000, progress: 1 }
+ ]
+ },
+ {
+ description: 'Test bounds point of step-start easing with reverse direction',
+ effect: {
+ delay: 1000,
+ duration: 1000,
+ fill: 'both',
+ direction: 'reverse',
+ easing: 'steps(2, start)'
+ },
+ conditions: [
+ { currentTime: 0, progress: 1 },
+ { currentTime: 1001, progress: 1 },
+ { currentTime: 1500, progress: 1 },
+ { currentTime: 1501, progress: 0.5 },
+ { currentTime: 2000, progress: 0 },
+ { currentTime: 2500, progress: 0 }
+ ]
+ },
+ {
+ description: 'Test bounds point of step-start easing ' +
+ 'with iterationStart not at a transition point',
+ effect: {
+ delay: 1000,
+ duration: 1000,
+ fill: 'both',
+ iterationStart: 0.25,
+ easing: 'steps(2, start)'
+ },
+ conditions: [
+ { currentTime: 0, progress: 0.5 },
+ { currentTime: 999, progress: 0.5 },
+ { currentTime: 1000, progress: 0.5 },
+ { currentTime: 1249, progress: 0.5 },
+ { currentTime: 1250, progress: 1 },
+ { currentTime: 1749, progress: 1 },
+ { currentTime: 1750, progress: 0.5 },
+ { currentTime: 2000, progress: 0.5 },
+ { currentTime: 2500, progress: 0.5 },
+ ]
+ },
+ {
+ description: 'Test bounds point of step-start easing ' +
+ 'with iterationStart and delay',
+ effect: {
+ delay: 1000,
+ duration: 1000,
+ fill: 'both',
+ iterationStart: 0.5,
+ easing: 'steps(2, start)'
+ },
+ conditions: [
+ { currentTime: 0, progress: 0.5 },
+ { currentTime: 999, progress: 0.5 },
+ { currentTime: 1000, progress: 1 },
+ { currentTime: 1499, progress: 1 },
+ { currentTime: 1500, progress: 0.5 },
+ { currentTime: 2000, progress: 1 }
+ ]
+ },
+ {
+ description: 'Test bounds point of step-start easing ' +
+ 'with iterationStart and reverse direction',
+ effect: {
+ delay: 1000,
+ duration: 1000,
+ fill: 'both',
+ iterationStart: 0.5,
+ direction: 'reverse',
+ easing: 'steps(2, start)'
+ },
+ conditions: [
+ { currentTime: 0, progress: 1 },
+ { currentTime: 1000, progress: 1 },
+ { currentTime: 1001, progress: 0.5 },
+ { currentTime: 1499, progress: 0.5 },
+ { currentTime: 1500, progress: 1 },
+ { currentTime: 1999, progress: 1 },
+ { currentTime: 2000, progress: 0.5 },
+ { currentTime: 2500, progress: 0.5 }
+ ]
+ },
+ {
+ description: 'Test bounds point of step(4, start) easing ' +
+ 'with iterationStart 0.75 and delay',
+ effect: {
+ duration: 1000,
+ fill: 'both',
+ delay: 1000,
+ iterationStart: 0.75,
+ easing: 'steps(4, start)'
+ },
+ conditions: [
+ { currentTime: 0, progress: 0.75 },
+ { currentTime: 999, progress: 0.75 },
+ { currentTime: 1000, progress: 1 },
+ { currentTime: 2000, progress: 1 },
+ { currentTime: 2500, progress: 1 }
+ ]
+ },
+ {
+ description: 'Test bounds point of step-start easing ' +
+ 'with alternate direction',
+ effect: {
+ duration: 1000,
+ fill: 'both',
+ delay: 1000,
+ iterations: 2,
+ iterationStart: 1.5,
+ direction: 'alternate',
+ easing: 'steps(2, start)'
+ },
+ conditions: [
+ { currentTime: 0, progress: 1 },
+ { currentTime: 1000, progress: 1 },
+ { currentTime: 1001, progress: 0.5 },
+ { currentTime: 2999, progress: 1 },
+ { currentTime: 3000, progress: 0.5 },
+ { currentTime: 3500, progress: 0.5 }
+ ]
+ },
+ {
+ description: 'Test bounds point of step-start easing ' +
+ 'with alternate-reverse direction',
+ effect: {
+ duration: 1000,
+ fill: 'both',
+ delay: 1000,
+ iterations: 2,
+ iterationStart: 0.5,
+ direction: 'alternate-reverse',
+ easing: 'steps(2, start)'
+ },
+ conditions: [
+ { currentTime: 0, progress: 1 },
+ { currentTime: 1000, progress: 1 },
+ { currentTime: 1001, progress: 0.5 },
+ { currentTime: 2999, progress: 1 },
+ { currentTime: 3000, progress: 0.5 },
+ { currentTime: 3500, progress: 0.5 }
+ ]
+ },
+ {
+ description: 'Test bounds point of step-end easing',
+ effect: {
+ delay: 1000,
+ duration: 1000,
+ fill: 'both',
+ easing: 'steps(2, end)'
+ },
+ conditions: [
+ { currentTime: 0, progress: 0 },
+ { currentTime: 999, progress: 0 },
+ { currentTime: 1000, progress: 0 },
+ { currentTime: 1499, progress: 0 },
+ { currentTime: 1500, progress: 0.5 },
+ { currentTime: 2000, progress: 1 }
+ ]
+ },
+ {
+ description: 'Test bounds point of step-end easing ' +
+ 'with iterationStart and delay',
+ effect: {
+ duration: 1000,
+ fill: 'both',
+ delay: 1000,
+ iterationStart: 0.5,
+ easing: 'steps(2, end)'
+ },
+ conditions: [
+ { currentTime: 0, progress: 0 },
+ { currentTime: 999, progress: 0 },
+ { currentTime: 1000, progress: 0.5 },
+ { currentTime: 1499, progress: 0.5 },
+ { currentTime: 1500, progress: 0 },
+ { currentTime: 1999, progress: 0 },
+ { currentTime: 2000, progress: 0.5 },
+ { currentTime: 2500, progress: 0.5 }
+ ]
+ },
+ {
+ description: 'Test bounds point of step-end easing ' +
+ 'with iterationStart not at a transition point',
+ effect: {
+ delay: 1000,
+ duration: 1000,
+ fill: 'both',
+ iterationStart: 0.75,
+ easing: 'steps(2, end)'
+ },
+ conditions: [
+ { currentTime: 0, progress: 0.5 },
+ { currentTime: 999, progress: 0.5 },
+ { currentTime: 1000, progress: 0.5 },
+ { currentTime: 1249, progress: 0.5 },
+ { currentTime: 1250, progress: 0 },
+ { currentTime: 1749, progress: 0 },
+ { currentTime: 1750, progress: 0.5 },
+ { currentTime: 2000, progress: 0.5 },
+ { currentTime: 2500, progress: 0.5 },
+ ]
+ },
+ {
+ description: 'Test bounds point of steps(jump-both) easing',
+ effect: {
+ delay: 1000,
+ duration: 1000,
+ fill: 'both',
+ easing: 'steps(2, jump-both)'
+ },
+ conditions: [
+ { currentTime: 0, progress: 0 },
+ { currentTime: 999, progress: 0 },
+ { currentTime: 1000, progress: 1/3 },
+ { currentTime: 1499, progress: 1/3 },
+ { currentTime: 1500, progress: 2/3 },
+ { currentTime: 2000, progress: 1 }
+ ]
+ },
+ {
+ description: 'Test bounds point of steps(jump-both) easing ' +
+ 'with iterationStart and delay',
+ effect: {
+ duration: 1000,
+ fill: 'both',
+ delay: 1000,
+ iterationStart: 0.5,
+ easing: 'steps(2, jump-both)'
+ },
+ conditions: [
+ { currentTime: 0, progress: 1/3 },
+ { currentTime: 999, progress: 1/3 },
+ { currentTime: 1000, progress: 2/3 },
+ { currentTime: 1499, progress: 2/3 },
+ { currentTime: 1500, progress: 1/3 },
+ { currentTime: 1999, progress: 1/3 },
+ { currentTime: 2000, progress: 2/3 },
+ { currentTime: 2500, progress: 2/3 }
+ ]
+ },
+ {
+ description: 'Test bounds point of steps(jump-both) easing ' +
+ 'with iterationStart not at a transition point',
+ effect: {
+ delay: 1000,
+ duration: 1000,
+ fill: 'both',
+ iterationStart: 0.75,
+ easing: 'steps(2, jump-both)'
+ },
+ conditions: [
+ { currentTime: 0, progress: 2/3 },
+ { currentTime: 999, progress: 2/3 },
+ { currentTime: 1000, progress: 2/3 },
+ { currentTime: 1249, progress: 2/3 },
+ { currentTime: 1250, progress: 1/3 },
+ { currentTime: 1749, progress: 1/3 },
+ { currentTime: 1750, progress: 2/3 },
+ { currentTime: 2000, progress: 2/3 },
+ { currentTime: 2500, progress: 2/3 }
+ ]
+ },
+ {
+ description: 'Test bounds point of steps(jump-none) easing',
+ effect: {
+ delay: 1000,
+ duration: 1000,
+ fill: 'both',
+ easing: 'steps(2, jump-none)'
+ },
+ conditions: [
+ { currentTime: 0, progress: 0 },
+ { currentTime: 1000, progress: 0 },
+ { currentTime: 1499, progress: 0 },
+ { currentTime: 1500, progress: 1 },
+ { currentTime: 2000, progress: 1 }
+ ]
+ },
+ {
+ description: 'Test bounds point of steps(jump-none) easing ' +
+ 'with iterationStart and delay',
+ effect: {
+ duration: 1000,
+ fill: 'both',
+ delay: 1000,
+ iterationStart: 0.5,
+ easing: 'steps(2, jump-none)'
+ },
+ conditions: [
+ { currentTime: 0, progress: 0 },
+ { currentTime: 999, progress: 0 },
+ { currentTime: 1000, progress: 1 },
+ { currentTime: 1499, progress: 1 },
+ { currentTime: 1500, progress: 0 },
+ { currentTime: 1999, progress: 0 },
+ { currentTime: 2000, progress: 1 },
+ { currentTime: 2500, progress: 1 }
+ ]
+ },
+ {
+ description: 'Test bounds point of steps(jump-none) easing ' +
+ 'with iterationStart not at a transition point',
+ effect: {
+ delay: 1000,
+ duration: 1000,
+ fill: 'both',
+ iterationStart: 0.75,
+ easing: 'steps(2, jump-none)'
+ },
+ conditions: [
+ { currentTime: 0, progress: 1 },
+ { currentTime: 999, progress: 1 },
+ { currentTime: 1000, progress: 1 },
+ { currentTime: 1249, progress: 1 },
+ { currentTime: 1250, progress: 0 },
+ { currentTime: 1749, progress: 0 },
+ { currentTime: 1750, progress: 1 },
+ { currentTime: 2000, progress: 1 },
+ { currentTime: 2500, progress: 1 }
+ ]
+ },
+];
+
+for (const options of gStepTimingFunctionTests) {
+ test(t => {
+ const target = createDiv(t);
+ const animation = target.animate(null, options.effect);
+ for (const condition of options.conditions) {
+ animation.currentTime = condition.currentTime;
+ assert_equals(animation.effect.getComputedTiming().progress,
+ condition.progress,
+ `Progress at ${animation.currentTime}ms`);
+ }
+ }, options.description);
+}
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/web-animations/timing-model/timelines/document-timelines.html b/testing/web-platform/tests/web-animations/timing-model/timelines/document-timelines.html
new file mode 100644
index 0000000000..c71d73331c
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/timing-model/timelines/document-timelines.html
@@ -0,0 +1,50 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>Document timelines</title>
+<link rel="help" href="https://drafts.csswg.org/web-animations/#document-timelines">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../../testcommon.js"></script>
+<div id="log"></div>
+<script>
+'use strict';
+
+async_test(t => {
+ assert_greater_than_equal(document.timeline.currentTime, 0,
+ 'The current time is initially is positive or zero');
+
+ // document.timeline.currentTime should be set even before document
+ // load fires. We expect this code to be run before document load and hence
+ // the above assertion is sufficient.
+ // If the following assertion fails, this test needs to be redesigned.
+ assert_not_equals(document.readyState, 'complete',
+ 'Test is running prior to document load');
+
+ // Test that the document timeline's current time is measured from
+ // navigationStart.
+ //
+ // We can't just compare document.timeline.currentTime to
+ // window.performance.now() because currentTime is only updated on a sample
+ // so we use requestAnimationFrame instead.
+ window.requestAnimationFrame(rafTime => {
+ t.step(() => {
+ assert_equals(document.timeline.currentTime, rafTime,
+ 'The current time matches requestAnimationFrame time');
+ });
+ t.done();
+ });
+}, 'Document timelines report current time relative to navigationStart');
+
+async_test(t => {
+ window.requestAnimationFrame(rafTime => {
+ t.step(() => {
+ const iframe = document.createElement('iframe');
+ document.body.appendChild(iframe);
+ assert_greater_than_equal(iframe.contentDocument.timeline.currentTime, 0,
+ 'The current time of a new iframe is initially is positive or zero');
+ });
+ t.done();
+ });
+}, 'Child frames do not report negative initial times');
+
+</script>
diff --git a/testing/web-platform/tests/web-animations/timing-model/timelines/resources/target-frame.html b/testing/web-platform/tests/web-animations/timing-model/timelines/resources/target-frame.html
new file mode 100644
index 0000000000..18ee4fd8a2
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/timing-model/timelines/resources/target-frame.html
@@ -0,0 +1,76 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <style type="text/css">
+ #target {
+ background: green;
+ height: 50px;
+ width: 50px;
+ }
+ </style>
+</head>
+<body>
+ <div id="target"></div>
+</body>
+<script src="../../../testcommon.js"></script>
+<script type="text/javascript">
+ function sendResult(message) {
+ top.postMessage(message, '*');
+ }
+
+ function waitForAnimationReady(anim) {
+ // Resolution of the ready promise, though UA dependent, is expected to
+ // happen within a few frames. Throttling rendering of the frame owning
+ // the animation's timeline may delay resolution of the ready promise,
+ // resulting in a test failure.
+ let frameTimeout = 10;
+ let resolved = false;
+ return new Promise((resolve, reject) => {
+ anim.ready.then(() => {
+ resolved = true;
+ resolve('PASS');
+ });
+ const tick = () => {
+ requestAnimationFrame(() => {
+ if (!resolved) {
+ if (--frameTimeout == 0)
+ resolve('FAIL: Animation is still pending');
+ else
+ tick();
+ }
+ });
+ };
+ tick();
+ });
+ }
+
+ function verifyAnimationIsUpdating() {
+ return new Promise(resolve => {
+ waitForAnimationFrames(3).then(() => {
+ const opacity = getComputedStyle(target).opacity;
+ const result =
+ (opacity == 1) ? 'FAIL: opacity remained unchanged' : 'PASS';
+ resolve(result);
+ });
+ });
+ }
+
+ async function runTest() {
+ const anim = document.getAnimations()[0];
+ if (!anim) {
+ setResult('FAIL: Failed to create animation');
+ return;
+ }
+ waitForAnimationReady(anim).then(result => {
+ if (result != 'PASS') {
+ sendResult(result);
+ return;
+ }
+ verifyAnimationIsUpdating().then(result => {
+ sendResult(result);
+ });
+ });
+ }
+</script>
+</html>
diff --git a/testing/web-platform/tests/web-animations/timing-model/timelines/resources/timeline-frame.html b/testing/web-platform/tests/web-animations/timing-model/timelines/resources/timeline-frame.html
new file mode 100644
index 0000000000..9c8cdabc9d
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/timing-model/timelines/resources/timeline-frame.html
@@ -0,0 +1,20 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+</head>
+<script type="text/javascript">
+ const targetWindow = window.top.a;
+ const element = targetWindow.document.getElementById('target');
+ const keyframes = { opacity: [1, 0.2] };
+ const options = {
+ duration: 1000,
+ // Use this document's timeline rather then the timeline of the
+ // element's document.
+ timeline: document.timeline,
+ fill: 'forwards'
+ };
+ element.animate(keyframes, options);
+ targetWindow.runTest();
+</script>
+</html>
diff --git a/testing/web-platform/tests/web-animations/timing-model/timelines/sibling-iframe-timeline.html b/testing/web-platform/tests/web-animations/timing-model/timelines/sibling-iframe-timeline.html
new file mode 100644
index 0000000000..8a611c8579
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/timing-model/timelines/sibling-iframe-timeline.html
@@ -0,0 +1,58 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Animate using sibling iframe's timeline</title>
+</head>
+<body></body>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script type="text/javascript">
+ 'use strict';
+
+ function crossSiteUrl(filename) {
+ const url =
+ get_host_info().HTTP_REMOTE_ORIGIN +
+ '/web-animations/timing-model/timelines/resources/' +
+ filename;
+ return url;
+ }
+
+ function loadFrame(name, path, hidden) {
+ return new Promise(resolve => {
+ const frame = document.createElement('iframe');
+ if (hidden)
+ frame.style = 'visibility: hidden;';
+ frame.name = name;
+ document.body.appendChild(frame);
+ frame.onload = () => {
+ resolve();
+ }
+ frame.src = crossSiteUrl(path);
+ });
+ }
+
+ function waitForTestResults() {
+ return new Promise(resolve => {
+ const listener = (evt) => {
+ window.removeEventListener('message', listener);
+ resolve(evt.data);
+ };
+ window.addEventListener('message', listener);
+ });
+ }
+
+ promise_test(async t => {
+ const promise = waitForTestResults().then((data) => {
+ assert_equals(data, 'PASS');
+ });
+ // Animate an element in frame A.
+ await loadFrame('a', 'target-frame.html', false);
+ // Animation's timeline is in hidden frame B.
+ await loadFrame('b', 'timeline-frame.html', true);
+
+ return promise;
+ }, 'animation tied to another frame\'s timeline runs properly');
+</script>
+</html>
diff --git a/testing/web-platform/tests/web-animations/timing-model/timelines/timelines.html b/testing/web-platform/tests/web-animations/timing-model/timelines/timelines.html
new file mode 100644
index 0000000000..1609f85939
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/timing-model/timelines/timelines.html
@@ -0,0 +1,144 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>Timelines</title>
+<link rel="help" href="https://drafts.csswg.org/web-animations/#timelines">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../../testcommon.js"></script>
+<style>
+@keyframes opacity-animation {
+ from { opacity: 1; }
+ to { opacity: 0; }
+}
+</style>
+<div id="log"></div>
+<script>
+'use strict';
+
+promise_test(t => {
+ const valueAtStart = document.timeline.currentTime;
+ const timeAtStart = window.performance.now();
+ while (window.performance.now() - timeAtStart < 50) {
+ // Wait 50ms
+ }
+ assert_equals(document.timeline.currentTime, valueAtStart,
+ 'Timeline time does not change within an animation frame');
+ return waitForAnimationFrames(1).then(() => {
+ assert_greater_than(document.timeline.currentTime, valueAtStart,
+ 'Timeline time increases between animation frames');
+ });
+}, 'Timeline time increases once per animation frame');
+
+async_test(t => {
+ const iframe = document.createElement('iframe');
+ iframe.width = 10;
+ iframe.height = 10;
+
+ iframe.addEventListener('load', t.step_func(() => {
+ const iframeTimeline = iframe.contentDocument.timeline;
+ const valueAtStart = iframeTimeline.currentTime;
+ const timeAtStart = window.performance.now();
+ while (iframe.contentWindow.performance.now() - timeAtStart < 50) {
+ // Wait 50ms
+ }
+ assert_equals(iframeTimeline.currentTime, valueAtStart,
+ 'Timeline time within an iframe does not change within an '
+ + ' animation frame');
+
+ iframe.contentWindow.requestAnimationFrame(t.step_func_done(() => {
+ assert_greater_than(iframeTimeline.currentTime, valueAtStart,
+ 'Timeline time within an iframe increases between animation frames');
+ iframe.remove();
+ }));
+ }));
+
+ document.body.appendChild(iframe);
+}, 'Timeline time increases once per animation frame in an iframe');
+
+async_test(t => {
+ const startTime = document.timeline.currentTime;
+ let firstRafTime;
+
+ requestAnimationFrame(() => {
+ t.step(() => {
+ assert_greater_than_equal(document.timeline.currentTime, startTime,
+ 'Timeline time should have progressed');
+ firstRafTime = document.timeline.currentTime;
+ });
+ });
+
+ requestAnimationFrame(() => {
+ t.step(() => {
+ assert_equals(document.timeline.currentTime, firstRafTime,
+ 'Timeline time should be the same');
+ });
+ t.done();
+ });
+}, 'Timeline time should be the same for all RAF callbacks in an animation'
+ + ' frame');
+
+promise_test(async t => {
+ // A microtask checkpoint is run as part of the process of updating
+ // timelines to ensure that any microtasks queued during promise
+ // resolution are handled before dispatching animation events.
+ const div = createDiv(t);
+ const events = [];
+ let microtaskFrameTime = undefined;
+ let finishFrameTime = undefined;
+ const waitForMicrotask = (animation) => {
+ return new Promise(resolve => {
+ queueMicrotask(() => {
+ events.push('microtask');
+ microtaskFrameTime = document.timeline.currentTime;
+ resolve();
+ });
+ });
+ }
+ const waitForFinishEvent = (animation) => {
+ return new Promise(resolve => {
+ animation.onfinish = (event) => {
+ events.push('finish');
+ finishFrameTime = event.timelineTime;
+ resolve();
+ };
+ });
+ }
+
+ await waitForNextFrame();
+
+ const animation = div.animate({ opacity: [0, 1] }, 1000 * MS_PER_SEC);
+ const finishPromise = waitForFinishEvent(animation);
+ await animation.ready;
+
+ // Advance the timing to effect end, to asynchronously queue up a finish task.
+ // Queue up microtask, which must be processed ahead of the finish event.
+ // See "Perform a microtask checkpoint" step in
+ // https://www.w3.org/TR/web-animations-1/#timelines.
+ animation.currentTime = animation.effect.getComputedTiming().duration;
+ const microtaskPromise = waitForMicrotask(animation);
+ await Promise.all([finishPromise, microtaskPromise]);
+ assert_array_equals(events, ['microtask', 'finish']);
+ assert_times_equal(microtaskFrameTime, finishFrameTime);
+
+}, 'Performs a microtask checkpoint after updating timelines');
+
+async_test(t => {
+ const div = createDiv(t);
+ let readyPromiseRan = false;
+ let finishedPromiseRan = false;
+ div.style.animation = 'opacity-animation 1ms';
+ let anim = div.getAnimations()[0];
+ anim.ready.then(t.step_func(() => {
+ readyPromiseRan = true;
+ }));
+ div.addEventListener('animationstart', t.step_func(() => {
+ assert_true(readyPromiseRan, 'It should run ready promise before animationstart event');
+ }));
+ anim.finished.then(t.step_func(() => {
+ finishedPromiseRan = true;
+ }));
+ div.addEventListener('animationend', t.step_func_done(() => {
+ assert_true(finishedPromiseRan, 'It should run finished promise before animationend event');
+ }));
+}, 'Runs finished promise before animation events');
+</script>
diff --git a/testing/web-platform/tests/web-animations/timing-model/timelines/update-and-send-events-replacement.html b/testing/web-platform/tests/web-animations/timing-model/timelines/update-and-send-events-replacement.html
new file mode 100644
index 0000000000..d6ed734831
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/timing-model/timelines/update-and-send-events-replacement.html
@@ -0,0 +1,1017 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>Update animations and send events (replacement)</title>
+<link rel="help" href="https://drafts.csswg.org/web-animations/#update-animations-and-send-events">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../../testcommon.js"></script>
+<style>
+@keyframes opacity-animation {
+ to { opacity: 1 }
+}
+</style>
+<div id="log"></div>
+<script>
+'use strict';
+
+promise_test(async t => {
+ const div = createDiv(t);
+
+ const animA = div.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' });
+ const animB = div.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' });
+ await animA.finished;
+
+ assert_equals(animA.replaceState, 'removed');
+ assert_equals(animB.replaceState, 'active');
+}, 'Removes an animation when another covers the same properties');
+
+promise_test(async t => {
+ const div = createDiv(t);
+
+ const animA = div.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' });
+ await animA.finished;
+
+ assert_equals(animA.replaceState, 'active');
+
+ const animB = div.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' });
+ await animB.finished;
+
+ assert_equals(animA.replaceState, 'removed');
+ assert_equals(animB.replaceState, 'active');
+}, 'Removes an animation after another animation finishes');
+
+promise_test(async t => {
+ const div = createDiv(t);
+
+ const animA = div.animate(
+ { opacity: 1, width: '100px' },
+ { duration: 1, fill: 'forwards' }
+ );
+ await animA.finished;
+
+ assert_equals(animA.replaceState, 'active');
+
+ const animB = div.animate(
+ { width: '200px' },
+ { duration: 1, fill: 'forwards' }
+ );
+ await animB.finished;
+
+ assert_equals(animA.replaceState, 'active');
+ assert_equals(animB.replaceState, 'active');
+
+ const animC = div.animate(
+ { opacity: 0.5 },
+ { duration: 1, fill: 'forwards' }
+ );
+ await animC.finished;
+
+ assert_equals(animA.replaceState, 'removed');
+ assert_equals(animB.replaceState, 'active');
+ assert_equals(animC.replaceState, 'active');
+}, 'Removes an animation after multiple other animations finish');
+
+promise_test(async t => {
+ const div = createDiv(t);
+
+ const animA = div.animate(
+ { opacity: 1 },
+ { duration: 100 * MS_PER_SEC, fill: 'forwards' }
+ );
+ const animB = div.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' });
+ await animB.finished;
+
+ assert_equals(animB.replaceState, 'active');
+ assert_equals(animB.replaceState, 'active');
+
+ // Seek animA to just before it finishes since we want to test the behavior
+ // when the animation finishes by the ticking of the timeline, not by seeking
+ // (that is covered in a separate test).
+
+ animA.currentTime = 99.99 * MS_PER_SEC;
+ await animA.finished;
+
+ assert_equals(animA.replaceState, 'removed');
+ assert_equals(animB.replaceState, 'active');
+}, 'Removes an animation after it finishes');
+
+promise_test(async t => {
+ const div = createDiv(t);
+
+ const animA = div.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' });
+ const animB = div.animate(
+ { opacity: 1 },
+ { duration: 100 * MS_PER_SEC, fill: 'forwards' }
+ );
+ await animA.finished;
+
+ assert_equals(animA.replaceState, 'active');
+ assert_equals(animB.replaceState, 'active');
+
+ animB.finish();
+
+ // Replacement should not happen until the next time the "update animations
+ // and send events" procedure runs.
+
+ assert_equals(animA.replaceState, 'active');
+ assert_equals(animB.replaceState, 'active');
+
+ await waitForNextFrame();
+
+ assert_equals(animA.replaceState, 'removed');
+ assert_equals(animB.replaceState, 'active');
+}, 'Removes an animation after seeking another animation');
+
+promise_test(async t => {
+ const div = createDiv(t);
+
+ const animA = div.animate(
+ { opacity: 1 },
+ { duration: 100 * MS_PER_SEC, fill: 'forwards' }
+ );
+ const animB = div.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' });
+ await animB.finished;
+
+ assert_equals(animA.replaceState, 'active');
+ assert_equals(animB.replaceState, 'active');
+
+ animA.finish();
+
+ // Replacement should not happen until the next time the "update animations
+ // and send events" procedure runs.
+
+ assert_equals(animA.replaceState, 'active');
+ assert_equals(animB.replaceState, 'active');
+
+ await waitForNextFrame();
+
+ assert_equals(animA.replaceState, 'removed');
+ assert_equals(animB.replaceState, 'active');
+}, 'Removes an animation after seeking it');
+
+promise_test(async t => {
+ const div = createDiv(t);
+
+ const animA = div.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' });
+ const animB = div.animate({ opacity: 1 }, 1);
+ await animA.finished;
+
+ assert_equals(animA.replaceState, 'active');
+ assert_equals(animB.replaceState, 'active');
+
+ animB.effect.updateTiming({ fill: 'forwards' });
+
+ // Replacement should not happen until the next time the "update animations
+ // and send events" procedure runs.
+
+ assert_equals(animA.replaceState, 'active');
+ assert_equals(animB.replaceState, 'active');
+
+ await waitForNextFrame();
+
+ assert_equals(animA.replaceState, 'removed');
+ assert_equals(animB.replaceState, 'active');
+}, 'Removes an animation after updating the fill mode of another animation');
+
+promise_test(async t => {
+ const div = createDiv(t);
+
+ const animA = div.animate({ opacity: 1 }, 1);
+ const animB = div.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' });
+ await animA.finished;
+
+ assert_equals(animA.replaceState, 'active');
+ assert_equals(animB.replaceState, 'active');
+
+ animA.effect.updateTiming({ fill: 'forwards' });
+
+ // Replacement should not happen until the next time the "update animations
+ // and send events" procedure runs.
+
+ assert_equals(animA.replaceState, 'active');
+ assert_equals(animB.replaceState, 'active');
+
+ await waitForNextFrame();
+
+ assert_equals(animA.replaceState, 'removed');
+ assert_equals(animB.replaceState, 'active');
+}, 'Removes an animation after updating its fill mode');
+
+promise_test(async t => {
+ const div = createDiv(t);
+
+ const animA = div.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' });
+ const animB = div.animate({ opacity: 1 }, 1);
+ await animA.finished;
+
+ assert_equals(animA.replaceState, 'active');
+ assert_equals(animB.replaceState, 'active');
+
+ animB.effect = new KeyframeEffect(
+ div,
+ { opacity: 1 },
+ {
+ duration: 1,
+ fill: 'forwards',
+ }
+ );
+
+ assert_equals(animA.replaceState, 'active');
+ assert_equals(animB.replaceState, 'active');
+
+ await waitForNextFrame();
+
+ assert_equals(animA.replaceState, 'removed');
+ assert_equals(animB.replaceState, 'active');
+}, "Removes an animation after updating another animation's effect to one with different timing");
+
+promise_test(async t => {
+ const div = createDiv(t);
+
+ const animA = div.animate({ opacity: 1 }, 1);
+ const animB = div.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' });
+ await animB.finished;
+
+ assert_equals(animA.replaceState, 'active');
+ assert_equals(animB.replaceState, 'active');
+
+ animA.effect = new KeyframeEffect(
+ div,
+ { opacity: 1 },
+ {
+ duration: 1,
+ fill: 'forwards',
+ }
+ );
+
+ assert_equals(animA.replaceState, 'active');
+ assert_equals(animB.replaceState, 'active');
+
+ await waitForNextFrame();
+
+ assert_equals(animA.replaceState, 'removed');
+ assert_equals(animB.replaceState, 'active');
+}, 'Removes an animation after updating its effect to one with different timing');
+
+promise_test(async t => {
+ const div = createDiv(t);
+
+ const animA = div.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' });
+ const animB = div.animate(
+ { opacity: 1 },
+ { duration: 100 * MS_PER_SEC, fill: 'forwards' }
+ );
+
+ await animA.finished;
+
+ // Set up a timeline that makes animB finished
+ animB.timeline = new DocumentTimeline({
+ originTime:
+ document.timeline.currentTime - 100 * MS_PER_SEC - animB.startTime,
+ });
+
+ assert_equals(animA.replaceState, 'active');
+ assert_equals(animB.replaceState, 'active');
+
+ await waitForNextFrame();
+
+ assert_equals(animA.replaceState, 'removed');
+ assert_equals(animB.replaceState, 'active');
+}, "Removes an animation after updating another animation's timeline");
+
+promise_test(async t => {
+ const div = createDiv(t);
+
+ const animA = div.animate(
+ { opacity: 1 },
+ { duration: 100 * MS_PER_SEC, fill: 'forwards' }
+ );
+ const animB = div.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' });
+
+ await animB.finished;
+
+ // Set up a timeline that makes animA finished
+ animA.timeline = new DocumentTimeline({
+ originTime:
+ document.timeline.currentTime - 100 * MS_PER_SEC - animA.startTime,
+ });
+
+ assert_equals(animA.replaceState, 'active');
+ assert_equals(animB.replaceState, 'active');
+
+ await waitForNextFrame();
+
+ assert_equals(animA.replaceState, 'removed');
+ assert_equals(animB.replaceState, 'active');
+}, 'Removes an animation after updating its timeline');
+
+promise_test(async t => {
+ const div = createDiv(t);
+
+ const animA = div.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' });
+ const animB = div.animate(
+ { width: '100px' },
+ { duration: 1, fill: 'forwards' }
+ );
+ await animA.finished;
+
+ assert_equals(animA.replaceState, 'active');
+ assert_equals(animB.replaceState, 'active');
+
+ animB.effect.setKeyframes({ width: '100px', opacity: 1 });
+
+ assert_equals(animA.replaceState, 'active');
+ assert_equals(animB.replaceState, 'active');
+
+ await waitForNextFrame();
+
+ assert_equals(animA.replaceState, 'removed');
+ assert_equals(animB.replaceState, 'active');
+}, "Removes an animation after updating another animation's effect's properties");
+
+promise_test(async t => {
+ const div = createDiv(t);
+
+ const animA = div.animate(
+ { opacity: 1, width: '100px' },
+ { duration: 1, fill: 'forwards' }
+ );
+ const animB = div.animate(
+ { width: '200px' },
+ { duration: 1, fill: 'forwards' }
+ );
+ await animA.finished;
+
+ assert_equals(animA.replaceState, 'active');
+ assert_equals(animB.replaceState, 'active');
+
+ animA.effect.setKeyframes({ width: '100px' });
+
+ assert_equals(animA.replaceState, 'active');
+ assert_equals(animB.replaceState, 'active');
+
+ await waitForNextFrame();
+
+ assert_equals(animA.replaceState, 'removed');
+ assert_equals(animB.replaceState, 'active');
+}, "Removes an animation after updating its effect's properties");
+
+promise_test(async t => {
+ const div = createDiv(t);
+
+ const animA = div.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' });
+ const animB = div.animate(
+ { width: '100px' },
+ { duration: 1, fill: 'forwards' }
+ );
+ await animA.finished;
+
+ assert_equals(animA.replaceState, 'active');
+ assert_equals(animB.replaceState, 'active');
+
+ animB.effect = new KeyframeEffect(
+ div,
+ { width: '100px', opacity: 1 },
+ { duration: 1, fill: 'forwards' }
+ );
+
+ assert_equals(animA.replaceState, 'active');
+ assert_equals(animB.replaceState, 'active');
+
+ await waitForNextFrame();
+
+ assert_equals(animA.replaceState, 'removed');
+ assert_equals(animB.replaceState, 'active');
+}, "Removes an animation after updating another animation's effect to one with different properties");
+
+promise_test(async t => {
+ const div = createDiv(t);
+
+ const animA = div.animate(
+ { opacity: 1, width: '100px' },
+ { duration: 1, fill: 'forwards' }
+ );
+ const animB = div.animate(
+ { width: '200px' },
+ { duration: 1, fill: 'forwards' }
+ );
+ await animA.finished;
+
+ assert_equals(animA.replaceState, 'active');
+ assert_equals(animB.replaceState, 'active');
+
+ animA.effect = new KeyframeEffect(
+ div,
+ { width: '100px' },
+ {
+ duration: 1,
+ fill: 'forwards',
+ }
+ );
+
+ assert_equals(animA.replaceState, 'active');
+ assert_equals(animB.replaceState, 'active');
+
+ await waitForNextFrame();
+
+ assert_equals(animA.replaceState, 'removed');
+ assert_equals(animB.replaceState, 'active');
+}, 'Removes an animation after updating its effect to one with different properties');
+
+promise_test(async t => {
+ const div = createDiv(t);
+
+ const animA = div.animate(
+ { marginLeft: '10px' },
+ { duration: 1, fill: 'forwards' }
+ );
+ const animB = div.animate(
+ { margin: '20px' },
+ { duration: 1, fill: 'forwards' }
+ );
+ await animA.finished;
+
+ assert_equals(animA.replaceState, 'removed');
+ assert_equals(animB.replaceState, 'active');
+}, 'Removes an animation when another animation uses a shorthand');
+
+promise_test(async t => {
+ const div = createDiv(t);
+
+ const animA = div.animate(
+ { margin: '10px' },
+ { duration: 1, fill: 'forwards' }
+ );
+ const animB = div.animate(
+ {
+ marginLeft: '10px',
+ marginTop: '20px',
+ marginRight: '30px',
+ marginBottom: '40px',
+ },
+ { duration: 1, fill: 'forwards' }
+ );
+ await animA.finished;
+
+ assert_equals(animA.replaceState, 'removed');
+ assert_equals(animB.replaceState, 'active');
+}, 'Removes an animation that uses a shorthand');
+
+promise_test(async t => {
+ const div = createDiv(t);
+
+ const animA = div.animate(
+ { marginLeft: '10px' },
+ { duration: 1, fill: 'forwards' }
+ );
+ const animB = div.animate(
+ { marginInlineStart: '20px' },
+ { duration: 1, fill: 'forwards' }
+ );
+ await animA.finished;
+
+ assert_equals(animA.replaceState, 'removed');
+ assert_equals(animB.replaceState, 'active');
+}, 'Removes an animation by another animation using logical properties');
+
+promise_test(async t => {
+ const div = createDiv(t);
+
+ const animA = div.animate(
+ { marginInlineStart: '10px' },
+ { duration: 1, fill: 'forwards' }
+ );
+ const animB = div.animate(
+ { marginLeft: '20px' },
+ { duration: 1, fill: 'forwards' }
+ );
+ await animA.finished;
+
+ assert_equals(animA.replaceState, 'removed');
+ assert_equals(animB.replaceState, 'active');
+}, 'Removes an animation using logical properties');
+
+promise_test(async t => {
+ const div = createDiv(t);
+
+ const animA = div.animate(
+ { marginTop: '10px' },
+ { duration: 1, fill: 'forwards' }
+ );
+ const animB = div.animate(
+ { marginInlineStart: '20px' },
+ { duration: 1, fill: 'forwards' }
+ );
+ await animA.finished;
+
+ assert_equals(animA.replaceState, 'active');
+ assert_equals(animB.replaceState, 'active');
+
+ div.style.writingMode = 'vertical-rl';
+
+ assert_equals(animA.replaceState, 'active');
+ assert_equals(animB.replaceState, 'active');
+
+ await waitForNextFrame();
+
+ assert_equals(animA.replaceState, 'removed');
+ assert_equals(animB.replaceState, 'active');
+}, 'Removes an animation by another animation using logical properties after updating the context');
+
+promise_test(async t => {
+ const divA = createDiv(t);
+ const divB = createDiv(t);
+
+ const animA = divA.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' });
+ const animB = divB.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' });
+ await animA.finished;
+
+ assert_equals(animA.replaceState, 'active');
+ assert_equals(animB.replaceState, 'active');
+
+ animB.effect.target = divA;
+
+ assert_equals(animA.replaceState, 'active');
+ assert_equals(animB.replaceState, 'active');
+
+ await waitForNextFrame();
+
+ assert_equals(animA.replaceState, 'removed');
+ assert_equals(animB.replaceState, 'active');
+}, "Removes an animation after updating another animation's effect's target");
+
+promise_test(async t => {
+ const divA = createDiv(t);
+ const divB = createDiv(t);
+
+ const animA = divA.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' });
+ const animB = divB.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' });
+ await animA.finished;
+
+ assert_equals(animA.replaceState, 'active');
+ assert_equals(animB.replaceState, 'active');
+
+ animA.effect.target = divB;
+
+ assert_equals(animA.replaceState, 'active');
+ assert_equals(animB.replaceState, 'active');
+
+ await waitForNextFrame();
+
+ assert_equals(animA.replaceState, 'removed');
+ assert_equals(animB.replaceState, 'active');
+}, "Removes an animation after updating its effect's target");
+
+promise_test(async t => {
+ const divA = createDiv(t);
+ const divB = createDiv(t);
+
+ const animA = divA.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' });
+ const animB = divB.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' });
+ await animA.finished;
+
+ assert_equals(animA.replaceState, 'active');
+ assert_equals(animB.replaceState, 'active');
+
+ animB.effect = new KeyframeEffect(
+ divA,
+ { opacity: 1 },
+ {
+ duration: 1,
+ fill: 'forwards',
+ }
+ );
+
+ assert_equals(animA.replaceState, 'active');
+ assert_equals(animB.replaceState, 'active');
+
+ await waitForNextFrame();
+
+ assert_equals(animA.replaceState, 'removed');
+ assert_equals(animB.replaceState, 'active');
+}, "Removes an animation after updating another animation's effect to one with a different target");
+
+promise_test(async t => {
+ const divA = createDiv(t);
+ const divB = createDiv(t);
+
+ const animA = divA.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' });
+ const animB = divB.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' });
+ await animA.finished;
+
+ assert_equals(animA.replaceState, 'active');
+ assert_equals(animB.replaceState, 'active');
+
+ animA.effect = new KeyframeEffect(
+ divB,
+ { opacity: 1 },
+ {
+ duration: 1,
+ fill: 'forwards',
+ }
+ );
+
+ assert_equals(animA.replaceState, 'active');
+ assert_equals(animB.replaceState, 'active');
+
+ await waitForNextFrame();
+
+ assert_equals(animA.replaceState, 'removed');
+ assert_equals(animB.replaceState, 'active');
+}, 'Removes an animation after updating its effect to one with a different target');
+
+promise_test(async t => {
+ const div = createDiv(t);
+ div.style.animation = 'opacity-animation 1ms forwards';
+ const cssAnimation = div.getAnimations()[0];
+
+ const scriptAnimation = div.animate(
+ { opacity: 1 },
+ {
+ duration: 1,
+ fill: 'forwards',
+ }
+ );
+ await scriptAnimation.finished;
+
+ assert_equals(cssAnimation.replaceState, 'active');
+ assert_equals(scriptAnimation.replaceState, 'active');
+}, 'Does NOT remove a CSS animation tied to markup');
+
+promise_test(async t => {
+ const div = createDiv(t);
+ div.style.animation = 'opacity-animation 1ms forwards';
+ const cssAnimation = div.getAnimations()[0];
+
+ // Break tie to markup
+ div.style.animationName = 'none';
+ assert_equals(cssAnimation.playState, 'idle');
+
+ // Restart animation
+ cssAnimation.play();
+
+ const scriptAnimation = div.animate(
+ { opacity: 1 },
+ {
+ duration: 1,
+ fill: 'forwards',
+ }
+ );
+ await scriptAnimation.finished;
+
+ assert_equals(cssAnimation.replaceState, 'removed');
+ assert_equals(scriptAnimation.replaceState, 'active');
+}, 'Removes a CSS animation no longer tied to markup');
+
+promise_test(async t => {
+ // Setup transition
+ const div = createDiv(t);
+ div.style.opacity = '0';
+ div.style.transition = 'opacity 1ms';
+ getComputedStyle(div).opacity;
+ div.style.opacity = '1';
+ const cssTransition = div.getAnimations()[0];
+ cssTransition.effect.updateTiming({ fill: 'forwards' });
+
+ const scriptAnimation = div.animate(
+ { opacity: 1 },
+ {
+ duration: 1,
+ fill: 'forwards',
+ }
+ );
+ await scriptAnimation.finished;
+
+ assert_equals(cssTransition.replaceState, 'active');
+ assert_equals(scriptAnimation.replaceState, 'active');
+}, 'Does NOT remove a CSS transition tied to markup');
+
+promise_test(async t => {
+ // Setup transition
+ const div = createDiv(t);
+ div.style.opacity = '0';
+ div.style.transition = 'opacity 1ms';
+ getComputedStyle(div).opacity;
+ div.style.opacity = '1';
+ const cssTransition = div.getAnimations()[0];
+ cssTransition.effect.updateTiming({ fill: 'forwards' });
+
+ // Break tie to markup
+ div.style.transitionProperty = 'none';
+ assert_equals(cssTransition.playState, 'idle');
+
+ // Restart transition
+ cssTransition.play();
+
+ const scriptAnimation = div.animate(
+ { opacity: 1 },
+ {
+ duration: 1,
+ fill: 'forwards',
+ }
+ );
+ await scriptAnimation.finished;
+
+ assert_equals(cssTransition.replaceState, 'removed');
+ assert_equals(scriptAnimation.replaceState, 'active');
+}, 'Removes a CSS transition no longer tied to markup');
+
+promise_test(async t => {
+ const div = createDiv(t);
+
+ const animA = div.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' });
+ const animB = div.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' });
+ const eventWatcher = new EventWatcher(t, animA, 'remove');
+
+ const event = await eventWatcher.wait_for('remove');
+
+ assert_times_equal(event.timelineTime, document.timeline.currentTime);
+ assert_times_equal(event.currentTime, 1);
+}, 'Dispatches an event when removing');
+
+promise_test(async t => {
+ const div = createDiv(t);
+
+ const animA = div.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' });
+ const animB = div.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' });
+ const eventWatcher = new EventWatcher(t, animA, 'remove');
+
+ await eventWatcher.wait_for('remove');
+
+ // Check we don't get another event
+ animA.addEventListener(
+ 'remove',
+ t.step_func(() => {
+ assert_unreached('remove event should not be fired a second time');
+ })
+ );
+
+ // Restart animation
+ animA.play();
+
+ await waitForNextFrame();
+
+ // Finish animation
+ animA.finish();
+
+ await waitForNextFrame();
+}, 'Does NOT dispatch a remove event twice');
+
+promise_test(async t => {
+ const div = createDiv(t);
+
+ const animA = div.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' });
+ const animB = div.animate(
+ { opacity: 1 },
+ { duration: 100 * MS_PER_SEC, fill: 'forwards' }
+ );
+ await animA.finished;
+
+ assert_equals(animA.replaceState, 'active');
+
+ animB.finish();
+ animB.currentTime = 0;
+
+ await waitForNextFrame();
+
+ assert_equals(animA.replaceState, 'active');
+}, "Does NOT remove an animation after making a redundant change to another animation's current time");
+
+promise_test(async t => {
+ const div = createDiv(t);
+
+ const animA = div.animate(
+ { opacity: 1 },
+ { duration: 100 * MS_PER_SEC, fill: 'forwards' }
+ );
+ const animB = div.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' });
+ await animB.finished;
+
+ assert_equals(animA.replaceState, 'active');
+
+ animA.finish();
+ animA.currentTime = 0;
+
+ await waitForNextFrame();
+
+ assert_equals(animA.replaceState, 'active');
+}, 'Does NOT remove an animation after making a redundant change to its current time');
+
+promise_test(async t => {
+ const div = createDiv(t);
+
+ const animA = div.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' });
+ const animB = div.animate(
+ { opacity: 1 },
+ { duration: 100 * MS_PER_SEC, fill: 'forwards' }
+ );
+ await animA.finished;
+
+ assert_equals(animA.replaceState, 'active');
+
+ // Set up a timeline that makes animB finished but then restore it
+ animB.timeline = new DocumentTimeline({
+ originTime:
+ document.timeline.currentTime - 100 * MS_PER_SEC - animB.startTime,
+ });
+ animB.timeline = document.timeline;
+
+ await waitForNextFrame();
+
+ assert_equals(animA.replaceState, 'active');
+}, "Does NOT remove an animation after making a redundant change to another animation's timeline");
+
+promise_test(async t => {
+ const div = createDiv(t);
+
+ const animA = div.animate(
+ { opacity: 1 },
+ { duration: 100 * MS_PER_SEC, fill: 'forwards' }
+ );
+ const animB = div.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' });
+ await animB.finished;
+
+ assert_equals(animA.replaceState, 'active');
+
+ // Set up a timeline that makes animA finished but then restore it
+ animA.timeline = new DocumentTimeline({
+ originTime:
+ document.timeline.currentTime - 100 * MS_PER_SEC - animA.startTime,
+ });
+ animA.timeline = document.timeline;
+
+ await waitForNextFrame();
+
+ assert_equals(animA.replaceState, 'active');
+}, 'Does NOT remove an animation after making a redundant change to its timeline');
+
+promise_test(async t => {
+ const div = createDiv(t);
+ const animA = div.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' });
+ const animB = div.animate(
+ { marginLeft: '100px' },
+ {
+ duration: 1,
+ fill: 'forwards',
+ }
+ );
+ await animA.finished;
+
+ assert_equals(animA.replaceState, 'active');
+
+ // Redundant change
+ animB.effect.setKeyframes({ marginLeft: '100px', opacity: 1 });
+ animB.effect.setKeyframes({ marginLeft: '100px' });
+
+ await waitForNextFrame();
+
+ assert_equals(animA.replaceState, 'active');
+}, "Does NOT remove an animation after making a redundant change to another animation's effect's properties");
+
+promise_test(async t => {
+ const div = createDiv(t);
+ const animA = div.animate(
+ { marginLeft: '100px' },
+ {
+ duration: 1,
+ fill: 'forwards',
+ }
+ );
+ const animB = div.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' });
+ await animA.finished;
+
+ assert_equals(animA.replaceState, 'active');
+
+ // Redundant change
+ animA.effect.setKeyframes({ opacity: 1 });
+ animA.effect.setKeyframes({ marginLeft: '100px' });
+
+ await waitForNextFrame();
+
+ assert_equals(animA.replaceState, 'active');
+}, "Does NOT remove an animation after making a redundant change to its effect's properties");
+
+promise_test(async t => {
+ const div = createDiv(t);
+
+ const animA = div.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' });
+ const animB = div.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' });
+ animB.timeline = new DocumentTimeline();
+
+ await animA.finished;
+
+ // If, for example, we only update the timeline for animA before checking
+ // replacement state, then animB will not be finished and animA will not be
+ // replaced.
+
+ assert_equals(animA.replaceState, 'removed');
+ assert_equals(animB.replaceState, 'active');
+}, 'Updates ALL timelines before checking for replacement');
+
+promise_test(async t => {
+ const div = createDiv(t);
+ const animA = div.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' });
+ const animB = div.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' });
+
+ const events = [];
+ const logEvent = (targetName, eventType) => {
+ events.push(`${targetName}:${eventType}`);
+ };
+
+ animA.addEventListener('finish', () => logEvent('animA', 'finish'));
+ animA.addEventListener('remove', () => logEvent('animA', 'remove'));
+ animB.addEventListener('finish', () => logEvent('animB', 'finish'));
+ animB.addEventListener('remove', () => logEvent('animB', 'remove'));
+
+ await animA.finished;
+
+ // Allow all events to be dispatched
+
+ await waitForNextFrame();
+
+ assert_array_equals(events, [
+ 'animA:finish',
+ 'animB:finish',
+ 'animA:remove',
+ ]);
+}, 'Dispatches remove events after finish events');
+
+promise_test(async t => {
+ const div = createDiv(t);
+ const animA = div.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' });
+ const animB = div.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' });
+
+ const eventWatcher = new EventWatcher(t, animA, 'remove');
+
+ await animA.finished;
+
+ let rAFReceived = false;
+ requestAnimationFrame(() => (rAFReceived = true));
+
+ await eventWatcher.wait_for('remove');
+
+ assert_false(
+ rAFReceived,
+ 'remove event should be fired before requestAnimationFrame'
+ );
+}, 'Fires remove event before requestAnimationFrame');
+
+promise_test(async t => {
+ const div = createDiv(t);
+ const animA = div.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' });
+ const animB = div.animate(
+ { width: '100px' },
+ { duration: 1, fill: 'forwards' }
+ );
+ const animC = div.animate(
+ { opacity: 0.5, width: '200px' },
+ { duration: 1, fill: 'forwards' }
+ );
+
+ // In the event handler for animA (which should be fired before that of animB)
+ // we make a change to animC so that it no longer covers animB.
+ //
+ // If the remove event for animB is not already queued by this point, it will
+ // fail to fire.
+ animA.addEventListener('remove', () => {
+ animC.effect.setKeyframes({
+ opacity: 0.5,
+ });
+ });
+
+ const eventWatcher = new EventWatcher(t, animB, 'remove');
+ await eventWatcher.wait_for('remove');
+
+ assert_equals(animA.replaceState, 'removed');
+ assert_equals(animB.replaceState, 'removed');
+ assert_equals(animC.replaceState, 'active');
+}, 'Queues all remove events before running them');
+
+promise_test(async t => {
+ const outerIframe = document.createElement('iframe');
+ outerIframe.width = 10;
+ outerIframe.height = 10;
+ await insertFrameAndAwaitLoad(t, outerIframe, document);
+
+ const innerIframe = document.createElement('iframe');
+ innerIframe.width = 10;
+ innerIframe.height = 10;
+ await insertFrameAndAwaitLoad(t, innerIframe, outerIframe.contentDocument);
+
+ const div = createDiv(t, innerIframe.contentDocument);
+
+ const animA = div.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' });
+ const animB = div.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' });
+
+ // Sanity check: The timeline for these animations should be the default
+ // document timeline for div.
+ assert_equals(animA.timeline, innerIframe.contentDocument.timeline);
+ assert_equals(animB.timeline, innerIframe.contentDocument.timeline);
+
+ await animA.finished;
+
+ assert_equals(animA.replaceState, 'removed');
+ assert_equals(animB.replaceState, 'active');
+}, 'Performs removal in deeply nested iframes');
+
+</script>
diff --git a/testing/web-platform/tests/web-animations/timing-model/timelines/update-and-send-events.html b/testing/web-platform/tests/web-animations/timing-model/timelines/update-and-send-events.html
new file mode 100644
index 0000000000..255e013f27
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/timing-model/timelines/update-and-send-events.html
@@ -0,0 +1,257 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>Update animations and send events</title>
+<meta name="timeout" content="long">
+<link rel="help" href="https://drafts.csswg.org/web-animations/#update-animations-and-send-events">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../../testcommon.js"></script>
+<div id="log"></div>
+<script>
+'use strict';
+
+promise_test(async t => {
+ const div = createDiv(t);
+ const animation = div.animate(null, 100 * MS_PER_SEC);
+
+ // The ready promise should be resolved as part of micro-task checkpoint
+ // after updating the current time of all timeslines in the procedure to
+ // "update animations and send events".
+ await animation.ready;
+
+ let rAFReceived = false;
+ requestAnimationFrame(() => rAFReceived = true);
+
+ const eventWatcher = new EventWatcher(t, animation, 'cancel');
+ animation.cancel();
+
+ await eventWatcher.wait_for('cancel');
+
+ assert_false(rAFReceived,
+ 'cancel event should be fired before requestAnimationFrame');
+}, 'Fires cancel event before requestAnimationFrame');
+
+promise_test(async t => {
+ const div = createDiv(t);
+ const animation = div.animate(null, 100 * MS_PER_SEC);
+
+ // Like the above test, the ready promise should be resolved micro-task
+ // checkpoint after updating the current time of all timeslines in the
+ // procedure to "update animations and send events".
+ await animation.ready;
+
+ let rAFReceived = false;
+ requestAnimationFrame(() => rAFReceived = true);
+
+ const eventWatcher = new EventWatcher(t, animation, 'finish');
+ animation.finish();
+
+ await eventWatcher.wait_for('finish');
+
+ assert_false(rAFReceived,
+ 'finish event should be fired before requestAnimationFrame');
+}, 'Fires finish event before requestAnimationFrame');
+
+function animationType(anim) {
+ if (anim instanceof CSSAnimation) {
+ return 'CSSAnimation';
+ } else if (anim instanceof CSSTransition) {
+ return 'CSSTransition';
+ } else {
+ return 'ScriptAnimation';
+ }
+}
+
+promise_test(async t => {
+ createStyle(t, { '@keyframes anim': '' });
+ const div = createDiv(t);
+
+ getComputedStyle(div).marginLeft;
+ div.style = 'animation: anim 100s; ' +
+ 'transition: margin-left 100s; ' +
+ 'margin-left: 100px;';
+ div.animate(null, 100 * MS_PER_SEC);
+ const animations = div.getAnimations();
+
+ let receivedEvents = [];
+ animations.forEach(anim => {
+ anim.onfinish = event => {
+ receivedEvents.push({
+ type: animationType(anim) + ':' + event.type,
+ timeStamp: event.timeStamp
+ });
+ };
+ });
+
+ await Promise.all(animations.map(anim => anim.ready));
+
+ // Setting current time to the time just before the effect end.
+ animations.forEach(anim => anim.currentTime = 100 * MS_PER_SEC - 1);
+
+ await waitForNextFrame();
+
+ assert_array_equals(receivedEvents.map(event => event.type),
+ [ 'CSSTransition:finish', 'CSSAnimation:finish',
+ 'ScriptAnimation:finish' ],
+ 'finish events for various animation type should be sorted by composite ' +
+ 'order');
+}, 'Sorts finish events by composite order');
+
+promise_test(async t => {
+ createStyle(t, { '@keyframes anim': '' });
+ const div = createDiv(t);
+
+ let receivedEvents = [];
+ function receiveEvent(type, timeStamp) {
+ receivedEvents.push({ type, timeStamp });
+ }
+
+ div.onanimationcancel = event => receiveEvent(event.type, event.timeStamp);
+ div.ontransitioncancel = event => receiveEvent(event.type, event.timeStamp);
+
+ getComputedStyle(div).marginLeft;
+ div.style = 'animation: anim 100s; ' +
+ 'transition: margin-left 100s; ' +
+ 'margin-left: 100px;';
+ div.animate(null, 100 * MS_PER_SEC);
+ const animations = div.getAnimations();
+
+ animations.forEach(anim => {
+ anim.oncancel = event => {
+ receiveEvent(animationType(anim) + ':' + event.type, event.timeStamp);
+ };
+ });
+
+ await Promise.all(animations.map(anim => anim.ready));
+
+ const timeInAnimationReady = document.timeline.currentTime;
+
+ // Call cancel() in reverse composite order. I.e. canceling for script
+ // animation happen first, then for CSS animation and CSS transition.
+ // 'cancel' events for these animations should be sorted by composite
+ // order.
+ animations.reverse().forEach(anim => anim.cancel());
+
+ // requestAnimationFrame callback which is actually the _same_ frame since we
+ // are currently operating in the `ready` callbac of the animations which
+ // happens as part of the "Update animations and send events" procedure
+ // _before_ we run animation frame callbacks.
+ await waitForAnimationFrames(1);
+
+ assert_times_equal(timeInAnimationReady, document.timeline.currentTime,
+ 'A rAF callback should happen in the same frame');
+
+ assert_array_equals(receivedEvents.map(event => event.type),
+ // This ordering needs more clarification in the spec, but the intention is
+ // that the cancel playback event fires before the equivalent CSS cancel
+ // event in each case.
+ [ 'CSSTransition:cancel', 'CSSAnimation:cancel', 'ScriptAnimation:cancel',
+ 'transitioncancel', 'animationcancel' ],
+ 'cancel events should be sorted by composite order');
+}, 'Sorts cancel events by composite order');
+
+promise_test(async t => {
+ const div = createDiv(t);
+ getComputedStyle(div).marginLeft;
+ div.style = 'transition: margin-left 100s; margin-left: 100px;';
+ const anim = div.getAnimations()[0];
+
+ let receivedEvents = [];
+ anim.oncancel = event => receivedEvents.push(event);
+
+ const eventWatcher = new EventWatcher(t, div, 'transitionstart');
+ await eventWatcher.wait_for('transitionstart');
+
+ const timeInEventCallback = document.timeline.currentTime;
+
+ // Calling cancel() queues a cancel event
+ anim.cancel();
+
+ await waitForAnimationFrames(1);
+ assert_times_equal(timeInEventCallback, document.timeline.currentTime,
+ 'A rAF callback should happen in the same frame');
+
+ assert_array_equals(receivedEvents, [],
+ 'The queued cancel event shouldn\'t be dispatched in the same frame');
+
+ await waitForAnimationFrames(1);
+ assert_array_equals(receivedEvents.map(event => event.type), ['cancel'],
+ 'The cancel event should be dispatched in a later frame');
+}, 'Queues a cancel event in transitionstart event callback');
+
+promise_test(async t => {
+ const div = createDiv(t);
+ getComputedStyle(div).marginLeft;
+ div.style = 'transition: margin-left 100s; margin-left: 100px;';
+ const anim = div.getAnimations()[0];
+
+ let receivedEvents = [];
+ anim.oncancel = event => receivedEvents.push(event);
+ div.ontransitioncancel = event => receivedEvents.push(event);
+
+ await anim.ready;
+
+ anim.cancel();
+
+ await waitForAnimationFrames(1);
+
+ assert_array_equals(receivedEvents.map(event => event.type),
+ [ 'cancel', 'transitioncancel' ],
+ 'Playback and CSS events for the same transition should be sorted by ' +
+ 'schedule event time and composite order');
+}, 'Sorts events for the same transition');
+
+promise_test(async t => {
+ const div = createDiv(t);
+ const anim = div.animate(null, 100 * MS_PER_SEC);
+
+ let receivedEvents = [];
+ anim.oncancel = event => receivedEvents.push(event);
+ anim.onfinish = event => receivedEvents.push(event);
+
+ await anim.ready;
+
+ anim.finish();
+ anim.cancel();
+
+ await waitForAnimationFrames(1);
+
+ assert_array_equals(receivedEvents.map(event => event.type),
+ [ 'finish', 'cancel' ],
+ 'Calling finish() synchronously queues a finish event when updating the ' +
+ 'finish state so it should appear before the cancel event');
+}, 'Playback events with the same timeline retain the order in which they are' +
+ 'queued');
+
+promise_test(async t => {
+ const div = createDiv(t);
+
+ // Create two animations with separate timelines
+
+ const timelineA = document.timeline;
+ const animA = div.animate(null, 100 * MS_PER_SEC);
+
+ const timelineB = new DocumentTimeline();
+ const animB = new Animation(
+ new KeyframeEffect(div, null, 100 * MS_PER_SEC),
+ timelineB
+ );
+ animB.play();
+
+ animA.currentTime = 99.9 * MS_PER_SEC;
+ animB.currentTime = 99.9 * MS_PER_SEC;
+
+ // When the next tick happens both animations should be updated, and we will
+ // notice that they are now finished. As a result their finished promise
+ // callbacks should be queued. All of that should happen before we run the
+ // next microtask checkpoint and actually run the promise callbacks and
+ // hence the calls to cancel should not stop the existing callbacks from
+ // being run.
+
+ animA.finished.then(() => { animB.cancel() });
+ animB.finished.then(() => { animA.cancel() });
+
+ await Promise.all([animA.finished, animB.finished]);
+}, 'All timelines are updated before running microtasks');
+
+</script>