summaryrefslogtreecommitdiffstats
path: root/dom/animation/test/chrome/test_animation_observers.html
diff options
context:
space:
mode:
Diffstat (limited to 'dom/animation/test/chrome/test_animation_observers.html')
-rw-r--r--dom/animation/test/chrome/test_animation_observers.html1177
1 files changed, 1177 insertions, 0 deletions
diff --git a/dom/animation/test/chrome/test_animation_observers.html b/dom/animation/test/chrome/test_animation_observers.html
new file mode 100644
index 000000000..237128e04
--- /dev/null
+++ b/dom/animation/test/chrome/test_animation_observers.html
@@ -0,0 +1,1177 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title>Test chrome-only MutationObserver animation notifications</title>
+<script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+<script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
+<script src="../testcommon.js"></script>
+<link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+<style>
+@keyframes anim {
+ to { transform: translate(100px); }
+}
+@keyframes anotherAnim {
+ to { transform: translate(0px); }
+}
+#target {
+ width: 100px;
+ height: 100px;
+ background-color: yellow;
+ line-height: 16px;
+}
+</style>
+<div id=container><div id=target></div></div>
+<script>
+var div = document.getElementById("target");
+var gRecords = [];
+var gObserver = new MutationObserver(function(newRecords) {
+ gRecords.push(...newRecords);
+});
+
+// Asynchronous testing framework based on layout/style/test/animation_utils.js.
+
+var gTests = [];
+var gCurrentTestName;
+
+function addAsyncAnimTest(aName, aOptions, aTestGenerator) {
+ aTestGenerator.testName = aName;
+ aTestGenerator.options = aOptions || {};
+ gTests.push(aTestGenerator);
+}
+
+function runAsyncTest(aTestGenerator) {
+ return waitForFrame().then(function() {
+ var generator;
+
+ function step(arg) {
+ var next;
+ try {
+ next = generator.next(arg);
+ } catch (e) {
+ return Promise.reject(e);
+ }
+ if (next.done) {
+ return Promise.resolve(next.value);
+ } else {
+ return Promise.resolve(next.value).then(step);
+ }
+ }
+
+ var subtree = aTestGenerator.options.subtree;
+
+ gCurrentTestName = aTestGenerator.testName;
+ if (subtree) {
+ gCurrentTestName += ":subtree";
+ }
+
+ gRecords = [];
+ gObserver.disconnect();
+ gObserver.observe(aTestGenerator.options.observe,
+ { animations: true, subtree: subtree});
+
+ generator = aTestGenerator();
+ return step();
+ });
+};
+
+function runAllAsyncTests() {
+ return gTests.reduce(function(sequence, test) {
+ return sequence.then(() => runAsyncTest(test));
+ }, Promise.resolve());
+}
+
+// Wrap is and ok with versions that prepend the current sub-test name
+// to the assertion description.
+var old_is = is, old_ok = ok;
+is = function(a, b, message) {
+ if (gCurrentTestName && message) {
+ message = `[${gCurrentTestName}] ${message}`;
+ }
+ old_is(a, b, message);
+}
+ok = function(a, message) {
+ if (gCurrentTestName && message) {
+ message = `[${gCurrentTestName}] ${message}`;
+ }
+ old_ok(a, message);
+}
+
+// Adds an event listener and returns a Promise that is resolved when the
+// event listener is called.
+function await_event(aElement, aEventName) {
+ return new Promise(function(aResolve) {
+ function listener(aEvent) {
+ aElement.removeEventListener(aEventName, listener);
+ aResolve();
+ }
+ aElement.addEventListener(aEventName, listener, false);
+ });
+}
+
+function assert_record_list(actual, expected, desc, index, listName) {
+ is(actual.length, expected.length, `${desc} - record[${index}].${listName} length`);
+ if (actual.length != expected.length) {
+ return;
+ }
+ for (var i = 0; i < actual.length; i++) {
+ ok(actual.indexOf(expected[i]) != -1,
+ `${desc} - record[${index}].${listName} contains expected Animation`);
+ }
+}
+
+function assert_records(expected, desc) {
+ var records = gRecords;
+ gRecords = [];
+ is(records.length, expected.length, `${desc} - number of records`);
+ if (records.length != expected.length) {
+ return;
+ }
+ for (var i = 0; i < records.length; i++) {
+ assert_record_list(records[i].addedAnimations, expected[i].added, desc, i, "addedAnimations");
+ assert_record_list(records[i].changedAnimations, expected[i].changed, desc, i, "changedAnimations");
+ assert_record_list(records[i].removedAnimations, expected[i].removed, desc, i, "removedAnimations");
+ }
+}
+
+// -- Tests ------------------------------------------------------------------
+
+// We run all tests first targeting the div and observing the div, then again
+// targeting the div and observing its parent while using the subtree:true
+// MutationObserver option.
+
+[
+ { observe: div, target: div, subtree: false },
+ { observe: div.parentNode, target: div, subtree: true },
+].forEach(function(aOptions) {
+
+ var e = aOptions.target;
+
+ // Test that starting a single transition that completes normally
+ // dispatches an added notification and then a removed notification.
+ addAsyncAnimTest("single_transition", aOptions, function*() {
+ // Start a transition.
+ e.style = "transition: background-color 100s; background-color: lime;";
+
+ // Register for the end of the transition.
+ var transitionEnd = await_event(e, "transitionend");
+
+ // The transition should cause the creation of a single Animation.
+ var animations = e.getAnimations();
+ is(animations.length, 1, "getAnimations().length after transition start");
+
+ // Wait for the single MutationRecord for the Animation addition to
+ // be delivered.
+ yield waitForFrame();
+ assert_records([{ added: animations, changed: [], removed: [] }],
+ "records after transition start");
+
+ // Advance until near the end of the transition, then wait for it to finish.
+ animations[0].currentTime = 99900;
+ yield transitionEnd;
+
+ // After the transition has finished, the Animation should disappear.
+ is(e.getAnimations().length, 0,
+ "getAnimations().length after transition end");
+
+ // Wait for the change MutationRecord for seeking the Animation to be
+ // delivered, followed by the the removal MutationRecord.
+ yield waitForFrame();
+ assert_records([{ added: [], changed: animations, removed: [] },
+ { added: [], changed: [], removed: animations }],
+ "records after transition end");
+
+ e.style = "";
+ });
+
+ // Test that starting a single transition that is cancelled by resetting
+ // the transition-property property dispatches an added notification and
+ // then a removed notification.
+ addAsyncAnimTest("single_transition_cancelled_property", aOptions, function*() {
+ // Start a long transition.
+ e.style = "transition: background-color 100s; background-color: lime;";
+
+ // The transition should cause the creation of a single Animation.
+ var animations = e.getAnimations();
+ is(animations.length, 1, "getAnimations().length after transition start");
+
+ // Wait for the single MutationRecord for the Animation addition to
+ // be delivered.
+ yield waitForFrame();
+ assert_records([{ added: animations, changed: [], removed: [] }],
+ "records after transition start");
+
+ // Cancel the transition by setting transition-property.
+ e.style.transitionProperty = "none";
+
+ // Wait for the single MutationRecord for the Animation removal to
+ // be delivered.
+ yield waitForFrame();
+ assert_records([{ added: [], changed: [], removed: animations }],
+ "records after transition end");
+
+ e.style = "";
+ });
+
+ // Test that starting a single transition that is cancelled by setting
+ // style to the currently animated value dispatches an added
+ // notification and then a removed notification.
+ addAsyncAnimTest("single_transition_cancelled_value", aOptions, function*() {
+ // Start a long transition with a predictable value.
+ e.style = "transition: background-color 100s steps(2, end) -51s; background-color: lime;";
+
+ // The transition should cause the creation of a single Animation.
+ var animations = e.getAnimations();
+ is(animations.length, 1, "getAnimations().length after transition start");
+
+ // Wait for the single MutationRecord for the Animation addition to
+ // be delivered.
+ yield waitForFrame();
+ assert_records([{ added: animations, changed: [], removed: [] }],
+ "records after transition start");
+
+ // Cancel the transition by setting the current animation value.
+ var value = "rgb(128, 255, 0)";
+ is(getComputedStyle(e).backgroundColor, value, "half-way transition value");
+ e.style.backgroundColor = value;
+
+ // Wait for the single MutationRecord for the Animation removal to
+ // be delivered.
+ yield waitForFrame();
+ assert_records([{ added: [], changed: [], removed: animations }],
+ "records after transition end");
+
+ e.style = "";
+ });
+
+ // Test that starting a single transition that is cancelled by setting
+ // style to a non-interpolable value dispatches an added notification
+ // and then a removed notification.
+ addAsyncAnimTest("single_transition_cancelled_noninterpolable", aOptions, function*() {
+ // Start a long transition.
+ e.style = "transition: line-height 100s; line-height: 100px;";
+
+ // The transition should cause the creation of a single Animation.
+ var animations = e.getAnimations();
+ is(animations.length, 1, "getAnimations().length after transition start");
+
+ // Wait for the single MutationRecord for the Animation addition to
+ // be delivered.
+ yield waitForFrame();
+ assert_records([{ added: animations, changed: [], removed: [] }],
+ "records after transition start");
+
+ // Cancel the transition by setting line-height to a non-interpolable value.
+ e.style.lineHeight = "normal";
+
+ // Wait for the single MutationRecord for the Animation removal to
+ // be delivered.
+ yield waitForFrame();
+ assert_records([{ added: [], changed: [], removed: animations }],
+ "records after transition end");
+
+ e.style = "";
+ });
+
+ // Test that starting a single transition and then reversing it
+ // dispatches an added notification, then a simultaneous removed and
+ // added notification, then a removed notification once finished.
+ addAsyncAnimTest("single_transition_reversed", aOptions, function*() {
+ // Start a long transition.
+ e.style = "transition: background-color 100s step-start; background-color: lime;";
+
+ // The transition should cause the creation of a single Animation.
+ var animations = e.getAnimations();
+ is(animations.length, 1, "getAnimations().length after transition start");
+
+ var firstAnimation = animations[0];
+
+ // Wait for the single MutationRecord for the Animation addition to
+ // be delivered.
+ yield waitForFrame();
+ assert_records([{ added: [firstAnimation], changed: [], removed: [] }],
+ "records after transition start");
+
+ // Wait for the Animation to be playing, then seek well into
+ // the transition.
+ yield firstAnimation.ready;
+ firstAnimation.currentTime = 50 * MS_PER_SEC;
+
+ // Reverse the transition by setting the background-color back to its
+ // original value.
+ e.style.backgroundColor = "yellow";
+
+ // The reversal should cause the creation of a new Animation.
+ animations = e.getAnimations();
+ is(animations.length, 1, "getAnimations().length after transition reversal");
+
+ var secondAnimation = animations[0];
+
+ ok(firstAnimation != secondAnimation,
+ "second Animation should be different from the first");
+
+ // Wait for the change Mutation record from seeking the first animation
+ // to be delivered, followed by a subsequent MutationRecord for the removal
+ // of the original Animation and the addition of the new Animation.
+ yield waitForFrame();
+ assert_records([{ added: [], changed: [firstAnimation], removed: [] },
+ { added: [secondAnimation], changed: [],
+ removed: [firstAnimation] }],
+ "records after transition reversal");
+
+ // Cancel the transition.
+ e.style.transitionProperty = "none";
+
+ // Wait for the single MutationRecord for the Animation removal to
+ // be delivered.
+ yield waitForFrame();
+ assert_records([{ added: [], changed: [], removed: [secondAnimation] }],
+ "records after transition end");
+
+ e.style = "";
+ });
+
+ // Test that multiple transitions starting and ending on an element
+ // at the same time get batched up into a single MutationRecord.
+ addAsyncAnimTest("multiple_transitions", aOptions, function*() {
+ // Start three long transitions.
+ e.style = "transition-duration: 100s; " +
+ "transition-property: color, background-color, line-height; " +
+ "color: blue; background-color: lime; line-height: 24px;";
+
+ // The transitions should cause the creation of three Animations.
+ var animations = e.getAnimations();
+ is(animations.length, 3, "getAnimations().length after transition starts");
+
+ // Wait for the single MutationRecord for the Animation additions to
+ // be delivered.
+ yield waitForFrame();
+ assert_records([{ added: animations, changed: [], removed: [] }],
+ "records after transition starts");
+
+ // Wait for the Animations to get going.
+ yield animations[0].ready;
+ is(animations.filter(p => p.playState == "running").length, 3,
+ "number of running Animations");
+
+ // Seek well into each animation.
+ animations.forEach(p => p.currentTime = 50 * MS_PER_SEC);
+
+ // Prepare the set of expected change MutationRecords, one for each
+ // animation that was seeked.
+ var seekRecords = animations.map(
+ p => ({ added: [], changed: [p], removed: [] })
+ );
+
+ // Cancel one of the transitions by setting transition-property.
+ e.style.transitionProperty = "background-color, line-height";
+
+ var colorAnimation = animations.filter(p => p.playState != "running");
+ var otherAnimations = animations.filter(p => p.playState == "running");
+
+ is(colorAnimation.length, 1,
+ "number of non-running Animations after cancelling one");
+ is(otherAnimations.length, 2,
+ "number of running Animations after cancelling one");
+
+ // Wait for the MutationRecords to be delivered: one for each animation
+ // that was seeked, followed by one for the removal of the color animation.
+ yield waitForFrame();
+ assert_records(seekRecords.concat(
+ { added: [], changed: [], removed: colorAnimation }),
+ "records after color transition end");
+
+ // Cancel the remaining transitions.
+ e.style.transitionProperty = "none";
+
+ // Wait for the MutationRecord for the other two Animation
+ // removals to be delivered.
+ yield waitForFrame();
+ assert_records([{ added: [], changed: [], removed: otherAnimations }],
+ "records after other transition ends");
+
+ e.style = "";
+ });
+
+ // Test that starting a single animation that completes normally
+ // dispatches an added notification and then a removed notification.
+ addAsyncAnimTest("single_animation", aOptions, function*() {
+ // Start an animation.
+ e.style = "animation: anim 100s;";
+
+ // Register for the end of the animation.
+ var animationEnd = await_event(e, "animationend");
+
+ // The animation should cause the creation of a single Animation.
+ var animations = e.getAnimations();
+ is(animations.length, 1, "getAnimations().length after animation start");
+
+ // Wait for the single MutationRecord for the Animation addition to
+ // be delivered.
+ yield waitForFrame();
+ assert_records([{ added: animations, changed: [], removed: [] }],
+ "records after animation start");
+
+ // Advance until near the end of the animation, then wait for it to finish.
+ animations[0].currentTime = 99900;
+ yield animationEnd;
+
+ // After the animation has finished, the Animation should disappear.
+ is(e.getAnimations().length, 0,
+ "getAnimations().length after animation end");
+
+ // Wait for the change MutationRecord from seeking the Animation to
+ // be delivered, followed by a further MutationRecord for the Animation
+ // removal.
+ yield waitForFrame();
+ assert_records([{ added: [], changed: animations, removed: [] },
+ { added: [], changed: [], removed: animations }],
+ "records after animation end");
+
+ e.style = "";
+ });
+
+ // Test that starting a single animation that is cancelled by resetting
+ // the animation-name property dispatches an added notification and
+ // then a removed notification.
+ addAsyncAnimTest("single_animation_cancelled_name", aOptions, function*() {
+ // Start a long animation.
+ e.style = "animation: anim 100s;";
+
+ // The animation should cause the creation of a single Animation.
+ var animations = e.getAnimations();
+ is(animations.length, 1, "getAnimations().length after animation start");
+
+ // Wait for the single MutationRecord for the Animation addition to
+ // be delivered.
+ yield waitForFrame();
+ assert_records([{ added: animations, changed: [], removed: [] }],
+ "records after animation start");
+
+ // Cancel the animation by setting animation-name.
+ e.style.animationName = "none";
+
+ // Wait for the single MutationRecord for the Animation removal to
+ // be delivered.
+ yield waitForFrame();
+ assert_records([{ added: [], changed: [], removed: animations }],
+ "records after animation end");
+
+ e.style = "";
+ });
+
+ // Test that starting a single animation that is cancelled by updating
+ // the animation-duration property dispatches an added notification and
+ // then a removed notification.
+ addAsyncAnimTest("single_animation_cancelled_duration", aOptions, function*() {
+ // Start a long animation.
+ e.style = "animation: anim 100s;";
+
+ // The animation should cause the creation of a single Animation.
+ var animations = e.getAnimations();
+ is(animations.length, 1, "getAnimations().length after animation start");
+
+ // Wait for the single MutationRecord for the Animation addition to
+ // be delivered.
+ yield waitForFrame();
+ assert_records([{ added: animations, changed: [], removed: [] }],
+ "records after animation start");
+
+ // Advance the animation by a second.
+ animations[0].currentTime += 1 * MS_PER_SEC;
+
+ // Cancel the animation by setting animation-duration to a value less
+ // than a second.
+ e.style.animationDuration = "0.1s";
+
+ // Wait for the change MutationRecord from seeking the Animation to
+ // be delivered, followed by a further MutationRecord for the Animation
+ // removal.
+ yield waitForFrame();
+ assert_records([{ added: [], changed: animations, removed: [] },
+ { added: [], changed: [], removed: animations }],
+ "records after animation end");
+
+ e.style = "";
+ });
+
+ // Test that starting a single animation that is cancelled by updating
+ // the animation-delay property dispatches an added notification and
+ // then a removed notification.
+ addAsyncAnimTest("single_animation_cancelled_delay", aOptions, function*() {
+ // Start a long animation.
+ e.style = "animation: anim 100s;";
+
+ // The animation should cause the creation of a single Animation.
+ var animations = e.getAnimations();
+ is(animations.length, 1, "getAnimations().length after animation start");
+
+ // Wait for the single MutationRecord for the Animation addition to
+ // be delivered.
+ yield waitForFrame();
+ assert_records([{ added: animations, changed: [], removed: [] }],
+ "records after animation start");
+
+ // Cancel the animation by setting animation-delay.
+ e.style.animationDelay = "-200s";
+
+ // Wait for the single MutationRecord for the Animation removal to
+ // be delivered.
+ yield waitForFrame();
+ assert_records([{ added: [], changed: [], removed: animations }],
+ "records after animation end");
+
+ e.style = "";
+ });
+
+ // Test that starting a single animation that is cancelled by updating
+ // the animation-fill-mode property dispatches an added notification and
+ // then a removed notification.
+ addAsyncAnimTest("single_animation_cancelled_fill", aOptions, function*() {
+ // Start a short, filled animation.
+ e.style = "animation: anim 100s forwards;";
+
+ // Register for the end of the animation.
+ var animationEnd = await_event(e, "animationend");
+
+ // The animation should cause the creation of a single Animation.
+ var animations = e.getAnimations();
+ is(animations.length, 1, "getAnimations().length after animation start");
+
+ // Wait for the single MutationRecord for the Animation addition to
+ // be delivered.
+ yield waitForFrame();
+ assert_records([{ added: animations, changed: [], removed: [] }],
+ "records after animation start");
+
+ // Advance until near the end of the animation, then wait for it to finish.
+ animations[0].currentTime = 99900;
+ yield animationEnd;
+
+ // The only MutationRecord at this point should be the change from
+ // seeking the Animation.
+ assert_records([{ added: [], changed: animations, removed: [] }],
+ "records after animation starts filling");
+
+ // Cancel the animation by setting animation-fill-mode.
+ e.style.animationFillMode = "none";
+
+ // Wait for the single MutationRecord for the Animation removal to
+ // be delivered.
+ yield waitForFrame();
+ assert_records([{ added: [], changed: [], removed: animations }],
+ "records after animation end");
+
+ e.style = "";
+ });
+
+ // Test that starting a single animation that is cancelled by updating
+ // the animation-iteration-count property dispatches an added notification
+ // and then a removed notification.
+ addAsyncAnimTest("single_animation_cancelled_iteration_count",
+ aOptions, function*() {
+ // Start a short, repeated animation.
+ e.style = "animation: anim 0.5s infinite;";
+
+ // The animation should cause the creation of a single Animation.
+ var animations = e.getAnimations();
+ is(animations.length, 1, "getAnimations().length after animation start");
+
+ // Wait for the single MutationRecord for the Animation addition to
+ // be delivered.
+ yield waitForFrame();
+ assert_records([{ added: animations, changed: [], removed: [] }],
+ "records after animation start");
+
+ // Advance the animation until we are past the first iteration.
+ animations[0].currentTime += 1 * MS_PER_SEC;
+
+ // The only MutationRecord at this point should be the change from
+ // seeking the Animation.
+ yield waitForFrame();
+ assert_records([{ added: [], changed: animations, removed: [] }],
+ "records after seeking animations");
+
+ // Cancel the animation by setting animation-iteration-count.
+ e.style.animationIterationCount = "1";
+
+ // Wait for the single MutationRecord for the Animation removal to
+ // be delivered.
+ yield waitForFrame();
+ assert_records([{ added: [], changed: [], removed: animations }],
+ "records after animation end");
+
+ e.style = "";
+ });
+
+ // Test that updating an animation property dispatches a changed notification.
+ [
+ { name: "duration", prop: "animationDuration", val: "200s" },
+ { name: "timing", prop: "animationTimingFunction", val: "linear" },
+ { name: "iteration", prop: "animationIterationCount", val: "2" },
+ { name: "direction", prop: "animationDirection", val: "reverse" },
+ { name: "state", prop: "animationPlayState", val: "paused" },
+ { name: "delay", prop: "animationDelay", val: "-1s" },
+ { name: "fill", prop: "animationFillMode", val: "both" },
+ ].forEach(function(aChangeTest) {
+ addAsyncAnimTest(`single_animation_change_${aChangeTest.name}`, aOptions, function*() {
+ // Start a long animation.
+ e.style = "animation: anim 100s;";
+
+ // The animation should cause the creation of a single Animation.
+ var animations = e.getAnimations();
+ is(animations.length, 1, "getAnimations().length after animation start");
+
+ // Wait for the single MutationRecord for the Animation addition to
+ // be delivered.
+ yield waitForFrame();
+ assert_records([{ added: animations, changed: [], removed: [] }],
+ "records after animation start");
+
+ // Change a property of the animation such that it keeps running.
+ e.style[aChangeTest.prop] = aChangeTest.val;
+
+ // Wait for the single MutationRecord for the Animation change to
+ // be delivered.
+ yield waitForFrame();
+ assert_records([{ added: [], changed: animations, removed: [] }],
+ "records after animation change");
+
+ // Cancel the animation.
+ e.style.animationName = "none";
+
+ // Wait for the addition, change and removal MutationRecords to be delivered.
+ yield waitForFrame();
+ assert_records([{ added: [], changed: [], removed: animations }],
+ "records after animation end");
+
+ e.style = "";
+ });
+ });
+
+ // Test that calling finish() on a paused (but otherwise finished) animation
+ // dispatches a changed notification.
+ addAsyncAnimTest("finish_from_pause", aOptions, function*() {
+ // Start a long animation
+ e.style = "animation: anim 100s forwards";
+
+ // The animation should cause the creation of a single Animation.
+ var animations = e.getAnimations();
+ is(animations.length, 1, "getAnimations().length after animation start");
+
+ // Wait for the single MutationRecord for the Animation addition to
+ // be delivered.
+ yield waitForFrame();
+ assert_records([{ added: animations, changed: [], removed: [] }],
+ "records after animation start");
+
+ // Wait until the animation is playing.
+ yield animations[0].ready;
+
+ // Finish and pause.
+ animations[0].finish();
+ animations[0].pause();
+
+ // Wait for the pause to complete.
+ yield animations[0].ready;
+ is(animations[0].playState, "paused",
+ "playState after finishing and pausing");
+
+ // We should have two MutationRecords for the Animation changes:
+ // one for the finish, one for the pause.
+ assert_records([{ added: [], changed: animations, removed: [] },
+ { added: [], changed: animations, removed: [] }],
+ "records after finish() and pause()");
+
+ // Call finish() again.
+ animations[0].finish();
+ is(animations[0].playState, "finished",
+ "playState after finishing from paused state");
+
+ // Wait for the single MutationRecord for the Animation change to
+ // be delivered. Even though the currentTime does not change, the
+ // playState will change.
+ yield waitForFrame();
+ assert_records([{ added: [], changed: animations, removed: [] }],
+ "records after finish() and pause()");
+
+ // Cancel the animation.
+ e.style = "";
+
+ // Wait for the single removal notification.
+ yield waitForFrame();
+ assert_records([{ added: [], changed: [], removed: animations }],
+ "records after animation end");
+ });
+
+ // Test that calling finish() on a pause-pending (but otherwise finished)
+ // animation dispatches a changed notification.
+ addAsyncAnimTest("finish_from_pause_pending", aOptions, function*() {
+ // Start a long animation
+ e.style = "animation: anim 100s forwards";
+
+ // The animation should cause the creation of a single Animation.
+ var animations = e.getAnimations();
+ is(animations.length, 1, "getAnimations().length after animation start");
+
+ // Wait for the single MutationRecord for the Animation addition to
+ // be delivered.
+ yield waitForFrame();
+ assert_records([{ added: animations, changed: [], removed: [] }],
+ "records after animation start");
+
+ // Wait until the animation is playing.
+ yield animations[0].ready;
+
+ // Finish and pause.
+ animations[0].finish();
+ animations[0].pause();
+ is(animations[0].playState, "pending",
+ "playState after finishing and calling pause()");
+
+ // Call finish() again to abort the pause
+ animations[0].finish();
+ is(animations[0].playState, "finished",
+ "playState after finishing and calling pause()");
+
+ // Wait for three MutationRecords for the Animation changes to
+ // be delivered: one for each finish(), pause(), finish() operation.
+ yield waitForFrame();
+ assert_records([{ added: [], changed: animations, removed: [] },
+ { added: [], changed: animations, removed: [] },
+ { added: [], changed: animations, removed: [] }],
+ "records after finish(), pause(), finish()");
+
+ // Cancel the animation.
+ e.style = "";
+
+ // Wait for the single removal notification.
+ yield waitForFrame();
+ assert_records([{ added: [], changed: [], removed: animations }],
+ "records after animation end");
+ });
+
+ // Test that calling play() on a paused Animation dispatches a changed
+ // notification.
+ addAsyncAnimTest("play", aOptions, function*() {
+ // Start a long, paused animation
+ e.style = "animation: anim 100s paused";
+
+ // The animation should cause the creation of a single Animation.
+ var animations = e.getAnimations();
+ is(animations.length, 1, "getAnimations().length after animation start");
+
+ // Wait for the single MutationRecord for the Animation addition to
+ // be delivered.
+ yield waitForFrame();
+ assert_records([{ added: animations, changed: [], removed: [] }],
+ "records after animation start");
+
+ // Wait until the animation is ready
+ yield animations[0].ready;
+
+ // Play
+ animations[0].play();
+
+ // Wait for the single MutationRecord for the Animation change to
+ // be delivered.
+ yield animations[0].ready;
+ assert_records([{ added: [], changed: animations, removed: [] }],
+ "records after play()");
+
+ // Redundant play
+ animations[0].play();
+
+ // Wait to ensure no change is dispatched
+ yield waitForFrame();
+ assert_records([], "records after redundant play()");
+
+ // Cancel the animation.
+ e.style = "";
+
+ // Wait for the single removal notification.
+ yield waitForFrame();
+ assert_records([{ added: [], changed: [], removed: animations }],
+ "records after animation end");
+ });
+
+ // Test that calling play() on a finished Animation that fills forwards
+ // dispatches a changed notification.
+ addAsyncAnimTest("play_filling_forwards", aOptions, function*() {
+ // Start a long animation with a forwards fill
+ e.style = "animation: anim 100s forwards";
+
+ // The animation should cause the creation of a single Animation.
+ var animations = e.getAnimations();
+ is(animations.length, 1, "getAnimations().length after animation start");
+
+ // Wait for the single MutationRecord for the Animation addition to
+ // be delivered.
+ yield waitForFrame();
+ assert_records([{ added: animations, changed: [], removed: [] }],
+ "records after animation start");
+
+ // Wait until the animation is ready
+ yield animations[0].ready;
+
+ // Seek to the end
+ animations[0].finish();
+
+ // Wait for the single MutationRecord for the Animation change to
+ // be delivered.
+ yield waitForFrame();
+ assert_records([{ added: [], changed: animations, removed: [] }],
+ "records after finish()");
+
+ // Since we are filling forwards, calling play() should produce a
+ // change record since the animation remains relevant.
+ animations[0].play();
+
+ // Wait for the single MutationRecord for the Animation change to
+ // be delivered.
+ yield waitForFrame();
+ assert_records([{ added: [], changed: animations, removed: [] }],
+ "records after play()");
+
+ // Cancel the animation.
+ e.style = "";
+
+ // Wait for the single removal notification.
+ yield waitForFrame();
+ assert_records([{ added: [], changed: [], removed: animations }],
+ "records after animation end");
+ });
+
+ // Test that calling play() on a finished Animation that does *not* fill
+ // forwards dispatches an addition notification.
+ addAsyncAnimTest("play_after_finish", aOptions, function*() {
+ // Start a long animation
+ e.style = "animation: anim 100s";
+
+ // The animation should cause the creation of a single Animation.
+ var animations = e.getAnimations();
+ is(animations.length, 1, "getAnimations().length after animation start");
+
+ // Wait for the single MutationRecord for the Animation addition to
+ // be delivered.
+ yield waitForFrame();
+ assert_records([{ added: animations, changed: [], removed: [] }],
+ "records after animation start");
+
+ // Wait until the animation is ready
+ yield animations[0].ready;
+
+ // Seek to the end
+ animations[0].finish();
+
+ // Wait for the single MutationRecord for the Animation removal to
+ // be delivered.
+ yield waitForFrame();
+ assert_records([{ added: [], changed: [], removed: animations }],
+ "records after finish()");
+
+ // Since we are *not* filling forwards, calling play() is equivalent
+ // to creating a new animation since it becomes relevant again.
+ animations[0].play();
+
+ // Wait for the single MutationRecord for the Animation addition to
+ // be delivered.
+ yield waitForFrame();
+ assert_records([{ added: animations, changed: [], removed: [] }],
+ "records after play()");
+
+ // Cancel the animation.
+ e.style = "";
+
+ // Wait for the single removal notification.
+ yield waitForFrame();
+ assert_records([{ added: [], changed: [], removed: animations }],
+ "records after animation end");
+ });
+
+ // Test that calling pause() on an Animation dispatches a changed
+ // notification.
+ addAsyncAnimTest("pause", aOptions, function*() {
+ // Start a long animation
+ e.style = "animation: anim 100s";
+
+ // The animation should cause the creation of a single Animation.
+ var animations = e.getAnimations();
+ is(animations.length, 1, "getAnimations().length after animation start");
+
+ // Wait for the single MutationRecord for the Animation addition to
+ // be delivered.
+ yield waitForFrame();
+ assert_records([{ added: animations, changed: [], removed: [] }],
+ "records after animation start");
+
+ // Wait until the animation is ready
+ yield animations[0].ready;
+
+ // Pause
+ animations[0].pause();
+
+ // Wait for the single MutationRecord for the Animation change to
+ // be delivered.
+ yield animations[0].ready;
+ assert_records([{ added: [], changed: animations, removed: [] }],
+ "records after pause()");
+
+ // Redundant pause
+ animations[0].pause();
+
+ // Wait to ensure no change is dispatched
+ yield animations[0].ready;
+ assert_records([], "records after redundant pause()");
+
+ // Cancel the animation.
+ e.style = "";
+
+ // Wait for the single removal notification.
+ yield waitForFrame();
+ assert_records([{ added: [], changed: [], removed: animations }],
+ "records after animation end");
+ });
+
+ // Test that calling pause() on an Animation that is pause-pending
+ // does not dispatch an additional changed notification.
+ addAsyncAnimTest("pause_while_pause_pending", aOptions, function*() {
+ // Start a long animation
+ e.style = "animation: anim 100s";
+
+ // The animation should cause the creation of a single Animation.
+ var animations = e.getAnimations();
+ is(animations.length, 1, "getAnimations().length after animation start");
+
+ // Wait for the single MutationRecord for the Animation addition to
+ // be delivered.
+ yield waitForFrame();
+ assert_records([{ added: animations, changed: [], removed: [] }],
+ "records after animation start");
+
+ // Wait until the animation is ready
+ yield animations[0].ready;
+
+ // Pause
+ animations[0].pause();
+
+ // We are now pause-pending, but pause again
+ animations[0].pause();
+
+ // We should only get a single change record
+ yield animations[0].ready;
+ assert_records([{ added: [], changed: animations, removed: [] }],
+ "records after pause()");
+
+ // Cancel the animation.
+ e.style = "";
+
+ // Wait for the single removal notification.
+ yield waitForFrame();
+ assert_records([{ added: [], changed: [], removed: animations }],
+ "records after animation end");
+ });
+
+ // Test that calling play() on an Animation that is pause-pending
+ // dispatches a changed notification.
+ addAsyncAnimTest("aborted_pause", aOptions, function*() {
+ // Start a long animation
+ e.style = "animation: anim 100s";
+
+ // The animation should cause the creation of a single Animation.
+ var animations = e.getAnimations();
+ is(animations.length, 1, "getAnimations().length after animation start");
+
+ // Wait for the single MutationRecord for the Animation addition to
+ // be delivered.
+ yield waitForFrame();
+ assert_records([{ added: animations, changed: [], removed: [] }],
+ "records after animation start");
+
+ // Wait until the animation is ready
+ yield animations[0].ready;
+
+ // Pause
+ animations[0].pause();
+
+ // We are now pause-pending. If we play() now, we will abort the pause
+ animations[0].play();
+
+ // We should get two change records
+ yield animations[0].ready;
+ assert_records([{ added: [], changed: animations, removed: [] },
+ { added: [], changed: animations, removed: [] }],
+ "records after aborting a pause()");
+
+ // Cancel the animation.
+ e.style = "";
+
+ // Wait for the single removal notification.
+ yield waitForFrame();
+ assert_records([{ added: [], changed: [], removed: animations }],
+ "records after animation end");
+ });
+
+ // Test that a non-cancelling change to an animation followed immediately by a
+ // cancelling change will only send an animation removal notification.
+ addAsyncAnimTest("coalesce_change_cancel", aOptions, function*() {
+ // Start a long animation.
+ e.style = "animation: anim 100s;";
+
+ // The animation should cause the creation of a single Animation.
+ var animations = e.getAnimations();
+ is(animations.length, 1, "getAnimations().length after animation start");
+
+ // Wait for the single MutationRecord for the Animation addition to
+ // be delivered.
+ yield waitForFrame();
+ assert_records([{ added: animations, changed: [], removed: [] }],
+ "records after animation start");
+
+ // Update the animation's delay such that it is still running.
+ e.style.animationDelay = "-1s";
+
+ // Then cancel the animation by updating its duration.
+ e.style.animationDuration = "0.5s";
+
+ // We should get a single removal notification.
+ yield waitForFrame();
+ assert_records([{ added: [], changed: [], removed: animations }],
+ "records after animation end");
+
+ e.style = "";
+ });
+
+});
+
+addAsyncAnimTest("tree_ordering", { observe: div, subtree: true }, function*() {
+ // Add style for pseudo elements
+ var extraStyle = document.createElement('style');
+ document.head.appendChild(extraStyle);
+ var sheet = extraStyle.sheet;
+ var rules = { ".before::before": "animation: anim 100s;",
+ ".after::after" : "animation: anim 100s, anim 100s;" };
+ for (var selector in rules) {
+ sheet.insertRule(selector + '{' + rules[selector] + '}',
+ sheet.cssRules.length);
+ }
+
+ // Create a tree with two children:
+ //
+ // div
+ // (::before)
+ // (::after)
+ // / \
+ // childA childB(::before)
+ var childA = document.createElement("div");
+ var childB = document.createElement("div");
+
+ div.appendChild(childA);
+ div.appendChild(childB);
+
+ // Start an animation on each (using order: childB, div, childA)
+ //
+ // We include multiple animations on some nodes so that we can test batching
+ // works as expected later in this test.
+ childB.style = "animation: anim 100s";
+ div.style = "animation: anim 100s, anim 100s, anim 100s";
+ childA.style = "animation: anim 100s, anim 100s";
+
+ // Start animations targeting to pseudo element of div and childB.
+ childB.classList.add("before");
+ div.classList.add("after");
+ div.classList.add("before");
+
+ // Check all animations we have in this document
+ var docAnims = document.getAnimations();
+ is(docAnims.length, 10, "total animations");
+
+ var divAnimations = div.getAnimations();
+ var childAAnimations = childA.getAnimations();
+ var childBAnimations = childB.getAnimations();
+ var divBeforeAnimations =
+ [ for (x of docAnims) if (x.effect.target.parentElement == div &&
+ x.effect.target.type == "::before") x ];
+ var divAfterAnimations =
+ [ for (x of docAnims) if (x.effect.target.parentElement == div &&
+ x.effect.target.type == "::after") x ];
+ var childBPseudoAnimations =
+ [ for (x of docAnims) if (x.effect.target.parentElement == childB) x ];
+
+ // The order in which we get the corresponding records is currently
+ // based on the order we visit these nodes when updating styles.
+ //
+ // That is because we don't do any document-level batching of animation
+ // mutation records when we flush styles. We may introduce that in the
+ // future but for now all we are interested in testing here is that the order
+ // these records are dispatched is consistent between runs.
+ //
+ // We currently expect to get records in order div::after, childA, childB,
+ // childB::before, div, div::before
+ yield waitForFrame();
+ assert_records([{ added: divAfterAnimations, changed: [], removed: [] },
+ { added: childAAnimations, changed: [], removed: [] },
+ { added: childBAnimations, changed: [], removed: [] },
+ { added: childBPseudoAnimations, changed: [], removed: [] },
+ { added: divAnimations, changed: [], removed: [] },
+ { added: divBeforeAnimations, changed: [], removed: [] }],
+ "records after simultaneous animation start");
+
+ // The one case where we *do* currently perform document-level (or actually
+ // timeline-level) batching is when animations are updated from a refresh
+ // driver tick. In particular, this means that when animations finish
+ // naturally the removed records should be dispatched according to the
+ // position of the elements in the tree.
+
+ // First, flatten the set of animations. we put the animations targeting to
+ // pseudo elements last. (Actually, we don't care the order in the list.)
+ var animations = [ ...divAnimations,
+ ...childAAnimations,
+ ...childBAnimations,
+ ...divBeforeAnimations,
+ ...divAfterAnimations,
+ ...childBPseudoAnimations ];
+
+ // Fast-forward to *just* before the end of the animation.
+ animations.forEach(animation => animation.currentTime = 99999);
+
+ // Prepare the set of expected change MutationRecords, one for each
+ // animation that was seeked.
+ var seekRecords = animations.map(
+ p => ({ added: [], changed: [p], removed: [] })
+ );
+
+ yield await_event(div, "animationend");
+
+ // After the changed notifications, which will be dispatched in the order that
+ // the animations were seeked, we should get removal MutationRecords in order
+ // (div, div::before, div::after), childA, (childB, childB::before).
+ // Note: The animations targeting to the pseudo element are appended after
+ // the animations of its parent element.
+ divAnimations = [ ...divAnimations,
+ ...divBeforeAnimations,
+ ...divAfterAnimations ];
+ childBAnimations = [ ...childBAnimations, ...childBPseudoAnimations ];
+ assert_records(seekRecords.concat(
+ { added: [], changed: [], removed: divAnimations },
+ { added: [], changed: [], removed: childAAnimations },
+ { added: [], changed: [], removed: childBAnimations }),
+ "records after finishing");
+
+ // Clean up
+ div.classList.remove("before");
+ div.classList.remove("after");
+ div.style = "";
+ childA.remove();
+ childB.remove();
+ extraStyle.remove();
+});
+
+// Run the tests.
+SimpleTest.requestLongerTimeout(2);
+SimpleTest.waitForExplicitFinish();
+
+runAllAsyncTests().then(function() {
+ SimpleTest.finish();
+}, function(aError) {
+ ok(false, "Something failed: " + aError);
+});
+</script>