summaryrefslogtreecommitdiff
path: root/dom/animation/test
diff options
context:
space:
mode:
Diffstat (limited to 'dom/animation/test')
-rw-r--r--dom/animation/test/chrome.ini17
-rw-r--r--dom/animation/test/chrome/file_animate_xrays.html19
-rw-r--r--dom/animation/test/chrome/test_animate_xrays.html31
-rw-r--r--dom/animation/test/chrome/test_animation_observers.html1177
-rw-r--r--dom/animation/test/chrome/test_animation_performance_warning.html957
-rw-r--r--dom/animation/test/chrome/test_animation_properties.html993
-rw-r--r--dom/animation/test/chrome/test_generated_content_getAnimations.html83
-rw-r--r--dom/animation/test/chrome/test_observers_for_sync_api.html854
-rw-r--r--dom/animation/test/chrome/test_restyles.html815
-rw-r--r--dom/animation/test/chrome/test_running_on_compositor.html966
-rw-r--r--dom/animation/test/crashtests/1216842-1.html35
-rw-r--r--dom/animation/test/crashtests/1216842-2.html35
-rw-r--r--dom/animation/test/crashtests/1216842-3.html27
-rw-r--r--dom/animation/test/crashtests/1216842-4.html27
-rw-r--r--dom/animation/test/crashtests/1216842-5.html38
-rw-r--r--dom/animation/test/crashtests/1216842-6.html38
-rw-r--r--dom/animation/test/crashtests/1239889-1.html12
-rw-r--r--dom/animation/test/crashtests/1244595-1.html3
-rw-r--r--dom/animation/test/crashtests/1272475-1.html20
-rw-r--r--dom/animation/test/crashtests/1272475-2.html20
-rw-r--r--dom/animation/test/crashtests/1277272-1-inner.html19
-rw-r--r--dom/animation/test/crashtests/1277272-1.html26
-rw-r--r--dom/animation/test/crashtests/1278485-1.html26
-rw-r--r--dom/animation/test/crashtests/1290535-1.html20
-rw-r--r--dom/animation/test/crashtests/crashtests.list13
-rw-r--r--dom/animation/test/css-animations/file_animation-cancel.html154
-rw-r--r--dom/animation/test/css-animations/file_animation-computed-timing.html566
-rw-r--r--dom/animation/test/css-animations/file_animation-currenttime.html345
-rw-r--r--dom/animation/test/css-animations/file_animation-finish.html97
-rw-r--r--dom/animation/test/css-animations/file_animation-finished.html93
-rw-r--r--dom/animation/test/css-animations/file_animation-id.html24
-rw-r--r--dom/animation/test/css-animations/file_animation-pausing.html165
-rw-r--r--dom/animation/test/css-animations/file_animation-playstate.html71
-rw-r--r--dom/animation/test/css-animations/file_animation-ready.html149
-rw-r--r--dom/animation/test/css-animations/file_animation-reverse.html29
-rw-r--r--dom/animation/test/css-animations/file_animation-starttime.html383
-rw-r--r--dom/animation/test/css-animations/file_animations-dynamic-changes.html154
-rw-r--r--dom/animation/test/css-animations/file_cssanimation-animationname.html37
-rw-r--r--dom/animation/test/css-animations/file_document-get-animations.html276
-rw-r--r--dom/animation/test/css-animations/file_effect-target.html54
-rw-r--r--dom/animation/test/css-animations/file_element-get-animations.html445
-rw-r--r--dom/animation/test/css-animations/file_keyframeeffect-getkeyframes.html672
-rw-r--r--dom/animation/test/css-animations/file_pseudoElement-get-animations.html70
-rw-r--r--dom/animation/test/css-animations/test_animation-cancel.html15
-rw-r--r--dom/animation/test/css-animations/test_animation-computed-timing.html16
-rw-r--r--dom/animation/test/css-animations/test_animation-currenttime.html15
-rw-r--r--dom/animation/test/css-animations/test_animation-finish.html15
-rw-r--r--dom/animation/test/css-animations/test_animation-finished.html15
-rw-r--r--dom/animation/test/css-animations/test_animation-id.html15
-rw-r--r--dom/animation/test/css-animations/test_animation-pausing.html15
-rw-r--r--dom/animation/test/css-animations/test_animation-playstate.html15
-rw-r--r--dom/animation/test/css-animations/test_animation-ready.html15
-rw-r--r--dom/animation/test/css-animations/test_animation-reverse.html15
-rw-r--r--dom/animation/test/css-animations/test_animation-starttime.html15
-rw-r--r--dom/animation/test/css-animations/test_animations-dynamic-changes.html15
-rw-r--r--dom/animation/test/css-animations/test_cssanimation-animationname.html15
-rw-r--r--dom/animation/test/css-animations/test_document-get-animations.html15
-rw-r--r--dom/animation/test/css-animations/test_effect-target.html15
-rw-r--r--dom/animation/test/css-animations/test_element-get-animations.html15
-rw-r--r--dom/animation/test/css-animations/test_keyframeeffect-getkeyframes.html15
-rw-r--r--dom/animation/test/css-animations/test_pseudoElement-get-animations.html14
-rw-r--r--dom/animation/test/css-transitions/file_animation-cancel.html165
-rw-r--r--dom/animation/test/css-transitions/file_animation-computed-timing.html315
-rw-r--r--dom/animation/test/css-transitions/file_animation-currenttime.html307
-rw-r--r--dom/animation/test/css-transitions/file_animation-finished.html61
-rw-r--r--dom/animation/test/css-transitions/file_animation-pausing.html50
-rw-r--r--dom/animation/test/css-transitions/file_animation-ready.html96
-rw-r--r--dom/animation/test/css-transitions/file_animation-starttime.html284
-rw-r--r--dom/animation/test/css-transitions/file_csstransition-events.html223
-rw-r--r--dom/animation/test/css-transitions/file_csstransition-transitionproperty.html24
-rw-r--r--dom/animation/test/css-transitions/file_document-get-animations.html93
-rw-r--r--dom/animation/test/css-transitions/file_effect-target.html66
-rw-r--r--dom/animation/test/css-transitions/file_element-get-animations.html147
-rw-r--r--dom/animation/test/css-transitions/file_keyframeeffect-getkeyframes.html95
-rw-r--r--dom/animation/test/css-transitions/file_pseudoElement-get-animations.html45
-rw-r--r--dom/animation/test/css-transitions/file_setting-effect.html91
-rw-r--r--dom/animation/test/css-transitions/test_animation-cancel.html14
-rw-r--r--dom/animation/test/css-transitions/test_animation-computed-timing.html16
-rw-r--r--dom/animation/test/css-transitions/test_animation-currenttime.html14
-rw-r--r--dom/animation/test/css-transitions/test_animation-finished.html14
-rw-r--r--dom/animation/test/css-transitions/test_animation-pausing.html14
-rw-r--r--dom/animation/test/css-transitions/test_animation-ready.html14
-rw-r--r--dom/animation/test/css-transitions/test_animation-starttime.html14
-rw-r--r--dom/animation/test/css-transitions/test_csstransition-events.html14
-rw-r--r--dom/animation/test/css-transitions/test_csstransition-transitionproperty.html14
-rw-r--r--dom/animation/test/css-transitions/test_document-get-animations.html15
-rw-r--r--dom/animation/test/css-transitions/test_effect-target.html14
-rw-r--r--dom/animation/test/css-transitions/test_element-get-animations.html14
-rw-r--r--dom/animation/test/css-transitions/test_keyframeeffect-getkeyframes.html14
-rw-r--r--dom/animation/test/css-transitions/test_pseudoElement-get-animations.html14
-rw-r--r--dom/animation/test/css-transitions/test_setting-effect.html14
-rw-r--r--dom/animation/test/document-timeline/file_document-timeline.html135
-rw-r--r--dom/animation/test/document-timeline/test_document-timeline.html14
-rw-r--r--dom/animation/test/document-timeline/test_request_animation_frame.html27
-rw-r--r--dom/animation/test/mochitest.ini111
-rw-r--r--dom/animation/test/mozilla/file_cubic_bezier_limits.html167
-rw-r--r--dom/animation/test/mozilla/file_deferred_start.html121
-rw-r--r--dom/animation/test/mozilla/file_disable_animations_api_core.html30
-rw-r--r--dom/animation/test/mozilla/file_disabled_properties.html73
-rw-r--r--dom/animation/test/mozilla/file_discrete-animations.html170
-rw-r--r--dom/animation/test/mozilla/file_document-timeline-origin-time-range.html30
-rw-r--r--dom/animation/test/mozilla/file_hide_and_show.html162
-rw-r--r--dom/animation/test/mozilla/file_partial_keyframes.html41
-rw-r--r--dom/animation/test/mozilla/file_set-easing.html34
-rw-r--r--dom/animation/test/mozilla/file_spacing_property_order.html33
-rw-r--r--dom/animation/test/mozilla/file_spacing_transform.html240
-rw-r--r--dom/animation/test/mozilla/file_transform_limits.html55
-rw-r--r--dom/animation/test/mozilla/file_transition_finish_on_compositor.html67
-rw-r--r--dom/animation/test/mozilla/file_underlying-discrete-value.html192
-rw-r--r--dom/animation/test/mozilla/test_cubic_bezier_limits.html14
-rw-r--r--dom/animation/test/mozilla/test_deferred_start.html14
-rw-r--r--dom/animation/test/mozilla/test_disable_animations_api_core.html14
-rw-r--r--dom/animation/test/mozilla/test_disabled_properties.html14
-rw-r--r--dom/animation/test/mozilla/test_discrete-animations.html18
-rw-r--r--dom/animation/test/mozilla/test_document-timeline-origin-time-range.html14
-rw-r--r--dom/animation/test/mozilla/test_hide_and_show.html14
-rw-r--r--dom/animation/test/mozilla/test_partial_keyframes.html14
-rw-r--r--dom/animation/test/mozilla/test_set-easing.html14
-rw-r--r--dom/animation/test/mozilla/test_spacing_property_order.html14
-rw-r--r--dom/animation/test/mozilla/test_spacing_transform.html14
-rw-r--r--dom/animation/test/mozilla/test_transform_limits.html14
-rw-r--r--dom/animation/test/mozilla/test_transition_finish_on_compositor.html14
-rw-r--r--dom/animation/test/mozilla/test_underlying-discrete-value.html15
-rw-r--r--dom/animation/test/style/file_animation-seeking-with-current-time.html121
-rw-r--r--dom/animation/test/style/file_animation-seeking-with-start-time.html121
-rw-r--r--dom/animation/test/style/file_animation-setting-effect.html125
-rw-r--r--dom/animation/test/style/file_animation-setting-spacing.html111
-rw-r--r--dom/animation/test/style/test_animation-seeking-with-current-time.html15
-rw-r--r--dom/animation/test/style/test_animation-seeking-with-start-time.html15
-rw-r--r--dom/animation/test/style/test_animation-setting-effect.html15
-rw-r--r--dom/animation/test/style/test_animation-setting-spacing.html14
-rw-r--r--dom/animation/test/testcommon.js216
132 files changed, 15256 insertions, 0 deletions
diff --git a/dom/animation/test/chrome.ini b/dom/animation/test/chrome.ini
new file mode 100644
index 0000000000..9026bcbd2e
--- /dev/null
+++ b/dom/animation/test/chrome.ini
@@ -0,0 +1,17 @@
+[DEFAULT]
+support-files =
+ testcommon.js
+ ../../imptests/testharness.js
+ ../../imptests/testharnessreport.js
+ !/dom/animation/test/chrome/file_animate_xrays.html
+
+[chrome/test_animate_xrays.html]
+# file_animate_xrays.html needs to go in mochitest.ini since it is served
+# over HTTP
+[chrome/test_animation_observers.html]
+[chrome/test_animation_performance_warning.html]
+[chrome/test_animation_properties.html]
+[chrome/test_generated_content_getAnimations.html]
+[chrome/test_observers_for_sync_api.html]
+[chrome/test_restyles.html]
+[chrome/test_running_on_compositor.html]
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..8a68fc548f
--- /dev/null
+++ b/dom/animation/test/chrome/file_animate_xrays.html
@@ -0,0 +1,19 @@
+<!doctype html>
+<html>
+<head>
+<meta charset=utf-8>
+<script>
+Element.prototype.animate = function() {
+ throw 'Called animate() as defined in content document';
+}
+// Bug 1211783: Use KeyframeEffect (not KeyframeEffectReadOnly) here
+for (var obj of [KeyframeEffectReadOnly, Animation]) {
+ obj = function() {
+ throw 'Called overridden ' + String(obj) + ' constructor';
+ };
+}
+</script>
+<body>
+<div id="target"></div>
+</body>
+</html>
diff --git a/dom/animation/test/chrome/test_animate_xrays.html b/dom/animation/test/chrome/test_animate_xrays.html
new file mode 100644
index 0000000000..56b981bf15
--- /dev/null
+++ b/dom/animation/test/chrome/test_animate_xrays.html
@@ -0,0 +1,31 @@
+<!doctype html>
+<head>
+<meta charset=utf-8>
+<script type="application/javascript" src="../testharness.js"></script>
+<script type="application/javascript" src="../testharnessreport.js"></script>
+<script type="application/javascript" src="../testcommon.js"></script>
+</head>
+<body>
+<a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1045994"
+ target="_blank">Mozilla Bug 1045994</a>
+<div id="log"></div>
+<iframe id="iframe"
+ src="http://example.org/tests/dom/animation/test/chrome/file_animate_xrays.html"></iframe>
+<script>
+'use strict';
+
+var win = document.getElementById('iframe').contentWindow;
+
+async_test(function(t) {
+ window.addEventListener('load', t.step_func(function() {
+ var target = win.document.getElementById('target');
+ var anim = target.animate({ opacity: [ 0, 1 ] }, 100 * MS_PER_SEC);
+ // In the x-ray case, the frames object will be given an opaque wrapper
+ // so it won't be possible to fetch any frames from it.
+ assert_equals(anim.effect.getKeyframes().length, 0);
+ t.done();
+ }));
+}, 'Calling animate() across x-rays');
+
+</script>
+</body>
diff --git a/dom/animation/test/chrome/test_animation_observers.html b/dom/animation/test/chrome/test_animation_observers.html
new file mode 100644
index 0000000000..237128e041
--- /dev/null
+++ b/dom/animation/test/chrome/test_animation_observers.html
@@ -0,0 +1,1177 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title>Test chrome-only MutationObserver animation notifications</title>
+<script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+<script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
+<script src="../testcommon.js"></script>
+<link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+<style>
+@keyframes anim {
+ to { transform: translate(100px); }
+}
+@keyframes anotherAnim {
+ to { transform: translate(0px); }
+}
+#target {
+ width: 100px;
+ height: 100px;
+ background-color: yellow;
+ line-height: 16px;
+}
+</style>
+<div id=container><div id=target></div></div>
+<script>
+var div = document.getElementById("target");
+var gRecords = [];
+var gObserver = new MutationObserver(function(newRecords) {
+ gRecords.push(...newRecords);
+});
+
+// Asynchronous testing framework based on layout/style/test/animation_utils.js.
+
+var gTests = [];
+var gCurrentTestName;
+
+function addAsyncAnimTest(aName, aOptions, aTestGenerator) {
+ aTestGenerator.testName = aName;
+ aTestGenerator.options = aOptions || {};
+ gTests.push(aTestGenerator);
+}
+
+function runAsyncTest(aTestGenerator) {
+ return waitForFrame().then(function() {
+ var generator;
+
+ function step(arg) {
+ var next;
+ try {
+ next = generator.next(arg);
+ } catch (e) {
+ return Promise.reject(e);
+ }
+ if (next.done) {
+ return Promise.resolve(next.value);
+ } else {
+ return Promise.resolve(next.value).then(step);
+ }
+ }
+
+ var subtree = aTestGenerator.options.subtree;
+
+ gCurrentTestName = aTestGenerator.testName;
+ if (subtree) {
+ gCurrentTestName += ":subtree";
+ }
+
+ gRecords = [];
+ gObserver.disconnect();
+ gObserver.observe(aTestGenerator.options.observe,
+ { animations: true, subtree: subtree});
+
+ generator = aTestGenerator();
+ return step();
+ });
+};
+
+function runAllAsyncTests() {
+ return gTests.reduce(function(sequence, test) {
+ return sequence.then(() => runAsyncTest(test));
+ }, Promise.resolve());
+}
+
+// Wrap is and ok with versions that prepend the current sub-test name
+// to the assertion description.
+var old_is = is, old_ok = ok;
+is = function(a, b, message) {
+ if (gCurrentTestName && message) {
+ message = `[${gCurrentTestName}] ${message}`;
+ }
+ old_is(a, b, message);
+}
+ok = function(a, message) {
+ if (gCurrentTestName && message) {
+ message = `[${gCurrentTestName}] ${message}`;
+ }
+ old_ok(a, message);
+}
+
+// Adds an event listener and returns a Promise that is resolved when the
+// event listener is called.
+function await_event(aElement, aEventName) {
+ return new Promise(function(aResolve) {
+ function listener(aEvent) {
+ aElement.removeEventListener(aEventName, listener);
+ aResolve();
+ }
+ aElement.addEventListener(aEventName, listener, false);
+ });
+}
+
+function assert_record_list(actual, expected, desc, index, listName) {
+ is(actual.length, expected.length, `${desc} - record[${index}].${listName} length`);
+ if (actual.length != expected.length) {
+ return;
+ }
+ for (var i = 0; i < actual.length; i++) {
+ ok(actual.indexOf(expected[i]) != -1,
+ `${desc} - record[${index}].${listName} contains expected Animation`);
+ }
+}
+
+function assert_records(expected, desc) {
+ var records = gRecords;
+ gRecords = [];
+ is(records.length, expected.length, `${desc} - number of records`);
+ if (records.length != expected.length) {
+ return;
+ }
+ for (var i = 0; i < records.length; i++) {
+ assert_record_list(records[i].addedAnimations, expected[i].added, desc, i, "addedAnimations");
+ assert_record_list(records[i].changedAnimations, expected[i].changed, desc, i, "changedAnimations");
+ assert_record_list(records[i].removedAnimations, expected[i].removed, desc, i, "removedAnimations");
+ }
+}
+
+// -- Tests ------------------------------------------------------------------
+
+// We run all tests first targeting the div and observing the div, then again
+// targeting the div and observing its parent while using the subtree:true
+// MutationObserver option.
+
+[
+ { observe: div, target: div, subtree: false },
+ { observe: div.parentNode, target: div, subtree: true },
+].forEach(function(aOptions) {
+
+ var e = aOptions.target;
+
+ // Test that starting a single transition that completes normally
+ // dispatches an added notification and then a removed notification.
+ addAsyncAnimTest("single_transition", aOptions, function*() {
+ // Start a transition.
+ e.style = "transition: background-color 100s; background-color: lime;";
+
+ // Register for the end of the transition.
+ var transitionEnd = await_event(e, "transitionend");
+
+ // The transition should cause the creation of a single Animation.
+ var animations = e.getAnimations();
+ is(animations.length, 1, "getAnimations().length after transition start");
+
+ // Wait for the single MutationRecord for the Animation addition to
+ // be delivered.
+ yield waitForFrame();
+ assert_records([{ added: animations, changed: [], removed: [] }],
+ "records after transition start");
+
+ // Advance until near the end of the transition, then wait for it to finish.
+ animations[0].currentTime = 99900;
+ yield transitionEnd;
+
+ // After the transition has finished, the Animation should disappear.
+ is(e.getAnimations().length, 0,
+ "getAnimations().length after transition end");
+
+ // Wait for the change MutationRecord for seeking the Animation to be
+ // delivered, followed by the the removal MutationRecord.
+ yield waitForFrame();
+ assert_records([{ added: [], changed: animations, removed: [] },
+ { added: [], changed: [], removed: animations }],
+ "records after transition end");
+
+ e.style = "";
+ });
+
+ // Test that starting a single transition that is cancelled by resetting
+ // the transition-property property dispatches an added notification and
+ // then a removed notification.
+ addAsyncAnimTest("single_transition_cancelled_property", aOptions, function*() {
+ // Start a long transition.
+ e.style = "transition: background-color 100s; background-color: lime;";
+
+ // The transition should cause the creation of a single Animation.
+ var animations = e.getAnimations();
+ is(animations.length, 1, "getAnimations().length after transition start");
+
+ // Wait for the single MutationRecord for the Animation addition to
+ // be delivered.
+ yield waitForFrame();
+ assert_records([{ added: animations, changed: [], removed: [] }],
+ "records after transition start");
+
+ // Cancel the transition by setting transition-property.
+ e.style.transitionProperty = "none";
+
+ // Wait for the single MutationRecord for the Animation removal to
+ // be delivered.
+ yield waitForFrame();
+ assert_records([{ added: [], changed: [], removed: animations }],
+ "records after transition end");
+
+ e.style = "";
+ });
+
+ // Test that starting a single transition that is cancelled by setting
+ // style to the currently animated value dispatches an added
+ // notification and then a removed notification.
+ addAsyncAnimTest("single_transition_cancelled_value", aOptions, function*() {
+ // Start a long transition with a predictable value.
+ e.style = "transition: background-color 100s steps(2, end) -51s; background-color: lime;";
+
+ // The transition should cause the creation of a single Animation.
+ var animations = e.getAnimations();
+ is(animations.length, 1, "getAnimations().length after transition start");
+
+ // Wait for the single MutationRecord for the Animation addition to
+ // be delivered.
+ yield waitForFrame();
+ assert_records([{ added: animations, changed: [], removed: [] }],
+ "records after transition start");
+
+ // Cancel the transition by setting the current animation value.
+ var value = "rgb(128, 255, 0)";
+ is(getComputedStyle(e).backgroundColor, value, "half-way transition value");
+ e.style.backgroundColor = value;
+
+ // Wait for the single MutationRecord for the Animation removal to
+ // be delivered.
+ yield waitForFrame();
+ assert_records([{ added: [], changed: [], removed: animations }],
+ "records after transition end");
+
+ e.style = "";
+ });
+
+ // Test that starting a single transition that is cancelled by setting
+ // style to a non-interpolable value dispatches an added notification
+ // and then a removed notification.
+ addAsyncAnimTest("single_transition_cancelled_noninterpolable", aOptions, function*() {
+ // Start a long transition.
+ e.style = "transition: line-height 100s; line-height: 100px;";
+
+ // The transition should cause the creation of a single Animation.
+ var animations = e.getAnimations();
+ is(animations.length, 1, "getAnimations().length after transition start");
+
+ // Wait for the single MutationRecord for the Animation addition to
+ // be delivered.
+ yield waitForFrame();
+ assert_records([{ added: animations, changed: [], removed: [] }],
+ "records after transition start");
+
+ // Cancel the transition by setting line-height to a non-interpolable value.
+ e.style.lineHeight = "normal";
+
+ // Wait for the single MutationRecord for the Animation removal to
+ // be delivered.
+ yield waitForFrame();
+ assert_records([{ added: [], changed: [], removed: animations }],
+ "records after transition end");
+
+ e.style = "";
+ });
+
+ // Test that starting a single transition and then reversing it
+ // dispatches an added notification, then a simultaneous removed and
+ // added notification, then a removed notification once finished.
+ addAsyncAnimTest("single_transition_reversed", aOptions, function*() {
+ // Start a long transition.
+ e.style = "transition: background-color 100s step-start; background-color: lime;";
+
+ // The transition should cause the creation of a single Animation.
+ var animations = e.getAnimations();
+ is(animations.length, 1, "getAnimations().length after transition start");
+
+ var firstAnimation = animations[0];
+
+ // Wait for the single MutationRecord for the Animation addition to
+ // be delivered.
+ yield waitForFrame();
+ assert_records([{ added: [firstAnimation], changed: [], removed: [] }],
+ "records after transition start");
+
+ // Wait for the Animation to be playing, then seek well into
+ // the transition.
+ yield firstAnimation.ready;
+ firstAnimation.currentTime = 50 * MS_PER_SEC;
+
+ // Reverse the transition by setting the background-color back to its
+ // original value.
+ e.style.backgroundColor = "yellow";
+
+ // The reversal should cause the creation of a new Animation.
+ animations = e.getAnimations();
+ is(animations.length, 1, "getAnimations().length after transition reversal");
+
+ var secondAnimation = animations[0];
+
+ ok(firstAnimation != secondAnimation,
+ "second Animation should be different from the first");
+
+ // Wait for the change Mutation record from seeking the first animation
+ // to be delivered, followed by a subsequent MutationRecord for the removal
+ // of the original Animation and the addition of the new Animation.
+ yield waitForFrame();
+ assert_records([{ added: [], changed: [firstAnimation], removed: [] },
+ { added: [secondAnimation], changed: [],
+ removed: [firstAnimation] }],
+ "records after transition reversal");
+
+ // Cancel the transition.
+ e.style.transitionProperty = "none";
+
+ // Wait for the single MutationRecord for the Animation removal to
+ // be delivered.
+ yield waitForFrame();
+ assert_records([{ added: [], changed: [], removed: [secondAnimation] }],
+ "records after transition end");
+
+ e.style = "";
+ });
+
+ // Test that multiple transitions starting and ending on an element
+ // at the same time get batched up into a single MutationRecord.
+ addAsyncAnimTest("multiple_transitions", aOptions, function*() {
+ // Start three long transitions.
+ e.style = "transition-duration: 100s; " +
+ "transition-property: color, background-color, line-height; " +
+ "color: blue; background-color: lime; line-height: 24px;";
+
+ // The transitions should cause the creation of three Animations.
+ var animations = e.getAnimations();
+ is(animations.length, 3, "getAnimations().length after transition starts");
+
+ // Wait for the single MutationRecord for the Animation additions to
+ // be delivered.
+ yield waitForFrame();
+ assert_records([{ added: animations, changed: [], removed: [] }],
+ "records after transition starts");
+
+ // Wait for the Animations to get going.
+ yield animations[0].ready;
+ is(animations.filter(p => p.playState == "running").length, 3,
+ "number of running Animations");
+
+ // Seek well into each animation.
+ animations.forEach(p => p.currentTime = 50 * MS_PER_SEC);
+
+ // Prepare the set of expected change MutationRecords, one for each
+ // animation that was seeked.
+ var seekRecords = animations.map(
+ p => ({ added: [], changed: [p], removed: [] })
+ );
+
+ // Cancel one of the transitions by setting transition-property.
+ e.style.transitionProperty = "background-color, line-height";
+
+ var colorAnimation = animations.filter(p => p.playState != "running");
+ var otherAnimations = animations.filter(p => p.playState == "running");
+
+ is(colorAnimation.length, 1,
+ "number of non-running Animations after cancelling one");
+ is(otherAnimations.length, 2,
+ "number of running Animations after cancelling one");
+
+ // Wait for the MutationRecords to be delivered: one for each animation
+ // that was seeked, followed by one for the removal of the color animation.
+ yield waitForFrame();
+ assert_records(seekRecords.concat(
+ { added: [], changed: [], removed: colorAnimation }),
+ "records after color transition end");
+
+ // Cancel the remaining transitions.
+ e.style.transitionProperty = "none";
+
+ // Wait for the MutationRecord for the other two Animation
+ // removals to be delivered.
+ yield waitForFrame();
+ assert_records([{ added: [], changed: [], removed: otherAnimations }],
+ "records after other transition ends");
+
+ e.style = "";
+ });
+
+ // Test that starting a single animation that completes normally
+ // dispatches an added notification and then a removed notification.
+ addAsyncAnimTest("single_animation", aOptions, function*() {
+ // Start an animation.
+ e.style = "animation: anim 100s;";
+
+ // Register for the end of the animation.
+ var animationEnd = await_event(e, "animationend");
+
+ // The animation should cause the creation of a single Animation.
+ var animations = e.getAnimations();
+ is(animations.length, 1, "getAnimations().length after animation start");
+
+ // Wait for the single MutationRecord for the Animation addition to
+ // be delivered.
+ yield waitForFrame();
+ assert_records([{ added: animations, changed: [], removed: [] }],
+ "records after animation start");
+
+ // Advance until near the end of the animation, then wait for it to finish.
+ animations[0].currentTime = 99900;
+ yield animationEnd;
+
+ // After the animation has finished, the Animation should disappear.
+ is(e.getAnimations().length, 0,
+ "getAnimations().length after animation end");
+
+ // Wait for the change MutationRecord from seeking the Animation to
+ // be delivered, followed by a further MutationRecord for the Animation
+ // removal.
+ yield waitForFrame();
+ assert_records([{ added: [], changed: animations, removed: [] },
+ { added: [], changed: [], removed: animations }],
+ "records after animation end");
+
+ e.style = "";
+ });
+
+ // Test that starting a single animation that is cancelled by resetting
+ // the animation-name property dispatches an added notification and
+ // then a removed notification.
+ addAsyncAnimTest("single_animation_cancelled_name", aOptions, function*() {
+ // Start a long animation.
+ e.style = "animation: anim 100s;";
+
+ // The animation should cause the creation of a single Animation.
+ var animations = e.getAnimations();
+ is(animations.length, 1, "getAnimations().length after animation start");
+
+ // Wait for the single MutationRecord for the Animation addition to
+ // be delivered.
+ yield waitForFrame();
+ assert_records([{ added: animations, changed: [], removed: [] }],
+ "records after animation start");
+
+ // Cancel the animation by setting animation-name.
+ e.style.animationName = "none";
+
+ // Wait for the single MutationRecord for the Animation removal to
+ // be delivered.
+ yield waitForFrame();
+ assert_records([{ added: [], changed: [], removed: animations }],
+ "records after animation end");
+
+ e.style = "";
+ });
+
+ // Test that starting a single animation that is cancelled by updating
+ // the animation-duration property dispatches an added notification and
+ // then a removed notification.
+ addAsyncAnimTest("single_animation_cancelled_duration", aOptions, function*() {
+ // Start a long animation.
+ e.style = "animation: anim 100s;";
+
+ // The animation should cause the creation of a single Animation.
+ var animations = e.getAnimations();
+ is(animations.length, 1, "getAnimations().length after animation start");
+
+ // Wait for the single MutationRecord for the Animation addition to
+ // be delivered.
+ yield waitForFrame();
+ assert_records([{ added: animations, changed: [], removed: [] }],
+ "records after animation start");
+
+ // Advance the animation by a second.
+ animations[0].currentTime += 1 * MS_PER_SEC;
+
+ // Cancel the animation by setting animation-duration to a value less
+ // than a second.
+ e.style.animationDuration = "0.1s";
+
+ // Wait for the change MutationRecord from seeking the Animation to
+ // be delivered, followed by a further MutationRecord for the Animation
+ // removal.
+ yield waitForFrame();
+ assert_records([{ added: [], changed: animations, removed: [] },
+ { added: [], changed: [], removed: animations }],
+ "records after animation end");
+
+ e.style = "";
+ });
+
+ // Test that starting a single animation that is cancelled by updating
+ // the animation-delay property dispatches an added notification and
+ // then a removed notification.
+ addAsyncAnimTest("single_animation_cancelled_delay", aOptions, function*() {
+ // Start a long animation.
+ e.style = "animation: anim 100s;";
+
+ // The animation should cause the creation of a single Animation.
+ var animations = e.getAnimations();
+ is(animations.length, 1, "getAnimations().length after animation start");
+
+ // Wait for the single MutationRecord for the Animation addition to
+ // be delivered.
+ yield waitForFrame();
+ assert_records([{ added: animations, changed: [], removed: [] }],
+ "records after animation start");
+
+ // Cancel the animation by setting animation-delay.
+ e.style.animationDelay = "-200s";
+
+ // Wait for the single MutationRecord for the Animation removal to
+ // be delivered.
+ yield waitForFrame();
+ assert_records([{ added: [], changed: [], removed: animations }],
+ "records after animation end");
+
+ e.style = "";
+ });
+
+ // Test that starting a single animation that is cancelled by updating
+ // the animation-fill-mode property dispatches an added notification and
+ // then a removed notification.
+ addAsyncAnimTest("single_animation_cancelled_fill", aOptions, function*() {
+ // Start a short, filled animation.
+ e.style = "animation: anim 100s forwards;";
+
+ // Register for the end of the animation.
+ var animationEnd = await_event(e, "animationend");
+
+ // The animation should cause the creation of a single Animation.
+ var animations = e.getAnimations();
+ is(animations.length, 1, "getAnimations().length after animation start");
+
+ // Wait for the single MutationRecord for the Animation addition to
+ // be delivered.
+ yield waitForFrame();
+ assert_records([{ added: animations, changed: [], removed: [] }],
+ "records after animation start");
+
+ // Advance until near the end of the animation, then wait for it to finish.
+ animations[0].currentTime = 99900;
+ yield animationEnd;
+
+ // The only MutationRecord at this point should be the change from
+ // seeking the Animation.
+ assert_records([{ added: [], changed: animations, removed: [] }],
+ "records after animation starts filling");
+
+ // Cancel the animation by setting animation-fill-mode.
+ e.style.animationFillMode = "none";
+
+ // Wait for the single MutationRecord for the Animation removal to
+ // be delivered.
+ yield waitForFrame();
+ assert_records([{ added: [], changed: [], removed: animations }],
+ "records after animation end");
+
+ e.style = "";
+ });
+
+ // Test that starting a single animation that is cancelled by updating
+ // the animation-iteration-count property dispatches an added notification
+ // and then a removed notification.
+ addAsyncAnimTest("single_animation_cancelled_iteration_count",
+ aOptions, function*() {
+ // Start a short, repeated animation.
+ e.style = "animation: anim 0.5s infinite;";
+
+ // The animation should cause the creation of a single Animation.
+ var animations = e.getAnimations();
+ is(animations.length, 1, "getAnimations().length after animation start");
+
+ // Wait for the single MutationRecord for the Animation addition to
+ // be delivered.
+ yield waitForFrame();
+ assert_records([{ added: animations, changed: [], removed: [] }],
+ "records after animation start");
+
+ // Advance the animation until we are past the first iteration.
+ animations[0].currentTime += 1 * MS_PER_SEC;
+
+ // The only MutationRecord at this point should be the change from
+ // seeking the Animation.
+ yield waitForFrame();
+ assert_records([{ added: [], changed: animations, removed: [] }],
+ "records after seeking animations");
+
+ // Cancel the animation by setting animation-iteration-count.
+ e.style.animationIterationCount = "1";
+
+ // Wait for the single MutationRecord for the Animation removal to
+ // be delivered.
+ yield waitForFrame();
+ assert_records([{ added: [], changed: [], removed: animations }],
+ "records after animation end");
+
+ e.style = "";
+ });
+
+ // Test that updating an animation property dispatches a changed notification.
+ [
+ { name: "duration", prop: "animationDuration", val: "200s" },
+ { name: "timing", prop: "animationTimingFunction", val: "linear" },
+ { name: "iteration", prop: "animationIterationCount", val: "2" },
+ { name: "direction", prop: "animationDirection", val: "reverse" },
+ { name: "state", prop: "animationPlayState", val: "paused" },
+ { name: "delay", prop: "animationDelay", val: "-1s" },
+ { name: "fill", prop: "animationFillMode", val: "both" },
+ ].forEach(function(aChangeTest) {
+ addAsyncAnimTest(`single_animation_change_${aChangeTest.name}`, aOptions, function*() {
+ // Start a long animation.
+ e.style = "animation: anim 100s;";
+
+ // The animation should cause the creation of a single Animation.
+ var animations = e.getAnimations();
+ is(animations.length, 1, "getAnimations().length after animation start");
+
+ // Wait for the single MutationRecord for the Animation addition to
+ // be delivered.
+ yield waitForFrame();
+ assert_records([{ added: animations, changed: [], removed: [] }],
+ "records after animation start");
+
+ // Change a property of the animation such that it keeps running.
+ e.style[aChangeTest.prop] = aChangeTest.val;
+
+ // Wait for the single MutationRecord for the Animation change to
+ // be delivered.
+ yield waitForFrame();
+ assert_records([{ added: [], changed: animations, removed: [] }],
+ "records after animation change");
+
+ // Cancel the animation.
+ e.style.animationName = "none";
+
+ // Wait for the addition, change and removal MutationRecords to be delivered.
+ yield waitForFrame();
+ assert_records([{ added: [], changed: [], removed: animations }],
+ "records after animation end");
+
+ e.style = "";
+ });
+ });
+
+ // Test that calling finish() on a paused (but otherwise finished) animation
+ // dispatches a changed notification.
+ addAsyncAnimTest("finish_from_pause", aOptions, function*() {
+ // Start a long animation
+ e.style = "animation: anim 100s forwards";
+
+ // The animation should cause the creation of a single Animation.
+ var animations = e.getAnimations();
+ is(animations.length, 1, "getAnimations().length after animation start");
+
+ // Wait for the single MutationRecord for the Animation addition to
+ // be delivered.
+ yield waitForFrame();
+ assert_records([{ added: animations, changed: [], removed: [] }],
+ "records after animation start");
+
+ // Wait until the animation is playing.
+ yield animations[0].ready;
+
+ // Finish and pause.
+ animations[0].finish();
+ animations[0].pause();
+
+ // Wait for the pause to complete.
+ yield animations[0].ready;
+ is(animations[0].playState, "paused",
+ "playState after finishing and pausing");
+
+ // We should have two MutationRecords for the Animation changes:
+ // one for the finish, one for the pause.
+ assert_records([{ added: [], changed: animations, removed: [] },
+ { added: [], changed: animations, removed: [] }],
+ "records after finish() and pause()");
+
+ // Call finish() again.
+ animations[0].finish();
+ is(animations[0].playState, "finished",
+ "playState after finishing from paused state");
+
+ // Wait for the single MutationRecord for the Animation change to
+ // be delivered. Even though the currentTime does not change, the
+ // playState will change.
+ yield waitForFrame();
+ assert_records([{ added: [], changed: animations, removed: [] }],
+ "records after finish() and pause()");
+
+ // Cancel the animation.
+ e.style = "";
+
+ // Wait for the single removal notification.
+ yield waitForFrame();
+ assert_records([{ added: [], changed: [], removed: animations }],
+ "records after animation end");
+ });
+
+ // Test that calling finish() on a pause-pending (but otherwise finished)
+ // animation dispatches a changed notification.
+ addAsyncAnimTest("finish_from_pause_pending", aOptions, function*() {
+ // Start a long animation
+ e.style = "animation: anim 100s forwards";
+
+ // The animation should cause the creation of a single Animation.
+ var animations = e.getAnimations();
+ is(animations.length, 1, "getAnimations().length after animation start");
+
+ // Wait for the single MutationRecord for the Animation addition to
+ // be delivered.
+ yield waitForFrame();
+ assert_records([{ added: animations, changed: [], removed: [] }],
+ "records after animation start");
+
+ // Wait until the animation is playing.
+ yield animations[0].ready;
+
+ // Finish and pause.
+ animations[0].finish();
+ animations[0].pause();
+ is(animations[0].playState, "pending",
+ "playState after finishing and calling pause()");
+
+ // Call finish() again to abort the pause
+ animations[0].finish();
+ is(animations[0].playState, "finished",
+ "playState after finishing and calling pause()");
+
+ // Wait for three MutationRecords for the Animation changes to
+ // be delivered: one for each finish(), pause(), finish() operation.
+ yield waitForFrame();
+ assert_records([{ added: [], changed: animations, removed: [] },
+ { added: [], changed: animations, removed: [] },
+ { added: [], changed: animations, removed: [] }],
+ "records after finish(), pause(), finish()");
+
+ // Cancel the animation.
+ e.style = "";
+
+ // Wait for the single removal notification.
+ yield waitForFrame();
+ assert_records([{ added: [], changed: [], removed: animations }],
+ "records after animation end");
+ });
+
+ // Test that calling play() on a paused Animation dispatches a changed
+ // notification.
+ addAsyncAnimTest("play", aOptions, function*() {
+ // Start a long, paused animation
+ e.style = "animation: anim 100s paused";
+
+ // The animation should cause the creation of a single Animation.
+ var animations = e.getAnimations();
+ is(animations.length, 1, "getAnimations().length after animation start");
+
+ // Wait for the single MutationRecord for the Animation addition to
+ // be delivered.
+ yield waitForFrame();
+ assert_records([{ added: animations, changed: [], removed: [] }],
+ "records after animation start");
+
+ // Wait until the animation is ready
+ yield animations[0].ready;
+
+ // Play
+ animations[0].play();
+
+ // Wait for the single MutationRecord for the Animation change to
+ // be delivered.
+ yield animations[0].ready;
+ assert_records([{ added: [], changed: animations, removed: [] }],
+ "records after play()");
+
+ // Redundant play
+ animations[0].play();
+
+ // Wait to ensure no change is dispatched
+ yield waitForFrame();
+ assert_records([], "records after redundant play()");
+
+ // Cancel the animation.
+ e.style = "";
+
+ // Wait for the single removal notification.
+ yield waitForFrame();
+ assert_records([{ added: [], changed: [], removed: animations }],
+ "records after animation end");
+ });
+
+ // Test that calling play() on a finished Animation that fills forwards
+ // dispatches a changed notification.
+ addAsyncAnimTest("play_filling_forwards", aOptions, function*() {
+ // Start a long animation with a forwards fill
+ e.style = "animation: anim 100s forwards";
+
+ // The animation should cause the creation of a single Animation.
+ var animations = e.getAnimations();
+ is(animations.length, 1, "getAnimations().length after animation start");
+
+ // Wait for the single MutationRecord for the Animation addition to
+ // be delivered.
+ yield waitForFrame();
+ assert_records([{ added: animations, changed: [], removed: [] }],
+ "records after animation start");
+
+ // Wait until the animation is ready
+ yield animations[0].ready;
+
+ // Seek to the end
+ animations[0].finish();
+
+ // Wait for the single MutationRecord for the Animation change to
+ // be delivered.
+ yield waitForFrame();
+ assert_records([{ added: [], changed: animations, removed: [] }],
+ "records after finish()");
+
+ // Since we are filling forwards, calling play() should produce a
+ // change record since the animation remains relevant.
+ animations[0].play();
+
+ // Wait for the single MutationRecord for the Animation change to
+ // be delivered.
+ yield waitForFrame();
+ assert_records([{ added: [], changed: animations, removed: [] }],
+ "records after play()");
+
+ // Cancel the animation.
+ e.style = "";
+
+ // Wait for the single removal notification.
+ yield waitForFrame();
+ assert_records([{ added: [], changed: [], removed: animations }],
+ "records after animation end");
+ });
+
+ // Test that calling play() on a finished Animation that does *not* fill
+ // forwards dispatches an addition notification.
+ addAsyncAnimTest("play_after_finish", aOptions, function*() {
+ // Start a long animation
+ e.style = "animation: anim 100s";
+
+ // The animation should cause the creation of a single Animation.
+ var animations = e.getAnimations();
+ is(animations.length, 1, "getAnimations().length after animation start");
+
+ // Wait for the single MutationRecord for the Animation addition to
+ // be delivered.
+ yield waitForFrame();
+ assert_records([{ added: animations, changed: [], removed: [] }],
+ "records after animation start");
+
+ // Wait until the animation is ready
+ yield animations[0].ready;
+
+ // Seek to the end
+ animations[0].finish();
+
+ // Wait for the single MutationRecord for the Animation removal to
+ // be delivered.
+ yield waitForFrame();
+ assert_records([{ added: [], changed: [], removed: animations }],
+ "records after finish()");
+
+ // Since we are *not* filling forwards, calling play() is equivalent
+ // to creating a new animation since it becomes relevant again.
+ animations[0].play();
+
+ // Wait for the single MutationRecord for the Animation addition to
+ // be delivered.
+ yield waitForFrame();
+ assert_records([{ added: animations, changed: [], removed: [] }],
+ "records after play()");
+
+ // Cancel the animation.
+ e.style = "";
+
+ // Wait for the single removal notification.
+ yield waitForFrame();
+ assert_records([{ added: [], changed: [], removed: animations }],
+ "records after animation end");
+ });
+
+ // Test that calling pause() on an Animation dispatches a changed
+ // notification.
+ addAsyncAnimTest("pause", aOptions, function*() {
+ // Start a long animation
+ e.style = "animation: anim 100s";
+
+ // The animation should cause the creation of a single Animation.
+ var animations = e.getAnimations();
+ is(animations.length, 1, "getAnimations().length after animation start");
+
+ // Wait for the single MutationRecord for the Animation addition to
+ // be delivered.
+ yield waitForFrame();
+ assert_records([{ added: animations, changed: [], removed: [] }],
+ "records after animation start");
+
+ // Wait until the animation is ready
+ yield animations[0].ready;
+
+ // Pause
+ animations[0].pause();
+
+ // Wait for the single MutationRecord for the Animation change to
+ // be delivered.
+ yield animations[0].ready;
+ assert_records([{ added: [], changed: animations, removed: [] }],
+ "records after pause()");
+
+ // Redundant pause
+ animations[0].pause();
+
+ // Wait to ensure no change is dispatched
+ yield animations[0].ready;
+ assert_records([], "records after redundant pause()");
+
+ // Cancel the animation.
+ e.style = "";
+
+ // Wait for the single removal notification.
+ yield waitForFrame();
+ assert_records([{ added: [], changed: [], removed: animations }],
+ "records after animation end");
+ });
+
+ // Test that calling pause() on an Animation that is pause-pending
+ // does not dispatch an additional changed notification.
+ addAsyncAnimTest("pause_while_pause_pending", aOptions, function*() {
+ // Start a long animation
+ e.style = "animation: anim 100s";
+
+ // The animation should cause the creation of a single Animation.
+ var animations = e.getAnimations();
+ is(animations.length, 1, "getAnimations().length after animation start");
+
+ // Wait for the single MutationRecord for the Animation addition to
+ // be delivered.
+ yield waitForFrame();
+ assert_records([{ added: animations, changed: [], removed: [] }],
+ "records after animation start");
+
+ // Wait until the animation is ready
+ yield animations[0].ready;
+
+ // Pause
+ animations[0].pause();
+
+ // We are now pause-pending, but pause again
+ animations[0].pause();
+
+ // We should only get a single change record
+ yield animations[0].ready;
+ assert_records([{ added: [], changed: animations, removed: [] }],
+ "records after pause()");
+
+ // Cancel the animation.
+ e.style = "";
+
+ // Wait for the single removal notification.
+ yield waitForFrame();
+ assert_records([{ added: [], changed: [], removed: animations }],
+ "records after animation end");
+ });
+
+ // Test that calling play() on an Animation that is pause-pending
+ // dispatches a changed notification.
+ addAsyncAnimTest("aborted_pause", aOptions, function*() {
+ // Start a long animation
+ e.style = "animation: anim 100s";
+
+ // The animation should cause the creation of a single Animation.
+ var animations = e.getAnimations();
+ is(animations.length, 1, "getAnimations().length after animation start");
+
+ // Wait for the single MutationRecord for the Animation addition to
+ // be delivered.
+ yield waitForFrame();
+ assert_records([{ added: animations, changed: [], removed: [] }],
+ "records after animation start");
+
+ // Wait until the animation is ready
+ yield animations[0].ready;
+
+ // Pause
+ animations[0].pause();
+
+ // We are now pause-pending. If we play() now, we will abort the pause
+ animations[0].play();
+
+ // We should get two change records
+ yield animations[0].ready;
+ assert_records([{ added: [], changed: animations, removed: [] },
+ { added: [], changed: animations, removed: [] }],
+ "records after aborting a pause()");
+
+ // Cancel the animation.
+ e.style = "";
+
+ // Wait for the single removal notification.
+ yield waitForFrame();
+ assert_records([{ added: [], changed: [], removed: animations }],
+ "records after animation end");
+ });
+
+ // Test that a non-cancelling change to an animation followed immediately by a
+ // cancelling change will only send an animation removal notification.
+ addAsyncAnimTest("coalesce_change_cancel", aOptions, function*() {
+ // Start a long animation.
+ e.style = "animation: anim 100s;";
+
+ // The animation should cause the creation of a single Animation.
+ var animations = e.getAnimations();
+ is(animations.length, 1, "getAnimations().length after animation start");
+
+ // Wait for the single MutationRecord for the Animation addition to
+ // be delivered.
+ yield waitForFrame();
+ assert_records([{ added: animations, changed: [], removed: [] }],
+ "records after animation start");
+
+ // Update the animation's delay such that it is still running.
+ e.style.animationDelay = "-1s";
+
+ // Then cancel the animation by updating its duration.
+ e.style.animationDuration = "0.5s";
+
+ // We should get a single removal notification.
+ yield waitForFrame();
+ assert_records([{ added: [], changed: [], removed: animations }],
+ "records after animation end");
+
+ e.style = "";
+ });
+
+});
+
+addAsyncAnimTest("tree_ordering", { observe: div, subtree: true }, function*() {
+ // Add style for pseudo elements
+ var extraStyle = document.createElement('style');
+ document.head.appendChild(extraStyle);
+ var sheet = extraStyle.sheet;
+ var rules = { ".before::before": "animation: anim 100s;",
+ ".after::after" : "animation: anim 100s, anim 100s;" };
+ for (var selector in rules) {
+ sheet.insertRule(selector + '{' + rules[selector] + '}',
+ sheet.cssRules.length);
+ }
+
+ // Create a tree with two children:
+ //
+ // div
+ // (::before)
+ // (::after)
+ // / \
+ // childA childB(::before)
+ var childA = document.createElement("div");
+ var childB = document.createElement("div");
+
+ div.appendChild(childA);
+ div.appendChild(childB);
+
+ // Start an animation on each (using order: childB, div, childA)
+ //
+ // We include multiple animations on some nodes so that we can test batching
+ // works as expected later in this test.
+ childB.style = "animation: anim 100s";
+ div.style = "animation: anim 100s, anim 100s, anim 100s";
+ childA.style = "animation: anim 100s, anim 100s";
+
+ // Start animations targeting to pseudo element of div and childB.
+ childB.classList.add("before");
+ div.classList.add("after");
+ div.classList.add("before");
+
+ // Check all animations we have in this document
+ var docAnims = document.getAnimations();
+ is(docAnims.length, 10, "total animations");
+
+ var divAnimations = div.getAnimations();
+ var childAAnimations = childA.getAnimations();
+ var childBAnimations = childB.getAnimations();
+ var divBeforeAnimations =
+ [ for (x of docAnims) if (x.effect.target.parentElement == div &&
+ x.effect.target.type == "::before") x ];
+ var divAfterAnimations =
+ [ for (x of docAnims) if (x.effect.target.parentElement == div &&
+ x.effect.target.type == "::after") x ];
+ var childBPseudoAnimations =
+ [ for (x of docAnims) if (x.effect.target.parentElement == childB) x ];
+
+ // The order in which we get the corresponding records is currently
+ // based on the order we visit these nodes when updating styles.
+ //
+ // That is because we don't do any document-level batching of animation
+ // mutation records when we flush styles. We may introduce that in the
+ // future but for now all we are interested in testing here is that the order
+ // these records are dispatched is consistent between runs.
+ //
+ // We currently expect to get records in order div::after, childA, childB,
+ // childB::before, div, div::before
+ yield waitForFrame();
+ assert_records([{ added: divAfterAnimations, changed: [], removed: [] },
+ { added: childAAnimations, changed: [], removed: [] },
+ { added: childBAnimations, changed: [], removed: [] },
+ { added: childBPseudoAnimations, changed: [], removed: [] },
+ { added: divAnimations, changed: [], removed: [] },
+ { added: divBeforeAnimations, changed: [], removed: [] }],
+ "records after simultaneous animation start");
+
+ // The one case where we *do* currently perform document-level (or actually
+ // timeline-level) batching is when animations are updated from a refresh
+ // driver tick. In particular, this means that when animations finish
+ // naturally the removed records should be dispatched according to the
+ // position of the elements in the tree.
+
+ // First, flatten the set of animations. we put the animations targeting to
+ // pseudo elements last. (Actually, we don't care the order in the list.)
+ var animations = [ ...divAnimations,
+ ...childAAnimations,
+ ...childBAnimations,
+ ...divBeforeAnimations,
+ ...divAfterAnimations,
+ ...childBPseudoAnimations ];
+
+ // Fast-forward to *just* before the end of the animation.
+ animations.forEach(animation => animation.currentTime = 99999);
+
+ // Prepare the set of expected change MutationRecords, one for each
+ // animation that was seeked.
+ var seekRecords = animations.map(
+ p => ({ added: [], changed: [p], removed: [] })
+ );
+
+ yield await_event(div, "animationend");
+
+ // After the changed notifications, which will be dispatched in the order that
+ // the animations were seeked, we should get removal MutationRecords in order
+ // (div, div::before, div::after), childA, (childB, childB::before).
+ // Note: The animations targeting to the pseudo element are appended after
+ // the animations of its parent element.
+ divAnimations = [ ...divAnimations,
+ ...divBeforeAnimations,
+ ...divAfterAnimations ];
+ childBAnimations = [ ...childBAnimations, ...childBPseudoAnimations ];
+ assert_records(seekRecords.concat(
+ { added: [], changed: [], removed: divAnimations },
+ { added: [], changed: [], removed: childAAnimations },
+ { added: [], changed: [], removed: childBAnimations }),
+ "records after finishing");
+
+ // Clean up
+ div.classList.remove("before");
+ div.classList.remove("after");
+ div.style = "";
+ childA.remove();
+ childB.remove();
+ extraStyle.remove();
+});
+
+// Run the tests.
+SimpleTest.requestLongerTimeout(2);
+SimpleTest.waitForExplicitFinish();
+
+runAllAsyncTests().then(function() {
+ SimpleTest.finish();
+}, function(aError) {
+ ok(false, "Something failed: " + aError);
+});
+</script>
diff --git a/dom/animation/test/chrome/test_animation_performance_warning.html b/dom/animation/test/chrome/test_animation_performance_warning.html
new file mode 100644
index 0000000000..a3bd63efcd
--- /dev/null
+++ b/dom/animation/test/chrome/test_animation_performance_warning.html
@@ -0,0 +1,957 @@
+<!doctype html>
+<head>
+<meta charset=utf-8>
+<title>Bug 1196114 - Test metadata related to which animation properties
+ are running on the compositor</title>
+<script type="application/javascript" src="../testharness.js"></script>
+<script type="application/javascript" src="../testharnessreport.js"></script>
+<script type="application/javascript" src="../testcommon.js"></script>
+<style>
+.compositable {
+ /* Element needs geometry to be eligible for layerization */
+ width: 100px;
+ height: 100px;
+ background-color: white;
+}
+@keyframes fade {
+ from { opacity: 1 }
+ to { opacity: 0 }
+}
+</style>
+</head>
+<body>
+<a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1196114"
+ target="_blank">Mozilla Bug 1196114</a>
+<div id="log"></div>
+<script>
+'use strict';
+
+// This is used for obtaining localized strings.
+var gStringBundle;
+
+W3CTest.runner.requestLongerTimeout(2);
+
+SpecialPowers.pushPrefEnv({ "set": [
+ ["general.useragent.locale", "en-US"],
+ // Need to set devPixelsPerPx explicitly to gain
+ // consistent pixel values in warning messages
+ // regardless of platform DPIs.
+ ["layout.css.devPixelsPerPx", 1],
+ ] },
+ start);
+
+function compare_property_state(a, b) {
+ if (a.property > b.property) {
+ return -1;
+ } else if (a.property < b.property) {
+ return 1;
+ }
+ if (a.runningOnCompositor != b.runningOnCompositor) {
+ return a.runningOnCompositor ? 1 : -1;
+ }
+ return a.warning > b.warning ? -1 : 1;
+}
+
+function assert_animation_property_state_equals(actual, expected) {
+ assert_equals(actual.length, expected.length, 'Number of properties');
+
+ var sortedActual = actual.sort(compare_property_state);
+ var sortedExpected = expected.sort(compare_property_state);
+
+ for (var i = 0; i < sortedActual.length; i++) {
+ assert_equals(sortedActual[i].property,
+ sortedExpected[i].property,
+ 'CSS property name should match');
+ assert_equals(sortedActual[i].runningOnCompositor,
+ sortedExpected[i].runningOnCompositor,
+ 'runningOnCompositor property should match');
+ if (sortedExpected[i].warning instanceof RegExp) {
+ assert_regexp_match(sortedActual[i].warning,
+ sortedExpected[i].warning,
+ 'warning message should match');
+ } else if (sortedExpected[i].warning) {
+ assert_equals(sortedActual[i].warning,
+ gStringBundle.GetStringFromName(sortedExpected[i].warning),
+ 'warning message should match');
+ }
+ }
+}
+
+// Check that the animation is running on compositor and
+// warning property is not set for the CSS property regardless
+// expected values.
+function assert_property_state_on_compositor(actual, expected) {
+ assert_equals(actual.length, expected.length);
+
+ var sortedActual = actual.sort(compare_property_state);
+ var sortedExpected = expected.sort(compare_property_state);
+
+ for (var i = 0; i < sortedActual.length; i++) {
+ assert_equals(sortedActual[i].property,
+ sortedExpected[i].property,
+ 'CSS property name should match');
+ assert_true(sortedActual[i].runningOnCompositor,
+ 'runningOnCompositor property should be true on ' +
+ sortedActual[i].property);
+ assert_not_exists(sortedActual[i], 'warning',
+ 'warning property should not be set');
+ }
+}
+
+var gAnimationsTests = [
+ {
+ desc: 'animations on compositor',
+ frames: {
+ opacity: [0, 1]
+ },
+ expected: [
+ {
+ property: 'opacity',
+ runningOnCompositor: true
+ }
+ ]
+ },
+ {
+ desc: 'animations on main thread',
+ frames: {
+ backgroundColor: ['white', 'red']
+ },
+ expected: [
+ {
+ property: 'background-color',
+ runningOnCompositor: false
+ }
+ ]
+ },
+ {
+ desc: 'animations on both threads',
+ frames: {
+ backgroundColor: ['white', 'red'],
+ transform: ['translate(0px)', 'translate(100px)']
+ },
+ expected: [
+ {
+ property: 'background-color',
+ runningOnCompositor: false
+ },
+ {
+ property: 'transform',
+ runningOnCompositor: true
+ }
+ ]
+ },
+ {
+ desc: 'two animation properties on compositor thread',
+ frames: {
+ opacity: [0, 1],
+ transform: ['translate(0px)', 'translate(100px)']
+ },
+ expected: [
+ {
+ property: 'opacity',
+ runningOnCompositor: true
+ },
+ {
+ property: 'transform',
+ runningOnCompositor: true
+ }
+ ]
+ },
+ {
+ desc: 'opacity on compositor with animation of geometric properties',
+ frames: {
+ width: ['100px', '200px'],
+ opacity: [0, 1]
+ },
+ expected: [
+ {
+ property: 'width',
+ runningOnCompositor: false
+ },
+ {
+ property: 'opacity',
+ runningOnCompositor: true
+ }
+ ]
+ },
+];
+
+// Test cases that check results of adding/removing a 'width' property on the
+// same animation object.
+var gAnimationWithGeometricKeyframeTests = [
+ {
+ desc: 'transform',
+ frames: {
+ transform: ['translate(0px)', 'translate(100px)']
+ },
+ expected: {
+ withoutGeometric: [
+ {
+ property: 'transform',
+ runningOnCompositor: true
+ }
+ ],
+ withGeometric: [
+ {
+ property: 'width',
+ runningOnCompositor: false
+ },
+ {
+ property: 'transform',
+ runningOnCompositor: false,
+ warning: 'CompositorAnimationWarningTransformWithGeometricProperties'
+ }
+ ]
+ }
+ },
+ {
+ desc: 'opacity and transform',
+ frames: {
+ opacity: [0, 1],
+ transform: ['translate(0px)', 'translate(100px)']
+ },
+ expected: {
+ withoutGeometric: [
+ {
+ property: 'opacity',
+ runningOnCompositor: true
+ },
+ {
+ property: 'transform',
+ runningOnCompositor: true
+ }
+ ],
+ withGeometric: [
+ {
+ property: 'width',
+ runningOnCompositor: false
+ },
+ {
+ property: 'opacity',
+ runningOnCompositor: true
+ },
+ {
+ property: 'transform',
+ runningOnCompositor: false,
+ warning: 'CompositorAnimationWarningTransformWithGeometricProperties'
+ }
+ ]
+ }
+ },
+];
+
+// Performance warning tests that set and clear a style property.
+var gPerformanceWarningTestsStyle = [
+ {
+ desc: 'preserve-3d transform',
+ frames: {
+ transform: ['translate(0px)', 'translate(100px)']
+ },
+ style: 'transform-style: preserve-3d',
+ expected: [
+ {
+ property: 'transform',
+ runningOnCompositor: false,
+ warning: 'CompositorAnimationWarningTransformPreserve3D'
+ }
+ ]
+ },
+ {
+ desc: 'transform with backface-visibility:hidden',
+ frames: {
+ transform: ['translate(0px)', 'translate(100px)']
+ },
+ style: 'backface-visibility: hidden;',
+ expected: [
+ {
+ property: 'transform',
+ runningOnCompositor: false,
+ warning: 'CompositorAnimationWarningTransformBackfaceVisibilityHidden'
+ }
+ ]
+ },
+ {
+ desc: 'opacity and transform with preserve-3d',
+ frames: {
+ opacity: [0, 1],
+ transform: ['translate(0px)', 'translate(100px)']
+ },
+ style: 'transform-style: preserve-3d',
+ expected: [
+ {
+ property: 'opacity',
+ runningOnCompositor: true
+ },
+ {
+ property: 'transform',
+ runningOnCompositor: false,
+ warning: 'CompositorAnimationWarningTransformPreserve3D'
+ }
+ ]
+ },
+ {
+ desc: 'opacity and transform with backface-visibility:hidden',
+ frames: {
+ opacity: [0, 1],
+ transform: ['translate(0px)', 'translate(100px)']
+ },
+ style: 'backface-visibility: hidden;',
+ expected: [
+ {
+ property: 'opacity',
+ runningOnCompositor: true
+ },
+ {
+ property: 'transform',
+ runningOnCompositor: false,
+ warning: 'CompositorAnimationWarningTransformBackfaceVisibilityHidden'
+ }
+ ]
+ },
+];
+
+// Performance warning tests that set and clear the id property
+var gPerformanceWarningTestsId= [
+ {
+ desc: 'moz-element referencing a transform',
+ frames: {
+ transform: ['translate(0px)', 'translate(100px)']
+ },
+ id: 'transformed',
+ createelement: 'width:100px; height:100px; background: -moz-element(#transformed)',
+ expected: [
+ {
+ property: 'transform',
+ runningOnCompositor: false,
+ warning: 'CompositorAnimationWarningHasRenderingObserver'
+ }
+ ]
+ },
+];
+
+var gMultipleAsyncAnimationsTests = [
+ {
+ desc: 'opacity and transform with preserve-3d',
+ style: 'transform-style: preserve-3d',
+ animations: [
+ {
+ frames: {
+ transform: ['translate(0px)', 'translate(100px)']
+ },
+ expected: [
+ {
+ property: 'transform',
+ runningOnCompositor: false,
+ warning: 'CompositorAnimationWarningTransformPreserve3D'
+ }
+ ]
+ },
+ {
+ frames: {
+ opacity: [0, 1]
+ },
+ expected: [
+ {
+ property: 'opacity',
+ runningOnCompositor: true,
+ }
+ ]
+ }
+ ],
+ },
+ {
+ desc: 'opacity and transform with backface-visibility:hidden',
+ style: 'backface-visibility: hidden;',
+ animations: [
+ {
+ frames: {
+ transform: ['translate(0px)', 'translate(100px)']
+ },
+ expected: [
+ {
+ property: 'transform',
+ runningOnCompositor: false,
+ warning: 'CompositorAnimationWarningTransformBackfaceVisibilityHidden'
+ }
+ ]
+ },
+ {
+ frames: {
+ opacity: [0, 1]
+ },
+ expected: [
+ {
+ property: 'opacity',
+ runningOnCompositor: true,
+ }
+ ]
+ }
+ ],
+ },
+];
+
+// Test cases that check results of adding/removing a 'width' keyframe on the
+// same animation object, where multiple animation objects belong to the same
+// element.
+// The 'width' property is added to animations[1].
+var gMultipleAsyncAnimationsWithGeometricKeyframeTests = [
+ {
+ desc: 'transform and opacity with geometric keyframes',
+ animations: [
+ {
+ frames: {
+ transform: ['translate(0px)', 'translate(100px)']
+ },
+ expected: {
+ withoutGeometric: [
+ {
+ property: 'transform',
+ runningOnCompositor: true
+ }
+ ],
+ withGeometric: [
+ {
+ property: 'transform',
+ runningOnCompositor: false,
+ warning: 'CompositorAnimationWarningTransformWithGeometricProperties'
+ }
+ ]
+ }
+ },
+ {
+ frames: {
+ opacity: [0, 1]
+ },
+ expected: {
+ withoutGeometric: [
+ {
+ property: 'opacity',
+ runningOnCompositor: true,
+ }
+ ],
+ withGeometric: [
+ {
+ property: 'width',
+ runningOnCompositor: false,
+ },
+ {
+ property: 'opacity',
+ runningOnCompositor: true,
+ }
+ ]
+ }
+ }
+ ],
+ },
+ {
+ desc: 'opacity and transform with geometric keyframes',
+ animations: [
+ {
+ frames: {
+ opacity: [0, 1]
+ },
+ expected: {
+ withoutGeometric: [
+ {
+ property: 'opacity',
+ runningOnCompositor: true,
+ }
+ ],
+ withGeometric: [
+ {
+ property: 'opacity',
+ runningOnCompositor: true,
+ }
+ ]
+ }
+ },
+ {
+ frames: {
+ transform: ['translate(0px)', 'translate(100px)']
+ },
+ expected: {
+ withoutGeometric: [
+ {
+ property: 'transform',
+ runningOnCompositor: true
+ }
+ ],
+ withGeometric: [
+ {
+ property: 'width',
+ runningOnCompositor: false,
+ },
+ {
+ property: 'transform',
+ runningOnCompositor: false,
+ warning: 'CompositorAnimationWarningTransformWithGeometricProperties'
+ }
+ ]
+ }
+ }
+ ]
+ },
+];
+
+// Test cases that check results of adding/removing 'width' animation on the
+// same element which has async animations.
+var gMultipleAsyncAnimationsWithGeometricAnimationTests = [
+ {
+ desc: 'transform',
+ animations: [
+ {
+ frames: {
+ transform: ['translate(0px)', 'translate(100px)']
+ },
+ expected: [
+ {
+ property: 'transform',
+ runningOnCompositor: false,
+ warning: 'CompositorAnimationWarningTransformWithGeometricProperties'
+ }
+ ]
+ },
+ ]
+ },
+ {
+ desc: 'opacity',
+ animations: [
+ {
+ frames: {
+ opacity: [0, 1]
+ },
+ expected: [
+ {
+ property: 'opacity',
+ runningOnCompositor: true
+ }
+ ]
+ },
+ ]
+ },
+ {
+ desc: 'opacity and transform',
+ animations: [
+ {
+ frames: {
+ transform: ['translate(0px)', 'translate(100px)']
+ },
+ expected: [
+ {
+ property: 'transform',
+ runningOnCompositor: false,
+ warning: 'CompositorAnimationWarningTransformWithGeometricProperties'
+ }
+ ]
+ },
+ {
+ frames: {
+ opacity: [0, 1]
+ },
+ expected: [
+ {
+ property: 'opacity',
+ runningOnCompositor: true,
+ }
+ ]
+ }
+ ],
+ },
+];
+
+var gAnimationsOnTooSmallElementTests = [
+ {
+ desc: 'opacity on too small element',
+ frames: {
+ opacity: [0, 1]
+ },
+ style: { style: 'width: 8px; height: 8px; background-color: red;' +
+ // We need to set transform here to try creating an
+ // individual frame for this opacity element.
+ // Without this, this small element is created on the same
+ // nsIFrame of mochitest iframe, i.e. the document which are
+ // running this test, as a result the layer corresponding
+ // to the frame is sent to compositor.
+ 'transform: translateX(100px);' },
+ expected: [
+ {
+ property: 'opacity',
+ runningOnCompositor: false,
+ warning: /Animation cannot be run on the compositor because frame size \(8, 8\) is smaller than \(16, 16\)/
+ }
+ ]
+ },
+ {
+ desc: 'transform on too small element',
+ frames: {
+ transform: ['translate(0px)', 'translate(100px)']
+ },
+ style: { style: 'width: 8px; height: 8px; background-color: red;' },
+ expected: [
+ {
+ property: 'transform',
+ runningOnCompositor: false,
+ warning: /Animation cannot be run on the compositor because frame size \(8, 8\) is smaller than \(16, 16\)/
+ }
+ ]
+ },
+];
+
+function start() {
+ var bundleService = SpecialPowers.Cc['@mozilla.org/intl/stringbundle;1']
+ .getService(SpecialPowers.Ci.nsIStringBundleService);
+ gStringBundle = bundleService
+ .createBundle("chrome://global/locale/layout_errors.properties");
+
+ gAnimationsTests.forEach(function(subtest) {
+ promise_test(function(t) {
+ var animation = addDivAndAnimate(t,
+ { class: 'compositable' },
+ subtest.frames, 100 * MS_PER_SEC);
+ return animation.ready.then(function() {
+ assert_animation_property_state_equals(
+ animation.effect.getProperties(),
+ subtest.expected);
+ });
+ }, subtest.desc);
+ });
+
+ gAnimationWithGeometricKeyframeTests.forEach(function(subtest) {
+ promise_test(function(t) {
+ var animation = addDivAndAnimate(t,
+ { class: 'compositable' },
+ subtest.frames, 100 * MS_PER_SEC);
+ return animation.ready.then(function() {
+ // First, a transform animation is running on compositor.
+ assert_animation_property_state_equals(
+ animation.effect.getProperties(),
+ subtest.expected.withoutGeometric);
+ }).then(function() {
+ // Add a 'width' property.
+ var keyframes = animation.effect.getKeyframes();
+
+ keyframes[0].width = '100px';
+ keyframes[1].width = '200px';
+
+ animation.effect.setKeyframes(keyframes);
+ return waitForFrame();
+ }).then(function() {
+ // Now the transform animation is not running on compositor because of
+ // the 'width' property.
+ assert_animation_property_state_equals(
+ animation.effect.getProperties(),
+ subtest.expected.withGeometric);
+ }).then(function() {
+ // Remove the 'width' property.
+ var keyframes = animation.effect.getKeyframes();
+
+ delete keyframes[0].width;
+ delete keyframes[1].width;
+
+ animation.effect.setKeyframes(keyframes);
+ return waitForFrame();
+ }).then(function() {
+ // Finally, the transform animation is running on compositor.
+ assert_animation_property_state_equals(
+ animation.effect.getProperties(),
+ subtest.expected.withoutGeometric);
+ });
+ }, 'An animation has: ' + subtest.desc);
+ });
+
+ gPerformanceWarningTestsStyle.forEach(function(subtest) {
+ promise_test(function(t) {
+ var animation = addDivAndAnimate(t,
+ { class: 'compositable' },
+ subtest.frames, 100 * MS_PER_SEC);
+ return animation.ready.then(function() {
+ assert_property_state_on_compositor(
+ animation.effect.getProperties(),
+ subtest.expected);
+ animation.effect.target.style = subtest.style;
+ return waitForFrame();
+ }).then(function() {
+ assert_animation_property_state_equals(
+ animation.effect.getProperties(),
+ subtest.expected);
+ animation.effect.target.style = '';
+ return waitForFrame();
+ }).then(function() {
+ assert_property_state_on_compositor(
+ animation.effect.getProperties(),
+ subtest.expected);
+ });
+ }, subtest.desc);
+ });
+
+ gPerformanceWarningTestsId.forEach(function(subtest) {
+ promise_test(function(t) {
+ if (subtest.createelement) {
+ addDiv(t, { style: subtest.createelement });
+ }
+
+ var animation = addDivAndAnimate(t,
+ { class: 'compositable' },
+ subtest.frames, 100 * MS_PER_SEC);
+ return animation.ready.then(function() {
+ assert_property_state_on_compositor(
+ animation.effect.getProperties(),
+ subtest.expected);
+ animation.effect.target.id = subtest.id;
+ return waitForFrame();
+ }).then(function() {
+ assert_animation_property_state_equals(
+ animation.effect.getProperties(),
+ subtest.expected);
+ animation.effect.target.id = '';
+ return waitForFrame();
+ }).then(function() {
+ assert_property_state_on_compositor(
+ animation.effect.getProperties(),
+ subtest.expected);
+ });
+ }, subtest.desc);
+ });
+
+ gMultipleAsyncAnimationsTests.forEach(function(subtest) {
+ promise_test(function(t) {
+ var div = addDiv(t, { class: 'compositable' });
+ var animations = subtest.animations.map(function(anim) {
+ var animation = div.animate(anim.frames, 100 * MS_PER_SEC);
+
+ // Bind expected values to animation object.
+ animation.expected = anim.expected;
+ return animation;
+ });
+ return waitForAllAnimations(animations).then(function() {
+ animations.forEach(function(anim) {
+ assert_property_state_on_compositor(
+ anim.effect.getProperties(),
+ anim.expected);
+ });
+ div.style = subtest.style;
+ return waitForFrame();
+ }).then(function() {
+ animations.forEach(function(anim) {
+ assert_animation_property_state_equals(
+ anim.effect.getProperties(),
+ anim.expected);
+ });
+ div.style = '';
+ return waitForFrame();
+ }).then(function() {
+ animations.forEach(function(anim) {
+ assert_property_state_on_compositor(
+ anim.effect.getProperties(),
+ anim.expected);
+ });
+ });
+ }, 'Multiple animations: ' + subtest.desc);
+ });
+
+ gMultipleAsyncAnimationsWithGeometricKeyframeTests.forEach(function(subtest) {
+ promise_test(function(t) {
+ var div = addDiv(t, { class: 'compositable' });
+ var animations = subtest.animations.map(function(anim) {
+ var animation = div.animate(anim.frames, 100 * MS_PER_SEC);
+
+ // Bind expected values to animation object.
+ animation.expected = anim.expected;
+ return animation;
+ });
+ return waitForAllAnimations(animations).then(function() {
+ // First, all animations are running on compositor.
+ animations.forEach(function(anim) {
+ assert_animation_property_state_equals(
+ anim.effect.getProperties(),
+ anim.expected.withoutGeometric);
+ });
+ }).then(function() {
+ // Add a 'width' property to animations[1].
+ var keyframes = animations[1].effect.getKeyframes();
+
+ keyframes[0].width = '100px';
+ keyframes[1].width = '200px';
+
+ animations[1].effect.setKeyframes(keyframes);
+ return waitForFrame();
+ }).then(function() {
+ // Now the transform animation is not running on compositor because of
+ // the 'width' property.
+ animations.forEach(function(anim) {
+ assert_animation_property_state_equals(
+ anim.effect.getProperties(),
+ anim.expected.withGeometric);
+ });
+ }).then(function() {
+ // Remove the 'width' property from animations[1].
+ var keyframes = animations[1].effect.getKeyframes();
+
+ delete keyframes[0].width;
+ delete keyframes[1].width;
+
+ animations[1].effect.setKeyframes(keyframes);
+ return waitForFrame();
+ }).then(function() {
+ // Finally, all animations are running on compositor.
+ animations.forEach(function(anim) {
+ assert_animation_property_state_equals(
+ anim.effect.getProperties(),
+ anim.expected.withoutGeometric);
+ });
+ });
+ }, 'Multiple animations with geometric property: ' + subtest.desc);
+ });
+
+ gMultipleAsyncAnimationsWithGeometricAnimationTests.forEach(function(subtest) {
+ promise_test(function(t) {
+ var div = addDiv(t, { class: 'compositable' });
+ var animations = subtest.animations.map(function(anim) {
+ var animation = div.animate(anim.frames, 100 * MS_PER_SEC);
+
+ // Bind expected values to animation object.
+ animation.expected = anim.expected;
+ return animation;
+ });
+
+ var widthAnimation;
+
+ return waitForAllAnimations(animations).then(function() {
+ animations.forEach(function(anim) {
+ assert_property_state_on_compositor(
+ anim.effect.getProperties(),
+ anim.expected);
+ });
+ }).then(function() {
+ // Append 'width' animation on the same element.
+ widthAnimation = div.animate({ width: ['100px', '200px'] },
+ 100 * MS_PER_SEC);
+ return waitForFrame();
+ }).then(function() {
+ // Now transform animations are not running on compositor because of
+ // the 'width' animation.
+ animations.forEach(function(anim) {
+ assert_animation_property_state_equals(
+ anim.effect.getProperties(),
+ anim.expected);
+ });
+ // Remove the 'width' animation.
+ widthAnimation.cancel();
+ return waitForFrame();
+ }).then(function() {
+ // Now all animations are running on compositor.
+ animations.forEach(function(anim) {
+ assert_property_state_on_compositor(
+ anim.effect.getProperties(),
+ anim.expected);
+ });
+ });
+ }, 'Multiple async animations and geometric animation: ' + subtest.desc);
+ });
+
+ gAnimationsOnTooSmallElementTests.forEach(function(subtest) {
+ promise_test(function(t) {
+ var div = addDiv(t, subtest.style);
+ var animation = div.animate(subtest.frames, 100 * MS_PER_SEC);
+ return animation.ready.then(function() {
+ assert_animation_property_state_equals(
+ animation.effect.getProperties(),
+ subtest.expected);
+ });
+ }, subtest.desc);
+ });
+
+ promise_test(function(t) {
+ var animation = addDivAndAnimate(t,
+ { class: 'compositable' },
+ { transform: [ 'translate(0px)',
+ 'translate(100px)'] },
+ 100 * MS_PER_SEC);
+ return animation.ready.then(function() {
+ assert_animation_property_state_equals(
+ animation.effect.getProperties(),
+ [ { property: 'transform', runningOnCompositor: true } ]);
+ animation.effect.target.style = 'width: 10000px; height: 10000px';
+ return waitForFrame();
+ }).then(function() {
+ // viewport depends on test environment.
+ var expectedWarning = new RegExp(
+ "Animation cannot be run on the compositor because the frame size " +
+ "\\(10000, 10000\\) is bigger than the viewport \\(\\d+, \\d+\\) " +
+ "or the visual rectangle \\(10000, 10000\\) is larger than the " +
+ "maximum allowed value \\(\\d+\\)");
+ assert_animation_property_state_equals(
+ animation.effect.getProperties(),
+ [ {
+ property: 'transform',
+ runningOnCompositor: false,
+ warning: expectedWarning
+ } ]);
+ animation.effect.target.style = 'width: 100px; height: 100px';
+ return waitForFrame();
+ }).then(function() {
+ // FIXME: Bug 1253164: the animation should get back on compositor.
+ assert_animation_property_state_equals(
+ animation.effect.getProperties(),
+ [ { property: 'transform', runningOnCompositor: false } ]);
+ });
+ }, 'transform on too big element');
+
+ promise_test(function(t) {
+ var svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
+ svg.setAttribute('width', '100');
+ svg.setAttribute('height', '100');
+ var rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
+ rect.setAttribute('width', '100');
+ rect.setAttribute('height', '100');
+ rect.setAttribute('fill', 'red');
+ svg.appendChild(rect);
+ document.body.appendChild(svg);
+ t.add_cleanup(function() {
+ svg.remove();
+ });
+
+ var animation = svg.animate(
+ { transform: ['translate(0px)', 'translate(100px)'] }, 100 * MS_PER_SEC);
+ return animation.ready.then(function() {
+ assert_animation_property_state_equals(
+ animation.effect.getProperties(),
+ [ { property: 'transform', runningOnCompositor: true } ]);
+ svg.setAttribute('transform', 'translate(10, 20)');
+ return waitForFrame();
+ }).then(function() {
+ assert_animation_property_state_equals(
+ animation.effect.getProperties(),
+ [ {
+ property: 'transform',
+ runningOnCompositor: false,
+ warning: 'CompositorAnimationWarningTransformSVG'
+ } ]);
+ svg.removeAttribute('transform');
+ return waitForFrame();
+ }).then(function() {
+ assert_animation_property_state_equals(
+ animation.effect.getProperties(),
+ [ { property: 'transform', runningOnCompositor: true } ]);
+ });
+ }, 'transform of nsIFrame with SVG transform');
+
+ promise_test(function(t) {
+ var div = addDiv(t, { class: 'compositable',
+ style: 'animation: fade 100s' });
+ var cssAnimation = div.getAnimations()[0];
+ var scriptAnimation = div.animate({ opacity: [ 1, 0 ] }, 100 * MS_PER_SEC);
+ return scriptAnimation.ready.then(function() {
+ assert_animation_property_state_equals(
+ cssAnimation.effect.getProperties(),
+ [ { property: 'opacity', runningOnCompositor: true } ]);
+ assert_animation_property_state_equals(
+ scriptAnimation.effect.getProperties(),
+ [ { property: 'opacity', runningOnCompositor: true } ]);
+ });
+ }, 'overridden animation');
+}
+
+</script>
+
+</body>
diff --git a/dom/animation/test/chrome/test_animation_properties.html b/dom/animation/test/chrome/test_animation_properties.html
new file mode 100644
index 0000000000..5349013062
--- /dev/null
+++ b/dom/animation/test/chrome/test_animation_properties.html
@@ -0,0 +1,993 @@
+<!doctype html>
+<head>
+<meta charset=utf-8>
+<title>Bug 1254419 - Test the values returned by
+ KeyframeEffectReadOnly.getProperties()</title>
+<script type="application/javascript" src="../testharness.js"></script>
+<script type="application/javascript" src="../testharnessreport.js"></script>
+<script type="application/javascript" src="../testcommon.js"></script>
+</head>
+<body>
+<a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1254419"
+ target="_blank">Mozilla Bug 1254419</a>
+<div id="log"></div>
+<style>
+
+:root {
+ --var-100px: 100px;
+ --var-100px-200px: 100px 200px;
+}
+div {
+ font-size: 10px; /* For calculating em-based units */
+}
+</style>
+<script>
+'use strict';
+
+function assert_properties_equal(actual, expected) {
+ assert_equals(actual.length, expected.length);
+
+ var compareProperties = (a, b) =>
+ a.property == b.property ? 0 : (a.property < b.property ? -1 : 1);
+
+ var sortedActual = actual.sort(compareProperties);
+ var sortedExpected = expected.sort(compareProperties);
+
+ // We want to serialize the values in the following form:
+ //
+ // { offset: 0, easing: linear, composite: replace, value: 5px }, ...
+ //
+ // So that we can just compare strings and, in the failure case,
+ // easily see where the differences lie.
+ var serializeMember = value => {
+ return typeof value === 'undefined' ? '<not set>' : value;
+ }
+ var serializeValues = values =>
+ values.map(value =>
+ '{ ' +
+ [ 'offset', 'value', 'easing', 'composite' ].map(
+ member => `${member}: ${serializeMember(value[member])}`
+ ).join(', ') +
+ ' }')
+ .join(', ');
+
+ for (var i = 0; i < sortedActual.length; i++) {
+ assert_equals(sortedActual[i].property,
+ sortedExpected[i].property,
+ 'CSS property name should match');
+ assert_equals(serializeValues(sortedActual[i].values),
+ serializeValues(sortedExpected[i].values),
+ `Values arrays do not match for `
+ + `${sortedActual[i].property} property`);
+ }
+}
+
+// Shorthand for constructing a value object
+function value(offset, value, composite, easing) {
+ return { offset: offset, value: value, easing: easing, composite: composite };
+}
+
+var gTests = [
+
+ // ---------------------------------------------------------------------
+ //
+ // Tests for property-indexed specifications
+ //
+ // ---------------------------------------------------------------------
+
+ { desc: 'a one-property two-value property-indexed specification',
+ frames: { left: ['10px', '20px'] },
+ expected: [ { property: 'left',
+ values: [ value(0, '10px', 'replace', 'linear'),
+ value(1, '20px', 'replace') ] } ]
+ },
+ { desc: 'a one-shorthand-property two-value property-indexed'
+ + ' specification',
+ frames: { margin: ['10px', '10px 20px 30px 40px'] },
+ expected: [ { property: 'margin-top',
+ values: [ value(0, '10px', 'replace', 'linear'),
+ value(1, '10px', 'replace') ] },
+ { property: 'margin-right',
+ values: [ value(0, '10px', 'replace', 'linear'),
+ value(1, '20px', 'replace') ] },
+ { property: 'margin-bottom',
+ values: [ value(0, '10px', 'replace', 'linear'),
+ value(1, '30px', 'replace') ] },
+ { property: 'margin-left',
+ values: [ value(0, '10px', 'replace', 'linear'),
+ value(1, '40px', 'replace') ] } ]
+ },
+ { desc: 'a two-property (one shorthand and one of its longhand'
+ + ' components) two-value property-indexed specification',
+ frames: { marginTop: ['50px', '60px'],
+ margin: ['10px', '10px 20px 30px 40px'] },
+ expected: [ { property: 'margin-top',
+ values: [ value(0, '50px', 'replace', 'linear'),
+ value(1, '60px', 'replace') ] },
+ { property: 'margin-right',
+ values: [ value(0, '10px', 'replace', 'linear'),
+ value(1, '20px', 'replace') ] },
+ { property: 'margin-bottom',
+ values: [ value(0, '10px', 'replace', 'linear'),
+ value(1, '30px', 'replace') ] },
+ { property: 'margin-left',
+ values: [ value(0, '10px', 'replace', 'linear'),
+ value(1, '40px', 'replace') ] } ]
+ },
+ { desc: 'a two-property property-indexed specification with different'
+ + ' numbers of values',
+ frames: { left: ['10px', '20px', '30px'],
+ top: ['40px', '50px'] },
+ expected: [ { property: 'left',
+ values: [ value(0, '10px', 'replace', 'linear'),
+ value(0.5, '20px', 'replace', 'linear'),
+ value(1, '30px', 'replace') ] },
+ { property: 'top',
+ values: [ value(0, '40px', 'replace', 'linear'),
+ value(1, '50px', 'replace') ] } ]
+ },
+ { desc: 'a property-indexed specification with an invalid value',
+ frames: { left: ['10px', '20px', '30px', '40px', '50px'],
+ top: ['15px', '25px', 'invalid', '45px', '55px'] },
+ expected: [ { property: 'left',
+ values: [ value(0, '10px', 'replace', 'linear'),
+ value(0.25, '20px', 'replace', 'linear'),
+ value(0.5, '30px', 'replace', 'linear'),
+ value(0.75, '40px', 'replace', 'linear'),
+ value(1, '50px', 'replace') ] },
+ { property: 'top',
+ values: [ value(0, '15px', 'replace', 'linear'),
+ value(0.25, '25px', 'replace', 'linear'),
+ value(0.75, '45px', 'replace', 'linear'),
+ value(1, '55px', 'replace') ] } ]
+ },
+ { desc: 'a one-property two-value property-indexed specification that'
+ + ' needs to stringify its values',
+ frames: { opacity: [0, 1] },
+ expected: [ { property: 'opacity',
+ values: [ value(0, '0', 'replace', 'linear'),
+ value(1, '1', 'replace') ] } ]
+ },
+ { desc: 'a property-indexed keyframe where a lesser shorthand precedes'
+ + ' a greater shorthand',
+ frames: { borderLeft: [ '1px solid rgb(1, 2, 3)',
+ '2px solid rgb(4, 5, 6)' ],
+ border: [ '3px dotted rgb(7, 8, 9)',
+ '4px dashed rgb(10, 11, 12)' ] },
+ expected: [ { property: 'border-bottom-color',
+ values: [ value(0, 'rgb(7, 8, 9)', 'replace', 'linear'),
+ value(1, 'rgb(10, 11, 12)', 'replace') ] },
+ { property: 'border-left-color',
+ values: [ value(0, 'rgb(1, 2, 3)', 'replace', 'linear'),
+ value(1, 'rgb(4, 5, 6)', 'replace') ] },
+ { property: 'border-right-color',
+ values: [ value(0, 'rgb(7, 8, 9)', 'replace', 'linear'),
+ value(1, 'rgb(10, 11, 12)', 'replace') ] },
+ { property: 'border-top-color',
+ values: [ value(0, 'rgb(7, 8, 9)', 'replace', 'linear'),
+ value(1, 'rgb(10, 11, 12)', 'replace') ] },
+ { property: 'border-bottom-width',
+ values: [ value(0, '3px', 'replace', 'linear'),
+ value(1, '4px', 'replace') ] },
+ { property: 'border-left-width',
+ values: [ value(0, '1px', 'replace', 'linear'),
+ value(1, '2px', 'replace') ] },
+ { property: 'border-right-width',
+ values: [ value(0, '3px', 'replace', 'linear'),
+ value(1, '4px', 'replace') ] },
+ { property: 'border-top-width',
+ values: [ value(0, '3px', 'replace', 'linear'),
+ value(1, '4px', 'replace') ] },
+ { property: 'border-bottom-style',
+ values: [ value(0, 'dotted', 'replace', 'linear'),
+ value(1, 'dashed', 'replace') ] },
+ { property: 'border-left-style',
+ values: [ value(0, 'solid', 'replace', 'linear'),
+ value(1, 'solid', 'replace') ] },
+ { property: 'border-right-style',
+ values: [ value(0, 'dotted', 'replace', 'linear'),
+ value(1, 'dashed', 'replace') ] },
+ { property: 'border-top-style',
+ values: [ value(0, 'dotted', 'replace', 'linear'),
+ value(1, 'dashed', 'replace') ] },
+ { property: 'border-image-outset',
+ values: [ value(0, '0 0 0 0', 'replace', 'linear'),
+ value(1, '0 0 0 0', 'replace') ] },
+ { property: 'border-image-repeat',
+ values: [ value(0, 'stretch stretch', 'replace', 'linear'),
+ value(1, 'stretch stretch', 'replace') ] },
+ { property: 'border-image-slice',
+ values: [ value(0, '100% 100% 100% 100%',
+ 'replace', 'linear'),
+ value(1, '100% 100% 100% 100%', 'replace') ] },
+ { property: 'border-image-source',
+ values: [ value(0, 'none', 'replace', 'linear'),
+ value(1, 'none', 'replace') ] },
+ { property: 'border-image-width',
+ values: [ value(0, '1 1 1 1', 'replace', 'linear'),
+ value(1, '1 1 1 1', 'replace') ] },
+ { property: '-moz-border-bottom-colors',
+ values: [ value(0, 'none', 'replace', 'linear'),
+ value(1, 'none', 'replace') ] },
+ { property: '-moz-border-left-colors',
+ values: [ value(0, 'none', 'replace', 'linear'),
+ value(1, 'none', 'replace') ] },
+ { property: '-moz-border-right-colors',
+ values: [ value(0, 'none', 'replace', 'linear'),
+ value(1, 'none', 'replace') ] },
+ { property: '-moz-border-top-colors',
+ values: [ value(0, 'none', 'replace', 'linear'),
+ value(1, 'none', 'replace') ] } ]
+ },
+ { desc: 'a property-indexed keyframe where a greater shorthand precedes'
+ + ' a lesser shorthand',
+ frames: { border: [ '3px dotted rgb(7, 8, 9)',
+ '4px dashed rgb(10, 11, 12)' ],
+ borderLeft: [ '1px solid rgb(1, 2, 3)',
+ '2px solid rgb(4, 5, 6)' ] },
+ expected: [ { property: 'border-bottom-color',
+ values: [ value(0, 'rgb(7, 8, 9)', 'replace', 'linear'),
+ value(1, 'rgb(10, 11, 12)', 'replace') ] },
+ { property: 'border-left-color',
+ values: [ value(0, 'rgb(1, 2, 3)', 'replace', 'linear'),
+ value(1, 'rgb(4, 5, 6)', 'replace') ] },
+ { property: 'border-right-color',
+ values: [ value(0, 'rgb(7, 8, 9)', 'replace', 'linear'),
+ value(1, 'rgb(10, 11, 12)', 'replace') ] },
+ { property: 'border-top-color',
+ values: [ value(0, 'rgb(7, 8, 9)', 'replace', 'linear'),
+ value(1, 'rgb(10, 11, 12)', 'replace') ] },
+ { property: 'border-bottom-width',
+ values: [ value(0, '3px', 'replace', 'linear'),
+ value(1, '4px', 'replace') ] },
+ { property: 'border-left-width',
+ values: [ value(0, '1px', 'replace', 'linear'),
+ value(1, '2px', 'replace') ] },
+ { property: 'border-right-width',
+ values: [ value(0, '3px', 'replace', 'linear'),
+ value(1, '4px', 'replace') ] },
+ { property: 'border-top-width',
+ values: [ value(0, '3px', 'replace', 'linear'),
+ value(1, '4px', 'replace') ] },
+ { property: 'border-bottom-style',
+ values: [ value(0, 'dotted', 'replace', 'linear'),
+ value(1, 'dashed', 'replace') ] },
+ { property: 'border-left-style',
+ values: [ value(0, 'solid', 'replace', 'linear'),
+ value(1, 'solid', 'replace') ] },
+ { property: 'border-right-style',
+ values: [ value(0, 'dotted', 'replace', 'linear'),
+ value(1, 'dashed', 'replace') ] },
+ { property: 'border-top-style',
+ values: [ value(0, 'dotted', 'replace', 'linear'),
+ value(1, 'dashed', 'replace') ] },
+ { property: 'border-image-outset',
+ values: [ value(0, '0 0 0 0', 'replace', 'linear'),
+ value(1, '0 0 0 0', 'replace') ] },
+ { property: 'border-image-repeat',
+ values: [ value(0, 'stretch stretch', 'replace', 'linear'),
+ value(1, 'stretch stretch', 'replace') ] },
+ { property: 'border-image-slice',
+ values: [ value(0, '100% 100% 100% 100%',
+ 'replace', 'linear'),
+ value(1, '100% 100% 100% 100%', 'replace') ] },
+ { property: 'border-image-source',
+ values: [ value(0, 'none', 'replace', 'linear'),
+ value(1, 'none', 'replace') ] },
+ { property: 'border-image-width',
+ values: [ value(0, '1 1 1 1', 'replace', 'linear'),
+ value(1, '1 1 1 1', 'replace') ] },
+ { property: '-moz-border-bottom-colors',
+ values: [ value(0, 'none', 'replace', 'linear'),
+ value(1, 'none', 'replace') ] },
+ { property: '-moz-border-left-colors',
+ values: [ value(0, 'none', 'replace', 'linear'),
+ value(1, 'none', 'replace') ] },
+ { property: '-moz-border-right-colors',
+ values: [ value(0, 'none', 'replace', 'linear'),
+ value(1, 'none', 'replace') ] },
+ { property: '-moz-border-top-colors',
+ values: [ value(0, 'none', 'replace', 'linear'),
+ value(1, 'none', 'replace') ] } ]
+ },
+
+ // ---------------------------------------------------------------------
+ //
+ // Tests for keyframe sequences
+ //
+ // ---------------------------------------------------------------------
+
+ { desc: 'a keyframe sequence specification with repeated values at'
+ + ' offset 0/1 with different easings',
+ frames: [ { offset: 0.0, left: '100px', easing: 'ease' },
+ { offset: 0.0, left: '200px', easing: 'ease' },
+ { offset: 0.5, left: '300px', easing: 'linear' },
+ { offset: 1.0, left: '400px', easing: 'ease-out' },
+ { offset: 1.0, left: '500px', easing: 'step-end' } ],
+ expected: [ { property: 'left',
+ values: [ value(0, '100px', 'replace'),
+ value(0, '200px', 'replace', 'ease'),
+ value(0.5, '300px', 'replace', 'linear'),
+ value(1, '400px', 'replace'),
+ value(1, '500px', 'replace') ] } ]
+ },
+ { desc: 'a one-property two-keyframe sequence',
+ frames: [ { offset: 0, left: '10px' },
+ { offset: 1, left: '20px' } ],
+ expected: [ { property: 'left',
+ values: [ value(0, '10px', 'replace', 'linear'),
+ value(1, '20px', 'replace') ] } ]
+ },
+ { desc: 'a two-property two-keyframe sequence',
+ frames: [ { offset: 0, left: '10px', top: '30px' },
+ { offset: 1, left: '20px', top: '40px' } ],
+ expected: [ { property: 'left',
+ values: [ value(0, '10px', 'replace', 'linear'),
+ value(1, '20px', 'replace') ] },
+ { property: 'top',
+ values: [ value(0, '30px', 'replace', 'linear'),
+ value(1, '40px', 'replace') ] } ]
+ },
+ { desc: 'a one shorthand property two-keyframe sequence',
+ frames: [ { offset: 0, margin: '10px' },
+ { offset: 1, margin: '20px 30px 40px 50px' } ],
+ expected: [ { property: 'margin-top',
+ values: [ value(0, '10px', 'replace', 'linear'),
+ value(1, '20px', 'replace') ] },
+ { property: 'margin-right',
+ values: [ value(0, '10px', 'replace', 'linear'),
+ value(1, '30px', 'replace') ] },
+ { property: 'margin-bottom',
+ values: [ value(0, '10px', 'replace', 'linear'),
+ value(1, '40px', 'replace') ] },
+ { property: 'margin-left',
+ values: [ value(0, '10px', 'replace', 'linear'),
+ value(1, '50px', 'replace') ] } ]
+ },
+ { desc: 'a two-property (a shorthand and one of its component longhands)'
+ + ' two-keyframe sequence',
+ frames: [ { offset: 0, margin: '10px', marginTop: '20px' },
+ { offset: 1, marginTop: '70px',
+ margin: '30px 40px 50px 60px' } ],
+ expected: [ { property: 'margin-top',
+ values: [ value(0, '20px', 'replace', 'linear'),
+ value(1, '70px', 'replace') ] },
+ { property: 'margin-right',
+ values: [ value(0, '10px', 'replace', 'linear'),
+ value(1, '40px', 'replace') ] },
+ { property: 'margin-bottom',
+ values: [ value(0, '10px', 'replace', 'linear'),
+ value(1, '50px', 'replace') ] },
+ { property: 'margin-left',
+ values: [ value(0, '10px', 'replace', 'linear'),
+ value(1, '60px', 'replace') ] } ]
+ },
+ { desc: 'a keyframe sequence with duplicate values for a given interior'
+ + ' offset',
+ frames: [ { offset: 0.0, left: '10px' },
+ { offset: 0.5, left: '20px' },
+ { offset: 0.5, left: '30px' },
+ { offset: 0.5, left: '40px' },
+ { offset: 1.0, left: '50px' } ],
+ expected: [ { property: 'left',
+ values: [ value(0, '10px', 'replace', 'linear'),
+ value(0.5, '20px', 'replace'),
+ value(0.5, '40px', 'replace', 'linear'),
+ value(1, '50px', 'replace') ] } ]
+ },
+ { desc: 'a keyframe sequence with duplicate values for offsets 0 and 1',
+ frames: [ { offset: 0, left: '10px' },
+ { offset: 0, left: '20px' },
+ { offset: 0, left: '30px' },
+ { offset: 1, left: '40px' },
+ { offset: 1, left: '50px' },
+ { offset: 1, left: '60px' } ],
+ expected: [ { property: 'left',
+ values: [ value(0, '10px', 'replace'),
+ value(0, '30px', 'replace', 'linear'),
+ value(1, '40px', 'replace'),
+ value(1, '60px', 'replace') ] } ]
+ },
+ { desc: 'a two-property four-keyframe sequence',
+ frames: [ { offset: 0, left: '10px' },
+ { offset: 0, top: '20px' },
+ { offset: 1, top: '30px' },
+ { offset: 1, left: '40px' } ],
+ expected: [ { property: 'left',
+ values: [ value(0, '10px', 'replace', 'linear'),
+ value(1, '40px', 'replace') ] },
+ { property: 'top',
+ values: [ value(0, '20px', 'replace', 'linear'),
+ value(1, '30px', 'replace') ] } ]
+ },
+ { desc: 'a one-property keyframe sequence with some omitted offsets',
+ frames: [ { offset: 0.00, left: '10px' },
+ { offset: 0.25, left: '20px' },
+ { left: '30px' },
+ { left: '40px' },
+ { offset: 1.00, left: '50px' } ],
+ expected: [ { property: 'left',
+ values: [ value(0, '10px', 'replace', 'linear'),
+ value(0.25, '20px', 'replace', 'linear'),
+ value(0.5, '30px', 'replace', 'linear'),
+ value(0.75, '40px', 'replace', 'linear'),
+ value(1, '50px', 'replace') ] } ]
+ },
+ { desc: 'a two-property keyframe sequence with some omitted offsets',
+ frames: [ { offset: 0.00, left: '10px', top: '20px' },
+ { offset: 0.25, left: '30px' },
+ { left: '40px' },
+ { left: '50px', top: '60px' },
+ { offset: 1.00, left: '70px', top: '80px' } ],
+ expected: [ { property: 'left',
+ values: [ value(0, '10px', 'replace', 'linear'),
+ value(0.25, '30px', 'replace', 'linear'),
+ value(0.5, '40px', 'replace', 'linear'),
+ value(0.75, '50px', 'replace', 'linear'),
+ value(1, '70px', 'replace') ] },
+ { property: 'top',
+ values: [ value(0, '20px', 'replace', 'linear'),
+ value(0.75, '60px', 'replace', 'linear'),
+ value(1, '80px', 'replace') ] } ]
+ },
+ { desc: 'a one-property keyframe sequence with all omitted offsets',
+ frames: [ { left: '10px' },
+ { left: '20px' },
+ { left: '30px' },
+ { left: '40px' },
+ { left: '50px' } ],
+ expected: [ { property: 'left',
+ values: [ value(0, '10px', 'replace', 'linear'),
+ value(0.25, '20px', 'replace', 'linear'),
+ value(0.5, '30px', 'replace', 'linear'),
+ value(0.75, '40px', 'replace', 'linear'),
+ value(1, '50px', 'replace') ] } ]
+ },
+ { desc: 'a keyframe sequence with different easing values, but the'
+ + ' same easing value for a given offset',
+ frames: [ { offset: 0.0, easing: 'ease', left: '10px'},
+ { offset: 0.0, easing: 'ease', top: '20px'},
+ { offset: 0.5, easing: 'linear', left: '30px' },
+ { offset: 0.5, easing: 'linear', top: '40px' },
+ { offset: 1.0, easing: 'step-end', left: '50px' },
+ { offset: 1.0, easing: 'step-end', top: '60px' } ],
+ expected: [ { property: 'left',
+ values: [ value(0, '10px', 'replace', 'ease'),
+ value(0.5, '30px', 'replace', 'linear'),
+ value(1, '50px', 'replace') ] },
+ { property: 'top',
+ values: [ value(0, '20px', 'replace', 'ease'),
+ value(0.5, '40px', 'replace', 'linear'),
+ value(1, '60px', 'replace') ] } ]
+ },
+ { desc: 'a one-property two-keyframe sequence that needs to'
+ + ' stringify its values',
+ frames: [ { offset: 0, opacity: 0 },
+ { offset: 1, opacity: 1 } ],
+ expected: [ { property: 'opacity',
+ values: [ value(0, '0', 'replace', 'linear'),
+ value(1, '1', 'replace') ] } ]
+ },
+ { desc: 'a keyframe sequence where shorthand precedes longhand',
+ frames: [ { offset: 0, margin: '10px', marginRight: '20px' },
+ { offset: 1, margin: '30px' } ],
+ expected: [ { property: 'margin-top',
+ values: [ value(0, '10px', 'replace', 'linear'),
+ value(1, '30px', 'replace') ] },
+ { property: 'margin-right',
+ values: [ value(0, '20px', 'replace', 'linear'),
+ value(1, '30px', 'replace') ] },
+ { property: 'margin-bottom',
+ values: [ value(0, '10px', 'replace', 'linear'),
+ value(1, '30px', 'replace') ] },
+ { property: 'margin-left',
+ values: [ value(0, '10px', 'replace', 'linear'),
+ value(1, '30px', 'replace') ] } ]
+ },
+ { desc: 'a keyframe sequence where longhand precedes shorthand',
+ frames: [ { offset: 0, marginRight: '20px', margin: '10px' },
+ { offset: 1, margin: '30px' } ],
+ expected: [ { property: 'margin-top',
+ values: [ value(0, '10px', 'replace', 'linear'),
+ value(1, '30px', 'replace') ] },
+ { property: 'margin-right',
+ values: [ value(0, '20px', 'replace', 'linear'),
+ value(1, '30px', 'replace') ] },
+ { property: 'margin-bottom',
+ values: [ value(0, '10px', 'replace', 'linear'),
+ value(1, '30px', 'replace') ] },
+ { property: 'margin-left',
+ values: [ value(0, '10px', 'replace', 'linear'),
+ value(1, '30px', 'replace') ] } ]
+ },
+ { desc: 'a keyframe sequence where lesser shorthand precedes greater'
+ + ' shorthand',
+ frames: [ { offset: 0, borderLeft: '1px solid rgb(1, 2, 3)',
+ border: '2px dotted rgb(4, 5, 6)' },
+ { offset: 1, border: '3px dashed rgb(7, 8, 9)' } ],
+ expected: [ { property: 'border-bottom-color',
+ values: [ value(0, 'rgb(4, 5, 6)', 'replace', 'linear'),
+ value(1, 'rgb(7, 8, 9)', 'replace') ] },
+ { property: 'border-left-color',
+ values: [ value(0, 'rgb(1, 2, 3)', 'replace', 'linear'),
+ value(1, 'rgb(7, 8, 9)', 'replace') ] },
+ { property: 'border-right-color',
+ values: [ value(0, 'rgb(4, 5, 6)', 'replace', 'linear'),
+ value(1, 'rgb(7, 8, 9)', 'replace') ] },
+ { property: 'border-top-color',
+ values: [ value(0, 'rgb(4, 5, 6)', 'replace', 'linear'),
+ value(1, 'rgb(7, 8, 9)', 'replace') ] },
+ { property: 'border-bottom-width',
+ values: [ value(0, '2px', 'replace', 'linear'),
+ value(1, '3px', 'replace') ] },
+ { property: 'border-left-width',
+ values: [ value(0, '1px', 'replace', 'linear'),
+ value(1, '3px', 'replace') ] },
+ { property: 'border-right-width',
+ values: [ value(0, '2px', 'replace', 'linear'),
+ value(1, '3px', 'replace') ] },
+ { property: 'border-top-width',
+ values: [ value(0, '2px', 'replace', 'linear'),
+ value(1, '3px', 'replace') ] },
+ { property: 'border-bottom-style',
+ values: [ value(0, 'dotted', 'replace', 'linear'),
+ value(1, 'dashed', 'replace') ] },
+ { property: 'border-left-style',
+ values: [ value(0, 'solid', 'replace', 'linear'),
+ value(1, 'dashed', 'replace') ] },
+ { property: 'border-right-style',
+ values: [ value(0, 'dotted', 'replace', 'linear'),
+ value(1, 'dashed', 'replace') ] },
+ { property: 'border-top-style',
+ values: [ value(0, 'dotted', 'replace', 'linear'),
+ value(1, 'dashed', 'replace') ] },
+ { property: 'border-image-outset',
+ values: [ value(0, '0 0 0 0', 'replace', 'linear'),
+ value(1, '0 0 0 0', 'replace') ] },
+ { property: 'border-image-repeat',
+ values: [ value(0, 'stretch stretch', 'replace', 'linear'),
+ value(1, 'stretch stretch', 'replace') ] },
+ { property: 'border-image-slice',
+ values: [ value(0, '100% 100% 100% 100%',
+ 'replace', 'linear'),
+ value(1, '100% 100% 100% 100%', 'replace') ] },
+ { property: 'border-image-source',
+ values: [ value(0, 'none', 'replace', 'linear'),
+ value(1, 'none', 'replace') ] },
+ { property: 'border-image-width',
+ values: [ value(0, '1 1 1 1', 'replace', 'linear'),
+ value(1, '1 1 1 1', 'replace') ] },
+ { property: '-moz-border-bottom-colors',
+ values: [ value(0, 'none', 'replace', 'linear'),
+ value(1, 'none', 'replace') ] },
+ { property: '-moz-border-left-colors',
+ values: [ value(0, 'none', 'replace', 'linear'),
+ value(1, 'none', 'replace') ] },
+ { property: '-moz-border-right-colors',
+ values: [ value(0, 'none', 'replace', 'linear'),
+ value(1, 'none', 'replace') ] },
+ { property: '-moz-border-top-colors',
+ values: [ value(0, 'none', 'replace', 'linear'),
+ value(1, 'none', 'replace') ] } ]
+ },
+ { desc: 'a keyframe sequence where greater shorthand precedes'
+ + ' lesser shorthand',
+ frames: [ { offset: 0, border: '2px dotted rgb(4, 5, 6)',
+ borderLeft: '1px solid rgb(1, 2, 3)' },
+ { offset: 1, border: '3px dashed rgb(7, 8, 9)' } ],
+ expected: [ { property: 'border-bottom-color',
+ values: [ value(0, 'rgb(4, 5, 6)', 'replace', 'linear'),
+ value(1, 'rgb(7, 8, 9)', 'replace') ] },
+ { property: 'border-left-color',
+ values: [ value(0, 'rgb(1, 2, 3)', 'replace', 'linear'),
+ value(1, 'rgb(7, 8, 9)', 'replace') ] },
+ { property: 'border-right-color',
+ values: [ value(0, 'rgb(4, 5, 6)', 'replace', 'linear'),
+ value(1, 'rgb(7, 8, 9)', 'replace') ] },
+ { property: 'border-top-color',
+ values: [ value(0, 'rgb(4, 5, 6)', 'replace', 'linear'),
+ value(1, 'rgb(7, 8, 9)', 'replace') ] },
+ { property: 'border-bottom-width',
+ values: [ value(0, '2px', 'replace', 'linear'),
+ value(1, '3px', 'replace') ] },
+ { property: 'border-left-width',
+ values: [ value(0, '1px', 'replace', 'linear'),
+ value(1, '3px', 'replace') ] },
+ { property: 'border-right-width',
+ values: [ value(0, '2px', 'replace', 'linear'),
+ value(1, '3px', 'replace') ] },
+ { property: 'border-top-width',
+ values: [ value(0, '2px', 'replace', 'linear'),
+ value(1, '3px', 'replace') ] },
+ { property: 'border-bottom-style',
+ values: [ value(0, 'dotted', 'replace', 'linear'),
+ value(1, 'dashed', 'replace') ] },
+ { property: 'border-left-style',
+ values: [ value(0, 'solid', 'replace', 'linear'),
+ value(1, 'dashed', 'replace') ] },
+ { property: 'border-right-style',
+ values: [ value(0, 'dotted', 'replace', 'linear'),
+ value(1, 'dashed', 'replace') ] },
+ { property: 'border-top-style',
+ values: [ value(0, 'dotted', 'replace', 'linear'),
+ value(1, 'dashed', 'replace') ] },
+ { property: 'border-image-outset',
+ values: [ value(0, '0 0 0 0', 'replace', 'linear'),
+ value(1, '0 0 0 0', 'replace') ] },
+ { property: 'border-image-repeat',
+ values: [ value(0, 'stretch stretch', 'replace', 'linear'),
+ value(1, 'stretch stretch', 'replace') ] },
+ { property: 'border-image-slice',
+ values: [ value(0, '100% 100% 100% 100%',
+ 'replace', 'linear'),
+ value(1, '100% 100% 100% 100%', 'replace') ] },
+ { property: 'border-image-source',
+ values: [ value(0, 'none', 'replace', 'linear'),
+ value(1, 'none', 'replace') ] },
+ { property: 'border-image-width',
+ values: [ value(0, '1 1 1 1', 'replace', 'linear'),
+ value(1, '1 1 1 1', 'replace') ] },
+ { property: '-moz-border-bottom-colors',
+ values: [ value(0, 'none', 'replace', 'linear'),
+ value(1, 'none', 'replace') ] },
+ { property: '-moz-border-left-colors',
+ values: [ value(0, 'none', 'replace', 'linear'),
+ value(1, 'none', 'replace') ] },
+ { property: '-moz-border-right-colors',
+ values: [ value(0, 'none', 'replace', 'linear'),
+ value(1, 'none', 'replace') ] },
+ { property: '-moz-border-top-colors',
+ values: [ value(0, 'none', 'replace', 'linear'),
+ value(1, 'none', 'replace') ] } ]
+ },
+
+ // ---------------------------------------------------------------------
+ //
+ // Tests for unit conversion
+ //
+ // ---------------------------------------------------------------------
+
+ { desc: 'em units are resolved to px values',
+ frames: { left: ['10em', '20em'] },
+ expected: [ { property: 'left',
+ values: [ value(0, '100px', 'replace', 'linear'),
+ value(1, '200px', 'replace') ] } ]
+ },
+ { desc: 'calc() expressions are resolved to the equivalent units',
+ frames: { left: ['calc(10em + 10px)', 'calc(10em + 10%)'] },
+ expected: [ { property: 'left',
+ values: [ value(0, 'calc(110px)', 'replace', 'linear'),
+ value(1, 'calc(100px + 10%)', 'replace') ] } ]
+ },
+
+ // ---------------------------------------------------------------------
+ //
+ // Tests for CSS variable handling conversion
+ //
+ // ---------------------------------------------------------------------
+
+ { desc: 'CSS variables are resolved to their corresponding values',
+ frames: { left: ['10px', 'var(--var-100px)'] },
+ expected: [ { property: 'left',
+ values: [ value(0, '10px', 'replace', 'linear'),
+ value(1, '100px', 'replace') ] } ]
+ },
+ { desc: 'CSS variables in calc() expressions are resolved',
+ frames: { left: ['10px', 'calc(var(--var-100px) / 2 - 10%)'] },
+ expected: [ { property: 'left',
+ values: [ value(0, '10px', 'replace', 'linear'),
+ value(1, 'calc(50px + -10%)', 'replace') ] } ]
+ },
+ { desc: 'CSS variables in shorthands are resolved to their corresponding'
+ + ' values',
+ frames: { margin: ['10px', 'var(--var-100px-200px)'] },
+ expected: [ { property: 'margin-top',
+ values: [ value(0, '10px', 'replace', 'linear'),
+ value(1, '100px', 'replace') ] },
+ { property: 'margin-right',
+ values: [ value(0, '10px', 'replace', 'linear'),
+ value(1, '200px', 'replace') ] },
+ { property: 'margin-bottom',
+ values: [ value(0, '10px', 'replace', 'linear'),
+ value(1, '100px', 'replace') ] },
+ { property: 'margin-left',
+ values: [ value(0, '10px', 'replace', 'linear'),
+ value(1, '200px', 'replace') ] } ]
+ },
+
+ // ---------------------------------------------------------------------
+ //
+ // Tests for properties that parse correctly but which we fail to
+ // convert to computed values.
+ //
+ // ---------------------------------------------------------------------
+
+ { desc: 'a property that can\'t be resolved to computed values in'
+ + ' initial keyframe',
+ frames: [ { margin: '5px', simulateComputeValuesFailure: true },
+ { margin: '5px' } ],
+ expected: [ ]
+ },
+ { desc: 'a property that can\'t be resolved to computed values in'
+ + ' initial keyframe where we have enough values to create'
+ + ' a final segment',
+ frames: [ { margin: '5px', simulateComputeValuesFailure: true },
+ { margin: '5px' },
+ { margin: '5px' } ],
+ expected: [ ]
+ },
+ { desc: 'a property that can\'t be resolved to computed values in'
+ + ' initial overlapping keyframes (first in series of two)',
+ frames: [ { margin: '5px', offset: 0,
+ simulateComputeValuesFailure: true },
+ { margin: '5px', offset: 0 },
+ { margin: '5px' } ],
+ expected: [ { property: 'margin-top',
+ values: [ value(0, '5px', 'replace', 'linear'),
+ value(1, '5px', 'replace') ] },
+ { property: 'margin-right',
+ values: [ value(0, '5px', 'replace', 'linear'),
+ value(1, '5px', 'replace') ] },
+ { property: 'margin-bottom',
+ values: [ value(0, '5px', 'replace', 'linear'),
+ value(1, '5px', 'replace') ] },
+ { property: 'margin-left',
+ values: [ value(0, '5px', 'replace', 'linear'),
+ value(1, '5px', 'replace') ] } ]
+ },
+ { desc: 'a property that can\'t be resolved to computed values in'
+ + ' initial overlapping keyframes (second in series of two)',
+ frames: [ { margin: '5px', offset: 0 },
+ { margin: '5px', offset: 0,
+ simulateComputeValuesFailure: true },
+ { margin: '5px' } ],
+ expected: [ { property: 'margin-top',
+ values: [ value(0, '5px', 'replace', 'linear'),
+ value(1, '5px', 'replace') ] },
+ { property: 'margin-right',
+ values: [ value(0, '5px', 'replace', 'linear'),
+ value(1, '5px', 'replace') ] },
+ { property: 'margin-bottom',
+ values: [ value(0, '5px', 'replace', 'linear'),
+ value(1, '5px', 'replace') ] },
+ { property: 'margin-left',
+ values: [ value(0, '5px', 'replace', 'linear'),
+ value(1, '5px', 'replace') ] } ]
+ },
+ { desc: 'a property that can\'t be resolved to computed values in'
+ + ' initial overlapping keyframes (second in series of three)',
+ frames: [ { margin: '5px', offset: 0 },
+ { margin: '5px', offset: 0,
+ simulateComputeValuesFailure: true },
+ { margin: '5px', offset: 0 },
+ { margin: '5px' } ],
+ expected: [ { property: 'margin-top',
+ values: [ value(0, '5px', 'replace'),
+ value(0, '5px', 'replace', 'linear'),
+ value(1, '5px', 'replace') ] },
+ { property: 'margin-right',
+ values: [ value(0, '5px', 'replace'),
+ value(0, '5px', 'replace', 'linear'),
+ value(1, '5px', 'replace') ] },
+ { property: 'margin-bottom',
+ values: [ value(0, '5px', 'replace'),
+ value(0, '5px', 'replace', 'linear'),
+ value(1, '5px', 'replace') ] },
+ { property: 'margin-left',
+ values: [ value(0, '5px', 'replace'),
+ value(0, '5px', 'replace', 'linear'),
+ value(1, '5px', 'replace') ] } ]
+ },
+ { desc: 'a property that can\'t be resolved to computed values in'
+ + ' final keyframe',
+ frames: [ { margin: '5px' },
+ { margin: '5px', simulateComputeValuesFailure: true } ],
+ expected: [ ]
+ },
+ { desc: 'a property that can\'t be resolved to computed values in'
+ + ' final keyframe where it forms the last segment in the series',
+ frames: [ { margin: '5px' },
+ { margin: '5px',
+ marginLeft: '5px',
+ marginRight: '5px',
+ marginBottom: '5px',
+ // margin-top sorts last and only it will be missing since
+ // the other longhand components are specified
+ simulateComputeValuesFailure: true } ],
+ expected: [ { property: 'margin-bottom',
+ values: [ value(0, '5px', 'replace', 'linear'),
+ value(1, '5px', 'replace') ] },
+ { property: 'margin-left',
+ values: [ value(0, '5px', 'replace', 'linear'),
+ value(1, '5px', 'replace') ] },
+ { property: 'margin-right',
+ values: [ value(0, '5px', 'replace', 'linear'),
+ value(1, '5px', 'replace') ] } ]
+ },
+ { desc: 'a property that can\'t be resolved to computed values in'
+ + ' final keyframe where we have enough values to create'
+ + ' an initial segment',
+ frames: [ { margin: '5px' },
+ { margin: '5px' },
+ { margin: '5px', simulateComputeValuesFailure: true } ],
+ expected: [ ]
+ },
+ { desc: 'a property that can\'t be resolved to computed values in'
+ + ' final overlapping keyframes (first in series of two)',
+ frames: [ { margin: '5px' },
+ { margin: '5px', offset: 1,
+ simulateComputeValuesFailure: true },
+ { margin: '5px', offset: 1 } ],
+ expected: [ { property: 'margin-top',
+ values: [ value(0, '5px', 'replace', 'linear'),
+ value(1, '5px', 'replace') ] },
+ { property: 'margin-right',
+ values: [ value(0, '5px', 'replace', 'linear'),
+ value(1, '5px', 'replace') ] },
+ { property: 'margin-bottom',
+ values: [ value(0, '5px', 'replace', 'linear'),
+ value(1, '5px', 'replace') ] },
+ { property: 'margin-left',
+ values: [ value(0, '5px', 'replace', 'linear'),
+ value(1, '5px', 'replace') ] } ]
+ },
+ { desc: 'a property that can\'t be resolved to computed values in'
+ + ' final overlapping keyframes (second in series of two)',
+ frames: [ { margin: '5px' },
+ { margin: '5px', offset: 1 },
+ { margin: '5px', offset: 1,
+ simulateComputeValuesFailure: true } ],
+ expected: [ { property: 'margin-top',
+ values: [ value(0, '5px', 'replace', 'linear'),
+ value(1, '5px', 'replace') ] },
+ { property: 'margin-right',
+ values: [ value(0, '5px', 'replace', 'linear'),
+ value(1, '5px', 'replace') ] },
+ { property: 'margin-bottom',
+ values: [ value(0, '5px', 'replace', 'linear'),
+ value(1, '5px', 'replace') ] },
+ { property: 'margin-left',
+ values: [ value(0, '5px', 'replace', 'linear'),
+ value(1, '5px', 'replace') ] } ]
+ },
+ { desc: 'a property that can\'t be resolved to computed values in'
+ + ' final overlapping keyframes (second in series of three)',
+ frames: [ { margin: '5px' },
+ { margin: '5px', offset: 1 },
+ { margin: '5px', offset: 1,
+ simulateComputeValuesFailure: true },
+ { margin: '5px', offset: 1 } ],
+ expected: [ { property: 'margin-top',
+ values: [ value(0, '5px', 'replace', 'linear'),
+ value(1, '5px', 'replace'),
+ value(1, '5px', 'replace') ] },
+ { property: 'margin-right',
+ values: [ value(0, '5px', 'replace', 'linear'),
+ value(1, '5px', 'replace'),
+ value(1, '5px', 'replace') ] },
+ { property: 'margin-bottom',
+ values: [ value(0, '5px', 'replace', 'linear'),
+ value(1, '5px', 'replace'),
+ value(1, '5px', 'replace') ] },
+ { property: 'margin-left',
+ values: [ value(0, '5px', 'replace', 'linear'),
+ value(1, '5px', 'replace'),
+ value(1, '5px', 'replace') ] } ]
+ },
+ { desc: 'a property that can\'t be resolved to computed values in'
+ + ' intermediate keyframe',
+ frames: [ { margin: '5px' },
+ { margin: '5px', simulateComputeValuesFailure: true },
+ { margin: '5px' } ],
+ expected: [ { property: 'margin-top',
+ values: [ value(0, '5px', 'replace', 'linear'),
+ value(1, '5px', 'replace') ] },
+ { property: 'margin-right',
+ values: [ value(0, '5px', 'replace', 'linear'),
+ value(1, '5px', 'replace') ] },
+ { property: 'margin-bottom',
+ values: [ value(0, '5px', 'replace', 'linear'),
+ value(1, '5px', 'replace') ] },
+ { property: 'margin-left',
+ values: [ value(0, '5px', 'replace', 'linear'),
+ value(1, '5px', 'replace') ] } ]
+ },
+ { desc: 'a property that can\'t be resolved to computed values in'
+ + ' initial keyframe along with other values',
+ // simulateComputeValuesFailure only applies to shorthands so we can set
+ // it on the same keyframe and it will only apply to |margin| and not
+ // |left|.
+ frames: [ { margin: '77%', left: '10px',
+ simulateComputeValuesFailure: true },
+ { margin: '5px', left: '20px' } ],
+ expected: [ { property: 'left',
+ values: [ value(0, '10px', 'replace', 'linear'),
+ value(1, '20px', 'replace') ] } ],
+ },
+ { desc: 'a property that can\'t be resolved to computed values in'
+ + ' initial keyframe along with other values where those'
+ + ' values sort after the property with missing values',
+ frames: [ { margin: '77%', right: '10px',
+ simulateComputeValuesFailure: true },
+ { margin: '5px', right: '20px' } ],
+ expected: [ { property: 'right',
+ values: [ value(0, '10px', 'replace', 'linear'),
+ value(1, '20px', 'replace') ] } ],
+ },
+ { desc: 'a property that can\'t be resolved to computed values in'
+ + ' final keyframe along with other values',
+ frames: [ { margin: '5px', left: '10px' },
+ { margin: '5px', left: '20px',
+ simulateComputeValuesFailure: true } ],
+ expected: [ { property: 'left',
+ values: [ value(0, '10px', 'replace', 'linear'),
+ value(1, '20px', 'replace') ] } ],
+ },
+ { desc: 'a property that can\'t be resolved to computed values in'
+ + ' final keyframe along with other values where those'
+ + ' values sort after the property with missing values',
+ frames: [ { margin: '5px', right: '10px' },
+ { margin: '5px', right: '20px',
+ simulateComputeValuesFailure: true } ],
+ expected: [ { property: 'right',
+ values: [ value(0, '10px', 'replace', 'linear'),
+ value(1, '20px', 'replace') ] } ],
+ },
+ { desc: 'a property that can\'t be resolved to computed values in'
+ + ' an intermediate keyframe along with other values',
+ frames: [ { margin: '5px', left: '10px' },
+ { margin: '5px', left: '20px',
+ simulateComputeValuesFailure: true },
+ { margin: '5px', left: '30px' } ],
+ expected: [ { property: 'margin-top',
+ values: [ value(0, '5px', 'replace', 'linear'),
+ value(1, '5px', 'replace') ] },
+ { property: 'margin-right',
+ values: [ value(0, '5px', 'replace', 'linear'),
+ value(1, '5px', 'replace') ] },
+ { property: 'margin-bottom',
+ values: [ value(0, '5px', 'replace', 'linear'),
+ value(1, '5px', 'replace') ] },
+ { property: 'margin-left',
+ values: [ value(0, '5px', 'replace', 'linear'),
+ value(1, '5px', 'replace') ] },
+ { property: 'left',
+ values: [ value(0, '10px', 'replace', 'linear'),
+ value(0.5, '20px', 'replace', 'linear'),
+ value(1, '30px', 'replace') ] } ]
+ },
+ { desc: 'a property that can\'t be resolved to computed values in'
+ + ' an intermediate keyframe by itself',
+ frames: [ { margin: '5px', left: '10px' },
+ { margin: '5px',
+ simulateComputeValuesFailure: true },
+ { margin: '5px', left: '30px' } ],
+ expected: [ { property: 'margin-top',
+ values: [ value(0, '5px', 'replace', 'linear'),
+ value(1, '5px', 'replace') ] },
+ { property: 'margin-right',
+ values: [ value(0, '5px', 'replace', 'linear'),
+ value(1, '5px', 'replace') ] },
+ { property: 'margin-bottom',
+ values: [ value(0, '5px', 'replace', 'linear'),
+ value(1, '5px', 'replace') ] },
+ { property: 'margin-left',
+ values: [ value(0, '5px', 'replace', 'linear'),
+ value(1, '5px', 'replace') ] },
+ { property: 'left',
+ values: [ value(0, '10px', 'replace', 'linear'),
+ value(1, '30px', 'replace') ] } ]
+ },
+];
+
+gTests.forEach(function(subtest) {
+ test(function(t) {
+ var div = addDiv(t);
+ var animation = div.animate(subtest.frames, 100 * MS_PER_SEC);
+ assert_properties_equal(animation.effect.getProperties(),
+ subtest.expected);
+ }, subtest.desc);
+});
+
+</script>
+</body>
diff --git a/dom/animation/test/chrome/test_generated_content_getAnimations.html b/dom/animation/test/chrome/test_generated_content_getAnimations.html
new file mode 100644
index 0000000000..04e0f9bd09
--- /dev/null
+++ b/dom/animation/test/chrome/test_generated_content_getAnimations.html
@@ -0,0 +1,83 @@
+<!DOCTYPE html>
+<head>
+<meta charset=utf-8>
+<title>Test getAnimations() for generated-content elements</title>
+<script type="application/javascript" src="../testharness.js"></script>
+<script type="application/javascript" src="../testharnessreport.js"></script>
+<script type="application/javascript" src="../testcommon.js"></script>
+<style>
+@keyframes anim { }
+@keyframes anim2 { }
+.before::before {
+ content: '';
+ animation: anim 100s;
+}
+.after::after {
+ content: '';
+ animation: anim 100s, anim2 100s;
+}
+</style>
+</head>
+<body>
+<div id='root' class='before after'>
+ <div class='before'></div>
+ <div></div>
+</div>
+<script>
+'use strict';
+
+const {Cc, Ci, Cu} = SpecialPowers;
+
+function getWalker(node) {
+ var walker = Cc["@mozilla.org/inspector/deep-tree-walker;1"].
+ createInstance(Ci.inIDeepTreeWalker);
+ walker.showAnonymousContent = true;
+ walker.init(node.ownerDocument, Ci.nsIDOMNodeFilter.SHOW_ALL);
+ walker.currentNode = node;
+ return walker;
+}
+
+test(function(t) {
+ var root = document.getElementById('root');
+ // Flush first to make sure the generated-content elements are ready
+ // in the tree.
+ flushComputedStyle(root);
+ var before = getWalker(root).firstChild();
+ var after = getWalker(root).lastChild();
+
+ // Sanity Checks
+ assert_equals(document.getAnimations().length, 4,
+ 'All animations in this document');
+ assert_equals(before.tagName, '_moz_generated_content_before',
+ 'First child is ::before element');
+ assert_equals(after.tagName, '_moz_generated_content_after',
+ 'Last child is ::after element');
+
+ // Test Element.getAnimations() for generated-content elements
+ assert_equals(before.getAnimations().length, 1,
+ 'Animations of ::before generated-content element');
+ assert_equals(after.getAnimations().length, 2,
+ 'Animations of ::after generated-content element');
+}, 'Element.getAnimations() used on generated-content elements');
+
+test(function(t) {
+ var root = document.getElementById('root');
+ flushComputedStyle(root);
+ var walker = getWalker(root);
+
+ var animations = [];
+ var element = walker.currentNode;
+ while (element) {
+ if (element.getAnimations) {
+ animations = [...animations, ...element.getAnimations()];
+ }
+ element = walker.nextNode();
+ }
+
+ assert_equals(animations.length, document.getAnimations().length,
+ 'The number of animations got by DeepTreeWalker and ' +
+ 'document.getAnimations() should be the same');
+}, 'Element.getAnimations() used by traversing DeepTreeWalker');
+
+</script>
+</body>
diff --git a/dom/animation/test/chrome/test_observers_for_sync_api.html b/dom/animation/test/chrome/test_observers_for_sync_api.html
new file mode 100644
index 0000000000..20c3f3670d
--- /dev/null
+++ b/dom/animation/test/chrome/test_observers_for_sync_api.html
@@ -0,0 +1,854 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title>
+Test chrome-only MutationObserver animation notifications for sync APIs
+</title>
+<script type="application/javascript" src="../testharness.js"></script>
+<script type="application/javascript" src="../testharnessreport.js"></script>
+<script type="application/javascript" src="../testcommon.js"></script>
+<div id="log"></div>
+<style>
+@keyframes anim {
+ to { transform: translate(100px); }
+}
+@keyframes anotherAnim {
+ to { transform: translate(0px); }
+}
+</style>
+<script>
+
+function assert_record_list(actual, expected, desc, index, listName) {
+ assert_equals(actual.length, expected.length,
+ `${desc} - record[${index}].${listName} length`);
+ if (actual.length != expected.length) {
+ return;
+ }
+ for (var i = 0; i < actual.length; i++) {
+ assert_not_equals(actual.indexOf(expected[i]), -1,
+ `${desc} - record[${index}].${listName} contains expected Animation`);
+ }
+}
+
+function assert_equals_records(actual, expected, desc) {
+ assert_equals(actual.length, expected.length, `${desc} - number of records`);
+ if (actual.length != expected.length) {
+ return;
+ }
+ for (var i = 0; i < actual.length; i++) {
+ assert_record_list(actual[i].addedAnimations,
+ expected[i].added, desc, i, "addedAnimations");
+ assert_record_list(actual[i].changedAnimations,
+ expected[i].changed, desc, i, "changedAnimations");
+ assert_record_list(actual[i].removedAnimations,
+ expected[i].removed, desc, i, "removedAnimations");
+ }
+}
+
+// Create a pseudo element
+function createPseudo(test, element, type) {
+ addStyle(test, { '@keyframes anim': '',
+ ['.pseudo::' + type]: 'animation: anim 10s;' });
+ element.classList.add('pseudo');
+ var anims = document.getAnimations();
+ assert_true(anims.length >= 1);
+ var anim = anims[anims.length - 1];
+ assert_equals(anim.effect.target.parentElement, element);
+ assert_equals(anim.effect.target.type, '::' + type);
+ anim.cancel();
+ return anim.effect.target;
+}
+
+[ { subtree: false },
+ { subtree: true }
+].forEach(aOptions => {
+ test(t => {
+ var div = addDiv(t);
+ var observer =
+ setupSynchronousObserver(t,
+ aOptions.subtree ? div.parentNode : div,
+ aOptions.subtree);
+
+ var anim = div.animate({ opacity: [ 0, 1 ] }, 200 * MS_PER_SEC);
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [anim], changed: [], removed: [] }],
+ "records after animation is added");
+
+ anim.effect.timing.duration = 100 * MS_PER_SEC;
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [anim], removed: [] }],
+ "records after duration is changed");
+
+ anim.effect.timing.duration = 100 * MS_PER_SEC;
+ assert_equals_records(observer.takeRecords(),
+ [], "records after assigning same value");
+
+ anim.currentTime = anim.effect.timing.duration * 2;
+ anim.finish();
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [], removed: [anim] }],
+ "records after animation end");
+
+ anim.effect.timing.duration = anim.effect.timing.duration * 3;
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [anim], changed: [], removed: [] }],
+ "records after animation restarted");
+
+ anim.effect.timing.duration = "auto";
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [], removed: [anim] }],
+ "records after duration set \"auto\"");
+
+ anim.effect.timing.duration = "auto";
+ assert_equals_records(observer.takeRecords(),
+ [], "records after assigning same value \"auto\"");
+ }, "change_duration_and_currenttime");
+
+ test(t => {
+ var div = addDiv(t);
+ var observer =
+ setupSynchronousObserver(t,
+ aOptions.subtree ? div.parentNode : div,
+ aOptions.subtree);
+
+ var anim = div.animate({ opacity: [ 0, 1 ] }, 100 * MS_PER_SEC);
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [anim], changed: [], removed: [] }],
+ "records after animation is added");
+
+ anim.effect.timing.endDelay = 10 * MS_PER_SEC;
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [anim], removed: [] }],
+ "records after endDelay is changed");
+
+ anim.effect.timing.endDelay = 10 * MS_PER_SEC;
+ assert_equals_records(observer.takeRecords(),
+ [], "records after assigning same value");
+
+ anim.currentTime = 109 * MS_PER_SEC;
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [], removed: [anim] }],
+ "records after currentTime during endDelay");
+
+ anim.effect.timing.endDelay = -110 * MS_PER_SEC;
+ assert_equals_records(observer.takeRecords(),
+ [], "records after assigning negative value");
+ }, "change_enddelay_and_currenttime");
+
+ test(t => {
+ var div = addDiv(t);
+ var observer =
+ setupSynchronousObserver(t,
+ aOptions.subtree ? div.parentNode : div,
+ aOptions.subtree);
+
+ var anim = div.animate({ opacity: [ 0, 1 ] },
+ { duration: 100 * MS_PER_SEC,
+ endDelay: -100 * MS_PER_SEC });
+ assert_equals_records(observer.takeRecords(),
+ [], "records after animation is added");
+ }, "zero_end_time");
+
+ test(t => {
+ var div = addDiv(t);
+ var observer =
+ setupSynchronousObserver(t,
+ aOptions.subtree ? div.parentNode : div,
+ aOptions.subtree);
+
+ var anim = div.animate({ opacity: [ 0, 1 ] }, 100 * MS_PER_SEC);
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [anim], changed: [], removed: [] }],
+ "records after animation is added");
+
+ anim.effect.timing.iterations = 2;
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [anim], removed: [] }],
+ "records after iterations is changed");
+
+ anim.effect.timing.iterations = 2;
+ assert_equals_records(observer.takeRecords(),
+ [], "records after assigning same value");
+
+ anim.effect.timing.iterations = 0;
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [], removed: [anim] }],
+ "records after animation end");
+
+ anim.effect.timing.iterations = Infinity;
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [anim], changed: [], removed: [] }],
+ "records after animation restarted");
+ }, "change_iterations");
+
+ test(t => {
+ var div = addDiv(t);
+ var observer =
+ setupSynchronousObserver(t,
+ aOptions.subtree ? div.parentNode : div,
+ aOptions.subtree);
+
+ var anim = div.animate({ opacity: [ 0, 1 ] }, 100 * MS_PER_SEC);
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [anim], changed: [], removed: [] }],
+ "records after animation is added");
+
+ anim.effect.timing.delay = 100;
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [anim], removed: [] }],
+ "records after delay is changed");
+
+ anim.effect.timing.delay = 100;
+ assert_equals_records(observer.takeRecords(),
+ [], "records after assigning same value");
+
+ anim.effect.timing.delay = -100 * MS_PER_SEC;
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [], removed: [anim] }],
+ "records after animation end");
+
+ anim.effect.timing.delay = 0;
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [anim], changed: [], removed: [] }],
+ "records after animation restarted");
+ }, "change_delay");
+
+ test(t => {
+ var div = addDiv(t);
+ var observer =
+ setupSynchronousObserver(t,
+ aOptions.subtree ? div.parentNode : div,
+ aOptions.subtree);
+
+ var anim = div.animate({ opacity: [ 0, 1 ] },
+ { duration: 100 * MS_PER_SEC,
+ easing: "steps(2, start)" });
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [anim], changed: [], removed: [] }],
+ "records after animation is added");
+
+ anim.effect.timing.easing = "steps(2, end)";
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [anim], removed: [] }],
+ "records after easing is changed");
+
+ anim.effect.timing.easing = "steps(2, end)";
+ assert_equals_records(observer.takeRecords(),
+ [], "records after assigning same value");
+ }, "change_easing");
+
+ test(t => {
+ var div = addDiv(t);
+ var observer =
+ setupSynchronousObserver(t,
+ aOptions.subtree ? div.parentNode : div,
+ aOptions.subtree);
+
+ var anim = div.animate({ opacity: [ 0, 1 ] },
+ { duration: 100, delay: -100 });
+ assert_equals_records(observer.takeRecords(),
+ [], "records after assigning negative value");
+ }, "negative_delay_in_constructor");
+
+ test(t => {
+ var div = addDiv(t);
+ var observer =
+ setupSynchronousObserver(t,
+ aOptions.subtree ? div.parentNode : div,
+ aOptions.subtree);
+
+ var effect = new KeyframeEffectReadOnly(null,
+ { opacity: [ 0, 1 ] },
+ { duration: 100 * MS_PER_SEC });
+ var anim = new Animation(effect, document.timeline);
+ anim.play();
+ assert_equals_records(observer.takeRecords(),
+ [], "no records after animation is added");
+ }, "create_animation_without_target");
+
+ test(t => {
+ var div = addDiv(t);
+ var observer =
+ setupSynchronousObserver(t,
+ aOptions.subtree ? div.parentNode : div,
+ aOptions.subtree);
+
+ var anim = div.animate({ opacity: [ 0, 1 ] },
+ { duration: 100 * MS_PER_SEC });
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [anim], changed: [], removed: [] }],
+ "records after animation is added");
+
+ anim.effect.target = div;
+ assert_equals_records(observer.takeRecords(),
+ [], "no records after setting the same target");
+
+ anim.effect.target = null;
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [], removed: [anim] }],
+ "records after setting null");
+
+ anim.effect.target = null;
+ assert_equals_records(observer.takeRecords(),
+ [], "records after setting redundant null");
+ }, "set_redundant_animation_target");
+
+ test(t => {
+ var div = addDiv(t);
+ var observer =
+ setupSynchronousObserver(t,
+ aOptions.subtree ? div.parentNode : div,
+ aOptions.subtree);
+
+ var anim = div.animate({ opacity: [ 0, 1 ] },
+ { duration: 100 * MS_PER_SEC });
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [anim], changed: [], removed: [] }],
+ "records after animation is added");
+
+ anim.effect = null;
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [], removed: [anim] }],
+ "records after animation is removed");
+ }, "set_null_animation_effect");
+
+ test(t => {
+ var div = addDiv(t);
+ var observer =
+ setupSynchronousObserver(t,
+ aOptions.subtree ? div.parentNode : div,
+ aOptions.subtree);
+
+ var anim = new Animation();
+ anim.play();
+ anim.effect = new KeyframeEffect(div, { opacity: [ 0, 1 ] },
+ 100 * MS_PER_SEC);
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [anim], changed: [], removed: [] }],
+ "records after animation is added");
+ }, "set_effect_on_null_effect_animation");
+
+ test(t => {
+ var div = addDiv(t);
+ var observer =
+ setupSynchronousObserver(t,
+ aOptions.subtree ? div.parentNode : div,
+ aOptions.subtree);
+
+ var anim = div.animate({ marginLeft: [ "0px", "100px" ] },
+ 100 * MS_PER_SEC);
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [anim], changed: [], removed: [] }],
+ "records after animation is added");
+
+ anim.effect = new KeyframeEffect(div, { opacity: [ 0, 1 ] },
+ 100 * MS_PER_SEC);
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [anim], removed: [] }],
+ "records after replace effects");
+ }, "replace_effect_targeting_on_the_same_element");
+
+ test(t => {
+ var div = addDiv(t);
+ var observer =
+ setupSynchronousObserver(t,
+ aOptions.subtree ? div.parentNode : div,
+ aOptions.subtree);
+
+ var anim = div.animate({ marginLeft: [ "0px", "100px" ] },
+ 100 * MS_PER_SEC);
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [anim], changed: [], removed: [] }],
+ "records after animation is added");
+
+ anim.currentTime = 60 * MS_PER_SEC;
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [anim], removed: [] }],
+ "records after animation is changed");
+
+ anim.effect = new KeyframeEffect(div, { opacity: [ 0, 1 ] },
+ 50 * MS_PER_SEC);
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [], removed: [anim] }],
+ "records after replacing effects");
+ }, "replace_effect_targeting_on_the_same_element_not_in_effect");
+
+ test(t => {
+ var div = addDiv(t);
+ var observer =
+ setupSynchronousObserver(t,
+ aOptions.subtree ? div.parentNode : div,
+ aOptions.subtree);
+
+ var anim = div.animate([ { marginLeft: "0px" },
+ { marginLeft: "-20px" },
+ { marginLeft: "100px" },
+ { marginLeft: "50px" } ],
+ { duration: 100 * MS_PER_SEC });
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [anim], changed: [], removed: [] }],
+ "records after animation is added");
+
+ anim.effect.spacing = "paced(margin-left)";
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [anim], removed: [] }],
+ "records after animation is changed");
+ }, "set_spacing");
+
+ test(t => {
+ var div = addDiv(t);
+ var observer =
+ setupSynchronousObserver(t,
+ aOptions.subtree ? div.parentNode : div,
+ aOptions.subtree);
+
+ var anim = div.animate([ { marginLeft: "0px" },
+ { marginLeft: "-20px" },
+ { marginLeft: "100px" },
+ { marginLeft: "50px" } ],
+ { duration: 100 * MS_PER_SEC,
+ spacing: "paced(margin-left)" });
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [anim], changed: [], removed: [] }],
+ "records after animation is added");
+
+ anim.effect.spacing = "paced(animation-duration)";
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [anim], removed: [] }],
+ "records after setting a non-animatable paced property");
+ }, "set_spacing_on_a_non-animatable_property");
+
+ test(t => {
+ var div = addDiv(t);
+ var observer =
+ setupSynchronousObserver(t,
+ aOptions.subtree ? div.parentNode : div,
+ aOptions.subtree);
+
+ var anim = div.animate([ { marginLeft: "0px" },
+ { marginLeft: "-20px" },
+ { marginLeft: "100px" },
+ { marginLeft: "50px" } ],
+ { duration: 100 * MS_PER_SEC,
+ spacing: "paced(margin-left)" });
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [anim], changed: [], removed: [] }],
+ "records after animation is added");
+
+ anim.effect.spacing = "paced(margin-left)";
+ assert_equals_records(observer.takeRecords(),
+ [], "no record after setting the same spacing");
+ }, "set_the_same_spacing");
+
+ // Test that starting a single animation that is cancelled by calling
+ // cancel() dispatches an added notification and then a removed
+ // notification.
+ test(t => {
+ var div = addDiv(t, { style: "animation: anim 100s forwards" });
+ var observer =
+ setupSynchronousObserver(t,
+ aOptions.subtree ? div.parentNode : div,
+ aOptions.subtree);
+
+ var animations = div.getAnimations();
+ assert_equals(animations.length, 1,
+ "getAnimations().length after animation start");
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: animations, changed: [], removed: [] }],
+ "records after animation start");
+
+ animations[0].cancel();
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [], removed: animations }],
+ "records after animation end");
+
+ // Re-trigger the animation.
+ animations[0].play();
+
+ // Single MutationRecord for the Animation (re-)addition.
+ assert_equals_records(observer.takeRecords(),
+ [{ added: animations, changed: [], removed: [] }],
+ "records after animation start");
+ }, "single_animation_cancelled_api");
+
+ // Test that updating a property on the Animation object dispatches a changed
+ // notification.
+ [
+ { prop: "playbackRate", val: 0.5 },
+ { prop: "startTime", val: 50 * MS_PER_SEC },
+ { prop: "currentTime", val: 50 * MS_PER_SEC },
+ ].forEach(function(aChangeTest) {
+ test(t => {
+ // We use a forwards fill mode so that even if the change we make causes
+ // the animation to become finished, it will still be "relevant" so we
+ // won't mark it as removed.
+ var div = addDiv(t, { style: "animation: anim 100s forwards" });
+ var observer =
+ setupSynchronousObserver(t,
+ aOptions.subtree ? div.parentNode : div,
+ aOptions.subtree);
+
+ var animations = div.getAnimations();
+ assert_equals(animations.length, 1,
+ "getAnimations().length after animation start");
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: animations, changed: [], removed: [] }],
+ "records after animation start");
+
+ // Update the property.
+ animations[0][aChangeTest.prop] = aChangeTest.val;
+
+ // Make a redundant change.
+ animations[0][aChangeTest.prop] = animations[0][aChangeTest.prop];
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: animations, removed: [] }],
+ "records after animation property change");
+ }, `single_animation_api_change_${aChangeTest.prop}`);
+ });
+
+ // Test that making a redundant change to currentTime while an Animation
+ // is pause-pending still generates a change MutationRecord since setting
+ // the currentTime to any value in this state aborts the pending pause.
+ test(t => {
+ var div = addDiv(t, { style: "animation: anim 100s" });
+ var observer =
+ setupSynchronousObserver(t,
+ aOptions.subtree ? div.parentNode : div,
+ aOptions.subtree);
+
+ var animations = div.getAnimations();
+ assert_equals(animations.length, 1,
+ "getAnimations().length after animation start");
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: animations, changed: [], removed: [] }],
+ "records after animation start");
+
+ animations[0].pause();
+
+ // We are now pause-pending. Even if we make a redundant change to the
+ // currentTime, we should still get a change record because setting the
+ // currentTime while pause-pending has the effect of cancelling a pause.
+ animations[0].currentTime = animations[0].currentTime;
+
+ // Two MutationRecords for the Animation changes: one for pausing, one
+ // for aborting the pause.
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: animations, removed: [] },
+ { added: [], changed: animations, removed: [] }],
+ "records after pausing then seeking");
+ }, "change_currentTime_while_pause_pending");
+
+ // Test that calling finish() on a forwards-filling Animation dispatches
+ // a changed notification.
+ test(t => {
+ var div = addDiv(t, { style: "animation: anim 100s forwards" });
+ var observer =
+ setupSynchronousObserver(t,
+ aOptions.subtree ? div.parentNode : div,
+ aOptions.subtree);
+
+ var animations = div.getAnimations();
+ assert_equals(animations.length, 1,
+ "getAnimations().length after animation start");
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: animations, changed: [], removed: [] }],
+ "records after animation start");
+
+ animations[0].finish();
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: animations, removed: [] }],
+ "records after finish()");
+
+ // Redundant finish.
+ animations[0].finish();
+
+ // Ensure no change records.
+ assert_equals_records(observer.takeRecords(),
+ [], "records after redundant finish()");
+ }, "finish_with_forwards_fill");
+
+ // Test that calling finish() on an Animation that does not fill forwards,
+ // dispatches a removal notification.
+ test(t => {
+ var div = addDiv(t, { style: "animation: anim 100s" });
+ var observer =
+ setupSynchronousObserver(t,
+ aOptions.subtree ? div.parentNode : div,
+ aOptions.subtree);
+
+ var animations = div.getAnimations();
+ assert_equals(animations.length, 1,
+ "getAnimations().length after animation start");
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: animations, changed: [], removed: [] }],
+ "records after animation start");
+
+ animations[0].finish();
+
+ // Single MutationRecord for the Animation removal.
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [], removed: animations }],
+ "records after finishing");
+ }, "finish_without_fill");
+
+ // Test that calling finish() on a forwards-filling Animation dispatches
+ test(t => {
+ var div = addDiv(t, { style: "animation: anim 100s" });
+ var observer =
+ setupSynchronousObserver(t,
+ aOptions.subtree ? div.parentNode : div,
+ aOptions.subtree);
+
+ var animation = div.getAnimations()[0];
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [animation], changed: [], removed: []}],
+ "records after creation");
+ animation.id = "new id";
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [animation], removed: []}],
+ "records after id is changed");
+
+ animation.id = "new id";
+ assert_equals_records(observer.takeRecords(),
+ [], "records after assigning same value with id");
+ }, "change_id");
+
+ // Test that calling reverse() dispatches a changed notification.
+ test(t => {
+ var div = addDiv(t, { style: "animation: anim 100s both" });
+ var observer =
+ setupSynchronousObserver(t,
+ aOptions.subtree ? div.parentNode : div,
+ aOptions.subtree);
+
+ var animations = div.getAnimations();
+ assert_equals(animations.length, 1,
+ "getAnimations().length after animation start");
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: animations, changed: [], removed: [] }],
+ "records after animation start");
+
+ animations[0].reverse();
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: animations, removed: [] }],
+ "records after calling reverse()");
+ }, "reverse");
+
+ // Test that calling reverse() does *not* dispatch a changed notification
+ // when playbackRate == 0.
+ test(t => {
+ var div = addDiv(t, { style: "animation: anim 100s both" });
+ var observer =
+ setupSynchronousObserver(t,
+ aOptions.subtree ? div.parentNode : div,
+ aOptions.subtree);
+
+ var animations = div.getAnimations();
+ assert_equals(animations.length, 1,
+ "getAnimations().length after animation start");
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: animations, changed: [], removed: [] }],
+ "records after animation start");
+
+ // Seek to the middle and set playbackRate to zero.
+ animations[0].currentTime = 50 * MS_PER_SEC;
+ animations[0].playbackRate = 0;
+
+ // Two MutationRecords, one for each change.
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: animations, removed: [] },
+ { added: [], changed: animations, removed: [] }],
+ "records after seeking and setting playbackRate");
+
+ animations[0].reverse();
+
+ // We should get no notifications.
+ assert_equals_records(observer.takeRecords(),
+ [], "records after calling reverse()");
+ }, "reverse_with_zero_playbackRate");
+
+ // Test that attempting to start an animation that should already be finished
+ // does not send any notifications.
+ test(t => {
+ // Start an animation that should already be finished.
+ var div = addDiv(t, { style: "animation: anim 1s -2s;" });
+ var observer =
+ setupSynchronousObserver(t,
+ aOptions.subtree ? div.parentNode : div,
+ aOptions.subtree);
+
+ // The animation should cause no Animations to be created.
+ var animations = div.getAnimations();
+ assert_equals(animations.length, 0,
+ "getAnimations().length after animation start");
+
+ // And we should get no notifications.
+ assert_equals_records(observer.takeRecords(),
+ [], "records after attempted animation start");
+ }, "already_finished");
+
+ test(t => {
+ var div = addDiv(t, { style: "animation: anim 100s, anotherAnim 100s" });
+ var observer =
+ setupSynchronousObserver(t,
+ aOptions.subtree ? div.parentNode : div,
+ aOptions.subtree);
+
+ var animations = div.getAnimations();
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: animations, changed: [], removed: []}],
+ "records after creation");
+
+ div.style.animation = "anotherAnim 100s, anim 100s";
+ animations = div.getAnimations();
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: animations, removed: []}],
+ "records after the order is changed");
+
+ div.style.animation = "anotherAnim 100s, anim 100s";
+
+ assert_equals_records(observer.takeRecords(),
+ [], "no records after applying the same order");
+ }, "animtion_order_change");
+
+});
+
+test(t => {
+ var div = addDiv(t);
+ var observer = setupSynchronousObserver(t, div, true);
+
+ var child = document.createElement("div");
+ div.appendChild(child);
+
+ var anim1 = div.animate({ marginLeft: [ "0px", "50px" ] },
+ 100 * MS_PER_SEC);
+ var anim2 = child.animate({ marginLeft: [ "0px", "100px" ] },
+ 50 * MS_PER_SEC);
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [anim1], changed: [], removed: [] },
+ { added: [anim2], changed: [], removed: [] }],
+ "records after animation is added");
+
+ // After setting a new effect, we remove the current animation, anim1,
+ // because it is no longer attached to |div|, and then remove the previous
+ // animation, anim2. Finally, add back the anim1 which is in effect on
+ // |child| now. In addition, we sort them by tree order and they are
+ // batched.
+ anim1.effect = anim2.effect;
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [], removed: [anim1] }, // div
+ { added: [anim1], changed: [], removed: [anim2] }], // child
+ "records after animation effects are changed");
+}, "set_effect_with_previous_animation");
+
+test(t => {
+ var div = addDiv(t);
+ var observer = setupSynchronousObserver(t, document, true);
+
+ var anim = div.animate({ opacity: [ 0, 1 ] },
+ { duration: 100 * MS_PER_SEC });
+
+ var newTarget = document.createElement("div");
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [anim], changed: [], removed: [] }],
+ "records after animation is added");
+
+ anim.effect.target = null;
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [], removed: [anim] }],
+ "records after setting null");
+
+ anim.effect.target = div;
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [anim], changed: [], removed: [] }],
+ "records after setting a target");
+
+ anim.effect.target = addDiv(t);
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [], removed: [anim] },
+ { added: [anim], changed: [], removed: [] }],
+ "records after setting a different target");
+}, "set_animation_target");
+
+test(t => {
+ var div = addDiv(t);
+ var pseudoTarget = createPseudo(t, div, 'before');
+ var observer = setupSynchronousObserver(t, div, true);
+
+ var anim = pseudoTarget.animate({ opacity: [ 0, 1 ] }, 200 * MS_PER_SEC);
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [anim], changed: [], removed: [] }],
+ "records after animation is added");
+
+ anim.effect.timing.duration = 100 * MS_PER_SEC;
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [anim], removed: [] }],
+ "records after duration is changed");
+
+ anim.effect.timing.duration = 100 * MS_PER_SEC;
+ assert_equals_records(observer.takeRecords(),
+ [], "records after assigning same value");
+
+ anim.currentTime = anim.effect.timing.duration * 2;
+ anim.finish();
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [], removed: [anim] }],
+ "records after animation end");
+
+ anim.effect.timing.duration = anim.effect.timing.duration * 3;
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [anim], changed: [], removed: [] }],
+ "records after animation restarted");
+
+ anim.effect.timing.duration = "auto";
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [], removed: [anim] }],
+ "records after duration set \"auto\"");
+
+ anim.effect.timing.duration = "auto";
+ assert_equals_records(observer.takeRecords(),
+ [], "records after assigning same value \"auto\"");
+}, "change_duration_and_currenttime_on_pseudo_elements");
+
+test(t => {
+ var div = addDiv(t);
+ var pseudoTarget = createPseudo(t, div, 'before');
+ var observer = setupSynchronousObserver(t, div, false);
+
+ var anim = div.animate({ opacity: [ 0, 1 ] },
+ { duration: 100 * MS_PER_SEC });
+ var pAnim = pseudoTarget.animate({ opacity: [ 0, 1 ] },
+ { duration: 100 * MS_PER_SEC });
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [anim], changed: [], removed: [] }],
+ "records after animation is added");
+
+ anim.finish();
+ pAnim.finish();
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [], removed: [anim] }],
+ "records after animation is finished");
+}, "exclude_animations_targeting_pseudo_elements");
+
+</script>
diff --git a/dom/animation/test/chrome/test_restyles.html b/dom/animation/test/chrome/test_restyles.html
new file mode 100644
index 0000000000..e59967c19d
--- /dev/null
+++ b/dom/animation/test/chrome/test_restyles.html
@@ -0,0 +1,815 @@
+<!doctype html>
+<head>
+<meta charset=utf-8>
+<title>Tests restyles caused by animations</title>
+<script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+<script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
+<script src="chrome://mochikit/content/tests/SimpleTest/SpawnTask.js"></script>
+<script src="chrome://mochikit/content/tests/SimpleTest/paint_listener.js"></script>
+<script src="../testcommon.js"></script>
+<link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+<style>
+@keyframes opacity {
+ from { opacity: 1; }
+ to { opacity: 0; }
+}
+@keyframes background-color {
+ from { background-color: red; }
+ to { background-color: blue; }
+}
+@keyframes rotate {
+ from { transform: rotate(0deg); }
+ to { transform: rotate(360deg); }
+}
+div {
+ /* Element needs geometry to be eligible for layerization */
+ width: 100px;
+ height: 100px;
+ background-color: white;
+}
+</style>
+</head>
+<body>
+<script>
+'use strict';
+
+function observeStyling(frameCount, onFrame) {
+ var docShell = window.QueryInterface(SpecialPowers.Ci.nsIInterfaceRequestor)
+ .getInterface(SpecialPowers.Ci.nsIWebNavigation)
+ .QueryInterface(SpecialPowers.Ci.nsIDocShell);
+
+ docShell.recordProfileTimelineMarkers = true;
+ docShell.popProfileTimelineMarkers();
+
+ return new Promise(function(resolve) {
+ return waitForAnimationFrames(frameCount, onFrame).then(function() {
+ var markers = docShell.popProfileTimelineMarkers();
+ docShell.recordProfileTimelineMarkers = false;
+ var stylingMarkers = markers.filter(function(marker, index) {
+ return marker.name == 'Styles' &&
+ (marker.restyleHint == 'eRestyle_CSSAnimations' ||
+ marker.restyleHint == 'eRestyle_CSSTransitions');
+ });
+ resolve(stylingMarkers);
+ });
+ });
+}
+
+function ensureElementRemoval(aElement) {
+ return new Promise(function(resolve) {
+ aElement.remove();
+ waitForAllPaintsFlushed(resolve);
+ });
+}
+
+SimpleTest.waitForExplicitFinish();
+
+var omtaEnabled = isOMTAEnabled();
+
+var isAndroid = !!navigator.userAgent.includes("Android");
+
+function add_task_if_omta_enabled(test) {
+ if (!omtaEnabled) {
+ info(test.name + " is skipped because OMTA is disabled");
+ return;
+ }
+ add_task(test);
+}
+
+// We need to wait for all paints before running tests to avoid contaminations
+// from styling of this document itself.
+waitForAllPaints(function() {
+ add_task(function* restyling_for_main_thread_animations() {
+ var div = addDiv(null, { style: 'animation: background-color 100s' });
+ var animation = div.getAnimations()[0];
+
+ yield animation.ready;
+ ok(!animation.isRunningOnCompositor);
+
+ var markers = yield observeStyling(5);
+ is(markers.length, 5,
+ 'CSS animations running on the main-thread should update style ' +
+ 'on the main thread');
+ yield ensureElementRemoval(div);
+ });
+
+ add_task_if_omta_enabled(function* no_restyling_for_compositor_animations() {
+ var div = addDiv(null, { style: 'animation: opacity 100s' });
+ var animation = div.getAnimations()[0];
+
+ yield animation.ready;
+ ok(animation.isRunningOnCompositor);
+
+ var markers = yield observeStyling(5);
+ is(markers.length, 0,
+ 'CSS animations running on the compositor should not update style ' +
+ 'on the main thread');
+ yield ensureElementRemoval(div);
+ });
+
+ add_task_if_omta_enabled(function* no_restyling_for_compositor_transitions() {
+ var div = addDiv(null, { style: 'transition: opacity 100s; opacity: 0' });
+ getComputedStyle(div).opacity;
+ div.style.opacity = 1;
+
+ var animation = div.getAnimations()[0];
+
+ yield animation.ready;
+ ok(animation.isRunningOnCompositor);
+
+ var markers = yield observeStyling(5);
+ is(markers.length, 0,
+ 'CSS transitions running on the compositor should not update style ' +
+ 'on the main thread');
+ yield ensureElementRemoval(div);
+ });
+
+ add_task_if_omta_enabled(function* no_restyling_when_animation_duration_is_changed() {
+ var div = addDiv(null, { style: 'animation: opacity 100s' });
+ var animation = div.getAnimations()[0];
+
+ yield animation.ready;
+ ok(animation.isRunningOnCompositor);
+
+ div.animationDuration = '200s';
+
+ var markers = yield observeStyling(5);
+ is(markers.length, 0,
+ 'Animations running on the compositor should not update style ' +
+ 'on the main thread');
+ yield ensureElementRemoval(div);
+ });
+
+ add_task_if_omta_enabled(function* only_one_restyling_after_finish_is_called() {
+ var div = addDiv(null, { style: 'animation: opacity 100s' });
+ var animation = div.getAnimations()[0];
+
+ yield animation.ready;
+ ok(animation.isRunningOnCompositor);
+
+ animation.finish();
+
+ var markers = yield observeStyling(5);
+ is(markers.length, 1,
+ 'Animations running on the compositor should only update style ' +
+ 'once after finish() is called');
+ yield ensureElementRemoval(div);
+ });
+
+ add_task(function* no_restyling_mouse_movement_on_finished_transition() {
+ var div = addDiv(null, { style: 'transition: opacity 1ms; opacity: 0' });
+ getComputedStyle(div).opacity;
+ div.style.opacity = 1;
+
+ var animation = div.getAnimations()[0];
+ var initialRect = div.getBoundingClientRect();
+
+ yield animation.finished;
+
+ var mouseX = initialRect.left + initialRect.width / 2;
+ var mouseY = initialRect.top + initialRect.height / 2;
+ var markers = yield observeStyling(5, function() {
+ // We can't use synthesizeMouse here since synthesizeMouse causes
+ // layout flush.
+ synthesizeMouseAtPoint(mouseX++, mouseY++,
+ { type: 'mousemove' }, window);
+ });
+
+ is(markers.length, 0,
+ 'Bug 1219236: Finished transitions should never cause restyles ' +
+ 'when mouse is moved on the animations');
+ yield ensureElementRemoval(div);
+ });
+
+ add_task(function* no_restyling_mouse_movement_on_finished_animation() {
+ var div = addDiv(null, { style: 'animation: opacity 1ms' });
+ var animation = div.getAnimations()[0];
+
+ var initialRect = div.getBoundingClientRect();
+
+ yield animation.finished;
+
+ var mouseX = initialRect.left + initialRect.width / 2;
+ var mouseY = initialRect.top + initialRect.height / 2;
+ var markers = yield observeStyling(5, function() {
+ // We can't use synthesizeMouse here since synthesizeMouse causes
+ // layout flush.
+ synthesizeMouseAtPoint(mouseX++, mouseY++,
+ { type: 'mousemove' }, window);
+ });
+
+ is(markers.length, 0,
+ 'Bug 1219236: Finished animations should never cause restyles ' +
+ 'when mouse is moved on the animations');
+ yield ensureElementRemoval(div);
+ });
+
+ add_task_if_omta_enabled(function* no_restyling_compositor_animations_out_of_view_element() {
+ if (!SpecialPowers.getBoolPref('dom.animations.offscreen-throttling')) {
+ return;
+ }
+
+ var div = addDiv(null,
+ { style: 'animation: opacity 100s; transform: translateY(-400px);' });
+ var animation = div.getAnimations()[0];
+
+ yield animation.ready;
+ ok(!animation.isRunningOnCompositor);
+
+ var markers = yield observeStyling(5);
+
+ is(markers.length, 0,
+ 'Animations running on the compositor in an out-of-view element ' +
+ 'should never cause restyles');
+ yield ensureElementRemoval(div);
+ });
+
+ add_task(function* no_restyling_main_thread_animations_out_of_view_element() {
+ if (!SpecialPowers.getBoolPref('dom.animations.offscreen-throttling')) {
+ return;
+ }
+
+ var div = addDiv(null,
+ { style: 'animation: background-color 100s; transform: translateY(-400px);' });
+ var animation = div.getAnimations()[0];
+
+ yield animation.ready;
+ var markers = yield observeStyling(5);
+
+ is(markers.length, 0,
+ 'Animations running on the main-thread in an out-of-view element ' +
+ 'should never cause restyles');
+ yield ensureElementRemoval(div);
+ });
+
+ add_task_if_omta_enabled(function* no_restyling_compositor_animations_in_scrolled_out_element() {
+ if (!SpecialPowers.getBoolPref('dom.animations.offscreen-throttling')) {
+ return;
+ }
+
+ /*
+ On Android the opacity animation runs on the compositor even if it is
+ scrolled out of view. We will fix this in bug 1247800.
+ */
+ if (isAndroid) {
+ return;
+ }
+ var parentElement = addDiv(null,
+ { style: 'overflow-y: scroll; height: 20px;' });
+ var div = addDiv(null,
+ { style: 'animation: opacity 100s; position: relative; top: 100px;' });
+ parentElement.appendChild(div);
+ var animation = div.getAnimations()[0];
+
+ yield animation.ready;
+
+ var markers = yield observeStyling(5);
+
+ is(markers.length, 0,
+ 'Animations running on the compositor for elements ' +
+ 'which are scrolled out should never cause restyles');
+
+ yield ensureElementRemoval(parentElement);
+ });
+
+ add_task(function* no_restyling_main_thread_animations_in_scrolled_out_element() {
+ if (!SpecialPowers.getBoolPref('dom.animations.offscreen-throttling')) {
+ return;
+ }
+
+ /*
+ On Android throttled animations are left behind on the main thread in some
+ frames, We will fix this in bug 1247800.
+ */
+ if (isAndroid) {
+ return;
+ }
+
+ var parentElement = addDiv(null,
+ { style: 'overflow-y: scroll; height: 20px;' });
+ var div = addDiv(null,
+ { style: 'animation: background-color 100s; position: relative; top: 100px;' });
+ parentElement.appendChild(div);
+ var animation = div.getAnimations()[0];
+
+ yield animation.ready;
+ var markers = yield observeStyling(5);
+
+ is(markers.length, 0,
+ 'Animations running on the main-thread for elements ' +
+ 'which are scrolled out should never cause restyles');
+
+ yield ensureElementRemoval(parentElement);
+ });
+
+ add_task(function* no_restyling_main_thread_animations_in_nested_scrolled_out_element() {
+ if (!SpecialPowers.getBoolPref('dom.animations.offscreen-throttling')) {
+ return;
+ }
+
+ /*
+ On Android throttled animations are left behind on the main thread in some
+ frames, We will fix this in bug 1247800.
+ */
+ if (isAndroid) {
+ return;
+ }
+
+ var grandParent = addDiv(null,
+ { style: 'overflow-y: scroll; height: 20px;' });
+ var parentElement = addDiv(null,
+ { style: 'overflow-y: scroll; height: 100px;' });
+ var div = addDiv(null,
+ { style: 'animation: background-color 100s; position: relative; top: 100px;' });
+ grandParent.appendChild(parentElement);
+ parentElement.appendChild(div);
+ var animation = div.getAnimations()[0];
+
+ yield animation.ready;
+ var markers = yield observeStyling(5);
+
+ is(markers.length, 0,
+ 'Animations running on the main-thread which are in nested elements ' +
+ 'which are scrolled out should never cause restyles');
+
+ yield ensureElementRemoval(grandParent);
+ });
+
+ add_task_if_omta_enabled(function* no_restyling_compositor_animations_in_visiblily_hidden_element() {
+ var div = addDiv(null,
+ { style: 'animation: opacity 100s; visibility: hidden' });
+ var animation = div.getAnimations()[0];
+
+ yield animation.ready;
+ ok(!animation.isRunningOnCompositor);
+
+ var markers = yield observeStyling(5);
+
+ todo_is(markers.length, 0,
+ 'Bug 1237454: Animations running on the compositor in ' +
+ 'visibility hidden element should never cause restyles');
+ yield ensureElementRemoval(div);
+ });
+
+ add_task(function* restyling_main_thread_animations_moved_in_view_by_scrolling() {
+ if (!SpecialPowers.getBoolPref('dom.animations.offscreen-throttling')) {
+ return;
+ }
+
+ /*
+ On Android throttled animations are left behind on the main thread in some
+ frames, We will fix this in bug 1247800.
+ */
+ if (isAndroid) {
+ return;
+ }
+
+ var parentElement = addDiv(null,
+ { style: 'overflow-y: scroll; height: 20px;' });
+ var div = addDiv(null,
+ { style: 'animation: background-color 100s; position: relative; top: 100px;' });
+ parentElement.appendChild(div);
+ var animation = div.getAnimations()[0];
+
+ var parentRect = parentElement.getBoundingClientRect();
+ var centerX = parentRect.left + parentRect.width / 2;
+ var centerY = parentRect.top + parentRect.height / 2;
+
+ yield animation.ready;
+
+ var markers = yield observeStyling(1, function() {
+ // We can't use synthesizeWheel here since synthesizeWheel causes
+ // layout flush.
+ synthesizeWheelAtPoint(centerX, centerY,
+ { deltaMode: WheelEvent.DOM_DELTA_PIXEL,
+ deltaY: 100 });
+ });
+
+ is(markers.length, 1,
+ 'Animations running on the main-thread which were in scrolled out ' +
+ 'elements should update restyling soon after the element moved in ' +
+ 'view by scrolling');
+
+ yield ensureElementRemoval(parentElement);
+ });
+
+ add_task(function* restyling_main_thread_animations_moved_in_view_by_scrolling() {
+ if (!SpecialPowers.getBoolPref('dom.animations.offscreen-throttling')) {
+ return;
+ }
+
+ var grandParent = addDiv(null,
+ { style: 'overflow-y: scroll; height: 20px;' });
+ var parentElement = addDiv(null,
+ { style: 'overflow-y: scroll; height: 200px;' });
+ var div = addDiv(null,
+ { style: 'animation: background-color 100s; position: relative; top: 100px;' });
+ grandParent.appendChild(parentElement);
+ parentElement.appendChild(div);
+ var animation = div.getAnimations()[0];
+
+ var parentRect = grandParent.getBoundingClientRect();
+ var centerX = parentRect.left + parentRect.width / 2;
+ var centerY = parentRect.top + parentRect.height / 2;
+
+ yield animation.ready;
+
+ var markers = yield observeStyling(1, function() {
+ // We can't use synthesizeWheel here since synthesizeWheel causes
+ // layout flush.
+ synthesizeWheelAtPoint(centerX, centerY,
+ { deltaMode: WheelEvent.DOM_DELTA_PIXEL,
+ deltaY: 100 });
+ });
+
+ // FIXME: We should reduce a redundant restyle here.
+ ok(markers.length >= 1,
+ 'Animations running on the main-thread which were in nested scrolled ' +
+ 'out elements should update restyle soon after the element moved ' +
+ 'in view by scrolling');
+
+ yield ensureElementRemoval(grandParent);
+ });
+
+ add_task(function* restyling_main_thread_animations_move_out_of_view_by_scrolling() {
+ if (!SpecialPowers.getBoolPref('dom.animations.offscreen-throttling')) {
+ return;
+ }
+
+ /*
+ On Android throttled animations are left behind on the main thread in some
+ frames, We will fix this in bug 1247800.
+ */
+ if (isAndroid) {
+ return;
+ }
+
+ var parentElement = addDiv(null,
+ { style: 'overflow-y: scroll; height: 200px;' });
+ var div = addDiv(null,
+ { style: 'animation: background-color 100s;' });
+ var pad = addDiv(null,
+ { style: 'height: 400px;' });
+ parentElement.appendChild(div);
+ parentElement.appendChild(pad);
+ var animation = div.getAnimations()[0];
+
+ var parentRect = parentElement.getBoundingClientRect();
+ var centerX = parentRect.left + parentRect.width / 2;
+ var centerY = parentRect.top + parentRect.height / 2;
+
+ yield animation.ready;
+
+ // We can't use synthesizeWheel here since synthesizeWheel causes
+ // layout flush.
+ synthesizeWheelAtPoint(centerX, centerY,
+ { deltaMode: WheelEvent.DOM_DELTA_PIXEL,
+ deltaY: 200 });
+
+ var markers = yield observeStyling(5);
+
+ // FIXME: We should reduce a redundant restyle here.
+ ok(markers.length >= 0,
+ 'Animations running on the main-thread which are in scrolled out ' +
+ 'elements should throttle restyling');
+
+ yield ensureElementRemoval(parentElement);
+ });
+
+ add_task(function* restyling_main_thread_animations_moved_in_view_by_resizing() {
+ if (!SpecialPowers.getBoolPref('dom.animations.offscreen-throttling')) {
+ return;
+ }
+
+ var parentElement = addDiv(null,
+ { style: 'overflow-y: scroll; height: 20px;' });
+ var div = addDiv(null,
+ { style: 'animation: background-color 100s; position: relative; top: 100px;' });
+ parentElement.appendChild(div);
+ var animation = div.getAnimations()[0];
+
+ yield animation.ready;
+
+ var markers = yield observeStyling(1, function() {
+ parentElement.style.height = '100px';
+ });
+
+ is(markers.length, 1,
+ 'Animations running on the main-thread which was in scrolled out ' +
+ 'elements should update restyling soon after the element moved in ' +
+ 'view by resizing');
+
+ yield ensureElementRemoval(parentElement);
+ });
+
+ add_task(function* no_restyling_main_thread_animations_in_visiblily_hidden_element() {
+ var div = addDiv(null,
+ { style: 'animation: background-color 100s; visibility: hidden' });
+ var animation = div.getAnimations()[0];
+
+ yield animation.ready;
+ var markers = yield observeStyling(5);
+
+ todo_is(markers.length, 0,
+ 'Bug 1237454: Animations running on the main-thread in ' +
+ 'visibility hidden element should never cause restyles');
+ yield ensureElementRemoval(div);
+ });
+
+ add_task_if_omta_enabled(function* no_restyling_compositor_animations_after_pause_is_called() {
+ var div = addDiv(null, { style: 'animation: opacity 100s' });
+ var animation = div.getAnimations()[0];
+
+ yield animation.ready;
+ ok(animation.isRunningOnCompositor);
+
+ animation.pause();
+
+ yield animation.ready;
+
+ var markers = yield observeStyling(5);
+ is(markers.length, 0,
+ 'Bug 1232563: Paused animations running on the compositor should ' +
+ 'never cause restyles once after pause() is called');
+ yield ensureElementRemoval(div);
+ });
+
+ add_task(function* no_restyling_main_thread_animations_after_pause_is_called() {
+ var div = addDiv(null, { style: 'animation: background-color 100s' });
+ var animation = div.getAnimations()[0];
+
+ yield animation.ready;
+
+ animation.pause();
+
+ yield animation.ready;
+
+ var markers = yield observeStyling(5);
+ is(markers.length, 0,
+ 'Bug 1232563: Paused animations running on the main-thread should ' +
+ 'never cause restyles after pause() is called');
+ yield ensureElementRemoval(div);
+ });
+
+ add_task_if_omta_enabled(function* only_one_restyling_when_current_time_is_set_to_middle_of_duration() {
+ var div = addDiv(null, { style: 'animation: opacity 100s' });
+ var animation = div.getAnimations()[0];
+
+ yield animation.ready;
+
+ animation.currentTime = 50 * MS_PER_SEC;
+
+ var markers = yield observeStyling(5);
+ is(markers.length, 1,
+ 'Bug 1235478: Animations running on the compositor should only once ' +
+ 'update style when currentTime is set to middle of duration time');
+ yield ensureElementRemoval(div);
+ });
+
+ add_task_if_omta_enabled(function* change_duration_and_currenttime() {
+ var div = addDiv(null);
+ var animation = div.animate({ opacity: [ 0, 1 ] }, 100 * MS_PER_SEC);
+
+ yield animation.ready;
+ ok(animation.isRunningOnCompositor);
+
+ // Set currentTime to a time longer than duration.
+ animation.currentTime = 500 * MS_PER_SEC;
+
+ // Now the animation immediately get back from compositor.
+ ok(!animation.isRunningOnCompositor);
+
+ // Extend the duration.
+ animation.effect.timing.duration = 800 * MS_PER_SEC;
+ var markers = yield observeStyling(5);
+ is(markers.length, 1,
+ 'Animations running on the compositor should update style ' +
+ 'when timing.duration is made longer than the current time');
+
+ yield ensureElementRemoval(div);
+ });
+
+ add_task(function* script_animation_on_display_none_element() {
+ var div = addDiv(null);
+ var animation = div.animate({ backgroundColor: [ 'red', 'blue' ] },
+ 100 * MS_PER_SEC);
+
+ yield animation.ready;
+
+ div.style.display = 'none';
+
+ // We need to wait a frame to apply display:none style.
+ yield waitForFrame();
+
+ is(animation.playState, 'running',
+ 'Script animations keep running even when the target element has ' +
+ '"display: none" style');
+
+ ok(!animation.isRunningOnCompositor,
+ 'Script animations on "display:none" element should not run on the ' +
+ 'compositor');
+
+ var markers = yield observeStyling(5);
+ is(markers.length, 0,
+ 'Script animations on "display: none" element should not update styles');
+
+ div.style.display = '';
+
+ // We need to wait a frame to unapply display:none style.
+ yield waitForFrame();
+
+ var markers = yield observeStyling(5);
+ is(markers.length, 5,
+ 'Script animations restored from "display: none" state should update ' +
+ 'styles');
+
+ yield ensureElementRemoval(div);
+ });
+
+ add_task_if_omta_enabled(function* compositable_script_animation_on_display_none_element() {
+ var div = addDiv(null);
+ var animation = div.animate({ opacity: [ 0, 1 ] }, 100 * MS_PER_SEC);
+
+ yield animation.ready;
+
+ div.style.display = 'none';
+
+ // We need to wait a frame to apply display:none style.
+ yield waitForFrame();
+
+ is(animation.playState, 'running',
+ 'Opacity script animations keep running even when the target element ' +
+ 'has "display: none" style');
+
+ ok(!animation.isRunningOnCompositor,
+ 'Opacity script animations on "display:none" element should not ' +
+ 'run on the compositor');
+
+ var markers = yield observeStyling(5);
+ is(markers.length, 0,
+ 'Opacity script animations on "display: none" element should not ' +
+ 'update styles');
+
+ div.style.display = '';
+
+ // We need to wait a frame to unapply display:none style.
+ yield waitForFrame();
+
+ ok(animation.isRunningOnCompositor,
+ 'Opacity script animations restored from "display: none" should be ' +
+ 'run on the compositor');
+
+ yield ensureElementRemoval(div);
+ });
+
+ add_task(function* restyling_for_empty_keyframes() {
+ var div = addDiv(null);
+ var animation = div.animate({ }, 100 * MS_PER_SEC);
+
+ yield animation.ready;
+ var markers = yield observeStyling(5);
+
+ is(markers.length, 0,
+ 'Animations with no keyframes should not cause restyles');
+
+ animation.effect.setKeyframes({ backgroundColor: ['red', 'blue'] });
+ markers = yield observeStyling(5);
+
+ is(markers.length, 5,
+ 'Setting valid keyframes should cause regular animation restyles to ' +
+ 'occur');
+
+ animation.effect.setKeyframes({ });
+ markers = yield observeStyling(5);
+
+ is(markers.length, 1,
+ 'Setting an empty set of keyframes should trigger a single restyle ' +
+ 'to remove the previous animated style');
+
+ yield ensureElementRemoval(div);
+ });
+
+ add_task_if_omta_enabled(function* no_restyling_when_animation_style_when_re_setting_same_animation_property() {
+ var div = addDiv(null, { style: 'animation: opacity 100s' });
+ var animation = div.getAnimations()[0];
+ yield animation.ready;
+ ok(animation.isRunningOnCompositor);
+ // Apply the same animation style
+ div.style.animation = 'opacity 100s';
+ var markers = yield observeStyling(5);
+ is(markers.length, 0,
+ 'Applying same animation style ' +
+ 'should never cause restyles');
+ yield ensureElementRemoval(div);
+ });
+
+ add_task(function* necessary_update_should_be_invoked() {
+ var div = addDiv(null, { style: 'animation: background-color 100s' });
+ var animation = div.getAnimations()[0];
+ yield animation.ready;
+ yield waitForAnimationFrames(5);
+ // Apply another animation style
+ div.style.animation = 'background-color 110s';
+ var animation = div.getAnimations()[0];
+ var markers = yield observeStyling(5);
+ is(markers.length, 5,
+ 'Applying animation style with different duration ' +
+ 'should cause restyles on every frame.');
+ yield ensureElementRemoval(div);
+ });
+
+ add_task_if_omta_enabled(
+ function* changing_cascading_result_for_main_thread_animation() {
+ var div = addDiv(null, { style: 'background-color: blue' });
+ var animation = div.animate({ opacity: [0, 1],
+ backgroundColor: ['green', 'red'] },
+ 100 * MS_PER_SEC);
+ yield animation.ready;
+ ok(animation.isRunningOnCompositor,
+ 'The opacity animation is running on the compositor');
+ // Make the background-color style as !important to cause an update
+ // to the cascade.
+ // Bug 1300982: The background-color animation should be no longer
+ // running on the main thread.
+ div.style.setProperty('background-color', '1', 'important');
+ var markers = yield observeStyling(5);
+ todo_is(markers.length, 0,
+ 'Changing cascading result for the property running on the main ' +
+ 'thread does not cause synchronization layer of opacity animation ' +
+ 'running on the compositor');
+ yield ensureElementRemoval(div);
+ }
+ );
+
+ add_task(function* restyling_for_animation_on_orphaned_element() {
+ var div = addDiv(null);
+ var animation = div.animate({ marginLeft: [ '0px', '100px' ] },
+ 100 * MS_PER_SEC);
+
+ yield animation.ready;
+
+ div.remove();
+
+ var markers = yield observeStyling(5);
+ is(markers.length, 0,
+ 'Animation on orphaned element should not cause restyles');
+
+ document.body.appendChild(div);
+
+ markers = yield observeStyling(1);
+ // We are observing restyles in rAF callback which is processed before
+ // restyling process in each frame, so in the first frame there should be
+ // no observed restyle since we don't process restyle while the element
+ // is not attached to the document.
+ is(markers.length, 0,
+ 'We observe no restyle in the first frame right after re-atatching ' +
+ 'to the document');
+ markers = yield observeStyling(5);
+ is(markers.length, 5,
+ 'Animation on re-attached to the document begins to update style');
+
+ yield ensureElementRemoval(div);
+ });
+
+ add_task_if_omta_enabled(
+ // Tests that if we remove an element from the document whose animation
+ // cascade needs recalculating, that it is correctly updated when it is
+ // re-attached to the document.
+ function* restyling_for_opacity_animation_on_re_attached_element() {
+ var div = addDiv(null, { style: 'opacity: 1 ! important' });
+ var animation = div.animate({ opacity: [0, 1] }, 100 * MS_PER_SEC);
+
+ yield animation.ready;
+ ok(!animation.isRunningOnCompositor,
+ 'The opacity animation overridden by an !important rule is NOT ' +
+ 'running on the compositor');
+
+ // Drop the !important rule to update the cascade.
+ div.style.setProperty('opacity', '1', '');
+
+ div.remove();
+
+ var markers = yield observeStyling(5);
+ is(markers.length, 0,
+ 'Opacity animation on orphaned element should not cause restyles');
+
+ document.body.appendChild(div);
+
+ // Need a frame to give the animation a chance to be sent to the
+ // compositor.
+ yield waitForFrame();
+
+ ok(animation.isRunningOnCompositor,
+ 'The opacity animation which is no longer overridden by the ' +
+ '!important rule begins running on the compositor even if the ' +
+ '!important rule had been dropped before the target element was ' +
+ 'removed');
+
+ yield ensureElementRemoval(div);
+ }
+ );
+
+});
+
+</script>
+</body>
diff --git a/dom/animation/test/chrome/test_running_on_compositor.html b/dom/animation/test/chrome/test_running_on_compositor.html
new file mode 100644
index 0000000000..cd6c679b8e
--- /dev/null
+++ b/dom/animation/test/chrome/test_running_on_compositor.html
@@ -0,0 +1,966 @@
+<!doctype html>
+<head>
+<meta charset=utf-8>
+<title>Bug 1045994 - Add a chrome-only property to inspect if an animation is
+ running on the compositor or not</title>
+<script type="application/javascript" src="../testharness.js"></script>
+<script type="application/javascript" src="../testharnessreport.js"></script>
+<script type="application/javascript" src="../testcommon.js"></script>
+<style>
+@keyframes anim {
+ to { transform: translate(100px) }
+}
+@keyframes transform-starts-with-none {
+ 0% { transform: none }
+ 99% { transform: none }
+ 100% { transform: translate(100px) }
+}
+@keyframes opacity {
+ to { opacity: 0 }
+}
+@keyframes background_and_translate {
+ to { background-color: red; transform: translate(100px); }
+}
+@keyframes background {
+ to { background-color: red; }
+}
+@keyframes rotate {
+ from { transform: rotate(0deg); }
+ to { transform: rotate(360deg); }
+}
+@keyframes rotate-and-opacity {
+ from { transform: rotate(0deg); opacity: 1;}
+ to { transform: rotate(360deg); opacity: 0;}
+}
+div {
+ /* Element needs geometry to be eligible for layerization */
+ width: 100px;
+ height: 100px;
+ background-color: white;
+}
+</style>
+</head>
+<body>
+<a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1045994"
+ target="_blank">Mozilla Bug 1045994</a>
+<div id="log"></div>
+<script>
+'use strict';
+
+/** Test for bug 1045994 - Add a chrome-only property to inspect if an
+ animation is running on the compositor or not **/
+
+var omtaEnabled = isOMTAEnabled();
+
+function assert_animation_is_running_on_compositor(animation, desc) {
+ assert_equals(animation.isRunningOnCompositor, omtaEnabled,
+ desc + ' at ' + animation.currentTime + 'ms');
+}
+
+function assert_animation_is_not_running_on_compositor(animation, desc) {
+ assert_equals(animation.isRunningOnCompositor, false,
+ desc + ' at ' + animation.currentTime + 'ms');
+}
+
+promise_test(function(t) {
+ // FIXME: When we implement Element.animate, use that here instead of CSS
+ // so that we remove any dependency on the CSS mapping.
+ var div = addDiv(t, { style: 'animation: anim 100s' });
+ var animation = div.getAnimations()[0];
+
+ return animation.ready.then(function() {
+ assert_animation_is_running_on_compositor(animation,
+ 'Animation reports that it is running on the compositor'
+ + ' during playback');
+
+ div.style.animationPlayState = 'paused';
+
+ return animation.ready;
+ }).then(function() {
+ assert_animation_is_not_running_on_compositor(animation,
+ 'Animation reports that it is NOT running on the compositor'
+ + ' when paused');
+ });
+}, '');
+
+promise_test(function(t) {
+ var div = addDiv(t, { style: 'animation: background 100s' });
+ var animation = div.getAnimations()[0];
+
+ return animation.ready.then(function() {
+ assert_animation_is_not_running_on_compositor(animation,
+ 'Animation reports that it is NOT running on the compositor'
+ + ' for animation of "background"');
+ });
+}, 'isRunningOnCompositor is false for animation of "background"');
+
+promise_test(function(t) {
+ var div = addDiv(t, { style: 'animation: background_and_translate 100s' });
+ var animation = div.getAnimations()[0];
+
+ return animation.ready.then(function() {
+ assert_animation_is_running_on_compositor(animation,
+ 'Animation reports that it is running on the compositor'
+ + ' when the animation has two properties, where one can run'
+ + ' on the compositor, the other cannot');
+ });
+}, 'isRunningOnCompositor is true if the animation has at least one ' +
+ 'property can run on compositor');
+
+promise_test(function(t) {
+ var div = addDiv(t, { style: 'animation: anim 100s' });
+ var animation = div.getAnimations()[0];
+
+ return animation.ready.then(function() {
+ animation.pause();
+ return animation.ready;
+ }).then(function() {
+ assert_animation_is_not_running_on_compositor(animation,
+ 'Animation reports that it is NOT running on the compositor'
+ + ' when animation.pause() is called');
+ });
+}, 'isRunningOnCompositor is false when the animation.pause() is called');
+
+promise_test(function(t) {
+ var div = addDiv(t, { style: 'animation: anim 100s' });
+ var animation = div.getAnimations()[0];
+
+ return animation.ready.then(function() {
+ animation.finish();
+ assert_animation_is_not_running_on_compositor(animation,
+ 'Animation reports that it is NOT running on the compositor'
+ + ' immediately after animation.finish() is called');
+ // Check that we don't set the flag back again on the next tick.
+ return waitForFrame();
+ }).then(function() {
+ assert_animation_is_not_running_on_compositor(animation,
+ 'Animation reports that it is NOT running on the compositor'
+ + ' on the next tick after animation.finish() is called');
+ });
+}, 'isRunningOnCompositor is false when the animation.finish() is called');
+
+promise_test(function(t) {
+ var div = addDiv(t, { style: 'animation: anim 100s' });
+ var animation = div.getAnimations()[0];
+
+ return animation.ready.then(function() {
+ animation.currentTime = 100 * MS_PER_SEC;
+ assert_animation_is_not_running_on_compositor(animation,
+ 'Animation reports that it is NOT running on the compositor'
+ + ' immediately after manually seeking the animation to the end');
+ // Check that we don't set the flag back again on the next tick.
+ return waitForFrame();
+ }).then(function() {
+ assert_animation_is_not_running_on_compositor(animation,
+ 'Animation reports that it is NOT running on the compositor'
+ + ' on the next tick after manually seeking the animation to the end');
+ });
+}, 'isRunningOnCompositor is false when manually seeking the animation to ' +
+ 'the end');
+
+promise_test(function(t) {
+ var div = addDiv(t, { style: 'animation: anim 100s' });
+ var animation = div.getAnimations()[0];
+
+ return animation.ready.then(function() {
+ animation.cancel();
+ assert_animation_is_not_running_on_compositor(animation,
+ 'Animation reports that it is NOT running on the compositor'
+ + ' immediately after animation.cancel() is called');
+ // Check that we don't set the flag back again on the next tick.
+ return waitForFrame();
+ }).then(function() {
+ assert_animation_is_not_running_on_compositor(animation,
+ 'Animation reports that it is NOT running on the compositor'
+ + ' on the next tick after animation.cancel() is called');
+ });
+}, 'isRunningOnCompositor is false when animation.cancel() is called');
+
+// This is to test that we don't simply clobber the flag when ticking
+// animations and then set it again during painting.
+promise_test(function(t) {
+ var div = addDiv(t, { style: 'animation: anim 100s' });
+ var animation = div.getAnimations()[0];
+
+ return animation.ready.then(function() {
+ return new Promise(function(resolve) {
+ window.requestAnimationFrame(function() {
+ t.step(function() {
+ assert_animation_is_running_on_compositor(animation,
+ 'Animation reports that it is running on the compositor'
+ + ' in requestAnimationFrame callback');
+ });
+
+ resolve();
+ });
+ });
+ });
+}, 'isRunningOnCompositor is true in requestAnimationFrame callback');
+
+promise_test(function(t) {
+ var div = addDiv(t, { style: 'animation: anim 100s' });
+ var animation = div.getAnimations()[0];
+
+ return animation.ready.then(function() {
+ return new Promise(function(resolve) {
+ var observer = new MutationObserver(function(records) {
+ var changedAnimation;
+
+ records.forEach(function(record) {
+ changedAnimation =
+ record.changedAnimations.find(function(changedAnim) {
+ return changedAnim == animation;
+ });
+ });
+
+ t.step(function() {
+ assert_true(!!changedAnimation, 'The animation should be recorded '
+ + 'as one of the changedAnimations');
+
+ assert_animation_is_running_on_compositor(animation,
+ 'Animation reports that it is running on the compositor'
+ + ' in MutationObserver callback');
+ });
+
+ resolve();
+ });
+ observer.observe(div, { animations: true, subtree: false });
+ t.add_cleanup(function() {
+ observer.disconnect();
+ });
+ div.style.animationDuration = "200s";
+ });
+ });
+}, 'isRunningOnCompositor is true in MutationObserver callback');
+
+// This is to test that we don't temporarily clear the flag when forcing
+// an unthrottled sample.
+promise_test(function(t) {
+ return new Promise(function(resolve) {
+ // Needs scrollbars to cause overflow.
+ SpecialPowers.pushPrefEnv({ set: [["ui.showHideScrollbars", 1]] },
+ resolve);
+ }).then(function() {
+ var div = addDiv(t, { style: 'animation: rotate 100s' });
+ var animation = div.getAnimations()[0];
+
+ return animation.ready.then(function() {
+ return new Promise(function(resolve) {
+ var timeAtStart = window.performance.now();
+ function handleFrame() {
+ t.step(function() {
+ assert_animation_is_running_on_compositor(animation,
+ 'Animation reports that it is running on the compositor'
+ + ' in requestAnimationFrame callback');
+ });
+
+ // we have to wait at least 200ms because this animation is
+ // unthrottled on every 200ms.
+ // See http://hg.mozilla.org/mozilla-central/file/cafb1c90f794/layout/style/AnimationCommon.cpp#l863
+ if (window.performance.now() - timeAtStart > 200) {
+ resolve();
+ return;
+ }
+ window.requestAnimationFrame(handleFrame);
+ }
+ window.requestAnimationFrame(handleFrame);
+ });
+ });
+ });
+}, 'isRunningOnCompositor remains true in requestAnimationFrameCallback for ' +
+ 'overflow animation');
+
+promise_test(function(t) {
+ var div = addDiv(t, { style: 'transition: opacity 100s; opacity: 1' });
+
+ getComputedStyle(div).opacity;
+
+ div.style.opacity = 0;
+ var animation = div.getAnimations()[0];
+
+ return animation.ready.then(function() {
+ assert_animation_is_running_on_compositor(animation,
+ 'Transition reports that it is running on the compositor'
+ + ' during playback for opacity transition');
+ });
+}, 'isRunningOnCompositor for transitions');
+
+promise_test(function(t) {
+ var div = addDiv(t, { style: 'animation: rotate-and-opacity 100s; ' +
+ 'backface-visibility: hidden; ' +
+ 'transform: none !important;' });
+ var animation = div.getAnimations()[0];
+
+ return animation.ready.then(function() {
+ assert_animation_is_running_on_compositor(animation,
+ 'If an animation has a property that can run on the compositor and a '
+ + 'property that cannot (due to Gecko limitations) but where the latter'
+ + 'property is overridden in the CSS cascade, the animation should '
+ + 'still report that it is running on the compositor');
+ });
+}, 'isRunningOnCompositor is true when a property that would otherwise block ' +
+ 'running on the compositor is overridden in the CSS cascade');
+
+promise_test(function(t) {
+ var animation = addDivAndAnimate(t,
+ {},
+ { opacity: [ 0, 1 ] }, 200 * MS_PER_SEC);
+
+ return animation.ready.then(function() {
+ assert_animation_is_running_on_compositor(animation,
+ 'Animation reports that it is running on the compositor');
+
+ animation.currentTime = 150 * MS_PER_SEC;
+ animation.effect.timing.duration = 100 * MS_PER_SEC;
+
+ assert_animation_is_not_running_on_compositor(animation,
+ 'Animation reports that it is NOT running on the compositor'
+ + ' when the animation is set a shorter duration than current time');
+ });
+}, 'animation is immediately removed from compositor' +
+ 'when timing.duration is made shorter than the current time');
+
+promise_test(function(t) {
+ var animation = addDivAndAnimate(t,
+ {},
+ { opacity: [ 0, 1 ] }, 100 * MS_PER_SEC);
+
+ return animation.ready.then(function() {
+ assert_animation_is_running_on_compositor(animation,
+ 'Animation reports that it is running on the compositor');
+
+ animation.currentTime = 500 * MS_PER_SEC;
+
+ assert_animation_is_not_running_on_compositor(animation,
+ 'Animation reports that it is NOT running on the compositor'
+ + ' when finished');
+
+ animation.effect.timing.duration = 1000 * MS_PER_SEC;
+ return waitForFrame();
+ }).then(function() {
+ assert_animation_is_running_on_compositor(animation,
+ 'Animation reports that it is running on the compositor'
+ + ' when restarted');
+ });
+}, 'animation is added to compositor' +
+ ' when timing.duration is made longer than the current time');
+
+promise_test(function(t) {
+ var animation = addDivAndAnimate(t,
+ {},
+ { opacity: [ 0, 1 ] }, 100 * MS_PER_SEC);
+
+ return animation.ready.then(function() {
+ assert_animation_is_running_on_compositor(animation,
+ 'Animation reports that it is running on the compositor');
+
+ animation.effect.timing.endDelay = 100 * MS_PER_SEC;
+
+ assert_animation_is_running_on_compositor(animation,
+ 'Animation reports that it is running on the compositor'
+ + ' when endDelay is changed');
+
+ animation.currentTime = 110 * MS_PER_SEC;
+ return waitForFrame();
+ }).then(function() {
+ assert_animation_is_not_running_on_compositor(animation,
+ 'Animation reports that it is NOT running on the compositor'
+ + ' when currentTime is during endDelay');
+ });
+}, 'animation is removed from compositor' +
+ ' when current time is made longer than the duration even during endDelay');
+
+promise_test(function(t) {
+ var animation = addDivAndAnimate(t,
+ {},
+ { opacity: [ 0, 1 ] }, 100 * MS_PER_SEC);
+
+ return animation.ready.then(function() {
+ assert_animation_is_running_on_compositor(animation,
+ 'Animation reports that it is running on the compositor');
+
+ animation.effect.timing.endDelay = -200 * MS_PER_SEC;
+ return waitForFrame();
+ }).then(function() {
+ assert_animation_is_not_running_on_compositor(animation,
+ 'Animation reports that it is NOT running on the compositor'
+ + ' when endTime is negative value');
+ });
+}, 'animation is removed from compositor' +
+ ' when endTime is negative value');
+
+promise_test(function(t) {
+ var animation = addDivAndAnimate(t,
+ {},
+ { opacity: [ 0, 1 ] }, 200 * MS_PER_SEC);
+
+ return animation.ready.then(function() {
+ assert_animation_is_running_on_compositor(animation,
+ 'Animation reports that it is running on the compositor');
+
+ animation.effect.timing.endDelay = -100 * MS_PER_SEC;
+ return waitForFrame();
+ }).then(function() {
+ assert_animation_is_running_on_compositor(animation,
+ 'Animation reports that it is running on the compositor'
+ + ' when endTime is positive and endDelay is negative');
+ animation.currentTime = 110 * MS_PER_SEC;
+ return waitForFrame();
+ }).then(function() {
+ assert_animation_is_not_running_on_compositor(animation,
+ 'Animation reports that it is NOT running on the compositor'
+ + ' when currentTime is after endTime');
+ });
+}, 'animation is NOT running on compositor' +
+ ' when endTime is positive and endDelay is negative');
+
+promise_test(function(t) {
+ var effect = new KeyframeEffect(null,
+ { opacity: [ 0, 1 ] },
+ 100 * MS_PER_SEC);
+ var animation = new Animation(effect, document.timeline);
+ animation.play();
+
+ var div = addDiv(t);
+
+ return animation.ready.then(function() {
+ assert_animation_is_not_running_on_compositor(animation,
+ 'Animation with null target reports that it is not running ' +
+ 'on the compositor');
+
+ animation.effect.target = div;
+ return waitForFrame();
+ }).then(function() {
+ assert_animation_is_running_on_compositor(animation,
+ 'Animation reports that it is running on the compositor ' +
+ 'after setting a valid target');
+ });
+}, 'animation is added to the compositor when setting a valid target');
+
+promise_test(function(t) {
+ var div = addDiv(t);
+ var animation = div.animate({ opacity: [ 0, 1 ] }, 100 * MS_PER_SEC);
+
+ return animation.ready.then(function() {
+ assert_animation_is_running_on_compositor(animation,
+ 'Animation reports that it is running on the compositor');
+
+ animation.effect.target = null;
+ assert_animation_is_not_running_on_compositor(animation,
+ 'Animation reports that it is NOT running on the ' +
+ 'compositor after setting null target');
+ });
+}, 'animation is removed from the compositor when setting null target');
+
+promise_test(function(t) {
+ var div = addDiv(t);
+ var animation = div.animate({ opacity: [ 0, 1 ] },
+ { duration: 100 * MS_PER_SEC,
+ delay: 100 * MS_PER_SEC,
+ fill: 'backwards' });
+
+ return animation.ready.then(function() {
+ assert_animation_is_running_on_compositor(animation,
+ 'Animation with fill:backwards in delay phase reports ' +
+ 'that it is running on the compositor');
+
+ animation.currentTime = 100 * MS_PER_SEC;
+ return waitForFrame();
+ }).then(function() {
+ assert_animation_is_running_on_compositor(animation,
+ 'Animation with fill:backwards in delay phase reports ' +
+ 'that it is running on the compositor after delay phase');
+ });
+}, 'animation with fill:backwards in delay phase is running on the ' +
+ ' main-thread while it is in delay phase');
+
+promise_test(function(t) {
+ var div = addDiv(t);
+ var animation = div.animate([{ opacity: 1, offset: 0 },
+ { opacity: 1, offset: 0.99 },
+ { opacity: 0, offset: 1 }], 100 * MS_PER_SEC);
+
+ var another = addDiv(t);
+
+ return animation.ready.then(function() {
+ assert_animation_is_running_on_compositor(animation,
+ 'Opacity animation on a 100% opacity keyframe reports ' +
+ 'that it is running on the compositor from the begining');
+
+ animation.effect.target = another;
+ return waitForFrame();
+ }).then(function() {
+ assert_animation_is_running_on_compositor(animation,
+ 'Opacity animation on a 100% opacity keyframe keeps ' +
+ 'running on the compositor after changing the target ' +
+ 'element');
+ });
+}, '100% opacity animations with keeps running on the ' +
+ 'compositor after changing the target element');
+
+promise_test(function(t) {
+ var div = addDiv(t);
+ var animation = div.animate({ color: ['red', 'black'] }, 100 * MS_PER_SEC);
+
+ return animation.ready.then(function() {
+ assert_animation_is_not_running_on_compositor(animation,
+ 'Color animation reports that it is not running on the ' +
+ 'compositor');
+
+ animation.effect.setKeyframes([{ opacity: 1, offset: 0 },
+ { opacity: 1, offset: 0.99 },
+ { opacity: 0, offset: 1 }]);
+ return waitForFrame();
+ }).then(function() {
+ assert_animation_is_running_on_compositor(animation,
+ '100% opacity animation set by using setKeyframes reports ' +
+ 'that it is running on the compositor');
+ });
+}, '100% opacity animation set up by converting an existing animation with ' +
+ 'cannot be run on the compositor, is running on the compositor');
+
+promise_test(function(t) {
+ var div = addDiv(t);
+ var animation = div.animate({ color: ['red', 'black'] }, 100 * MS_PER_SEC);
+ var effect = new KeyframeEffect(div,
+ [{ opacity: 1, offset: 0 },
+ { opacity: 1, offset: 0.99 },
+ { opacity: 0, offset: 1 }],
+ 100 * MS_PER_SEC);
+
+ return animation.ready.then(function() {
+ assert_animation_is_not_running_on_compositor(animation,
+ 'Color animation reports that it is not running on the ' +
+ 'compositor');
+
+ animation.effect = effect;
+ return waitForFrame();
+ }).then(function() {
+ assert_animation_is_running_on_compositor(animation,
+ '100% opacity animation set up by changing effects reports ' +
+ 'that it is running on the compositor');
+ });
+}, '100% opacity animation set up by changing the effects on an existing ' +
+ 'animation which cannot be run on the compositor, is running on the ' +
+ 'compositor');
+
+promise_test(function(t) {
+ var div = addDiv(t, { style: "opacity: 1 ! important" });
+
+ var animation = div.animate({ opacity: [0, 1] }, 100 * MS_PER_SEC);
+
+ return animation.ready.then(function() {
+ assert_animation_is_not_running_on_compositor(animation,
+ 'Opacity animation on an element which has 100% opacity style with ' +
+ '!important flag reports that it is not running on the compositor');
+ // Clear important flag from the opacity style on the target element.
+ div.style.setProperty("opacity", "1", "");
+ return waitForFrame();
+ }).then(function() {
+ assert_animation_is_running_on_compositor(animation,
+ 'Opacity animation reports that it is running on the compositor after '
+ + 'clearing the !important flag');
+ });
+}, 'Clearing *important* opacity style on the target element sends the ' +
+ 'animation to the compositor');
+
+promise_test(function(t) {
+ var div = addDiv(t);
+ var lowerAnimation = div.animate({ opacity: [1, 0] }, 100 * MS_PER_SEC);
+ var higherAnimation = div.animate({ opacity: [0, 1] }, 100 * MS_PER_SEC);
+
+ return Promise.all([lowerAnimation.ready, higherAnimation.ready]).then(function() {
+ assert_animation_is_running_on_compositor(higherAnimation,
+ 'A higher-priority opacity animation on an element ' +
+ 'reports that it is running on the compositor');
+ assert_animation_is_running_on_compositor(lowerAnimation,
+ 'A lower-priority opacity animation on the same ' +
+ 'element also reports that it is running on the compositor');
+ });
+}, 'Opacity animations on the same element run on the compositor');
+
+promise_test(function(t) {
+ var div = addDiv(t, { style: 'transition: opacity 100s; opacity: 1' });
+
+ getComputedStyle(div).opacity;
+
+ div.style.opacity = 0;
+ getComputedStyle(div).opacity;
+
+ var transition = div.getAnimations()[0];
+ var animation = div.animate({ opacity: [0, 1] }, 100 * MS_PER_SEC);
+
+ return Promise.all([transition.ready, animation.ready]).then(function() {
+ assert_animation_is_running_on_compositor(animation,
+ 'An opacity animation on an element reports that' +
+ 'that it is running on the compositor');
+ assert_animation_is_running_on_compositor(transition,
+ 'An opacity transition on the same element reports that ' +
+ 'it is running on the compositor');
+ });
+}, 'Both of transition and script animation on the same element run on the ' +
+ 'compositor');
+
+promise_test(function(t) {
+ var div = addDiv(t);
+ var importantOpacityElement = addDiv(t, { style: "opacity: 1 ! important" });
+
+ var animation = div.animate({ opacity: [1, 0] }, 100 * MS_PER_SEC);
+
+ return animation.ready.then(function() {
+ assert_animation_is_running_on_compositor(animation,
+ 'Opacity animation on an element reports ' +
+ 'that it is running on the compositor');
+
+ animation.effect.target = null;
+ return waitForFrame();
+ }).then(function() {
+ assert_animation_is_not_running_on_compositor(animation,
+ 'Animation is no longer running on the compositor after ' +
+ 'removing from the element');
+ animation.effect.target = importantOpacityElement;
+ return waitForFrame();
+ }).then(function() {
+ assert_animation_is_not_running_on_compositor(animation,
+ 'Animation is NOT running on the compositor even after ' +
+ 'being applied to a different element which has an ' +
+ '!important opacity declaration');
+ });
+}, 'Animation continues not running on the compositor after being ' +
+ 'applied to an element which has an important declaration and ' +
+ 'having previously been temporarily associated with no target element');
+
+promise_test(function(t) {
+ var div = addDiv(t);
+ var another = addDiv(t);
+
+ var lowerAnimation = div.animate({ opacity: [1, 0] }, 100 * MS_PER_SEC);
+ var higherAnimation = another.animate({ opacity: [0, 1] }, 100 * MS_PER_SEC);
+
+ return Promise.all([lowerAnimation.ready, higherAnimation.ready]).then(function() {
+ assert_animation_is_running_on_compositor(lowerAnimation,
+ 'An opacity animation on an element reports that ' +
+ 'it is running on the compositor');
+ assert_animation_is_running_on_compositor(higherAnimation,
+ 'Opacity animation on a different element reports ' +
+ 'that it is running on the compositor');
+
+ lowerAnimation.effect.target = null;
+ return waitForFrame();
+ }).then(function() {
+ assert_animation_is_not_running_on_compositor(lowerAnimation,
+ 'Animation is no longer running on the compositor after ' +
+ 'being removed from the element');
+ lowerAnimation.effect.target = another;
+ return waitForFrame();
+ }).then(function() {
+ assert_animation_is_running_on_compositor(lowerAnimation,
+ 'A lower-priority animation begins running ' +
+ 'on the compositor after being applied to an element ' +
+ 'which has a higher-priority animation');
+ assert_animation_is_running_on_compositor(higherAnimation,
+ 'A higher-priority animation continues to run on the ' +
+ 'compositor even after a lower-priority animation is ' +
+ 'applied to the same element');
+ });
+}, 'Animation begins running on the compositor after being applied ' +
+ 'to an element which has a higher-priority animation and after ' +
+ 'being temporarily associated with no target element');
+
+promise_test(function(t) {
+ var div = addDiv(t);
+ var another = addDiv(t);
+
+ var lowerAnimation = div.animate({ opacity: [1, 0] }, 100 * MS_PER_SEC);
+ var higherAnimation = another.animate({ opacity: [0, 1] }, 100 * MS_PER_SEC);
+
+ return Promise.all([lowerAnimation.ready, higherAnimation.ready]).then(function() {
+ assert_animation_is_running_on_compositor(lowerAnimation,
+ 'An opacity animation on an element reports that ' +
+ 'it is running on the compositor');
+ assert_animation_is_running_on_compositor(higherAnimation,
+ 'Opacity animation on a different element reports ' +
+ 'that it is running on the compositor');
+
+ higherAnimation.effect.target = null;
+ return waitForFrame();
+ }).then(function() {
+ assert_animation_is_not_running_on_compositor(higherAnimation,
+ 'Animation is no longer running on the compositor after ' +
+ 'being removed from the element');
+ higherAnimation.effect.target = div;
+ return waitForFrame();
+ }).then(function() {
+ assert_animation_is_running_on_compositor(lowerAnimation,
+ 'Animation continues running on the compositor after ' +
+ 'a higher-priority animation applied to the same element');
+ assert_animation_is_running_on_compositor(higherAnimation,
+ 'A higher-priority animation begins to running on the ' +
+ 'compositor after being applied to an element which has ' +
+ 'a lower-priority-animation');
+ });
+}, 'Animation begins running on the compositor after being applied ' +
+ 'to an element which has a lower-priority animation once after ' +
+ 'disassociating with an element');
+
+var delayPhaseTests = [
+ {
+ desc: 'script animation of opacity',
+ setupAnimation: function(t) {
+ return addDiv(t).animate(
+ { opacity: [0, 1] },
+ { delay: 100 * MS_PER_SEC, duration: 100 * MS_PER_SEC });
+ },
+ },
+ {
+ desc: 'script animation of transform',
+ setupAnimation: function(t) {
+ return addDiv(t).animate(
+ { transform: ['translateX(0px)', 'translateX(100px)'] },
+ { delay: 100 * MS_PER_SEC, duration: 100 * MS_PER_SEC });
+ },
+ },
+ {
+ desc: 'CSS animation of opacity',
+ setupAnimation: function(t) {
+ return addDiv(t, { style: 'animation: opacity 100s 100s' })
+ .getAnimations()[0];
+ },
+ },
+ {
+ desc: 'CSS animation of transform',
+ setupAnimation: function(t) {
+ return addDiv(t, { style: 'animation: anim 100s 100s' })
+ .getAnimations()[0];
+ },
+ },
+ {
+ desc: 'CSS transition of opacity',
+ setupAnimation: function(t) {
+ var div = addDiv(t, { style: 'transition: opacity 100s 100s' });
+ getComputedStyle(div).opacity;
+
+ div.style.opacity = 0;
+ return div.getAnimations()[0];
+ },
+ },
+ {
+ desc: 'CSS transition of transform',
+ setupAnimation: function(t) {
+ var div = addDiv(t, { style: 'transition: transform 100s 100s' });
+ getComputedStyle(div).transform;
+
+ div.style.transform = 'translateX(100px)';
+ return div.getAnimations()[0];
+ },
+ },
+];
+
+delayPhaseTests.forEach(function(test) {
+ promise_test(function(t) {
+ var animation = test.setupAnimation(t);
+
+ return animation.ready.then(function() {
+ assert_animation_is_running_on_compositor(animation,
+ test.desc + ' reports that it is running on the '
+ + 'compositor even though it is in the delay phase');
+ });
+ }, 'isRunningOnCompositor for ' + test.desc + ' is true even though ' +
+ 'it is in the delay phase');
+});
+
+// The purpose of thie test cases is to check that
+// NS_FRAME_MAY_BE_TRANSFORMED flag on the associated nsIFrame persists
+// after transform style on the frame is removed.
+var delayPhaseWithTransformStyleTests = [
+ {
+ desc: 'script animation of transform with transform style',
+ setupAnimation: function(t) {
+ return addDiv(t, { style: 'transform: translateX(10px)' }).animate(
+ { transform: ['translateX(0px)', 'translateX(100px)'] },
+ { delay: 100 * MS_PER_SEC, duration: 100 * MS_PER_SEC });
+ },
+ },
+ {
+ desc: 'CSS animation of transform with transform style',
+ setupAnimation: function(t) {
+ return addDiv(t, { style: 'animation: anim 100s 100s;' +
+ 'transform: translateX(10px)' })
+ .getAnimations()[0];
+ },
+ },
+ {
+ desc: 'CSS transition of transform with transform style',
+ setupAnimation: function(t) {
+ var div = addDiv(t, { style: 'transition: transform 100s 100s;' +
+ 'transform: translateX(10px)'});
+ getComputedStyle(div).transform;
+
+ div.style.transform = 'translateX(100px)';
+ return div.getAnimations()[0];
+ },
+ },
+];
+
+delayPhaseWithTransformStyleTests.forEach(function(test) {
+ promise_test(function(t) {
+ var animation = test.setupAnimation(t);
+
+ return animation.ready.then(function() {
+ assert_animation_is_running_on_compositor(animation,
+ test.desc + ' reports that it is running on the '
+ + 'compositor even though it is in the delay phase');
+ }).then(function() {
+ // Remove the initial transform style during delay phase.
+ animation.effect.target.style.transform = 'none';
+ return animation.ready;
+ }).then(function() {
+ assert_animation_is_running_on_compositor(animation,
+ test.desc + ' reports that it keeps running on the '
+ + 'compositor after removing the initial transform style');
+ });
+ }, 'isRunningOnCompositor for ' + test.desc + ' is true after removing ' +
+ 'the initial transform style during the delay phase');
+});
+
+var startsWithNoneTests = [
+ {
+ desc: 'script animation of transform starts with transform:none segment',
+ setupAnimation: function(t) {
+ return addDiv(t).animate(
+ { transform: ['none', 'none', 'translateX(100px)'] }, 100 * MS_PER_SEC);
+ },
+ },
+ {
+ desc: 'CSS animation of transform starts with transform:none segment',
+ setupAnimation: function(t) {
+ return addDiv(t,
+ { style: 'animation: transform-starts-with-none 100s 100s' })
+ .getAnimations()[0];
+ },
+ },
+];
+
+startsWithNoneTests.forEach(function(test) {
+ promise_test(function(t) {
+ var animation = test.setupAnimation(t);
+
+ return animation.ready.then(function() {
+ assert_animation_is_running_on_compositor(animation,
+ test.desc + ' reports that it is running on the '
+ + 'compositor even though it is in transform:none segment');
+ });
+ }, 'isRunningOnCompositor for ' + test.desc + ' is true even though ' +
+ 'it is in transform:none segment');
+});
+
+promise_test(function(t) {
+ var div = addDiv(t, { style: 'opacity: 1 ! important' });
+
+ var animation = div.animate(
+ { opacity: [0, 1] },
+ { delay: 100 * MS_PER_SEC, duration: 100 * MS_PER_SEC });
+
+ return animation.ready.then(function() {
+ assert_animation_is_not_running_on_compositor(animation,
+ 'Opacity animation on an element which has opacity:1 important style'
+ + 'reports that it is not running on the compositor');
+ // Clear the opacity style on the target element.
+ div.style.setProperty("opacity", "1", "");
+ return waitForFrame();
+ }).then(function() {
+ assert_animation_is_running_on_compositor(animation,
+ 'Opacity animations reports that it is running on the compositor after '
+ + 'clearing the opacity style on the element');
+ });
+}, 'Clearing *important* opacity style on the target element sends the ' +
+ 'animation to the compositor even if the animation is in the delay phase');
+
+promise_test(function(t) {
+ var opaqueDiv = addDiv(t, { style: 'opacity: 1 ! important' });
+ var anotherDiv = addDiv(t);
+
+ var animation = opaqueDiv.animate(
+ { opacity: [0, 1] },
+ { delay: 100 * MS_PER_SEC, duration: 100 * MS_PER_SEC });
+
+ return animation.ready.then(function() {
+ assert_animation_is_not_running_on_compositor(animation,
+ 'Opacity animation on an element which has opacity:1 important style'
+ + 'reports that it is not running on the compositor');
+ // Changing target element to another element which has no opacity style.
+ animation.effect.target = anotherDiv;
+ return waitForFrame();
+ }).then(function() {
+ assert_animation_is_running_on_compositor(animation,
+ 'Opacity animations reports that it is running on the compositor after '
+ + 'changing the target element to another elemenent having no '
+ + 'opacity style');
+ });
+}, 'Changing target element of opacity animation sends the animation to the ' +
+ 'the compositor even if the animation is in the delay phase');
+
+promise_test(function(t) {
+ var animation =
+ addDivAndAnimate(t,
+ {},
+ { width: ['100px', '200px'] },
+ { duration: 100 * MS_PER_SEC, delay: 100 * MS_PER_SEC });
+
+ return animation.ready.then(function() {
+ assert_animation_is_not_running_on_compositor(animation,
+ 'Width animation reports that it is not running on the compositor '
+ + 'in the delay phase');
+ // Changing to property runnable on the compositor.
+ animation.effect.setKeyframes({ opacity: [0, 1] });
+ return waitForFrame();
+ }).then(function() {
+ assert_animation_is_running_on_compositor(animation,
+ 'Opacity animation reports that it is running on the compositor '
+ + 'after changing the property from width property in the delay phase');
+ });
+}, 'Dynamic change to a property runnable on the compositor ' +
+ 'in the delay phase');
+
+promise_test(function(t) {
+ var div = addDiv(t, { style: 'transition: opacity 100s; ' +
+ 'opacity: 0 !important' });
+ getComputedStyle(div).opacity;
+
+ div.style.setProperty('opacity', '1', 'important');
+ getComputedStyle(div).opacity;
+
+ var animation = div.getAnimations()[0];
+
+ return animation.ready.then(function() {
+ assert_animation_is_running_on_compositor(animation,
+ 'Transition reports that it is running on the compositor even if the ' +
+ 'property is overridden by an !important rule');
+ });
+}, 'Transitions override important rules');
+
+promise_test(function(t) {
+ var div = addDiv(t, { style: 'transition: opacity 100s; ' +
+ 'opacity: 0 !important' });
+ getComputedStyle(div).opacity;
+
+ div.animate({ opacity: [ 0, 1 ] }, 100 * MS_PER_SEC);
+
+ div.style.setProperty('opacity', '1', 'important');
+ getComputedStyle(div).opacity;
+
+ var [transition, animation] = div.getAnimations();
+
+ return Promise.all([transition.ready, animation.ready]).then(function() {
+ assert_animation_is_not_running_on_compositor(transition,
+ 'Transition suppressed by an animation which is overridden by an ' +
+ '!important rule reports that it is NOT running on the compositor');
+ assert_animation_is_not_running_on_compositor(animation,
+ 'Animation overridden by an !important rule reports that it is ' +
+ 'NOT running on the compositor');
+ });
+}, 'Neither transition nor animation does run on the compositor if the ' +
+ 'property is overridden by an !important rule');
+
+</script>
+</body>
diff --git a/dom/animation/test/crashtests/1216842-1.html b/dom/animation/test/crashtests/1216842-1.html
new file mode 100644
index 0000000000..8df6808aed
--- /dev/null
+++ b/dom/animation/test/crashtests/1216842-1.html
@@ -0,0 +1,35 @@
+<!doctype html>
+<html class="reftest-wait">
+ <head>
+ <title>Bug 1216842: effect-level easing function produces negative values (compositor thread)</title>
+ <style>
+ #target {
+ width: 100px; height: 100px;
+ background: blue;
+ }
+ </style>
+ </head>
+ <body>
+ <div id="target"></div>
+ </body>
+ <script>
+ var target = document.getElementById("target");
+ var effect =
+ new KeyframeEffectReadOnly(
+ target,
+ { opacity: [0, 1] },
+ {
+ fill: "forwards",
+ /* The function produces negative values in (0, 0.766312060) */
+ easing: "cubic-bezier(0,-0.5,1,-0.5)",
+ duration: 100,
+ iterations: 0.75 /* To finish in the extraporation range */
+ }
+ );
+ var animation = new Animation(effect, document.timeline);
+ animation.play();
+ animation.finished.then(function() {
+ document.documentElement.className = "";
+ });
+ </script>
+</html>
diff --git a/dom/animation/test/crashtests/1216842-2.html b/dom/animation/test/crashtests/1216842-2.html
new file mode 100644
index 0000000000..ec2a2e167e
--- /dev/null
+++ b/dom/animation/test/crashtests/1216842-2.html
@@ -0,0 +1,35 @@
+<!doctype html>
+<html class="reftest-wait">
+ <head>
+ <title>Bug 1216842: effect-level easing function produces values greater than 1 (compositor thread)</title>
+ <style>
+ #target {
+ width: 100px; height: 100px;
+ background: blue;
+ }
+ </style>
+ </head>
+ <body>
+ <div id="target"></div>
+ </body>
+ <script>
+ var target = document.getElementById("target");
+ var effect =
+ new KeyframeEffectReadOnly(
+ target,
+ { opacity: [0, 1] },
+ {
+ fill: "forwards",
+ /* The function produces values greater than 1 in (0.23368794, 1) */
+ easing: "cubic-bezier(0,1.5,1,1.5)",
+ duration: 100,
+ iterations: 0.25 /* To finish in the extraporation range */
+ }
+ );
+ var animation = new Animation(effect, document.timeline);
+ animation.play();
+ animation.finished.then(function() {
+ document.documentElement.className = "";
+ });
+ </script>
+</html>
diff --git a/dom/animation/test/crashtests/1216842-3.html b/dom/animation/test/crashtests/1216842-3.html
new file mode 100644
index 0000000000..2e5a762aa0
--- /dev/null
+++ b/dom/animation/test/crashtests/1216842-3.html
@@ -0,0 +1,27 @@
+<!doctype html>
+<html class="reftest-wait">
+ <head>
+ <title>Bug 1216842: effect-level easing function produces values greater than 1 (main-thread)</title>
+ </head>
+ <body>
+ <div id="target"></div>
+ </body>
+ <script>
+ var target = document.getElementById("target");
+ var effect =
+ new KeyframeEffectReadOnly(
+ target,
+ { color: ["red", "blue"] },
+ {
+ fill: "forwards",
+ /* The function produces values greater than 1 in (0.23368794, 1) */
+ easing: "cubic-bezier(0,1.5,1,1.5)",
+ duration: 100
+ }
+ );
+ var animation = new Animation(effect, document.timeline);
+ animation.pause();
+ animation.currentTime = 250;
+ document.documentElement.className = "";
+ </script>
+</html>
diff --git a/dom/animation/test/crashtests/1216842-4.html b/dom/animation/test/crashtests/1216842-4.html
new file mode 100644
index 0000000000..2951adc959
--- /dev/null
+++ b/dom/animation/test/crashtests/1216842-4.html
@@ -0,0 +1,27 @@
+<!doctype html>
+<html class="reftest-wait">
+ <head>
+ <title>Bug 1216842: effect-level easing function produces negative values (main-thread)</title>
+ </head>
+ <body>
+ <div id="target"></div>
+ </body>
+ <script>
+ var target = document.getElementById("target");
+ var effect =
+ new KeyframeEffectReadOnly(
+ target,
+ { color: ["red", "blue"] },
+ {
+ fill: "forwards",
+ /* The function produces negative values in (0, 0.766312060) */
+ easing: "cubic-bezier(0,-0.5,1,-0.5)",
+ duration: 100
+ }
+ );
+ var animation = new Animation(effect, document.timeline);
+ animation.pause();
+ animation.currentTime = 250;
+ document.documentElement.className = "";
+ </script>
+</html>
diff --git a/dom/animation/test/crashtests/1216842-5.html b/dom/animation/test/crashtests/1216842-5.html
new file mode 100644
index 0000000000..65c64fa481
--- /dev/null
+++ b/dom/animation/test/crashtests/1216842-5.html
@@ -0,0 +1,38 @@
+<!doctype html>
+<html class="reftest-wait">
+ <head>
+ <title>
+ Bug 1216842: effect-level easing function produces negative values passed
+ to step-end function (compositor thread)
+ </title>
+ <style>
+ #target {
+ width: 100px; height: 100px;
+ background: blue;
+ }
+ </style>
+ </head>
+ <body>
+ <div id="target"></div>
+ </body>
+ <script>
+ var target = document.getElementById("target");
+ var effect =
+ new KeyframeEffectReadOnly(
+ target,
+ { opacity: [0, 1], easing: "step-end" },
+ {
+ fill: "forwards",
+ /* The function produces negative values in (0, 0.766312060) */
+ easing: "cubic-bezier(0,-0.5,1,-0.5)",
+ duration: 100,
+ iterations: 0.75 /* To finish in the extraporation range */
+ }
+ );
+ var animation = new Animation(effect, document.timeline);
+ animation.play();
+ animation.finished.then(function() {
+ document.documentElement.className = "";
+ });
+ </script>
+</html>
diff --git a/dom/animation/test/crashtests/1216842-6.html b/dom/animation/test/crashtests/1216842-6.html
new file mode 100644
index 0000000000..a588c68f1e
--- /dev/null
+++ b/dom/animation/test/crashtests/1216842-6.html
@@ -0,0 +1,38 @@
+<!doctype html>
+<html class="reftest-wait">
+ <head>
+ <title>
+ Bug 1216842: effect-level easing function produces values greater than 1
+ which are passed to step-end function (compositor thread)
+ </title>
+ <style>
+ #target {
+ width: 100px; height: 100px;
+ background: blue;
+ }
+ </style>
+ </head>
+ <body>
+ <div id="target"></div>
+ </body>
+ <script>
+ var target = document.getElementById("target");
+ var effect =
+ new KeyframeEffectReadOnly(
+ target,
+ { opacity: [0, 1], easing: "step-end" },
+ {
+ fill: "forwards",
+ /* The function produces values greater than 1 in (0.23368794, 1) */
+ easing: "cubic-bezier(0,1.5,1,1.5)",
+ duration: 100,
+ iterations: 0.25 /* To finish in the extraporation range */
+ }
+ );
+ var animation = new Animation(effect, document.timeline);
+ animation.play();
+ animation.finished.then(function() {
+ document.documentElement.className = "";
+ });
+ </script>
+</html>
diff --git a/dom/animation/test/crashtests/1239889-1.html b/dom/animation/test/crashtests/1239889-1.html
new file mode 100644
index 0000000000..476f3322b4
--- /dev/null
+++ b/dom/animation/test/crashtests/1239889-1.html
@@ -0,0 +1,12 @@
+<!doctype html>
+<html>
+ <head>
+ <title>Bug 1239889</title>
+ </head>
+ <body>
+ </body>
+ <script>
+ var div = document.createElement('div');
+ var effect = new KeyframeEffectReadOnly(div, { opacity: [0, 1] });
+ </script>
+</html>
diff --git a/dom/animation/test/crashtests/1244595-1.html b/dom/animation/test/crashtests/1244595-1.html
new file mode 100644
index 0000000000..13b2e2d7e7
--- /dev/null
+++ b/dom/animation/test/crashtests/1244595-1.html
@@ -0,0 +1,3 @@
+<div id=target><script>
+ var player = target.animate([{background: 'green'}, {background: 'green'}]);
+</script>
diff --git a/dom/animation/test/crashtests/1272475-1.html b/dom/animation/test/crashtests/1272475-1.html
new file mode 100644
index 0000000000..e0b0495388
--- /dev/null
+++ b/dom/animation/test/crashtests/1272475-1.html
@@ -0,0 +1,20 @@
+<!doctype html>
+<html>
+ <head>
+ <title>Bug 1272475 - scale function with an extreme large value</title>
+ <script>
+ function test() {
+ var div = document.createElement("div");
+ div.setAttribute("style", "width: 1px; height: 1px; " +
+ "background: red;");
+ document.body.appendChild(div);
+ div.animate([ { "transform": "scale(8)" },
+ { "transform": "scale(9.5e+307)" },
+ { "transform": "scale(32)" } ],
+ { "duration": 1000, "fill": "both" });
+ }
+ </script>
+ </head>
+ <body onload="test()">
+ </body>
+</html>
diff --git a/dom/animation/test/crashtests/1272475-2.html b/dom/animation/test/crashtests/1272475-2.html
new file mode 100644
index 0000000000..da0e8605bd
--- /dev/null
+++ b/dom/animation/test/crashtests/1272475-2.html
@@ -0,0 +1,20 @@
+<!doctype html>
+<html>
+ <head>
+ <title>Bug 1272475 - rotate function with an extreme large value</title>
+ <script>
+ function test() {
+ var div = document.createElement("div");
+ div.setAttribute("style", "width: 100px; height: 100px; " +
+ "background: red;");
+ document.body.appendChild(div);
+ div.animate([ { "transform": "rotate(8rad)" },
+ { "transform": "rotate(9.5e+307rad)" },
+ { "transform": "rotate(32rad)" } ],
+ { "duration": 1000, "fill": "both" });
+ }
+ </script>
+ </head>
+ <body onload="test()">
+ </body>
+</html>
diff --git a/dom/animation/test/crashtests/1277272-1-inner.html b/dom/animation/test/crashtests/1277272-1-inner.html
new file mode 100644
index 0000000000..2ba52174dd
--- /dev/null
+++ b/dom/animation/test/crashtests/1277272-1-inner.html
@@ -0,0 +1,19 @@
+<!doctype html>
+<head>
+<script>
+function start() {
+ var animation = document.body.animate([{marks: 'crop'},{marks: 'crop'}], 12);
+ document.write('<html><body></body></html>');
+
+ setTimeout(function() { animation.play(); }, 4);
+ setTimeout(function() {
+ animation.timeline = undefined;
+ SpecialPowers.Cu.forceGC();
+ window.top.continueTest();
+ }, 5);
+}
+</script>
+</head>
+<body onload="start()"></body>
+</html>
+
diff --git a/dom/animation/test/crashtests/1277272-1.html b/dom/animation/test/crashtests/1277272-1.html
new file mode 100644
index 0000000000..f398bcf6d9
--- /dev/null
+++ b/dom/animation/test/crashtests/1277272-1.html
@@ -0,0 +1,26 @@
+<!doctype html>
+<html class="reftest-wait">
+<head>
+<script>
+var count = 0;
+
+function start() {
+ if (++count > 10) {
+ document.documentElement.className = "";
+ return;
+ }
+
+ var frame = document.getElementById("frame");
+ frame.src = "./1277272-1-inner.html";
+}
+
+function continueTest() {
+ setTimeout(start.bind(window), 1);
+}
+
+</script>
+</head>
+<body onload="start()"></body>
+<iframe id="frame">
+</html>
+
diff --git a/dom/animation/test/crashtests/1278485-1.html b/dom/animation/test/crashtests/1278485-1.html
new file mode 100644
index 0000000000..e7347f5d84
--- /dev/null
+++ b/dom/animation/test/crashtests/1278485-1.html
@@ -0,0 +1,26 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="UTF-8">
+<script>
+
+function boom()
+{
+ document.body.animate([],
+ { duration: 6,
+ easing: "cubic-bezier(0, -1e+39, 0, 0)" });
+ document.body.animate([],
+ { duration: 6,
+ easing: "cubic-bezier(0, 1e+39, 0, 0)" });
+ document.body.animate([],
+ { duration: 6,
+ easing: "cubic-bezier(0, 0, 0, -1e+39)" });
+ document.body.animate([],
+ { duration: 6,
+ easing: "cubic-bezier(0, 0, 0, 1e+39)" });
+}
+
+</script>
+</head>
+<body onload="boom();"></body>
+</html>
diff --git a/dom/animation/test/crashtests/1290535-1.html b/dom/animation/test/crashtests/1290535-1.html
new file mode 100644
index 0000000000..20b44d8bfc
--- /dev/null
+++ b/dom/animation/test/crashtests/1290535-1.html
@@ -0,0 +1,20 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>Bug 1290535 - Sort paced subproperties of a shorthand property</title>
+<meta charset="UTF-8">
+<script>
+
+function test()
+{
+ var div = document.createElement('div');
+ document.documentElement.appendChild(div);
+ div.animate([ { borderRadius: "0", borderTopRightRadius: "0" },
+ { borderRadius: "50%" } ],
+ { spacing:"paced(border-radius)" });
+}
+
+</script>
+</head>
+<body onload="test();"></body>
+</html>
diff --git a/dom/animation/test/crashtests/crashtests.list b/dom/animation/test/crashtests/crashtests.list
new file mode 100644
index 0000000000..f61d7f876e
--- /dev/null
+++ b/dom/animation/test/crashtests/crashtests.list
@@ -0,0 +1,13 @@
+pref(dom.animations-api.core.enabled,true) load 1239889-1.html
+pref(dom.animations-api.core.enabled,true) load 1244595-1.html
+pref(dom.animations-api.core.enabled,true) load 1216842-1.html
+pref(dom.animations-api.core.enabled,true) load 1216842-2.html
+pref(dom.animations-api.core.enabled,true) load 1216842-3.html
+pref(dom.animations-api.core.enabled,true) load 1216842-4.html
+pref(dom.animations-api.core.enabled,true) load 1216842-5.html
+pref(dom.animations-api.core.enabled,true) load 1216842-6.html
+pref(dom.animations-api.core.enabled,true) load 1272475-1.html
+pref(dom.animations-api.core.enabled,true) load 1272475-2.html
+pref(dom.animations-api.core.enabled,true) load 1278485-1.html
+pref(dom.animations-api.core.enabled,true) load 1277272-1.html
+pref(dom.animations-api.core.enabled,true) load 1290535-1.html
diff --git a/dom/animation/test/css-animations/file_animation-cancel.html b/dom/animation/test/css-animations/file_animation-cancel.html
new file mode 100644
index 0000000000..85499addfd
--- /dev/null
+++ b/dom/animation/test/css-animations/file_animation-cancel.html
@@ -0,0 +1,154 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="../testcommon.js"></script>
+<style>
+@keyframes translateAnim {
+ to { transform: translate(100px) }
+}
+@keyframes marginLeftAnim {
+ to { margin-left: 100px }
+}
+@keyframes marginLeftAnim100To200 {
+ from { margin-left: 100px }
+ to { margin-left: 200px }
+}
+</style>
+<body>
+<script>
+'use strict';
+
+promise_test(function(t) {
+ var div = addDiv(t, { style: 'animation: translateAnim 100s' });
+ var animation = div.getAnimations()[0];
+
+ return animation.ready.then(function() {
+ assert_not_equals(getComputedStyle(div).transform, 'none',
+ 'transform style is animated before cancelling');
+ animation.cancel();
+ assert_equals(getComputedStyle(div).transform, 'none',
+ 'transform style is no longer animated after cancelling');
+ });
+}, 'Animated style is cleared after cancelling a running CSS animation');
+
+promise_test(function(t) {
+ var div = addDiv(t, { style: 'animation: translateAnim 100s forwards' });
+ var animation = div.getAnimations()[0];
+ animation.finish();
+
+ return animation.ready.then(function() {
+ assert_not_equals(getComputedStyle(div).transform, 'none',
+ 'transform style is filling before cancelling');
+ animation.cancel();
+ assert_equals(getComputedStyle(div).transform, 'none',
+ 'fill style is cleared after cancelling');
+ });
+}, 'Animated style is cleared after cancelling a filling CSS animation');
+
+promise_test(function(t) {
+ var div = addDiv(t, { style: 'animation: translateAnim 100s' });
+ var animation = div.getAnimations()[0];
+ div.addEventListener('animationend', t.step_func(function() {
+ assert_unreached('Got unexpected end event on cancelled animation');
+ }));
+
+ return animation.ready.then(function() {
+ // Seek to just before the end then cancel
+ animation.currentTime = 99.9 * 1000;
+ animation.cancel();
+
+ // Then wait a couple of frames and check that no event was dispatched
+ return waitForAnimationFrames(2);
+ });
+}, 'Cancelled CSS animations do not dispatch events');
+
+test(function(t) {
+ var div = addDiv(t, { style: 'animation: marginLeftAnim 100s linear' });
+ var animation = div.getAnimations()[0];
+ animation.cancel();
+
+ assert_equals(getComputedStyle(div).marginLeft, '0px',
+ 'margin-left style is not animated after cancelling');
+
+ animation.currentTime = 50 * 1000;
+ assert_equals(getComputedStyle(div).marginLeft, '50px',
+ 'margin-left style is updated when cancelled animation is'
+ + ' seeked');
+}, 'After cancelling an animation, it can still be seeked');
+
+promise_test(function(t) {
+ var div =
+ addDiv(t, { style: 'animation: marginLeftAnim100To200 100s linear' });
+ var animation = div.getAnimations()[0];
+
+ return animation.ready.then(function() {
+ animation.cancel();
+ assert_equals(getComputedStyle(div).marginLeft, '0px',
+ 'margin-left style is not animated after cancelling');
+ animation.play();
+ assert_equals(getComputedStyle(div).marginLeft, '100px',
+ 'margin-left style is animated after re-starting animation');
+ return animation.ready;
+ }).then(function() {
+ assert_equals(animation.playState, 'running',
+ 'Animation succeeds in running after being re-started');
+ });
+}, 'After cancelling an animation, it can still be re-used');
+
+test(function(t) {
+ var div =
+ addDiv(t, { style: 'animation: marginLeftAnim100To200 100s linear' });
+ var animation = div.getAnimations()[0];
+ animation.cancel();
+ assert_equals(getComputedStyle(div).marginLeft, '0px',
+ 'margin-left style is not animated after cancelling');
+
+ // Trigger a change to some animation properties and check that this
+ // doesn't cause the animation to become live again
+ div.style.animationDuration = '200s';
+ flushComputedStyle(div);
+ assert_equals(getComputedStyle(div).marginLeft, '0px',
+ 'margin-left style is still not animated after updating'
+ + ' animation-duration');
+ assert_equals(animation.playState, 'idle',
+ 'Animation is still idle after updating animation-duration');
+}, 'After cancelling an animation, updating animation properties doesn\'t make'
+ + ' it live again');
+
+test(function(t) {
+ var div =
+ addDiv(t, { style: 'animation: marginLeftAnim100To200 100s linear' });
+ var animation = div.getAnimations()[0];
+ animation.cancel();
+ assert_equals(getComputedStyle(div).marginLeft, '0px',
+ 'margin-left style is not animated after cancelling');
+
+ // Make some changes to animation-play-state and check that the
+ // animation doesn't become live again. This is because it should be
+ // possible to cancel an animation from script such that all future
+ // changes to style are ignored.
+
+ // Redundant change
+ div.style.animationPlayState = 'running';
+ assert_equals(animation.playState, 'idle',
+ 'Animation is still idle after a redundant change to'
+ + ' animation-play-state');
+
+ // Pause
+ div.style.animationPlayState = 'paused';
+ assert_equals(animation.playState, 'idle',
+ 'Animation is still idle after setting'
+ + ' animation-play-state: paused');
+
+ // Play
+ div.style.animationPlayState = 'running';
+ assert_equals(animation.playState, 'idle',
+ 'Animation is still idle after re-setting'
+ + ' animation-play-state: running');
+
+}, 'After cancelling an animation, updating animation-play-state doesn\'t'
+ + ' make it live again');
+
+done();
+</script>
+</body>
+</html>
diff --git a/dom/animation/test/css-animations/file_animation-computed-timing.html b/dom/animation/test/css-animations/file_animation-computed-timing.html
new file mode 100644
index 0000000000..53597a473a
--- /dev/null
+++ b/dom/animation/test/css-animations/file_animation-computed-timing.html
@@ -0,0 +1,566 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="../testcommon.js"></script>
+<style>
+@keyframes moveAnimation {
+ from { margin-left: 100px }
+ to { margin-left: 200px }
+}
+</style>
+<body>
+<script>
+
+'use strict';
+
+// --------------------
+// delay
+// --------------------
+test(function(t) {
+ var div = addDiv(t, {style: 'animation: moveAnimation 100s'});
+ var effect = div.getAnimations()[0].effect;
+ assert_equals(effect.getComputedTiming().delay, 0,
+ 'Initial value of delay');
+}, 'delay of a new animation');
+
+test(function(t) {
+ var div = addDiv(t, {style: 'animation: moveAnimation 100s -10s'});
+ var effect = div.getAnimations()[0].effect;
+ assert_equals(effect.getComputedTiming().delay, -10 * MS_PER_SEC,
+ 'Initial value of delay');
+}, 'Negative delay of a new animation');
+
+test(function(t) {
+ var div = addDiv(t, {style: 'animation: moveAnimation 100s 10s'});
+ var effect = div.getAnimations()[0].effect;
+ assert_equals(effect.getComputedTiming().delay, 10 * MS_PER_SEC,
+ 'Initial value of delay');
+}, 'Positive delay of a new animation');
+
+
+// --------------------
+// endDelay
+// --------------------
+test(function(t) {
+ var div = addDiv(t, {style: 'animation: moveAnimation 100s'});
+ var effect = div.getAnimations()[0].effect;
+ assert_equals(effect.getComputedTiming().endDelay, 0,
+ 'Initial value of endDelay');
+}, 'endDelay of a new animation');
+
+
+// --------------------
+// fill
+// --------------------
+test(function(t) {
+ var getEffectWithFill = function(fill) {
+ var div = addDiv(t, {style: 'animation: moveAnimation 100s ' + fill});
+ return div.getAnimations()[0].effect;
+ };
+
+ var effect = getEffectWithFill('');
+ assert_equals(effect.getComputedTiming().fill, 'none',
+ 'Initial value of fill');
+ effect = getEffectWithFill('forwards');
+ assert_equals(effect.getComputedTiming().fill, 'forwards',
+ 'Fill forwards');
+ effect = getEffectWithFill('backwards');
+ assert_equals(effect.getComputedTiming().fill, 'backwards',
+ 'Fill backwards');
+ effect = getEffectWithFill('both');
+ assert_equals(effect.getComputedTiming().fill, 'both',
+ 'Fill forwards and backwards');
+}, 'fill of a new animation');
+
+
+// --------------------
+// iterationStart
+// --------------------
+test(function(t) {
+ var div = addDiv(t, {style: 'animation: moveAnimation 100s'});
+ var effect = div.getAnimations()[0].effect;
+ assert_equals(effect.getComputedTiming().iterationStart, 0,
+ 'Initial value of iterationStart');
+}, 'iterationStart of a new animation');
+
+
+// --------------------
+// iterations
+// --------------------
+test(function(t) {
+ var div = addDiv(t, {style: 'animation: moveAnimation 100s'});
+ var effect = div.getAnimations()[0].effect;
+ assert_equals(effect.getComputedTiming().iterations, 1,
+ 'Initial value of iterations');
+}, 'iterations of a new animation');
+
+test(function(t) {
+ var div = addDiv(t, {style: 'animation: moveAnimation 100s 2016.5'});
+ var effect = div.getAnimations()[0].effect;
+ assert_equals(effect.getComputedTiming().iterations, 2016.5,
+ 'Initial value of iterations');
+}, 'iterations of a finitely repeating animation');
+
+test(function(t) {
+ var div = addDiv(t, {style: 'animation: moveAnimation 100s infinite'});
+ var effect = div.getAnimations()[0].effect;
+ assert_equals(effect.getComputedTiming().iterations, Infinity,
+ 'Initial value of iterations');
+}, 'iterations of an infinitely repeating animation');
+
+
+// --------------------
+// duration
+// --------------------
+test(function(t) {
+ var div = addDiv(t, {style: 'animation: moveAnimation 100s -10s infinite'});
+ var effect = div.getAnimations()[0].effect;
+ assert_equals(effect.getComputedTiming().duration, 100 * MS_PER_SEC,
+ 'Initial value of duration');
+}, 'duration of a new animation');
+
+
+// --------------------
+// direction
+// --------------------
+test(function(t) {
+ var getEffectWithDir = function(dir) {
+ var div = addDiv(t, {style: 'animation: moveAnimation 100s ' + dir});
+ return div.getAnimations()[0].effect;
+ };
+
+ var effect = getEffectWithDir('');
+ assert_equals(effect.getComputedTiming().direction, 'normal',
+ 'Initial value of normal direction');
+ effect = getEffectWithDir('reverse');
+ assert_equals(effect.getComputedTiming().direction, 'reverse',
+ 'Initial value of reverse direction');
+ effect = getEffectWithDir('alternate');
+ assert_equals(effect.getComputedTiming().direction, 'alternate',
+ 'Initial value of alternate direction');
+ effect = getEffectWithDir('alternate-reverse');
+ assert_equals(effect.getComputedTiming().direction, 'alternate-reverse',
+ 'Initial value of alternate-reverse direction');
+}, 'direction of a new animation');
+
+
+// --------------------
+// easing
+// --------------------
+test(function(t) {
+ var div = addDiv(t, {style: 'animation: moveAnimation 100s'});
+ var effect = div.getAnimations()[0].effect;
+ assert_equals(effect.getComputedTiming().easing, 'linear',
+ 'Initial value of easing');
+}, 'easing of a new animation');
+
+
+// ------------------------------
+// endTime
+// = max(start delay + active duration + end delay, 0)
+// --------------------
+test(function(t) {
+ var div = addDiv(t, {style: 'animation: moveAnimation 100s'});
+ var effect = div.getAnimations()[0].effect;
+ assert_equals(effect.getComputedTiming().endTime, 100 * MS_PER_SEC,
+ 'Initial value of endTime');
+}, 'endTime of an new animation');
+
+test(function(t) {
+ var div = addDiv(t, {style: 'animation: moveAnimation 100s -5s'});
+ var effect = div.getAnimations()[0].effect;
+ var answer = (100 - 5) * MS_PER_SEC;
+ assert_equals(effect.getComputedTiming().endTime, answer,
+ 'Initial value of endTime');
+}, 'endTime of an animation with a negative delay');
+
+test(function(t) {
+ var div = addDiv(t, {style: 'animation: moveAnimation 10s -100s infinite'});
+ var effect = div.getAnimations()[0].effect;
+ assert_equals(effect.getComputedTiming().endTime, Infinity,
+ 'Initial value of endTime');
+}, 'endTime of an infinitely repeating animation');
+
+test(function(t) {
+ var div = addDiv(t, {style: 'animation: moveAnimation 0s 100s infinite'});
+ var effect = div.getAnimations()[0].effect;
+ assert_equals(effect.getComputedTiming().endTime, 100 * MS_PER_SEC,
+ 'Initial value of endTime');
+}, 'endTime of an infinitely repeating zero-duration animation');
+
+test(function(t) {
+ // Fill forwards so div.getAnimations()[0] won't return an
+ // undefined value.
+ var div = addDiv(t, {style: 'animation: moveAnimation 10s -100s forwards'});
+ var effect = div.getAnimations()[0].effect;
+ assert_equals(effect.getComputedTiming().endTime, 0,
+ 'Initial value of endTime');
+}, 'endTime of an animation that finishes before its startTime');
+
+
+// --------------------
+// activeDuration
+// = iteration duration * iteration count
+// --------------------
+test(function(t) {
+ var div = addDiv(t, {style: 'animation: moveAnimation 100s 5'});
+ var effect = div.getAnimations()[0].effect;
+ var answer = 100 * MS_PER_SEC * 5;
+ assert_equals(effect.getComputedTiming().activeDuration, answer,
+ 'Initial value of activeDuration');
+}, 'activeDuration of a new animation');
+
+test(function(t) {
+ var div = addDiv(t, {style: 'animation: moveAnimation 100s infinite'});
+ var effect = div.getAnimations()[0].effect;
+ assert_equals(effect.getComputedTiming().activeDuration, Infinity,
+ 'Initial value of activeDuration');
+}, 'activeDuration of an infinitely repeating animation');
+
+test(function(t) {
+ var div = addDiv(t, {style: 'animation: moveAnimation 0s 1s infinite'});
+ var effect = div.getAnimations()[0].effect;
+ // If either the iteration duration or iteration count are zero,
+ // the active duration is zero.
+ assert_equals(effect.getComputedTiming().activeDuration, 0,
+ 'Initial value of activeDuration');
+}, 'activeDuration of an infinitely repeating zero-duration animation');
+
+test(function(t) {
+ var div = addDiv(t, {style: 'animation: moveAnimation 100s 1s 0'});
+ var effect = div.getAnimations()[0].effect;
+ // If either the iteration duration or iteration count are zero,
+ // the active duration is zero.
+ assert_equals(effect.getComputedTiming().activeDuration, 0,
+ 'Initial value of activeDuration');
+}, 'activeDuration of an animation with zero iterations');
+
+
+// --------------------
+// localTime
+// --------------------
+test(function(t) {
+ var div = addDiv(t, {style: 'animation: moveAnimation 100s'});
+ var effect = div.getAnimations()[0].effect;
+ assert_equals(effect.getComputedTiming().localTime, 0,
+ 'Initial value of localTime');
+}, 'localTime of a new animation');
+
+test(function(t) {
+ var div = addDiv(t, {style: 'animation: moveAnimation 100s'});
+ var anim = div.getAnimations()[0];
+ anim.currentTime = 5 * MS_PER_SEC;
+ assert_equals(anim.effect.getComputedTiming().localTime, anim.currentTime,
+ 'current localTime after setting currentTime');
+}, 'localTime of an animation is always equal to currentTime');
+
+promise_test(function(t) {
+ var div = addDiv(t, {style: 'animation: moveAnimation 100s'});
+
+ var anim = div.getAnimations()[0];
+ anim.playbackRate = 2; // 2 times faster
+
+ return anim.ready.then(function() {
+ assert_equals(anim.effect.getComputedTiming().localTime, anim.currentTime,
+ 'localTime is equal to currentTime');
+ return waitForFrame();
+ }).then(function() {
+ assert_equals(anim.effect.getComputedTiming().localTime, anim.currentTime,
+ 'localTime is equal to currentTime');
+ });
+}, 'localTime reflects playbackRate immediately');
+
+test(function(t) {
+ var div = addDiv(t);
+ var effect = new KeyframeEffectReadOnly(div, {left: ["0px", "100px"]});
+
+ assert_equals(effect.getComputedTiming().localTime, null,
+ 'localTime for orphaned effect');
+}, 'localTime of an AnimationEffect without an Animation');
+
+
+// --------------------
+// progress
+// Note: Default timing function is linear.
+// --------------------
+test(function(t) {
+ [{fill: '', progress: [ null, null ]},
+ {fill: 'none', progress: [ null, null ]},
+ {fill: 'forwards', progress: [ null, 1.0 ]},
+ {fill: 'backwards', progress: [ 0.0, null ]},
+ {fill: 'both', progress: [ 0.0, 1.0 ]}]
+ .forEach(function(test) {
+ var div =
+ addDiv(t, {style: 'animation: moveAnimation 100s 10s ' + test.fill});
+ var anim = div.getAnimations()[0];
+ assert_true(anim.effect.getComputedTiming().progress === test.progress[0],
+ 'initial progress with "' + test.fill + '" fill');
+ anim.finish();
+ assert_true(anim.effect.getComputedTiming().progress === test.progress[1],
+ 'finished progress with "' + test.fill + '" fill');
+ });
+}, 'progress of an animation with different fill modes');
+
+test(function(t) {
+ var div = addDiv(t, {style: 'animation: moveAnimation 10s 10 both'});
+ var anim = div.getAnimations()[0];
+
+ assert_equals(anim.effect.getComputedTiming().progress, 0.0,
+ 'Initial value of progress');
+ anim.currentTime += 2.5 * MS_PER_SEC;
+ assert_equals(anim.effect.getComputedTiming().progress, 0.25,
+ 'Value of progress');
+ anim.currentTime += 5 * MS_PER_SEC;
+ assert_equals(anim.effect.getComputedTiming().progress, 0.75,
+ 'Value of progress');
+ anim.currentTime += 5 * MS_PER_SEC;
+ assert_equals(anim.effect.getComputedTiming().progress, 0.25,
+ 'Value of progress');
+ anim.finish()
+ assert_equals(anim.effect.getComputedTiming().progress, 1.0,
+ 'Value of progress');
+}, 'progress of an integral repeating animation with normal direction');
+
+test(function(t) {
+ var div = addDiv(t);
+ // Note: FillMode here is "both" because
+ // 1. Since this a zero-duration animation, it will already have finished
+ // so it won't be returned by getAnimations() unless it fills forwards.
+ // 2. Fill backwards, so the progress before phase wouldn't be
+ // unresolved (null value).
+ var div = addDiv(t, {style: 'animation: moveAnimation 0s infinite both'});
+ var anim = div.getAnimations()[0];
+
+ assert_equals(anim.effect.getComputedTiming().progress, 1.0,
+ 'Initial value of progress in after phase');
+
+ // Seek backwards
+ anim.currentTime -= 1 * MS_PER_SEC;
+ assert_equals(anim.effect.getComputedTiming().progress, 0.0,
+ 'Value of progress before phase');
+}, 'progress of an infinitely repeating zero-duration animation');
+
+test(function(t) {
+ // Default iterations = 1
+ var div = addDiv(t, {style: 'animation: moveAnimation 0s both'});
+ var anim = div.getAnimations()[0];
+
+ assert_equals(anim.effect.getComputedTiming().progress, 1.0,
+ 'Initial value of progress in after phase');
+
+ // Seek backwards
+ anim.currentTime -= 1 * MS_PER_SEC;
+ assert_equals(anim.effect.getComputedTiming().progress, 0.0,
+ 'Value of progress before phase');
+}, 'progress of a finitely repeating zero-duration animation');
+
+test(function(t) {
+ var div = addDiv(t, {style: 'animation: moveAnimation 0s 5s 10.25 both'});
+ var anim = div.getAnimations()[0];
+
+ assert_equals(anim.effect.getComputedTiming().progress, 0.0,
+ 'Initial value of progress (before phase)');
+
+ // Using iteration duration of 1 now.
+ // currentIteration now is floor(10.25) = 10, so progress should be 25%.
+ anim.finish();
+ assert_equals(anim.effect.getComputedTiming().progress, 0.25,
+ 'Value of progress in after phase');
+}, 'progress of a non-integral repeating zero-duration animation');
+
+test(function(t) {
+ var div = addDiv(t, {style: 'animation: moveAnimation 0s 5s 10.25 both reverse'});
+ var anim = div.getAnimations()[0];
+
+ assert_equals(anim.effect.getComputedTiming().progress, 1.0,
+ 'Initial value of progress (before phase)');
+
+ // Seek forwards
+ anim.finish();
+ assert_equals(anim.effect.getComputedTiming().progress, 0.75,
+ 'Value of progress in after phase');
+}, 'Progress of a non-integral repeating zero-duration animation ' +
+ 'with reversing direction');
+
+test(function(t) {
+ var div = addDiv(t, {style: 'animation: moveAnimation 10s 10.25 both alternate'});
+ var anim = div.getAnimations()[0];
+
+ assert_equals(anim.effect.getComputedTiming().progress, 0.0,
+ 'Initial value of progress');
+ anim.currentTime += 2.5 * MS_PER_SEC;
+ assert_equals(anim.effect.getComputedTiming().progress, 0.25,
+ 'Value of progress');
+ anim.currentTime += 5 * MS_PER_SEC;
+ assert_equals(anim.effect.getComputedTiming().progress, 0.75,
+ 'Value of progress');
+ anim.currentTime += 5 * MS_PER_SEC;
+ assert_equals(anim.effect.getComputedTiming().progress, 0.75,
+ 'Value of progress');
+ anim.finish()
+ assert_equals(anim.effect.getComputedTiming().progress, 0.25,
+ 'Value of progress');
+}, 'progress of a non-integral repeating animation ' +
+ 'with alternate direction');
+
+test(function(t) {
+ var div = addDiv(t, {style: 'animation: moveAnimation 10s 10.25 both alternate-reverse'});
+ var anim = div.getAnimations()[0];
+
+ assert_equals(anim.effect.getComputedTiming().progress, 1.0,
+ 'Initial value of progress');
+ anim.currentTime += 2.5 * MS_PER_SEC;
+ assert_equals(anim.effect.getComputedTiming().progress, 0.75,
+ 'Value of progress');
+ anim.currentTime += 5 * MS_PER_SEC;
+ assert_equals(anim.effect.getComputedTiming().progress, 0.25,
+ 'Value of progress');
+ anim.currentTime += 5 * MS_PER_SEC;
+ assert_equals(anim.effect.getComputedTiming().progress, 0.25,
+ 'Value of progress');
+ anim.finish()
+ assert_equals(anim.effect.getComputedTiming().progress, 0.75,
+ 'Value of progress');
+}, 'progress of a non-integral repeating animation ' +
+ 'with alternate-reversing direction');
+
+test(function(t) {
+ var div = addDiv(t, {style: 'animation: moveAnimation 0s 10.25 both alternate'});
+ var anim = div.getAnimations()[0];
+
+ assert_equals(anim.effect.getComputedTiming().progress, 0.25,
+ 'Initial value of progress');
+ anim.currentTime += 2.5 * MS_PER_SEC;
+ assert_equals(anim.effect.getComputedTiming().progress, 0.25,
+ 'Value of progress');
+ anim.currentTime -= 5 * MS_PER_SEC;
+ assert_equals(anim.effect.getComputedTiming().progress, 0.0,
+ 'Value of progress');
+ anim.finish()
+ assert_equals(anim.effect.getComputedTiming().progress, 0.25,
+ 'Value of progress');
+}, 'progress of a non-integral repeating zero-duration animation ' +
+ 'with alternate direction');
+
+test(function(t) {
+ var div = addDiv(t, {style: 'animation: moveAnimation 0s 10.25 both alternate-reverse'});
+ var anim = div.getAnimations()[0];
+
+ assert_equals(anim.effect.getComputedTiming().progress, 0.75,
+ 'Initial value of progress');
+ anim.currentTime += 2.5 * MS_PER_SEC;
+ assert_equals(anim.effect.getComputedTiming().progress, 0.75,
+ 'Value of progress');
+ anim.currentTime -= 5 * MS_PER_SEC;
+ assert_equals(anim.effect.getComputedTiming().progress, 1.0,
+ 'Value of progress');
+ anim.finish()
+ assert_equals(anim.effect.getComputedTiming().progress, 0.75,
+ 'Value of progress');
+}, 'progress of a non-integral repeating zero-duration animation ' +
+ 'with alternate-reverse direction');
+
+
+// --------------------
+// currentIteration
+// --------------------
+test(function(t) {
+ var div = addDiv(t, {style: 'animation: moveAnimation 100s 2s'});
+ var effect = div.getAnimations()[0].effect;
+ assert_equals(effect.getComputedTiming().currentIteration, null,
+ 'Initial value of currentIteration before phase');
+}, 'currentIteration of a new animation with no backwards fill is unresolved ' +
+ 'in before phase');
+
+test(function(t) {
+ var div = addDiv(t, {style: 'animation: moveAnimation 100s'});
+ var anim = div.getAnimations()[0];
+ assert_equals(anim.effect.getComputedTiming().currentIteration, 0,
+ 'Initial value of currentIteration');
+}, 'currentIteration of a new animation is zero');
+
+test(function(t) {
+ // Note: FillMode here is "both" because
+ // 1. Since this a zero-duration animation, it will already have finished
+ // so it won't be returned by getAnimations() unless it fills forwards.
+ // 2. Fill backwards, so the currentIteration (before phase) wouldn't be
+ // unresolved (null value).
+ var div = addDiv(t, {style: 'animation: moveAnimation 0s infinite both'});
+ var anim = div.getAnimations()[0];
+
+ assert_equals(anim.effect.getComputedTiming().currentIteration, Infinity,
+ 'Initial value of currentIteration in after phase');
+
+ // Seek backwards
+ anim.currentTime -= 2 * MS_PER_SEC;
+ assert_equals(anim.effect.getComputedTiming().currentIteration, 0,
+ 'Value of currentIteration count during before phase');
+}, 'currentIteration of an infinitely repeating zero-duration animation');
+
+test(function(t) {
+ var div = addDiv(t, {style: 'animation: moveAnimation 0s 10.5 both'});
+ var anim = div.getAnimations()[0];
+
+ // Note: currentIteration = ceil(iteration start + iteration count) - 1
+ assert_equals(anim.effect.getComputedTiming().currentIteration, 10,
+ 'Initial value of currentIteration');
+
+ // Seek backwards
+ anim.currentTime -= 2 * MS_PER_SEC;
+ assert_equals(anim.effect.getComputedTiming().currentIteration, 0,
+ 'Value of currentIteration count during before phase');
+}, 'currentIteration of a finitely repeating zero-duration animation');
+
+test(function(t) {
+ var div = addDiv(t, {style: 'animation: moveAnimation 100s 5.5 forwards'});
+ var anim = div.getAnimations()[0];
+
+ assert_equals(anim.effect.getComputedTiming().currentIteration, 0,
+ 'Initial value of currentIteration');
+ // The 3rd iteration
+ // Note: currentIteration = floor(scaled active time / iteration duration)
+ anim.currentTime = 250 * MS_PER_SEC;
+ assert_equals(anim.effect.getComputedTiming().currentIteration, 2,
+ 'Value of currentIteration during the 3rd iteration');
+ // Finish
+ anim.finish();
+ assert_equals(anim.effect.getComputedTiming().currentIteration, 5,
+ 'Value of currentIteration in after phase');
+}, 'currentIteration of an animation with a non-integral iteration count');
+
+test(function(t) {
+ var div = addDiv(t, {style: 'animation: moveAnimation 100s 2 forwards'});
+ var anim = div.getAnimations()[0];
+
+ assert_equals(anim.effect.getComputedTiming().currentIteration, 0,
+ 'Initial value of currentIteration');
+ // Finish
+ anim.finish();
+ assert_equals(anim.effect.getComputedTiming().currentIteration, 1,
+ 'Value of currentIteration in after phase');
+}, 'currentIteration of an animation with an integral iteration count');
+
+test(function(t) {
+ var div = addDiv(t, {style: 'animation: moveAnimation 100s forwards'});
+ var anim = div.getAnimations()[0];
+ assert_equals(anim.effect.getComputedTiming().currentIteration, 0,
+ 'Initial value of currentIteration');
+ // Finish
+ anim.finish();
+ assert_equals(anim.effect.getComputedTiming().currentIteration, 0,
+ 'Value of currentIteration in after phase');
+}, 'currentIteration of an animation with a default iteration count');
+
+test(function(t) {
+ var div = addDiv(t);
+ var effect = new KeyframeEffectReadOnly(div, {left: ["0px", "100px"]});
+
+ assert_equals(effect.getComputedTiming().currentIteration, null,
+ 'currentIteration for orphaned effect');
+}, 'currentIteration of an AnimationEffect without an Animation');
+
+// TODO: If iteration duration is Infinity, currentIteration is 0.
+// However, we cannot set iteration duration to Infinity in CSS Animation now.
+
+done();
+</script>
+</body>
diff --git a/dom/animation/test/css-animations/file_animation-currenttime.html b/dom/animation/test/css-animations/file_animation-currenttime.html
new file mode 100644
index 0000000000..ec6fb3f1a8
--- /dev/null
+++ b/dom/animation/test/css-animations/file_animation-currenttime.html
@@ -0,0 +1,345 @@
+<!doctype html>
+<html>
+ <head>
+ <meta charset=utf-8>
+ <title>Tests for the effect of setting a CSS animation's
+ Animation.currentTime</title>
+ <style>
+
+.animated-div {
+ margin-left: 10px;
+ /* Make it easier to calculate expected values: */
+ animation-timing-function: linear ! important;
+}
+
+@keyframes anim {
+ from { margin-left: 100px; }
+ to { margin-left: 200px; }
+}
+
+ </style>
+ <script src="../testcommon.js"></script>
+ </head>
+ <body>
+ <script type="text/javascript">
+
+'use strict';
+
+// TODO: We should separate this test(Testing for CSS Animation events /
+// Testing for currentTime of Web Animation).
+// e.g:
+// CSS Animation events test :
+// - check the firing an event using Animation.currentTime
+// The current Time of Web Animation test :
+// - check an current time value on several situation(init / processing..)
+// - Based on W3C Spec, check the behavior of setting current time.
+
+// TODO: Once the computedTiming property is implemented, add checks to the
+// checker helpers to ensure that computedTiming's properties are updated as
+// expected.
+// See https://bugzilla.mozilla.org/show_bug.cgi?id=1108055
+
+const CSS_ANIM_EVENTS =
+ ['animationstart', 'animationiteration', 'animationend'];
+
+test(function(t)
+{
+ var div = addDiv(t, {'class': 'animated-div'});
+ div.style.animation = "anim 100s";
+ var animation = div.getAnimations()[0];
+
+ // Animations shouldn't start until the next paint tick, so:
+ assert_equals(animation.currentTime, 0,
+ 'Animation.currentTime should be zero when an animation ' +
+ 'is initially created');
+
+ // Make sure the animation is running before we set the current time.
+ animation.startTime = animation.timeline.currentTime;
+
+ animation.currentTime = 50 * MS_PER_SEC;
+ assert_times_equal(animation.currentTime, 50 * MS_PER_SEC,
+ 'Check setting of currentTime actually works');
+}, 'Sanity test to check round-tripping assigning to new animation\'s ' +
+ 'currentTime');
+
+promise_test(function(t) {
+ var div = addDiv(t, {'class': 'animated-div'});
+ var eventWatcher = new EventWatcher(t, div, CSS_ANIM_EVENTS);
+ div.style.animation = "anim 100s 100s";
+ var animation = div.getAnimations()[0];
+
+ return animation.ready.then(function() {
+ // the 0.0001 here is for rounding error
+ assert_less_than_equal(animation.currentTime,
+ animation.timeline.currentTime - animation.startTime + 0.0001,
+ 'Animation.currentTime should be less than the local time ' +
+ 'equivalent of the timeline\'s currentTime on the first paint tick ' +
+ 'after animation creation');
+
+ animation.currentTime = 100 * MS_PER_SEC;
+ return eventWatcher.wait_for('animationstart');
+ }).then(function() {
+ animation.currentTime = 200 * MS_PER_SEC;
+ return eventWatcher.wait_for('animationend');
+ });
+}, 'Skipping forward through animation');
+
+promise_test(function(t) {
+ var div = addDiv(t, {'class': 'animated-div'});
+ var eventWatcher = new EventWatcher(t, div, CSS_ANIM_EVENTS);
+ div.style.animation = "anim 100s 100s";
+ var animation = div.getAnimations()[0];
+ animation.currentTime = 200 * MS_PER_SEC;
+ var previousTimelineTime = animation.timeline.currentTime;
+
+ return eventWatcher.wait_for(['animationstart',
+ 'animationend']).then(function() {
+ assert_true(document.timeline.currentTime - previousTimelineTime <
+ 100 * MS_PER_SEC,
+ 'Sanity check that seeking worked rather than the events ' +
+ 'firing after normal playback through the very long ' +
+ 'animation duration');
+
+ animation.currentTime = 150 * MS_PER_SEC;
+ return eventWatcher.wait_for('animationstart');
+ }).then(function() {
+ animation.currentTime = 0;
+ return eventWatcher.wait_for('animationend');
+ });
+}, 'Skipping backwards through animation');
+
+// Next we have multiple tests to check that redundant currentTime changes do
+// NOT dispatch events. It's impossible to distinguish between events not being
+// dispatched and events just taking an incredibly long time to dispatch
+// without waiting an infinitely long time. Obviously we don't want to do that
+// (block this test from finishing forever), so instead we just listen for
+// events until two animation frames (i.e. requestAnimationFrame callbacks)
+// have happened, then assume that no events will ever be dispatched for the
+// redundant changes if no events were detected in that time.
+
+promise_test(function(t) {
+ var div = addDiv(t, {'class': 'animated-div'});
+ var eventWatcher = new EventWatcher(t, div, CSS_ANIM_EVENTS);
+ div.style.animation = "anim 100s 100s";
+ var animation = div.getAnimations()[0];
+
+ animation.currentTime = 150 * MS_PER_SEC;
+ animation.currentTime = 50 * MS_PER_SEC;
+
+ return waitForAnimationFrames(2);
+}, 'Redundant change, before -> active, then back');
+
+promise_test(function(t) {
+ var div = addDiv(t, {'class': 'animated-div'});
+ var eventWatcher = new EventWatcher(t, div, CSS_ANIM_EVENTS);
+ div.style.animation = "anim 100s 100s";
+ var animation = div.getAnimations()[0];
+
+ animation.currentTime = 250 * MS_PER_SEC;
+ animation.currentTime = 50 * MS_PER_SEC;
+
+ return waitForAnimationFrames(2);
+}, 'Redundant change, before -> after, then back');
+
+promise_test(function(t) {
+ var div = addDiv(t, {'class': 'animated-div'});
+ var eventWatcher = new EventWatcher(t, div, CSS_ANIM_EVENTS);
+ div.style.animation = "anim 100s 100s";
+ var animation = div.getAnimations()[0];
+
+ var retPromise = eventWatcher.wait_for('animationstart').then(function() {
+ animation.currentTime = 50 * MS_PER_SEC;
+ animation.currentTime = 150 * MS_PER_SEC;
+
+ return waitForAnimationFrames(2);
+ });
+ // get us into the initial state:
+ animation.currentTime = 150 * MS_PER_SEC;
+
+ return retPromise;
+}, 'Redundant change, active -> before, then back');
+
+promise_test(function(t) {
+ var div = addDiv(t, {'class': 'animated-div'});
+ var eventWatcher = new EventWatcher(t, div, CSS_ANIM_EVENTS);
+ div.style.animation = "anim 100s 100s";
+ var animation = div.getAnimations()[0];
+
+ var retPromise = eventWatcher.wait_for('animationstart').then(function() {
+ animation.currentTime = 250 * MS_PER_SEC;
+ animation.currentTime = 150 * MS_PER_SEC;
+
+ return waitForAnimationFrames(2);
+ });
+ // get us into the initial state:
+ animation.currentTime = 150 * MS_PER_SEC;
+
+ return retPromise;
+}, 'Redundant change, active -> after, then back');
+
+promise_test(function(t) {
+ var div = addDiv(t, {'class': 'animated-div'});
+ var eventWatcher = new EventWatcher(t, div, CSS_ANIM_EVENTS);
+ div.style.animation = "anim 100s 100s";
+ var animation = div.getAnimations()[0];
+
+ var retPromise = eventWatcher.wait_for(['animationstart',
+ 'animationend']).then(function() {
+ animation.currentTime = 50 * MS_PER_SEC;
+ animation.currentTime = 250 * MS_PER_SEC;
+
+ return waitForAnimationFrames(2);
+ });
+ // get us into the initial state:
+ animation.currentTime = 250 * MS_PER_SEC;
+
+ return retPromise;
+}, 'Redundant change, after -> before, then back');
+
+promise_test(function(t) {
+ var div = addDiv(t, {'class': 'animated-div'});
+ var eventWatcher = new EventWatcher(t, div, CSS_ANIM_EVENTS);
+ div.style.animation = "anim 100s 100s";
+ var animation = div.getAnimations()[0];
+
+ var retPromise = eventWatcher.wait_for(['animationstart',
+ 'animationend']).then(function() {
+ animation.currentTime = 150 * MS_PER_SEC;
+ animation.currentTime = 250 * MS_PER_SEC;
+
+ return waitForAnimationFrames(2);
+ });
+ // get us into the initial state:
+ animation.currentTime = 250 * MS_PER_SEC;
+
+ return retPromise;
+}, 'Redundant change, after -> active, then back');
+
+promise_test(function(t) {
+ var div = addDiv(t, {'class': 'animated-div'});
+ var eventWatcher = new EventWatcher(t, div, CSS_ANIM_EVENTS);
+ div.style.animation = "anim 100s"
+ var animation = div.getAnimations()[0];
+
+ animation.pause();
+ animation.currentTime = 150 * MS_PER_SEC;
+
+ return eventWatcher.wait_for(['animationstart',
+ 'animationend']).then(function() {
+ animation.currentTime = 50 * MS_PER_SEC;
+ return eventWatcher.wait_for('animationstart');
+ });
+}, 'Seeking finished -> paused dispatches animationstart');
+
+promise_test(function(t) {
+ var div = addDiv(t, {'class': 'animated-div'});
+ div.style.animation = "anim 100s";
+
+ var animation = div.getAnimations()[0];
+
+ return animation.ready.then(function() {
+ var exception;
+ try {
+ animation.currentTime = null;
+ } catch (e) {
+ exception = e;
+ }
+ assert_equals(exception.name, 'TypeError',
+ 'Expect TypeError exception on trying to set ' +
+ 'Animation.currentTime to null');
+ });
+}, 'Setting currentTime to null');
+
+promise_test(function(t) {
+ var div = addDiv(t, {'class': 'animated-div'});
+ div.style.animation = 'anim 100s';
+
+ var animation = div.getAnimations()[0];
+ var pauseTime;
+
+ return animation.ready.then(function() {
+ assert_not_equals(animation.currentTime, null,
+ 'Animation.currentTime not null on ready Promise resolve');
+ animation.pause();
+ return animation.ready;
+ }).then(function() {
+ pauseTime = animation.currentTime;
+ return waitForFrame();
+ }).then(function() {
+ assert_equals(animation.currentTime, pauseTime,
+ 'Animation.currentTime is unchanged after pausing');
+ });
+}, 'Animation.currentTime after pausing');
+
+promise_test(function(t) {
+ var div = addDiv(t, {'class': 'animated-div'});
+ div.style.animation = "anim 100s";
+ var animation = div.getAnimations()[0];
+
+ return animation.ready.then(function() {
+ // just before animation ends:
+ animation.currentTime = 100 * MS_PER_SEC - 1;
+ return waitForAnimationFrames(2);
+ }).then(function() {
+ assert_equals(animation.currentTime, 100 * MS_PER_SEC,
+ 'Animation.currentTime should not continue to increase after the ' +
+ 'animation has finished');
+ });
+}, 'Animation.currentTime clamping');
+
+promise_test(function(t) {
+ var div = addDiv(t, {'class': 'animated-div'});
+ div.style.animation = "anim 100s";
+ var animation = div.getAnimations()[0];
+
+ return animation.ready.then(function() {
+ // play backwards:
+ animation.playbackRate = -1;
+
+ // just before animation ends (at the "start"):
+ animation.currentTime = 1;
+
+ return waitForAnimationFrames(2);
+ }).then(function() {
+ assert_equals(animation.currentTime, 0,
+ 'Animation.currentTime should not continue to decrease after an ' +
+ 'animation running in reverse has finished and currentTime is zero');
+ });
+}, 'Animation.currentTime clamping for reversed animation');
+
+test(function(t) {
+ var div = addDiv(t, {'class': 'animated-div'});
+ div.style.animation = 'anim 100s';
+ var animation = div.getAnimations()[0];
+ animation.cancel();
+
+ assert_equals(animation.currentTime, null,
+ 'The currentTime of a cancelled animation should be null');
+}, 'Animation.currentTime after cancelling');
+
+promise_test(function(t) {
+ var div = addDiv(t, {'class': 'animated-div'});
+ div.style.animation = 'anim 100s';
+ var animation = div.getAnimations()[0];
+
+ return animation.ready.then(function() {
+ animation.finish();
+
+ // Initiate a pause then abort it
+ animation.pause();
+ animation.play();
+
+ // Wait to return to running state
+ return animation.ready;
+ }).then(function() {
+ assert_true(animation.currentTime < 100 * 1000,
+ 'After aborting a pause when finished, the currentTime should'
+ + ' jump back towards the start of the animation');
+ });
+}, 'After aborting a pause when finished, the call to play() should rewind'
+ + ' the current time');
+
+done();
+ </script>
+ </body>
+</html>
diff --git a/dom/animation/test/css-animations/file_animation-finish.html b/dom/animation/test/css-animations/file_animation-finish.html
new file mode 100644
index 0000000000..996cb2ce7f
--- /dev/null
+++ b/dom/animation/test/css-animations/file_animation-finish.html
@@ -0,0 +1,97 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="../testcommon.js"></script>
+<style>
+@keyframes anim {
+ from { margin-left: 100px; }
+ to { margin-left: 200px; }
+}
+</style>
+<body>
+<script>
+
+'use strict';
+
+const ANIM_PROP_VAL = 'anim 100s';
+const ANIM_DURATION = 100000; // ms
+
+test(function(t) {
+ var div = addDiv(t);
+ div.style.animation = ANIM_PROP_VAL;
+ div.style.animationIterationCount = 'infinite';
+ var animation = div.getAnimations()[0];
+
+ var threw = false;
+ try {
+ animation.finish();
+ } catch (e) {
+ threw = true;
+ assert_equals(e.name, 'InvalidStateError',
+ 'Exception should be an InvalidStateError exception when ' +
+ 'trying to finish an infinite animation');
+ }
+ assert_true(threw,
+ 'Expect InvalidStateError exception trying to finish an ' +
+ 'infinite animation');
+}, 'Test exceptions when finishing infinite animation');
+
+async_test(function(t) {
+ var div = addDiv(t);
+ div.style.animation = ANIM_PROP_VAL + ' paused';
+ var animation = div.getAnimations()[0];
+
+ animation.ready.then(t.step_func(function() {
+ animation.finish();
+ assert_equals(animation.playState, 'finished',
+ 'The play state of a paused animation should become ' +
+ '"finished" after finish() is called');
+ assert_approx_equals(animation.startTime,
+ animation.timeline.currentTime - ANIM_DURATION,
+ 0.0001,
+ 'The start time of a paused animation should be set ' +
+ 'after calling finish()');
+ t.done();
+ }));
+}, 'Test finish() while paused');
+
+test(function(t) {
+ var div = addDiv(t);
+ div.style.animation = ANIM_PROP_VAL + ' paused';
+ var animation = div.getAnimations()[0];
+
+ // Update playbackRate so we can test that the calculated startTime
+ // respects it
+ animation.playbackRate = 2;
+
+ // While animation is still pause-pending call finish()
+ animation.finish();
+
+ assert_equals(animation.playState, 'finished',
+ 'The play state of a pause-pending animation should become ' +
+ '"finished" after finish() is called');
+ assert_approx_equals(animation.startTime,
+ animation.timeline.currentTime - ANIM_DURATION / 2,
+ 0.0001,
+ 'The start time of a pause-pending animation should ' +
+ 'be set after calling finish()');
+}, 'Test finish() while pause-pending with positive playbackRate');
+
+test(function(t) {
+ var div = addDiv(t);
+ div.style.animation = ANIM_PROP_VAL + ' paused';
+ var animation = div.getAnimations()[0];
+
+ animation.playbackRate = -2;
+ animation.finish();
+
+ assert_equals(animation.playState, 'finished',
+ 'The play state of a pause-pending animation should become ' +
+ '"finished" after finish() is called');
+ assert_equals(animation.startTime, animation.timeline.currentTime,
+ 'The start time of a pause-pending animation should be ' +
+ 'set after calling finish()');
+}, 'Test finish() while pause-pending with negative playbackRate');
+
+done();
+</script>
+</body>
diff --git a/dom/animation/test/css-animations/file_animation-finished.html b/dom/animation/test/css-animations/file_animation-finished.html
new file mode 100644
index 0000000000..c296abb117
--- /dev/null
+++ b/dom/animation/test/css-animations/file_animation-finished.html
@@ -0,0 +1,93 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="../testcommon.js"></script>
+<style>
+@keyframes abc {
+ to { transform: translate(10px) }
+}
+@keyframes def {}
+</style>
+<body>
+<script>
+'use strict';
+
+const ANIM_PROP_VAL = 'abc 100s';
+const ANIM_DURATION = 100 * MS_PER_SEC;
+
+promise_test(function(t) {
+ var div = addDiv(t);
+ // Set up pending animation
+ div.style.animation = ANIM_PROP_VAL;
+ var animation = div.getAnimations()[0];
+ var previousFinishedPromise = animation.finished;
+ // Set up listeners on finished promise
+ var retPromise = animation.finished.then(function() {
+ assert_unreached('finished promise is fulfilled');
+ }).catch(function(err) {
+ assert_equals(err.name, 'AbortError',
+ 'finished promise is rejected with AbortError');
+ assert_not_equals(animation.finished, previousFinishedPromise,
+ 'Finished promise should change after the original is ' +
+ 'rejected');
+ });
+
+ // Now cancel the animation and flush styles
+ div.style.animation = '';
+ window.getComputedStyle(div).animation;
+
+ return retPromise;
+}, 'finished promise is rejected when an animation is cancelled by resetting ' +
+ 'the animation property');
+
+promise_test(function(t) {
+ var div = addDiv(t);
+ // As before, but this time instead of removing all animations, simply update
+ // the list of animations. At least for Firefox, updating is a different
+ // code path.
+
+ // Set up pending animation
+ div.style.animation = ANIM_PROP_VAL;
+ var animation = div.getAnimations()[0];
+ var previousFinishedPromise = animation.finished;
+
+ // Set up listeners on finished promise
+ var retPromise = animation.finished.then(function() {
+ assert_unreached('finished promise was fulfilled');
+ }).catch(function(err) {
+ assert_equals(err.name, 'AbortError',
+ 'finished promise is rejected with AbortError');
+ assert_not_equals(animation.finished, previousFinishedPromise,
+ 'Finished promise should change after the original is ' +
+ 'rejected');
+ });
+
+ // Now update the animation and flush styles
+ div.style.animation = 'def 100s';
+ window.getComputedStyle(div).animation;
+
+ return retPromise;
+}, 'finished promise is rejected when an animation is cancelled by changing ' +
+ 'the animation property');
+
+promise_test(function(t) {
+ var div = addDiv(t);
+ div.style.animation = ANIM_PROP_VAL;
+ var animation = div.getAnimations()[0];
+ var previousFinishedPromise = animation.finished;
+ animation.currentTime = ANIM_DURATION;
+ return animation.finished.then(function() {
+ div.style.animationPlayState = 'running';
+ return waitForAnimationFrames(2);
+ }).then(function() {
+ assert_equals(animation.finished, previousFinishedPromise,
+ 'Should not replay when animation-play-state changes to ' +
+ '"running" on finished animation');
+ assert_equals(animation.currentTime, ANIM_DURATION,
+ 'currentTime should not change when animation-play-state ' +
+ 'changes to "running" on finished animation');
+ });
+}, 'Test finished promise changes when animationPlayState set to running');
+
+done();
+</script>
+</body>
diff --git a/dom/animation/test/css-animations/file_animation-id.html b/dom/animation/test/css-animations/file_animation-id.html
new file mode 100644
index 0000000000..dbd5ee0eef
--- /dev/null
+++ b/dom/animation/test/css-animations/file_animation-id.html
@@ -0,0 +1,24 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="../testcommon.js"></script>
+<style>
+@keyframes abc { }
+</style>
+<body>
+<script>
+'use strict';
+
+test(function(t) {
+ var div = addDiv(t);
+ div.style.animation = 'abc 100s';
+ var animation = div.getAnimations()[0];
+ assert_equals(animation.id, '', 'id for CSS Animation is initially empty');
+ animation.id = 'anim'
+
+ assert_equals(animation.id, 'anim', 'animation.id reflects the value set');
+}, 'Animation.id for CSS Animations');
+
+done();
+</script>
+</body>
+</html>
diff --git a/dom/animation/test/css-animations/file_animation-pausing.html b/dom/animation/test/css-animations/file_animation-pausing.html
new file mode 100644
index 0000000000..7176a0c1d6
--- /dev/null
+++ b/dom/animation/test/css-animations/file_animation-pausing.html
@@ -0,0 +1,165 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="../testcommon.js"></script>
+<style>
+@keyframes anim {
+ 0% { margin-left: 0px }
+ 100% { margin-left: 10000px }
+}
+</style>
+<body>
+<script>
+'use strict';
+
+function getMarginLeft(cs) {
+ return parseFloat(cs.marginLeft);
+}
+
+promise_test(function(t) {
+ var div = addDiv(t);
+ var cs = window.getComputedStyle(div);
+ div.style.animation = 'anim 1000s paused';
+ var animation = div.getAnimations()[0];
+ assert_equals(getMarginLeft(cs), 0,
+ 'Initial value of margin-left is zero');
+ animation.play();
+
+ return animation.ready.then(waitForFrame).then(function() {
+ assert_true(getMarginLeft(cs) > 0,
+ 'Playing value of margin-left is greater than zero');
+ });
+}, 'play() overrides animation-play-state');
+
+promise_test(function(t) {
+ var div = addDiv(t);
+ var cs = window.getComputedStyle(div);
+ div.style.animation = 'anim 1000s paused';
+ var animation = div.getAnimations()[0];
+ assert_equals(getMarginLeft(cs), 0,
+ 'Initial value of margin-left is zero');
+
+ animation.pause();
+ div.style.animationPlayState = 'running';
+
+ return animation.ready.then(waitForFrame).then(function() {
+ assert_equals(cs.animationPlayState, 'running',
+ 'animation-play-state is running');
+ assert_equals(getMarginLeft(cs), 0,
+ 'Paused value of margin-left is zero');
+ });
+}, 'pause() overrides animation-play-state');
+
+promise_test(function(t) {
+ var div = addDiv(t);
+ var cs = window.getComputedStyle(div);
+ div.style.animation = 'anim 1000s paused';
+ var animation = div.getAnimations()[0];
+ assert_equals(getMarginLeft(cs), 0,
+ 'Initial value of margin-left is zero');
+ animation.play();
+ var previousAnimVal;
+
+ return animation.ready.then(function() {
+ div.style.animationPlayState = 'running';
+ cs.animationPlayState; // Trigger style resolution
+ return waitForFrame();
+ }).then(function() {
+ assert_equals(cs.animationPlayState, 'running',
+ 'animation-play-state is running');
+ div.style.animationPlayState = 'paused';
+ return animation.ready;
+ }).then(function() {
+ assert_equals(cs.animationPlayState, 'paused',
+ 'animation-play-state is paused');
+ previousAnimVal = getMarginLeft(cs);
+ return waitForFrame();
+ }).then(function() {
+ assert_equals(getMarginLeft(cs), previousAnimVal,
+ 'Animated value of margin-left does not change when'
+ + ' paused by style');
+ });
+}, 'play() is overridden by later setting "animation-play-state: paused"');
+
+promise_test(function(t) {
+ var div = addDiv(t);
+ var cs = window.getComputedStyle(div);
+ div.style.animation = 'anim 1000s';
+ var animation = div.getAnimations()[0];
+ assert_equals(getMarginLeft(cs), 0,
+ 'Initial value of margin-left is zero');
+
+ // Set the specified style first. If implementations fail to
+ // apply the style changes first, they will ignore the redundant
+ // call to play() and fail to correctly override the pause style.
+ div.style.animationPlayState = 'paused';
+ animation.play();
+ var previousAnimVal = getMarginLeft(cs);
+
+ return animation.ready.then(waitForFrame).then(function() {
+ assert_equals(cs.animationPlayState, 'paused',
+ 'animation-play-state is paused');
+ assert_true(getMarginLeft(cs) > previousAnimVal,
+ 'Playing value of margin-left is increasing');
+ });
+}, 'play() flushes pending changes to animation-play-state first');
+
+promise_test(function(t) {
+ var div = addDiv(t);
+ var cs = window.getComputedStyle(div);
+ div.style.animation = 'anim 1000s paused';
+ var animation = div.getAnimations()[0];
+ assert_equals(getMarginLeft(cs), 0,
+ 'Initial value of margin-left is zero');
+
+ // Unlike the previous test for play(), since calling pause() is sticky,
+ // we'll apply it even if the underlying style also says we're paused.
+ //
+ // We would like to test that implementations flush styles before running
+ // pause() but actually there's no style we can currently set that will
+ // change the behavior of pause(). That may change in the future
+ // (e.g. if we introduce animation-timeline or animation-playback-rate etc.).
+ //
+ // For now this just serves as a sanity check that we do the same thing
+ // even if we set style before calling the API.
+ div.style.animationPlayState = 'running';
+ animation.pause();
+ var previousAnimVal = getMarginLeft(cs);
+
+ return animation.ready.then(waitForFrame).then(function() {
+ assert_equals(cs.animationPlayState, 'running',
+ 'animation-play-state is running');
+ assert_equals(getMarginLeft(cs), previousAnimVal,
+ 'Paused value of margin-left does not change');
+ });
+}, 'pause() applies pending changes to animation-play-state first');
+// (Note that we can't actually test for this; see comment above, in test-body.)
+
+promise_test(function(t) {
+ var div = addDiv(t, { style: 'animation: anim 1000s' });
+ var animation = div.getAnimations()[0];
+ var readyPromiseRun = false;
+
+ return animation.ready.then(function() {
+ div.style.animationPlayState = 'paused';
+ assert_equals(animation.playState, 'pending', 'Animation is pause pending');
+
+ // Set current time
+ animation.currentTime = 5 * MS_PER_SEC;
+ assert_equals(animation.playState, 'paused',
+ 'Animation is paused immediately after setting currentTime');
+ assert_equals(animation.startTime, null,
+ 'Animation startTime is unresolved immediately after ' +
+ 'setting currentTime');
+ assert_equals(animation.currentTime, 5 * MS_PER_SEC,
+ 'Animation currentTime does not change when forcing a ' +
+ 'pause operation to complete');
+
+ // The ready promise should now be resolved. If it's not then test will
+ // probably time out before anything else happens that causes it to resolve.
+ return animation.ready;
+ });
+}, 'Setting the current time completes a pending pause');
+
+done();
+</script>
+</body>
diff --git a/dom/animation/test/css-animations/file_animation-playstate.html b/dom/animation/test/css-animations/file_animation-playstate.html
new file mode 100644
index 0000000000..ce9839f382
--- /dev/null
+++ b/dom/animation/test/css-animations/file_animation-playstate.html
@@ -0,0 +1,71 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="../testcommon.js"></script>
+<style>
+@keyframes anim { }
+</style>
+<body>
+<script>
+'use strict';
+
+promise_test(function(t) {
+ var div = addDiv(t);
+ var cs = window.getComputedStyle(div);
+ div.style.animation = 'anim 1000s';
+ var animation = div.getAnimations()[0];
+ assert_equals(animation.playState, 'pending');
+
+ return animation.ready.then(function() {
+ assert_equals(animation.playState, 'running');
+ });
+}, 'Animation returns correct playState when running');
+
+promise_test(function(t) {
+ var div = addDiv(t);
+ var cs = window.getComputedStyle(div);
+ div.style.animation = 'anim 1000s paused';
+ var animation = div.getAnimations()[0];
+ assert_equals(animation.playState, 'pending');
+
+ return animation.ready.then(function() {
+ assert_equals(animation.playState, 'paused');
+ });
+}, 'Animation returns correct playState when paused');
+
+promise_test(function(t) {
+ var div = addDiv(t);
+ var cs = window.getComputedStyle(div);
+ div.style.animation = 'anim 1000s';
+ var animation = div.getAnimations()[0];
+ animation.pause();
+ assert_equals(animation.playState, 'pending');
+
+ return animation.ready.then(function() {
+ assert_equals(animation.playState, 'paused');
+ });
+}, 'Animation.playState updates when paused by script');
+
+test(function(t) {
+ var div = addDiv(t);
+ var cs = window.getComputedStyle(div);
+ div.style.animation = 'anim 1000s paused';
+ var animation = div.getAnimations()[0];
+ div.style.animationPlayState = 'running';
+
+ // This test also checks that calling playState flushes style
+ assert_equals(animation.playState, 'pending',
+ 'Animation.playState reports pending after updating'
+ + ' animation-play-state (got: ' + animation.playState + ')');
+}, 'Animation.playState updates when resumed by setting style');
+
+test(function(t) {
+ var div = addDiv(t);
+ div.style.animation = 'anim 1000s';
+ var animation = div.getAnimations()[0];
+ animation.cancel();
+ assert_equals(animation.playState, 'idle');
+}, 'Animation returns correct playState when cancelled');
+
+done();
+</script>
+</body>
diff --git a/dom/animation/test/css-animations/file_animation-ready.html b/dom/animation/test/css-animations/file_animation-ready.html
new file mode 100644
index 0000000000..9318a1a182
--- /dev/null
+++ b/dom/animation/test/css-animations/file_animation-ready.html
@@ -0,0 +1,149 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="../testcommon.js"></script>
+<style>
+@keyframes abc {
+ to { transform: translate(10px) }
+}
+</style>
+<body>
+<script>
+'use strict';
+
+promise_test(function(t) {
+ var div = addDiv(t);
+ div.style.animation = 'abc 100s paused';
+ var animation = div.getAnimations()[0];
+ var originalReadyPromise = animation.ready;
+
+ return animation.ready.then(function() {
+ div.style.animationPlayState = 'running';
+ assert_not_equals(animation.ready, originalReadyPromise,
+ 'After updating animation-play-state a new ready promise'
+ + ' object is created');
+ });
+}, 'A new ready promise is created when setting animation-play-state: running');
+
+promise_test(function(t) {
+ var div = addDiv(t);
+
+ // Set up pending animation
+ div.style.animation = 'abc 100s';
+ var animation = div.getAnimations()[0];
+ assert_equals(animation.playState, 'pending',
+ 'Animation is initially pending');
+
+ // Set up listeners on ready promise
+ var retPromise = animation.ready.then(function() {
+ assert_unreached('ready promise is fulfilled');
+ }).catch(function(err) {
+ assert_equals(err.name, 'AbortError',
+ 'ready promise is rejected with AbortError');
+ });
+
+ // Now cancel the animation and flush styles
+ div.style.animation = '';
+ window.getComputedStyle(div).animation;
+
+ return retPromise;
+}, 'ready promise is rejected when an animation is cancelled by resetting'
+ + ' the animation property');
+
+promise_test(function(t) {
+ var div = addDiv(t);
+
+ // As before, but this time instead of removing all animations, simply update
+ // the list of animations. At least for Firefox, updating is a different
+ // code path.
+
+ // Set up pending animation
+ div.style.animation = 'abc 100s';
+ var animation = div.getAnimations()[0];
+ assert_equals(animation.playState, 'pending',
+ 'Animation is initially pending');
+
+ // Set up listeners on ready promise
+ var retPromise = animation.ready.then(function() {
+ assert_unreached('ready promise was fulfilled');
+ }).catch(function(err) {
+ assert_equals(err.name, 'AbortError',
+ 'ready promise is rejected with AbortError');
+ });
+
+ // Now update the animation and flush styles
+ div.style.animation = 'def 100s';
+ window.getComputedStyle(div).animation;
+
+ return retPromise;
+}, 'ready promise is rejected when an animation is cancelled by updating'
+ + ' the animation property');
+
+promise_test(function(t) {
+ var div = addDiv(t, { style: 'animation: abc 100s' });
+ var animation = div.getAnimations()[0];
+ var originalReadyPromise = animation.ready;
+
+ return animation.ready.then(function() {
+ div.style.animationPlayState = 'paused';
+ assert_not_equals(animation.ready, originalReadyPromise,
+ 'A new Promise object is generated when setting'
+ + ' animation-play-state: paused');
+ });
+}, 'A new ready promise is created when setting animation-play-state: paused');
+
+promise_test(function(t) {
+ var div = addDiv(t, { style: 'animation: abc 100s' });
+ var animation = div.getAnimations()[0];
+
+ return animation.ready.then(function() {
+ div.style.animationPlayState = 'paused';
+ var firstReadyPromise = animation.ready;
+ animation.pause();
+ assert_equals(animation.ready, firstReadyPromise,
+ 'Ready promise objects are identical after redundant pause');
+ });
+}, 'Pausing twice re-uses the same Promise');
+
+promise_test(function(t) {
+ var div = addDiv(t, { style: 'animation: abc 100s' });
+ var animation = div.getAnimations()[0];
+
+ return animation.ready.then(function() {
+ div.style.animationPlayState = 'paused';
+
+ // Flush style and verify we're pending at the same time
+ assert_equals(animation.playState, 'pending', 'Animation is pending');
+ var pauseReadyPromise = animation.ready;
+
+ // Now play again immediately
+ div.style.animationPlayState = 'running';
+ assert_equals(animation.playState, 'pending', 'Animation is still pending');
+ assert_equals(animation.ready, pauseReadyPromise,
+ 'The pause Promise is re-used when playing while waiting'
+ + ' to pause');
+
+ return animation.ready;
+ }).then(function() {
+ assert_equals(animation.playState, 'running',
+ 'Animation is running after aborting a pause');
+ });
+}, 'If a pause operation is interrupted, the ready promise is reused');
+
+promise_test(function(t) {
+ var div = addDiv(t, { style: 'animation: abc 100s' });
+ var animation = div.getAnimations()[0];
+
+ return animation.ready.then(function() {
+ div.style.animationPlayState = 'paused';
+ return animation.ready;
+ }).then(function(resolvedAnimation) {
+ assert_equals(resolvedAnimation, animation,
+ 'Promise received when ready Promise for a pause operation'
+ + ' is completed is the animation on which the pause was'
+ + ' performed');
+ });
+}, 'When a pause is complete the Promise callback gets the correct animation');
+
+done();
+</script>
+</body>
diff --git a/dom/animation/test/css-animations/file_animation-reverse.html b/dom/animation/test/css-animations/file_animation-reverse.html
new file mode 100644
index 0000000000..5060fa55f1
--- /dev/null
+++ b/dom/animation/test/css-animations/file_animation-reverse.html
@@ -0,0 +1,29 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="../testcommon.js"></script>
+<style>
+@keyframes anim {
+ to { transform: translate(100px) }
+}
+</style>
+<body>
+<script>
+'use strict';
+
+test(function(t) {
+ var div = addDiv(t, { style: 'animation: anim 100s' });
+ var animation = div.getAnimations()[0];
+ div.style.animation = "";
+ flushComputedStyle(div);
+
+ assert_equals(animation.currentTime, null);
+ animation.reverse();
+
+ assert_equals(animation.currentTime, 100 * MS_PER_SEC,
+ 'animation.currentTime should be its effect end');
+}, 'reverse() from idle state starts playing the animation');
+
+
+done();
+</script>
+</body>
diff --git a/dom/animation/test/css-animations/file_animation-starttime.html b/dom/animation/test/css-animations/file_animation-starttime.html
new file mode 100644
index 0000000000..46144464c0
--- /dev/null
+++ b/dom/animation/test/css-animations/file_animation-starttime.html
@@ -0,0 +1,383 @@
+<!doctype html>
+<html>
+ <head>
+ <meta charset=utf-8>
+ <title>Tests for the effect of setting a CSS animation's
+ Animation.startTime</title>
+ <style>
+
+.animated-div {
+ margin-left: 10px;
+ /* Make it easier to calculate expected values: */
+ animation-timing-function: linear ! important;
+}
+
+@keyframes anim {
+ from { margin-left: 100px; }
+ to { margin-left: 200px; }
+}
+
+ </style>
+ <script src="../testcommon.js"></script>
+ </head>
+ <body>
+ <script type="text/javascript">
+
+'use strict';
+
+// TODO: We should separate this test(Testing for CSS Animation events /
+// Testing for start time of Web Animation).
+// e.g:
+// CSS Animation events test:
+// - check the firing an event after setting an Animation.startTime
+// The start time of Web Animation test:
+// - check an start time value on several situation(init / processing..)
+// - Based on W3C Spec, check the behavior of setting current time.
+
+// TODO: Once the computedTiming property is implemented, add checks to the
+// checker helpers to ensure that computedTiming's properties are updated as
+// expected.
+// See https://bugzilla.mozilla.org/show_bug.cgi?id=1108055
+
+const CSS_ANIM_EVENTS =
+ ['animationstart', 'animationiteration', 'animationend'];
+
+test(function(t)
+{
+ var div = addDiv(t, { 'style': 'animation: anim 100s' });
+ var animation = div.getAnimations()[0];
+
+ assert_equals(animation.startTime, null, 'startTime is unresolved');
+}, 'startTime of a newly created (play-pending) animation is unresolved');
+
+test(function(t)
+{
+ var div = addDiv(t, { 'style': 'animation: anim 100s paused' });
+ var animation = div.getAnimations()[0];
+ assert_equals(animation.startTime, null, 'startTime is unresolved');
+}, 'startTime of a newly created (pause-pending) animation is unresolved');
+
+promise_test(function(t)
+{
+ var div = addDiv(t, { 'style': 'animation: anim 100s' });
+ var animation = div.getAnimations()[0];
+
+ return animation.ready.then(function() {
+ assert_true(animation.startTime > 0,
+ 'startTime is resolved when running');
+ });
+}, 'startTime is resolved when running');
+
+promise_test(function(t)
+{
+ var div = addDiv(t, { 'style': 'animation: anim 100s paused' });
+ var animation = div.getAnimations()[0];
+
+ return animation.ready.then(function() {
+ assert_equals(animation.startTime, null,
+ 'startTime is unresolved when paused');
+ });
+}, 'startTime is unresolved when paused');
+
+promise_test(function(t)
+{
+ var div = addDiv(t, { 'style': 'animation: anim 100s' });
+ var animation = div.getAnimations()[0];
+
+ return animation.ready.then(function() {
+ div.style.animationPlayState = 'paused';
+ getComputedStyle(div).animationPlayState;
+
+ assert_not_equals(animation.startTime, null,
+ 'startTime is resolved when pause-pending');
+
+ div.style.animationPlayState = 'running';
+ getComputedStyle(div).animationPlayState;
+
+ assert_not_equals(animation.startTime, null,
+ 'startTime is preserved when a pause is aborted');
+ });
+}, 'startTime while pause-pending and play-pending');
+
+promise_test(function(t) {
+ var div = addDiv(t, { 'style': 'animation: anim 100s' });
+ var animation = div.getAnimations()[0];
+ // Seek to end to put us in the finished state
+ animation.currentTime = 100 * MS_PER_SEC;
+
+ return animation.ready.then(function() {
+ // Call play() which puts us back in the running state
+ animation.play();
+
+ assert_equals(animation.startTime, null, 'startTime is unresolved');
+ });
+}, 'startTime while play-pending from finished state');
+
+test(function(t) {
+ var div = addDiv(t, { 'style': 'animation: anim 100s' });
+ var animation = div.getAnimations()[0];
+ animation.finish();
+ // Call play() which puts us back in the running state
+ animation.play();
+
+ assert_equals(animation.startTime, null, 'startTime is unresolved');
+}, 'startTime while play-pending from finished state using finish()');
+
+promise_test(function(t) {
+ var div = addDiv(t, { style: 'animation: anim 100s' });
+ var animation = div.getAnimations()[0];
+
+ assert_equals(animation.startTime, null, 'The initial startTime is null');
+ var initialTimelineTime = document.timeline.currentTime;
+
+ return animation.ready.then(function() {
+ assert_true(animation.startTime > initialTimelineTime,
+ 'After the animation has started, startTime is greater than ' +
+ 'the time when it was started');
+ var startTimeBeforePausing = animation.startTime;
+
+ div.style.animationPlayState = 'paused';
+ // Flush styles just in case querying animation.startTime doesn't flush
+ // styles (which would be a bug in of itself and could mask a further bug
+ // by causing startTime to appear to not change).
+ getComputedStyle(div).animationPlayState;
+
+ assert_equals(animation.startTime, startTimeBeforePausing,
+ 'The startTime does not change when pausing-pending');
+ return animation.ready;
+ }).then(function() {
+ assert_equals(animation.startTime, null,
+ 'After actually pausing, the startTime of an animation ' +
+ 'is null');
+ });
+}, 'Pausing should make the startTime become null');
+
+test(function(t)
+{
+ var div = addDiv(t, {'class': 'animated-div'});
+ div.style.animation = 'anim 100s 100s';
+ var animation = div.getAnimations()[0];
+ var currentTime = animation.timeline.currentTime;
+ animation.startTime = currentTime;
+
+ assert_times_equal(animation.startTime, currentTime,
+ 'Check setting of startTime actually works');
+}, 'Sanity test to check round-tripping assigning to a new animation\'s ' +
+ 'startTime');
+
+promise_test(function(t) {
+ var div = addDiv(t, {'class': 'animated-div'});
+ var eventWatcher = new EventWatcher(t, div, CSS_ANIM_EVENTS);
+ div.style.animation = 'anim 100s 100s';
+ var animation = div.getAnimations()[0];
+
+ return animation.ready.then(function() {
+ assert_less_than_equal(animation.startTime, animation.timeline.currentTime,
+ 'Animation.startTime should be less than the timeline\'s ' +
+ 'currentTime on the first paint tick after animation creation');
+
+ animation.startTime = animation.timeline.currentTime - 100 * MS_PER_SEC;
+ return eventWatcher.wait_for('animationstart');
+ }).then(function() {
+ animation.startTime = animation.timeline.currentTime - 200 * MS_PER_SEC;
+ return eventWatcher.wait_for('animationend');
+ });
+}, 'Skipping forward through animation');
+
+promise_test(function(t) {
+ var div = addDiv(t, {'class': 'animated-div'});
+ var eventWatcher = new EventWatcher(t, div, CSS_ANIM_EVENTS);
+ div.style.animation = 'anim 100s 100s';
+ var animation = div.getAnimations()[0];
+ animation.startTime = animation.timeline.currentTime - 200 * MS_PER_SEC;
+ var previousTimelineTime = animation.timeline.currentTime;
+
+ return eventWatcher.wait_for(['animationstart',
+ 'animationend']).then(function() {
+ assert_true(document.timeline.currentTime - previousTimelineTime <
+ 100 * MS_PER_SEC,
+ 'Sanity check that seeking worked rather than the events ' +
+ 'firing after normal playback through the very long ' +
+ 'animation duration');
+
+ animation.startTime = animation.timeline.currentTime - 150 * MS_PER_SEC;
+
+ // Despite going backwards from after the end of the animation (to being
+ // in the active interval), we now expect an 'animationstart' event
+ // because the animation should go from being inactive to active.
+ return eventWatcher.wait_for('animationstart');
+ }).then(function() {
+ animation.startTime = animation.timeline.currentTime;
+
+ // Despite going backwards from just after the active interval starts to
+ // the animation start time, we now expect an animationend event
+ // because we went from inside to outside the active interval.
+ return eventWatcher.wait_for('animationend');
+ }).then(function() {
+ assert_less_than_equal(animation.startTime, animation.timeline.currentTime,
+ 'Animation.startTime should be less than the timeline\'s ' +
+ 'currentTime on the first paint tick after animation creation');
+ });
+}, 'Skipping backwards through animation');
+
+// Next we have multiple tests to check that redundant startTime changes do NOT
+// dispatch events. It's impossible to distinguish between events not being
+// dispatched and events just taking an incredibly long time to dispatch
+// without waiting an infinitely long time. Obviously we don't want to do that
+// (block this test from finishing forever), so instead we just listen for
+// events until two animation frames (i.e. requestAnimationFrame callbacks)
+// have happened, then assume that no events will ever be dispatched for the
+// redundant changes if no events were detected in that time.
+
+promise_test(function(t) {
+ var div = addDiv(t, {'class': 'animated-div'});
+ var eventWatcher = new EventWatcher(t, div, CSS_ANIM_EVENTS);
+ div.style.animation = "anim 100s 100s";
+ var animation = div.getAnimations()[0];
+
+ animation.startTime = animation.timeline.currentTime - 150 * MS_PER_SEC;
+ animation.startTime = animation.timeline.currentTime - 50 * MS_PER_SEC;
+
+ return waitForAnimationFrames(2);
+}, 'Redundant change, before -> active, then back');
+
+promise_test(function(t) {
+ var div = addDiv(t, {'class': 'animated-div'});
+ var eventWatcher = new EventWatcher(t, div, CSS_ANIM_EVENTS);
+ div.style.animation = "anim 100s 100s";
+ var animation = div.getAnimations()[0];
+
+ animation.startTime = animation.timeline.currentTime - 250 * MS_PER_SEC;
+ animation.startTime = animation.timeline.currentTime - 50 * MS_PER_SEC;
+
+ return waitForAnimationFrames(2);
+}, 'Redundant change, before -> after, then back');
+
+promise_test(function(t) {
+ var div = addDiv(t, {'class': 'animated-div'});
+ var eventWatcher = new EventWatcher(t, div, CSS_ANIM_EVENTS);
+ div.style.animation = "anim 100s 100s";
+ var animation = div.getAnimations()[0];
+
+ var retPromise = eventWatcher.wait_for('animationstart').then(function() {
+ animation.startTime = animation.timeline.currentTime - 50 * MS_PER_SEC;
+ animation.startTime = animation.timeline.currentTime - 150 * MS_PER_SEC;
+
+ return waitForAnimationFrames(2);
+ });
+ // get us into the initial state:
+ animation.startTime = animation.timeline.currentTime - 150 * MS_PER_SEC;
+
+ return retPromise;
+}, 'Redundant change, active -> before, then back');
+
+promise_test(function(t) {
+ var div = addDiv(t, {'class': 'animated-div'});
+ var eventWatcher = new EventWatcher(t, div, CSS_ANIM_EVENTS);
+ div.style.animation = "anim 100s 100s";
+ var animation = div.getAnimations()[0];
+
+ var retPromise = eventWatcher.wait_for('animationstart').then(function() {
+ animation.startTime = animation.timeline.currentTime - 250 * MS_PER_SEC;
+ animation.startTime = animation.timeline.currentTime - 150 * MS_PER_SEC;
+
+ return waitForAnimationFrames(2);
+ });
+ // get us into the initial state:
+ animation.startTime = animation.timeline.currentTime - 150 * MS_PER_SEC;
+
+ return retPromise;
+}, 'Redundant change, active -> after, then back');
+
+promise_test(function(t) {
+ var div = addDiv(t, {'class': 'animated-div'});
+ var eventWatcher = new EventWatcher(t, div, CSS_ANIM_EVENTS);
+ div.style.animation = "anim 100s 100s";
+ var animation = div.getAnimations()[0];
+
+ var retPromise = eventWatcher.wait_for(['animationstart',
+ 'animationend']).then(function() {
+ animation.startTime = animation.timeline.currentTime - 50 * MS_PER_SEC;
+ animation.startTime = animation.timeline.currentTime - 250 * MS_PER_SEC;
+
+ return waitForAnimationFrames(2);
+ });
+ // get us into the initial state:
+ animation.startTime = animation.timeline.currentTime - 250 * MS_PER_SEC;
+
+ return retPromise;
+}, 'Redundant change, after -> before, then back');
+
+promise_test(function(t) {
+ var div = addDiv(t, {'class': 'animated-div'});
+ var eventWatcher = new EventWatcher(t, div, CSS_ANIM_EVENTS);
+ div.style.animation = "anim 100s 100s";
+ var animation = div.getAnimations()[0];
+
+ var retPromise = eventWatcher.wait_for(['animationstart',
+ 'animationend']).then(function() {
+ animation.startTime = animation.timeline.currentTime - 150 * MS_PER_SEC;
+ animation.startTime = animation.timeline.currentTime - 250 * MS_PER_SEC;
+
+ return waitForAnimationFrames(2);
+
+ });
+ // get us into the initial state:
+ animation.startTime = animation.timeline.currentTime - 250 * MS_PER_SEC;
+
+ return retPromise;
+}, 'Redundant change, after -> active, then back');
+
+promise_test(function(t) {
+ var div = addDiv(t, {'class': 'animated-div'});
+ div.style.animation = 'anim 100s 100s';
+ var animation = div.getAnimations()[0];
+ var storedCurrentTime;
+
+ return animation.ready.then(function() {
+ storedCurrentTime = animation.currentTime;
+ animation.startTime = null;
+ return animation.ready;
+ }).then(function() {
+ assert_equals(animation.currentTime, storedCurrentTime,
+ 'Test that hold time is correct');
+ });
+}, 'Setting startTime to null');
+
+promise_test(function(t) {
+ var div = addDiv(t, {'class': 'animated-div'});
+ div.style.animation = 'anim 100s';
+ var animation = div.getAnimations()[0];
+
+ return animation.ready.then(function() {
+ var savedStartTime = animation.startTime;
+
+ assert_not_equals(animation.startTime, null,
+ 'Animation.startTime not null on ready Promise resolve');
+
+ animation.pause();
+ return animation.ready;
+ }).then(function() {
+ assert_equals(animation.startTime, null,
+ 'Animation.startTime is null after paused');
+ assert_equals(animation.playState, 'paused',
+ 'Animation.playState is "paused" after pause() call');
+ });
+}, 'Animation.startTime after pausing');
+
+promise_test(function(t) {
+ var div = addDiv(t, {'class': 'animated-div'});
+ div.style.animation = 'anim 100s';
+ var animation = div.getAnimations()[0];
+
+ return animation.ready.then(function() {
+ animation.cancel();
+ assert_equals(animation.startTime, null,
+ 'The startTime of a cancelled animation should be null');
+ });
+}, 'Animation.startTime after cancelling');
+
+done();
+ </script>
+ </body>
+</html>
diff --git a/dom/animation/test/css-animations/file_animations-dynamic-changes.html b/dom/animation/test/css-animations/file_animations-dynamic-changes.html
new file mode 100644
index 0000000000..8f16536ae9
--- /dev/null
+++ b/dom/animation/test/css-animations/file_animations-dynamic-changes.html
@@ -0,0 +1,154 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="../testcommon.js"></script>
+<style>
+@keyframes anim1 {
+ to { left: 100px }
+}
+@keyframes anim2 { }
+</style>
+<body>
+<script>
+'use strict';
+
+promise_test(function(t) {
+ var div = addDiv(t);
+ div.style.animation = 'anim1 100s';
+ var originalAnimation = div.getAnimations()[0];
+ var originalStartTime;
+ var originalCurrentTime;
+
+ // Wait a moment so we can confirm the startTime doesn't change (and doesn't
+ // simply reflect the current time).
+ return originalAnimation.ready.then(function() {
+ originalStartTime = originalAnimation.startTime;
+ originalCurrentTime = originalAnimation.currentTime;
+
+ // Wait a moment so we can confirm the startTime doesn't change (and
+ // doesn't simply reflect the current time).
+ return waitForFrame();
+ }).then(function() {
+ div.style.animationDuration = '200s';
+ var animation = div.getAnimations()[0];
+ assert_equals(animation, originalAnimation,
+ 'The same Animation is returned after updating'
+ + ' animation duration');
+ assert_equals(animation.startTime, originalStartTime,
+ 'Animations returned by getAnimations preserve'
+ + ' their startTime even when they are updated');
+ // Sanity check
+ assert_not_equals(animation.currentTime, originalCurrentTime,
+ 'Animation.currentTime has updated in next'
+ + ' requestAnimationFrame callback');
+ });
+}, 'Animations preserve their startTime when changed');
+
+test(function(t) {
+ var div = addDiv(t);
+ div.style.animation = 'anim1 100s, anim1 100s';
+
+ // Store original state
+ var animations = div.getAnimations();
+ var animation1 = animations[0];
+ var animation2 = animations[1];
+
+ // Update first in list
+ div.style.animationDuration = '200s, 100s';
+ animations = div.getAnimations();
+ assert_equals(animations[0], animation1,
+ 'First Animation is in same position after update');
+ assert_equals(animations[1], animation2,
+ 'Second Animation is in same position after update');
+}, 'Updated Animations maintain their order in the list');
+
+promise_test(function(t) {
+ var div = addDiv(t);
+ div.style.animation = 'anim1 200s, anim1 100s';
+
+ // Store original state
+ var animations = div.getAnimations();
+ var animation1 = animations[0];
+ var animation2 = animations[1];
+
+ // Wait before continuing so we can compare start times (otherwise the
+ // new Animation objects and existing Animation objects will all have the same
+ // start time).
+ return waitForAllAnimations(animations).then(waitForFrame).then(function() {
+ // Swap duration of first and second in list and prepend animation at the
+ // same time
+ div.style.animation = 'anim1 100s, anim1 100s, anim1 200s';
+ animations = div.getAnimations();
+ assert_true(animations[0] !== animation1 && animations[0] !== animation2,
+ 'New Animation is prepended to start of list');
+ assert_equals(animations[1], animation1,
+ 'First Animation is in second position after update');
+ assert_equals(animations[2], animation2,
+ 'Second Animation is in third position after update');
+ assert_equals(animations[1].startTime, animations[2].startTime,
+ 'Old Animations have the same start time');
+ // TODO: Check that animations[0].startTime === null
+ return animations[0].ready;
+ }).then(function() {
+ assert_true(animations[0].startTime > animations[1].startTime,
+ 'New Animation has later start time');
+ });
+}, 'Only the startTimes of existing animations are preserved');
+
+promise_test(function(t) {
+ var div = addDiv(t);
+ div.style.animation = 'anim1 100s, anim1 100s';
+ var secondAnimation = div.getAnimations()[1];
+
+ // Wait before continuing so we can compare start times
+ return secondAnimation.ready.then(waitForFrame).then(function() {
+ // Trim list of animations
+ div.style.animationName = 'anim1';
+ var animations = div.getAnimations();
+ assert_equals(animations.length, 1, 'List of Animations was trimmed');
+ assert_equals(animations[0], secondAnimation,
+ 'Remaining Animation is the second one in the list');
+ assert_equals(typeof(animations[0].startTime), 'number',
+ 'Remaining Animation has resolved startTime');
+ assert_true(animations[0].startTime < animations[0].timeline.currentTime,
+ 'Remaining Animation preserves startTime');
+ });
+}, 'Animations are removed from the start of the list while preserving'
+ + ' the state of existing Animations');
+
+promise_test(function(t) {
+ var div = addDiv(t);
+ div.style.animation = 'anim1 100s';
+ var firstAddedAnimation = div.getAnimations()[0],
+ secondAddedAnimation,
+ animations;
+
+ // Wait and add second Animation
+ return firstAddedAnimation.ready.then(waitForFrame).then(function() {
+ div.style.animation = 'anim1 100s, anim1 100s';
+ secondAddedAnimation = div.getAnimations()[0];
+
+ // Wait again and add another Animation
+ return secondAddedAnimation.ready.then(waitForFrame);
+ }).then(function() {
+ div.style.animation = 'anim1 100s, anim2 100s, anim1 100s';
+ animations = div.getAnimations();
+ assert_not_equals(firstAddedAnimation, secondAddedAnimation,
+ 'New Animations are added to start of the list');
+ assert_equals(animations[0], secondAddedAnimation,
+ 'Second Animation remains in same position after'
+ + ' interleaving');
+ assert_equals(animations[2], firstAddedAnimation,
+ 'First Animation remains in same position after'
+ + ' interleaving');
+ return animations[1].ready;
+ }).then(function() {
+ assert_true(animations[1].startTime > animations[0].startTime,
+ 'Interleaved animation starts later than existing animations');
+ assert_true(animations[0].startTime > animations[2].startTime,
+ 'Original animations retain their start time');
+ });
+}, 'Animation state is preserved when interleaving animations in list');
+
+done();
+</script>
+</body>
diff --git a/dom/animation/test/css-animations/file_cssanimation-animationname.html b/dom/animation/test/css-animations/file_cssanimation-animationname.html
new file mode 100644
index 0000000000..fd69d85777
--- /dev/null
+++ b/dom/animation/test/css-animations/file_cssanimation-animationname.html
@@ -0,0 +1,37 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="../testcommon.js"></script>
+<style>
+@keyframes xyz {
+ to { left: 100px }
+}
+</style>
+<body>
+<script>
+'use strict';
+
+test(function(t) {
+ var div = addDiv(t);
+ div.style.animation = 'xyz 100s';
+ assert_equals(div.getAnimations()[0].animationName, 'xyz',
+ 'Animation name matches keyframes rule name');
+}, 'Animation name makes keyframe rule');
+
+test(function(t) {
+ var div = addDiv(t);
+ div.style.animation = 'x\\yz 100s';
+ assert_equals(div.getAnimations()[0].animationName, 'xyz',
+ 'Escaped animation name matches keyframes rule name');
+}, 'Escaped animation name');
+
+test(function(t) {
+ var div = addDiv(t);
+ div.style.animation = 'x\\79 z 100s';
+ assert_equals(div.getAnimations()[0].animationName, 'xyz',
+ 'Hex-escaped animation name matches keyframes rule'
+ + ' name');
+}, 'Animation name with hex-escape');
+
+done();
+</script>
+</body>
diff --git a/dom/animation/test/css-animations/file_document-get-animations.html b/dom/animation/test/css-animations/file_document-get-animations.html
new file mode 100644
index 0000000000..abe02d7fc3
--- /dev/null
+++ b/dom/animation/test/css-animations/file_document-get-animations.html
@@ -0,0 +1,276 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="../testcommon.js"></script>
+<style>
+@keyframes animLeft {
+ to { left: 100px }
+}
+@keyframes animTop {
+ to { top: 100px }
+}
+@keyframes animBottom {
+ to { bottom: 100px }
+}
+@keyframes animRight {
+ to { right: 100px }
+}
+</style>
+<body>
+<script>
+'use strict';
+
+test(function(t) {
+ assert_equals(document.getAnimations().length, 0,
+ 'getAnimations returns an empty sequence for a document'
+ + ' with no animations');
+}, 'getAnimations for non-animated content');
+
+test(function(t) {
+ var div = addDiv(t);
+
+ // Add an animation
+ div.style.animation = 'animLeft 100s';
+ assert_equals(document.getAnimations().length, 1,
+ 'getAnimations returns a running CSS Animation');
+
+ // Add another animation
+ div.style.animation = 'animLeft 100s, animTop 100s';
+ assert_equals(document.getAnimations().length, 2,
+ 'getAnimations returns two running CSS Animations');
+
+ // Remove both
+ div.style.animation = '';
+ assert_equals(document.getAnimations().length, 0,
+ 'getAnimations returns no running CSS Animations');
+}, 'getAnimations for CSS Animations');
+
+test(function(t) {
+ var div = addDiv(t);
+ div.style.animation = 'animLeft 100s, animTop 100s, animRight 100s, ' +
+ 'animBottom 100s';
+
+ var animations = document.getAnimations();
+ assert_equals(animations.length, 4,
+ 'getAnimations returns all running CSS Animations');
+ assert_equals(animations[0].animationName, 'animLeft',
+ 'Order of first animation returned');
+ assert_equals(animations[1].animationName, 'animTop',
+ 'Order of second animation returned');
+ assert_equals(animations[2].animationName, 'animRight',
+ 'Order of third animation returned');
+ assert_equals(animations[3].animationName, 'animBottom',
+ 'Order of fourth animation returned');
+}, 'Order of CSS Animations - within an element');
+
+test(function(t) {
+ var div1 = addDiv(t, { style: 'animation: animLeft 100s' });
+ var div2 = addDiv(t, { style: 'animation: animLeft 100s' });
+ var div3 = addDiv(t, { style: 'animation: animLeft 100s' });
+ var div4 = addDiv(t, { style: 'animation: animLeft 100s' });
+
+ var animations = document.getAnimations();
+ assert_equals(animations.length, 4,
+ 'getAnimations returns all running CSS Animations');
+ assert_equals(animations[0].effect.target, div1,
+ 'Order of first animation returned');
+ assert_equals(animations[1].effect.target, div2,
+ 'Order of second animation returned');
+ assert_equals(animations[2].effect.target, div3,
+ 'Order of third animation returned');
+ assert_equals(animations[3].effect.target, div4,
+ 'Order of fourth animation returned');
+
+ // Order should be depth-first pre-order so add some depth as follows:
+ //
+ // <parent>
+ // / |
+ // 2 3
+ // / \
+ // 1 4
+ //
+ // Which should give: 2, 1, 4, 3
+ div2.appendChild(div1);
+ div2.appendChild(div4);
+ animations = document.getAnimations();
+ assert_equals(animations[0].effect.target, div2,
+ 'Order of first animation returned after tree surgery');
+ assert_equals(animations[1].effect.target, div1,
+ 'Order of second animation returned after tree surgery');
+ assert_equals(animations[2].effect.target, div4,
+ 'Order of third animation returned after tree surgery');
+ assert_equals(animations[3].effect.target, div3,
+ 'Order of fourth animation returned after tree surgery');
+
+}, 'Order of CSS Animations - across elements');
+
+test(function(t) {
+ var div1 = addDiv(t, { style: 'animation: animLeft 100s, animTop 100s' });
+ var div2 = addDiv(t, { style: 'animation: animBottom 100s' });
+
+ var expectedResults = [ [ div1, 'animLeft' ],
+ [ div1, 'animTop' ],
+ [ div2, 'animBottom' ] ];
+ var animations = document.getAnimations();
+ assert_equals(animations.length, expectedResults.length,
+ 'getAnimations returns all running CSS Animations');
+ animations.forEach(function(anim, i) {
+ assert_equals(anim.effect.target, expectedResults[i][0],
+ 'Target of animation in position ' + i);
+ assert_equals(anim.animationName, expectedResults[i][1],
+ 'Name of animation in position ' + i);
+ });
+
+ // Modify tree structure and animation list
+ div2.appendChild(div1);
+ div1.style.animation = 'animLeft 100s, animRight 100s, animTop 100s';
+
+ expectedResults = [ [ div2, 'animBottom' ],
+ [ div1, 'animLeft' ],
+ [ div1, 'animRight' ],
+ [ div1, 'animTop' ] ];
+ animations = document.getAnimations();
+ assert_equals(animations.length, expectedResults.length,
+ 'getAnimations returns all running CSS Animations after ' +
+ 'making changes');
+ animations.forEach(function(anim, i) {
+ assert_equals(anim.effect.target, expectedResults[i][0],
+ 'Target of animation in position ' + i + ' after changes');
+ assert_equals(anim.animationName, expectedResults[i][1],
+ 'Name of animation in position ' + i + ' after changes');
+ });
+}, 'Order of CSS Animations - across and within elements');
+
+test(function(t) {
+ var div = addDiv(t, { style: 'animation: animLeft 100s, animTop 100s' });
+ var animLeft = document.getAnimations()[0];
+ assert_equals(animLeft.animationName, 'animLeft',
+ 'Originally, animLeft animation comes first');
+
+ // Disassociate animLeft from markup and restart
+ div.style.animation = 'animTop 100s';
+ animLeft.play();
+
+ var animations = document.getAnimations();
+ assert_equals(animations.length, 2,
+ 'getAnimations returns markup-bound and free animations');
+ assert_equals(animations[0].animationName, 'animTop',
+ 'Markup-bound animations come first');
+ assert_equals(animations[1], animLeft, 'Free animations come last');
+}, 'Order of CSS Animations - markup-bound vs free animations');
+
+test(function(t) {
+ var div = addDiv(t, { style: 'animation: animLeft 100s, animTop 100s' });
+ var animLeft = document.getAnimations()[0];
+ var animTop = document.getAnimations()[1];
+
+ // Disassociate both animations from markup and restart in opposite order
+ div.style.animation = '';
+ animTop.play();
+ animLeft.play();
+
+ var animations = document.getAnimations();
+ assert_equals(animations.length, 2,
+ 'getAnimations returns free animations');
+ assert_equals(animations[0], animTop,
+ 'Free animations are returned in the order they are started');
+ assert_equals(animations[1], animLeft,
+ 'Animations started later are returned later');
+
+ // Restarting an animation should have no effect
+ animTop.cancel();
+ animTop.play();
+ assert_equals(document.getAnimations()[0], animTop,
+ 'After restarting, the ordering of free animations' +
+ ' does not change');
+}, 'Order of CSS Animations - free animations');
+
+test(function(t) {
+ // Add an animation first
+ var div = addDiv(t, { style: 'animation: animLeft 100s' });
+ div.style.top = '0px';
+ div.style.transition = 'all 100s';
+ flushComputedStyle(div);
+
+ // *Then* add a transition
+ div.style.top = '100px';
+ flushComputedStyle(div);
+
+ // Although the transition was added later, it should come first in the list
+ var animations = document.getAnimations();
+ assert_equals(animations.length, 2,
+ 'Both CSS animations and transitions are returned');
+ assert_class_string(animations[0], 'CSSTransition', 'Transition comes first');
+ assert_class_string(animations[1], 'CSSAnimation', 'Animation comes second');
+}, 'Order of CSS Animations and CSS Transitions');
+
+test(function(t) {
+ var div = addDiv(t, { style: 'animation: animLeft 100s forwards' });
+ div.getAnimations()[0].finish();
+ assert_equals(document.getAnimations().length, 1,
+ 'Forwards-filling CSS animations are returned');
+}, 'Finished but filling CSS Animations are returned');
+
+test(function(t) {
+ var div = addDiv(t, { style: 'animation: animLeft 100s' });
+ div.getAnimations()[0].finish();
+ assert_equals(document.getAnimations().length, 0,
+ 'Non-filling finished CSS animations are not returned');
+}, 'Finished but not filling CSS Animations are not returned');
+
+test(function(t) {
+ var div = addDiv(t, { style: 'animation: animLeft 100s 100s' });
+ assert_equals(document.getAnimations().length, 1,
+ 'Yet-to-start CSS animations are returned');
+}, 'Yet-to-start CSS Animations are returned');
+
+test(function(t) {
+ var div = addDiv(t, { style: 'animation: animLeft 100s' });
+ div.getAnimations()[0].cancel();
+ assert_equals(document.getAnimations().length, 0,
+ 'CSS animations cancelled by the API are not returned');
+}, 'CSS Animations cancelled via the API are not returned');
+
+test(function(t) {
+ var div = addDiv(t, { style: 'animation: animLeft 100s' });
+ var anim = div.getAnimations()[0];
+ anim.cancel();
+ anim.play();
+ assert_equals(document.getAnimations().length, 1,
+ 'CSS animations cancelled and restarted by the API are ' +
+ 'returned');
+}, 'CSS Animations cancelled and restarted via the API are returned');
+
+test(function(t) {
+ addStyle(t, { '#parent::after': 'animation: animLeft 10s;',
+ '#parent::before': 'animation: animRight 10s;' });
+ // create two divs with these arrangement:
+ // parent
+ // ::before,
+ // ::after
+ // |
+ // child
+ var parent = addDiv(t, { 'id': 'parent' });
+ var child = addDiv(t);
+ parent.appendChild(child);
+ [parent, child].forEach((div) => {
+ div.setAttribute('style', 'animation: animBottom 10s');
+ });
+
+ var anims = document.getAnimations();
+ assert_equals(anims.length, 4,
+ 'CSS animations on both pseudo-elements and elements ' +
+ 'are returned');
+ assert_equals(anims[0].effect.target, parent,
+ 'The animation targeting the parent element comes first');
+ assert_equals(anims[1].effect.target.type, '::before',
+ 'The animation targeting the ::before element comes second');
+ assert_equals(anims[2].effect.target.type, '::after',
+ 'The animation targeting the ::after element comes third');
+ assert_equals(anims[3].effect.target, child,
+ 'The animation targeting the child element comes last');
+}, 'CSS Animations targetting (pseudo-)elements should have correct order ' +
+ 'after sorting');
+
+done();
+</script>
+</body>
diff --git a/dom/animation/test/css-animations/file_effect-target.html b/dom/animation/test/css-animations/file_effect-target.html
new file mode 100644
index 0000000000..006028e345
--- /dev/null
+++ b/dom/animation/test/css-animations/file_effect-target.html
@@ -0,0 +1,54 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="../testcommon.js"></script>
+<style>
+@keyframes anim { }
+</style>
+<body>
+<script>
+'use strict';
+
+test(function(t) {
+ var div = addDiv(t);
+ div.style.animation = 'anim 100s';
+ var animation = div.getAnimations()[0];
+ assert_equals(animation.effect.target, div,
+ 'Animation.target is the animatable div');
+}, 'Returned CSS animations have the correct effect target');
+
+test(function(t) {
+ addStyle(t, { '.after::after': 'animation: anim 10s, anim 100s;' });
+ var div = addDiv(t, { class: 'after' });
+ var anims = document.getAnimations();
+ assert_equals(anims.length, 2,
+ 'Got animations running on ::after pseudo element');
+ assert_equals(anims[0].effect.target, anims[1].effect.target,
+ 'Both animations return the same target object');
+}, 'effect.target should return the same CSSPseudoElement object each time');
+
+test(function(t) {
+ addStyle(t, { '.after::after': 'animation: anim 10s;' });
+ var div = addDiv(t, { class: 'after' });
+ var pseudoTarget = document.getAnimations()[0].effect.target;
+ var effect = new KeyframeEffectReadOnly(pseudoTarget,
+ { background: ["blue", "red"] },
+ 3 * MS_PER_SEC);
+ var newAnim = new Animation(effect, document.timeline);
+ newAnim.play();
+ var anims = document.getAnimations();
+ assert_equals(anims.length, 2,
+ 'Got animations running on ::after pseudo element');
+ assert_not_equals(anims[0], newAnim,
+ 'The scriped-generated animation appears last');
+ assert_equals(newAnim.effect.target, pseudoTarget,
+ 'The effect.target of the scripted-generated animation is ' +
+ 'the same as the one from the argument of ' +
+ 'KeyframeEffectReadOnly constructor');
+ assert_equals(anims[0].effect.target, newAnim.effect.target,
+ 'Both animations return the same target object');
+}, 'effect.target from the script-generated animation should return the same ' +
+ 'CSSPseudoElement object as that from the CSS generated animation');
+
+done();
+</script>
+</body>
diff --git a/dom/animation/test/css-animations/file_element-get-animations.html b/dom/animation/test/css-animations/file_element-get-animations.html
new file mode 100644
index 0000000000..68386c98b0
--- /dev/null
+++ b/dom/animation/test/css-animations/file_element-get-animations.html
@@ -0,0 +1,445 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="../testcommon.js"></script>
+<style>
+@keyframes anim1 {
+ to { left: 100px }
+}
+@keyframes anim2 {
+ to { top: 100px }
+}
+@keyframes multiPropAnim {
+ to { background: green, opacity: 0.5, left: 100px, top: 100px }
+}
+@keyframes empty { }
+</style>
+<body>
+<script>
+'use strict';
+
+test(function(t) {
+ var div = addDiv(t);
+ assert_equals(div.getAnimations().length, 0,
+ 'getAnimations returns an empty sequence for an element'
+ + ' with no animations');
+}, 'getAnimations for non-animated content');
+
+promise_test(function(t) {
+ var div = addDiv(t);
+
+ // FIXME: This test does too many things. It should be split up.
+
+ // Add an animation
+ div.style.animation = 'anim1 100s';
+ var animations = div.getAnimations();
+ assert_equals(animations.length, 1,
+ 'getAnimations returns an Animation running CSS Animations');
+ return animations[0].ready.then(function() {
+ var startTime = animations[0].startTime;
+ assert_true(startTime > 0 && startTime <= document.timeline.currentTime,
+ 'CSS animation has a sensible start time');
+
+ // Wait a moment then add a second animation.
+ //
+ // We wait for the next frame so that we can test that the start times of
+ // the animations differ.
+ return waitForFrame();
+ }).then(function() {
+ div.style.animation = 'anim1 100s, anim2 100s';
+ animations = div.getAnimations();
+ assert_equals(animations.length, 2,
+ 'getAnimations returns one Animation for each value of'
+ + ' animation-name');
+ // Wait until both Animations are ready
+ // (We don't make any assumptions about the order of the Animations since
+ // that is the purpose of the following test.)
+ return waitForAllAnimations(animations);
+ }).then(function() {
+ assert_true(animations[0].startTime < animations[1].startTime,
+ 'Additional Animations for CSS animations start after the original'
+ + ' animation and appear later in the list');
+ });
+}, 'getAnimations for CSS Animations');
+
+test(function(t) {
+ var div = addDiv(t, { style: 'animation: anim1 100s' });
+ assert_class_string(div.getAnimations()[0], 'CSSAnimation',
+ 'Interface of returned animation is CSSAnimation');
+}, 'getAnimations returns CSSAnimation objects for CSS Animations');
+
+test(function(t) {
+ var div = addDiv(t);
+
+ // Add an animation that targets multiple properties
+ div.style.animation = 'multiPropAnim 100s';
+ assert_equals(div.getAnimations().length, 1,
+ 'getAnimations returns only one Animation for a CSS Animation'
+ + ' that targets multiple properties');
+}, 'getAnimations for multi-property animations');
+
+promise_test(function(t) {
+ var div = addDiv(t);
+
+ // Add an animation
+ div.style.backgroundColor = 'red';
+ div.style.animation = 'anim1 100s';
+ window.getComputedStyle(div).backgroundColor;
+
+ // Wait until a frame after the animation starts, then add a transition
+ var animations = div.getAnimations();
+ return animations[0].ready.then(waitForFrame).then(function() {
+ div.style.transition = 'all 100s';
+ div.style.backgroundColor = 'green';
+
+ animations = div.getAnimations();
+ assert_equals(animations.length, 2,
+ 'getAnimations returns Animations for both animations and'
+ + ' transitions that run simultaneously');
+ assert_class_string(animations[0], 'CSSTransition',
+ 'First-returned animation is the CSS Transition');
+ assert_class_string(animations[1], 'CSSAnimation',
+ 'Second-returned animation is the CSS Animation');
+ });
+}, 'getAnimations for both CSS Animations and CSS Transitions at once');
+
+async_test(function(t) {
+ var div = addDiv(t);
+
+ // Set up event listener
+ div.addEventListener('animationend', t.step_func(function() {
+ assert_equals(div.getAnimations().length, 0,
+ 'getAnimations does not return Animations for finished '
+ + ' (and non-forwards-filling) CSS Animations');
+ t.done();
+ }));
+
+ // Add a very short animation
+ div.style.animation = 'anim1 0.01s';
+}, 'getAnimations for CSS Animations that have finished');
+
+async_test(function(t) {
+ var div = addDiv(t);
+
+ // Set up event listener
+ div.addEventListener('animationend', t.step_func(function() {
+ assert_equals(div.getAnimations().length, 1,
+ 'getAnimations returns Animations for CSS Animations that have'
+ + ' finished but are filling forwards');
+ t.done();
+ }));
+
+ // Add a very short animation
+ div.style.animation = 'anim1 0.01s forwards';
+}, 'getAnimations for CSS Animations that have finished but are'
+ + ' forwards filling');
+
+test(function(t) {
+ var div = addDiv(t);
+ div.style.animation = 'none 100s';
+
+ var animations = div.getAnimations();
+ assert_equals(animations.length, 0,
+ 'getAnimations returns an empty sequence for an element'
+ + ' with animation-name: none');
+
+ div.style.animation = 'none 100s, anim1 100s';
+ animations = div.getAnimations();
+ assert_equals(animations.length, 1,
+ 'getAnimations returns Animations only for those CSS Animations whose'
+ + ' animation-name is not none');
+}, 'getAnimations for CSS Animations with animation-name: none');
+
+test(function(t) {
+ var div = addDiv(t);
+ div.style.animation = 'missing 100s';
+ var animations = div.getAnimations();
+ assert_equals(animations.length, 0,
+ 'getAnimations returns an empty sequence for an element'
+ + ' with animation-name: missing');
+
+ div.style.animation = 'anim1 100s, missing 100s';
+ animations = div.getAnimations();
+ assert_equals(animations.length, 1,
+ 'getAnimations returns Animations only for those CSS Animations whose'
+ + ' animation-name is found');
+}, 'getAnimations for CSS Animations with animation-name: missing');
+
+promise_test(function(t) {
+ var div = addDiv(t);
+ div.style.animation = 'anim1 100s, notyet 100s';
+ var animations = div.getAnimations();
+ assert_equals(animations.length, 1,
+ 'getAnimations initally only returns Animations for CSS Animations whose'
+ + ' animation-name is found');
+
+ return animations[0].ready.then(waitForFrame).then(function() {
+ var keyframes = '@keyframes notyet { to { left: 100px; } }';
+ document.styleSheets[0].insertRule(keyframes, 0);
+ animations = div.getAnimations();
+ assert_equals(animations.length, 2,
+ 'getAnimations includes Animation when @keyframes rule is added'
+ + ' later');
+ return waitForAllAnimations(animations);
+ }).then(function() {
+ assert_true(animations[0].startTime < animations[1].startTime,
+ 'Newly added animation has a later start time');
+ document.styleSheets[0].deleteRule(0);
+ });
+}, 'getAnimations for CSS Animations where the @keyframes rule is added'
+ + ' later');
+
+test(function(t) {
+ var div = addDiv(t);
+ div.style.animation = 'anim1 100s, anim1 100s';
+ assert_equals(div.getAnimations().length, 2,
+ 'getAnimations returns one Animation for each CSS animation-name'
+ + ' even if the names are duplicated');
+}, 'getAnimations for CSS Animations with duplicated animation-name');
+
+test(function(t) {
+ var div = addDiv(t);
+ div.style.animation = 'empty 100s';
+ assert_equals(div.getAnimations().length, 1,
+ 'getAnimations returns Animations for CSS animations with an'
+ + ' empty keyframes rule');
+}, 'getAnimations for CSS Animations with empty keyframes rule');
+
+promise_test(function(t) {
+ var div = addDiv(t);
+ div.style.animation = 'anim1 100s 100s';
+ var animations = div.getAnimations();
+ assert_equals(animations.length, 1,
+ 'getAnimations returns animations for CSS animations whose'
+ + ' delay makes them start later');
+ return animations[0].ready.then(waitForFrame).then(function() {
+ assert_true(animations[0].startTime <= document.timeline.currentTime,
+ 'For CSS Animations in delay phase, the start time of the Animation is'
+ + ' not in the future');
+ });
+}, 'getAnimations for CSS animations in delay phase');
+
+test(function(t) {
+ var div = addDiv(t);
+ div.style.animation = 'anim1 0s 100s';
+ assert_equals(div.getAnimations().length, 1,
+ 'getAnimations returns animations for CSS animations whose'
+ + ' duration is zero');
+ div.remove();
+}, 'getAnimations for zero-duration CSS Animations');
+
+test(function(t) {
+ var div = addDiv(t);
+ div.style.animation = 'anim1 100s';
+ var originalAnimation = div.getAnimations()[0];
+
+ // Update pause state (an Animation change)
+ div.style.animationPlayState = 'paused';
+ var pendingAnimation = div.getAnimations()[0];
+ assert_equals(pendingAnimation.playState, 'pending',
+ 'animation\'s play state is updated');
+ assert_equals(originalAnimation, pendingAnimation,
+ 'getAnimations returns the same objects even when their'
+ + ' play state changes');
+
+ // Update duration (an Animation change)
+ div.style.animationDuration = '200s';
+ var extendedAnimation = div.getAnimations()[0];
+ // FIXME: Check extendedAnimation.effect.timing.duration has changed once the
+ // API is available
+ assert_equals(originalAnimation, extendedAnimation,
+ 'getAnimations returns the same objects even when their'
+ + ' duration changes');
+}, 'getAnimations returns objects with the same identity');
+
+test(function(t) {
+ var div = addDiv(t);
+ div.style.animation = 'anim1 100s';
+
+ assert_equals(div.getAnimations().length, 1,
+ 'getAnimations returns an animation before cancelling');
+
+ var animation = div.getAnimations()[0];
+
+ animation.cancel();
+ assert_equals(div.getAnimations().length, 0,
+ 'getAnimations does not return cancelled animations');
+
+ animation.play();
+ assert_equals(div.getAnimations().length, 1,
+ 'getAnimations returns cancelled animations that have been re-started');
+
+}, 'getAnimations for CSS Animations that are cancelled');
+
+promise_test(function(t) {
+ var div = addDiv(t);
+ div.style.animation = 'anim2 100s';
+
+ return div.getAnimations()[0].ready.then(function() {
+ // Prepend to the list and test that even though anim1 was triggered
+ // *after* anim2, it should come first because it appears first
+ // in the animation-name property.
+ div.style.animation = 'anim1 100s, anim2 100s';
+ var anims = div.getAnimations();
+ assert_equals(anims[0].animationName, 'anim1',
+ 'animation order after prepending to list');
+ assert_equals(anims[1].animationName, 'anim2',
+ 'animation order after prepending to list');
+
+ // Normally calling cancel and play would this push anim1 to the top of
+ // the stack but it shouldn't for CSS animations that map an the
+ // animation-name property.
+ var anim1 = anims[0];
+ anim1.cancel();
+ anim1.play();
+ anims = div.getAnimations();
+ assert_equals(anims[0].animationName, 'anim1',
+ 'animation order after cancelling and restarting');
+ assert_equals(anims[1].animationName, 'anim2',
+ 'animation order after cancelling and restarting');
+ });
+}, 'getAnimations for CSS Animations follows animation-name order');
+
+test(function(t) {
+ addStyle(t, { '#target::after': 'animation: anim1 10s;',
+ '#target::before': 'animation: anim1 10s;' });
+ var target = addDiv(t, { 'id': 'target' });
+ target.style.animation = 'anim1 100s';
+
+ var animations = target.getAnimations({ subtree: false });
+ assert_equals(animations.length, 1,
+ 'Should find only the element');
+ assert_equals(animations[0].effect.target, target,
+ 'Effect target should be the element');
+}, 'Test AnimationFilter{ subtree: false } with single element');
+
+test(function(t) {
+ addStyle(t, { '#target::after': 'animation: anim1 10s;',
+ '#target::before': 'animation: anim1 10s;' });
+ var target = addDiv(t, { 'id': 'target' });
+ target.style.animation = 'anim1 100s';
+
+ var animations = target.getAnimations({ subtree: true });
+ assert_equals(animations.length, 3,
+ 'getAnimations({ subtree: true }) ' +
+ 'should return animations on pseudo-elements');
+ assert_equals(animations[0].effect.target, target,
+ 'The animation targeting the parent element ' +
+ 'should be returned first');
+ assert_equals(animations[1].effect.target.type, '::before',
+ 'The animation targeting the ::before pseudo-element ' +
+ 'should be returned second');
+ assert_equals(animations[2].effect.target.type, '::after',
+ 'The animation targeting the ::after pesudo-element ' +
+ 'should be returned last');
+}, 'Test AnimationFilter{ subtree: true } with single element');
+
+test(function(t) {
+ addStyle(t, { '#parent::after': 'animation: anim1 10s;',
+ '#parent::before': 'animation: anim1 10s;',
+ '#child::after': 'animation: anim1 10s;',
+ '#child::before': 'animation: anim1 10s;' });
+ var parent = addDiv(t, { 'id': 'parent' });
+ parent.style.animation = 'anim1 100s';
+ var child = addDiv(t, { 'id': 'child' });
+ child.style.animation = 'anim1 100s';
+ parent.appendChild(child);
+
+ var animations = parent.getAnimations({ subtree: false });
+ assert_equals(animations.length, 1,
+ 'Should find only the element even if it has a child');
+ assert_equals(animations[0].effect.target, parent,
+ 'Effect target shuld be the element');
+}, 'Test AnimationFilter{ subtree: false } with element that has a child');
+
+test(function(t) {
+ addStyle(t, { '#parent::after': 'animation: anim1 10s;',
+ '#parent::before': 'animation: anim1 10s;',
+ '#child::after': 'animation: anim1 10s;',
+ '#child::before': 'animation: anim1 10s;' });
+ var parent = addDiv(t, { 'id': 'parent' });
+ var child = addDiv(t, { 'id': 'child' });
+ parent.style.animation = 'anim1 100s';
+ child.style.animation = 'anim1 100s';
+ parent.appendChild(child);
+
+ var animations = parent.getAnimations({ subtree: true });
+ assert_equals(animations.length, 6,
+ 'Should find all elements, pesudo-elements that parent has');
+
+ assert_equals(animations[0].effect.target, parent,
+ 'The animation targeting the parent element ' +
+ 'should be returned first');
+ assert_equals(animations[1].effect.target.type, '::before',
+ 'The animation targeting the ::before pseudo-element ' +
+ 'should be returned second');
+ assert_equals(animations[1].effect.target.parentElement, parent,
+ 'This ::before element should be child of parent element');
+ assert_equals(animations[2].effect.target.type, '::after',
+ 'The animation targeting the ::after pesudo-element ' +
+ 'should be returned third');
+ assert_equals(animations[2].effect.target.parentElement, parent,
+ 'This ::after element should be child of parent element');
+
+ assert_equals(animations[3].effect.target, child,
+ 'The animation targeting the child element ' +
+ 'should be returned fourth');
+ assert_equals(animations[4].effect.target.type, '::before',
+ 'The animation targeting the ::before pseudo-element ' +
+ 'should be returned fifth');
+ assert_equals(animations[4].effect.target.parentElement, child,
+ 'This ::before element should be child of child element');
+ assert_equals(animations[5].effect.target.type, '::after',
+ 'The animation targeting the ::after pesudo-element ' +
+ 'should be returned last');
+ assert_equals(animations[5].effect.target.parentElement, child,
+ 'This ::after element should be child of child element');
+}, 'Test AnimationFilter{ subtree: true } with element that has a child');
+
+test(function(t) {
+ var parent = addDiv(t, { 'id': 'parent' });
+ var child1 = addDiv(t, { 'id': 'child1' });
+ var grandchild1 = addDiv(t, { 'id': 'grandchild1' });
+ var grandchild2 = addDiv(t, { 'id': 'grandchild2' });
+ var child2 = addDiv(t, { 'id': 'child2' });
+
+ parent.style.animation = 'anim1 100s';
+ child1.style.animation = 'anim1 100s';
+ grandchild1.style.animation = 'anim1 100s';
+ grandchild2.style.animation = 'anim1 100s';
+ child2.style.animation = 'anim1 100s';
+
+ parent.appendChild(child1);
+ child1.appendChild(grandchild1);
+ child1.appendChild(grandchild2);
+ parent.appendChild(child2);
+
+ var animations = parent.getAnimations({ subtree: true });
+ assert_equals(
+ parent.getAnimations({ subtree: true }).length, 5,
+ 'Should find all descendants of the element');
+
+ assert_equals(animations[0].effect.target, parent,
+ 'The animation targeting the parent element ' +
+ 'should be returned first');
+
+ assert_equals(animations[1].effect.target, child1,
+ 'The animation targeting the child1 element ' +
+ 'should be returned second');
+
+ assert_equals(animations[2].effect.target, grandchild1,
+ 'The animation targeting the grandchild1 element ' +
+ 'should be returned third');
+
+ assert_equals(animations[3].effect.target, grandchild2,
+ 'The animation targeting the grandchild2 element ' +
+ 'should be returned fourth');
+
+ assert_equals(animations[4].effect.target, child2,
+ 'The animation targeting the child2 element ' +
+ 'should be returned last');
+
+}, 'Test AnimationFilter{ subtree: true } with element that has many descendant');
+
+done();
+</script>
+</body>
diff --git a/dom/animation/test/css-animations/file_keyframeeffect-getkeyframes.html b/dom/animation/test/css-animations/file_keyframeeffect-getkeyframes.html
new file mode 100644
index 0000000000..15e2d23f18
--- /dev/null
+++ b/dom/animation/test/css-animations/file_keyframeeffect-getkeyframes.html
@@ -0,0 +1,672 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="../testcommon.js"></script>
+<style>
+@keyframes anim-empty { }
+
+@keyframes anim-empty-frames {
+ from { }
+ to { }
+}
+
+@keyframes anim-only-timing {
+ from { animation-timing-function: linear; }
+ to { }
+}
+
+@keyframes anim-only-non-animatable {
+ from { animation-duration: 3s; }
+ to { animation-duration: 5s; }
+}
+
+@keyframes anim-simple {
+ from { color: black; }
+ to { color: white; }
+}
+
+@keyframes anim-simple-three {
+ from { color: black; }
+ 50% { color: blue; }
+ to { color: white; }
+}
+
+@keyframes anim-simple-timing {
+ from { color: black; animation-timing-function: linear; }
+ 50% { color: blue; animation-timing-function: ease-in-out; }
+ to { color: white; animation-timing-function: step-end; }
+}
+
+@keyframes anim-simple-timing-some {
+ from { color: black; animation-timing-function: linear; }
+ 50% { color: blue; }
+ to { color: white; }
+}
+
+@keyframes anim-simple-shorthand {
+ from { margin: 8px; }
+ to { margin: 16px; }
+}
+
+@keyframes anim-omit-to {
+ from { color: blue; }
+}
+
+@keyframes anim-omit-from {
+ to { color: blue; }
+}
+
+@keyframes anim-omit-from-to {
+ 50% { color: blue; }
+}
+
+@keyframes anim-partially-omit-to {
+ from { margin-top: 50px;
+ margin-bottom: 100px; }
+ to { margin-top: 150px !important; /* ignored */
+ margin-bottom: 200px; }
+}
+
+@keyframes anim-different-props {
+ from { color: black; margin-top: 8px; }
+ 25% { color: blue; }
+ 75% { margin-top: 12px; }
+ to { color: white; margin-top: 16px }
+}
+
+@keyframes anim-different-props-and-easing {
+ from { color: black; margin-top: 8px; animation-timing-function: linear; }
+ 25% { color: blue; animation-timing-function: step-end; }
+ 75% { margin-top: 12px; animation-timing-function: ease-in; }
+ to { color: white; margin-top: 16px }
+}
+
+@keyframes anim-merge-offset {
+ from { color: black; }
+ to { color: white; }
+ from { margin-top: 8px; }
+ to { margin-top: 16px; }
+}
+
+@keyframes anim-merge-offset-and-easing {
+ from { color: black; animation-timing-function: step-end; }
+ to { color: white; }
+ from { margin-top: 8px; animation-timing-function: linear; }
+ to { margin-top: 16px; }
+ from { font-size: 16px; animation-timing-function: step-end; }
+ to { font-size: 32px; }
+ from { padding-left: 2px; animation-timing-function: linear; }
+ to { padding-left: 4px; }
+}
+
+@keyframes anim-no-merge-equiv-easing {
+ from { margin-top: 0px; animation-timing-function: steps(1, end); }
+ from { margin-right: 0px; animation-timing-function: step-end; }
+ from { margin-bottom: 0px; animation-timing-function: steps(1); }
+ 50% { margin-top: 10px; animation-timing-function: step-end; }
+ 50% { margin-right: 10px; animation-timing-function: step-end; }
+ 50% { margin-bottom: 10px; animation-timing-function: step-end; }
+ to { margin-top: 20px; margin-right: 20px; margin-bottom: 20px; }
+}
+
+@keyframes anim-overriding {
+ from { padding-top: 50px }
+ 50%, from { padding-top: 30px } /* wins: 0% */
+ 75%, 85%, 50% { padding-top: 20px } /* wins: 75%, 50% */
+ 100%, 85% { padding-top: 70px } /* wins: 100% */
+ 85.1% { padding-top: 60px } /* wins: 85.1% */
+ 85% { padding-top: 30px } /* wins: 85% */
+}
+
+@keyframes anim-filter {
+ to { filter: blur(5px) sepia(60%) saturate(30%); }
+}
+
+@keyframes anim-text-shadow {
+ to { text-shadow: none; }
+}
+
+@keyframes anim-background-size {
+ to { background-size: 50%, 6px, contain }
+}
+
+:root {
+ --var-100px: 100px;
+}
+@keyframes anim-variables {
+ to { transform: translate(var(--var-100px), 0) }
+}
+@keyframes anim-variables-shorthand {
+ to { margin: var(--var-100px) }
+}
+</style>
+<body>
+<script>
+"use strict";
+
+function getKeyframes(e) {
+ return e.getAnimations()[0].effect.getKeyframes();
+}
+
+function assert_frames_equal(a, b, name) {
+ assert_equals(Object.keys(a).sort().toString(),
+ Object.keys(b).sort().toString(),
+ "properties on " + name);
+ for (var p in a) {
+ if (p === 'offset' || p === 'computedOffset') {
+ assert_approx_equals(a[p], b[p], 0.00001,
+ "value for '" + p + "' on " + name);
+ } else {
+ assert_equals(a[p], b[p], "value for '" + p + "' on " + name);
+ }
+ }
+}
+
+// animation-timing-function values to test with, where the value
+// is exactly the same as its serialization, sorted by the order
+// getKeyframes() will group frames with the same easing function
+// together (by nsTimingFunction::Compare).
+const kTimingFunctionValues = [
+ "ease",
+ "linear",
+ "ease-in",
+ "ease-out",
+ "ease-in-out",
+ "steps(1, start)",
+ "steps(2, start)",
+ "steps(1)",
+ "steps(2)",
+ "cubic-bezier(0, 0, 1, 1)",
+ "cubic-bezier(0, 0.25, 0.75, 1)"
+];
+
+test(function(t) {
+ var div = addDiv(t);
+
+ div.style.animation = 'anim-empty 100s';
+ assert_equals(getKeyframes(div).length, 0,
+ "number of frames with empty @keyframes");
+
+ div.style.animation = 'anim-empty-frames 100s';
+ assert_equals(getKeyframes(div).length, 0,
+ "number of frames when @keyframes has empty keyframes");
+
+ div.style.animation = 'anim-only-timing 100s';
+ assert_equals(getKeyframes(div).length, 0,
+ "number of frames when @keyframes only has keyframes with " +
+ "animation-timing-function");
+
+ div.style.animation = 'anim-only-non-animatable 100s';
+ assert_equals(getKeyframes(div).length, 0,
+ "number of frames when @keyframes only has frames with " +
+ "non-animatable properties");
+}, 'KeyframeEffectReadOnly.getKeyframes() returns no frames for various kinds'
+ + ' of empty enimations');
+
+test(function(t) {
+ var div = addDiv(t);
+
+ div.style.animation = 'anim-simple 100s';
+ var frames = getKeyframes(div);
+
+ assert_equals(frames.length, 2, "number of frames");
+
+ var expected = [
+ { offset: 0, computedOffset: 0, easing: "ease",
+ color: "rgb(0, 0, 0)" },
+ { offset: 1, computedOffset: 1, easing: "ease",
+ color: "rgb(255, 255, 255)" },
+ ];
+
+ for (var i = 0; i < frames.length; i++) {
+ assert_frames_equal(frames[i], expected[i], "ComputedKeyframe #" + i);
+ }
+}, 'KeyframeEffectReadOnly.getKeyframes() returns expected frames for a simple'
+ + 'animation');
+
+test(function(t) {
+ kTimingFunctionValues.forEach(function(easing) {
+ var div = addDiv(t);
+
+ div.style.animation = 'anim-simple-three 100s ' + easing;
+ var frames = getKeyframes(div);
+
+ assert_equals(frames.length, 3, "number of frames");
+
+ for (var i = 0; i < frames.length; i++) {
+ assert_equals(frames[i].easing, easing,
+ "value for 'easing' on ComputedKeyframe #" + i);
+ }
+ });
+}, 'KeyframeEffectReadOnly.getKeyframes() returns frames with expected easing'
+ + ' values, when the easing comes from animation-timing-function on the'
+ + ' element');
+
+test(function(t) {
+ var div = addDiv(t);
+
+ div.style.animation = 'anim-simple-timing 100s';
+ var frames = getKeyframes(div);
+
+ assert_equals(frames.length, 3, "number of frames");
+ assert_equals(frames[0].easing, "linear",
+ "value of 'easing' on ComputedKeyframe #0");
+ assert_equals(frames[1].easing, "ease-in-out",
+ "value of 'easing' on ComputedKeyframe #1");
+ assert_equals(frames[2].easing, "steps(1)",
+ "value of 'easing' on ComputedKeyframe #2");
+}, 'KeyframeEffectReadOnly.getKeyframes() returns frames with expected easing'
+ + ' values, when the easing is specified on each keyframe');
+
+test(function(t) {
+ var div = addDiv(t);
+
+ div.style.animation = 'anim-simple-timing-some 100s step-start';
+ var frames = getKeyframes(div);
+
+ assert_equals(frames.length, 3, "number of frames");
+ assert_equals(frames[0].easing, "linear",
+ "value of 'easing' on ComputedKeyframe #0");
+ assert_equals(frames[1].easing, "steps(1, start)",
+ "value of 'easing' on ComputedKeyframe #1");
+ assert_equals(frames[2].easing, "steps(1, start)",
+ "value of 'easing' on ComputedKeyframe #2");
+}, 'KeyframeEffectReadOnly.getKeyframes() returns frames with expected easing'
+ + ' values, when the easing is specified on some keyframes');
+
+test(function(t) {
+ var div = addDiv(t);
+
+ div.style.animation = 'anim-simple-shorthand 100s';
+ var frames = getKeyframes(div);
+
+ assert_equals(frames.length, 2, "number of frames");
+
+ var expected = [
+ { offset: 0, computedOffset: 0, easing: "ease",
+ marginBottom: "8px", marginLeft: "8px",
+ marginRight: "8px", marginTop: "8px" },
+ { offset: 1, computedOffset: 1, easing: "ease",
+ marginBottom: "16px", marginLeft: "16px",
+ marginRight: "16px", marginTop: "16px" },
+ ];
+
+ for (var i = 0; i < frames.length; i++) {
+ assert_frames_equal(frames[i], expected[i], "ComputedKeyframe #" + i);
+ }
+}, 'KeyframeEffectReadOnly.getKeyframes() returns expected frames for a simple'
+ + ' animation that specifies a single shorthand property');
+
+test(function(t) {
+ var div = addDiv(t);
+
+ div.style.animation = 'anim-omit-to 100s';
+ div.style.color = 'white';
+ var frames = getKeyframes(div);
+
+ assert_equals(frames.length, 2, "number of frames");
+
+ var expected = [
+ { offset: 0, computedOffset: 0, easing: "ease",
+ color: "rgb(0, 0, 255)" },
+ { offset: 1, computedOffset: 1, easing: "ease",
+ color: "rgb(255, 255, 255)" },
+ ];
+
+ for (var i = 0; i < frames.length; i++) {
+ assert_frames_equal(frames[i], expected[i], "ComputedKeyframe #" + i);
+ }
+}, 'KeyframeEffectReadOnly.getKeyframes() returns expected frames for an ' +
+ 'animation with a 0% keyframe and no 100% keyframe');
+
+test(function(t) {
+ var div = addDiv(t);
+
+ div.style.animation = 'anim-omit-from 100s';
+ div.style.color = 'white';
+ var frames = getKeyframes(div);
+
+ assert_equals(frames.length, 2, "number of frames");
+
+ var expected = [
+ { offset: 0, computedOffset: 0, easing: "ease",
+ color: "rgb(255, 255, 255)" },
+ { offset: 1, computedOffset: 1, easing: "ease",
+ color: "rgb(0, 0, 255)" },
+ ];
+
+ for (var i = 0; i < frames.length; i++) {
+ assert_frames_equal(frames[i], expected[i], "ComputedKeyframe #" + i);
+ }
+}, 'KeyframeEffectReadOnly.getKeyframes() returns expected frames for an ' +
+ 'animation with a 100% keyframe and no 0% keyframe');
+
+test(function(t) {
+ var div = addDiv(t);
+
+ div.style.animation = 'anim-omit-from-to 100s';
+ div.style.color = 'white';
+ var frames = getKeyframes(div);
+
+ assert_equals(frames.length, 3, "number of frames");
+
+ var expected = [
+ { offset: 0, computedOffset: 0, easing: "ease",
+ color: "rgb(255, 255, 255)" },
+ { offset: 0.5, computedOffset: 0.5, easing: "ease",
+ color: "rgb(0, 0, 255)" },
+ { offset: 1, computedOffset: 1, easing: "ease",
+ color: "rgb(255, 255, 255)" },
+ ];
+
+ for (var i = 0; i < frames.length; i++) {
+ assert_frames_equal(frames[i], expected[i], "ComputedKeyframe #" + i);
+ }
+}, 'KeyframeEffectReadOnly.getKeyframes() returns expected frames for an ' +
+ 'animation with no 0% or 100% keyframe but with a 50% keyframe');
+
+test(function(t) {
+ var div = addDiv(t);
+
+ div.style.animation = 'anim-partially-omit-to 100s';
+ div.style.marginTop = '250px';
+ var frames = getKeyframes(div);
+
+ assert_equals(frames.length, 2, "number of frames");
+
+ var expected = [
+ { offset: 0, computedOffset: 0, easing: "ease",
+ marginTop: '50px', marginBottom: '100px' },
+ { offset: 1, computedOffset: 1, easing: "ease",
+ marginTop: '250px', marginBottom: '200px' },
+ ];
+
+ for (var i = 0; i < frames.length; i++) {
+ assert_frames_equal(frames[i], expected[i], "ComputedKeyframe #" + i);
+ }
+}, 'KeyframeEffectReadOnly.getKeyframes() returns expected frames for an ' +
+ 'animation with a partially complete 100% keyframe (because the ' +
+ '!important rule is ignored)');
+
+test(function(t) {
+ var div = addDiv(t);
+
+ div.style.animation = 'anim-different-props 100s';
+ var frames = getKeyframes(div);
+
+ assert_equals(frames.length, 4, "number of frames");
+
+ var expected = [
+ { offset: 0, computedOffset: 0, easing: "ease",
+ color: "rgb(0, 0, 0)", marginTop: "8px" },
+ { offset: 0.25, computedOffset: 0.25, easing: "ease",
+ color: "rgb(0, 0, 255)" },
+ { offset: 0.75, computedOffset: 0.75, easing: "ease",
+ marginTop: "12px" },
+ { offset: 1, computedOffset: 1, easing: "ease",
+ color: "rgb(255, 255, 255)", marginTop: "16px" },
+ ];
+
+ for (var i = 0; i < frames.length; i++) {
+ assert_frames_equal(frames[i], expected[i], "ComputedKeyframe #" + i);
+ }
+}, 'KeyframeEffectReadOnly.getKeyframes() returns expected frames for an ' +
+ 'animation with different properties on different keyframes, all ' +
+ 'with the same easing function');
+
+test(function(t) {
+ var div = addDiv(t);
+
+ div.style.animation = 'anim-different-props-and-easing 100s';
+ var frames = getKeyframes(div);
+
+ assert_equals(frames.length, 4, "number of frames");
+
+ var expected = [
+ { offset: 0, computedOffset: 0, easing: "linear",
+ color: "rgb(0, 0, 0)", marginTop: "8px" },
+ { offset: 0.25, computedOffset: 0.25, easing: "steps(1)",
+ color: "rgb(0, 0, 255)" },
+ { offset: 0.75, computedOffset: 0.75, easing: "ease-in",
+ marginTop: "12px" },
+ { offset: 1, computedOffset: 1, easing: "ease",
+ color: "rgb(255, 255, 255)", marginTop: "16px" },
+ ];
+
+ for (var i = 0; i < frames.length; i++) {
+ assert_frames_equal(frames[i], expected[i], "ComputedKeyframe #" + i);
+ }
+}, 'KeyframeEffectReadOnly.getKeyframes() returns expected frames for an ' +
+ 'animation with different properties on different keyframes, with ' +
+ 'a different easing function on each');
+
+test(function(t) {
+ var div = addDiv(t);
+
+ div.style.animation = 'anim-merge-offset 100s';
+ var frames = getKeyframes(div);
+
+ assert_equals(frames.length, 2, "number of frames");
+
+ var expected = [
+ { offset: 0, computedOffset: 0, easing: "ease",
+ color: "rgb(0, 0, 0)", marginTop: "8px" },
+ { offset: 1, computedOffset: 1, easing: "ease",
+ color: "rgb(255, 255, 255)", marginTop: "16px" },
+ ];
+
+ for (var i = 0; i < frames.length; i++) {
+ assert_frames_equal(frames[i], expected[i], "ComputedKeyframe #" + i);
+ }
+}, 'KeyframeEffectReadOnly.getKeyframes() returns expected frames for an ' +
+ 'animation with multiple keyframes for the same time, and all with ' +
+ 'the same easing function');
+
+test(function(t) {
+ var div = addDiv(t);
+
+ div.style.animation = 'anim-merge-offset-and-easing 100s';
+ var frames = getKeyframes(div);
+
+ assert_equals(frames.length, 3, "number of frames");
+
+ var expected = [
+ { offset: 0, computedOffset: 0, easing: "steps(1)",
+ color: "rgb(0, 0, 0)", fontSize: "16px" },
+ { offset: 0, computedOffset: 0, easing: "linear",
+ marginTop: "8px", paddingLeft: "2px" },
+ { offset: 1, computedOffset: 1, easing: "ease",
+ color: "rgb(255, 255, 255)", fontSize: "32px", marginTop: "16px",
+ paddingLeft: "4px" },
+ ];
+
+ for (var i = 0; i < frames.length; i++) {
+ assert_frames_equal(frames[i], expected[i], "ComputedKeyframe #" + i);
+ }
+}, 'KeyframeEffectReadOnly.getKeyframes() returns expected frames for an ' +
+ 'animation with multiple keyframes for the same time and with ' +
+ 'different easing functions');
+
+test(function(t) {
+ var div = addDiv(t);
+
+ div.style.animation = 'anim-no-merge-equiv-easing 100s';
+ var frames = getKeyframes(div);
+
+ assert_equals(frames.length, 3, "number of frames");
+
+ var expected = [
+ { offset: 0, computedOffset: 0, easing: "steps(1)",
+ marginTop: "0px", marginRight: "0px", marginBottom: "0px" },
+ { offset: 0.5, computedOffset: 0.5, easing: "steps(1)",
+ marginTop: "10px", marginRight: "10px", marginBottom: "10px" },
+ { offset: 1, computedOffset: 1, easing: "ease",
+ marginTop: "20px", marginRight: "20px", marginBottom: "20px" },
+ ];
+
+ for (var i = 0; i < frames.length; i++) {
+ assert_frames_equal(frames[i], expected[i], "ComputedKeyframe #" + i);
+ }
+}, 'KeyframeEffectReadOnly.getKeyframes() returns expected frames for an ' +
+ 'animation with multiple keyframes for the same time and with ' +
+ 'different but equivalent easing functions');
+
+test(function(t) {
+ var div = addDiv(t);
+
+ div.style.animation = 'anim-overriding 100s';
+ var frames = getKeyframes(div);
+
+ assert_equals(frames.length, 6, "number of frames");
+
+ var expected = [
+ { offset: 0, computedOffset: 0, easing: "ease",
+ paddingTop: "30px" },
+ { offset: 0.5, computedOffset: 0.5, easing: "ease",
+ paddingTop: "20px" },
+ { offset: 0.75, computedOffset: 0.75, easing: "ease",
+ paddingTop: "20px" },
+ { offset: 0.85, computedOffset: 0.85, easing: "ease",
+ paddingTop: "30px" },
+ { offset: 0.851, computedOffset: 0.851, easing: "ease",
+ paddingTop: "60px" },
+ { offset: 1, computedOffset: 1, easing: "ease",
+ paddingTop: "70px" },
+ ];
+
+ for (var i = 0; i < frames.length; i++) {
+ assert_frames_equal(frames[i], expected[i], "ComputedKeyframe #" + i);
+ }
+}, 'KeyframeEffectReadOnly.getKeyframes() returns expected frames for ' +
+ 'overlapping keyframes');
+
+// Gecko-specific test case: We are specifically concerned here that the
+// computed value for filter, "none", is correctly represented.
+
+test(function(t) {
+ var div = addDiv(t);
+
+ div.style.animation = 'anim-filter 100s';
+ var frames = getKeyframes(div);
+
+ assert_equals(frames.length, 2, "number of frames");
+
+ var expected = [
+ { offset: 0, computedOffset: 0, easing: "ease",
+ filter: "none" },
+ { offset: 1, computedOffset: 1, easing: "ease",
+ filter: "blur(5px) sepia(60%) saturate(30%)" },
+ ];
+
+ for (var i = 0; i < frames.length; i++) {
+ assert_frames_equal(frames[i], expected[i], "ComputedKeyframe #" + i);
+ }
+}, 'KeyframeEffectReadOnly.getKeyframes() returns expected values for ' +
+ 'animations with filter properties and missing keyframes');
+
+// Gecko-specific test case: We are specifically concerned here that the
+// computed value for text-shadow and a "none" specified on a keyframe
+// are correctly represented.
+
+test(function(t) {
+ var div = addDiv(t);
+
+ div.style.textShadow = '1px 1px 2px black, 0 0 16px blue, 0 0 3.2px blue';
+ div.style.animation = 'anim-text-shadow 100s';
+ var frames = getKeyframes(div);
+
+ assert_equals(frames.length, 2, "number of frames");
+
+ var expected = [
+ { offset: 0, computedOffset: 0, easing: "ease",
+ textShadow: "1px 1px 2px 0px rgb(0, 0, 0),"
+ + " 0px 0px 16px 0px rgb(0, 0, 255),"
+ + " 0px 0px 3.2px 0px rgb(0, 0, 255)" },
+ { offset: 1, computedOffset: 1, easing: "ease", textShadow: "none" },
+ ];
+
+ for (var i = 0; i < frames.length; i++) {
+ assert_frames_equal(frames[i], expected[i], "ComputedKeyframe #" + i);
+ }
+}, 'KeyframeEffectReadOnly.getKeyframes() returns expected values for ' +
+ 'animations with text-shadow properties and missing keyframes');
+
+// Gecko-specific test case: We are specifically concerned here that the
+// initial value for background-size and the specified list are correctly
+// represented.
+
+test(function(t) {
+ var div = addDiv(t);
+
+ div.style.animation = 'anim-background-size 100s';
+ var frames = getKeyframes(div);
+
+ assert_equals(frames.length, 2, "number of frames");
+
+ var expected = [
+ { offset: 0, computedOffset: 0, easing: "ease",
+ backgroundSize: "auto auto" },
+ { offset: 1, computedOffset: 1, easing: "ease",
+ backgroundSize: "50% auto, 6px auto, contain" },
+ ];
+
+ for (var i = 0; i < frames.length; i++) {
+ assert_frames_equal(frames[i], expected[i], "ComputedKeyframe #" + i);
+ }
+
+ // Test inheriting a background-size value
+
+ expected[0].backgroundSize = div.style.backgroundSize =
+ "30px auto, 40% auto, auto auto";
+ frames = getKeyframes(div);
+
+ for (var i = 0; i < frames.length; i++) {
+ assert_frames_equal(frames[i], expected[i], "ComputedKeyframe #" + i
+ + " after updating current style");
+ }
+}, 'KeyframeEffectReadOnly.getKeyframes() returns expected values for ' +
+ 'animations with background-size properties and missing keyframes');
+
+test(function(t) {
+ var div = addDiv(t);
+ div.style.animation = 'anim-variables 100s';
+
+ var frames = getKeyframes(div);
+
+ var expected = [
+ { offset: 0, computedOffset: 0, easing: "ease",
+ transform: "none" },
+ { offset: 1, computedOffset: 1, easing: "ease",
+ transform: "translate(100px, 0px)" },
+ ];
+ for (var i = 0; i < frames.length; i++) {
+ assert_frames_equal(frames[i], expected[i], "ComputedKeyframe #" + i);
+ }
+}, 'KeyframeEffectReadOnly.getKeyframes() returns expected values for ' +
+ 'animations with CSS variables as keyframe values');
+
+test(function(t) {
+ var div = addDiv(t);
+ div.style.animation = 'anim-variables-shorthand 100s';
+
+ var frames = getKeyframes(div);
+
+ var expected = [
+ { offset: 0, computedOffset: 0, easing: "ease",
+ marginBottom: "0px",
+ marginLeft: "0px",
+ marginRight: "0px",
+ marginTop: "0px" },
+ { offset: 1, computedOffset: 1, easing: "ease",
+ marginBottom: "100px",
+ marginLeft: "100px",
+ marginRight: "100px",
+ marginTop: "100px" },
+ ];
+ for (var i = 0; i < frames.length; i++) {
+ assert_frames_equal(frames[i], expected[i], "ComputedKeyframe #" + i);
+ }
+}, 'KeyframeEffectReadOnly.getKeyframes() returns expected values for ' +
+ 'animations with CSS variables as keyframe values in a shorthand property');
+done();
+</script>
+</body>
diff --git a/dom/animation/test/css-animations/file_pseudoElement-get-animations.html b/dom/animation/test/css-animations/file_pseudoElement-get-animations.html
new file mode 100644
index 0000000000..bebe145334
--- /dev/null
+++ b/dom/animation/test/css-animations/file_pseudoElement-get-animations.html
@@ -0,0 +1,70 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="../testcommon.js"></script>
+<style>
+@keyframes anim1 { }
+@keyframes anim2 { }
+.before::before {
+ animation: anim1 10s;
+}
+.after-with-mix-anims-trans::after {
+ content: '';
+ animation: anim1 10s, anim2 10s;
+ width: 0px;
+ height: 0px;
+ transition: all 100s;
+}
+.after-change::after {
+ width: 100px;
+ height: 100px;
+}
+</style>
+<body>
+<script>
+'use strict';
+
+test(function(t) {
+ var div = addDiv(t, { class: 'before' });
+ var pseudoTarget = document.getAnimations()[0].effect.target;
+ assert_equals(pseudoTarget.getAnimations().length, 1,
+ 'Expected number of animations are returned');
+ assert_equals(pseudoTarget.getAnimations()[0].animationName, 'anim1',
+ 'CSS animation name matches');
+}, 'getAnimations returns CSSAnimation objects');
+
+test(function(t) {
+ var div = addDiv(t, { class: 'after-with-mix-anims-trans' });
+ // Trigger transitions
+ flushComputedStyle(div);
+ div.classList.add('after-change');
+
+ // Create additional animation on the pseudo-element from script
+ var pseudoTarget = document.getAnimations()[0].effect.target;
+ var effect = new KeyframeEffectReadOnly(pseudoTarget,
+ { background: ["blue", "red"] },
+ 3 * MS_PER_SEC);
+ var newAnimation = new Animation(effect, document.timeline);
+ newAnimation.id = 'scripted-anim';
+ newAnimation.play();
+
+ // Check order - the script-generated animation should appear later
+ var anims = pseudoTarget.getAnimations();
+ assert_equals(anims.length, 5,
+ 'Got expected number of animations/trnasitions running on ' +
+ '::after pseudo element');
+ assert_equals(anims[0].transitionProperty, 'height',
+ '1st animation is the 1st transition sorted by name');
+ assert_equals(anims[1].transitionProperty, 'width',
+ '2nd animation is the 2nd transition sorted by name ');
+ assert_equals(anims[2].animationName, 'anim1',
+ '3rd animation is the 1st animation in animation-name list');
+ assert_equals(anims[3].animationName, 'anim2',
+ '4rd animation is the 2nd animation in animation-name list');
+ assert_equals(anims[4].id, 'scripted-anim',
+ 'Animation added by script appears last');
+}, 'getAnimations returns css transitions/animations, and script-generated ' +
+ 'animations in the expected order');
+
+done();
+</script>
+</body>
diff --git a/dom/animation/test/css-animations/test_animation-cancel.html b/dom/animation/test/css-animations/test_animation-cancel.html
new file mode 100644
index 0000000000..15c9b482fe
--- /dev/null
+++ b/dom/animation/test/css-animations/test_animation-cancel.html
@@ -0,0 +1,15 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+'use strict';
+setup({explicit_done: true});
+SpecialPowers.pushPrefEnv(
+ { "set": [["dom.animations-api.core.enabled", true]]},
+ function() {
+ window.open("file_animation-cancel.html");
+ });
+</script>
+</html>
diff --git a/dom/animation/test/css-animations/test_animation-computed-timing.html b/dom/animation/test/css-animations/test_animation-computed-timing.html
new file mode 100644
index 0000000000..c1b40aaf36
--- /dev/null
+++ b/dom/animation/test/css-animations/test_animation-computed-timing.html
@@ -0,0 +1,16 @@
+<!doctype html>
+<html>
+<meta charset=utf-8>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+'use strict';
+setup({explicit_done: true});
+SpecialPowers.pushPrefEnv(
+ { "set": [["dom.animations-api.core.enabled", true]]},
+ function() {
+ window.open("file_animation-computed-timing.html");
+ });
+</script>
+</html>
diff --git a/dom/animation/test/css-animations/test_animation-currenttime.html b/dom/animation/test/css-animations/test_animation-currenttime.html
new file mode 100644
index 0000000000..7e3a8d74d6
--- /dev/null
+++ b/dom/animation/test/css-animations/test_animation-currenttime.html
@@ -0,0 +1,15 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+'use strict';
+setup({explicit_done: true});
+SpecialPowers.pushPrefEnv(
+ { "set": [["dom.animations-api.core.enabled", true]]},
+ function() {
+ window.open("file_animation-currenttime.html");
+ });
+</script>
+</html>
diff --git a/dom/animation/test/css-animations/test_animation-finish.html b/dom/animation/test/css-animations/test_animation-finish.html
new file mode 100644
index 0000000000..abbd267d82
--- /dev/null
+++ b/dom/animation/test/css-animations/test_animation-finish.html
@@ -0,0 +1,15 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+'use strict';
+setup({explicit_done: true});
+SpecialPowers.pushPrefEnv(
+ { "set": [["dom.animations-api.core.enabled", true]]},
+ function() {
+ window.open("file_animation-finish.html");
+ });
+</script>
+</html>
diff --git a/dom/animation/test/css-animations/test_animation-finished.html b/dom/animation/test/css-animations/test_animation-finished.html
new file mode 100644
index 0000000000..295ffe0af6
--- /dev/null
+++ b/dom/animation/test/css-animations/test_animation-finished.html
@@ -0,0 +1,15 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+'use strict';
+setup({explicit_done: true});
+SpecialPowers.pushPrefEnv(
+ { "set": [["dom.animations-api.core.enabled", true]]},
+ function() {
+ window.open("file_animation-finished.html");
+ });
+</script>
+</html>
diff --git a/dom/animation/test/css-animations/test_animation-id.html b/dom/animation/test/css-animations/test_animation-id.html
new file mode 100644
index 0000000000..c23501b8d6
--- /dev/null
+++ b/dom/animation/test/css-animations/test_animation-id.html
@@ -0,0 +1,15 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+'use strict';
+setup({explicit_done: true});
+SpecialPowers.pushPrefEnv(
+ { "set": [["dom.animations-api.core.enabled", true]]},
+ function() {
+ window.open("file_animation-id.html");
+ });
+</script>
+</html>
diff --git a/dom/animation/test/css-animations/test_animation-pausing.html b/dom/animation/test/css-animations/test_animation-pausing.html
new file mode 100644
index 0000000000..10be1ddf02
--- /dev/null
+++ b/dom/animation/test/css-animations/test_animation-pausing.html
@@ -0,0 +1,15 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+'use strict';
+setup({explicit_done: true});
+SpecialPowers.pushPrefEnv(
+ { "set": [["dom.animations-api.core.enabled", true]]},
+ function() {
+ window.open("file_animation-pausing.html");
+ });
+</script>
+</html>
diff --git a/dom/animation/test/css-animations/test_animation-playstate.html b/dom/animation/test/css-animations/test_animation-playstate.html
new file mode 100644
index 0000000000..54c8e1f104
--- /dev/null
+++ b/dom/animation/test/css-animations/test_animation-playstate.html
@@ -0,0 +1,15 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+'use strict';
+setup({explicit_done: true});
+SpecialPowers.pushPrefEnv(
+ { "set": [["dom.animations-api.core.enabled", true]]},
+ function() {
+ window.open("file_animation-playstate.html");
+ });
+</script>
+</html>
diff --git a/dom/animation/test/css-animations/test_animation-ready.html b/dom/animation/test/css-animations/test_animation-ready.html
new file mode 100644
index 0000000000..445f751b46
--- /dev/null
+++ b/dom/animation/test/css-animations/test_animation-ready.html
@@ -0,0 +1,15 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+'use strict';
+setup({explicit_done: true});
+SpecialPowers.pushPrefEnv(
+ { "set": [["dom.animations-api.core.enabled", true]]},
+ function() {
+ window.open("file_animation-ready.html");
+ });
+</script>
+</html>
diff --git a/dom/animation/test/css-animations/test_animation-reverse.html b/dom/animation/test/css-animations/test_animation-reverse.html
new file mode 100644
index 0000000000..673b1e0d3a
--- /dev/null
+++ b/dom/animation/test/css-animations/test_animation-reverse.html
@@ -0,0 +1,15 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+'use strict';
+setup({explicit_done: true});
+SpecialPowers.pushPrefEnv(
+ { "set": [["dom.animations-api.core.enabled", true]]},
+ function() {
+ window.open("file_animation-reverse.html");
+ });
+</script>
+</html>
diff --git a/dom/animation/test/css-animations/test_animation-starttime.html b/dom/animation/test/css-animations/test_animation-starttime.html
new file mode 100644
index 0000000000..dfae89ffa0
--- /dev/null
+++ b/dom/animation/test/css-animations/test_animation-starttime.html
@@ -0,0 +1,15 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+'use strict';
+setup({explicit_done: true});
+SpecialPowers.pushPrefEnv(
+ { "set": [["dom.animations-api.core.enabled", true]]},
+ function() {
+ window.open("file_animation-starttime.html");
+ });
+</script>
+</html>
diff --git a/dom/animation/test/css-animations/test_animations-dynamic-changes.html b/dom/animation/test/css-animations/test_animations-dynamic-changes.html
new file mode 100644
index 0000000000..ce4eb378dc
--- /dev/null
+++ b/dom/animation/test/css-animations/test_animations-dynamic-changes.html
@@ -0,0 +1,15 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+'use strict';
+setup({explicit_done: true});
+SpecialPowers.pushPrefEnv(
+ { "set": [["dom.animations-api.core.enabled", true]]},
+ function() {
+ window.open("file_animations-dynamic-changes.html");
+ });
+</script>
+</html>
diff --git a/dom/animation/test/css-animations/test_cssanimation-animationname.html b/dom/animation/test/css-animations/test_cssanimation-animationname.html
new file mode 100644
index 0000000000..ccddecc338
--- /dev/null
+++ b/dom/animation/test/css-animations/test_cssanimation-animationname.html
@@ -0,0 +1,15 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+'use strict';
+setup({explicit_done: true});
+SpecialPowers.pushPrefEnv(
+ { "set": [["dom.animations-api.core.enabled", true]]},
+ function() {
+ window.open("file_cssanimation-animationname.html");
+ });
+</script>
+</html>
diff --git a/dom/animation/test/css-animations/test_document-get-animations.html b/dom/animation/test/css-animations/test_document-get-animations.html
new file mode 100644
index 0000000000..dc964e62c8
--- /dev/null
+++ b/dom/animation/test/css-animations/test_document-get-animations.html
@@ -0,0 +1,15 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+'use strict';
+setup({explicit_done: true});
+SpecialPowers.pushPrefEnv(
+ { "set": [["dom.animations-api.core.enabled", true]]},
+ function() {
+ window.open("file_document-get-animations.html");
+ });
+</script>
+</html>
diff --git a/dom/animation/test/css-animations/test_effect-target.html b/dom/animation/test/css-animations/test_effect-target.html
new file mode 100644
index 0000000000..6c230c729f
--- /dev/null
+++ b/dom/animation/test/css-animations/test_effect-target.html
@@ -0,0 +1,15 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+'use strict';
+setup({explicit_done: true});
+SpecialPowers.pushPrefEnv(
+ { "set": [["dom.animations-api.core.enabled", true]]},
+ function() {
+ window.open("file_effect-target.html");
+ });
+</script>
+</html>
diff --git a/dom/animation/test/css-animations/test_element-get-animations.html b/dom/animation/test/css-animations/test_element-get-animations.html
new file mode 100644
index 0000000000..7b39e65ccb
--- /dev/null
+++ b/dom/animation/test/css-animations/test_element-get-animations.html
@@ -0,0 +1,15 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+'use strict';
+setup({explicit_done: true});
+SpecialPowers.pushPrefEnv(
+ { "set": [["dom.animations-api.core.enabled", true]]},
+ function() {
+ window.open("file_element-get-animations.html");
+ });
+</script>
+</html>
diff --git a/dom/animation/test/css-animations/test_keyframeeffect-getkeyframes.html b/dom/animation/test/css-animations/test_keyframeeffect-getkeyframes.html
new file mode 100644
index 0000000000..3cf2270084
--- /dev/null
+++ b/dom/animation/test/css-animations/test_keyframeeffect-getkeyframes.html
@@ -0,0 +1,15 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+'use strict';
+setup({explicit_done: true});
+SpecialPowers.pushPrefEnv(
+ { "set": [["dom.animations-api.core.enabled", true]]},
+ function() {
+ window.open("file_keyframeeffect-getkeyframes.html");
+ });
+</script>
+</html>
diff --git a/dom/animation/test/css-animations/test_pseudoElement-get-animations.html b/dom/animation/test/css-animations/test_pseudoElement-get-animations.html
new file mode 100644
index 0000000000..1e0dc5c825
--- /dev/null
+++ b/dom/animation/test/css-animations/test_pseudoElement-get-animations.html
@@ -0,0 +1,14 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+'use strict';
+setup({explicit_done: true});
+SpecialPowers.pushPrefEnv(
+ { "set": [["dom.animations-api.core.enabled", true]]},
+ function() {
+ window.open("file_pseudoElement-get-animations.html");
+ });
+</script>
diff --git a/dom/animation/test/css-transitions/file_animation-cancel.html b/dom/animation/test/css-transitions/file_animation-cancel.html
new file mode 100644
index 0000000000..6094b383f3
--- /dev/null
+++ b/dom/animation/test/css-transitions/file_animation-cancel.html
@@ -0,0 +1,165 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="../testcommon.js"></script>
+<body>
+<script>
+'use strict';
+
+promise_test(function(t) {
+ var div = addDiv(t, { style: 'margin-left: 0px' });
+ flushComputedStyle(div);
+
+ div.style.transition = 'margin-left 100s';
+ div.style.marginLeft = '1000px';
+ flushComputedStyle(div);
+
+ var animation = div.getAnimations()[0];
+ return animation.ready.then(waitForFrame).then(function() {
+ assert_not_equals(getComputedStyle(div).marginLeft, '1000px',
+ 'transform style is animated before cancelling');
+ animation.cancel();
+ assert_equals(getComputedStyle(div).marginLeft, div.style.marginLeft,
+ 'transform style is no longer animated after cancelling');
+ });
+}, 'Animated style is cleared after cancelling a running CSS transition');
+
+promise_test(function(t) {
+ var div = addDiv(t, { style: 'margin-left: 0px' });
+ flushComputedStyle(div);
+
+ div.style.transition = 'margin-left 100s';
+ div.style.marginLeft = '1000px';
+ flushComputedStyle(div);
+
+ div.addEventListener('transitionend', function() {
+ assert_unreached('Got unexpected end event on cancelled transition');
+ });
+
+ var animation = div.getAnimations()[0];
+ return animation.ready.then(function() {
+ // Seek to just before the end then cancel
+ animation.currentTime = 99.9 * 1000;
+ animation.cancel();
+
+ // Then wait a couple of frames and check that no event was dispatched
+ return waitForAnimationFrames(2);
+ });
+}, 'Cancelled CSS transitions do not dispatch events');
+
+promise_test(function(t) {
+ var div = addDiv(t, { style: 'margin-left: 0px' });
+ flushComputedStyle(div);
+
+ div.style.transition = 'margin-left 100s';
+ div.style.marginLeft = '1000px';
+ flushComputedStyle(div);
+
+ var animation = div.getAnimations()[0];
+ return animation.ready.then(function() {
+ animation.cancel();
+ assert_equals(getComputedStyle(div).marginLeft, '1000px',
+ 'margin-left style is not animated after cancelling');
+ animation.play();
+ assert_equals(getComputedStyle(div).marginLeft, '0px',
+ 'margin-left style is animated after re-starting transition');
+ return animation.ready;
+ }).then(function() {
+ assert_equals(animation.playState, 'running',
+ 'Transition succeeds in running after being re-started');
+ });
+}, 'After cancelling a transition, it can still be re-used');
+
+promise_test(function(t) {
+ var div = addDiv(t, { style: 'margin-left: 0px' });
+ flushComputedStyle(div);
+
+ div.style.transition = 'margin-left 100s';
+ div.style.marginLeft = '1000px';
+ flushComputedStyle(div);
+
+ var animation = div.getAnimations()[0];
+ return animation.ready.then(function() {
+ animation.finish();
+ animation.cancel();
+ assert_equals(getComputedStyle(div).marginLeft, '1000px',
+ 'margin-left style is not animated after cancelling');
+ animation.play();
+ assert_equals(getComputedStyle(div).marginLeft, '0px',
+ 'margin-left style is animated after re-starting transition');
+ return animation.ready;
+ }).then(function() {
+ assert_equals(animation.playState, 'running',
+ 'Transition succeeds in running after being re-started');
+ });
+}, 'After cancelling a finished transition, it can still be re-used');
+
+test(function(t) {
+ var div = addDiv(t, { style: 'margin-left: 0px' });
+ flushComputedStyle(div);
+
+ div.style.transition = 'margin-left 100s';
+ div.style.marginLeft = '1000px';
+ flushComputedStyle(div);
+
+ var animation = div.getAnimations()[0];
+ animation.cancel();
+ assert_equals(getComputedStyle(div).marginLeft, '1000px',
+ 'margin-left style is not animated after cancelling');
+
+ // Trigger a change to a transition property and check that this
+ // doesn't cause the animation to become live again
+ div.style.transitionDuration = '200s';
+ flushComputedStyle(div);
+ assert_equals(getComputedStyle(div).marginLeft, '1000px',
+ 'margin-left style is still not animated after updating'
+ + ' transition-duration');
+ assert_equals(animation.playState, 'idle',
+ 'Transition is still idle after updating transition-duration');
+}, 'After cancelling a transition, updating transition properties doesn\'t make'
+ + ' it live again');
+
+promise_test(function(t) {
+ var div = addDiv(t, { style: 'margin-left: 0px' });
+ flushComputedStyle(div);
+
+ div.style.transition = 'margin-left 100s';
+ div.style.marginLeft = '1000px';
+ flushComputedStyle(div);
+
+ var animation = div.getAnimations()[0];
+ return animation.ready.then(function() {
+ assert_equals(animation.playState, 'running');
+ div.style.display = 'none';
+ return waitForFrame();
+ }).then(function() {
+ assert_equals(animation.playState, 'idle');
+ assert_equals(getComputedStyle(div).marginLeft, '1000px');
+ });
+}, 'Setting display:none on an element cancels its transitions');
+
+promise_test(function(t) {
+ var parentDiv = addDiv(t);
+ var childDiv = document.createElement('div');
+ parentDiv.appendChild(childDiv);
+ childDiv.setAttribute('style', 'margin-left: 0px');
+
+ flushComputedStyle(childDiv);
+
+ childDiv.style.transition = 'margin-left 100s';
+ childDiv.style.marginLeft = '1000px';
+ flushComputedStyle(childDiv);
+
+ var animation = childDiv.getAnimations()[0];
+ return animation.ready.then(function() {
+ assert_equals(animation.playState, 'running');
+ parentDiv.style.display = 'none';
+ return waitForFrame();
+ }).then(function() {
+ assert_equals(animation.playState, 'idle');
+ assert_equals(getComputedStyle(childDiv).marginLeft, '1000px');
+ });
+}, 'Setting display:none cancels transitions on a child element');
+
+done();
+</script>
+</body>
diff --git a/dom/animation/test/css-transitions/file_animation-computed-timing.html b/dom/animation/test/css-transitions/file_animation-computed-timing.html
new file mode 100644
index 0000000000..2dac82d753
--- /dev/null
+++ b/dom/animation/test/css-transitions/file_animation-computed-timing.html
@@ -0,0 +1,315 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="../testcommon.js"></script>
+<style>
+
+.animated-div {
+ margin-left: 100px;
+}
+
+</style>
+<body>
+<script>
+
+'use strict';
+
+// --------------------
+// delay
+// --------------------
+test(function(t) {
+ var div = addDiv(t, {'class': 'animated-div'});
+ div.style.transition = 'margin-left 10s';
+ flushComputedStyle(div);
+ div.style.marginLeft = '10px';
+
+
+ var effect = div.getAnimations()[0].effect;
+ assert_equals(effect.getComputedTiming().delay, 0,
+ 'Initial value of delay');
+}, 'delay of a new tranisition');
+
+test(function(t) {
+ var div = addDiv(t, {'class': 'animated-div'});
+ div.style.transition = 'margin-left 10s 10s';
+ flushComputedStyle(div);
+ div.style.marginLeft = '10px';
+
+ var effect = div.getAnimations()[0].effect;
+ assert_equals(effect.getComputedTiming().delay, 10000,
+ 'Initial value of delay');
+}, 'Positive delay of a new transition');
+
+test(function(t) {
+ var div = addDiv(t, {'class': 'animated-div'});
+ div.style.transition = 'margin-left 10s -5s';
+ flushComputedStyle(div);
+ div.style.marginLeft = '10px';
+
+ var effect = div.getAnimations()[0].effect;
+ assert_equals(effect.getComputedTiming().delay, -5000,
+ 'Initial value of delay');
+}, 'Negative delay of a new transition');
+
+
+// --------------------
+// endDelay
+// --------------------
+test(function(t) {
+ var div = addDiv(t, {'class': 'animated-div'});
+ div.style.transition = 'margin-left 10s';
+ flushComputedStyle(div);
+ div.style.marginLeft = '10px';
+
+ var effect = div.getAnimations()[0].effect;
+ assert_equals(effect.getComputedTiming().endDelay, 0,
+ 'Initial value of endDelay');
+}, 'endDelay of a new transition');
+
+
+// --------------------
+// fill
+// --------------------
+test(function(t) {
+ var div = addDiv(t, {'class': 'animated-div'});
+ div.style.transition = 'margin-left 10s';
+ flushComputedStyle(div);
+ div.style.marginLeft = '10px';
+
+ var effect = div.getAnimations()[0].effect;
+ assert_equals(effect.getComputedTiming().fill, 'backwards',
+ 'Fill backwards');
+}, 'fill of a new transition');
+
+
+// --------------------
+// iterationStart
+// --------------------
+test(function(t) {
+ var div = addDiv(t, {'class': 'animated-div'});
+ div.style.transition = 'margin-left 10s';
+ flushComputedStyle(div);
+ div.style.marginLeft = '10px';
+
+ var effect = div.getAnimations()[0].effect;
+ assert_equals(effect.getComputedTiming().iterationStart, 0,
+ 'Initial value of iterationStart');
+}, 'iterationStart of a new transition');
+
+
+// --------------------
+// iterations
+// --------------------
+test(function(t) {
+ var div = addDiv(t, {'class': 'animated-div'});
+ div.style.transition = 'margin-left 10s';
+ flushComputedStyle(div);
+ div.style.marginLeft = '10px';
+
+ var effect = div.getAnimations()[0].effect;
+ assert_equals(effect.getComputedTiming().iterations, 1,
+ 'Initial value of iterations');
+}, 'iterations of a new transition');
+
+
+// --------------------
+// duration
+// --------------------
+test(function(t) {
+ var div = addDiv(t, {'class': 'animated-div'});
+ div.style.transition = 'margin-left 10s';
+ flushComputedStyle(div);
+ div.style.marginLeft = '10px';
+
+ var effect = div.getAnimations()[0].effect;
+ assert_equals(effect.getComputedTiming().duration, 10000,
+ 'Initial value of duration');
+}, 'duration of a new transition');
+
+
+// --------------------
+// direction
+// --------------------
+test(function(t) {
+ var div = addDiv(t, {'class': 'animated-div'});
+ div.style.transition = 'margin-left 10s';
+ flushComputedStyle(div);
+ div.style.marginLeft = '10px';
+
+ var effect = div.getAnimations()[0].effect;
+ assert_equals(effect.getComputedTiming().direction, 'normal',
+ 'Initial value of direction');
+}, 'direction of a new transition');
+
+
+// --------------------
+// easing
+// --------------------
+test(function(t) {
+ var div = addDiv(t, {'class': 'animated-div'});
+ div.style.transition = 'margin-left 10s';
+ flushComputedStyle(div);
+ div.style.marginLeft = '10px';
+
+ var effect = div.getAnimations()[0].effect;
+ assert_equals(effect.getComputedTiming().easing, 'linear',
+ 'Initial value of easing');
+}, 'easing of a new transition');
+
+
+// ------------------------------
+// endTime
+// = max(start delay + active duration + end delay, 0)
+// --------------------
+test(function(t) {
+ var div = addDiv(t, {'class': 'animated-div'});
+ div.style.transition = 'margin-left 100s -5s';
+ flushComputedStyle(div);
+ div.style.marginLeft = '10px';
+
+ var effect = div.getAnimations()[0].effect;
+ var answer = 100000 - 5000; // ms
+ assert_equals(effect.getComputedTiming().endTime, answer,
+ 'Initial value of endTime');
+}, 'endTime of a new transition');
+
+
+// --------------------
+// activeDuration
+// = iteration duration * iteration count(==1)
+// --------------------
+test(function(t) {
+ var div = addDiv(t, {'class': 'animated-div'});
+ div.style.transition = 'margin-left 100s -5s';
+ flushComputedStyle(div);
+ div.style.marginLeft = '10px';
+
+ var effect = div.getAnimations()[0].effect;
+ assert_equals(effect.getComputedTiming().activeDuration, 100000,
+ 'Initial value of activeDuration');
+}, 'activeDuration of a new transition');
+
+
+// --------------------
+// localTime
+// --------------------
+test(function(t) {
+ var div = addDiv(t, {'class': 'animated-div'});
+ div.style.transition = 'margin-left 100s';
+ flushComputedStyle(div);
+ div.style.marginLeft = '10px';
+
+ var effect = div.getAnimations()[0].effect;
+ assert_equals(effect.getComputedTiming().localTime, 0,
+ 'Initial value of localTime');
+}, 'localTime of a new transition');
+
+test(function(t) {
+ var div = addDiv(t, {'class': 'animated-div'});
+ div.style.transition = 'margin-left 100s';
+ flushComputedStyle(div);
+ div.style.marginLeft = '10px';
+
+ var anim = div.getAnimations()[0];
+ anim.currentTime = 5000;
+ assert_equals(anim.effect.getComputedTiming().localTime, anim.currentTime,
+ 'current localTime after setting currentTime');
+}, 'localTime is always equal to currentTime');
+
+async_test(function(t) {
+ var div = addDiv(t, {'class': 'animated-div'});
+ div.style.transition = 'margin-left 100s';
+ flushComputedStyle(div);
+ div.style.marginLeft = '10px';
+
+ var anim = div.getAnimations()[0];
+ anim.playbackRate = 2; // 2 times faster
+
+ anim.ready.then(t.step_func(function() {
+ assert_equals(anim.effect.getComputedTiming().localTime, anim.currentTime,
+ 'localTime is equal to currentTime');
+ return waitForFrame();
+ })).then(t.step_func_done(function() {
+ assert_equals(anim.effect.getComputedTiming().localTime, anim.currentTime,
+ 'localTime is equal to currentTime');
+ }));
+}, 'localTime reflects playbackRate immediately');
+
+
+// --------------------
+// progress
+// --------------------
+test(function(t) {
+ var div = addDiv(t, {'class': 'animated-div'});
+ div.style.transition = 'margin-left 10.5s';
+ flushComputedStyle(div);
+ div.style.marginLeft = '10px';
+
+ var effect = div.getAnimations()[0].effect;
+ assert_equals(effect.getComputedTiming().progress, 0.0,
+ 'Initial value of progress');
+}, 'progress of a new transition');
+
+test(function(t) {
+ var div = addDiv(t, {'class': 'animated-div'});
+ div.style.transition = 'margin-left 10.5s 2s';
+ flushComputedStyle(div);
+ div.style.marginLeft = '10px';
+
+ var effect = div.getAnimations()[0].effect;
+ assert_equals(effect.getComputedTiming().progress, 0.0,
+ 'Initial value of progress');
+}, 'progress of a new transition with positive delay in before phase');
+
+test(function(t) {
+ var div = addDiv(t, {'class': 'animated-div'});
+ div.style.transition = 'margin-left 10.5s';
+ flushComputedStyle(div);
+ div.style.marginLeft = '10px';
+
+ var anim = div.getAnimations()[0];
+ anim.finish()
+ assert_equals(anim.effect.getComputedTiming().progress, null,
+ 'finished progress');
+}, 'progress of a finished transition');
+
+
+// --------------------
+// currentIteration
+// --------------------
+test(function(t) {
+ var div = addDiv(t, {'class': 'animated-div'});
+ div.style.transition = 'margin-left 10s';
+ flushComputedStyle(div);
+ div.style.marginLeft = '10px';
+
+ var effect = div.getAnimations()[0].effect;
+ assert_equals(effect.getComputedTiming().currentIteration, 0,
+ 'Initial value of currentIteration');
+}, 'currentIteration of a new transition');
+
+test(function(t) {
+ var div = addDiv(t, {'class': 'animated-div'});
+ div.style.transition = 'margin-left 10s 2s';
+ flushComputedStyle(div);
+ div.style.marginLeft = '10px';
+
+ var effect = div.getAnimations()[0].effect;
+ assert_equals(effect.getComputedTiming().currentIteration, 0,
+ 'Initial value of currentIteration');
+}, 'currentIteration of a new transition with positive delay in before phase');
+
+test(function(t) {
+ var div = addDiv(t, {'class': 'animated-div'});
+ div.style.transition = 'margin-left 10s';
+ flushComputedStyle(div);
+ div.style.marginLeft = '10px';
+
+ var anim = div.getAnimations()[0];
+ anim.finish();
+ assert_equals(anim.effect.getComputedTiming().currentIteration, null,
+ 'finished currentIteration');
+}, 'currentIteration of a finished transition');
+
+done();
+</script>
+</body>
diff --git a/dom/animation/test/css-transitions/file_animation-currenttime.html b/dom/animation/test/css-transitions/file_animation-currenttime.html
new file mode 100644
index 0000000000..2a0f105d20
--- /dev/null
+++ b/dom/animation/test/css-transitions/file_animation-currenttime.html
@@ -0,0 +1,307 @@
+<!doctype html>
+<html>
+ <head>
+ <meta charset=utf-8>
+ <title>Tests for the effect of setting a CSS transition's
+ Animation.currentTime</title>
+ <style>
+
+.animated-div {
+ margin-left: 100px;
+ transition: margin-left 1000s linear 1000s;
+}
+
+ </style>
+ <script src="../testcommon.js"></script>
+ </head>
+ <body>
+ <script type="text/javascript">
+
+'use strict';
+
+// TODO: Once the computedTiming property is implemented, add checks to the
+// checker helpers to ensure that computedTiming's properties are updated as
+// expected.
+// See https://bugzilla.mozilla.org/show_bug.cgi?id=1108055
+
+
+const ANIM_DELAY_MS = 1000000; // 1000s
+const ANIM_DUR_MS = 1000000; // 1000s
+
+/**
+ * These helpers get the value that the currentTime needs to be set to, to put
+ * an animation that uses the above ANIM_DELAY_MS and ANIM_DUR_MS values into
+ * the middle of various phases or points through the active duration.
+ */
+function currentTimeForBeforePhase() {
+ return ANIM_DELAY_MS / 2;
+}
+function currentTimeForActivePhase() {
+ return ANIM_DELAY_MS + ANIM_DUR_MS / 2;
+}
+function currentTimeForAfterPhase() {
+ return ANIM_DELAY_MS + ANIM_DUR_MS + ANIM_DELAY_MS / 2;
+}
+function currentTimeForStartOfActiveInterval() {
+ return ANIM_DELAY_MS;
+}
+function currentTimeForFiftyPercentThroughActiveInterval() {
+ return ANIM_DELAY_MS + ANIM_DUR_MS * 0.5;
+}
+function currentTimeForEndOfActiveInterval() {
+ return ANIM_DELAY_MS + ANIM_DUR_MS;
+}
+
+
+// Expected computed 'margin-left' values at points during the active interval:
+// When we assert_between_inclusive using these values we could in theory cause
+// intermittent failure due to very long delays between paints, but since the
+// active duration is 1000s long, a delay would need to be around 100s to cause
+// that. If that's happening then there are likely other issues that should be
+// fixed, so a failure to make us look into that seems like a good thing.
+const INITIAL_POSITION = 100;
+const TEN_PCT_POSITION = 110;
+const FIFTY_PCT_POSITION = 150;
+const END_POSITION = 200;
+
+
+// The terms used for the naming of the following helper functions refer to
+// terms used in the Web Animations specification for specific phases of an
+// animation. The terms can be found here:
+//
+// http://w3c.github.io/web-animations/#animation-effect-phases-and-states
+
+// Called when currentTime is set to zero (the beginning of the start delay).
+function checkStateOnSettingCurrentTimeToZero(animation)
+{
+ // We don't test animation.currentTime since our caller just set it.
+
+ assert_equals(animation.playState, 'running',
+ 'Animation.playState should be "running" at the start of ' +
+ 'the start delay');
+
+ assert_equals(animation.effect.target.style.animationPlayState, 'running',
+ 'Animation.effect.target.style.animationPlayState should be ' +
+ '"running" at the start of the start delay');
+
+ var div = animation.effect.target;
+ var marginLeft = parseFloat(getComputedStyle(div).marginLeft);
+ assert_equals(marginLeft, UNANIMATED_POSITION,
+ 'the computed value of margin-left should be unaffected ' +
+ 'at the beginning of the start delay');
+}
+
+// Called when the ready Promise's callbacks should happen
+function checkStateOnReadyPromiseResolved(animation)
+{
+ // the 0.0001 here is for rounding error
+ assert_less_than_equal(animation.currentTime,
+ animation.timeline.currentTime - animation.startTime + 0.0001,
+ 'Animation.currentTime should be less than the local time ' +
+ 'equivalent of the timeline\'s currentTime on the first paint tick ' +
+ 'after animation creation');
+
+ assert_equals(animation.playState, 'running',
+ 'Animation.playState should be "running" on the first paint ' +
+ 'tick after animation creation');
+
+ var div = animation.effect.target;
+ var marginLeft = parseFloat(getComputedStyle(div).marginLeft);
+ assert_equals(marginLeft, INITIAL_POSITION,
+ 'the computed value of margin-left should be unaffected ' +
+ 'by an animation with a delay on ready Promise resolve');
+}
+
+// Called when currentTime is set to the time the active interval starts.
+function checkStateAtActiveIntervalStartTime(animation)
+{
+ // We don't test animation.currentTime since our caller just set it.
+
+ assert_equals(animation.playState, 'running',
+ 'Animation.playState should be "running" at the start of ' +
+ 'the active interval');
+
+ var div = animation.effect.target;
+ var marginLeft = parseFloat(getComputedStyle(div).marginLeft);
+ assert_between_inclusive(marginLeft, INITIAL_POSITION, TEN_PCT_POSITION,
+ 'the computed value of margin-left should be close to the value at the ' +
+ 'beginning of the animation');
+}
+
+function checkStateAtFiftyPctOfActiveInterval(animation)
+{
+ // We don't test animation.currentTime since our caller just set it.
+
+ var div = animation.effect.target;
+ var marginLeft = parseFloat(getComputedStyle(div).marginLeft);
+ assert_equals(marginLeft, FIFTY_PCT_POSITION,
+ 'the computed value of margin-left should be half way through the ' +
+ 'animation at the midpoint of the active interval');
+}
+
+// Called when currentTime is set to the time the active interval ends.
+function checkStateAtActiveIntervalEndTime(animation)
+{
+ // We don't test animation.currentTime since our caller just set it.
+
+ assert_equals(animation.playState, 'finished',
+ 'Animation.playState should be "finished" at the end of ' +
+ 'the active interval');
+
+ var div = animation.effect.target;
+ var marginLeft = parseFloat(getComputedStyle(div).marginLeft);
+ assert_equals(marginLeft, END_POSITION,
+ 'the computed value of margin-left should be the final transitioned-to ' +
+ 'value at the end of the active duration');
+}
+
+test(function(t)
+{
+ var div = addDiv(t, {'class': 'animated-div'});
+ flushComputedStyle(div);
+ div.style.marginLeft = '200px'; // initiate transition
+
+ var animation = div.getAnimations()[0];
+ assert_equals(animation.currentTime, 0, 'currentTime should be zero');
+}, 'currentTime of a newly created transition is zero');
+
+
+test(function(t)
+{
+ var div = addDiv(t, {'class': 'animated-div'});
+ flushComputedStyle(div);
+ div.style.marginLeft = '200px'; // initiate transition
+
+ var animation = div.getAnimations()[0];
+
+ // So that animation is running instead of paused when we set currentTime:
+ animation.startTime = animation.timeline.currentTime;
+
+ animation.currentTime = 10;
+ assert_equals(animation.currentTime, 10,
+ 'Check setting of currentTime actually works');
+}, 'Sanity test to check round-tripping assigning to new animation\'s ' +
+ 'currentTime');
+
+
+async_test(function(t) {
+ var div = addDiv(t, {'class': 'animated-div'});
+ var eventWatcher = new EventWatcher(t, div, 'transitionend');
+
+ flushComputedStyle(div);
+ div.style.marginLeft = '200px'; // initiate transition
+
+ var animation = div.getAnimations()[0];
+
+ animation.ready.then(t.step_func(function() {
+ checkStateOnReadyPromiseResolved(animation);
+
+ animation.currentTime = currentTimeForStartOfActiveInterval();
+ checkStateAtActiveIntervalStartTime(animation);
+
+ animation.currentTime = currentTimeForFiftyPercentThroughActiveInterval();
+ checkStateAtFiftyPctOfActiveInterval(animation);
+
+ animation.currentTime = currentTimeForEndOfActiveInterval();
+ return eventWatcher.wait_for('transitionend');
+ })).then(t.step_func(function() {
+ checkStateAtActiveIntervalEndTime(animation);
+ })).catch(t.step_func(function(reason) {
+ assert_unreached(reason);
+ })).then(function() {
+ t.done();
+ });
+}, 'Skipping forward through transition');
+
+
+test(function(t) {
+ var div = addDiv(t, {'class': 'animated-div'});
+ var eventWatcher = new EventWatcher(t, div, 'transitionend');
+
+ flushComputedStyle(div);
+ div.style.marginLeft = '200px'; // initiate transition
+
+ var animation = div.getAnimations()[0];
+
+ // Unlike in the case of CSS animations, we cannot skip to the end and skip
+ // backwards since when we reach the end the transition effect is removed and
+ // changes to the Animation object no longer affect the element. For
+ // this reason we only skip forwards as far as the 50% through point.
+
+ animation.ready.then(t.step_func(function() {
+ animation.currentTime = currentTimeForFiftyPercentThroughActiveInterval();
+ checkStateAtFiftyPctOfActiveInterval(animation);
+
+ animation.currentTime = currentTimeForStartOfActiveInterval();
+
+ // Despite going backwards from being in the active interval to being
+ // before it, we now expect a 'transitionend' event because the transition
+ // should go from being active to inactive.
+ //
+ // Calling checkStateAtActiveIntervalStartTime will check computed style,
+ // causing computed style to be updated and the 'transitionend' event to
+ // be dispatched synchronously. We need to call wait_for first
+ // otherwise eventWatcher will assert that the event was unexpected.
+ eventWatcher.wait_for('transitionend').then(function() {
+ t.done();
+ });
+ checkStateAtActiveIntervalStartTime(animation);
+ }));
+}, 'Skipping backwards through transition');
+
+
+async_test(function(t) {
+ var div = addDiv(t, {'class': 'animated-div'});
+ flushComputedStyle(div);
+ div.style.marginLeft = '200px'; // initiate transition
+
+ var animation = div.getAnimations()[0];
+
+ animation.ready.then(t.step_func(function() {
+ var exception;
+ try {
+ animation.currentTime = null;
+ } catch (e) {
+ exception = e;
+ }
+ assert_equals(exception.name, 'TypeError',
+ 'Expect TypeError exception on trying to set ' +
+ 'Animation.currentTime to null');
+ })).catch(t.step_func(function(reason) {
+ assert_unreached(reason);
+ })).then(function() {
+ t.done();
+ });
+}, 'Setting currentTime to null');
+
+
+async_test(function(t) {
+ var div = addDiv(t, {'class': 'animated-div'});
+ flushComputedStyle(div);
+ div.style.marginLeft = '200px'; // initiate transition
+
+ var animation = div.getAnimations()[0];
+ var pauseTime;
+
+ animation.ready.then(t.step_func(function() {
+ assert_not_equals(animation.currentTime, null,
+ 'Animation.currentTime not null on ready Promise resolve');
+ animation.pause();
+ return animation.ready;
+ })).then(t.step_func(function() {
+ pauseTime = animation.currentTime;
+ return waitForFrame();
+ })).then(t.step_func(function() {
+ assert_equals(animation.currentTime, pauseTime,
+ 'Animation.currentTime is unchanged after pausing');
+ })).catch(t.step_func(function(reason) {
+ assert_unreached(reason);
+ })).then(function() {
+ t.done();
+ });
+}, 'Animation.currentTime after pausing');
+
+done();
+ </script>
+ </body>
+</html>
diff --git a/dom/animation/test/css-transitions/file_animation-finished.html b/dom/animation/test/css-transitions/file_animation-finished.html
new file mode 100644
index 0000000000..2f6bcf47d8
--- /dev/null
+++ b/dom/animation/test/css-transitions/file_animation-finished.html
@@ -0,0 +1,61 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="../testcommon.js"></script>
+<style>
+
+.animated-div {
+ margin-left: 100px;
+ transition: margin-left 1000s linear 1000s;
+}
+
+</style>
+<body>
+<script>
+
+'use strict';
+
+const ANIM_DELAY_MS = 1000000; // 1000s
+const ANIM_DUR_MS = 1000000; // 1000s
+
+async_test(function(t) {
+ var div = addDiv(t, {'class': 'animated-div'});
+ flushComputedStyle(div);
+ div.style.marginLeft = '200px'; // initiate transition
+
+ var animation = div.getAnimations()[0];
+
+ animation.finish();
+
+ animation.finished.then(t.step_func(function() {
+ animation.play();
+ assert_equals(animation.currentTime, 0,
+ 'Replaying a finished transition should reset its ' +
+ 'currentTime');
+ t.done();
+ }));
+}, 'Test restarting a finished transition');
+
+async_test(function(t) {
+ var div = addDiv(t, {'class': 'animated-div'});
+ flushComputedStyle(div);
+ div.style.marginLeft = '200px'; // initiate transition
+
+ var animation = div.getAnimations()[0];
+
+ animation.ready.then(function() {
+ animation.playbackRate = -1;
+ return animation.finished;
+ }).then(t.step_func(function() {
+ animation.play();
+ // FIXME: once animation.effect.computedTiming.endTime is available (bug
+ // 1108055) we should use that here.
+ assert_equals(animation.currentTime, ANIM_DELAY_MS + ANIM_DUR_MS,
+ 'Replaying a finished reversed transition should reset ' +
+ 'its currentTime to the end of the effect');
+ t.done();
+ }));
+}, 'Test restarting a reversed finished transition');
+
+done();
+</script>
+</body>
diff --git a/dom/animation/test/css-transitions/file_animation-pausing.html b/dom/animation/test/css-transitions/file_animation-pausing.html
new file mode 100644
index 0000000000..b2f2d46188
--- /dev/null
+++ b/dom/animation/test/css-transitions/file_animation-pausing.html
@@ -0,0 +1,50 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="../testcommon.js"></script>
+<body>
+<script>
+'use strict';
+
+function getMarginLeft(cs) {
+ return parseFloat(cs.marginLeft);
+}
+
+async_test(function(t) {
+ var div = addDiv(t);
+ var cs = window.getComputedStyle(div);
+
+ div.style.marginLeft = '0px';
+ cs.marginLeft; // Flush style to set up transition start point
+ div.style.transition = 'margin-left 100s';
+ div.style.marginLeft = '10000px';
+ cs.marginLeft;
+
+ var animation = div.getAnimations()[0];
+ assert_equals(getMarginLeft(cs), 0,
+ 'Initial value of margin-left is zero');
+ var previousAnimVal = getMarginLeft(cs);
+
+ animation.ready.then(waitForFrame).then(t.step_func(function() {
+ assert_true(getMarginLeft(cs) > previousAnimVal,
+ 'margin-left is initially increasing');
+ animation.pause();
+ return animation.ready;
+ })).then(t.step_func(function() {
+ previousAnimVal = getMarginLeft(cs);
+ return waitForFrame();
+ })).then(t.step_func(function() {
+ assert_equals(getMarginLeft(cs), previousAnimVal,
+ 'margin-left does not increase after calling pause()');
+ previousAnimVal = getMarginLeft(cs);
+ animation.play();
+ return animation.ready.then(waitForFrame);
+ })).then(t.step_func(function() {
+ assert_true(getMarginLeft(cs) > previousAnimVal,
+ 'margin-left increases after calling play()');
+ t.done();
+ }));
+}, 'pause() and play() a transition');
+
+done();
+</script>
+</body>
diff --git a/dom/animation/test/css-transitions/file_animation-ready.html b/dom/animation/test/css-transitions/file_animation-ready.html
new file mode 100644
index 0000000000..f141da7960
--- /dev/null
+++ b/dom/animation/test/css-transitions/file_animation-ready.html
@@ -0,0 +1,96 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="../testcommon.js"></script>
+<body>
+<script>
+'use strict';
+
+async_test(function(t) {
+ var div = addDiv(t);
+ div.style.transform = 'translate(0px)';
+ window.getComputedStyle(div).transform;
+ div.style.transition = 'transform 100s';
+ div.style.transform = 'translate(10px)';
+ window.getComputedStyle(div).transform;
+
+ var animation = div.getAnimations()[0];
+ var originalReadyPromise = animation.ready;
+
+ animation.ready.then(t.step_func(function() {
+ assert_equals(animation.ready, originalReadyPromise,
+ 'Ready promise is the same object when playing completes');
+ animation.pause();
+ assert_not_equals(animation.ready, originalReadyPromise,
+ 'Ready promise object identity differs when pausing');
+ t.done();
+ }));
+}, 'A new ready promise is created each time play() is called'
+ + ' the animation property');
+
+async_test(function(t) {
+ var div = addDiv(t);
+
+ // Set up pending transition
+ div.style.transform = 'translate(0px)';
+ window.getComputedStyle(div).transform;
+ div.style.transition = 'transform 100s';
+ div.style.transform = 'translate(10px)';
+ window.getComputedStyle(div).transform;
+
+ var animation = div.getAnimations()[0];
+ assert_equals(animation.playState, 'pending', 'Animation is initially pending');
+
+ // Set up listeners on ready promise
+ animation.ready.then(t.step_func(function() {
+ assert_unreached('ready promise was fulfilled');
+ })).catch(t.step_func(function(err) {
+ assert_equals(err.name, 'AbortError',
+ 'ready promise is rejected with AbortError');
+ assert_equals(animation.playState, 'idle',
+ 'Animation is idle after transition was cancelled');
+ })).then(t.step_func(function() {
+ t.done();
+ }));
+
+ // Now remove transform from transition-property and flush styles
+ div.style.transitionProperty = 'none';
+ window.getComputedStyle(div).transitionProperty;
+
+}, 'ready promise is rejected when a transition is cancelled by updating'
+ + ' transition-property');
+
+async_test(function(t) {
+ var div = addDiv(t);
+
+ // Set up pending transition
+ div.style.marginLeft = '0px';
+ window.getComputedStyle(div).marginLeft;
+ div.style.transition = 'margin-left 100s';
+ div.style.marginLeft = '100px';
+ window.getComputedStyle(div).marginLeft;
+
+ var animation = div.getAnimations()[0];
+ assert_equals(animation.playState, 'pending', 'Animation is initially pending');
+
+ // Set up listeners on ready promise
+ animation.ready.then(t.step_func(function() {
+ assert_unreached('ready promise was fulfilled');
+ })).catch(t.step_func(function(err) {
+ assert_equals(err.name, 'AbortError',
+ 'ready promise is rejected with AbortError');
+ assert_equals(animation.playState, 'idle',
+ 'Animation is idle after transition was cancelled');
+ })).then(t.step_func(function() {
+ t.done();
+ }));
+
+ // Now update the transition to animate to something not-interpolable
+ div.style.marginLeft = 'auto';
+ window.getComputedStyle(div).marginLeft;
+
+}, 'ready promise is rejected when a transition is cancelled by changing'
+ + ' the transition property to something not interpolable');
+
+done();
+</script>
+</body>
diff --git a/dom/animation/test/css-transitions/file_animation-starttime.html b/dom/animation/test/css-transitions/file_animation-starttime.html
new file mode 100644
index 0000000000..a156ba0a08
--- /dev/null
+++ b/dom/animation/test/css-transitions/file_animation-starttime.html
@@ -0,0 +1,284 @@
+<!doctype html>
+<html>
+ <head>
+ <meta charset=utf-8>
+ <title>Tests for the effect of setting a CSS transition's
+ Animation.startTime</title>
+ <style>
+
+.animated-div {
+ margin-left: 100px;
+ transition: margin-left 1000s linear 1000s;
+}
+
+ </style>
+ <script src="../testcommon.js"></script>
+ </head>
+ <body>
+ <script type="text/javascript">
+
+'use strict';
+
+// TODO: Once the computedTiming property is implemented, add checks to the
+// checker helpers to ensure that computedTiming's properties are updated as
+// expected.
+// See https://bugzilla.mozilla.org/show_bug.cgi?id=1108055
+
+
+const ANIM_DELAY_MS = 1000000; // 1000s
+const ANIM_DUR_MS = 1000000; // 1000s
+
+/**
+ * These helpers get the value that the startTime needs to be set to, to put an
+ * animation that uses the above ANIM_DELAY_MS and ANIM_DUR_MS values into the
+ * middle of various phases or points through the active duration.
+ */
+function startTimeForBeforePhase(timeline) {
+ return timeline.currentTime - ANIM_DELAY_MS / 2;
+}
+function startTimeForActivePhase(timeline) {
+ return timeline.currentTime - ANIM_DELAY_MS - ANIM_DUR_MS / 2;
+}
+function startTimeForAfterPhase(timeline) {
+ return timeline.currentTime - ANIM_DELAY_MS - ANIM_DUR_MS - ANIM_DELAY_MS / 2;
+}
+function startTimeForStartOfActiveInterval(timeline) {
+ return timeline.currentTime - ANIM_DELAY_MS;
+}
+function startTimeForFiftyPercentThroughActiveInterval(timeline) {
+ return timeline.currentTime - ANIM_DELAY_MS - ANIM_DUR_MS * 0.5;
+}
+function startTimeForEndOfActiveInterval(timeline) {
+ return timeline.currentTime - ANIM_DELAY_MS - ANIM_DUR_MS;
+}
+
+
+// Expected computed 'margin-left' values at points during the active interval:
+// When we assert_between_inclusive using these values we could in theory cause
+// intermittent failure due to very long delays between paints, but since the
+// active duration is 1000s long, a delay would need to be around 100s to cause
+// that. If that's happening then there are likely other issues that should be
+// fixed, so a failure to make us look into that seems like a good thing.
+const INITIAL_POSITION = 100;
+const TEN_PCT_POSITION = 110;
+const FIFTY_PCT_POSITION = 150;
+const END_POSITION = 200;
+
+// The terms used for the naming of the following helper functions refer to
+// terms used in the Web Animations specification for specific phases of an
+// animation. The terms can be found here:
+//
+// https://w3c.github.io/web-animations/#animation-effect-phases-and-states
+//
+// Note the distinction between the "animation start time" which occurs before
+// the start delay and the start of the active interval which occurs after it.
+
+// Called when the ready Promise's callbacks should happen
+function checkStateOnReadyPromiseResolved(animation)
+{
+ assert_less_than_equal(animation.startTime, animation.timeline.currentTime,
+ 'Animation.startTime should be less than the timeline\'s ' +
+ 'currentTime on the first paint tick after animation creation');
+
+ assert_equals(animation.playState, 'running',
+ 'Animation.playState should be "running" on the first paint ' +
+ 'tick after animation creation');
+
+ var div = animation.effect.target;
+ var marginLeft = parseFloat(getComputedStyle(div).marginLeft);
+ assert_equals(marginLeft, INITIAL_POSITION,
+ 'the computed value of margin-left should be unaffected ' +
+ 'by an animation with a delay on ready Promise resolve');
+}
+
+// Called when startTime is set to the time the active interval starts.
+function checkStateAtActiveIntervalStartTime(animation)
+{
+ // We don't test animation.startTime since our caller just set it.
+
+ assert_equals(animation.playState, 'running',
+ 'Animation.playState should be "running" at the start of ' +
+ 'the active interval');
+
+ var div = animation.effect.target;
+ var marginLeft = parseFloat(getComputedStyle(div).marginLeft);
+ assert_between_inclusive(marginLeft, INITIAL_POSITION, TEN_PCT_POSITION,
+ 'the computed value of margin-left should be close to the value at the ' +
+ 'beginning of the animation');
+}
+
+function checkStateAtFiftyPctOfActiveInterval(animation)
+{
+ // We don't test animation.startTime since our caller just set it.
+
+ var div = animation.effect.target;
+ var marginLeft = parseFloat(getComputedStyle(div).marginLeft);
+ assert_equals(marginLeft, FIFTY_PCT_POSITION,
+ 'the computed value of margin-left should be half way through the ' +
+ 'animation at the midpoint of the active interval');
+}
+
+// Called when startTime is set to the time the active interval ends.
+function checkStateAtActiveIntervalEndTime(animation)
+{
+ // We don't test animation.startTime since our caller just set it.
+
+ assert_equals(animation.playState, 'finished',
+ 'Animation.playState should be "finished" at the end of ' +
+ 'the active interval');
+
+ var div = animation.effect.target;
+ var marginLeft = parseFloat(getComputedStyle(div).marginLeft);
+ assert_equals(marginLeft, END_POSITION,
+ 'the computed value of margin-left should be the final transitioned-to ' +
+ 'value at the end of the active duration');
+}
+
+test(function(t)
+{
+ var div = addDiv(t, {'class': 'animated-div'});
+ flushComputedStyle(div);
+ div.style.marginLeft = '200px'; // initiate transition
+
+ var animation = div.getAnimations()[0];
+ assert_equals(animation.startTime, null, 'startTime is unresolved');
+}, 'startTime of a newly created transition is unresolved');
+
+
+test(function(t)
+{
+ var div = addDiv(t, {'class': 'animated-div'});
+ flushComputedStyle(div);
+ div.style.marginLeft = '200px'; // initiate transition
+
+ var animation = div.getAnimations()[0];
+ var currentTime = animation.timeline.currentTime;
+ animation.startTime = currentTime;
+ assert_approx_equals(animation.startTime, currentTime, 0.0001, // rounding error
+ 'Check setting of startTime actually works');
+}, 'Sanity test to check round-tripping assigning to new animation\'s ' +
+ 'startTime');
+
+
+async_test(function(t) {
+ var div = addDiv(t, {'class': 'animated-div'});
+ var eventWatcher = new EventWatcher(t, div, 'transitionend');
+
+ flushComputedStyle(div);
+ div.style.marginLeft = '200px'; // initiate transition
+
+ var animation = div.getAnimations()[0];
+
+ animation.ready.then(t.step_func(function() {
+ checkStateOnReadyPromiseResolved(animation);
+
+ animation.startTime = startTimeForStartOfActiveInterval(animation.timeline);
+ checkStateAtActiveIntervalStartTime(animation);
+
+ animation.startTime =
+ startTimeForFiftyPercentThroughActiveInterval(animation.timeline);
+ checkStateAtFiftyPctOfActiveInterval(animation);
+
+ animation.startTime = startTimeForEndOfActiveInterval(animation.timeline);
+ return eventWatcher.wait_for('transitionend');
+ })).then(t.step_func(function() {
+ checkStateAtActiveIntervalEndTime(animation);
+ })).catch(t.step_func(function(reason) {
+ assert_unreached(reason);
+ })).then(function() {
+ t.done();
+ });
+}, 'Skipping forward through animation');
+
+
+test(function(t) {
+ var div = addDiv(t, {'class': 'animated-div'});
+ var eventWatcher = new EventWatcher(t, div, 'transitionend');
+
+ flushComputedStyle(div);
+ div.style.marginLeft = '200px'; // initiate transition
+
+ var animation = div.getAnimations()[0];
+
+ // Unlike in the case of CSS animations, we cannot skip to the end and skip
+ // backwards since when we reach the end the transition effect is removed and
+ // changes to the Animation object no longer affect the element. For
+ // this reason we only skip forwards as far as the 90% through point.
+
+ animation.startTime =
+ startTimeForFiftyPercentThroughActiveInterval(animation.timeline);
+ checkStateAtFiftyPctOfActiveInterval(animation);
+
+ animation.startTime = startTimeForStartOfActiveInterval(animation.timeline);
+
+ // Despite going backwards from being in the active interval to being before
+ // it, we now expect an 'animationend' event because the animation should go
+ // from being active to inactive.
+ //
+ // Calling checkStateAtActiveIntervalStartTime will check computed style,
+ // causing computed style to be updated and the 'transitionend' event to
+ // be dispatched synchronously. We need to call waitForEvent first
+ // otherwise eventWatcher will assert that the event was unexpected.
+ eventWatcher.wait_for('transitionend').then(function() {
+ t.done();
+ });
+ checkStateAtActiveIntervalStartTime(animation);
+}, 'Skipping backwards through transition');
+
+
+async_test(function(t) {
+ var div = addDiv(t, {'class': 'animated-div'});
+
+ flushComputedStyle(div);
+ div.style.marginLeft = '200px'; // initiate transition
+
+ var animation = div.getAnimations()[0];
+
+ var storedCurrentTime;
+
+ animation.ready.then(t.step_func(function() {
+ storedCurrentTime = animation.currentTime;
+ animation.startTime = null;
+ return animation.ready;
+ })).catch(t.step_func(function(reason) {
+ assert_unreached(reason);
+ })).then(t.step_func(function() {
+ assert_equals(animation.currentTime, storedCurrentTime,
+ 'Test that hold time is correct');
+ t.done();
+ }));
+}, 'Setting startTime to null');
+
+
+async_test(function(t) {
+ var div = addDiv(t, {'class': 'animated-div'});
+
+ flushComputedStyle(div);
+ div.style.marginLeft = '200px'; // initiate transition
+
+ var animation = div.getAnimations()[0];
+
+ animation.ready.then(t.step_func(function() {
+ var savedStartTime = animation.startTime;
+
+ assert_not_equals(animation.startTime, null,
+ 'Animation.startTime not null on ready Promise resolve');
+
+ animation.pause();
+ return animation.ready;
+ })).then(t.step_func(function() {
+ assert_equals(animation.startTime, null,
+ 'Animation.startTime is null after paused');
+ assert_equals(animation.playState, 'paused',
+ 'Animation.playState is "paused" after pause() call');
+ })).catch(t.step_func(function(reason) {
+ assert_unreached(reason);
+ })).then(function() {
+ t.done();
+ });
+}, 'Animation.startTime after paused');
+
+done();
+ </script>
+ </body>
+</html>
diff --git a/dom/animation/test/css-transitions/file_csstransition-events.html b/dom/animation/test/css-transitions/file_csstransition-events.html
new file mode 100644
index 0000000000..5011bc1309
--- /dev/null
+++ b/dom/animation/test/css-transitions/file_csstransition-events.html
@@ -0,0 +1,223 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>Tests for CSS-Transition events</title>
+<link rel="help" href="https://drafts.csswg.org/css-transitions-2/#transition-events">
+<script src="../testcommon.js"></script>
+<body>
+<script>
+'use strict';
+
+/**
+ * Helper class to record the elapsedTime member of each event.
+ * The EventWatcher class in testharness.js allows us to wait on
+ * multiple events in a certain order but only records the event
+ * parameters of the most recent event.
+ */
+function TransitionEventHandler(target) {
+ this.target = target;
+ this.target.ontransitionrun = function(evt) {
+ this.transitionrun = evt.elapsedTime;
+ }.bind(this);
+ this.target.ontransitionstart = function(evt) {
+ this.transitionstart = evt.elapsedTime;
+ }.bind(this);
+ this.target.ontransitionend = function(evt) {
+ this.transitionend = evt.elapsedTime;
+ }.bind(this);
+}
+
+TransitionEventHandler.prototype.clear = function() {
+ this.transitionrun = undefined;
+ this.transitionstart = undefined;
+ this.transitionend = undefined;
+};
+
+function setupTransition(t, transitionStyle) {
+ var div, watcher, handler, transition;
+ transitionStyle = transitionStyle || 'transition: margin-left 100s 100s';
+ div = addDiv(t, { style: transitionStyle });
+ watcher = new EventWatcher(t, div, [ 'transitionrun',
+ 'transitionstart',
+ 'transitionend' ]);
+ handler = new TransitionEventHandler(div);
+ flushComputedStyle(div);
+
+ div.style.marginLeft = '100px';
+ flushComputedStyle(div);
+
+ transition = div.getAnimations()[0];
+
+ return [transition, watcher, handler];
+}
+
+// On the next frame (i.e. when events are queued), whether or not the
+// transition is still pending depends on the implementation.
+promise_test(function(t) {
+ var [transition, watcher, handler] = setupTransition(t);
+ return watcher.wait_for('transitionrun').then(function(evt) {
+ assert_equals(evt.elapsedTime, 0.0);
+ });
+}, 'Idle -> Pending or Before');
+
+promise_test(function(t) {
+ var [transition, watcher, handler] = setupTransition(t);
+ // Force the transition to leave the idle phase
+ transition.startTime = document.timeline.currentTime;
+ return watcher.wait_for('transitionrun').then(function(evt) {
+ assert_equals(evt.elapsedTime, 0.0);
+ });
+}, 'Idle -> Before');
+
+promise_test(function(t) {
+ var [transition, watcher, handler] = setupTransition(t);
+ // Seek to Active phase.
+ transition.currentTime = 100 * MS_PER_SEC;
+ transition.pause();
+ return watcher.wait_for([ 'transitionrun',
+ 'transitionstart' ]).then(function(evt) {
+ assert_equals(handler.transitionrun, 0.0);
+ assert_equals(handler.transitionstart, 0.0);
+ });
+}, 'Idle or Pending -> Active');
+
+promise_test(function(t) {
+ var [transition, watcher, handler] = setupTransition(t);
+ // Seek to After phase.
+ transition.finish();
+ return watcher.wait_for([ 'transitionrun',
+ 'transitionstart',
+ 'transitionend' ]).then(function(evt) {
+ assert_equals(handler.transitionrun, 0.0);
+ assert_equals(handler.transitionstart, 0.0);
+ assert_equals(handler.transitionend, 100.0);
+ });
+}, 'Idle or Pending -> After');
+
+promise_test(function(t) {
+ var [transition, watcher, handler] = setupTransition(t);
+
+ return Promise.all([ watcher.wait_for('transitionrun'),
+ transition.ready ]).then(function() {
+ transition.currentTime = 100 * MS_PER_SEC;
+ return watcher.wait_for('transitionstart');
+ }).then(function() {
+ assert_equals(handler.transitionstart, 0.0);
+ });
+}, 'Before -> Active');
+
+promise_test(function(t) {
+ var [transition, watcher, handler] = setupTransition(t);
+ return Promise.all([ watcher.wait_for('transitionrun'),
+ transition.ready ]).then(function() {
+ // Seek to After phase.
+ transition.currentTime = 200 * MS_PER_SEC;
+ return watcher.wait_for([ 'transitionstart', 'transitionend' ]);
+ }).then(function(evt) {
+ assert_equals(handler.transitionstart, 0.0);
+ assert_equals(handler.transitionend, 100.0);
+ });
+}, 'Before -> After');
+
+promise_test(function(t) {
+ var [transition, watcher, handler] = setupTransition(t);
+ // Seek to Active phase.
+ transition.currentTime = 100 * MS_PER_SEC;
+ return watcher.wait_for([ 'transitionrun',
+ 'transitionstart' ]).then(function(evt) {
+ // Seek to Before phase.
+ transition.currentTime = 0;
+ return watcher.wait_for('transitionend');
+ }).then(function(evt) {
+ assert_equals(evt.elapsedTime, 0.0);
+ });
+}, 'Active -> Before');
+
+promise_test(function(t) {
+ var [transition, watcher, handler] = setupTransition(t);
+ // Seek to Active phase.
+ transition.currentTime = 100 * MS_PER_SEC;
+ return watcher.wait_for([ 'transitionrun',
+ 'transitionstart' ]).then(function(evt) {
+ // Seek to After phase.
+ transition.currentTime = 200 * MS_PER_SEC;
+ return watcher.wait_for('transitionend');
+ }).then(function(evt) {
+ assert_equals(evt.elapsedTime, 100.0);
+ });
+}, 'Active -> After');
+
+promise_test(function(t) {
+ var [transition, watcher, handler] = setupTransition(t);
+ // Seek to After phase.
+ transition.finish();
+ return watcher.wait_for([ 'transitionrun',
+ 'transitionstart',
+ 'transitionend' ]).then(function(evt) {
+ // Seek to Before phase.
+ transition.currentTime = 0;
+ return watcher.wait_for([ 'transitionstart', 'transitionend' ]);
+ }).then(function(evt) {
+ assert_equals(handler.transitionstart, 100.0);
+ assert_equals(handler.transitionend, 0.0);
+ });
+}, 'After -> Before');
+
+promise_test(function(t) {
+ var [transition, watcher, handler] = setupTransition(t);
+ // Seek to After phase.
+ transition.finish();
+ return watcher.wait_for([ 'transitionrun',
+ 'transitionstart',
+ 'transitionend' ]).then(function(evt) {
+ // Seek to Active phase.
+ transition.currentTime = 100 * MS_PER_SEC;
+ return watcher.wait_for('transitionstart');
+ }).then(function(evt) {
+ assert_equals(evt.elapsedTime, 100.0);
+ });
+}, 'After -> Active');
+
+promise_test(function(t) {
+ var [transition, watcher, handler] =
+ setupTransition(t, 'transition: margin-left 100s -50s');
+
+ return watcher.wait_for([ 'transitionrun',
+ 'transitionstart' ]).then(function() {
+ assert_equals(handler.transitionrun, 50.0);
+ assert_equals(handler.transitionstart, 50.0);
+ transition.finish();
+ return watcher.wait_for('transitionend');
+ }).then(function(evt) {
+ assert_equals(evt.elapsedTime, 100.0);
+ });
+}, 'Calculating the interval start and end time with negative start delay.');
+
+promise_test(function(t) {
+ var [transition, watcher, handler] = setupTransition(t);
+
+ return watcher.wait_for('transitionrun').then(function(evt) {
+ // We can't set the end delay via generated effect timing.
+ // Because CSS-Transition use the AnimationEffectTimingReadOnly.
+ transition.effect = new KeyframeEffect(handler.target,
+ { marginleft: [ '0px', '100px' ]},
+ { duration: 100 * MS_PER_SEC,
+ endDelay: -50 * MS_PER_SEC });
+ // Seek to Before and play.
+ transition.cancel();
+ transition.play();
+ return watcher.wait_for('transitionstart');
+ }).then(function() {
+ assert_equals(handler.transitionstart, 0.0);
+
+ // Seek to After phase.
+ transition.finish();
+ return watcher.wait_for('transitionend');
+ }).then(function(evt) {
+ assert_equals(evt.elapsedTime, 50.0);
+ });
+}, 'Calculating the interval start and end time with negative end delay.');
+
+done();
+</script>
+</body>
+</html>
diff --git a/dom/animation/test/css-transitions/file_csstransition-transitionproperty.html b/dom/animation/test/css-transitions/file_csstransition-transitionproperty.html
new file mode 100644
index 0000000000..176cc5a4df
--- /dev/null
+++ b/dom/animation/test/css-transitions/file_csstransition-transitionproperty.html
@@ -0,0 +1,24 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="../testcommon.js"></script>
+<body>
+<script>
+'use strict';
+
+test(function(t) {
+ var div = addDiv(t);
+
+ // Add a transition
+ div.style.left = '0px';
+ window.getComputedStyle(div).transitionProperty;
+ div.style.transition = 'all 100s';
+ div.style.left = '100px';
+
+ assert_equals(div.getAnimations()[0].transitionProperty, 'left',
+ 'The transitionProperty for the corresponds to the specific ' +
+ 'property being transitioned');
+}, 'CSSTransition.transitionProperty');
+
+done();
+</script>
+</body>
diff --git a/dom/animation/test/css-transitions/file_document-get-animations.html b/dom/animation/test/css-transitions/file_document-get-animations.html
new file mode 100644
index 0000000000..a5d55b76cd
--- /dev/null
+++ b/dom/animation/test/css-transitions/file_document-get-animations.html
@@ -0,0 +1,93 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="../testcommon.js"></script>
+<body>
+<script>
+'use strict';
+
+test(function(t) {
+ assert_equals(document.getAnimations().length, 0,
+ 'getAnimations returns an empty sequence for a document'
+ + ' with no animations');
+}, 'getAnimations for non-animated content');
+
+test(function(t) {
+ var div = addDiv(t);
+
+ // Add a couple of transitions
+ div.style.left = '0px';
+ div.style.top = '0px';
+ getComputedStyle(div).transitionProperty;
+
+ div.style.transition = 'all 100s';
+ div.style.left = '100px';
+ div.style.top = '100px';
+ assert_equals(document.getAnimations().length, 2,
+ 'getAnimations returns two running CSS Transitions');
+
+ // Remove both
+ div.style.transitionProperty = 'none';
+ assert_equals(document.getAnimations().length, 0,
+ 'getAnimations returns no running CSS Transitions');
+}, 'getAnimations for CSS Transitions');
+
+test(function(t) {
+ addStyle(t, { '.init::after': 'content: ""; width: 0px; ' +
+ 'transition: all 100s;',
+ '.init::before': 'content: ""; width: 0px; ' +
+ 'transition: all 10s;',
+ '.change::after': 'width: 100px;',
+ '.change::before': 'width: 100px;' });
+ // create two divs with these arrangement:
+ // parent
+ // ::before,
+ // ::after
+ // |
+ // child
+ var parent = addDiv(t);
+ var child = addDiv(t);
+ parent.appendChild(child);
+
+ parent.style.left = '0px';
+ parent.style.transition = 'left 10s';
+ parent.classList.add('init');
+ child.style.left = '0px';
+ child.style.transition = 'left 10s';
+ flushComputedStyle(parent);
+
+ parent.style.left = '100px';
+ parent.classList.add('change');
+ child.style.left = '100px';
+
+ var anims = document.getAnimations();
+ assert_equals(anims.length, 4,
+ 'CSS transition on both pseudo-elements and elements ' +
+ 'are returned');
+ assert_equals(anims[0].effect.target, parent,
+ 'The animation targeting the parent element comes first');
+ assert_equals(anims[1].effect.target.type, '::before',
+ 'The animation targeting the ::before element comes second');
+ assert_equals(anims[2].effect.target.type, '::after',
+ 'The animation targeting the ::after element comes third');
+ assert_equals(anims[3].effect.target, child,
+ 'The animation targeting the child element comes last');
+}, 'CSS Transitions targetting (pseudo-)elements should have correct order ' +
+ 'after sorting');
+
+async_test(function(t) {
+ var div = addDiv(t, { style: 'left: 0px; transition: all 50ms' });
+ flushComputedStyle(div);
+
+ div.style.left = '100px';
+ var animations = div.getAnimations();
+ assert_equals(animations.length, 1, 'Got transition');
+ animations[0].finished.then(t.step_func(function() {
+ assert_equals(document.getAnimations().length, 0,
+ 'No animations returned');
+ t.done();
+ }));
+}, 'Transitions are not returned after they have finished');
+
+done();
+</script>
+</body>
diff --git a/dom/animation/test/css-transitions/file_effect-target.html b/dom/animation/test/css-transitions/file_effect-target.html
new file mode 100644
index 0000000000..0f67b0b9aa
--- /dev/null
+++ b/dom/animation/test/css-transitions/file_effect-target.html
@@ -0,0 +1,66 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="../testcommon.js"></script>
+<body>
+<script>
+'use strict';
+
+test(function(t) {
+ var div = addDiv(t);
+
+ div.style.left = '0px';
+ window.getComputedStyle(div).transitionProperty;
+ div.style.transition = 'left 100s';
+ div.style.left = '100px';
+
+ var animation = div.getAnimations()[0];
+ assert_equals(animation.effect.target, div,
+ 'Animation.target is the animatable div');
+}, 'Returned CSS transitions have the correct Animation.target');
+
+test(function(t) {
+ addStyle(t, { '.init::after': 'content: ""; width: 0px; height: 0px; ' +
+ 'transition: all 10s;',
+ '.change::after': 'width: 100px; height: 100px;' });
+ var div = addDiv(t, { class: 'init' });
+ flushComputedStyle(div);
+ div.classList.add('change');
+
+ var anims = document.getAnimations();
+ assert_equals(anims.length, 2,
+ 'Got transitions running on ::after pseudo element');
+ assert_equals(anims[0].effect.target, anims[1].effect.target,
+ 'Both transitions return the same target object');
+}, 'effect.target should return the same CSSPseudoElement object each time');
+
+test(function(t) {
+ addStyle(t, { '.init::after': 'content: ""; width: 0px; transition: all 10s;',
+ '.change::after': 'width: 100px;' });
+ var div = addDiv(t, { class: 'init' });
+ flushComputedStyle(div);
+ div.classList.add('change');
+ var pseudoTarget = document.getAnimations()[0].effect.target;
+ var effect = new KeyframeEffectReadOnly(pseudoTarget,
+ { background: ["blue", "red"] },
+ 3000);
+ var newAnim = new Animation(effect, document.timeline);
+ newAnim.play();
+
+ var anims = document.getAnimations();
+ assert_equals(anims.length, 2,
+ 'Got animations running on ::after pseudo element');
+ assert_not_equals(anims[0], newAnim,
+ 'The scriped-generated animation appears last');
+ assert_equals(newAnim.effect.target, pseudoTarget,
+ 'The effect.target of the scripted-generated animation is ' +
+ 'the same as the one from the argument of ' +
+ 'KeyframeEffectReadOnly constructor');
+ assert_equals(anims[0].effect.target, newAnim.effect.target,
+ 'Both the transition and the scripted-generated animation ' +
+ 'return the same target object');
+}, 'effect.target from the script-generated animation should return the same ' +
+ 'CSSPseudoElement object as that from the CSS generated transition');
+
+done();
+</script>
+</body>
diff --git a/dom/animation/test/css-transitions/file_element-get-animations.html b/dom/animation/test/css-transitions/file_element-get-animations.html
new file mode 100644
index 0000000000..0ce145da04
--- /dev/null
+++ b/dom/animation/test/css-transitions/file_element-get-animations.html
@@ -0,0 +1,147 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="../testcommon.js"></script>
+<body>
+<script>
+'use strict';
+
+async_test(function(t) {
+ var div = addDiv(t);
+
+ // FIXME: This test does too many things. It should be split up.
+
+ // Add a couple of transitions
+ div.style.left = '0px';
+ div.style.top = '0px';
+ window.getComputedStyle(div).transitionProperty;
+
+ div.style.transition = 'all 100s';
+ div.style.left = '100px';
+ div.style.top = '100px';
+
+ var animations = div.getAnimations();
+ assert_equals(animations.length, 2,
+ 'getAnimations() returns one Animation per transitioning property');
+ waitForAllAnimations(animations).then(t.step_func(function() {
+ var startTime = animations[0].startTime;
+ assert_true(startTime > 0 && startTime <= document.timeline.currentTime,
+ 'CSS transitions have sensible start times');
+ assert_equals(animations[0].startTime, animations[1].startTime,
+ 'CSS transitions started together have the same start time');
+ // Wait a moment then add a third transition
+ return waitForFrame();
+ })).then(t.step_func(function() {
+ div.style.backgroundColor = 'green';
+ animations = div.getAnimations();
+ assert_equals(animations.length, 3,
+ 'getAnimations returns Animations for all running CSS Transitions');
+ return waitForAllAnimations(animations);
+ })).then(t.step_func(function() {
+ assert_less_than(animations[1].startTime, animations[2].startTime,
+ 'Animation for additional CSS transition starts after the original'
+ + ' transitions and appears later in the list');
+ t.done();
+ }));
+}, 'getAnimations for CSS Transitions');
+
+test(function(t) {
+ var div = addDiv(t, { style: 'left: 0px; transition: all 100s' });
+
+ flushComputedStyle(div);
+ div.style.left = '100px';
+
+ assert_class_string(div.getAnimations()[0], 'CSSTransition',
+ 'Interface of returned animation is CSSTransition');
+}, 'getAnimations returns CSSTransition objects for CSS Transitions');
+
+async_test(function(t) {
+ var div = addDiv(t);
+
+ // Set up event listener
+ div.addEventListener('transitionend', t.step_func(function() {
+ assert_equals(div.getAnimations().length, 0,
+ 'getAnimations does not return finished CSS Transitions');
+ t.done();
+ }));
+
+ // Add a very short transition
+ div.style.left = '0px';
+ window.getComputedStyle(div).left;
+
+ div.style.transition = 'all 0.01s';
+ div.style.left = '100px';
+ window.getComputedStyle(div).left;
+}, 'getAnimations for CSS Transitions that have finished');
+
+test(function(t) {
+ var div = addDiv(t);
+
+ // Try to transition non-animatable property animation-duration
+ div.style.animationDuration = '10s';
+ window.getComputedStyle(div).animationDuration;
+
+ div.style.transition = 'all 100s';
+ div.style.animationDuration = '100s';
+
+ assert_equals(div.getAnimations().length, 0,
+ 'getAnimations returns an empty sequence for a transition'
+ + ' of a non-animatable property');
+}, 'getAnimations for transition on non-animatable property');
+
+test(function(t) {
+ var div = addDiv(t);
+
+ div.style.setProperty('-vendor-unsupported', '0px', '');
+ window.getComputedStyle(div).transitionProperty;
+ div.style.transition = 'all 100s';
+ div.style.setProperty('-vendor-unsupported', '100px', '');
+
+ assert_equals(div.getAnimations().length, 0,
+ 'getAnimations returns an empty sequence for a transition'
+ + ' of an unsupported property');
+}, 'getAnimations for transition on unsupported property');
+
+test(function(t) {
+ var div = addDiv(t, { style: 'transform: translate(0px); ' +
+ 'opacity: 0; ' +
+ 'border-width: 0px; ' + // Shorthand
+ 'border-style: solid' });
+ getComputedStyle(div).transform;
+
+ div.style.transition = 'all 100s';
+ div.style.transform = 'translate(100px)';
+ div.style.opacity = '1';
+ div.style.borderWidth = '1px';
+
+ var animations = div.getAnimations();
+ assert_equals(animations.length, 6,
+ 'Generated expected number of transitions');
+ assert_equals(animations[0].transitionProperty, 'border-bottom-width');
+ assert_equals(animations[1].transitionProperty, 'border-left-width');
+ assert_equals(animations[2].transitionProperty, 'border-right-width');
+ assert_equals(animations[3].transitionProperty, 'border-top-width');
+ assert_equals(animations[4].transitionProperty, 'opacity');
+ assert_equals(animations[5].transitionProperty, 'transform');
+}, 'getAnimations sorts simultaneous transitions by name');
+
+test(function(t) {
+ var div = addDiv(t, { style: 'transform: translate(0px); ' +
+ 'opacity: 0' });
+ getComputedStyle(div).transform;
+
+ div.style.transition = 'all 100s';
+ div.style.transform = 'translate(100px)';
+ assert_equals(div.getAnimations().length, 1,
+ 'Initially there is only one (transform) transition');
+ div.style.opacity = '1';
+ assert_equals(div.getAnimations().length, 2,
+ 'Then a second (opacity) transition is added');
+
+ var animations = div.getAnimations();
+ assert_equals(animations[0].transitionProperty, 'transform');
+ assert_equals(animations[1].transitionProperty, 'opacity');
+}, 'getAnimations sorts transitions by when they were generated');
+
+done();
+</script>
+</body>
diff --git a/dom/animation/test/css-transitions/file_keyframeeffect-getkeyframes.html b/dom/animation/test/css-transitions/file_keyframeeffect-getkeyframes.html
new file mode 100644
index 0000000000..7bbf76fa7a
--- /dev/null
+++ b/dom/animation/test/css-transitions/file_keyframeeffect-getkeyframes.html
@@ -0,0 +1,95 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="../testcommon.js"></script>
+<style>
+:root {
+ --var-100px: 100px;
+}
+</style>
+<body>
+<script>
+'use strict';
+
+function getKeyframes(e) {
+ return e.getAnimations()[0].effect.getKeyframes();
+}
+
+function assert_frames_equal(a, b, name) {
+ assert_equals(Object.keys(a).sort().toString(),
+ Object.keys(b).sort().toString(),
+ "properties on " + name);
+ for (var p in a) {
+ assert_equals(a[p], b[p], "value for '" + p + "' on " + name);
+ }
+}
+
+test(function(t) {
+ var div = addDiv(t);
+
+ div.style.left = '0px';
+ window.getComputedStyle(div).transitionProperty;
+ div.style.transition = 'left 100s';
+ div.style.left = '100px';
+
+ var frames = getKeyframes(div);
+
+ assert_equals(frames.length, 2, "number of frames");
+
+ var expected = [
+ { offset: 0, computedOffset: 0, easing: "ease", left: "0px" },
+ { offset: 1, computedOffset: 1, easing: "linear", left: "100px" },
+ ];
+
+ for (var i = 0; i < frames.length; i++) {
+ assert_frames_equal(frames[i], expected[i], "ComputedKeyframe #" + i);
+ }
+}, 'KeyframeEffectReadOnly.getKeyframes() returns expected frames for a simple'
+ + ' transition');
+
+test(function(t) {
+ var div = addDiv(t);
+
+ div.style.left = '0px';
+ window.getComputedStyle(div).transitionProperty;
+ div.style.transition = 'left 100s steps(2,end)';
+ div.style.left = '100px';
+
+ var frames = getKeyframes(div);
+
+ assert_equals(frames.length, 2, "number of frames");
+
+ var expected = [
+ { offset: 0, computedOffset: 0, easing: "steps(2)", left: "0px" },
+ { offset: 1, computedOffset: 1, easing: "linear", left: "100px" },
+ ];
+
+ for (var i = 0; i < frames.length; i++) {
+ assert_frames_equal(frames[i], expected[i], "ComputedKeyframe #" + i);
+ }
+}, 'KeyframeEffectReadOnly.getKeyframes() returns expected frames for a simple'
+ + ' transition with a non-default easing function');
+
+test(function(t) {
+ var div = addDiv(t);
+ div.style.left = '0px';
+ window.getComputedStyle(div).transitionProperty;
+ div.style.transition = 'left 100s';
+ div.style.left = 'var(--var-100px)';
+
+ var frames = getKeyframes(div);
+
+ // CSS transition endpoints are based on the computed value so we
+ // shouldn't see the variable reference
+ var expected = [
+ { offset: 0, computedOffset: 0, easing: 'ease', left: '0px' },
+ { offset: 1, computedOffset: 1, easing: 'linear', left: '100px' },
+ ];
+ for (var i = 0; i < frames.length; i++) {
+ assert_frames_equal(frames[i], expected[i], "ComputedKeyframe #" + i);
+ }
+}, 'KeyframeEffectReadOnly.getKeyframes() returns expected frames for a'
+ + ' transition with a CSS variable endpoint');
+
+done();
+</script>
+</body>
diff --git a/dom/animation/test/css-transitions/file_pseudoElement-get-animations.html b/dom/animation/test/css-transitions/file_pseudoElement-get-animations.html
new file mode 100644
index 0000000000..5683a14a14
--- /dev/null
+++ b/dom/animation/test/css-transitions/file_pseudoElement-get-animations.html
@@ -0,0 +1,45 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="../testcommon.js"></script>
+<style>
+.init::before {
+ content: '';
+ height: 0px;
+ width: 0px;
+ opacity: 0;
+ transition: all 100s;
+}
+.change::before {
+ height: 100px;
+ width: 100px;
+ opacity: 1;
+}
+</style>
+<body>
+<script>
+'use strict';
+
+test(function(t) {
+ var div = addDiv(t, { class: 'init' });
+ flushComputedStyle(div);
+ div.classList.add('change');
+
+ // Sanity checks
+ assert_equals(document.getAnimations().length, 3,
+ 'Got expected number of animations on document');
+ var pseudoTarget = document.getAnimations()[0].effect.target;
+ assert_class_string(pseudoTarget, 'CSSPseudoElement',
+ 'Got pseudo-element target');
+
+ // Check animations returned from the pseudo element are in correct order
+ var anims = pseudoTarget.getAnimations();
+ assert_equals(anims.length, 3,
+ 'Got expected number of animations on pseudo-element');
+ assert_equals(anims[0].transitionProperty, 'height');
+ assert_equals(anims[1].transitionProperty, 'opacity');
+ assert_equals(anims[2].transitionProperty, 'width');
+}, 'getAnimations sorts simultaneous transitions by name');
+
+done();
+</script>
+</body>
diff --git a/dom/animation/test/css-transitions/file_setting-effect.html b/dom/animation/test/css-transitions/file_setting-effect.html
new file mode 100644
index 0000000000..c61877194e
--- /dev/null
+++ b/dom/animation/test/css-transitions/file_setting-effect.html
@@ -0,0 +1,91 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src='../testcommon.js'></script>
+<body>
+<script>
+'use strict';
+
+promise_test(function(t) {
+ var div = addDiv(t);
+ div.style.left = '0px';
+
+ div.style.transition = 'left 100s';
+ flushComputedStyle(div);
+ div.style.left = '100px';
+
+ var transition = div.getAnimations()[0];
+ return transition.ready.then(function() {
+ transition.currentTime = 50 * MS_PER_SEC;
+ transition.effect = null;
+ assert_equals(transition.transitionProperty, 'left');
+ assert_equals(transition.playState, 'finished');
+ assert_equals(window.getComputedStyle(div).left, '100px');
+ });
+}, 'Test for removing a transition effect');
+
+promise_test(function(t) {
+ var div = addDiv(t);
+ div.style.left = '0px';
+
+ div.style.transition = 'left 100s';
+ flushComputedStyle(div);
+ div.style.left = '100px';
+
+ var transition = div.getAnimations()[0];
+ return transition.ready.then(function() {
+ transition.currentTime = 50 * MS_PER_SEC;
+ transition.effect = new KeyframeEffect(div,
+ { marginLeft: [ '0px' , '100px'] },
+ 100 * MS_PER_SEC);
+ assert_equals(transition.transitionProperty, 'left');
+ assert_equals(transition.playState, 'running');
+ assert_equals(window.getComputedStyle(div).left, '100px');
+ assert_equals(window.getComputedStyle(div).marginLeft, '50px');
+ });
+}, 'Test for replacing the transition effect by a new keyframe effect');
+
+promise_test(function(t) {
+ var div = addDiv(t);
+ div.style.left = '0px';
+ div.style.width = '0px';
+
+ div.style.transition = 'left 100s';
+ flushComputedStyle(div);
+ div.style.left = '100px';
+
+ var transition = div.getAnimations()[0];
+ return transition.ready.then(function() {
+ transition.currentTime = 50 * MS_PER_SEC;
+ transition.effect = new KeyframeEffect(div,
+ { marginLeft: [ '0px' , '100px'] },
+ 20 * MS_PER_SEC);
+ assert_equals(transition.playState, 'finished');
+ });
+}, 'Test for setting a new keyframe effect with a shorter duration');
+
+promise_test(function(t) {
+ var div = addDiv(t);
+ div.style.left = '0px';
+ div.style.width = '0px';
+
+ div.style.transition = 'left 100s';
+ flushComputedStyle(div);
+ div.style.left = '100px';
+
+ var transition = div.getAnimations()[0];
+ assert_equals(transition.playState, 'pending');
+
+ transition.effect = new KeyframeEffect(div,
+ { marginLeft: [ '0px' , '100px'] },
+ 100 * MS_PER_SEC);
+ assert_equals(transition.transitionProperty, 'left');
+ assert_equals(transition.playState, 'pending');
+
+ return transition.ready.then(function() {
+ assert_equals(transition.playState, 'running');
+ });
+}, 'Test for setting a new keyframe effect to a pending transition');
+
+done();
+</script>
+</body>
diff --git a/dom/animation/test/css-transitions/test_animation-cancel.html b/dom/animation/test/css-transitions/test_animation-cancel.html
new file mode 100644
index 0000000000..949e0843e4
--- /dev/null
+++ b/dom/animation/test/css-transitions/test_animation-cancel.html
@@ -0,0 +1,14 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+'use strict';
+setup({explicit_done: true});
+SpecialPowers.pushPrefEnv(
+ { "set": [["dom.animations-api.core.enabled", true]]},
+ function() {
+ window.open("file_animation-cancel.html");
+ });
+</script>
diff --git a/dom/animation/test/css-transitions/test_animation-computed-timing.html b/dom/animation/test/css-transitions/test_animation-computed-timing.html
new file mode 100644
index 0000000000..c1b40aaf36
--- /dev/null
+++ b/dom/animation/test/css-transitions/test_animation-computed-timing.html
@@ -0,0 +1,16 @@
+<!doctype html>
+<html>
+<meta charset=utf-8>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+'use strict';
+setup({explicit_done: true});
+SpecialPowers.pushPrefEnv(
+ { "set": [["dom.animations-api.core.enabled", true]]},
+ function() {
+ window.open("file_animation-computed-timing.html");
+ });
+</script>
+</html>
diff --git a/dom/animation/test/css-transitions/test_animation-currenttime.html b/dom/animation/test/css-transitions/test_animation-currenttime.html
new file mode 100644
index 0000000000..30b0ed030c
--- /dev/null
+++ b/dom/animation/test/css-transitions/test_animation-currenttime.html
@@ -0,0 +1,14 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+'use strict';
+setup({explicit_done: true});
+SpecialPowers.pushPrefEnv(
+ { "set": [["dom.animations-api.core.enabled", true]]},
+ function() {
+ window.open("file_animation-currenttime.html");
+ });
+</script>
diff --git a/dom/animation/test/css-transitions/test_animation-finished.html b/dom/animation/test/css-transitions/test_animation-finished.html
new file mode 100644
index 0000000000..f2ed7f80b4
--- /dev/null
+++ b/dom/animation/test/css-transitions/test_animation-finished.html
@@ -0,0 +1,14 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+'use strict';
+setup({explicit_done: true});
+SpecialPowers.pushPrefEnv(
+ { "set": [["dom.animations-api.core.enabled", true]]},
+ function() {
+ window.open("file_animation-finished.html");
+ });
+</script>
diff --git a/dom/animation/test/css-transitions/test_animation-pausing.html b/dom/animation/test/css-transitions/test_animation-pausing.html
new file mode 100644
index 0000000000..67484a2a53
--- /dev/null
+++ b/dom/animation/test/css-transitions/test_animation-pausing.html
@@ -0,0 +1,14 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+'use strict';
+setup({explicit_done: true});
+SpecialPowers.pushPrefEnv(
+ { "set": [["dom.animations-api.core.enabled", true]]},
+ function() {
+ window.open("file_animation-pausing.html");
+ });
+</script>
diff --git a/dom/animation/test/css-transitions/test_animation-ready.html b/dom/animation/test/css-transitions/test_animation-ready.html
new file mode 100644
index 0000000000..a928ded641
--- /dev/null
+++ b/dom/animation/test/css-transitions/test_animation-ready.html
@@ -0,0 +1,14 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+'use strict';
+setup({explicit_done: true});
+SpecialPowers.pushPrefEnv(
+ { "set": [["dom.animations-api.core.enabled", true]]},
+ function() {
+ window.open("file_animation-ready.html");
+ });
+</script>
diff --git a/dom/animation/test/css-transitions/test_animation-starttime.html b/dom/animation/test/css-transitions/test_animation-starttime.html
new file mode 100644
index 0000000000..8a8c85f2dd
--- /dev/null
+++ b/dom/animation/test/css-transitions/test_animation-starttime.html
@@ -0,0 +1,14 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+'use strict';
+setup({explicit_done: true});
+SpecialPowers.pushPrefEnv(
+ { "set": [["dom.animations-api.core.enabled", true]]},
+ function() {
+ window.open("file_animation-starttime.html");
+ });
+</script>
diff --git a/dom/animation/test/css-transitions/test_csstransition-events.html b/dom/animation/test/css-transitions/test_csstransition-events.html
new file mode 100644
index 0000000000..92559ad679
--- /dev/null
+++ b/dom/animation/test/css-transitions/test_csstransition-events.html
@@ -0,0 +1,14 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+'use strict';
+setup({explicit_done: true});
+SpecialPowers.pushPrefEnv(
+ { "set": [["dom.animations-api.core.enabled", true]]},
+ function() {
+ window.open("file_csstransition-events.html");
+ });
+</script>
diff --git a/dom/animation/test/css-transitions/test_csstransition-transitionproperty.html b/dom/animation/test/css-transitions/test_csstransition-transitionproperty.html
new file mode 100644
index 0000000000..0aa1912d96
--- /dev/null
+++ b/dom/animation/test/css-transitions/test_csstransition-transitionproperty.html
@@ -0,0 +1,14 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+'use strict';
+setup({explicit_done: true});
+SpecialPowers.pushPrefEnv(
+ { "set": [["dom.animations-api.core.enabled", true]]},
+ function() {
+ window.open("file_csstransition-transitionproperty.html");
+ });
+</script>
diff --git a/dom/animation/test/css-transitions/test_document-get-animations.html b/dom/animation/test/css-transitions/test_document-get-animations.html
new file mode 100644
index 0000000000..dc964e62c8
--- /dev/null
+++ b/dom/animation/test/css-transitions/test_document-get-animations.html
@@ -0,0 +1,15 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+'use strict';
+setup({explicit_done: true});
+SpecialPowers.pushPrefEnv(
+ { "set": [["dom.animations-api.core.enabled", true]]},
+ function() {
+ window.open("file_document-get-animations.html");
+ });
+</script>
+</html>
diff --git a/dom/animation/test/css-transitions/test_effect-target.html b/dom/animation/test/css-transitions/test_effect-target.html
new file mode 100644
index 0000000000..f3ae72229c
--- /dev/null
+++ b/dom/animation/test/css-transitions/test_effect-target.html
@@ -0,0 +1,14 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+'use strict';
+setup({explicit_done: true});
+SpecialPowers.pushPrefEnv(
+ { "set": [["dom.animations-api.core.enabled", true]]},
+ function() {
+ window.open("file_effect-target.html");
+ });
+</script>
diff --git a/dom/animation/test/css-transitions/test_element-get-animations.html b/dom/animation/test/css-transitions/test_element-get-animations.html
new file mode 100644
index 0000000000..87abdfa730
--- /dev/null
+++ b/dom/animation/test/css-transitions/test_element-get-animations.html
@@ -0,0 +1,14 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+'use strict';
+setup({explicit_done: true});
+SpecialPowers.pushPrefEnv(
+ { "set": [["dom.animations-api.core.enabled", true]]},
+ function() {
+ window.open("file_element-get-animations.html");
+ });
+</script>
diff --git a/dom/animation/test/css-transitions/test_keyframeeffect-getkeyframes.html b/dom/animation/test/css-transitions/test_keyframeeffect-getkeyframes.html
new file mode 100644
index 0000000000..dcc54255d5
--- /dev/null
+++ b/dom/animation/test/css-transitions/test_keyframeeffect-getkeyframes.html
@@ -0,0 +1,14 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+'use strict';
+setup({explicit_done: true});
+SpecialPowers.pushPrefEnv(
+ { "set": [["dom.animations-api.core.enabled", true]]},
+ function() {
+ window.open("file_keyframeeffect-getkeyframes.html");
+ });
+</script>
diff --git a/dom/animation/test/css-transitions/test_pseudoElement-get-animations.html b/dom/animation/test/css-transitions/test_pseudoElement-get-animations.html
new file mode 100644
index 0000000000..1e0dc5c825
--- /dev/null
+++ b/dom/animation/test/css-transitions/test_pseudoElement-get-animations.html
@@ -0,0 +1,14 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+'use strict';
+setup({explicit_done: true});
+SpecialPowers.pushPrefEnv(
+ { "set": [["dom.animations-api.core.enabled", true]]},
+ function() {
+ window.open("file_pseudoElement-get-animations.html");
+ });
+</script>
diff --git a/dom/animation/test/css-transitions/test_setting-effect.html b/dom/animation/test/css-transitions/test_setting-effect.html
new file mode 100644
index 0000000000..a9654ec556
--- /dev/null
+++ b/dom/animation/test/css-transitions/test_setting-effect.html
@@ -0,0 +1,14 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src='/resources/testharness.js'></script>
+<script src='/resources/testharnessreport.js'></script>
+<div id='log'></div>
+<script>
+'use strict';
+setup({explicit_done: true});
+SpecialPowers.pushPrefEnv(
+ { 'set': [['dom.animations-api.core.enabled', true]]},
+ function() {
+ window.open('file_setting-effect.html');
+ });
+</script>
diff --git a/dom/animation/test/document-timeline/file_document-timeline.html b/dom/animation/test/document-timeline/file_document-timeline.html
new file mode 100644
index 0000000000..7ec48e2915
--- /dev/null
+++ b/dom/animation/test/document-timeline/file_document-timeline.html
@@ -0,0 +1,135 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>Web Animations API: DocumentTimeline tests</title>
+<script src="../testcommon.js"></script>
+<iframe src="data:text/html;charset=utf-8," width="10" height="10" id="iframe"></iframe>
+<iframe src="data:text/html;charset=utf-8,%3Chtml%20style%3D%22display%3Anone%22%3E%3C%2Fhtml%3E" width="10" height="10" id="hidden-iframe"></iframe>
+<script>
+'use strict';
+
+test(function() {
+ assert_equals(document.timeline, document.timeline,
+ 'document.timeline returns the same object every time');
+ var iframe = document.getElementById('iframe');
+ assert_not_equals(document.timeline, iframe.contentDocument.timeline,
+ 'document.timeline returns a different object for each document');
+ assert_not_equals(iframe.contentDocument.timeline, null,
+ 'document.timeline on an iframe is not null');
+},
+'document.timeline identity tests',
+{
+ help: 'http://dev.w3.org/fxtf/web-animations/#the-document-timeline',
+ assert: [ 'Each document has a timeline called the document timeline' ],
+ author: 'Brian Birtles'
+});
+
+async_test(function(t) {
+ assert_true(document.timeline.currentTime > 0,
+ 'document.timeline.currentTime is positive');
+ // document.timeline.currentTime should be set even before document
+ // load fires. We expect this code to be run before document load and hence
+ // the above assertion is sufficient.
+ // If the following assertion fails, this test needs to be redesigned.
+ assert_true(document.readyState !== 'complete',
+ 'Test is running prior to document load');
+
+ // Test that the document timeline's current time is measured from
+ // navigationStart.
+ //
+ // We can't just compare document.timeline.currentTime to
+ // window.performance.now() because currentTime is only updated on a sample
+ // so we use requestAnimationFrame instead.
+ window.requestAnimationFrame(t.step_func(function(rafTime) {
+ assert_equals(document.timeline.currentTime, rafTime,
+ 'document.timeline.currentTime matches' +
+ ' requestAnimationFrame time');
+ t.done();
+ }));
+},
+'document.timeline.currentTime value tests',
+{
+ help: [
+ 'http://dev.w3.org/fxtf/web-animations/#the-global-clock',
+ 'http://dev.w3.org/fxtf/web-animations/#the-document-timeline'
+ ],
+ assert: [
+ 'The global clock is a source of monotonically increasing time values',
+ 'The time values of the document timeline are calculated as a fixed' +
+ ' offset from the global clock',
+ 'the zero time corresponds to the navigationStart moment',
+ 'the time value of each document timeline must be equal to the time ' +
+ 'passed to animation frame request callbacks for that browsing context'
+ ],
+ author: 'Brian Birtles'
+});
+
+async_test(function(t) {
+ var valueAtStart = document.timeline.currentTime;
+ var timeAtStart = window.performance.now();
+ while (window.performance.now() - timeAtStart < 100) {
+ // Wait 100ms
+ }
+ assert_equals(document.timeline.currentTime, valueAtStart,
+ 'document.timeline.currentTime does not change within a script block');
+ window.requestAnimationFrame(t.step_func(function() {
+ assert_true(document.timeline.currentTime > valueAtStart,
+ 'document.timeline.currentTime increases between script blocks');
+ t.done();
+ }));
+},
+'document.timeline.currentTime liveness tests',
+{
+ help: 'http://dev.w3.org/fxtf/web-animations/#script-execution-and-live-updates-to-the-model',
+ assert: [ 'The value returned by the currentTime attribute of a' +
+ ' document timeline will not change within a script block' ],
+ author: 'Brian Birtles'
+});
+
+test(function() {
+ var hiddenIFrame = document.getElementById('hidden-iframe');
+ assert_equals(typeof hiddenIFrame.contentDocument.timeline.currentTime,
+ 'number',
+ 'currentTime of an initially hidden subframe\'s timeline is a number');
+ assert_true(hiddenIFrame.contentDocument.timeline.currentTime >= 0,
+ 'currentTime of an initially hidden subframe\'s timeline is >= 0');
+}, 'document.timeline.currentTime hidden subframe test');
+
+async_test(function(t) {
+ var hiddenIFrame = document.getElementById('hidden-iframe');
+
+ // Don't run the test until after the iframe has completed loading or else the
+ // contentDocument may change.
+ var testToRunOnLoad = t.step_func(function() {
+ // Remove display:none
+ hiddenIFrame.style.display = 'block';
+ window.getComputedStyle(hiddenIFrame).display;
+
+ window.requestAnimationFrame(t.step_func(function() {
+ assert_true(hiddenIFrame.contentDocument.timeline.currentTime > 0,
+ 'document.timeline.currentTime is positive after removing'
+ + ' display:none');
+ var previousValue = hiddenIFrame.contentDocument.timeline.currentTime;
+
+ // Re-introduce display:none
+ hiddenIFrame.style.display = 'none';
+ window.getComputedStyle(hiddenIFrame).display;
+
+ window.requestAnimationFrame(t.step_func(function() {
+ assert_true(
+ hiddenIFrame.contentDocument.timeline.currentTime >= previousValue,
+ 'document.timeline.currentTime does not go backwards after'
+ + ' re-setting display:none');
+ t.done();
+ }));
+ }));
+ });
+
+ if (hiddenIFrame.contentDocument.readyState === 'complete') {
+ testToRunOnLoad();
+ } else {
+ hiddenIFrame.addEventListener("load", testToRunOnLoad);
+ }
+}, 'document.timeline.currentTime hidden subframe dynamic test');
+
+done();
+</script>
diff --git a/dom/animation/test/document-timeline/test_document-timeline.html b/dom/animation/test/document-timeline/test_document-timeline.html
new file mode 100644
index 0000000000..812d88ef2a
--- /dev/null
+++ b/dom/animation/test/document-timeline/test_document-timeline.html
@@ -0,0 +1,14 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+'use strict';
+setup({explicit_done: true});
+SpecialPowers.pushPrefEnv(
+ { "set": [["dom.animations-api.core.enabled", true]]},
+ function() {
+ window.open("file_document-timeline.html");
+ });
+</script>
diff --git a/dom/animation/test/document-timeline/test_request_animation_frame.html b/dom/animation/test/document-timeline/test_request_animation_frame.html
new file mode 100644
index 0000000000..302a385b76
--- /dev/null
+++ b/dom/animation/test/document-timeline/test_request_animation_frame.html
@@ -0,0 +1,27 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title>Test RequestAnimationFrame Timestamps are monotonically increasing</title>
+<script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+<script>
+ var lastRequestAnimationFrameTimestamp = 0;
+ var requestAnimationFrameCount = 20;
+ var currentCount = 0;
+
+ // Test that all timestamps are always increasing
+ // and do not ever go backwards
+ function rafCallback(aTimestamp) {
+ SimpleTest.ok(aTimestamp > lastRequestAnimationFrameTimestamp,
+ "New RequestAnimationFrame timestamp should be later than the previous RequestAnimationFrame timestamp");
+ lastRequestAnimationFrameTimestamp = aTimestamp;
+ if (currentCount == requestAnimationFrameCount) {
+ SimpleTest.finish();
+ } else {
+ currentCount++;
+ window.requestAnimationFrame(rafCallback);
+ }
+ }
+
+ window.requestAnimationFrame(rafCallback);
+ SimpleTest.waitForExplicitFinish();
+</script>
diff --git a/dom/animation/test/mochitest.ini b/dom/animation/test/mochitest.ini
new file mode 100644
index 0000000000..feb424518d
--- /dev/null
+++ b/dom/animation/test/mochitest.ini
@@ -0,0 +1,111 @@
+[DEFAULT]
+# Support files for chrome tests that we want to load over HTTP need
+# to go in here, not chrome.ini.
+support-files =
+ chrome/file_animate_xrays.html
+ css-animations/file_animation-cancel.html
+ css-animations/file_animation-computed-timing.html
+ css-animations/file_animation-currenttime.html
+ css-animations/file_animation-finish.html
+ css-animations/file_animation-finished.html
+ css-animations/file_animation-id.html
+ css-animations/file_animation-pausing.html
+ css-animations/file_animation-playstate.html
+ css-animations/file_animation-ready.html
+ css-animations/file_animation-reverse.html
+ css-animations/file_animation-starttime.html
+ css-animations/file_animations-dynamic-changes.html
+ css-animations/file_cssanimation-animationname.html
+ css-animations/file_document-get-animations.html
+ css-animations/file_effect-target.html
+ css-animations/file_element-get-animations.html
+ css-animations/file_keyframeeffect-getkeyframes.html
+ css-animations/file_pseudoElement-get-animations.html
+ css-transitions/file_animation-cancel.html
+ css-transitions/file_animation-computed-timing.html
+ css-transitions/file_animation-currenttime.html
+ css-transitions/file_animation-finished.html
+ css-transitions/file_animation-pausing.html
+ css-transitions/file_animation-ready.html
+ css-transitions/file_animation-starttime.html
+ css-transitions/file_csstransition-transitionproperty.html
+ css-transitions/file_document-get-animations.html
+ css-transitions/file_effect-target.html
+ css-transitions/file_element-get-animations.html
+ css-transitions/file_keyframeeffect-getkeyframes.html
+ css-transitions/file_pseudoElement-get-animations.html
+ css-transitions/file_setting-effect.html
+ document-timeline/file_document-timeline.html
+ mozilla/file_cubic_bezier_limits.html
+ mozilla/file_deferred_start.html
+ mozilla/file_disabled_properties.html
+ mozilla/file_disable_animations_api_core.html
+ mozilla/file_discrete-animations.html
+ mozilla/file_document-timeline-origin-time-range.html
+ mozilla/file_hide_and_show.html
+ mozilla/file_partial_keyframes.html
+ mozilla/file_spacing_property_order.html
+ mozilla/file_spacing_transform.html
+ mozilla/file_transform_limits.html
+ mozilla/file_transition_finish_on_compositor.html
+ mozilla/file_underlying-discrete-value.html
+ mozilla/file_set-easing.html
+ style/file_animation-seeking-with-current-time.html
+ style/file_animation-seeking-with-start-time.html
+ style/file_animation-setting-effect.html
+ style/file_animation-setting-spacing.html
+ testcommon.js
+
+[css-animations/test_animations-dynamic-changes.html]
+[css-animations/test_animation-cancel.html]
+[css-animations/test_animation-computed-timing.html]
+[css-animations/test_animation-currenttime.html]
+[css-animations/test_animation-finish.html]
+[css-animations/test_animation-finished.html]
+[css-animations/test_animation-id.html]
+[css-animations/test_animation-pausing.html]
+[css-animations/test_animation-playstate.html]
+[css-animations/test_animation-ready.html]
+[css-animations/test_animation-reverse.html]
+[css-animations/test_animation-starttime.html]
+[css-animations/test_cssanimation-animationname.html]
+[css-animations/test_document-get-animations.html]
+[css-animations/test_effect-target.html]
+[css-animations/test_element-get-animations.html]
+[css-animations/test_keyframeeffect-getkeyframes.html]
+[css-animations/test_pseudoElement-get-animations.html]
+[css-transitions/test_animation-cancel.html]
+[css-transitions/test_animation-computed-timing.html]
+[css-transitions/test_animation-currenttime.html]
+[css-transitions/test_animation-finished.html]
+[css-transitions/test_animation-pausing.html]
+[css-transitions/test_animation-ready.html]
+[css-transitions/test_animation-starttime.html]
+[css-transitions/test_csstransition-transitionproperty.html]
+[css-transitions/test_document-get-animations.html]
+[css-transitions/test_effect-target.html]
+[css-transitions/test_element-get-animations.html]
+[css-transitions/test_keyframeeffect-getkeyframes.html]
+[css-transitions/test_pseudoElement-get-animations.html]
+[css-transitions/test_setting-effect.html]
+[document-timeline/test_document-timeline.html]
+[document-timeline/test_request_animation_frame.html]
+[mozilla/test_cubic_bezier_limits.html]
+[mozilla/test_deferred_start.html]
+[mozilla/test_disable_animations_api_core.html]
+[mozilla/test_disabled_properties.html]
+[mozilla/test_discrete-animations.html]
+[mozilla/test_document-timeline-origin-time-range.html]
+[mozilla/test_hide_and_show.html]
+[mozilla/test_partial_keyframes.html]
+[mozilla/test_set-easing.html]
+[mozilla/test_spacing_property_order.html]
+[mozilla/test_spacing_transform.html]
+[mozilla/test_transform_limits.html]
+[mozilla/test_transition_finish_on_compositor.html]
+skip-if = toolkit == 'android'
+[mozilla/test_underlying-discrete-value.html]
+[style/test_animation-seeking-with-current-time.html]
+[style/test_animation-seeking-with-start-time.html]
+[style/test_animation-setting-effect.html]
+[style/test_animation-setting-spacing.html]
diff --git a/dom/animation/test/mozilla/file_cubic_bezier_limits.html b/dom/animation/test/mozilla/file_cubic_bezier_limits.html
new file mode 100644
index 0000000000..a0378f3953
--- /dev/null
+++ b/dom/animation/test/mozilla/file_cubic_bezier_limits.html
@@ -0,0 +1,167 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="../testcommon.js"></script>
+<body>
+<style>
+@keyframes anim {
+ to { margin-left: 100px; }
+}
+
+.transition-div {
+ margin-left: 100px;
+}
+</style>
+<script>
+'use strict';
+
+// We clamp +infinity or -inifinity value in floating point to
+// maximum floating point value or -maxinum floating point value.
+const max_float = 3.40282e+38;
+
+test(function(t) {
+ var div = addDiv(t);
+ var anim = div.animate({ }, 100 * MS_PER_SEC);
+
+ anim.effect.timing.easing = 'cubic-bezier(0, 1e+39, 0, 0)';
+ assert_equals(anim.effect.timing.easing,
+ 'cubic-bezier(0, ' + max_float + ', 0, 0)',
+ 'y1 control point for effect easing is out of upper boundary');
+
+ anim.effect.timing.easing = 'cubic-bezier(0, 0, 0, 1e+39)';
+ assert_equals(anim.effect.timing.easing,
+ 'cubic-bezier(0, 0, 0, ' + max_float + ')',
+ 'y2 control point for effect easing is out of upper boundary');
+
+ anim.effect.timing.easing = 'cubic-bezier(0, -1e+39, 0, 0)';
+ assert_equals(anim.effect.timing.easing,
+ 'cubic-bezier(0, ' + -max_float + ', 0, 0)',
+ 'y1 control point for effect easing is out of lower boundary');
+
+ anim.effect.timing.easing = 'cubic-bezier(0, 0, 0, -1e+39)';
+ assert_equals(anim.effect.timing.easing,
+ 'cubic-bezier(0, 0, 0, ' + -max_float + ')',
+ 'y2 control point for effect easing is out of lower boundary');
+
+}, 'Clamp y1 and y2 control point out of boundaries for effect easing' );
+
+test(function(t) {
+ var div = addDiv(t);
+ var anim = div.animate({ }, 100 * MS_PER_SEC);
+
+ anim.effect.setKeyframes([ { easing: 'cubic-bezier(0, 1e+39, 0, 0)' }]);
+ assert_equals(anim.effect.getKeyframes()[0].easing,
+ 'cubic-bezier(0, ' + max_float + ', 0, 0)',
+ 'y1 control point for keyframe easing is out of upper boundary');
+
+ anim.effect.setKeyframes([ { easing: 'cubic-bezier(0, 0, 0, 1e+39)' }]);
+ assert_equals(anim.effect.getKeyframes()[0].easing,
+ 'cubic-bezier(0, 0, 0, ' + max_float + ')',
+ 'y2 control point for keyframe easing is out of upper boundary');
+
+ anim.effect.setKeyframes([ { easing: 'cubic-bezier(0, -1e+39, 0, 0)' }]);
+ assert_equals(anim.effect.getKeyframes()[0].easing,
+ 'cubic-bezier(0, ' + -max_float + ', 0, 0)',
+ 'y1 control point for keyframe easing is out of lower boundary');
+
+ anim.effect.setKeyframes([ { easing: 'cubic-bezier(0, 0, 0, -1e+39)' }]);
+ assert_equals(anim.effect.getKeyframes()[0].easing,
+ 'cubic-bezier(0, 0, 0, ' + -max_float + ')',
+ 'y2 control point for keyframe easing is out of lower boundary');
+
+}, 'Clamp y1 and y2 control point out of boundaries for keyframe easing' );
+
+test(function(t) {
+ var div = addDiv(t);
+
+ div.style.animation = 'anim 100s cubic-bezier(0, 1e+39, 0, 0)';
+
+ assert_equals(div.getAnimations()[0].effect.getKeyframes()[0].easing,
+ 'cubic-bezier(0, ' + max_float + ', 0, 0)',
+ 'y1 control point for CSS animation is out of upper boundary');
+
+ div.style.animation = 'anim 100s cubic-bezier(0, 0, 0, 1e+39)';
+ assert_equals(div.getAnimations()[0].effect.getKeyframes()[0].easing,
+ 'cubic-bezier(0, 0, 0, ' + max_float + ')',
+ 'y2 control point for CSS animation is out of upper boundary');
+
+ div.style.animation = 'anim 100s cubic-bezier(0, -1e+39, 0, 0)';
+ assert_equals(div.getAnimations()[0].effect.getKeyframes()[0].easing,
+ 'cubic-bezier(0, ' + -max_float + ', 0, 0)',
+ 'y1 control point for CSS animation is out of lower boundary');
+
+ div.style.animation = 'anim 100s cubic-bezier(0, 0, 0, -1e+39)';
+ assert_equals(div.getAnimations()[0].effect.getKeyframes()[0].easing,
+ 'cubic-bezier(0, 0, 0, ' + -max_float + ')',
+ 'y2 control point for CSS animation is out of lower boundary');
+
+}, 'Clamp y1 and y2 control point out of boundaries for CSS animation' );
+
+test(function(t) {
+ var div = addDiv(t, {'class': 'transition-div'});
+
+ div.style.transition = 'margin-left 100s cubic-bezier(0, 1e+39, 0, 0)';
+ flushComputedStyle(div);
+ div.style.marginLeft = '0px';
+ assert_equals(div.getAnimations()[0].effect.getKeyframes()[0].easing,
+ 'cubic-bezier(0, ' + max_float + ', 0, 0)',
+ 'y1 control point for CSS transition on upper boundary');
+ div.style.transition = '';
+ div.style.marginLeft = '';
+
+ div.style.transition = 'margin-left 100s cubic-bezier(0, 0, 0, 1e+39)';
+ flushComputedStyle(div);
+ div.style.marginLeft = '0px';
+ assert_equals(div.getAnimations()[0].effect.getKeyframes()[0].easing,
+ 'cubic-bezier(0, 0, 0, ' + max_float + ')',
+ 'y2 control point for CSS transition on upper boundary');
+ div.style.transition = '';
+ div.style.marginLeft = '';
+
+ div.style.transition = 'margin-left 100s cubic-bezier(0, -1e+39, 0, 0)';
+ flushComputedStyle(div);
+ div.style.marginLeft = '0px';
+ assert_equals(div.getAnimations()[0].effect.getKeyframes()[0].easing,
+ 'cubic-bezier(0, ' + -max_float + ', 0, 0)',
+ 'y1 control point for CSS transition on lower boundary');
+ div.style.transition = '';
+ div.style.marginLeft = '';
+
+ div.style.transition = 'margin-left 100s cubic-bezier(0, 0, 0, -1e+39)';
+ flushComputedStyle(div);
+ div.style.marginLeft = '0px';
+ assert_equals(div.getAnimations()[0].effect.getKeyframes()[0].easing,
+ 'cubic-bezier(0, 0, 0, ' + -max_float + ')',
+ 'y2 control point for CSS transition on lower boundary');
+
+}, 'Clamp y1 and y2 control point out of boundaries for CSS transition' );
+
+test(function(t) {
+ var div = addDiv(t);
+ var anim = div.animate({ }, { duration: 100 * MS_PER_SEC, fill: 'forwards' });
+
+ anim.pause();
+ // The positive steepest function on both edges.
+ anim.effect.timing.easing = 'cubic-bezier(0, 1e+39, 0, 1e+39)';
+ assert_equals(anim.effect.getComputedTiming().progress, 0.0,
+ 'progress on lower edge for the highest value of y1 and y2 control points');
+
+ anim.finish();
+ assert_equals(anim.effect.getComputedTiming().progress, 1.0,
+ 'progress on upper edge for the highest value of y1 and y2 control points');
+
+ // The negative steepest function on both edges.
+ anim.effect.timing.easing = 'cubic-bezier(0, -1e+39, 0, -1e+39)';
+ anim.currentTime = 0;
+ assert_equals(anim.effect.getComputedTiming().progress, 0.0,
+ 'progress on lower edge for the lowest value of y1 and y2 control points');
+
+ anim.finish();
+ assert_equals(anim.effect.getComputedTiming().progress, 1.0,
+ 'progress on lower edge for the lowest value of y1 and y2 control points');
+
+}, 'Calculated values on both edges');
+
+done();
+
+</script>
+</body>
diff --git a/dom/animation/test/mozilla/file_deferred_start.html b/dom/animation/test/mozilla/file_deferred_start.html
new file mode 100644
index 0000000000..3be3f56aae
--- /dev/null
+++ b/dom/animation/test/mozilla/file_deferred_start.html
@@ -0,0 +1,121 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="../testcommon.js"></script>
+<script src="/tests/SimpleTest/paint_listener.js"></script>
+<style>
+@keyframes empty { }
+@keyframes animTransform {
+ from { transform: translate(0px); }
+ to { transform: translate(100px); }
+}
+.target {
+ /* Element needs geometry to be eligible for layerization */
+ width: 100px;
+ height: 100px;
+ background-color: white;
+}
+</style>
+<body>
+<script>
+'use strict';
+
+function waitForDocLoad() {
+ return new Promise(function(resolve, reject) {
+ if (document.readyState === 'complete') {
+ resolve();
+ } else {
+ window.addEventListener('load', resolve);
+ }
+ });
+}
+
+function waitForPaints() {
+ return new Promise(function(resolve, reject) {
+ waitForAllPaintsFlushed(resolve);
+ });
+}
+
+promise_test(function(t) {
+ var div = addDiv(t);
+ var cs = window.getComputedStyle(div);
+
+ // Test that empty animations actually start.
+ //
+ // Normally we tie the start of animations to when their first frame of
+ // the animation is rendered. However, for animations that don't actually
+ // trigger a paint (e.g. because they are empty, or are animating something
+ // that doesn't render or is offscreen) we want to make sure they still
+ // start.
+ //
+ // Before we start, wait for the document to finish loading. This is because
+ // during loading we will have other paint events taking place which might,
+ // by luck, happen to trigger animations that otherwise would not have been
+ // triggered, leading to false positives.
+ //
+ // As a result, it's better to wait until we have a more stable state before
+ // continuing.
+ var promiseCallbackDone = false;
+ return waitForDocLoad().then(function() {
+ div.style.animation = 'empty 1000s';
+ var animation = div.getAnimations()[0];
+
+ return animation.ready.then(function() {
+ promiseCallbackDone = true;
+ }).catch(function() {
+ assert_unreached('ready promise was rejected');
+ });
+ }).then(function() {
+ // We need to wait for up to three frames. This is because in some
+ // cases it can take up to two frames for the initial layout
+ // to take place. Even after that happens we don't actually resolve the
+ // ready promise until the following tick.
+ return waitForAnimationFrames(3);
+ }).then(function() {
+ assert_true(promiseCallbackDone,
+ 'ready promise for an empty animation was resolved'
+ + ' within three animation frames');
+ });
+}, 'Animation.ready is resolved for an empty animation');
+
+// Test that compositor animations with delays get synced correctly
+//
+// NOTE: It is important that we DON'T use
+// SpecialPowers.DOMWindowUtils.advanceTimeAndRefresh here since that takes
+// us through a different code path.
+promise_test(function(t) {
+ // This test only applies to compositor animations
+ if (!isOMTAEnabled()) {
+ return;
+ }
+
+ // Setup animation
+ var div = addDiv(t);
+ div.classList.add('target');
+ div.style.animation = 'animTransform 100s -50s forwards';
+ var animation = div.getAnimations()[0];
+
+ return waitForPaints(function() {
+ var transformStr =
+ SpecialPowers.DOMWindowUtils.getOMTAStyle(div, 'transform');
+
+ var matrixComponents =
+ transformStr.startsWith('matrix(')
+ ? transformStr.substring('matrix('.length, transformStr.length-1)
+ .split(',')
+ .map(component => Number(component))
+ : [];
+ assert_equals(matrixComponents.length, 6,
+ 'Got a valid transform matrix on the compositor'
+ + ' (got: "' + transformStr + '")');
+
+ // If the delay has been applied correctly we should be at least
+ // half-way through the animation
+ assert_true(matrixComponents[4] >= 50,
+ 'Animation is at least half-way through on the compositor'
+ + ' (got translation of ' + matrixComponents[4] + ')');
+ });
+}, 'Starting an animation with a delay starts from the correct point');
+
+done();
+</script>
+</body>
diff --git a/dom/animation/test/mozilla/file_disable_animations_api_core.html b/dom/animation/test/mozilla/file_disable_animations_api_core.html
new file mode 100644
index 0000000000..ef77988d9a
--- /dev/null
+++ b/dom/animation/test/mozilla/file_disable_animations_api_core.html
@@ -0,0 +1,30 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="../testcommon.js"></script>
+<body>
+<script>
+'use strict';
+
+test(function(t) {
+ var div = addDiv(t);
+ var anim =
+ div.animate({ marginLeft: ['0px', '10px'] },
+ { duration: 100 * MS_PER_SEC,
+ easing: 'linear',
+ iterations: 10,
+ iterationComposite: 'accumulate' });
+ anim.pause();
+
+ // NOTE: We can't check iterationComposite value itself though API since
+ // Animation.effect is also behind the the Web Animations API. So we just
+ // check that style value is not affected by iterationComposite.
+ anim.currentTime = 200 * MS_PER_SEC;
+ assert_equals(getComputedStyle(div).marginLeft, '0px',
+ 'Animated style should not be accumulated when the Web Animations API is ' +
+ 'not enabled even if accumulate is specified in the constructor');
+}, 'iterationComposite should not affect at all if the Web Animations API ' +
+ 'is not enabled');
+
+done();
+</script>
+</body>
diff --git a/dom/animation/test/mozilla/file_disabled_properties.html b/dom/animation/test/mozilla/file_disabled_properties.html
new file mode 100644
index 0000000000..f1b72973f4
--- /dev/null
+++ b/dom/animation/test/mozilla/file_disabled_properties.html
@@ -0,0 +1,73 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="../testcommon.js"></script>
+<body>
+<script>
+'use strict';
+
+function waitForSetPref(pref, value) {
+ return new Promise(function(resolve, reject) {
+ SpecialPowers.pushPrefEnv({ 'set': [[pref, value]] }, resolve);
+ });
+}
+
+/*
+ * These tests rely on the fact that the -webkit-text-fill-color property
+ * is disabled by the layout.css.prefixes.webkit pref. If we ever remove that
+ * pref we will need to substitute some other pref:property combination.
+ */
+
+promise_test(function(t) {
+ return waitForSetPref('layout.css.prefixes.webkit', true).then(() => {
+ var anim = addDiv(t).animate({ webkitTextFillColor: [ 'green', 'blue' ]});
+ assert_equals(anim.effect.getKeyframes().length, 2,
+ 'A property-indexed keyframe specifying only enabled'
+ + ' properties produces keyframes');
+ return waitForSetPref('layout.css.prefixes.webkit', false);
+ }).then(() => {
+ var anim = addDiv(t).animate({ webkitTextFillColor: [ 'green', 'blue' ]});
+ assert_equals(anim.effect.getKeyframes().length, 0,
+ 'A property-indexed keyframe specifying only disabled'
+ + ' properties produces no keyframes');
+ });
+}, 'Specifying a disabled property using a property-indexed keyframe');
+
+promise_test(function(t) {
+ var createAnim = () => {
+ var anim = addDiv(t).animate([ { webkitTextFillColor: 'green' },
+ { webkitTextFillColor: 'blue' } ]);
+ assert_equals(anim.effect.getKeyframes().length, 2,
+ 'Animation specified using a keyframe sequence should'
+ + ' return the same number of keyframes regardless of'
+ + ' whether or not the specified properties are disabled');
+ return anim;
+ };
+
+ var assert_has_property = (anim, index, descr, property) => {
+ assert_true(
+ anim.effect.getKeyframes()[index].hasOwnProperty(property),
+ `${descr} should have the '${property}' property`);
+ };
+ var assert_does_not_have_property = (anim, index, descr, property) => {
+ assert_false(
+ anim.effect.getKeyframes()[index].hasOwnProperty(property),
+ `${descr} should NOT have the '${property}' property`);
+ };
+
+ return waitForSetPref('layout.css.prefixes.webkit', true).then(() => {
+ var anim = createAnim();
+ assert_has_property(anim, 0, 'Initial keyframe', 'webkitTextFillColor');
+ assert_has_property(anim, 1, 'Final keyframe', 'webkitTextFillColor');
+ return waitForSetPref('layout.css.prefixes.webkit', false);
+ }).then(() => {
+ var anim = createAnim();
+ assert_does_not_have_property(anim, 0, 'Initial keyframe',
+ 'webkitTextFillColor');
+ assert_does_not_have_property(anim, 1, 'Final keyframe',
+ 'webkitTextFillColor');
+ });
+}, 'Specifying a disabled property using a keyframe sequence');
+
+done();
+</script>
+</body>
diff --git a/dom/animation/test/mozilla/file_discrete-animations.html b/dom/animation/test/mozilla/file_discrete-animations.html
new file mode 100644
index 0000000000..35e818a909
--- /dev/null
+++ b/dom/animation/test/mozilla/file_discrete-animations.html
@@ -0,0 +1,170 @@
+<!doctype html>
+<head>
+<meta charset=utf-8>
+<title>Test Mozilla-specific discrete animatable properties</title>
+<script type="application/javascript" src="../testcommon.js"></script>
+</head>
+<body>
+<script>
+"use strict";
+
+const gMozillaSpecificProperties = {
+ "-moz-appearance": {
+ // https://drafts.csswg.org/css-align/#propdef-align-content
+ from: "button",
+ to: "none"
+ },
+ "-moz-border-bottom-colors": {
+ from: "rgb(255, 0, 0) rgb(255, 0, 0) rgb(255, 0, 0) rgb(255, 0, 0)",
+ to: "rgb(0, 255, 0) rgb(0, 255, 0) rgb(0, 255, 0) rgb(0, 255, 0)"
+ },
+ "-moz-border-left-colors": {
+ from: "rgb(255, 0, 0) rgb(255, 0, 0) rgb(255, 0, 0) rgb(255, 0, 0)",
+ to: "rgb(0, 255, 0) rgb(0, 255, 0) rgb(0, 255, 0) rgb(0, 255, 0)"
+ },
+ "-moz-border-right-colors": {
+ from: "rgb(255, 0, 0) rgb(255, 0, 0) rgb(255, 0, 0) rgb(255, 0, 0)",
+ to: "rgb(0, 255, 0) rgb(0, 255, 0) rgb(0, 255, 0) rgb(0, 255, 0)"
+ },
+ "-moz-border-top-colors": {
+ from: "rgb(255, 0, 0) rgb(255, 0, 0) rgb(255, 0, 0) rgb(255, 0, 0)",
+ to: "rgb(0, 255, 0) rgb(0, 255, 0) rgb(0, 255, 0) rgb(0, 255, 0)"
+ },
+ "-moz-box-align": {
+ // https://developer.mozilla.org/en/docs/Web/CSS/box-align
+ from: "center",
+ to: "stretch"
+ },
+ "-moz-box-direction": {
+ // https://developer.mozilla.org/en/docs/Web/CSS/box-direction
+ from: "reverse",
+ to: "normal"
+ },
+ "-moz-box-ordinal-group": {
+ // https://developer.mozilla.org/en/docs/Web/CSS/box-ordinal-group
+ from: "1",
+ to: "5"
+ },
+ "-moz-box-orient": {
+ // https://www.w3.org/TR/css-flexbox-1/
+ from: "horizontal",
+ to: "vertical"
+ },
+ "-moz-box-pack": {
+ // https://www.w3.org/TR/2009/WD-css3-flexbox-20090723/#propdef-box-pack
+ from: "center",
+ to: "end"
+ },
+ "-moz-float-edge": {
+ // https://developer.mozilla.org/en/docs/Web/CSS/-moz-float-edge
+ from: "margin-box",
+ to: "content-box"
+ },
+ "-moz-force-broken-image-icon": {
+ // https://developer.mozilla.org/en/docs/Web/CSS/-moz-force-broken-image-icon
+ from: "1",
+ to: "5"
+ },
+ "image-rendering": {
+ // https://drafts.csswg.org/css-images-3/#propdef-image-rendering
+ from: "-moz-crisp-edges",
+ to: "auto"
+ },
+ "-moz-stack-sizing": {
+ // https://developer.mozilla.org/en/docs/Web/CSS/-moz-stack-sizing
+ from: "ignore",
+ to: "stretch-to-fit"
+ },
+ "-moz-tab-size": {
+ // https://drafts.csswg.org/css-text-3/#propdef-tab-size
+ from: "1",
+ to: "5"
+ },
+ "-moz-text-size-adjust": {
+ // https://drafts.csswg.org/css-size-adjust/#propdef-text-size-adjust
+ from: "none",
+ to: "auto"
+ },
+ "-webkit-text-stroke-width": {
+ // https://compat.spec.whatwg.org/#propdef--webkit-text-stroke-width
+ from: "10px",
+ to: "50px"
+ }
+}
+
+for (let property in gMozillaSpecificProperties) {
+ const testData = gMozillaSpecificProperties[property];
+ const from = testData.from;
+ const to = testData.to;
+ const idlName = propertyToIDL(property);
+ const keyframes = {};
+ keyframes[idlName] = [from, to];
+
+ test(t => {
+ const div = addDiv(t);
+ const animation = div.animate(keyframes,
+ { duration: 1000, fill: "both" });
+ testAnimationSamples(animation, idlName,
+ [{ time: 0, expected: from.toLowerCase() },
+ { time: 499, expected: from.toLowerCase() },
+ { time: 500, expected: to.toLowerCase() },
+ { time: 1000, expected: to.toLowerCase() }]);
+ }, property + " should animate between '"
+ + from + "' and '" + to + "' with linear easing");
+
+ test(function(t) {
+ // Easing: http://cubic-bezier.com/#.68,0,1,.01
+ // With this curve, we don't reach the 50% point until about 95% of
+ // the time has expired.
+ const div = addDiv(t);
+ const animation = div.animate(keyframes,
+ { duration: 1000, fill: "both",
+ easing: "cubic-bezier(0.68,0,1,0.01)" });
+ testAnimationSamples(animation, idlName,
+ [{ time: 0, expected: from.toLowerCase() },
+ { time: 940, expected: from.toLowerCase() },
+ { time: 960, expected: to.toLowerCase() }]);
+ }, property + " should animate between '"
+ + from + "' and '" + to + "' with effect easing");
+
+ test(function(t) {
+ // Easing: http://cubic-bezier.com/#.68,0,1,.01
+ // With this curve, we don't reach the 50% point until about 95% of
+ // the time has expired.
+ keyframes.easing = "cubic-bezier(0.68,0,1,0.01)";
+ const div = addDiv(t);
+ const animation = div.animate(keyframes,
+ { duration: 1000, fill: "both" });
+ testAnimationSamples(animation, idlName,
+ [{ time: 0, expected: from.toLowerCase() },
+ { time: 940, expected: from.toLowerCase() },
+ { time: 960, expected: to.toLowerCase() }]);
+ }, property + " should animate between '"
+ + from + "' and '" + to + "' with keyframe easing");
+}
+
+function propertyToIDL(property) {
+ var prefixMatch = property.match(/^-(\w+)-/);
+ if (prefixMatch) {
+ var prefix = prefixMatch[1] === "moz" ? "Moz" : prefixMatch[1];
+ property = prefix + property.substring(prefixMatch[0].length - 1);
+ }
+ // https://drafts.csswg.org/cssom/#css-property-to-idl-attribute
+ return property.replace(/-([a-z])/gi, function(str, group) {
+ return group.toUpperCase();
+ });
+}
+
+function testAnimationSamples(animation, idlName, testSamples) {
+ const target = animation.effect.target;
+ testSamples.forEach(testSample => {
+ animation.currentTime = testSample.time;
+ assert_equals(getComputedStyle(target)[idlName], testSample.expected,
+ "The value should be " + testSample.expected +
+ " at " + testSample.time + "ms");
+ });
+}
+
+done();
+</script>
+</body>
diff --git a/dom/animation/test/mozilla/file_document-timeline-origin-time-range.html b/dom/animation/test/mozilla/file_document-timeline-origin-time-range.html
new file mode 100644
index 0000000000..083bf0903b
--- /dev/null
+++ b/dom/animation/test/mozilla/file_document-timeline-origin-time-range.html
@@ -0,0 +1,30 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="../testcommon.js"></script>
+<body>
+<script>
+'use strict';
+
+// If the originTime parameter passed to the DocumentTimeline exceeds
+// the range of the internal storage type (a signed 64-bit integer number
+// of ticks--a platform-dependent unit) then we should throw.
+// Infinity isn't allowed as an origin time value and clamping to just
+// inside the allowed range will just mean we overflow elsewhere.
+
+test(function(t) {
+ assert_throws({ name: 'TypeError'},
+ function() {
+ new DocumentTimeline({ originTime: Number.MAX_SAFE_INTEGER });
+ });
+}, 'Calculated current time is positive infinity');
+
+test(function(t) {
+ assert_throws({ name: 'TypeError'},
+ function() {
+ new DocumentTimeline({ originTime: -1 * Number.MAX_SAFE_INTEGER });
+ });
+}, 'Calculated current time is negative infinity');
+
+done();
+</script>
+</body>
diff --git a/dom/animation/test/mozilla/file_hide_and_show.html b/dom/animation/test/mozilla/file_hide_and_show.html
new file mode 100644
index 0000000000..0771fcce1f
--- /dev/null
+++ b/dom/animation/test/mozilla/file_hide_and_show.html
@@ -0,0 +1,162 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="../testcommon.js"></script>
+<style>
+@keyframes move {
+ 100% {
+ transform: translateX(100px);
+ }
+}
+
+</style>
+<body>
+<script>
+'use strict';
+
+test(function(t) {
+ var div = addDiv(t, { style: 'animation: move 100s infinite' });
+ assert_equals(div.getAnimations().length, 1,
+ 'display:initial element has animations');
+
+ div.style.display = 'none';
+ assert_equals(div.getAnimations().length, 0,
+ 'display:none element has no animations');
+}, 'Animation stops playing when the element style display is set to "none"');
+
+test(function(t) {
+ var parentElement = addDiv(t);
+ var div = addDiv(t, { style: 'animation: move 100s infinite' });
+ parentElement.appendChild(div);
+ assert_equals(div.getAnimations().length, 1,
+ 'display:initial element has animations');
+
+ parentElement.style.display = 'none';
+ assert_equals(div.getAnimations().length, 0,
+ 'Element in display:none subtree has no animations');
+}, 'Animation stops playing when its parent element style display is set ' +
+ 'to "none"');
+
+test(function(t) {
+ var div = addDiv(t, { style: 'animation: move 100s infinite' });
+ assert_equals(div.getAnimations().length, 1,
+ 'display:initial element has animations');
+
+ div.style.display = 'none';
+ assert_equals(div.getAnimations().length, 0,
+ 'display:none element has no animations');
+
+ div.style.display = '';
+ assert_equals(div.getAnimations().length, 1,
+ 'Element which is no longer display:none has animations ' +
+ 'again');
+}, 'Animation starts playing when the element gets shown from ' +
+ '"display:none" state');
+
+test(function(t) {
+ var parentElement = addDiv(t);
+ var div = addDiv(t, { style: 'animation: move 100s infinite' });
+ parentElement.appendChild(div);
+ assert_equals(div.getAnimations().length, 1,
+ 'display:initial element has animations');
+
+ parentElement.style.display = 'none';
+ assert_equals(div.getAnimations().length, 0,
+ 'Element in display:none subtree has no animations');
+
+ parentElement.style.display = '';
+ assert_equals(div.getAnimations().length, 1,
+ 'Element which is no longer in display:none subtree has ' +
+ 'animations again');
+}, 'Animation starts playing when its parent element is shown from ' +
+ '"display:none" state');
+
+test(function(t) {
+ var div = addDiv(t, { style: 'animation: move 100s forwards' });
+ assert_equals(div.getAnimations().length, 1,
+ 'display:initial element has animations');
+
+ var animation = div.getAnimations()[0];
+ animation.finish();
+ assert_equals(div.getAnimations().length, 1,
+ 'Element has finished animation if the animation ' +
+ 'fill-mode is forwards');
+
+ div.style.display = 'none';
+ assert_equals(animation.playState, 'idle',
+ 'The animation.playState should be idle');
+
+ assert_equals(div.getAnimations().length, 0,
+ 'display:none element has no animations');
+
+ div.style.display = '';
+ assert_equals(div.getAnimations().length, 1,
+ 'Element which is no longer display:none has animations ' +
+ 'again');
+ assert_not_equals(div.getAnimations()[0], animation,
+ 'Restarted animation is a newly-generated animation');
+
+}, 'Animation which has already finished starts playing when the element ' +
+ 'gets shown from "display:none" state');
+
+test(function(t) {
+ var parentElement = addDiv(t);
+ var div = addDiv(t, { style: 'animation: move 100s forwards' });
+ parentElement.appendChild(div);
+ assert_equals(div.getAnimations().length, 1,
+ 'display:initial element has animations');
+
+ var animation = div.getAnimations()[0];
+ animation.finish();
+ assert_equals(div.getAnimations().length, 1,
+ 'Element has finished animation if the animation ' +
+ 'fill-mode is forwards');
+
+ parentElement.style.display = 'none';
+ assert_equals(animation.playState, 'idle',
+ 'The animation.playState should be idle');
+ assert_equals(div.getAnimations().length, 0,
+ 'Element in display:none subtree has no animations');
+
+ parentElement.style.display = '';
+ assert_equals(div.getAnimations().length, 1,
+ 'Element which is no longer in display:none subtree has ' +
+ 'animations again');
+
+ assert_not_equals(div.getAnimations()[0], animation,
+ 'Restarted animation is a newly-generated animation');
+
+}, 'Animation with fill:forwards which has already finished starts playing ' +
+ 'when its parent element is shown from "display:none" state');
+
+test(function(t) {
+ var parentElement = addDiv(t);
+ var div = addDiv(t, { style: 'animation: move 100s' });
+ parentElement.appendChild(div);
+ assert_equals(div.getAnimations().length, 1,
+ 'display:initial element has animations');
+
+ var animation = div.getAnimations()[0];
+ animation.finish();
+ assert_equals(div.getAnimations().length, 0,
+ 'Element does not have finished animations');
+
+ parentElement.style.display = 'none';
+ assert_equals(animation.playState, 'idle',
+ 'The animation.playState should be idle');
+ assert_equals(div.getAnimations().length, 0,
+ 'Element in display:none subtree has no animations');
+
+ parentElement.style.display = '';
+ assert_equals(div.getAnimations().length, 1,
+ 'Element which is no longer in display:none subtree has ' +
+ 'animations again');
+
+ assert_not_equals(div.getAnimations()[0], animation,
+ 'Restarted animation is a newly-generated animation');
+
+}, 'CSS Animation which has already finished starts playing when its parent ' +
+ 'element is shown from "display:none" state');
+
+done();
+</script>
+</body>
diff --git a/dom/animation/test/mozilla/file_partial_keyframes.html b/dom/animation/test/mozilla/file_partial_keyframes.html
new file mode 100644
index 0000000000..68832be7a0
--- /dev/null
+++ b/dom/animation/test/mozilla/file_partial_keyframes.html
@@ -0,0 +1,41 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="../testcommon.js"></script>
+<body>
+<script>
+'use strict';
+
+// Tests for cases we currently don't handle and should throw an exception for.
+var gTests = [
+ { desc: "single Keyframe with no offset",
+ keyframes: [{ left: "100px" }] },
+ { desc: "multiple Keyframes with missing 0% Keyframe",
+ keyframes: [{ left: "100px", offset: 0.25 },
+ { left: "200px", offset: 0.50 },
+ { left: "300px", offset: 1.00 }] },
+ { desc: "multiple Keyframes with missing 100% Keyframe",
+ keyframes: [{ left: "100px", offset: 0.00 },
+ { left: "200px", offset: 0.50 },
+ { left: "300px", offset: 0.75 }] },
+ { desc: "multiple Keyframes with missing properties on first Keyframe",
+ keyframes: [{ left: "100px", offset: 0.0 },
+ { left: "200px", top: "200px", offset: 0.5 },
+ { left: "300px", top: "300px", offset: 1.0 }] },
+ { desc: "multiple Keyframes with missing properties on last Keyframe",
+ keyframes: [{ left: "100px", top: "200px", offset: 0.0 },
+ { left: "200px", top: "200px", offset: 0.5 },
+ { left: "300px", offset: 1.0 }] },
+];
+
+gTests.forEach(function(subtest) {
+ test(function(t) {
+ var div = addDiv(t);
+ assert_throws("NotSupportedError", function() {
+ new KeyframeEffectReadOnly(div, subtest.keyframes);
+ });
+ }, "KeyframeEffectReadOnly constructor throws with " + subtest.desc);
+});
+
+done();
+</script>
+</body>
diff --git a/dom/animation/test/mozilla/file_set-easing.html b/dom/animation/test/mozilla/file_set-easing.html
new file mode 100644
index 0000000000..072b125cb0
--- /dev/null
+++ b/dom/animation/test/mozilla/file_set-easing.html
@@ -0,0 +1,34 @@
+<!doctype html>
+<head>
+<meta charset=utf-8>
+<title>Test setting easing in sandbox</title>
+<script src="../testcommon.js"></script>
+</head>
+<body>
+<script>
+"use strict";
+
+test(function(t) {
+ const div = document.createElement("div");
+ document.body.appendChild(div);
+ div.animate({ opacity: [0, 1] }, 100000 );
+
+ const contentScript = function() {
+ try {
+ document.getAnimations()[0].effect.timing.easing = "linear";
+ assert_true(true, 'Setting easing should not throw in sandbox');
+ } catch (e) {
+ assert_unreached('Setting easing threw ' + e);
+ }
+ };
+
+ const sandbox = new SpecialPowers.Cu.Sandbox(window);
+ sandbox.importFunction(document, "document");
+ sandbox.importFunction(assert_true, "assert_true");
+ sandbox.importFunction(assert_unreached, "assert_unreached");
+ SpecialPowers.Cu.evalInSandbox(`(${contentScript.toSource()})()`, sandbox);
+}, 'Setting easing should not throw any exceptions in sandbox');
+
+done();
+</script>
+</body>
diff --git a/dom/animation/test/mozilla/file_spacing_property_order.html b/dom/animation/test/mozilla/file_spacing_property_order.html
new file mode 100644
index 0000000000..1338d60811
--- /dev/null
+++ b/dom/animation/test/mozilla/file_spacing_property_order.html
@@ -0,0 +1,33 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="../testcommon.js"></script>
+<body>
+<script>
+'use strict';
+
+test(function(t) {
+ var div = document.createElement('div');
+ document.documentElement.appendChild(div);
+ var anim = div.animate([ { borderRadius: "0", borderTopRightRadius: "10%" },
+ { borderTopLeftRadius: "20%",
+ borderTopRightRadius: "30%",
+ borderBottomRightRadius: "40%",
+ borderBottomLeftRadius: "50%" },
+ { borderRadius: "50%" } ],
+ { spacing:"paced(border-radius)" });
+
+ var frames = anim.effect.getKeyframes();
+ var dist = [ 0,
+ Math.sqrt(20 * 20 + (30 - 10) * (30 - 10) + 40 * 40 + 50 * 50),
+ Math.sqrt((50 - 20) * (50 - 20) + (50 - 30) * (50 - 30) +
+ (50 - 40) * (50 - 40) + (50 - 50) * (50 - 50)) ];
+ var cumDist = [];
+ dist.reduce(function(prev, curr, i) { return cumDist[i] = prev + curr; }, 0);
+ assert_approx_equals(frames[1].computedOffset, cumDist[1] / cumDist[2],
+ 0.0001, 'frame offset');
+}, 'Test for the longhand components of the shorthand property surely sorted' );
+
+done();
+
+</script>
+</body>
diff --git a/dom/animation/test/mozilla/file_spacing_transform.html b/dom/animation/test/mozilla/file_spacing_transform.html
new file mode 100644
index 0000000000..0de7737864
--- /dev/null
+++ b/dom/animation/test/mozilla/file_spacing_transform.html
@@ -0,0 +1,240 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="../testcommon.js"></script>
+<body>
+<script>
+'use strict';
+
+const pi = Math.PI;
+const cos = Math.cos;
+const sin = Math.sin;
+const tan = Math.tan;
+const sqrt = Math.sqrt;
+
+// Help function for testing the computed offsets by the distance array.
+function assert_animation_offsets(anim, dist) {
+ const epsilon = 0.00000001;
+ const frames = anim.effect.getKeyframes();
+ const cumDist = dist.reduce( (prev, curr) => {
+ prev.push(prev.length == 0 ? curr : curr + prev[prev.length - 1]);
+ return prev;
+ }, []);
+
+ const total = cumDist[cumDist.length - 1];
+ for (var i = 0; i < frames.length; ++i) {
+ assert_approx_equals(frames[i].computedOffset, cumDist[i] / total,
+ epsilon, 'computedOffset of frame ' + i);
+ }
+}
+
+function getAngleDist(rotate1, rotate2) {
+ function quaternion(axis, angle) {
+ var x = axis[0] * sin(angle/2.0);
+ var y = axis[1] * sin(angle/2.0);
+ var z = axis[2] * sin(angle/2.0);
+ var w = cos(angle/2.0);
+ return { 'x': x, 'y': y, 'z': z, 'w': w };
+ }
+ var q1 = quaternion(rotate1.axis, rotate1.angle);
+ var q2 = quaternion(rotate2.axis, rotate2.angle);
+ var dotProduct = q1.x * q2.x + q1.y * q2.y + q1.z * q2.z + q1.w * q2.w;
+ return 2.0 * Math.acos(dotProduct);
+}
+
+function createMatrix(elements, Is3D) {
+ return (Is3D ? "matrix3d" : "matrix") + "(" + elements.join() + ")";
+}
+
+test(function(t) {
+ var anim = addDiv(t).animate([ { transform: "none" },
+ { transform: "translate(-20px)" },
+ { transform: "translate(100px)" },
+ { transform: "translate(50px)"} ],
+ { spacing: "paced(transform)" });
+ assert_animation_offsets(anim, [ 0, 20, 120, 50 ]);
+}, 'Test spacing on translate' );
+
+test(function(t) {
+ var anim =
+ addDiv(t).animate([ { transform: "none" },
+ { transform: "translate3d(-20px, 10px, 100px)" },
+ { transform: "translate3d(100px, 200px, 50px)" },
+ { transform: "translate(50px, -10px)"} ],
+ { spacing: "paced(transform)" });
+ var dist = [ 0,
+ Math.sqrt(20 * 20 + 10 * 10 + 100 * 100),
+ Math.sqrt(120 * 120 + 190 * 190 + 50 * 50),
+ Math.sqrt(50 * 50 + 210 * 210 + 50 * 50) ];
+ assert_animation_offsets(anim, dist);
+}, 'Test spacing on translate3d' );
+
+test(function(t) {
+ var anim = addDiv(t).animate([ { transform: "scale(0.5)" },
+ { transform: "scale(4.5)" },
+ { transform: "scale(2.5)" },
+ { transform: "none"} ],
+ { spacing: "paced(transform)" });
+ assert_animation_offsets(anim, [ 0, 4.0, 2.0, 1.5 ]);
+}, 'Test spacing on scale' );
+
+test(function(t) {
+ var anim = addDiv(t).animate([ { transform: "scale(0.5, 0.5)" },
+ { transform: "scale3d(4.5, 5.0, 2.5)" },
+ { transform: "scale3d(2.5, 1.0, 2.0)" },
+ { transform: "scale3d(1, 0.5, 1.0)"} ],
+ { spacing:"paced(transform)" });
+ var dist = [ 0,
+ Math.sqrt(4.0 * 4.0 + 4.5 * 4.5 + 1.5 * 1.5),
+ Math.sqrt(2.0 * 2.0 + 4.0 * 4.0 + 0.5 * 0.5),
+ Math.sqrt(1.5 * 1.5 + 0.5 * 0.5 + 1.0 * 1.0) ];
+ assert_animation_offsets(anim, dist);
+}, 'Test spacing on scale3d' );
+
+test(function(t) {
+ var anim = addDiv(t).animate([ { transform: "rotate(60deg)" },
+ { transform: "none" },
+ { transform: "rotate(720deg)" },
+ { transform: "rotate(-360deg)"} ],
+ { spacing: "paced(transform)" });
+ assert_animation_offsets(anim, [ 0, 60, 720, 1080 ]);
+}, 'Test spacing on rotate' );
+
+test(function(t) {
+ var anim = addDiv(t).animate([ { transform: "rotate3d(1,0,0,60deg)" },
+ { transform: "rotate3d(1,0,0,70deg)" },
+ { transform: "rotate3d(0,0,1,-110deg)" },
+ { transform: "rotate3d(1,0,0,219deg)"} ],
+ { spacing: "paced(transform)" });
+ var dist = [ 0,
+ getAngleDist({ axis: [1,0,0], angle: 60 * pi / 180 },
+ { axis: [1,0,0], angle: 70 * pi / 180 }),
+ getAngleDist({ axis: [0,1,0], angle: 70 * pi / 180 },
+ { axis: [0,0,1], angle: -110 * pi / 180 }),
+ getAngleDist({ axis: [0,0,1], angle: -110 * pi / 180 },
+ { axis: [1,0,0], angle: 219 * pi / 180 }) ];
+ assert_animation_offsets(anim, dist);
+}, 'Test spacing on rotate3d' );
+
+test(function(t) {
+ var anim = addDiv(t).animate([ { transform: "skew(60deg)" },
+ { transform: "none" },
+ { transform: "skew(-90deg)" },
+ { transform: "skew(90deg)"} ],
+ { spacing: "paced(transform)" });
+ assert_animation_offsets(anim, [ 0, 60, 90, 180 ]);
+}, 'Test spacing on skew' );
+
+test(function(t) {
+ var anim = addDiv(t).animate([ { transform: "skew(60deg, 30deg)" },
+ { transform: "none" },
+ { transform: "skew(-90deg, 60deg)" },
+ { transform: "skew(90deg, 60deg)"} ],
+ { spacing: "paced(transform)" });
+ var dist = [ 0,
+ sqrt(60 * 60 + 30 * 30),
+ sqrt(90 * 90 + 60 * 60),
+ sqrt(180 * 180 + 0) ];
+ assert_animation_offsets(anim, dist);
+}, 'Test spacing on skew along both X and Y' );
+
+test(function(t) {
+ // We calculate the distance of two perspective functions by converting them
+ // into two matrix3ds, and then do matrix decomposition to get two
+ // perspective vectors, so the equivalent perspective vectors are:
+ // perspective 1: (0, 0, -1/128, 1);
+ // perspective 2: (0, 0, -1/infinity = 0, 1);
+ // perspective 3: (0, 0, -1/1024, 1);
+ // perspective 4: (0, 0, -1/32, 1);
+ var anim = addDiv(t).animate([ { transform: "perspective(128px)" },
+ { transform: "none" },
+ { transform: "perspective(1024px)" },
+ { transform: "perspective(32px)"} ],
+ { spacing: "paced(transform)" });
+ assert_animation_offsets(anim,
+ [ 0, 1/128, 1/1024, 1/32 - 1/1024 ]);
+}, 'Test spacing on perspective' );
+
+test(function(t) {
+ var anim =
+ addDiv(t).animate([ { transform: "none" },
+ { transform: "rotate(180deg) translate(0px)" },
+ { transform: "rotate(180deg) translate(1000px)" },
+ { transform: "rotate(360deg) translate(1000px)"} ],
+ { spacing: "paced(transform)" });
+ var dist = [ 0,
+ sqrt(pi * pi + 0),
+ sqrt(1000 * 1000),
+ sqrt(pi * pi + 0) ];
+ assert_animation_offsets(anim, dist);
+}, 'Test spacing on matched transform lists' );
+
+test(function(t) {
+ // matrix1 => translate(100px, 50px), skewX(60deg).
+ // matrix2 => translate(1000px), rotate(180deg).
+ // matrix3 => translate(1000px), scale(1.5, 0.7).
+ const matrix1 = createMatrix([ 1, 0, tan(pi/4.0), 1, 100, 50 ]);
+ const matrix2 = createMatrix([ cos(pi), sin(pi),
+ -sin(pi), cos(pi),
+ 1000, 0 ]);
+ const matrix3 = createMatrix([ 1.5, 0, 0, 0.7, 1000, 0 ]);
+ var anim = addDiv(t).animate([ { transform: "none" },
+ { transform: matrix1 },
+ { transform: matrix2 },
+ { transform: matrix3 } ],
+ { spacing: "paced(transform)" });
+ var dist = [ 0,
+ sqrt(100 * 100 + 50 * 50 + pi/4 * pi/4),
+ sqrt(900 * 900 + 50 * 50 + pi * pi + pi/4 * pi/4),
+ sqrt(pi * pi + 0.5 * 0.5 + 0.3 * 0.3) ];
+ assert_animation_offsets(anim, dist);
+}, 'Test spacing on matrix' );
+
+test(function(t) {
+ // matrix1 => translate3d(100px, 50px, -10px), skew(60deg).
+ // matrix2 => translate3d(1000px, 0, 0), rotate3d(1, 0, 0, 180deg).
+ // matrix3 => translate3d(1000px, 0, 0), scale3d(1.5, 0.7, 2.2).
+ const matrix1 = createMatrix([ 1, 0, 0, 0,
+ tan(pi/4.0), 1, 0, 0,
+ 0, 0, 1, 0,
+ 100, 50, -10, 1 ], true);
+ const matrix2 = createMatrix([ 1, 0, 0, 0,
+ 0, cos(pi), sin(pi), 0,
+ 0, -sin(pi), cos(pi), 0,
+ 1000, 0, 0, 1 ], true);
+ const matrix3 = createMatrix([ 1.5, 0, 0, 0,
+ 0, 0.7, 0, 0,
+ 0, 0, 2.2, 0,
+ 1000, 0, 0, 1 ], true);
+ var anim = addDiv(t).animate([ { transform: "none" },
+ { transform: matrix1 },
+ { transform: matrix2 },
+ { transform: matrix3 } ],
+ { spacing: "paced(transform)" });
+ var dist = [ 0,
+ sqrt(100 * 100 + 50 * 50 + 10 * 10 + pi/4 * pi/4),
+ sqrt(900 * 900 + 50 * 50 + 10 * 10 + pi/4 * pi/4 + pi * pi),
+ sqrt(0.5 * 0.5 + 0.3 * 0.3 + 1.2 * 1.2 + pi * pi) ];
+ assert_animation_offsets(anim, dist);
+}, 'Test spacing on matrix3d' );
+
+test(function(t) {
+ var anim =
+ addDiv(t).animate([ { transform: "none" },
+ { transform: "translate(100px, 50px) skew(45deg)" },
+ { transform: "translate(1000px) " +
+ "rotate3d(1, 0, 0, 180deg)" },
+ { transform: "translate(1000px) " +
+ "scale3d(2.5, 0.5, 0.7)" } ],
+ { spacing: "paced(transform)" });
+
+ var dist = [ 0,
+ sqrt(100 * 100 + 50 * 50 + pi/4 * pi/4),
+ sqrt(900 * 900 + 50 * 50 + pi/4 * pi/4 + pi * pi),
+ sqrt(1.5 * 1.5 + 0.5 * 0.5 + 0.3 * 0.3 + pi * pi) ];
+ assert_animation_offsets(anim, dist);
+}, 'Test spacing on mismatched transform list' );
+
+done();
+
+</script>
+</body>
diff --git a/dom/animation/test/mozilla/file_transform_limits.html b/dom/animation/test/mozilla/file_transform_limits.html
new file mode 100644
index 0000000000..d4c813c67d
--- /dev/null
+++ b/dom/animation/test/mozilla/file_transform_limits.html
@@ -0,0 +1,55 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="../testcommon.js"></script>
+<body>
+<script>
+'use strict';
+
+// We clamp +infinity or -inifinity value in floating point to
+// maximum floating point value or -maximum floating point value.
+const max_float = 3.40282e+38;
+
+test(function(t) {
+ var div = addDiv(t);
+ div.style = "width: 1px; height: 1px;";
+ var anim = div.animate([ { transform: 'scale(1)' },
+ { transform: 'scale(3.5e+38)'},
+ { transform: 'scale(3)' } ], 100 * MS_PER_SEC);
+
+ anim.pause();
+ anim.currentTime = 50 * MS_PER_SEC;
+ assert_equals(getComputedStyle(div).transform,
+ 'matrix(' + max_float + ', 0, 0, ' + max_float + ', 0, 0)');
+}, 'Test that the parameter of transform scale is clamped' );
+
+test(function(t) {
+ var div = addDiv(t);
+ div.style = "width: 1px; height: 1px;";
+ var anim = div.animate([ { transform: 'translate(1px)' },
+ { transform: 'translate(3.5e+38px)'},
+ { transform: 'translate(3px)' } ], 100 * MS_PER_SEC);
+
+ anim.pause();
+ anim.currentTime = 50 * MS_PER_SEC;
+ assert_equals(getComputedStyle(div).transform,
+ 'matrix(1, 0, 0, 1, ' + max_float + ', 0)');
+}, 'Test that the parameter of transform translate is clamped' );
+
+test(function(t) {
+ var div = addDiv(t);
+ div.style = "width: 1px; height: 1px;";
+ var anim = div.animate([ { transform: 'matrix(0.5, 0, 0, 0.5, 0, 0)' },
+ { transform: 'matrix(2, 0, 0, 2, 3.5e+38, 0)'},
+ { transform: 'matrix(0, 2, 0, -2, 0, 0)' } ],
+ 100 * MS_PER_SEC);
+
+ anim.pause();
+ anim.currentTime = 50 * MS_PER_SEC;
+ assert_equals(getComputedStyle(div).transform,
+ 'matrix(2, 0, 0, 2, ' + max_float + ', 0)');
+}, 'Test that the parameter of transform matrix is clamped' );
+
+done();
+
+</script>
+</body>
diff --git a/dom/animation/test/mozilla/file_transition_finish_on_compositor.html b/dom/animation/test/mozilla/file_transition_finish_on_compositor.html
new file mode 100644
index 0000000000..4912d05dd1
--- /dev/null
+++ b/dom/animation/test/mozilla/file_transition_finish_on_compositor.html
@@ -0,0 +1,67 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="../testcommon.js"></script>
+<script src="/tests/SimpleTest/paint_listener.js"></script>
+<style>
+div {
+ /* Element needs geometry to be eligible for layerization */
+ width: 100px;
+ height: 100px;
+ background-color: white;
+}
+</style>
+<body>
+<script>
+'use strict';
+
+function waitForPaints() {
+ return new Promise(function(resolve, reject) {
+ waitForAllPaintsFlushed(resolve);
+ });
+}
+
+promise_test(t => {
+ // This test only applies to compositor animations
+ if (!isOMTAEnabled()) {
+ return;
+ }
+
+ var div = addDiv(t, { style: 'transition: transform 50ms; ' +
+ 'transform: translateX(0px)' });
+ getComputedStyle(div).transform;
+
+ div.style.transform = 'translateX(100px)';
+
+ var timeBeforeStart = window.performance.now();
+ return waitForPaints().then(() => {
+ // If it took over 50ms to paint the transition, we have no luck
+ // to test it. This situation will happen if GC runs while waiting for the
+ // paint.
+ if (window.performance.now() - timeBeforeStart >= 50) {
+ return;
+ }
+
+ var transform =
+ SpecialPowers.DOMWindowUtils.getOMTAStyle(div, 'transform');
+ assert_not_equals(transform, '',
+ 'The transition style is applied on the compositor');
+
+ // Generate artificial busyness on the main thread for 100ms.
+ var timeAtStart = window.performance.now();
+ while (window.performance.now() - timeAtStart < 100) {}
+
+ // Now the transition on the compositor should finish but stay at the final
+ // position because there was no chance to pull the transition back from
+ // the compositor.
+ transform =
+ SpecialPowers.DOMWindowUtils.getOMTAStyle(div, 'transform');
+ assert_equals(transform, 'matrix(1, 0, 0, 1, 100, 0)',
+ 'The final transition style is still applied on the ' +
+ 'compositor');
+ });
+}, 'Transition on the compositor keeps the final style while the main thread ' +
+ 'is busy even if the transition finished on the compositor');
+
+done();
+</script>
+</body>
diff --git a/dom/animation/test/mozilla/file_underlying-discrete-value.html b/dom/animation/test/mozilla/file_underlying-discrete-value.html
new file mode 100644
index 0000000000..3be01b9041
--- /dev/null
+++ b/dom/animation/test/mozilla/file_underlying-discrete-value.html
@@ -0,0 +1,192 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="../testcommon.js"></script>
+<body>
+<script>
+"use strict";
+
+// Tests that we correctly extract the underlying value when the animation
+// type is 'discrete'.
+const discreteTests = [
+ {
+ stylesheet: {
+ "@keyframes keyframes":
+ "from { align-content: flex-start; } to { align-content: flex-end; } "
+ },
+ expectedKeyframes: [
+ { computedOffset: 0, alignContent: "flex-start" },
+ { computedOffset: 1, alignContent: "flex-end" }
+ ],
+ explanation: "Test for fully-specified keyframes"
+ },
+ {
+ stylesheet: {
+ "@keyframes keyframes": "from { align-content: flex-start; }"
+ },
+ // The value of 100% should be 'stretch',
+ // but we are not supporting underlying value.
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=1295401
+ expectedKeyframes: [
+ { computedOffset: 0, alignContent: "flex-start" },
+ { computedOffset: 1, alignContent: "unset" }
+ ],
+ explanation: "Test for 0% keyframe only"
+ },
+ {
+ stylesheet: {
+ "@keyframes keyframes": "to { align-content: flex-end; }"
+ },
+ // The value of 0% should be 'stretch',
+ // but we are not supporting underlying value.
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=1295401
+ expectedKeyframes: [
+ { computedOffset: 0, alignContent: "unset" },
+ { computedOffset: 1, alignContent: "flex-end" }
+ ],
+ explanation: "Test for 100% keyframe only"
+ },
+ {
+ stylesheet: {
+ "@keyframes keyframes": "50% { align-content: center; }",
+ "#target": "align-content: space-between;"
+ },
+ expectedKeyframes: [
+ { computedOffset: 0, alignContent: "space-between" },
+ { computedOffset: 0.5, alignContent: "center" },
+ { computedOffset: 1, alignContent: "space-between" }
+ ],
+ explanation: "Test for no 0%/100% keyframes " +
+ "and specified style on target element"
+ },
+ {
+ stylesheet: {
+ "@keyframes keyframes": "50% { align-content: center; }"
+ },
+ attributes: {
+ style: "align-content: space-between"
+ },
+ expectedKeyframes: [
+ { computedOffset: 0, alignContent: "space-between" },
+ { computedOffset: 0.5, alignContent: "center" },
+ { computedOffset: 1, alignContent: "space-between" }
+ ],
+ explanation: "Test for no 0%/100% keyframes " +
+ "and specified style on target element using style attribute"
+ },
+ {
+ stylesheet: {
+ "@keyframes keyframes": "50% { align-content: center; }",
+ "#target": "align-content: inherit;"
+ },
+ // The value of 0%/100% should be 'stretch',
+ // but we are not supporting underlying value.
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=1295401
+ expectedKeyframes: [
+ { computedOffset: 0, alignContent: "inherit" },
+ { computedOffset: 0.5, alignContent: "center" },
+ { computedOffset: 1, alignContent: "inherit" }
+ ],
+ explanation: "Test for no 0%/100% keyframes " +
+ "and 'inherit' specified on target element"
+ },
+ {
+ stylesheet: {
+ "@keyframes keyframes": "50% { align-content: center; }",
+ ".target": "align-content: space-between;"
+ },
+ attributes: {
+ class: "target"
+ },
+ expectedKeyframes: [
+ { computedOffset: 0, alignContent: "space-between" },
+ { computedOffset: 0.5, alignContent: "center" },
+ { computedOffset: 1, alignContent: "space-between" }
+ ],
+ explanation: "Test for no 0%/100% keyframes " +
+ "and specified style on target element using class selector"
+ },
+ {
+ stylesheet: {
+ "@keyframes keyframes": "50% { align-content: center; }",
+ "div": "align-content: space-between;"
+ },
+ expectedKeyframes: [
+ { computedOffset: 0, alignContent: "space-between" },
+ { computedOffset: 0.5, alignContent: "center" },
+ { computedOffset: 1, alignContent: "space-between" }
+ ],
+ explanation: "Test for no 0%/100% keyframes " +
+ "and specified style on target element using type selector"
+ },
+ {
+ stylesheet: {
+ "@keyframes keyframes": "50% { align-content: center; }",
+ "div": "align-content: space-between;",
+ ".target": "align-content: flex-start;",
+ "#target": "align-content: flex-end;"
+ },
+ attributes: {
+ class: "target"
+ },
+ expectedKeyframes: [
+ { computedOffset: 0, alignContent: "flex-end" },
+ { computedOffset: 0.5, alignContent: "center" },
+ { computedOffset: 1, alignContent: "flex-end" }
+ ],
+ explanation: "Test for no 0%/100% keyframes " +
+ "and specified style on target element " +
+ "using ID selector that overrides class selector"
+ },
+ {
+ stylesheet: {
+ "@keyframes keyframes": "50% { align-content: center; }",
+ "div": "align-content: space-between !important;",
+ ".target": "align-content: flex-start;",
+ "#target": "align-content: flex-end;"
+ },
+ attributes: {
+ class: "target"
+ },
+ expectedKeyframes: [
+ { computedOffset: 0, alignContent: "space-between" },
+ { computedOffset: 0.5, alignContent: "center" },
+ { computedOffset: 1, alignContent: "space-between" }
+ ],
+ explanation: "Test for no 0%/100% keyframes " +
+ "and specified style on target element " +
+ "using important type selector that overrides other rules"
+ },
+];
+
+discreteTests.forEach(testcase => {
+ test(t => {
+ addStyle(t, testcase.stylesheet);
+
+ const div = addDiv(t, { "id": "target" });
+ if (testcase.attributes) {
+ for (let attributeName in testcase.attributes) {
+ div.setAttribute(attributeName, testcase.attributes[attributeName]);
+ }
+ }
+ div.style.animation = "keyframes 100s";
+
+ const keyframes = div.getAnimations()[0].effect.getKeyframes();
+ const expectedKeyframes = testcase.expectedKeyframes;
+ assert_equals(keyframes.length, expectedKeyframes.length,
+ `keyframes.length should be ${ expectedKeyframes.length }`);
+
+ keyframes.forEach((keyframe, index) => {
+ const expectedKeyframe = expectedKeyframes[index];
+ assert_equals(keyframe.computedOffset, expectedKeyframe.computedOffset,
+ `computedOffset of keyframes[${ index }] should be ` +
+ `${ expectedKeyframe.computedOffset }`);
+ assert_equals(keyframe.alignContent, expectedKeyframe.alignContent,
+ `alignContent of keyframes[${ index }] should be ` +
+ `${ expectedKeyframe.alignContent }`);
+ });
+ }, testcase.explanation);
+});
+
+done();
+</script>
+</body>
diff --git a/dom/animation/test/mozilla/test_cubic_bezier_limits.html b/dom/animation/test/mozilla/test_cubic_bezier_limits.html
new file mode 100644
index 0000000000..e67e5dbbb0
--- /dev/null
+++ b/dom/animation/test/mozilla/test_cubic_bezier_limits.html
@@ -0,0 +1,14 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+'use strict';
+setup({explicit_done: true});
+SpecialPowers.pushPrefEnv(
+ { "set": [["dom.animations-api.core.enabled", true]]},
+ function() {
+ window.open("file_cubic_bezier_limits.html");
+ });
+</script>
diff --git a/dom/animation/test/mozilla/test_deferred_start.html b/dom/animation/test/mozilla/test_deferred_start.html
new file mode 100644
index 0000000000..4db4bf6766
--- /dev/null
+++ b/dom/animation/test/mozilla/test_deferred_start.html
@@ -0,0 +1,14 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+'use strict';
+setup({explicit_done: true});
+SpecialPowers.pushPrefEnv(
+ { "set": [["dom.animations-api.core.enabled", true]]},
+ function() {
+ window.open("file_deferred_start.html");
+ });
+</script>
diff --git a/dom/animation/test/mozilla/test_disable_animations_api_core.html b/dom/animation/test/mozilla/test_disable_animations_api_core.html
new file mode 100644
index 0000000000..cfb64e5371
--- /dev/null
+++ b/dom/animation/test/mozilla/test_disable_animations_api_core.html
@@ -0,0 +1,14 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+'use strict';
+setup({explicit_done: true});
+SpecialPowers.pushPrefEnv(
+ { "set": [["dom.animations-api.core.enabled", false]]},
+ function() {
+ window.open("file_disable_animations_api_core.html");
+ });
+</script>
diff --git a/dom/animation/test/mozilla/test_disabled_properties.html b/dom/animation/test/mozilla/test_disabled_properties.html
new file mode 100644
index 0000000000..86d02e6b69
--- /dev/null
+++ b/dom/animation/test/mozilla/test_disabled_properties.html
@@ -0,0 +1,14 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+'use strict';
+setup({explicit_done: true});
+SpecialPowers.pushPrefEnv(
+ { "set": [["dom.animations-api.core.enabled", true]]},
+ function() {
+ window.open("file_disabled_properties.html");
+ });
+</script>
diff --git a/dom/animation/test/mozilla/test_discrete-animations.html b/dom/animation/test/mozilla/test_discrete-animations.html
new file mode 100644
index 0000000000..2a36bd50e0
--- /dev/null
+++ b/dom/animation/test/mozilla/test_discrete-animations.html
@@ -0,0 +1,18 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+'use strict';
+setup({explicit_done: true});
+SpecialPowers.pushPrefEnv(
+ { "set": [
+ ["dom.animations-api.core.enabled", true],
+ ["layout.css.osx-font-smoothing.enabled", true],
+ ["layout.css.prefixes.webkit", true]
+ ] },
+ function() {
+ window.open("file_discrete-animations.html");
+ });
+</script>
diff --git a/dom/animation/test/mozilla/test_document-timeline-origin-time-range.html b/dom/animation/test/mozilla/test_document-timeline-origin-time-range.html
new file mode 100644
index 0000000000..f73c233d39
--- /dev/null
+++ b/dom/animation/test/mozilla/test_document-timeline-origin-time-range.html
@@ -0,0 +1,14 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+'use strict';
+setup({explicit_done: true});
+SpecialPowers.pushPrefEnv(
+ { "set": [["dom.animations-api.core.enabled", true]]},
+ function() {
+ window.open("file_document-timeline-origin-time-range.html");
+ });
+</script>
diff --git a/dom/animation/test/mozilla/test_hide_and_show.html b/dom/animation/test/mozilla/test_hide_and_show.html
new file mode 100644
index 0000000000..929a31bd47
--- /dev/null
+++ b/dom/animation/test/mozilla/test_hide_and_show.html
@@ -0,0 +1,14 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+'use strict';
+setup({explicit_done: true});
+SpecialPowers.pushPrefEnv(
+ { "set": [["dom.animations-api.core.enabled", true]]},
+ function() {
+ window.open("file_hide_and_show.html");
+ });
+</script>
diff --git a/dom/animation/test/mozilla/test_partial_keyframes.html b/dom/animation/test/mozilla/test_partial_keyframes.html
new file mode 100644
index 0000000000..28eb4c5881
--- /dev/null
+++ b/dom/animation/test/mozilla/test_partial_keyframes.html
@@ -0,0 +1,14 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+'use strict';
+setup({explicit_done: true});
+SpecialPowers.pushPrefEnv(
+ { "set": [["dom.animations-api.core.enabled", true]]},
+ function() {
+ window.open("file_partial_keyframes.html");
+ });
+</script>
diff --git a/dom/animation/test/mozilla/test_set-easing.html b/dom/animation/test/mozilla/test_set-easing.html
new file mode 100644
index 0000000000..e0069ff1c8
--- /dev/null
+++ b/dom/animation/test/mozilla/test_set-easing.html
@@ -0,0 +1,14 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+'use strict';
+setup({explicit_done: true});
+SpecialPowers.pushPrefEnv(
+ { "set": [["dom.animations-api.core.enabled", true]]},
+ function() {
+ window.open("file_set-easing.html");
+ });
+</script>
diff --git a/dom/animation/test/mozilla/test_spacing_property_order.html b/dom/animation/test/mozilla/test_spacing_property_order.html
new file mode 100644
index 0000000000..afcc12bedf
--- /dev/null
+++ b/dom/animation/test/mozilla/test_spacing_property_order.html
@@ -0,0 +1,14 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+'use strict';
+setup({explicit_done: true});
+SpecialPowers.pushPrefEnv(
+ { "set": [["dom.animations-api.core.enabled", true]]},
+ function() {
+ window.open("file_spacing_property_order.html");
+ });
+</script>
diff --git a/dom/animation/test/mozilla/test_spacing_transform.html b/dom/animation/test/mozilla/test_spacing_transform.html
new file mode 100644
index 0000000000..38dce7e99c
--- /dev/null
+++ b/dom/animation/test/mozilla/test_spacing_transform.html
@@ -0,0 +1,14 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+'use strict';
+setup({explicit_done: true});
+SpecialPowers.pushPrefEnv(
+ { "set": [["dom.animations-api.core.enabled", true]]},
+ function() {
+ window.open("file_spacing_transform.html");
+ });
+</script>
diff --git a/dom/animation/test/mozilla/test_transform_limits.html b/dom/animation/test/mozilla/test_transform_limits.html
new file mode 100644
index 0000000000..6c9b5e4fa9
--- /dev/null
+++ b/dom/animation/test/mozilla/test_transform_limits.html
@@ -0,0 +1,14 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+'use strict';
+setup({explicit_done: true});
+SpecialPowers.pushPrefEnv(
+ { "set": [["dom.animations-api.core.enabled", true]]},
+ function() {
+ window.open("file_transform_limits.html");
+ });
+</script>
diff --git a/dom/animation/test/mozilla/test_transition_finish_on_compositor.html b/dom/animation/test/mozilla/test_transition_finish_on_compositor.html
new file mode 100644
index 0000000000..357e5297e9
--- /dev/null
+++ b/dom/animation/test/mozilla/test_transition_finish_on_compositor.html
@@ -0,0 +1,14 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+'use strict';
+setup({explicit_done: true});
+SpecialPowers.pushPrefEnv(
+ { "set": [["dom.animations-api.core.enabled", true]]},
+ function() {
+ window.open("file_transition_finish_on_compositor.html");
+ });
+</script>
diff --git a/dom/animation/test/mozilla/test_underlying-discrete-value.html b/dom/animation/test/mozilla/test_underlying-discrete-value.html
new file mode 100644
index 0000000000..7feee53a15
--- /dev/null
+++ b/dom/animation/test/mozilla/test_underlying-discrete-value.html
@@ -0,0 +1,15 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+'use strict';
+setup({explicit_done: true});
+SpecialPowers.pushPrefEnv(
+ { "set": [["dom.animations-api.core.enabled", true]]},
+ function() {
+ window.open("file_underlying-discrete-value.html");
+ });
+</script>
+</html>
diff --git a/dom/animation/test/style/file_animation-seeking-with-current-time.html b/dom/animation/test/style/file_animation-seeking-with-current-time.html
new file mode 100644
index 0000000000..c3a5903948
--- /dev/null
+++ b/dom/animation/test/style/file_animation-seeking-with-current-time.html
@@ -0,0 +1,121 @@
+<!doctype html>
+<html>
+ <head>
+ <meta charset=utf-8>
+ <title>Tests for seeking using Animation.currentTime</title>
+ <style>
+.animated-div {
+ margin-left: -10px;
+ animation-timing-function: linear ! important;
+}
+
+@keyframes anim {
+ from { margin-left: 0px; }
+ to { margin-left: 100px; }
+}
+ </style>
+ <script src="../testcommon.js"></script>
+ </head>
+ <body>
+ <script type="text/javascript">
+'use strict';
+
+function assert_marginLeft_equals(target, expect, description) {
+ var marginLeft = parseFloat(getComputedStyle(target).marginLeft);
+ assert_equals(marginLeft, expect, description);
+}
+
+promise_test(function(t) {
+ var div = addDiv(t, {'class': 'animated-div'});
+ div.style.animation = "anim 100s";
+ var animation = div.getAnimations()[0];
+
+ return animation.ready.then(function() {
+ animation.currentTime = 90 * MS_PER_SEC;
+ assert_marginLeft_equals(div, 90,
+ 'Computed style is updated when seeking forwards in active interval');
+
+ animation.currentTime = 10 * MS_PER_SEC;
+ assert_marginLeft_equals(div, 10,
+ 'Computed style is updated when seeking backwards in active interval');
+ });
+}, 'Seeking forwards and backward in active interval');
+
+promise_test(function(t) {
+ var div = addDiv(t, {'class': 'animated-div'});
+ div.style.animation = "anim 100s 100s";
+ var animation = div.getAnimations()[0];
+
+ return animation.ready.then(function(t) {
+ assert_marginLeft_equals(div, -10,
+ 'Computed style is unaffected in before phase with no backwards fill');
+
+ // before -> active (non-active -> active)
+ animation.currentTime = 150 * MS_PER_SEC;
+ assert_marginLeft_equals(div, 50,
+ 'Computed style is updated when seeking forwards from ' +
+ 'not \'in effect\' to \'in effect\' state');
+ });
+}, 'Seeking to non-\'in effect\' from \'in effect\' (before -> active)');
+
+promise_test(function(t) {
+ var div = addDiv(t, {'class': 'animated-div'});
+ div.style.animation = "anim 100s 100s";
+ var animation = div.getAnimations()[0];
+
+ return animation.ready.then(function(t) {
+ // move to after phase
+ animation.currentTime = 250 * MS_PER_SEC;
+ assert_marginLeft_equals(div, -10,
+ 'Computed style is unaffected in after phase with no forwards fill');
+
+ // after -> active (non-active -> active)
+ animation.currentTime = 150 * MS_PER_SEC;
+ assert_marginLeft_equals(div, 50,
+ 'Computed style is updated when seeking backwards from ' +
+ 'not \'in effect\' to \'in effect\' state');
+ });
+}, 'Seeking to non-\'in effect\' from \'in effect\' (after -> active)');
+
+promise_test(function(t) {
+ var div = addDiv(t, {'class': 'animated-div'});
+ div.style.animation = "anim 100s 100s";
+ var animation = div.getAnimations()[0];
+
+ return animation.ready.then(function(t) {
+ // move to active phase
+ animation.currentTime = 150 * MS_PER_SEC;
+ assert_marginLeft_equals(div, 50,
+ 'Computed value is set during active phase');
+
+ // active -> before
+ animation.currentTime = 50 * MS_PER_SEC;
+ assert_marginLeft_equals(div, -10,
+ 'Computed value is not effected after seeking backwards from ' +
+ '\'in effect\' to not \'in effect\' state');
+ });
+}, 'Seeking to \'in effect\' from non-\'in effect\' (active -> before)');
+
+promise_test(function(t) {
+ var div = addDiv(t, {'class': 'animated-div'});
+ div.style.animation = "anim 100s 100s";
+ var animation = div.getAnimations()[0];
+
+ return animation.ready.then(function(t) {
+ // move to active phase
+ animation.currentTime = 150 * MS_PER_SEC;
+ assert_marginLeft_equals(div, 50,
+ 'Computed value is set during active phase');
+
+ // active -> after
+ animation.currentTime = 250 * MS_PER_SEC;
+ assert_marginLeft_equals(div, -10,
+ 'Computed value is not affected after seeking forwards from ' +
+ '\'in effect\' to not \'in effect\' state');
+ });
+}, 'Seeking to \'in effect\' from non-\'in effect\' (active -> after)');
+
+done();
+ </script>
+ </body>
+</html>
diff --git a/dom/animation/test/style/file_animation-seeking-with-start-time.html b/dom/animation/test/style/file_animation-seeking-with-start-time.html
new file mode 100644
index 0000000000..ba09827c63
--- /dev/null
+++ b/dom/animation/test/style/file_animation-seeking-with-start-time.html
@@ -0,0 +1,121 @@
+<!doctype html>
+<html>
+ <head>
+ <meta charset=utf-8>
+ <title>Tests for seeking using Animation.startTime</title>
+ <style>
+.animated-div {
+ margin-left: -10px;
+ animation-timing-function: linear ! important;
+}
+
+@keyframes anim {
+ from { margin-left: 0px; }
+ to { margin-left: 100px; }
+}
+ </style>
+ <script src="../testcommon.js"></script>
+ </head>
+ <body>
+ <script type="text/javascript">
+'use strict';
+
+function assert_marginLeft_equals(target, expect, description) {
+ var marginLeft = parseFloat(getComputedStyle(target).marginLeft);
+ assert_equals(marginLeft, expect, description);
+}
+
+promise_test(function(t) {
+ var div = addDiv(t, {'class': 'animated-div'});
+ div.style.animation = "anim 100s";
+ var animation = div.getAnimations()[0];
+
+ return animation.ready.then(function() {
+ animation.startTime = animation.timeline.currentTime - 90 * MS_PER_SEC
+ assert_marginLeft_equals(div, 90,
+ 'Computed style is updated when seeking forwards in active interval');
+
+ animation.startTime = animation.timeline.currentTime - 10 * MS_PER_SEC;
+ assert_marginLeft_equals(div, 10,
+ 'Computed style is updated when seeking backwards in active interval');
+ });
+}, 'Seeking forwards and backward in active interval');
+
+promise_test(function(t) {
+ var div = addDiv(t, {'class': 'animated-div'});
+ div.style.animation = "anim 100s 100s";
+ var animation = div.getAnimations()[0];
+
+ return animation.ready.then(function(t) {
+ assert_marginLeft_equals(div, -10,
+ 'Computed style is unaffected in before phase with no backwards fill');
+
+ // before -> active (non-active -> active)
+ animation.startTime = animation.timeline.currentTime - 150 * MS_PER_SEC;
+ assert_marginLeft_equals(div, 50,
+ 'Computed style is updated when seeking forwards from ' +
+ 'not \'in effect\' to \'in effect\' state');
+ });
+}, 'Seeking to non-\'in effect\' from \'in effect\' (before -> active)');
+
+promise_test(function(t) {
+ var div = addDiv(t, {'class': 'animated-div'});
+ div.style.animation = "anim 100s 100s";
+ var animation = div.getAnimations()[0];
+
+ return animation.ready.then(function(t) {
+ // move to after phase
+ animation.startTime = animation.timeline.currentTime - 250 * MS_PER_SEC;
+ assert_marginLeft_equals(div, -10,
+ 'Computed style is unaffected in after phase with no forwards fill');
+
+ // after -> active (non-active -> active)
+ animation.startTime = animation.timeline.currentTime - 150 * MS_PER_SEC;
+ assert_marginLeft_equals(div, 50,
+ 'Computed style is updated when seeking backwards from ' +
+ 'not \'in effect\' to \'in effect\' state');
+ });
+}, 'Seeking to non-\'in effect\' from \'in effect\' (after -> active)');
+
+promise_test(function(t) {
+ var div = addDiv(t, {'class': 'animated-div'});
+ div.style.animation = "anim 100s 100s";
+ var animation = div.getAnimations()[0];
+
+ return animation.ready.then(function(t) {
+ // move to active phase
+ animation.startTime = animation.timeline.currentTime - 150 * MS_PER_SEC;
+ assert_marginLeft_equals(div, 50,
+ 'Computed value is set during active phase');
+
+ // active -> before
+ animation.startTime = animation.timeline.currentTime - 50 * MS_PER_SEC;
+ assert_marginLeft_equals(div, -10,
+ 'Computed value is not affected after seeking backwards from ' +
+ '\'in effect\' to not \'in effect\' state');
+ });
+}, 'Seeking to \'in effect\' from non-\'in effect\' (active -> before)');
+
+promise_test(function(t) {
+ var div = addDiv(t, {'class': 'animated-div'});
+ div.style.animation = "anim 100s 100s";
+ var animation = div.getAnimations()[0];
+
+ return animation.ready.then(function(t) {
+ // move to active phase
+ animation.startTime = animation.timeline.currentTime - 150 * MS_PER_SEC;
+ assert_marginLeft_equals(div, 50,
+ 'Computed value is set during active phase');
+
+ // active -> after
+ animation.startTime = animation.timeline.currentTime - 250 * MS_PER_SEC;
+ assert_marginLeft_equals(div, -10,
+ 'Computed value is not affected after seeking forwards from ' +
+ '\'in effect\' to not \'in effect\' state');
+ });
+}, 'Seeking to \'in effect\' from non-\'in effect\' (active -> after)');
+
+done();
+ </script>
+ </body>
+</html>
diff --git a/dom/animation/test/style/file_animation-setting-effect.html b/dom/animation/test/style/file_animation-setting-effect.html
new file mode 100644
index 0000000000..cf50cf2cee
--- /dev/null
+++ b/dom/animation/test/style/file_animation-setting-effect.html
@@ -0,0 +1,125 @@
+<!doctype html>
+<html>
+ <head>
+ <meta charset=utf-8>
+ <title>Tests for setting effects by using Animation.effect</title>
+ <script src='../testcommon.js'></script>
+ </head>
+ <body>
+ <script type='text/javascript'>
+
+'use strict';
+
+test(function(t) {
+ var target = addDiv(t);
+ var anim = new Animation();
+ anim.effect = new KeyframeEffectReadOnly(target,
+ { marginLeft: [ '0px', '100px' ] },
+ 100 * MS_PER_SEC);
+ anim.currentTime = 50 * MS_PER_SEC;
+ assert_equals(getComputedStyle(target).marginLeft, '50px');
+}, 'After setting target effect on an animation with null effect, the ' +
+ 'animation still works');
+
+test(function(t) {
+ var target = addDiv(t);
+ target.style.marginLeft = '10px';
+ var anim = target.animate({ marginLeft: [ '0px', '100px' ] },
+ 100 * MS_PER_SEC);
+ anim.currentTime = 50 * MS_PER_SEC;
+ assert_equals(getComputedStyle(target).marginLeft, '50px');
+
+ anim.effect = null;
+ assert_equals(getComputedStyle(target).marginLeft, '10px');
+}, 'After setting null target effect, the computed style of the target ' +
+ 'element becomes the initial value');
+
+test(function(t) {
+ var target = addDiv(t);
+ var animA = target.animate({ marginLeft: [ '0px', '100px' ] },
+ 100 * MS_PER_SEC);
+ var animB = new Animation();
+ animA.currentTime = 50 * MS_PER_SEC;
+ animB.currentTime = 20 * MS_PER_SEC;
+ assert_equals(getComputedStyle(target).marginLeft, '50px',
+ 'original computed style of the target element');
+
+ animB.effect = animA.effect;
+ assert_equals(getComputedStyle(target).marginLeft, '20px',
+ 'new computed style of the target element');
+}, 'After setting the target effect from an existing animation, the computed ' +
+ 'style of the target effect should reflect the time of the updated ' +
+ 'animation.');
+
+test(function(t) {
+ var target = addDiv(t);
+ target.style.marginTop = '-10px';
+ var animA = target.animate({ marginLeft: [ '0px', '100px' ] },
+ 100 * MS_PER_SEC);
+ var animB = target.animate({ marginTop: [ '0px', '100px' ] },
+ 50 * MS_PER_SEC);
+ animA.currentTime = 50 * MS_PER_SEC;
+ animB.currentTime = 10 * MS_PER_SEC;
+ assert_equals(getComputedStyle(target).marginLeft, '50px',
+ 'original margin-left of the target element');
+ assert_equals(getComputedStyle(target).marginTop, '20px',
+ 'original margin-top of the target element');
+
+ animB.effect = animA.effect;
+ assert_equals(getComputedStyle(target).marginLeft, '10px',
+ 'new margin-left of the target element');
+ assert_equals(getComputedStyle(target).marginTop, '-10px',
+ 'new margin-top of the target element');
+}, 'After setting target effect with an animation to another animation which ' +
+ 'also has an target effect and both animation effects target to the same ' +
+ 'element, the computed style of this element should reflect the time and ' +
+ 'effect of the animation that was set');
+
+test(function(t) {
+ var targetA = addDiv(t);
+ var targetB = addDiv(t);
+ targetB.style.marginLeft = '-10px';
+ var animA = targetA.animate({ marginLeft: [ '0px', '100px' ] },
+ 100 * MS_PER_SEC);
+ var animB = targetB.animate({ marginLeft: [ '0px', '100px' ] },
+ 50 * MS_PER_SEC);
+ animA.currentTime = 50 * MS_PER_SEC;
+ animB.currentTime = 10 * MS_PER_SEC;
+ assert_equals(getComputedStyle(targetA).marginLeft, '50px',
+ 'original margin-left of the first element');
+ assert_equals(getComputedStyle(targetB).marginLeft, '20px',
+ 'original margin-left of the second element');
+
+ animB.effect = animA.effect;
+ assert_equals(getComputedStyle(targetA).marginLeft, '10px',
+ 'new margin-left of the first element');
+ assert_equals(getComputedStyle(targetB).marginLeft, '-10px',
+ 'new margin-left of the second element');
+}, 'After setting target effect with an animation to another animation which ' +
+ 'also has an target effect and these animation effects target to ' +
+ 'different elements, the computed styles of the two elements should ' +
+ 'reflect the time and effect of the animation that was set');
+
+test(function(t) {
+ var target = addDiv(t);
+ var animA = target.animate({ marginLeft: [ '0px', '100px' ] },
+ 50 * MS_PER_SEC);
+ var animB = target.animate({ marginTop: [ '0px', '50px' ] },
+ 100 * MS_PER_SEC);
+ animA.currentTime = 20 * MS_PER_SEC;
+ animB.currentTime = 30 * MS_PER_SEC;
+ assert_equals(getComputedStyle(target).marginLeft, '40px');
+ assert_equals(getComputedStyle(target).marginTop, '15px');
+
+ var effectA = animA.effect;
+ animA.effect = animB.effect;
+ animB.effect = effectA;
+ assert_equals(getComputedStyle(target).marginLeft, '60px');
+ assert_equals(getComputedStyle(target).marginTop, '10px');
+}, 'After swapping effects of two playing animations, both animations are ' +
+ 'still running with the same current time');
+
+done();
+ </script>
+ </body>
+</html>
diff --git a/dom/animation/test/style/file_animation-setting-spacing.html b/dom/animation/test/style/file_animation-setting-spacing.html
new file mode 100644
index 0000000000..6098b74336
--- /dev/null
+++ b/dom/animation/test/style/file_animation-setting-spacing.html
@@ -0,0 +1,111 @@
+<!doctype html>
+<head>
+<meta charset=utf-8>
+<title>Tests for setting spacing by using KeyframeEffect.spacing</title>
+<script src='../testcommon.js'></script>
+</head>
+<body>
+<script>
+'use strict';
+
+function calculateInterpolation(pacedDistances, values, progress) {
+ if (progress == 0.0) {
+ return values[0];
+ } else if (progress == 1.0) {
+ return values[valus.length - 1];
+ }
+
+ const cumDist = pacedDistances.reduce( (prev, curr) => {
+ prev.push(prev.length == 0 ? curr : curr + prev[prev.length - 1]);
+ return prev;
+ }, []);
+
+ const last = cumDist[cumDist.length - 1];
+ const offsets = cumDist.map( (curr) => { return curr / last; } );
+
+ let idx = 0;
+ for (let i = 0; i < offsets.length - 1; ++i) {
+ if (progress >= offsets[i] && progress < offsets[i + 1]) {
+ idx = i;
+ break;
+ }
+ }
+
+ const ratio = (progress - offsets[idx]) / (offsets[idx + 1] - offsets[idx]);
+ return values[idx] + ratio * (values[idx + 1] - values[idx]) + 'px';
+}
+
+promise_test(function(t) {
+ var target = addDiv(t);
+ var anim = target.animate([ { marginLeft: '0px' },
+ { marginLeft: '-20px' },
+ { marginLeft: '100px' },
+ { marginLeft: '50px' } ],
+ 100 * MS_PER_SEC);
+
+ return anim.ready.then(function() {
+ anim.currentTime = 50 * MS_PER_SEC;
+ assert_equals(getComputedStyle(target).marginLeft, '40px',
+ 'computed value before setting a new spacing');
+
+ var dist = [0, 20, 120, 50];
+ var marginLeftValues = [0, -20, 100, 50];
+ anim.effect.spacing = 'paced(margin-left)';
+ assert_equals(getComputedStyle(target).marginLeft,
+ calculateInterpolation(dist, marginLeftValues, 0.5),
+ 'computed value after setting a new spacing');
+ });
+}, 'Test for setting spacing from distribute to paced');
+
+promise_test(function(t) {
+ var target = addDiv(t);
+ var anim = target.animate([ { marginLeft: '0px' },
+ { marginLeft: '-20px' },
+ { marginLeft: '100px' },
+ { marginLeft: '50px' } ],
+ { duration: 100 * MS_PER_SEC,
+ spacing: 'paced(margin-left)' });
+
+ return anim.ready.then(function() {
+ var dist = [0, 20, 120, 50];
+ var marginLeftValues = [0, -20, 100, 50];
+ anim.currentTime = 50 * MS_PER_SEC;
+ assert_equals(getComputedStyle(target).marginLeft,
+ calculateInterpolation(dist, marginLeftValues, 0.5),
+ 'computed value before setting a new spacing');
+
+ anim.effect.spacing = 'distribute';
+ assert_equals(getComputedStyle(target).marginLeft, '40px',
+ 'computed value after setting a new spacing');
+ });
+}, 'Test for setting spacing from paced to distribute');
+
+promise_test(function(t) {
+ var target = addDiv(t);
+ var anim =
+ target.animate([ { marginLeft: '0px', borderRadius: '0%' },
+ { marginLeft: '-20px', borderRadius: '50%' },
+ { marginLeft: '100px', borderRadius: '25%' },
+ { marginLeft: '50px', borderRadius: '100%' } ],
+ { duration: 100 * MS_PER_SEC,
+ spacing: 'paced(margin-left)' });
+
+ return anim.ready.then(function() {
+ var dist = [0, 20, 120, 50];
+ var marginLeftValues = [0, -20, 100, 50];
+ anim.currentTime = 50 * MS_PER_SEC;
+ assert_equals(getComputedStyle(target).marginLeft,
+ calculateInterpolation(dist, marginLeftValues, 0.5),
+ 'computed value before setting a new spacing');
+
+ dist = [0, 50, 25, 75];
+ anim.effect.spacing = 'paced(border-radius)';
+ assert_equals(getComputedStyle(target).marginLeft,
+ calculateInterpolation(dist, marginLeftValues, 0.5),
+ 'computed value after setting a new spacing');
+ });
+}, 'Test for setting spacing from paced to a different paced');
+
+done();
+</script>
+</body>
diff --git a/dom/animation/test/style/test_animation-seeking-with-current-time.html b/dom/animation/test/style/test_animation-seeking-with-current-time.html
new file mode 100644
index 0000000000..386e577886
--- /dev/null
+++ b/dom/animation/test/style/test_animation-seeking-with-current-time.html
@@ -0,0 +1,15 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+'use strict';
+setup({explicit_done: true});
+SpecialPowers.pushPrefEnv(
+ { "set": [["dom.animations-api.core.enabled", true]]},
+ function() {
+ window.open("file_animation-seeking-with-current-time.html");
+ });
+</script>
+</html>
diff --git a/dom/animation/test/style/test_animation-seeking-with-start-time.html b/dom/animation/test/style/test_animation-seeking-with-start-time.html
new file mode 100644
index 0000000000..0a1691a082
--- /dev/null
+++ b/dom/animation/test/style/test_animation-seeking-with-start-time.html
@@ -0,0 +1,15 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+'use strict';
+setup({explicit_done: true});
+SpecialPowers.pushPrefEnv(
+ { "set": [["dom.animations-api.core.enabled", true]]},
+ function() {
+ window.open("file_animation-seeking-with-start-time.html");
+ });
+</script>
+</html>
diff --git a/dom/animation/test/style/test_animation-setting-effect.html b/dom/animation/test/style/test_animation-setting-effect.html
new file mode 100644
index 0000000000..1199b3e752
--- /dev/null
+++ b/dom/animation/test/style/test_animation-setting-effect.html
@@ -0,0 +1,15 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src='/resources/testharness.js'></script>
+<script src='/resources/testharnessreport.js'></script>
+<div id='log'></div>
+<script>
+'use strict';
+setup({explicit_done: true});
+SpecialPowers.pushPrefEnv(
+ { 'set': [['dom.animations-api.core.enabled', true]]},
+ function() {
+ window.open('file_animation-setting-effect.html');
+ });
+</script>
+</html>
diff --git a/dom/animation/test/style/test_animation-setting-spacing.html b/dom/animation/test/style/test_animation-setting-spacing.html
new file mode 100644
index 0000000000..1c703e2a3c
--- /dev/null
+++ b/dom/animation/test/style/test_animation-setting-spacing.html
@@ -0,0 +1,14 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src='/resources/testharness.js'></script>
+<script src='/resources/testharnessreport.js'></script>
+<div id='log'></div>
+<script>
+'use strict';
+setup({explicit_done: true});
+SpecialPowers.pushPrefEnv(
+ { 'set': [['dom.animations-api.core.enabled', true]]},
+ function() {
+ window.open('file_animation-setting-spacing.html');
+ });
+</script>
diff --git a/dom/animation/test/testcommon.js b/dom/animation/test/testcommon.js
new file mode 100644
index 0000000000..b9001e4f42
--- /dev/null
+++ b/dom/animation/test/testcommon.js
@@ -0,0 +1,216 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Use this variable if you specify duration or some other properties
+ * for script animation.
+ * E.g., div.animate({ opacity: [0, 1] }, 100 * MS_PER_SEC);
+ *
+ * NOTE: Creating animations with short duration may cause intermittent
+ * failures in asynchronous test. For example, the short duration animation
+ * might be finished when animation.ready has been fulfilled because of slow
+ * platforms or busyness of the main thread.
+ * Setting short duration to cancel its animation does not matter but
+ * if you don't want to cancel the animation, consider using longer duration.
+ */
+const MS_PER_SEC = 1000;
+
+/* The recommended minimum precision to use for time values[1].
+ *
+ * [1] https://w3c.github.io/web-animations/#precision-of-time-values
+ */
+var TIME_PRECISION = 0.0005; // ms
+
+/*
+ * Allow implementations to substitute an alternative method for comparing
+ * times based on their precision requirements.
+ */
+function assert_times_equal(actual, expected, description) {
+ assert_approx_equals(actual, expected, TIME_PRECISION, description);
+}
+
+/**
+ * Appends a div to the document body and creates an animation on the div.
+ * NOTE: This function asserts when trying to create animations with durations
+ * shorter than 100s because the shorter duration may cause intermittent
+ * failures. If you are not sure how long it is suitable, use 100s; it's
+ * long enough but shorter than our test framework timeout (330s).
+ * If you really need to use shorter durations, use animate() function directly.
+ *
+ * @param t The testharness.js Test object. If provided, this will be used
+ * to register a cleanup callback to remove the div when the test
+ * finishes.
+ * @param attrs A dictionary object with attribute names and values to set on
+ * the div.
+ * @param frames The keyframes passed to Element.animate().
+ * @param options The options passed to Element.animate().
+ */
+function addDivAndAnimate(t, attrs, frames, options) {
+ let animDur = (typeof options === 'object') ?
+ options.duration : options;
+ assert_greater_than_equal(animDur, 100 * MS_PER_SEC,
+ 'Clients of this addDivAndAnimate API must request a duration ' +
+ 'of at least 100s, to avoid intermittent failures from e.g.' +
+ 'the main thread being busy for an extended period');
+
+ return addDiv(t, attrs).animate(frames, options);
+}
+
+/**
+ * Appends a div to the document body.
+ *
+ * @param t The testharness.js Test object. If provided, this will be used
+ * to register a cleanup callback to remove the div when the test
+ * finishes.
+ *
+ * @param attrs A dictionary object with attribute names and values to set on
+ * the div.
+ */
+function addDiv(t, attrs) {
+ var div = document.createElement('div');
+ if (attrs) {
+ for (var attrName in attrs) {
+ div.setAttribute(attrName, attrs[attrName]);
+ }
+ }
+ document.body.appendChild(div);
+ if (t && typeof t.add_cleanup === 'function') {
+ t.add_cleanup(function() {
+ if (div.parentNode) {
+ div.parentNode.removeChild(div);
+ }
+ });
+ }
+ return div;
+}
+
+/**
+ * Appends a style div to the document head.
+ *
+ * @param t The testharness.js Test object. If provided, this will be used
+ * to register a cleanup callback to remove the style element
+ * when the test finishes.
+ *
+ * @param rules A dictionary object with selector names and rules to set on
+ * the style sheet.
+ */
+function addStyle(t, rules) {
+ var extraStyle = document.createElement('style');
+ document.head.appendChild(extraStyle);
+ if (rules) {
+ var sheet = extraStyle.sheet;
+ for (var selector in rules) {
+ sheet.insertRule(selector + '{' + rules[selector] + '}',
+ sheet.cssRules.length);
+ }
+ }
+
+ if (t && typeof t.add_cleanup === 'function') {
+ t.add_cleanup(function() {
+ extraStyle.remove();
+ });
+ }
+}
+
+/**
+ * Promise wrapper for requestAnimationFrame.
+ */
+function waitForFrame() {
+ return new Promise(function(resolve, reject) {
+ window.requestAnimationFrame(resolve);
+ });
+}
+
+/**
+ * Returns a Promise that is resolved after the given number of consecutive
+ * animation frames have occured (using requestAnimationFrame callbacks).
+ *
+ * @param frameCount The number of animation frames.
+ * @param onFrame An optional function to be processed in each animation frame.
+ */
+function waitForAnimationFrames(frameCount, onFrame) {
+ return new Promise(function(resolve, reject) {
+ function handleFrame() {
+ if (onFrame && typeof onFrame === 'function') {
+ onFrame();
+ }
+ if (--frameCount <= 0) {
+ resolve();
+ } else {
+ window.requestAnimationFrame(handleFrame); // wait another frame
+ }
+ }
+ window.requestAnimationFrame(handleFrame);
+ });
+}
+
+/**
+ * Wrapper that takes a sequence of N animations and returns:
+ *
+ * Promise.all([animations[0].ready, animations[1].ready, ... animations[N-1].ready]);
+ */
+function waitForAllAnimations(animations) {
+ return Promise.all(animations.map(function(animation) {
+ return animation.ready;
+ }));
+}
+
+/**
+ * Flush the computed style for the given element. This is useful, for example,
+ * when we are testing a transition and need the initial value of a property
+ * to be computed so that when we synchronouslyet set it to a different value
+ * we actually get a transition instead of that being the initial value.
+ */
+function flushComputedStyle(elem) {
+ var cs = window.getComputedStyle(elem);
+ cs.marginLeft;
+}
+
+if (opener) {
+ for (var funcName of ["async_test", "assert_not_equals", "assert_equals",
+ "assert_approx_equals", "assert_less_than",
+ "assert_less_than_equal", "assert_greater_than",
+ "assert_between_inclusive",
+ "assert_true", "assert_false",
+ "assert_class_string", "assert_throws",
+ "assert_unreached", "promise_test", "test"]) {
+ window[funcName] = opener[funcName].bind(opener);
+ }
+
+ window.EventWatcher = opener.EventWatcher;
+
+ function done() {
+ opener.add_completion_callback(function() {
+ self.close();
+ });
+ opener.done();
+ }
+}
+
+/**
+ * Return a new MutaionObserver which started 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: subtree });
+ return observer;
+}
+
+/**
+ * Returns true if off-main-thread animations.
+ */
+function isOMTAEnabled() {
+ const OMTAPrefKey = 'layers.offmainthreadcomposition.async-animations';
+ return SpecialPowers.DOMWindowUtils.layerManagerRemote &&
+ SpecialPowers.getBoolPref(OMTAPrefKey);
+}