summaryrefslogtreecommitdiffstats
path: root/dom/animation/test/chrome/test_animation_performance_warning.html
diff options
context:
space:
mode:
Diffstat (limited to 'dom/animation/test/chrome/test_animation_performance_warning.html')
-rw-r--r--dom/animation/test/chrome/test_animation_performance_warning.html1692
1 files changed, 1692 insertions, 0 deletions
diff --git a/dom/animation/test/chrome/test_animation_performance_warning.html b/dom/animation/test/chrome/test_animation_performance_warning.html
new file mode 100644
index 0000000000..ca0572cfaa
--- /dev/null
+++ b/dom/animation/test/chrome/test_animation_performance_warning.html
@@ -0,0 +1,1692 @@
+<!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>
+<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.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: false,
+ warning: 'CompositorAnimationWarningTransformWithGeometricProperties'
+ }
+ ]
+ }
+ },
+ {
+ desc: 'translate',
+ frames: {
+ translate: ['0px', '100px']
+ },
+ expected: {
+ withoutGeometric: [
+ {
+ property: 'translate',
+ runningOnCompositor: true
+ }
+ ],
+ withGeometric: [
+ {
+ property: 'width',
+ runningOnCompositor: false
+ },
+ {
+ property: 'translate',
+ runningOnCompositor: false,
+ warning: 'CompositorAnimationWarningTransformWithGeometricProperties'
+ }
+ ]
+ }
+ },
+ {
+ 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: false,
+ warning: 'CompositorAnimationWarningTransformWithGeometricProperties'
+ },
+ {
+ property: 'translate',
+ runningOnCompositor: false,
+ warning: 'CompositorAnimationWarningTransformWithGeometricProperties'
+ }
+ ]
+ }
+ },
+ ].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: false,
+ warning: 'CompositorAnimationWarningTransformWithSyncGeometricAnimations'
+ }
+ ]);
+ }, `${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: false,
+ warning: 'CompositorAnimationWarningTransformBackfaceVisibilityHidden'
+ }
+ ]
+ },
+ {
+ desc: 'translate with backface-visibility:hidden',
+ frames: {
+ translate: ['0px', '100px']
+ },
+ style: 'backface-visibility: hidden;',
+ expected: [
+ {
+ property: 'translate',
+ runningOnCompositor: false,
+ warning: 'CompositorAnimationWarningTransformBackfaceVisibilityHidden'
+ }
+ ]
+ },
+ {
+ 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: false,
+ warning: 'CompositorAnimationWarningTransformBackfaceVisibilityHidden'
+ },
+ {
+ property: 'translate',
+ runningOnCompositor: false,
+ warning: 'CompositorAnimationWarningTransformBackfaceVisibilityHidden'
+ }
+ ]
+ },
+ ].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: false,
+ warning: 'CompositorAnimationWarningTransformBackfaceVisibilityHidden'
+ }
+ ]
+ },
+ {
+ frames: {
+ translate: ['0px', '100px']
+ },
+ expected: [
+ {
+ property: 'translate',
+ runningOnCompositor: false,
+ warning: 'CompositorAnimationWarningTransformBackfaceVisibilityHidden'
+ }
+ ]
+ },
+ {
+ 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: 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'
+ }
+ ]
+ }
+ }
+ ]
+ },
+ {
+ 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: false,
+ warning: 'CompositorAnimationWarningTransformWithGeometricProperties'
+ }
+ ]
+ }
+ }
+ ]
+ },
+ ].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: false,
+ warning: 'CompositorAnimationWarningTransformWithGeometricProperties'
+ }
+ ]
+ },
+ ]
+ },
+ {
+ desc: 'translate',
+ animations: [
+ {
+ frames: {
+ translate: ['0px', '100px']
+ },
+ expected: [
+ {
+ property: 'translate',
+ runningOnCompositor: false,
+ warning: 'CompositorAnimationWarningTransformWithGeometricProperties'
+ }
+ ]
+ },
+ ]
+ },
+ {
+ 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: false,
+ warning: 'CompositorAnimationWarningTransformWithGeometricProperties'
+ }
+ ]
+ },
+ {
+ frames: {
+ translate: ['0px', '100px']
+ },
+ expected: [
+ {
+ property: 'translate',
+ runningOnCompositor: false,
+ warning: 'CompositorAnimationWarningTransformWithGeometricProperties'
+ }
+ ]
+ },
+ {
+ 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 waitForPaints();
+
+ 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(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 waitForPaints();
+
+ assert_animation_property_state_equals(
+ animA.effect.getProperties(),
+ [
+ { property: 'transform',
+ runningOnCompositor: false,
+ warning: 'CompositorAnimationWarningTransformWithSyncGeometricAnimations'
+ } ]);
+ assert_animation_property_state_equals(
+ animB.effect.getProperties(),
+ [
+ { property: 'translate',
+ runningOnCompositor: false,
+ warning: 'CompositorAnimationWarningTransformWithSyncGeometricAnimations'
+ } ]);
+ }, '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 waitForPaints();
+
+ assert_animation_property_state_equals(
+ animB.effect.getProperties(),
+ [ { property: 'transform',
+ runningOnCompositor: false,
+ warning: 'CompositorAnimationWarningTransformWithSyncGeometricAnimations'
+ } ]);
+ assert_animation_property_state_equals(
+ animC.effect.getProperties(),
+ [
+ { property: 'translate',
+ runningOnCompositor: false,
+ warning: 'CompositorAnimationWarningTransformWithSyncGeometricAnimations'
+ } ]);
+ }, '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: false,
+ warning: 'CompositorAnimationWarningTransformWithSyncGeometricAnimations'
+ } ]);
+ }, '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 waitForPaints();
+
+ 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(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]);
+
+ 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 waitForPaints();
+
+ let animB = elemB.animate({ transform: [ 'translate(0px)',
+ 'translate(100px)' ] },
+ 100 * MS_PER_SEC);
+ await animB.ready;
+
+ 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 waitForPaints();
+
+ let animB = elemB.animate({ marginLeft: [ '0px', '100px' ] },
+ 100 * MS_PER_SEC);
+ await animB.ready;
+
+ 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 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 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: false,
+ warning: 'CompositorAnimationWarningTransformWithSyncGeometricAnimations'
+ } ]);
+ }, '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 waitForPaints();
+
+ assert_animation_property_state_equals(
+ animA.effect.getProperties(),
+ [ { property: 'transform',
+ runningOnCompositor: false } ]);
+ // Restart animation
+ animA.pause();
+ animA.play();
+ await animA.ready;
+
+ 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 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
+ .createBundle("chrome://global/locale/layout_errors.properties");
+
+ 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');
+
+ done();
+}
+
+</script>
+
+</body>