summaryrefslogtreecommitdiffstats
path: root/testing/web-platform/tests/web-animations/interfaces/KeyframeEffect
diff options
context:
space:
mode:
Diffstat (limited to 'testing/web-platform/tests/web-animations/interfaces/KeyframeEffect')
-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
10 files changed, 1686 insertions, 0 deletions
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>