| <!doctype html> |
| <meta charset=utf-8> |
| <title>Animation.commitStyles</title> |
| <link rel="help" href="https://drafts.csswg.org/web-animations/#dom-animation-commitstyles"> |
| <script src="/resources/testharness.js"></script> |
| <script src="/resources/testharnessreport.js"></script> |
| <script src="../../testcommon.js"></script> |
| <style> |
| .pseudo::before {content: '';} |
| .pseudo::after {content: '';} |
| .pseudo::marker {content: '';} |
| </style> |
| <body> |
| <div id="log"></div> |
| <script> |
| 'use strict'; |
| |
| function assert_numeric_style_equals(opacity, expected, description) { |
| return assert_approx_equals( |
| parseFloat(opacity), |
| expected, |
| 0.0001, |
| description |
| ); |
| } |
| |
| test(t => { |
| const div = createDiv(t); |
| div.style.opacity = '0.1'; |
| |
| const animation = div.animate( |
| { opacity: 0.2 }, |
| { duration: 1, fill: 'forwards' } |
| ); |
| animation.finish(); |
| |
| animation.commitStyles(); |
| |
| // Cancel the animation so we can inspect the underlying style |
| animation.cancel(); |
| |
| assert_numeric_style_equals(getComputedStyle(div).opacity, 0.2); |
| }, 'Commits styles'); |
| |
| promise_test(async t => { |
| const div = createDiv(t); |
| div.style.opacity = '0.1'; |
| |
| const animA = div.animate( |
| { opacity: 0.2 }, |
| { duration: 1, fill: 'forwards' } |
| ); |
| const animB = div.animate( |
| { opacity: 0.3 }, |
| { duration: 1, fill: 'forwards' } |
| ); |
| |
| await animA.finished; |
| |
| animB.cancel(); |
| |
| animA.commitStyles(); |
| |
| assert_numeric_style_equals(getComputedStyle(div).opacity, 0.2); |
| }, 'Commits styles for an animation that has been removed'); |
| |
| test(t => { |
| const div = createDiv(t); |
| div.style.margin = '10px'; |
| |
| const animation = div.animate( |
| { margin: '20px' }, |
| { duration: 1, fill: 'forwards' } |
| ); |
| animation.finish(); |
| |
| animation.commitStyles(); |
| |
| animation.cancel(); |
| |
| assert_equals(div.style.marginLeft, '20px'); |
| }, 'Commits shorthand styles'); |
| |
| test(t => { |
| const div = createDiv(t); |
| div.style.marginLeft = '10px'; |
| |
| const animation = div.animate( |
| { marginInlineStart: '20px' }, |
| { duration: 1, fill: 'forwards' } |
| ); |
| animation.finish(); |
| |
| animation.commitStyles(); |
| |
| animation.cancel(); |
| |
| assert_equals(div.style.marginLeft, '20px'); |
| }, 'Commits logical properties'); |
| |
| test(t => { |
| const div = createDiv(t); |
| div.style.marginLeft = '10px'; |
| |
| const animation = div.animate({ opacity: [0.2, 0.7] }, 1000); |
| animation.currentTime = 500; |
| animation.commitStyles(); |
| animation.cancel(); |
| |
| assert_numeric_style_equals(getComputedStyle(div).opacity, 0.45); |
| }, 'Commits values calculated mid-interval'); |
| |
| test(t => { |
| const div = createDiv(t); |
| div.style.setProperty('--target', '0.5'); |
| |
| const animation = div.animate( |
| { opacity: 'var(--target)' }, |
| { duration: 1, fill: 'forwards' } |
| ); |
| animation.finish(); |
| animation.commitStyles(); |
| animation.cancel(); |
| |
| assert_numeric_style_equals(getComputedStyle(div).opacity, 0.5); |
| |
| // Changes to the variable should have no effect |
| div.style.setProperty('--target', '1'); |
| |
| assert_numeric_style_equals(getComputedStyle(div).opacity, 0.5); |
| }, 'Commits variables as their computed values'); |
| |
| test(t => { |
| const div = createDiv(t); |
| div.style.fontSize = '10px'; |
| |
| const animation = div.animate( |
| { width: '10em' }, |
| { duration: 1, fill: 'forwards' } |
| ); |
| animation.finish(); |
| animation.commitStyles(); |
| animation.cancel(); |
| |
| assert_numeric_style_equals(getComputedStyle(div).width, 100); |
| |
| // Changes to the font-size should have no effect |
| div.style.fontSize = '20px'; |
| |
| assert_numeric_style_equals(getComputedStyle(div).width, 100); |
| }, 'Commits em units as pixel values'); |
| |
| promise_test(async t => { |
| const div = createDiv(t); |
| div.style.opacity = '0.1'; |
| |
| const animA = div.animate( |
| { opacity: '0.2' }, |
| { duration: 1, fill: 'forwards' } |
| ); |
| const animB = div.animate( |
| { opacity: '0.2', composite: 'add' }, |
| { duration: 1, fill: 'forwards' } |
| ); |
| const animC = div.animate( |
| { opacity: '0.3', composite: 'add' }, |
| { duration: 1, fill: 'forwards' } |
| ); |
| |
| animA.persist(); |
| animB.persist(); |
| |
| await animB.finished; |
| |
| // The values above have been chosen such that various error conditions |
| // produce results that all differ from the desired result: |
| // |
| // Expected result: |
| // |
| // animA + animB = 0.4 |
| // |
| // Likely error results: |
| // |
| // <underlying> = 0.1 |
| // (Commit didn't work at all) |
| // |
| // animB = 0.2 |
| // (Didn't add at all when resolving) |
| // |
| // <underlying> + animB = 0.3 |
| // (Added to the underlying value instead of lower-priority animations when |
| // resolving) |
| // |
| // <underlying> + animA + animB = 0.5 |
| // (Didn't respect the composite mode of lower-priority animations) |
| // |
| // animA + animB + animC = 0.7 |
| // (Resolved the whole stack, not just up to the target effect) |
| // |
| |
| animB.commitStyles(); |
| |
| animA.cancel(); |
| animB.cancel(); |
| animC.cancel(); |
| |
| assert_numeric_style_equals(getComputedStyle(div).opacity, 0.4); |
| }, 'Commits the intermediate value of an animation in the middle of stack'); |
| |
| promise_test(async t => { |
| const div = createDiv(t); |
| div.style.opacity = '0.1'; |
| |
| // Setup animation |
| const animation = div.animate( |
| { opacity: 0.2 }, |
| { duration: 1, fill: 'forwards' } |
| ); |
| animation.finish(); |
| |
| // Setup observer |
| const mutationRecords = []; |
| const observer = new MutationObserver(mutations => { |
| mutationRecords.push(...mutations); |
| }); |
| observer.observe(div, { attributes: true, attributeOldValue: true }); |
| |
| animation.commitStyles(); |
| |
| // Wait for mutation records to be dispatched |
| await Promise.resolve(); |
| |
| assert_equals(mutationRecords.length, 1, 'Should have one mutation record'); |
| |
| const mutation = mutationRecords[0]; |
| assert_equals(mutation.type, 'attributes'); |
| assert_equals(mutation.oldValue, 'opacity: 0.1;'); |
| |
| observer.disconnect(); |
| }, 'Triggers mutation observers when updating style'); |
| |
| promise_test(async t => { |
| const div = createDiv(t); |
| div.style.opacity = '0.2'; |
| |
| // Setup animation |
| const animation = div.animate( |
| { opacity: 0.2 }, |
| { duration: 1, fill: 'forwards' } |
| ); |
| animation.finish(); |
| |
| // Setup observer |
| const mutationRecords = []; |
| const observer = new MutationObserver(mutations => { |
| mutationRecords.push(...mutations); |
| }); |
| observer.observe(div, { attributes: true }); |
| |
| animation.commitStyles(); |
| |
| // Wait for mutation records to be dispatched |
| await Promise.resolve(); |
| |
| assert_equals(mutationRecords.length, 0, 'Should have no mutation records'); |
| |
| observer.disconnect(); |
| }, 'Does NOT trigger mutation observers when the change to style is redundant'); |
| |
| test(t => { |
| |
| const div = createDiv(t); |
| div.classList.add('pseudo'); |
| const animation = div.animate( |
| { opacity: 0 }, |
| { duration: 1, fill: 'forwards', pseudoElement: '::before' } |
| ); |
| |
| assert_throws_dom('NoModificationAllowedError', () => { |
| animation.commitStyles(); |
| }); |
| }, 'Throws if the target element is a pseudo element'); |
| |
| test(t => { |
| const animation = createDiv(t).animate( |
| { opacity: 0 }, |
| { duration: 1, fill: 'forwards' } |
| ); |
| |
| const nonStyleElement |
| = document.createElementNS('http://example.org/test', 'test'); |
| document.body.appendChild(nonStyleElement); |
| animation.effect.target = nonStyleElement; |
| |
| assert_throws_dom('NoModificationAllowedError', () => { |
| animation.commitStyles(); |
| }); |
| |
| nonStyleElement.remove(); |
| }, 'Throws if the target element is not something with a style attribute'); |
| |
| test(t => { |
| const div = createDiv(t); |
| const animation = div.animate( |
| { opacity: 0 }, |
| { duration: 1, fill: 'forwards' } |
| ); |
| |
| div.style.display = 'none'; |
| |
| assert_throws_dom('InvalidStateError', () => { |
| animation.commitStyles(); |
| }); |
| }, 'Throws if the target effect is display:none'); |
| |
| test(t => { |
| const container = createDiv(t); |
| const div = createDiv(t); |
| container.append(div); |
| |
| const animation = div.animate( |
| { opacity: 0 }, |
| { duration: 1, fill: 'forwards' } |
| ); |
| |
| container.style.display = 'none'; |
| |
| assert_throws_dom('InvalidStateError', () => { |
| animation.commitStyles(); |
| }); |
| }, "Throws if the target effect's ancestor is display:none"); |
| |
| test(t => { |
| const container = createDiv(t); |
| const div = createDiv(t); |
| container.append(div); |
| |
| const animation = div.animate( |
| { opacity: 0 }, |
| { duration: 1, fill: 'forwards' } |
| ); |
| |
| container.style.display = 'contents'; |
| |
| // Should NOT throw |
| animation.commitStyles(); |
| }, 'Treats display:contents as rendered'); |
| |
| test(t => { |
| const container = createDiv(t); |
| const div = createDiv(t); |
| container.append(div); |
| |
| const animation = div.animate( |
| { opacity: 0 }, |
| { duration: 1, fill: 'forwards' } |
| ); |
| |
| div.style.display = 'contents'; |
| container.style.display = 'none'; |
| |
| assert_throws_dom('InvalidStateError', () => { |
| animation.commitStyles(); |
| }); |
| }, 'Treats display:contents in a display:none subtree as not rendered'); |
| |
| test(t => { |
| const div = createDiv(t); |
| const animation = div.animate( |
| { opacity: 0 }, |
| { duration: 1, fill: 'forwards' } |
| ); |
| |
| div.remove(); |
| |
| assert_throws_dom('InvalidStateError', () => { |
| animation.commitStyles(); |
| }); |
| }, 'Throws if the target effect is disconnected'); |
| |
| test(t => { |
| const div = createDiv(t); |
| div.classList.add('pseudo'); |
| const animation = div.animate( |
| { opacity: 0 }, |
| { duration: 1, fill: 'forwards', pseudoElement: '::before' } |
| ); |
| |
| div.remove(); |
| |
| assert_throws_dom('NoModificationAllowedError', () => { |
| animation.commitStyles(); |
| }); |
| }, 'Checks the pseudo element condition before the not rendered condition'); |
| |
| </script> |
| </body> |