<!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); SpecialPowers.pushPrefEnv({ "set": [ ["general.useragent.locale", "en-US"], // 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_property_state_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'); } } var gAnimationsTests = [ { 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 } ] }, ]; // Test cases that check results of adding/removing a 'width' property on the // same animation object. var gAnimationWithGeometricKeyframeTests = [ { 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' } ] } }, ]; // Performance warning tests that set and clear a style property. var gPerformanceWarningTestsStyle = [ { 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' } ] }, ]; // Performance warning tests that set and clear the id property var gPerformanceWarningTestsId= [ { 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' } ] }, ]; var gMultipleAsyncAnimationsTests = [ { 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, } ] } ], }, ]; // Test cases that check results of 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]. var gMultipleAsyncAnimationsWithGeometricKeyframeTests = [ { 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' } ] } } ] }, ]; // Test cases that check results of adding/removing 'width' animation on the // same element which has async animations. var gMultipleAsyncAnimationsWithGeometricAnimationTests = [ { 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, } ] } ], }, ]; var gAnimationsOnTooSmallElementTests = [ { desc: 'opacity on too 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: false, warning: /Animation cannot be run on the compositor because frame size \(8, 8\) is smaller than \(16, 16\)/ } ] }, { desc: 'transform on too small element', frames: { transform: ['translate(0px)', 'translate(100px)'] }, style: { style: 'width: 8px; height: 8px; background-color: red;' }, expected: [ { property: 'transform', runningOnCompositor: false, warning: /Animation cannot be run on the compositor because frame size \(8, 8\) is smaller than \(16, 16\)/ } ] }, ]; function start() { var bundleService = SpecialPowers.Cc['@mozilla.org/intl/stringbundle;1'] .getService(SpecialPowers.Ci.nsIStringBundleService); gStringBundle = bundleService .createBundle("chrome://global/locale/layout_errors.properties"); gAnimationsTests.forEach(function(subtest) { promise_test(function(t) { var animation = addDivAndAnimate(t, { class: 'compositable' }, subtest.frames, 100 * MS_PER_SEC); return animation.ready.then(function() { assert_animation_property_state_equals( animation.effect.getProperties(), subtest.expected); }); }, subtest.desc); }); gAnimationWithGeometricKeyframeTests.forEach(function(subtest) { promise_test(function(t) { var animation = addDivAndAnimate(t, { class: 'compositable' }, subtest.frames, 100 * MS_PER_SEC); return animation.ready.then(function() { // First, a transform animation is running on compositor. assert_animation_property_state_equals( animation.effect.getProperties(), subtest.expected.withoutGeometric); }).then(function() { // Add a 'width' property. var keyframes = animation.effect.getKeyframes(); keyframes[0].width = '100px'; keyframes[1].width = '200px'; animation.effect.setKeyframes(keyframes); return waitForFrame(); }).then(function() { // 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(function() { // Remove the 'width' property. var keyframes = animation.effect.getKeyframes(); delete keyframes[0].width; delete keyframes[1].width; animation.effect.setKeyframes(keyframes); return waitForFrame(); }).then(function() { // Finally, the transform animation is running on compositor. assert_animation_property_state_equals( animation.effect.getProperties(), subtest.expected.withoutGeometric); }); }, 'An animation has: ' + subtest.desc); }); gPerformanceWarningTestsStyle.forEach(function(subtest) { promise_test(function(t) { var animation = addDivAndAnimate(t, { class: 'compositable' }, subtest.frames, 100 * MS_PER_SEC); return animation.ready.then(function() { assert_property_state_on_compositor( animation.effect.getProperties(), subtest.expected); animation.effect.target.style = subtest.style; return waitForFrame(); }).then(function() { assert_animation_property_state_equals( animation.effect.getProperties(), subtest.expected); animation.effect.target.style = ''; return waitForFrame(); }).then(function() { assert_property_state_on_compositor( animation.effect.getProperties(), subtest.expected); }); }, subtest.desc); }); gPerformanceWarningTestsId.forEach(function(subtest) { promise_test(function(t) { if (subtest.createelement) { addDiv(t, { style: subtest.createelement }); } var animation = addDivAndAnimate(t, { class: 'compositable' }, subtest.frames, 100 * MS_PER_SEC); return animation.ready.then(function() { assert_property_state_on_compositor( animation.effect.getProperties(), subtest.expected); animation.effect.target.id = subtest.id; return waitForFrame(); }).then(function() { assert_animation_property_state_equals( animation.effect.getProperties(), subtest.expected); animation.effect.target.id = ''; return waitForFrame(); }).then(function() { assert_property_state_on_compositor( animation.effect.getProperties(), subtest.expected); }); }, subtest.desc); }); gMultipleAsyncAnimationsTests.forEach(function(subtest) { promise_test(function(t) { var div = addDiv(t, { class: 'compositable' }); var animations = subtest.animations.map(function(anim) { var animation = div.animate(anim.frames, 100 * MS_PER_SEC); // Bind expected values to animation object. animation.expected = anim.expected; return animation; }); return waitForAllAnimations(animations).then(function() { animations.forEach(function(anim) { assert_property_state_on_compositor( anim.effect.getProperties(), anim.expected); }); div.style = subtest.style; return waitForFrame(); }).then(function() { animations.forEach(function(anim) { assert_animation_property_state_equals( anim.effect.getProperties(), anim.expected); }); div.style = ''; return waitForFrame(); }).then(function() { animations.forEach(function(anim) { assert_property_state_on_compositor( anim.effect.getProperties(), anim.expected); }); }); }, 'Multiple animations: ' + subtest.desc); }); gMultipleAsyncAnimationsWithGeometricKeyframeTests.forEach(function(subtest) { promise_test(function(t) { var div = addDiv(t, { class: 'compositable' }); var animations = subtest.animations.map(function(anim) { var animation = div.animate(anim.frames, 100 * MS_PER_SEC); // Bind expected values to animation object. animation.expected = anim.expected; return animation; }); return waitForAllAnimations(animations).then(function() { // First, all animations are running on compositor. animations.forEach(function(anim) { assert_animation_property_state_equals( anim.effect.getProperties(), anim.expected.withoutGeometric); }); }).then(function() { // 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(function() { // Now the transform animation is not running on compositor because of // the 'width' property. animations.forEach(function(anim) { assert_animation_property_state_equals( anim.effect.getProperties(), anim.expected.withGeometric); }); }).then(function() { // 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(function() { // Finally, all animations are running on compositor. animations.forEach(function(anim) { assert_animation_property_state_equals( anim.effect.getProperties(), anim.expected.withoutGeometric); }); }); }, 'Multiple animations with geometric property: ' + subtest.desc); }); gMultipleAsyncAnimationsWithGeometricAnimationTests.forEach(function(subtest) { promise_test(function(t) { var div = addDiv(t, { class: 'compositable' }); var animations = subtest.animations.map(function(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 waitForAllAnimations(animations).then(function() { animations.forEach(function(anim) { assert_property_state_on_compositor( anim.effect.getProperties(), anim.expected); }); }).then(function() { // Append 'width' animation on the same element. widthAnimation = div.animate({ width: ['100px', '200px'] }, 100 * MS_PER_SEC); return waitForFrame(); }).then(function() { // Now transform animations are not running on compositor because of // the 'width' animation. animations.forEach(function(anim) { assert_animation_property_state_equals( anim.effect.getProperties(), anim.expected); }); // Remove the 'width' animation. widthAnimation.cancel(); return waitForFrame(); }).then(function() { // Now all animations are running on compositor. animations.forEach(function(anim) { assert_property_state_on_compositor( anim.effect.getProperties(), anim.expected); }); }); }, 'Multiple async animations and geometric animation: ' + subtest.desc); }); gAnimationsOnTooSmallElementTests.forEach(function(subtest) { promise_test(function(t) { var div = addDiv(t, subtest.style); var animation = div.animate(subtest.frames, 100 * MS_PER_SEC); return animation.ready.then(function() { assert_animation_property_state_equals( animation.effect.getProperties(), subtest.expected); }); }, subtest.desc); }); promise_test(function(t) { var animation = addDivAndAnimate(t, { class: 'compositable' }, { transform: [ 'translate(0px)', 'translate(100px)'] }, 100 * MS_PER_SEC); return animation.ready.then(function() { assert_animation_property_state_equals( animation.effect.getProperties(), [ { property: 'transform', runningOnCompositor: true } ]); animation.effect.target.style = 'width: 10000px; height: 10000px'; return waitForFrame(); }).then(function() { // viewport depends on test environment. var expectedWarning = new RegExp( "Animation cannot be run on the compositor because the frame size " + "\\(10000, 10000\\) is bigger than the viewport \\(\\d+, \\d+\\) " + "or the visual rectangle \\(10000, 10000\\) is larger than the " + "maximum allowed value \\(\\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(function() { // 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'); promise_test(function(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(function() { svg.remove(); }); var animation = svg.animate( { transform: ['translate(0px)', 'translate(100px)'] }, 100 * MS_PER_SEC); return animation.ready.then(function() { assert_animation_property_state_equals( animation.effect.getProperties(), [ { property: 'transform', runningOnCompositor: true } ]); svg.setAttribute('transform', 'translate(10, 20)'); return waitForFrame(); }).then(function() { assert_animation_property_state_equals( animation.effect.getProperties(), [ { property: 'transform', runningOnCompositor: false, warning: 'CompositorAnimationWarningTransformSVG' } ]); svg.removeAttribute('transform'); return waitForFrame(); }).then(function() { assert_animation_property_state_equals( animation.effect.getProperties(), [ { property: 'transform', runningOnCompositor: true } ]); }); }, 'transform of nsIFrame with SVG transform'); promise_test(function(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 scriptAnimation.ready.then(function() { 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'); } </script> </body>