Bug 1329077 - Revert the hack that opens a new window to use requestIdleCallback. r?boris draft
authorHiroyuki Ikezoe <hikezoe@mozilla.com>
Sat, 20 May 2017 20:33:26 +0900
changeset 581966 46c0a643304095a32fca82f816bcff768aa870c3
parent 579879 2c783a7b6d05b4b2b417bc5f21b7e40cbf3df077
child 581967 039d6d3e4c1bf916e9176dc4c61491436e7b6d04
push id59931
push userhikezoe@mozilla.com
push dateSat, 20 May 2017 11:34:33 +0000
reviewersboris
bugs1329077, 1314959
milestone55.0a1
Bug 1329077 - Revert the hack that opens a new window to use requestIdleCallback. r?boris requestIdleCallback has been enable by default in bug 1314959. MozReview-Commit-ID: 7rwqdsCdjNb
dom/animation/test/chrome.ini
dom/animation/test/chrome/file_animation_performance_warning.html
dom/animation/test/chrome/test_animation_performance_warning.html
dom/animation/test/testcommon.js
--- a/dom/animation/test/chrome.ini
+++ b/dom/animation/test/chrome.ini
@@ -1,15 +1,14 @@
 [DEFAULT]
 support-files =
   testcommon.js
   ../../imptests/testharness.js
   ../../imptests/testharnessreport.js
   !/dom/animation/test/chrome/file_animate_xrays.html
-  chrome/file_animation_performance_warning.html
 
 [chrome/test_animate_xrays.html]
 # file_animate_xrays.html needs to go in mochitest.ini since it is served
 # over HTTP
 [chrome/test_animation_observers_async.html]
 [chrome/test_animation_observers_sync.html]
 [chrome/test_animation_performance_warning.html]
 [chrome/test_animation_properties.html]
deleted file mode 100644
--- a/dom/animation/test/chrome/file_animation_performance_warning.html
+++ /dev/null
@@ -1,1265 +0,0 @@
-<!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="../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);
-
-/**
- * Promise wrapper for requestIdleCallback.
- * NOTE: DON'T move this function into testcommon.js until requestIdleCallback
- * is enabled by default on all channels.
- */
-function waitForIdleCallback() {
-  return new Promise(function(resolve, reject) {
-    window.requestIdleCallback(resolve);
-  });
-}
-
-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(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);
-  });
-}
-
-// 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(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);
-  });
-}
-
-// 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(function(t) {
-      const keyframes = {
-        [propertyToIDL(property)]: [ '100px', '200px' ],
-        transform: [ 'translate(0px)', 'translate(100px)' ]
-      };
-      var animation = addDivAndAnimate(t, { class: 'compositable' },
-                                       keyframes, 100 * MS_PER_SEC);
-
-      return animation.ready.then(function() {
-        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(function(t) {
-      var animation = addDivAndAnimate(t, { class: 'compositable' },
-                                       subtest.frames, 100 * MS_PER_SEC);
-      return animation.ready.then(function() {
-        assert_all_properties_running_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_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(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_all_properties_running_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_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(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(anim => {
-          assert_all_properties_running_on_compositor(
-            anim.effect.getProperties(),
-            anim.expected);
-        });
-        div.style = subtest.style;
-        return waitForFrame();
-      }).then(function() {
-        animations.forEach(anim => {
-          assert_animation_property_state_equals(
-            anim.effect.getProperties(),
-            anim.expected);
-        });
-        div.style = '';
-        return waitForFrame();
-      }).then(function() {
-        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(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(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(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(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(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(anim => {
-          assert_all_properties_running_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(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(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(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);
-  });
-}
-
-function testSynchronizedAnimations() {
-  promise_test(function(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 Promise.all([animA.ready, animB.ready])
-      .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(function(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 Promise.all([animA.ready, animB.ready])
-      .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(function(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 waitForIdleCallback()
-      .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(function(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 Promise.all([animA.ready, animB.ready])
-      .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(function(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 waitForFrame()
-      .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(function(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 waitForFrame()
-      .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(function(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 Promise.all([animA.ready, animB.ready])
-      .then(() => {
-        assert_animation_property_state_equals(
-          animA.effect.getProperties(),
-          [ { property: 'transform', runningOnCompositor: true } ]);
-      });
-  }, 'Paused animations are not synchronized');
-
-  promise_test(function(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 Promise.all([animA.ready, animB.ready])
-      .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(function(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 Promise.all([animA.ready, animB.ready])
-      .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(function(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 Promise.all([animA.ready, animB.ready])
-      .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(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 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(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');
-
-  done();
-}
-
-start();
-
-</script>
-
-</body>
--- a/dom/animation/test/chrome/test_animation_performance_warning.html
+++ b/dom/animation/test/chrome/test_animation_performance_warning.html
@@ -1,21 +1,1276 @@
 <!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';
-setup({explicit_done: true});
-SpecialPowers.pushPrefEnv(
-  { "set": [["dom.requestIdleCallback.enabled", true],
-            ["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]]},
-  function() {
-    // Open a new window after enabling requestIdleCallback so that we can
-    // use the function in the new window.
-    window.open("file_animation_performance_warning.html");
+
+// 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);
+
+/**
+ * Promise wrapper for requestIdleCallback.
+ * NOTE: DON'T move this function into testcommon.js until requestIdleCallback
+ * is enabled by default on all channels.
+ */
+function waitForIdleCallback() {
+  return new Promise(function(resolve, reject) {
+    window.requestIdleCallback(resolve);
+  });
+}
+
+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(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);
+  });
+}
+
+// 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(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);
+  });
+}
+
+// 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(function(t) {
+      const keyframes = {
+        [propertyToIDL(property)]: [ '100px', '200px' ],
+        transform: [ 'translate(0px)', 'translate(100px)' ]
+      };
+      var animation = addDivAndAnimate(t, { class: 'compositable' },
+                                       keyframes, 100 * MS_PER_SEC);
+
+      return animation.ready.then(function() {
+        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(function(t) {
+      var animation = addDivAndAnimate(t, { class: 'compositable' },
+                                       subtest.frames, 100 * MS_PER_SEC);
+      return animation.ready.then(function() {
+        assert_all_properties_running_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_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(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_all_properties_running_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_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(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(anim => {
+          assert_all_properties_running_on_compositor(
+            anim.effect.getProperties(),
+            anim.expected);
+        });
+        div.style = subtest.style;
+        return waitForFrame();
+      }).then(function() {
+        animations.forEach(anim => {
+          assert_animation_property_state_equals(
+            anim.effect.getProperties(),
+            anim.expected);
+        });
+        div.style = '';
+        return waitForFrame();
+      }).then(function() {
+        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(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(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(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(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(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(anim => {
+          assert_all_properties_running_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(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(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(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);
+  });
+}
+
+function testSynchronizedAnimations() {
+  promise_test(function(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 Promise.all([animA.ready, animB.ready])
+      .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(function(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 Promise.all([animA.ready, animB.ready])
+      .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(function(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 waitForIdleCallback()
+      .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(function(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 Promise.all([animA.ready, animB.ready])
+      .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(function(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 waitForFrame()
+      .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(function(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 waitForFrame()
+      .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(function(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 Promise.all([animA.ready, animB.ready])
+      .then(() => {
+        assert_animation_property_state_equals(
+          animA.effect.getProperties(),
+          [ { property: 'transform', runningOnCompositor: true } ]);
+      });
+  }, 'Paused animations are not synchronized');
+
+  promise_test(function(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 Promise.all([animA.ready, animB.ready])
+      .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(function(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 Promise.all([animA.ready, animB.ready])
+      .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(function(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 Promise.all([animA.ready, animB.ready])
+      .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(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 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(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');
+
+  done();
+}
+
+start();
+
 </script>
+
+</body>
--- a/dom/animation/test/testcommon.js
+++ b/dom/animation/test/testcommon.js
@@ -264,29 +264,25 @@ function flushComputedStyle(elem) {
   var cs = getComputedStyle(elem);
   cs.marginLeft;
 }
 
 if (opener) {
   for (var funcName of ["async_test", "assert_not_equals", "assert_equals",
                         "assert_approx_equals", "assert_less_than",
                         "assert_less_than_equal", "assert_greater_than",
-                        "assert_greater_than_equal",
-                        "assert_not_exists",
                         "assert_between_inclusive",
                         "assert_true", "assert_false",
                         "assert_class_string", "assert_throws",
                         "assert_unreached", "assert_regexp_match",
                         "promise_test", "test"]) {
     window[funcName] = opener[funcName].bind(opener);
   }
 
   window.EventWatcher = opener.EventWatcher;
-  // Used for requestLongerTimeout.
-  window.W3CTest = opener.W3CTest;
 
   function done() {
     opener.add_completion_callback(function() {
       self.close();
     });
     opener.done();
   }
 }