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.html18
-rw-r--r--dom/animation/test/chrome/test_animate_xrays.html40
-rw-r--r--dom/animation/test/chrome/test_animation_observers_async.html654
-rw-r--r--dom/animation/test/chrome/test_animation_observers_sync.html1587
-rw-r--r--dom/animation/test/chrome/test_animation_performance_warning.html1693
-rw-r--r--dom/animation/test/chrome/test_animation_properties.html837
-rw-r--r--dom/animation/test/chrome/test_animation_properties_display.html34
-rw-r--r--dom/animation/test/chrome/test_cssanimation_missing_keyframes.html66
-rw-r--r--dom/animation/test/chrome/test_generated_content_getAnimations.html81
-rw-r--r--dom/animation/test/chrome/test_keyframe_effect_xrays.html45
-rw-r--r--dom/animation/test/chrome/test_mutation_observer_for_element_removal_in_shadow_tree.html45
-rw-r--r--dom/animation/test/chrome/test_running_on_compositor.html1656
12 files changed, 6756 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 0000000000..2fa15b1764
--- /dev/null
+++ b/dom/animation/test/chrome/file_animate_xrays.html
@@ -0,0 +1,18 @@
+<!doctype html>
+<html>
+<head>
+<meta charset=utf-8>
+<script>
+Element.prototype.animate = function() {
+ throw 'Called animate() as defined in content document';
+}
+for (let name of ["KeyframeEffect", "Animation"]) {
+ this[name] = function() {
+ throw `Called overridden ${name} 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 0000000000..64df6db720
--- /dev/null
+++ b/dom/animation/test/chrome/test_animate_xrays.html
@@ -0,0 +1,40 @@
+<!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=1414674"
+ target="_blank">Mozilla Bug 1414674</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: [ 1, 0 ]}, 100 * MS_PER_SEC);
+ // The frames object should be accessible via x-ray.
+ var frames = anim.effect.getKeyframes();
+ assert_equals(frames.length, 2,
+ "frames for Element.animate should be non-zero");
+ assert_equals(frames[0].opacity, "1",
+ "first frame opacity for Element.animate should be specified value");
+ assert_equals(frames[0].computedOffset, 0,
+ "first frame offset for Element.animate should be 0");
+ assert_equals(frames[1].opacity, "0",
+ "last frame opacity for Element.animate should be specified value");
+ assert_equals(frames[1].computedOffset, 1,
+ "last frame offset for Element.animate should be 1");
+ t.done();
+ }));
+}, 'Calling animate() across x-rays');
+
+</script>
+</body>
diff --git a/dom/animation/test/chrome/test_animation_observers_async.html b/dom/animation/test/chrome/test_animation_observers_async.html
new file mode 100644
index 0000000000..912d73a896
--- /dev/null
+++ b/dom/animation/test/chrome/test_animation_observers_async.html
@@ -0,0 +1,654 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title>
+Test chrome-only MutationObserver animation notifications (async tests)
+</title>
+<!--
+
+ This file contains tests for animation mutation observers that require
+ some asynchronous steps (e.g. waiting for animation events).
+
+ Where possible, however, we prefer to write synchronous tests since they are
+ less to timeout when run on automation. These synchronous tests are located
+ in test_animation_observers_sync.html.
+
+-->
+<script type="application/javascript" src="../testharness.js"></script>
+<script type="application/javascript" src="../testharnessreport.js"></script>
+<script src="../testcommon.js"></script>
+<div id="log"></div>
+<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(newRecords => {
+ gRecords.push(...newRecords);
+});
+
+function setupAsynchronousObserver(t, options) {
+
+ gRecords = [];
+ t.add_cleanup(() => {
+ gObserver.disconnect();
+ });
+ gObserver.observe(options.subtree ? div.parentNode : div,
+ { animations: true, subtree: options.subtree });
+}
+
+// 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(aResolve => {
+ function listener(aEvent) {
+ aElement.removeEventListener(aEventName, listener);
+ aResolve();
+ }
+ aElement.addEventListener(aEventName, listener);
+ });
+}
+
+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_records(expected, desc) {
+ var records = gRecords;
+ gRecords = [];
+ assert_equals(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");
+ }
+}
+
+function assert_records_any_order(expected, desc) {
+ // Generate a unique label for each Animation object.
+ let animation_labels = new Map();
+ let animation_counter = 0;
+ for (let record of gRecords) {
+ for (let a of [...record.addedAnimations, ...record.changedAnimations, ...record.removedAnimations]) {
+ if (!animation_labels.has(a)) {
+ animation_labels.set(a, ++animation_counter);
+ }
+ }
+ }
+ for (let record of expected) {
+ for (let a of [...record.added, ...record.changed, ...record.removed]) {
+ if (!animation_labels.has(a)) {
+ animation_labels.set(a, ++animation_counter);
+ }
+ }
+ }
+
+ function record_label(record) {
+ // Generate a label of the form:
+ //
+ // <added-animations>:<changed-animations>:<removed-animations>
+ let added = record.addedAnimations || record.added;
+ let changed = record.changedAnimations || record.changed;
+ let removed = record.removedAnimations || record.removed;
+ return [added .map(a => animation_labels.get(a)).sort().join(),
+ changed.map(a => animation_labels.get(a)).sort().join(),
+ removed.map(a => animation_labels.get(a)).sort().join()]
+ .join(":");
+ }
+
+ // Sort records by their label.
+ gRecords.sort((a, b) => record_label(a) < record_label(b));
+ expected.sort((a, b) => record_label(a) < record_label(b));
+
+ // Assert the sorted record lists are equal.
+ assert_records(expected, desc);
+}
+
+// -- 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.
+
+function runTest() {
+ [
+ { observe: div, target: div, subtree: false },
+ { observe: div.parentNode, target: div, subtree: true },
+ ].forEach(aOptions => {
+
+ var e = aOptions.target;
+
+ promise_test(t => {
+ setupAsynchronousObserver(t, aOptions);
+ // Clear all styles once test finished since we re-use the same element
+ // in all test cases.
+ t.add_cleanup(() => {
+ e.style = "";
+ flushComputedStyle(e);
+ });
+
+ // 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();
+ assert_equals(animations.length, 1,
+ "getAnimations().length after transition start");
+
+ // Wait for the single MutationRecord for the Animation addition to
+ // be delivered.
+ return waitForFrame().then(() => {
+ 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;
+ }).then(() => {
+ return transitionEnd;
+ }).then(() => {
+ // After the transition has finished, the Animation should disappear.
+ assert_equals(e.getAnimations().length, 0,
+ "getAnimations().length after transition end");
+
+ // Wait for the change MutationRecord for seeking the Animation to be
+ // delivered, followed by the removal MutationRecord.
+ return waitForFrame();
+ }).then(() => {
+ assert_records([{ added: [], changed: animations, removed: [] },
+ { added: [], changed: [], removed: animations }],
+ "records after transition end");
+ });
+ }, `single_transition ${aOptions.subtree ? ': subtree' : ''}`);
+
+ // Test that starting a single animation that completes normally
+ // dispatches an added notification and then a removed notification.
+ promise_test(t => {
+ setupAsynchronousObserver(t, aOptions);
+ t.add_cleanup(() => {
+ e.style = "";
+ flushComputedStyle(e);
+ });
+
+ // 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();
+ assert_equals(animations.length, 1,
+ "getAnimations().length after animation start");
+
+ // Wait for the single MutationRecord for the Animation addition to
+ // be delivered.
+ return waitForFrame().then(() => {
+ 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;
+ return animationEnd;
+ }).then(() => {
+ // After the animation has finished, the Animation should disappear.
+ assert_equals(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.
+ return waitForFrame();
+ }).then(() => {
+ assert_records([{ added: [], changed: animations, removed: [] },
+ { added: [], changed: [], removed: animations }],
+ "records after animation end");
+ });
+ }, `single_animation ${aOptions.subtree ? ': subtree' : ''}`);
+
+ // 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.
+ promise_test(t => {
+ setupAsynchronousObserver(t, aOptions);
+ t.add_cleanup(() => {
+ e.style = "";
+ flushComputedStyle(e);
+ });
+
+ // 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();
+ assert_equals(animations.length, 1,
+ "getAnimations().length after animation start");
+
+ // Wait for the single MutationRecord for the Animation addition to
+ // be delivered.
+ return waitForFrame().then(() => {
+ 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;
+ return animationEnd;
+ }).then(() => {
+ // 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";
+ // Explicitly flush style to make sure the above style change happens.
+ // Normally we don't need explicit style flush if there is a waitForFrame()
+ // call but in this particular case we are in the middle of animation events'
+ // callback handling and requestAnimationFrame handling so that we have no
+ // chance to process styling even after the requestAnimationFrame handling.
+ flushComputedStyle(e);
+
+ // Wait for the single MutationRecord for the Animation removal to
+ // be delivered.
+ return waitForFrame();
+ }).then(() => {
+ assert_records([{ added: [], changed: [], removed: animations }],
+ "records after animation end");
+ });
+ }, `single_animation_cancelled_fill ${aOptions.subtree ? ': subtree' : ''}`);
+
+ // Test that calling finish() on a paused (but otherwise finished) animation
+ // dispatches a changed notification.
+ promise_test(t => {
+ setupAsynchronousObserver(t, aOptions);
+ t.add_cleanup(() => {
+ e.style = "";
+ flushComputedStyle(e);
+ });
+
+ // Start a long animation
+ e.style = "animation: anim 100s forwards";
+
+ // The animation should cause the creation of a single Animation.
+ var animations = e.getAnimations();
+ assert_equals(animations.length, 1,
+ "getAnimations().length after animation start");
+
+ // Wait for the single MutationRecord for the Animation addition to
+ // be delivered.
+ return waitForFrame().then(() => {
+ assert_records([{ added: animations, changed: [], removed: [] }],
+ "records after animation start");
+
+ // Wait until the animation is playing.
+ return animations[0].ready;
+ }).then(() => {
+ // Finish and pause.
+ animations[0].finish();
+ animations[0].pause();
+
+ // Wait for the pause to complete.
+ return animations[0].ready;
+ }).then(() => {
+ assert_true(
+ !animations[0].pending && 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();
+ assert_equals(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.
+ return waitForFrame();
+ }).then(() => {
+ assert_records([{ added: [], changed: animations, removed: [] }],
+ "records after finish() and pause()");
+
+ // Cancel the animation.
+ e.style = "";
+
+ // Wait for the single removal notification.
+ return waitForFrame();
+ }).then(() => {
+ assert_records([{ added: [], changed: [], removed: animations }],
+ "records after animation end");
+ });
+ }, `finish_from_pause ${aOptions.subtree ? ': subtree' : ''}`);
+
+ // Test that calling play() on a paused Animation dispatches a changed
+ // notification.
+ promise_test(t => {
+ setupAsynchronousObserver(t, aOptions);
+ t.add_cleanup(() => {
+ e.style = "";
+ flushComputedStyle(e);
+ });
+
+ // 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();
+ assert_equals(animations.length, 1,
+ "getAnimations().length after animation start");
+
+ // Wait for the single MutationRecord for the Animation addition to
+ // be delivered.
+ return waitForFrame().then(() => {
+ assert_records([{ added: animations, changed: [], removed: [] }],
+ "records after animation start");
+
+ // Wait until the animation is ready
+ return animations[0].ready;
+ }).then(() => {
+ // Play
+ animations[0].play();
+
+ // Wait for the single MutationRecord for the Animation change to
+ // be delivered.
+ return animations[0].ready;
+ }).then(() => {
+ assert_records([{ added: [], changed: animations, removed: [] }],
+ "records after play()");
+
+ // Redundant play
+ animations[0].play();
+
+ // Wait to ensure no change is dispatched
+ return waitForFrame();
+ }).then(() => {
+ assert_records([], "records after redundant play()");
+
+ // Cancel the animation.
+ e.style = "";
+
+ // Wait for the single removal notification.
+ return waitForFrame();
+ }).then(() => {
+ assert_records([{ added: [], changed: [], removed: animations }],
+ "records after animation end");
+ });
+ }, `play ${aOptions.subtree ? ': subtree' : ''}`);
+
+ // Test that a non-cancelling change to an animation followed immediately by a
+ // cancelling change will only send an animation removal notification.
+ promise_test(t => {
+ setupAsynchronousObserver(t, aOptions);
+ t.add_cleanup(() => {
+ e.style = "";
+ flushComputedStyle(e);
+ });
+
+ // Start a long animation.
+ e.style = "animation: anim 100s;";
+
+ // The animation should cause the creation of a single Animation.
+ var animations = e.getAnimations();
+ assert_equals(animations.length, 1,
+ "getAnimations().length after animation start");
+
+ // Wait for the single MutationRecord for the Animation addition to
+ // be delivered.
+ return waitForFrame().then(() => {;
+ 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.
+ return waitForFrame();
+ }).then(() => {
+ assert_records([{ added: [], changed: [], removed: animations }],
+ "records after animation end");
+ });
+ }, `coalesce_change_cancel ${aOptions.subtree ? ': subtree' : ''}`);
+
+ });
+}
+
+promise_test(async t => {
+ setupAsynchronousObserver(t, { observe: div, subtree: true });
+ t.add_cleanup(() => {
+ div.style = "";
+ flushComputedStyle(div);
+ });
+
+ // 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; content: '';",
+ ".after::after" : "animation: anim 100s, anim 100s; " +
+ "content: '';"};
+ 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();
+ assert_equals(docAnims.length, 10, "total animations");
+
+ var divAnimations = div.getAnimations();
+ var childAAnimations = childA.getAnimations();
+ var childBAnimations = childB.getAnimations();
+
+ var divBeforeAnimations =
+ docAnims.filter(x => (x.effect.target == div &&
+ x.effect.pseudoElement == "::before"));
+ var divAfterAnimations =
+ docAnims.filter(x => (x.effect.target == div &&
+ x.effect.pseudoElement == "::after"));
+ var childBPseudoAnimations =
+ docAnims.filter(x => (x.effect.target == childB &&
+ x.effect.pseudoElement == "::before"));
+
+ var seekRecords;
+ // 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
+ // right records are generated, but we allow them to occur in any order.
+ await waitForFrame();
+
+ assert_records_any_order(
+ [{ 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 ];
+
+ await Promise.all(animations.map(animation => animation.ready));
+
+ // 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.
+ seekRecords = animations.map(
+ p => ({ added: [], changed: [p], removed: [] })
+ );
+
+ await Promise.all(animations.map(animation => animation.finished));
+
+ // 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();
+}, "tree_ordering: subtree");
+
+// Test that animations removed by auto-removal trigger an event
+promise_test(async t => {
+ setupAsynchronousObserver(t, { observe: div, subtree: false });
+
+ // Start two animations such that one will be auto-removed
+ const animA = div.animate(
+ { opacity: 1 },
+ { duration: 100 * MS_PER_SEC, fill: 'forwards' }
+ );
+ const animB = div.animate(
+ { opacity: 1 },
+ { duration: 100 * MS_PER_SEC, fill: 'forwards' }
+ );
+
+ // Wait for the MutationRecords corresponding to each addition.
+ await waitForNextFrame();
+
+ assert_records(
+ [
+ { added: [animA], changed: [], removed: [] },
+ { added: [animB], changed: [], removed: [] },
+ ],
+ 'records after animation start'
+ );
+
+ // Finish the animations -- this should cause animA to be replaced, and
+ // automatically removed.
+ animA.finish();
+ animB.finish();
+
+ // Wait for the MutationRecords corresponding to the timing changes and the
+ // subsequent removal to be delivered.
+ await waitForNextFrame();
+
+ assert_records(
+ [
+ { added: [], changed: [animA], removed: [] },
+ { added: [], changed: [animB], removed: [] },
+ { added: [], changed: [], removed: [animA] },
+ ],
+ 'records after finishing'
+ );
+
+ // Restore animA.
+ animA.persist();
+
+ // Wait for the MutationRecord corresponding to the re-addition of animA.
+ await waitForNextFrame();
+
+ assert_records(
+ [{ added: [animA], changed: [], removed: [] }],
+ 'records after persisting'
+ );
+
+ // Tidy up
+ animA.cancel();
+ animB.cancel();
+
+ await waitForNextFrame();
+
+ assert_records(
+ [
+ { added: [], changed: [], removed: [animA] },
+ { added: [], changed: [], removed: [animB] },
+ ],
+ 'records after tidying up end'
+ );
+}, 'Animations automatically removed are reported');
+runTest();
+</script>
diff --git a/dom/animation/test/chrome/test_animation_observers_sync.html b/dom/animation/test/chrome/test_animation_observers_sync.html
new file mode 100644
index 0000000000..ec760031e1
--- /dev/null
+++ b/dom/animation/test/chrome/test_animation_observers_sync.html
@@ -0,0 +1,1587 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title>
+Test chrome-only MutationObserver animation notifications (sync tests)
+</title>
+<!--
+
+ This file contains synchronous tests for animation mutation observers.
+
+ In general we prefer to write synchronous tests since they are less likely to
+ timeout when run on automation. Tests that require asynchronous steps (e.g.
+ waiting on events) should be added to test_animations_observers_async.html
+ instead.
+
+-->
+<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>
+
+/**
+ * Return a new MutationObserver which observing |target| element
+ * with { animations: true, subtree: |subtree| } option.
+ *
+ * NOTE: This observer should be used only with takeRecords(). If any of
+ * MutationRecords are observed in the callback of the MutationObserver,
+ * it will raise an assertion.
+ */
+function setupSynchronousObserver(t, target, subtree) {
+ var observer = new MutationObserver(records => {
+ assert_unreached("Any MutationRecords should not be observed in this " +
+ "callback");
+ });
+ t.add_cleanup(() => {
+ observer.disconnect();
+ });
+ observer.observe(target, { animations: true, subtree });
+ return observer;
+}
+
+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");
+ }
+}
+
+function runTest() {
+ [ { 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.updateTiming({ duration: 100 * MS_PER_SEC });
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [anim], removed: [] }],
+ "records after duration is changed");
+
+ anim.effect.updateTiming({ duration: 100 * MS_PER_SEC });
+ assert_equals_records(observer.takeRecords(),
+ [], "records after assigning same value");
+
+ anim.currentTime = anim.effect.getComputedTiming().duration * 2;
+ anim.finish();
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [], removed: [anim] }],
+ "records after animation end");
+
+ anim.effect.updateTiming({
+ duration: anim.effect.getComputedTiming().duration * 3
+ });
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [anim], changed: [], removed: [] }],
+ "records after animation restarted");
+
+ anim.effect.updateTiming({ duration: 'auto' });
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [], removed: [anim] }],
+ "records after duration set \"auto\"");
+
+ anim.effect.updateTiming({ 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.updateTiming({ endDelay: 10 * MS_PER_SEC });
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [anim], removed: [] }],
+ "records after endDelay is changed");
+
+ anim.effect.updateTiming({ 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.updateTiming({ 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.updateTiming({ iterations: 2 });
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [anim], removed: [] }],
+ "records after iterations is changed");
+
+ anim.effect.updateTiming({ iterations: 2 });
+ assert_equals_records(observer.takeRecords(),
+ [], "records after assigning same value");
+
+ anim.effect.updateTiming({ iterations: 0 });
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [], removed: [anim] }],
+ "records after animation end");
+
+ anim.effect.updateTiming({ 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.updateTiming({ delay: 100 });
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [anim], removed: [] }],
+ "records after delay is changed");
+
+ anim.effect.updateTiming({ delay: 100 });
+ assert_equals_records(observer.takeRecords(),
+ [], "records after assigning same value");
+
+ anim.effect.updateTiming({ delay: -100 * MS_PER_SEC });
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [], removed: [anim] }],
+ "records after animation end");
+
+ anim.effect.updateTiming({ 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.updateTiming({ easing: "steps(2, end)" });
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [anim], removed: [] }],
+ "records after easing is changed");
+
+ anim.effect.updateTiming({ 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 KeyframeEffect(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({ }, 100 * MS_PER_SEC);
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [anim], changed: [], removed: [] }],
+ "records after animation is added");
+
+ anim.effect.composite = "add";
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [anim], removed: [] }],
+ "records after composite is changed");
+
+ anim.effect.composite = "add";
+ assert_equals_records(observer.takeRecords(),
+ [], "no record after setting the same composite");
+
+ }, "set_composite");
+
+ // 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(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.
+ // eslint-disable-next-line no-self-assign
+ 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.
+ // eslint-disable-next-line no-self-assign
+ 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 reverse() on an Animation does *not* dispatch a changed
+ // notification when it throws an exception.
+ test(t => {
+ // Start an infinite animation
+ var div = addDiv(t, { style: "animation: anim 10s infinite" });
+ 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");
+
+ // Shift the animation into the future such that when we call reverse
+ // it will try to seek to the (infinite) end.
+ animations[0].startTime = 100 * MS_PER_SEC;
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: animations, removed: [] }],
+ "records after adjusting startTime");
+
+ // Reverse: should throw
+ assert_throws('InvalidStateError', () => {
+ animations[0].reverse();
+ }, 'reverse() on future infinite animation throws an exception');
+
+ // We should get no notifications.
+ assert_equals_records(observer.takeRecords(),
+ [], "records after calling reverse()");
+ }, "reverse_with_exception");
+
+ // 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,
+ aOptions.subtree ? div.parentNode : div,
+ aOptions.subtree);
+
+ var anim = div.animate({ opacity: [ 0, 1 ] },
+ { duration: 100 * MS_PER_SEC,
+ iterationComposite: 'replace' });
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [anim], changed: [], removed: [] }],
+ "records after animation is added");
+
+ anim.effect.iterationComposite = 'accumulate';
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [anim], removed: [] }],
+ "records after iterationComposite is changed");
+
+ anim.effect.iterationComposite = 'accumulate';
+ assert_equals_records(observer.takeRecords(),
+ [], "no record after setting the same iterationComposite");
+
+ }, "set_iterationComposite");
+
+ 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.setKeyframes({ opacity: 0.1 });
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [anim], removed: [] }],
+ "records after keyframes are changed");
+
+ anim.effect.setKeyframes({ opacity: 0.1 });
+ assert_equals_records(observer.takeRecords(),
+ [], "no record after setting the same keyframes");
+
+ anim.effect.setKeyframes(null);
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [anim], removed: [] }],
+ "records after keyframes are set to empty");
+
+ }, "set_keyframes");
+
+ // Test that starting a single transition that is cancelled by resetting
+ // the transition-property property dispatches an added notification and
+ // then a removed notification.
+ test(t => {
+ var div =
+ addDiv(t, { style: "transition: background-color 100s; " +
+ "background-color: yellow;" });
+ var observer =
+ setupSynchronousObserver(t,
+ aOptions.subtree ? div.parentNode : div,
+ aOptions.subtree);
+
+ getComputedStyle(div).transitionProperty;
+ div.style.backgroundColor = "lime";
+
+ // The transition should cause the creation of a single Animation.
+ var animations = div.getAnimations();
+ assert_equals(animations.length, 1,
+ "getAnimations().length after transition start");
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: animations, changed: [], removed: [] }],
+ "records after transition start");
+
+ // Cancel the transition by setting transition-property.
+ div.style.transitionProperty = "none";
+ getComputedStyle(div).transitionProperty;
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [], removed: animations }],
+ "records after transition end");
+ }, "single_transition_cancelled_property");
+
+ // 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.
+ test(t => {
+ // A long transition with a predictable value.
+ var div =
+ addDiv(t, { style: "transition: z-index 100s -51s; " +
+ "z-index: 10;" });
+ var observer =
+ setupSynchronousObserver(t,
+ aOptions.subtree ? div.parentNode : div,
+ aOptions.subtree);
+ getComputedStyle(div).transitionProperty;
+ div.style.zIndex = "100";
+
+ // The transition should cause the creation of a single Animation.
+ var animations = div.getAnimations();
+ assert_equals(animations.length, 1,
+ "getAnimations().length after transition start");
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: animations, changed: [], removed: [] }],
+ "records after transition start");
+
+ // Cancel the transition by setting the current animation value.
+ let value = "83";
+ assert_equals(getComputedStyle(div).zIndex, value,
+ "half-way transition value");
+ div.style.zIndex = value;
+ getComputedStyle(div).transitionProperty;
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [], removed: animations }],
+ "records after transition end");
+ }, "single_transition_cancelled_value");
+
+ // 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.
+ test(t => {
+ var div =
+ addDiv(t, { style: "transition: line-height 100s; " +
+ "line-height: 16px;" });
+ var observer =
+ setupSynchronousObserver(t,
+ aOptions.subtree ? div.parentNode : div,
+ aOptions.subtree);
+
+ getComputedStyle(div).transitionProperty;
+ div.style.lineHeight = "100px";
+
+ // The transition should cause the creation of a single Animation.
+ var animations = div.getAnimations();
+ assert_equals(animations.length, 1,
+ "getAnimations().length after transition start");
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: animations, changed: [], removed: [] }],
+ "records after transition start");
+
+ // Cancel the transition by setting line-height to a non-interpolable value.
+ div.style.lineHeight = "normal";
+ getComputedStyle(div).transitionProperty;
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [], removed: animations }],
+ "records after transition end");
+ }, "single_transition_cancelled_noninterpolable");
+
+ // 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.
+ test(t => {
+ var div =
+ addDiv(t, { style: "transition: background-color 100s step-start; " +
+ "background-color: yellow;" });
+ var observer =
+ setupSynchronousObserver(t,
+ aOptions.subtree ? div.parentNode : div,
+ aOptions.subtree);
+
+ getComputedStyle(div).transitionProperty;
+ div.style.backgroundColor = "lime";
+
+ var animations = div.getAnimations();
+
+ // The transition should cause the creation of a single Animation.
+ assert_equals(animations.length, 1,
+ "getAnimations().length after transition start");
+
+ var firstAnimation = animations[0];
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [firstAnimation], changed: [], removed: [] }],
+ "records after transition start");
+
+ firstAnimation.currentTime = 50 * MS_PER_SEC;
+
+ // Reverse the transition by setting the background-color back to its
+ // original value.
+ div.style.backgroundColor = "yellow";
+
+ // The reversal should cause the creation of a new Animation.
+ animations = div.getAnimations();
+ assert_equals(animations.length, 1,
+ "getAnimations().length after transition reversal");
+
+ var secondAnimation = animations[0];
+
+ assert_true(firstAnimation != secondAnimation,
+ "second Animation should be different from the first");
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [firstAnimation], removed: [] },
+ { added: [secondAnimation], changed: [], removed: [firstAnimation] }],
+ "records after transition reversal");
+
+ // Cancel the transition.
+ div.style.transitionProperty = "none";
+ getComputedStyle(div).transitionProperty;
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [], removed: [secondAnimation] }],
+ "records after transition end");
+ }, "single_transition_reversed");
+
+ // Test that multiple transitions starting and ending on an element
+ // at the same time get batched up into a single MutationRecord.
+ test(t => {
+ var div =
+ addDiv(t, { style: "transition-duration: 100s; " +
+ "transition-property: color, background-color, line-height" +
+ "background-color: yellow; line-height: 16px" });
+ var observer =
+ setupSynchronousObserver(t,
+ aOptions.subtree ? div.parentNode : div,
+ aOptions.subtree);
+ getComputedStyle(div).transitionProperty;
+
+ div.style.backgroundColor = "lime";
+ div.style.color = "blue";
+ div.style.lineHeight = "24px";
+
+ // The transitions should cause the creation of three Animations.
+ var animations = div.getAnimations();
+ assert_equals(animations.length, 3,
+ "getAnimations().length after transition starts");
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: animations, changed: [], removed: [] }],
+ "records after transition starts");
+
+ assert_equals(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.
+ div.style.transitionProperty = "background-color, line-height";
+
+ var colorAnimation = animations.filter(p => p.playState != "running");
+ var otherAnimations = animations.filter(p => p.playState == "running");
+
+ assert_equals(colorAnimation.length, 1,
+ "number of non-running Animations after cancelling one");
+ assert_equals(otherAnimations.length, 2,
+ "number of running Animations after cancelling one");
+
+ assert_equals_records(observer.takeRecords(),
+ seekRecords.concat({ added: [], changed: [], removed: colorAnimation }),
+ "records after color transition end");
+
+ // Cancel the remaining transitions.
+ div.style.transitionProperty = "none";
+ getComputedStyle(div).transitionProperty;
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [], removed: otherAnimations }],
+ "records after other transition ends");
+ }, "multiple_transitions");
+
+ // Test that starting a single animation that is cancelled by resetting
+ // the animation-name property dispatches an added notification and
+ // then a removed notification.
+ test(t => {
+ var div =
+ addDiv(t, { style: "animation: anim 100s" });
+ var observer =
+ setupSynchronousObserver(t,
+ aOptions.subtree ? div.parentNode : div,
+ aOptions.subtree);
+
+ // The animation should cause the creation of a single Animation.
+ 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");
+
+ // Cancel the animation by setting animation-name.
+ div.style.animationName = "none";
+ getComputedStyle(div).animationName;
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [], removed: animations }],
+ "records after animation end");
+ }, "single_animation_cancelled_name");
+
+ // Test that starting a single animation that is cancelled by updating
+ // the animation-duration property dispatches an added notification and
+ // then a removed notification.
+ test(t => {
+ var div =
+ addDiv(t, { style: "animation: anim 100s" });
+ var observer =
+ setupSynchronousObserver(t,
+ aOptions.subtree ? div.parentNode : div,
+ aOptions.subtree);
+
+ // The animation should cause the creation of a single Animation.
+ 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");
+
+ // 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.
+ div.style.animationDuration = "0.1s";
+ getComputedStyle(div).animationName;
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: animations, removed: [] },
+ { added: [], changed: [], removed: animations }],
+ "records after animation end");
+ }, "single_animation_cancelled_duration");
+
+ // Test that starting a single animation that is cancelled by updating
+ // the animation-delay property dispatches an added notification and
+ // then a removed notification.
+ test(t => {
+ var div =
+ addDiv(t, { style: "animation: anim 100s" });
+ var observer =
+ setupSynchronousObserver(t,
+ aOptions.subtree ? div.parentNode : div,
+ aOptions.subtree);
+
+ // The animation should cause the creation of a single Animation.
+ 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");
+
+ // Cancel the animation by setting animation-delay.
+ div.style.animationDelay = "-200s";
+ getComputedStyle(div).animationName;
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [], removed: animations }],
+ "records after animation end");
+ }, "single_animation_cancelled_delay");
+
+ // 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.
+ test(t => {
+ // A short, repeated animation.
+ var div =
+ addDiv(t, { style: "animation: anim 0.5s infinite;" });
+ var observer =
+ setupSynchronousObserver(t,
+ aOptions.subtree ? div.parentNode : div,
+ aOptions.subtree);
+
+ // The animation should cause the creation of a single Animation.
+ 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");
+
+ // Advance the animation until we are past the first iteration.
+ animations[0].currentTime += 1 * MS_PER_SEC;
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: animations, removed: [] }],
+ "records after seeking animations");
+
+ // Cancel the animation by setting animation-iteration-count.
+ div.style.animationIterationCount = "1";
+ getComputedStyle(div).animationName;
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [], removed: animations }],
+ "records after animation end");
+ }, "single_animation_cancelled_iteration_count");
+
+ // 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(aChangeTest => {
+ test(t => {
+ // Start a long animation.
+ var div = addDiv(t, { style: "animation: anim 100s;" });
+ var observer =
+ setupSynchronousObserver(t,
+ aOptions.subtree ? div.parentNode : div,
+ aOptions.subtree);
+
+ // The animation should cause the creation of a single Animation.
+ 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");
+
+ // Change a property of the animation such that it keeps running.
+ div.style[aChangeTest.prop] = aChangeTest.val;
+ getComputedStyle(div).animationName;
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: animations, removed: [] }],
+ "records after animation change");
+
+ // Cancel the animation.
+ div.style.animationName = "none";
+ getComputedStyle(div).animationName;
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [], removed: animations }],
+ "records after animation end");
+ }, `single_animation_change_${aChangeTest.name}`);
+ });
+
+ // Test that calling finish() on a pause-pending (but otherwise finished)
+ // 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);
+
+ // The animation should cause the creation of a single Animation.
+ 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");
+
+ // Finish and pause.
+ animations[0].finish();
+ animations[0].pause();
+ assert_true(animations[0].pending && animations[0].playState === "paused",
+ "playState after finishing and calling pause()");
+
+ // Call finish() again to abort the pause
+ animations[0].finish();
+ assert_equals(animations[0].playState, "finished",
+ "playState after finishing again");
+
+ // Wait for three MutationRecords for the Animation changes to
+ // be delivered: one for each finish(), pause(), finish() operation.
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: animations, removed: [] },
+ { added: [], changed: animations, removed: [] },
+ { added: [], changed: animations, removed: [] }],
+ "records after finish(), pause(), finish()");
+
+ // Cancel the animation.
+ div.style = "";
+ getComputedStyle(div).animationName;
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [], removed: animations }],
+ "records after animation end");
+ }, "finish_from_pause_pending");
+
+ // Test that calling play() on a finished Animation that fills forwards
+ // dispatches a changed notification.
+ test(t => {
+ // Animation with a forwards fill
+ var div =
+ addDiv(t, { style: "animation: anim 100s forwards" });
+ var observer =
+ setupSynchronousObserver(t,
+ aOptions.subtree ? div.parentNode : div,
+ aOptions.subtree);
+
+ // The animation should cause the creation of a single Animation.
+ 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 end
+ animations[0].finish();
+
+ assert_equals_records(observer.takeRecords(),
+ [{ 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();
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: animations, removed: [] }],
+ "records after play()");
+
+ // Cancel the animation.
+ div.style = "";
+ getComputedStyle(div).animationName;
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [], removed: animations }],
+ "records after animation end");
+ }, "play_filling_forwards");
+
+ // Test that calling pause() on an Animation dispatches a changed
+ // notification.
+ test(t => {
+ var div =
+ addDiv(t, { style: "animation: anim 100s" });
+ var observer =
+ setupSynchronousObserver(t,
+ aOptions.subtree ? div.parentNode : div,
+ aOptions.subtree);
+
+ // The animation should cause the creation of a single Animation.
+ 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");
+
+ // Pause
+ animations[0].pause();
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: animations, removed: [] }],
+ "records after pause()");
+
+ // Redundant pause
+ animations[0].pause();
+
+ assert_equals_records(observer.takeRecords(),
+ [], "records after redundant pause()");
+
+ // Cancel the animation.
+ div.style = "";
+ getComputedStyle(div).animationName;
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [], removed: animations }],
+ "records after animation end");
+ }, "pause");
+
+ // Test that calling pause() on an Animation that is pause-pending
+ // does not dispatch an additional changed notification.
+ test(t => {
+ var div =
+ addDiv(t, { style: "animation: anim 100s" });
+ var observer =
+ setupSynchronousObserver(t,
+ aOptions.subtree ? div.parentNode : div,
+ aOptions.subtree);
+
+ // The animation should cause the creation of a single Animation.
+ 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");
+
+ // Pause
+ animations[0].pause();
+
+ // We are now pause-pending, but pause again
+ animations[0].pause();
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: animations, removed: [] }],
+ "records after pause()");
+
+ // Cancel the animation.
+ div.style = "";
+ getComputedStyle(div).animationName;
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [], removed: animations }],
+ "records after animation end");
+ }, "pause_while_pause_pending");
+
+ // Test that calling play() on an Animation that is pause-pending
+ // dispatches a changed notification.
+ test(t => {
+ var div =
+ addDiv(t, { style: "animation: anim 100s" });
+ var observer =
+ setupSynchronousObserver(t,
+ aOptions.subtree ? div.parentNode : div,
+ aOptions.subtree);
+
+ // The animation should cause the creation of a single Animation.
+ 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");
+
+ // Pause
+ animations[0].pause();
+
+ // We are now pause-pending. If we play() now, we will abort the pause
+ animations[0].play();
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: animations, removed: [] },
+ { added: [], changed: animations, removed: [] }],
+ "records after aborting a pause()");
+
+ // Cancel the animation.
+ div.style = "";
+ getComputedStyle(div).animationName;
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [], removed: animations }],
+ "records after animation end");
+ }, "aborted_pause");
+
+ // Test that calling play() on a finished Animation that does *not* fill
+ // forwards dispatches an addition notification.
+ test(t => {
+ var div =
+ addDiv(t, { style: "animation: anim 100s" });
+ var observer =
+ setupSynchronousObserver(t,
+ aOptions.subtree ? div.parentNode : div,
+ aOptions.subtree);
+
+ // The animation should cause the creation of a single Animation.
+ 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 end
+ animations[0].finish();
+
+ assert_equals_records(observer.takeRecords(),
+ [{ 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();
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: animations, changed: [], removed: [] }],
+ "records after play()");
+
+ // Cancel the animation.
+ div.style = "";
+ getComputedStyle(div).animationName;
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [], removed: animations }],
+ "records after animation end");
+ }, "play_after_finish");
+
+ });
+
+ 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 observer = setupSynchronousObserver(t, div, true);
+
+ var anim = div.animate({ opacity: [ 0, 1 ] },
+ { duration: 200 * MS_PER_SEC,
+ pseudoElement: '::before' });
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [anim], changed: [], removed: [] }],
+ "records after animation is added");
+
+ anim.effect.updateTiming({ duration: 100 * MS_PER_SEC });
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [anim], removed: [] }],
+ "records after duration is changed");
+
+ anim.effect.updateTiming({ duration: 100 * MS_PER_SEC });
+ assert_equals_records(observer.takeRecords(),
+ [], "records after assigning same value");
+
+ anim.currentTime = anim.effect.getComputedTiming().duration * 2;
+ anim.finish();
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [], removed: [anim] }],
+ "records after animation end");
+
+ anim.effect.updateTiming({
+ duration: anim.effect.getComputedTiming().duration * 3
+ });
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [anim], changed: [], removed: [] }],
+ "records after animation restarted");
+
+ anim.effect.updateTiming({ duration: "auto" });
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [], removed: [anim] }],
+ "records after duration set \"auto\"");
+
+ anim.effect.updateTiming({ 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 observer = setupSynchronousObserver(t, div, false);
+
+ var anim = div.animate({ opacity: [ 0, 1 ] },
+ { duration: 100 * MS_PER_SEC });
+ var pAnim = div.animate({ opacity: [ 0, 1 ] },
+ { duration: 100 * MS_PER_SEC,
+ pseudoElement: "::before" });
+
+ 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");
+}
+
+W3CTest.runner.expectAssertions(0, 12); // bug 1189015
+setup({explicit_done: true});
+SpecialPowers.pushPrefEnv(
+ {
+ set: [
+ ["dom.animations-api.timelines.enabled", true],
+ ],
+ },
+ function() {
+ runTest();
+ done();
+ }
+);
+
+</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 0000000000..13df5d7842
--- /dev/null
+++ b/dom/animation/test/chrome/test_animation_performance_warning.html
@@ -0,0 +1,1693 @@
+<!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 }
+}
+@keyframes translate {
+ from { transform: none }
+ to { transform: translate(100px) }
+}
+</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);
+
+const Services = SpecialPowers.Services;
+Services.locale.requestedLocales = ["en-US"];
+
+SpecialPowers.pushPrefEnv({ "set": [
+ // Need to set devPixelsPerPx explicitly to gain
+ // consistent pixel values in warning messages
+ // regardless of platform DPIs.
+ ["layout.css.devPixelsPerPx", 1],
+ ["layout.animation.prerender.partial", false],
+ ] },
+ 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_all_properties_running_on_compositor(actual, expected) {
+ assert_equals(actual.length, expected.length);
+
+ var sortedActual = actual.sort(compare_property_state);
+ var sortedExpected = expected.sort(compare_property_state);
+
+ for (var i = 0; i < sortedActual.length; i++) {
+ assert_equals(sortedActual[i].property,
+ sortedExpected[i].property,
+ 'CSS property name should match');
+ assert_true(sortedActual[i].runningOnCompositor,
+ 'runningOnCompositor property should be true on ' +
+ sortedActual[i].property);
+ assert_not_exists(sortedActual[i], 'warning',
+ 'warning property should not be set');
+ }
+}
+
+function testBasicOperation() {
+ [
+ {
+ desc: 'animations on compositor',
+ frames: {
+ opacity: [0, 1]
+ },
+ expected: [
+ {
+ property: 'opacity',
+ runningOnCompositor: true
+ }
+ ]
+ },
+ {
+ desc: 'animations on main thread',
+ frames: {
+ zIndex: ['0', '999']
+ },
+ expected: [
+ {
+ property: 'z-index',
+ runningOnCompositor: false
+ }
+ ]
+ },
+ {
+ desc: 'animations on both threads',
+ frames: {
+ zIndex: ['0', '999'],
+ transform: ['translate(0px)', 'translate(100px)']
+ },
+ expected: [
+ {
+ property: 'z-index',
+ 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: 'two transform-like animation properties on compositor thread',
+ frames: {
+ transform: ['translate(0px)', 'translate(100px)'],
+ translate: ['0px', '100px']
+ },
+ expected: [
+ {
+ property: 'transform',
+ runningOnCompositor: true
+ },
+ {
+ property: 'translate',
+ runningOnCompositor: true
+ }
+ ]
+ },
+ {
+ desc: 'opacity on compositor with animation of geometric properties',
+ frames: {
+ width: ['100px', '200px'],
+ opacity: [0, 1]
+ },
+ expected: [
+ {
+ property: 'width',
+ runningOnCompositor: false
+ },
+ {
+ property: 'opacity',
+ runningOnCompositor: true
+ }
+ ]
+ },
+ ].forEach(subtest => {
+ promise_test(async t => {
+ var animation = addDivAndAnimate(t, { class: 'compositable' },
+ subtest.frames, 100 * MS_PER_SEC);
+ await waitForPaints();
+ assert_animation_property_state_equals(
+ animation.effect.getProperties(),
+ subtest.expected);
+ }, subtest.desc);
+ });
+}
+
+// Test adding/removing a 'width' property on the same animation object.
+function testKeyframesWithGeometricProperties() {
+ [
+ {
+ desc: 'transform',
+ frames: {
+ transform: ['translate(0px)', 'translate(100px)']
+ },
+ expected: {
+ withoutGeometric: [
+ {
+ property: 'transform',
+ runningOnCompositor: true
+ }
+ ],
+ withGeometric: [
+ {
+ property: 'width',
+ runningOnCompositor: false
+ },
+ {
+ property: 'transform',
+ runningOnCompositor: true,
+ }
+ ]
+ }
+ },
+ {
+ desc: 'translate',
+ frames: {
+ translate: ['0px', '100px']
+ },
+ expected: {
+ withoutGeometric: [
+ {
+ property: 'translate',
+ runningOnCompositor: true
+ }
+ ],
+ withGeometric: [
+ {
+ property: 'width',
+ runningOnCompositor: false
+ },
+ {
+ property: 'translate',
+ runningOnCompositor: true,
+ }
+ ]
+ }
+ },
+ {
+ desc: 'opacity and transform-like properties',
+ frames: {
+ opacity: [0, 1],
+ transform: ['translate(0px)', 'translate(100px)'],
+ translate: ['0px', '100px']
+ },
+ expected: {
+ withoutGeometric: [
+ {
+ property: 'opacity',
+ runningOnCompositor: true
+ },
+ {
+ property: 'transform',
+ runningOnCompositor: true
+ },
+ {
+ property: 'translate',
+ runningOnCompositor: true
+ }
+ ],
+ withGeometric: [
+ {
+ property: 'width',
+ runningOnCompositor: false
+ },
+ {
+ property: 'opacity',
+ runningOnCompositor: true
+ },
+ {
+ property: 'transform',
+ runningOnCompositor: true,
+ },
+ {
+ property: 'translate',
+ runningOnCompositor: true,
+ }
+ ]
+ }
+ },
+ ].forEach(subtest => {
+ promise_test(async t => {
+ var animation = addDivAndAnimate(t, { class: 'compositable' },
+ subtest.frames, 100 * MS_PER_SEC);
+ await waitForPaints();
+
+ // First, a transform animation is running on compositor.
+ assert_animation_property_state_equals(
+ animation.effect.getProperties(),
+ subtest.expected.withoutGeometric);
+
+ // Add a 'width' property.
+ var keyframes = animation.effect.getKeyframes();
+
+ keyframes[0].width = '100px';
+ keyframes[1].width = '200px';
+
+ animation.effect.setKeyframes(keyframes);
+ await waitForFrame();
+
+ // 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);
+
+ // Remove the 'width' property.
+ var keyframes = animation.effect.getKeyframes();
+
+ delete keyframes[0].width;
+ delete keyframes[1].width;
+
+ animation.effect.setKeyframes(keyframes);
+ await waitForFrame();
+
+ // Finally, the transform animation is running on compositor.
+ assert_animation_property_state_equals(
+ animation.effect.getProperties(),
+ subtest.expected.withoutGeometric);
+ }, 'An animation has: ' + subtest.desc);
+ });
+}
+
+// Test that the expected set of geometric properties all block transform
+// animations.
+function testSetOfGeometricProperties() {
+ const geometricProperties = [
+ 'width', 'height',
+ 'top', 'right', 'bottom', 'left',
+ 'margin-top', 'margin-right', 'margin-bottom', 'margin-left',
+ 'padding-top', 'padding-right', 'padding-bottom', 'padding-left'
+ ];
+
+ geometricProperties.forEach(property => {
+ promise_test(async t => {
+ const keyframes = {
+ [propertyToIDL(property)]: [ '100px', '200px' ],
+ transform: [ 'translate(0px)', 'translate(100px)' ]
+ };
+ var animation = addDivAndAnimate(t, { class: 'compositable' },
+ keyframes, 100 * MS_PER_SEC);
+
+ await waitForPaints();
+ assert_animation_property_state_equals(
+ animation.effect.getProperties(),
+ [
+ {
+ property,
+ runningOnCompositor: false
+ },
+ {
+ property: 'transform',
+ runningOnCompositor: true,
+ }
+ ]);
+ }, `${property} is treated as a geometric property`);
+ });
+}
+
+// Performance warning tests that set and clear a style property.
+function testStyleChanges() {
+ [
+ {
+ desc: 'preserve-3d transform',
+ frames: {
+ transform: ['translate(0px)', 'translate(100px)']
+ },
+ style: 'transform-style: preserve-3d',
+ expected: [
+ {
+ property: 'transform',
+ runningOnCompositor: true,
+ }
+ ]
+ },
+ {
+ desc: 'preserve-3d translate',
+ frames: {
+ translate: ['0px', '100px']
+ },
+ style: 'transform-style: preserve-3d',
+ expected: [
+ {
+ property: 'translate',
+ runningOnCompositor: true,
+ }
+ ]
+ },
+ {
+ desc: 'transform with backface-visibility:hidden',
+ frames: {
+ transform: ['translate(0px)', 'translate(100px)']
+ },
+ style: 'backface-visibility: hidden;',
+ expected: [
+ {
+ property: 'transform',
+ runningOnCompositor: true,
+ }
+ ]
+ },
+ {
+ desc: 'translate with backface-visibility:hidden',
+ frames: {
+ translate: ['0px', '100px']
+ },
+ style: 'backface-visibility: hidden;',
+ expected: [
+ {
+ property: 'translate',
+ runningOnCompositor: true,
+ }
+ ]
+ },
+ {
+ desc: 'opacity and transform-like properties with preserve-3d',
+ frames: {
+ opacity: [0, 1],
+ transform: ['translate(0px)', 'translate(100px)'],
+ translate: ['0px', '100px']
+ },
+ style: 'transform-style: preserve-3d',
+ expected: [
+ {
+ property: 'opacity',
+ runningOnCompositor: true
+ },
+ {
+ property: 'transform',
+ runningOnCompositor: true,
+ },
+ {
+ property: 'translate',
+ runningOnCompositor: true,
+ }
+ ]
+ },
+ {
+ desc: 'opacity and transform-like properties with ' +
+ 'backface-visibility:hidden',
+ frames: {
+ opacity: [0, 1],
+ transform: ['translate(0px)', 'translate(100px)'],
+ translate: ['0px', '100px']
+ },
+ style: 'backface-visibility: hidden;',
+ expected: [
+ {
+ property: 'opacity',
+ runningOnCompositor: true
+ },
+ {
+ property: 'transform',
+ runningOnCompositor: true,
+ },
+ {
+ property: 'translate',
+ runningOnCompositor: true,
+ }
+ ]
+ },
+ ].forEach(subtest => {
+ promise_test(async t => {
+ var animation = addDivAndAnimate(t, { class: 'compositable' },
+ subtest.frames, 100 * MS_PER_SEC);
+ await waitForPaints();
+ assert_all_properties_running_on_compositor(
+ animation.effect.getProperties(),
+ subtest.expected);
+ animation.effect.target.style = subtest.style;
+ await waitForFrame();
+
+ assert_animation_property_state_equals(
+ animation.effect.getProperties(),
+ subtest.expected);
+ animation.effect.target.style = '';
+ await waitForFrame();
+
+ assert_all_properties_running_on_compositor(
+ animation.effect.getProperties(),
+ subtest.expected);
+ }, subtest.desc);
+ });
+}
+
+// Performance warning tests that set and clear the id property
+function testIdChanges() {
+ [
+ {
+ desc: 'moz-element referencing a transform',
+ frames: {
+ transform: ['translate(0px)', 'translate(100px)']
+ },
+ id: 'transformed',
+ createelement: 'width:100px; height:100px; background: -moz-element(#transformed)',
+ expected: [
+ {
+ property: 'transform',
+ runningOnCompositor: false,
+ warning: 'CompositorAnimationWarningHasRenderingObserver'
+ }
+ ]
+ },
+ {
+ desc: 'moz-element referencing a translate',
+ frames: {
+ translate: ['0px', '100px']
+ },
+ id: 'transformed',
+ createelement: 'width:100px; height:100px; background: -moz-element(#transformed)',
+ expected: [
+ {
+ property: 'translate',
+ runningOnCompositor: false,
+ warning: 'CompositorAnimationWarningHasRenderingObserver'
+ }
+ ]
+ },
+ {
+ desc: 'moz-element referencing a translate and transform',
+ frames: {
+ transform: ['translate(0px)', 'translate(100px)'],
+ translate: ['0px', '100px']
+ },
+ id: 'transformed',
+ createelement: 'width:100px; height:100px; background: -moz-element(#transformed)',
+ expected: [
+ {
+ property: 'translate',
+ runningOnCompositor: false,
+ warning: 'CompositorAnimationWarningHasRenderingObserver'
+ },
+ {
+ property: 'transform',
+ runningOnCompositor: false,
+ warning: 'CompositorAnimationWarningHasRenderingObserver'
+ }
+ ]
+ },
+ ].forEach(subtest => {
+ promise_test(async t => {
+ if (subtest.createelement) {
+ addDiv(t, { style: subtest.createelement });
+ }
+
+ var animation = addDivAndAnimate(t, { class: 'compositable' },
+ subtest.frames, 100 * MS_PER_SEC);
+ await waitForPaints();
+
+ assert_all_properties_running_on_compositor(
+ animation.effect.getProperties(),
+ subtest.expected);
+ animation.effect.target.id = subtest.id;
+ await waitForFrame();
+
+ assert_animation_property_state_equals(
+ animation.effect.getProperties(),
+ subtest.expected);
+ animation.effect.target.id = '';
+ await waitForFrame();
+
+ assert_all_properties_running_on_compositor(
+ animation.effect.getProperties(),
+ subtest.expected);
+ }, subtest.desc);
+ });
+}
+
+function testMultipleAnimations() {
+ [
+ {
+ desc: 'opacity and transform-like properties with preserve-3d',
+ style: 'transform-style: preserve-3d',
+ animations: [
+ {
+ frames: {
+ transform: ['translate(0px)', 'translate(100px)']
+ },
+ expected: [
+ {
+ property: 'transform',
+ runningOnCompositor: true,
+ }
+ ]
+ },
+ {
+ frames: {
+ translate: ['0px', '100px']
+ },
+ expected: [
+ {
+ property: 'translate',
+ runningOnCompositor: true,
+ }
+ ]
+ },
+ {
+ frames: {
+ opacity: [0, 1]
+ },
+ expected: [
+ {
+ property: 'opacity',
+ runningOnCompositor: true,
+ }
+ ]
+ }
+ ],
+ },
+ {
+ desc: 'opacity and transform-like properties with ' +
+ 'backface-visibility:hidden',
+ style: 'backface-visibility: hidden;',
+ animations: [
+ {
+ frames: {
+ transform: ['translate(0px)', 'translate(100px)']
+ },
+ expected: [
+ {
+ property: 'transform',
+ runningOnCompositor: true,
+ }
+ ]
+ },
+ {
+ frames: {
+ translate: ['0px', '100px']
+ },
+ expected: [
+ {
+ property: 'translate',
+ runningOnCompositor: true,
+ }
+ ]
+ },
+ {
+ frames: {
+ opacity: [0, 1]
+ },
+ expected: [
+ {
+ property: 'opacity',
+ runningOnCompositor: true,
+ }
+ ]
+ }
+ ],
+ },
+ ].forEach(subtest => {
+ promise_test(async t => {
+ var div = addDiv(t, { class: 'compositable' });
+ var animations = subtest.animations.map(anim => {
+ var animation = div.animate(anim.frames, 100 * MS_PER_SEC);
+
+ // Bind expected values to animation object.
+ animation.expected = anim.expected;
+ return animation;
+ });
+ await waitForPaints();
+
+ animations.forEach(anim => {
+ assert_all_properties_running_on_compositor(
+ anim.effect.getProperties(),
+ anim.expected);
+ });
+ div.style = subtest.style;
+ await waitForFrame();
+
+ animations.forEach(anim => {
+ assert_animation_property_state_equals(
+ anim.effect.getProperties(),
+ anim.expected);
+ });
+ div.style = '';
+ await waitForFrame();
+
+ animations.forEach(anim => {
+ assert_all_properties_running_on_compositor(
+ anim.effect.getProperties(),
+ anim.expected);
+ });
+ }, 'Multiple animations: ' + subtest.desc);
+ });
+}
+
+// Test adding/removing a 'width' keyframe on the same animation object, where
+// multiple animation objects belong to the same element.
+// The 'width' property is added to animations[1].
+function testMultipleAnimationsWithGeometricKeyframes() {
+ [
+ {
+ desc: 'transform and opacity with geometric keyframes',
+ animations: [
+ {
+ frames: {
+ transform: ['translate(0px)', 'translate(100px)']
+ },
+ expected: {
+ withoutGeometric: [
+ {
+ property: 'transform',
+ runningOnCompositor: true
+ }
+ ],
+ withGeometric: [
+ {
+ property: 'transform',
+ runningOnCompositor: true,
+ }
+ ]
+ }
+ },
+ {
+ 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: true,
+ }
+ ]
+ }
+ }
+ ]
+ },
+ {
+ desc: 'opacity and translate with geometric keyframes',
+ animations: [
+ {
+ frames: {
+ opacity: [0, 1]
+ },
+ expected: {
+ withoutGeometric: [
+ {
+ property: 'opacity',
+ runningOnCompositor: true,
+ }
+ ],
+ withGeometric: [
+ {
+ property: 'opacity',
+ runningOnCompositor: true,
+ }
+ ]
+ }
+ },
+ {
+ frames: {
+ translate: ['0px', '100px']
+ },
+ expected: {
+ withoutGeometric: [
+ {
+ property: 'translate',
+ runningOnCompositor: true
+ }
+ ],
+ withGeometric: [
+ {
+ property: 'width',
+ runningOnCompositor: false,
+ },
+ {
+ property: 'translate',
+ runningOnCompositor: true,
+ }
+ ]
+ }
+ }
+ ]
+ },
+ ].forEach(subtest => {
+ promise_test(async t => {
+ var div = addDiv(t, { class: 'compositable' });
+ var animations = subtest.animations.map(anim => {
+ var animation = div.animate(anim.frames, 100 * MS_PER_SEC);
+
+ // Bind expected values to animation object.
+ animation.expected = anim.expected;
+ return animation;
+ });
+ await waitForPaints();
+ // First, all animations are running on compositor.
+ animations.forEach(anim => {
+ assert_animation_property_state_equals(
+ anim.effect.getProperties(),
+ anim.expected.withoutGeometric);
+ });
+
+ // 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);
+ await waitForFrame();
+
+ // Now the transform animation is not running on compositor because of
+ // the 'width' property.
+ animations.forEach(anim => {
+ assert_animation_property_state_equals(
+ anim.effect.getProperties(),
+ anim.expected.withGeometric);
+ });
+
+ // 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);
+ await waitForFrame();
+
+ // Finally, all animations are running on compositor.
+ animations.forEach(anim => {
+ assert_animation_property_state_equals(
+ anim.effect.getProperties(),
+ anim.expected.withoutGeometric);
+ });
+ }, 'Multiple animations with geometric property: ' + subtest.desc);
+ });
+}
+
+// Tests adding/removing 'width' animation on the same element which has async
+// animations.
+function testMultipleAnimationsWithGeometricAnimations() {
+ [
+ {
+ desc: 'transform',
+ animations: [
+ {
+ frames: {
+ transform: ['translate(0px)', 'translate(100px)']
+ },
+ expected: [
+ {
+ property: 'transform',
+ runningOnCompositor: true,
+ }
+ ]
+ },
+ ]
+ },
+ {
+ desc: 'translate',
+ animations: [
+ {
+ frames: {
+ translate: ['0px', '100px']
+ },
+ expected: [
+ {
+ property: 'translate',
+ runningOnCompositor: true,
+ }
+ ]
+ },
+ ]
+ },
+ {
+ desc: 'opacity',
+ animations: [
+ {
+ frames: {
+ opacity: [0, 1]
+ },
+ expected: [
+ {
+ property: 'opacity',
+ runningOnCompositor: true
+ }
+ ]
+ },
+ ]
+ },
+ {
+ desc: 'opacity, transform, and translate',
+ animations: [
+ {
+ frames: {
+ transform: ['translate(0px)', 'translate(100px)']
+ },
+ expected: [
+ {
+ property: 'transform',
+ runningOnCompositor: true,
+ }
+ ]
+ },
+ {
+ frames: {
+ translate: ['0px', '100px']
+ },
+ expected: [
+ {
+ property: 'translate',
+ runningOnCompositor: true,
+ }
+ ]
+ },
+ {
+ frames: {
+ opacity: [0, 1]
+ },
+ expected: [
+ {
+ property: 'opacity',
+ runningOnCompositor: true,
+ }
+ ]
+ }
+ ],
+ },
+ ].forEach(subtest => {
+ promise_test(async t => {
+ var div = addDiv(t, { class: 'compositable' });
+ var animations = subtest.animations.map(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;
+
+ await waitForPaints();
+ animations.forEach(anim => {
+ assert_all_properties_running_on_compositor(
+ anim.effect.getProperties(),
+ anim.expected);
+ });
+
+ // Append 'width' animation on the same element.
+ widthAnimation = div.animate({ width: ['100px', '200px'] },
+ 100 * MS_PER_SEC);
+ await waitForFrame();
+
+ // Now transform animations are not running on compositor because of
+ // the 'width' animation.
+ animations.forEach(anim => {
+ assert_animation_property_state_equals(
+ anim.effect.getProperties(),
+ anim.expected);
+ });
+ // Remove the 'width' animation.
+ widthAnimation.cancel();
+ await waitForFrame();
+
+ // Now all animations are running on compositor.
+ animations.forEach(anim => {
+ assert_all_properties_running_on_compositor(
+ anim.effect.getProperties(),
+ anim.expected);
+ });
+ }, 'Multiple async animations and geometric animation: ' + subtest.desc);
+ });
+}
+
+function testSmallElements() {
+ [
+ {
+ desc: 'opacity on small element',
+ frames: {
+ opacity: [0, 1]
+ },
+ style: { style: 'width: 8px; height: 8px; background-color: red;' +
+ // We need to set transform here to try creating an
+ // individual frame for this opacity element.
+ // Without this, this small element is created on the same
+ // nsIFrame of mochitest iframe, i.e. the document which are
+ // running this test, as a result the layer corresponding
+ // to the frame is sent to compositor.
+ 'transform: translateX(100px);' },
+ expected: [
+ {
+ property: 'opacity',
+ runningOnCompositor: true
+ }
+ ]
+ },
+ {
+ desc: 'transform on small element',
+ frames: {
+ transform: ['translate(0px)', 'translate(100px)']
+ },
+ style: { style: 'width: 8px; height: 8px; background-color: red;' },
+ expected: [
+ {
+ property: 'transform',
+ runningOnCompositor: true
+ }
+ ]
+ },
+ {
+ desc: 'translate on small element',
+ frames: {
+ translate: ['0px', '100px']
+ },
+ style: { style: 'width: 8px; height: 8px; background-color: red;' },
+ expected: [
+ {
+ property: 'translate',
+ runningOnCompositor: true
+ }
+ ]
+ },
+ ].forEach(subtest => {
+ promise_test(async t => {
+ var div = addDiv(t, subtest.style);
+ var animation = div.animate(subtest.frames, 100 * MS_PER_SEC);
+ await waitForPaints();
+
+ assert_animation_property_state_equals(
+ animation.effect.getProperties(),
+ subtest.expected);
+ }, subtest.desc);
+ });
+}
+
+function testSynchronizedAnimations() {
+ promise_test(async t => {
+ const elemA = addDiv(t, { class: 'compositable' });
+ const elemB = addDiv(t, { class: 'compositable' });
+
+ const animA = elemA.animate({ transform: [ 'translate(0px)',
+ 'translate(100px)' ] },
+ 100 * MS_PER_SEC);
+ const animB = elemB.animate({ marginLeft: [ '0px', '100px' ] },
+ 100 * MS_PER_SEC);
+
+ await Promise.all([animA.ready, animB.ready]);
+ await waitForPaints();
+
+ assert_animation_property_state_equals(
+ animA.effect.getProperties(),
+ [ { property: 'transform',
+ runningOnCompositor: true,
+ } ]);
+ }, 'Animations created within the same tick are synchronized'
+ + ' (compositor animation created first)');
+
+ promise_test(async t => {
+ const elemA = addDiv(t, { class: 'compositable' });
+ const elemB = addDiv(t, { class: 'compositable' });
+ const elemC = addDiv(t, { class: 'compositable' });
+
+ const animA = elemA.animate({ transform: [ 'translate(0px)',
+ 'translate(100px)' ] },
+ 100 * MS_PER_SEC);
+ const animB = elemB.animate({ translate: [ '0px', '100px' ] },
+ 100 * MS_PER_SEC);
+ const animC = elemC.animate({ marginLeft: [ '0px', '100px' ] },
+ 100 * MS_PER_SEC);
+
+ await Promise.all([animA.ready, animB.ready, animC.ready]);
+ await waitForPaints();
+
+ assert_animation_property_state_equals(
+ animA.effect.getProperties(),
+ [
+ { property: 'transform',
+ runningOnCompositor: true,
+ } ]);
+ assert_animation_property_state_equals(
+ animB.effect.getProperties(),
+ [
+ { property: 'translate',
+ runningOnCompositor: true,
+ } ]);
+ }, 'Animations created within the same tick are synchronized'
+ + ' (compositor animation created first/second)');
+
+ promise_test(async t => {
+ const elemA = addDiv(t, { class: 'compositable' });
+ const elemB = addDiv(t, { class: 'compositable' });
+ const elemC = addDiv(t, { class: 'compositable' });
+
+ const animA = elemA.animate({ marginLeft: [ '0px', '100px' ] },
+ 100 * MS_PER_SEC);
+ const animB = elemB.animate({ transform: [ 'translate(0px)',
+ 'translate(100px)' ] },
+ 100 * MS_PER_SEC);
+ const animC = elemC.animate({ translate: [ '0px', '100px' ] },
+ 100 * MS_PER_SEC);
+
+ await Promise.all([animA.ready, animB.ready, animC.ready]);
+ await waitForPaints();
+
+ assert_animation_property_state_equals(
+ animB.effect.getProperties(),
+ [ { property: 'transform',
+ runningOnCompositor: true,
+ } ]);
+ assert_animation_property_state_equals(
+ animC.effect.getProperties(),
+ [
+ { property: 'translate',
+ runningOnCompositor: true,
+ } ]);
+ }, 'Animations created within the same tick are synchronized'
+ + ' (compositor animation created second/third)');
+
+ promise_test(async t => {
+ const attrs = { class: 'compositable',
+ style: 'transition: all 100s' };
+ const elemA = addDiv(t, attrs);
+ const elemB = addDiv(t, attrs);
+ elemA.style.transform = 'translate(0px)';
+ elemB.style.marginLeft = '0px';
+ getComputedStyle(elemA).transform;
+ getComputedStyle(elemB).marginLeft;
+
+ // Generally the sequence of steps is as follows:
+ //
+ // Tick -> requestAnimationFrame -> Style -> Paint -> Events (-> Tick...)
+ //
+ // In this test we want to set up two transitions during the "Events"
+ // stage but only flush style for one such that the second one is actually
+ // generated during the "Style" stage of the *next* tick.
+ //
+ // Web content often generates transitions in this way (that is, it doesn't
+ // pay regard to when style is flushed and nor should it). However, we
+ // still want transitions generated in this way to be synchronized.
+ let timeForFirstFrame;
+ await waitForIdle();
+
+ timeForFirstFrame = document.timeline.currentTime;
+ elemA.style.transform = 'translate(100px)';
+ // Flush style to trigger first transition
+ getComputedStyle(elemA).transform;
+ elemB.style.marginLeft = '100px';
+ // DON'T flush style here (this includes calling getAnimations!)
+ await waitForFrame();
+
+ assert_not_equals(timeForFirstFrame, document.timeline.currentTime,
+ 'Should be on the other side of a tick');
+ // Wait another tick so we can let the transition be started
+ // by regular style resolution.
+ await waitForFrame();
+
+ const transitionA = elemA.getAnimations()[0];
+ assert_animation_property_state_equals(
+ transitionA.effect.getProperties(),
+ [ { property: 'transform',
+ runningOnCompositor: true,
+ } ]);
+ }, 'Transitions created before and after a tick are synchronized');
+
+ promise_test(async t => {
+ const elemA = addDiv(t, { class: 'compositable' });
+ const elemB = addDiv(t, { class: 'compositable' });
+
+ const animA = elemA.animate({ transform: [ 'translate(0px)',
+ 'translate(100px)' ],
+ opacity: [ 0, 1 ] },
+ 100 * MS_PER_SEC);
+ const animB = elemB.animate({ marginLeft: [ '0px', '100px' ] },
+ 100 * MS_PER_SEC);
+
+ await Promise.all([animA.ready, animB.ready]);
+ await waitForPaints();
+
+ assert_animation_property_state_equals(
+ animA.effect.getProperties(),
+ [ { property: 'transform',
+ runningOnCompositor: true,
+ },
+ { property: 'opacity',
+ runningOnCompositor: true
+ } ]);
+ }, 'Opacity animations on the same element continue running on the'
+ + ' compositor when transform animations are synchronized with geometric'
+ + ' animations');
+
+ promise_test(async t => {
+ const transitionElem = addDiv(t, {
+ style: 'margin-left: 0px; transition: margin-left 100s',
+ });
+ getComputedStyle(transitionElem).marginLeft;
+
+ await waitForFrame();
+
+ transitionElem.style.marginLeft = '100px';
+ const cssTransition = transitionElem.getAnimations()[0];
+
+ const animationElem = addDiv(t, {
+ class: 'compositable',
+ style: 'animation: translate 100s',
+ });
+ const cssAnimation = animationElem.getAnimations()[0];
+
+ await Promise.all([cssTransition.ready, cssAnimation.ready]);
+ await waitForPaints();
+
+ assert_animation_property_state_equals(cssAnimation.effect.getProperties(),
+ [{ property: 'transform',
+ runningOnCompositor: true }]);
+ }, 'CSS Animations are NOT synchronized with CSS Transitions');
+
+ promise_test(async t => {
+ const elemA = addDiv(t, { class: 'compositable' });
+ const elemB = addDiv(t, { class: 'compositable' });
+
+ const animA = elemA.animate({ marginLeft: [ '0px', '100px' ] },
+ 100 * MS_PER_SEC);
+ await animA.ready;
+ await waitForPaints();
+
+ let animB = elemB.animate({ transform: [ 'translate(0px)',
+ 'translate(100px)' ] },
+ 100 * MS_PER_SEC);
+ await animB.ready;
+ await waitForPaints();
+
+ assert_animation_property_state_equals(
+ animB.effect.getProperties(),
+ [ { property: 'transform',
+ runningOnCompositor: true } ]);
+ }, 'Transform animations are NOT synchronized with geometric animations'
+ + ' started in the previous frame');
+
+ promise_test(async t => {
+ const elemA = addDiv(t, { class: 'compositable' });
+ const elemB = addDiv(t, { class: 'compositable' });
+
+ const animA = elemA.animate({ transform: [ 'translate(0px)',
+ 'translate(100px)' ] },
+ 100 * MS_PER_SEC);
+ await animA.ready;
+ await waitForPaints();
+
+ let animB = elemB.animate({ marginLeft: [ '0px', '100px' ] },
+ 100 * MS_PER_SEC);
+ await animB.ready;
+ await waitForPaints();
+
+ assert_animation_property_state_equals(
+ animA.effect.getProperties(),
+ [ { property: 'transform',
+ runningOnCompositor: true } ]);
+ }, 'Transform animations are NOT synchronized with geometric animations'
+ + ' started in the next frame');
+
+ promise_test(async t => {
+ const elemA = addDiv(t, { class: 'compositable' });
+ const elemB = addDiv(t, { class: 'compositable' });
+
+ const animA = elemA.animate({ transform: [ 'translate(0px)',
+ 'translate(100px)' ] },
+ 100 * MS_PER_SEC);
+ const animB = elemB.animate({ marginLeft: [ '0px', '100px' ] },
+ 100 * MS_PER_SEC);
+ animB.pause();
+
+ await animA.ready;
+ await waitForPaints();
+
+ assert_animation_property_state_equals(
+ animA.effect.getProperties(),
+ [ { property: 'transform', runningOnCompositor: true } ]);
+ }, 'Paused animations are not synchronized');
+
+ promise_test(async t => {
+ const elemA = addDiv(t, { class: 'compositable' });
+ const elemB = addDiv(t, { class: 'compositable' });
+
+ const animA = elemA.animate({ transform: [ 'translate(0px)',
+ 'translate(100px)' ] },
+ 100 * MS_PER_SEC);
+ const animB = elemB.animate({ marginLeft: [ '0px', '100px' ] },
+ 100 * MS_PER_SEC);
+
+ // Seek one of the animations so that their start times will differ
+ animA.currentTime = 5000;
+
+ await Promise.all([animA.ready, animB.ready]);
+ await waitForPaints();
+
+ assert_not_equals(animA.startTime, animB.startTime,
+ 'Animations should have different start times');
+ assert_animation_property_state_equals(
+ animA.effect.getProperties(),
+ [ { property: 'transform',
+ runningOnCompositor: true,
+ } ]);
+ }, 'Animations are synchronized based on when they are started'
+ + ' and NOT their start time');
+
+ promise_test(async t => {
+ const elemA = addDiv(t, { class: 'compositable' });
+ const elemB = addDiv(t, { class: 'compositable' });
+
+ const animA = elemA.animate({ transform: [ 'translate(0px)',
+ 'translate(100px)' ] },
+ 100 * MS_PER_SEC);
+ const animB = elemB.animate({ marginLeft: [ '0px', '100px' ] },
+ 100 * MS_PER_SEC);
+
+ await Promise.all([animA.ready, animB.ready]);
+ await waitForPaints();
+
+ assert_animation_property_state_equals(
+ animA.effect.getProperties(),
+ [ { property: 'transform',
+ runningOnCompositor: true } ]);
+ // Restart animation
+ animA.pause();
+ animA.play();
+ await animA.ready;
+ await waitForPaints();
+
+ assert_animation_property_state_equals(
+ animA.effect.getProperties(),
+ [ { property: 'transform',
+ runningOnCompositor: true } ]);
+ }, 'An initially synchronized animation may be unsynchronized if restarted');
+
+ promise_test(async t => {
+ const elemA = addDiv(t, { class: 'compositable' });
+ const elemB = addDiv(t, { class: 'compositable' });
+
+ const animA = elemA.animate({ transform: [ 'translate(0px)',
+ 'translate(100px)' ] },
+ 100 * MS_PER_SEC);
+ const animB = elemB.animate({ marginLeft: [ '0px', '100px' ] },
+ 100 * MS_PER_SEC);
+
+ // Clear target effect
+ animB.effect.target = null;
+
+ await Promise.all([animA.ready, animB.ready]);
+ await waitForPaints();
+
+ assert_animation_property_state_equals(
+ animA.effect.getProperties(),
+ [ { property: 'transform',
+ runningOnCompositor: true } ]);
+ }, 'A geometric animation with no target element is not synchronized');
+}
+
+function testTooLargeFrame() {
+ [
+ {
+ property: 'transform',
+ frames: { transform: ['translate(0px)', 'translate(100px)'] },
+ },
+ {
+ property: 'translate',
+ frames: { translate: ['0px', '100px'] },
+ },
+ ].forEach(subtest => {
+ promise_test(async t => {
+ var animation = addDivAndAnimate(t,
+ { class: 'compositable' },
+ subtest.frames,
+ 100 * MS_PER_SEC);
+ await waitForPaints();
+
+ assert_animation_property_state_equals(
+ animation.effect.getProperties(),
+ [ { property: subtest.property, runningOnCompositor: true } ]);
+ animation.effect.target.style = 'width: 10000px; height: 10000px';
+ await waitForFrame();
+
+ // viewport depends on test environment.
+ var expectedWarning = new RegExp(
+ "Animation cannot be run on the compositor because the area of the frame " +
+ "\\(\\d+\\) is too large relative to the viewport " +
+ "\\(larger than \\d+\\)");
+ assert_animation_property_state_equals(
+ animation.effect.getProperties(),
+ [ {
+ property: subtest.property,
+ runningOnCompositor: false,
+ warning: expectedWarning
+ } ]);
+ animation.effect.target.style = 'width: 100px; height: 100px';
+ await waitForFrame();
+
+ // With WebRender we appear to stick to the previous layerization decision
+ // after changing the bounds back to a smaller object.
+ const isWebRender =
+ SpecialPowers.DOMWindowUtils.layerManagerType.startsWith('WebRender');
+ assert_animation_property_state_equals(
+ animation.effect.getProperties(),
+ [ { property: subtest.property, runningOnCompositor: !isWebRender } ]);
+ }, subtest.property + ' on too big element - area');
+
+ promise_test(async t => {
+ var animation = addDivAndAnimate(t,
+ { class: 'compositable' },
+ subtest.frames,
+ 100 * MS_PER_SEC);
+ await waitForPaints();
+
+ assert_animation_property_state_equals(
+ animation.effect.getProperties(),
+ [ { property: subtest.property, runningOnCompositor: true } ]);
+ animation.effect.target.style = 'width: 20000px; height: 1px';
+ await waitForFrame();
+
+ // viewport depends on test environment.
+ var expectedWarning = new RegExp(
+ "Animation cannot be run on the compositor because the frame size " +
+ "\\(20000, 1\\) is too large relative to the viewport " +
+ "\\(larger than \\(\\d+, \\d+\\)\\) or larger than the " +
+ "maximum allowed value \\(\\d+, \\d+\\)");
+ assert_animation_property_state_equals(
+ animation.effect.getProperties(),
+ [ {
+ property: subtest.property,
+ runningOnCompositor: false,
+ warning: expectedWarning
+ } ]);
+ animation.effect.target.style = 'width: 100px; height: 100px';
+ await waitForFrame();
+
+ const isWebRender =
+ SpecialPowers.DOMWindowUtils.layerManagerType.startsWith('WebRender');
+ assert_animation_property_state_equals(
+ animation.effect.getProperties(),
+ [ { property: subtest.property, runningOnCompositor: !isWebRender } ]);
+ }, subtest.property + ' on too big element - dimensions');
+ });
+}
+
+function testTransformSVG() {
+ [
+ {
+ property: 'transform',
+ frames: { transform: ['translate(0px)', 'translate(100px)'] },
+ },
+ {
+ property: 'translate',
+ frames: { translate: ['0px', '100px'] },
+ },
+ {
+ property: 'rotate',
+ frames: { rotate: ['0deg', '45deg'] },
+ },
+ {
+ property: 'scale',
+ frames: { scale: ['1', '2'] },
+ },
+ ].forEach(subtest => {
+ promise_test(async 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(() => {
+ svg.remove();
+ });
+
+ var animation = svg.animate(subtest.frames, 100 * MS_PER_SEC);
+ await waitForPaints();
+
+ assert_animation_property_state_equals(
+ animation.effect.getProperties(),
+ [ { property: subtest.property, runningOnCompositor: true } ]);
+ svg.setAttribute('transform', 'translate(10, 20)');
+ await waitForFrame();
+
+ assert_animation_property_state_equals(
+ animation.effect.getProperties(),
+ [ {
+ property: subtest.property,
+ runningOnCompositor: false,
+ warning: 'CompositorAnimationWarningTransformSVG'
+ } ]);
+ svg.removeAttribute('transform');
+ await waitForFrame();
+
+ assert_animation_property_state_equals(
+ animation.effect.getProperties(),
+ [ { property: subtest.property, runningOnCompositor: true } ]);
+ }, subtest.property + ' of nsIFrame with SVG transform');
+ });
+}
+
+function testImportantRuleOverride() {
+ promise_test(async t => {
+ const elem = addDiv(t, { class: 'compositable' });
+ const anim = elem.animate({ translate: [ '0px', '100px' ],
+ rotate: ['0deg', '90deg'] },
+ 100 * MS_PER_SEC);
+
+ await waitForAnimationReadyToRestyle(anim);
+ await waitForPaints();
+
+ assert_animation_property_state_equals(
+ anim.effect.getProperties(),
+ [ { property: 'translate', runningOnCompositor: true },
+ { property: 'rotate', runningOnCompositor: true } ]
+ );
+
+ elem.style.setProperty('rotate', '45deg', 'important');
+ getComputedStyle(elem).rotate;
+
+ await waitForFrame();
+
+ assert_animation_property_state_equals(
+ anim.effect.getProperties(),
+ [
+ {
+ property: 'translate',
+ runningOnCompositor: false,
+ warning:
+ 'CompositorAnimationWarningTransformIsBlockedByImportantRules'
+ },
+ {
+ property: 'rotate',
+ runningOnCompositor: false,
+ warning:
+ 'CompositorAnimationWarningTransformIsBlockedByImportantRules'
+ },
+ ]
+ );
+ }, 'The animations of transform-like properties are not running on the ' +
+ 'compositor because any of the properties has important rules');
+}
+
+function testCurrentColor() {
+ if (SpecialPowers.DOMWindowUtils.layerManagerType.startsWith('WebRender')) {
+ return; // skip this test until bug 1510030 landed.
+ }
+ promise_test(async t => {
+ const animation = addDivAndAnimate(t, { class: 'compositable' },
+ { backgroundColor: [ 'currentColor',
+ 'red' ] },
+ 100 * MS_PER_SEC);
+ await waitForPaints();
+
+ assert_animation_property_state_equals(
+ animation.effect.getProperties(),
+ [ { property: 'background-color',
+ runningOnCompositor: false,
+ warning: 'CompositorAnimationWarningHasCurrentColor'
+ } ]);
+ }, 'Background color animations with `current-color` don\'t run on the '
+ + 'compositor');
+}
+
+function start() {
+ var bundleService = SpecialPowers.Cc['@mozilla.org/intl/stringbundle;1']
+ .getService(SpecialPowers.Ci.nsIStringBundleService);
+ gStringBundle = bundleService
+ .createBundle("chrome://global/locale/layout_errors.properties");
+
+ testBasicOperation();
+ testKeyframesWithGeometricProperties();
+ testSetOfGeometricProperties();
+ testStyleChanges();
+ testIdChanges();
+ testMultipleAnimations();
+ testMultipleAnimationsWithGeometricKeyframes();
+ testMultipleAnimationsWithGeometricAnimations();
+ testSmallElements();
+ testSynchronizedAnimations();
+ testTooLargeFrame();
+ testTransformSVG();
+ testImportantRuleOverride();
+ testCurrentColor();
+
+ promise_test(async 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);
+
+ await waitForPaints();
+ 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');
+
+ promise_test(async t => {
+ const keyframes = {
+ width: [ '100px', '200px' ],
+ transform: [ 'translate(0px)', 'translate(100px)' ],
+ "--foo": ["--bar", "--baz"],
+ };
+ const animation = addDivAndAnimate(t, { class: 'compositable' },
+ keyframes, 100 * MS_PER_SEC);
+ await waitForPaints();
+
+ assert_true(true, "Didn't crash");
+ }, 'Warning with custom props');
+
+ done();
+}
+
+</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 0000000000..497b04e068
--- /dev/null
+++ b/dom/animation/test/chrome/test_animation_properties.html
@@ -0,0 +1,837 @@
+<!doctype html>
+<head>
+<meta charset=utf-8>
+<title>Bug 1254419 - Test the values returned by
+ KeyframeEffect.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>
+@property --my-color {
+ syntax: "<color>";
+ inherits: true;
+ initial-value: "gold";
+}
+
+:root {
+ --var-100px: 100px;
+ --var-100px-200px: 100px 200px;
+}
+div {
+ font-size: 10px; /* For calculating em-based units */
+}
+</style>
+<script>
+'use strict';
+
+var gTests = [
+
+ // ---------------------------------------------------------------------
+ //
+ // Tests for property-indexed specifications
+ //
+ // ---------------------------------------------------------------------
+
+ { desc: 'a one-property two-value property-indexed specification',
+ frames: { left: ['10px', '20px'] },
+ expected: [ { property: 'left',
+ values: [ valueFormat(0, '10px', 'replace', 'linear'),
+ valueFormat(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: [ valueFormat(0, '10px', 'replace', 'linear'),
+ valueFormat(1, '10px', 'replace') ] },
+ { property: 'margin-right',
+ values: [ valueFormat(0, '10px', 'replace', 'linear'),
+ valueFormat(1, '20px', 'replace') ] },
+ { property: 'margin-bottom',
+ values: [ valueFormat(0, '10px', 'replace', 'linear'),
+ valueFormat(1, '30px', 'replace') ] },
+ { property: 'margin-left',
+ values: [ valueFormat(0, '10px', 'replace', 'linear'),
+ valueFormat(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: [ valueFormat(0, '50px', 'replace', 'linear'),
+ valueFormat(1, '60px', 'replace') ] },
+ { property: 'margin-right',
+ values: [ valueFormat(0, '10px', 'replace', 'linear'),
+ valueFormat(1, '20px', 'replace') ] },
+ { property: 'margin-bottom',
+ values: [ valueFormat(0, '10px', 'replace', 'linear'),
+ valueFormat(1, '30px', 'replace') ] },
+ { property: 'margin-left',
+ values: [ valueFormat(0, '10px', 'replace', 'linear'),
+ valueFormat(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: [ valueFormat(0, '10px', 'replace', 'linear'),
+ valueFormat(0.5, '20px', 'replace', 'linear'),
+ valueFormat(1, '30px', 'replace') ] },
+ { property: 'top',
+ values: [ valueFormat(0, '40px', 'replace', 'linear'),
+ valueFormat(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: [ valueFormat(0, '10px', 'replace', 'linear'),
+ valueFormat(0.25, '20px', 'replace', 'linear'),
+ valueFormat(0.5, '30px', 'replace', 'linear'),
+ valueFormat(0.75, '40px', 'replace', 'linear'),
+ valueFormat(1, '50px', 'replace') ] },
+ { property: 'top',
+ values: [ valueFormat(0, '15px', 'replace', 'linear'),
+ valueFormat(0.25, '25px', 'replace', 'linear'),
+ valueFormat(0.75, '45px', 'replace', 'linear'),
+ valueFormat(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: [ valueFormat(0, '0', 'replace', 'linear'),
+ valueFormat(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: [ valueFormat(0, 'rgb(7, 8, 9)', 'replace', 'linear'),
+ valueFormat(1, 'rgb(10, 11, 12)', 'replace') ] },
+ { property: 'border-left-color',
+ values: [ valueFormat(0, 'rgb(1, 2, 3)', 'replace', 'linear'),
+ valueFormat(1, 'rgb(4, 5, 6)', 'replace') ] },
+ { property: 'border-right-color',
+ values: [ valueFormat(0, 'rgb(7, 8, 9)', 'replace', 'linear'),
+ valueFormat(1, 'rgb(10, 11, 12)', 'replace') ] },
+ { property: 'border-top-color',
+ values: [ valueFormat(0, 'rgb(7, 8, 9)', 'replace', 'linear'),
+ valueFormat(1, 'rgb(10, 11, 12)', 'replace') ] },
+ { property: 'border-bottom-width',
+ values: [ valueFormat(0, '3px', 'replace', 'linear'),
+ valueFormat(1, '4px', 'replace') ] },
+ { property: 'border-left-width',
+ values: [ valueFormat(0, '1px', 'replace', 'linear'),
+ valueFormat(1, '2px', 'replace') ] },
+ { property: 'border-right-width',
+ values: [ valueFormat(0, '3px', 'replace', 'linear'),
+ valueFormat(1, '4px', 'replace') ] },
+ { property: 'border-top-width',
+ values: [ valueFormat(0, '3px', 'replace', 'linear'),
+ valueFormat(1, '4px', 'replace') ] },
+ { property: 'border-bottom-style',
+ values: [ valueFormat(0, 'dotted', 'replace', 'linear'),
+ valueFormat(1, 'dashed', 'replace') ] },
+ { property: 'border-left-style',
+ values: [ valueFormat(0, 'solid', 'replace', 'linear'),
+ valueFormat(1, 'solid', 'replace') ] },
+ { property: 'border-right-style',
+ values: [ valueFormat(0, 'dotted', 'replace', 'linear'),
+ valueFormat(1, 'dashed', 'replace') ] },
+ { property: 'border-top-style',
+ values: [ valueFormat(0, 'dotted', 'replace', 'linear'),
+ valueFormat(1, 'dashed', 'replace') ] },
+ { property: 'border-image-outset',
+ values: [ valueFormat(0, '0', 'replace', 'linear'),
+ valueFormat(1, '0', 'replace') ] },
+ { property: 'border-image-repeat',
+ values: [ valueFormat(0, 'stretch', 'replace', 'linear'),
+ valueFormat(1, 'stretch', 'replace') ] },
+ { property: 'border-image-slice',
+ values: [ valueFormat(0, '100%', 'replace', 'linear'),
+ valueFormat(1, '100%', 'replace') ] },
+ { property: 'border-image-source',
+ values: [ valueFormat(0, 'none', 'replace', 'linear'),
+ valueFormat(1, 'none', 'replace') ] },
+ { property: 'border-image-width',
+ values: [ valueFormat(0, '1', 'replace', 'linear'),
+ valueFormat(1, '1', '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: [ valueFormat(0, 'rgb(7, 8, 9)', 'replace', 'linear'),
+ valueFormat(1, 'rgb(10, 11, 12)', 'replace') ] },
+ { property: 'border-left-color',
+ values: [ valueFormat(0, 'rgb(1, 2, 3)', 'replace', 'linear'),
+ valueFormat(1, 'rgb(4, 5, 6)', 'replace') ] },
+ { property: 'border-right-color',
+ values: [ valueFormat(0, 'rgb(7, 8, 9)', 'replace', 'linear'),
+ valueFormat(1, 'rgb(10, 11, 12)', 'replace') ] },
+ { property: 'border-top-color',
+ values: [ valueFormat(0, 'rgb(7, 8, 9)', 'replace', 'linear'),
+ valueFormat(1, 'rgb(10, 11, 12)', 'replace') ] },
+ { property: 'border-bottom-width',
+ values: [ valueFormat(0, '3px', 'replace', 'linear'),
+ valueFormat(1, '4px', 'replace') ] },
+ { property: 'border-left-width',
+ values: [ valueFormat(0, '1px', 'replace', 'linear'),
+ valueFormat(1, '2px', 'replace') ] },
+ { property: 'border-right-width',
+ values: [ valueFormat(0, '3px', 'replace', 'linear'),
+ valueFormat(1, '4px', 'replace') ] },
+ { property: 'border-top-width',
+ values: [ valueFormat(0, '3px', 'replace', 'linear'),
+ valueFormat(1, '4px', 'replace') ] },
+ { property: 'border-bottom-style',
+ values: [ valueFormat(0, 'dotted', 'replace', 'linear'),
+ valueFormat(1, 'dashed', 'replace') ] },
+ { property: 'border-left-style',
+ values: [ valueFormat(0, 'solid', 'replace', 'linear'),
+ valueFormat(1, 'solid', 'replace') ] },
+ { property: 'border-right-style',
+ values: [ valueFormat(0, 'dotted', 'replace', 'linear'),
+ valueFormat(1, 'dashed', 'replace') ] },
+ { property: 'border-top-style',
+ values: [ valueFormat(0, 'dotted', 'replace', 'linear'),
+ valueFormat(1, 'dashed', 'replace') ] },
+ { property: 'border-image-outset',
+ values: [ valueFormat(0, '0', 'replace', 'linear'),
+ valueFormat(1, '0', 'replace') ] },
+ { property: 'border-image-repeat',
+ values: [ valueFormat(0, 'stretch', 'replace', 'linear'),
+ valueFormat(1, 'stretch', 'replace') ] },
+ { property: 'border-image-slice',
+ values: [ valueFormat(0, '100%', 'replace', 'linear'),
+ valueFormat(1, '100%', 'replace') ] },
+ { property: 'border-image-source',
+ values: [ valueFormat(0, 'none', 'replace', 'linear'),
+ valueFormat(1, 'none', 'replace') ] },
+ { property: 'border-image-width',
+ values: [ valueFormat(0, '1', 'replace', 'linear'),
+ valueFormat(1, '1', 'replace') ] },
+ ]
+ },
+ { desc: 'custom registered property',
+ frames: { "--my-color": ['red', 'blue'] },
+ expected: [ { property: '--my-color',
+ values: [ valueFormat(0, 'red', 'replace', 'linear'),
+ valueFormat(1, 'blue', '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: [ valueFormat(0, '100px', 'replace'),
+ valueFormat(0, '200px', 'replace', 'ease'),
+ valueFormat(0.5, '300px', 'replace', 'linear'),
+ valueFormat(1, '400px', 'replace'),
+ valueFormat(1, '500px', 'replace') ] } ]
+ },
+ { desc: 'a one-property two-keyframe sequence',
+ frames: [ { offset: 0, left: '10px' },
+ { offset: 1, left: '20px' } ],
+ expected: [ { property: 'left',
+ values: [ valueFormat(0, '10px', 'replace', 'linear'),
+ valueFormat(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: [ valueFormat(0, '10px', 'replace', 'linear'),
+ valueFormat(1, '20px', 'replace') ] },
+ { property: 'top',
+ values: [ valueFormat(0, '30px', 'replace', 'linear'),
+ valueFormat(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: [ valueFormat(0, '10px', 'replace', 'linear'),
+ valueFormat(1, '20px', 'replace') ] },
+ { property: 'margin-right',
+ values: [ valueFormat(0, '10px', 'replace', 'linear'),
+ valueFormat(1, '30px', 'replace') ] },
+ { property: 'margin-bottom',
+ values: [ valueFormat(0, '10px', 'replace', 'linear'),
+ valueFormat(1, '40px', 'replace') ] },
+ { property: 'margin-left',
+ values: [ valueFormat(0, '10px', 'replace', 'linear'),
+ valueFormat(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: [ valueFormat(0, '20px', 'replace', 'linear'),
+ valueFormat(1, '70px', 'replace') ] },
+ { property: 'margin-right',
+ values: [ valueFormat(0, '10px', 'replace', 'linear'),
+ valueFormat(1, '40px', 'replace') ] },
+ { property: 'margin-bottom',
+ values: [ valueFormat(0, '10px', 'replace', 'linear'),
+ valueFormat(1, '50px', 'replace') ] },
+ { property: 'margin-left',
+ values: [ valueFormat(0, '10px', 'replace', 'linear'),
+ valueFormat(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: [ valueFormat(0, '10px', 'replace', 'linear'),
+ valueFormat(0.5, '20px', 'replace'),
+ valueFormat(0.5, '40px', 'replace', 'linear'),
+ valueFormat(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: [ valueFormat(0, '10px', 'replace'),
+ valueFormat(0, '30px', 'replace', 'linear'),
+ valueFormat(1, '40px', 'replace'),
+ valueFormat(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: [ valueFormat(0, '10px', 'replace', 'linear'),
+ valueFormat(1, '40px', 'replace') ] },
+ { property: 'top',
+ values: [ valueFormat(0, '20px', 'replace', 'linear'),
+ valueFormat(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: [ valueFormat(0, '10px', 'replace', 'linear'),
+ valueFormat(0.25, '20px', 'replace', 'linear'),
+ valueFormat(0.5, '30px', 'replace', 'linear'),
+ valueFormat(0.75, '40px', 'replace', 'linear'),
+ valueFormat(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: [ valueFormat(0, '10px', 'replace', 'linear'),
+ valueFormat(0.25, '30px', 'replace', 'linear'),
+ valueFormat(0.5, '40px', 'replace', 'linear'),
+ valueFormat(0.75, '50px', 'replace', 'linear'),
+ valueFormat(1, '70px', 'replace') ] },
+ { property: 'top',
+ values: [ valueFormat(0, '20px', 'replace', 'linear'),
+ valueFormat(0.75, '60px', 'replace', 'linear'),
+ valueFormat(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: [ valueFormat(0, '10px', 'replace', 'linear'),
+ valueFormat(0.25, '20px', 'replace', 'linear'),
+ valueFormat(0.5, '30px', 'replace', 'linear'),
+ valueFormat(0.75, '40px', 'replace', 'linear'),
+ valueFormat(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: [ valueFormat(0, '10px', 'replace', 'ease'),
+ valueFormat(0.5, '30px', 'replace', 'linear'),
+ valueFormat(1, '50px', 'replace') ] },
+ { property: 'top',
+ values: [ valueFormat(0, '20px', 'replace', 'ease'),
+ valueFormat(0.5, '40px', 'replace', 'linear'),
+ valueFormat(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: [ valueFormat(0, '0', 'replace', 'linear'),
+ valueFormat(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: [ valueFormat(0, '10px', 'replace', 'linear'),
+ valueFormat(1, '30px', 'replace') ] },
+ { property: 'margin-right',
+ values: [ valueFormat(0, '20px', 'replace', 'linear'),
+ valueFormat(1, '30px', 'replace') ] },
+ { property: 'margin-bottom',
+ values: [ valueFormat(0, '10px', 'replace', 'linear'),
+ valueFormat(1, '30px', 'replace') ] },
+ { property: 'margin-left',
+ values: [ valueFormat(0, '10px', 'replace', 'linear'),
+ valueFormat(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: [ valueFormat(0, '10px', 'replace', 'linear'),
+ valueFormat(1, '30px', 'replace') ] },
+ { property: 'margin-right',
+ values: [ valueFormat(0, '20px', 'replace', 'linear'),
+ valueFormat(1, '30px', 'replace') ] },
+ { property: 'margin-bottom',
+ values: [ valueFormat(0, '10px', 'replace', 'linear'),
+ valueFormat(1, '30px', 'replace') ] },
+ { property: 'margin-left',
+ values: [ valueFormat(0, '10px', 'replace', 'linear'),
+ valueFormat(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: [ valueFormat(0, 'rgb(4, 5, 6)', 'replace', 'linear'),
+ valueFormat(1, 'rgb(7, 8, 9)', 'replace') ] },
+ { property: 'border-left-color',
+ values: [ valueFormat(0, 'rgb(1, 2, 3)', 'replace', 'linear'),
+ valueFormat(1, 'rgb(7, 8, 9)', 'replace') ] },
+ { property: 'border-right-color',
+ values: [ valueFormat(0, 'rgb(4, 5, 6)', 'replace', 'linear'),
+ valueFormat(1, 'rgb(7, 8, 9)', 'replace') ] },
+ { property: 'border-top-color',
+ values: [ valueFormat(0, 'rgb(4, 5, 6)', 'replace', 'linear'),
+ valueFormat(1, 'rgb(7, 8, 9)', 'replace') ] },
+ { property: 'border-bottom-width',
+ values: [ valueFormat(0, '2px', 'replace', 'linear'),
+ valueFormat(1, '3px', 'replace') ] },
+ { property: 'border-left-width',
+ values: [ valueFormat(0, '1px', 'replace', 'linear'),
+ valueFormat(1, '3px', 'replace') ] },
+ { property: 'border-right-width',
+ values: [ valueFormat(0, '2px', 'replace', 'linear'),
+ valueFormat(1, '3px', 'replace') ] },
+ { property: 'border-top-width',
+ values: [ valueFormat(0, '2px', 'replace', 'linear'),
+ valueFormat(1, '3px', 'replace') ] },
+ { property: 'border-bottom-style',
+ values: [ valueFormat(0, 'dotted', 'replace', 'linear'),
+ valueFormat(1, 'dashed', 'replace') ] },
+ { property: 'border-left-style',
+ values: [ valueFormat(0, 'solid', 'replace', 'linear'),
+ valueFormat(1, 'dashed', 'replace') ] },
+ { property: 'border-right-style',
+ values: [ valueFormat(0, 'dotted', 'replace', 'linear'),
+ valueFormat(1, 'dashed', 'replace') ] },
+ { property: 'border-top-style',
+ values: [ valueFormat(0, 'dotted', 'replace', 'linear'),
+ valueFormat(1, 'dashed', 'replace') ] },
+ { property: 'border-image-outset',
+ values: [ valueFormat(0, '0', 'replace', 'linear'),
+ valueFormat(1, '0', 'replace') ] },
+ { property: 'border-image-repeat',
+ values: [ valueFormat(0, 'stretch', 'replace', 'linear'),
+ valueFormat(1, 'stretch', 'replace') ] },
+ { property: 'border-image-slice',
+ values: [ valueFormat(0, '100%', 'replace', 'linear'),
+ valueFormat(1, '100%', 'replace') ] },
+ { property: 'border-image-source',
+ values: [ valueFormat(0, 'none', 'replace', 'linear'),
+ valueFormat(1, 'none', 'replace') ] },
+ { property: 'border-image-width',
+ values: [ valueFormat(0, '1', 'replace', 'linear'),
+ valueFormat(1, '1', '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: [ valueFormat(0, 'rgb(4, 5, 6)', 'replace', 'linear'),
+ valueFormat(1, 'rgb(7, 8, 9)', 'replace') ] },
+ { property: 'border-left-color',
+ values: [ valueFormat(0, 'rgb(1, 2, 3)', 'replace', 'linear'),
+ valueFormat(1, 'rgb(7, 8, 9)', 'replace') ] },
+ { property: 'border-right-color',
+ values: [ valueFormat(0, 'rgb(4, 5, 6)', 'replace', 'linear'),
+ valueFormat(1, 'rgb(7, 8, 9)', 'replace') ] },
+ { property: 'border-top-color',
+ values: [ valueFormat(0, 'rgb(4, 5, 6)', 'replace', 'linear'),
+ valueFormat(1, 'rgb(7, 8, 9)', 'replace') ] },
+ { property: 'border-bottom-width',
+ values: [ valueFormat(0, '2px', 'replace', 'linear'),
+ valueFormat(1, '3px', 'replace') ] },
+ { property: 'border-left-width',
+ values: [ valueFormat(0, '1px', 'replace', 'linear'),
+ valueFormat(1, '3px', 'replace') ] },
+ { property: 'border-right-width',
+ values: [ valueFormat(0, '2px', 'replace', 'linear'),
+ valueFormat(1, '3px', 'replace') ] },
+ { property: 'border-top-width',
+ values: [ valueFormat(0, '2px', 'replace', 'linear'),
+ valueFormat(1, '3px', 'replace') ] },
+ { property: 'border-bottom-style',
+ values: [ valueFormat(0, 'dotted', 'replace', 'linear'),
+ valueFormat(1, 'dashed', 'replace') ] },
+ { property: 'border-left-style',
+ values: [ valueFormat(0, 'solid', 'replace', 'linear'),
+ valueFormat(1, 'dashed', 'replace') ] },
+ { property: 'border-right-style',
+ values: [ valueFormat(0, 'dotted', 'replace', 'linear'),
+ valueFormat(1, 'dashed', 'replace') ] },
+ { property: 'border-top-style',
+ values: [ valueFormat(0, 'dotted', 'replace', 'linear'),
+ valueFormat(1, 'dashed', 'replace') ] },
+ { property: 'border-image-outset',
+ values: [ valueFormat(0, '0', 'replace', 'linear'),
+ valueFormat(1, '0', 'replace') ] },
+ { property: 'border-image-repeat',
+ values: [ valueFormat(0, 'stretch', 'replace', 'linear'),
+ valueFormat(1, 'stretch', 'replace') ] },
+ { property: 'border-image-slice',
+ values: [ valueFormat(0, '100%', 'replace', 'linear'),
+ valueFormat(1, '100%', 'replace') ] },
+ { property: 'border-image-source',
+ values: [ valueFormat(0, 'none', 'replace', 'linear'),
+ valueFormat(1, 'none', 'replace') ] },
+ { property: 'border-image-width',
+ values: [ valueFormat(0, '1', 'replace', 'linear'),
+ valueFormat(1, '1', 'replace') ] },
+ ]
+ },
+
+ // ---------------------------------------------------------------------
+ //
+ // Tests for unit conversion
+ //
+ // ---------------------------------------------------------------------
+
+ { desc: 'em units are resolved to px values',
+ frames: { left: ['10em', '20em'] },
+ expected: [ { property: 'left',
+ values: [ valueFormat(0, '100px', 'replace', 'linear'),
+ valueFormat(1, '200px', 'replace') ] } ]
+ },
+ { desc: 'calc() expressions are resolved to the equivalent units',
+ frames: { left: ['calc(10em + 10px)', 'calc(10em + 10%)'] },
+ expected: [ { property: 'left',
+ values: [ valueFormat(0, '110px', 'replace', 'linear'),
+ valueFormat(1, 'calc(10% + 100px)', '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: [ valueFormat(0, '10px', 'replace', 'linear'),
+ valueFormat(1, '100px', 'replace') ] } ]
+ },
+ { desc: 'CSS variables in calc() expressions are resolved',
+ frames: { left: ['10px', 'calc(var(--var-100px) / 2 - 10%)'] },
+ expected: [ { property: 'left',
+ values: [ valueFormat(0, '10px', 'replace', 'linear'),
+ valueFormat(1, 'calc(-10% + 50px)', 'replace') ] } ]
+ },
+ { desc: 'CSS variables in shorthands are resolved to their corresponding'
+ + ' values',
+ frames: { margin: ['10px', 'var(--var-100px-200px)'] },
+ expected: [ { property: 'margin-top',
+ values: [ valueFormat(0, '10px', 'replace', 'linear'),
+ valueFormat(1, '100px', 'replace') ] },
+ { property: 'margin-right',
+ values: [ valueFormat(0, '10px', 'replace', 'linear'),
+ valueFormat(1, '200px', 'replace') ] },
+ { property: 'margin-bottom',
+ values: [ valueFormat(0, '10px', 'replace', 'linear'),
+ valueFormat(1, '100px', 'replace') ] },
+ { property: 'margin-left',
+ values: [ valueFormat(0, '10px', 'replace', 'linear'),
+ valueFormat(1, '200px', 'replace') ] } ]
+ },
+
+ // ---------------------------------------------------------------------
+ //
+ // Tests for properties that parse correctly but which we fail to
+ // convert to computed values.
+ //
+ // ---------------------------------------------------------------------
+
+ { desc: 'a missing property in initial keyframe',
+ frames: [ { },
+ { margin: '5px' } ],
+ expected: [ { property: 'margin-top',
+ values: [ valueFormat(0, undefined, 'replace', 'linear'),
+ valueFormat(1, '5px', 'replace') ] },
+ { property: 'margin-right',
+ values: [ valueFormat(0, undefined, 'replace', 'linear'),
+ valueFormat(1, '5px', 'replace') ] },
+ { property: 'margin-bottom',
+ values: [ valueFormat(0, undefined, 'replace', 'linear'),
+ valueFormat(1, '5px', 'replace') ] },
+ { property: 'margin-left',
+ values: [ valueFormat(0, undefined, 'replace', 'linear'),
+ valueFormat(1, '5px', 'replace') ] } ]
+ },
+ { desc: 'a missing property in initial keyframe and there are some ' +
+ 'keyframes with the same offset',
+ frames: [ { },
+ { margin: '10px', offset: 0.5 },
+ { margin: '20px', offset: 0.5 },
+ { margin: '30px'} ],
+ expected: [ { property: 'margin-top',
+ values: [ valueFormat(0, undefined, 'replace', 'linear'),
+ valueFormat(0.5, '10px', 'replace'),
+ valueFormat(0.5, '20px', 'replace', 'linear'),
+ valueFormat(1, '30px', 'replace') ] },
+ { property: 'margin-right',
+ values: [ valueFormat(0, undefined, 'replace', 'linear'),
+ valueFormat(0.5, '10px', 'replace'),
+ valueFormat(0.5, '20px', 'replace', 'linear'),
+ valueFormat(1, '30px', 'replace') ] },
+ { property: 'margin-bottom',
+ values: [ valueFormat(0, undefined, 'replace', 'linear'),
+ valueFormat(0.5, '10px', 'replace'),
+ valueFormat(0.5, '20px', 'replace', 'linear'),
+ valueFormat(1, '30px', 'replace') ] },
+ { property: 'margin-left',
+ values: [ valueFormat(0, undefined, 'replace', 'linear'),
+ valueFormat(0.5, '10px', 'replace'),
+ valueFormat(0.5, '20px', 'replace', 'linear'),
+ valueFormat(1, '30px', 'replace') ] } ]
+ },
+ { desc: 'a missing property in final keyframe',
+ frames: [ { margin: '5px' },
+ { } ],
+ expected: [ { property: 'margin-top',
+ values: [ valueFormat(0, '5px', 'replace', 'linear'),
+ valueFormat(1, undefined, 'replace') ] },
+ { property: 'margin-right',
+ values: [ valueFormat(0, '5px', 'replace', 'linear'),
+ valueFormat(1, undefined, 'replace') ] },
+ { property: 'margin-bottom',
+ values: [ valueFormat(0, '5px', 'replace', 'linear'),
+ valueFormat(1, undefined, 'replace') ] },
+ { property: 'margin-left',
+ values: [ valueFormat(0, '5px', 'replace', 'linear'),
+ valueFormat(1, undefined, 'replace') ] } ]
+ },
+ { desc: 'a missing property in final keyframe and there are some ' +
+ 'keyframes with the same offsets',
+ frames: [ { margin: '5px' },
+ { margin: '10px', offset: 0.5 },
+ { margin: '20px', offset: 0.5 },
+ { } ],
+ expected: [ { property: 'margin-top',
+ values: [ valueFormat(0, '5px', 'replace', 'linear'),
+ valueFormat(0.5, '10px', 'replace'),
+ valueFormat(0.5, '20px', 'replace', 'linear'),
+ valueFormat(1, undefined, 'replace') ] },
+ { property: 'margin-right',
+ values: [ valueFormat(0, '5px', 'replace', 'linear'),
+ valueFormat(0.5, '10px', 'replace'),
+ valueFormat(0.5, '20px', 'replace', 'linear'),
+ valueFormat(1, undefined, 'replace') ] },
+ { property: 'margin-bottom',
+ values: [ valueFormat(0, '5px', 'replace', 'linear'),
+ valueFormat(0.5, '10px', 'replace'),
+ valueFormat(0.5, '20px', 'replace', 'linear'),
+ valueFormat(1, undefined, 'replace') ] },
+ { property: 'margin-left',
+ values: [ valueFormat(0, '5px', 'replace', 'linear'),
+ valueFormat(0.5, '10px', 'replace'),
+ valueFormat(0.5, '20px', 'replace', 'linear'),
+ valueFormat(1, undefined, 'replace') ] } ]
+ },
+ { desc: 'a missing property in final keyframe where it forms the last'
+ + ' segment in the series',
+ frames: [ { margin: '5px' },
+ { marginLeft: '5px',
+ marginRight: '5px',
+ marginBottom: '5px' } ],
+ expected: [ { property: 'margin-bottom',
+ values: [ valueFormat(0, '5px', 'replace', 'linear'),
+ valueFormat(1, '5px', 'replace') ] },
+ { property: 'margin-left',
+ values: [ valueFormat(0, '5px', 'replace', 'linear'),
+ valueFormat(1, '5px', 'replace') ] },
+ { property: 'margin-right',
+ values: [ valueFormat(0, '5px', 'replace', 'linear'),
+ valueFormat(1, '5px', 'replace') ] },
+ { property: 'margin-top',
+ values: [ valueFormat(0, '5px', 'replace', 'linear'),
+ valueFormat(1, undefined, 'replace') ] } ]
+ },
+ { desc: 'a missing property in initial keyframe along with other values',
+ frames: [ { left: '10px' },
+ { margin: '5px', left: '20px' } ],
+ expected: [ { property: 'left',
+ values: [ valueFormat(0, '10px', 'replace', 'linear'),
+ valueFormat(1, '20px', 'replace') ] },
+ { property: 'margin-top',
+ values: [ valueFormat(0, undefined, 'replace', 'linear'),
+ valueFormat(1, '5px', 'replace') ] },
+ { property: 'margin-right',
+ values: [ valueFormat(0, undefined, 'replace', 'linear'),
+ valueFormat(1, '5px', 'replace') ] },
+ { property: 'margin-bottom',
+ values: [ valueFormat(0, undefined, 'replace', 'linear'),
+ valueFormat(1, '5px', 'replace') ] },
+ { property: 'margin-left',
+ values: [ valueFormat(0, undefined, 'replace', 'linear'),
+ valueFormat(1, '5px', 'replace') ] } ]
+ },
+ { desc: 'a missing property in final keyframe along with other values',
+ frames: [ { margin: '5px', left: '10px' },
+ { left: '20px' } ],
+ expected: [ { property: 'left',
+ values: [ valueFormat(0, '10px', 'replace', 'linear'),
+ valueFormat(1, '20px', 'replace') ] },
+ { property: 'margin-top',
+ values: [ valueFormat(0, '5px', 'replace', 'linear'),
+ valueFormat(1, undefined, 'replace') ] },
+ { property: 'margin-right',
+ values: [ valueFormat(0, '5px', 'replace', 'linear'),
+ valueFormat(1, undefined, 'replace') ] },
+ { property: 'margin-bottom',
+ values: [ valueFormat(0, '5px', 'replace', 'linear'),
+ valueFormat(1, undefined, 'replace') ] },
+ { property: 'margin-left',
+ values: [ valueFormat(0, '5px', 'replace', 'linear'),
+ valueFormat(1, undefined, 'replace') ] } ]
+ },
+ { desc: 'missing properties in both of initial and final keyframe',
+ frames: [ { left: '5px', offset: 0.5 } ],
+ expected: [ { property: 'left',
+ values: [ valueFormat(0, undefined, 'replace', 'linear'),
+ valueFormat(0.5, '5px', 'replace', 'linear'),
+ valueFormat(1, undefined, 'replace') ] } ]
+ },
+ { desc: 'missing propertes in both of initial and final keyframe along '
+ + 'with other values',
+ frames: [ { left: '5px', offset: 0 },
+ { right: '5px', offset: 0.5 },
+ { left: '10px', offset: 1 } ],
+ expected: [ { property: 'left',
+ values: [ valueFormat(0, '5px', 'replace', 'linear'),
+ valueFormat(1, '10px', 'replace') ] },
+ { property: 'right',
+ values: [ valueFormat(0, undefined, 'replace', 'linear'),
+ valueFormat(0.5, '5px', 'replace', 'linear'),
+ valueFormat(1, undefined, 'replace') ] } ]
+ },
+
+ { desc: 'a missing property in final keyframe with duplicate offset ' +
+ + 'along with other values',
+ frames: [ { left: '5px', right: '5px', offset: 0 },
+ { left: '8px', right: '8px', offset: 0 },
+ { left: '10px', offset: 1 } ],
+ expected: [ { property: 'left',
+ values: [ valueFormat(0, '5px', 'replace'),
+ valueFormat(0, '8px', 'replace', 'linear'),
+ valueFormat(1, '10px', 'replace') ] },
+ { property: 'right',
+ values: [ valueFormat(0, '5px', 'replace'),
+ valueFormat(0, '8px', 'replace', 'linear'),
+ valueFormat(1, undefined, 'replace') ] } ]
+ },
+
+ { desc: 'a missing property in initial keyframe with duplicate offset '
+ + 'along with other values',
+ frames: [ { left: '10px', offset: 0 },
+ { left: '8px', right: '8px', offset: 1 },
+ { left: '5px', right: '5px', offset: 1 } ],
+ expected: [ { property: 'left',
+ values: [ valueFormat(0, '10px', 'replace', 'linear'),
+ valueFormat(1, '8px', 'replace'),
+ valueFormat(1, '5px', 'replace') ] },
+ { property: 'right',
+ values: [ valueFormat(0, undefined, 'replace', 'linear'),
+ valueFormat(1, '8px', 'replace'),
+ valueFormat(1, '5px', 'replace') ] } ]
+ },
+];
+
+gTests.forEach(function(subtest) {
+ test(function(t) {
+ var div = addDiv(t);
+ var animation = div.animate(subtest.frames, 100 * MS_PER_SEC);
+ // Flush styles since getProperties currently does not. Rather, it
+ // returns the actual properties in use at the current time.
+ // However, we want to test what these properties will look like
+ // after the next restyle.
+ getComputedStyle(div).opacity;
+ assert_properties_equal(
+ animation.effect.getProperties(),
+ subtest.expected
+ );
+ }, subtest.desc);
+});
+
+</script>
+</body>
diff --git a/dom/animation/test/chrome/test_animation_properties_display.html b/dom/animation/test/chrome/test_animation_properties_display.html
new file mode 100644
index 0000000000..1d558d2114
--- /dev/null
+++ b/dom/animation/test/chrome/test_animation_properties_display.html
@@ -0,0 +1,34 @@
+<!doctype html>
+<head>
+<meta charset=utf-8>
+<title>Bug 1536688 - Test that 'display' is not included in
+ KeyframeEffect.getProperties() when using shorthand 'all'</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=1536688"
+ target="_blank">Mozilla Bug 1536688</a>
+<div id="log"></div>
+<script>
+'use strict';
+
+test(t => {
+ const div = addDiv(t);
+ const animation = div.animate(
+ { all: ['unset', 'unset'] },
+ 100 * MS_PER_SEC
+ );
+ // Flush styles since getProperties does not.
+ getComputedStyle(div).opacity;
+
+ const properties = animation.effect.getProperties();
+ assert_false(
+ properties.some(property => property.property === 'display'),
+ 'Should not have a property for display'
+ );
+});
+
+</script>
+</body>
diff --git a/dom/animation/test/chrome/test_cssanimation_missing_keyframes.html b/dom/animation/test/chrome/test_cssanimation_missing_keyframes.html
new file mode 100644
index 0000000000..8c599655b7
--- /dev/null
+++ b/dom/animation/test/chrome/test_cssanimation_missing_keyframes.html
@@ -0,0 +1,66 @@
+<!doctype html>
+<head>
+<meta charset=utf-8>
+<title>Bug 1339332 - Test for missing keyframes in CSS Animation</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=1339332"
+ target="_blank">Mozilla Bug 1339332</a>
+<div id="log"></div>
+<style>
+@keyframes missingFrom {
+ to {
+ text-align: right;
+ }
+}
+@keyframes missingBoth {
+ 50% {
+ text-align: right;
+ }
+}
+@keyframes missingTo {
+ from {
+ text-align: right;
+ }
+}
+</style>
+<script>
+'use strict';
+
+const gTests = [
+ { desc: 'missing "from" keyframe',
+ animationName: 'missingFrom',
+ expected: [{ property: 'text-align',
+ values: [valueFormat(0, undefined, 'replace', 'ease'),
+ valueFormat(1, 'right', 'replace')] } ]
+ },
+ { desc: 'missing "to" keyframe',
+ animationName: 'missingTo',
+ expected: [{ property: 'text-align',
+ values: [valueFormat(0, 'right', 'replace', 'ease'),
+ valueFormat(1, undefined, 'replace')] } ]
+ },
+ { desc: 'missing "from" and "to" keyframes',
+ animationName: 'missingBoth',
+ expected: [{ property: 'text-align',
+ values: [valueFormat(0, undefined, 'replace', 'ease'),
+ valueFormat(.5, 'right', 'replace', 'ease'),
+ valueFormat(1, undefined, 'replace')] } ]
+ },
+];
+
+gTests.forEach(function(subtest) {
+ test(function(t) {
+ const div = addDiv(t);
+ div.style.animation = `${ subtest.animationName } 1000s`;
+ const animation = div.getAnimations()[0];
+ 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 0000000000..41010ca917
--- /dev/null
+++ b/dom/animation/test/chrome/test_generated_content_getAnimations.html
@@ -0,0 +1,81 @@
+<!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';
+
+function getWalker(node) {
+ var walker = Cc["@mozilla.org/inspector/deep-tree-walker;1"].
+ createInstance(Ci.inIDeepTreeWalker);
+ walker.showAnonymousContent = true;
+ walker.init(node.ownerDocument, NodeFilter.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_keyframe_effect_xrays.html b/dom/animation/test/chrome/test_keyframe_effect_xrays.html
new file mode 100644
index 0000000000..ca3e712ac5
--- /dev/null
+++ b/dom/animation/test/chrome/test_keyframe_effect_xrays.html
@@ -0,0 +1,45 @@
+<!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=1414674"
+ target="_blank">Mozilla Bug 1414674</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 effect = new win.KeyframeEffect(target, [
+ {opacity: 1, offset: 0},
+ {opacity: 0, offset: 1},
+ ], {duration: 100 * MS_PER_SEC, fill: "forwards"});
+ // The frames object should be accessible via x-ray.
+ var frames = effect.getKeyframes();
+ assert_equals(frames.length, 2,
+ "frames for KeyframeEffect ctor should be non-zero");
+ assert_equals(frames[0].opacity, "1",
+ "first frame opacity for KeyframeEffect ctor should be specified value");
+ assert_equals(frames[0].computedOffset, 0,
+ "first frame offset for KeyframeEffect ctor should be 0");
+ assert_equals(frames[1].opacity, "0",
+ "last frame opacity for KeyframeEffect ctor should be specified value");
+ assert_equals(frames[1].computedOffset, 1,
+ "last frame offset for KeyframeEffect ctor should be 1");
+ var animation = new win.Animation(effect, document.timeline);
+ animation.play();
+ t.done();
+ }));
+}, 'Calling KeyframeEffect() ctor across x-rays');
+
+</script>
+</body>
diff --git a/dom/animation/test/chrome/test_mutation_observer_for_element_removal_in_shadow_tree.html b/dom/animation/test/chrome/test_mutation_observer_for_element_removal_in_shadow_tree.html
new file mode 100644
index 0000000000..f8efaa6baf
--- /dev/null
+++ b/dom/animation/test/chrome/test_mutation_observer_for_element_removal_in_shadow_tree.html
@@ -0,0 +1,45 @@
+<!DOCTYPE html>
+<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>
+<div id="log"></div>
+<script>
+
+promise_test(async t => {
+ // Set up a MutationObserver for animations.
+ const observer = new MutationObserver(() => {});
+ observer.observe(document.documentElement, {
+ animations: true,
+ subtree: true,
+ });
+
+ // Create a CSS transition in a shadow tree.
+ let s = document.createElement('shadow-test');
+ document.documentElement.appendChild(s);
+ s.attachShadow({mode:"open"});
+
+ let property = 'opacity';
+ let initial = '1';
+ let finalValue = '0';
+
+ let div = document.createElement('div');
+ div.style = `${property}:${initial};transition:${property} 2s;`
+
+ s.shadowRoot.appendChild(div);
+ div.offsetWidth;
+
+ div.style[property] = finalValue;
+
+ const eventWatcher = new EventWatcher(t, div, ['transitionstart']);
+
+ // Trigger a CSS transition.
+ getComputedStyle(div)[property];
+
+ // Wait for a transitionend event to make sure the transition has been started.
+ await eventWatcher.wait_for('transitionstart');
+
+ // Now remove the element to notify it to the observer
+ div.parentNode.removeChild(div);
+});
+</script>
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 0000000000..d8c1d0573e
--- /dev/null
+++ b/dom/animation/test/chrome/test_running_on_compositor.html
@@ -0,0 +1,1656 @@
+<!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 zIndex_and_translate {
+ to { z-index: 999; transform: translate(100px); }
+}
+@keyframes z-index {
+ to { z-index: 999; }
+}
+@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 **/
+
+const 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(async 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];
+
+ // If the animation starts at the current timeline time, we need to wait for
+ // one more frame to avoid receiving the fake timer-based MozAfterPaint event.
+ // FIXME: Bug 1419226: Drop this 'animation.ready' and 'waitForFrame'. Once
+ // MozAfterPaint is fired reliably, we just need to wait for a MozAfterPaint
+ // here.
+ await animation.ready;
+
+ if (animationStartsRightNow(animation)) {
+ await waitForNextFrame();
+ }
+
+ await waitForPaints();
+
+ assert_animation_is_running_on_compositor(animation,
+ 'Animation reports that it is running on the compositor'
+ + ' during playback');
+
+ div.style.animationPlayState = 'paused';
+
+ await animation.ready;
+
+ assert_animation_is_not_running_on_compositor(animation,
+ 'Animation reports that it is NOT running on the compositor'
+ + ' when paused');
+}, '');
+
+promise_test(async t => {
+ var div = addDiv(t, { style: 'animation: z-index 100s' });
+ var animation = div.getAnimations()[0];
+
+ await waitForPaints();
+
+ assert_animation_is_not_running_on_compositor(animation,
+ 'Animation reports that it is NOT running on the compositor'
+ + ' for animation of "z-index"');
+}, 'isRunningOnCompositor is false for animation of "z-index"');
+
+promise_test(async t => {
+ var div = addDiv(t, { style: 'animation: zIndex_and_translate 100s' });
+ var animation = div.getAnimations()[0];
+
+ await waitForPaints();
+
+ 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(async t => {
+ var div = addDiv(t, { style: 'animation: anim 100s' });
+ var animation = div.getAnimations()[0];
+
+ await waitForPaints();
+
+ animation.pause();
+ await animation.ready;
+
+ 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(async t => {
+ var div = addDiv(t, { style: 'animation: anim 100s' });
+ var animation = div.getAnimations()[0];
+
+ await waitForPaints();
+
+ 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.
+ await waitForFrame();
+
+ 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(async t => {
+ var div = addDiv(t, { style: 'animation: anim 100s' });
+ var animation = div.getAnimations()[0];
+
+ await waitForPaints();
+
+ 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.
+ await waitForFrame();
+
+ 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(async t => {
+ var div = addDiv(t, { style: 'animation: anim 100s' });
+ var animation = div.getAnimations()[0];
+
+ await waitForPaints();
+
+ 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.
+ await waitForFrame();
+
+ 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(async t => {
+ var div = addDiv(t, { style: 'animation: anim 100s' });
+ var animation = div.getAnimations()[0];
+
+ await waitForPaints();
+
+ await new Promise(resolve => {
+ window.requestAnimationFrame(() => {
+ t.step(() => {
+ 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(async t => {
+ var div = addDiv(t, { style: 'animation: anim 100s' });
+ var animation = div.getAnimations()[0];
+
+ await waitForPaints();
+
+ await new Promise(resolve => {
+ var observer = new MutationObserver(records => {
+ var changedAnimation;
+
+ records.forEach(record => {
+ changedAnimation =
+ record.changedAnimations.find(changedAnim => {
+ return changedAnim == animation;
+ });
+ });
+
+ t.step(() => {
+ 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(() => {
+ 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(async t => {
+ var div = addDiv(t, { style: 'animation: rotate 100s' });
+ var animation = div.getAnimations()[0];
+
+ await waitForPaints();
+
+ await new Promise(resolve => {
+ var timeAtStart = window.performance.now();
+ function handleFrame() {
+ t.step(() => {
+ 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 https://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(async t => {
+ var div = addDiv(t, { style: 'transition: opacity 100s; opacity: 1' });
+
+ getComputedStyle(div).opacity;
+
+ div.style.opacity = 0;
+ var animation = div.getAnimations()[0];
+
+ await waitForPaints();
+
+ 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(async t => {
+ var div = addDiv(t, { style: 'animation: rotate-and-opacity 100s; ' +
+ 'backface-visibility: hidden; ' +
+ 'transform: none !important;' });
+ var animation = div.getAnimations()[0];
+
+ await waitForPaints();
+
+ 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(async t => {
+ var animation = addDivAndAnimate(t,
+ {},
+ { opacity: [ 0, 1 ] }, 200 * MS_PER_SEC);
+
+ await waitForPaints();
+
+ assert_animation_is_running_on_compositor(animation,
+ 'Animation reports that it is running on the compositor');
+
+ animation.currentTime = 150 * MS_PER_SEC;
+ animation.effect.updateTiming({ 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 the duration is made shorter than the current time');
+
+promise_test(async t => {
+ var animation = addDivAndAnimate(t,
+ {},
+ { opacity: [ 0, 1 ] }, 100 * MS_PER_SEC);
+
+ await waitForPaints();
+
+ 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.updateTiming({ duration: 1000 * MS_PER_SEC });
+ await waitForFrame();
+
+ assert_animation_is_running_on_compositor(animation,
+ 'Animation reports that it is running on the compositor'
+ + ' when restarted');
+}, 'animation is added to compositor' +
+ ' when the duration is made longer than the current time');
+
+promise_test(async t => {
+ var animation = addDivAndAnimate(t,
+ {},
+ { opacity: [ 0, 1 ] }, 100 * MS_PER_SEC);
+
+ await waitForPaints();
+
+ assert_animation_is_running_on_compositor(animation,
+ 'Animation reports that it is running on the compositor');
+
+ animation.effect.updateTiming({ 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;
+ await waitForFrame();
+
+ 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(async t => {
+ var animation = addDivAndAnimate(t,
+ {},
+ { opacity: [ 0, 1 ] }, 100 * MS_PER_SEC);
+
+ await waitForPaints();
+
+ assert_animation_is_running_on_compositor(animation,
+ 'Animation reports that it is running on the compositor');
+
+ animation.effect.updateTiming({ endDelay: -200 * MS_PER_SEC });
+ await waitForFrame();
+
+ 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(async t => {
+ var animation = addDivAndAnimate(t,
+ {},
+ { opacity: [ 0, 1 ] }, 200 * MS_PER_SEC);
+
+ await waitForPaints();
+
+ assert_animation_is_running_on_compositor(animation,
+ 'Animation reports that it is running on the compositor');
+
+ animation.effect.updateTiming({ endDelay: -100 * MS_PER_SEC });
+ await waitForFrame();
+
+ 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;
+ await waitForFrame();
+
+ 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(async 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);
+
+ await waitForPaints();
+
+ 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;
+ await waitForFrame();
+
+ 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(async t => {
+ var div = addDiv(t);
+ var animation = div.animate({ opacity: [ 0, 1 ] }, 100 * MS_PER_SEC);
+
+ await waitForPaints();
+
+ 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(async t => {
+ var div = addDiv(t);
+ var animation = div.animate({ opacity: [ 0, 1 ] },
+ { duration: 100 * MS_PER_SEC,
+ delay: 100 * MS_PER_SEC,
+ fill: 'backwards' });
+
+ await waitForPaints();
+
+ 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;
+ await waitForFrame();
+
+ 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 ' +
+ ' compositor while it is in delay phase');
+
+promise_test(async t => {
+ const animation = addDiv(t).animate({ opacity: [ 0, 1 ] }, 100 * MS_PER_SEC);
+ animation.playbackRate = -1;
+ animation.currentTime = 200 * MS_PER_SEC;
+
+ await waitForPaints();
+
+ assert_animation_is_running_on_compositor(animation,
+ 'Animation with negative playback rate is runnning on the'
+ + ' compositor even before it reaches the active interval');
+}, 'animation with negative playback rate is sent to the compositor even in'
+ + ' after phase');
+
+promise_test(async 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);
+
+ await waitForPaints();
+
+ 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;
+ await waitForFrame();
+
+ 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(async t => {
+ var div = addDiv(t);
+ var animation = div.animate({ color: ['red', 'black'] }, 100 * MS_PER_SEC);
+
+ await waitForPaints();
+
+ 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 }]);
+ await waitForFrame();
+
+ 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(async 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);
+
+ await waitForPaints();
+
+ assert_animation_is_not_running_on_compositor(animation,
+ 'Color animation reports that it is not running on the ' +
+ 'compositor');
+
+ animation.effect = effect;
+ await waitForFrame();
+
+ 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(async t => {
+ var div = addDiv(t, { style: "opacity: 1 ! important" });
+
+ var animation = div.animate({ opacity: [0, 1] }, 100 * MS_PER_SEC);
+
+ await waitForPaints();
+
+ 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", "");
+ await waitForFrame();
+
+ 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(async 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);
+
+ await waitForPaints();
+
+ 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(async 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);
+
+ await waitForPaints();
+
+ 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(async t => {
+ var div = addDiv(t);
+ var importantOpacityElement = addDiv(t, { style: "opacity: 1 ! important" });
+
+ var animation = div.animate({ opacity: [1, 0] }, 100 * MS_PER_SEC);
+
+ await waitForPaints();
+
+ assert_animation_is_running_on_compositor(animation,
+ 'Opacity animation on an element reports ' +
+ 'that it is running on the compositor');
+
+ animation.effect.target = null;
+ await waitForFrame();
+
+ 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;
+ await waitForFrame();
+
+ 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(async 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);
+
+ await waitForPaints();
+
+ 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;
+ await waitForFrame();
+
+ 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;
+ await waitForFrame();
+
+ 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(async 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);
+
+ await waitForPaints();
+
+ 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;
+ await waitForFrame();
+
+ 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;
+ await waitForFrame();
+
+ 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: t => {
+ return addDiv(t).animate(
+ { opacity: [0, 1] },
+ { delay: 100 * MS_PER_SEC, duration: 100 * MS_PER_SEC });
+ },
+ },
+ {
+ desc: 'script animation of transform',
+ setupAnimation: 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: t => {
+ return addDiv(t, { style: 'animation: opacity 100s 100s' })
+ .getAnimations()[0];
+ },
+ },
+ {
+ desc: 'CSS animation of transform',
+ setupAnimation: t => {
+ return addDiv(t, { style: 'animation: anim 100s 100s' })
+ .getAnimations()[0];
+ },
+ },
+ {
+ desc: 'CSS transition of opacity',
+ setupAnimation: 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: t => {
+ var div = addDiv(t, { style: 'transition: transform 100s 100s' });
+ getComputedStyle(div).transform;
+
+ div.style.transform = 'translateX(100px)';
+ return div.getAnimations()[0];
+ },
+ },
+];
+
+delayPhaseTests.forEach(test => {
+ promise_test(async t => {
+ var animation = test.setupAnimation(t);
+
+ await waitForPaints();
+
+ 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: 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: t => {
+ return addDiv(t, { style: 'animation: anim 100s 100s;' +
+ 'transform: translateX(10px)' })
+ .getAnimations()[0];
+ },
+ },
+ {
+ desc: 'CSS transition of transform with transform style',
+ setupAnimation: 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(test => {
+ promise_test(async t => {
+ var animation = test.setupAnimation(t);
+
+ await waitForPaints();
+
+ 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');
+
+ // Remove the initial transform style during delay phase.
+ animation.effect.target.style.transform = 'none';
+ await animation.ready;
+
+ 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: 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: t => {
+ return addDiv(t,
+ { style: 'animation: transform-starts-with-none 100s 100s' })
+ .getAnimations()[0];
+ },
+ },
+];
+
+startsWithNoneTests.forEach(test => {
+ promise_test(async t => {
+ var animation = test.setupAnimation(t);
+
+ await waitForPaints();
+
+ 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(async 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 });
+
+ await waitForPaints();
+
+ 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", "");
+ await waitForFrame();
+
+ 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(async 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 });
+
+ await waitForPaints();
+
+ 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;
+ await waitForFrame();
+
+ 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(async t => {
+ var animation =
+ addDivAndAnimate(t,
+ {},
+ { width: ['100px', '200px'] },
+ { duration: 100 * MS_PER_SEC, delay: 100 * MS_PER_SEC });
+
+ await waitForPaints();
+
+ 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] });
+ await waitForFrame();
+
+ 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(async 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];
+
+ await waitForPaints();
+
+ 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(async 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();
+
+ await waitForPaints();
+
+ 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');
+
+promise_test(async t => {
+ var div = addDiv(t, { style: 'display: table' });
+ var animation =
+ div.animate({ transform: ['rotate(0deg)', 'rotate(360deg)'] },
+ 100 * MS_PER_SEC);
+
+ await waitForAnimationReadyToRestyle(animation);
+
+ await waitForPaints();
+
+ assert_animation_is_running_on_compositor(animation,
+ 'Transform animation on display:table element should be running on the'
+ + ' compositor');
+}, 'Transform animation on display:table element runs on the compositor');
+
+promise_test(async t => {
+ const div = addDiv(t, { style: 'display: table' });
+ const animation = div.animate(null, 100 * MS_PER_SEC);
+ const effect = new KeyframeEffect(div,
+ { transform: ['none', 'none']},
+ 100 * MS_PER_SEC);
+
+ await waitForAnimationReadyToRestyle(animation);
+
+ animation.effect = effect;
+
+ await waitForNextFrame();
+ await waitForPaints();
+
+ assert_animation_is_running_on_compositor(
+ animation,
+ 'Transform animation on table element should be running on the compositor'
+ );
+}, 'Empty transform effect assigned after the fact to display:table content'
+ + ' runs on the compositor');
+
+promise_test(async t => {
+ const div = addDiv(t);
+ const animation = div.animate({ backgroundColor: ['blue', 'green'] },
+ 100 * MS_PER_SEC);
+
+ await waitForAnimationReadyToRestyle(animation);
+ await waitForPaints();
+
+ assert_animation_is_running_on_compositor(animation,
+ 'background-color animation should be running on the compositor');
+}, 'background-color animation runs on the compositor');
+
+promise_test(async t => {
+ const div = addDiv(t);
+ const animation = div.animate({ backgroundColor: ['blue', 'green'] },
+ 100 * MS_PER_SEC);
+
+ await waitForAnimationReadyToRestyle(animation);
+ await waitForPaints();
+
+ assert_animation_is_running_on_compositor(animation,
+ 'background-color animation should be running on the compositor');
+
+ // Add a red opaque background image covering the background color animation.
+ div.style.backgroundImage =
+ 'url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAIAAAAC64' +
+ 'paAAAAG0lEQVR42mP8z0A%2BYKJA76jmUc2jmkc1U0EzACKcASfOgGoMAAAAAElFTkSuQmCC)';
+
+ await waitForAnimationReadyToRestyle(animation);
+ await waitForPaints();
+
+ // Bug 1712246. We should optimize this case eventually.
+ //assert_animation_is_not_running_on_compositor(animation,
+ // 'Opaque background image stops background-color animations from running ' +
+ // 'on the compositor');
+}, 'Opaque background image stops background-color animations from running ' +
+ ' on the compositor');
+
+promise_test(async t => {
+ await SpecialPowers.pushPrefEnv({
+ set: [["gfx.omta.background-color", false]]
+ });
+
+ const div = addDiv(t);
+ const animation = div.animate({ backgroundColor: ['blue', 'green'] },
+ 100 * MS_PER_SEC);
+
+ await waitForAnimationReadyToRestyle(animation);
+ await waitForPaints();
+
+ assert_animation_is_not_running_on_compositor(animation,
+ 'background-color animation should NOT be running on the compositor ' +
+ 'if the pref is disabled');
+}, 'background-color animation does not run on the compositor if the pref ' +
+ 'is disabled');
+
+promise_test(async t => {
+ const div = addDiv(t);
+ const animation = div.animate({ translate: ['0px', '100px'] },
+ 100 * MS_PER_SEC);
+
+ await waitForAnimationReadyToRestyle(animation);
+ await waitForPaints();
+
+ assert_animation_is_running_on_compositor(animation,
+ 'translate animation should be running on the compositor');
+}, 'translate animation runs on the compositor');
+
+promise_test(async t => {
+ const div = addDiv(t);
+ const animation = div.animate({ rotate: ['0deg', '45deg'] },
+ 100 * MS_PER_SEC);
+
+ await waitForAnimationReadyToRestyle(animation);
+ await waitForPaints();
+
+ assert_animation_is_running_on_compositor(animation,
+ 'rotate animation should be running on the compositor');
+}, 'rotate animation runs on the compositor');
+
+promise_test(async t => {
+ const div = addDiv(t);
+ const animation = div.animate({ scale: ['1 1', '2 2'] },
+ 100 * MS_PER_SEC);
+
+ await waitForAnimationReadyToRestyle(animation);
+ await waitForPaints();
+
+ assert_animation_is_running_on_compositor(animation,
+ 'scale animation should be running on the compositor');
+}, 'scale animation runs on the compositor');
+
+promise_test(async t => {
+ const div = addDiv(t);
+ const animation = div.animate({ translate: ['0px', '100px'],
+ rotate: ['0deg', '45deg'],
+ transform: ['translate(20px)',
+ 'translate(30px)'] },
+ 100 * MS_PER_SEC);
+
+ await waitForAnimationReadyToRestyle(animation);
+ await waitForPaints();
+
+ assert_animation_is_running_on_compositor(animation,
+ 'multiple transform-like properties animation should be running on the ' +
+ 'compositor');
+
+ const properties = animation.effect.getProperties();
+ properties.forEach(property => {
+ assert_true(property.runningOnCompositor,
+ property.property + ' is running on the compositor');
+ });
+}, 'Multiple transform-like properties animation runs on the compositor');
+
+promise_test(async t => {
+ const div = addDiv(t);
+ const animation = div.animate({ offsetPath: ['none', 'none'] },
+ 100 * MS_PER_SEC);
+
+ await waitForAnimationReadyToRestyle(animation);
+ await waitForPaints();
+
+ assert_animation_is_running_on_compositor(animation,
+ 'offset-path animation should be running on the compositor even if ' +
+ 'it is always none');
+}, 'offset-path none animation runs on the compositor');
+
+promise_test(async t => {
+ const div = addDiv(t);
+ const animation = div.animate({ offsetPath: ['path("M0 0l100 100")',
+ 'path("M0 0l200 200")'] },
+ 100 * MS_PER_SEC);
+
+ await waitForAnimationReadyToRestyle(animation);
+ await waitForPaints();
+
+ assert_animation_is_running_on_compositor(animation,
+ 'offset-path:path() animation should be running on the compositor');
+}, 'offset-path:path() animation runs on the compositor');
+
+promise_test(async t => {
+ const div = addDiv(t);
+ const animation = div.animate({ offsetPath: ['ray(0deg)',
+ 'ray(180deg)'] },
+ 100 * MS_PER_SEC);
+
+ await waitForAnimationReadyToRestyle(animation);
+ await waitForPaints();
+
+ assert_animation_is_running_on_compositor(animation,
+ 'offset-path:ray() animation should be running on the compositor');
+}, 'offset-path:ray() animation runs on the compositor');
+
+promise_test(async t => {
+ const div = addDiv(t);
+ const animation = div.animate({ offsetPath: ['inset(0px)',
+ 'inset(10px)'] },
+ 100 * MS_PER_SEC);
+
+ await waitForAnimationReadyToRestyle(animation);
+ await waitForPaints();
+
+ assert_animation_is_running_on_compositor(animation,
+ 'offset-path:inset() animation should be running on the compositor');
+}, 'offset-path:inset() animation runs on the compositor');
+
+promise_test(async t => {
+ const div = addDiv(t);
+ const animation = div.animate({ offsetPath: ['circle(10px)',
+ 'circle(20px)'] },
+ 100 * MS_PER_SEC);
+
+ await waitForAnimationReadyToRestyle(animation);
+ await waitForPaints();
+
+ assert_animation_is_running_on_compositor(animation,
+ 'offset-path:circle() animation should be running on the compositor');
+}, 'offset-path:circle() animation runs on the compositor');
+
+promise_test(async t => {
+ const div = addDiv(t);
+ const animation = div.animate({ offsetPath: ['ellipse(10px 20px)',
+ 'ellipse(20px 40px)'] },
+ 100 * MS_PER_SEC);
+
+ await waitForAnimationReadyToRestyle(animation);
+ await waitForPaints();
+
+ assert_animation_is_running_on_compositor(animation,
+ 'offset-path:ellipse() animation should be running on the compositor');
+}, 'offset-path:ellipse() animation runs on the compositor');
+
+promise_test(async t => {
+ const div = addDiv(t);
+ const animation = div.animate({ offsetPath: ['polygon(0px 0px)',
+ 'polygon(50px 50px)'] },
+ 100 * MS_PER_SEC);
+
+ await waitForAnimationReadyToRestyle(animation);
+ await waitForPaints();
+
+ assert_animation_is_running_on_compositor(animation,
+ 'offset-path:polygon() animation should be running on the compositor');
+}, 'offset-path:polygon() animation runs on the compositor');
+
+promise_test(async t => {
+ const div = addDiv(t);
+ const animation = div.animate({ offsetPath: ['padding-box',
+ 'padding-box'] },
+ 100 * MS_PER_SEC);
+
+ await waitForAnimationReadyToRestyle(animation);
+ await waitForPaints();
+
+ assert_animation_is_running_on_compositor(animation,
+ 'offset-path:padding-box animation should be running on the compositor');
+}, 'offset-path:padding-box animation runs on the compositor');
+
+promise_test(async t => {
+ const div = addDiv(t);
+ const animation = div.animate({ offsetPath: ['content-box',
+ 'content-box'] },
+ 100 * MS_PER_SEC);
+
+ await waitForAnimationReadyToRestyle(animation);
+ await waitForPaints();
+
+ assert_animation_is_running_on_compositor(animation,
+ 'offset-path:content-box animation should be running on the compositor');
+}, 'offset-path:content-box animation runs on the compositor');
+
+promise_test(async t => {
+ const div = addDiv(t);
+ const animation = div.animate({ offsetPath: ['xywh(0% 0% 10px 10px)',
+ 'xywh(10% 10% 20px 20px)'] },
+ 100 * MS_PER_SEC);
+
+ await waitForAnimationReadyToRestyle(animation);
+ await waitForPaints();
+
+ assert_animation_is_running_on_compositor(animation,
+ 'offset-path:xywh() animation should be running on the compositor');
+}, 'offset-path:xywh() animation runs on the compositor');
+
+promise_test(async t => {
+ const div = addDiv(t);
+ const animation = div.animate({ offsetPath: ['rect(0% 0% 10px 10px)',
+ 'rect(10% 10% 20px 20px)'] },
+ 100 * MS_PER_SEC);
+
+ await waitForAnimationReadyToRestyle(animation);
+ await waitForPaints();
+
+ assert_animation_is_running_on_compositor(animation,
+ 'offset-path:rect() animation should be running on the compositor');
+}, 'offset-path:rect() animation runs on the compositor');
+
+promise_test(async t => {
+ const div = addDiv(t);
+ const animation = div.animate({ offsetDistance: ['0%', '100%'] },
+ 100 * MS_PER_SEC);
+
+ await waitForAnimationReadyToRestyle(animation);
+ await waitForPaints();
+
+ assert_animation_is_not_running_on_compositor(animation,
+ 'offset-distance animation is not running on the compositor because ' +
+ 'offset-path is none');
+
+ const newAnim = div.animate({ offsetPath: ['None', 'None'] },
+ 100 * MS_PER_SEC);
+ await waitForAnimationReadyToRestyle(newAnim);
+ await waitForPaints();
+
+ assert_animation_is_running_on_compositor(animation,
+ 'offset-distance animation should be running on the compositor');
+ assert_animation_is_running_on_compositor(newAnim,
+ 'new added offset-path animation should be running on the compositor');
+}, 'offset-distance animation runs on the compositor');
+
+promise_test(async t => {
+ const div = addDiv(t);
+ const animation = div.animate({ offsetRotate: ['0deg', '45deg'] },
+ 100 * MS_PER_SEC);
+
+ await waitForAnimationReadyToRestyle(animation);
+ await waitForPaints();
+
+ assert_animation_is_not_running_on_compositor(animation,
+ 'offset-rotate animation is not running on the compositor because ' +
+ 'offset-path is none');
+
+ const newAnim = div.animate({ offsetPath: ['None', 'None'] },
+ 100 * MS_PER_SEC);
+ await waitForAnimationReadyToRestyle(newAnim);
+ await waitForPaints();
+
+ assert_animation_is_running_on_compositor(animation,
+ 'offset-rotate animation should be running on the compositor');
+ assert_animation_is_running_on_compositor(newAnim,
+ 'new added offset-path animation should be running on the compositor');
+}, 'offset-rotate animation runs on the compositor');
+
+promise_test(async t => {
+ const div = addDiv(t);
+ const animation = div.animate({ offsetAnchor: ['0% 0%', '100% 100%'] },
+ 100 * MS_PER_SEC);
+
+ await waitForAnimationReadyToRestyle(animation);
+ await waitForPaints();
+
+ assert_animation_is_not_running_on_compositor(animation,
+ 'offset-anchor animation is not running on the compositor because ' +
+ 'offset-path is none');
+
+ const newAnim = div.animate({ offsetPath: ['None', 'None'] },
+ 100 * MS_PER_SEC);
+ await waitForAnimationReadyToRestyle(newAnim);
+ await waitForPaints();
+
+ assert_animation_is_running_on_compositor(animation,
+ 'offset-anchor animation should be running on the compositor');
+ assert_animation_is_running_on_compositor(newAnim,
+ 'new added offset-path animation should be running on the compositor');
+}, 'offset-anchor animation runs on the compositor');
+
+promise_test(async t => {
+ const div = addDiv(t);
+ const animation = div.animate({ offsetPosition: ['0% 0%', '100% 100%'] },
+ 100 * MS_PER_SEC);
+
+ await waitForAnimationReadyToRestyle(animation);
+ await waitForPaints();
+
+ assert_animation_is_not_running_on_compositor(animation,
+ 'offset-position animation is not running on the compositor because ' +
+ 'offset-path is none');
+
+ const newAnim = div.animate({ offsetPath: ['None', 'None'] },
+ 100 * MS_PER_SEC);
+ await waitForAnimationReadyToRestyle(newAnim);
+ await waitForPaints();
+
+ assert_animation_is_running_on_compositor(animation,
+ 'offset-position animation should be running on the compositor');
+ assert_animation_is_running_on_compositor(newAnim,
+ 'new added offset-path animation should be running on the compositor');
+}, 'offset-position animation runs on the compositor');
+
+promise_test(async t => {
+ const div = addDiv(t);
+ const animation = div.animate({ translate: ['0px', '100px'],
+ rotate: ['0deg', '45deg'],
+ transform: ['translate(0px)',
+ 'translate(100px)'],
+ offsetDistance: ['0%', '100%'] },
+ 100 * MS_PER_SEC);
+ await waitForAnimationReadyToRestyle(animation);
+ await waitForPaints();
+
+ assert_animation_is_running_on_compositor(animation,
+ 'Animation is running on the compositor even though we do not have ' +
+ 'offset-path');
+
+ div.style.offsetPath = 'path("M50 0v100")';
+ getComputedStyle(div).offsetPath;
+
+ await waitForPaints();
+
+ assert_animation_is_running_on_compositor(animation,
+ 'Animation is running on the compositor');
+
+}, 'Multiple transform-like properties (include motion-path) animation runs ' +
+ 'on the compositor');
+
+promise_test(async t => {
+ const div = addDiv(t);
+ const animation = div.animate({ translate: ['0px', '100px'],
+ rotate: ['0deg', '45deg'],
+ transform: ['translate(20px)',
+ 'translate(30px)'],
+ offsetDistance: ['0%', '100%'] },
+ 100 * MS_PER_SEC);
+
+ div.style.setProperty('translate', '50px', 'important');
+ getComputedStyle(div).translate;
+
+ await waitForAnimationReadyToRestyle(animation);
+ await waitForPaints();
+
+ assert_animation_is_not_running_on_compositor(animation,
+ 'Animation overridden by an !important rule reports that it is ' +
+ 'NOT running on the compositor');
+
+ const properties = animation.effect.getProperties();
+ properties.forEach(property => {
+ assert_true(!property.runningOnCompositor,
+ property.property + ' is not running on the compositor');
+ });
+}, 'Multiple transform-like properties animation does not runs on the ' +
+ 'compositor because one of the transform-like property is overridden ' +
+ 'by an !important rule');
+
+// FIXME: Bug 1593106: We should still run the animations on the compositor if
+// offset-* doesn't have any effect.
+promise_test(async t => {
+ const div = addDiv(t);
+ const animation = div.animate({ translate: ['0px', '100px'],
+ rotate: ['0deg', '45deg'],
+ transform: ['translate(0px)',
+ 'translate(100px)'],
+ offsetDistance: ['0%', '100%'] },
+ 100 * MS_PER_SEC);
+
+ div.style.setProperty('offset-distance', '50%', 'important');
+ getComputedStyle(div).offsetDistance;
+
+ await waitForAnimationReadyToRestyle(animation);
+ await waitForPaints();
+
+ assert_animation_is_not_running_on_compositor(animation,
+ 'Animation overridden by an !important rule reports that it is ' +
+ 'NOT running on the compositor');
+
+ const properties = animation.effect.getProperties();
+ properties.forEach(property => {
+ assert_true(!property.runningOnCompositor,
+ property.property + ' is not running on the compositor');
+ });
+}, 'Multiple transform-like properties animation does not runs on the ' +
+ 'compositor because one of the offset-* property is overridden ' +
+ 'by an !important rule');
+
+promise_test(async t => {
+ const div = addDiv(t);
+ const animation = div.animate({ rotate: ['0deg', '45deg'],
+ transform: ['translate(20px)',
+ 'translate(30px)'],
+ offsetDistance: ['0%', '100%'] },
+ 100 * MS_PER_SEC);
+
+ div.style.setProperty('translate', '50px', 'important');
+ getComputedStyle(div).translate;
+
+ await waitForAnimationReadyToRestyle(animation);
+ await waitForPaints();
+
+ assert_animation_is_running_on_compositor(animation,
+ 'Animation is still running on the compositor');
+
+ const properties = animation.effect.getProperties();
+ properties.forEach(property => {
+ assert_true(property.runningOnCompositor,
+ property.property + ' is running on the compositor');
+ });
+}, 'Multiple transform-like properties animation still runs on the ' +
+ 'compositor because the overridden-by-!important property does not have ' +
+ 'animation');
+
+promise_test(async t => {
+ // We should run the animations on the compositor for this case:
+ // 1. A transition of 'translate'
+ // 2. An !important rule on 'translate'
+ // 3. An animation of 'scale'
+ const div = addDiv(t, { style: 'translate: 100px !important;' });
+ const animation = div.animate({ rotate: ['0deg', '45deg'] },
+ 100 * MS_PER_SEC);
+ div.style.transition = 'translate 100s';
+ getComputedStyle(div).transition;
+
+ await waitForAnimationReadyToRestyle(animation);
+ await waitForPaints();
+
+ assert_animation_is_running_on_compositor(animation,
+ 'rotate animation should be running on the compositor');
+
+ div.style.setProperty('translate', '200px', 'important');
+ getComputedStyle(div).translate;
+
+ const anims = div.getAnimations();
+ await waitForPaints();
+
+ assert_animation_is_running_on_compositor(anims[0],
+ `${anims[0].effect.getProperties()[0].property} animation should be ` +
+ `running on the compositor`);
+ assert_animation_is_running_on_compositor(anims[1],
+ `${anims[1].effect.getProperties()[0].property} animation should be ` +
+ `running on the compositor`);
+}, 'Transform-like animations and transitions still runs on the compositor ' +
+ 'because the !important rule is overridden by a transition, and the ' +
+ 'transition property does not have animations');
+
+promise_test(async t => {
+ const container = addDiv(t, { style: 'transform-style: preserve-3d;' });
+ const targetA = addDiv(t, { style: 'transform-style: preserve-3d' });
+ const targetB = addDiv(t, { style: 'transform-style: preserve-3d' });
+ const targetC = addDiv(t);
+ container.appendChild(targetA);
+ targetA.append(targetB);
+ targetB.append(targetC);
+
+ const animation1 = targetA.animate({ rotate: ['0 0 1 0deg', '1 1 1 45deg'] },
+ 100 * MS_PER_SEC);
+
+ const animation2 = targetC.animate({ rotate: ['0 0 1 0deg', '0 1 1 100deg'] },
+ 100 * MS_PER_SEC);
+
+ await waitForAnimationReadyToRestyle(animation1);
+ await waitForAnimationReadyToRestyle(animation2);
+ await waitForPaints();
+
+ assert_animation_is_running_on_compositor(animation1,
+ 'rotate animation in the 3d rendering context should be running on the ' +
+ 'compositor');
+ assert_animation_is_running_on_compositor(animation2,
+ 'rotate animation in the 3d rendering context should be running on the ' +
+ 'compositor');
+}, 'Transform-like animations in the 3d rendering context should runs on the ' +
+ 'compositor');
+
+promise_test(async t => {
+ const container = addDiv(t, { style: 'transform-style: preserve-3d;' });
+ const target = addDiv(t, { style: 'transform-style: preserve-3d;' });
+ const innerA = addDiv(t, { style: 'width: 50px; height: 50px;' });
+ // The frame of innerB is too large, so this makes its ancenstors and children
+ // in the 3d context be not allowed the async animations.
+ const innerB = addDiv(t, { style: 'rotate: 0 1 1 100deg; ' +
+ 'transform-style: preserve-3d; ' +
+ 'text-indent: -9999em' });
+ const innerB2 = addDiv(t, { style: 'rotate: 0 1 1 45deg;' });
+ const innerBText = document.createTextNode("innerB");
+ container.appendChild(target);
+ target.appendChild(innerA);
+ target.appendChild(innerB);
+ innerB.appendChild(innerBText);
+ innerB.appendChild(innerB2);
+
+ const animation1 = target.animate({ rotate: ['0 0 1 0deg', '1 1 1 45deg'] },
+ 100 * MS_PER_SEC);
+
+ const animation2 = innerA.animate({ rotate: ['0 0 1 0deg', '0 1 1 100deg'] },
+ 100 * MS_PER_SEC);
+
+ const animation3 = innerB2.animate({ rotate: ['0 0 1 0deg', '0 1 1 90deg'] },
+ 100 * MS_PER_SEC);
+
+ await waitForAnimationReadyToRestyle(animation1);
+ await waitForAnimationReadyToRestyle(animation2);
+ await waitForAnimationReadyToRestyle(animation3);
+ await waitForPaints();
+
+ const isPartialPrerenderEnabled =
+ SpecialPowers.getBoolPref('layout.animation.prerender.partial');
+
+ if (isPartialPrerenderEnabled) {
+ assert_animation_is_running_on_compositor(animation1,
+ 'rotate animation in the 3d rendering context should be running on ' +
+ 'the compositor even if one of its inner frames is too large');
+ assert_animation_is_running_on_compositor(animation2,
+ 'rotate animation in the 3d rendering context is still running on ' +
+ 'the compositor because its display item is created earlier');
+ assert_animation_is_running_on_compositor(animation3,
+ 'rotate animation in the 3d rendering context should be running on ' +
+ 'the compositor even if one of its parent frames is too large');
+ } else {
+ assert_animation_is_not_running_on_compositor(animation1,
+ 'rotate animation in the 3d rendering context should not be running on ' +
+ 'the compositor because one of its inner frames is too large');
+ assert_animation_is_running_on_compositor(animation2,
+ 'rotate animation in the 3d rendering context is still running on ' +
+ 'the compositor because its display item is created earlier');
+ assert_animation_is_not_running_on_compositor(animation3,
+ 'rotate animation in the 3d rendering context should not be running on ' +
+ 'the compositor because one of its parent frames is too large');
+ }
+ innerBText.remove();
+}, 'Transform-like animations in the 3d rendering context should run on ' +
+ 'the compositor even if it is the ancestor of ones whose frames are too ' +
+ 'large or its ancestor is not allowed to run on the compositor');
+
+promise_test(async t => {
+ const container = addDiv(t, { style: 'transform-style: preserve-3d;' });
+ const target = addDiv(t, { style: 'transform-style: preserve-3d;' });
+ // The frame of innerA is too large, so this makes its ancenstors and children
+ // in the 3d context be not allowed the async animations.
+ const innerA = addDiv(t, { style: 'transform-style: preserve-3d; ' +
+ 'text-indent: -9999em' });
+ const innerAText = document.createTextNode("innerA");
+ const innerB = addDiv(t, { style: 'width: 50px; height: 50px;' });
+ container.appendChild(target);
+ target.appendChild(innerA);
+ target.appendChild(innerB);
+ innerA.appendChild(innerAText);
+
+ const animation1 = target.animate({ rotate: ['0 0 1 0deg', '1 1 1 45deg'] },
+ 100 * MS_PER_SEC);
+
+ const animation2 = innerA.animate({ rotate: ['0 0 1 0deg', '0 1 1 100deg'] },
+ 100 * MS_PER_SEC);
+
+ const animation3 = innerB.animate({ rotate: ['0 0 1 0deg', '0 1 1 90deg'] },
+ 100 * MS_PER_SEC);
+
+ await waitForAnimationReadyToRestyle(animation1);
+ await waitForAnimationReadyToRestyle(animation2);
+ await waitForAnimationReadyToRestyle(animation3);
+ await waitForPaints();
+
+ const isPartialPrerenderEnabled =
+ SpecialPowers.getBoolPref('layout.animation.prerender.partial');
+
+ if (isPartialPrerenderEnabled) {
+ assert_animation_is_running_on_compositor(animation1,
+ 'rotate animation in the 3d rendering context should be running on ' +
+ 'the compositor even if one of its inner frames is too large');
+ assert_animation_is_running_on_compositor(animation2,
+ 'rotate animation in the 3d rendering context should be running on ' +
+ 'the compositor even if its frame size is too large');
+ assert_animation_is_running_on_compositor(animation3,
+ 'rotate animation in the 3d rendering context should be running on ' +
+ 'the compositor even if its previous sibling frame is too large');
+ } else {
+ assert_animation_is_not_running_on_compositor(animation1,
+ 'rotate animation in the 3d rendering context should not be running on ' +
+ 'the compositor because one of its inner frames is too large');
+ assert_animation_is_not_running_on_compositor(animation2,
+ 'rotate animation in the 3d rendering context should not be running on ' +
+ 'the compositor because its frame size is too large');
+ assert_animation_is_not_running_on_compositor(animation3,
+ 'rotate animation in the 3d rendering context should not be running on ' +
+ 'the compositor because its previous sibling frame is too large');
+ }
+ innerAText.remove();
+}, 'Transform-like animations in the 3d rendering context should run on ' +
+ 'the compositor even if its previous sibling frame size is too large');
+
+</script>
+</body>