Bug 1301305 - Add tests for transform animations synchronized with geometric animations: r?hiro draft
authorBrian Birtles <birtles@gmail.com>
Mon, 05 Dec 2016 21:56:05 -1000
changeset 447584 ec757beec11a107272e69ab40323fac4e3980911
parent 447583 64322a0166d97b85ba3e4d7f752c8501774b77ef
child 539054 f46b0831e4d9a2aaae47c1b777e103736d5da01d
push id38084
push userbbirtles@mozilla.com
push dateTue, 06 Dec 2016 07:57:40 +0000
reviewershiro
bugs1301305
milestone53.0a1
Bug 1301305 - Add tests for transform animations synchronized with geometric animations: r?hiro MozReview-Commit-ID: Ay7xqfyW0N2
dom/animation/test/chrome/test_animation_performance_warning.html
dom/animation/test/testcommon.js
--- a/dom/animation/test/chrome/test_animation_performance_warning.html
+++ b/dom/animation/test/chrome/test_animation_performance_warning.html
@@ -880,31 +880,295 @@ function testSmallElements() {
         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() {
--- a/dom/animation/test/testcommon.js
+++ b/dom/animation/test/testcommon.js
@@ -158,16 +158,25 @@ function propertyToIDL(property) {
  */
 function waitForFrame() {
   return new Promise(function(resolve, reject) {
     window.requestAnimationFrame(resolve);
   });
 }
 
 /**
+ * Promise wrapper for requestIdleCallback.
+ */
+function waitForIdleCallback() {
+  return new Promise(function(resolve, reject) {
+    window.requestIdleCallback(resolve);
+  });
+}
+
+/**
  * Returns a Promise that is resolved after the given number of consecutive
  * animation frames have occured (using requestAnimationFrame callbacks).
  *
  * @param frameCount  The number of animation frames.
  * @param onFrame  An optional function to be processed in each animation frame.
  */
 function waitForAnimationFrames(frameCount, onFrame) {
   return new Promise(function(resolve, reject) {