| <!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> |