commitStyles.html (17143B)
1 <!doctype html> 2 <meta charset=utf-8> 3 <title>Animation.commitStyles</title> 4 <link rel="help" href="https://drafts.csswg.org/web-animations/#dom-animation-commitstyles"> 5 <script src="/resources/testharness.js"></script> 6 <script src="/resources/testharnessreport.js"></script> 7 <script src="../../testcommon.js"></script> 8 <style> 9 .pseudo::before {content: '';} 10 .pseudo::after {content: '';} 11 .pseudo::marker {content: '';} 12 </style> 13 <body> 14 <div id="log"></div> 15 <script> 16 'use strict'; 17 18 function assert_numeric_style_equals(opacity, expected, description) { 19 return assert_approx_equals( 20 parseFloat(opacity), 21 expected, 22 0.0001, 23 description 24 ); 25 } 26 27 // ------------------------- 28 // Tests covering elements not capable of having a style attribute 29 // -------------------- 30 31 test(t => { 32 const animation = createDiv(t).animate( 33 { opacity: 0 }, 34 { duration: 1 } 35 ); 36 37 const nonStyleElement = document.createElementNS( 38 'http://example.org/test', 39 'test' 40 ); 41 document.body.appendChild(nonStyleElement); 42 animation.effect.target = nonStyleElement; 43 44 assert_throws_dom('NoModificationAllowedError', () => { 45 animation.commitStyles(); 46 }); 47 48 nonStyleElement.remove(); 49 }, 'Throws if the target element is not something with a style attribute'); 50 51 test(t => { 52 const div = createDiv(t); 53 div.classList.add('pseudo'); 54 const animation = div.animate( 55 { opacity: 0 }, 56 { duration: 1, pseudoElement: '::before' } 57 ); 58 59 assert_throws_dom('NoModificationAllowedError', () => { 60 animation.commitStyles(); 61 }); 62 }, 'Throws if the target element is a pseudo element'); 63 64 test(t => { 65 const div = createDiv(t); 66 div.classList.add('pseudo'); 67 const animation = div.animate( 68 { opacity: 0 }, 69 { duration: 1, pseudoElement: '::before' } 70 ); 71 72 div.remove(); 73 74 assert_throws_dom('NoModificationAllowedError', () => { 75 animation.commitStyles(); 76 }); 77 }, 'Checks the pseudo element condition before the not rendered condition'); 78 79 // ------------------------- 80 // Tests covering elements that are not being rendered 81 // ------------------------- 82 83 test(t => { 84 const div = createDiv(t); 85 const animation = div.animate( 86 { opacity: 0 }, 87 { duration: 1 } 88 ); 89 90 div.remove(); 91 92 assert_throws_dom('InvalidStateError', () => { 93 animation.commitStyles(); 94 }); 95 }, 'Throws if the target effect is disconnected'); 96 97 test(t => { 98 const div = createDiv(t); 99 const animation = div.animate( 100 { opacity: 0 }, 101 { duration: 1 } 102 ); 103 104 div.style.display = 'none'; 105 106 assert_throws_dom('InvalidStateError', () => { 107 animation.commitStyles(); 108 }); 109 }, 'Throws if the target effect is display:none'); 110 111 test(t => { 112 const container = createDiv(t); 113 const div = createDiv(t); 114 container.append(div); 115 116 const animation = div.animate( 117 { opacity: 0 }, 118 { duration: 1 } 119 ); 120 121 container.style.display = 'none'; 122 123 assert_throws_dom('InvalidStateError', () => { 124 animation.commitStyles(); 125 }); 126 }, "Throws if the target effect's ancestor is display:none"); 127 128 test(t => { 129 const container = createDiv(t); 130 const div = createDiv(t); 131 container.append(div); 132 133 const animation = div.animate( 134 { opacity: 0 }, 135 { duration: 1 } 136 ); 137 138 container.style.display = 'contents'; 139 140 // Should NOT throw 141 animation.commitStyles(); 142 }, 'Treats display:contents as rendered'); 143 144 test(t => { 145 const container = createDiv(t); 146 const div = createDiv(t); 147 container.append(div); 148 149 const animation = div.animate( 150 { opacity: 0 }, 151 { duration: 1 } 152 ); 153 154 div.style.display = 'contents'; 155 container.style.display = 'none'; 156 157 assert_throws_dom('InvalidStateError', () => { 158 animation.commitStyles(); 159 }); 160 }, 'Treats display:contents in a display:none subtree as not rendered'); 161 162 // ------------------------- 163 // Tests covering various parts of the active interval 164 // ------------------------- 165 166 test(t => { 167 const div = createDiv(t); 168 div.style.opacity = '0.1'; 169 170 const animation = div.animate( 171 { opacity: 0.2 }, 172 { duration: 1 } 173 ); 174 animation.finish(); 175 176 animation.commitStyles(); 177 178 assert_numeric_style_equals(getComputedStyle(div).opacity, 0.2); 179 }, 'Commits styles'); 180 181 test(t => { 182 const div = createDiv(t); 183 div.style.opacity = '0.1'; 184 185 const animation = div.animate( 186 { opacity: [0.5, 1] }, 187 { duration: 1 } 188 ); 189 animation.playbackRate = -1; 190 animation.finish(); 191 192 animation.commitStyles(); 193 194 assert_numeric_style_equals(getComputedStyle(div).opacity, 0.5); 195 }, 'Commits styles for backwards animation'); 196 197 test(t => { 198 const div = createDiv(t); 199 div.style.marginLeft = '10px'; 200 201 const animation = div.animate({ opacity: [0.2, 0.7] }, 1000); 202 animation.currentTime = 500; 203 animation.commitStyles(); 204 animation.cancel(); 205 206 assert_numeric_style_equals(getComputedStyle(div).opacity, 0.45); 207 }, 'Commits values calculated mid-interval'); 208 209 test(t => { 210 const div = createDiv(t); 211 div.style.opacity = '0'; 212 213 const animation = div.animate( 214 { opacity: 1 }, 215 { duration: 1000, delay: 1000 } 216 ); 217 animation.currentTime = 500; 218 animation.commitStyles(); 219 animation.cancel(); 220 221 assert_numeric_style_equals(getComputedStyle(div).opacity, 0); 222 }, 'Commits values during the start delay'); 223 224 test(t => { 225 const div = createDiv(t); 226 div.style.opacity = '0'; 227 228 const animation = div.animate( 229 { opacity: 1 }, 230 { duration: 1000, delay: 1000 } 231 ); 232 animation.currentTime = 2100; 233 animation.commitStyles(); 234 235 assert_numeric_style_equals(getComputedStyle(div).opacity, 0); 236 }, 'Commits value after the active interval'); 237 238 // ------------------------- 239 // Tests covering various parts of the stack 240 // ------------------------- 241 242 promise_test(async t => { 243 const div = createDiv(t); 244 div.style.opacity = '0.1'; 245 246 const animA = div.animate( 247 { opacity: '0.2' }, 248 { duration: 1, fill: 'forwards' } 249 ); 250 const animB = div.animate( 251 { opacity: '0.2', composite: 'add' }, 252 { duration: 1 } 253 ); 254 const animC = div.animate( 255 { opacity: '0.3', composite: 'add' }, 256 { duration: 1 } 257 ); 258 259 animA.persist(); 260 animB.persist(); 261 262 await animB.finished; 263 264 // The values above have been chosen such that various error conditions 265 // produce results that all differ from the desired result: 266 // 267 // Expected result: 268 // 269 // animA + animB = 0.4 270 // 271 // Likely error results: 272 // 273 // <underlying> = 0.1 274 // (Commit didn't work at all) 275 // 276 // animB = 0.2 277 // (Didn't add at all when resolving) 278 // 279 // <underlying> + animB = 0.3 280 // (Added to the underlying value instead of lower-priority animations when 281 // resolving) 282 // 283 // <underlying> + animA + animB = 0.5 284 // (Didn't respect the composite mode of lower-priority animations) 285 // 286 // animA + animB + animC = 0.7 287 // (Resolved the whole stack, not just up to the target effect) 288 // 289 290 animB.commitStyles(); 291 292 animA.cancel(); 293 294 assert_numeric_style_equals(getComputedStyle(div).opacity, 0.4); 295 }, 'Commits the intermediate value of an animation in the middle of stack'); 296 297 promise_test(async t => { 298 const div = createDiv(t); 299 div.style.opacity = '0.1'; 300 301 const animA = div.animate( 302 { opacity: '0.2' }, 303 { duration: 1 } 304 ); 305 const animB = div.animate( 306 { opacity: '0.2', composite: 'add' }, 307 { duration: 1 } 308 ); 309 const animC = div.animate( 310 { opacity: '0.3', composite: 'add' }, 311 { duration: 1 } 312 ); 313 314 animA.persist(); 315 animB.persist(); 316 317 await animB.finished; 318 319 // The values above have been chosen such that various error conditions 320 // produce results that all differ from the desired result: 321 // 322 // Expected result: 323 // 324 // animA + animB = 0.4 325 // 326 // Likely error results: 327 // 328 // <underlying> = 0.1 329 // (Commit didn't work at all) 330 // 331 // animB = 0.2 332 // (Didn't add at all when resolving) 333 // 334 // <underlying> + animB = 0.3 335 // (Added to the underlying value instead of lower-priority animations when 336 // resolving) 337 // 338 // <underlying> + animA + animB = 0.5 339 // (Didn't respect the composite mode of lower-priority animations) 340 // 341 // animA + animB + animC = 0.7 342 // (Resolved the whole stack, not just up to the target effect) 343 // 344 345 animA.commitStyles(); 346 animB.commitStyles(); 347 348 assert_numeric_style_equals(getComputedStyle(div).opacity, 0.4); 349 }, 'Commits the intermediate value of an animation up to the middle of the stack'); 350 351 promise_test(async t => { 352 const div = createDiv(t); 353 div.style.opacity = '0.1'; 354 355 const animA = div.animate( 356 { opacity: 0.2 }, 357 { duration: 1 } 358 ); 359 const animB = div.animate( 360 { opacity: 0.3 }, 361 { duration: 1 } 362 ); 363 364 await animA.finished; 365 366 animB.cancel(); 367 368 animA.commitStyles(); 369 370 assert_numeric_style_equals(getComputedStyle(div).opacity, 0.2); 371 }, 'Commits styles for an animation that has been removed'); 372 373 promise_test(async t => { 374 const div = createDiv(t); 375 div.style.opacity = '0.1'; 376 377 const animA = div.animate( 378 { opacity: '0.2', composite: 'add' }, 379 { duration: 1, fill: 'forwards' } 380 ); 381 const animB = div.animate( 382 { opacity: '0.2', composite: 'add' }, 383 { duration: 1 } 384 ); 385 const animC = div.animate( 386 { opacity: '0.3', composite: 'add' }, 387 { duration: 1 } 388 ); 389 390 animA.persist(); 391 animB.persist(); 392 await animB.finished; 393 394 // The error cases are similar to the above test with one additional case; 395 // verifying that the animations composite on top of the correct underlying 396 // base style. 397 // 398 // Expected result: 399 // 400 // <underlying> + animA + animB = 0.5 401 // 402 // Additional error results: 403 // 404 // <underlying> + animA + animB + animC + animA + animB = 1.0 (saturates) 405 // (Added to the computed value instead of underlying value when 406 // resolving) 407 // 408 // animA + animB = 0.4 409 // Failed to composite on top of underlying value. 410 // 411 412 animB.commitStyles(); 413 414 animA.cancel(); 415 416 assert_numeric_style_equals(getComputedStyle(div).opacity, 0.5); 417 }, 'Commit composites on top of the underlying value'); 418 419 // ------------------------- 420 // Tests covering handling of logical properties 421 // ------------------------- 422 423 test(t => { 424 const div = createDiv(t); 425 div.style.marginLeft = '10px'; 426 427 const animation = div.animate( 428 { marginInlineStart: '20px' }, 429 { duration: 1 } 430 ); 431 animation.finish(); 432 433 animation.commitStyles(); 434 435 assert_equals(getComputedStyle(div).marginLeft, '20px'); 436 }, 'Commits logical properties'); 437 438 test(t => { 439 const div = createDiv(t); 440 div.style.marginLeft = '10px'; 441 442 const animation = div.animate( 443 { marginInlineStart: '20px' }, 444 { duration: 1 } 445 ); 446 animation.finish(); 447 448 animation.commitStyles(); 449 450 assert_equals(div.style.marginLeft, '20px'); 451 }, 'Commits logical properties as physical properties'); 452 453 test(t => { 454 const div = createDiv(t); 455 div.style.fontSize = '10px'; 456 457 const animation = div.animate( 458 { width: '10em' }, 459 { duration: 1 } 460 ); 461 animation.finish(); 462 animation.commitStyles(); 463 464 assert_numeric_style_equals(getComputedStyle(div).width, 100); 465 466 div.style.fontSize = '20px'; 467 assert_numeric_style_equals( 468 getComputedStyle(div).width, 469 100, 470 'Changes to the font-size should have no effect' 471 ); 472 }, 'Commits em units as pixel values'); 473 474 // ------------------------- 475 // Tests covering CSS variables 476 // ------------------------- 477 478 test(t => { 479 const div = createDiv(t); 480 div.style.setProperty('--target', '0.5'); 481 div.style.opacity = 'var(--target)'; 482 const animation = div.animate( 483 { '--target': 0.8 }, 484 { duration: 1 } 485 ); 486 animation.finish(); 487 animation.commitStyles(); 488 489 assert_numeric_style_equals(getComputedStyle(div).opacity, 0.8); 490 }, 'Commits custom variables'); 491 492 test(t => { 493 const div = createDiv(t); 494 div.style.setProperty('--target', '0.5'); 495 496 const animation = div.animate( 497 { opacity: 'var(--target)' }, 498 { duration: 1 } 499 ); 500 animation.finish(); 501 animation.commitStyles(); 502 503 assert_numeric_style_equals(getComputedStyle(div).opacity, 0.5); 504 505 // Changes to the variable should have no effect 506 div.style.setProperty('--target', '1'); 507 508 assert_numeric_style_equals(getComputedStyle(div).opacity, 0.5); 509 }, 'Commits variable references as their computed values'); 510 511 // ------------------------- 512 // Tests covering the composition of specific properties 513 // (e.g. line-height, transforms) 514 // ------------------------- 515 516 test(t => { 517 const div = createDiv(t); 518 div.style.fontSize = '10px'; 519 520 const animation = div.animate( 521 { lineHeight: '1.5' }, 522 { duration: 1 } 523 ); 524 animation.finish(); 525 animation.commitStyles(); 526 527 assert_numeric_style_equals(getComputedStyle(div).lineHeight, 15); 528 assert_equals( 529 div.style.lineHeight, 530 '1.5', 531 'line-height is committed as a relative value' 532 ); 533 534 div.style.fontSize = '20px'; 535 assert_numeric_style_equals( 536 getComputedStyle(div).lineHeight, 537 30, 538 'Changes to the font-size should affect the committed line-height' 539 ); 540 }, 'Commits relative line-height'); 541 542 test(t => { 543 const div = createDiv(t); 544 const animation = div.animate( 545 { transform: 'translate(20px, 20px)' }, 546 { duration: 1 } 547 ); 548 animation.finish(); 549 animation.commitStyles(); 550 551 assert_equals( 552 getComputedStyle(div).transform, 553 'matrix(1, 0, 0, 1, 20, 20)' 554 ); 555 }, 'Commits transforms'); 556 557 test(t => { 558 const div = createDiv(t); 559 div.style.translate = '100px'; 560 div.style.rotate = '45deg'; 561 div.style.scale = '2'; 562 563 const animation = div.animate( 564 { translate: '200px', rotate: '90deg', scale: 3 }, 565 { duration: 1 } 566 ); 567 animation.finish(); 568 569 animation.commitStyles(); 570 571 assert_equals(getComputedStyle(div).translate, '200px'); 572 assert_equals(getComputedStyle(div).rotate, '90deg'); 573 assert_numeric_style_equals(getComputedStyle(div).scale, 3); 574 }, 'Commits styles for individual transform properties'); 575 576 test(t => { 577 const div = createDiv(t); 578 const animation = div.animate( 579 { transform: 'translate(20px, 20px)' }, 580 { duration: 1 } 581 ); 582 animation.finish(); 583 animation.commitStyles(); 584 585 assert_equals(div.style.transform, 'translate(20px, 20px)'); 586 }, 'Commits transforms as a transform list'); 587 588 test(t => { 589 const div = createDiv(t); 590 div.style.width = '200px'; 591 div.style.height = '200px'; 592 593 const animation = div.animate( 594 { transform: ['translate(100%, 0%)', 'scale(3)'] }, 595 1000 596 ); 597 animation.currentTime = 500; 598 animation.commitStyles(); 599 animation.cancel(); 600 601 // TODO(https://github.com/w3c/csswg-drafts/issues/2854): 602 // We can't check the committed value directly since it is not specced yet in this case, 603 // but it should still produce the correct resolved value. 604 assert_equals( 605 getComputedStyle(div).transform, 606 'matrix(2, 0, 0, 2, 100, 0)', 607 'Resolved transform is correct after commit.' 608 ); 609 }, 'Commits matrix-interpolated relative transforms'); 610 611 test(t => { 612 const div = createDiv(t); 613 div.style.width = '200px'; 614 div.style.height = '200px'; 615 616 const animation = div.animate({ transform: ['none', 'none'] }, 1000); 617 animation.currentTime = 500; 618 animation.commitStyles(); 619 animation.cancel(); 620 621 assert_equals( 622 div.style.transform, 623 'none', 624 'Resolved transform is correct after commit.' 625 ); 626 }, "Commits 'none' transform"); 627 628 test(t => { 629 const div = createDiv(t); 630 div.style.margin = '10px'; 631 632 const animation = div.animate( 633 { margin: '20px' }, 634 { duration: 1 } 635 ); 636 animation.finish(); 637 638 animation.commitStyles(); 639 640 assert_equals(div.style.marginLeft, '20px'); 641 }, 'Commits shorthand styles'); 642 643 // ------------------------- 644 // Tests related to setting the style attributes 645 // (e.g. mutation observer related ones) 646 // ------------------------- 647 648 promise_test(async t => { 649 const div = createDiv(t); 650 div.style.opacity = '0.1'; 651 652 // Setup animation 653 const animation = div.animate( 654 { opacity: 0.2 }, 655 { duration: 1 } 656 ); 657 animation.finish(); 658 659 // Setup observer 660 const mutationRecords = []; 661 const observer = new MutationObserver(mutations => { 662 mutationRecords.push(...mutations); 663 }); 664 observer.observe(div, { attributes: true, attributeOldValue: true }); 665 666 animation.commitStyles(); 667 668 // Wait for mutation records to be dispatched 669 await Promise.resolve(); 670 671 assert_equals(mutationRecords.length, 1, 'Should have one mutation record'); 672 673 const mutation = mutationRecords[0]; 674 assert_equals(mutation.type, 'attributes'); 675 assert_equals(mutation.oldValue, 'opacity: 0.1;'); 676 677 observer.disconnect(); 678 }, 'Triggers mutation observers when updating style'); 679 680 promise_test(async t => { 681 const div = createDiv(t); 682 div.style.opacity = '0.2'; 683 684 // Setup animation 685 const animation = div.animate( 686 { opacity: 0.2 }, 687 { duration: 1 } 688 ); 689 animation.finish(); 690 691 // Setup observer 692 const mutationRecords = []; 693 const observer = new MutationObserver(mutations => { 694 mutationRecords.push(...mutations); 695 }); 696 observer.observe(div, { attributes: true }); 697 698 animation.commitStyles(); 699 700 // Wait for mutation records to be dispatched 701 await Promise.resolve(); 702 703 assert_equals(mutationRecords.length, 0, 'Should have no mutation records'); 704 705 observer.disconnect(); 706 }, 'Does NOT trigger mutation observers when the change to style is redundant'); 707 708 </script> 709 </body>