зеркало из https://github.com/mozilla/gecko-dev.git
1303 строки
41 KiB
HTML
1303 строки
41 KiB
HTML
<!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 }
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1196114"
|
|
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.Cu.import("resource://gre/modules/Services.jsm");
|
|
Services.locale.setRequestedLocales(["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],
|
|
] },
|
|
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: {
|
|
backgroundColor: ['white', 'red']
|
|
},
|
|
expected: [
|
|
{
|
|
property: 'background-color',
|
|
runningOnCompositor: false
|
|
}
|
|
]
|
|
},
|
|
{
|
|
desc: 'animations on both threads',
|
|
frames: {
|
|
backgroundColor: ['white', 'red'],
|
|
transform: ['translate(0px)', 'translate(100px)']
|
|
},
|
|
expected: [
|
|
{
|
|
property: 'background-color',
|
|
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: '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(t => {
|
|
var animation = addDivAndAnimate(t, { class: 'compositable' },
|
|
subtest.frames, 100 * MS_PER_SEC);
|
|
return waitForPaints().then(() => {
|
|
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: false,
|
|
warning: 'CompositorAnimationWarningTransformWithGeometricProperties'
|
|
}
|
|
]
|
|
}
|
|
},
|
|
{
|
|
desc: 'opacity and transform',
|
|
frames: {
|
|
opacity: [0, 1],
|
|
transform: ['translate(0px)', 'translate(100px)']
|
|
},
|
|
expected: {
|
|
withoutGeometric: [
|
|
{
|
|
property: 'opacity',
|
|
runningOnCompositor: true
|
|
},
|
|
{
|
|
property: 'transform',
|
|
runningOnCompositor: true
|
|
}
|
|
],
|
|
withGeometric: [
|
|
{
|
|
property: 'width',
|
|
runningOnCompositor: false
|
|
},
|
|
{
|
|
property: 'opacity',
|
|
runningOnCompositor: true
|
|
},
|
|
{
|
|
property: 'transform',
|
|
runningOnCompositor: false,
|
|
warning: 'CompositorAnimationWarningTransformWithGeometricProperties'
|
|
}
|
|
]
|
|
}
|
|
},
|
|
].forEach(subtest => {
|
|
promise_test(t => {
|
|
var animation = addDivAndAnimate(t, { class: 'compositable' },
|
|
subtest.frames, 100 * MS_PER_SEC);
|
|
return waitForPaints().then(() => {
|
|
// First, a transform animation is running on compositor.
|
|
assert_animation_property_state_equals(
|
|
animation.effect.getProperties(),
|
|
subtest.expected.withoutGeometric);
|
|
}).then(() => {
|
|
// Add a 'width' property.
|
|
var keyframes = animation.effect.getKeyframes();
|
|
|
|
keyframes[0].width = '100px';
|
|
keyframes[1].width = '200px';
|
|
|
|
animation.effect.setKeyframes(keyframes);
|
|
return waitForFrame();
|
|
}).then(() => {
|
|
// 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);
|
|
}).then(() => {
|
|
// Remove the 'width' property.
|
|
var keyframes = animation.effect.getKeyframes();
|
|
|
|
delete keyframes[0].width;
|
|
delete keyframes[1].width;
|
|
|
|
animation.effect.setKeyframes(keyframes);
|
|
return waitForFrame();
|
|
}).then(() => {
|
|
// 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(t => {
|
|
const keyframes = {
|
|
[propertyToIDL(property)]: [ '100px', '200px' ],
|
|
transform: [ 'translate(0px)', 'translate(100px)' ]
|
|
};
|
|
var animation = addDivAndAnimate(t, { class: 'compositable' },
|
|
keyframes, 100 * MS_PER_SEC);
|
|
|
|
return waitForPaints().then(() => {
|
|
assert_animation_property_state_equals(
|
|
animation.effect.getProperties(),
|
|
[
|
|
{
|
|
property,
|
|
runningOnCompositor: false
|
|
},
|
|
{
|
|
property: 'transform',
|
|
runningOnCompositor: false,
|
|
warning: 'CompositorAnimationWarningTransformWithSyncGeometricAnimations'
|
|
}
|
|
]);
|
|
}, 'Transform animation should be run on the main thread');
|
|
}, `${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: false,
|
|
warning: 'CompositorAnimationWarningTransformPreserve3D'
|
|
}
|
|
]
|
|
},
|
|
{
|
|
desc: 'transform with backface-visibility:hidden',
|
|
frames: {
|
|
transform: ['translate(0px)', 'translate(100px)']
|
|
},
|
|
style: 'backface-visibility: hidden;',
|
|
expected: [
|
|
{
|
|
property: 'transform',
|
|
runningOnCompositor: false,
|
|
warning: 'CompositorAnimationWarningTransformBackfaceVisibilityHidden'
|
|
}
|
|
]
|
|
},
|
|
{
|
|
desc: 'opacity and transform with preserve-3d',
|
|
frames: {
|
|
opacity: [0, 1],
|
|
transform: ['translate(0px)', 'translate(100px)']
|
|
},
|
|
style: 'transform-style: preserve-3d',
|
|
expected: [
|
|
{
|
|
property: 'opacity',
|
|
runningOnCompositor: true
|
|
},
|
|
{
|
|
property: 'transform',
|
|
runningOnCompositor: false,
|
|
warning: 'CompositorAnimationWarningTransformPreserve3D'
|
|
}
|
|
]
|
|
},
|
|
{
|
|
desc: 'opacity and transform with backface-visibility:hidden',
|
|
frames: {
|
|
opacity: [0, 1],
|
|
transform: ['translate(0px)', 'translate(100px)']
|
|
},
|
|
style: 'backface-visibility: hidden;',
|
|
expected: [
|
|
{
|
|
property: 'opacity',
|
|
runningOnCompositor: true
|
|
},
|
|
{
|
|
property: 'transform',
|
|
runningOnCompositor: false,
|
|
warning: 'CompositorAnimationWarningTransformBackfaceVisibilityHidden'
|
|
}
|
|
]
|
|
},
|
|
].forEach(subtest => {
|
|
promise_test(t => {
|
|
var animation = addDivAndAnimate(t, { class: 'compositable' },
|
|
subtest.frames, 100 * MS_PER_SEC);
|
|
return waitForPaints().then(() => {
|
|
assert_all_properties_running_on_compositor(
|
|
animation.effect.getProperties(),
|
|
subtest.expected);
|
|
animation.effect.target.style = subtest.style;
|
|
return waitForFrame();
|
|
}).then(() => {
|
|
assert_animation_property_state_equals(
|
|
animation.effect.getProperties(),
|
|
subtest.expected);
|
|
animation.effect.target.style = '';
|
|
return waitForFrame();
|
|
}).then(() => {
|
|
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'
|
|
}
|
|
]
|
|
},
|
|
].forEach(subtest => {
|
|
promise_test(t => {
|
|
if (subtest.createelement) {
|
|
addDiv(t, { style: subtest.createelement });
|
|
}
|
|
|
|
var animation = addDivAndAnimate(t, { class: 'compositable' },
|
|
subtest.frames, 100 * MS_PER_SEC);
|
|
return waitForPaints().then(() => {
|
|
assert_all_properties_running_on_compositor(
|
|
animation.effect.getProperties(),
|
|
subtest.expected);
|
|
animation.effect.target.id = subtest.id;
|
|
return waitForFrame();
|
|
}).then(() => {
|
|
assert_animation_property_state_equals(
|
|
animation.effect.getProperties(),
|
|
subtest.expected);
|
|
animation.effect.target.id = '';
|
|
return waitForFrame();
|
|
}).then(() => {
|
|
assert_all_properties_running_on_compositor(
|
|
animation.effect.getProperties(),
|
|
subtest.expected);
|
|
});
|
|
}, subtest.desc);
|
|
});
|
|
}
|
|
|
|
function testMultipleAnimations() {
|
|
[
|
|
{
|
|
desc: 'opacity and transform with preserve-3d',
|
|
style: 'transform-style: preserve-3d',
|
|
animations: [
|
|
{
|
|
frames: {
|
|
transform: ['translate(0px)', 'translate(100px)']
|
|
},
|
|
expected: [
|
|
{
|
|
property: 'transform',
|
|
runningOnCompositor: false,
|
|
warning: 'CompositorAnimationWarningTransformPreserve3D'
|
|
}
|
|
]
|
|
},
|
|
{
|
|
frames: {
|
|
opacity: [0, 1]
|
|
},
|
|
expected: [
|
|
{
|
|
property: 'opacity',
|
|
runningOnCompositor: true,
|
|
}
|
|
]
|
|
}
|
|
],
|
|
},
|
|
{
|
|
desc: 'opacity and transform with backface-visibility:hidden',
|
|
style: 'backface-visibility: hidden;',
|
|
animations: [
|
|
{
|
|
frames: {
|
|
transform: ['translate(0px)', 'translate(100px)']
|
|
},
|
|
expected: [
|
|
{
|
|
property: 'transform',
|
|
runningOnCompositor: false,
|
|
warning: 'CompositorAnimationWarningTransformBackfaceVisibilityHidden'
|
|
}
|
|
]
|
|
},
|
|
{
|
|
frames: {
|
|
opacity: [0, 1]
|
|
},
|
|
expected: [
|
|
{
|
|
property: 'opacity',
|
|
runningOnCompositor: true,
|
|
}
|
|
]
|
|
}
|
|
],
|
|
},
|
|
].forEach(subtest => {
|
|
promise_test(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;
|
|
});
|
|
return waitForPaints().then(() => {
|
|
animations.forEach(anim => {
|
|
assert_all_properties_running_on_compositor(
|
|
anim.effect.getProperties(),
|
|
anim.expected);
|
|
});
|
|
div.style = subtest.style;
|
|
return waitForFrame();
|
|
}).then(() => {
|
|
animations.forEach(anim => {
|
|
assert_animation_property_state_equals(
|
|
anim.effect.getProperties(),
|
|
anim.expected);
|
|
});
|
|
div.style = '';
|
|
return waitForFrame();
|
|
}).then(() => {
|
|
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: false,
|
|
warning: 'CompositorAnimationWarningTransformWithGeometricProperties'
|
|
}
|
|
]
|
|
}
|
|
},
|
|
{
|
|
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: false,
|
|
warning: 'CompositorAnimationWarningTransformWithGeometricProperties'
|
|
}
|
|
]
|
|
}
|
|
}
|
|
]
|
|
},
|
|
].forEach(subtest => {
|
|
promise_test(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;
|
|
});
|
|
return waitForPaints().then(() => {
|
|
// First, all animations are running on compositor.
|
|
animations.forEach(anim => {
|
|
assert_animation_property_state_equals(
|
|
anim.effect.getProperties(),
|
|
anim.expected.withoutGeometric);
|
|
});
|
|
}).then(() => {
|
|
// 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);
|
|
return waitForFrame();
|
|
}).then(() => {
|
|
// 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);
|
|
});
|
|
}).then(() => {
|
|
// 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);
|
|
return waitForFrame();
|
|
}).then(() => {
|
|
// 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: false,
|
|
warning: 'CompositorAnimationWarningTransformWithGeometricProperties'
|
|
}
|
|
]
|
|
},
|
|
]
|
|
},
|
|
{
|
|
desc: 'opacity',
|
|
animations: [
|
|
{
|
|
frames: {
|
|
opacity: [0, 1]
|
|
},
|
|
expected: [
|
|
{
|
|
property: 'opacity',
|
|
runningOnCompositor: true
|
|
}
|
|
]
|
|
},
|
|
]
|
|
},
|
|
{
|
|
desc: 'opacity and transform',
|
|
animations: [
|
|
{
|
|
frames: {
|
|
transform: ['translate(0px)', 'translate(100px)']
|
|
},
|
|
expected: [
|
|
{
|
|
property: 'transform',
|
|
runningOnCompositor: false,
|
|
warning: 'CompositorAnimationWarningTransformWithGeometricProperties'
|
|
}
|
|
]
|
|
},
|
|
{
|
|
frames: {
|
|
opacity: [0, 1]
|
|
},
|
|
expected: [
|
|
{
|
|
property: 'opacity',
|
|
runningOnCompositor: true,
|
|
}
|
|
]
|
|
}
|
|
],
|
|
},
|
|
].forEach(subtest => {
|
|
promise_test(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;
|
|
|
|
return waitForPaints().then(() => {
|
|
animations.forEach(anim => {
|
|
assert_all_properties_running_on_compositor(
|
|
anim.effect.getProperties(),
|
|
anim.expected);
|
|
});
|
|
}).then(() => {
|
|
// Append 'width' animation on the same element.
|
|
widthAnimation = div.animate({ width: ['100px', '200px'] },
|
|
100 * MS_PER_SEC);
|
|
return waitForFrame();
|
|
}).then(() => {
|
|
// 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();
|
|
return waitForFrame();
|
|
}).then(() => {
|
|
// 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
|
|
}
|
|
]
|
|
},
|
|
].forEach(subtest => {
|
|
promise_test(t => {
|
|
var div = addDiv(t, subtest.style);
|
|
var animation = div.animate(subtest.frames, 100 * MS_PER_SEC);
|
|
return waitForPaints().then(() => {
|
|
assert_animation_property_state_equals(
|
|
animation.effect.getProperties(),
|
|
subtest.expected);
|
|
});
|
|
}, subtest.desc);
|
|
});
|
|
}
|
|
|
|
function testSynchronizedAnimations() {
|
|
promise_test(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);
|
|
|
|
return waitForPaints()
|
|
.then(() => {
|
|
assert_animation_property_state_equals(
|
|
animA.effect.getProperties(),
|
|
[ { property: 'transform',
|
|
runningOnCompositor: false,
|
|
warning: 'CompositorAnimationWarningTransformWithSyncGeometricAnimations'
|
|
} ]);
|
|
});
|
|
}, 'Animations created within the same tick are synchronized'
|
|
+ ' (compositor animation created first)');
|
|
|
|
promise_test(t => {
|
|
const elemA = addDiv(t, { class: 'compositable' });
|
|
const elemB = 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);
|
|
|
|
return waitForPaints()
|
|
.then(() => {
|
|
assert_animation_property_state_equals(
|
|
animB.effect.getProperties(),
|
|
[ { property: 'transform',
|
|
runningOnCompositor: false,
|
|
warning: 'CompositorAnimationWarningTransformWithSyncGeometricAnimations'
|
|
} ]);
|
|
});
|
|
}, 'Animations created within the same tick are synchronized'
|
|
+ ' (compositor animation created second)');
|
|
|
|
promise_test(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;
|
|
return waitForIdle()
|
|
.then(() => {
|
|
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!)
|
|
return waitForFrame();
|
|
}).then(() => {
|
|
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.
|
|
return waitForFrame();
|
|
}).then(() => {
|
|
const transitionA = elemA.getAnimations()[0];
|
|
assert_animation_property_state_equals(
|
|
transitionA.effect.getProperties(),
|
|
[ { property: 'transform',
|
|
runningOnCompositor: false,
|
|
warning: 'CompositorAnimationWarningTransformWithSyncGeometricAnimations'
|
|
} ]);
|
|
});
|
|
}, 'Transitions created before and after a tick are synchronized');
|
|
|
|
promise_test(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);
|
|
|
|
return waitForPaints()
|
|
.then(() => {
|
|
assert_animation_property_state_equals(
|
|
animA.effect.getProperties(),
|
|
[ { property: 'transform',
|
|
runningOnCompositor: false,
|
|
warning: 'CompositorAnimationWarningTransformWithSyncGeometricAnimations'
|
|
},
|
|
{ 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(t => {
|
|
const elemA = addDiv(t, { class: 'compositable' });
|
|
const elemB = addDiv(t, { class: 'compositable' });
|
|
|
|
const animA = elemA.animate({ marginLeft: [ '0px', '100px' ] },
|
|
100 * MS_PER_SEC);
|
|
let animB;
|
|
|
|
return waitForPaints()
|
|
.then(() => {
|
|
animB = elemB.animate({ transform: [ 'translate(0px)',
|
|
'translate(100px)' ] },
|
|
100 * MS_PER_SEC);
|
|
return animB.ready;
|
|
}).then(() => {
|
|
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(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);
|
|
let animB;
|
|
|
|
return waitForPaints()
|
|
.then(() => {
|
|
animB = elemB.animate({ marginLeft: [ '0px', '100px' ] },
|
|
100 * MS_PER_SEC);
|
|
return animB.ready;
|
|
}).then(() => {
|
|
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(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();
|
|
|
|
return waitForPaints()
|
|
.then(() => {
|
|
assert_animation_property_state_equals(
|
|
animA.effect.getProperties(),
|
|
[ { property: 'transform', runningOnCompositor: true } ]);
|
|
});
|
|
}, 'Paused animations are not synchronized');
|
|
|
|
promise_test(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;
|
|
|
|
return waitForPaints()
|
|
.then(() => {
|
|
assert_not_equals(animA.startTime, animB.startTime,
|
|
'Animations should have different start times');
|
|
assert_animation_property_state_equals(
|
|
animA.effect.getProperties(),
|
|
[ { property: 'transform',
|
|
runningOnCompositor: false,
|
|
warning: 'CompositorAnimationWarningTransformWithSyncGeometricAnimations'
|
|
} ]);
|
|
});
|
|
}, 'Animations are synchronized based on when they are started'
|
|
+ ' and NOT their start time');
|
|
|
|
promise_test(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);
|
|
|
|
return waitForPaints()
|
|
.then(() => {
|
|
assert_animation_property_state_equals(
|
|
animA.effect.getProperties(),
|
|
[ { property: 'transform',
|
|
runningOnCompositor: false } ]);
|
|
// Restart animation
|
|
animA.pause();
|
|
animA.play();
|
|
return animA.ready;
|
|
}).then(() => {
|
|
assert_animation_property_state_equals(
|
|
animA.effect.getProperties(),
|
|
[ { property: 'transform',
|
|
runningOnCompositor: true } ]);
|
|
});
|
|
}, 'An initially synchronized animation may be unsynchronized if restarted');
|
|
|
|
promise_test(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;
|
|
|
|
return waitForPaints()
|
|
.then(() => {
|
|
assert_animation_property_state_equals(
|
|
animA.effect.getProperties(),
|
|
[ { property: 'transform',
|
|
runningOnCompositor: true } ]);
|
|
});
|
|
}, 'A geometric animation with no target element is not synchronized');
|
|
}
|
|
|
|
function start() {
|
|
var bundleService = SpecialPowers.Cc['@mozilla.org/intl/stringbundle;1']
|
|
.getService(SpecialPowers.Ci.nsIStringBundleService);
|
|
gStringBundle = bundleService
|
|
.createBundle("chrome://global/locale/layout_errors.properties");
|
|
|
|
testBasicOperation();
|
|
testKeyframesWithGeometricProperties();
|
|
testSetOfGeometricProperties();
|
|
testStyleChanges();
|
|
testIdChanges();
|
|
testMultipleAnimations();
|
|
testMultipleAnimationsWithGeometricKeyframes();
|
|
testMultipleAnimationsWithGeometricAnimations();
|
|
testSmallElements();
|
|
testSynchronizedAnimations();
|
|
|
|
promise_test(t => {
|
|
var animation = addDivAndAnimate(t,
|
|
{ class: 'compositable' },
|
|
{ transform: [ 'translate(0px)',
|
|
'translate(100px)'] },
|
|
100 * MS_PER_SEC);
|
|
return waitForPaints().then(() => {
|
|
assert_animation_property_state_equals(
|
|
animation.effect.getProperties(),
|
|
[ { property: 'transform', runningOnCompositor: true } ]);
|
|
animation.effect.target.style = 'width: 5200px; height: 5200px';
|
|
return waitForFrame();
|
|
}).then(() => {
|
|
// 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: 'transform',
|
|
runningOnCompositor: false,
|
|
warning: expectedWarning
|
|
} ]);
|
|
animation.effect.target.style = 'width: 100px; height: 100px';
|
|
return waitForFrame();
|
|
}).then(() => {
|
|
// FIXME: Bug 1253164: the animation should get back on compositor.
|
|
assert_animation_property_state_equals(
|
|
animation.effect.getProperties(),
|
|
[ { property: 'transform', runningOnCompositor: false } ]);
|
|
});
|
|
}, 'transform on too big element - area');
|
|
|
|
promise_test(t => {
|
|
var animation = addDivAndAnimate(t,
|
|
{ class: 'compositable' },
|
|
{ transform: [ 'translate(0px)',
|
|
'translate(100px)'] },
|
|
100 * MS_PER_SEC);
|
|
return waitForPaints().then(() => {
|
|
assert_animation_property_state_equals(
|
|
animation.effect.getProperties(),
|
|
[ { property: 'transform', runningOnCompositor: true } ]);
|
|
animation.effect.target.style = 'width: 5200px; height: 1px';
|
|
return waitForFrame();
|
|
}).then(() => {
|
|
// viewport depends on test environment.
|
|
var expectedWarning = new RegExp(
|
|
"Animation cannot be run on the compositor because the frame size " +
|
|
"\\(5200, 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: 'transform',
|
|
runningOnCompositor: false,
|
|
warning: expectedWarning
|
|
} ]);
|
|
animation.effect.target.style = 'width: 100px; height: 100px';
|
|
return waitForFrame();
|
|
}).then(() => {
|
|
// FIXME: Bug 1253164: the animation should get back on compositor.
|
|
assert_animation_property_state_equals(
|
|
animation.effect.getProperties(),
|
|
[ { property: 'transform', runningOnCompositor: false } ]);
|
|
});
|
|
}, 'transform on too big element - dimensions');
|
|
|
|
promise_test(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(
|
|
{ transform: ['translate(0px)', 'translate(100px)'] }, 100 * MS_PER_SEC);
|
|
return waitForPaints().then(() => {
|
|
assert_animation_property_state_equals(
|
|
animation.effect.getProperties(),
|
|
[ { property: 'transform', runningOnCompositor: true } ]);
|
|
svg.setAttribute('transform', 'translate(10, 20)');
|
|
return waitForFrame();
|
|
}).then(() => {
|
|
assert_animation_property_state_equals(
|
|
animation.effect.getProperties(),
|
|
[ {
|
|
property: 'transform',
|
|
runningOnCompositor: false,
|
|
warning: 'CompositorAnimationWarningTransformSVG'
|
|
} ]);
|
|
svg.removeAttribute('transform');
|
|
return waitForFrame();
|
|
}).then(() => {
|
|
assert_animation_property_state_equals(
|
|
animation.effect.getProperties(),
|
|
[ { property: 'transform', runningOnCompositor: true } ]);
|
|
});
|
|
}, 'transform of nsIFrame with SVG transform');
|
|
|
|
promise_test(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);
|
|
return waitForPaints().then(() => {
|
|
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');
|
|
|
|
done();
|
|
}
|
|
|
|
start();
|
|
|
|
</script>
|
|
|
|
</body>
|