Source code

Revision control

Copy as Markdown

Other Tools

Test Info:

<!doctype html>
<head>
<meta charset=utf-8>
<title>Bug 1196114 - Test metadata related to which animation properties
are running on the compositor</title>
<script type="application/javascript" src="../testharness.js"></script>
<script type="application/javascript" src="../testharnessreport.js"></script>
<script type="application/javascript" src="../testcommon.js"></script>
<style>
.compositable {
/* Element needs geometry to be eligible for layerization */
width: 100px;
height: 100px;
background-color: white;
}
@keyframes fade {
from { opacity: 1 }
to { opacity: 0 }
}
@keyframes translate {
from { transform: none }
to { transform: translate(100px) }
}
</style>
</head>
<body>
target="_blank">Mozilla Bug 1196114</a>
<div id="log"></div>
<script>
'use strict';
// This is used for obtaining localized strings.
var gStringBundle;
W3CTest.runner.requestLongerTimeout(2);
const Services = SpecialPowers.Services;
Services.locale.requestedLocales = ["en-US"];
SpecialPowers.pushPrefEnv({ "set": [
// Need to set devPixelsPerPx explicitly to gain
// consistent pixel values in warning messages
// regardless of platform DPIs.
["layout.css.devPixelsPerPx", 1],
["layout.animation.prerender.partial", false],
] },
start);
function compare_property_state(a, b) {
if (a.property > b.property) {
return -1;
} else if (a.property < b.property) {
return 1;
}
if (a.runningOnCompositor != b.runningOnCompositor) {
return a.runningOnCompositor ? 1 : -1;
}
return a.warning > b.warning ? -1 : 1;
}
function assert_animation_property_state_equals(actual, expected) {
assert_equals(actual.length, expected.length, 'Number of properties');
var sortedActual = actual.sort(compare_property_state);
var sortedExpected = expected.sort(compare_property_state);
for (var i = 0; i < sortedActual.length; i++) {
assert_equals(sortedActual[i].property,
sortedExpected[i].property,
'CSS property name should match');
assert_equals(sortedActual[i].runningOnCompositor,
sortedExpected[i].runningOnCompositor,
'runningOnCompositor property should match');
if (sortedExpected[i].warning instanceof RegExp) {
assert_regexp_match(sortedActual[i].warning,
sortedExpected[i].warning,
'warning message should match');
} else if (sortedExpected[i].warning) {
assert_equals(sortedActual[i].warning,
gStringBundle.GetStringFromName(sortedExpected[i].warning),
'warning message should match');
}
}
}
// Check that the animation is running on compositor and
// warning property is not set for the CSS property regardless
// expected values.
function assert_all_properties_running_on_compositor(actual, expected) {
assert_equals(actual.length, expected.length);
var sortedActual = actual.sort(compare_property_state);
var sortedExpected = expected.sort(compare_property_state);
for (var i = 0; i < sortedActual.length; i++) {
assert_equals(sortedActual[i].property,
sortedExpected[i].property,
'CSS property name should match');
assert_true(sortedActual[i].runningOnCompositor,
'runningOnCompositor property should be true on ' +
sortedActual[i].property);
assert_not_exists(sortedActual[i], 'warning',
'warning property should not be set');
}
}
function testBasicOperation() {
[
{
desc: 'animations on compositor',
frames: {
opacity: [0, 1]
},
expected: [
{
property: 'opacity',
runningOnCompositor: true
}
]
},
{
desc: 'animations on main thread',
frames: {
zIndex: ['0', '999']
},
expected: [
{
property: 'z-index',
runningOnCompositor: false
}
]
},
{
desc: 'animations on both threads',
frames: {
zIndex: ['0', '999'],
transform: ['translate(0px)', 'translate(100px)']
},
expected: [
{
property: 'z-index',
runningOnCompositor: false
},
{
property: 'transform',
runningOnCompositor: true
}
]
},
{
desc: 'two animation properties on compositor thread',
frames: {
opacity: [0, 1],
transform: ['translate(0px)', 'translate(100px)']
},
expected: [
{
property: 'opacity',
runningOnCompositor: true
},
{
property: 'transform',
runningOnCompositor: true
}
]
},
{
desc: 'two transform-like animation properties on compositor thread',
frames: {
transform: ['translate(0px)', 'translate(100px)'],
translate: ['0px', '100px']
},
expected: [
{
property: 'transform',
runningOnCompositor: true
},
{
property: 'translate',
runningOnCompositor: true
}
]
},
{
desc: 'opacity on compositor with animation of geometric properties',
frames: {
width: ['100px', '200px'],
opacity: [0, 1]
},
expected: [
{
property: 'width',
runningOnCompositor: false
},
{
property: 'opacity',
runningOnCompositor: true
}
]
},
].forEach(subtest => {
promise_test(async t => {
var animation = addDivAndAnimate(t, { class: 'compositable' },
subtest.frames, 100 * MS_PER_SEC);
await waitForPaints();
assert_animation_property_state_equals(
animation.effect.getProperties(),
subtest.expected);
}, subtest.desc);
});
}
// Test adding/removing a 'width' property on the same animation object.
function testKeyframesWithGeometricProperties() {
[
{
desc: 'transform',
frames: {
transform: ['translate(0px)', 'translate(100px)']
},
expected: {
withoutGeometric: [
{
property: 'transform',
runningOnCompositor: true
}
],
withGeometric: [
{
property: 'width',
runningOnCompositor: false
},
{
property: 'transform',
runningOnCompositor: true,
}
]
}
},
{
desc: 'translate',
frames: {
translate: ['0px', '100px']
},
expected: {
withoutGeometric: [
{
property: 'translate',
runningOnCompositor: true
}
],
withGeometric: [
{
property: 'width',
runningOnCompositor: false
},
{
property: 'translate',
runningOnCompositor: true,
}
]
}
},
{
desc: 'opacity and transform-like properties',
frames: {
opacity: [0, 1],
transform: ['translate(0px)', 'translate(100px)'],
translate: ['0px', '100px']
},
expected: {
withoutGeometric: [
{
property: 'opacity',
runningOnCompositor: true
},
{
property: 'transform',
runningOnCompositor: true
},
{
property: 'translate',
runningOnCompositor: true
}
],
withGeometric: [
{
property: 'width',
runningOnCompositor: false
},
{
property: 'opacity',
runningOnCompositor: true
},
{
property: 'transform',
runningOnCompositor: true,
},
{
property: 'translate',
runningOnCompositor: true,
}
]
}
},
].forEach(subtest => {
promise_test(async t => {
var animation = addDivAndAnimate(t, { class: 'compositable' },
subtest.frames, 100 * MS_PER_SEC);
await waitForPaints();
// First, a transform animation is running on compositor.
assert_animation_property_state_equals(
animation.effect.getProperties(),
subtest.expected.withoutGeometric);
// Add a 'width' property.
var keyframes = animation.effect.getKeyframes();
keyframes[0].width = '100px';
keyframes[1].width = '200px';
animation.effect.setKeyframes(keyframes);
await waitForFrame();
// Now the transform animation is not running on compositor because of
// the 'width' property.
assert_animation_property_state_equals(
animation.effect.getProperties(),
subtest.expected.withGeometric);
// Remove the 'width' property.
var keyframes = animation.effect.getKeyframes();
delete keyframes[0].width;
delete keyframes[1].width;
animation.effect.setKeyframes(keyframes);
await waitForFrame();
// Finally, the transform animation is running on compositor.
assert_animation_property_state_equals(
animation.effect.getProperties(),
subtest.expected.withoutGeometric);
}, 'An animation has: ' + subtest.desc);
});
}
// Test that the expected set of geometric properties all block transform
// animations.
function testSetOfGeometricProperties() {
const geometricProperties = [
'width', 'height',
'top', 'right', 'bottom', 'left',
'margin-top', 'margin-right', 'margin-bottom', 'margin-left',
'padding-top', 'padding-right', 'padding-bottom', 'padding-left'
];
geometricProperties.forEach(property => {
promise_test(async t => {
const keyframes = {
[propertyToIDL(property)]: [ '100px', '200px' ],
transform: [ 'translate(0px)', 'translate(100px)' ]
};
var animation = addDivAndAnimate(t, { class: 'compositable' },
keyframes, 100 * MS_PER_SEC);
await waitForPaints();
assert_animation_property_state_equals(
animation.effect.getProperties(),
[
{
property,
runningOnCompositor: false
},
{
property: 'transform',
runningOnCompositor: true,
}
]);
}, `${property} is treated as a geometric property`);
});
}
// Performance warning tests that set and clear a style property.
function testStyleChanges() {
[
{
desc: 'preserve-3d transform',
frames: {
transform: ['translate(0px)', 'translate(100px)']
},
style: 'transform-style: preserve-3d',
expected: [
{
property: 'transform',
runningOnCompositor: true,
}
]
},
{
desc: 'preserve-3d translate',
frames: {
translate: ['0px', '100px']
},
style: 'transform-style: preserve-3d',
expected: [
{
property: 'translate',
runningOnCompositor: true,
}
]
},
{
desc: 'transform with backface-visibility:hidden',
frames: {
transform: ['translate(0px)', 'translate(100px)']
},
style: 'backface-visibility: hidden;',
expected: [
{
property: 'transform',
runningOnCompositor: true,
}
]
},
{
desc: 'translate with backface-visibility:hidden',
frames: {
translate: ['0px', '100px']
},
style: 'backface-visibility: hidden;',
expected: [
{
property: 'translate',
runningOnCompositor: true,
}
]
},
{
desc: 'opacity and transform-like properties with preserve-3d',
frames: {
opacity: [0, 1],
transform: ['translate(0px)', 'translate(100px)'],
translate: ['0px', '100px']
},
style: 'transform-style: preserve-3d',
expected: [
{
property: 'opacity',
runningOnCompositor: true
},
{
property: 'transform',
runningOnCompositor: true,
},
{
property: 'translate',
runningOnCompositor: true,
}
]
},
{
desc: 'opacity and transform-like properties with ' +
'backface-visibility:hidden',
frames: {
opacity: [0, 1],
transform: ['translate(0px)', 'translate(100px)'],
translate: ['0px', '100px']
},
style: 'backface-visibility: hidden;',
expected: [
{
property: 'opacity',
runningOnCompositor: true
},
{
property: 'transform',
runningOnCompositor: true,
},
{
property: 'translate',
runningOnCompositor: true,
}
]
},
].forEach(subtest => {
promise_test(async t => {
var animation = addDivAndAnimate(t, { class: 'compositable' },
subtest.frames, 100 * MS_PER_SEC);
await waitForPaints();
assert_all_properties_running_on_compositor(
animation.effect.getProperties(),
subtest.expected);
animation.effect.target.style = subtest.style;
await waitForFrame();
assert_animation_property_state_equals(
animation.effect.getProperties(),
subtest.expected);
animation.effect.target.style = '';
await waitForFrame();
assert_all_properties_running_on_compositor(
animation.effect.getProperties(),
subtest.expected);
}, subtest.desc);
});
}
// Performance warning tests that set and clear the id property
function testIdChanges() {
[
{
desc: 'moz-element referencing a transform',
frames: {
transform: ['translate(0px)', 'translate(100px)']
},
id: 'transformed',
createelement: 'width:100px; height:100px; background: -moz-element(#transformed)',
expected: [
{
property: 'transform',
runningOnCompositor: false,
warning: 'CompositorAnimationWarningHasRenderingObserver'
}
]
},
{
desc: 'moz-element referencing a translate',
frames: {
translate: ['0px', '100px']
},
id: 'transformed',
createelement: 'width:100px; height:100px; background: -moz-element(#transformed)',
expected: [
{
property: 'translate',
runningOnCompositor: false,
warning: 'CompositorAnimationWarningHasRenderingObserver'
}
]
},
{
desc: 'moz-element referencing a translate and transform',
frames: {
transform: ['translate(0px)', 'translate(100px)'],
translate: ['0px', '100px']
},
id: 'transformed',
createelement: 'width:100px; height:100px; background: -moz-element(#transformed)',
expected: [
{
property: 'translate',
runningOnCompositor: false,
warning: 'CompositorAnimationWarningHasRenderingObserver'
},
{
property: 'transform',
runningOnCompositor: false,
warning: 'CompositorAnimationWarningHasRenderingObserver'
}
]
},
].forEach(subtest => {
promise_test(async t => {
if (subtest.createelement) {
addDiv(t, { style: subtest.createelement });
}
var animation = addDivAndAnimate(t, { class: 'compositable' },
subtest.frames, 100 * MS_PER_SEC);
await waitForPaints();
assert_all_properties_running_on_compositor(
animation.effect.getProperties(),
subtest.expected);
animation.effect.target.id = subtest.id;
await waitForFrame();
assert_animation_property_state_equals(
animation.effect.getProperties(),
subtest.expected);
animation.effect.target.id = '';
await waitForFrame();
assert_all_properties_running_on_compositor(
animation.effect.getProperties(),
subtest.expected);
}, subtest.desc);
});
}
function testMultipleAnimations() {
[
{
desc: 'opacity and transform-like properties with preserve-3d',
style: 'transform-style: preserve-3d',
animations: [
{
frames: {
transform: ['translate(0px)', 'translate(100px)']
},
expected: [
{
property: 'transform',
runningOnCompositor: true,
}
]
},
{
frames: {
translate: ['0px', '100px']
},
expected: [
{
property: 'translate',
runningOnCompositor: true,
}
]
},
{
frames: {
opacity: [0, 1]
},
expected: [
{
property: 'opacity',
runningOnCompositor: true,
}
]
}
],
},
{
desc: 'opacity and transform-like properties with ' +
'backface-visibility:hidden',
style: 'backface-visibility: hidden;',
animations: [
{
frames: {
transform: ['translate(0px)', 'translate(100px)']
},
expected: [
{
property: 'transform',
runningOnCompositor: true,
}
]
},
{
frames: {
translate: ['0px', '100px']
},
expected: [
{
property: 'translate',
runningOnCompositor: true,
}
]
},
{
frames: {
opacity: [0, 1]
},
expected: [
{
property: 'opacity',
runningOnCompositor: true,
}
]
}
],
},
].forEach(subtest => {
promise_test(async t => {
var div = addDiv(t, { class: 'compositable' });
var animations = subtest.animations.map(anim => {
var animation = div.animate(anim.frames, 100 * MS_PER_SEC);
// Bind expected values to animation object.
animation.expected = anim.expected;
return animation;
});
await waitForPaints();
animations.forEach(anim => {
assert_all_properties_running_on_compositor(
anim.effect.getProperties(),
anim.expected);
});
div.style = subtest.style;
await waitForFrame();
animations.forEach(anim => {
assert_animation_property_state_equals(
anim.effect.getProperties(),
anim.expected);
});
div.style = '';
await waitForFrame();
animations.forEach(anim => {
assert_all_properties_running_on_compositor(
anim.effect.getProperties(),
anim.expected);
});
}, 'Multiple animations: ' + subtest.desc);
});
}
// Test adding/removing a 'width' keyframe on the same animation object, where
// multiple animation objects belong to the same element.
// The 'width' property is added to animations[1].
function testMultipleAnimationsWithGeometricKeyframes() {
[
{
desc: 'transform and opacity with geometric keyframes',
animations: [
{
frames: {
transform: ['translate(0px)', 'translate(100px)']
},
expected: {
withoutGeometric: [
{
property: 'transform',
runningOnCompositor: true
}
],
withGeometric: [
{
property: 'transform',
runningOnCompositor: true,
}
]
}
},
{
frames: {
opacity: [0, 1]
},
expected: {
withoutGeometric: [
{
property: 'opacity',
runningOnCompositor: true,
}
],
withGeometric: [
{
property: 'width',
runningOnCompositor: false,
},
{
property: 'opacity',
runningOnCompositor: true,
}
]
}
}
],
},
{
desc: 'opacity and transform with geometric keyframes',
animations: [
{
frames: {
opacity: [0, 1]
},
expected: {
withoutGeometric: [
{
property: 'opacity',
runningOnCompositor: true,
}
],
withGeometric: [
{
property: 'opacity',
runningOnCompositor: true,
}
]
}
},
{
frames: {
transform: ['translate(0px)', 'translate(100px)']
},
expected: {
withoutGeometric: [
{
property: 'transform',
runningOnCompositor: true
}
],
withGeometric: [
{
property: 'width',
runningOnCompositor: false,
},
{
property: 'transform',
runningOnCompositor: true,
}
]
}
}
]
},
{
desc: 'opacity and translate with geometric keyframes',
animations: [
{
frames: {
opacity: [0, 1]
},
expected: {
withoutGeometric: [
{
property: 'opacity',
runningOnCompositor: true,
}
],
withGeometric: [
{
property: 'opacity',
runningOnCompositor: true,
}
]
}
},
{
frames: {
translate: ['0px', '100px']
},
expected: {
withoutGeometric: [
{
property: 'translate',
runningOnCompositor: true
}
],
withGeometric: [
{
property: 'width',
runningOnCompositor: false,
},
{
property: 'translate',
runningOnCompositor: true,
}
]
}
}
]
},
].forEach(subtest => {
promise_test(async t => {
var div = addDiv(t, { class: 'compositable' });
var animations = subtest.animations.map(anim => {
var animation = div.animate(anim.frames, 100 * MS_PER_SEC);
// Bind expected values to animation object.
animation.expected = anim.expected;
return animation;
});
await waitForPaints();
// First, all animations are running on compositor.
animations.forEach(anim => {
assert_animation_property_state_equals(
anim.effect.getProperties(),
anim.expected.withoutGeometric);
});
// Add a 'width' property to animations[1].
var keyframes = animations[1].effect.getKeyframes();
keyframes[0].width = '100px';
keyframes[1].width = '200px';
animations[1].effect.setKeyframes(keyframes);
await waitForFrame();
// Now the transform animation is not running on compositor because of
// the 'width' property.
animations.forEach(anim => {
assert_animation_property_state_equals(
anim.effect.getProperties(),
anim.expected.withGeometric);
});
// Remove the 'width' property from animations[1].
var keyframes = animations[1].effect.getKeyframes();
delete keyframes[0].width;
delete keyframes[1].width;
animations[1].effect.setKeyframes(keyframes);
await waitForFrame();
// Finally, all animations are running on compositor.
animations.forEach(anim => {
assert_animation_property_state_equals(
anim.effect.getProperties(),
anim.expected.withoutGeometric);
});
}, 'Multiple animations with geometric property: ' + subtest.desc);
});
}
// Tests adding/removing 'width' animation on the same element which has async
// animations.
function testMultipleAnimationsWithGeometricAnimations() {
[
{
desc: 'transform',
animations: [
{
frames: {
transform: ['translate(0px)', 'translate(100px)']
},
expected: [
{
property: 'transform',
runningOnCompositor: true,
}
]
},
]
},
{
desc: 'translate',
animations: [
{
frames: {
translate: ['0px', '100px']
},
expected: [
{
property: 'translate',
runningOnCompositor: true,
}
]
},
]
},
{
desc: 'opacity',
animations: [
{
frames: {
opacity: [0, 1]
},
expected: [
{
property: 'opacity',
runningOnCompositor: true
}
]
},
]
},
{
desc: 'opacity, transform, and translate',
animations: [
{
frames: {
transform: ['translate(0px)', 'translate(100px)']
},
expected: [
{
property: 'transform',
runningOnCompositor: true,
}
]
},
{
frames: {
translate: ['0px', '100px']
},
expected: [
{
property: 'translate',
runningOnCompositor: true,
}
]
},
{
frames: {
opacity: [0, 1]
},
expected: [
{
property: 'opacity',
runningOnCompositor: true,
}
]
}
],
},
].forEach(subtest => {
promise_test(async t => {
var div = addDiv(t, { class: 'compositable' });
var animations = subtest.animations.map(anim => {
var animation = div.animate(anim.frames, 100 * MS_PER_SEC);
// Bind expected values to animation object.
animation.expected = anim.expected;
return animation;
});
var widthAnimation;
await waitForPaints();
animations.forEach(anim => {
assert_all_properties_running_on_compositor(
anim.effect.getProperties(),
anim.expected);
});
// Append 'width' animation on the same element.
widthAnimation = div.animate({ width: ['100px', '200px'] },
100 * MS_PER_SEC);
await waitForFrame();
// Now transform animations are not running on compositor because of
// the 'width' animation.
animations.forEach(anim => {
assert_animation_property_state_equals(
anim.effect.getProperties(),
anim.expected);
});
// Remove the 'width' animation.
widthAnimation.cancel();
await waitForFrame();
// Now all animations are running on compositor.
animations.forEach(anim => {
assert_all_properties_running_on_compositor(
anim.effect.getProperties(),
anim.expected);
});
}, 'Multiple async animations and geometric animation: ' + subtest.desc);
});
}
function testSmallElements() {
[
{
desc: 'opacity on small element',
frames: {
opacity: [0, 1]
},
style: { style: 'width: 8px; height: 8px; background-color: red;' +
// We need to set transform here to try creating an
// individual frame for this opacity element.
// Without this, this small element is created on the same
// nsIFrame of mochitest iframe, i.e. the document which are
// running this test, as a result the layer corresponding
// to the frame is sent to compositor.
'transform: translateX(100px);' },
expected: [
{
property: 'opacity',
runningOnCompositor: true
}
]
},
{
desc: 'transform on small element',
frames: {
transform: ['translate(0px)', 'translate(100px)']
},
style: { style: 'width: 8px; height: 8px; background-color: red;' },
expected: [
{
property: 'transform',
runningOnCompositor: true
}
]
},
{
desc: 'translate on small element',
frames: {
translate: ['0px', '100px']
},
style: { style: 'width: 8px; height: 8px; background-color: red;' },
expected: [
{
property: 'translate',
runningOnCompositor: true
}
]
},
].forEach(subtest => {
promise_test(async t => {
var div = addDiv(t, subtest.style);
var animation = div.animate(subtest.frames, 100 * MS_PER_SEC);
await waitForPaints();
assert_animation_property_state_equals(
animation.effect.getProperties(),
subtest.expected);
}, subtest.desc);
});
}
function testSynchronizedAnimations() {
promise_test(async t => {
const elemA = addDiv(t, { class: 'compositable' });
const elemB = addDiv(t, { class: 'compositable' });
const animA = elemA.animate({ transform: [ 'translate(0px)',
'translate(100px)' ] },
100 * MS_PER_SEC);
const animB = elemB.animate({ marginLeft: [ '0px', '100px' ] },
100 * MS_PER_SEC);
await Promise.all([animA.ready, animB.ready]);
await waitForPaints();
assert_animation_property_state_equals(
animA.effect.getProperties(),
[ { property: 'transform',
runningOnCompositor: true,
} ]);
}, 'Animations created within the same tick are synchronized'
+ ' (compositor animation created first)');
promise_test(async t => {
const elemA = addDiv(t, { class: 'compositable' });
const elemB = addDiv(t, { class: 'compositable' });
const elemC = addDiv(t, { class: 'compositable' });
const animA = elemA.animate({ transform: [ 'translate(0px)',
'translate(100px)' ] },
100 * MS_PER_SEC);
const animB = elemB.animate({ translate: [ '0px', '100px' ] },
100 * MS_PER_SEC);
const animC = elemC.animate({ marginLeft: [ '0px', '100px' ] },
100 * MS_PER_SEC);
await Promise.all([animA.ready, animB.ready, animC.ready]);
await waitForPaints();
assert_animation_property_state_equals(
animA.effect.getProperties(),
[
{ property: 'transform',
runningOnCompositor: true,
} ]);
assert_animation_property_state_equals(
animB.effect.getProperties(),
[
{ property: 'translate',
runningOnCompositor: true,
} ]);
}, 'Animations created within the same tick are synchronized'
+ ' (compositor animation created first/second)');
promise_test(async t => {
const elemA = addDiv(t, { class: 'compositable' });
const elemB = addDiv(t, { class: 'compositable' });
const elemC = addDiv(t, { class: 'compositable' });
const animA = elemA.animate({ marginLeft: [ '0px', '100px' ] },
100 * MS_PER_SEC);
const animB = elemB.animate({ transform: [ 'translate(0px)',
'translate(100px)' ] },
100 * MS_PER_SEC);
const animC = elemC.animate({ translate: [ '0px', '100px' ] },
100 * MS_PER_SEC);
await Promise.all([animA.ready, animB.ready, animC.ready]);
await waitForPaints();
assert_animation_property_state_equals(
animB.effect.getProperties(),
[ { property: 'transform',
runningOnCompositor: true,
} ]);
assert_animation_property_state_equals(
animC.effect.getProperties(),
[
{ property: 'translate',
runningOnCompositor: true,
} ]);
}, 'Animations created within the same tick are synchronized'
+ ' (compositor animation created second/third)');
promise_test(async t => {
const attrs = { class: 'compositable',
style: 'transition: all 100s' };
const elemA = addDiv(t, attrs);
const elemB = addDiv(t, attrs);
elemA.style.transform = 'translate(0px)';
elemB.style.marginLeft = '0px';
getComputedStyle(elemA).transform;
getComputedStyle(elemB).marginLeft;
// Generally the sequence of steps is as follows:
//
// Tick -> requestAnimationFrame -> Style -> Paint -> Events (-> Tick...)
//
// In this test we want to set up two transitions during the "Events"
// stage but only flush style for one such that the second one is actually
// generated during the "Style" stage of the *next* tick.
//
// Web content often generates transitions in this way (that is, it doesn't
// pay regard to when style is flushed and nor should it). However, we
// still want transitions generated in this way to be synchronized.
let timeForFirstFrame;
await waitForIdle();
timeForFirstFrame = document.timeline.currentTime;
elemA.style.transform = 'translate(100px)';
// Flush style to trigger first transition
getComputedStyle(elemA).transform;
elemB.style.marginLeft = '100px';
// DON'T flush style here (this includes calling getAnimations!)
await waitForFrame();
assert_not_equals(timeForFirstFrame, document.timeline.currentTime,
'Should be on the other side of a tick');
// Wait another tick so we can let the transition be started
// by regular style resolution.
await waitForFrame();
const transitionA = elemA.getAnimations()[0];
assert_animation_property_state_equals(
transitionA.effect.getProperties(),
[ { property: 'transform',
runningOnCompositor: true,
} ]);
}, 'Transitions created before and after a tick are synchronized');
promise_test(async t => {
const elemA = addDiv(t, { class: 'compositable' });
const elemB = addDiv(t, { class: 'compositable' });
const animA = elemA.animate({ transform: [ 'translate(0px)',
'translate(100px)' ],
opacity: [ 0, 1 ] },
100 * MS_PER_SEC);
const animB = elemB.animate({ marginLeft: [ '0px', '100px' ] },
100 * MS_PER_SEC);
await Promise.all([animA.ready, animB.ready]);
await waitForPaints();
assert_animation_property_state_equals(
animA.effect.getProperties(),
[ { property: 'transform',
runningOnCompositor: true,
},
{ property: 'opacity',
runningOnCompositor: true
} ]);
}, 'Opacity animations on the same element continue running on the'
+ ' compositor when transform animations are synchronized with geometric'
+ ' animations');
promise_test(async t => {
const transitionElem = addDiv(t, {
style: 'margin-left: 0px; transition: margin-left 100s',
});
getComputedStyle(transitionElem).marginLeft;
await waitForFrame();
transitionElem.style.marginLeft = '100px';
const cssTransition = transitionElem.getAnimations()[0];
const animationElem = addDiv(t, {
class: 'compositable',
style: 'animation: translate 100s',
});
const cssAnimation = animationElem.getAnimations()[0];
await Promise.all([cssTransition.ready, cssAnimation.ready]);
await waitForPaints();
assert_animation_property_state_equals(cssAnimation.effect.getProperties(),
[{ property: 'transform',
runningOnCompositor: true }]);
}, 'CSS Animations are NOT synchronized with CSS Transitions');
promise_test(async t => {
const elemA = addDiv(t, { class: 'compositable' });
const elemB = addDiv(t, { class: 'compositable' });
const animA = elemA.animate({ marginLeft: [ '0px', '100px' ] },
100 * MS_PER_SEC);
await animA.ready;
await waitForPaints();
let animB = elemB.animate({ transform: [ 'translate(0px)',
'translate(100px)' ] },
100 * MS_PER_SEC);
await animB.ready;
await waitForPaints();
assert_animation_property_state_equals(
animB.effect.getProperties(),
[ { property: 'transform',
runningOnCompositor: true } ]);
}, 'Transform animations are NOT synchronized with geometric animations'
+ ' started in the previous frame');
promise_test(async t => {
const elemA = addDiv(t, { class: 'compositable' });
const elemB = addDiv(t, { class: 'compositable' });
const animA = elemA.animate({ transform: [ 'translate(0px)',
'translate(100px)' ] },
100 * MS_PER_SEC);
await animA.ready;
await waitForPaints();
let animB = elemB.animate({ marginLeft: [ '0px', '100px' ] },
100 * MS_PER_SEC);
await animB.ready;
await waitForPaints();
assert_animation_property_state_equals(
animA.effect.getProperties(),
[ { property: 'transform',
runningOnCompositor: true } ]);
}, 'Transform animations are NOT synchronized with geometric animations'
+ ' started in the next frame');
promise_test(async t => {
const elemA = addDiv(t, { class: 'compositable' });
const elemB = addDiv(t, { class: 'compositable' });
const animA = elemA.animate({ transform: [ 'translate(0px)',
'translate(100px)' ] },
100 * MS_PER_SEC);
const animB = elemB.animate({ marginLeft: [ '0px', '100px' ] },
100 * MS_PER_SEC);
animB.pause();
await animA.ready;
await waitForPaints();
assert_animation_property_state_equals(
animA.effect.getProperties(),
[ { property: 'transform', runningOnCompositor: true } ]);
}, 'Paused animations are not synchronized');
promise_test(async t => {
const elemA = addDiv(t, { class: 'compositable' });
const elemB = addDiv(t, { class: 'compositable' });
const animA = elemA.animate({ transform: [ 'translate(0px)',
'translate(100px)' ] },
100 * MS_PER_SEC);
const animB = elemB.animate({ marginLeft: [ '0px', '100px' ] },
100 * MS_PER_SEC);
// Seek one of the animations so that their start times will differ
animA.currentTime = 5000;
await Promise.all([animA.ready, animB.ready]);
await waitForPaints();
assert_not_equals(animA.startTime, animB.startTime,
'Animations should have different start times');
assert_animation_property_state_equals(
animA.effect.getProperties(),
[ { property: 'transform',
runningOnCompositor: true,
} ]);
}, 'Animations are synchronized based on when they are started'
+ ' and NOT their start time');
promise_test(async t => {
const elemA = addDiv(t, { class: 'compositable' });
const elemB = addDiv(t, { class: 'compositable' });
const animA = elemA.animate({ transform: [ 'translate(0px)',
'translate(100px)' ] },
100 * MS_PER_SEC);
const animB = elemB.animate({ marginLeft: [ '0px', '100px' ] },
100 * MS_PER_SEC);
await Promise.all([animA.ready, animB.ready]);
await waitForPaints();
assert_animation_property_state_equals(
animA.effect.getProperties(),
[ { property: 'transform',
runningOnCompositor: true } ]);
// Restart animation
animA.pause();
animA.play();
await animA.ready;
await waitForPaints();
assert_animation_property_state_equals(
animA.effect.getProperties(),
[ { property: 'transform',
runningOnCompositor: true } ]);
}, 'An initially synchronized animation may be unsynchronized if restarted');
promise_test(async t => {
const elemA = addDiv(t, { class: 'compositable' });
const elemB = addDiv(t, { class: 'compositable' });
const animA = elemA.animate({ transform: [ 'translate(0px)',
'translate(100px)' ] },
100 * MS_PER_SEC);
const animB = elemB.animate({ marginLeft: [ '0px', '100px' ] },
100 * MS_PER_SEC);
// Clear target effect
animB.effect.target = null;
await Promise.all([animA.ready, animB.ready]);
await waitForPaints();
assert_animation_property_state_equals(
animA.effect.getProperties(),
[ { property: 'transform',
runningOnCompositor: true } ]);
}, 'A geometric animation with no target element is not synchronized');
}
function testTooLargeFrame() {
[
{
property: 'transform',
frames: { transform: ['translate(0px)', 'translate(100px)'] },
},
{
property: 'translate',
frames: { translate: ['0px', '100px'] },
},
].forEach(subtest => {
promise_test(async t => {
var animation = addDivAndAnimate(t,
{ class: 'compositable' },
subtest.frames,
100 * MS_PER_SEC);
await waitForPaints();
assert_animation_property_state_equals(
animation.effect.getProperties(),
[ { property: subtest.property, runningOnCompositor: true } ]);
animation.effect.target.style = 'width: 10000px; height: 10000px';
await waitForFrame();
// viewport depends on test environment.
var expectedWarning = new RegExp(
"Animation cannot be run on the compositor because the area of the frame " +
"\\(\\d+\\) is too large relative to the viewport " +
"\\(larger than \\d+\\)");
assert_animation_property_state_equals(
animation.effect.getProperties(),
[ {
property: subtest.property,
runningOnCompositor: false,
warning: expectedWarning
} ]);
animation.effect.target.style = 'width: 100px; height: 100px';
await waitForFrame();
// With WebRender we appear to stick to the previous layerization decision
// after changing the bounds back to a smaller object.
const isWebRender =
SpecialPowers.DOMWindowUtils.layerManagerType.startsWith('WebRender');
assert_animation_property_state_equals(
animation.effect.getProperties(),
[ { property: subtest.property, runningOnCompositor: !isWebRender } ]);
}, subtest.property + ' on too big element - area');
promise_test(async t => {
var animation = addDivAndAnimate(t,
{ class: 'compositable' },
subtest.frames,
100 * MS_PER_SEC);
await waitForPaints();
assert_animation_property_state_equals(
animation.effect.getProperties(),
[ { property: subtest.property, runningOnCompositor: true } ]);
animation.effect.target.style = 'width: 20000px; height: 1px';
await waitForFrame();
// viewport depends on test environment.
var expectedWarning = new RegExp(
"Animation cannot be run on the compositor because the frame size " +
"\\(20000, 1\\) is too large relative to the viewport " +
"\\(larger than \\(\\d+, \\d+\\)\\) or larger than the " +
"maximum allowed value \\(\\d+, \\d+\\)");
assert_animation_property_state_equals(
animation.effect.getProperties(),
[ {
property: subtest.property,
runningOnCompositor: false,
warning: expectedWarning
} ]);
animation.effect.target.style = 'width: 100px; height: 100px';
await waitForFrame();
const isWebRender =
SpecialPowers.DOMWindowUtils.layerManagerType.startsWith('WebRender');
assert_animation_property_state_equals(
animation.effect.getProperties(),
[ { property: subtest.property, runningOnCompositor: !isWebRender } ]);
}, subtest.property + ' on too big element - dimensions');
});
}
function testTransformSVG() {
[
{
property: 'transform',
frames: { transform: ['translate(0px)', 'translate(100px)'] },
},
{
property: 'translate',
frames: { translate: ['0px', '100px'] },
},
{
property: 'rotate',
frames: { rotate: ['0deg', '45deg'] },
},
{
property: 'scale',
frames: { scale: ['1', '2'] },
},
].forEach(subtest => {
promise_test(async t => {
var svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
svg.setAttribute('width', '100');
svg.setAttribute('height', '100');
var rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
rect.setAttribute('width', '100');
rect.setAttribute('height', '100');
rect.setAttribute('fill', 'red');
svg.appendChild(rect);
document.body.appendChild(svg);
t.add_cleanup(() => {
svg.remove();
});
var animation = svg.animate(subtest.frames, 100 * MS_PER_SEC);
await waitForPaints();
assert_animation_property_state_equals(
animation.effect.getProperties(),
[ { property: subtest.property, runningOnCompositor: true } ]);
svg.setAttribute('transform', 'translate(10, 20)');
await waitForFrame();
assert_animation_property_state_equals(
animation.effect.getProperties(),
[ {
property: subtest.property,
runningOnCompositor: false,
warning: 'CompositorAnimationWarningTransformSVG'
} ]);
svg.removeAttribute('transform');
await waitForFrame();
assert_animation_property_state_equals(
animation.effect.getProperties(),
[ { property: subtest.property, runningOnCompositor: true } ]);
}, subtest.property + ' of nsIFrame with SVG transform');
});
}
function testImportantRuleOverride() {
promise_test(async t => {
const elem = addDiv(t, { class: 'compositable' });
const anim = elem.animate({ translate: [ '0px', '100px' ],
rotate: ['0deg', '90deg'] },
100 * MS_PER_SEC);
await waitForAnimationReadyToRestyle(anim);
await waitForPaints();
assert_animation_property_state_equals(
anim.effect.getProperties(),
[ { property: 'translate', runningOnCompositor: true },
{ property: 'rotate', runningOnCompositor: true } ]
);
elem.style.setProperty('rotate', '45deg', 'important');
getComputedStyle(elem).rotate;
await waitForFrame();
assert_animation_property_state_equals(
anim.effect.getProperties(),
[
{
property: 'translate',
runningOnCompositor: false,
warning:
'CompositorAnimationWarningTransformIsBlockedByImportantRules'
},
{
property: 'rotate',
runningOnCompositor: false,
warning:
'CompositorAnimationWarningTransformIsBlockedByImportantRules'
},
]
);
}, 'The animations of transform-like properties are not running on the ' +
'compositor because any of the properties has important rules');
}
function testCurrentColor() {
if (SpecialPowers.DOMWindowUtils.layerManagerType.startsWith('WebRender')) {
return; // skip this test until bug 1510030 landed.
}
promise_test(async t => {
const animation = addDivAndAnimate(t, { class: 'compositable' },
{ backgroundColor: [ 'currentColor',
'red' ] },
100 * MS_PER_SEC);
await waitForPaints();
assert_animation_property_state_equals(
animation.effect.getProperties(),
[ { property: 'background-color',
runningOnCompositor: false,
warning: 'CompositorAnimationWarningHasCurrentColor'
} ]);
}, 'Background color animations with `current-color` don\'t run on the '
+ 'compositor');
}
function start() {
var bundleService = SpecialPowers.Cc['@mozilla.org/intl/stringbundle;1']
.getService(SpecialPowers.Ci.nsIStringBundleService);
gStringBundle = bundleService
testBasicOperation();
testKeyframesWithGeometricProperties();
testSetOfGeometricProperties();
testStyleChanges();
testIdChanges();
testMultipleAnimations();
testMultipleAnimationsWithGeometricKeyframes();
testMultipleAnimationsWithGeometricAnimations();
testSmallElements();
testSynchronizedAnimations();
testTooLargeFrame();
testTransformSVG();
testImportantRuleOverride();
testCurrentColor();
promise_test(async t => {
var div = addDiv(t, { class: 'compositable',
style: 'animation: fade 100s' });
var cssAnimation = div.getAnimations()[0];
var scriptAnimation = div.animate({ opacity: [ 1, 0 ] }, 100 * MS_PER_SEC);
await waitForPaints();
assert_animation_property_state_equals(
cssAnimation.effect.getProperties(),
[ { property: 'opacity', runningOnCompositor: true } ]);
assert_animation_property_state_equals(
scriptAnimation.effect.getProperties(),
[ { property: 'opacity', runningOnCompositor: true } ]);
}, 'overridden animation');
promise_test(async t => {
const keyframes = {
width: [ '100px', '200px' ],
transform: [ 'translate(0px)', 'translate(100px)' ],
"--foo": ["--bar", "--baz"],
};
const animation = addDivAndAnimate(t, { class: 'compositable' },
keyframes, 100 * MS_PER_SEC);
await waitForPaints();
assert_true(true, "Didn't crash");
}, 'Warning with custom props');
done();
}
</script>
</body>