| <!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(getComputedStyle(div).marginLeft, '20px'); | |
| }, 'Commits logical properties'); | |
| 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 as physical 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 variable references as their computed values'); | |
| test(t => { | |
| const div = createDiv(t); | |
| div.style.setProperty('--target', '0.5'); | |
| div.style.opacity = 'var(--target)'; | |
| const animation = div.animate( | |
| { '--target': 0.8 }, | |
| { duration: 1, fill: 'forwards' } | |
| ); | |
| animation.finish(); | |
| animation.commitStyles(); | |
| animation.cancel(); | |
| assert_numeric_style_equals(getComputedStyle(div).opacity, 0.8); | |
| }, 'Commits custom variables'); | |
| 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); | |
| div.style.fontSize = '20px'; | |
| assert_numeric_style_equals(getComputedStyle(div).width, 100, | |
| "Changes to the font-size should have no effect"); | |
| }, 'Commits em units as pixel values'); | |
| test(t => { | |
| const div = createDiv(t); | |
| div.style.fontSize = '10px'; | |
| const animation = div.animate( | |
| { lineHeight: '1.5' }, | |
| { duration: 1, fill: 'forwards' } | |
| ); | |
| animation.finish(); | |
| animation.commitStyles(); | |
| animation.cancel(); | |
| assert_numeric_style_equals(getComputedStyle(div).lineHeight, 15); | |
| assert_equals(div.style.lineHeight, "1.5", "line-height is committed as a relative value"); | |
| div.style.fontSize = '20px'; | |
| assert_numeric_style_equals(getComputedStyle(div).lineHeight, 30, | |
| "Changes to the font-size should affect the committed line-height"); | |
| }, 'Commits relative line-height'); | |
| test(t => { | |
| const div = createDiv(t); | |
| const animation = div.animate( | |
| { transform: 'translate(20px, 20px)' }, | |
| { duration: 1, fill: 'forwards' } | |
| ); | |
| animation.finish(); | |
| animation.commitStyles(); | |
| animation.cancel(); | |
| assert_equals(getComputedStyle(div).transform, 'matrix(1, 0, 0, 1, 20, 20)'); | |
| }, 'Commits transforms'); | |
| test(t => { | |
| const div = createDiv(t); | |
| const animation = div.animate( | |
| { transform: 'translate(20px, 20px)' }, | |
| { duration: 1, fill: 'forwards' } | |
| ); | |
| animation.finish(); | |
| animation.commitStyles(); | |
| animation.cancel(); | |
| assert_equals(div.style.transform, 'translate(20px, 20px)'); | |
| }, 'Commits transforms as a transform list'); | |
| test(t => { | |
| const div = createDiv(t); | |
| div.style.width = '200px'; | |
| div.style.height = '200px'; | |
| const animation = div.animate({ transform: ["translate(100%, 0%)", "scale(3)"] }, 1000); | |
| animation.currentTime = 500; | |
| animation.commitStyles(); | |
| animation.cancel(); | |
| // TODO(https://github.com/w3c/csswg-drafts/issues/2854): | |
| // We can't check the committed value directly since it is not specced yet in this case, | |
| // but it should still produce the correct resolved value. | |
| assert_equals(getComputedStyle(div).transform, "matrix(2, 0, 0, 2, 100, 0)", | |
| "Resolved transform is correct after commit."); | |
| }, 'Commits matrix-interpolated relative transforms'); | |
| test(t => { | |
| const div = createDiv(t); | |
| div.style.width = '200px'; | |
| div.style.height = '200px'; | |
| const animation = div.animate({ transform: ["none", "none"] }, 1000); | |
| animation.currentTime = 500; | |
| animation.commitStyles(); | |
| animation.cancel(); | |
| assert_equals(div.style.transform, "none", | |
| "Resolved transform is correct after commit."); | |
| }, 'Commits "none" transform'); | |
| 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'; | |
| const animA = div.animate( | |
| { opacity: '0.2', composite: 'add' }, | |
| { 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 error cases are similar to the above test with one additional case; | |
| // verifying that the animations composite on top of the correct underlying | |
| // base style. | |
| // | |
| // Expected result: | |
| // | |
| // <underlying> + animA + animB = 0.5 | |
| // | |
| // Additional error results: | |
| // | |
| // <underlying> + animA + animB + animC + animA + animB = 1.0 (saturates) | |
| // (Added to the computed value instead of underlying value when | |
| // resolving) | |
| // | |
| // animA + animB = 0.4 | |
| // Failed to composite on top of underlying value. | |
| // | |
| animB.commitStyles(); | |
| animA.cancel(); | |
| animB.cancel(); | |
| animC.cancel(); | |
| assert_numeric_style_equals(getComputedStyle(div).opacity, 0.5); | |
| }, 'Commit composites on top of the underlying value'); | |
| 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> |