Bug 1305325 - Part 15: Tests composed style for missing keyframe for properties runnning on the compositor. r=birtles
authorHiroyuki Ikezoe <hiikezoe@mozilla-japan.org>
Sun, 04 Dec 2016 08:07:40 +0900
changeset 325237 944f98dcb83b740910d547b97181258ed0890afe
parent 325236 fdaaa1b24b8c88e0b56eb052bb8dbdf858381537
child 325238 be8333efe3623aec192916f058e80bed3872feac
push id24
push usermaklebus@msu.edu
push dateTue, 20 Dec 2016 03:11:33 +0000
reviewersbirtles
bugs1305325
milestone53.0a1
Bug 1305325 - Part 15: Tests composed style for missing keyframe for properties runnning on the compositor. r=birtles The error value, 0.01, used in this test is the same as we used in test_animation_omta.html. MozReview-Commit-ID: 50g3k43yAgu
dom/animation/test/mochitest.ini
dom/animation/test/style/file_missing-keyframe-on-compositor.html
dom/animation/test/style/test_missing-keyframe-on-compositor.html
dom/animation/test/testcommon.js
--- a/dom/animation/test/mochitest.ini
+++ b/dom/animation/test/mochitest.ini
@@ -49,16 +49,17 @@ support-files =
   mozilla/file_transition_finish_on_compositor.html
   mozilla/file_underlying-discrete-value.html
   mozilla/file_set-easing.html
   style/file_animation-seeking-with-current-time.html
   style/file_animation-seeking-with-start-time.html
   style/file_animation-setting-effect.html
   style/file_animation-setting-spacing.html
   style/file_missing-keyframe.html
+  style/file_missing-keyframe-on-compositor.html
   testcommon.js
 
 [css-animations/test_animations-dynamic-changes.html]
 [css-animations/test_animation-cancel.html]
 [css-animations/test_animation-computed-timing.html]
 [css-animations/test_animation-currenttime.html]
 [css-animations/test_animation-finish.html]
 [css-animations/test_animation-finished.html]
@@ -104,8 +105,9 @@ support-files =
 [mozilla/test_transition_finish_on_compositor.html]
 skip-if = toolkit == 'android'
 [mozilla/test_underlying-discrete-value.html]
 [style/test_animation-seeking-with-current-time.html]
 [style/test_animation-seeking-with-start-time.html]
 [style/test_animation-setting-effect.html]
 [style/test_animation-setting-spacing.html]
 [style/test_missing-keyframe.html]
+[style/test_missing-keyframe-on-compositor.html]
new file mode 100644
--- /dev/null
+++ b/dom/animation/test/style/file_missing-keyframe-on-compositor.html
@@ -0,0 +1,476 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="../testcommon.js"></script>
+<script src="/tests/SimpleTest/paint_listener.js"></script>
+<style>
+div {
+  /* Element needs geometry to be eligible for layerization */
+  width: 100px;
+  height: 100px;
+  background-color: white;
+}
+</style>
+<body>
+<script>
+'use strict';
+
+if (!SpecialPowers.DOMWindowUtils.layerManagerRemote ||
+    !SpecialPowers.getBoolPref(
+      'layers.offmainthreadcomposition.async-animations')) {
+  // If OMTA is disabled, nothing to run.
+  done();
+}
+
+function waitForPaintsFlushed() {
+  return new Promise(function(resolve, reject) {
+    waitForAllPaintsFlushed(resolve);
+  });
+}
+
+// Note that promise tests run in sequence so this ensures the document is
+// loaded before any of the other tests run.
+promise_test(t => {
+  // Without this, the first test case fails on Android.
+  return waitForDocumentLoad();
+}, 'Ensure document has been loaded');
+
+promise_test(t => {
+  useTestRefreshMode(t);
+
+  var div = addDiv(t, { style: 'opacity: 0.1' });
+  div.animate({ opacity: 1 }, 100 * MS_PER_SEC);
+
+  return waitForPaintsFlushed().then(() => {
+    var opacity =
+      SpecialPowers.DOMWindowUtils.getOMTAStyle(div, 'opacity');
+    assert_equals(opacity, '0.1',
+                  'The initial opacity value should be the base value');
+  });
+}, 'Initial opacity value for animation with no no keyframe at offset 0');
+
+promise_test(t => {
+  useTestRefreshMode(t);
+
+  var div = addDiv(t, { style: 'opacity: 0.1' });
+  div.animate({ opacity: [ 0.5, 1 ] }, 100 * MS_PER_SEC);
+  div.animate({ opacity: 1 }, 100 * MS_PER_SEC);
+
+  return waitForPaintsFlushed().then(() => {
+    var opacity =
+      SpecialPowers.DOMWindowUtils.getOMTAStyle(div, 'opacity');
+    assert_equals(opacity, '0.5',
+                  'The initial opacity value should be the value of ' +
+                  'lower-priority animation value');
+  });
+}, 'Initial opacity value for animation with no keyframe at offset 0 when ' +
+   'there is a lower-priority animation');
+
+promise_test(t => {
+  useTestRefreshMode(t);
+
+  var div =
+    addDiv(t, { style: 'opacity: 0.1; transition: opacity 100s linear' });
+  getComputedStyle(div).opacity;
+
+  div.style.opacity = '0.5';
+  div.animate({ opacity: 1 }, 100 * MS_PER_SEC);
+
+  return waitForPaintsFlushed().then(() => {
+    var opacity =
+      SpecialPowers.DOMWindowUtils.getOMTAStyle(div, 'opacity');
+    assert_equals(opacity, '0.1',
+                  'The initial opacity value should be the initial value of ' +
+                  'the transition');
+  });
+}, 'Initial opacity value for animation with no keyframe at offset 0 when ' +
+   'there is a transition on the same property');
+
+promise_test(t => {
+  useTestRefreshMode(t);
+
+  var div = addDiv(t, { style: 'opacity: 0' });
+  div.animate([{ offset: 0, opacity: 1 }], 100 * MS_PER_SEC);
+
+  return waitForPaintsFlushed().then(() => {
+    SpecialPowers.DOMWindowUtils.advanceTimeAndRefresh(50 * MS_PER_SEC);
+
+    var opacity =
+      SpecialPowers.DOMWindowUtils.getOMTAStyle(div, 'opacity');
+    assert_equals(opacity, '0.5',
+                  'Opacity value at 50% should be composed onto the base ' +
+                  'value');
+  });
+}, 'Opacity value for animation with no keyframe at offset 1 at 50% ');
+
+promise_test(t => {
+  useTestRefreshMode(t);
+
+  var div = addDiv(t, { style: 'opacity: 0' });
+  div.animate({ opacity: [ 0.5, 0.5 ] }, 100 * MS_PER_SEC);
+  div.animate([{ offset: 0, opacity: 1 }], 100 * MS_PER_SEC);
+
+  return waitForPaintsFlushed().then(() => {
+    SpecialPowers.DOMWindowUtils.advanceTimeAndRefresh(50 * MS_PER_SEC);
+
+    var opacity =
+      SpecialPowers.DOMWindowUtils.getOMTAStyle(div, 'opacity');
+    assert_equals(opacity, '0.75', // (0.5 + 1) * 0.5
+                  'Opacity value at 50% should be composed onto the value ' +
+                  'of middle of lower-priority animation');
+  });
+}, 'Opacity value for animation with no keyframe at offset 1 at 50% when ' +
+   'there is a lower-priority animation');
+
+promise_test(t => {
+  useTestRefreshMode(t);
+
+  var div =
+    addDiv(t, { style: 'opacity: 0; transition: opacity 100s linear' });
+  getComputedStyle(div).opacity;
+
+  div.style.opacity = '0.5';
+  div.animate([{ offset: 0, opacity: 1 }], 100 * MS_PER_SEC);
+
+  return waitForPaintsFlushed().then(() => {
+    SpecialPowers.DOMWindowUtils.advanceTimeAndRefresh(50 * MS_PER_SEC);
+
+    var opacity =
+      SpecialPowers.DOMWindowUtils.getOMTAStyle(div, 'opacity');
+    assert_equals(opacity, '0.625', // ((0 + 0.5) * 0.5 + 1) * 0.5
+                  'Opacity value at 50% should be composed onto the value ' +
+                  'of middle of transition');
+  });
+}, 'Opacity value for animation with no keyframe at offset 1 at 50% when ' +
+   'there is a transition on the same property');
+
+promise_test(t => {
+  useTestRefreshMode(t);
+
+  var div = addDiv(t);
+  var lowerAnimation = div.animate({ opacity: [ 0.5, 1 ] }, 100 * MS_PER_SEC);
+  var higherAnimation = div.animate({ opacity: 1 }, 100 * MS_PER_SEC);
+
+  return waitForPaintsFlushed().then(() => {
+    lowerAnimation.pause();
+    return waitForPaintsFlushed();
+  }).then(() => {
+    SpecialPowers.DOMWindowUtils.advanceTimeAndRefresh(50 * MS_PER_SEC);
+
+    var opacity =
+      SpecialPowers.DOMWindowUtils.getOMTAStyle(div, 'opacity');
+    // The underlying value is the value that is staying at 0ms of the
+    // lowerAnimation, that is 0.5.
+    // (0.5 + 1.0) * (50 * MS_PER_SEC / 100 * MS_PER_SEC) = 0.75.
+    assert_equals(opacity, '0.75',
+                  'Composed opacity value should be composed onto the value ' +
+                  'of lower-priority paused animation');
+  });
+}, 'Opacity value for animation with no keyframe at offset 0 at 50% when ' +
+   'composed onto a paused underlying animation');
+
+promise_test(t => {
+  useTestRefreshMode(t);
+
+  var div = addDiv(t);
+  var lowerAnimation = div.animate({ opacity: [ 0.5, 1 ] }, 100 * MS_PER_SEC);
+  var higherAnimation = div.animate({ opacity: 1 }, 100 * MS_PER_SEC);
+
+  return waitForPaintsFlushed().then(() => {
+    lowerAnimation.playbackRate = 0;
+    return waitForPaintsFlushed();
+  }).then(() => {
+    SpecialPowers.DOMWindowUtils.advanceTimeAndRefresh(50 * MS_PER_SEC);
+
+    var opacity =
+      SpecialPowers.DOMWindowUtils.getOMTAStyle(div, 'opacity');
+    // The underlying value is the value that is staying at 0ms of the
+    // lowerAnimation, that is 0.5.
+    // (0.5 + 1.0) * (50 * MS_PER_SEC / 100 * MS_PER_SEC) = 0.75.
+    assert_equals(opacity, '0.75',
+                  'Composed opacity value should be composed onto the value ' +
+                  'of lower-priority zero playback rate animation');
+  });
+}, 'Opacity value for animation with no keyframe at offset 0 at 50% when ' +
+   'composed onto a zero playback rate underlying animation');
+
+promise_test(t => {
+  useTestRefreshMode(t);
+
+  var div = addDiv(t);
+  var lowerAnimation = div.animate({ opacity: [ 1, 0.5 ] }, 100 * MS_PER_SEC);
+  var higherAnimation = div.animate({ opacity: 1 }, 100 * MS_PER_SEC);
+
+  return waitForPaintsFlushed().then(() => {
+    lowerAnimation.effect.timing.duration = 0;
+    lowerAnimation.effect.timing.fill = 'forwards';
+    return waitForPaintsFlushed();
+  }).then(() => {
+    SpecialPowers.DOMWindowUtils.advanceTimeAndRefresh(50 * MS_PER_SEC);
+
+    var opacity =
+      SpecialPowers.DOMWindowUtils.getOMTAStyle(div, 'opacity');
+    // The underlying value is the value that is filling forwards state of the
+    // lowerAnimation, that is 0.5.
+    // (0.5 + 1.0) * (50 * MS_PER_SEC / 100 * MS_PER_SEC) = 0.75.
+    assert_equals(opacity, '0.75',
+                  'Composed opacity value should be composed onto the value ' +
+                  'of lower-priority zero active duration animation');
+  });
+}, 'Opacity value for animation with no keyframe at offset 0 at 50% when ' +
+   'composed onto a zero active duration underlying animation');
+
+promise_test(t => {
+  useTestRefreshMode(t);
+
+  var div = addDiv(t, { style: 'transform: translateX(100px)' });
+  div.animate({ transform: 'translateX(200px)' }, 100 * MS_PER_SEC);
+
+  return waitForPaintsFlushed().then(() => {
+    var transform =
+      SpecialPowers.DOMWindowUtils.getOMTAStyle(div, 'transform');
+    assert_matrix_equals(transform, 'matrix(1, 0, 0, 1, 100, 0)',
+      'The initial transform value should be the base value');
+  });
+}, 'Initial transform value for animation with no keyframe at offset 0');
+
+promise_test(t => {
+  useTestRefreshMode(t);
+
+  var div = addDiv(t, { style: 'transform: translateX(100px)' });
+  div.animate({ transform: [ 'translateX(200px)', 'translateX(300px)' ] },
+              100 * MS_PER_SEC);
+  div.animate({ transform: 'translateX(400px)' }, 100 * MS_PER_SEC);
+
+  return waitForPaintsFlushed().then(() => {
+    var transform =
+      SpecialPowers.DOMWindowUtils.getOMTAStyle(div, 'transform');
+    assert_matrix_equals(transform, 'matrix(1, 0, 0, 1, 200, 0)',
+      'The initial transform value should be lower-priority animation value');
+  });
+}, 'Initial transform value for animation with no keyframe at offset 0 when ' +
+   'there is a lower-priority animation');
+
+promise_test(t => {
+  useTestRefreshMode(t);
+
+  var div =
+    addDiv(t, { style: 'transform: translateX(100px);' +
+                       'transition: transform 100s linear' });
+  getComputedStyle(div).transform;
+
+  div.style.transform = 'translateX(200px)';
+  div.animate({ transform: 'translateX(400px)' }, 100 * MS_PER_SEC);
+
+  return waitForPaintsFlushed().then(() => {
+    var transform =
+      SpecialPowers.DOMWindowUtils.getOMTAStyle(div, 'transform');
+    assert_matrix_equals(transform, 'matrix(1, 0, 0, 1, 100, 0)',
+      'The initial transform value should be the initial value of the ' +
+      'transition');
+  });
+}, 'Initial transform value for animation with no keyframe at offset 0 when ' +
+   'there is a transition');
+
+promise_test(t => {
+  useTestRefreshMode(t);
+
+  var div = addDiv(t, { style: 'transform: translateX(100px)' });
+  div.animate([{ offset: 0, transform: 'translateX(200pX)' }],
+              100 * MS_PER_SEC);
+
+  return waitForPaintsFlushed().then(() => {
+    SpecialPowers.DOMWindowUtils.advanceTimeAndRefresh(50 * MS_PER_SEC);
+
+    var transform =
+      SpecialPowers.DOMWindowUtils.getOMTAStyle(div, 'transform');
+    assert_matrix_equals(transform, 'matrix(1, 0, 0, 1, 150, 0)',
+      'Transform value at 50% should be the base value');
+  });
+}, 'Transform value for animation with no keyframe at offset 1 at 50%');
+
+promise_test(t => {
+  useTestRefreshMode(t);
+
+  var div = addDiv(t, { style: 'transform: translateX(100px)' });
+  div.animate({ transform: [ 'translateX(200px)', 'translateX(200px)' ] },
+              100 * MS_PER_SEC);
+  div.animate([{ offset: 0, transform: 'translateX(300px)' }],
+              100 * MS_PER_SEC);
+
+  return waitForPaintsFlushed().then(() => {
+    SpecialPowers.DOMWindowUtils.advanceTimeAndRefresh(50 * MS_PER_SEC);
+
+    var transform =
+      SpecialPowers.DOMWindowUtils.getOMTAStyle(div, 'transform');
+    assert_matrix_equals(transform, 'matrix(1, 0, 0, 1, 250, 0)',
+      'The final transform value should be the base value');
+  });
+}, 'Transform value for animation with no keyframe at offset 1 at 50% when ' +
+   'there is a lower-priority animation');
+
+promise_test(t => {
+  useTestRefreshMode(t);
+
+  var div =
+    addDiv(t, { style: 'transform: translateX(100px);' +
+                       'transition: transform 100s linear' });
+  getComputedStyle(div).transform;
+
+  div.style.transform = 'translateX(200px)';
+  div.animate([{ offset: 0, transform: 'translateX(300px)' }],
+              100 * MS_PER_SEC);
+
+  return waitForPaintsFlushed().then(() => {
+    SpecialPowers.DOMWindowUtils.advanceTimeAndRefresh(50 * MS_PER_SEC);
+
+    var transform =
+      SpecialPowers.DOMWindowUtils.getOMTAStyle(div, 'transform');
+                                                   // (150px + 300px) * 0.5
+    assert_matrix_equals(transform, 'matrix(1, 0, 0, 1, 225, 0)',
+      'The final transform value should be the final value of the transition');
+  });
+}, 'Transform value for animation with no keyframe at offset 1 at 50% when ' +
+   'there is a transition');
+
+promise_test(t => {
+  useTestRefreshMode(t);
+
+  var div = addDiv(t);
+  var lowerAnimation =
+    div.animate({ transform: [ 'translateX(100px)', 'translateX(200px)' ] },
+                100 * MS_PER_SEC);
+  var higherAnimation = div.animate({ transform: 'translateX(300px)' },
+                                    100 * MS_PER_SEC);
+
+  return waitForPaintsFlushed().then(() => {
+    lowerAnimation.pause();
+    return waitForPaintsFlushed();
+  }).then(() => {
+    SpecialPowers.DOMWindowUtils.advanceTimeAndRefresh(50 * MS_PER_SEC);
+
+    var transform =
+      SpecialPowers.DOMWindowUtils.getOMTAStyle(div, 'transform');
+    // The underlying value is the value that is staying at 0ms of the
+    // lowerAnimation, that is 100px.
+    // (100px + 300px) * (50 * MS_PER_SEC / 100 * MS_PER_SEC) = 200px.
+    assert_matrix_equals(transform, 'matrix(1, 0, 0, 1, 200, 0)',
+      'Composed transform value should be composed onto the value of ' +
+      'lower-priority paused animation');
+  });
+}, 'Transform value for animation with no keyframe at offset 0 at 50% when ' +
+   'composed onto a paused underlying animation');
+
+promise_test(t => {
+  useTestRefreshMode(t);
+
+  var div = addDiv(t);
+  var lowerAnimation =
+    div.animate({ transform: [ 'translateX(100px)', 'translateX(200px)' ] },
+                100 * MS_PER_SEC);
+  var higherAnimation = div.animate({ transform: 'translateX(300px)' },
+                                    100 * MS_PER_SEC);
+
+  return waitForPaintsFlushed().then(() => {
+    lowerAnimation.playbackRate = 0;
+    return waitForPaintsFlushed();
+  }).then(() => {
+    SpecialPowers.DOMWindowUtils.advanceTimeAndRefresh(50 * MS_PER_SEC);
+
+    var transform =
+      SpecialPowers.DOMWindowUtils.getOMTAStyle(div, 'transform');
+    // The underlying value is the value that is staying at 0ms of the
+    // lowerAnimation, that is 100px.
+    // (100px + 300px) * (50 * MS_PER_SEC / 100 * MS_PER_SEC) = 200px.
+    assert_matrix_equals(transform, 'matrix(1, 0, 0, 1, 200, 0)',
+      'Composed transform value should be composed onto the value of ' +
+      'lower-priority zero playback rate animation');
+  });
+}, 'Transform value for animation with no keyframe at offset 0 at 50% when ' +
+   'composed onto a zero playback rate underlying animation');
+
+promise_test(t => {
+  useTestRefreshMode(t);
+
+  var div = addDiv(t);
+  var lowerAnimation =
+    div.animate({ transform: [ 'translateX(100px)', 'translateX(200px)' ] },
+                { duration: 10 * MS_PER_SEC,
+                  fill: 'forwards' });
+  var higherAnimation = div.animate({ transform: 'translateX(300px)' },
+                                    100 * MS_PER_SEC);
+
+  return waitForPaintsFlushed().then(() => {
+    SpecialPowers.DOMWindowUtils.advanceTimeAndRefresh(50 * MS_PER_SEC);
+
+    // We need to wait for a paint so that we can send the state of the lower
+    // animation that is actually finished at this point.
+    return waitForPaintsFlushed();
+  }).then(() => {
+    var transform =
+      SpecialPowers.DOMWindowUtils.getOMTAStyle(div, 'transform');
+    // (200px + 300px) * (50 * MS_PER_SEC / 100 * MS_PER_SEC) = 250px.
+    assert_matrix_equals(transform, 'matrix(1, 0, 0, 1, 250, 0)',
+      'Composed transform value should be composed onto the value of ' +
+      'lower-priority animation with fill:forwards');
+  });
+}, 'Transform value for animation with no keyframe at offset 0 at 50% when ' +
+   'composed onto a underlying animation with fill:forwards');
+
+promise_test(t => {
+  useTestRefreshMode(t);
+
+  var div = addDiv(t);
+  var lowerAnimation =
+    div.animate({ transform: [ 'translateX(100px)', 'translateX(200px)' ] },
+                { duration: 10 * MS_PER_SEC,
+                  endDelay: -5 * MS_PER_SEC,
+                  fill: 'forwards' });
+  var higherAnimation = div.animate({ transform: 'translateX(300px)' },
+                                    100 * MS_PER_SEC);
+
+  return waitForPaintsFlushed().then(() => {
+    SpecialPowers.DOMWindowUtils.advanceTimeAndRefresh(50 * MS_PER_SEC);
+
+    // We need to wait for a paint just like the above test.
+    return waitForPaintsFlushed();
+  }).then(() => {
+    var transform =
+      SpecialPowers.DOMWindowUtils.getOMTAStyle(div, 'transform');
+    // (150px + 300px) * (50 * MS_PER_SEC / 100 * MS_PER_SEC) = 225px.
+    assert_matrix_equals(transform, 'matrix(1, 0, 0, 1, 225, 0)',
+      'Composed transform value should be composed onto the value of ' +
+      'lower-priority animation with fill:forwards and negative endDelay');
+  });
+}, 'Transform value for animation with no keyframe at offset 0 at 50% when ' +
+   'composed onto a underlying animation with fill:forwards and negative ' +
+   'endDelay');
+
+promise_test(t => {
+  useTestRefreshMode(t);
+
+  var div = addDiv(t);
+  var lowerAnimation =
+    div.animate({ transform: [ 'translateX(100px)', 'translateX(200px)' ] },
+                { duration: 10 * MS_PER_SEC,
+                  endDelay: 100 * MS_PER_SEC,
+                  fill: 'forwards' });
+  var higherAnimation = div.animate({ transform: 'translateX(300px)' },
+                                    100 * MS_PER_SEC);
+
+  return waitForPaintsFlushed().then(() => {
+    SpecialPowers.DOMWindowUtils.advanceTimeAndRefresh(50 * MS_PER_SEC);
+
+    var transform =
+      SpecialPowers.DOMWindowUtils.getOMTAStyle(div, 'transform');
+    // (200px + 300px) * 0.5
+    assert_matrix_equals(transform, 'matrix(1, 0, 0, 1, 250, 0)',
+      'Composed transform value should be composed onto the value of ' +
+      'lower-priority animation with fill:forwards during positive endDelay');
+  });
+}, 'Transform value for animation with no keyframe at offset 0 at 50% when ' +
+   'composed onto a underlying animation with fill:forwards during positive ' +
+   'endDelay');
+
+done();
+</script>
+</body>
new file mode 100644
--- /dev/null
+++ b/dom/animation/test/style/test_missing-keyframe-on-compositor.html
@@ -0,0 +1,15 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src='/resources/testharness.js'></script>
+<script src='/resources/testharnessreport.js'></script>
+<div id='log'></div>
+<script>
+'use strict';
+setup({explicit_done: true});
+SpecialPowers.pushPrefEnv(
+  { 'set': [['dom.animations-api.core.enabled', true]] },
+  function() {
+    window.open('file_missing-keyframe-on-compositor.html');
+  });
+</script>
+</html>
--- a/dom/animation/test/testcommon.js
+++ b/dom/animation/test/testcommon.js
@@ -24,16 +24,41 @@ var TIME_PRECISION = 0.0005; // ms
 /*
  * Allow implementations to substitute an alternative method for comparing
  * times based on their precision requirements.
  */
 function assert_times_equal(actual, expected, description) {
   assert_approx_equals(actual, expected, TIME_PRECISION, description);
 }
 
+/*
+ * Compare matrix string like 'matrix(1, 0, 0, 1, 100, 0)'.
+ * This function allows error, 0.01, because on Android when we are scaling down
+ * the document, it results in some errors.
+ */
+function assert_matrix_equals(actual, expected, description) {
+  var matrixRegExp = /^matrix\((.+),(.+),(.+),(.+),(.+),(.+)\)/;
+  assert_regexp_match(actual, matrixRegExp,
+    'Actual value should be a matrix')
+  assert_regexp_match(expected, matrixRegExp,
+    'Expected value should be a matrix');
+
+  var actualMatrixArray = actual.match(matrixRegExp).slice(1).map(Number);
+  var expectedMatrixArray = expected.match(matrixRegExp).slice(1).map(Number);
+
+  assert_equals(actualMatrixArray.length, expectedMatrixArray.length,
+    'Array lengths should be equal (got \'' + expected + '\' and \'' + actual +
+    '\'): ' + description);
+  for (var i = 0; i < actualMatrixArray.length; i++) {
+    assert_approx_equals(actualMatrixArray[i], expectedMatrixArray[i], 0.01,
+      'Matrix array should be equal (got \'' + expected + '\' and \'' + actual +
+      '\'): ' + description);
+  }
+}
+
 /**
  * Appends a div to the document body and creates an animation on the div.
  * NOTE: This function asserts when trying to create animations with durations
  * shorter than 100s because the shorter duration may cause intermittent
  * failures.  If you are not sure how long it is suitable, use 100s; it's
  * long enough but shorter than our test framework timeout (330s).
  * If you really need to use shorter durations, use animate() function directly.
  *
@@ -168,17 +193,18 @@ function flushComputedStyle(elem) {
 
 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_between_inclusive",
                         "assert_true", "assert_false",
                         "assert_class_string", "assert_throws",
-                        "assert_unreached", "promise_test", "test"]) {
+                        "assert_unreached", "assert_regexp_match",
+                        "promise_test", "test"]) {
     window[funcName] = opener[funcName].bind(opener);
   }
 
   window.EventWatcher = opener.EventWatcher;
 
   function done() {
     opener.add_completion_callback(function() {
       self.close();
@@ -201,16 +227,39 @@ function setupSynchronousObserver(t, tar
    });
   t.add_cleanup(() => {
     observer.disconnect();
   });
   observer.observe(target, { animations: true, subtree: subtree });
   return observer;
 }
 
+/*
+ * Returns a promise that is resolved when the document has finished loading.
+ */
+function waitForDocumentLoad() {
+  return new Promise(function(resolve, reject) {
+    if (document.readyState === "complete") {
+      resolve();
+    } else {
+      window.addEventListener("load", resolve);
+    }
+  });
+}
+
+/*
+ * Enters test refresh mode, and restores the mode when |t| finishes.
+ */
+function useTestRefreshMode(t) {
+  SpecialPowers.DOMWindowUtils.advanceTimeAndRefresh(0);
+  t.add_cleanup(() => {
+    SpecialPowers.DOMWindowUtils.restoreNormalRefresh();
+  });
+}
+
 /**
  * Returns true if off-main-thread animations.
  */
 function isOMTAEnabled() {
   const OMTAPrefKey = 'layers.offmainthreadcomposition.async-animations';
   return SpecialPowers.DOMWindowUtils.layerManagerRemote &&
          SpecialPowers.getBoolPref(OMTAPrefKey);
 }