diff options
Diffstat (limited to 'testing/web-platform/tests/web-animations/interfaces/KeyframeEffect')
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> |