summaryrefslogtreecommitdiffstats
path: root/testing/web-platform/tests/web-animations/interfaces/Animation
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 09:22:09 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 09:22:09 +0000
commit43a97878ce14b72f0981164f87f2e35e14151312 (patch)
tree620249daf56c0258faa40cbdcf9cfba06de2a846 /testing/web-platform/tests/web-animations/interfaces/Animation
parentInitial commit. (diff)
downloadfirefox-upstream.tar.xz
firefox-upstream.zip
Adding upstream version 110.0.1.upstream/110.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'testing/web-platform/tests/web-animations/interfaces/Animation')
-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.html113
-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.html371
18 files changed, 2288 insertions, 0 deletions
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..d599fd72ea
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/interfaces/Animation/constructor.html
@@ -0,0 +1,113 @@
+<!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');
+
+</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..b41f748720
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/interfaces/Animation/style-change-events.html
@@ -0,0 +1,371 @@
+<!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),
+ 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>