blob: f16412fbcf70ba5ef9cb49f8e0647e98de23a596 [file] [log] [blame]
<!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>