<!doctype html>
<head>
<meta charset=utf-8>
<title>Bug 1196114 - Test metadata related to which animation properties
       are running on the compositor</title>
<script type="application/javascript" src="../testharness.js"></script>
<script type="application/javascript" src="../testharnessreport.js"></script>
<script type="application/javascript" src="../testcommon.js"></script>
<style>
.compositable {
  /* Element needs geometry to be eligible for layerization */
  width: 100px;
  height: 100px;
  background-color: white;
}
@keyframes fade {
  from { opacity: 1 }
  to   { opacity: 0 }
}
</style>
</head>
<body>
<a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1196114"
  target="_blank">Mozilla Bug 1196114</a>
<div id="log"></div>
<script>
'use strict';

// This is used for obtaining localized strings.
var gStringBundle;

W3CTest.runner.requestLongerTimeout(2);

SpecialPowers.pushPrefEnv({ "set": [
                            ["general.useragent.locale", "en-US"],
                            // Need to set devPixelsPerPx explicitly to gain
                            // consistent pixel values in warning messages
                            // regardless of platform DPIs.
                            ["layout.css.devPixelsPerPx", 1],
                          ] },
                          start);

function compare_property_state(a, b) {
  if (a.property > b.property) {
    return -1;
  } else if (a.property < b.property) {
    return 1;
  }
  if (a.runningOnCompositor != b.runningOnCompositor) {
    return a.runningOnCompositor ? 1 : -1;
  }
  return a.warning > b.warning ? -1 : 1;
}

function assert_animation_property_state_equals(actual, expected) {
  assert_equals(actual.length, expected.length, 'Number of properties');

  var sortedActual = actual.sort(compare_property_state);
  var sortedExpected = expected.sort(compare_property_state);

  for (var i = 0; i < sortedActual.length; i++) {
    assert_equals(sortedActual[i].property,
                  sortedExpected[i].property,
                  'CSS property name should match');
    assert_equals(sortedActual[i].runningOnCompositor,
                  sortedExpected[i].runningOnCompositor,
                  'runningOnCompositor property should match');
    if (sortedExpected[i].warning instanceof RegExp) {
      assert_regexp_match(sortedActual[i].warning,
                          sortedExpected[i].warning,
                          'warning message should match');
    } else if (sortedExpected[i].warning) {
      assert_equals(sortedActual[i].warning,
                    gStringBundle.GetStringFromName(sortedExpected[i].warning),
                    'warning message should match');
    }
  }
}

// Check that the animation is running on compositor and
// warning property is not set for the CSS property regardless
// expected values.
function assert_property_state_on_compositor(actual, expected) {
  assert_equals(actual.length, expected.length);

  var sortedActual = actual.sort(compare_property_state);
  var sortedExpected = expected.sort(compare_property_state);

  for (var i = 0; i < sortedActual.length; i++) {
    assert_equals(sortedActual[i].property,
                  sortedExpected[i].property,
                  'CSS property name should match');
    assert_true(sortedActual[i].runningOnCompositor,
                'runningOnCompositor property should be true on ' +
                sortedActual[i].property);
    assert_not_exists(sortedActual[i], 'warning',
                      'warning property should not be set');
  }
}

var gAnimationsTests = [
  {
    desc: 'animations on compositor',
    frames: {
      opacity: [0, 1]
    },
    expected: [
      {
        property: 'opacity',
        runningOnCompositor: true
      }
    ]
  },
  {
    desc: 'animations on main thread',
    frames: {
      backgroundColor: ['white', 'red']
    },
    expected: [
      {
        property: 'background-color',
        runningOnCompositor: false
      }
    ]
  },
  {
    desc: 'animations on both threads',
    frames: {
      backgroundColor: ['white', 'red'],
      transform: ['translate(0px)', 'translate(100px)']
    },
    expected: [
      {
        property: 'background-color',
        runningOnCompositor: false
      },
      {
        property: 'transform',
        runningOnCompositor: true
      }
    ]
  },
  {
    desc: 'two animation properties on compositor thread',
    frames: {
      opacity: [0, 1],
      transform: ['translate(0px)', 'translate(100px)']
    },
    expected: [
      {
        property: 'opacity',
        runningOnCompositor: true
      },
      {
        property: 'transform',
        runningOnCompositor: true
      }
    ]
  },
  {
    desc: 'opacity on compositor with animation of geometric properties',
    frames: {
      width: ['100px', '200px'],
      opacity: [0, 1]
    },
    expected: [
      {
        property: 'width',
        runningOnCompositor: false
      },
      {
        property: 'opacity',
        runningOnCompositor: true
      }
    ]
  },
];

// Test cases that check results of adding/removing a 'width' property on the
// same animation object.
var gAnimationWithGeometricKeyframeTests = [
  {
    desc: 'transform',
    frames: {
      transform: ['translate(0px)', 'translate(100px)']
    },
    expected: {
      withoutGeometric: [
        {
          property: 'transform',
          runningOnCompositor: true
        }
      ],
      withGeometric: [
        {
          property: 'width',
          runningOnCompositor: false
        },
        {
          property: 'transform',
          runningOnCompositor: false,
          warning: 'CompositorAnimationWarningTransformWithGeometricProperties'
        }
      ]
    }
  },
  {
    desc: 'opacity and transform',
    frames: {
      opacity: [0, 1],
      transform: ['translate(0px)', 'translate(100px)']
    },
    expected: {
      withoutGeometric: [
        {
          property: 'opacity',
          runningOnCompositor: true
        },
        {
          property: 'transform',
          runningOnCompositor: true
        }
      ],
      withGeometric: [
        {
          property: 'width',
          runningOnCompositor: false
        },
        {
          property: 'opacity',
          runningOnCompositor: true
        },
        {
          property: 'transform',
          runningOnCompositor: false,
          warning: 'CompositorAnimationWarningTransformWithGeometricProperties'
        }
      ]
    }
  },
];

// Performance warning tests that set and clear a style property.
var gPerformanceWarningTestsStyle = [
  {
    desc: 'preserve-3d transform',
    frames: {
      transform: ['translate(0px)', 'translate(100px)']
    },
    style: 'transform-style: preserve-3d',
    expected: [
      {
        property: 'transform',
        runningOnCompositor: false,
        warning: 'CompositorAnimationWarningTransformPreserve3D'
      }
    ]
  },
  {
    desc: 'transform with backface-visibility:hidden',
    frames: {
      transform: ['translate(0px)', 'translate(100px)']
    },
    style: 'backface-visibility: hidden;',
    expected: [
      {
        property: 'transform',
        runningOnCompositor: false,
        warning: 'CompositorAnimationWarningTransformBackfaceVisibilityHidden'
      }
    ]
  },
  {
    desc: 'opacity and transform with preserve-3d',
    frames: {
      opacity: [0, 1],
      transform: ['translate(0px)', 'translate(100px)']
    },
    style: 'transform-style: preserve-3d',
    expected: [
      {
        property: 'opacity',
        runningOnCompositor: true
      },
      {
        property: 'transform',
        runningOnCompositor: false,
        warning: 'CompositorAnimationWarningTransformPreserve3D'
      }
    ]
  },
  {
    desc: 'opacity and transform with backface-visibility:hidden',
    frames: {
      opacity: [0, 1],
      transform: ['translate(0px)', 'translate(100px)']
    },
    style: 'backface-visibility: hidden;',
    expected: [
      {
        property: 'opacity',
        runningOnCompositor: true
      },
      {
        property: 'transform',
        runningOnCompositor: false,
        warning: 'CompositorAnimationWarningTransformBackfaceVisibilityHidden'
      }
    ]
  },
];

// Performance warning tests that set and clear the id property
var gPerformanceWarningTestsId= [
  {
    desc: 'moz-element referencing a transform',
    frames: {
      transform: ['translate(0px)', 'translate(100px)']
    },
    id: 'transformed',
    createelement: 'width:100px; height:100px; background: -moz-element(#transformed)',
    expected: [
      {
        property: 'transform',
        runningOnCompositor: false,
        warning: 'CompositorAnimationWarningHasRenderingObserver'
      }
    ]
  },
];

var gMultipleAsyncAnimationsTests = [
  {
    desc: 'opacity and transform with preserve-3d',
    style: 'transform-style: preserve-3d',
    animations: [
      {
        frames: {
          transform: ['translate(0px)', 'translate(100px)']
        },
        expected: [
          {
            property: 'transform',
            runningOnCompositor: false,
            warning: 'CompositorAnimationWarningTransformPreserve3D'
          }
        ]
      },
      {
        frames: {
          opacity: [0, 1]
        },
        expected: [
          {
            property: 'opacity',
            runningOnCompositor: true,
          }
        ]
      }
    ],
  },
  {
    desc: 'opacity and transform with backface-visibility:hidden',
    style: 'backface-visibility: hidden;',
    animations: [
      {
        frames: {
          transform: ['translate(0px)', 'translate(100px)']
        },
        expected: [
          {
            property: 'transform',
            runningOnCompositor: false,
            warning: 'CompositorAnimationWarningTransformBackfaceVisibilityHidden'
          }
        ]
      },
      {
        frames: {
          opacity: [0, 1]
        },
        expected: [
          {
            property: 'opacity',
            runningOnCompositor: true,
          }
        ]
      }
    ],
  },
];

// Test cases that check results of adding/removing a 'width' keyframe on the
// same animation object, where multiple animation objects belong to the same
// element.
// The 'width' property is added to animations[1].
var gMultipleAsyncAnimationsWithGeometricKeyframeTests = [
  {
    desc: 'transform and opacity with geometric keyframes',
    animations: [
      {
        frames: {
          transform: ['translate(0px)', 'translate(100px)']
        },
        expected: {
          withoutGeometric: [
            {
              property: 'transform',
              runningOnCompositor: true
            }
          ],
          withGeometric: [
            {
              property: 'transform',
              runningOnCompositor: false,
              warning: 'CompositorAnimationWarningTransformWithGeometricProperties'
            }
          ]
        }
      },
      {
        frames: {
          opacity: [0, 1]
        },
        expected: {
          withoutGeometric: [
            {
              property: 'opacity',
              runningOnCompositor: true,
            }
          ],
          withGeometric: [
            {
              property: 'width',
              runningOnCompositor: false,
            },
            {
              property: 'opacity',
              runningOnCompositor: true,
            }
          ]
        }
      }
    ],
  },
  {
    desc: 'opacity and transform with geometric keyframes',
    animations: [
      {
        frames: {
          opacity: [0, 1]
        },
        expected: {
          withoutGeometric: [
            {
              property: 'opacity',
              runningOnCompositor: true,
            }
          ],
          withGeometric: [
            {
              property: 'opacity',
              runningOnCompositor: true,
            }
          ]
        }
      },
      {
        frames: {
          transform: ['translate(0px)', 'translate(100px)']
        },
        expected: {
          withoutGeometric: [
            {
              property: 'transform',
              runningOnCompositor: true
            }
          ],
          withGeometric: [
            {
              property: 'width',
              runningOnCompositor: false,
            },
            {
              property: 'transform',
              runningOnCompositor: false,
              warning: 'CompositorAnimationWarningTransformWithGeometricProperties'
            }
          ]
        }
      }
    ]
  },
];

// Test cases that check results of adding/removing 'width' animation on the
// same element which has async animations.
var gMultipleAsyncAnimationsWithGeometricAnimationTests = [
  {
    desc: 'transform',
    animations: [
      {
        frames: {
          transform: ['translate(0px)', 'translate(100px)']
        },
        expected: [
          {
            property: 'transform',
            runningOnCompositor: false,
            warning: 'CompositorAnimationWarningTransformWithGeometricProperties'
          }
        ]
      },
    ]
  },
  {
    desc: 'opacity',
    animations: [
      {
        frames: {
          opacity: [0, 1]
        },
        expected: [
          {
            property: 'opacity',
            runningOnCompositor: true
          }
        ]
      },
    ]
  },
  {
    desc: 'opacity and transform',
    animations: [
      {
        frames: {
          transform: ['translate(0px)', 'translate(100px)']
        },
        expected: [
          {
            property: 'transform',
            runningOnCompositor: false,
            warning: 'CompositorAnimationWarningTransformWithGeometricProperties'
          }
        ]
      },
      {
        frames: {
          opacity: [0, 1]
        },
        expected: [
          {
            property: 'opacity',
            runningOnCompositor: true,
          }
        ]
      }
    ],
  },
];

var gAnimationsOnTooSmallElementTests = [
  {
    desc: 'opacity on too small element',
    frames: {
      opacity: [0, 1]
    },
    style: { style: 'width: 8px; height: 8px; background-color: red;' +
                    // We need to set transform here to try creating an
                    // individual frame for this opacity element.
                    // Without this, this small element is created on the same
                    // nsIFrame of mochitest iframe, i.e. the document which are
                    // running this test, as a result the layer corresponding
                    // to the frame is sent to compositor.
                    'transform: translateX(100px);' },
    expected: [
      {
        property: 'opacity',
        runningOnCompositor: false,
        warning: /Animation cannot be run on the compositor because frame size \(8, 8\) is smaller than \(16, 16\)/
      }
    ]
  },
  {
    desc: 'transform on too small element',
    frames: {
      transform: ['translate(0px)', 'translate(100px)']
    },
    style: { style: 'width: 8px; height: 8px; background-color: red;' },
    expected: [
      {
        property: 'transform',
        runningOnCompositor: false,
        warning: /Animation cannot be run on the compositor because frame size \(8, 8\) is smaller than \(16, 16\)/
      }
    ]
  },
];

function start() {
  var bundleService = SpecialPowers.Cc['@mozilla.org/intl/stringbundle;1']
    .getService(SpecialPowers.Ci.nsIStringBundleService);
  gStringBundle = bundleService
    .createBundle("chrome://global/locale/layout_errors.properties");

  gAnimationsTests.forEach(function(subtest) {
    promise_test(function(t) {
      var animation = addDivAndAnimate(t,
                                       { class: 'compositable' },
                                       subtest.frames, 100 * MS_PER_SEC);
      return animation.ready.then(function() {
        assert_animation_property_state_equals(
          animation.effect.getProperties(),
          subtest.expected);
      });
    }, subtest.desc);
  });

  gAnimationWithGeometricKeyframeTests.forEach(function(subtest) {
    promise_test(function(t) {
      var animation = addDivAndAnimate(t,
                                       { class: 'compositable' },
                                       subtest.frames, 100 * MS_PER_SEC);
      return animation.ready.then(function() {
        // First, a transform animation is running on compositor.
        assert_animation_property_state_equals(
          animation.effect.getProperties(),
          subtest.expected.withoutGeometric);
      }).then(function() {
        // Add a 'width' property.
        var keyframes = animation.effect.getKeyframes();

        keyframes[0].width = '100px';
        keyframes[1].width = '200px';

        animation.effect.setKeyframes(keyframes);
        return waitForFrame();
      }).then(function() {
        // Now the transform animation is not running on compositor because of
        // the 'width' property.
        assert_animation_property_state_equals(
          animation.effect.getProperties(),
          subtest.expected.withGeometric);
      }).then(function() {
        // Remove the 'width' property.
        var keyframes = animation.effect.getKeyframes();

        delete keyframes[0].width;
        delete keyframes[1].width;

        animation.effect.setKeyframes(keyframes);
        return waitForFrame();
      }).then(function() {
        // Finally, the transform animation is running on compositor.
        assert_animation_property_state_equals(
          animation.effect.getProperties(),
          subtest.expected.withoutGeometric);
      });
    }, 'An animation has: ' + subtest.desc);
  });

  gPerformanceWarningTestsStyle.forEach(function(subtest) {
    promise_test(function(t) {
      var animation = addDivAndAnimate(t,
                                       { class: 'compositable' },
                                       subtest.frames, 100 * MS_PER_SEC);
      return animation.ready.then(function() {
        assert_property_state_on_compositor(
          animation.effect.getProperties(),
          subtest.expected);
        animation.effect.target.style = subtest.style;
        return waitForFrame();
      }).then(function() {
        assert_animation_property_state_equals(
          animation.effect.getProperties(),
          subtest.expected);
        animation.effect.target.style = '';
        return waitForFrame();
      }).then(function() {
        assert_property_state_on_compositor(
          animation.effect.getProperties(),
          subtest.expected);
      });
    }, subtest.desc);
  });

  gPerformanceWarningTestsId.forEach(function(subtest) {
    promise_test(function(t) {
      if (subtest.createelement) {
        addDiv(t, { style: subtest.createelement });
      }

      var animation = addDivAndAnimate(t,
                                       { class: 'compositable' },
                                       subtest.frames, 100 * MS_PER_SEC);
      return animation.ready.then(function() {
        assert_property_state_on_compositor(
          animation.effect.getProperties(),
          subtest.expected);
        animation.effect.target.id = subtest.id;
        return waitForFrame();
      }).then(function() {
        assert_animation_property_state_equals(
          animation.effect.getProperties(),
          subtest.expected);
        animation.effect.target.id = '';
        return waitForFrame();
      }).then(function() {
        assert_property_state_on_compositor(
          animation.effect.getProperties(),
          subtest.expected);
      });
    }, subtest.desc);
  });

  gMultipleAsyncAnimationsTests.forEach(function(subtest) {
    promise_test(function(t) {
      var div = addDiv(t, { class: 'compositable' });
      var animations = subtest.animations.map(function(anim) {
        var animation = div.animate(anim.frames, 100 * MS_PER_SEC);

        // Bind expected values to animation object.
        animation.expected = anim.expected;
        return animation;
      });
      return waitForAllAnimations(animations).then(function() {
        animations.forEach(function(anim) {
          assert_property_state_on_compositor(
            anim.effect.getProperties(),
            anim.expected);
        });
        div.style = subtest.style;
        return waitForFrame();
      }).then(function() {
        animations.forEach(function(anim) {
          assert_animation_property_state_equals(
            anim.effect.getProperties(),
            anim.expected);
        });
        div.style = '';
        return waitForFrame();
      }).then(function() {
        animations.forEach(function(anim) {
          assert_property_state_on_compositor(
            anim.effect.getProperties(),
            anim.expected);
        });
      });
    }, 'Multiple animations: ' + subtest.desc);
  });

  gMultipleAsyncAnimationsWithGeometricKeyframeTests.forEach(function(subtest) {
    promise_test(function(t) {
      var div = addDiv(t, { class: 'compositable' });
      var animations = subtest.animations.map(function(anim) {
        var animation = div.animate(anim.frames, 100 * MS_PER_SEC);

        // Bind expected values to animation object.
        animation.expected = anim.expected;
        return animation;
      });
      return waitForAllAnimations(animations).then(function() {
        // First, all animations are running on compositor.
        animations.forEach(function(anim) {
          assert_animation_property_state_equals(
            anim.effect.getProperties(),
            anim.expected.withoutGeometric);
        });
      }).then(function() {
        // Add a 'width' property to animations[1].
        var keyframes = animations[1].effect.getKeyframes();

        keyframes[0].width = '100px';
        keyframes[1].width = '200px';

        animations[1].effect.setKeyframes(keyframes);
        return waitForFrame();
      }).then(function() {
        // Now the transform animation is not running on compositor because of
        // the 'width' property.
        animations.forEach(function(anim) {
          assert_animation_property_state_equals(
            anim.effect.getProperties(),
            anim.expected.withGeometric);
        });
      }).then(function() {
        // Remove the 'width' property from animations[1].
        var keyframes = animations[1].effect.getKeyframes();

        delete keyframes[0].width;
        delete keyframes[1].width;

        animations[1].effect.setKeyframes(keyframes);
        return waitForFrame();
      }).then(function() {
        // Finally, all animations are running on compositor.
        animations.forEach(function(anim) {
          assert_animation_property_state_equals(
            anim.effect.getProperties(),
            anim.expected.withoutGeometric);
        });
      });
    }, 'Multiple animations with geometric property: ' + subtest.desc);
  });

  gMultipleAsyncAnimationsWithGeometricAnimationTests.forEach(function(subtest) {
    promise_test(function(t) {
      var div = addDiv(t, { class: 'compositable' });
      var animations = subtest.animations.map(function(anim) {
        var animation = div.animate(anim.frames, 100 * MS_PER_SEC);

        // Bind expected values to animation object.
        animation.expected = anim.expected;
        return animation;
      });

      var widthAnimation;

      return waitForAllAnimations(animations).then(function() {
        animations.forEach(function(anim) {
          assert_property_state_on_compositor(
            anim.effect.getProperties(),
            anim.expected);
        });
      }).then(function() {
        // Append 'width' animation on the same element.
        widthAnimation = div.animate({ width: ['100px', '200px'] },
                                     100 * MS_PER_SEC);
        return waitForFrame();
      }).then(function() {
        // Now transform animations are not running on compositor because of
        // the 'width' animation.
        animations.forEach(function(anim) {
          assert_animation_property_state_equals(
            anim.effect.getProperties(),
            anim.expected);
        });
        // Remove the 'width' animation.
        widthAnimation.cancel();
        return waitForFrame();
      }).then(function() {
        // Now all animations are running on compositor.
        animations.forEach(function(anim) {
          assert_property_state_on_compositor(
            anim.effect.getProperties(),
            anim.expected);
        });
      });
    }, 'Multiple async animations and geometric animation: ' + subtest.desc);
  });

  gAnimationsOnTooSmallElementTests.forEach(function(subtest) {
    promise_test(function(t) {
    var div = addDiv(t, subtest.style);
    var animation = div.animate(subtest.frames, 100 * MS_PER_SEC);
      return animation.ready.then(function() {
        assert_animation_property_state_equals(
          animation.effect.getProperties(),
          subtest.expected);
      });
    }, subtest.desc);
  });

  promise_test(function(t) {
    var animation = addDivAndAnimate(t,
                                     { class: 'compositable' },
                                     { transform: [ 'translate(0px)',
                                                    'translate(100px)'] },
                                     100 * MS_PER_SEC);
    return animation.ready.then(function() {
      assert_animation_property_state_equals(
        animation.effect.getProperties(),
        [ { property: 'transform', runningOnCompositor: true } ]);
      animation.effect.target.style = 'width: 10000px; height: 10000px';
      return waitForFrame();
    }).then(function() {
      // viewport depends on test environment.
      var expectedWarning = new RegExp(
        "Animation cannot be run on the compositor because the frame size " +
        "\\(10000, 10000\\) is bigger than the viewport \\(\\d+, \\d+\\) " +
        "or the visual rectangle \\(10000, 10000\\) is larger than the " +
        "maximum allowed value \\(\\d+\\)");
      assert_animation_property_state_equals(
        animation.effect.getProperties(),
        [ {
          property: 'transform',
          runningOnCompositor: false,
          warning: expectedWarning
        } ]);
      animation.effect.target.style = 'width: 100px; height: 100px';
      return waitForFrame();
    }).then(function() {
      // FIXME: Bug 1253164: the animation should get back on compositor.
      assert_animation_property_state_equals(
        animation.effect.getProperties(),
        [ { property: 'transform', runningOnCompositor: false } ]);
    });
  }, 'transform on too big element');

  promise_test(function(t) {
    var svg  = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
    svg.setAttribute('width', '100');
    svg.setAttribute('height', '100');
    var rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
    rect.setAttribute('width', '100');
    rect.setAttribute('height', '100');
    rect.setAttribute('fill', 'red');
    svg.appendChild(rect);
    document.body.appendChild(svg);
    t.add_cleanup(function() {
      svg.remove();
    });

    var animation = svg.animate(
      { transform: ['translate(0px)', 'translate(100px)'] }, 100 * MS_PER_SEC);
    return animation.ready.then(function() {
      assert_animation_property_state_equals(
        animation.effect.getProperties(),
        [ { property: 'transform', runningOnCompositor: true } ]);
      svg.setAttribute('transform', 'translate(10, 20)');
      return waitForFrame();
    }).then(function() {
      assert_animation_property_state_equals(
        animation.effect.getProperties(),
        [ {
          property: 'transform',
          runningOnCompositor: false,
          warning: 'CompositorAnimationWarningTransformSVG'
        } ]);
      svg.removeAttribute('transform');
      return waitForFrame();
    }).then(function() {
      assert_animation_property_state_equals(
        animation.effect.getProperties(),
        [ { property: 'transform', runningOnCompositor: true } ]);
    });
  }, 'transform of nsIFrame with SVG transform');

  promise_test(function(t) {
    var div = addDiv(t, { class: 'compositable',
                          style: 'animation: fade 100s' });
    var cssAnimation = div.getAnimations()[0];
    var scriptAnimation = div.animate({ opacity: [ 1, 0 ] }, 100 * MS_PER_SEC);
    return scriptAnimation.ready.then(function() {
      assert_animation_property_state_equals(
        cssAnimation.effect.getProperties(),
        [ { property: 'opacity', runningOnCompositor: true } ]);
      assert_animation_property_state_equals(
        scriptAnimation.effect.getProperties(),
        [ { property: 'opacity', runningOnCompositor: true } ]);
    });
  }, 'overridden animation');
}

</script>

</body>