| <!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', |
| 'textOrientation', |
| 'unicodeBidi', |
| 'willChange', |
| 'writingMode', |
| |
| 'unsupportedProperty', |
| |
| '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(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({ name: '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(() => { |
| 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(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(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({ name: '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> |