summaryrefslogtreecommitdiffstats
path: root/dom/animation/test/chrome
diff options
context:
space:
mode:
Diffstat (limited to 'dom/animation/test/chrome')
-rw-r--r--dom/animation/test/chrome/file_animate_xrays.html19
-rw-r--r--dom/animation/test/chrome/test_animate_xrays.html31
-rw-r--r--dom/animation/test/chrome/test_animation_observers.html1177
-rw-r--r--dom/animation/test/chrome/test_animation_performance_warning.html957
-rw-r--r--dom/animation/test/chrome/test_animation_properties.html993
-rw-r--r--dom/animation/test/chrome/test_generated_content_getAnimations.html83
-rw-r--r--dom/animation/test/chrome/test_observers_for_sync_api.html854
-rw-r--r--dom/animation/test/chrome/test_restyles.html815
-rw-r--r--dom/animation/test/chrome/test_running_on_compositor.html966
9 files changed, 5895 insertions, 0 deletions
diff --git a/dom/animation/test/chrome/file_animate_xrays.html b/dom/animation/test/chrome/file_animate_xrays.html
new file mode 100644
index 000000000..8a68fc548
--- /dev/null
+++ b/dom/animation/test/chrome/file_animate_xrays.html
@@ -0,0 +1,19 @@
+<!doctype html>
+<html>
+<head>
+<meta charset=utf-8>
+<script>
+Element.prototype.animate = function() {
+ throw 'Called animate() as defined in content document';
+}
+// Bug 1211783: Use KeyframeEffect (not KeyframeEffectReadOnly) here
+for (var obj of [KeyframeEffectReadOnly, Animation]) {
+ obj = function() {
+ throw 'Called overridden ' + String(obj) + ' constructor';
+ };
+}
+</script>
+<body>
+<div id="target"></div>
+</body>
+</html>
diff --git a/dom/animation/test/chrome/test_animate_xrays.html b/dom/animation/test/chrome/test_animate_xrays.html
new file mode 100644
index 000000000..56b981bf1
--- /dev/null
+++ b/dom/animation/test/chrome/test_animate_xrays.html
@@ -0,0 +1,31 @@
+<!doctype html>
+<head>
+<meta charset=utf-8>
+<script type="application/javascript" src="../testharness.js"></script>
+<script type="application/javascript" src="../testharnessreport.js"></script>
+<script type="application/javascript" src="../testcommon.js"></script>
+</head>
+<body>
+<a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1045994"
+ target="_blank">Mozilla Bug 1045994</a>
+<div id="log"></div>
+<iframe id="iframe"
+ src="http://example.org/tests/dom/animation/test/chrome/file_animate_xrays.html"></iframe>
+<script>
+'use strict';
+
+var win = document.getElementById('iframe').contentWindow;
+
+async_test(function(t) {
+ window.addEventListener('load', t.step_func(function() {
+ var target = win.document.getElementById('target');
+ var anim = target.animate({ opacity: [ 0, 1 ] }, 100 * MS_PER_SEC);
+ // In the x-ray case, the frames object will be given an opaque wrapper
+ // so it won't be possible to fetch any frames from it.
+ assert_equals(anim.effect.getKeyframes().length, 0);
+ t.done();
+ }));
+}, 'Calling animate() across x-rays');
+
+</script>
+</body>
diff --git a/dom/animation/test/chrome/test_animation_observers.html b/dom/animation/test/chrome/test_animation_observers.html
new file mode 100644
index 000000000..237128e04
--- /dev/null
+++ b/dom/animation/test/chrome/test_animation_observers.html
@@ -0,0 +1,1177 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title>Test chrome-only MutationObserver animation notifications</title>
+<script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+<script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
+<script src="../testcommon.js"></script>
+<link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+<style>
+@keyframes anim {
+ to { transform: translate(100px); }
+}
+@keyframes anotherAnim {
+ to { transform: translate(0px); }
+}
+#target {
+ width: 100px;
+ height: 100px;
+ background-color: yellow;
+ line-height: 16px;
+}
+</style>
+<div id=container><div id=target></div></div>
+<script>
+var div = document.getElementById("target");
+var gRecords = [];
+var gObserver = new MutationObserver(function(newRecords) {
+ gRecords.push(...newRecords);
+});
+
+// Asynchronous testing framework based on layout/style/test/animation_utils.js.
+
+var gTests = [];
+var gCurrentTestName;
+
+function addAsyncAnimTest(aName, aOptions, aTestGenerator) {
+ aTestGenerator.testName = aName;
+ aTestGenerator.options = aOptions || {};
+ gTests.push(aTestGenerator);
+}
+
+function runAsyncTest(aTestGenerator) {
+ return waitForFrame().then(function() {
+ var generator;
+
+ function step(arg) {
+ var next;
+ try {
+ next = generator.next(arg);
+ } catch (e) {
+ return Promise.reject(e);
+ }
+ if (next.done) {
+ return Promise.resolve(next.value);
+ } else {
+ return Promise.resolve(next.value).then(step);
+ }
+ }
+
+ var subtree = aTestGenerator.options.subtree;
+
+ gCurrentTestName = aTestGenerator.testName;
+ if (subtree) {
+ gCurrentTestName += ":subtree";
+ }
+
+ gRecords = [];
+ gObserver.disconnect();
+ gObserver.observe(aTestGenerator.options.observe,
+ { animations: true, subtree: subtree});
+
+ generator = aTestGenerator();
+ return step();
+ });
+};
+
+function runAllAsyncTests() {
+ return gTests.reduce(function(sequence, test) {
+ return sequence.then(() => runAsyncTest(test));
+ }, Promise.resolve());
+}
+
+// Wrap is and ok with versions that prepend the current sub-test name
+// to the assertion description.
+var old_is = is, old_ok = ok;
+is = function(a, b, message) {
+ if (gCurrentTestName && message) {
+ message = `[${gCurrentTestName}] ${message}`;
+ }
+ old_is(a, b, message);
+}
+ok = function(a, message) {
+ if (gCurrentTestName && message) {
+ message = `[${gCurrentTestName}] ${message}`;
+ }
+ old_ok(a, message);
+}
+
+// Adds an event listener and returns a Promise that is resolved when the
+// event listener is called.
+function await_event(aElement, aEventName) {
+ return new Promise(function(aResolve) {
+ function listener(aEvent) {
+ aElement.removeEventListener(aEventName, listener);
+ aResolve();
+ }
+ aElement.addEventListener(aEventName, listener, false);
+ });
+}
+
+function assert_record_list(actual, expected, desc, index, listName) {
+ is(actual.length, expected.length, `${desc} - record[${index}].${listName} length`);
+ if (actual.length != expected.length) {
+ return;
+ }
+ for (var i = 0; i < actual.length; i++) {
+ ok(actual.indexOf(expected[i]) != -1,
+ `${desc} - record[${index}].${listName} contains expected Animation`);
+ }
+}
+
+function assert_records(expected, desc) {
+ var records = gRecords;
+ gRecords = [];
+ is(records.length, expected.length, `${desc} - number of records`);
+ if (records.length != expected.length) {
+ return;
+ }
+ for (var i = 0; i < records.length; i++) {
+ assert_record_list(records[i].addedAnimations, expected[i].added, desc, i, "addedAnimations");
+ assert_record_list(records[i].changedAnimations, expected[i].changed, desc, i, "changedAnimations");
+ assert_record_list(records[i].removedAnimations, expected[i].removed, desc, i, "removedAnimations");
+ }
+}
+
+// -- Tests ------------------------------------------------------------------
+
+// We run all tests first targeting the div and observing the div, then again
+// targeting the div and observing its parent while using the subtree:true
+// MutationObserver option.
+
+[
+ { observe: div, target: div, subtree: false },
+ { observe: div.parentNode, target: div, subtree: true },
+].forEach(function(aOptions) {
+
+ var e = aOptions.target;
+
+ // Test that starting a single transition that completes normally
+ // dispatches an added notification and then a removed notification.
+ addAsyncAnimTest("single_transition", aOptions, function*() {
+ // Start a transition.
+ e.style = "transition: background-color 100s; background-color: lime;";
+
+ // Register for the end of the transition.
+ var transitionEnd = await_event(e, "transitionend");
+
+ // The transition should cause the creation of a single Animation.
+ var animations = e.getAnimations();
+ is(animations.length, 1, "getAnimations().length after transition start");
+
+ // Wait for the single MutationRecord for the Animation addition to
+ // be delivered.
+ yield waitForFrame();
+ assert_records([{ added: animations, changed: [], removed: [] }],
+ "records after transition start");
+
+ // Advance until near the end of the transition, then wait for it to finish.
+ animations[0].currentTime = 99900;
+ yield transitionEnd;
+
+ // After the transition has finished, the Animation should disappear.
+ is(e.getAnimations().length, 0,
+ "getAnimations().length after transition end");
+
+ // Wait for the change MutationRecord for seeking the Animation to be
+ // delivered, followed by the the removal MutationRecord.
+ yield waitForFrame();
+ assert_records([{ added: [], changed: animations, removed: [] },
+ { added: [], changed: [], removed: animations }],
+ "records after transition end");
+
+ e.style = "";
+ });
+
+ // Test that starting a single transition that is cancelled by resetting
+ // the transition-property property dispatches an added notification and
+ // then a removed notification.
+ addAsyncAnimTest("single_transition_cancelled_property", aOptions, function*() {
+ // Start a long transition.
+ e.style = "transition: background-color 100s; background-color: lime;";
+
+ // The transition should cause the creation of a single Animation.
+ var animations = e.getAnimations();
+ is(animations.length, 1, "getAnimations().length after transition start");
+
+ // Wait for the single MutationRecord for the Animation addition to
+ // be delivered.
+ yield waitForFrame();
+ assert_records([{ added: animations, changed: [], removed: [] }],
+ "records after transition start");
+
+ // Cancel the transition by setting transition-property.
+ e.style.transitionProperty = "none";
+
+ // Wait for the single MutationRecord for the Animation removal to
+ // be delivered.
+ yield waitForFrame();
+ assert_records([{ added: [], changed: [], removed: animations }],
+ "records after transition end");
+
+ e.style = "";
+ });
+
+ // Test that starting a single transition that is cancelled by setting
+ // style to the currently animated value dispatches an added
+ // notification and then a removed notification.
+ addAsyncAnimTest("single_transition_cancelled_value", aOptions, function*() {
+ // Start a long transition with a predictable value.
+ e.style = "transition: background-color 100s steps(2, end) -51s; background-color: lime;";
+
+ // The transition should cause the creation of a single Animation.
+ var animations = e.getAnimations();
+ is(animations.length, 1, "getAnimations().length after transition start");
+
+ // Wait for the single MutationRecord for the Animation addition to
+ // be delivered.
+ yield waitForFrame();
+ assert_records([{ added: animations, changed: [], removed: [] }],
+ "records after transition start");
+
+ // Cancel the transition by setting the current animation value.
+ var value = "rgb(128, 255, 0)";
+ is(getComputedStyle(e).backgroundColor, value, "half-way transition value");
+ e.style.backgroundColor = value;
+
+ // Wait for the single MutationRecord for the Animation removal to
+ // be delivered.
+ yield waitForFrame();
+ assert_records([{ added: [], changed: [], removed: animations }],
+ "records after transition end");
+
+ e.style = "";
+ });
+
+ // Test that starting a single transition that is cancelled by setting
+ // style to a non-interpolable value dispatches an added notification
+ // and then a removed notification.
+ addAsyncAnimTest("single_transition_cancelled_noninterpolable", aOptions, function*() {
+ // Start a long transition.
+ e.style = "transition: line-height 100s; line-height: 100px;";
+
+ // The transition should cause the creation of a single Animation.
+ var animations = e.getAnimations();
+ is(animations.length, 1, "getAnimations().length after transition start");
+
+ // Wait for the single MutationRecord for the Animation addition to
+ // be delivered.
+ yield waitForFrame();
+ assert_records([{ added: animations, changed: [], removed: [] }],
+ "records after transition start");
+
+ // Cancel the transition by setting line-height to a non-interpolable value.
+ e.style.lineHeight = "normal";
+
+ // Wait for the single MutationRecord for the Animation removal to
+ // be delivered.
+ yield waitForFrame();
+ assert_records([{ added: [], changed: [], removed: animations }],
+ "records after transition end");
+
+ e.style = "";
+ });
+
+ // Test that starting a single transition and then reversing it
+ // dispatches an added notification, then a simultaneous removed and
+ // added notification, then a removed notification once finished.
+ addAsyncAnimTest("single_transition_reversed", aOptions, function*() {
+ // Start a long transition.
+ e.style = "transition: background-color 100s step-start; background-color: lime;";
+
+ // The transition should cause the creation of a single Animation.
+ var animations = e.getAnimations();
+ is(animations.length, 1, "getAnimations().length after transition start");
+
+ var firstAnimation = animations[0];
+
+ // Wait for the single MutationRecord for the Animation addition to
+ // be delivered.
+ yield waitForFrame();
+ assert_records([{ added: [firstAnimation], changed: [], removed: [] }],
+ "records after transition start");
+
+ // Wait for the Animation to be playing, then seek well into
+ // the transition.
+ yield firstAnimation.ready;
+ firstAnimation.currentTime = 50 * MS_PER_SEC;
+
+ // Reverse the transition by setting the background-color back to its
+ // original value.
+ e.style.backgroundColor = "yellow";
+
+ // The reversal should cause the creation of a new Animation.
+ animations = e.getAnimations();
+ is(animations.length, 1, "getAnimations().length after transition reversal");
+
+ var secondAnimation = animations[0];
+
+ ok(firstAnimation != secondAnimation,
+ "second Animation should be different from the first");
+
+ // Wait for the change Mutation record from seeking the first animation
+ // to be delivered, followed by a subsequent MutationRecord for the removal
+ // of the original Animation and the addition of the new Animation.
+ yield waitForFrame();
+ assert_records([{ added: [], changed: [firstAnimation], removed: [] },
+ { added: [secondAnimation], changed: [],
+ removed: [firstAnimation] }],
+ "records after transition reversal");
+
+ // Cancel the transition.
+ e.style.transitionProperty = "none";
+
+ // Wait for the single MutationRecord for the Animation removal to
+ // be delivered.
+ yield waitForFrame();
+ assert_records([{ added: [], changed: [], removed: [secondAnimation] }],
+ "records after transition end");
+
+ e.style = "";
+ });
+
+ // Test that multiple transitions starting and ending on an element
+ // at the same time get batched up into a single MutationRecord.
+ addAsyncAnimTest("multiple_transitions", aOptions, function*() {
+ // Start three long transitions.
+ e.style = "transition-duration: 100s; " +
+ "transition-property: color, background-color, line-height; " +
+ "color: blue; background-color: lime; line-height: 24px;";
+
+ // The transitions should cause the creation of three Animations.
+ var animations = e.getAnimations();
+ is(animations.length, 3, "getAnimations().length after transition starts");
+
+ // Wait for the single MutationRecord for the Animation additions to
+ // be delivered.
+ yield waitForFrame();
+ assert_records([{ added: animations, changed: [], removed: [] }],
+ "records after transition starts");
+
+ // Wait for the Animations to get going.
+ yield animations[0].ready;
+ is(animations.filter(p => p.playState == "running").length, 3,
+ "number of running Animations");
+
+ // Seek well into each animation.
+ animations.forEach(p => p.currentTime = 50 * MS_PER_SEC);
+
+ // Prepare the set of expected change MutationRecords, one for each
+ // animation that was seeked.
+ var seekRecords = animations.map(
+ p => ({ added: [], changed: [p], removed: [] })
+ );
+
+ // Cancel one of the transitions by setting transition-property.
+ e.style.transitionProperty = "background-color, line-height";
+
+ var colorAnimation = animations.filter(p => p.playState != "running");
+ var otherAnimations = animations.filter(p => p.playState == "running");
+
+ is(colorAnimation.length, 1,
+ "number of non-running Animations after cancelling one");
+ is(otherAnimations.length, 2,
+ "number of running Animations after cancelling one");
+
+ // Wait for the MutationRecords to be delivered: one for each animation
+ // that was seeked, followed by one for the removal of the color animation.
+ yield waitForFrame();
+ assert_records(seekRecords.concat(
+ { added: [], changed: [], removed: colorAnimation }),
+ "records after color transition end");
+
+ // Cancel the remaining transitions.
+ e.style.transitionProperty = "none";
+
+ // Wait for the MutationRecord for the other two Animation
+ // removals to be delivered.
+ yield waitForFrame();
+ assert_records([{ added: [], changed: [], removed: otherAnimations }],
+ "records after other transition ends");
+
+ e.style = "";
+ });
+
+ // Test that starting a single animation that completes normally
+ // dispatches an added notification and then a removed notification.
+ addAsyncAnimTest("single_animation", aOptions, function*() {
+ // Start an animation.
+ e.style = "animation: anim 100s;";
+
+ // Register for the end of the animation.
+ var animationEnd = await_event(e, "animationend");
+
+ // The animation should cause the creation of a single Animation.
+ var animations = e.getAnimations();
+ is(animations.length, 1, "getAnimations().length after animation start");
+
+ // Wait for the single MutationRecord for the Animation addition to
+ // be delivered.
+ yield waitForFrame();
+ assert_records([{ added: animations, changed: [], removed: [] }],
+ "records after animation start");
+
+ // Advance until near the end of the animation, then wait for it to finish.
+ animations[0].currentTime = 99900;
+ yield animationEnd;
+
+ // After the animation has finished, the Animation should disappear.
+ is(e.getAnimations().length, 0,
+ "getAnimations().length after animation end");
+
+ // Wait for the change MutationRecord from seeking the Animation to
+ // be delivered, followed by a further MutationRecord for the Animation
+ // removal.
+ yield waitForFrame();
+ assert_records([{ added: [], changed: animations, removed: [] },
+ { added: [], changed: [], removed: animations }],
+ "records after animation end");
+
+ e.style = "";
+ });
+
+ // Test that starting a single animation that is cancelled by resetting
+ // the animation-name property dispatches an added notification and
+ // then a removed notification.
+ addAsyncAnimTest("single_animation_cancelled_name", aOptions, function*() {
+ // Start a long animation.
+ e.style = "animation: anim 100s;";
+
+ // The animation should cause the creation of a single Animation.
+ var animations = e.getAnimations();
+ is(animations.length, 1, "getAnimations().length after animation start");
+
+ // Wait for the single MutationRecord for the Animation addition to
+ // be delivered.
+ yield waitForFrame();
+ assert_records([{ added: animations, changed: [], removed: [] }],
+ "records after animation start");
+
+ // Cancel the animation by setting animation-name.
+ e.style.animationName = "none";
+
+ // Wait for the single MutationRecord for the Animation removal to
+ // be delivered.
+ yield waitForFrame();
+ assert_records([{ added: [], changed: [], removed: animations }],
+ "records after animation end");
+
+ e.style = "";
+ });
+
+ // Test that starting a single animation that is cancelled by updating
+ // the animation-duration property dispatches an added notification and
+ // then a removed notification.
+ addAsyncAnimTest("single_animation_cancelled_duration", aOptions, function*() {
+ // Start a long animation.
+ e.style = "animation: anim 100s;";
+
+ // The animation should cause the creation of a single Animation.
+ var animations = e.getAnimations();
+ is(animations.length, 1, "getAnimations().length after animation start");
+
+ // Wait for the single MutationRecord for the Animation addition to
+ // be delivered.
+ yield waitForFrame();
+ assert_records([{ added: animations, changed: [], removed: [] }],
+ "records after animation start");
+
+ // Advance the animation by a second.
+ animations[0].currentTime += 1 * MS_PER_SEC;
+
+ // Cancel the animation by setting animation-duration to a value less
+ // than a second.
+ e.style.animationDuration = "0.1s";
+
+ // Wait for the change MutationRecord from seeking the Animation to
+ // be delivered, followed by a further MutationRecord for the Animation
+ // removal.
+ yield waitForFrame();
+ assert_records([{ added: [], changed: animations, removed: [] },
+ { added: [], changed: [], removed: animations }],
+ "records after animation end");
+
+ e.style = "";
+ });
+
+ // Test that starting a single animation that is cancelled by updating
+ // the animation-delay property dispatches an added notification and
+ // then a removed notification.
+ addAsyncAnimTest("single_animation_cancelled_delay", aOptions, function*() {
+ // Start a long animation.
+ e.style = "animation: anim 100s;";
+
+ // The animation should cause the creation of a single Animation.
+ var animations = e.getAnimations();
+ is(animations.length, 1, "getAnimations().length after animation start");
+
+ // Wait for the single MutationRecord for the Animation addition to
+ // be delivered.
+ yield waitForFrame();
+ assert_records([{ added: animations, changed: [], removed: [] }],
+ "records after animation start");
+
+ // Cancel the animation by setting animation-delay.
+ e.style.animationDelay = "-200s";
+
+ // Wait for the single MutationRecord for the Animation removal to
+ // be delivered.
+ yield waitForFrame();
+ assert_records([{ added: [], changed: [], removed: animations }],
+ "records after animation end");
+
+ e.style = "";
+ });
+
+ // Test that starting a single animation that is cancelled by updating
+ // the animation-fill-mode property dispatches an added notification and
+ // then a removed notification.
+ addAsyncAnimTest("single_animation_cancelled_fill", aOptions, function*() {
+ // Start a short, filled animation.
+ e.style = "animation: anim 100s forwards;";
+
+ // Register for the end of the animation.
+ var animationEnd = await_event(e, "animationend");
+
+ // The animation should cause the creation of a single Animation.
+ var animations = e.getAnimations();
+ is(animations.length, 1, "getAnimations().length after animation start");
+
+ // Wait for the single MutationRecord for the Animation addition to
+ // be delivered.
+ yield waitForFrame();
+ assert_records([{ added: animations, changed: [], removed: [] }],
+ "records after animation start");
+
+ // Advance until near the end of the animation, then wait for it to finish.
+ animations[0].currentTime = 99900;
+ yield animationEnd;
+
+ // The only MutationRecord at this point should be the change from
+ // seeking the Animation.
+ assert_records([{ added: [], changed: animations, removed: [] }],
+ "records after animation starts filling");
+
+ // Cancel the animation by setting animation-fill-mode.
+ e.style.animationFillMode = "none";
+
+ // Wait for the single MutationRecord for the Animation removal to
+ // be delivered.
+ yield waitForFrame();
+ assert_records([{ added: [], changed: [], removed: animations }],
+ "records after animation end");
+
+ e.style = "";
+ });
+
+ // Test that starting a single animation that is cancelled by updating
+ // the animation-iteration-count property dispatches an added notification
+ // and then a removed notification.
+ addAsyncAnimTest("single_animation_cancelled_iteration_count",
+ aOptions, function*() {
+ // Start a short, repeated animation.
+ e.style = "animation: anim 0.5s infinite;";
+
+ // The animation should cause the creation of a single Animation.
+ var animations = e.getAnimations();
+ is(animations.length, 1, "getAnimations().length after animation start");
+
+ // Wait for the single MutationRecord for the Animation addition to
+ // be delivered.
+ yield waitForFrame();
+ assert_records([{ added: animations, changed: [], removed: [] }],
+ "records after animation start");
+
+ // Advance the animation until we are past the first iteration.
+ animations[0].currentTime += 1 * MS_PER_SEC;
+
+ // The only MutationRecord at this point should be the change from
+ // seeking the Animation.
+ yield waitForFrame();
+ assert_records([{ added: [], changed: animations, removed: [] }],
+ "records after seeking animations");
+
+ // Cancel the animation by setting animation-iteration-count.
+ e.style.animationIterationCount = "1";
+
+ // Wait for the single MutationRecord for the Animation removal to
+ // be delivered.
+ yield waitForFrame();
+ assert_records([{ added: [], changed: [], removed: animations }],
+ "records after animation end");
+
+ e.style = "";
+ });
+
+ // Test that updating an animation property dispatches a changed notification.
+ [
+ { name: "duration", prop: "animationDuration", val: "200s" },
+ { name: "timing", prop: "animationTimingFunction", val: "linear" },
+ { name: "iteration", prop: "animationIterationCount", val: "2" },
+ { name: "direction", prop: "animationDirection", val: "reverse" },
+ { name: "state", prop: "animationPlayState", val: "paused" },
+ { name: "delay", prop: "animationDelay", val: "-1s" },
+ { name: "fill", prop: "animationFillMode", val: "both" },
+ ].forEach(function(aChangeTest) {
+ addAsyncAnimTest(`single_animation_change_${aChangeTest.name}`, aOptions, function*() {
+ // Start a long animation.
+ e.style = "animation: anim 100s;";
+
+ // The animation should cause the creation of a single Animation.
+ var animations = e.getAnimations();
+ is(animations.length, 1, "getAnimations().length after animation start");
+
+ // Wait for the single MutationRecord for the Animation addition to
+ // be delivered.
+ yield waitForFrame();
+ assert_records([{ added: animations, changed: [], removed: [] }],
+ "records after animation start");
+
+ // Change a property of the animation such that it keeps running.
+ e.style[aChangeTest.prop] = aChangeTest.val;
+
+ // Wait for the single MutationRecord for the Animation change to
+ // be delivered.
+ yield waitForFrame();
+ assert_records([{ added: [], changed: animations, removed: [] }],
+ "records after animation change");
+
+ // Cancel the animation.
+ e.style.animationName = "none";
+
+ // Wait for the addition, change and removal MutationRecords to be delivered.
+ yield waitForFrame();
+ assert_records([{ added: [], changed: [], removed: animations }],
+ "records after animation end");
+
+ e.style = "";
+ });
+ });
+
+ // Test that calling finish() on a paused (but otherwise finished) animation
+ // dispatches a changed notification.
+ addAsyncAnimTest("finish_from_pause", aOptions, function*() {
+ // Start a long animation
+ e.style = "animation: anim 100s forwards";
+
+ // The animation should cause the creation of a single Animation.
+ var animations = e.getAnimations();
+ is(animations.length, 1, "getAnimations().length after animation start");
+
+ // Wait for the single MutationRecord for the Animation addition to
+ // be delivered.
+ yield waitForFrame();
+ assert_records([{ added: animations, changed: [], removed: [] }],
+ "records after animation start");
+
+ // Wait until the animation is playing.
+ yield animations[0].ready;
+
+ // Finish and pause.
+ animations[0].finish();
+ animations[0].pause();
+
+ // Wait for the pause to complete.
+ yield animations[0].ready;
+ is(animations[0].playState, "paused",
+ "playState after finishing and pausing");
+
+ // We should have two MutationRecords for the Animation changes:
+ // one for the finish, one for the pause.
+ assert_records([{ added: [], changed: animations, removed: [] },
+ { added: [], changed: animations, removed: [] }],
+ "records after finish() and pause()");
+
+ // Call finish() again.
+ animations[0].finish();
+ is(animations[0].playState, "finished",
+ "playState after finishing from paused state");
+
+ // Wait for the single MutationRecord for the Animation change to
+ // be delivered. Even though the currentTime does not change, the
+ // playState will change.
+ yield waitForFrame();
+ assert_records([{ added: [], changed: animations, removed: [] }],
+ "records after finish() and pause()");
+
+ // Cancel the animation.
+ e.style = "";
+
+ // Wait for the single removal notification.
+ yield waitForFrame();
+ assert_records([{ added: [], changed: [], removed: animations }],
+ "records after animation end");
+ });
+
+ // Test that calling finish() on a pause-pending (but otherwise finished)
+ // animation dispatches a changed notification.
+ addAsyncAnimTest("finish_from_pause_pending", aOptions, function*() {
+ // Start a long animation
+ e.style = "animation: anim 100s forwards";
+
+ // The animation should cause the creation of a single Animation.
+ var animations = e.getAnimations();
+ is(animations.length, 1, "getAnimations().length after animation start");
+
+ // Wait for the single MutationRecord for the Animation addition to
+ // be delivered.
+ yield waitForFrame();
+ assert_records([{ added: animations, changed: [], removed: [] }],
+ "records after animation start");
+
+ // Wait until the animation is playing.
+ yield animations[0].ready;
+
+ // Finish and pause.
+ animations[0].finish();
+ animations[0].pause();
+ is(animations[0].playState, "pending",
+ "playState after finishing and calling pause()");
+
+ // Call finish() again to abort the pause
+ animations[0].finish();
+ is(animations[0].playState, "finished",
+ "playState after finishing and calling pause()");
+
+ // Wait for three MutationRecords for the Animation changes to
+ // be delivered: one for each finish(), pause(), finish() operation.
+ yield waitForFrame();
+ assert_records([{ added: [], changed: animations, removed: [] },
+ { added: [], changed: animations, removed: [] },
+ { added: [], changed: animations, removed: [] }],
+ "records after finish(), pause(), finish()");
+
+ // Cancel the animation.
+ e.style = "";
+
+ // Wait for the single removal notification.
+ yield waitForFrame();
+ assert_records([{ added: [], changed: [], removed: animations }],
+ "records after animation end");
+ });
+
+ // Test that calling play() on a paused Animation dispatches a changed
+ // notification.
+ addAsyncAnimTest("play", aOptions, function*() {
+ // Start a long, paused animation
+ e.style = "animation: anim 100s paused";
+
+ // The animation should cause the creation of a single Animation.
+ var animations = e.getAnimations();
+ is(animations.length, 1, "getAnimations().length after animation start");
+
+ // Wait for the single MutationRecord for the Animation addition to
+ // be delivered.
+ yield waitForFrame();
+ assert_records([{ added: animations, changed: [], removed: [] }],
+ "records after animation start");
+
+ // Wait until the animation is ready
+ yield animations[0].ready;
+
+ // Play
+ animations[0].play();
+
+ // Wait for the single MutationRecord for the Animation change to
+ // be delivered.
+ yield animations[0].ready;
+ assert_records([{ added: [], changed: animations, removed: [] }],
+ "records after play()");
+
+ // Redundant play
+ animations[0].play();
+
+ // Wait to ensure no change is dispatched
+ yield waitForFrame();
+ assert_records([], "records after redundant play()");
+
+ // Cancel the animation.
+ e.style = "";
+
+ // Wait for the single removal notification.
+ yield waitForFrame();
+ assert_records([{ added: [], changed: [], removed: animations }],
+ "records after animation end");
+ });
+
+ // Test that calling play() on a finished Animation that fills forwards
+ // dispatches a changed notification.
+ addAsyncAnimTest("play_filling_forwards", aOptions, function*() {
+ // Start a long animation with a forwards fill
+ e.style = "animation: anim 100s forwards";
+
+ // The animation should cause the creation of a single Animation.
+ var animations = e.getAnimations();
+ is(animations.length, 1, "getAnimations().length after animation start");
+
+ // Wait for the single MutationRecord for the Animation addition to
+ // be delivered.
+ yield waitForFrame();
+ assert_records([{ added: animations, changed: [], removed: [] }],
+ "records after animation start");
+
+ // Wait until the animation is ready
+ yield animations[0].ready;
+
+ // Seek to the end
+ animations[0].finish();
+
+ // Wait for the single MutationRecord for the Animation change to
+ // be delivered.
+ yield waitForFrame();
+ assert_records([{ added: [], changed: animations, removed: [] }],
+ "records after finish()");
+
+ // Since we are filling forwards, calling play() should produce a
+ // change record since the animation remains relevant.
+ animations[0].play();
+
+ // Wait for the single MutationRecord for the Animation change to
+ // be delivered.
+ yield waitForFrame();
+ assert_records([{ added: [], changed: animations, removed: [] }],
+ "records after play()");
+
+ // Cancel the animation.
+ e.style = "";
+
+ // Wait for the single removal notification.
+ yield waitForFrame();
+ assert_records([{ added: [], changed: [], removed: animations }],
+ "records after animation end");
+ });
+
+ // Test that calling play() on a finished Animation that does *not* fill
+ // forwards dispatches an addition notification.
+ addAsyncAnimTest("play_after_finish", aOptions, function*() {
+ // Start a long animation
+ e.style = "animation: anim 100s";
+
+ // The animation should cause the creation of a single Animation.
+ var animations = e.getAnimations();
+ is(animations.length, 1, "getAnimations().length after animation start");
+
+ // Wait for the single MutationRecord for the Animation addition to
+ // be delivered.
+ yield waitForFrame();
+ assert_records([{ added: animations, changed: [], removed: [] }],
+ "records after animation start");
+
+ // Wait until the animation is ready
+ yield animations[0].ready;
+
+ // Seek to the end
+ animations[0].finish();
+
+ // Wait for the single MutationRecord for the Animation removal to
+ // be delivered.
+ yield waitForFrame();
+ assert_records([{ added: [], changed: [], removed: animations }],
+ "records after finish()");
+
+ // Since we are *not* filling forwards, calling play() is equivalent
+ // to creating a new animation since it becomes relevant again.
+ animations[0].play();
+
+ // Wait for the single MutationRecord for the Animation addition to
+ // be delivered.
+ yield waitForFrame();
+ assert_records([{ added: animations, changed: [], removed: [] }],
+ "records after play()");
+
+ // Cancel the animation.
+ e.style = "";
+
+ // Wait for the single removal notification.
+ yield waitForFrame();
+ assert_records([{ added: [], changed: [], removed: animations }],
+ "records after animation end");
+ });
+
+ // Test that calling pause() on an Animation dispatches a changed
+ // notification.
+ addAsyncAnimTest("pause", aOptions, function*() {
+ // Start a long animation
+ e.style = "animation: anim 100s";
+
+ // The animation should cause the creation of a single Animation.
+ var animations = e.getAnimations();
+ is(animations.length, 1, "getAnimations().length after animation start");
+
+ // Wait for the single MutationRecord for the Animation addition to
+ // be delivered.
+ yield waitForFrame();
+ assert_records([{ added: animations, changed: [], removed: [] }],
+ "records after animation start");
+
+ // Wait until the animation is ready
+ yield animations[0].ready;
+
+ // Pause
+ animations[0].pause();
+
+ // Wait for the single MutationRecord for the Animation change to
+ // be delivered.
+ yield animations[0].ready;
+ assert_records([{ added: [], changed: animations, removed: [] }],
+ "records after pause()");
+
+ // Redundant pause
+ animations[0].pause();
+
+ // Wait to ensure no change is dispatched
+ yield animations[0].ready;
+ assert_records([], "records after redundant pause()");
+
+ // Cancel the animation.
+ e.style = "";
+
+ // Wait for the single removal notification.
+ yield waitForFrame();
+ assert_records([{ added: [], changed: [], removed: animations }],
+ "records after animation end");
+ });
+
+ // Test that calling pause() on an Animation that is pause-pending
+ // does not dispatch an additional changed notification.
+ addAsyncAnimTest("pause_while_pause_pending", aOptions, function*() {
+ // Start a long animation
+ e.style = "animation: anim 100s";
+
+ // The animation should cause the creation of a single Animation.
+ var animations = e.getAnimations();
+ is(animations.length, 1, "getAnimations().length after animation start");
+
+ // Wait for the single MutationRecord for the Animation addition to
+ // be delivered.
+ yield waitForFrame();
+ assert_records([{ added: animations, changed: [], removed: [] }],
+ "records after animation start");
+
+ // Wait until the animation is ready
+ yield animations[0].ready;
+
+ // Pause
+ animations[0].pause();
+
+ // We are now pause-pending, but pause again
+ animations[0].pause();
+
+ // We should only get a single change record
+ yield animations[0].ready;
+ assert_records([{ added: [], changed: animations, removed: [] }],
+ "records after pause()");
+
+ // Cancel the animation.
+ e.style = "";
+
+ // Wait for the single removal notification.
+ yield waitForFrame();
+ assert_records([{ added: [], changed: [], removed: animations }],
+ "records after animation end");
+ });
+
+ // Test that calling play() on an Animation that is pause-pending
+ // dispatches a changed notification.
+ addAsyncAnimTest("aborted_pause", aOptions, function*() {
+ // Start a long animation
+ e.style = "animation: anim 100s";
+
+ // The animation should cause the creation of a single Animation.
+ var animations = e.getAnimations();
+ is(animations.length, 1, "getAnimations().length after animation start");
+
+ // Wait for the single MutationRecord for the Animation addition to
+ // be delivered.
+ yield waitForFrame();
+ assert_records([{ added: animations, changed: [], removed: [] }],
+ "records after animation start");
+
+ // Wait until the animation is ready
+ yield animations[0].ready;
+
+ // Pause
+ animations[0].pause();
+
+ // We are now pause-pending. If we play() now, we will abort the pause
+ animations[0].play();
+
+ // We should get two change records
+ yield animations[0].ready;
+ assert_records([{ added: [], changed: animations, removed: [] },
+ { added: [], changed: animations, removed: [] }],
+ "records after aborting a pause()");
+
+ // Cancel the animation.
+ e.style = "";
+
+ // Wait for the single removal notification.
+ yield waitForFrame();
+ assert_records([{ added: [], changed: [], removed: animations }],
+ "records after animation end");
+ });
+
+ // Test that a non-cancelling change to an animation followed immediately by a
+ // cancelling change will only send an animation removal notification.
+ addAsyncAnimTest("coalesce_change_cancel", aOptions, function*() {
+ // Start a long animation.
+ e.style = "animation: anim 100s;";
+
+ // The animation should cause the creation of a single Animation.
+ var animations = e.getAnimations();
+ is(animations.length, 1, "getAnimations().length after animation start");
+
+ // Wait for the single MutationRecord for the Animation addition to
+ // be delivered.
+ yield waitForFrame();
+ assert_records([{ added: animations, changed: [], removed: [] }],
+ "records after animation start");
+
+ // Update the animation's delay such that it is still running.
+ e.style.animationDelay = "-1s";
+
+ // Then cancel the animation by updating its duration.
+ e.style.animationDuration = "0.5s";
+
+ // We should get a single removal notification.
+ yield waitForFrame();
+ assert_records([{ added: [], changed: [], removed: animations }],
+ "records after animation end");
+
+ e.style = "";
+ });
+
+});
+
+addAsyncAnimTest("tree_ordering", { observe: div, subtree: true }, function*() {
+ // Add style for pseudo elements
+ var extraStyle = document.createElement('style');
+ document.head.appendChild(extraStyle);
+ var sheet = extraStyle.sheet;
+ var rules = { ".before::before": "animation: anim 100s;",
+ ".after::after" : "animation: anim 100s, anim 100s;" };
+ for (var selector in rules) {
+ sheet.insertRule(selector + '{' + rules[selector] + '}',
+ sheet.cssRules.length);
+ }
+
+ // Create a tree with two children:
+ //
+ // div
+ // (::before)
+ // (::after)
+ // / \
+ // childA childB(::before)
+ var childA = document.createElement("div");
+ var childB = document.createElement("div");
+
+ div.appendChild(childA);
+ div.appendChild(childB);
+
+ // Start an animation on each (using order: childB, div, childA)
+ //
+ // We include multiple animations on some nodes so that we can test batching
+ // works as expected later in this test.
+ childB.style = "animation: anim 100s";
+ div.style = "animation: anim 100s, anim 100s, anim 100s";
+ childA.style = "animation: anim 100s, anim 100s";
+
+ // Start animations targeting to pseudo element of div and childB.
+ childB.classList.add("before");
+ div.classList.add("after");
+ div.classList.add("before");
+
+ // Check all animations we have in this document
+ var docAnims = document.getAnimations();
+ is(docAnims.length, 10, "total animations");
+
+ var divAnimations = div.getAnimations();
+ var childAAnimations = childA.getAnimations();
+ var childBAnimations = childB.getAnimations();
+ var divBeforeAnimations =
+ [ for (x of docAnims) if (x.effect.target.parentElement == div &&
+ x.effect.target.type == "::before") x ];
+ var divAfterAnimations =
+ [ for (x of docAnims) if (x.effect.target.parentElement == div &&
+ x.effect.target.type == "::after") x ];
+ var childBPseudoAnimations =
+ [ for (x of docAnims) if (x.effect.target.parentElement == childB) x ];
+
+ // The order in which we get the corresponding records is currently
+ // based on the order we visit these nodes when updating styles.
+ //
+ // That is because we don't do any document-level batching of animation
+ // mutation records when we flush styles. We may introduce that in the
+ // future but for now all we are interested in testing here is that the order
+ // these records are dispatched is consistent between runs.
+ //
+ // We currently expect to get records in order div::after, childA, childB,
+ // childB::before, div, div::before
+ yield waitForFrame();
+ assert_records([{ added: divAfterAnimations, changed: [], removed: [] },
+ { added: childAAnimations, changed: [], removed: [] },
+ { added: childBAnimations, changed: [], removed: [] },
+ { added: childBPseudoAnimations, changed: [], removed: [] },
+ { added: divAnimations, changed: [], removed: [] },
+ { added: divBeforeAnimations, changed: [], removed: [] }],
+ "records after simultaneous animation start");
+
+ // The one case where we *do* currently perform document-level (or actually
+ // timeline-level) batching is when animations are updated from a refresh
+ // driver tick. In particular, this means that when animations finish
+ // naturally the removed records should be dispatched according to the
+ // position of the elements in the tree.
+
+ // First, flatten the set of animations. we put the animations targeting to
+ // pseudo elements last. (Actually, we don't care the order in the list.)
+ var animations = [ ...divAnimations,
+ ...childAAnimations,
+ ...childBAnimations,
+ ...divBeforeAnimations,
+ ...divAfterAnimations,
+ ...childBPseudoAnimations ];
+
+ // Fast-forward to *just* before the end of the animation.
+ animations.forEach(animation => animation.currentTime = 99999);
+
+ // Prepare the set of expected change MutationRecords, one for each
+ // animation that was seeked.
+ var seekRecords = animations.map(
+ p => ({ added: [], changed: [p], removed: [] })
+ );
+
+ yield await_event(div, "animationend");
+
+ // After the changed notifications, which will be dispatched in the order that
+ // the animations were seeked, we should get removal MutationRecords in order
+ // (div, div::before, div::after), childA, (childB, childB::before).
+ // Note: The animations targeting to the pseudo element are appended after
+ // the animations of its parent element.
+ divAnimations = [ ...divAnimations,
+ ...divBeforeAnimations,
+ ...divAfterAnimations ];
+ childBAnimations = [ ...childBAnimations, ...childBPseudoAnimations ];
+ assert_records(seekRecords.concat(
+ { added: [], changed: [], removed: divAnimations },
+ { added: [], changed: [], removed: childAAnimations },
+ { added: [], changed: [], removed: childBAnimations }),
+ "records after finishing");
+
+ // Clean up
+ div.classList.remove("before");
+ div.classList.remove("after");
+ div.style = "";
+ childA.remove();
+ childB.remove();
+ extraStyle.remove();
+});
+
+// Run the tests.
+SimpleTest.requestLongerTimeout(2);
+SimpleTest.waitForExplicitFinish();
+
+runAllAsyncTests().then(function() {
+ SimpleTest.finish();
+}, function(aError) {
+ ok(false, "Something failed: " + aError);
+});
+</script>
diff --git a/dom/animation/test/chrome/test_animation_performance_warning.html b/dom/animation/test/chrome/test_animation_performance_warning.html
new file mode 100644
index 000000000..a3bd63efc
--- /dev/null
+++ b/dom/animation/test/chrome/test_animation_performance_warning.html
@@ -0,0 +1,957 @@
+<!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>
diff --git a/dom/animation/test/chrome/test_animation_properties.html b/dom/animation/test/chrome/test_animation_properties.html
new file mode 100644
index 000000000..534901306
--- /dev/null
+++ b/dom/animation/test/chrome/test_animation_properties.html
@@ -0,0 +1,993 @@
+<!doctype html>
+<head>
+<meta charset=utf-8>
+<title>Bug 1254419 - Test the values returned by
+ KeyframeEffectReadOnly.getProperties()</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>
+</head>
+<body>
+<a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1254419"
+ target="_blank">Mozilla Bug 1254419</a>
+<div id="log"></div>
+<style>
+
+:root {
+ --var-100px: 100px;
+ --var-100px-200px: 100px 200px;
+}
+div {
+ font-size: 10px; /* For calculating em-based units */
+}
+</style>
+<script>
+'use strict';
+
+function assert_properties_equal(actual, expected) {
+ assert_equals(actual.length, expected.length);
+
+ var compareProperties = (a, b) =>
+ a.property == b.property ? 0 : (a.property < b.property ? -1 : 1);
+
+ var sortedActual = actual.sort(compareProperties);
+ var sortedExpected = expected.sort(compareProperties);
+
+ // We want to serialize the values in the following form:
+ //
+ // { offset: 0, easing: linear, composite: replace, value: 5px }, ...
+ //
+ // So that we can just compare strings and, in the failure case,
+ // easily see where the differences lie.
+ var serializeMember = value => {
+ return typeof value === 'undefined' ? '<not set>' : value;
+ }
+ var serializeValues = values =>
+ values.map(value =>
+ '{ ' +
+ [ 'offset', 'value', 'easing', 'composite' ].map(
+ member => `${member}: ${serializeMember(value[member])}`
+ ).join(', ') +
+ ' }')
+ .join(', ');
+
+ for (var i = 0; i < sortedActual.length; i++) {
+ assert_equals(sortedActual[i].property,
+ sortedExpected[i].property,
+ 'CSS property name should match');
+ assert_equals(serializeValues(sortedActual[i].values),
+ serializeValues(sortedExpected[i].values),
+ `Values arrays do not match for `
+ + `${sortedActual[i].property} property`);
+ }
+}
+
+// Shorthand for constructing a value object
+function value(offset, value, composite, easing) {
+ return { offset: offset, value: value, easing: easing, composite: composite };
+}
+
+var gTests = [
+
+ // ---------------------------------------------------------------------
+ //
+ // Tests for property-indexed specifications
+ //
+ // ---------------------------------------------------------------------
+
+ { desc: 'a one-property two-value property-indexed specification',
+ frames: { left: ['10px', '20px'] },
+ expected: [ { property: 'left',
+ values: [ value(0, '10px', 'replace', 'linear'),
+ value(1, '20px', 'replace') ] } ]
+ },
+ { desc: 'a one-shorthand-property two-value property-indexed'
+ + ' specification',
+ frames: { margin: ['10px', '10px 20px 30px 40px'] },
+ expected: [ { property: 'margin-top',
+ values: [ value(0, '10px', 'replace', 'linear'),
+ value(1, '10px', 'replace') ] },
+ { property: 'margin-right',
+ values: [ value(0, '10px', 'replace', 'linear'),
+ value(1, '20px', 'replace') ] },
+ { property: 'margin-bottom',
+ values: [ value(0, '10px', 'replace', 'linear'),
+ value(1, '30px', 'replace') ] },
+ { property: 'margin-left',
+ values: [ value(0, '10px', 'replace', 'linear'),
+ value(1, '40px', 'replace') ] } ]
+ },
+ { desc: 'a two-property (one shorthand and one of its longhand'
+ + ' components) two-value property-indexed specification',
+ frames: { marginTop: ['50px', '60px'],
+ margin: ['10px', '10px 20px 30px 40px'] },
+ expected: [ { property: 'margin-top',
+ values: [ value(0, '50px', 'replace', 'linear'),
+ value(1, '60px', 'replace') ] },
+ { property: 'margin-right',
+ values: [ value(0, '10px', 'replace', 'linear'),
+ value(1, '20px', 'replace') ] },
+ { property: 'margin-bottom',
+ values: [ value(0, '10px', 'replace', 'linear'),
+ value(1, '30px', 'replace') ] },
+ { property: 'margin-left',
+ values: [ value(0, '10px', 'replace', 'linear'),
+ value(1, '40px', 'replace') ] } ]
+ },
+ { desc: 'a two-property property-indexed specification with different'
+ + ' numbers of values',
+ frames: { left: ['10px', '20px', '30px'],
+ top: ['40px', '50px'] },
+ expected: [ { property: 'left',
+ values: [ value(0, '10px', 'replace', 'linear'),
+ value(0.5, '20px', 'replace', 'linear'),
+ value(1, '30px', 'replace') ] },
+ { property: 'top',
+ values: [ value(0, '40px', 'replace', 'linear'),
+ value(1, '50px', 'replace') ] } ]
+ },
+ { desc: 'a property-indexed specification with an invalid value',
+ frames: { left: ['10px', '20px', '30px', '40px', '50px'],
+ top: ['15px', '25px', 'invalid', '45px', '55px'] },
+ expected: [ { property: 'left',
+ values: [ value(0, '10px', 'replace', 'linear'),
+ value(0.25, '20px', 'replace', 'linear'),
+ value(0.5, '30px', 'replace', 'linear'),
+ value(0.75, '40px', 'replace', 'linear'),
+ value(1, '50px', 'replace') ] },
+ { property: 'top',
+ values: [ value(0, '15px', 'replace', 'linear'),
+ value(0.25, '25px', 'replace', 'linear'),
+ value(0.75, '45px', 'replace', 'linear'),
+ value(1, '55px', 'replace') ] } ]
+ },
+ { desc: 'a one-property two-value property-indexed specification that'
+ + ' needs to stringify its values',
+ frames: { opacity: [0, 1] },
+ expected: [ { property: 'opacity',
+ values: [ value(0, '0', 'replace', 'linear'),
+ value(1, '1', 'replace') ] } ]
+ },
+ { desc: 'a property-indexed keyframe where a lesser shorthand precedes'
+ + ' a greater shorthand',
+ frames: { borderLeft: [ '1px solid rgb(1, 2, 3)',
+ '2px solid rgb(4, 5, 6)' ],
+ border: [ '3px dotted rgb(7, 8, 9)',
+ '4px dashed rgb(10, 11, 12)' ] },
+ expected: [ { property: 'border-bottom-color',
+ values: [ value(0, 'rgb(7, 8, 9)', 'replace', 'linear'),
+ value(1, 'rgb(10, 11, 12)', 'replace') ] },
+ { property: 'border-left-color',
+ values: [ value(0, 'rgb(1, 2, 3)', 'replace', 'linear'),
+ value(1, 'rgb(4, 5, 6)', 'replace') ] },
+ { property: 'border-right-color',
+ values: [ value(0, 'rgb(7, 8, 9)', 'replace', 'linear'),
+ value(1, 'rgb(10, 11, 12)', 'replace') ] },
+ { property: 'border-top-color',
+ values: [ value(0, 'rgb(7, 8, 9)', 'replace', 'linear'),
+ value(1, 'rgb(10, 11, 12)', 'replace') ] },
+ { property: 'border-bottom-width',
+ values: [ value(0, '3px', 'replace', 'linear'),
+ value(1, '4px', 'replace') ] },
+ { property: 'border-left-width',
+ values: [ value(0, '1px', 'replace', 'linear'),
+ value(1, '2px', 'replace') ] },
+ { property: 'border-right-width',
+ values: [ value(0, '3px', 'replace', 'linear'),
+ value(1, '4px', 'replace') ] },
+ { property: 'border-top-width',
+ values: [ value(0, '3px', 'replace', 'linear'),
+ value(1, '4px', 'replace') ] },
+ { property: 'border-bottom-style',
+ values: [ value(0, 'dotted', 'replace', 'linear'),
+ value(1, 'dashed', 'replace') ] },
+ { property: 'border-left-style',
+ values: [ value(0, 'solid', 'replace', 'linear'),
+ value(1, 'solid', 'replace') ] },
+ { property: 'border-right-style',
+ values: [ value(0, 'dotted', 'replace', 'linear'),
+ value(1, 'dashed', 'replace') ] },
+ { property: 'border-top-style',
+ values: [ value(0, 'dotted', 'replace', 'linear'),
+ value(1, 'dashed', 'replace') ] },
+ { property: 'border-image-outset',
+ values: [ value(0, '0 0 0 0', 'replace', 'linear'),
+ value(1, '0 0 0 0', 'replace') ] },
+ { property: 'border-image-repeat',
+ values: [ value(0, 'stretch stretch', 'replace', 'linear'),
+ value(1, 'stretch stretch', 'replace') ] },
+ { property: 'border-image-slice',
+ values: [ value(0, '100% 100% 100% 100%',
+ 'replace', 'linear'),
+ value(1, '100% 100% 100% 100%', 'replace') ] },
+ { property: 'border-image-source',
+ values: [ value(0, 'none', 'replace', 'linear'),
+ value(1, 'none', 'replace') ] },
+ { property: 'border-image-width',
+ values: [ value(0, '1 1 1 1', 'replace', 'linear'),
+ value(1, '1 1 1 1', 'replace') ] },
+ { property: '-moz-border-bottom-colors',
+ values: [ value(0, 'none', 'replace', 'linear'),
+ value(1, 'none', 'replace') ] },
+ { property: '-moz-border-left-colors',
+ values: [ value(0, 'none', 'replace', 'linear'),
+ value(1, 'none', 'replace') ] },
+ { property: '-moz-border-right-colors',
+ values: [ value(0, 'none', 'replace', 'linear'),
+ value(1, 'none', 'replace') ] },
+ { property: '-moz-border-top-colors',
+ values: [ value(0, 'none', 'replace', 'linear'),
+ value(1, 'none', 'replace') ] } ]
+ },
+ { desc: 'a property-indexed keyframe where a greater shorthand precedes'
+ + ' a lesser shorthand',
+ frames: { border: [ '3px dotted rgb(7, 8, 9)',
+ '4px dashed rgb(10, 11, 12)' ],
+ borderLeft: [ '1px solid rgb(1, 2, 3)',
+ '2px solid rgb(4, 5, 6)' ] },
+ expected: [ { property: 'border-bottom-color',
+ values: [ value(0, 'rgb(7, 8, 9)', 'replace', 'linear'),
+ value(1, 'rgb(10, 11, 12)', 'replace') ] },
+ { property: 'border-left-color',
+ values: [ value(0, 'rgb(1, 2, 3)', 'replace', 'linear'),
+ value(1, 'rgb(4, 5, 6)', 'replace') ] },
+ { property: 'border-right-color',
+ values: [ value(0, 'rgb(7, 8, 9)', 'replace', 'linear'),
+ value(1, 'rgb(10, 11, 12)', 'replace') ] },
+ { property: 'border-top-color',
+ values: [ value(0, 'rgb(7, 8, 9)', 'replace', 'linear'),
+ value(1, 'rgb(10, 11, 12)', 'replace') ] },
+ { property: 'border-bottom-width',
+ values: [ value(0, '3px', 'replace', 'linear'),
+ value(1, '4px', 'replace') ] },
+ { property: 'border-left-width',
+ values: [ value(0, '1px', 'replace', 'linear'),
+ value(1, '2px', 'replace') ] },
+ { property: 'border-right-width',
+ values: [ value(0, '3px', 'replace', 'linear'),
+ value(1, '4px', 'replace') ] },
+ { property: 'border-top-width',
+ values: [ value(0, '3px', 'replace', 'linear'),
+ value(1, '4px', 'replace') ] },
+ { property: 'border-bottom-style',
+ values: [ value(0, 'dotted', 'replace', 'linear'),
+ value(1, 'dashed', 'replace') ] },
+ { property: 'border-left-style',
+ values: [ value(0, 'solid', 'replace', 'linear'),
+ value(1, 'solid', 'replace') ] },
+ { property: 'border-right-style',
+ values: [ value(0, 'dotted', 'replace', 'linear'),
+ value(1, 'dashed', 'replace') ] },
+ { property: 'border-top-style',
+ values: [ value(0, 'dotted', 'replace', 'linear'),
+ value(1, 'dashed', 'replace') ] },
+ { property: 'border-image-outset',
+ values: [ value(0, '0 0 0 0', 'replace', 'linear'),
+ value(1, '0 0 0 0', 'replace') ] },
+ { property: 'border-image-repeat',
+ values: [ value(0, 'stretch stretch', 'replace', 'linear'),
+ value(1, 'stretch stretch', 'replace') ] },
+ { property: 'border-image-slice',
+ values: [ value(0, '100% 100% 100% 100%',
+ 'replace', 'linear'),
+ value(1, '100% 100% 100% 100%', 'replace') ] },
+ { property: 'border-image-source',
+ values: [ value(0, 'none', 'replace', 'linear'),
+ value(1, 'none', 'replace') ] },
+ { property: 'border-image-width',
+ values: [ value(0, '1 1 1 1', 'replace', 'linear'),
+ value(1, '1 1 1 1', 'replace') ] },
+ { property: '-moz-border-bottom-colors',
+ values: [ value(0, 'none', 'replace', 'linear'),
+ value(1, 'none', 'replace') ] },
+ { property: '-moz-border-left-colors',
+ values: [ value(0, 'none', 'replace', 'linear'),
+ value(1, 'none', 'replace') ] },
+ { property: '-moz-border-right-colors',
+ values: [ value(0, 'none', 'replace', 'linear'),
+ value(1, 'none', 'replace') ] },
+ { property: '-moz-border-top-colors',
+ values: [ value(0, 'none', 'replace', 'linear'),
+ value(1, 'none', 'replace') ] } ]
+ },
+
+ // ---------------------------------------------------------------------
+ //
+ // Tests for keyframe sequences
+ //
+ // ---------------------------------------------------------------------
+
+ { desc: 'a keyframe sequence specification with repeated values at'
+ + ' offset 0/1 with different easings',
+ frames: [ { offset: 0.0, left: '100px', easing: 'ease' },
+ { offset: 0.0, left: '200px', easing: 'ease' },
+ { offset: 0.5, left: '300px', easing: 'linear' },
+ { offset: 1.0, left: '400px', easing: 'ease-out' },
+ { offset: 1.0, left: '500px', easing: 'step-end' } ],
+ expected: [ { property: 'left',
+ values: [ value(0, '100px', 'replace'),
+ value(0, '200px', 'replace', 'ease'),
+ value(0.5, '300px', 'replace', 'linear'),
+ value(1, '400px', 'replace'),
+ value(1, '500px', 'replace') ] } ]
+ },
+ { desc: 'a one-property two-keyframe sequence',
+ frames: [ { offset: 0, left: '10px' },
+ { offset: 1, left: '20px' } ],
+ expected: [ { property: 'left',
+ values: [ value(0, '10px', 'replace', 'linear'),
+ value(1, '20px', 'replace') ] } ]
+ },
+ { desc: 'a two-property two-keyframe sequence',
+ frames: [ { offset: 0, left: '10px', top: '30px' },
+ { offset: 1, left: '20px', top: '40px' } ],
+ expected: [ { property: 'left',
+ values: [ value(0, '10px', 'replace', 'linear'),
+ value(1, '20px', 'replace') ] },
+ { property: 'top',
+ values: [ value(0, '30px', 'replace', 'linear'),
+ value(1, '40px', 'replace') ] } ]
+ },
+ { desc: 'a one shorthand property two-keyframe sequence',
+ frames: [ { offset: 0, margin: '10px' },
+ { offset: 1, margin: '20px 30px 40px 50px' } ],
+ expected: [ { property: 'margin-top',
+ values: [ value(0, '10px', 'replace', 'linear'),
+ value(1, '20px', 'replace') ] },
+ { property: 'margin-right',
+ values: [ value(0, '10px', 'replace', 'linear'),
+ value(1, '30px', 'replace') ] },
+ { property: 'margin-bottom',
+ values: [ value(0, '10px', 'replace', 'linear'),
+ value(1, '40px', 'replace') ] },
+ { property: 'margin-left',
+ values: [ value(0, '10px', 'replace', 'linear'),
+ value(1, '50px', 'replace') ] } ]
+ },
+ { desc: 'a two-property (a shorthand and one of its component longhands)'
+ + ' two-keyframe sequence',
+ frames: [ { offset: 0, margin: '10px', marginTop: '20px' },
+ { offset: 1, marginTop: '70px',
+ margin: '30px 40px 50px 60px' } ],
+ expected: [ { property: 'margin-top',
+ values: [ value(0, '20px', 'replace', 'linear'),
+ value(1, '70px', 'replace') ] },
+ { property: 'margin-right',
+ values: [ value(0, '10px', 'replace', 'linear'),
+ value(1, '40px', 'replace') ] },
+ { property: 'margin-bottom',
+ values: [ value(0, '10px', 'replace', 'linear'),
+ value(1, '50px', 'replace') ] },
+ { property: 'margin-left',
+ values: [ value(0, '10px', 'replace', 'linear'),
+ value(1, '60px', 'replace') ] } ]
+ },
+ { desc: 'a keyframe sequence with duplicate values for a given interior'
+ + ' offset',
+ frames: [ { offset: 0.0, left: '10px' },
+ { offset: 0.5, left: '20px' },
+ { offset: 0.5, left: '30px' },
+ { offset: 0.5, left: '40px' },
+ { offset: 1.0, left: '50px' } ],
+ expected: [ { property: 'left',
+ values: [ value(0, '10px', 'replace', 'linear'),
+ value(0.5, '20px', 'replace'),
+ value(0.5, '40px', 'replace', 'linear'),
+ value(1, '50px', 'replace') ] } ]
+ },
+ { desc: 'a keyframe sequence with duplicate values for offsets 0 and 1',
+ frames: [ { offset: 0, left: '10px' },
+ { offset: 0, left: '20px' },
+ { offset: 0, left: '30px' },
+ { offset: 1, left: '40px' },
+ { offset: 1, left: '50px' },
+ { offset: 1, left: '60px' } ],
+ expected: [ { property: 'left',
+ values: [ value(0, '10px', 'replace'),
+ value(0, '30px', 'replace', 'linear'),
+ value(1, '40px', 'replace'),
+ value(1, '60px', 'replace') ] } ]
+ },
+ { desc: 'a two-property four-keyframe sequence',
+ frames: [ { offset: 0, left: '10px' },
+ { offset: 0, top: '20px' },
+ { offset: 1, top: '30px' },
+ { offset: 1, left: '40px' } ],
+ expected: [ { property: 'left',
+ values: [ value(0, '10px', 'replace', 'linear'),
+ value(1, '40px', 'replace') ] },
+ { property: 'top',
+ values: [ value(0, '20px', 'replace', 'linear'),
+ value(1, '30px', 'replace') ] } ]
+ },
+ { desc: 'a one-property keyframe sequence with some omitted offsets',
+ frames: [ { offset: 0.00, left: '10px' },
+ { offset: 0.25, left: '20px' },
+ { left: '30px' },
+ { left: '40px' },
+ { offset: 1.00, left: '50px' } ],
+ expected: [ { property: 'left',
+ values: [ value(0, '10px', 'replace', 'linear'),
+ value(0.25, '20px', 'replace', 'linear'),
+ value(0.5, '30px', 'replace', 'linear'),
+ value(0.75, '40px', 'replace', 'linear'),
+ value(1, '50px', 'replace') ] } ]
+ },
+ { desc: 'a two-property keyframe sequence with some omitted offsets',
+ frames: [ { offset: 0.00, left: '10px', top: '20px' },
+ { offset: 0.25, left: '30px' },
+ { left: '40px' },
+ { left: '50px', top: '60px' },
+ { offset: 1.00, left: '70px', top: '80px' } ],
+ expected: [ { property: 'left',
+ values: [ value(0, '10px', 'replace', 'linear'),
+ value(0.25, '30px', 'replace', 'linear'),
+ value(0.5, '40px', 'replace', 'linear'),
+ value(0.75, '50px', 'replace', 'linear'),
+ value(1, '70px', 'replace') ] },
+ { property: 'top',
+ values: [ value(0, '20px', 'replace', 'linear'),
+ value(0.75, '60px', 'replace', 'linear'),
+ value(1, '80px', 'replace') ] } ]
+ },
+ { desc: 'a one-property keyframe sequence with all omitted offsets',
+ frames: [ { left: '10px' },
+ { left: '20px' },
+ { left: '30px' },
+ { left: '40px' },
+ { left: '50px' } ],
+ expected: [ { property: 'left',
+ values: [ value(0, '10px', 'replace', 'linear'),
+ value(0.25, '20px', 'replace', 'linear'),
+ value(0.5, '30px', 'replace', 'linear'),
+ value(0.75, '40px', 'replace', 'linear'),
+ value(1, '50px', 'replace') ] } ]
+ },
+ { desc: 'a keyframe sequence with different easing values, but the'
+ + ' same easing value for a given offset',
+ frames: [ { offset: 0.0, easing: 'ease', left: '10px'},
+ { offset: 0.0, easing: 'ease', top: '20px'},
+ { offset: 0.5, easing: 'linear', left: '30px' },
+ { offset: 0.5, easing: 'linear', top: '40px' },
+ { offset: 1.0, easing: 'step-end', left: '50px' },
+ { offset: 1.0, easing: 'step-end', top: '60px' } ],
+ expected: [ { property: 'left',
+ values: [ value(0, '10px', 'replace', 'ease'),
+ value(0.5, '30px', 'replace', 'linear'),
+ value(1, '50px', 'replace') ] },
+ { property: 'top',
+ values: [ value(0, '20px', 'replace', 'ease'),
+ value(0.5, '40px', 'replace', 'linear'),
+ value(1, '60px', 'replace') ] } ]
+ },
+ { desc: 'a one-property two-keyframe sequence that needs to'
+ + ' stringify its values',
+ frames: [ { offset: 0, opacity: 0 },
+ { offset: 1, opacity: 1 } ],
+ expected: [ { property: 'opacity',
+ values: [ value(0, '0', 'replace', 'linear'),
+ value(1, '1', 'replace') ] } ]
+ },
+ { desc: 'a keyframe sequence where shorthand precedes longhand',
+ frames: [ { offset: 0, margin: '10px', marginRight: '20px' },
+ { offset: 1, margin: '30px' } ],
+ expected: [ { property: 'margin-top',
+ values: [ value(0, '10px', 'replace', 'linear'),
+ value(1, '30px', 'replace') ] },
+ { property: 'margin-right',
+ values: [ value(0, '20px', 'replace', 'linear'),
+ value(1, '30px', 'replace') ] },
+ { property: 'margin-bottom',
+ values: [ value(0, '10px', 'replace', 'linear'),
+ value(1, '30px', 'replace') ] },
+ { property: 'margin-left',
+ values: [ value(0, '10px', 'replace', 'linear'),
+ value(1, '30px', 'replace') ] } ]
+ },
+ { desc: 'a keyframe sequence where longhand precedes shorthand',
+ frames: [ { offset: 0, marginRight: '20px', margin: '10px' },
+ { offset: 1, margin: '30px' } ],
+ expected: [ { property: 'margin-top',
+ values: [ value(0, '10px', 'replace', 'linear'),
+ value(1, '30px', 'replace') ] },
+ { property: 'margin-right',
+ values: [ value(0, '20px', 'replace', 'linear'),
+ value(1, '30px', 'replace') ] },
+ { property: 'margin-bottom',
+ values: [ value(0, '10px', 'replace', 'linear'),
+ value(1, '30px', 'replace') ] },
+ { property: 'margin-left',
+ values: [ value(0, '10px', 'replace', 'linear'),
+ value(1, '30px', 'replace') ] } ]
+ },
+ { desc: 'a keyframe sequence where lesser shorthand precedes greater'
+ + ' shorthand',
+ frames: [ { offset: 0, borderLeft: '1px solid rgb(1, 2, 3)',
+ border: '2px dotted rgb(4, 5, 6)' },
+ { offset: 1, border: '3px dashed rgb(7, 8, 9)' } ],
+ expected: [ { property: 'border-bottom-color',
+ values: [ value(0, 'rgb(4, 5, 6)', 'replace', 'linear'),
+ value(1, 'rgb(7, 8, 9)', 'replace') ] },
+ { property: 'border-left-color',
+ values: [ value(0, 'rgb(1, 2, 3)', 'replace', 'linear'),
+ value(1, 'rgb(7, 8, 9)', 'replace') ] },
+ { property: 'border-right-color',
+ values: [ value(0, 'rgb(4, 5, 6)', 'replace', 'linear'),
+ value(1, 'rgb(7, 8, 9)', 'replace') ] },
+ { property: 'border-top-color',
+ values: [ value(0, 'rgb(4, 5, 6)', 'replace', 'linear'),
+ value(1, 'rgb(7, 8, 9)', 'replace') ] },
+ { property: 'border-bottom-width',
+ values: [ value(0, '2px', 'replace', 'linear'),
+ value(1, '3px', 'replace') ] },
+ { property: 'border-left-width',
+ values: [ value(0, '1px', 'replace', 'linear'),
+ value(1, '3px', 'replace') ] },
+ { property: 'border-right-width',
+ values: [ value(0, '2px', 'replace', 'linear'),
+ value(1, '3px', 'replace') ] },
+ { property: 'border-top-width',
+ values: [ value(0, '2px', 'replace', 'linear'),
+ value(1, '3px', 'replace') ] },
+ { property: 'border-bottom-style',
+ values: [ value(0, 'dotted', 'replace', 'linear'),
+ value(1, 'dashed', 'replace') ] },
+ { property: 'border-left-style',
+ values: [ value(0, 'solid', 'replace', 'linear'),
+ value(1, 'dashed', 'replace') ] },
+ { property: 'border-right-style',
+ values: [ value(0, 'dotted', 'replace', 'linear'),
+ value(1, 'dashed', 'replace') ] },
+ { property: 'border-top-style',
+ values: [ value(0, 'dotted', 'replace', 'linear'),
+ value(1, 'dashed', 'replace') ] },
+ { property: 'border-image-outset',
+ values: [ value(0, '0 0 0 0', 'replace', 'linear'),
+ value(1, '0 0 0 0', 'replace') ] },
+ { property: 'border-image-repeat',
+ values: [ value(0, 'stretch stretch', 'replace', 'linear'),
+ value(1, 'stretch stretch', 'replace') ] },
+ { property: 'border-image-slice',
+ values: [ value(0, '100% 100% 100% 100%',
+ 'replace', 'linear'),
+ value(1, '100% 100% 100% 100%', 'replace') ] },
+ { property: 'border-image-source',
+ values: [ value(0, 'none', 'replace', 'linear'),
+ value(1, 'none', 'replace') ] },
+ { property: 'border-image-width',
+ values: [ value(0, '1 1 1 1', 'replace', 'linear'),
+ value(1, '1 1 1 1', 'replace') ] },
+ { property: '-moz-border-bottom-colors',
+ values: [ value(0, 'none', 'replace', 'linear'),
+ value(1, 'none', 'replace') ] },
+ { property: '-moz-border-left-colors',
+ values: [ value(0, 'none', 'replace', 'linear'),
+ value(1, 'none', 'replace') ] },
+ { property: '-moz-border-right-colors',
+ values: [ value(0, 'none', 'replace', 'linear'),
+ value(1, 'none', 'replace') ] },
+ { property: '-moz-border-top-colors',
+ values: [ value(0, 'none', 'replace', 'linear'),
+ value(1, 'none', 'replace') ] } ]
+ },
+ { desc: 'a keyframe sequence where greater shorthand precedes'
+ + ' lesser shorthand',
+ frames: [ { offset: 0, border: '2px dotted rgb(4, 5, 6)',
+ borderLeft: '1px solid rgb(1, 2, 3)' },
+ { offset: 1, border: '3px dashed rgb(7, 8, 9)' } ],
+ expected: [ { property: 'border-bottom-color',
+ values: [ value(0, 'rgb(4, 5, 6)', 'replace', 'linear'),
+ value(1, 'rgb(7, 8, 9)', 'replace') ] },
+ { property: 'border-left-color',
+ values: [ value(0, 'rgb(1, 2, 3)', 'replace', 'linear'),
+ value(1, 'rgb(7, 8, 9)', 'replace') ] },
+ { property: 'border-right-color',
+ values: [ value(0, 'rgb(4, 5, 6)', 'replace', 'linear'),
+ value(1, 'rgb(7, 8, 9)', 'replace') ] },
+ { property: 'border-top-color',
+ values: [ value(0, 'rgb(4, 5, 6)', 'replace', 'linear'),
+ value(1, 'rgb(7, 8, 9)', 'replace') ] },
+ { property: 'border-bottom-width',
+ values: [ value(0, '2px', 'replace', 'linear'),
+ value(1, '3px', 'replace') ] },
+ { property: 'border-left-width',
+ values: [ value(0, '1px', 'replace', 'linear'),
+ value(1, '3px', 'replace') ] },
+ { property: 'border-right-width',
+ values: [ value(0, '2px', 'replace', 'linear'),
+ value(1, '3px', 'replace') ] },
+ { property: 'border-top-width',
+ values: [ value(0, '2px', 'replace', 'linear'),
+ value(1, '3px', 'replace') ] },
+ { property: 'border-bottom-style',
+ values: [ value(0, 'dotted', 'replace', 'linear'),
+ value(1, 'dashed', 'replace') ] },
+ { property: 'border-left-style',
+ values: [ value(0, 'solid', 'replace', 'linear'),
+ value(1, 'dashed', 'replace') ] },
+ { property: 'border-right-style',
+ values: [ value(0, 'dotted', 'replace', 'linear'),
+ value(1, 'dashed', 'replace') ] },
+ { property: 'border-top-style',
+ values: [ value(0, 'dotted', 'replace', 'linear'),
+ value(1, 'dashed', 'replace') ] },
+ { property: 'border-image-outset',
+ values: [ value(0, '0 0 0 0', 'replace', 'linear'),
+ value(1, '0 0 0 0', 'replace') ] },
+ { property: 'border-image-repeat',
+ values: [ value(0, 'stretch stretch', 'replace', 'linear'),
+ value(1, 'stretch stretch', 'replace') ] },
+ { property: 'border-image-slice',
+ values: [ value(0, '100% 100% 100% 100%',
+ 'replace', 'linear'),
+ value(1, '100% 100% 100% 100%', 'replace') ] },
+ { property: 'border-image-source',
+ values: [ value(0, 'none', 'replace', 'linear'),
+ value(1, 'none', 'replace') ] },
+ { property: 'border-image-width',
+ values: [ value(0, '1 1 1 1', 'replace', 'linear'),
+ value(1, '1 1 1 1', 'replace') ] },
+ { property: '-moz-border-bottom-colors',
+ values: [ value(0, 'none', 'replace', 'linear'),
+ value(1, 'none', 'replace') ] },
+ { property: '-moz-border-left-colors',
+ values: [ value(0, 'none', 'replace', 'linear'),
+ value(1, 'none', 'replace') ] },
+ { property: '-moz-border-right-colors',
+ values: [ value(0, 'none', 'replace', 'linear'),
+ value(1, 'none', 'replace') ] },
+ { property: '-moz-border-top-colors',
+ values: [ value(0, 'none', 'replace', 'linear'),
+ value(1, 'none', 'replace') ] } ]
+ },
+
+ // ---------------------------------------------------------------------
+ //
+ // Tests for unit conversion
+ //
+ // ---------------------------------------------------------------------
+
+ { desc: 'em units are resolved to px values',
+ frames: { left: ['10em', '20em'] },
+ expected: [ { property: 'left',
+ values: [ value(0, '100px', 'replace', 'linear'),
+ value(1, '200px', 'replace') ] } ]
+ },
+ { desc: 'calc() expressions are resolved to the equivalent units',
+ frames: { left: ['calc(10em + 10px)', 'calc(10em + 10%)'] },
+ expected: [ { property: 'left',
+ values: [ value(0, 'calc(110px)', 'replace', 'linear'),
+ value(1, 'calc(100px + 10%)', 'replace') ] } ]
+ },
+
+ // ---------------------------------------------------------------------
+ //
+ // Tests for CSS variable handling conversion
+ //
+ // ---------------------------------------------------------------------
+
+ { desc: 'CSS variables are resolved to their corresponding values',
+ frames: { left: ['10px', 'var(--var-100px)'] },
+ expected: [ { property: 'left',
+ values: [ value(0, '10px', 'replace', 'linear'),
+ value(1, '100px', 'replace') ] } ]
+ },
+ { desc: 'CSS variables in calc() expressions are resolved',
+ frames: { left: ['10px', 'calc(var(--var-100px) / 2 - 10%)'] },
+ expected: [ { property: 'left',
+ values: [ value(0, '10px', 'replace', 'linear'),
+ value(1, 'calc(50px + -10%)', 'replace') ] } ]
+ },
+ { desc: 'CSS variables in shorthands are resolved to their corresponding'
+ + ' values',
+ frames: { margin: ['10px', 'var(--var-100px-200px)'] },
+ expected: [ { property: 'margin-top',
+ values: [ value(0, '10px', 'replace', 'linear'),
+ value(1, '100px', 'replace') ] },
+ { property: 'margin-right',
+ values: [ value(0, '10px', 'replace', 'linear'),
+ value(1, '200px', 'replace') ] },
+ { property: 'margin-bottom',
+ values: [ value(0, '10px', 'replace', 'linear'),
+ value(1, '100px', 'replace') ] },
+ { property: 'margin-left',
+ values: [ value(0, '10px', 'replace', 'linear'),
+ value(1, '200px', 'replace') ] } ]
+ },
+
+ // ---------------------------------------------------------------------
+ //
+ // Tests for properties that parse correctly but which we fail to
+ // convert to computed values.
+ //
+ // ---------------------------------------------------------------------
+
+ { desc: 'a property that can\'t be resolved to computed values in'
+ + ' initial keyframe',
+ frames: [ { margin: '5px', simulateComputeValuesFailure: true },
+ { margin: '5px' } ],
+ expected: [ ]
+ },
+ { desc: 'a property that can\'t be resolved to computed values in'
+ + ' initial keyframe where we have enough values to create'
+ + ' a final segment',
+ frames: [ { margin: '5px', simulateComputeValuesFailure: true },
+ { margin: '5px' },
+ { margin: '5px' } ],
+ expected: [ ]
+ },
+ { desc: 'a property that can\'t be resolved to computed values in'
+ + ' initial overlapping keyframes (first in series of two)',
+ frames: [ { margin: '5px', offset: 0,
+ simulateComputeValuesFailure: true },
+ { margin: '5px', offset: 0 },
+ { margin: '5px' } ],
+ expected: [ { property: 'margin-top',
+ values: [ value(0, '5px', 'replace', 'linear'),
+ value(1, '5px', 'replace') ] },
+ { property: 'margin-right',
+ values: [ value(0, '5px', 'replace', 'linear'),
+ value(1, '5px', 'replace') ] },
+ { property: 'margin-bottom',
+ values: [ value(0, '5px', 'replace', 'linear'),
+ value(1, '5px', 'replace') ] },
+ { property: 'margin-left',
+ values: [ value(0, '5px', 'replace', 'linear'),
+ value(1, '5px', 'replace') ] } ]
+ },
+ { desc: 'a property that can\'t be resolved to computed values in'
+ + ' initial overlapping keyframes (second in series of two)',
+ frames: [ { margin: '5px', offset: 0 },
+ { margin: '5px', offset: 0,
+ simulateComputeValuesFailure: true },
+ { margin: '5px' } ],
+ expected: [ { property: 'margin-top',
+ values: [ value(0, '5px', 'replace', 'linear'),
+ value(1, '5px', 'replace') ] },
+ { property: 'margin-right',
+ values: [ value(0, '5px', 'replace', 'linear'),
+ value(1, '5px', 'replace') ] },
+ { property: 'margin-bottom',
+ values: [ value(0, '5px', 'replace', 'linear'),
+ value(1, '5px', 'replace') ] },
+ { property: 'margin-left',
+ values: [ value(0, '5px', 'replace', 'linear'),
+ value(1, '5px', 'replace') ] } ]
+ },
+ { desc: 'a property that can\'t be resolved to computed values in'
+ + ' initial overlapping keyframes (second in series of three)',
+ frames: [ { margin: '5px', offset: 0 },
+ { margin: '5px', offset: 0,
+ simulateComputeValuesFailure: true },
+ { margin: '5px', offset: 0 },
+ { margin: '5px' } ],
+ expected: [ { property: 'margin-top',
+ values: [ value(0, '5px', 'replace'),
+ value(0, '5px', 'replace', 'linear'),
+ value(1, '5px', 'replace') ] },
+ { property: 'margin-right',
+ values: [ value(0, '5px', 'replace'),
+ value(0, '5px', 'replace', 'linear'),
+ value(1, '5px', 'replace') ] },
+ { property: 'margin-bottom',
+ values: [ value(0, '5px', 'replace'),
+ value(0, '5px', 'replace', 'linear'),
+ value(1, '5px', 'replace') ] },
+ { property: 'margin-left',
+ values: [ value(0, '5px', 'replace'),
+ value(0, '5px', 'replace', 'linear'),
+ value(1, '5px', 'replace') ] } ]
+ },
+ { desc: 'a property that can\'t be resolved to computed values in'
+ + ' final keyframe',
+ frames: [ { margin: '5px' },
+ { margin: '5px', simulateComputeValuesFailure: true } ],
+ expected: [ ]
+ },
+ { desc: 'a property that can\'t be resolved to computed values in'
+ + ' final keyframe where it forms the last segment in the series',
+ frames: [ { margin: '5px' },
+ { margin: '5px',
+ marginLeft: '5px',
+ marginRight: '5px',
+ marginBottom: '5px',
+ // margin-top sorts last and only it will be missing since
+ // the other longhand components are specified
+ simulateComputeValuesFailure: true } ],
+ expected: [ { property: 'margin-bottom',
+ values: [ value(0, '5px', 'replace', 'linear'),
+ value(1, '5px', 'replace') ] },
+ { property: 'margin-left',
+ values: [ value(0, '5px', 'replace', 'linear'),
+ value(1, '5px', 'replace') ] },
+ { property: 'margin-right',
+ values: [ value(0, '5px', 'replace', 'linear'),
+ value(1, '5px', 'replace') ] } ]
+ },
+ { desc: 'a property that can\'t be resolved to computed values in'
+ + ' final keyframe where we have enough values to create'
+ + ' an initial segment',
+ frames: [ { margin: '5px' },
+ { margin: '5px' },
+ { margin: '5px', simulateComputeValuesFailure: true } ],
+ expected: [ ]
+ },
+ { desc: 'a property that can\'t be resolved to computed values in'
+ + ' final overlapping keyframes (first in series of two)',
+ frames: [ { margin: '5px' },
+ { margin: '5px', offset: 1,
+ simulateComputeValuesFailure: true },
+ { margin: '5px', offset: 1 } ],
+ expected: [ { property: 'margin-top',
+ values: [ value(0, '5px', 'replace', 'linear'),
+ value(1, '5px', 'replace') ] },
+ { property: 'margin-right',
+ values: [ value(0, '5px', 'replace', 'linear'),
+ value(1, '5px', 'replace') ] },
+ { property: 'margin-bottom',
+ values: [ value(0, '5px', 'replace', 'linear'),
+ value(1, '5px', 'replace') ] },
+ { property: 'margin-left',
+ values: [ value(0, '5px', 'replace', 'linear'),
+ value(1, '5px', 'replace') ] } ]
+ },
+ { desc: 'a property that can\'t be resolved to computed values in'
+ + ' final overlapping keyframes (second in series of two)',
+ frames: [ { margin: '5px' },
+ { margin: '5px', offset: 1 },
+ { margin: '5px', offset: 1,
+ simulateComputeValuesFailure: true } ],
+ expected: [ { property: 'margin-top',
+ values: [ value(0, '5px', 'replace', 'linear'),
+ value(1, '5px', 'replace') ] },
+ { property: 'margin-right',
+ values: [ value(0, '5px', 'replace', 'linear'),
+ value(1, '5px', 'replace') ] },
+ { property: 'margin-bottom',
+ values: [ value(0, '5px', 'replace', 'linear'),
+ value(1, '5px', 'replace') ] },
+ { property: 'margin-left',
+ values: [ value(0, '5px', 'replace', 'linear'),
+ value(1, '5px', 'replace') ] } ]
+ },
+ { desc: 'a property that can\'t be resolved to computed values in'
+ + ' final overlapping keyframes (second in series of three)',
+ frames: [ { margin: '5px' },
+ { margin: '5px', offset: 1 },
+ { margin: '5px', offset: 1,
+ simulateComputeValuesFailure: true },
+ { margin: '5px', offset: 1 } ],
+ expected: [ { property: 'margin-top',
+ values: [ value(0, '5px', 'replace', 'linear'),
+ value(1, '5px', 'replace'),
+ value(1, '5px', 'replace') ] },
+ { property: 'margin-right',
+ values: [ value(0, '5px', 'replace', 'linear'),
+ value(1, '5px', 'replace'),
+ value(1, '5px', 'replace') ] },
+ { property: 'margin-bottom',
+ values: [ value(0, '5px', 'replace', 'linear'),
+ value(1, '5px', 'replace'),
+ value(1, '5px', 'replace') ] },
+ { property: 'margin-left',
+ values: [ value(0, '5px', 'replace', 'linear'),
+ value(1, '5px', 'replace'),
+ value(1, '5px', 'replace') ] } ]
+ },
+ { desc: 'a property that can\'t be resolved to computed values in'
+ + ' intermediate keyframe',
+ frames: [ { margin: '5px' },
+ { margin: '5px', simulateComputeValuesFailure: true },
+ { margin: '5px' } ],
+ expected: [ { property: 'margin-top',
+ values: [ value(0, '5px', 'replace', 'linear'),
+ value(1, '5px', 'replace') ] },
+ { property: 'margin-right',
+ values: [ value(0, '5px', 'replace', 'linear'),
+ value(1, '5px', 'replace') ] },
+ { property: 'margin-bottom',
+ values: [ value(0, '5px', 'replace', 'linear'),
+ value(1, '5px', 'replace') ] },
+ { property: 'margin-left',
+ values: [ value(0, '5px', 'replace', 'linear'),
+ value(1, '5px', 'replace') ] } ]
+ },
+ { desc: 'a property that can\'t be resolved to computed values in'
+ + ' initial keyframe along with other values',
+ // simulateComputeValuesFailure only applies to shorthands so we can set
+ // it on the same keyframe and it will only apply to |margin| and not
+ // |left|.
+ frames: [ { margin: '77%', left: '10px',
+ simulateComputeValuesFailure: true },
+ { margin: '5px', left: '20px' } ],
+ expected: [ { property: 'left',
+ values: [ value(0, '10px', 'replace', 'linear'),
+ value(1, '20px', 'replace') ] } ],
+ },
+ { desc: 'a property that can\'t be resolved to computed values in'
+ + ' initial keyframe along with other values where those'
+ + ' values sort after the property with missing values',
+ frames: [ { margin: '77%', right: '10px',
+ simulateComputeValuesFailure: true },
+ { margin: '5px', right: '20px' } ],
+ expected: [ { property: 'right',
+ values: [ value(0, '10px', 'replace', 'linear'),
+ value(1, '20px', 'replace') ] } ],
+ },
+ { desc: 'a property that can\'t be resolved to computed values in'
+ + ' final keyframe along with other values',
+ frames: [ { margin: '5px', left: '10px' },
+ { margin: '5px', left: '20px',
+ simulateComputeValuesFailure: true } ],
+ expected: [ { property: 'left',
+ values: [ value(0, '10px', 'replace', 'linear'),
+ value(1, '20px', 'replace') ] } ],
+ },
+ { desc: 'a property that can\'t be resolved to computed values in'
+ + ' final keyframe along with other values where those'
+ + ' values sort after the property with missing values',
+ frames: [ { margin: '5px', right: '10px' },
+ { margin: '5px', right: '20px',
+ simulateComputeValuesFailure: true } ],
+ expected: [ { property: 'right',
+ values: [ value(0, '10px', 'replace', 'linear'),
+ value(1, '20px', 'replace') ] } ],
+ },
+ { desc: 'a property that can\'t be resolved to computed values in'
+ + ' an intermediate keyframe along with other values',
+ frames: [ { margin: '5px', left: '10px' },
+ { margin: '5px', left: '20px',
+ simulateComputeValuesFailure: true },
+ { margin: '5px', left: '30px' } ],
+ expected: [ { property: 'margin-top',
+ values: [ value(0, '5px', 'replace', 'linear'),
+ value(1, '5px', 'replace') ] },
+ { property: 'margin-right',
+ values: [ value(0, '5px', 'replace', 'linear'),
+ value(1, '5px', 'replace') ] },
+ { property: 'margin-bottom',
+ values: [ value(0, '5px', 'replace', 'linear'),
+ value(1, '5px', 'replace') ] },
+ { property: 'margin-left',
+ values: [ value(0, '5px', 'replace', 'linear'),
+ value(1, '5px', 'replace') ] },
+ { property: 'left',
+ values: [ value(0, '10px', 'replace', 'linear'),
+ value(0.5, '20px', 'replace', 'linear'),
+ value(1, '30px', 'replace') ] } ]
+ },
+ { desc: 'a property that can\'t be resolved to computed values in'
+ + ' an intermediate keyframe by itself',
+ frames: [ { margin: '5px', left: '10px' },
+ { margin: '5px',
+ simulateComputeValuesFailure: true },
+ { margin: '5px', left: '30px' } ],
+ expected: [ { property: 'margin-top',
+ values: [ value(0, '5px', 'replace', 'linear'),
+ value(1, '5px', 'replace') ] },
+ { property: 'margin-right',
+ values: [ value(0, '5px', 'replace', 'linear'),
+ value(1, '5px', 'replace') ] },
+ { property: 'margin-bottom',
+ values: [ value(0, '5px', 'replace', 'linear'),
+ value(1, '5px', 'replace') ] },
+ { property: 'margin-left',
+ values: [ value(0, '5px', 'replace', 'linear'),
+ value(1, '5px', 'replace') ] },
+ { property: 'left',
+ values: [ value(0, '10px', 'replace', 'linear'),
+ value(1, '30px', 'replace') ] } ]
+ },
+];
+
+gTests.forEach(function(subtest) {
+ test(function(t) {
+ var div = addDiv(t);
+ var animation = div.animate(subtest.frames, 100 * MS_PER_SEC);
+ assert_properties_equal(animation.effect.getProperties(),
+ subtest.expected);
+ }, subtest.desc);
+});
+
+</script>
+</body>
diff --git a/dom/animation/test/chrome/test_generated_content_getAnimations.html b/dom/animation/test/chrome/test_generated_content_getAnimations.html
new file mode 100644
index 000000000..04e0f9bd0
--- /dev/null
+++ b/dom/animation/test/chrome/test_generated_content_getAnimations.html
@@ -0,0 +1,83 @@
+<!DOCTYPE html>
+<head>
+<meta charset=utf-8>
+<title>Test getAnimations() for generated-content elements</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>
+@keyframes anim { }
+@keyframes anim2 { }
+.before::before {
+ content: '';
+ animation: anim 100s;
+}
+.after::after {
+ content: '';
+ animation: anim 100s, anim2 100s;
+}
+</style>
+</head>
+<body>
+<div id='root' class='before after'>
+ <div class='before'></div>
+ <div></div>
+</div>
+<script>
+'use strict';
+
+const {Cc, Ci, Cu} = SpecialPowers;
+
+function getWalker(node) {
+ var walker = Cc["@mozilla.org/inspector/deep-tree-walker;1"].
+ createInstance(Ci.inIDeepTreeWalker);
+ walker.showAnonymousContent = true;
+ walker.init(node.ownerDocument, Ci.nsIDOMNodeFilter.SHOW_ALL);
+ walker.currentNode = node;
+ return walker;
+}
+
+test(function(t) {
+ var root = document.getElementById('root');
+ // Flush first to make sure the generated-content elements are ready
+ // in the tree.
+ flushComputedStyle(root);
+ var before = getWalker(root).firstChild();
+ var after = getWalker(root).lastChild();
+
+ // Sanity Checks
+ assert_equals(document.getAnimations().length, 4,
+ 'All animations in this document');
+ assert_equals(before.tagName, '_moz_generated_content_before',
+ 'First child is ::before element');
+ assert_equals(after.tagName, '_moz_generated_content_after',
+ 'Last child is ::after element');
+
+ // Test Element.getAnimations() for generated-content elements
+ assert_equals(before.getAnimations().length, 1,
+ 'Animations of ::before generated-content element');
+ assert_equals(after.getAnimations().length, 2,
+ 'Animations of ::after generated-content element');
+}, 'Element.getAnimations() used on generated-content elements');
+
+test(function(t) {
+ var root = document.getElementById('root');
+ flushComputedStyle(root);
+ var walker = getWalker(root);
+
+ var animations = [];
+ var element = walker.currentNode;
+ while (element) {
+ if (element.getAnimations) {
+ animations = [...animations, ...element.getAnimations()];
+ }
+ element = walker.nextNode();
+ }
+
+ assert_equals(animations.length, document.getAnimations().length,
+ 'The number of animations got by DeepTreeWalker and ' +
+ 'document.getAnimations() should be the same');
+}, 'Element.getAnimations() used by traversing DeepTreeWalker');
+
+</script>
+</body>
diff --git a/dom/animation/test/chrome/test_observers_for_sync_api.html b/dom/animation/test/chrome/test_observers_for_sync_api.html
new file mode 100644
index 000000000..20c3f3670
--- /dev/null
+++ b/dom/animation/test/chrome/test_observers_for_sync_api.html
@@ -0,0 +1,854 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title>
+Test chrome-only MutationObserver animation notifications for sync APIs
+</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>
+<div id="log"></div>
+<style>
+@keyframes anim {
+ to { transform: translate(100px); }
+}
+@keyframes anotherAnim {
+ to { transform: translate(0px); }
+}
+</style>
+<script>
+
+function assert_record_list(actual, expected, desc, index, listName) {
+ assert_equals(actual.length, expected.length,
+ `${desc} - record[${index}].${listName} length`);
+ if (actual.length != expected.length) {
+ return;
+ }
+ for (var i = 0; i < actual.length; i++) {
+ assert_not_equals(actual.indexOf(expected[i]), -1,
+ `${desc} - record[${index}].${listName} contains expected Animation`);
+ }
+}
+
+function assert_equals_records(actual, expected, desc) {
+ assert_equals(actual.length, expected.length, `${desc} - number of records`);
+ if (actual.length != expected.length) {
+ return;
+ }
+ for (var i = 0; i < actual.length; i++) {
+ assert_record_list(actual[i].addedAnimations,
+ expected[i].added, desc, i, "addedAnimations");
+ assert_record_list(actual[i].changedAnimations,
+ expected[i].changed, desc, i, "changedAnimations");
+ assert_record_list(actual[i].removedAnimations,
+ expected[i].removed, desc, i, "removedAnimations");
+ }
+}
+
+// Create a pseudo element
+function createPseudo(test, element, type) {
+ addStyle(test, { '@keyframes anim': '',
+ ['.pseudo::' + type]: 'animation: anim 10s;' });
+ element.classList.add('pseudo');
+ var anims = document.getAnimations();
+ assert_true(anims.length >= 1);
+ var anim = anims[anims.length - 1];
+ assert_equals(anim.effect.target.parentElement, element);
+ assert_equals(anim.effect.target.type, '::' + type);
+ anim.cancel();
+ return anim.effect.target;
+}
+
+[ { subtree: false },
+ { subtree: true }
+].forEach(aOptions => {
+ test(t => {
+ var div = addDiv(t);
+ var observer =
+ setupSynchronousObserver(t,
+ aOptions.subtree ? div.parentNode : div,
+ aOptions.subtree);
+
+ var anim = div.animate({ opacity: [ 0, 1 ] }, 200 * MS_PER_SEC);
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [anim], changed: [], removed: [] }],
+ "records after animation is added");
+
+ anim.effect.timing.duration = 100 * MS_PER_SEC;
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [anim], removed: [] }],
+ "records after duration is changed");
+
+ anim.effect.timing.duration = 100 * MS_PER_SEC;
+ assert_equals_records(observer.takeRecords(),
+ [], "records after assigning same value");
+
+ anim.currentTime = anim.effect.timing.duration * 2;
+ anim.finish();
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [], removed: [anim] }],
+ "records after animation end");
+
+ anim.effect.timing.duration = anim.effect.timing.duration * 3;
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [anim], changed: [], removed: [] }],
+ "records after animation restarted");
+
+ anim.effect.timing.duration = "auto";
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [], removed: [anim] }],
+ "records after duration set \"auto\"");
+
+ anim.effect.timing.duration = "auto";
+ assert_equals_records(observer.takeRecords(),
+ [], "records after assigning same value \"auto\"");
+ }, "change_duration_and_currenttime");
+
+ test(t => {
+ var div = addDiv(t);
+ var observer =
+ setupSynchronousObserver(t,
+ aOptions.subtree ? div.parentNode : div,
+ aOptions.subtree);
+
+ var anim = div.animate({ opacity: [ 0, 1 ] }, 100 * MS_PER_SEC);
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [anim], changed: [], removed: [] }],
+ "records after animation is added");
+
+ anim.effect.timing.endDelay = 10 * MS_PER_SEC;
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [anim], removed: [] }],
+ "records after endDelay is changed");
+
+ anim.effect.timing.endDelay = 10 * MS_PER_SEC;
+ assert_equals_records(observer.takeRecords(),
+ [], "records after assigning same value");
+
+ anim.currentTime = 109 * MS_PER_SEC;
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [], removed: [anim] }],
+ "records after currentTime during endDelay");
+
+ anim.effect.timing.endDelay = -110 * MS_PER_SEC;
+ assert_equals_records(observer.takeRecords(),
+ [], "records after assigning negative value");
+ }, "change_enddelay_and_currenttime");
+
+ test(t => {
+ var div = addDiv(t);
+ var observer =
+ setupSynchronousObserver(t,
+ aOptions.subtree ? div.parentNode : div,
+ aOptions.subtree);
+
+ var anim = div.animate({ opacity: [ 0, 1 ] },
+ { duration: 100 * MS_PER_SEC,
+ endDelay: -100 * MS_PER_SEC });
+ assert_equals_records(observer.takeRecords(),
+ [], "records after animation is added");
+ }, "zero_end_time");
+
+ test(t => {
+ var div = addDiv(t);
+ var observer =
+ setupSynchronousObserver(t,
+ aOptions.subtree ? div.parentNode : div,
+ aOptions.subtree);
+
+ var anim = div.animate({ opacity: [ 0, 1 ] }, 100 * MS_PER_SEC);
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [anim], changed: [], removed: [] }],
+ "records after animation is added");
+
+ anim.effect.timing.iterations = 2;
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [anim], removed: [] }],
+ "records after iterations is changed");
+
+ anim.effect.timing.iterations = 2;
+ assert_equals_records(observer.takeRecords(),
+ [], "records after assigning same value");
+
+ anim.effect.timing.iterations = 0;
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [], removed: [anim] }],
+ "records after animation end");
+
+ anim.effect.timing.iterations = Infinity;
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [anim], changed: [], removed: [] }],
+ "records after animation restarted");
+ }, "change_iterations");
+
+ test(t => {
+ var div = addDiv(t);
+ var observer =
+ setupSynchronousObserver(t,
+ aOptions.subtree ? div.parentNode : div,
+ aOptions.subtree);
+
+ var anim = div.animate({ opacity: [ 0, 1 ] }, 100 * MS_PER_SEC);
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [anim], changed: [], removed: [] }],
+ "records after animation is added");
+
+ anim.effect.timing.delay = 100;
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [anim], removed: [] }],
+ "records after delay is changed");
+
+ anim.effect.timing.delay = 100;
+ assert_equals_records(observer.takeRecords(),
+ [], "records after assigning same value");
+
+ anim.effect.timing.delay = -100 * MS_PER_SEC;
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [], removed: [anim] }],
+ "records after animation end");
+
+ anim.effect.timing.delay = 0;
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [anim], changed: [], removed: [] }],
+ "records after animation restarted");
+ }, "change_delay");
+
+ test(t => {
+ var div = addDiv(t);
+ var observer =
+ setupSynchronousObserver(t,
+ aOptions.subtree ? div.parentNode : div,
+ aOptions.subtree);
+
+ var anim = div.animate({ opacity: [ 0, 1 ] },
+ { duration: 100 * MS_PER_SEC,
+ easing: "steps(2, start)" });
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [anim], changed: [], removed: [] }],
+ "records after animation is added");
+
+ anim.effect.timing.easing = "steps(2, end)";
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [anim], removed: [] }],
+ "records after easing is changed");
+
+ anim.effect.timing.easing = "steps(2, end)";
+ assert_equals_records(observer.takeRecords(),
+ [], "records after assigning same value");
+ }, "change_easing");
+
+ test(t => {
+ var div = addDiv(t);
+ var observer =
+ setupSynchronousObserver(t,
+ aOptions.subtree ? div.parentNode : div,
+ aOptions.subtree);
+
+ var anim = div.animate({ opacity: [ 0, 1 ] },
+ { duration: 100, delay: -100 });
+ assert_equals_records(observer.takeRecords(),
+ [], "records after assigning negative value");
+ }, "negative_delay_in_constructor");
+
+ test(t => {
+ var div = addDiv(t);
+ var observer =
+ setupSynchronousObserver(t,
+ aOptions.subtree ? div.parentNode : div,
+ aOptions.subtree);
+
+ var effect = new KeyframeEffectReadOnly(null,
+ { opacity: [ 0, 1 ] },
+ { duration: 100 * MS_PER_SEC });
+ var anim = new Animation(effect, document.timeline);
+ anim.play();
+ assert_equals_records(observer.takeRecords(),
+ [], "no records after animation is added");
+ }, "create_animation_without_target");
+
+ test(t => {
+ var div = addDiv(t);
+ var observer =
+ setupSynchronousObserver(t,
+ aOptions.subtree ? div.parentNode : div,
+ aOptions.subtree);
+
+ var anim = div.animate({ opacity: [ 0, 1 ] },
+ { duration: 100 * MS_PER_SEC });
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [anim], changed: [], removed: [] }],
+ "records after animation is added");
+
+ anim.effect.target = div;
+ assert_equals_records(observer.takeRecords(),
+ [], "no records after setting the same target");
+
+ anim.effect.target = null;
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [], removed: [anim] }],
+ "records after setting null");
+
+ anim.effect.target = null;
+ assert_equals_records(observer.takeRecords(),
+ [], "records after setting redundant null");
+ }, "set_redundant_animation_target");
+
+ test(t => {
+ var div = addDiv(t);
+ var observer =
+ setupSynchronousObserver(t,
+ aOptions.subtree ? div.parentNode : div,
+ aOptions.subtree);
+
+ var anim = div.animate({ opacity: [ 0, 1 ] },
+ { duration: 100 * MS_PER_SEC });
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [anim], changed: [], removed: [] }],
+ "records after animation is added");
+
+ anim.effect = null;
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [], removed: [anim] }],
+ "records after animation is removed");
+ }, "set_null_animation_effect");
+
+ test(t => {
+ var div = addDiv(t);
+ var observer =
+ setupSynchronousObserver(t,
+ aOptions.subtree ? div.parentNode : div,
+ aOptions.subtree);
+
+ var anim = new Animation();
+ anim.play();
+ anim.effect = new KeyframeEffect(div, { opacity: [ 0, 1 ] },
+ 100 * MS_PER_SEC);
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [anim], changed: [], removed: [] }],
+ "records after animation is added");
+ }, "set_effect_on_null_effect_animation");
+
+ test(t => {
+ var div = addDiv(t);
+ var observer =
+ setupSynchronousObserver(t,
+ aOptions.subtree ? div.parentNode : div,
+ aOptions.subtree);
+
+ var anim = div.animate({ marginLeft: [ "0px", "100px" ] },
+ 100 * MS_PER_SEC);
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [anim], changed: [], removed: [] }],
+ "records after animation is added");
+
+ anim.effect = new KeyframeEffect(div, { opacity: [ 0, 1 ] },
+ 100 * MS_PER_SEC);
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [anim], removed: [] }],
+ "records after replace effects");
+ }, "replace_effect_targeting_on_the_same_element");
+
+ test(t => {
+ var div = addDiv(t);
+ var observer =
+ setupSynchronousObserver(t,
+ aOptions.subtree ? div.parentNode : div,
+ aOptions.subtree);
+
+ var anim = div.animate({ marginLeft: [ "0px", "100px" ] },
+ 100 * MS_PER_SEC);
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [anim], changed: [], removed: [] }],
+ "records after animation is added");
+
+ anim.currentTime = 60 * MS_PER_SEC;
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [anim], removed: [] }],
+ "records after animation is changed");
+
+ anim.effect = new KeyframeEffect(div, { opacity: [ 0, 1 ] },
+ 50 * MS_PER_SEC);
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [], removed: [anim] }],
+ "records after replacing effects");
+ }, "replace_effect_targeting_on_the_same_element_not_in_effect");
+
+ test(t => {
+ var div = addDiv(t);
+ var observer =
+ setupSynchronousObserver(t,
+ aOptions.subtree ? div.parentNode : div,
+ aOptions.subtree);
+
+ var anim = div.animate([ { marginLeft: "0px" },
+ { marginLeft: "-20px" },
+ { marginLeft: "100px" },
+ { marginLeft: "50px" } ],
+ { duration: 100 * MS_PER_SEC });
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [anim], changed: [], removed: [] }],
+ "records after animation is added");
+
+ anim.effect.spacing = "paced(margin-left)";
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [anim], removed: [] }],
+ "records after animation is changed");
+ }, "set_spacing");
+
+ test(t => {
+ var div = addDiv(t);
+ var observer =
+ setupSynchronousObserver(t,
+ aOptions.subtree ? div.parentNode : div,
+ aOptions.subtree);
+
+ var anim = div.animate([ { marginLeft: "0px" },
+ { marginLeft: "-20px" },
+ { marginLeft: "100px" },
+ { marginLeft: "50px" } ],
+ { duration: 100 * MS_PER_SEC,
+ spacing: "paced(margin-left)" });
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [anim], changed: [], removed: [] }],
+ "records after animation is added");
+
+ anim.effect.spacing = "paced(animation-duration)";
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [anim], removed: [] }],
+ "records after setting a non-animatable paced property");
+ }, "set_spacing_on_a_non-animatable_property");
+
+ test(t => {
+ var div = addDiv(t);
+ var observer =
+ setupSynchronousObserver(t,
+ aOptions.subtree ? div.parentNode : div,
+ aOptions.subtree);
+
+ var anim = div.animate([ { marginLeft: "0px" },
+ { marginLeft: "-20px" },
+ { marginLeft: "100px" },
+ { marginLeft: "50px" } ],
+ { duration: 100 * MS_PER_SEC,
+ spacing: "paced(margin-left)" });
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [anim], changed: [], removed: [] }],
+ "records after animation is added");
+
+ anim.effect.spacing = "paced(margin-left)";
+ assert_equals_records(observer.takeRecords(),
+ [], "no record after setting the same spacing");
+ }, "set_the_same_spacing");
+
+ // Test that starting a single animation that is cancelled by calling
+ // cancel() dispatches an added notification and then a removed
+ // notification.
+ test(t => {
+ var div = addDiv(t, { style: "animation: anim 100s forwards" });
+ var observer =
+ setupSynchronousObserver(t,
+ aOptions.subtree ? div.parentNode : div,
+ aOptions.subtree);
+
+ var animations = div.getAnimations();
+ assert_equals(animations.length, 1,
+ "getAnimations().length after animation start");
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: animations, changed: [], removed: [] }],
+ "records after animation start");
+
+ animations[0].cancel();
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [], removed: animations }],
+ "records after animation end");
+
+ // Re-trigger the animation.
+ animations[0].play();
+
+ // Single MutationRecord for the Animation (re-)addition.
+ assert_equals_records(observer.takeRecords(),
+ [{ added: animations, changed: [], removed: [] }],
+ "records after animation start");
+ }, "single_animation_cancelled_api");
+
+ // Test that updating a property on the Animation object dispatches a changed
+ // notification.
+ [
+ { prop: "playbackRate", val: 0.5 },
+ { prop: "startTime", val: 50 * MS_PER_SEC },
+ { prop: "currentTime", val: 50 * MS_PER_SEC },
+ ].forEach(function(aChangeTest) {
+ test(t => {
+ // We use a forwards fill mode so that even if the change we make causes
+ // the animation to become finished, it will still be "relevant" so we
+ // won't mark it as removed.
+ var div = addDiv(t, { style: "animation: anim 100s forwards" });
+ var observer =
+ setupSynchronousObserver(t,
+ aOptions.subtree ? div.parentNode : div,
+ aOptions.subtree);
+
+ var animations = div.getAnimations();
+ assert_equals(animations.length, 1,
+ "getAnimations().length after animation start");
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: animations, changed: [], removed: [] }],
+ "records after animation start");
+
+ // Update the property.
+ animations[0][aChangeTest.prop] = aChangeTest.val;
+
+ // Make a redundant change.
+ animations[0][aChangeTest.prop] = animations[0][aChangeTest.prop];
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: animations, removed: [] }],
+ "records after animation property change");
+ }, `single_animation_api_change_${aChangeTest.prop}`);
+ });
+
+ // Test that making a redundant change to currentTime while an Animation
+ // is pause-pending still generates a change MutationRecord since setting
+ // the currentTime to any value in this state aborts the pending pause.
+ test(t => {
+ var div = addDiv(t, { style: "animation: anim 100s" });
+ var observer =
+ setupSynchronousObserver(t,
+ aOptions.subtree ? div.parentNode : div,
+ aOptions.subtree);
+
+ var animations = div.getAnimations();
+ assert_equals(animations.length, 1,
+ "getAnimations().length after animation start");
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: animations, changed: [], removed: [] }],
+ "records after animation start");
+
+ animations[0].pause();
+
+ // We are now pause-pending. Even if we make a redundant change to the
+ // currentTime, we should still get a change record because setting the
+ // currentTime while pause-pending has the effect of cancelling a pause.
+ animations[0].currentTime = animations[0].currentTime;
+
+ // Two MutationRecords for the Animation changes: one for pausing, one
+ // for aborting the pause.
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: animations, removed: [] },
+ { added: [], changed: animations, removed: [] }],
+ "records after pausing then seeking");
+ }, "change_currentTime_while_pause_pending");
+
+ // Test that calling finish() on a forwards-filling Animation dispatches
+ // a changed notification.
+ test(t => {
+ var div = addDiv(t, { style: "animation: anim 100s forwards" });
+ var observer =
+ setupSynchronousObserver(t,
+ aOptions.subtree ? div.parentNode : div,
+ aOptions.subtree);
+
+ var animations = div.getAnimations();
+ assert_equals(animations.length, 1,
+ "getAnimations().length after animation start");
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: animations, changed: [], removed: [] }],
+ "records after animation start");
+
+ animations[0].finish();
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: animations, removed: [] }],
+ "records after finish()");
+
+ // Redundant finish.
+ animations[0].finish();
+
+ // Ensure no change records.
+ assert_equals_records(observer.takeRecords(),
+ [], "records after redundant finish()");
+ }, "finish_with_forwards_fill");
+
+ // Test that calling finish() on an Animation that does not fill forwards,
+ // dispatches a removal notification.
+ test(t => {
+ var div = addDiv(t, { style: "animation: anim 100s" });
+ var observer =
+ setupSynchronousObserver(t,
+ aOptions.subtree ? div.parentNode : div,
+ aOptions.subtree);
+
+ var animations = div.getAnimations();
+ assert_equals(animations.length, 1,
+ "getAnimations().length after animation start");
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: animations, changed: [], removed: [] }],
+ "records after animation start");
+
+ animations[0].finish();
+
+ // Single MutationRecord for the Animation removal.
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [], removed: animations }],
+ "records after finishing");
+ }, "finish_without_fill");
+
+ // Test that calling finish() on a forwards-filling Animation dispatches
+ test(t => {
+ var div = addDiv(t, { style: "animation: anim 100s" });
+ var observer =
+ setupSynchronousObserver(t,
+ aOptions.subtree ? div.parentNode : div,
+ aOptions.subtree);
+
+ var animation = div.getAnimations()[0];
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [animation], changed: [], removed: []}],
+ "records after creation");
+ animation.id = "new id";
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [animation], removed: []}],
+ "records after id is changed");
+
+ animation.id = "new id";
+ assert_equals_records(observer.takeRecords(),
+ [], "records after assigning same value with id");
+ }, "change_id");
+
+ // Test that calling reverse() dispatches a changed notification.
+ test(t => {
+ var div = addDiv(t, { style: "animation: anim 100s both" });
+ var observer =
+ setupSynchronousObserver(t,
+ aOptions.subtree ? div.parentNode : div,
+ aOptions.subtree);
+
+ var animations = div.getAnimations();
+ assert_equals(animations.length, 1,
+ "getAnimations().length after animation start");
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: animations, changed: [], removed: [] }],
+ "records after animation start");
+
+ animations[0].reverse();
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: animations, removed: [] }],
+ "records after calling reverse()");
+ }, "reverse");
+
+ // Test that calling reverse() does *not* dispatch a changed notification
+ // when playbackRate == 0.
+ test(t => {
+ var div = addDiv(t, { style: "animation: anim 100s both" });
+ var observer =
+ setupSynchronousObserver(t,
+ aOptions.subtree ? div.parentNode : div,
+ aOptions.subtree);
+
+ var animations = div.getAnimations();
+ assert_equals(animations.length, 1,
+ "getAnimations().length after animation start");
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: animations, changed: [], removed: [] }],
+ "records after animation start");
+
+ // Seek to the middle and set playbackRate to zero.
+ animations[0].currentTime = 50 * MS_PER_SEC;
+ animations[0].playbackRate = 0;
+
+ // Two MutationRecords, one for each change.
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: animations, removed: [] },
+ { added: [], changed: animations, removed: [] }],
+ "records after seeking and setting playbackRate");
+
+ animations[0].reverse();
+
+ // We should get no notifications.
+ assert_equals_records(observer.takeRecords(),
+ [], "records after calling reverse()");
+ }, "reverse_with_zero_playbackRate");
+
+ // Test that attempting to start an animation that should already be finished
+ // does not send any notifications.
+ test(t => {
+ // Start an animation that should already be finished.
+ var div = addDiv(t, { style: "animation: anim 1s -2s;" });
+ var observer =
+ setupSynchronousObserver(t,
+ aOptions.subtree ? div.parentNode : div,
+ aOptions.subtree);
+
+ // The animation should cause no Animations to be created.
+ var animations = div.getAnimations();
+ assert_equals(animations.length, 0,
+ "getAnimations().length after animation start");
+
+ // And we should get no notifications.
+ assert_equals_records(observer.takeRecords(),
+ [], "records after attempted animation start");
+ }, "already_finished");
+
+ test(t => {
+ var div = addDiv(t, { style: "animation: anim 100s, anotherAnim 100s" });
+ var observer =
+ setupSynchronousObserver(t,
+ aOptions.subtree ? div.parentNode : div,
+ aOptions.subtree);
+
+ var animations = div.getAnimations();
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: animations, changed: [], removed: []}],
+ "records after creation");
+
+ div.style.animation = "anotherAnim 100s, anim 100s";
+ animations = div.getAnimations();
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: animations, removed: []}],
+ "records after the order is changed");
+
+ div.style.animation = "anotherAnim 100s, anim 100s";
+
+ assert_equals_records(observer.takeRecords(),
+ [], "no records after applying the same order");
+ }, "animtion_order_change");
+
+});
+
+test(t => {
+ var div = addDiv(t);
+ var observer = setupSynchronousObserver(t, div, true);
+
+ var child = document.createElement("div");
+ div.appendChild(child);
+
+ var anim1 = div.animate({ marginLeft: [ "0px", "50px" ] },
+ 100 * MS_PER_SEC);
+ var anim2 = child.animate({ marginLeft: [ "0px", "100px" ] },
+ 50 * MS_PER_SEC);
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [anim1], changed: [], removed: [] },
+ { added: [anim2], changed: [], removed: [] }],
+ "records after animation is added");
+
+ // After setting a new effect, we remove the current animation, anim1,
+ // because it is no longer attached to |div|, and then remove the previous
+ // animation, anim2. Finally, add back the anim1 which is in effect on
+ // |child| now. In addition, we sort them by tree order and they are
+ // batched.
+ anim1.effect = anim2.effect;
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [], removed: [anim1] }, // div
+ { added: [anim1], changed: [], removed: [anim2] }], // child
+ "records after animation effects are changed");
+}, "set_effect_with_previous_animation");
+
+test(t => {
+ var div = addDiv(t);
+ var observer = setupSynchronousObserver(t, document, true);
+
+ var anim = div.animate({ opacity: [ 0, 1 ] },
+ { duration: 100 * MS_PER_SEC });
+
+ var newTarget = document.createElement("div");
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [anim], changed: [], removed: [] }],
+ "records after animation is added");
+
+ anim.effect.target = null;
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [], removed: [anim] }],
+ "records after setting null");
+
+ anim.effect.target = div;
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [anim], changed: [], removed: [] }],
+ "records after setting a target");
+
+ anim.effect.target = addDiv(t);
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [], removed: [anim] },
+ { added: [anim], changed: [], removed: [] }],
+ "records after setting a different target");
+}, "set_animation_target");
+
+test(t => {
+ var div = addDiv(t);
+ var pseudoTarget = createPseudo(t, div, 'before');
+ var observer = setupSynchronousObserver(t, div, true);
+
+ var anim = pseudoTarget.animate({ opacity: [ 0, 1 ] }, 200 * MS_PER_SEC);
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [anim], changed: [], removed: [] }],
+ "records after animation is added");
+
+ anim.effect.timing.duration = 100 * MS_PER_SEC;
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [anim], removed: [] }],
+ "records after duration is changed");
+
+ anim.effect.timing.duration = 100 * MS_PER_SEC;
+ assert_equals_records(observer.takeRecords(),
+ [], "records after assigning same value");
+
+ anim.currentTime = anim.effect.timing.duration * 2;
+ anim.finish();
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [], removed: [anim] }],
+ "records after animation end");
+
+ anim.effect.timing.duration = anim.effect.timing.duration * 3;
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [anim], changed: [], removed: [] }],
+ "records after animation restarted");
+
+ anim.effect.timing.duration = "auto";
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [], removed: [anim] }],
+ "records after duration set \"auto\"");
+
+ anim.effect.timing.duration = "auto";
+ assert_equals_records(observer.takeRecords(),
+ [], "records after assigning same value \"auto\"");
+}, "change_duration_and_currenttime_on_pseudo_elements");
+
+test(t => {
+ var div = addDiv(t);
+ var pseudoTarget = createPseudo(t, div, 'before');
+ var observer = setupSynchronousObserver(t, div, false);
+
+ var anim = div.animate({ opacity: [ 0, 1 ] },
+ { duration: 100 * MS_PER_SEC });
+ var pAnim = pseudoTarget.animate({ opacity: [ 0, 1 ] },
+ { duration: 100 * MS_PER_SEC });
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [anim], changed: [], removed: [] }],
+ "records after animation is added");
+
+ anim.finish();
+ pAnim.finish();
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [], removed: [anim] }],
+ "records after animation is finished");
+}, "exclude_animations_targeting_pseudo_elements");
+
+</script>
diff --git a/dom/animation/test/chrome/test_restyles.html b/dom/animation/test/chrome/test_restyles.html
new file mode 100644
index 000000000..e59967c19
--- /dev/null
+++ b/dom/animation/test/chrome/test_restyles.html
@@ -0,0 +1,815 @@
+<!doctype html>
+<head>
+<meta charset=utf-8>
+<title>Tests restyles caused by animations</title>
+<script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+<script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
+<script src="chrome://mochikit/content/tests/SimpleTest/SpawnTask.js"></script>
+<script src="chrome://mochikit/content/tests/SimpleTest/paint_listener.js"></script>
+<script src="../testcommon.js"></script>
+<link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+<style>
+@keyframes opacity {
+ from { opacity: 1; }
+ to { opacity: 0; }
+}
+@keyframes background-color {
+ from { background-color: red; }
+ to { background-color: blue; }
+}
+@keyframes rotate {
+ from { transform: rotate(0deg); }
+ to { transform: rotate(360deg); }
+}
+div {
+ /* Element needs geometry to be eligible for layerization */
+ width: 100px;
+ height: 100px;
+ background-color: white;
+}
+</style>
+</head>
+<body>
+<script>
+'use strict';
+
+function observeStyling(frameCount, onFrame) {
+ var docShell = window.QueryInterface(SpecialPowers.Ci.nsIInterfaceRequestor)
+ .getInterface(SpecialPowers.Ci.nsIWebNavigation)
+ .QueryInterface(SpecialPowers.Ci.nsIDocShell);
+
+ docShell.recordProfileTimelineMarkers = true;
+ docShell.popProfileTimelineMarkers();
+
+ return new Promise(function(resolve) {
+ return waitForAnimationFrames(frameCount, onFrame).then(function() {
+ var markers = docShell.popProfileTimelineMarkers();
+ docShell.recordProfileTimelineMarkers = false;
+ var stylingMarkers = markers.filter(function(marker, index) {
+ return marker.name == 'Styles' &&
+ (marker.restyleHint == 'eRestyle_CSSAnimations' ||
+ marker.restyleHint == 'eRestyle_CSSTransitions');
+ });
+ resolve(stylingMarkers);
+ });
+ });
+}
+
+function ensureElementRemoval(aElement) {
+ return new Promise(function(resolve) {
+ aElement.remove();
+ waitForAllPaintsFlushed(resolve);
+ });
+}
+
+SimpleTest.waitForExplicitFinish();
+
+var omtaEnabled = isOMTAEnabled();
+
+var isAndroid = !!navigator.userAgent.includes("Android");
+
+function add_task_if_omta_enabled(test) {
+ if (!omtaEnabled) {
+ info(test.name + " is skipped because OMTA is disabled");
+ return;
+ }
+ add_task(test);
+}
+
+// We need to wait for all paints before running tests to avoid contaminations
+// from styling of this document itself.
+waitForAllPaints(function() {
+ add_task(function* restyling_for_main_thread_animations() {
+ var div = addDiv(null, { style: 'animation: background-color 100s' });
+ var animation = div.getAnimations()[0];
+
+ yield animation.ready;
+ ok(!animation.isRunningOnCompositor);
+
+ var markers = yield observeStyling(5);
+ is(markers.length, 5,
+ 'CSS animations running on the main-thread should update style ' +
+ 'on the main thread');
+ yield ensureElementRemoval(div);
+ });
+
+ add_task_if_omta_enabled(function* no_restyling_for_compositor_animations() {
+ var div = addDiv(null, { style: 'animation: opacity 100s' });
+ var animation = div.getAnimations()[0];
+
+ yield animation.ready;
+ ok(animation.isRunningOnCompositor);
+
+ var markers = yield observeStyling(5);
+ is(markers.length, 0,
+ 'CSS animations running on the compositor should not update style ' +
+ 'on the main thread');
+ yield ensureElementRemoval(div);
+ });
+
+ add_task_if_omta_enabled(function* no_restyling_for_compositor_transitions() {
+ var div = addDiv(null, { style: 'transition: opacity 100s; opacity: 0' });
+ getComputedStyle(div).opacity;
+ div.style.opacity = 1;
+
+ var animation = div.getAnimations()[0];
+
+ yield animation.ready;
+ ok(animation.isRunningOnCompositor);
+
+ var markers = yield observeStyling(5);
+ is(markers.length, 0,
+ 'CSS transitions running on the compositor should not update style ' +
+ 'on the main thread');
+ yield ensureElementRemoval(div);
+ });
+
+ add_task_if_omta_enabled(function* no_restyling_when_animation_duration_is_changed() {
+ var div = addDiv(null, { style: 'animation: opacity 100s' });
+ var animation = div.getAnimations()[0];
+
+ yield animation.ready;
+ ok(animation.isRunningOnCompositor);
+
+ div.animationDuration = '200s';
+
+ var markers = yield observeStyling(5);
+ is(markers.length, 0,
+ 'Animations running on the compositor should not update style ' +
+ 'on the main thread');
+ yield ensureElementRemoval(div);
+ });
+
+ add_task_if_omta_enabled(function* only_one_restyling_after_finish_is_called() {
+ var div = addDiv(null, { style: 'animation: opacity 100s' });
+ var animation = div.getAnimations()[0];
+
+ yield animation.ready;
+ ok(animation.isRunningOnCompositor);
+
+ animation.finish();
+
+ var markers = yield observeStyling(5);
+ is(markers.length, 1,
+ 'Animations running on the compositor should only update style ' +
+ 'once after finish() is called');
+ yield ensureElementRemoval(div);
+ });
+
+ add_task(function* no_restyling_mouse_movement_on_finished_transition() {
+ var div = addDiv(null, { style: 'transition: opacity 1ms; opacity: 0' });
+ getComputedStyle(div).opacity;
+ div.style.opacity = 1;
+
+ var animation = div.getAnimations()[0];
+ var initialRect = div.getBoundingClientRect();
+
+ yield animation.finished;
+
+ var mouseX = initialRect.left + initialRect.width / 2;
+ var mouseY = initialRect.top + initialRect.height / 2;
+ var markers = yield observeStyling(5, function() {
+ // We can't use synthesizeMouse here since synthesizeMouse causes
+ // layout flush.
+ synthesizeMouseAtPoint(mouseX++, mouseY++,
+ { type: 'mousemove' }, window);
+ });
+
+ is(markers.length, 0,
+ 'Bug 1219236: Finished transitions should never cause restyles ' +
+ 'when mouse is moved on the animations');
+ yield ensureElementRemoval(div);
+ });
+
+ add_task(function* no_restyling_mouse_movement_on_finished_animation() {
+ var div = addDiv(null, { style: 'animation: opacity 1ms' });
+ var animation = div.getAnimations()[0];
+
+ var initialRect = div.getBoundingClientRect();
+
+ yield animation.finished;
+
+ var mouseX = initialRect.left + initialRect.width / 2;
+ var mouseY = initialRect.top + initialRect.height / 2;
+ var markers = yield observeStyling(5, function() {
+ // We can't use synthesizeMouse here since synthesizeMouse causes
+ // layout flush.
+ synthesizeMouseAtPoint(mouseX++, mouseY++,
+ { type: 'mousemove' }, window);
+ });
+
+ is(markers.length, 0,
+ 'Bug 1219236: Finished animations should never cause restyles ' +
+ 'when mouse is moved on the animations');
+ yield ensureElementRemoval(div);
+ });
+
+ add_task_if_omta_enabled(function* no_restyling_compositor_animations_out_of_view_element() {
+ if (!SpecialPowers.getBoolPref('dom.animations.offscreen-throttling')) {
+ return;
+ }
+
+ var div = addDiv(null,
+ { style: 'animation: opacity 100s; transform: translateY(-400px);' });
+ var animation = div.getAnimations()[0];
+
+ yield animation.ready;
+ ok(!animation.isRunningOnCompositor);
+
+ var markers = yield observeStyling(5);
+
+ is(markers.length, 0,
+ 'Animations running on the compositor in an out-of-view element ' +
+ 'should never cause restyles');
+ yield ensureElementRemoval(div);
+ });
+
+ add_task(function* no_restyling_main_thread_animations_out_of_view_element() {
+ if (!SpecialPowers.getBoolPref('dom.animations.offscreen-throttling')) {
+ return;
+ }
+
+ var div = addDiv(null,
+ { style: 'animation: background-color 100s; transform: translateY(-400px);' });
+ var animation = div.getAnimations()[0];
+
+ yield animation.ready;
+ var markers = yield observeStyling(5);
+
+ is(markers.length, 0,
+ 'Animations running on the main-thread in an out-of-view element ' +
+ 'should never cause restyles');
+ yield ensureElementRemoval(div);
+ });
+
+ add_task_if_omta_enabled(function* no_restyling_compositor_animations_in_scrolled_out_element() {
+ if (!SpecialPowers.getBoolPref('dom.animations.offscreen-throttling')) {
+ return;
+ }
+
+ /*
+ On Android the opacity animation runs on the compositor even if it is
+ scrolled out of view. We will fix this in bug 1247800.
+ */
+ if (isAndroid) {
+ return;
+ }
+ var parentElement = addDiv(null,
+ { style: 'overflow-y: scroll; height: 20px;' });
+ var div = addDiv(null,
+ { style: 'animation: opacity 100s; position: relative; top: 100px;' });
+ parentElement.appendChild(div);
+ var animation = div.getAnimations()[0];
+
+ yield animation.ready;
+
+ var markers = yield observeStyling(5);
+
+ is(markers.length, 0,
+ 'Animations running on the compositor for elements ' +
+ 'which are scrolled out should never cause restyles');
+
+ yield ensureElementRemoval(parentElement);
+ });
+
+ add_task(function* no_restyling_main_thread_animations_in_scrolled_out_element() {
+ if (!SpecialPowers.getBoolPref('dom.animations.offscreen-throttling')) {
+ return;
+ }
+
+ /*
+ On Android throttled animations are left behind on the main thread in some
+ frames, We will fix this in bug 1247800.
+ */
+ if (isAndroid) {
+ return;
+ }
+
+ var parentElement = addDiv(null,
+ { style: 'overflow-y: scroll; height: 20px;' });
+ var div = addDiv(null,
+ { style: 'animation: background-color 100s; position: relative; top: 100px;' });
+ parentElement.appendChild(div);
+ var animation = div.getAnimations()[0];
+
+ yield animation.ready;
+ var markers = yield observeStyling(5);
+
+ is(markers.length, 0,
+ 'Animations running on the main-thread for elements ' +
+ 'which are scrolled out should never cause restyles');
+
+ yield ensureElementRemoval(parentElement);
+ });
+
+ add_task(function* no_restyling_main_thread_animations_in_nested_scrolled_out_element() {
+ if (!SpecialPowers.getBoolPref('dom.animations.offscreen-throttling')) {
+ return;
+ }
+
+ /*
+ On Android throttled animations are left behind on the main thread in some
+ frames, We will fix this in bug 1247800.
+ */
+ if (isAndroid) {
+ return;
+ }
+
+ var grandParent = addDiv(null,
+ { style: 'overflow-y: scroll; height: 20px;' });
+ var parentElement = addDiv(null,
+ { style: 'overflow-y: scroll; height: 100px;' });
+ var div = addDiv(null,
+ { style: 'animation: background-color 100s; position: relative; top: 100px;' });
+ grandParent.appendChild(parentElement);
+ parentElement.appendChild(div);
+ var animation = div.getAnimations()[0];
+
+ yield animation.ready;
+ var markers = yield observeStyling(5);
+
+ is(markers.length, 0,
+ 'Animations running on the main-thread which are in nested elements ' +
+ 'which are scrolled out should never cause restyles');
+
+ yield ensureElementRemoval(grandParent);
+ });
+
+ add_task_if_omta_enabled(function* no_restyling_compositor_animations_in_visiblily_hidden_element() {
+ var div = addDiv(null,
+ { style: 'animation: opacity 100s; visibility: hidden' });
+ var animation = div.getAnimations()[0];
+
+ yield animation.ready;
+ ok(!animation.isRunningOnCompositor);
+
+ var markers = yield observeStyling(5);
+
+ todo_is(markers.length, 0,
+ 'Bug 1237454: Animations running on the compositor in ' +
+ 'visibility hidden element should never cause restyles');
+ yield ensureElementRemoval(div);
+ });
+
+ add_task(function* restyling_main_thread_animations_moved_in_view_by_scrolling() {
+ if (!SpecialPowers.getBoolPref('dom.animations.offscreen-throttling')) {
+ return;
+ }
+
+ /*
+ On Android throttled animations are left behind on the main thread in some
+ frames, We will fix this in bug 1247800.
+ */
+ if (isAndroid) {
+ return;
+ }
+
+ var parentElement = addDiv(null,
+ { style: 'overflow-y: scroll; height: 20px;' });
+ var div = addDiv(null,
+ { style: 'animation: background-color 100s; position: relative; top: 100px;' });
+ parentElement.appendChild(div);
+ var animation = div.getAnimations()[0];
+
+ var parentRect = parentElement.getBoundingClientRect();
+ var centerX = parentRect.left + parentRect.width / 2;
+ var centerY = parentRect.top + parentRect.height / 2;
+
+ yield animation.ready;
+
+ var markers = yield observeStyling(1, function() {
+ // We can't use synthesizeWheel here since synthesizeWheel causes
+ // layout flush.
+ synthesizeWheelAtPoint(centerX, centerY,
+ { deltaMode: WheelEvent.DOM_DELTA_PIXEL,
+ deltaY: 100 });
+ });
+
+ is(markers.length, 1,
+ 'Animations running on the main-thread which were in scrolled out ' +
+ 'elements should update restyling soon after the element moved in ' +
+ 'view by scrolling');
+
+ yield ensureElementRemoval(parentElement);
+ });
+
+ add_task(function* restyling_main_thread_animations_moved_in_view_by_scrolling() {
+ if (!SpecialPowers.getBoolPref('dom.animations.offscreen-throttling')) {
+ return;
+ }
+
+ var grandParent = addDiv(null,
+ { style: 'overflow-y: scroll; height: 20px;' });
+ var parentElement = addDiv(null,
+ { style: 'overflow-y: scroll; height: 200px;' });
+ var div = addDiv(null,
+ { style: 'animation: background-color 100s; position: relative; top: 100px;' });
+ grandParent.appendChild(parentElement);
+ parentElement.appendChild(div);
+ var animation = div.getAnimations()[0];
+
+ var parentRect = grandParent.getBoundingClientRect();
+ var centerX = parentRect.left + parentRect.width / 2;
+ var centerY = parentRect.top + parentRect.height / 2;
+
+ yield animation.ready;
+
+ var markers = yield observeStyling(1, function() {
+ // We can't use synthesizeWheel here since synthesizeWheel causes
+ // layout flush.
+ synthesizeWheelAtPoint(centerX, centerY,
+ { deltaMode: WheelEvent.DOM_DELTA_PIXEL,
+ deltaY: 100 });
+ });
+
+ // FIXME: We should reduce a redundant restyle here.
+ ok(markers.length >= 1,
+ 'Animations running on the main-thread which were in nested scrolled ' +
+ 'out elements should update restyle soon after the element moved ' +
+ 'in view by scrolling');
+
+ yield ensureElementRemoval(grandParent);
+ });
+
+ add_task(function* restyling_main_thread_animations_move_out_of_view_by_scrolling() {
+ if (!SpecialPowers.getBoolPref('dom.animations.offscreen-throttling')) {
+ return;
+ }
+
+ /*
+ On Android throttled animations are left behind on the main thread in some
+ frames, We will fix this in bug 1247800.
+ */
+ if (isAndroid) {
+ return;
+ }
+
+ var parentElement = addDiv(null,
+ { style: 'overflow-y: scroll; height: 200px;' });
+ var div = addDiv(null,
+ { style: 'animation: background-color 100s;' });
+ var pad = addDiv(null,
+ { style: 'height: 400px;' });
+ parentElement.appendChild(div);
+ parentElement.appendChild(pad);
+ var animation = div.getAnimations()[0];
+
+ var parentRect = parentElement.getBoundingClientRect();
+ var centerX = parentRect.left + parentRect.width / 2;
+ var centerY = parentRect.top + parentRect.height / 2;
+
+ yield animation.ready;
+
+ // We can't use synthesizeWheel here since synthesizeWheel causes
+ // layout flush.
+ synthesizeWheelAtPoint(centerX, centerY,
+ { deltaMode: WheelEvent.DOM_DELTA_PIXEL,
+ deltaY: 200 });
+
+ var markers = yield observeStyling(5);
+
+ // FIXME: We should reduce a redundant restyle here.
+ ok(markers.length >= 0,
+ 'Animations running on the main-thread which are in scrolled out ' +
+ 'elements should throttle restyling');
+
+ yield ensureElementRemoval(parentElement);
+ });
+
+ add_task(function* restyling_main_thread_animations_moved_in_view_by_resizing() {
+ if (!SpecialPowers.getBoolPref('dom.animations.offscreen-throttling')) {
+ return;
+ }
+
+ var parentElement = addDiv(null,
+ { style: 'overflow-y: scroll; height: 20px;' });
+ var div = addDiv(null,
+ { style: 'animation: background-color 100s; position: relative; top: 100px;' });
+ parentElement.appendChild(div);
+ var animation = div.getAnimations()[0];
+
+ yield animation.ready;
+
+ var markers = yield observeStyling(1, function() {
+ parentElement.style.height = '100px';
+ });
+
+ is(markers.length, 1,
+ 'Animations running on the main-thread which was in scrolled out ' +
+ 'elements should update restyling soon after the element moved in ' +
+ 'view by resizing');
+
+ yield ensureElementRemoval(parentElement);
+ });
+
+ add_task(function* no_restyling_main_thread_animations_in_visiblily_hidden_element() {
+ var div = addDiv(null,
+ { style: 'animation: background-color 100s; visibility: hidden' });
+ var animation = div.getAnimations()[0];
+
+ yield animation.ready;
+ var markers = yield observeStyling(5);
+
+ todo_is(markers.length, 0,
+ 'Bug 1237454: Animations running on the main-thread in ' +
+ 'visibility hidden element should never cause restyles');
+ yield ensureElementRemoval(div);
+ });
+
+ add_task_if_omta_enabled(function* no_restyling_compositor_animations_after_pause_is_called() {
+ var div = addDiv(null, { style: 'animation: opacity 100s' });
+ var animation = div.getAnimations()[0];
+
+ yield animation.ready;
+ ok(animation.isRunningOnCompositor);
+
+ animation.pause();
+
+ yield animation.ready;
+
+ var markers = yield observeStyling(5);
+ is(markers.length, 0,
+ 'Bug 1232563: Paused animations running on the compositor should ' +
+ 'never cause restyles once after pause() is called');
+ yield ensureElementRemoval(div);
+ });
+
+ add_task(function* no_restyling_main_thread_animations_after_pause_is_called() {
+ var div = addDiv(null, { style: 'animation: background-color 100s' });
+ var animation = div.getAnimations()[0];
+
+ yield animation.ready;
+
+ animation.pause();
+
+ yield animation.ready;
+
+ var markers = yield observeStyling(5);
+ is(markers.length, 0,
+ 'Bug 1232563: Paused animations running on the main-thread should ' +
+ 'never cause restyles after pause() is called');
+ yield ensureElementRemoval(div);
+ });
+
+ add_task_if_omta_enabled(function* only_one_restyling_when_current_time_is_set_to_middle_of_duration() {
+ var div = addDiv(null, { style: 'animation: opacity 100s' });
+ var animation = div.getAnimations()[0];
+
+ yield animation.ready;
+
+ animation.currentTime = 50 * MS_PER_SEC;
+
+ var markers = yield observeStyling(5);
+ is(markers.length, 1,
+ 'Bug 1235478: Animations running on the compositor should only once ' +
+ 'update style when currentTime is set to middle of duration time');
+ yield ensureElementRemoval(div);
+ });
+
+ add_task_if_omta_enabled(function* change_duration_and_currenttime() {
+ var div = addDiv(null);
+ var animation = div.animate({ opacity: [ 0, 1 ] }, 100 * MS_PER_SEC);
+
+ yield animation.ready;
+ ok(animation.isRunningOnCompositor);
+
+ // Set currentTime to a time longer than duration.
+ animation.currentTime = 500 * MS_PER_SEC;
+
+ // Now the animation immediately get back from compositor.
+ ok(!animation.isRunningOnCompositor);
+
+ // Extend the duration.
+ animation.effect.timing.duration = 800 * MS_PER_SEC;
+ var markers = yield observeStyling(5);
+ is(markers.length, 1,
+ 'Animations running on the compositor should update style ' +
+ 'when timing.duration is made longer than the current time');
+
+ yield ensureElementRemoval(div);
+ });
+
+ add_task(function* script_animation_on_display_none_element() {
+ var div = addDiv(null);
+ var animation = div.animate({ backgroundColor: [ 'red', 'blue' ] },
+ 100 * MS_PER_SEC);
+
+ yield animation.ready;
+
+ div.style.display = 'none';
+
+ // We need to wait a frame to apply display:none style.
+ yield waitForFrame();
+
+ is(animation.playState, 'running',
+ 'Script animations keep running even when the target element has ' +
+ '"display: none" style');
+
+ ok(!animation.isRunningOnCompositor,
+ 'Script animations on "display:none" element should not run on the ' +
+ 'compositor');
+
+ var markers = yield observeStyling(5);
+ is(markers.length, 0,
+ 'Script animations on "display: none" element should not update styles');
+
+ div.style.display = '';
+
+ // We need to wait a frame to unapply display:none style.
+ yield waitForFrame();
+
+ var markers = yield observeStyling(5);
+ is(markers.length, 5,
+ 'Script animations restored from "display: none" state should update ' +
+ 'styles');
+
+ yield ensureElementRemoval(div);
+ });
+
+ add_task_if_omta_enabled(function* compositable_script_animation_on_display_none_element() {
+ var div = addDiv(null);
+ var animation = div.animate({ opacity: [ 0, 1 ] }, 100 * MS_PER_SEC);
+
+ yield animation.ready;
+
+ div.style.display = 'none';
+
+ // We need to wait a frame to apply display:none style.
+ yield waitForFrame();
+
+ is(animation.playState, 'running',
+ 'Opacity script animations keep running even when the target element ' +
+ 'has "display: none" style');
+
+ ok(!animation.isRunningOnCompositor,
+ 'Opacity script animations on "display:none" element should not ' +
+ 'run on the compositor');
+
+ var markers = yield observeStyling(5);
+ is(markers.length, 0,
+ 'Opacity script animations on "display: none" element should not ' +
+ 'update styles');
+
+ div.style.display = '';
+
+ // We need to wait a frame to unapply display:none style.
+ yield waitForFrame();
+
+ ok(animation.isRunningOnCompositor,
+ 'Opacity script animations restored from "display: none" should be ' +
+ 'run on the compositor');
+
+ yield ensureElementRemoval(div);
+ });
+
+ add_task(function* restyling_for_empty_keyframes() {
+ var div = addDiv(null);
+ var animation = div.animate({ }, 100 * MS_PER_SEC);
+
+ yield animation.ready;
+ var markers = yield observeStyling(5);
+
+ is(markers.length, 0,
+ 'Animations with no keyframes should not cause restyles');
+
+ animation.effect.setKeyframes({ backgroundColor: ['red', 'blue'] });
+ markers = yield observeStyling(5);
+
+ is(markers.length, 5,
+ 'Setting valid keyframes should cause regular animation restyles to ' +
+ 'occur');
+
+ animation.effect.setKeyframes({ });
+ markers = yield observeStyling(5);
+
+ is(markers.length, 1,
+ 'Setting an empty set of keyframes should trigger a single restyle ' +
+ 'to remove the previous animated style');
+
+ yield ensureElementRemoval(div);
+ });
+
+ add_task_if_omta_enabled(function* no_restyling_when_animation_style_when_re_setting_same_animation_property() {
+ var div = addDiv(null, { style: 'animation: opacity 100s' });
+ var animation = div.getAnimations()[0];
+ yield animation.ready;
+ ok(animation.isRunningOnCompositor);
+ // Apply the same animation style
+ div.style.animation = 'opacity 100s';
+ var markers = yield observeStyling(5);
+ is(markers.length, 0,
+ 'Applying same animation style ' +
+ 'should never cause restyles');
+ yield ensureElementRemoval(div);
+ });
+
+ add_task(function* necessary_update_should_be_invoked() {
+ var div = addDiv(null, { style: 'animation: background-color 100s' });
+ var animation = div.getAnimations()[0];
+ yield animation.ready;
+ yield waitForAnimationFrames(5);
+ // Apply another animation style
+ div.style.animation = 'background-color 110s';
+ var animation = div.getAnimations()[0];
+ var markers = yield observeStyling(5);
+ is(markers.length, 5,
+ 'Applying animation style with different duration ' +
+ 'should cause restyles on every frame.');
+ yield ensureElementRemoval(div);
+ });
+
+ add_task_if_omta_enabled(
+ function* changing_cascading_result_for_main_thread_animation() {
+ var div = addDiv(null, { style: 'background-color: blue' });
+ var animation = div.animate({ opacity: [0, 1],
+ backgroundColor: ['green', 'red'] },
+ 100 * MS_PER_SEC);
+ yield animation.ready;
+ ok(animation.isRunningOnCompositor,
+ 'The opacity animation is running on the compositor');
+ // Make the background-color style as !important to cause an update
+ // to the cascade.
+ // Bug 1300982: The background-color animation should be no longer
+ // running on the main thread.
+ div.style.setProperty('background-color', '1', 'important');
+ var markers = yield observeStyling(5);
+ todo_is(markers.length, 0,
+ 'Changing cascading result for the property running on the main ' +
+ 'thread does not cause synchronization layer of opacity animation ' +
+ 'running on the compositor');
+ yield ensureElementRemoval(div);
+ }
+ );
+
+ add_task(function* restyling_for_animation_on_orphaned_element() {
+ var div = addDiv(null);
+ var animation = div.animate({ marginLeft: [ '0px', '100px' ] },
+ 100 * MS_PER_SEC);
+
+ yield animation.ready;
+
+ div.remove();
+
+ var markers = yield observeStyling(5);
+ is(markers.length, 0,
+ 'Animation on orphaned element should not cause restyles');
+
+ document.body.appendChild(div);
+
+ markers = yield observeStyling(1);
+ // We are observing restyles in rAF callback which is processed before
+ // restyling process in each frame, so in the first frame there should be
+ // no observed restyle since we don't process restyle while the element
+ // is not attached to the document.
+ is(markers.length, 0,
+ 'We observe no restyle in the first frame right after re-atatching ' +
+ 'to the document');
+ markers = yield observeStyling(5);
+ is(markers.length, 5,
+ 'Animation on re-attached to the document begins to update style');
+
+ yield ensureElementRemoval(div);
+ });
+
+ add_task_if_omta_enabled(
+ // Tests that if we remove an element from the document whose animation
+ // cascade needs recalculating, that it is correctly updated when it is
+ // re-attached to the document.
+ function* restyling_for_opacity_animation_on_re_attached_element() {
+ var div = addDiv(null, { style: 'opacity: 1 ! important' });
+ var animation = div.animate({ opacity: [0, 1] }, 100 * MS_PER_SEC);
+
+ yield animation.ready;
+ ok(!animation.isRunningOnCompositor,
+ 'The opacity animation overridden by an !important rule is NOT ' +
+ 'running on the compositor');
+
+ // Drop the !important rule to update the cascade.
+ div.style.setProperty('opacity', '1', '');
+
+ div.remove();
+
+ var markers = yield observeStyling(5);
+ is(markers.length, 0,
+ 'Opacity animation on orphaned element should not cause restyles');
+
+ document.body.appendChild(div);
+
+ // Need a frame to give the animation a chance to be sent to the
+ // compositor.
+ yield waitForFrame();
+
+ ok(animation.isRunningOnCompositor,
+ 'The opacity animation which is no longer overridden by the ' +
+ '!important rule begins running on the compositor even if the ' +
+ '!important rule had been dropped before the target element was ' +
+ 'removed');
+
+ yield ensureElementRemoval(div);
+ }
+ );
+
+});
+
+</script>
+</body>
diff --git a/dom/animation/test/chrome/test_running_on_compositor.html b/dom/animation/test/chrome/test_running_on_compositor.html
new file mode 100644
index 000000000..cd6c679b8
--- /dev/null
+++ b/dom/animation/test/chrome/test_running_on_compositor.html
@@ -0,0 +1,966 @@
+<!doctype html>
+<head>
+<meta charset=utf-8>
+<title>Bug 1045994 - Add a chrome-only property to inspect if an animation is
+ running on the compositor or not</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>
+@keyframes anim {
+ to { transform: translate(100px) }
+}
+@keyframes transform-starts-with-none {
+ 0% { transform: none }
+ 99% { transform: none }
+ 100% { transform: translate(100px) }
+}
+@keyframes opacity {
+ to { opacity: 0 }
+}
+@keyframes background_and_translate {
+ to { background-color: red; transform: translate(100px); }
+}
+@keyframes background {
+ to { background-color: red; }
+}
+@keyframes rotate {
+ from { transform: rotate(0deg); }
+ to { transform: rotate(360deg); }
+}
+@keyframes rotate-and-opacity {
+ from { transform: rotate(0deg); opacity: 1;}
+ to { transform: rotate(360deg); opacity: 0;}
+}
+div {
+ /* Element needs geometry to be eligible for layerization */
+ width: 100px;
+ height: 100px;
+ background-color: white;
+}
+</style>
+</head>
+<body>
+<a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1045994"
+ target="_blank">Mozilla Bug 1045994</a>
+<div id="log"></div>
+<script>
+'use strict';
+
+/** Test for bug 1045994 - Add a chrome-only property to inspect if an
+ animation is running on the compositor or not **/
+
+var omtaEnabled = isOMTAEnabled();
+
+function assert_animation_is_running_on_compositor(animation, desc) {
+ assert_equals(animation.isRunningOnCompositor, omtaEnabled,
+ desc + ' at ' + animation.currentTime + 'ms');
+}
+
+function assert_animation_is_not_running_on_compositor(animation, desc) {
+ assert_equals(animation.isRunningOnCompositor, false,
+ desc + ' at ' + animation.currentTime + 'ms');
+}
+
+promise_test(function(t) {
+ // FIXME: When we implement Element.animate, use that here instead of CSS
+ // so that we remove any dependency on the CSS mapping.
+ var div = addDiv(t, { style: 'animation: anim 100s' });
+ var animation = div.getAnimations()[0];
+
+ return animation.ready.then(function() {
+ assert_animation_is_running_on_compositor(animation,
+ 'Animation reports that it is running on the compositor'
+ + ' during playback');
+
+ div.style.animationPlayState = 'paused';
+
+ return animation.ready;
+ }).then(function() {
+ assert_animation_is_not_running_on_compositor(animation,
+ 'Animation reports that it is NOT running on the compositor'
+ + ' when paused');
+ });
+}, '');
+
+promise_test(function(t) {
+ var div = addDiv(t, { style: 'animation: background 100s' });
+ var animation = div.getAnimations()[0];
+
+ return animation.ready.then(function() {
+ assert_animation_is_not_running_on_compositor(animation,
+ 'Animation reports that it is NOT running on the compositor'
+ + ' for animation of "background"');
+ });
+}, 'isRunningOnCompositor is false for animation of "background"');
+
+promise_test(function(t) {
+ var div = addDiv(t, { style: 'animation: background_and_translate 100s' });
+ var animation = div.getAnimations()[0];
+
+ return animation.ready.then(function() {
+ assert_animation_is_running_on_compositor(animation,
+ 'Animation reports that it is running on the compositor'
+ + ' when the animation has two properties, where one can run'
+ + ' on the compositor, the other cannot');
+ });
+}, 'isRunningOnCompositor is true if the animation has at least one ' +
+ 'property can run on compositor');
+
+promise_test(function(t) {
+ var div = addDiv(t, { style: 'animation: anim 100s' });
+ var animation = div.getAnimations()[0];
+
+ return animation.ready.then(function() {
+ animation.pause();
+ return animation.ready;
+ }).then(function() {
+ assert_animation_is_not_running_on_compositor(animation,
+ 'Animation reports that it is NOT running on the compositor'
+ + ' when animation.pause() is called');
+ });
+}, 'isRunningOnCompositor is false when the animation.pause() is called');
+
+promise_test(function(t) {
+ var div = addDiv(t, { style: 'animation: anim 100s' });
+ var animation = div.getAnimations()[0];
+
+ return animation.ready.then(function() {
+ animation.finish();
+ assert_animation_is_not_running_on_compositor(animation,
+ 'Animation reports that it is NOT running on the compositor'
+ + ' immediately after animation.finish() is called');
+ // Check that we don't set the flag back again on the next tick.
+ return waitForFrame();
+ }).then(function() {
+ assert_animation_is_not_running_on_compositor(animation,
+ 'Animation reports that it is NOT running on the compositor'
+ + ' on the next tick after animation.finish() is called');
+ });
+}, 'isRunningOnCompositor is false when the animation.finish() is called');
+
+promise_test(function(t) {
+ var div = addDiv(t, { style: 'animation: anim 100s' });
+ var animation = div.getAnimations()[0];
+
+ return animation.ready.then(function() {
+ animation.currentTime = 100 * MS_PER_SEC;
+ assert_animation_is_not_running_on_compositor(animation,
+ 'Animation reports that it is NOT running on the compositor'
+ + ' immediately after manually seeking the animation to the end');
+ // Check that we don't set the flag back again on the next tick.
+ return waitForFrame();
+ }).then(function() {
+ assert_animation_is_not_running_on_compositor(animation,
+ 'Animation reports that it is NOT running on the compositor'
+ + ' on the next tick after manually seeking the animation to the end');
+ });
+}, 'isRunningOnCompositor is false when manually seeking the animation to ' +
+ 'the end');
+
+promise_test(function(t) {
+ var div = addDiv(t, { style: 'animation: anim 100s' });
+ var animation = div.getAnimations()[0];
+
+ return animation.ready.then(function() {
+ animation.cancel();
+ assert_animation_is_not_running_on_compositor(animation,
+ 'Animation reports that it is NOT running on the compositor'
+ + ' immediately after animation.cancel() is called');
+ // Check that we don't set the flag back again on the next tick.
+ return waitForFrame();
+ }).then(function() {
+ assert_animation_is_not_running_on_compositor(animation,
+ 'Animation reports that it is NOT running on the compositor'
+ + ' on the next tick after animation.cancel() is called');
+ });
+}, 'isRunningOnCompositor is false when animation.cancel() is called');
+
+// This is to test that we don't simply clobber the flag when ticking
+// animations and then set it again during painting.
+promise_test(function(t) {
+ var div = addDiv(t, { style: 'animation: anim 100s' });
+ var animation = div.getAnimations()[0];
+
+ return animation.ready.then(function() {
+ return new Promise(function(resolve) {
+ window.requestAnimationFrame(function() {
+ t.step(function() {
+ assert_animation_is_running_on_compositor(animation,
+ 'Animation reports that it is running on the compositor'
+ + ' in requestAnimationFrame callback');
+ });
+
+ resolve();
+ });
+ });
+ });
+}, 'isRunningOnCompositor is true in requestAnimationFrame callback');
+
+promise_test(function(t) {
+ var div = addDiv(t, { style: 'animation: anim 100s' });
+ var animation = div.getAnimations()[0];
+
+ return animation.ready.then(function() {
+ return new Promise(function(resolve) {
+ var observer = new MutationObserver(function(records) {
+ var changedAnimation;
+
+ records.forEach(function(record) {
+ changedAnimation =
+ record.changedAnimations.find(function(changedAnim) {
+ return changedAnim == animation;
+ });
+ });
+
+ t.step(function() {
+ assert_true(!!changedAnimation, 'The animation should be recorded '
+ + 'as one of the changedAnimations');
+
+ assert_animation_is_running_on_compositor(animation,
+ 'Animation reports that it is running on the compositor'
+ + ' in MutationObserver callback');
+ });
+
+ resolve();
+ });
+ observer.observe(div, { animations: true, subtree: false });
+ t.add_cleanup(function() {
+ observer.disconnect();
+ });
+ div.style.animationDuration = "200s";
+ });
+ });
+}, 'isRunningOnCompositor is true in MutationObserver callback');
+
+// This is to test that we don't temporarily clear the flag when forcing
+// an unthrottled sample.
+promise_test(function(t) {
+ return new Promise(function(resolve) {
+ // Needs scrollbars to cause overflow.
+ SpecialPowers.pushPrefEnv({ set: [["ui.showHideScrollbars", 1]] },
+ resolve);
+ }).then(function() {
+ var div = addDiv(t, { style: 'animation: rotate 100s' });
+ var animation = div.getAnimations()[0];
+
+ return animation.ready.then(function() {
+ return new Promise(function(resolve) {
+ var timeAtStart = window.performance.now();
+ function handleFrame() {
+ t.step(function() {
+ assert_animation_is_running_on_compositor(animation,
+ 'Animation reports that it is running on the compositor'
+ + ' in requestAnimationFrame callback');
+ });
+
+ // we have to wait at least 200ms because this animation is
+ // unthrottled on every 200ms.
+ // See http://hg.mozilla.org/mozilla-central/file/cafb1c90f794/layout/style/AnimationCommon.cpp#l863
+ if (window.performance.now() - timeAtStart > 200) {
+ resolve();
+ return;
+ }
+ window.requestAnimationFrame(handleFrame);
+ }
+ window.requestAnimationFrame(handleFrame);
+ });
+ });
+ });
+}, 'isRunningOnCompositor remains true in requestAnimationFrameCallback for ' +
+ 'overflow animation');
+
+promise_test(function(t) {
+ var div = addDiv(t, { style: 'transition: opacity 100s; opacity: 1' });
+
+ getComputedStyle(div).opacity;
+
+ div.style.opacity = 0;
+ var animation = div.getAnimations()[0];
+
+ return animation.ready.then(function() {
+ assert_animation_is_running_on_compositor(animation,
+ 'Transition reports that it is running on the compositor'
+ + ' during playback for opacity transition');
+ });
+}, 'isRunningOnCompositor for transitions');
+
+promise_test(function(t) {
+ var div = addDiv(t, { style: 'animation: rotate-and-opacity 100s; ' +
+ 'backface-visibility: hidden; ' +
+ 'transform: none !important;' });
+ var animation = div.getAnimations()[0];
+
+ return animation.ready.then(function() {
+ assert_animation_is_running_on_compositor(animation,
+ 'If an animation has a property that can run on the compositor and a '
+ + 'property that cannot (due to Gecko limitations) but where the latter'
+ + 'property is overridden in the CSS cascade, the animation should '
+ + 'still report that it is running on the compositor');
+ });
+}, 'isRunningOnCompositor is true when a property that would otherwise block ' +
+ 'running on the compositor is overridden in the CSS cascade');
+
+promise_test(function(t) {
+ var animation = addDivAndAnimate(t,
+ {},
+ { opacity: [ 0, 1 ] }, 200 * MS_PER_SEC);
+
+ return animation.ready.then(function() {
+ assert_animation_is_running_on_compositor(animation,
+ 'Animation reports that it is running on the compositor');
+
+ animation.currentTime = 150 * MS_PER_SEC;
+ animation.effect.timing.duration = 100 * MS_PER_SEC;
+
+ assert_animation_is_not_running_on_compositor(animation,
+ 'Animation reports that it is NOT running on the compositor'
+ + ' when the animation is set a shorter duration than current time');
+ });
+}, 'animation is immediately removed from compositor' +
+ 'when timing.duration is made shorter than the current time');
+
+promise_test(function(t) {
+ var animation = addDivAndAnimate(t,
+ {},
+ { opacity: [ 0, 1 ] }, 100 * MS_PER_SEC);
+
+ return animation.ready.then(function() {
+ assert_animation_is_running_on_compositor(animation,
+ 'Animation reports that it is running on the compositor');
+
+ animation.currentTime = 500 * MS_PER_SEC;
+
+ assert_animation_is_not_running_on_compositor(animation,
+ 'Animation reports that it is NOT running on the compositor'
+ + ' when finished');
+
+ animation.effect.timing.duration = 1000 * MS_PER_SEC;
+ return waitForFrame();
+ }).then(function() {
+ assert_animation_is_running_on_compositor(animation,
+ 'Animation reports that it is running on the compositor'
+ + ' when restarted');
+ });
+}, 'animation is added to compositor' +
+ ' when timing.duration is made longer than the current time');
+
+promise_test(function(t) {
+ var animation = addDivAndAnimate(t,
+ {},
+ { opacity: [ 0, 1 ] }, 100 * MS_PER_SEC);
+
+ return animation.ready.then(function() {
+ assert_animation_is_running_on_compositor(animation,
+ 'Animation reports that it is running on the compositor');
+
+ animation.effect.timing.endDelay = 100 * MS_PER_SEC;
+
+ assert_animation_is_running_on_compositor(animation,
+ 'Animation reports that it is running on the compositor'
+ + ' when endDelay is changed');
+
+ animation.currentTime = 110 * MS_PER_SEC;
+ return waitForFrame();
+ }).then(function() {
+ assert_animation_is_not_running_on_compositor(animation,
+ 'Animation reports that it is NOT running on the compositor'
+ + ' when currentTime is during endDelay');
+ });
+}, 'animation is removed from compositor' +
+ ' when current time is made longer than the duration even during endDelay');
+
+promise_test(function(t) {
+ var animation = addDivAndAnimate(t,
+ {},
+ { opacity: [ 0, 1 ] }, 100 * MS_PER_SEC);
+
+ return animation.ready.then(function() {
+ assert_animation_is_running_on_compositor(animation,
+ 'Animation reports that it is running on the compositor');
+
+ animation.effect.timing.endDelay = -200 * MS_PER_SEC;
+ return waitForFrame();
+ }).then(function() {
+ assert_animation_is_not_running_on_compositor(animation,
+ 'Animation reports that it is NOT running on the compositor'
+ + ' when endTime is negative value');
+ });
+}, 'animation is removed from compositor' +
+ ' when endTime is negative value');
+
+promise_test(function(t) {
+ var animation = addDivAndAnimate(t,
+ {},
+ { opacity: [ 0, 1 ] }, 200 * MS_PER_SEC);
+
+ return animation.ready.then(function() {
+ assert_animation_is_running_on_compositor(animation,
+ 'Animation reports that it is running on the compositor');
+
+ animation.effect.timing.endDelay = -100 * MS_PER_SEC;
+ return waitForFrame();
+ }).then(function() {
+ assert_animation_is_running_on_compositor(animation,
+ 'Animation reports that it is running on the compositor'
+ + ' when endTime is positive and endDelay is negative');
+ animation.currentTime = 110 * MS_PER_SEC;
+ return waitForFrame();
+ }).then(function() {
+ assert_animation_is_not_running_on_compositor(animation,
+ 'Animation reports that it is NOT running on the compositor'
+ + ' when currentTime is after endTime');
+ });
+}, 'animation is NOT running on compositor' +
+ ' when endTime is positive and endDelay is negative');
+
+promise_test(function(t) {
+ var effect = new KeyframeEffect(null,
+ { opacity: [ 0, 1 ] },
+ 100 * MS_PER_SEC);
+ var animation = new Animation(effect, document.timeline);
+ animation.play();
+
+ var div = addDiv(t);
+
+ return animation.ready.then(function() {
+ assert_animation_is_not_running_on_compositor(animation,
+ 'Animation with null target reports that it is not running ' +
+ 'on the compositor');
+
+ animation.effect.target = div;
+ return waitForFrame();
+ }).then(function() {
+ assert_animation_is_running_on_compositor(animation,
+ 'Animation reports that it is running on the compositor ' +
+ 'after setting a valid target');
+ });
+}, 'animation is added to the compositor when setting a valid target');
+
+promise_test(function(t) {
+ var div = addDiv(t);
+ var animation = div.animate({ opacity: [ 0, 1 ] }, 100 * MS_PER_SEC);
+
+ return animation.ready.then(function() {
+ assert_animation_is_running_on_compositor(animation,
+ 'Animation reports that it is running on the compositor');
+
+ animation.effect.target = null;
+ assert_animation_is_not_running_on_compositor(animation,
+ 'Animation reports that it is NOT running on the ' +
+ 'compositor after setting null target');
+ });
+}, 'animation is removed from the compositor when setting null target');
+
+promise_test(function(t) {
+ var div = addDiv(t);
+ var animation = div.animate({ opacity: [ 0, 1 ] },
+ { duration: 100 * MS_PER_SEC,
+ delay: 100 * MS_PER_SEC,
+ fill: 'backwards' });
+
+ return animation.ready.then(function() {
+ assert_animation_is_running_on_compositor(animation,
+ 'Animation with fill:backwards in delay phase reports ' +
+ 'that it is running on the compositor');
+
+ animation.currentTime = 100 * MS_PER_SEC;
+ return waitForFrame();
+ }).then(function() {
+ assert_animation_is_running_on_compositor(animation,
+ 'Animation with fill:backwards in delay phase reports ' +
+ 'that it is running on the compositor after delay phase');
+ });
+}, 'animation with fill:backwards in delay phase is running on the ' +
+ ' main-thread while it is in delay phase');
+
+promise_test(function(t) {
+ var div = addDiv(t);
+ var animation = div.animate([{ opacity: 1, offset: 0 },
+ { opacity: 1, offset: 0.99 },
+ { opacity: 0, offset: 1 }], 100 * MS_PER_SEC);
+
+ var another = addDiv(t);
+
+ return animation.ready.then(function() {
+ assert_animation_is_running_on_compositor(animation,
+ 'Opacity animation on a 100% opacity keyframe reports ' +
+ 'that it is running on the compositor from the begining');
+
+ animation.effect.target = another;
+ return waitForFrame();
+ }).then(function() {
+ assert_animation_is_running_on_compositor(animation,
+ 'Opacity animation on a 100% opacity keyframe keeps ' +
+ 'running on the compositor after changing the target ' +
+ 'element');
+ });
+}, '100% opacity animations with keeps running on the ' +
+ 'compositor after changing the target element');
+
+promise_test(function(t) {
+ var div = addDiv(t);
+ var animation = div.animate({ color: ['red', 'black'] }, 100 * MS_PER_SEC);
+
+ return animation.ready.then(function() {
+ assert_animation_is_not_running_on_compositor(animation,
+ 'Color animation reports that it is not running on the ' +
+ 'compositor');
+
+ animation.effect.setKeyframes([{ opacity: 1, offset: 0 },
+ { opacity: 1, offset: 0.99 },
+ { opacity: 0, offset: 1 }]);
+ return waitForFrame();
+ }).then(function() {
+ assert_animation_is_running_on_compositor(animation,
+ '100% opacity animation set by using setKeyframes reports ' +
+ 'that it is running on the compositor');
+ });
+}, '100% opacity animation set up by converting an existing animation with ' +
+ 'cannot be run on the compositor, is running on the compositor');
+
+promise_test(function(t) {
+ var div = addDiv(t);
+ var animation = div.animate({ color: ['red', 'black'] }, 100 * MS_PER_SEC);
+ var effect = new KeyframeEffect(div,
+ [{ opacity: 1, offset: 0 },
+ { opacity: 1, offset: 0.99 },
+ { opacity: 0, offset: 1 }],
+ 100 * MS_PER_SEC);
+
+ return animation.ready.then(function() {
+ assert_animation_is_not_running_on_compositor(animation,
+ 'Color animation reports that it is not running on the ' +
+ 'compositor');
+
+ animation.effect = effect;
+ return waitForFrame();
+ }).then(function() {
+ assert_animation_is_running_on_compositor(animation,
+ '100% opacity animation set up by changing effects reports ' +
+ 'that it is running on the compositor');
+ });
+}, '100% opacity animation set up by changing the effects on an existing ' +
+ 'animation which cannot be run on the compositor, is running on the ' +
+ 'compositor');
+
+promise_test(function(t) {
+ var div = addDiv(t, { style: "opacity: 1 ! important" });
+
+ var animation = div.animate({ opacity: [0, 1] }, 100 * MS_PER_SEC);
+
+ return animation.ready.then(function() {
+ assert_animation_is_not_running_on_compositor(animation,
+ 'Opacity animation on an element which has 100% opacity style with ' +
+ '!important flag reports that it is not running on the compositor');
+ // Clear important flag from the opacity style on the target element.
+ div.style.setProperty("opacity", "1", "");
+ return waitForFrame();
+ }).then(function() {
+ assert_animation_is_running_on_compositor(animation,
+ 'Opacity animation reports that it is running on the compositor after '
+ + 'clearing the !important flag');
+ });
+}, 'Clearing *important* opacity style on the target element sends the ' +
+ 'animation to the compositor');
+
+promise_test(function(t) {
+ var div = addDiv(t);
+ var lowerAnimation = div.animate({ opacity: [1, 0] }, 100 * MS_PER_SEC);
+ var higherAnimation = div.animate({ opacity: [0, 1] }, 100 * MS_PER_SEC);
+
+ return Promise.all([lowerAnimation.ready, higherAnimation.ready]).then(function() {
+ assert_animation_is_running_on_compositor(higherAnimation,
+ 'A higher-priority opacity animation on an element ' +
+ 'reports that it is running on the compositor');
+ assert_animation_is_running_on_compositor(lowerAnimation,
+ 'A lower-priority opacity animation on the same ' +
+ 'element also reports that it is running on the compositor');
+ });
+}, 'Opacity animations on the same element run on the compositor');
+
+promise_test(function(t) {
+ var div = addDiv(t, { style: 'transition: opacity 100s; opacity: 1' });
+
+ getComputedStyle(div).opacity;
+
+ div.style.opacity = 0;
+ getComputedStyle(div).opacity;
+
+ var transition = div.getAnimations()[0];
+ var animation = div.animate({ opacity: [0, 1] }, 100 * MS_PER_SEC);
+
+ return Promise.all([transition.ready, animation.ready]).then(function() {
+ assert_animation_is_running_on_compositor(animation,
+ 'An opacity animation on an element reports that' +
+ 'that it is running on the compositor');
+ assert_animation_is_running_on_compositor(transition,
+ 'An opacity transition on the same element reports that ' +
+ 'it is running on the compositor');
+ });
+}, 'Both of transition and script animation on the same element run on the ' +
+ 'compositor');
+
+promise_test(function(t) {
+ var div = addDiv(t);
+ var importantOpacityElement = addDiv(t, { style: "opacity: 1 ! important" });
+
+ var animation = div.animate({ opacity: [1, 0] }, 100 * MS_PER_SEC);
+
+ return animation.ready.then(function() {
+ assert_animation_is_running_on_compositor(animation,
+ 'Opacity animation on an element reports ' +
+ 'that it is running on the compositor');
+
+ animation.effect.target = null;
+ return waitForFrame();
+ }).then(function() {
+ assert_animation_is_not_running_on_compositor(animation,
+ 'Animation is no longer running on the compositor after ' +
+ 'removing from the element');
+ animation.effect.target = importantOpacityElement;
+ return waitForFrame();
+ }).then(function() {
+ assert_animation_is_not_running_on_compositor(animation,
+ 'Animation is NOT running on the compositor even after ' +
+ 'being applied to a different element which has an ' +
+ '!important opacity declaration');
+ });
+}, 'Animation continues not running on the compositor after being ' +
+ 'applied to an element which has an important declaration and ' +
+ 'having previously been temporarily associated with no target element');
+
+promise_test(function(t) {
+ var div = addDiv(t);
+ var another = addDiv(t);
+
+ var lowerAnimation = div.animate({ opacity: [1, 0] }, 100 * MS_PER_SEC);
+ var higherAnimation = another.animate({ opacity: [0, 1] }, 100 * MS_PER_SEC);
+
+ return Promise.all([lowerAnimation.ready, higherAnimation.ready]).then(function() {
+ assert_animation_is_running_on_compositor(lowerAnimation,
+ 'An opacity animation on an element reports that ' +
+ 'it is running on the compositor');
+ assert_animation_is_running_on_compositor(higherAnimation,
+ 'Opacity animation on a different element reports ' +
+ 'that it is running on the compositor');
+
+ lowerAnimation.effect.target = null;
+ return waitForFrame();
+ }).then(function() {
+ assert_animation_is_not_running_on_compositor(lowerAnimation,
+ 'Animation is no longer running on the compositor after ' +
+ 'being removed from the element');
+ lowerAnimation.effect.target = another;
+ return waitForFrame();
+ }).then(function() {
+ assert_animation_is_running_on_compositor(lowerAnimation,
+ 'A lower-priority animation begins running ' +
+ 'on the compositor after being applied to an element ' +
+ 'which has a higher-priority animation');
+ assert_animation_is_running_on_compositor(higherAnimation,
+ 'A higher-priority animation continues to run on the ' +
+ 'compositor even after a lower-priority animation is ' +
+ 'applied to the same element');
+ });
+}, 'Animation begins running on the compositor after being applied ' +
+ 'to an element which has a higher-priority animation and after ' +
+ 'being temporarily associated with no target element');
+
+promise_test(function(t) {
+ var div = addDiv(t);
+ var another = addDiv(t);
+
+ var lowerAnimation = div.animate({ opacity: [1, 0] }, 100 * MS_PER_SEC);
+ var higherAnimation = another.animate({ opacity: [0, 1] }, 100 * MS_PER_SEC);
+
+ return Promise.all([lowerAnimation.ready, higherAnimation.ready]).then(function() {
+ assert_animation_is_running_on_compositor(lowerAnimation,
+ 'An opacity animation on an element reports that ' +
+ 'it is running on the compositor');
+ assert_animation_is_running_on_compositor(higherAnimation,
+ 'Opacity animation on a different element reports ' +
+ 'that it is running on the compositor');
+
+ higherAnimation.effect.target = null;
+ return waitForFrame();
+ }).then(function() {
+ assert_animation_is_not_running_on_compositor(higherAnimation,
+ 'Animation is no longer running on the compositor after ' +
+ 'being removed from the element');
+ higherAnimation.effect.target = div;
+ return waitForFrame();
+ }).then(function() {
+ assert_animation_is_running_on_compositor(lowerAnimation,
+ 'Animation continues running on the compositor after ' +
+ 'a higher-priority animation applied to the same element');
+ assert_animation_is_running_on_compositor(higherAnimation,
+ 'A higher-priority animation begins to running on the ' +
+ 'compositor after being applied to an element which has ' +
+ 'a lower-priority-animation');
+ });
+}, 'Animation begins running on the compositor after being applied ' +
+ 'to an element which has a lower-priority animation once after ' +
+ 'disassociating with an element');
+
+var delayPhaseTests = [
+ {
+ desc: 'script animation of opacity',
+ setupAnimation: function(t) {
+ return addDiv(t).animate(
+ { opacity: [0, 1] },
+ { delay: 100 * MS_PER_SEC, duration: 100 * MS_PER_SEC });
+ },
+ },
+ {
+ desc: 'script animation of transform',
+ setupAnimation: function(t) {
+ return addDiv(t).animate(
+ { transform: ['translateX(0px)', 'translateX(100px)'] },
+ { delay: 100 * MS_PER_SEC, duration: 100 * MS_PER_SEC });
+ },
+ },
+ {
+ desc: 'CSS animation of opacity',
+ setupAnimation: function(t) {
+ return addDiv(t, { style: 'animation: opacity 100s 100s' })
+ .getAnimations()[0];
+ },
+ },
+ {
+ desc: 'CSS animation of transform',
+ setupAnimation: function(t) {
+ return addDiv(t, { style: 'animation: anim 100s 100s' })
+ .getAnimations()[0];
+ },
+ },
+ {
+ desc: 'CSS transition of opacity',
+ setupAnimation: function(t) {
+ var div = addDiv(t, { style: 'transition: opacity 100s 100s' });
+ getComputedStyle(div).opacity;
+
+ div.style.opacity = 0;
+ return div.getAnimations()[0];
+ },
+ },
+ {
+ desc: 'CSS transition of transform',
+ setupAnimation: function(t) {
+ var div = addDiv(t, { style: 'transition: transform 100s 100s' });
+ getComputedStyle(div).transform;
+
+ div.style.transform = 'translateX(100px)';
+ return div.getAnimations()[0];
+ },
+ },
+];
+
+delayPhaseTests.forEach(function(test) {
+ promise_test(function(t) {
+ var animation = test.setupAnimation(t);
+
+ return animation.ready.then(function() {
+ assert_animation_is_running_on_compositor(animation,
+ test.desc + ' reports that it is running on the '
+ + 'compositor even though it is in the delay phase');
+ });
+ }, 'isRunningOnCompositor for ' + test.desc + ' is true even though ' +
+ 'it is in the delay phase');
+});
+
+// The purpose of thie test cases is to check that
+// NS_FRAME_MAY_BE_TRANSFORMED flag on the associated nsIFrame persists
+// after transform style on the frame is removed.
+var delayPhaseWithTransformStyleTests = [
+ {
+ desc: 'script animation of transform with transform style',
+ setupAnimation: function(t) {
+ return addDiv(t, { style: 'transform: translateX(10px)' }).animate(
+ { transform: ['translateX(0px)', 'translateX(100px)'] },
+ { delay: 100 * MS_PER_SEC, duration: 100 * MS_PER_SEC });
+ },
+ },
+ {
+ desc: 'CSS animation of transform with transform style',
+ setupAnimation: function(t) {
+ return addDiv(t, { style: 'animation: anim 100s 100s;' +
+ 'transform: translateX(10px)' })
+ .getAnimations()[0];
+ },
+ },
+ {
+ desc: 'CSS transition of transform with transform style',
+ setupAnimation: function(t) {
+ var div = addDiv(t, { style: 'transition: transform 100s 100s;' +
+ 'transform: translateX(10px)'});
+ getComputedStyle(div).transform;
+
+ div.style.transform = 'translateX(100px)';
+ return div.getAnimations()[0];
+ },
+ },
+];
+
+delayPhaseWithTransformStyleTests.forEach(function(test) {
+ promise_test(function(t) {
+ var animation = test.setupAnimation(t);
+
+ return animation.ready.then(function() {
+ assert_animation_is_running_on_compositor(animation,
+ test.desc + ' reports that it is running on the '
+ + 'compositor even though it is in the delay phase');
+ }).then(function() {
+ // Remove the initial transform style during delay phase.
+ animation.effect.target.style.transform = 'none';
+ return animation.ready;
+ }).then(function() {
+ assert_animation_is_running_on_compositor(animation,
+ test.desc + ' reports that it keeps running on the '
+ + 'compositor after removing the initial transform style');
+ });
+ }, 'isRunningOnCompositor for ' + test.desc + ' is true after removing ' +
+ 'the initial transform style during the delay phase');
+});
+
+var startsWithNoneTests = [
+ {
+ desc: 'script animation of transform starts with transform:none segment',
+ setupAnimation: function(t) {
+ return addDiv(t).animate(
+ { transform: ['none', 'none', 'translateX(100px)'] }, 100 * MS_PER_SEC);
+ },
+ },
+ {
+ desc: 'CSS animation of transform starts with transform:none segment',
+ setupAnimation: function(t) {
+ return addDiv(t,
+ { style: 'animation: transform-starts-with-none 100s 100s' })
+ .getAnimations()[0];
+ },
+ },
+];
+
+startsWithNoneTests.forEach(function(test) {
+ promise_test(function(t) {
+ var animation = test.setupAnimation(t);
+
+ return animation.ready.then(function() {
+ assert_animation_is_running_on_compositor(animation,
+ test.desc + ' reports that it is running on the '
+ + 'compositor even though it is in transform:none segment');
+ });
+ }, 'isRunningOnCompositor for ' + test.desc + ' is true even though ' +
+ 'it is in transform:none segment');
+});
+
+promise_test(function(t) {
+ var div = addDiv(t, { style: 'opacity: 1 ! important' });
+
+ var animation = div.animate(
+ { opacity: [0, 1] },
+ { delay: 100 * MS_PER_SEC, duration: 100 * MS_PER_SEC });
+
+ return animation.ready.then(function() {
+ assert_animation_is_not_running_on_compositor(animation,
+ 'Opacity animation on an element which has opacity:1 important style'
+ + 'reports that it is not running on the compositor');
+ // Clear the opacity style on the target element.
+ div.style.setProperty("opacity", "1", "");
+ return waitForFrame();
+ }).then(function() {
+ assert_animation_is_running_on_compositor(animation,
+ 'Opacity animations reports that it is running on the compositor after '
+ + 'clearing the opacity style on the element');
+ });
+}, 'Clearing *important* opacity style on the target element sends the ' +
+ 'animation to the compositor even if the animation is in the delay phase');
+
+promise_test(function(t) {
+ var opaqueDiv = addDiv(t, { style: 'opacity: 1 ! important' });
+ var anotherDiv = addDiv(t);
+
+ var animation = opaqueDiv.animate(
+ { opacity: [0, 1] },
+ { delay: 100 * MS_PER_SEC, duration: 100 * MS_PER_SEC });
+
+ return animation.ready.then(function() {
+ assert_animation_is_not_running_on_compositor(animation,
+ 'Opacity animation on an element which has opacity:1 important style'
+ + 'reports that it is not running on the compositor');
+ // Changing target element to another element which has no opacity style.
+ animation.effect.target = anotherDiv;
+ return waitForFrame();
+ }).then(function() {
+ assert_animation_is_running_on_compositor(animation,
+ 'Opacity animations reports that it is running on the compositor after '
+ + 'changing the target element to another elemenent having no '
+ + 'opacity style');
+ });
+}, 'Changing target element of opacity animation sends the animation to the ' +
+ 'the compositor even if the animation is in the delay phase');
+
+promise_test(function(t) {
+ var animation =
+ addDivAndAnimate(t,
+ {},
+ { width: ['100px', '200px'] },
+ { duration: 100 * MS_PER_SEC, delay: 100 * MS_PER_SEC });
+
+ return animation.ready.then(function() {
+ assert_animation_is_not_running_on_compositor(animation,
+ 'Width animation reports that it is not running on the compositor '
+ + 'in the delay phase');
+ // Changing to property runnable on the compositor.
+ animation.effect.setKeyframes({ opacity: [0, 1] });
+ return waitForFrame();
+ }).then(function() {
+ assert_animation_is_running_on_compositor(animation,
+ 'Opacity animation reports that it is running on the compositor '
+ + 'after changing the property from width property in the delay phase');
+ });
+}, 'Dynamic change to a property runnable on the compositor ' +
+ 'in the delay phase');
+
+promise_test(function(t) {
+ var div = addDiv(t, { style: 'transition: opacity 100s; ' +
+ 'opacity: 0 !important' });
+ getComputedStyle(div).opacity;
+
+ div.style.setProperty('opacity', '1', 'important');
+ getComputedStyle(div).opacity;
+
+ var animation = div.getAnimations()[0];
+
+ return animation.ready.then(function() {
+ assert_animation_is_running_on_compositor(animation,
+ 'Transition reports that it is running on the compositor even if the ' +
+ 'property is overridden by an !important rule');
+ });
+}, 'Transitions override important rules');
+
+promise_test(function(t) {
+ var div = addDiv(t, { style: 'transition: opacity 100s; ' +
+ 'opacity: 0 !important' });
+ getComputedStyle(div).opacity;
+
+ div.animate({ opacity: [ 0, 1 ] }, 100 * MS_PER_SEC);
+
+ div.style.setProperty('opacity', '1', 'important');
+ getComputedStyle(div).opacity;
+
+ var [transition, animation] = div.getAnimations();
+
+ return Promise.all([transition.ready, animation.ready]).then(function() {
+ assert_animation_is_not_running_on_compositor(transition,
+ 'Transition suppressed by an animation which is overridden by an ' +
+ '!important rule reports that it is NOT running on the compositor');
+ assert_animation_is_not_running_on_compositor(animation,
+ 'Animation overridden by an !important rule reports that it is ' +
+ 'NOT running on the compositor');
+ });
+}, 'Neither transition nor animation does run on the compositor if the ' +
+ 'property is overridden by an !important rule');
+
+</script>
+</body>