summaryrefslogtreecommitdiffstats
path: root/testing/web-platform/tests/web-animations/interfaces
diff options
context:
space:
mode:
Diffstat (limited to 'testing/web-platform/tests/web-animations/interfaces')
-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.html346
-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.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
-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.html266
39 files changed, 5944 insertions, 0 deletions
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..dad633ba9a
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/interfaces/Animatable/animate.html
@@ -0,0 +1,346 @@
+<!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',
+ '::placeholder',
+]) {
+ 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`);
+}
+
+</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..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>
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..30b2ee6f0c
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/interfaces/KeyframeEffect/target.html
@@ -0,0 +1,266 @@
+<!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',
+ '::placeholder',
+]) {
+ 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`);
+}
+
+</script>
+</body>