processing-a-keyframes-argument-001.html (17191B)
1 <!DOCTYPE html> 2 <meta charset=utf-8> 3 <title>Processing a keyframes argument (property access)</title> 4 <link rel="help" href="https://drafts.csswg.org/web-animations/#processing-a-keyframes-argument"> 5 <script src="/resources/testharness.js"></script> 6 <script src="/resources/testharnessreport.js"></script> 7 <script src="../../testcommon.js"></script> 8 <script src="../../resources/keyframe-utils.js"></script> 9 <body> 10 <div id="log"></div> 11 <div id="target"></div> 12 <script> 13 'use strict'; 14 15 // This file only tests the KeyframeEffect constructor since it is 16 // assumed that the implementation of the KeyframeEffect constructor, 17 // Animatable.animate() method, and KeyframeEffect.setKeyframes() method will 18 // all share common machinery and it is not necessary to test each method. 19 20 // Test that only animatable properties are accessed 21 22 const gNonAnimatableProps = [ 23 'animation', // Shorthands where all the longhand sub-properties are not 24 // animatable, are also not animatable. 25 'animationDelay', 26 'animationDirection', 27 'animationDuration', 28 'animationFillMode', 29 'animationIterationCount', 30 'animationName', 31 'animationPlayState', 32 'animationTimingFunction', 33 'transition', 34 'transitionDelay', 35 'transitionDuration', 36 'transitionProperty', 37 'transitionTimingFunction', 38 'contain', 39 'direction', 40 'textCombineUpright', 41 'textOrientation', 42 'unicodeBidi', 43 'willChange', 44 'writingMode', 45 46 'unsupportedProperty', 47 48 'float', // We use the string "cssFloat" to represent "float" property, and 49 // so reject "float" in the keyframe-like object. 50 'font-size', // Supported property that uses dashes 51 ]; 52 53 function TestKeyframe(testProp) { 54 let _propAccessCount = 0; 55 56 Object.defineProperty(this, testProp, { 57 get: () => { _propAccessCount++; }, 58 enumerable: true, 59 }); 60 61 Object.defineProperty(this, 'propAccessCount', { 62 get: () => _propAccessCount 63 }); 64 } 65 66 function GetTestKeyframeSequence(testProp) { 67 return [ new TestKeyframe(testProp) ] 68 } 69 70 for (const prop of gNonAnimatableProps) { 71 test(() => { 72 const testKeyframe = new TestKeyframe(prop); 73 74 new KeyframeEffect(null, testKeyframe); 75 76 assert_equals(testKeyframe.propAccessCount, 0, 'Accessor not called'); 77 }, `non-animatable property '${prop}' is not accessed when using` 78 + ' a property-indexed keyframe object'); 79 } 80 81 for (const prop of gNonAnimatableProps) { 82 test(() => { 83 const testKeyframes = GetTestKeyframeSequence(prop); 84 85 new KeyframeEffect(null, testKeyframes); 86 87 assert_equals(testKeyframes[0].propAccessCount, 0, 'Accessor not called'); 88 }, `non-animatable property '${prop}' is not accessed when using` 89 + ' a keyframe sequence'); 90 } 91 92 // Test equivalent forms of property-indexed and sequenced keyframe syntax 93 94 function assertEquivalentKeyframeSyntax(keyframesA, keyframesB) { 95 const processedKeyframesA = 96 new KeyframeEffect(null, keyframesA).getKeyframes(); 97 const processedKeyframesB = 98 new KeyframeEffect(null, keyframesB).getKeyframes(); 99 assert_frame_lists_equal(processedKeyframesA, processedKeyframesB); 100 } 101 102 const gEquivalentSyntaxTests = [ 103 { 104 description: 'two properties with one value', 105 indexedKeyframes: { 106 left: '100px', 107 opacity: ['1'], 108 }, 109 sequencedKeyframes: [ 110 { left: '100px', opacity: '1' }, 111 ], 112 }, 113 { 114 description: 'two properties with three values', 115 indexedKeyframes: { 116 left: ['10px', '100px', '150px'], 117 opacity: ['1', '0', '1'], 118 }, 119 sequencedKeyframes: [ 120 { left: '10px', opacity: '1' }, 121 { left: '100px', opacity: '0' }, 122 { left: '150px', opacity: '1' }, 123 ], 124 }, 125 { 126 description: 'two properties with different numbers of values', 127 indexedKeyframes: { 128 left: ['0px', '100px', '200px'], 129 opacity: ['0', '1'] 130 }, 131 sequencedKeyframes: [ 132 { left: '0px', opacity: '0' }, 133 { left: '100px' }, 134 { left: '200px', opacity: '1' }, 135 ], 136 }, 137 { 138 description: 'same easing applied to all keyframes', 139 indexedKeyframes: { 140 left: ['10px', '100px', '150px'], 141 opacity: ['1', '0', '1'], 142 easing: 'ease', 143 }, 144 sequencedKeyframes: [ 145 { left: '10px', opacity: '1', easing: 'ease' }, 146 { left: '100px', opacity: '0', easing: 'ease' }, 147 { left: '150px', opacity: '1', easing: 'ease' }, 148 ], 149 }, 150 { 151 description: 'same composite applied to all keyframes', 152 indexedKeyframes: { 153 left: ['0px', '100px'], 154 composite: 'add', 155 }, 156 sequencedKeyframes: [ 157 { left: '0px', composite: 'add' }, 158 { left: '100px', composite: 'add' }, 159 ], 160 }, 161 ]; 162 163 for (const {description, indexedKeyframes, sequencedKeyframes} of 164 gEquivalentSyntaxTests) { 165 test(() => { 166 assertEquivalentKeyframeSyntax(indexedKeyframes, sequencedKeyframes); 167 }, `Equivalent property-indexed and sequenced keyframes: ${description}`); 168 } 169 170 // Test handling of custom iterable objects. 171 172 function createIterable(iterations) { 173 return { 174 [Symbol.iterator]() { 175 let i = 0; 176 return { 177 next() { 178 return iterations[i++]; 179 }, 180 }; 181 }, 182 }; 183 } 184 185 test(() => { 186 const effect = new KeyframeEffect(null, createIterable([ 187 { done: false, value: { left: '100px' } }, 188 { done: false, value: { left: '300px' } }, 189 { done: false, value: { left: '200px' } }, 190 { done: true }, 191 ])); 192 assert_frame_lists_equal(effect.getKeyframes(), [ 193 { 194 offset: null, 195 computedOffset: 0, 196 easing: 'linear', 197 left: '100px', 198 composite: 'auto', 199 }, 200 { 201 offset: null, 202 computedOffset: 0.5, 203 easing: 'linear', 204 left: '300px', 205 composite: 'auto', 206 }, 207 { 208 offset: null, 209 computedOffset: 1, 210 easing: 'linear', 211 left: '200px', 212 composite: 'auto', 213 }, 214 ]); 215 }, 'Keyframes are read from a custom iterator'); 216 217 test(() => { 218 const keyframes = createIterable([ 219 { done: false, value: { left: '100px' } }, 220 { done: false, value: { left: '300px' } }, 221 { done: false, value: { left: '200px' } }, 222 { done: true }, 223 ]); 224 keyframes.easing = 'ease-in-out'; 225 keyframes.offset = '0.1'; 226 const effect = new KeyframeEffect(null, keyframes); 227 assert_frame_lists_equal(effect.getKeyframes(), [ 228 { 229 offset: null, 230 computedOffset: 0, 231 easing: 'linear', 232 left: '100px', 233 composite: 'auto', 234 }, 235 { 236 offset: null, 237 computedOffset: 0.5, 238 easing: 'linear', 239 left: '300px', 240 composite: 'auto', 241 }, 242 { 243 offset: null, 244 computedOffset: 1, 245 easing: 'linear', 246 left: '200px', 247 composite: 'auto', 248 }, 249 ]); 250 }, '\'easing\' and \'offset\' are ignored on iterable objects'); 251 252 test(() => { 253 const effect = new KeyframeEffect(null, createIterable([ 254 { done: false, value: { left: '100px', top: '200px' } }, 255 { done: false, value: { left: '300px' } }, 256 { done: false, value: { left: '200px', top: '100px' } }, 257 { done: true }, 258 ])); 259 assert_frame_lists_equal(effect.getKeyframes(), [ 260 { 261 offset: null, 262 computedOffset: 0, 263 easing: 'linear', 264 left: '100px', 265 top: '200px', 266 composite: 'auto', 267 }, 268 { 269 offset: null, 270 computedOffset: 0.5, 271 easing: 'linear', 272 left: '300px', 273 composite: 'auto', 274 }, 275 { 276 offset: null, 277 computedOffset: 1, 278 easing: 'linear', 279 left: '200px', 280 top: '100px', 281 composite: 'auto', 282 }, 283 ]); 284 }, 'Keyframes are read from a custom iterator with multiple properties' 285 + ' specified'); 286 287 test(() => { 288 const effect = new KeyframeEffect(null, createIterable([ 289 { done: false, value: { left: '100px' } }, 290 { done: false, value: { left: '250px', offset: 0.75 } }, 291 { done: false, value: { left: '200px' } }, 292 { done: true }, 293 ])); 294 assert_frame_lists_equal(effect.getKeyframes(), [ 295 { 296 offset: null, 297 computedOffset: 0, 298 easing: 'linear', 299 left: '100px', 300 composite: 'auto', 301 }, 302 { 303 offset: 0.75, 304 computedOffset: 0.75, 305 easing: 'linear', 306 left: '250px', 307 composite: 'auto', 308 }, 309 { 310 offset: null, 311 computedOffset: 1, 312 easing: 'linear', 313 left: '200px', 314 composite: 'auto', 315 }, 316 ]); 317 }, 'Keyframes are read from a custom iterator with where an offset is' 318 + ' specified'); 319 320 test(() => { 321 const test_error = { name: 'test' }; 322 const bad_keyframe = { get left() { throw test_error; } }; 323 assert_throws_exactly(test_error, () => { 324 new KeyframeEffect(null, createIterable([ 325 { done: false, value: { left: '100px' } }, 326 { done: false, value: bad_keyframe }, 327 { done: false, value: { left: '200px' } }, 328 { done: true }, 329 ])); 330 }); 331 }, 'If a keyframe throws for an animatable property, that exception should be' 332 + ' propagated'); 333 334 test(() => { 335 assert_throws_js(TypeError, () => { 336 new KeyframeEffect(null, createIterable([ 337 { done: false, value: { left: '100px' } }, 338 { done: false, value: 1234 }, 339 { done: false, value: { left: '200px' } }, 340 { done: true }, 341 ])); 342 }); 343 }, 'Reading from a custom iterator that returns a non-object keyframe' 344 + ' should throw'); 345 346 test(() => { 347 assert_throws_js(TypeError, () => { 348 new KeyframeEffect(null, createIterable([ 349 { done: false, value: { left: '100px', easing: '' } }, 350 { done: false, value: 1234 }, 351 { done: false, value: { left: '200px' } }, 352 { done: true }, 353 ])); 354 }); 355 }, 'Reading from a custom iterator that returns a non-object keyframe' 356 + ' and an invalid easing should throw'); 357 358 test(() => { 359 assert_throws_js(TypeError, () => { 360 new KeyframeEffect(null, createIterable([ 361 { done: false, value: { left: '100px' } }, 362 { done: false, value: { left: '150px', offset: 'o' } }, 363 { done: false, value: { left: '200px' } }, 364 { done: true }, 365 ])); 366 }); 367 }, 'Reading from a custom iterator that returns a keyframe with a non finite' 368 + ' floating-point offset value should throw'); 369 370 test(() => { 371 assert_throws_js(TypeError, () => { 372 new KeyframeEffect(null, createIterable([ 373 { done: false, value: { left: '100px', easing: '' } }, 374 { done: false, value: { left: '150px', offset: 'o' } }, 375 { done: false, value: { left: '200px' } }, 376 { done: true }, 377 ])); 378 }); 379 }, 'Reading from a custom iterator that returns a keyframe with a non finite' 380 + ' floating-point offset value and an invalid easing should throw'); 381 382 test(() => { 383 const effect = new KeyframeEffect(null, createIterable([ 384 { done: false, value: { left: '100px' } }, 385 { done: false }, // No value member; keyframe is undefined. 386 { done: false, value: { left: '200px' } }, 387 { done: true }, 388 ])); 389 assert_frame_lists_equal(effect.getKeyframes(), [ 390 { left: '100px', offset: null, computedOffset: 0, easing: 'linear', composite: 'auto' }, 391 { offset: null, computedOffset: 0.5, easing: 'linear', composite: 'auto' }, 392 { left: '200px', offset: null, computedOffset: 1, easing: 'linear', composite: 'auto' }, 393 ]); 394 }, 'An undefined keyframe returned from a custom iterator should be treated as a' 395 + ' default keyframe'); 396 397 test(() => { 398 const effect = new KeyframeEffect(null, createIterable([ 399 { done: false, value: { left: '100px' } }, 400 { done: false, value: null }, 401 { done: false, value: { left: '200px' } }, 402 { done: true }, 403 ])); 404 assert_frame_lists_equal(effect.getKeyframes(), [ 405 { left: '100px', offset: null, computedOffset: 0, easing: 'linear', composite: 'auto' }, 406 { offset: null, computedOffset: 0.5, easing: 'linear', composite: 'auto' }, 407 { left: '200px', offset: null, computedOffset: 1, easing: 'linear', composite: 'auto' }, 408 ]); 409 }, 'A null keyframe returned from a custom iterator should be treated as a' 410 + ' default keyframe'); 411 412 test(() => { 413 const effect = new KeyframeEffect(null, createIterable([ 414 { done: false, value: { left: ['100px', '200px'] } }, 415 { done: true }, 416 ])); 417 assert_frame_lists_equal(effect.getKeyframes(), [ 418 { offset: null, computedOffset: 1, easing: 'linear', composite: 'auto' } 419 ]); 420 }, 'A list of values returned from a custom iterator should be ignored'); 421 422 test(() => { 423 const test_error = { name: 'test' }; 424 const keyframe_obj = { 425 [Symbol.iterator]() { 426 return { next() { throw test_error; } }; 427 }, 428 }; 429 assert_throws_exactly(test_error, () => { 430 new KeyframeEffect(null, keyframe_obj); 431 }); 432 }, 'If a custom iterator throws from next(), the exception should be rethrown'); 433 434 // Test handling of invalid Symbol.iterator 435 436 test(() => { 437 const test_error = { name: 'test' }; 438 const keyframe_obj = { 439 [Symbol.iterator]() { 440 throw test_error; 441 }, 442 }; 443 assert_throws_exactly(test_error, () => { 444 new KeyframeEffect(null, keyframe_obj); 445 }); 446 }, 'Accessing a Symbol.iterator property that throws should rethrow'); 447 448 test(() => { 449 const keyframe_obj = { 450 [Symbol.iterator]() { 451 return 42; // Not an object. 452 }, 453 }; 454 assert_throws_js(TypeError, () => { 455 new KeyframeEffect(null, keyframe_obj); 456 }); 457 }, 'A non-object returned from the Symbol.iterator property should cause a' 458 + ' TypeError to be thrown'); 459 460 test(() => { 461 const keyframe = {}; 462 Object.defineProperty(keyframe, 'width', { value: '200px' }); 463 Object.defineProperty(keyframe, 'height', { 464 value: '100px', 465 enumerable: true, 466 }); 467 assert_equals(keyframe.width, '200px', 'width of keyframe is readable'); 468 assert_equals(keyframe.height, '100px', 'height of keyframe is readable'); 469 470 const effect = new KeyframeEffect(null, [keyframe, { height: '200px' }]); 471 472 assert_frame_lists_equal(effect.getKeyframes(), [ 473 { 474 offset: null, 475 computedOffset: 0, 476 easing: 'linear', 477 height: '100px', 478 composite: 'auto', 479 }, 480 { 481 offset: null, 482 computedOffset: 1, 483 easing: 'linear', 484 height: '200px', 485 composite: 'auto', 486 }, 487 ]); 488 }, 'Only enumerable properties on keyframes are read'); 489 490 test(() => { 491 const KeyframeParent = function() { this.width = '100px'; }; 492 KeyframeParent.prototype = { height: '100px' }; 493 const Keyframe = function() { this.top = '100px'; }; 494 Keyframe.prototype = Object.create(KeyframeParent.prototype); 495 Object.defineProperty(Keyframe.prototype, 'left', { 496 value: '100px', 497 enumerable: true, 498 }); 499 const keyframe = new Keyframe(); 500 501 const effect = new KeyframeEffect(null, [keyframe, { top: '200px' }]); 502 503 assert_frame_lists_equal(effect.getKeyframes(), [ 504 { 505 offset: null, 506 computedOffset: 0, 507 easing: 'linear', 508 top: '100px', 509 composite: 'auto', 510 }, 511 { 512 offset: null, 513 computedOffset: 1, 514 easing: 'linear', 515 top: '200px', 516 composite: 'auto', 517 }, 518 ]); 519 }, 'Only properties defined directly on keyframes are read'); 520 521 test(() => { 522 const keyframes = {}; 523 Object.defineProperty(keyframes, 'width', ['100px', '200px']); 524 Object.defineProperty(keyframes, 'height', { 525 value: ['100px', '200px'], 526 enumerable: true, 527 }); 528 529 const effect = new KeyframeEffect(null, keyframes); 530 531 assert_frame_lists_equal(effect.getKeyframes(), [ 532 { 533 offset: null, 534 computedOffset: 0, 535 easing: 'linear', 536 height: '100px', 537 composite: 'auto', 538 }, 539 { 540 offset: null, 541 computedOffset: 1, 542 easing: 'linear', 543 height: '200px', 544 composite: 'auto', 545 }, 546 ]); 547 }, 'Only enumerable properties on property-indexed keyframes are read'); 548 549 test(() => { 550 const KeyframesParent = function() { this.width = '100px'; }; 551 KeyframesParent.prototype = { height: '100px' }; 552 const Keyframes = function() { this.top = ['100px', '200px']; }; 553 Keyframes.prototype = Object.create(KeyframesParent.prototype); 554 Object.defineProperty(Keyframes.prototype, 'left', { 555 value: ['100px', '200px'], 556 enumerable: true, 557 }); 558 const keyframes = new Keyframes(); 559 560 const effect = new KeyframeEffect(null, keyframes); 561 562 assert_frame_lists_equal(effect.getKeyframes(), [ 563 { 564 offset: null, 565 computedOffset: 0, 566 easing: 'linear', 567 top: '100px', 568 composite: 'auto', 569 }, 570 { 571 offset: null, 572 computedOffset: 1, 573 easing: 'linear', 574 top: '200px', 575 composite: 'auto', 576 }, 577 ]); 578 }, 'Only properties defined directly on property-indexed keyframes are read'); 579 580 test(() => { 581 const expectedOrder = ['composite', 'easing', 'offset', 'left', 'marginLeft']; 582 const actualOrder = []; 583 const kf1 = {}; 584 for (const {prop, value} of [{ prop: 'marginLeft', value: '10px' }, 585 { prop: 'left', value: '20px' }, 586 { prop: 'offset', value: '0' }, 587 { prop: 'easing', value: 'linear' }, 588 { prop: 'composite', value: 'replace' }]) { 589 Object.defineProperty(kf1, prop, { 590 enumerable: true, 591 get: () => { actualOrder.push(prop); return value; } 592 }); 593 } 594 const kf2 = { marginLeft: '10px', left: '20px', offset: 1 }; 595 596 new KeyframeEffect(target, [kf1, kf2]); 597 598 assert_array_equals(actualOrder, expectedOrder, 'property access order'); 599 }, 'Properties are read in ascending order by Unicode codepoint'); 600 601 </script>