gecko-dev/dom/animation/test/chrome/test_animation_performance_...

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>