summaryrefslogtreecommitdiffstats
path: root/dom/animation/test
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--dom/animation/test/chrome.toml42
-rw-r--r--dom/animation/test/chrome/file_animate_xrays.html18
-rw-r--r--dom/animation/test/chrome/test_animate_xrays.html40
-rw-r--r--dom/animation/test/chrome/test_animation_observers_async.html654
-rw-r--r--dom/animation/test/chrome/test_animation_observers_sync.html1587
-rw-r--r--dom/animation/test/chrome/test_animation_performance_warning.html1693
-rw-r--r--dom/animation/test/chrome/test_animation_properties.html837
-rw-r--r--dom/animation/test/chrome/test_animation_properties_display.html34
-rw-r--r--dom/animation/test/chrome/test_cssanimation_missing_keyframes.html66
-rw-r--r--dom/animation/test/chrome/test_generated_content_getAnimations.html81
-rw-r--r--dom/animation/test/chrome/test_keyframe_effect_xrays.html45
-rw-r--r--dom/animation/test/chrome/test_mutation_observer_for_element_removal_in_shadow_tree.html45
-rw-r--r--dom/animation/test/chrome/test_running_on_compositor.html1656
-rw-r--r--dom/animation/test/crashtests/1134538.html8
-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.html16
-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.html25
-rw-r--r--dom/animation/test/crashtests/1278485-1.html26
-rw-r--r--dom/animation/test/crashtests/1282691-1.html23
-rw-r--r--dom/animation/test/crashtests/1291413-1.html20
-rw-r--r--dom/animation/test/crashtests/1291413-2.html21
-rw-r--r--dom/animation/test/crashtests/1304886-1.html14
-rw-r--r--dom/animation/test/crashtests/1309198-1.html40
-rw-r--r--dom/animation/test/crashtests/1322291-1.html24
-rw-r--r--dom/animation/test/crashtests/1322291-2.html31
-rw-r--r--dom/animation/test/crashtests/1322382-1.html16
-rw-r--r--dom/animation/test/crashtests/1323114-1.html12
-rw-r--r--dom/animation/test/crashtests/1323114-2.html18
-rw-r--r--dom/animation/test/crashtests/1323119-1.html13
-rw-r--r--dom/animation/test/crashtests/1324554-1.html19
-rw-r--r--dom/animation/test/crashtests/1325193-1.html18
-rw-r--r--dom/animation/test/crashtests/1330190-1.html11
-rw-r--r--dom/animation/test/crashtests/1330190-2.html36
-rw-r--r--dom/animation/test/crashtests/1330513-1.html8
-rw-r--r--dom/animation/test/crashtests/1332588-1.html25
-rw-r--r--dom/animation/test/crashtests/1333539-1.html30
-rw-r--r--dom/animation/test/crashtests/1333539-2.html38
-rw-r--r--dom/animation/test/crashtests/1334582-1.html11
-rw-r--r--dom/animation/test/crashtests/1334582-2.html11
-rw-r--r--dom/animation/test/crashtests/1334583-1.html9
-rw-r--r--dom/animation/test/crashtests/1335998-1.html28
-rw-r--r--dom/animation/test/crashtests/1343589-1.html18
-rw-r--r--dom/animation/test/crashtests/1359658-1.html33
-rw-r--r--dom/animation/test/crashtests/1373712-1.html11
-rw-r--r--dom/animation/test/crashtests/1379606-1.html21
-rw-r--r--dom/animation/test/crashtests/1393605-1.html15
-rw-r--r--dom/animation/test/crashtests/1400022-1.html10
-rw-r--r--dom/animation/test/crashtests/1401809.html14
-rw-r--r--dom/animation/test/crashtests/1411318-1.html15
-rw-r--r--dom/animation/test/crashtests/1467277-1.html6
-rw-r--r--dom/animation/test/crashtests/1468294-1.html7
-rw-r--r--dom/animation/test/crashtests/1524480-1.html37
-rw-r--r--dom/animation/test/crashtests/1575926.html24
-rw-r--r--dom/animation/test/crashtests/1585770.html22
-rw-r--r--dom/animation/test/crashtests/1604500-1.html24
-rw-r--r--dom/animation/test/crashtests/1611847.html23
-rw-r--r--dom/animation/test/crashtests/1612891-1.html15
-rw-r--r--dom/animation/test/crashtests/1612891-2.html15
-rw-r--r--dom/animation/test/crashtests/1612891-3.html10
-rw-r--r--dom/animation/test/crashtests/1633442.html15
-rw-r--r--dom/animation/test/crashtests/1633486.html20
-rw-r--r--dom/animation/test/crashtests/1656419.html23
-rw-r--r--dom/animation/test/crashtests/1699890.html13
-rw-r--r--dom/animation/test/crashtests/1706157.html19
-rw-r--r--dom/animation/test/crashtests/1714421.html8
-rw-r--r--dom/animation/test/crashtests/1807966.html13
-rw-r--r--dom/animation/test/crashtests/1875441.html12
-rw-r--r--dom/animation/test/crashtests/crashtests.list62
-rw-r--r--dom/animation/test/document-timeline/test_document-timeline.html147
-rw-r--r--dom/animation/test/document-timeline/test_request_animation_frame.html27
-rw-r--r--dom/animation/test/mochitest.toml106
-rw-r--r--dom/animation/test/mozilla/empty.html2
-rw-r--r--dom/animation/test/mozilla/file_deferred_start.html179
-rw-r--r--dom/animation/test/mozilla/file_disable_animations_api_compositing.html137
-rw-r--r--dom/animation/test/mozilla/file_disable_animations_api_timelines.html30
-rw-r--r--dom/animation/test/mozilla/file_discrete_animations.html122
-rw-r--r--dom/animation/test/mozilla/file_restyles.html2304
-rw-r--r--dom/animation/test/mozilla/file_transition_finish_on_compositor.html67
-rw-r--r--dom/animation/test/mozilla/test_cascade.html37
-rw-r--r--dom/animation/test/mozilla/test_cubic_bezier_limits.html168
-rw-r--r--dom/animation/test/mozilla/test_deferred_start.html19
-rw-r--r--dom/animation/test/mozilla/test_disable_animations_api_compositing.html14
-rw-r--r--dom/animation/test/mozilla/test_disable_animations_api_timelines.html16
-rw-r--r--dom/animation/test/mozilla/test_disabled_properties.html73
-rw-r--r--dom/animation/test/mozilla/test_discrete_animations.html16
-rw-r--r--dom/animation/test/mozilla/test_distance_of_basic_shape.html91
-rw-r--r--dom/animation/test/mozilla/test_distance_of_filter.html248
-rw-r--r--dom/animation/test/mozilla/test_distance_of_path_function.html140
-rw-r--r--dom/animation/test/mozilla/test_distance_of_transform.html404
-rw-r--r--dom/animation/test/mozilla/test_document_timeline_origin_time_range.html32
-rw-r--r--dom/animation/test/mozilla/test_event_listener_leaks.html43
-rw-r--r--dom/animation/test/mozilla/test_get_animations_on_scroll_animations.html74
-rw-r--r--dom/animation/test/mozilla/test_hide_and_show.html198
-rw-r--r--dom/animation/test/mozilla/test_moz_prefixed_properties.html90
-rw-r--r--dom/animation/test/mozilla/test_restyles.html22
-rw-r--r--dom/animation/test/mozilla/test_restyling_xhr_doc.html106
-rw-r--r--dom/animation/test/mozilla/test_set_easing.html36
-rw-r--r--dom/animation/test/mozilla/test_style_after_finished_on_compositor.html138
-rw-r--r--dom/animation/test/mozilla/test_transform_limits.html56
-rw-r--r--dom/animation/test/mozilla/test_transition_finish_on_compositor.html22
-rw-r--r--dom/animation/test/mozilla/test_underlying_discrete_value.html188
-rw-r--r--dom/animation/test/mozilla/test_unstyled.html54
-rw-r--r--dom/animation/test/mozilla/xhr_doc.html2
-rw-r--r--dom/animation/test/style/test_animation-seeking-with-current-time.html123
-rw-r--r--dom/animation/test/style/test_animation-seeking-with-start-time.html123
-rw-r--r--dom/animation/test/style/test_animation-setting-effect.html127
-rw-r--r--dom/animation/test/style/test_composite.html142
-rw-r--r--dom/animation/test/style/test_interpolation-from-interpolatematrix-to-none.html43
-rw-r--r--dom/animation/test/style/test_missing-keyframe-on-compositor.html577
-rw-r--r--dom/animation/test/style/test_missing-keyframe.html110
-rw-r--r--dom/animation/test/style/test_transform-non-normalizable-rotate3d.html28
-rw-r--r--dom/animation/test/testcommon.js512
121 files changed, 15309 insertions, 0 deletions
diff --git a/dom/animation/test/chrome.toml b/dom/animation/test/chrome.toml
new file mode 100644
index 0000000000..fa1d21457c
--- /dev/null
+++ b/dom/animation/test/chrome.toml
@@ -0,0 +1,42 @@
+[DEFAULT]
+prefs = [
+ "dom.animations-api.compositing.enabled=true",
+ "gfx.omta.background-color=true",
+ "layout.css.basic-shape-rect.enabled=true",
+ "layout.css.basic-shape-xywh.enabled=true",
+ "layout.css.individual-transform.enabled=true",
+ "layout.css.motion-path-basic-shapes.enabled=true",
+ "layout.css.motion-path-coord-box.enabled=true",
+ "layout.css.motion-path-offset-position.enabled=true",
+ "layout.css.motion-path-ray.enabled=true",
+]
+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_async.html"]
+
+["chrome/test_animation_observers_sync.html"]
+
+["chrome/test_animation_performance_warning.html"]
+
+["chrome/test_animation_properties.html"]
+
+["chrome/test_animation_properties_display.html"]
+
+["chrome/test_cssanimation_missing_keyframes.html"]
+
+["chrome/test_generated_content_getAnimations.html"]
+
+["chrome/test_keyframe_effect_xrays.html"]
+
+["chrome/test_mutation_observer_for_element_removal_in_shadow_tree.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..2fa15b1764
--- /dev/null
+++ b/dom/animation/test/chrome/file_animate_xrays.html
@@ -0,0 +1,18 @@
+<!doctype html>
+<html>
+<head>
+<meta charset=utf-8>
+<script>
+Element.prototype.animate = function() {
+ throw 'Called animate() as defined in content document';
+}
+for (let name of ["KeyframeEffect", "Animation"]) {
+ this[name] = function() {
+ throw `Called overridden ${name} constructor`;
+ };
+}
+</script>
+<body>
+<div id="target"></div>
+</body>
+</html>
diff --git a/dom/animation/test/chrome/test_animate_xrays.html b/dom/animation/test/chrome/test_animate_xrays.html
new file mode 100644
index 0000000000..64df6db720
--- /dev/null
+++ b/dom/animation/test/chrome/test_animate_xrays.html
@@ -0,0 +1,40 @@
+<!doctype html>
+<head>
+<meta charset=utf-8>
+<script type="application/javascript" src="../testharness.js"></script>
+<script type="application/javascript" src="../testharnessreport.js"></script>
+<script type="application/javascript" src="../testcommon.js"></script>
+</head>
+<body>
+<a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1414674"
+ target="_blank">Mozilla Bug 1414674</a>
+<div id="log"></div>
+<iframe id="iframe"
+ src="http://example.org/tests/dom/animation/test/chrome/file_animate_xrays.html"></iframe>
+<script>
+'use strict';
+
+var win = document.getElementById('iframe').contentWindow;
+
+async_test(function(t) {
+ window.addEventListener('load', t.step_func(function() {
+ var target = win.document.getElementById('target');
+ var anim = target.animate({opacity: [ 1, 0 ]}, 100 * MS_PER_SEC);
+ // The frames object should be accessible via x-ray.
+ var frames = anim.effect.getKeyframes();
+ assert_equals(frames.length, 2,
+ "frames for Element.animate should be non-zero");
+ assert_equals(frames[0].opacity, "1",
+ "first frame opacity for Element.animate should be specified value");
+ assert_equals(frames[0].computedOffset, 0,
+ "first frame offset for Element.animate should be 0");
+ assert_equals(frames[1].opacity, "0",
+ "last frame opacity for Element.animate should be specified value");
+ assert_equals(frames[1].computedOffset, 1,
+ "last frame offset for Element.animate should be 1");
+ t.done();
+ }));
+}, 'Calling animate() across x-rays');
+
+</script>
+</body>
diff --git a/dom/animation/test/chrome/test_animation_observers_async.html b/dom/animation/test/chrome/test_animation_observers_async.html
new file mode 100644
index 0000000000..912d73a896
--- /dev/null
+++ b/dom/animation/test/chrome/test_animation_observers_async.html
@@ -0,0 +1,654 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title>
+Test chrome-only MutationObserver animation notifications (async tests)
+</title>
+<!--
+
+ This file contains tests for animation mutation observers that require
+ some asynchronous steps (e.g. waiting for animation events).
+
+ Where possible, however, we prefer to write synchronous tests since they are
+ less to timeout when run on automation. These synchronous tests are located
+ in test_animation_observers_sync.html.
+
+-->
+<script type="application/javascript" src="../testharness.js"></script>
+<script type="application/javascript" src="../testharnessreport.js"></script>
+<script src="../testcommon.js"></script>
+<div id="log"></div>
+<style>
+@keyframes anim {
+ to { transform: translate(100px); }
+}
+@keyframes anotherAnim {
+ to { transform: translate(0px); }
+}
+#target {
+ width: 100px;
+ height: 100px;
+ background-color: yellow;
+ line-height: 16px;
+}
+</style>
+<div id=container><div id=target></div></div>
+<script>
+var div = document.getElementById("target");
+var gRecords = [];
+var gObserver = new MutationObserver(newRecords => {
+ gRecords.push(...newRecords);
+});
+
+function setupAsynchronousObserver(t, options) {
+
+ gRecords = [];
+ t.add_cleanup(() => {
+ gObserver.disconnect();
+ });
+ gObserver.observe(options.subtree ? div.parentNode : div,
+ { animations: true, subtree: options.subtree });
+}
+
+// Adds an event listener and returns a Promise that is resolved when the
+// event listener is called.
+function await_event(aElement, aEventName) {
+ return new Promise(aResolve => {
+ function listener(aEvent) {
+ aElement.removeEventListener(aEventName, listener);
+ aResolve();
+ }
+ aElement.addEventListener(aEventName, listener);
+ });
+}
+
+function assert_record_list(actual, expected, desc, index, listName) {
+ assert_equals(actual.length, expected.length,
+ `${desc} - record[${index}].${listName} length`);
+ if (actual.length != expected.length) {
+ return;
+ }
+ for (var i = 0; i < actual.length; i++) {
+ assert_not_equals(actual.indexOf(expected[i]), -1,
+ `${desc} - record[${index}].${listName} contains expected Animation`);
+ }
+}
+
+function assert_records(expected, desc) {
+ var records = gRecords;
+ gRecords = [];
+ assert_equals(records.length, expected.length, `${desc} - number of records`);
+ if (records.length != expected.length) {
+ return;
+ }
+ for (var i = 0; i < records.length; i++) {
+ assert_record_list(records[i].addedAnimations, expected[i].added, desc, i, "addedAnimations");
+ assert_record_list(records[i].changedAnimations, expected[i].changed, desc, i, "changedAnimations");
+ assert_record_list(records[i].removedAnimations, expected[i].removed, desc, i, "removedAnimations");
+ }
+}
+
+function assert_records_any_order(expected, desc) {
+ // Generate a unique label for each Animation object.
+ let animation_labels = new Map();
+ let animation_counter = 0;
+ for (let record of gRecords) {
+ for (let a of [...record.addedAnimations, ...record.changedAnimations, ...record.removedAnimations]) {
+ if (!animation_labels.has(a)) {
+ animation_labels.set(a, ++animation_counter);
+ }
+ }
+ }
+ for (let record of expected) {
+ for (let a of [...record.added, ...record.changed, ...record.removed]) {
+ if (!animation_labels.has(a)) {
+ animation_labels.set(a, ++animation_counter);
+ }
+ }
+ }
+
+ function record_label(record) {
+ // Generate a label of the form:
+ //
+ // <added-animations>:<changed-animations>:<removed-animations>
+ let added = record.addedAnimations || record.added;
+ let changed = record.changedAnimations || record.changed;
+ let removed = record.removedAnimations || record.removed;
+ return [added .map(a => animation_labels.get(a)).sort().join(),
+ changed.map(a => animation_labels.get(a)).sort().join(),
+ removed.map(a => animation_labels.get(a)).sort().join()]
+ .join(":");
+ }
+
+ // Sort records by their label.
+ gRecords.sort((a, b) => record_label(a) < record_label(b));
+ expected.sort((a, b) => record_label(a) < record_label(b));
+
+ // Assert the sorted record lists are equal.
+ assert_records(expected, desc);
+}
+
+// -- Tests ------------------------------------------------------------------
+
+// We run all tests first targeting the div and observing the div, then again
+// targeting the div and observing its parent while using the subtree:true
+// MutationObserver option.
+
+function runTest() {
+ [
+ { observe: div, target: div, subtree: false },
+ { observe: div.parentNode, target: div, subtree: true },
+ ].forEach(aOptions => {
+
+ var e = aOptions.target;
+
+ promise_test(t => {
+ setupAsynchronousObserver(t, aOptions);
+ // Clear all styles once test finished since we re-use the same element
+ // in all test cases.
+ t.add_cleanup(() => {
+ e.style = "";
+ flushComputedStyle(e);
+ });
+
+ // Start a transition.
+ e.style = "transition: background-color 100s; background-color: lime;";
+
+ // Register for the end of the transition.
+ var transitionEnd = await_event(e, "transitionend");
+
+ // The transition should cause the creation of a single Animation.
+ var animations = e.getAnimations();
+ assert_equals(animations.length, 1,
+ "getAnimations().length after transition start");
+
+ // Wait for the single MutationRecord for the Animation addition to
+ // be delivered.
+ return waitForFrame().then(() => {
+ assert_records([{ added: animations, changed: [], removed: [] }],
+ "records after transition start");
+
+ // Advance until near the end of the transition, then wait for it to
+ // finish.
+ animations[0].currentTime = 99900;
+ }).then(() => {
+ return transitionEnd;
+ }).then(() => {
+ // After the transition has finished, the Animation should disappear.
+ assert_equals(e.getAnimations().length, 0,
+ "getAnimations().length after transition end");
+
+ // Wait for the change MutationRecord for seeking the Animation to be
+ // delivered, followed by the removal MutationRecord.
+ return waitForFrame();
+ }).then(() => {
+ assert_records([{ added: [], changed: animations, removed: [] },
+ { added: [], changed: [], removed: animations }],
+ "records after transition end");
+ });
+ }, `single_transition ${aOptions.subtree ? ': subtree' : ''}`);
+
+ // Test that starting a single animation that completes normally
+ // dispatches an added notification and then a removed notification.
+ promise_test(t => {
+ setupAsynchronousObserver(t, aOptions);
+ t.add_cleanup(() => {
+ e.style = "";
+ flushComputedStyle(e);
+ });
+
+ // Start an animation.
+ e.style = "animation: anim 100s;";
+
+ // Register for the end of the animation.
+ var animationEnd = await_event(e, "animationend");
+
+ // The animation should cause the creation of a single Animation.
+ var animations = e.getAnimations();
+ assert_equals(animations.length, 1,
+ "getAnimations().length after animation start");
+
+ // Wait for the single MutationRecord for the Animation addition to
+ // be delivered.
+ return waitForFrame().then(() => {
+ assert_records([{ added: animations, changed: [], removed: [] }],
+ "records after animation start");
+
+ // Advance until near the end of the animation, then wait for it to finish.
+ animations[0].currentTime = 99900;
+ return animationEnd;
+ }).then(() => {
+ // After the animation has finished, the Animation should disappear.
+ assert_equals(e.getAnimations().length, 0,
+ "getAnimations().length after animation end");
+
+ // Wait for the change MutationRecord from seeking the Animation to
+ // be delivered, followed by a further MutationRecord for the Animation
+ // removal.
+ return waitForFrame();
+ }).then(() => {
+ assert_records([{ added: [], changed: animations, removed: [] },
+ { added: [], changed: [], removed: animations }],
+ "records after animation end");
+ });
+ }, `single_animation ${aOptions.subtree ? ': subtree' : ''}`);
+
+ // Test that starting a single animation that is cancelled by updating
+ // the animation-fill-mode property dispatches an added notification and
+ // then a removed notification.
+ promise_test(t => {
+ setupAsynchronousObserver(t, aOptions);
+ t.add_cleanup(() => {
+ e.style = "";
+ flushComputedStyle(e);
+ });
+
+ // Start a short, filled animation.
+ e.style = "animation: anim 100s forwards;";
+
+ // Register for the end of the animation.
+ var animationEnd = await_event(e, "animationend");
+
+ // The animation should cause the creation of a single Animation.
+ var animations = e.getAnimations();
+ assert_equals(animations.length, 1,
+ "getAnimations().length after animation start");
+
+ // Wait for the single MutationRecord for the Animation addition to
+ // be delivered.
+ return waitForFrame().then(() => {
+ assert_records([{ added: animations, changed: [], removed: [] }],
+ "records after animation start");
+
+ // Advance until near the end of the animation, then wait for it to finish.
+ animations[0].currentTime = 99900;
+ return animationEnd;
+ }).then(() => {
+ // The only MutationRecord at this point should be the change from
+ // seeking the Animation.
+ assert_records([{ added: [], changed: animations, removed: [] }],
+ "records after animation starts filling");
+
+ // Cancel the animation by setting animation-fill-mode.
+ e.style.animationFillMode = "none";
+ // Explicitly flush style to make sure the above style change happens.
+ // Normally we don't need explicit style flush if there is a waitForFrame()
+ // call but in this particular case we are in the middle of animation events'
+ // callback handling and requestAnimationFrame handling so that we have no
+ // chance to process styling even after the requestAnimationFrame handling.
+ flushComputedStyle(e);
+
+ // Wait for the single MutationRecord for the Animation removal to
+ // be delivered.
+ return waitForFrame();
+ }).then(() => {
+ assert_records([{ added: [], changed: [], removed: animations }],
+ "records after animation end");
+ });
+ }, `single_animation_cancelled_fill ${aOptions.subtree ? ': subtree' : ''}`);
+
+ // Test that calling finish() on a paused (but otherwise finished) animation
+ // dispatches a changed notification.
+ promise_test(t => {
+ setupAsynchronousObserver(t, aOptions);
+ t.add_cleanup(() => {
+ e.style = "";
+ flushComputedStyle(e);
+ });
+
+ // Start a long animation
+ e.style = "animation: anim 100s forwards";
+
+ // The animation should cause the creation of a single Animation.
+ var animations = e.getAnimations();
+ assert_equals(animations.length, 1,
+ "getAnimations().length after animation start");
+
+ // Wait for the single MutationRecord for the Animation addition to
+ // be delivered.
+ return waitForFrame().then(() => {
+ assert_records([{ added: animations, changed: [], removed: [] }],
+ "records after animation start");
+
+ // Wait until the animation is playing.
+ return animations[0].ready;
+ }).then(() => {
+ // Finish and pause.
+ animations[0].finish();
+ animations[0].pause();
+
+ // Wait for the pause to complete.
+ return animations[0].ready;
+ }).then(() => {
+ assert_true(
+ !animations[0].pending && animations[0].playState === "paused",
+ "playState after finishing and pausing");
+
+ // We should have two MutationRecords for the Animation changes:
+ // one for the finish, one for the pause.
+ assert_records([{ added: [], changed: animations, removed: [] },
+ { added: [], changed: animations, removed: [] }],
+ "records after finish() and pause()");
+
+ // Call finish() again.
+ animations[0].finish();
+ assert_equals(animations[0].playState, "finished",
+ "playState after finishing from paused state");
+
+ // Wait for the single MutationRecord for the Animation change to
+ // be delivered. Even though the currentTime does not change, the
+ // playState will change.
+ return waitForFrame();
+ }).then(() => {
+ assert_records([{ added: [], changed: animations, removed: [] }],
+ "records after finish() and pause()");
+
+ // Cancel the animation.
+ e.style = "";
+
+ // Wait for the single removal notification.
+ return waitForFrame();
+ }).then(() => {
+ assert_records([{ added: [], changed: [], removed: animations }],
+ "records after animation end");
+ });
+ }, `finish_from_pause ${aOptions.subtree ? ': subtree' : ''}`);
+
+ // Test that calling play() on a paused Animation dispatches a changed
+ // notification.
+ promise_test(t => {
+ setupAsynchronousObserver(t, aOptions);
+ t.add_cleanup(() => {
+ e.style = "";
+ flushComputedStyle(e);
+ });
+
+ // Start a long, paused animation
+ e.style = "animation: anim 100s paused";
+
+ // The animation should cause the creation of a single Animation.
+ var animations = e.getAnimations();
+ assert_equals(animations.length, 1,
+ "getAnimations().length after animation start");
+
+ // Wait for the single MutationRecord for the Animation addition to
+ // be delivered.
+ return waitForFrame().then(() => {
+ assert_records([{ added: animations, changed: [], removed: [] }],
+ "records after animation start");
+
+ // Wait until the animation is ready
+ return animations[0].ready;
+ }).then(() => {
+ // Play
+ animations[0].play();
+
+ // Wait for the single MutationRecord for the Animation change to
+ // be delivered.
+ return animations[0].ready;
+ }).then(() => {
+ assert_records([{ added: [], changed: animations, removed: [] }],
+ "records after play()");
+
+ // Redundant play
+ animations[0].play();
+
+ // Wait to ensure no change is dispatched
+ return waitForFrame();
+ }).then(() => {
+ assert_records([], "records after redundant play()");
+
+ // Cancel the animation.
+ e.style = "";
+
+ // Wait for the single removal notification.
+ return waitForFrame();
+ }).then(() => {
+ assert_records([{ added: [], changed: [], removed: animations }],
+ "records after animation end");
+ });
+ }, `play ${aOptions.subtree ? ': subtree' : ''}`);
+
+ // Test that a non-cancelling change to an animation followed immediately by a
+ // cancelling change will only send an animation removal notification.
+ promise_test(t => {
+ setupAsynchronousObserver(t, aOptions);
+ t.add_cleanup(() => {
+ e.style = "";
+ flushComputedStyle(e);
+ });
+
+ // Start a long animation.
+ e.style = "animation: anim 100s;";
+
+ // The animation should cause the creation of a single Animation.
+ var animations = e.getAnimations();
+ assert_equals(animations.length, 1,
+ "getAnimations().length after animation start");
+
+ // Wait for the single MutationRecord for the Animation addition to
+ // be delivered.
+ return waitForFrame().then(() => {;
+ assert_records([{ added: animations, changed: [], removed: [] }],
+ "records after animation start");
+
+ // Update the animation's delay such that it is still running.
+ e.style.animationDelay = "-1s";
+
+ // Then cancel the animation by updating its duration.
+ e.style.animationDuration = "0.5s";
+
+ // We should get a single removal notification.
+ return waitForFrame();
+ }).then(() => {
+ assert_records([{ added: [], changed: [], removed: animations }],
+ "records after animation end");
+ });
+ }, `coalesce_change_cancel ${aOptions.subtree ? ': subtree' : ''}`);
+
+ });
+}
+
+promise_test(async t => {
+ setupAsynchronousObserver(t, { observe: div, subtree: true });
+ t.add_cleanup(() => {
+ div.style = "";
+ flushComputedStyle(div);
+ });
+
+ // Add style for pseudo elements
+ var extraStyle = document.createElement('style');
+ document.head.appendChild(extraStyle);
+ var sheet = extraStyle.sheet;
+ var rules = { ".before::before": "animation: anim 100s; content: '';",
+ ".after::after" : "animation: anim 100s, anim 100s; " +
+ "content: '';"};
+ for (var selector in rules) {
+ sheet.insertRule(selector + '{' + rules[selector] + '}',
+ sheet.cssRules.length);
+ }
+
+ // Create a tree with two children:
+ //
+ // div
+ // (::before)
+ // (::after)
+ // / \
+ // childA childB(::before)
+ var childA = document.createElement("div");
+ var childB = document.createElement("div");
+
+ div.appendChild(childA);
+ div.appendChild(childB);
+
+ // Start an animation on each (using order: childB, div, childA)
+ //
+ // We include multiple animations on some nodes so that we can test batching
+ // works as expected later in this test.
+ childB.style = "animation: anim 100s";
+ div.style = "animation: anim 100s, anim 100s, anim 100s";
+ childA.style = "animation: anim 100s, anim 100s";
+
+ // Start animations targeting to pseudo element of div and childB.
+ childB.classList.add("before");
+ div.classList.add("after");
+ div.classList.add("before");
+
+ // Check all animations we have in this document
+ var docAnims = document.getAnimations();
+ assert_equals(docAnims.length, 10, "total animations");
+
+ var divAnimations = div.getAnimations();
+ var childAAnimations = childA.getAnimations();
+ var childBAnimations = childB.getAnimations();
+
+ var divBeforeAnimations =
+ docAnims.filter(x => (x.effect.target == div &&
+ x.effect.pseudoElement == "::before"));
+ var divAfterAnimations =
+ docAnims.filter(x => (x.effect.target == div &&
+ x.effect.pseudoElement == "::after"));
+ var childBPseudoAnimations =
+ docAnims.filter(x => (x.effect.target == childB &&
+ x.effect.pseudoElement == "::before"));
+
+ var seekRecords;
+ // The order in which we get the corresponding records is currently
+ // based on the order we visit these nodes when updating styles.
+ //
+ // That is because we don't do any document-level batching of animation
+ // mutation records when we flush styles. We may introduce that in the
+ // future but for now all we are interested in testing here is that the
+ // right records are generated, but we allow them to occur in any order.
+ await waitForFrame();
+
+ assert_records_any_order(
+ [{ added: divAfterAnimations, changed: [], removed: [] },
+ { added: childAAnimations, changed: [], removed: [] },
+ { added: childBAnimations, changed: [], removed: [] },
+ { added: childBPseudoAnimations, changed: [], removed: [] },
+ { added: divAnimations, changed: [], removed: [] },
+ { added: divBeforeAnimations, changed: [], removed: [] }],
+ "records after simultaneous animation start");
+
+ // The one case where we *do* currently perform document-level (or actually
+ // timeline-level) batching is when animations are updated from a refresh
+ // driver tick. In particular, this means that when animations finish
+ // naturally the removed records should be dispatched according to the
+ // position of the elements in the tree.
+
+ // First, flatten the set of animations. we put the animations targeting to
+ // pseudo elements last. (Actually, we don't care the order in the list.)
+ var animations = [ ...divAnimations,
+ ...childAAnimations,
+ ...childBAnimations,
+ ...divBeforeAnimations,
+ ...divAfterAnimations,
+ ...childBPseudoAnimations ];
+
+ await Promise.all(animations.map(animation => animation.ready));
+
+ // Fast-forward to *just* before the end of the animation.
+ animations.forEach(animation => animation.currentTime = 99999);
+
+ // Prepare the set of expected change MutationRecords, one for each
+ // animation that was seeked.
+ seekRecords = animations.map(
+ p => ({ added: [], changed: [p], removed: [] })
+ );
+
+ await Promise.all(animations.map(animation => animation.finished));
+
+ // After the changed notifications, which will be dispatched in the order that
+ // the animations were seeked, we should get removal MutationRecords in order
+ // (div, div::before, div::after), childA, (childB, childB::before).
+ // Note: The animations targeting to the pseudo element are appended after
+ // the animations of its parent element.
+ divAnimations = [ ...divAnimations,
+ ...divBeforeAnimations,
+ ...divAfterAnimations ];
+ childBAnimations = [ ...childBAnimations, ...childBPseudoAnimations ];
+ assert_records(seekRecords.concat(
+ { added: [], changed: [], removed: divAnimations },
+ { added: [], changed: [], removed: childAAnimations },
+ { added: [], changed: [], removed: childBAnimations }),
+ "records after finishing");
+
+ // Clean up
+ div.classList.remove("before");
+ div.classList.remove("after");
+ div.style = "";
+ childA.remove();
+ childB.remove();
+ extraStyle.remove();
+}, "tree_ordering: subtree");
+
+// Test that animations removed by auto-removal trigger an event
+promise_test(async t => {
+ setupAsynchronousObserver(t, { observe: div, subtree: false });
+
+ // Start two animations such that one will be auto-removed
+ const animA = div.animate(
+ { opacity: 1 },
+ { duration: 100 * MS_PER_SEC, fill: 'forwards' }
+ );
+ const animB = div.animate(
+ { opacity: 1 },
+ { duration: 100 * MS_PER_SEC, fill: 'forwards' }
+ );
+
+ // Wait for the MutationRecords corresponding to each addition.
+ await waitForNextFrame();
+
+ assert_records(
+ [
+ { added: [animA], changed: [], removed: [] },
+ { added: [animB], changed: [], removed: [] },
+ ],
+ 'records after animation start'
+ );
+
+ // Finish the animations -- this should cause animA to be replaced, and
+ // automatically removed.
+ animA.finish();
+ animB.finish();
+
+ // Wait for the MutationRecords corresponding to the timing changes and the
+ // subsequent removal to be delivered.
+ await waitForNextFrame();
+
+ assert_records(
+ [
+ { added: [], changed: [animA], removed: [] },
+ { added: [], changed: [animB], removed: [] },
+ { added: [], changed: [], removed: [animA] },
+ ],
+ 'records after finishing'
+ );
+
+ // Restore animA.
+ animA.persist();
+
+ // Wait for the MutationRecord corresponding to the re-addition of animA.
+ await waitForNextFrame();
+
+ assert_records(
+ [{ added: [animA], changed: [], removed: [] }],
+ 'records after persisting'
+ );
+
+ // Tidy up
+ animA.cancel();
+ animB.cancel();
+
+ await waitForNextFrame();
+
+ assert_records(
+ [
+ { added: [], changed: [], removed: [animA] },
+ { added: [], changed: [], removed: [animB] },
+ ],
+ 'records after tidying up end'
+ );
+}, 'Animations automatically removed are reported');
+runTest();
+</script>
diff --git a/dom/animation/test/chrome/test_animation_observers_sync.html b/dom/animation/test/chrome/test_animation_observers_sync.html
new file mode 100644
index 0000000000..ec760031e1
--- /dev/null
+++ b/dom/animation/test/chrome/test_animation_observers_sync.html
@@ -0,0 +1,1587 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title>
+Test chrome-only MutationObserver animation notifications (sync tests)
+</title>
+<!--
+
+ This file contains synchronous tests for animation mutation observers.
+
+ In general we prefer to write synchronous tests since they are less likely to
+ timeout when run on automation. Tests that require asynchronous steps (e.g.
+ waiting on events) should be added to test_animations_observers_async.html
+ instead.
+
+-->
+<script type="application/javascript" src="../testharness.js"></script>
+<script type="application/javascript" src="../testharnessreport.js"></script>
+<script type="application/javascript" src="../testcommon.js"></script>
+<div id="log"></div>
+<style>
+@keyframes anim {
+ to { transform: translate(100px); }
+}
+@keyframes anotherAnim {
+ to { transform: translate(0px); }
+}
+</style>
+<script>
+
+/**
+ * Return a new MutationObserver which observing |target| element
+ * with { animations: true, subtree: |subtree| } option.
+ *
+ * NOTE: This observer should be used only with takeRecords(). If any of
+ * MutationRecords are observed in the callback of the MutationObserver,
+ * it will raise an assertion.
+ */
+function setupSynchronousObserver(t, target, subtree) {
+ var observer = new MutationObserver(records => {
+ assert_unreached("Any MutationRecords should not be observed in this " +
+ "callback");
+ });
+ t.add_cleanup(() => {
+ observer.disconnect();
+ });
+ observer.observe(target, { animations: true, subtree });
+ return observer;
+}
+
+function assert_record_list(actual, expected, desc, index, listName) {
+ assert_equals(actual.length, expected.length,
+ `${desc} - record[${index}].${listName} length`);
+ if (actual.length != expected.length) {
+ return;
+ }
+ for (var i = 0; i < actual.length; i++) {
+ assert_not_equals(actual.indexOf(expected[i]), -1,
+ `${desc} - record[${index}].${listName} contains expected Animation`);
+ }
+}
+
+function assert_equals_records(actual, expected, desc) {
+ assert_equals(actual.length, expected.length, `${desc} - number of records`);
+ if (actual.length != expected.length) {
+ return;
+ }
+ for (var i = 0; i < actual.length; i++) {
+ assert_record_list(actual[i].addedAnimations,
+ expected[i].added, desc, i, "addedAnimations");
+ assert_record_list(actual[i].changedAnimations,
+ expected[i].changed, desc, i, "changedAnimations");
+ assert_record_list(actual[i].removedAnimations,
+ expected[i].removed, desc, i, "removedAnimations");
+ }
+}
+
+function runTest() {
+ [ { subtree: false },
+ { subtree: true }
+ ].forEach(aOptions => {
+ test(t => {
+ var div = addDiv(t);
+ var observer =
+ setupSynchronousObserver(t,
+ aOptions.subtree ? div.parentNode : div,
+ aOptions.subtree);
+
+ var anim = div.animate({ opacity: [ 0, 1 ] }, 200 * MS_PER_SEC);
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [anim], changed: [], removed: [] }],
+ "records after animation is added");
+
+ anim.effect.updateTiming({ duration: 100 * MS_PER_SEC });
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [anim], removed: [] }],
+ "records after duration is changed");
+
+ anim.effect.updateTiming({ duration: 100 * MS_PER_SEC });
+ assert_equals_records(observer.takeRecords(),
+ [], "records after assigning same value");
+
+ anim.currentTime = anim.effect.getComputedTiming().duration * 2;
+ anim.finish();
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [], removed: [anim] }],
+ "records after animation end");
+
+ anim.effect.updateTiming({
+ duration: anim.effect.getComputedTiming().duration * 3
+ });
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [anim], changed: [], removed: [] }],
+ "records after animation restarted");
+
+ anim.effect.updateTiming({ duration: 'auto' });
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [], removed: [anim] }],
+ "records after duration set \"auto\"");
+
+ anim.effect.updateTiming({ duration: 'auto' });
+ assert_equals_records(observer.takeRecords(),
+ [], "records after assigning same value \"auto\"");
+ }, "change_duration_and_currenttime");
+
+ test(t => {
+ var div = addDiv(t);
+ var observer =
+ setupSynchronousObserver(t,
+ aOptions.subtree ? div.parentNode : div,
+ aOptions.subtree);
+
+ var anim = div.animate({ opacity: [ 0, 1 ] }, 100 * MS_PER_SEC);
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [anim], changed: [], removed: [] }],
+ "records after animation is added");
+
+ anim.effect.updateTiming({ endDelay: 10 * MS_PER_SEC });
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [anim], removed: [] }],
+ "records after endDelay is changed");
+
+ anim.effect.updateTiming({ endDelay: 10 * MS_PER_SEC });
+ assert_equals_records(observer.takeRecords(),
+ [], "records after assigning same value");
+
+ anim.currentTime = 109 * MS_PER_SEC;
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [], removed: [anim] }],
+ "records after currentTime during endDelay");
+
+ anim.effect.updateTiming({ endDelay: -110 * MS_PER_SEC });
+ assert_equals_records(observer.takeRecords(),
+ [], "records after assigning negative value");
+ }, "change_enddelay_and_currenttime");
+
+ test(t => {
+ var div = addDiv(t);
+ var observer =
+ setupSynchronousObserver(t,
+ aOptions.subtree ? div.parentNode : div,
+ aOptions.subtree);
+
+ var anim = div.animate({ opacity: [ 0, 1 ] },
+ { duration: 100 * MS_PER_SEC,
+ endDelay: -100 * MS_PER_SEC });
+ assert_equals_records(observer.takeRecords(),
+ [], "records after animation is added");
+ }, "zero_end_time");
+
+ test(t => {
+ var div = addDiv(t);
+ var observer =
+ setupSynchronousObserver(t,
+ aOptions.subtree ? div.parentNode : div,
+ aOptions.subtree);
+
+ var anim = div.animate({ opacity: [ 0, 1 ] }, 100 * MS_PER_SEC);
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [anim], changed: [], removed: [] }],
+ "records after animation is added");
+
+ anim.effect.updateTiming({ iterations: 2 });
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [anim], removed: [] }],
+ "records after iterations is changed");
+
+ anim.effect.updateTiming({ iterations: 2 });
+ assert_equals_records(observer.takeRecords(),
+ [], "records after assigning same value");
+
+ anim.effect.updateTiming({ iterations: 0 });
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [], removed: [anim] }],
+ "records after animation end");
+
+ anim.effect.updateTiming({ iterations: Infinity });
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [anim], changed: [], removed: [] }],
+ "records after animation restarted");
+ }, "change_iterations");
+
+ test(t => {
+ var div = addDiv(t);
+ var observer =
+ setupSynchronousObserver(t,
+ aOptions.subtree ? div.parentNode : div,
+ aOptions.subtree);
+
+ var anim = div.animate({ opacity: [ 0, 1 ] }, 100 * MS_PER_SEC);
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [anim], changed: [], removed: [] }],
+ "records after animation is added");
+
+ anim.effect.updateTiming({ delay: 100 });
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [anim], removed: [] }],
+ "records after delay is changed");
+
+ anim.effect.updateTiming({ delay: 100 });
+ assert_equals_records(observer.takeRecords(),
+ [], "records after assigning same value");
+
+ anim.effect.updateTiming({ delay: -100 * MS_PER_SEC });
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [], removed: [anim] }],
+ "records after animation end");
+
+ anim.effect.updateTiming({ delay: 0 });
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [anim], changed: [], removed: [] }],
+ "records after animation restarted");
+ }, "change_delay");
+
+ test(t => {
+ var div = addDiv(t);
+ var observer =
+ setupSynchronousObserver(t,
+ aOptions.subtree ? div.parentNode : div,
+ aOptions.subtree);
+
+ var anim = div.animate({ opacity: [ 0, 1 ] },
+ { duration: 100 * MS_PER_SEC,
+ easing: "steps(2, start)" });
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [anim], changed: [], removed: [] }],
+ "records after animation is added");
+
+ anim.effect.updateTiming({ easing: "steps(2, end)" });
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [anim], removed: [] }],
+ "records after easing is changed");
+
+ anim.effect.updateTiming({ easing: "steps(2, end)" });
+ assert_equals_records(observer.takeRecords(),
+ [], "records after assigning same value");
+ }, "change_easing");
+
+ test(t => {
+ var div = addDiv(t);
+ var observer =
+ setupSynchronousObserver(t,
+ aOptions.subtree ? div.parentNode : div,
+ aOptions.subtree);
+
+ var anim = div.animate({ opacity: [ 0, 1 ] },
+ { duration: 100, delay: -100 });
+ assert_equals_records(observer.takeRecords(),
+ [], "records after assigning negative value");
+ }, "negative_delay_in_constructor");
+
+ test(t => {
+ var div = addDiv(t);
+ var observer =
+ setupSynchronousObserver(t,
+ aOptions.subtree ? div.parentNode : div,
+ aOptions.subtree);
+
+ var effect = new KeyframeEffect(null,
+ { opacity: [ 0, 1 ] },
+ { duration: 100 * MS_PER_SEC });
+ var anim = new Animation(effect, document.timeline);
+ anim.play();
+ assert_equals_records(observer.takeRecords(),
+ [], "no records after animation is added");
+ }, "create_animation_without_target");
+
+ test(t => {
+ var div = addDiv(t);
+ var observer =
+ setupSynchronousObserver(t,
+ aOptions.subtree ? div.parentNode : div,
+ aOptions.subtree);
+
+ var anim = div.animate({ opacity: [ 0, 1 ] },
+ { duration: 100 * MS_PER_SEC });
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [anim], changed: [], removed: [] }],
+ "records after animation is added");
+
+ anim.effect.target = div;
+ assert_equals_records(observer.takeRecords(),
+ [], "no records after setting the same target");
+
+ anim.effect.target = null;
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [], removed: [anim] }],
+ "records after setting null");
+
+ anim.effect.target = null;
+ assert_equals_records(observer.takeRecords(),
+ [], "records after setting redundant null");
+ }, "set_redundant_animation_target");
+
+ test(t => {
+ var div = addDiv(t);
+ var observer =
+ setupSynchronousObserver(t,
+ aOptions.subtree ? div.parentNode : div,
+ aOptions.subtree);
+
+ var anim = div.animate({ opacity: [ 0, 1 ] },
+ { duration: 100 * MS_PER_SEC });
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [anim], changed: [], removed: [] }],
+ "records after animation is added");
+
+ anim.effect = null;
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [], removed: [anim] }],
+ "records after animation is removed");
+ }, "set_null_animation_effect");
+
+ test(t => {
+ var div = addDiv(t);
+ var observer =
+ setupSynchronousObserver(t,
+ aOptions.subtree ? div.parentNode : div,
+ aOptions.subtree);
+
+ var anim = new Animation();
+ anim.play();
+ anim.effect = new KeyframeEffect(div, { opacity: [ 0, 1 ] },
+ 100 * MS_PER_SEC);
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [anim], changed: [], removed: [] }],
+ "records after animation is added");
+ }, "set_effect_on_null_effect_animation");
+
+ test(t => {
+ var div = addDiv(t);
+ var observer =
+ setupSynchronousObserver(t,
+ aOptions.subtree ? div.parentNode : div,
+ aOptions.subtree);
+
+ var anim = div.animate({ marginLeft: [ "0px", "100px" ] },
+ 100 * MS_PER_SEC);
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [anim], changed: [], removed: [] }],
+ "records after animation is added");
+
+ anim.effect = new KeyframeEffect(div, { opacity: [ 0, 1 ] },
+ 100 * MS_PER_SEC);
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [anim], removed: [] }],
+ "records after replace effects");
+ }, "replace_effect_targeting_on_the_same_element");
+
+ test(t => {
+ var div = addDiv(t);
+ var observer =
+ setupSynchronousObserver(t,
+ aOptions.subtree ? div.parentNode : div,
+ aOptions.subtree);
+
+ var anim = div.animate({ marginLeft: [ "0px", "100px" ] },
+ 100 * MS_PER_SEC);
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [anim], changed: [], removed: [] }],
+ "records after animation is added");
+
+ anim.currentTime = 60 * MS_PER_SEC;
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [anim], removed: [] }],
+ "records after animation is changed");
+
+ anim.effect = new KeyframeEffect(div, { opacity: [ 0, 1 ] },
+ 50 * MS_PER_SEC);
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [], removed: [anim] }],
+ "records after replacing effects");
+ }, "replace_effect_targeting_on_the_same_element_not_in_effect");
+
+ test(t => {
+ var div = addDiv(t);
+ var observer =
+ setupSynchronousObserver(t,
+ aOptions.subtree ? div.parentNode : div,
+ aOptions.subtree);
+
+ var anim = div.animate({ }, 100 * MS_PER_SEC);
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [anim], changed: [], removed: [] }],
+ "records after animation is added");
+
+ anim.effect.composite = "add";
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [anim], removed: [] }],
+ "records after composite is changed");
+
+ anim.effect.composite = "add";
+ assert_equals_records(observer.takeRecords(),
+ [], "no record after setting the same composite");
+
+ }, "set_composite");
+
+ // Test that starting a single animation that is cancelled by calling
+ // cancel() dispatches an added notification and then a removed
+ // notification.
+ test(t => {
+ var div = addDiv(t, { style: "animation: anim 100s forwards" });
+ var observer =
+ setupSynchronousObserver(t,
+ aOptions.subtree ? div.parentNode : div,
+ aOptions.subtree);
+
+ var animations = div.getAnimations();
+ assert_equals(animations.length, 1,
+ "getAnimations().length after animation start");
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: animations, changed: [], removed: [] }],
+ "records after animation start");
+
+ animations[0].cancel();
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [], removed: animations }],
+ "records after animation end");
+
+ // Re-trigger the animation.
+ animations[0].play();
+
+ // Single MutationRecord for the Animation (re-)addition.
+ assert_equals_records(observer.takeRecords(),
+ [{ added: animations, changed: [], removed: [] }],
+ "records after animation start");
+ }, "single_animation_cancelled_api");
+
+ // Test that updating a property on the Animation object dispatches a changed
+ // notification.
+ [
+ { prop: "playbackRate", val: 0.5 },
+ { prop: "startTime", val: 50 * MS_PER_SEC },
+ { prop: "currentTime", val: 50 * MS_PER_SEC },
+ ].forEach(aChangeTest => {
+ test(t => {
+ // We use a forwards fill mode so that even if the change we make causes
+ // the animation to become finished, it will still be "relevant" so we
+ // won't mark it as removed.
+ var div = addDiv(t, { style: "animation: anim 100s forwards" });
+ var observer =
+ setupSynchronousObserver(t,
+ aOptions.subtree ? div.parentNode : div,
+ aOptions.subtree);
+
+ var animations = div.getAnimations();
+ assert_equals(animations.length, 1,
+ "getAnimations().length after animation start");
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: animations, changed: [], removed: [] }],
+ "records after animation start");
+
+ // Update the property.
+ animations[0][aChangeTest.prop] = aChangeTest.val;
+
+ // Make a redundant change.
+ // eslint-disable-next-line no-self-assign
+ animations[0][aChangeTest.prop] = animations[0][aChangeTest.prop];
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: animations, removed: [] }],
+ "records after animation property change");
+ }, `single_animation_api_change_${aChangeTest.prop}`);
+ });
+
+ // Test that making a redundant change to currentTime while an Animation
+ // is pause-pending still generates a change MutationRecord since setting
+ // the currentTime to any value in this state aborts the pending pause.
+ test(t => {
+ var div = addDiv(t, { style: "animation: anim 100s" });
+ var observer =
+ setupSynchronousObserver(t,
+ aOptions.subtree ? div.parentNode : div,
+ aOptions.subtree);
+
+ var animations = div.getAnimations();
+ assert_equals(animations.length, 1,
+ "getAnimations().length after animation start");
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: animations, changed: [], removed: [] }],
+ "records after animation start");
+
+ animations[0].pause();
+
+ // We are now pause-pending. Even if we make a redundant change to the
+ // currentTime, we should still get a change record because setting the
+ // currentTime while pause-pending has the effect of cancelling a pause.
+ // eslint-disable-next-line no-self-assign
+ animations[0].currentTime = animations[0].currentTime;
+
+ // Two MutationRecords for the Animation changes: one for pausing, one
+ // for aborting the pause.
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: animations, removed: [] },
+ { added: [], changed: animations, removed: [] }],
+ "records after pausing then seeking");
+ }, "change_currentTime_while_pause_pending");
+
+ // Test that calling finish() on a forwards-filling Animation dispatches
+ // a changed notification.
+ test(t => {
+ var div = addDiv(t, { style: "animation: anim 100s forwards" });
+ var observer =
+ setupSynchronousObserver(t,
+ aOptions.subtree ? div.parentNode : div,
+ aOptions.subtree);
+
+ var animations = div.getAnimations();
+ assert_equals(animations.length, 1,
+ "getAnimations().length after animation start");
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: animations, changed: [], removed: [] }],
+ "records after animation start");
+
+ animations[0].finish();
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: animations, removed: [] }],
+ "records after finish()");
+
+ // Redundant finish.
+ animations[0].finish();
+
+ // Ensure no change records.
+ assert_equals_records(observer.takeRecords(),
+ [], "records after redundant finish()");
+ }, "finish_with_forwards_fill");
+
+ // Test that calling finish() on an Animation that does not fill forwards,
+ // dispatches a removal notification.
+ test(t => {
+ var div = addDiv(t, { style: "animation: anim 100s" });
+ var observer =
+ setupSynchronousObserver(t,
+ aOptions.subtree ? div.parentNode : div,
+ aOptions.subtree);
+
+ var animations = div.getAnimations();
+ assert_equals(animations.length, 1,
+ "getAnimations().length after animation start");
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: animations, changed: [], removed: [] }],
+ "records after animation start");
+
+ animations[0].finish();
+
+ // Single MutationRecord for the Animation removal.
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [], removed: animations }],
+ "records after finishing");
+ }, "finish_without_fill");
+
+ // Test that calling finish() on a forwards-filling Animation dispatches
+ test(t => {
+ var div = addDiv(t, { style: "animation: anim 100s" });
+ var observer =
+ setupSynchronousObserver(t,
+ aOptions.subtree ? div.parentNode : div,
+ aOptions.subtree);
+
+ var animation = div.getAnimations()[0];
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [animation], changed: [], removed: []}],
+ "records after creation");
+ animation.id = "new id";
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [animation], removed: []}],
+ "records after id is changed");
+
+ animation.id = "new id";
+ assert_equals_records(observer.takeRecords(),
+ [], "records after assigning same value with id");
+ }, "change_id");
+
+ // Test that calling reverse() dispatches a changed notification.
+ test(t => {
+ var div = addDiv(t, { style: "animation: anim 100s both" });
+ var observer =
+ setupSynchronousObserver(t,
+ aOptions.subtree ? div.parentNode : div,
+ aOptions.subtree);
+
+ var animations = div.getAnimations();
+ assert_equals(animations.length, 1,
+ "getAnimations().length after animation start");
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: animations, changed: [], removed: [] }],
+ "records after animation start");
+
+ animations[0].reverse();
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: animations, removed: [] }],
+ "records after calling reverse()");
+ }, "reverse");
+
+ // Test that calling reverse() does *not* dispatch a changed notification
+ // when playbackRate == 0.
+ test(t => {
+ var div = addDiv(t, { style: "animation: anim 100s both" });
+ var observer =
+ setupSynchronousObserver(t,
+ aOptions.subtree ? div.parentNode : div,
+ aOptions.subtree);
+
+ var animations = div.getAnimations();
+ assert_equals(animations.length, 1,
+ "getAnimations().length after animation start");
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: animations, changed: [], removed: [] }],
+ "records after animation start");
+
+ // Seek to the middle and set playbackRate to zero.
+ animations[0].currentTime = 50 * MS_PER_SEC;
+ animations[0].playbackRate = 0;
+
+ // Two MutationRecords, one for each change.
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: animations, removed: [] },
+ { added: [], changed: animations, removed: [] }],
+ "records after seeking and setting playbackRate");
+
+ animations[0].reverse();
+
+ // We should get no notifications.
+ assert_equals_records(observer.takeRecords(),
+ [], "records after calling reverse()");
+ }, "reverse_with_zero_playbackRate");
+
+ // Test that reverse() on an Animation does *not* dispatch a changed
+ // notification when it throws an exception.
+ test(t => {
+ // Start an infinite animation
+ var div = addDiv(t, { style: "animation: anim 10s infinite" });
+ var observer =
+ setupSynchronousObserver(t,
+ aOptions.subtree ? div.parentNode : div,
+ aOptions.subtree);
+
+ var animations = div.getAnimations();
+ assert_equals(animations.length, 1,
+ "getAnimations().length after animation start");
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: animations, changed: [], removed: [] }],
+ "records after animation start");
+
+ // Shift the animation into the future such that when we call reverse
+ // it will try to seek to the (infinite) end.
+ animations[0].startTime = 100 * MS_PER_SEC;
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: animations, removed: [] }],
+ "records after adjusting startTime");
+
+ // Reverse: should throw
+ assert_throws('InvalidStateError', () => {
+ animations[0].reverse();
+ }, 'reverse() on future infinite animation throws an exception');
+
+ // We should get no notifications.
+ assert_equals_records(observer.takeRecords(),
+ [], "records after calling reverse()");
+ }, "reverse_with_exception");
+
+ // Test that attempting to start an animation that should already be finished
+ // does not send any notifications.
+ test(t => {
+ // Start an animation that should already be finished.
+ var div = addDiv(t, { style: "animation: anim 1s -2s;" });
+ var observer =
+ setupSynchronousObserver(t,
+ aOptions.subtree ? div.parentNode : div,
+ aOptions.subtree);
+
+ // The animation should cause no Animations to be created.
+ var animations = div.getAnimations();
+ assert_equals(animations.length, 0,
+ "getAnimations().length after animation start");
+
+ // And we should get no notifications.
+ assert_equals_records(observer.takeRecords(),
+ [], "records after attempted animation start");
+ }, "already_finished");
+
+ test(t => {
+ var div = addDiv(t, { style: "animation: anim 100s, anotherAnim 100s" });
+ var observer =
+ setupSynchronousObserver(t,
+ aOptions.subtree ? div.parentNode : div,
+ aOptions.subtree);
+
+ var animations = div.getAnimations();
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: animations, changed: [], removed: []}],
+ "records after creation");
+
+ div.style.animation = "anotherAnim 100s, anim 100s";
+ animations = div.getAnimations();
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: animations, removed: []}],
+ "records after the order is changed");
+
+ div.style.animation = "anotherAnim 100s, anim 100s";
+
+ assert_equals_records(observer.takeRecords(),
+ [], "no records after applying the same order");
+ }, "animtion_order_change");
+
+ test(t => {
+ var div = addDiv(t);
+ var observer =
+ setupSynchronousObserver(t,
+ aOptions.subtree ? div.parentNode : div,
+ aOptions.subtree);
+
+ var anim = div.animate({ opacity: [ 0, 1 ] },
+ { duration: 100 * MS_PER_SEC,
+ iterationComposite: 'replace' });
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [anim], changed: [], removed: [] }],
+ "records after animation is added");
+
+ anim.effect.iterationComposite = 'accumulate';
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [anim], removed: [] }],
+ "records after iterationComposite is changed");
+
+ anim.effect.iterationComposite = 'accumulate';
+ assert_equals_records(observer.takeRecords(),
+ [], "no record after setting the same iterationComposite");
+
+ }, "set_iterationComposite");
+
+ test(t => {
+ var div = addDiv(t);
+ var observer =
+ setupSynchronousObserver(t,
+ aOptions.subtree ? div.parentNode : div,
+ aOptions.subtree);
+
+ var anim = div.animate({ opacity: [ 0, 1 ] },
+ { duration: 100 * MS_PER_SEC });
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [anim], changed: [], removed: [] }],
+ "records after animation is added");
+
+ anim.effect.setKeyframes({ opacity: 0.1 });
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [anim], removed: [] }],
+ "records after keyframes are changed");
+
+ anim.effect.setKeyframes({ opacity: 0.1 });
+ assert_equals_records(observer.takeRecords(),
+ [], "no record after setting the same keyframes");
+
+ anim.effect.setKeyframes(null);
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [anim], removed: [] }],
+ "records after keyframes are set to empty");
+
+ }, "set_keyframes");
+
+ // Test that starting a single transition that is cancelled by resetting
+ // the transition-property property dispatches an added notification and
+ // then a removed notification.
+ test(t => {
+ var div =
+ addDiv(t, { style: "transition: background-color 100s; " +
+ "background-color: yellow;" });
+ var observer =
+ setupSynchronousObserver(t,
+ aOptions.subtree ? div.parentNode : div,
+ aOptions.subtree);
+
+ getComputedStyle(div).transitionProperty;
+ div.style.backgroundColor = "lime";
+
+ // The transition should cause the creation of a single Animation.
+ var animations = div.getAnimations();
+ assert_equals(animations.length, 1,
+ "getAnimations().length after transition start");
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: animations, changed: [], removed: [] }],
+ "records after transition start");
+
+ // Cancel the transition by setting transition-property.
+ div.style.transitionProperty = "none";
+ getComputedStyle(div).transitionProperty;
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [], removed: animations }],
+ "records after transition end");
+ }, "single_transition_cancelled_property");
+
+ // Test that starting a single transition that is cancelled by setting
+ // style to the currently animated value dispatches an added
+ // notification and then a removed notification.
+ test(t => {
+ // A long transition with a predictable value.
+ var div =
+ addDiv(t, { style: "transition: z-index 100s -51s; " +
+ "z-index: 10;" });
+ var observer =
+ setupSynchronousObserver(t,
+ aOptions.subtree ? div.parentNode : div,
+ aOptions.subtree);
+ getComputedStyle(div).transitionProperty;
+ div.style.zIndex = "100";
+
+ // The transition should cause the creation of a single Animation.
+ var animations = div.getAnimations();
+ assert_equals(animations.length, 1,
+ "getAnimations().length after transition start");
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: animations, changed: [], removed: [] }],
+ "records after transition start");
+
+ // Cancel the transition by setting the current animation value.
+ let value = "83";
+ assert_equals(getComputedStyle(div).zIndex, value,
+ "half-way transition value");
+ div.style.zIndex = value;
+ getComputedStyle(div).transitionProperty;
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [], removed: animations }],
+ "records after transition end");
+ }, "single_transition_cancelled_value");
+
+ // Test that starting a single transition that is cancelled by setting
+ // style to a non-interpolable value dispatches an added notification
+ // and then a removed notification.
+ test(t => {
+ var div =
+ addDiv(t, { style: "transition: line-height 100s; " +
+ "line-height: 16px;" });
+ var observer =
+ setupSynchronousObserver(t,
+ aOptions.subtree ? div.parentNode : div,
+ aOptions.subtree);
+
+ getComputedStyle(div).transitionProperty;
+ div.style.lineHeight = "100px";
+
+ // The transition should cause the creation of a single Animation.
+ var animations = div.getAnimations();
+ assert_equals(animations.length, 1,
+ "getAnimations().length after transition start");
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: animations, changed: [], removed: [] }],
+ "records after transition start");
+
+ // Cancel the transition by setting line-height to a non-interpolable value.
+ div.style.lineHeight = "normal";
+ getComputedStyle(div).transitionProperty;
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [], removed: animations }],
+ "records after transition end");
+ }, "single_transition_cancelled_noninterpolable");
+
+ // Test that starting a single transition and then reversing it
+ // dispatches an added notification, then a simultaneous removed and
+ // added notification, then a removed notification once finished.
+ test(t => {
+ var div =
+ addDiv(t, { style: "transition: background-color 100s step-start; " +
+ "background-color: yellow;" });
+ var observer =
+ setupSynchronousObserver(t,
+ aOptions.subtree ? div.parentNode : div,
+ aOptions.subtree);
+
+ getComputedStyle(div).transitionProperty;
+ div.style.backgroundColor = "lime";
+
+ var animations = div.getAnimations();
+
+ // The transition should cause the creation of a single Animation.
+ assert_equals(animations.length, 1,
+ "getAnimations().length after transition start");
+
+ var firstAnimation = animations[0];
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [firstAnimation], changed: [], removed: [] }],
+ "records after transition start");
+
+ firstAnimation.currentTime = 50 * MS_PER_SEC;
+
+ // Reverse the transition by setting the background-color back to its
+ // original value.
+ div.style.backgroundColor = "yellow";
+
+ // The reversal should cause the creation of a new Animation.
+ animations = div.getAnimations();
+ assert_equals(animations.length, 1,
+ "getAnimations().length after transition reversal");
+
+ var secondAnimation = animations[0];
+
+ assert_true(firstAnimation != secondAnimation,
+ "second Animation should be different from the first");
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [firstAnimation], removed: [] },
+ { added: [secondAnimation], changed: [], removed: [firstAnimation] }],
+ "records after transition reversal");
+
+ // Cancel the transition.
+ div.style.transitionProperty = "none";
+ getComputedStyle(div).transitionProperty;
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [], removed: [secondAnimation] }],
+ "records after transition end");
+ }, "single_transition_reversed");
+
+ // Test that multiple transitions starting and ending on an element
+ // at the same time get batched up into a single MutationRecord.
+ test(t => {
+ var div =
+ addDiv(t, { style: "transition-duration: 100s; " +
+ "transition-property: color, background-color, line-height" +
+ "background-color: yellow; line-height: 16px" });
+ var observer =
+ setupSynchronousObserver(t,
+ aOptions.subtree ? div.parentNode : div,
+ aOptions.subtree);
+ getComputedStyle(div).transitionProperty;
+
+ div.style.backgroundColor = "lime";
+ div.style.color = "blue";
+ div.style.lineHeight = "24px";
+
+ // The transitions should cause the creation of three Animations.
+ var animations = div.getAnimations();
+ assert_equals(animations.length, 3,
+ "getAnimations().length after transition starts");
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: animations, changed: [], removed: [] }],
+ "records after transition starts");
+
+ assert_equals(animations.filter(p => p.playState == "running").length, 3,
+ "number of running Animations");
+
+ // Seek well into each animation.
+ animations.forEach(p => p.currentTime = 50 * MS_PER_SEC);
+
+ // Prepare the set of expected change MutationRecords, one for each
+ // animation that was seeked.
+ var seekRecords = animations.map(
+ p => ({ added: [], changed: [p], removed: [] })
+ );
+
+ // Cancel one of the transitions by setting transition-property.
+ div.style.transitionProperty = "background-color, line-height";
+
+ var colorAnimation = animations.filter(p => p.playState != "running");
+ var otherAnimations = animations.filter(p => p.playState == "running");
+
+ assert_equals(colorAnimation.length, 1,
+ "number of non-running Animations after cancelling one");
+ assert_equals(otherAnimations.length, 2,
+ "number of running Animations after cancelling one");
+
+ assert_equals_records(observer.takeRecords(),
+ seekRecords.concat({ added: [], changed: [], removed: colorAnimation }),
+ "records after color transition end");
+
+ // Cancel the remaining transitions.
+ div.style.transitionProperty = "none";
+ getComputedStyle(div).transitionProperty;
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [], removed: otherAnimations }],
+ "records after other transition ends");
+ }, "multiple_transitions");
+
+ // Test that starting a single animation that is cancelled by resetting
+ // the animation-name property dispatches an added notification and
+ // then a removed notification.
+ test(t => {
+ var div =
+ addDiv(t, { style: "animation: anim 100s" });
+ var observer =
+ setupSynchronousObserver(t,
+ aOptions.subtree ? div.parentNode : div,
+ aOptions.subtree);
+
+ // The animation should cause the creation of a single Animation.
+ var animations = div.getAnimations();
+ assert_equals(animations.length, 1,
+ "getAnimations().length after animation start");
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: animations, changed: [], removed: [] }],
+ "records after animation start");
+
+ // Cancel the animation by setting animation-name.
+ div.style.animationName = "none";
+ getComputedStyle(div).animationName;
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [], removed: animations }],
+ "records after animation end");
+ }, "single_animation_cancelled_name");
+
+ // Test that starting a single animation that is cancelled by updating
+ // the animation-duration property dispatches an added notification and
+ // then a removed notification.
+ test(t => {
+ var div =
+ addDiv(t, { style: "animation: anim 100s" });
+ var observer =
+ setupSynchronousObserver(t,
+ aOptions.subtree ? div.parentNode : div,
+ aOptions.subtree);
+
+ // The animation should cause the creation of a single Animation.
+ var animations = div.getAnimations();
+ assert_equals(animations.length, 1,
+ "getAnimations().length after animation start");
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: animations, changed: [], removed: [] }],
+ "records after animation start");
+
+ // Advance the animation by a second.
+ animations[0].currentTime += 1 * MS_PER_SEC;
+
+ // Cancel the animation by setting animation-duration to a value less
+ // than a second.
+ div.style.animationDuration = "0.1s";
+ getComputedStyle(div).animationName;
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: animations, removed: [] },
+ { added: [], changed: [], removed: animations }],
+ "records after animation end");
+ }, "single_animation_cancelled_duration");
+
+ // Test that starting a single animation that is cancelled by updating
+ // the animation-delay property dispatches an added notification and
+ // then a removed notification.
+ test(t => {
+ var div =
+ addDiv(t, { style: "animation: anim 100s" });
+ var observer =
+ setupSynchronousObserver(t,
+ aOptions.subtree ? div.parentNode : div,
+ aOptions.subtree);
+
+ // The animation should cause the creation of a single Animation.
+ var animations = div.getAnimations();
+ assert_equals(animations.length, 1,
+ "getAnimations().length after animation start");
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: animations, changed: [], removed: [] }],
+ "records after animation start");
+
+ // Cancel the animation by setting animation-delay.
+ div.style.animationDelay = "-200s";
+ getComputedStyle(div).animationName;
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [], removed: animations }],
+ "records after animation end");
+ }, "single_animation_cancelled_delay");
+
+ // Test that starting a single animation that is cancelled by updating
+ // the animation-iteration-count property dispatches an added notification
+ // and then a removed notification.
+ test(t => {
+ // A short, repeated animation.
+ var div =
+ addDiv(t, { style: "animation: anim 0.5s infinite;" });
+ var observer =
+ setupSynchronousObserver(t,
+ aOptions.subtree ? div.parentNode : div,
+ aOptions.subtree);
+
+ // The animation should cause the creation of a single Animation.
+ var animations = div.getAnimations();
+ assert_equals(animations.length, 1,
+ "getAnimations().length after animation start");
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: animations, changed: [], removed: [] }],
+ "records after animation start");
+
+ // Advance the animation until we are past the first iteration.
+ animations[0].currentTime += 1 * MS_PER_SEC;
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: animations, removed: [] }],
+ "records after seeking animations");
+
+ // Cancel the animation by setting animation-iteration-count.
+ div.style.animationIterationCount = "1";
+ getComputedStyle(div).animationName;
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [], removed: animations }],
+ "records after animation end");
+ }, "single_animation_cancelled_iteration_count");
+
+ // Test that updating an animation property dispatches a changed notification.
+ [
+ { name: "duration", prop: "animationDuration", val: "200s" },
+ { name: "timing", prop: "animationTimingFunction", val: "linear" },
+ { name: "iteration", prop: "animationIterationCount", val: "2" },
+ { name: "direction", prop: "animationDirection", val: "reverse" },
+ { name: "state", prop: "animationPlayState", val: "paused" },
+ { name: "delay", prop: "animationDelay", val: "-1s" },
+ { name: "fill", prop: "animationFillMode", val: "both" },
+ ].forEach(aChangeTest => {
+ test(t => {
+ // Start a long animation.
+ var div = addDiv(t, { style: "animation: anim 100s;" });
+ var observer =
+ setupSynchronousObserver(t,
+ aOptions.subtree ? div.parentNode : div,
+ aOptions.subtree);
+
+ // The animation should cause the creation of a single Animation.
+ var animations = div.getAnimations();
+ assert_equals(animations.length, 1,
+ "getAnimations().length after animation start");
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: animations, changed: [], removed: [] }],
+ "records after animation start");
+
+ // Change a property of the animation such that it keeps running.
+ div.style[aChangeTest.prop] = aChangeTest.val;
+ getComputedStyle(div).animationName;
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: animations, removed: [] }],
+ "records after animation change");
+
+ // Cancel the animation.
+ div.style.animationName = "none";
+ getComputedStyle(div).animationName;
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [], removed: animations }],
+ "records after animation end");
+ }, `single_animation_change_${aChangeTest.name}`);
+ });
+
+ // Test that calling finish() on a pause-pending (but otherwise finished)
+ // animation dispatches a changed notification.
+ test(t => {
+ var div =
+ addDiv(t, { style: "animation: anim 100s forwards" });
+ var observer =
+ setupSynchronousObserver(t,
+ aOptions.subtree ? div.parentNode : div,
+ aOptions.subtree);
+
+ // The animation should cause the creation of a single Animation.
+ var animations = div.getAnimations();
+ assert_equals(animations.length, 1,
+ "getAnimations().length after animation start");
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: animations, changed: [], removed: [] }],
+ "records after animation start");
+
+ // Finish and pause.
+ animations[0].finish();
+ animations[0].pause();
+ assert_true(animations[0].pending && animations[0].playState === "paused",
+ "playState after finishing and calling pause()");
+
+ // Call finish() again to abort the pause
+ animations[0].finish();
+ assert_equals(animations[0].playState, "finished",
+ "playState after finishing again");
+
+ // Wait for three MutationRecords for the Animation changes to
+ // be delivered: one for each finish(), pause(), finish() operation.
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: animations, removed: [] },
+ { added: [], changed: animations, removed: [] },
+ { added: [], changed: animations, removed: [] }],
+ "records after finish(), pause(), finish()");
+
+ // Cancel the animation.
+ div.style = "";
+ getComputedStyle(div).animationName;
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [], removed: animations }],
+ "records after animation end");
+ }, "finish_from_pause_pending");
+
+ // Test that calling play() on a finished Animation that fills forwards
+ // dispatches a changed notification.
+ test(t => {
+ // Animation with a forwards fill
+ var div =
+ addDiv(t, { style: "animation: anim 100s forwards" });
+ var observer =
+ setupSynchronousObserver(t,
+ aOptions.subtree ? div.parentNode : div,
+ aOptions.subtree);
+
+ // The animation should cause the creation of a single Animation.
+ var animations = div.getAnimations();
+ assert_equals(animations.length, 1,
+ "getAnimations().length after animation start");
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: animations, changed: [], removed: [] }],
+ "records after animation start");
+
+ // Seek to the end
+ animations[0].finish();
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: animations, removed: [] }],
+ "records after finish()");
+
+ // Since we are filling forwards, calling play() should produce a
+ // change record since the animation remains relevant.
+ animations[0].play();
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: animations, removed: [] }],
+ "records after play()");
+
+ // Cancel the animation.
+ div.style = "";
+ getComputedStyle(div).animationName;
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [], removed: animations }],
+ "records after animation end");
+ }, "play_filling_forwards");
+
+ // Test that calling pause() on an Animation dispatches a changed
+ // notification.
+ test(t => {
+ var div =
+ addDiv(t, { style: "animation: anim 100s" });
+ var observer =
+ setupSynchronousObserver(t,
+ aOptions.subtree ? div.parentNode : div,
+ aOptions.subtree);
+
+ // The animation should cause the creation of a single Animation.
+ var animations = div.getAnimations();
+ assert_equals(animations.length, 1,
+ "getAnimations().length after animation start");
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: animations, changed: [], removed: [] }],
+ "records after animation start");
+
+ // Pause
+ animations[0].pause();
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: animations, removed: [] }],
+ "records after pause()");
+
+ // Redundant pause
+ animations[0].pause();
+
+ assert_equals_records(observer.takeRecords(),
+ [], "records after redundant pause()");
+
+ // Cancel the animation.
+ div.style = "";
+ getComputedStyle(div).animationName;
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [], removed: animations }],
+ "records after animation end");
+ }, "pause");
+
+ // Test that calling pause() on an Animation that is pause-pending
+ // does not dispatch an additional changed notification.
+ test(t => {
+ var div =
+ addDiv(t, { style: "animation: anim 100s" });
+ var observer =
+ setupSynchronousObserver(t,
+ aOptions.subtree ? div.parentNode : div,
+ aOptions.subtree);
+
+ // The animation should cause the creation of a single Animation.
+ var animations = div.getAnimations();
+ assert_equals(animations.length, 1,
+ "getAnimations().length after animation start");
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: animations, changed: [], removed: [] }],
+ "records after animation start");
+
+ // Pause
+ animations[0].pause();
+
+ // We are now pause-pending, but pause again
+ animations[0].pause();
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: animations, removed: [] }],
+ "records after pause()");
+
+ // Cancel the animation.
+ div.style = "";
+ getComputedStyle(div).animationName;
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [], removed: animations }],
+ "records after animation end");
+ }, "pause_while_pause_pending");
+
+ // Test that calling play() on an Animation that is pause-pending
+ // dispatches a changed notification.
+ test(t => {
+ var div =
+ addDiv(t, { style: "animation: anim 100s" });
+ var observer =
+ setupSynchronousObserver(t,
+ aOptions.subtree ? div.parentNode : div,
+ aOptions.subtree);
+
+ // The animation should cause the creation of a single Animation.
+ var animations = div.getAnimations();
+ assert_equals(animations.length, 1,
+ "getAnimations().length after animation start");
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: animations, changed: [], removed: [] }],
+ "records after animation start");
+
+ // Pause
+ animations[0].pause();
+
+ // We are now pause-pending. If we play() now, we will abort the pause
+ animations[0].play();
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: animations, removed: [] },
+ { added: [], changed: animations, removed: [] }],
+ "records after aborting a pause()");
+
+ // Cancel the animation.
+ div.style = "";
+ getComputedStyle(div).animationName;
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [], removed: animations }],
+ "records after animation end");
+ }, "aborted_pause");
+
+ // Test that calling play() on a finished Animation that does *not* fill
+ // forwards dispatches an addition notification.
+ test(t => {
+ var div =
+ addDiv(t, { style: "animation: anim 100s" });
+ var observer =
+ setupSynchronousObserver(t,
+ aOptions.subtree ? div.parentNode : div,
+ aOptions.subtree);
+
+ // The animation should cause the creation of a single Animation.
+ var animations = div.getAnimations();
+ assert_equals(animations.length, 1,
+ "getAnimations().length after animation start");
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: animations, changed: [], removed: [] }],
+ "records after animation start");
+
+ // Seek to the end
+ animations[0].finish();
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [], removed: animations }],
+ "records after finish()");
+
+ // Since we are *not* filling forwards, calling play() is equivalent
+ // to creating a new animation since it becomes relevant again.
+ animations[0].play();
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: animations, changed: [], removed: [] }],
+ "records after play()");
+
+ // Cancel the animation.
+ div.style = "";
+ getComputedStyle(div).animationName;
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [], removed: animations }],
+ "records after animation end");
+ }, "play_after_finish");
+
+ });
+
+ test(t => {
+ var div = addDiv(t);
+ var observer = setupSynchronousObserver(t, div, true);
+
+ var child = document.createElement("div");
+ div.appendChild(child);
+
+ var anim1 = div.animate({ marginLeft: [ "0px", "50px" ] },
+ 100 * MS_PER_SEC);
+ var anim2 = child.animate({ marginLeft: [ "0px", "100px" ] },
+ 50 * MS_PER_SEC);
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [anim1], changed: [], removed: [] },
+ { added: [anim2], changed: [], removed: [] }],
+ "records after animation is added");
+
+ // After setting a new effect, we remove the current animation, anim1,
+ // because it is no longer attached to |div|, and then remove the previous
+ // animation, anim2. Finally, add back the anim1 which is in effect on
+ // |child| now. In addition, we sort them by tree order and they are
+ // batched.
+ anim1.effect = anim2.effect;
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [], removed: [anim1] }, // div
+ { added: [anim1], changed: [], removed: [anim2] }], // child
+ "records after animation effects are changed");
+ }, "set_effect_with_previous_animation");
+
+ test(t => {
+ var div = addDiv(t);
+ var observer = setupSynchronousObserver(t, document, true);
+
+ var anim = div.animate({ opacity: [ 0, 1 ] },
+ { duration: 100 * MS_PER_SEC });
+
+ var newTarget = document.createElement("div");
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [anim], changed: [], removed: [] }],
+ "records after animation is added");
+
+ anim.effect.target = null;
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [], removed: [anim] }],
+ "records after setting null");
+
+ anim.effect.target = div;
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [anim], changed: [], removed: [] }],
+ "records after setting a target");
+
+ anim.effect.target = addDiv(t);
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [], removed: [anim] },
+ { added: [anim], changed: [], removed: [] }],
+ "records after setting a different target");
+ }, "set_animation_target");
+
+ test(t => {
+ var div = addDiv(t);
+ var observer = setupSynchronousObserver(t, div, true);
+
+ var anim = div.animate({ opacity: [ 0, 1 ] },
+ { duration: 200 * MS_PER_SEC,
+ pseudoElement: '::before' });
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [anim], changed: [], removed: [] }],
+ "records after animation is added");
+
+ anim.effect.updateTiming({ duration: 100 * MS_PER_SEC });
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [anim], removed: [] }],
+ "records after duration is changed");
+
+ anim.effect.updateTiming({ duration: 100 * MS_PER_SEC });
+ assert_equals_records(observer.takeRecords(),
+ [], "records after assigning same value");
+
+ anim.currentTime = anim.effect.getComputedTiming().duration * 2;
+ anim.finish();
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [], removed: [anim] }],
+ "records after animation end");
+
+ anim.effect.updateTiming({
+ duration: anim.effect.getComputedTiming().duration * 3
+ });
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [anim], changed: [], removed: [] }],
+ "records after animation restarted");
+
+ anim.effect.updateTiming({ duration: "auto" });
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [], removed: [anim] }],
+ "records after duration set \"auto\"");
+
+ anim.effect.updateTiming({ duration: "auto" });
+ assert_equals_records(observer.takeRecords(),
+ [], "records after assigning same value \"auto\"");
+ }, "change_duration_and_currenttime_on_pseudo_elements");
+
+ test(t => {
+ var div = addDiv(t);
+ var observer = setupSynchronousObserver(t, div, false);
+
+ var anim = div.animate({ opacity: [ 0, 1 ] },
+ { duration: 100 * MS_PER_SEC });
+ var pAnim = div.animate({ opacity: [ 0, 1 ] },
+ { duration: 100 * MS_PER_SEC,
+ pseudoElement: "::before" });
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [anim], changed: [], removed: [] }],
+ "records after animation is added");
+
+ anim.finish();
+ pAnim.finish();
+
+ assert_equals_records(observer.takeRecords(),
+ [{ added: [], changed: [], removed: [anim] }],
+ "records after animation is finished");
+ }, "exclude_animations_targeting_pseudo_elements");
+}
+
+W3CTest.runner.expectAssertions(0, 12); // bug 1189015
+setup({explicit_done: true});
+SpecialPowers.pushPrefEnv(
+ {
+ set: [
+ ["dom.animations-api.timelines.enabled", true],
+ ],
+ },
+ function() {
+ runTest();
+ done();
+ }
+);
+
+</script>
diff --git a/dom/animation/test/chrome/test_animation_performance_warning.html b/dom/animation/test/chrome/test_animation_performance_warning.html
new file mode 100644
index 0000000000..13df5d7842
--- /dev/null
+++ b/dom/animation/test/chrome/test_animation_performance_warning.html
@@ -0,0 +1,1693 @@
+<!doctype html>
+<head>
+<meta charset=utf-8>
+<title>Bug 1196114 - Test metadata related to which animation properties
+ are running on the compositor</title>
+<script type="application/javascript" src="../testharness.js"></script>
+<script type="application/javascript" src="../testharnessreport.js"></script>
+<script type="application/javascript" src="../testcommon.js"></script>
+<style>
+.compositable {
+ /* Element needs geometry to be eligible for layerization */
+ width: 100px;
+ height: 100px;
+ background-color: white;
+}
+@keyframes fade {
+ from { opacity: 1 }
+ to { opacity: 0 }
+}
+@keyframes translate {
+ from { transform: none }
+ to { transform: translate(100px) }
+}
+</style>
+</head>
+<body>
+<a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1196114"
+ target="_blank">Mozilla Bug 1196114</a>
+<div id="log"></div>
+<script>
+'use strict';
+
+// This is used for obtaining localized strings.
+var gStringBundle;
+
+W3CTest.runner.requestLongerTimeout(2);
+
+const Services = SpecialPowers.Services;
+Services.locale.requestedLocales = ["en-US"];
+
+SpecialPowers.pushPrefEnv({ "set": [
+ // Need to set devPixelsPerPx explicitly to gain
+ // consistent pixel values in warning messages
+ // regardless of platform DPIs.
+ ["layout.css.devPixelsPerPx", 1],
+ ["layout.animation.prerender.partial", false],
+ ] },
+ start);
+
+function compare_property_state(a, b) {
+ if (a.property > b.property) {
+ return -1;
+ } else if (a.property < b.property) {
+ return 1;
+ }
+ if (a.runningOnCompositor != b.runningOnCompositor) {
+ return a.runningOnCompositor ? 1 : -1;
+ }
+ return a.warning > b.warning ? -1 : 1;
+}
+
+function assert_animation_property_state_equals(actual, expected) {
+ assert_equals(actual.length, expected.length, 'Number of properties');
+
+ var sortedActual = actual.sort(compare_property_state);
+ var sortedExpected = expected.sort(compare_property_state);
+
+ for (var i = 0; i < sortedActual.length; i++) {
+ assert_equals(sortedActual[i].property,
+ sortedExpected[i].property,
+ 'CSS property name should match');
+ assert_equals(sortedActual[i].runningOnCompositor,
+ sortedExpected[i].runningOnCompositor,
+ 'runningOnCompositor property should match');
+ if (sortedExpected[i].warning instanceof RegExp) {
+ assert_regexp_match(sortedActual[i].warning,
+ sortedExpected[i].warning,
+ 'warning message should match');
+ } else if (sortedExpected[i].warning) {
+ assert_equals(sortedActual[i].warning,
+ gStringBundle.GetStringFromName(sortedExpected[i].warning),
+ 'warning message should match');
+ }
+ }
+}
+
+// Check that the animation is running on compositor and
+// warning property is not set for the CSS property regardless
+// expected values.
+function assert_all_properties_running_on_compositor(actual, expected) {
+ assert_equals(actual.length, expected.length);
+
+ var sortedActual = actual.sort(compare_property_state);
+ var sortedExpected = expected.sort(compare_property_state);
+
+ for (var i = 0; i < sortedActual.length; i++) {
+ assert_equals(sortedActual[i].property,
+ sortedExpected[i].property,
+ 'CSS property name should match');
+ assert_true(sortedActual[i].runningOnCompositor,
+ 'runningOnCompositor property should be true on ' +
+ sortedActual[i].property);
+ assert_not_exists(sortedActual[i], 'warning',
+ 'warning property should not be set');
+ }
+}
+
+function testBasicOperation() {
+ [
+ {
+ desc: 'animations on compositor',
+ frames: {
+ opacity: [0, 1]
+ },
+ expected: [
+ {
+ property: 'opacity',
+ runningOnCompositor: true
+ }
+ ]
+ },
+ {
+ desc: 'animations on main thread',
+ frames: {
+ zIndex: ['0', '999']
+ },
+ expected: [
+ {
+ property: 'z-index',
+ runningOnCompositor: false
+ }
+ ]
+ },
+ {
+ desc: 'animations on both threads',
+ frames: {
+ zIndex: ['0', '999'],
+ transform: ['translate(0px)', 'translate(100px)']
+ },
+ expected: [
+ {
+ property: 'z-index',
+ runningOnCompositor: false
+ },
+ {
+ property: 'transform',
+ runningOnCompositor: true
+ }
+ ]
+ },
+ {
+ desc: 'two animation properties on compositor thread',
+ frames: {
+ opacity: [0, 1],
+ transform: ['translate(0px)', 'translate(100px)']
+ },
+ expected: [
+ {
+ property: 'opacity',
+ runningOnCompositor: true
+ },
+ {
+ property: 'transform',
+ runningOnCompositor: true
+ }
+ ]
+ },
+ {
+ desc: 'two transform-like animation properties on compositor thread',
+ frames: {
+ transform: ['translate(0px)', 'translate(100px)'],
+ translate: ['0px', '100px']
+ },
+ expected: [
+ {
+ property: 'transform',
+ runningOnCompositor: true
+ },
+ {
+ property: 'translate',
+ runningOnCompositor: true
+ }
+ ]
+ },
+ {
+ desc: 'opacity on compositor with animation of geometric properties',
+ frames: {
+ width: ['100px', '200px'],
+ opacity: [0, 1]
+ },
+ expected: [
+ {
+ property: 'width',
+ runningOnCompositor: false
+ },
+ {
+ property: 'opacity',
+ runningOnCompositor: true
+ }
+ ]
+ },
+ ].forEach(subtest => {
+ promise_test(async t => {
+ var animation = addDivAndAnimate(t, { class: 'compositable' },
+ subtest.frames, 100 * MS_PER_SEC);
+ await waitForPaints();
+ assert_animation_property_state_equals(
+ animation.effect.getProperties(),
+ subtest.expected);
+ }, subtest.desc);
+ });
+}
+
+// Test adding/removing a 'width' property on the same animation object.
+function testKeyframesWithGeometricProperties() {
+ [
+ {
+ desc: 'transform',
+ frames: {
+ transform: ['translate(0px)', 'translate(100px)']
+ },
+ expected: {
+ withoutGeometric: [
+ {
+ property: 'transform',
+ runningOnCompositor: true
+ }
+ ],
+ withGeometric: [
+ {
+ property: 'width',
+ runningOnCompositor: false
+ },
+ {
+ property: 'transform',
+ runningOnCompositor: true,
+ }
+ ]
+ }
+ },
+ {
+ desc: 'translate',
+ frames: {
+ translate: ['0px', '100px']
+ },
+ expected: {
+ withoutGeometric: [
+ {
+ property: 'translate',
+ runningOnCompositor: true
+ }
+ ],
+ withGeometric: [
+ {
+ property: 'width',
+ runningOnCompositor: false
+ },
+ {
+ property: 'translate',
+ runningOnCompositor: true,
+ }
+ ]
+ }
+ },
+ {
+ desc: 'opacity and transform-like properties',
+ frames: {
+ opacity: [0, 1],
+ transform: ['translate(0px)', 'translate(100px)'],
+ translate: ['0px', '100px']
+ },
+ expected: {
+ withoutGeometric: [
+ {
+ property: 'opacity',
+ runningOnCompositor: true
+ },
+ {
+ property: 'transform',
+ runningOnCompositor: true
+ },
+ {
+ property: 'translate',
+ runningOnCompositor: true
+ }
+ ],
+ withGeometric: [
+ {
+ property: 'width',
+ runningOnCompositor: false
+ },
+ {
+ property: 'opacity',
+ runningOnCompositor: true
+ },
+ {
+ property: 'transform',
+ runningOnCompositor: true,
+ },
+ {
+ property: 'translate',
+ runningOnCompositor: true,
+ }
+ ]
+ }
+ },
+ ].forEach(subtest => {
+ promise_test(async t => {
+ var animation = addDivAndAnimate(t, { class: 'compositable' },
+ subtest.frames, 100 * MS_PER_SEC);
+ await waitForPaints();
+
+ // First, a transform animation is running on compositor.
+ assert_animation_property_state_equals(
+ animation.effect.getProperties(),
+ subtest.expected.withoutGeometric);
+
+ // Add a 'width' property.
+ var keyframes = animation.effect.getKeyframes();
+
+ keyframes[0].width = '100px';
+ keyframes[1].width = '200px';
+
+ animation.effect.setKeyframes(keyframes);
+ await waitForFrame();
+
+ // Now the transform animation is not running on compositor because of
+ // the 'width' property.
+ assert_animation_property_state_equals(
+ animation.effect.getProperties(),
+ subtest.expected.withGeometric);
+
+ // Remove the 'width' property.
+ var keyframes = animation.effect.getKeyframes();
+
+ delete keyframes[0].width;
+ delete keyframes[1].width;
+
+ animation.effect.setKeyframes(keyframes);
+ await waitForFrame();
+
+ // Finally, the transform animation is running on compositor.
+ assert_animation_property_state_equals(
+ animation.effect.getProperties(),
+ subtest.expected.withoutGeometric);
+ }, 'An animation has: ' + subtest.desc);
+ });
+}
+
+// Test that the expected set of geometric properties all block transform
+// animations.
+function testSetOfGeometricProperties() {
+ const geometricProperties = [
+ 'width', 'height',
+ 'top', 'right', 'bottom', 'left',
+ 'margin-top', 'margin-right', 'margin-bottom', 'margin-left',
+ 'padding-top', 'padding-right', 'padding-bottom', 'padding-left'
+ ];
+
+ geometricProperties.forEach(property => {
+ promise_test(async t => {
+ const keyframes = {
+ [propertyToIDL(property)]: [ '100px', '200px' ],
+ transform: [ 'translate(0px)', 'translate(100px)' ]
+ };
+ var animation = addDivAndAnimate(t, { class: 'compositable' },
+ keyframes, 100 * MS_PER_SEC);
+
+ await waitForPaints();
+ assert_animation_property_state_equals(
+ animation.effect.getProperties(),
+ [
+ {
+ property,
+ runningOnCompositor: false
+ },
+ {
+ property: 'transform',
+ runningOnCompositor: true,
+ }
+ ]);
+ }, `${property} is treated as a geometric property`);
+ });
+}
+
+// Performance warning tests that set and clear a style property.
+function testStyleChanges() {
+ [
+ {
+ desc: 'preserve-3d transform',
+ frames: {
+ transform: ['translate(0px)', 'translate(100px)']
+ },
+ style: 'transform-style: preserve-3d',
+ expected: [
+ {
+ property: 'transform',
+ runningOnCompositor: true,
+ }
+ ]
+ },
+ {
+ desc: 'preserve-3d translate',
+ frames: {
+ translate: ['0px', '100px']
+ },
+ style: 'transform-style: preserve-3d',
+ expected: [
+ {
+ property: 'translate',
+ runningOnCompositor: true,
+ }
+ ]
+ },
+ {
+ desc: 'transform with backface-visibility:hidden',
+ frames: {
+ transform: ['translate(0px)', 'translate(100px)']
+ },
+ style: 'backface-visibility: hidden;',
+ expected: [
+ {
+ property: 'transform',
+ runningOnCompositor: true,
+ }
+ ]
+ },
+ {
+ desc: 'translate with backface-visibility:hidden',
+ frames: {
+ translate: ['0px', '100px']
+ },
+ style: 'backface-visibility: hidden;',
+ expected: [
+ {
+ property: 'translate',
+ runningOnCompositor: true,
+ }
+ ]
+ },
+ {
+ desc: 'opacity and transform-like properties with preserve-3d',
+ frames: {
+ opacity: [0, 1],
+ transform: ['translate(0px)', 'translate(100px)'],
+ translate: ['0px', '100px']
+ },
+ style: 'transform-style: preserve-3d',
+ expected: [
+ {
+ property: 'opacity',
+ runningOnCompositor: true
+ },
+ {
+ property: 'transform',
+ runningOnCompositor: true,
+ },
+ {
+ property: 'translate',
+ runningOnCompositor: true,
+ }
+ ]
+ },
+ {
+ desc: 'opacity and transform-like properties with ' +
+ 'backface-visibility:hidden',
+ frames: {
+ opacity: [0, 1],
+ transform: ['translate(0px)', 'translate(100px)'],
+ translate: ['0px', '100px']
+ },
+ style: 'backface-visibility: hidden;',
+ expected: [
+ {
+ property: 'opacity',
+ runningOnCompositor: true
+ },
+ {
+ property: 'transform',
+ runningOnCompositor: true,
+ },
+ {
+ property: 'translate',
+ runningOnCompositor: true,
+ }
+ ]
+ },
+ ].forEach(subtest => {
+ promise_test(async t => {
+ var animation = addDivAndAnimate(t, { class: 'compositable' },
+ subtest.frames, 100 * MS_PER_SEC);
+ await waitForPaints();
+ assert_all_properties_running_on_compositor(
+ animation.effect.getProperties(),
+ subtest.expected);
+ animation.effect.target.style = subtest.style;
+ await waitForFrame();
+
+ assert_animation_property_state_equals(
+ animation.effect.getProperties(),
+ subtest.expected);
+ animation.effect.target.style = '';
+ await waitForFrame();
+
+ assert_all_properties_running_on_compositor(
+ animation.effect.getProperties(),
+ subtest.expected);
+ }, subtest.desc);
+ });
+}
+
+// Performance warning tests that set and clear the id property
+function testIdChanges() {
+ [
+ {
+ desc: 'moz-element referencing a transform',
+ frames: {
+ transform: ['translate(0px)', 'translate(100px)']
+ },
+ id: 'transformed',
+ createelement: 'width:100px; height:100px; background: -moz-element(#transformed)',
+ expected: [
+ {
+ property: 'transform',
+ runningOnCompositor: false,
+ warning: 'CompositorAnimationWarningHasRenderingObserver'
+ }
+ ]
+ },
+ {
+ desc: 'moz-element referencing a translate',
+ frames: {
+ translate: ['0px', '100px']
+ },
+ id: 'transformed',
+ createelement: 'width:100px; height:100px; background: -moz-element(#transformed)',
+ expected: [
+ {
+ property: 'translate',
+ runningOnCompositor: false,
+ warning: 'CompositorAnimationWarningHasRenderingObserver'
+ }
+ ]
+ },
+ {
+ desc: 'moz-element referencing a translate and transform',
+ frames: {
+ transform: ['translate(0px)', 'translate(100px)'],
+ translate: ['0px', '100px']
+ },
+ id: 'transformed',
+ createelement: 'width:100px; height:100px; background: -moz-element(#transformed)',
+ expected: [
+ {
+ property: 'translate',
+ runningOnCompositor: false,
+ warning: 'CompositorAnimationWarningHasRenderingObserver'
+ },
+ {
+ property: 'transform',
+ runningOnCompositor: false,
+ warning: 'CompositorAnimationWarningHasRenderingObserver'
+ }
+ ]
+ },
+ ].forEach(subtest => {
+ promise_test(async t => {
+ if (subtest.createelement) {
+ addDiv(t, { style: subtest.createelement });
+ }
+
+ var animation = addDivAndAnimate(t, { class: 'compositable' },
+ subtest.frames, 100 * MS_PER_SEC);
+ await waitForPaints();
+
+ assert_all_properties_running_on_compositor(
+ animation.effect.getProperties(),
+ subtest.expected);
+ animation.effect.target.id = subtest.id;
+ await waitForFrame();
+
+ assert_animation_property_state_equals(
+ animation.effect.getProperties(),
+ subtest.expected);
+ animation.effect.target.id = '';
+ await waitForFrame();
+
+ assert_all_properties_running_on_compositor(
+ animation.effect.getProperties(),
+ subtest.expected);
+ }, subtest.desc);
+ });
+}
+
+function testMultipleAnimations() {
+ [
+ {
+ desc: 'opacity and transform-like properties with preserve-3d',
+ style: 'transform-style: preserve-3d',
+ animations: [
+ {
+ frames: {
+ transform: ['translate(0px)', 'translate(100px)']
+ },
+ expected: [
+ {
+ property: 'transform',
+ runningOnCompositor: true,
+ }
+ ]
+ },
+ {
+ frames: {
+ translate: ['0px', '100px']
+ },
+ expected: [
+ {
+ property: 'translate',
+ runningOnCompositor: true,
+ }
+ ]
+ },
+ {
+ frames: {
+ opacity: [0, 1]
+ },
+ expected: [
+ {
+ property: 'opacity',
+ runningOnCompositor: true,
+ }
+ ]
+ }
+ ],
+ },
+ {
+ desc: 'opacity and transform-like properties with ' +
+ 'backface-visibility:hidden',
+ style: 'backface-visibility: hidden;',
+ animations: [
+ {
+ frames: {
+ transform: ['translate(0px)', 'translate(100px)']
+ },
+ expected: [
+ {
+ property: 'transform',
+ runningOnCompositor: true,
+ }
+ ]
+ },
+ {
+ frames: {
+ translate: ['0px', '100px']
+ },
+ expected: [
+ {
+ property: 'translate',
+ runningOnCompositor: true,
+ }
+ ]
+ },
+ {
+ frames: {
+ opacity: [0, 1]
+ },
+ expected: [
+ {
+ property: 'opacity',
+ runningOnCompositor: true,
+ }
+ ]
+ }
+ ],
+ },
+ ].forEach(subtest => {
+ promise_test(async t => {
+ var div = addDiv(t, { class: 'compositable' });
+ var animations = subtest.animations.map(anim => {
+ var animation = div.animate(anim.frames, 100 * MS_PER_SEC);
+
+ // Bind expected values to animation object.
+ animation.expected = anim.expected;
+ return animation;
+ });
+ await waitForPaints();
+
+ animations.forEach(anim => {
+ assert_all_properties_running_on_compositor(
+ anim.effect.getProperties(),
+ anim.expected);
+ });
+ div.style = subtest.style;
+ await waitForFrame();
+
+ animations.forEach(anim => {
+ assert_animation_property_state_equals(
+ anim.effect.getProperties(),
+ anim.expected);
+ });
+ div.style = '';
+ await waitForFrame();
+
+ animations.forEach(anim => {
+ assert_all_properties_running_on_compositor(
+ anim.effect.getProperties(),
+ anim.expected);
+ });
+ }, 'Multiple animations: ' + subtest.desc);
+ });
+}
+
+// Test adding/removing a 'width' keyframe on the same animation object, where
+// multiple animation objects belong to the same element.
+// The 'width' property is added to animations[1].
+function testMultipleAnimationsWithGeometricKeyframes() {
+ [
+ {
+ desc: 'transform and opacity with geometric keyframes',
+ animations: [
+ {
+ frames: {
+ transform: ['translate(0px)', 'translate(100px)']
+ },
+ expected: {
+ withoutGeometric: [
+ {
+ property: 'transform',
+ runningOnCompositor: true
+ }
+ ],
+ withGeometric: [
+ {
+ property: 'transform',
+ runningOnCompositor: true,
+ }
+ ]
+ }
+ },
+ {
+ frames: {
+ opacity: [0, 1]
+ },
+ expected: {
+ withoutGeometric: [
+ {
+ property: 'opacity',
+ runningOnCompositor: true,
+ }
+ ],
+ withGeometric: [
+ {
+ property: 'width',
+ runningOnCompositor: false,
+ },
+ {
+ property: 'opacity',
+ runningOnCompositor: true,
+ }
+ ]
+ }
+ }
+ ],
+ },
+ {
+ desc: 'opacity and transform with geometric keyframes',
+ animations: [
+ {
+ frames: {
+ opacity: [0, 1]
+ },
+ expected: {
+ withoutGeometric: [
+ {
+ property: 'opacity',
+ runningOnCompositor: true,
+ }
+ ],
+ withGeometric: [
+ {
+ property: 'opacity',
+ runningOnCompositor: true,
+ }
+ ]
+ }
+ },
+ {
+ frames: {
+ transform: ['translate(0px)', 'translate(100px)']
+ },
+ expected: {
+ withoutGeometric: [
+ {
+ property: 'transform',
+ runningOnCompositor: true
+ }
+ ],
+ withGeometric: [
+ {
+ property: 'width',
+ runningOnCompositor: false,
+ },
+ {
+ property: 'transform',
+ runningOnCompositor: true,
+ }
+ ]
+ }
+ }
+ ]
+ },
+ {
+ desc: 'opacity and translate with geometric keyframes',
+ animations: [
+ {
+ frames: {
+ opacity: [0, 1]
+ },
+ expected: {
+ withoutGeometric: [
+ {
+ property: 'opacity',
+ runningOnCompositor: true,
+ }
+ ],
+ withGeometric: [
+ {
+ property: 'opacity',
+ runningOnCompositor: true,
+ }
+ ]
+ }
+ },
+ {
+ frames: {
+ translate: ['0px', '100px']
+ },
+ expected: {
+ withoutGeometric: [
+ {
+ property: 'translate',
+ runningOnCompositor: true
+ }
+ ],
+ withGeometric: [
+ {
+ property: 'width',
+ runningOnCompositor: false,
+ },
+ {
+ property: 'translate',
+ runningOnCompositor: true,
+ }
+ ]
+ }
+ }
+ ]
+ },
+ ].forEach(subtest => {
+ promise_test(async t => {
+ var div = addDiv(t, { class: 'compositable' });
+ var animations = subtest.animations.map(anim => {
+ var animation = div.animate(anim.frames, 100 * MS_PER_SEC);
+
+ // Bind expected values to animation object.
+ animation.expected = anim.expected;
+ return animation;
+ });
+ await waitForPaints();
+ // First, all animations are running on compositor.
+ animations.forEach(anim => {
+ assert_animation_property_state_equals(
+ anim.effect.getProperties(),
+ anim.expected.withoutGeometric);
+ });
+
+ // Add a 'width' property to animations[1].
+ var keyframes = animations[1].effect.getKeyframes();
+
+ keyframes[0].width = '100px';
+ keyframes[1].width = '200px';
+
+ animations[1].effect.setKeyframes(keyframes);
+ await waitForFrame();
+
+ // Now the transform animation is not running on compositor because of
+ // the 'width' property.
+ animations.forEach(anim => {
+ assert_animation_property_state_equals(
+ anim.effect.getProperties(),
+ anim.expected.withGeometric);
+ });
+
+ // Remove the 'width' property from animations[1].
+ var keyframes = animations[1].effect.getKeyframes();
+
+ delete keyframes[0].width;
+ delete keyframes[1].width;
+
+ animations[1].effect.setKeyframes(keyframes);
+ await waitForFrame();
+
+ // Finally, all animations are running on compositor.
+ animations.forEach(anim => {
+ assert_animation_property_state_equals(
+ anim.effect.getProperties(),
+ anim.expected.withoutGeometric);
+ });
+ }, 'Multiple animations with geometric property: ' + subtest.desc);
+ });
+}
+
+// Tests adding/removing 'width' animation on the same element which has async
+// animations.
+function testMultipleAnimationsWithGeometricAnimations() {
+ [
+ {
+ desc: 'transform',
+ animations: [
+ {
+ frames: {
+ transform: ['translate(0px)', 'translate(100px)']
+ },
+ expected: [
+ {
+ property: 'transform',
+ runningOnCompositor: true,
+ }
+ ]
+ },
+ ]
+ },
+ {
+ desc: 'translate',
+ animations: [
+ {
+ frames: {
+ translate: ['0px', '100px']
+ },
+ expected: [
+ {
+ property: 'translate',
+ runningOnCompositor: true,
+ }
+ ]
+ },
+ ]
+ },
+ {
+ desc: 'opacity',
+ animations: [
+ {
+ frames: {
+ opacity: [0, 1]
+ },
+ expected: [
+ {
+ property: 'opacity',
+ runningOnCompositor: true
+ }
+ ]
+ },
+ ]
+ },
+ {
+ desc: 'opacity, transform, and translate',
+ animations: [
+ {
+ frames: {
+ transform: ['translate(0px)', 'translate(100px)']
+ },
+ expected: [
+ {
+ property: 'transform',
+ runningOnCompositor: true,
+ }
+ ]
+ },
+ {
+ frames: {
+ translate: ['0px', '100px']
+ },
+ expected: [
+ {
+ property: 'translate',
+ runningOnCompositor: true,
+ }
+ ]
+ },
+ {
+ frames: {
+ opacity: [0, 1]
+ },
+ expected: [
+ {
+ property: 'opacity',
+ runningOnCompositor: true,
+ }
+ ]
+ }
+ ],
+ },
+ ].forEach(subtest => {
+ promise_test(async t => {
+ var div = addDiv(t, { class: 'compositable' });
+ var animations = subtest.animations.map(anim => {
+ var animation = div.animate(anim.frames, 100 * MS_PER_SEC);
+
+ // Bind expected values to animation object.
+ animation.expected = anim.expected;
+ return animation;
+ });
+
+ var widthAnimation;
+
+ await waitForPaints();
+ animations.forEach(anim => {
+ assert_all_properties_running_on_compositor(
+ anim.effect.getProperties(),
+ anim.expected);
+ });
+
+ // Append 'width' animation on the same element.
+ widthAnimation = div.animate({ width: ['100px', '200px'] },
+ 100 * MS_PER_SEC);
+ await waitForFrame();
+
+ // Now transform animations are not running on compositor because of
+ // the 'width' animation.
+ animations.forEach(anim => {
+ assert_animation_property_state_equals(
+ anim.effect.getProperties(),
+ anim.expected);
+ });
+ // Remove the 'width' animation.
+ widthAnimation.cancel();
+ await waitForFrame();
+
+ // Now all animations are running on compositor.
+ animations.forEach(anim => {
+ assert_all_properties_running_on_compositor(
+ anim.effect.getProperties(),
+ anim.expected);
+ });
+ }, 'Multiple async animations and geometric animation: ' + subtest.desc);
+ });
+}
+
+function testSmallElements() {
+ [
+ {
+ desc: 'opacity on small element',
+ frames: {
+ opacity: [0, 1]
+ },
+ style: { style: 'width: 8px; height: 8px; background-color: red;' +
+ // We need to set transform here to try creating an
+ // individual frame for this opacity element.
+ // Without this, this small element is created on the same
+ // nsIFrame of mochitest iframe, i.e. the document which are
+ // running this test, as a result the layer corresponding
+ // to the frame is sent to compositor.
+ 'transform: translateX(100px);' },
+ expected: [
+ {
+ property: 'opacity',
+ runningOnCompositor: true
+ }
+ ]
+ },
+ {
+ desc: 'transform on small element',
+ frames: {
+ transform: ['translate(0px)', 'translate(100px)']
+ },
+ style: { style: 'width: 8px; height: 8px; background-color: red;' },
+ expected: [
+ {
+ property: 'transform',
+ runningOnCompositor: true
+ }
+ ]
+ },
+ {
+ desc: 'translate on small element',
+ frames: {
+ translate: ['0px', '100px']
+ },
+ style: { style: 'width: 8px; height: 8px; background-color: red;' },
+ expected: [
+ {
+ property: 'translate',
+ runningOnCompositor: true
+ }
+ ]
+ },
+ ].forEach(subtest => {
+ promise_test(async t => {
+ var div = addDiv(t, subtest.style);
+ var animation = div.animate(subtest.frames, 100 * MS_PER_SEC);
+ await waitForPaints();
+
+ assert_animation_property_state_equals(
+ animation.effect.getProperties(),
+ subtest.expected);
+ }, subtest.desc);
+ });
+}
+
+function testSynchronizedAnimations() {
+ promise_test(async t => {
+ const elemA = addDiv(t, { class: 'compositable' });
+ const elemB = addDiv(t, { class: 'compositable' });
+
+ const animA = elemA.animate({ transform: [ 'translate(0px)',
+ 'translate(100px)' ] },
+ 100 * MS_PER_SEC);
+ const animB = elemB.animate({ marginLeft: [ '0px', '100px' ] },
+ 100 * MS_PER_SEC);
+
+ await Promise.all([animA.ready, animB.ready]);
+ await waitForPaints();
+
+ assert_animation_property_state_equals(
+ animA.effect.getProperties(),
+ [ { property: 'transform',
+ runningOnCompositor: true,
+ } ]);
+ }, 'Animations created within the same tick are synchronized'
+ + ' (compositor animation created first)');
+
+ promise_test(async t => {
+ const elemA = addDiv(t, { class: 'compositable' });
+ const elemB = addDiv(t, { class: 'compositable' });
+ const elemC = addDiv(t, { class: 'compositable' });
+
+ const animA = elemA.animate({ transform: [ 'translate(0px)',
+ 'translate(100px)' ] },
+ 100 * MS_PER_SEC);
+ const animB = elemB.animate({ translate: [ '0px', '100px' ] },
+ 100 * MS_PER_SEC);
+ const animC = elemC.animate({ marginLeft: [ '0px', '100px' ] },
+ 100 * MS_PER_SEC);
+
+ await Promise.all([animA.ready, animB.ready, animC.ready]);
+ await waitForPaints();
+
+ assert_animation_property_state_equals(
+ animA.effect.getProperties(),
+ [
+ { property: 'transform',
+ runningOnCompositor: true,
+ } ]);
+ assert_animation_property_state_equals(
+ animB.effect.getProperties(),
+ [
+ { property: 'translate',
+ runningOnCompositor: true,
+ } ]);
+ }, 'Animations created within the same tick are synchronized'
+ + ' (compositor animation created first/second)');
+
+ promise_test(async t => {
+ const elemA = addDiv(t, { class: 'compositable' });
+ const elemB = addDiv(t, { class: 'compositable' });
+ const elemC = addDiv(t, { class: 'compositable' });
+
+ const animA = elemA.animate({ marginLeft: [ '0px', '100px' ] },
+ 100 * MS_PER_SEC);
+ const animB = elemB.animate({ transform: [ 'translate(0px)',
+ 'translate(100px)' ] },
+ 100 * MS_PER_SEC);
+ const animC = elemC.animate({ translate: [ '0px', '100px' ] },
+ 100 * MS_PER_SEC);
+
+ await Promise.all([animA.ready, animB.ready, animC.ready]);
+ await waitForPaints();
+
+ assert_animation_property_state_equals(
+ animB.effect.getProperties(),
+ [ { property: 'transform',
+ runningOnCompositor: true,
+ } ]);
+ assert_animation_property_state_equals(
+ animC.effect.getProperties(),
+ [
+ { property: 'translate',
+ runningOnCompositor: true,
+ } ]);
+ }, 'Animations created within the same tick are synchronized'
+ + ' (compositor animation created second/third)');
+
+ promise_test(async t => {
+ const attrs = { class: 'compositable',
+ style: 'transition: all 100s' };
+ const elemA = addDiv(t, attrs);
+ const elemB = addDiv(t, attrs);
+ elemA.style.transform = 'translate(0px)';
+ elemB.style.marginLeft = '0px';
+ getComputedStyle(elemA).transform;
+ getComputedStyle(elemB).marginLeft;
+
+ // Generally the sequence of steps is as follows:
+ //
+ // Tick -> requestAnimationFrame -> Style -> Paint -> Events (-> Tick...)
+ //
+ // In this test we want to set up two transitions during the "Events"
+ // stage but only flush style for one such that the second one is actually
+ // generated during the "Style" stage of the *next* tick.
+ //
+ // Web content often generates transitions in this way (that is, it doesn't
+ // pay regard to when style is flushed and nor should it). However, we
+ // still want transitions generated in this way to be synchronized.
+ let timeForFirstFrame;
+ await waitForIdle();
+
+ timeForFirstFrame = document.timeline.currentTime;
+ elemA.style.transform = 'translate(100px)';
+ // Flush style to trigger first transition
+ getComputedStyle(elemA).transform;
+ elemB.style.marginLeft = '100px';
+ // DON'T flush style here (this includes calling getAnimations!)
+ await waitForFrame();
+
+ assert_not_equals(timeForFirstFrame, document.timeline.currentTime,
+ 'Should be on the other side of a tick');
+ // Wait another tick so we can let the transition be started
+ // by regular style resolution.
+ await waitForFrame();
+
+ const transitionA = elemA.getAnimations()[0];
+ assert_animation_property_state_equals(
+ transitionA.effect.getProperties(),
+ [ { property: 'transform',
+ runningOnCompositor: true,
+ } ]);
+ }, 'Transitions created before and after a tick are synchronized');
+
+ promise_test(async t => {
+ const elemA = addDiv(t, { class: 'compositable' });
+ const elemB = addDiv(t, { class: 'compositable' });
+
+ const animA = elemA.animate({ transform: [ 'translate(0px)',
+ 'translate(100px)' ],
+ opacity: [ 0, 1 ] },
+ 100 * MS_PER_SEC);
+ const animB = elemB.animate({ marginLeft: [ '0px', '100px' ] },
+ 100 * MS_PER_SEC);
+
+ await Promise.all([animA.ready, animB.ready]);
+ await waitForPaints();
+
+ assert_animation_property_state_equals(
+ animA.effect.getProperties(),
+ [ { property: 'transform',
+ runningOnCompositor: true,
+ },
+ { property: 'opacity',
+ runningOnCompositor: true
+ } ]);
+ }, 'Opacity animations on the same element continue running on the'
+ + ' compositor when transform animations are synchronized with geometric'
+ + ' animations');
+
+ promise_test(async t => {
+ const transitionElem = addDiv(t, {
+ style: 'margin-left: 0px; transition: margin-left 100s',
+ });
+ getComputedStyle(transitionElem).marginLeft;
+
+ await waitForFrame();
+
+ transitionElem.style.marginLeft = '100px';
+ const cssTransition = transitionElem.getAnimations()[0];
+
+ const animationElem = addDiv(t, {
+ class: 'compositable',
+ style: 'animation: translate 100s',
+ });
+ const cssAnimation = animationElem.getAnimations()[0];
+
+ await Promise.all([cssTransition.ready, cssAnimation.ready]);
+ await waitForPaints();
+
+ assert_animation_property_state_equals(cssAnimation.effect.getProperties(),
+ [{ property: 'transform',
+ runningOnCompositor: true }]);
+ }, 'CSS Animations are NOT synchronized with CSS Transitions');
+
+ promise_test(async t => {
+ const elemA = addDiv(t, { class: 'compositable' });
+ const elemB = addDiv(t, { class: 'compositable' });
+
+ const animA = elemA.animate({ marginLeft: [ '0px', '100px' ] },
+ 100 * MS_PER_SEC);
+ await animA.ready;
+ await waitForPaints();
+
+ let animB = elemB.animate({ transform: [ 'translate(0px)',
+ 'translate(100px)' ] },
+ 100 * MS_PER_SEC);
+ await animB.ready;
+ await waitForPaints();
+
+ assert_animation_property_state_equals(
+ animB.effect.getProperties(),
+ [ { property: 'transform',
+ runningOnCompositor: true } ]);
+ }, 'Transform animations are NOT synchronized with geometric animations'
+ + ' started in the previous frame');
+
+ promise_test(async t => {
+ const elemA = addDiv(t, { class: 'compositable' });
+ const elemB = addDiv(t, { class: 'compositable' });
+
+ const animA = elemA.animate({ transform: [ 'translate(0px)',
+ 'translate(100px)' ] },
+ 100 * MS_PER_SEC);
+ await animA.ready;
+ await waitForPaints();
+
+ let animB = elemB.animate({ marginLeft: [ '0px', '100px' ] },
+ 100 * MS_PER_SEC);
+ await animB.ready;
+ await waitForPaints();
+
+ assert_animation_property_state_equals(
+ animA.effect.getProperties(),
+ [ { property: 'transform',
+ runningOnCompositor: true } ]);
+ }, 'Transform animations are NOT synchronized with geometric animations'
+ + ' started in the next frame');
+
+ promise_test(async t => {
+ const elemA = addDiv(t, { class: 'compositable' });
+ const elemB = addDiv(t, { class: 'compositable' });
+
+ const animA = elemA.animate({ transform: [ 'translate(0px)',
+ 'translate(100px)' ] },
+ 100 * MS_PER_SEC);
+ const animB = elemB.animate({ marginLeft: [ '0px', '100px' ] },
+ 100 * MS_PER_SEC);
+ animB.pause();
+
+ await animA.ready;
+ await waitForPaints();
+
+ assert_animation_property_state_equals(
+ animA.effect.getProperties(),
+ [ { property: 'transform', runningOnCompositor: true } ]);
+ }, 'Paused animations are not synchronized');
+
+ promise_test(async t => {
+ const elemA = addDiv(t, { class: 'compositable' });
+ const elemB = addDiv(t, { class: 'compositable' });
+
+ const animA = elemA.animate({ transform: [ 'translate(0px)',
+ 'translate(100px)' ] },
+ 100 * MS_PER_SEC);
+ const animB = elemB.animate({ marginLeft: [ '0px', '100px' ] },
+ 100 * MS_PER_SEC);
+
+ // Seek one of the animations so that their start times will differ
+ animA.currentTime = 5000;
+
+ await Promise.all([animA.ready, animB.ready]);
+ await waitForPaints();
+
+ assert_not_equals(animA.startTime, animB.startTime,
+ 'Animations should have different start times');
+ assert_animation_property_state_equals(
+ animA.effect.getProperties(),
+ [ { property: 'transform',
+ runningOnCompositor: true,
+ } ]);
+ }, 'Animations are synchronized based on when they are started'
+ + ' and NOT their start time');
+
+ promise_test(async t => {
+ const elemA = addDiv(t, { class: 'compositable' });
+ const elemB = addDiv(t, { class: 'compositable' });
+
+ const animA = elemA.animate({ transform: [ 'translate(0px)',
+ 'translate(100px)' ] },
+ 100 * MS_PER_SEC);
+ const animB = elemB.animate({ marginLeft: [ '0px', '100px' ] },
+ 100 * MS_PER_SEC);
+
+ await Promise.all([animA.ready, animB.ready]);
+ await waitForPaints();
+
+ assert_animation_property_state_equals(
+ animA.effect.getProperties(),
+ [ { property: 'transform',
+ runningOnCompositor: true } ]);
+ // Restart animation
+ animA.pause();
+ animA.play();
+ await animA.ready;
+ await waitForPaints();
+
+ assert_animation_property_state_equals(
+ animA.effect.getProperties(),
+ [ { property: 'transform',
+ runningOnCompositor: true } ]);
+ }, 'An initially synchronized animation may be unsynchronized if restarted');
+
+ promise_test(async t => {
+ const elemA = addDiv(t, { class: 'compositable' });
+ const elemB = addDiv(t, { class: 'compositable' });
+
+ const animA = elemA.animate({ transform: [ 'translate(0px)',
+ 'translate(100px)' ] },
+ 100 * MS_PER_SEC);
+ const animB = elemB.animate({ marginLeft: [ '0px', '100px' ] },
+ 100 * MS_PER_SEC);
+
+ // Clear target effect
+ animB.effect.target = null;
+
+ await Promise.all([animA.ready, animB.ready]);
+ await waitForPaints();
+
+ assert_animation_property_state_equals(
+ animA.effect.getProperties(),
+ [ { property: 'transform',
+ runningOnCompositor: true } ]);
+ }, 'A geometric animation with no target element is not synchronized');
+}
+
+function testTooLargeFrame() {
+ [
+ {
+ property: 'transform',
+ frames: { transform: ['translate(0px)', 'translate(100px)'] },
+ },
+ {
+ property: 'translate',
+ frames: { translate: ['0px', '100px'] },
+ },
+ ].forEach(subtest => {
+ promise_test(async t => {
+ var animation = addDivAndAnimate(t,
+ { class: 'compositable' },
+ subtest.frames,
+ 100 * MS_PER_SEC);
+ await waitForPaints();
+
+ assert_animation_property_state_equals(
+ animation.effect.getProperties(),
+ [ { property: subtest.property, runningOnCompositor: true } ]);
+ animation.effect.target.style = 'width: 10000px; height: 10000px';
+ await waitForFrame();
+
+ // viewport depends on test environment.
+ var expectedWarning = new RegExp(
+ "Animation cannot be run on the compositor because the area of the frame " +
+ "\\(\\d+\\) is too large relative to the viewport " +
+ "\\(larger than \\d+\\)");
+ assert_animation_property_state_equals(
+ animation.effect.getProperties(),
+ [ {
+ property: subtest.property,
+ runningOnCompositor: false,
+ warning: expectedWarning
+ } ]);
+ animation.effect.target.style = 'width: 100px; height: 100px';
+ await waitForFrame();
+
+ // With WebRender we appear to stick to the previous layerization decision
+ // after changing the bounds back to a smaller object.
+ const isWebRender =
+ SpecialPowers.DOMWindowUtils.layerManagerType.startsWith('WebRender');
+ assert_animation_property_state_equals(
+ animation.effect.getProperties(),
+ [ { property: subtest.property, runningOnCompositor: !isWebRender } ]);
+ }, subtest.property + ' on too big element - area');
+
+ promise_test(async t => {
+ var animation = addDivAndAnimate(t,
+ { class: 'compositable' },
+ subtest.frames,
+ 100 * MS_PER_SEC);
+ await waitForPaints();
+
+ assert_animation_property_state_equals(
+ animation.effect.getProperties(),
+ [ { property: subtest.property, runningOnCompositor: true } ]);
+ animation.effect.target.style = 'width: 20000px; height: 1px';
+ await waitForFrame();
+
+ // viewport depends on test environment.
+ var expectedWarning = new RegExp(
+ "Animation cannot be run on the compositor because the frame size " +
+ "\\(20000, 1\\) is too large relative to the viewport " +
+ "\\(larger than \\(\\d+, \\d+\\)\\) or larger than the " +
+ "maximum allowed value \\(\\d+, \\d+\\)");
+ assert_animation_property_state_equals(
+ animation.effect.getProperties(),
+ [ {
+ property: subtest.property,
+ runningOnCompositor: false,
+ warning: expectedWarning
+ } ]);
+ animation.effect.target.style = 'width: 100px; height: 100px';
+ await waitForFrame();
+
+ const isWebRender =
+ SpecialPowers.DOMWindowUtils.layerManagerType.startsWith('WebRender');
+ assert_animation_property_state_equals(
+ animation.effect.getProperties(),
+ [ { property: subtest.property, runningOnCompositor: !isWebRender } ]);
+ }, subtest.property + ' on too big element - dimensions');
+ });
+}
+
+function testTransformSVG() {
+ [
+ {
+ property: 'transform',
+ frames: { transform: ['translate(0px)', 'translate(100px)'] },
+ },
+ {
+ property: 'translate',
+ frames: { translate: ['0px', '100px'] },
+ },
+ {
+ property: 'rotate',
+ frames: { rotate: ['0deg', '45deg'] },
+ },
+ {
+ property: 'scale',
+ frames: { scale: ['1', '2'] },
+ },
+ ].forEach(subtest => {
+ promise_test(async t => {
+ var svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
+ svg.setAttribute('width', '100');
+ svg.setAttribute('height', '100');
+ var rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
+ rect.setAttribute('width', '100');
+ rect.setAttribute('height', '100');
+ rect.setAttribute('fill', 'red');
+ svg.appendChild(rect);
+ document.body.appendChild(svg);
+ t.add_cleanup(() => {
+ svg.remove();
+ });
+
+ var animation = svg.animate(subtest.frames, 100 * MS_PER_SEC);
+ await waitForPaints();
+
+ assert_animation_property_state_equals(
+ animation.effect.getProperties(),
+ [ { property: subtest.property, runningOnCompositor: true } ]);
+ svg.setAttribute('transform', 'translate(10, 20)');
+ await waitForFrame();
+
+ assert_animation_property_state_equals(
+ animation.effect.getProperties(),
+ [ {
+ property: subtest.property,
+ runningOnCompositor: false,
+ warning: 'CompositorAnimationWarningTransformSVG'
+ } ]);
+ svg.removeAttribute('transform');
+ await waitForFrame();
+
+ assert_animation_property_state_equals(
+ animation.effect.getProperties(),
+ [ { property: subtest.property, runningOnCompositor: true } ]);
+ }, subtest.property + ' of nsIFrame with SVG transform');
+ });
+}
+
+function testImportantRuleOverride() {
+ promise_test(async t => {
+ const elem = addDiv(t, { class: 'compositable' });
+ const anim = elem.animate({ translate: [ '0px', '100px' ],
+ rotate: ['0deg', '90deg'] },
+ 100 * MS_PER_SEC);
+
+ await waitForAnimationReadyToRestyle(anim);
+ await waitForPaints();
+
+ assert_animation_property_state_equals(
+ anim.effect.getProperties(),
+ [ { property: 'translate', runningOnCompositor: true },
+ { property: 'rotate', runningOnCompositor: true } ]
+ );
+
+ elem.style.setProperty('rotate', '45deg', 'important');
+ getComputedStyle(elem).rotate;
+
+ await waitForFrame();
+
+ assert_animation_property_state_equals(
+ anim.effect.getProperties(),
+ [
+ {
+ property: 'translate',
+ runningOnCompositor: false,
+ warning:
+ 'CompositorAnimationWarningTransformIsBlockedByImportantRules'
+ },
+ {
+ property: 'rotate',
+ runningOnCompositor: false,
+ warning:
+ 'CompositorAnimationWarningTransformIsBlockedByImportantRules'
+ },
+ ]
+ );
+ }, 'The animations of transform-like properties are not running on the ' +
+ 'compositor because any of the properties has important rules');
+}
+
+function testCurrentColor() {
+ if (SpecialPowers.DOMWindowUtils.layerManagerType.startsWith('WebRender')) {
+ return; // skip this test until bug 1510030 landed.
+ }
+ promise_test(async t => {
+ const animation = addDivAndAnimate(t, { class: 'compositable' },
+ { backgroundColor: [ 'currentColor',
+ 'red' ] },
+ 100 * MS_PER_SEC);
+ await waitForPaints();
+
+ assert_animation_property_state_equals(
+ animation.effect.getProperties(),
+ [ { property: 'background-color',
+ runningOnCompositor: false,
+ warning: 'CompositorAnimationWarningHasCurrentColor'
+ } ]);
+ }, 'Background color animations with `current-color` don\'t run on the '
+ + 'compositor');
+}
+
+function start() {
+ var bundleService = SpecialPowers.Cc['@mozilla.org/intl/stringbundle;1']
+ .getService(SpecialPowers.Ci.nsIStringBundleService);
+ gStringBundle = bundleService
+ .createBundle("chrome://global/locale/layout_errors.properties");
+
+ testBasicOperation();
+ testKeyframesWithGeometricProperties();
+ testSetOfGeometricProperties();
+ testStyleChanges();
+ testIdChanges();
+ testMultipleAnimations();
+ testMultipleAnimationsWithGeometricKeyframes();
+ testMultipleAnimationsWithGeometricAnimations();
+ testSmallElements();
+ testSynchronizedAnimations();
+ testTooLargeFrame();
+ testTransformSVG();
+ testImportantRuleOverride();
+ testCurrentColor();
+
+ promise_test(async t => {
+ var div = addDiv(t, { class: 'compositable',
+ style: 'animation: fade 100s' });
+ var cssAnimation = div.getAnimations()[0];
+ var scriptAnimation = div.animate({ opacity: [ 1, 0 ] }, 100 * MS_PER_SEC);
+
+ await waitForPaints();
+ assert_animation_property_state_equals(
+ cssAnimation.effect.getProperties(),
+ [ { property: 'opacity', runningOnCompositor: true } ]);
+ assert_animation_property_state_equals(
+ scriptAnimation.effect.getProperties(),
+ [ { property: 'opacity', runningOnCompositor: true } ]);
+ }, 'overridden animation');
+
+ promise_test(async t => {
+ const keyframes = {
+ width: [ '100px', '200px' ],
+ transform: [ 'translate(0px)', 'translate(100px)' ],
+ "--foo": ["--bar", "--baz"],
+ };
+ const animation = addDivAndAnimate(t, { class: 'compositable' },
+ keyframes, 100 * MS_PER_SEC);
+ await waitForPaints();
+
+ assert_true(true, "Didn't crash");
+ }, 'Warning with custom props');
+
+ done();
+}
+
+</script>
+
+</body>
diff --git a/dom/animation/test/chrome/test_animation_properties.html b/dom/animation/test/chrome/test_animation_properties.html
new file mode 100644
index 0000000000..497b04e068
--- /dev/null
+++ b/dom/animation/test/chrome/test_animation_properties.html
@@ -0,0 +1,837 @@
+<!doctype html>
+<head>
+<meta charset=utf-8>
+<title>Bug 1254419 - Test the values returned by
+ KeyframeEffect.getProperties()</title>
+<script type="application/javascript" src="../testharness.js"></script>
+<script type="application/javascript" src="../testharnessreport.js"></script>
+<script type="application/javascript" src="../testcommon.js"></script>
+</head>
+<body>
+<a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1254419"
+ target="_blank">Mozilla Bug 1254419</a>
+<div id="log"></div>
+<style>
+@property --my-color {
+ syntax: "<color>";
+ inherits: true;
+ initial-value: "gold";
+}
+
+:root {
+ --var-100px: 100px;
+ --var-100px-200px: 100px 200px;
+}
+div {
+ font-size: 10px; /* For calculating em-based units */
+}
+</style>
+<script>
+'use strict';
+
+var gTests = [
+
+ // ---------------------------------------------------------------------
+ //
+ // Tests for property-indexed specifications
+ //
+ // ---------------------------------------------------------------------
+
+ { desc: 'a one-property two-value property-indexed specification',
+ frames: { left: ['10px', '20px'] },
+ expected: [ { property: 'left',
+ values: [ valueFormat(0, '10px', 'replace', 'linear'),
+ valueFormat(1, '20px', 'replace') ] } ]
+ },
+ { desc: 'a one-shorthand-property two-value property-indexed'
+ + ' specification',
+ frames: { margin: ['10px', '10px 20px 30px 40px'] },
+ expected: [ { property: 'margin-top',
+ values: [ valueFormat(0, '10px', 'replace', 'linear'),
+ valueFormat(1, '10px', 'replace') ] },
+ { property: 'margin-right',
+ values: [ valueFormat(0, '10px', 'replace', 'linear'),
+ valueFormat(1, '20px', 'replace') ] },
+ { property: 'margin-bottom',
+ values: [ valueFormat(0, '10px', 'replace', 'linear'),
+ valueFormat(1, '30px', 'replace') ] },
+ { property: 'margin-left',
+ values: [ valueFormat(0, '10px', 'replace', 'linear'),
+ valueFormat(1, '40px', 'replace') ] } ]
+ },
+ { desc: 'a two-property (one shorthand and one of its longhand'
+ + ' components) two-value property-indexed specification',
+ frames: { marginTop: ['50px', '60px'],
+ margin: ['10px', '10px 20px 30px 40px'] },
+ expected: [ { property: 'margin-top',
+ values: [ valueFormat(0, '50px', 'replace', 'linear'),
+ valueFormat(1, '60px', 'replace') ] },
+ { property: 'margin-right',
+ values: [ valueFormat(0, '10px', 'replace', 'linear'),
+ valueFormat(1, '20px', 'replace') ] },
+ { property: 'margin-bottom',
+ values: [ valueFormat(0, '10px', 'replace', 'linear'),
+ valueFormat(1, '30px', 'replace') ] },
+ { property: 'margin-left',
+ values: [ valueFormat(0, '10px', 'replace', 'linear'),
+ valueFormat(1, '40px', 'replace') ] } ]
+ },
+ { desc: 'a two-property property-indexed specification with different'
+ + ' numbers of values',
+ frames: { left: ['10px', '20px', '30px'],
+ top: ['40px', '50px'] },
+ expected: [ { property: 'left',
+ values: [ valueFormat(0, '10px', 'replace', 'linear'),
+ valueFormat(0.5, '20px', 'replace', 'linear'),
+ valueFormat(1, '30px', 'replace') ] },
+ { property: 'top',
+ values: [ valueFormat(0, '40px', 'replace', 'linear'),
+ valueFormat(1, '50px', 'replace') ] } ]
+ },
+ { desc: 'a property-indexed specification with an invalid value',
+ frames: { left: ['10px', '20px', '30px', '40px', '50px'],
+ top: ['15px', '25px', 'invalid', '45px', '55px'] },
+ expected: [ { property: 'left',
+ values: [ valueFormat(0, '10px', 'replace', 'linear'),
+ valueFormat(0.25, '20px', 'replace', 'linear'),
+ valueFormat(0.5, '30px', 'replace', 'linear'),
+ valueFormat(0.75, '40px', 'replace', 'linear'),
+ valueFormat(1, '50px', 'replace') ] },
+ { property: 'top',
+ values: [ valueFormat(0, '15px', 'replace', 'linear'),
+ valueFormat(0.25, '25px', 'replace', 'linear'),
+ valueFormat(0.75, '45px', 'replace', 'linear'),
+ valueFormat(1, '55px', 'replace') ] } ]
+ },
+ { desc: 'a one-property two-value property-indexed specification that'
+ + ' needs to stringify its values',
+ frames: { opacity: [0, 1] },
+ expected: [ { property: 'opacity',
+ values: [ valueFormat(0, '0', 'replace', 'linear'),
+ valueFormat(1, '1', 'replace') ] } ]
+ },
+ { desc: 'a property-indexed keyframe where a lesser shorthand precedes'
+ + ' a greater shorthand',
+ frames: { borderLeft: [ '1px solid rgb(1, 2, 3)',
+ '2px solid rgb(4, 5, 6)' ],
+ border: [ '3px dotted rgb(7, 8, 9)',
+ '4px dashed rgb(10, 11, 12)' ] },
+ expected: [ { property: 'border-bottom-color',
+ values: [ valueFormat(0, 'rgb(7, 8, 9)', 'replace', 'linear'),
+ valueFormat(1, 'rgb(10, 11, 12)', 'replace') ] },
+ { property: 'border-left-color',
+ values: [ valueFormat(0, 'rgb(1, 2, 3)', 'replace', 'linear'),
+ valueFormat(1, 'rgb(4, 5, 6)', 'replace') ] },
+ { property: 'border-right-color',
+ values: [ valueFormat(0, 'rgb(7, 8, 9)', 'replace', 'linear'),
+ valueFormat(1, 'rgb(10, 11, 12)', 'replace') ] },
+ { property: 'border-top-color',
+ values: [ valueFormat(0, 'rgb(7, 8, 9)', 'replace', 'linear'),
+ valueFormat(1, 'rgb(10, 11, 12)', 'replace') ] },
+ { property: 'border-bottom-width',
+ values: [ valueFormat(0, '3px', 'replace', 'linear'),
+ valueFormat(1, '4px', 'replace') ] },
+ { property: 'border-left-width',
+ values: [ valueFormat(0, '1px', 'replace', 'linear'),
+ valueFormat(1, '2px', 'replace') ] },
+ { property: 'border-right-width',
+ values: [ valueFormat(0, '3px', 'replace', 'linear'),
+ valueFormat(1, '4px', 'replace') ] },
+ { property: 'border-top-width',
+ values: [ valueFormat(0, '3px', 'replace', 'linear'),
+ valueFormat(1, '4px', 'replace') ] },
+ { property: 'border-bottom-style',
+ values: [ valueFormat(0, 'dotted', 'replace', 'linear'),
+ valueFormat(1, 'dashed', 'replace') ] },
+ { property: 'border-left-style',
+ values: [ valueFormat(0, 'solid', 'replace', 'linear'),
+ valueFormat(1, 'solid', 'replace') ] },
+ { property: 'border-right-style',
+ values: [ valueFormat(0, 'dotted', 'replace', 'linear'),
+ valueFormat(1, 'dashed', 'replace') ] },
+ { property: 'border-top-style',
+ values: [ valueFormat(0, 'dotted', 'replace', 'linear'),
+ valueFormat(1, 'dashed', 'replace') ] },
+ { property: 'border-image-outset',
+ values: [ valueFormat(0, '0', 'replace', 'linear'),
+ valueFormat(1, '0', 'replace') ] },
+ { property: 'border-image-repeat',
+ values: [ valueFormat(0, 'stretch', 'replace', 'linear'),
+ valueFormat(1, 'stretch', 'replace') ] },
+ { property: 'border-image-slice',
+ values: [ valueFormat(0, '100%', 'replace', 'linear'),
+ valueFormat(1, '100%', 'replace') ] },
+ { property: 'border-image-source',
+ values: [ valueFormat(0, 'none', 'replace', 'linear'),
+ valueFormat(1, 'none', 'replace') ] },
+ { property: 'border-image-width',
+ values: [ valueFormat(0, '1', 'replace', 'linear'),
+ valueFormat(1, '1', 'replace') ] },
+ ]
+ },
+ { desc: 'a property-indexed keyframe where a greater shorthand precedes'
+ + ' a lesser shorthand',
+ frames: { border: [ '3px dotted rgb(7, 8, 9)',
+ '4px dashed rgb(10, 11, 12)' ],
+ borderLeft: [ '1px solid rgb(1, 2, 3)',
+ '2px solid rgb(4, 5, 6)' ] },
+ expected: [ { property: 'border-bottom-color',
+ values: [ valueFormat(0, 'rgb(7, 8, 9)', 'replace', 'linear'),
+ valueFormat(1, 'rgb(10, 11, 12)', 'replace') ] },
+ { property: 'border-left-color',
+ values: [ valueFormat(0, 'rgb(1, 2, 3)', 'replace', 'linear'),
+ valueFormat(1, 'rgb(4, 5, 6)', 'replace') ] },
+ { property: 'border-right-color',
+ values: [ valueFormat(0, 'rgb(7, 8, 9)', 'replace', 'linear'),
+ valueFormat(1, 'rgb(10, 11, 12)', 'replace') ] },
+ { property: 'border-top-color',
+ values: [ valueFormat(0, 'rgb(7, 8, 9)', 'replace', 'linear'),
+ valueFormat(1, 'rgb(10, 11, 12)', 'replace') ] },
+ { property: 'border-bottom-width',
+ values: [ valueFormat(0, '3px', 'replace', 'linear'),
+ valueFormat(1, '4px', 'replace') ] },
+ { property: 'border-left-width',
+ values: [ valueFormat(0, '1px', 'replace', 'linear'),
+ valueFormat(1, '2px', 'replace') ] },
+ { property: 'border-right-width',
+ values: [ valueFormat(0, '3px', 'replace', 'linear'),
+ valueFormat(1, '4px', 'replace') ] },
+ { property: 'border-top-width',
+ values: [ valueFormat(0, '3px', 'replace', 'linear'),
+ valueFormat(1, '4px', 'replace') ] },
+ { property: 'border-bottom-style',
+ values: [ valueFormat(0, 'dotted', 'replace', 'linear'),
+ valueFormat(1, 'dashed', 'replace') ] },
+ { property: 'border-left-style',
+ values: [ valueFormat(0, 'solid', 'replace', 'linear'),
+ valueFormat(1, 'solid', 'replace') ] },
+ { property: 'border-right-style',
+ values: [ valueFormat(0, 'dotted', 'replace', 'linear'),
+ valueFormat(1, 'dashed', 'replace') ] },
+ { property: 'border-top-style',
+ values: [ valueFormat(0, 'dotted', 'replace', 'linear'),
+ valueFormat(1, 'dashed', 'replace') ] },
+ { property: 'border-image-outset',
+ values: [ valueFormat(0, '0', 'replace', 'linear'),
+ valueFormat(1, '0', 'replace') ] },
+ { property: 'border-image-repeat',
+ values: [ valueFormat(0, 'stretch', 'replace', 'linear'),
+ valueFormat(1, 'stretch', 'replace') ] },
+ { property: 'border-image-slice',
+ values: [ valueFormat(0, '100%', 'replace', 'linear'),
+ valueFormat(1, '100%', 'replace') ] },
+ { property: 'border-image-source',
+ values: [ valueFormat(0, 'none', 'replace', 'linear'),
+ valueFormat(1, 'none', 'replace') ] },
+ { property: 'border-image-width',
+ values: [ valueFormat(0, '1', 'replace', 'linear'),
+ valueFormat(1, '1', 'replace') ] },
+ ]
+ },
+ { desc: 'custom registered property',
+ frames: { "--my-color": ['red', 'blue'] },
+ expected: [ { property: '--my-color',
+ values: [ valueFormat(0, 'red', 'replace', 'linear'),
+ valueFormat(1, 'blue', 'replace') ] } ]
+ },
+
+ // ---------------------------------------------------------------------
+ //
+ // Tests for keyframe sequences
+ //
+ // ---------------------------------------------------------------------
+
+ { desc: 'a keyframe sequence specification with repeated values at'
+ + ' offset 0/1 with different easings',
+ frames: [ { offset: 0.0, left: '100px', easing: 'ease' },
+ { offset: 0.0, left: '200px', easing: 'ease' },
+ { offset: 0.5, left: '300px', easing: 'linear' },
+ { offset: 1.0, left: '400px', easing: 'ease-out' },
+ { offset: 1.0, left: '500px', easing: 'step-end' } ],
+ expected: [ { property: 'left',
+ values: [ valueFormat(0, '100px', 'replace'),
+ valueFormat(0, '200px', 'replace', 'ease'),
+ valueFormat(0.5, '300px', 'replace', 'linear'),
+ valueFormat(1, '400px', 'replace'),
+ valueFormat(1, '500px', 'replace') ] } ]
+ },
+ { desc: 'a one-property two-keyframe sequence',
+ frames: [ { offset: 0, left: '10px' },
+ { offset: 1, left: '20px' } ],
+ expected: [ { property: 'left',
+ values: [ valueFormat(0, '10px', 'replace', 'linear'),
+ valueFormat(1, '20px', 'replace') ] } ]
+ },
+ { desc: 'a two-property two-keyframe sequence',
+ frames: [ { offset: 0, left: '10px', top: '30px' },
+ { offset: 1, left: '20px', top: '40px' } ],
+ expected: [ { property: 'left',
+ values: [ valueFormat(0, '10px', 'replace', 'linear'),
+ valueFormat(1, '20px', 'replace') ] },
+ { property: 'top',
+ values: [ valueFormat(0, '30px', 'replace', 'linear'),
+ valueFormat(1, '40px', 'replace') ] } ]
+ },
+ { desc: 'a one shorthand property two-keyframe sequence',
+ frames: [ { offset: 0, margin: '10px' },
+ { offset: 1, margin: '20px 30px 40px 50px' } ],
+ expected: [ { property: 'margin-top',
+ values: [ valueFormat(0, '10px', 'replace', 'linear'),
+ valueFormat(1, '20px', 'replace') ] },
+ { property: 'margin-right',
+ values: [ valueFormat(0, '10px', 'replace', 'linear'),
+ valueFormat(1, '30px', 'replace') ] },
+ { property: 'margin-bottom',
+ values: [ valueFormat(0, '10px', 'replace', 'linear'),
+ valueFormat(1, '40px', 'replace') ] },
+ { property: 'margin-left',
+ values: [ valueFormat(0, '10px', 'replace', 'linear'),
+ valueFormat(1, '50px', 'replace') ] } ]
+ },
+ { desc: 'a two-property (a shorthand and one of its component longhands)'
+ + ' two-keyframe sequence',
+ frames: [ { offset: 0, margin: '10px', marginTop: '20px' },
+ { offset: 1, marginTop: '70px',
+ margin: '30px 40px 50px 60px' } ],
+ expected: [ { property: 'margin-top',
+ values: [ valueFormat(0, '20px', 'replace', 'linear'),
+ valueFormat(1, '70px', 'replace') ] },
+ { property: 'margin-right',
+ values: [ valueFormat(0, '10px', 'replace', 'linear'),
+ valueFormat(1, '40px', 'replace') ] },
+ { property: 'margin-bottom',
+ values: [ valueFormat(0, '10px', 'replace', 'linear'),
+ valueFormat(1, '50px', 'replace') ] },
+ { property: 'margin-left',
+ values: [ valueFormat(0, '10px', 'replace', 'linear'),
+ valueFormat(1, '60px', 'replace') ] } ]
+ },
+ { desc: 'a keyframe sequence with duplicate values for a given interior'
+ + ' offset',
+ frames: [ { offset: 0.0, left: '10px' },
+ { offset: 0.5, left: '20px' },
+ { offset: 0.5, left: '30px' },
+ { offset: 0.5, left: '40px' },
+ { offset: 1.0, left: '50px' } ],
+ expected: [ { property: 'left',
+ values: [ valueFormat(0, '10px', 'replace', 'linear'),
+ valueFormat(0.5, '20px', 'replace'),
+ valueFormat(0.5, '40px', 'replace', 'linear'),
+ valueFormat(1, '50px', 'replace') ] } ]
+ },
+ { desc: 'a keyframe sequence with duplicate values for offsets 0 and 1',
+ frames: [ { offset: 0, left: '10px' },
+ { offset: 0, left: '20px' },
+ { offset: 0, left: '30px' },
+ { offset: 1, left: '40px' },
+ { offset: 1, left: '50px' },
+ { offset: 1, left: '60px' } ],
+ expected: [ { property: 'left',
+ values: [ valueFormat(0, '10px', 'replace'),
+ valueFormat(0, '30px', 'replace', 'linear'),
+ valueFormat(1, '40px', 'replace'),
+ valueFormat(1, '60px', 'replace') ] } ]
+ },
+ { desc: 'a two-property four-keyframe sequence',
+ frames: [ { offset: 0, left: '10px' },
+ { offset: 0, top: '20px' },
+ { offset: 1, top: '30px' },
+ { offset: 1, left: '40px' } ],
+ expected: [ { property: 'left',
+ values: [ valueFormat(0, '10px', 'replace', 'linear'),
+ valueFormat(1, '40px', 'replace') ] },
+ { property: 'top',
+ values: [ valueFormat(0, '20px', 'replace', 'linear'),
+ valueFormat(1, '30px', 'replace') ] } ]
+ },
+ { desc: 'a one-property keyframe sequence with some omitted offsets',
+ frames: [ { offset: 0.00, left: '10px' },
+ { offset: 0.25, left: '20px' },
+ { left: '30px' },
+ { left: '40px' },
+ { offset: 1.00, left: '50px' } ],
+ expected: [ { property: 'left',
+ values: [ valueFormat(0, '10px', 'replace', 'linear'),
+ valueFormat(0.25, '20px', 'replace', 'linear'),
+ valueFormat(0.5, '30px', 'replace', 'linear'),
+ valueFormat(0.75, '40px', 'replace', 'linear'),
+ valueFormat(1, '50px', 'replace') ] } ]
+ },
+ { desc: 'a two-property keyframe sequence with some omitted offsets',
+ frames: [ { offset: 0.00, left: '10px', top: '20px' },
+ { offset: 0.25, left: '30px' },
+ { left: '40px' },
+ { left: '50px', top: '60px' },
+ { offset: 1.00, left: '70px', top: '80px' } ],
+ expected: [ { property: 'left',
+ values: [ valueFormat(0, '10px', 'replace', 'linear'),
+ valueFormat(0.25, '30px', 'replace', 'linear'),
+ valueFormat(0.5, '40px', 'replace', 'linear'),
+ valueFormat(0.75, '50px', 'replace', 'linear'),
+ valueFormat(1, '70px', 'replace') ] },
+ { property: 'top',
+ values: [ valueFormat(0, '20px', 'replace', 'linear'),
+ valueFormat(0.75, '60px', 'replace', 'linear'),
+ valueFormat(1, '80px', 'replace') ] } ]
+ },
+ { desc: 'a one-property keyframe sequence with all omitted offsets',
+ frames: [ { left: '10px' },
+ { left: '20px' },
+ { left: '30px' },
+ { left: '40px' },
+ { left: '50px' } ],
+ expected: [ { property: 'left',
+ values: [ valueFormat(0, '10px', 'replace', 'linear'),
+ valueFormat(0.25, '20px', 'replace', 'linear'),
+ valueFormat(0.5, '30px', 'replace', 'linear'),
+ valueFormat(0.75, '40px', 'replace', 'linear'),
+ valueFormat(1, '50px', 'replace') ] } ]
+ },
+ { desc: 'a keyframe sequence with different easing values, but the'
+ + ' same easing value for a given offset',
+ frames: [ { offset: 0.0, easing: 'ease', left: '10px'},
+ { offset: 0.0, easing: 'ease', top: '20px'},
+ { offset: 0.5, easing: 'linear', left: '30px' },
+ { offset: 0.5, easing: 'linear', top: '40px' },
+ { offset: 1.0, easing: 'step-end', left: '50px' },
+ { offset: 1.0, easing: 'step-end', top: '60px' } ],
+ expected: [ { property: 'left',
+ values: [ valueFormat(0, '10px', 'replace', 'ease'),
+ valueFormat(0.5, '30px', 'replace', 'linear'),
+ valueFormat(1, '50px', 'replace') ] },
+ { property: 'top',
+ values: [ valueFormat(0, '20px', 'replace', 'ease'),
+ valueFormat(0.5, '40px', 'replace', 'linear'),
+ valueFormat(1, '60px', 'replace') ] } ]
+ },
+ { desc: 'a one-property two-keyframe sequence that needs to'
+ + ' stringify its values',
+ frames: [ { offset: 0, opacity: 0 },
+ { offset: 1, opacity: 1 } ],
+ expected: [ { property: 'opacity',
+ values: [ valueFormat(0, '0', 'replace', 'linear'),
+ valueFormat(1, '1', 'replace') ] } ]
+ },
+ { desc: 'a keyframe sequence where shorthand precedes longhand',
+ frames: [ { offset: 0, margin: '10px', marginRight: '20px' },
+ { offset: 1, margin: '30px' } ],
+ expected: [ { property: 'margin-top',
+ values: [ valueFormat(0, '10px', 'replace', 'linear'),
+ valueFormat(1, '30px', 'replace') ] },
+ { property: 'margin-right',
+ values: [ valueFormat(0, '20px', 'replace', 'linear'),
+ valueFormat(1, '30px', 'replace') ] },
+ { property: 'margin-bottom',
+ values: [ valueFormat(0, '10px', 'replace', 'linear'),
+ valueFormat(1, '30px', 'replace') ] },
+ { property: 'margin-left',
+ values: [ valueFormat(0, '10px', 'replace', 'linear'),
+ valueFormat(1, '30px', 'replace') ] } ]
+ },
+ { desc: 'a keyframe sequence where longhand precedes shorthand',
+ frames: [ { offset: 0, marginRight: '20px', margin: '10px' },
+ { offset: 1, margin: '30px' } ],
+ expected: [ { property: 'margin-top',
+ values: [ valueFormat(0, '10px', 'replace', 'linear'),
+ valueFormat(1, '30px', 'replace') ] },
+ { property: 'margin-right',
+ values: [ valueFormat(0, '20px', 'replace', 'linear'),
+ valueFormat(1, '30px', 'replace') ] },
+ { property: 'margin-bottom',
+ values: [ valueFormat(0, '10px', 'replace', 'linear'),
+ valueFormat(1, '30px', 'replace') ] },
+ { property: 'margin-left',
+ values: [ valueFormat(0, '10px', 'replace', 'linear'),
+ valueFormat(1, '30px', 'replace') ] } ]
+ },
+ { desc: 'a keyframe sequence where lesser shorthand precedes greater'
+ + ' shorthand',
+ frames: [ { offset: 0, borderLeft: '1px solid rgb(1, 2, 3)',
+ border: '2px dotted rgb(4, 5, 6)' },
+ { offset: 1, border: '3px dashed rgb(7, 8, 9)' } ],
+ expected: [ { property: 'border-bottom-color',
+ values: [ valueFormat(0, 'rgb(4, 5, 6)', 'replace', 'linear'),
+ valueFormat(1, 'rgb(7, 8, 9)', 'replace') ] },
+ { property: 'border-left-color',
+ values: [ valueFormat(0, 'rgb(1, 2, 3)', 'replace', 'linear'),
+ valueFormat(1, 'rgb(7, 8, 9)', 'replace') ] },
+ { property: 'border-right-color',
+ values: [ valueFormat(0, 'rgb(4, 5, 6)', 'replace', 'linear'),
+ valueFormat(1, 'rgb(7, 8, 9)', 'replace') ] },
+ { property: 'border-top-color',
+ values: [ valueFormat(0, 'rgb(4, 5, 6)', 'replace', 'linear'),
+ valueFormat(1, 'rgb(7, 8, 9)', 'replace') ] },
+ { property: 'border-bottom-width',
+ values: [ valueFormat(0, '2px', 'replace', 'linear'),
+ valueFormat(1, '3px', 'replace') ] },
+ { property: 'border-left-width',
+ values: [ valueFormat(0, '1px', 'replace', 'linear'),
+ valueFormat(1, '3px', 'replace') ] },
+ { property: 'border-right-width',
+ values: [ valueFormat(0, '2px', 'replace', 'linear'),
+ valueFormat(1, '3px', 'replace') ] },
+ { property: 'border-top-width',
+ values: [ valueFormat(0, '2px', 'replace', 'linear'),
+ valueFormat(1, '3px', 'replace') ] },
+ { property: 'border-bottom-style',
+ values: [ valueFormat(0, 'dotted', 'replace', 'linear'),
+ valueFormat(1, 'dashed', 'replace') ] },
+ { property: 'border-left-style',
+ values: [ valueFormat(0, 'solid', 'replace', 'linear'),
+ valueFormat(1, 'dashed', 'replace') ] },
+ { property: 'border-right-style',
+ values: [ valueFormat(0, 'dotted', 'replace', 'linear'),
+ valueFormat(1, 'dashed', 'replace') ] },
+ { property: 'border-top-style',
+ values: [ valueFormat(0, 'dotted', 'replace', 'linear'),
+ valueFormat(1, 'dashed', 'replace') ] },
+ { property: 'border-image-outset',
+ values: [ valueFormat(0, '0', 'replace', 'linear'),
+ valueFormat(1, '0', 'replace') ] },
+ { property: 'border-image-repeat',
+ values: [ valueFormat(0, 'stretch', 'replace', 'linear'),
+ valueFormat(1, 'stretch', 'replace') ] },
+ { property: 'border-image-slice',
+ values: [ valueFormat(0, '100%', 'replace', 'linear'),
+ valueFormat(1, '100%', 'replace') ] },
+ { property: 'border-image-source',
+ values: [ valueFormat(0, 'none', 'replace', 'linear'),
+ valueFormat(1, 'none', 'replace') ] },
+ { property: 'border-image-width',
+ values: [ valueFormat(0, '1', 'replace', 'linear'),
+ valueFormat(1, '1', 'replace') ] },
+ ]
+ },
+ { desc: 'a keyframe sequence where greater shorthand precedes'
+ + ' lesser shorthand',
+ frames: [ { offset: 0, border: '2px dotted rgb(4, 5, 6)',
+ borderLeft: '1px solid rgb(1, 2, 3)' },
+ { offset: 1, border: '3px dashed rgb(7, 8, 9)' } ],
+ expected: [ { property: 'border-bottom-color',
+ values: [ valueFormat(0, 'rgb(4, 5, 6)', 'replace', 'linear'),
+ valueFormat(1, 'rgb(7, 8, 9)', 'replace') ] },
+ { property: 'border-left-color',
+ values: [ valueFormat(0, 'rgb(1, 2, 3)', 'replace', 'linear'),
+ valueFormat(1, 'rgb(7, 8, 9)', 'replace') ] },
+ { property: 'border-right-color',
+ values: [ valueFormat(0, 'rgb(4, 5, 6)', 'replace', 'linear'),
+ valueFormat(1, 'rgb(7, 8, 9)', 'replace') ] },
+ { property: 'border-top-color',
+ values: [ valueFormat(0, 'rgb(4, 5, 6)', 'replace', 'linear'),
+ valueFormat(1, 'rgb(7, 8, 9)', 'replace') ] },
+ { property: 'border-bottom-width',
+ values: [ valueFormat(0, '2px', 'replace', 'linear'),
+ valueFormat(1, '3px', 'replace') ] },
+ { property: 'border-left-width',
+ values: [ valueFormat(0, '1px', 'replace', 'linear'),
+ valueFormat(1, '3px', 'replace') ] },
+ { property: 'border-right-width',
+ values: [ valueFormat(0, '2px', 'replace', 'linear'),
+ valueFormat(1, '3px', 'replace') ] },
+ { property: 'border-top-width',
+ values: [ valueFormat(0, '2px', 'replace', 'linear'),
+ valueFormat(1, '3px', 'replace') ] },
+ { property: 'border-bottom-style',
+ values: [ valueFormat(0, 'dotted', 'replace', 'linear'),
+ valueFormat(1, 'dashed', 'replace') ] },
+ { property: 'border-left-style',
+ values: [ valueFormat(0, 'solid', 'replace', 'linear'),
+ valueFormat(1, 'dashed', 'replace') ] },
+ { property: 'border-right-style',
+ values: [ valueFormat(0, 'dotted', 'replace', 'linear'),
+ valueFormat(1, 'dashed', 'replace') ] },
+ { property: 'border-top-style',
+ values: [ valueFormat(0, 'dotted', 'replace', 'linear'),
+ valueFormat(1, 'dashed', 'replace') ] },
+ { property: 'border-image-outset',
+ values: [ valueFormat(0, '0', 'replace', 'linear'),
+ valueFormat(1, '0', 'replace') ] },
+ { property: 'border-image-repeat',
+ values: [ valueFormat(0, 'stretch', 'replace', 'linear'),
+ valueFormat(1, 'stretch', 'replace') ] },
+ { property: 'border-image-slice',
+ values: [ valueFormat(0, '100%', 'replace', 'linear'),
+ valueFormat(1, '100%', 'replace') ] },
+ { property: 'border-image-source',
+ values: [ valueFormat(0, 'none', 'replace', 'linear'),
+ valueFormat(1, 'none', 'replace') ] },
+ { property: 'border-image-width',
+ values: [ valueFormat(0, '1', 'replace', 'linear'),
+ valueFormat(1, '1', 'replace') ] },
+ ]
+ },
+
+ // ---------------------------------------------------------------------
+ //
+ // Tests for unit conversion
+ //
+ // ---------------------------------------------------------------------
+
+ { desc: 'em units are resolved to px values',
+ frames: { left: ['10em', '20em'] },
+ expected: [ { property: 'left',
+ values: [ valueFormat(0, '100px', 'replace', 'linear'),
+ valueFormat(1, '200px', 'replace') ] } ]
+ },
+ { desc: 'calc() expressions are resolved to the equivalent units',
+ frames: { left: ['calc(10em + 10px)', 'calc(10em + 10%)'] },
+ expected: [ { property: 'left',
+ values: [ valueFormat(0, '110px', 'replace', 'linear'),
+ valueFormat(1, 'calc(10% + 100px)', 'replace') ] } ]
+ },
+
+ // ---------------------------------------------------------------------
+ //
+ // Tests for CSS variable handling conversion
+ //
+ // ---------------------------------------------------------------------
+
+ { desc: 'CSS variables are resolved to their corresponding values',
+ frames: { left: ['10px', 'var(--var-100px)'] },
+ expected: [ { property: 'left',
+ values: [ valueFormat(0, '10px', 'replace', 'linear'),
+ valueFormat(1, '100px', 'replace') ] } ]
+ },
+ { desc: 'CSS variables in calc() expressions are resolved',
+ frames: { left: ['10px', 'calc(var(--var-100px) / 2 - 10%)'] },
+ expected: [ { property: 'left',
+ values: [ valueFormat(0, '10px', 'replace', 'linear'),
+ valueFormat(1, 'calc(-10% + 50px)', 'replace') ] } ]
+ },
+ { desc: 'CSS variables in shorthands are resolved to their corresponding'
+ + ' values',
+ frames: { margin: ['10px', 'var(--var-100px-200px)'] },
+ expected: [ { property: 'margin-top',
+ values: [ valueFormat(0, '10px', 'replace', 'linear'),
+ valueFormat(1, '100px', 'replace') ] },
+ { property: 'margin-right',
+ values: [ valueFormat(0, '10px', 'replace', 'linear'),
+ valueFormat(1, '200px', 'replace') ] },
+ { property: 'margin-bottom',
+ values: [ valueFormat(0, '10px', 'replace', 'linear'),
+ valueFormat(1, '100px', 'replace') ] },
+ { property: 'margin-left',
+ values: [ valueFormat(0, '10px', 'replace', 'linear'),
+ valueFormat(1, '200px', 'replace') ] } ]
+ },
+
+ // ---------------------------------------------------------------------
+ //
+ // Tests for properties that parse correctly but which we fail to
+ // convert to computed values.
+ //
+ // ---------------------------------------------------------------------
+
+ { desc: 'a missing property in initial keyframe',
+ frames: [ { },
+ { margin: '5px' } ],
+ expected: [ { property: 'margin-top',
+ values: [ valueFormat(0, undefined, 'replace', 'linear'),
+ valueFormat(1, '5px', 'replace') ] },
+ { property: 'margin-right',
+ values: [ valueFormat(0, undefined, 'replace', 'linear'),
+ valueFormat(1, '5px', 'replace') ] },
+ { property: 'margin-bottom',
+ values: [ valueFormat(0, undefined, 'replace', 'linear'),
+ valueFormat(1, '5px', 'replace') ] },
+ { property: 'margin-left',
+ values: [ valueFormat(0, undefined, 'replace', 'linear'),
+ valueFormat(1, '5px', 'replace') ] } ]
+ },
+ { desc: 'a missing property in initial keyframe and there are some ' +
+ 'keyframes with the same offset',
+ frames: [ { },
+ { margin: '10px', offset: 0.5 },
+ { margin: '20px', offset: 0.5 },
+ { margin: '30px'} ],
+ expected: [ { property: 'margin-top',
+ values: [ valueFormat(0, undefined, 'replace', 'linear'),
+ valueFormat(0.5, '10px', 'replace'),
+ valueFormat(0.5, '20px', 'replace', 'linear'),
+ valueFormat(1, '30px', 'replace') ] },
+ { property: 'margin-right',
+ values: [ valueFormat(0, undefined, 'replace', 'linear'),
+ valueFormat(0.5, '10px', 'replace'),
+ valueFormat(0.5, '20px', 'replace', 'linear'),
+ valueFormat(1, '30px', 'replace') ] },
+ { property: 'margin-bottom',
+ values: [ valueFormat(0, undefined, 'replace', 'linear'),
+ valueFormat(0.5, '10px', 'replace'),
+ valueFormat(0.5, '20px', 'replace', 'linear'),
+ valueFormat(1, '30px', 'replace') ] },
+ { property: 'margin-left',
+ values: [ valueFormat(0, undefined, 'replace', 'linear'),
+ valueFormat(0.5, '10px', 'replace'),
+ valueFormat(0.5, '20px', 'replace', 'linear'),
+ valueFormat(1, '30px', 'replace') ] } ]
+ },
+ { desc: 'a missing property in final keyframe',
+ frames: [ { margin: '5px' },
+ { } ],
+ expected: [ { property: 'margin-top',
+ values: [ valueFormat(0, '5px', 'replace', 'linear'),
+ valueFormat(1, undefined, 'replace') ] },
+ { property: 'margin-right',
+ values: [ valueFormat(0, '5px', 'replace', 'linear'),
+ valueFormat(1, undefined, 'replace') ] },
+ { property: 'margin-bottom',
+ values: [ valueFormat(0, '5px', 'replace', 'linear'),
+ valueFormat(1, undefined, 'replace') ] },
+ { property: 'margin-left',
+ values: [ valueFormat(0, '5px', 'replace', 'linear'),
+ valueFormat(1, undefined, 'replace') ] } ]
+ },
+ { desc: 'a missing property in final keyframe and there are some ' +
+ 'keyframes with the same offsets',
+ frames: [ { margin: '5px' },
+ { margin: '10px', offset: 0.5 },
+ { margin: '20px', offset: 0.5 },
+ { } ],
+ expected: [ { property: 'margin-top',
+ values: [ valueFormat(0, '5px', 'replace', 'linear'),
+ valueFormat(0.5, '10px', 'replace'),
+ valueFormat(0.5, '20px', 'replace', 'linear'),
+ valueFormat(1, undefined, 'replace') ] },
+ { property: 'margin-right',
+ values: [ valueFormat(0, '5px', 'replace', 'linear'),
+ valueFormat(0.5, '10px', 'replace'),
+ valueFormat(0.5, '20px', 'replace', 'linear'),
+ valueFormat(1, undefined, 'replace') ] },
+ { property: 'margin-bottom',
+ values: [ valueFormat(0, '5px', 'replace', 'linear'),
+ valueFormat(0.5, '10px', 'replace'),
+ valueFormat(0.5, '20px', 'replace', 'linear'),
+ valueFormat(1, undefined, 'replace') ] },
+ { property: 'margin-left',
+ values: [ valueFormat(0, '5px', 'replace', 'linear'),
+ valueFormat(0.5, '10px', 'replace'),
+ valueFormat(0.5, '20px', 'replace', 'linear'),
+ valueFormat(1, undefined, 'replace') ] } ]
+ },
+ { desc: 'a missing property in final keyframe where it forms the last'
+ + ' segment in the series',
+ frames: [ { margin: '5px' },
+ { marginLeft: '5px',
+ marginRight: '5px',
+ marginBottom: '5px' } ],
+ expected: [ { property: 'margin-bottom',
+ values: [ valueFormat(0, '5px', 'replace', 'linear'),
+ valueFormat(1, '5px', 'replace') ] },
+ { property: 'margin-left',
+ values: [ valueFormat(0, '5px', 'replace', 'linear'),
+ valueFormat(1, '5px', 'replace') ] },
+ { property: 'margin-right',
+ values: [ valueFormat(0, '5px', 'replace', 'linear'),
+ valueFormat(1, '5px', 'replace') ] },
+ { property: 'margin-top',
+ values: [ valueFormat(0, '5px', 'replace', 'linear'),
+ valueFormat(1, undefined, 'replace') ] } ]
+ },
+ { desc: 'a missing property in initial keyframe along with other values',
+ frames: [ { left: '10px' },
+ { margin: '5px', left: '20px' } ],
+ expected: [ { property: 'left',
+ values: [ valueFormat(0, '10px', 'replace', 'linear'),
+ valueFormat(1, '20px', 'replace') ] },
+ { property: 'margin-top',
+ values: [ valueFormat(0, undefined, 'replace', 'linear'),
+ valueFormat(1, '5px', 'replace') ] },
+ { property: 'margin-right',
+ values: [ valueFormat(0, undefined, 'replace', 'linear'),
+ valueFormat(1, '5px', 'replace') ] },
+ { property: 'margin-bottom',
+ values: [ valueFormat(0, undefined, 'replace', 'linear'),
+ valueFormat(1, '5px', 'replace') ] },
+ { property: 'margin-left',
+ values: [ valueFormat(0, undefined, 'replace', 'linear'),
+ valueFormat(1, '5px', 'replace') ] } ]
+ },
+ { desc: 'a missing property in final keyframe along with other values',
+ frames: [ { margin: '5px', left: '10px' },
+ { left: '20px' } ],
+ expected: [ { property: 'left',
+ values: [ valueFormat(0, '10px', 'replace', 'linear'),
+ valueFormat(1, '20px', 'replace') ] },
+ { property: 'margin-top',
+ values: [ valueFormat(0, '5px', 'replace', 'linear'),
+ valueFormat(1, undefined, 'replace') ] },
+ { property: 'margin-right',
+ values: [ valueFormat(0, '5px', 'replace', 'linear'),
+ valueFormat(1, undefined, 'replace') ] },
+ { property: 'margin-bottom',
+ values: [ valueFormat(0, '5px', 'replace', 'linear'),
+ valueFormat(1, undefined, 'replace') ] },
+ { property: 'margin-left',
+ values: [ valueFormat(0, '5px', 'replace', 'linear'),
+ valueFormat(1, undefined, 'replace') ] } ]
+ },
+ { desc: 'missing properties in both of initial and final keyframe',
+ frames: [ { left: '5px', offset: 0.5 } ],
+ expected: [ { property: 'left',
+ values: [ valueFormat(0, undefined, 'replace', 'linear'),
+ valueFormat(0.5, '5px', 'replace', 'linear'),
+ valueFormat(1, undefined, 'replace') ] } ]
+ },
+ { desc: 'missing propertes in both of initial and final keyframe along '
+ + 'with other values',
+ frames: [ { left: '5px', offset: 0 },
+ { right: '5px', offset: 0.5 },
+ { left: '10px', offset: 1 } ],
+ expected: [ { property: 'left',
+ values: [ valueFormat(0, '5px', 'replace', 'linear'),
+ valueFormat(1, '10px', 'replace') ] },
+ { property: 'right',
+ values: [ valueFormat(0, undefined, 'replace', 'linear'),
+ valueFormat(0.5, '5px', 'replace', 'linear'),
+ valueFormat(1, undefined, 'replace') ] } ]
+ },
+
+ { desc: 'a missing property in final keyframe with duplicate offset ' +
+ + 'along with other values',
+ frames: [ { left: '5px', right: '5px', offset: 0 },
+ { left: '8px', right: '8px', offset: 0 },
+ { left: '10px', offset: 1 } ],
+ expected: [ { property: 'left',
+ values: [ valueFormat(0, '5px', 'replace'),
+ valueFormat(0, '8px', 'replace', 'linear'),
+ valueFormat(1, '10px', 'replace') ] },
+ { property: 'right',
+ values: [ valueFormat(0, '5px', 'replace'),
+ valueFormat(0, '8px', 'replace', 'linear'),
+ valueFormat(1, undefined, 'replace') ] } ]
+ },
+
+ { desc: 'a missing property in initial keyframe with duplicate offset '
+ + 'along with other values',
+ frames: [ { left: '10px', offset: 0 },
+ { left: '8px', right: '8px', offset: 1 },
+ { left: '5px', right: '5px', offset: 1 } ],
+ expected: [ { property: 'left',
+ values: [ valueFormat(0, '10px', 'replace', 'linear'),
+ valueFormat(1, '8px', 'replace'),
+ valueFormat(1, '5px', 'replace') ] },
+ { property: 'right',
+ values: [ valueFormat(0, undefined, 'replace', 'linear'),
+ valueFormat(1, '8px', 'replace'),
+ valueFormat(1, '5px', 'replace') ] } ]
+ },
+];
+
+gTests.forEach(function(subtest) {
+ test(function(t) {
+ var div = addDiv(t);
+ var animation = div.animate(subtest.frames, 100 * MS_PER_SEC);
+ // Flush styles since getProperties currently does not. Rather, it
+ // returns the actual properties in use at the current time.
+ // However, we want to test what these properties will look like
+ // after the next restyle.
+ getComputedStyle(div).opacity;
+ assert_properties_equal(
+ animation.effect.getProperties(),
+ subtest.expected
+ );
+ }, subtest.desc);
+});
+
+</script>
+</body>
diff --git a/dom/animation/test/chrome/test_animation_properties_display.html b/dom/animation/test/chrome/test_animation_properties_display.html
new file mode 100644
index 0000000000..1d558d2114
--- /dev/null
+++ b/dom/animation/test/chrome/test_animation_properties_display.html
@@ -0,0 +1,34 @@
+<!doctype html>
+<head>
+<meta charset=utf-8>
+<title>Bug 1536688 - Test that 'display' is not included in
+ KeyframeEffect.getProperties() when using shorthand 'all'</title>
+<script type="application/javascript" src="../testharness.js"></script>
+<script type="application/javascript" src="../testharnessreport.js"></script>
+<script type="application/javascript" src="../testcommon.js"></script>
+</head>
+<body>
+<a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1536688"
+ target="_blank">Mozilla Bug 1536688</a>
+<div id="log"></div>
+<script>
+'use strict';
+
+test(t => {
+ const div = addDiv(t);
+ const animation = div.animate(
+ { all: ['unset', 'unset'] },
+ 100 * MS_PER_SEC
+ );
+ // Flush styles since getProperties does not.
+ getComputedStyle(div).opacity;
+
+ const properties = animation.effect.getProperties();
+ assert_false(
+ properties.some(property => property.property === 'display'),
+ 'Should not have a property for display'
+ );
+});
+
+</script>
+</body>
diff --git a/dom/animation/test/chrome/test_cssanimation_missing_keyframes.html b/dom/animation/test/chrome/test_cssanimation_missing_keyframes.html
new file mode 100644
index 0000000000..8c599655b7
--- /dev/null
+++ b/dom/animation/test/chrome/test_cssanimation_missing_keyframes.html
@@ -0,0 +1,66 @@
+<!doctype html>
+<head>
+<meta charset=utf-8>
+<title>Bug 1339332 - Test for missing keyframes in CSS Animation</title>
+<script type="application/javascript" src="../testharness.js"></script>
+<script type="application/javascript" src="../testharnessreport.js"></script>
+<script type="application/javascript" src="../testcommon.js"></script>
+</head>
+<body>
+<a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1339332"
+ target="_blank">Mozilla Bug 1339332</a>
+<div id="log"></div>
+<style>
+@keyframes missingFrom {
+ to {
+ text-align: right;
+ }
+}
+@keyframes missingBoth {
+ 50% {
+ text-align: right;
+ }
+}
+@keyframes missingTo {
+ from {
+ text-align: right;
+ }
+}
+</style>
+<script>
+'use strict';
+
+const gTests = [
+ { desc: 'missing "from" keyframe',
+ animationName: 'missingFrom',
+ expected: [{ property: 'text-align',
+ values: [valueFormat(0, undefined, 'replace', 'ease'),
+ valueFormat(1, 'right', 'replace')] } ]
+ },
+ { desc: 'missing "to" keyframe',
+ animationName: 'missingTo',
+ expected: [{ property: 'text-align',
+ values: [valueFormat(0, 'right', 'replace', 'ease'),
+ valueFormat(1, undefined, 'replace')] } ]
+ },
+ { desc: 'missing "from" and "to" keyframes',
+ animationName: 'missingBoth',
+ expected: [{ property: 'text-align',
+ values: [valueFormat(0, undefined, 'replace', 'ease'),
+ valueFormat(.5, 'right', 'replace', 'ease'),
+ valueFormat(1, undefined, 'replace')] } ]
+ },
+];
+
+gTests.forEach(function(subtest) {
+ test(function(t) {
+ const div = addDiv(t);
+ div.style.animation = `${ subtest.animationName } 1000s`;
+ const animation = div.getAnimations()[0];
+ assert_properties_equal(animation.effect.getProperties(),
+ subtest.expected);
+ }, subtest.desc);
+});
+
+</script>
+</body>
diff --git a/dom/animation/test/chrome/test_generated_content_getAnimations.html b/dom/animation/test/chrome/test_generated_content_getAnimations.html
new file mode 100644
index 0000000000..41010ca917
--- /dev/null
+++ b/dom/animation/test/chrome/test_generated_content_getAnimations.html
@@ -0,0 +1,81 @@
+<!DOCTYPE html>
+<head>
+<meta charset=utf-8>
+<title>Test getAnimations() for generated-content elements</title>
+<script type="application/javascript" src="../testharness.js"></script>
+<script type="application/javascript" src="../testharnessreport.js"></script>
+<script type="application/javascript" src="../testcommon.js"></script>
+<style>
+@keyframes anim { }
+@keyframes anim2 { }
+.before::before {
+ content: '';
+ animation: anim 100s;
+}
+.after::after {
+ content: '';
+ animation: anim 100s, anim2 100s;
+}
+</style>
+</head>
+<body>
+<div id='root' class='before after'>
+ <div class='before'></div>
+ <div></div>
+</div>
+<script>
+'use strict';
+
+function getWalker(node) {
+ var walker = Cc["@mozilla.org/inspector/deep-tree-walker;1"].
+ createInstance(Ci.inIDeepTreeWalker);
+ walker.showAnonymousContent = true;
+ walker.init(node.ownerDocument, NodeFilter.SHOW_ALL);
+ walker.currentNode = node;
+ return walker;
+}
+
+test(function(t) {
+ var root = document.getElementById('root');
+ // Flush first to make sure the generated-content elements are ready
+ // in the tree.
+ flushComputedStyle(root);
+ var before = getWalker(root).firstChild();
+ var after = getWalker(root).lastChild();
+
+ // Sanity Checks
+ assert_equals(document.getAnimations().length, 4,
+ 'All animations in this document');
+ assert_equals(before.tagName, '_moz_generated_content_before',
+ 'First child is ::before element');
+ assert_equals(after.tagName, '_moz_generated_content_after',
+ 'Last child is ::after element');
+
+ // Test Element.getAnimations() for generated-content elements
+ assert_equals(before.getAnimations().length, 1,
+ 'Animations of ::before generated-content element');
+ assert_equals(after.getAnimations().length, 2,
+ 'Animations of ::after generated-content element');
+}, 'Element.getAnimations() used on generated-content elements');
+
+test(function(t) {
+ var root = document.getElementById('root');
+ flushComputedStyle(root);
+ var walker = getWalker(root);
+
+ var animations = [];
+ var element = walker.currentNode;
+ while (element) {
+ if (element.getAnimations) {
+ animations = [...animations, ...element.getAnimations()];
+ }
+ element = walker.nextNode();
+ }
+
+ assert_equals(animations.length, document.getAnimations().length,
+ 'The number of animations got by DeepTreeWalker and ' +
+ 'document.getAnimations() should be the same');
+}, 'Element.getAnimations() used by traversing DeepTreeWalker');
+
+</script>
+</body>
diff --git a/dom/animation/test/chrome/test_keyframe_effect_xrays.html b/dom/animation/test/chrome/test_keyframe_effect_xrays.html
new file mode 100644
index 0000000000..ca3e712ac5
--- /dev/null
+++ b/dom/animation/test/chrome/test_keyframe_effect_xrays.html
@@ -0,0 +1,45 @@
+<!doctype html>
+<head>
+<meta charset=utf-8>
+<script type="application/javascript" src="../testharness.js"></script>
+<script type="application/javascript" src="../testharnessreport.js"></script>
+<script type="application/javascript" src="../testcommon.js"></script>
+</head>
+<body>
+<a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1414674"
+ target="_blank">Mozilla Bug 1414674</a>
+<div id="log"></div>
+<iframe id="iframe"
+ src="http://example.org/tests/dom/animation/test/chrome/file_animate_xrays.html"></iframe>
+<script>
+'use strict';
+
+var win = document.getElementById('iframe').contentWindow;
+
+async_test(function(t) {
+ window.addEventListener('load', t.step_func(function() {
+ var target = win.document.getElementById('target');
+ var effect = new win.KeyframeEffect(target, [
+ {opacity: 1, offset: 0},
+ {opacity: 0, offset: 1},
+ ], {duration: 100 * MS_PER_SEC, fill: "forwards"});
+ // The frames object should be accessible via x-ray.
+ var frames = effect.getKeyframes();
+ assert_equals(frames.length, 2,
+ "frames for KeyframeEffect ctor should be non-zero");
+ assert_equals(frames[0].opacity, "1",
+ "first frame opacity for KeyframeEffect ctor should be specified value");
+ assert_equals(frames[0].computedOffset, 0,
+ "first frame offset for KeyframeEffect ctor should be 0");
+ assert_equals(frames[1].opacity, "0",
+ "last frame opacity for KeyframeEffect ctor should be specified value");
+ assert_equals(frames[1].computedOffset, 1,
+ "last frame offset for KeyframeEffect ctor should be 1");
+ var animation = new win.Animation(effect, document.timeline);
+ animation.play();
+ t.done();
+ }));
+}, 'Calling KeyframeEffect() ctor across x-rays');
+
+</script>
+</body>
diff --git a/dom/animation/test/chrome/test_mutation_observer_for_element_removal_in_shadow_tree.html b/dom/animation/test/chrome/test_mutation_observer_for_element_removal_in_shadow_tree.html
new file mode 100644
index 0000000000..f8efaa6baf
--- /dev/null
+++ b/dom/animation/test/chrome/test_mutation_observer_for_element_removal_in_shadow_tree.html
@@ -0,0 +1,45 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<script type="application/javascript" src="../testharness.js"></script>
+<script type="application/javascript" src="../testharnessreport.js"></script>
+<script type="application/javascript" src="../testcommon.js"></script>
+<div id="log"></div>
+<script>
+
+promise_test(async t => {
+ // Set up a MutationObserver for animations.
+ const observer = new MutationObserver(() => {});
+ observer.observe(document.documentElement, {
+ animations: true,
+ subtree: true,
+ });
+
+ // Create a CSS transition in a shadow tree.
+ let s = document.createElement('shadow-test');
+ document.documentElement.appendChild(s);
+ s.attachShadow({mode:"open"});
+
+ let property = 'opacity';
+ let initial = '1';
+ let finalValue = '0';
+
+ let div = document.createElement('div');
+ div.style = `${property}:${initial};transition:${property} 2s;`
+
+ s.shadowRoot.appendChild(div);
+ div.offsetWidth;
+
+ div.style[property] = finalValue;
+
+ const eventWatcher = new EventWatcher(t, div, ['transitionstart']);
+
+ // Trigger a CSS transition.
+ getComputedStyle(div)[property];
+
+ // Wait for a transitionend event to make sure the transition has been started.
+ await eventWatcher.wait_for('transitionstart');
+
+ // Now remove the element to notify it to the observer
+ div.parentNode.removeChild(div);
+});
+</script>
diff --git a/dom/animation/test/chrome/test_running_on_compositor.html b/dom/animation/test/chrome/test_running_on_compositor.html
new file mode 100644
index 0000000000..d8c1d0573e
--- /dev/null
+++ b/dom/animation/test/chrome/test_running_on_compositor.html
@@ -0,0 +1,1656 @@
+<!doctype html>
+<head>
+<meta charset=utf-8>
+<title>Bug 1045994 - Add a chrome-only property to inspect if an animation is
+ running on the compositor or not</title>
+<script type="application/javascript" src="../testharness.js"></script>
+<script type="application/javascript" src="../testharnessreport.js"></script>
+<script type="application/javascript" src="../testcommon.js"></script>
+<style>
+@keyframes anim {
+ to { transform: translate(100px) }
+}
+@keyframes transform-starts-with-none {
+ 0% { transform: none }
+ 99% { transform: none }
+ 100% { transform: translate(100px) }
+}
+@keyframes opacity {
+ to { opacity: 0 }
+}
+@keyframes zIndex_and_translate {
+ to { z-index: 999; transform: translate(100px); }
+}
+@keyframes z-index {
+ to { z-index: 999; }
+}
+@keyframes rotate {
+ from { transform: rotate(0deg); }
+ to { transform: rotate(360deg); }
+}
+@keyframes rotate-and-opacity {
+ from { transform: rotate(0deg); opacity: 1;}
+ to { transform: rotate(360deg); opacity: 0;}
+}
+div {
+ /* Element needs geometry to be eligible for layerization */
+ width: 100px;
+ height: 100px;
+ background-color: white;
+}
+</style>
+</head>
+<body>
+<a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1045994"
+ target="_blank">Mozilla Bug 1045994</a>
+<div id="log"></div>
+<script>
+'use strict';
+
+/** Test for bug 1045994 - Add a chrome-only property to inspect if an
+ animation is running on the compositor or not **/
+
+const omtaEnabled = isOMTAEnabled();
+
+function assert_animation_is_running_on_compositor(animation, desc) {
+ assert_equals(animation.isRunningOnCompositor, omtaEnabled,
+ desc + ' at ' + animation.currentTime + 'ms');
+}
+
+function assert_animation_is_not_running_on_compositor(animation, desc) {
+ assert_equals(animation.isRunningOnCompositor, false,
+ desc + ' at ' + animation.currentTime + 'ms');
+}
+
+promise_test(async t => {
+ // FIXME: When we implement Element.animate, use that here instead of CSS
+ // so that we remove any dependency on the CSS mapping.
+ var div = addDiv(t, { style: 'animation: anim 100s' });
+ var animation = div.getAnimations()[0];
+
+ // If the animation starts at the current timeline time, we need to wait for
+ // one more frame to avoid receiving the fake timer-based MozAfterPaint event.
+ // FIXME: Bug 1419226: Drop this 'animation.ready' and 'waitForFrame'. Once
+ // MozAfterPaint is fired reliably, we just need to wait for a MozAfterPaint
+ // here.
+ await animation.ready;
+
+ if (animationStartsRightNow(animation)) {
+ await waitForNextFrame();
+ }
+
+ await waitForPaints();
+
+ assert_animation_is_running_on_compositor(animation,
+ 'Animation reports that it is running on the compositor'
+ + ' during playback');
+
+ div.style.animationPlayState = 'paused';
+
+ await animation.ready;
+
+ assert_animation_is_not_running_on_compositor(animation,
+ 'Animation reports that it is NOT running on the compositor'
+ + ' when paused');
+}, '');
+
+promise_test(async t => {
+ var div = addDiv(t, { style: 'animation: z-index 100s' });
+ var animation = div.getAnimations()[0];
+
+ await waitForPaints();
+
+ assert_animation_is_not_running_on_compositor(animation,
+ 'Animation reports that it is NOT running on the compositor'
+ + ' for animation of "z-index"');
+}, 'isRunningOnCompositor is false for animation of "z-index"');
+
+promise_test(async t => {
+ var div = addDiv(t, { style: 'animation: zIndex_and_translate 100s' });
+ var animation = div.getAnimations()[0];
+
+ await waitForPaints();
+
+ assert_animation_is_running_on_compositor(animation,
+ 'Animation reports that it is running on the compositor'
+ + ' when the animation has two properties, where one can run'
+ + ' on the compositor, the other cannot');
+}, 'isRunningOnCompositor is true if the animation has at least one ' +
+ 'property can run on compositor');
+
+promise_test(async t => {
+ var div = addDiv(t, { style: 'animation: anim 100s' });
+ var animation = div.getAnimations()[0];
+
+ await waitForPaints();
+
+ animation.pause();
+ await animation.ready;
+
+ assert_animation_is_not_running_on_compositor(animation,
+ 'Animation reports that it is NOT running on the compositor'
+ + ' when animation.pause() is called');
+}, 'isRunningOnCompositor is false when the animation.pause() is called');
+
+promise_test(async t => {
+ var div = addDiv(t, { style: 'animation: anim 100s' });
+ var animation = div.getAnimations()[0];
+
+ await waitForPaints();
+
+ animation.finish();
+ assert_animation_is_not_running_on_compositor(animation,
+ 'Animation reports that it is NOT running on the compositor'
+ + ' immediately after animation.finish() is called');
+ // Check that we don't set the flag back again on the next tick.
+ await waitForFrame();
+
+ assert_animation_is_not_running_on_compositor(animation,
+ 'Animation reports that it is NOT running on the compositor'
+ + ' on the next tick after animation.finish() is called');
+}, 'isRunningOnCompositor is false when the animation.finish() is called');
+
+promise_test(async t => {
+ var div = addDiv(t, { style: 'animation: anim 100s' });
+ var animation = div.getAnimations()[0];
+
+ await waitForPaints();
+
+ animation.currentTime = 100 * MS_PER_SEC;
+ assert_animation_is_not_running_on_compositor(animation,
+ 'Animation reports that it is NOT running on the compositor'
+ + ' immediately after manually seeking the animation to the end');
+ // Check that we don't set the flag back again on the next tick.
+ await waitForFrame();
+
+ assert_animation_is_not_running_on_compositor(animation,
+ 'Animation reports that it is NOT running on the compositor'
+ + ' on the next tick after manually seeking the animation to the end');
+}, 'isRunningOnCompositor is false when manually seeking the animation to ' +
+ 'the end');
+
+promise_test(async t => {
+ var div = addDiv(t, { style: 'animation: anim 100s' });
+ var animation = div.getAnimations()[0];
+
+ await waitForPaints();
+
+ animation.cancel();
+ assert_animation_is_not_running_on_compositor(animation,
+ 'Animation reports that it is NOT running on the compositor'
+ + ' immediately after animation.cancel() is called');
+ // Check that we don't set the flag back again on the next tick.
+ await waitForFrame();
+
+ assert_animation_is_not_running_on_compositor(animation,
+ 'Animation reports that it is NOT running on the compositor'
+ + ' on the next tick after animation.cancel() is called');
+}, 'isRunningOnCompositor is false when animation.cancel() is called');
+
+// This is to test that we don't simply clobber the flag when ticking
+// animations and then set it again during painting.
+promise_test(async t => {
+ var div = addDiv(t, { style: 'animation: anim 100s' });
+ var animation = div.getAnimations()[0];
+
+ await waitForPaints();
+
+ await new Promise(resolve => {
+ window.requestAnimationFrame(() => {
+ t.step(() => {
+ assert_animation_is_running_on_compositor(animation,
+ 'Animation reports that it is running on the compositor'
+ + ' in requestAnimationFrame callback');
+ });
+
+ resolve();
+ });
+ });
+}, 'isRunningOnCompositor is true in requestAnimationFrame callback');
+
+promise_test(async t => {
+ var div = addDiv(t, { style: 'animation: anim 100s' });
+ var animation = div.getAnimations()[0];
+
+ await waitForPaints();
+
+ await new Promise(resolve => {
+ var observer = new MutationObserver(records => {
+ var changedAnimation;
+
+ records.forEach(record => {
+ changedAnimation =
+ record.changedAnimations.find(changedAnim => {
+ return changedAnim == animation;
+ });
+ });
+
+ t.step(() => {
+ assert_true(!!changedAnimation, 'The animation should be recorded '
+ + 'as one of the changedAnimations');
+
+ assert_animation_is_running_on_compositor(animation,
+ 'Animation reports that it is running on the compositor'
+ + ' in MutationObserver callback');
+ });
+
+ resolve();
+ });
+ observer.observe(div, { animations: true, subtree: false });
+ t.add_cleanup(() => {
+ observer.disconnect();
+ });
+ div.style.animationDuration = "200s";
+ });
+}, 'isRunningOnCompositor is true in MutationObserver callback');
+
+// This is to test that we don't temporarily clear the flag when forcing
+// an unthrottled sample.
+promise_test(async t => {
+ var div = addDiv(t, { style: 'animation: rotate 100s' });
+ var animation = div.getAnimations()[0];
+
+ await waitForPaints();
+
+ await new Promise(resolve => {
+ var timeAtStart = window.performance.now();
+ function handleFrame() {
+ t.step(() => {
+ assert_animation_is_running_on_compositor(animation,
+ 'Animation reports that it is running on the compositor'
+ + ' in requestAnimationFrame callback');
+ });
+
+ // we have to wait at least 200ms because this animation is
+ // unthrottled on every 200ms.
+ // See https://hg.mozilla.org/mozilla-central/file/cafb1c90f794/layout/style/AnimationCommon.cpp#l863
+ if (window.performance.now() - timeAtStart > 200) {
+ resolve();
+ return;
+ }
+ window.requestAnimationFrame(handleFrame);
+ }
+ window.requestAnimationFrame(handleFrame);
+ });
+}, 'isRunningOnCompositor remains true in requestAnimationFrameCallback for ' +
+ 'overflow animation');
+
+promise_test(async t => {
+ var div = addDiv(t, { style: 'transition: opacity 100s; opacity: 1' });
+
+ getComputedStyle(div).opacity;
+
+ div.style.opacity = 0;
+ var animation = div.getAnimations()[0];
+
+ await waitForPaints();
+
+ assert_animation_is_running_on_compositor(animation,
+ 'Transition reports that it is running on the compositor'
+ + ' during playback for opacity transition');
+}, 'isRunningOnCompositor for transitions');
+
+promise_test(async t => {
+ var div = addDiv(t, { style: 'animation: rotate-and-opacity 100s; ' +
+ 'backface-visibility: hidden; ' +
+ 'transform: none !important;' });
+ var animation = div.getAnimations()[0];
+
+ await waitForPaints();
+
+ assert_animation_is_running_on_compositor(animation,
+ 'If an animation has a property that can run on the compositor and a '
+ + 'property that cannot (due to Gecko limitations) but where the latter'
+ + 'property is overridden in the CSS cascade, the animation should '
+ + 'still report that it is running on the compositor');
+}, 'isRunningOnCompositor is true when a property that would otherwise block ' +
+ 'running on the compositor is overridden in the CSS cascade');
+
+promise_test(async t => {
+ var animation = addDivAndAnimate(t,
+ {},
+ { opacity: [ 0, 1 ] }, 200 * MS_PER_SEC);
+
+ await waitForPaints();
+
+ assert_animation_is_running_on_compositor(animation,
+ 'Animation reports that it is running on the compositor');
+
+ animation.currentTime = 150 * MS_PER_SEC;
+ animation.effect.updateTiming({ duration: 100 * MS_PER_SEC });
+
+ assert_animation_is_not_running_on_compositor(animation,
+ 'Animation reports that it is NOT running on the compositor'
+ + ' when the animation is set a shorter duration than current time');
+}, 'animation is immediately removed from compositor' +
+ 'when the duration is made shorter than the current time');
+
+promise_test(async t => {
+ var animation = addDivAndAnimate(t,
+ {},
+ { opacity: [ 0, 1 ] }, 100 * MS_PER_SEC);
+
+ await waitForPaints();
+
+ assert_animation_is_running_on_compositor(animation,
+ 'Animation reports that it is running on the compositor');
+
+ animation.currentTime = 500 * MS_PER_SEC;
+
+ assert_animation_is_not_running_on_compositor(animation,
+ 'Animation reports that it is NOT running on the compositor'
+ + ' when finished');
+
+ animation.effect.updateTiming({ duration: 1000 * MS_PER_SEC });
+ await waitForFrame();
+
+ assert_animation_is_running_on_compositor(animation,
+ 'Animation reports that it is running on the compositor'
+ + ' when restarted');
+}, 'animation is added to compositor' +
+ ' when the duration is made longer than the current time');
+
+promise_test(async t => {
+ var animation = addDivAndAnimate(t,
+ {},
+ { opacity: [ 0, 1 ] }, 100 * MS_PER_SEC);
+
+ await waitForPaints();
+
+ assert_animation_is_running_on_compositor(animation,
+ 'Animation reports that it is running on the compositor');
+
+ animation.effect.updateTiming({ endDelay: 100 * MS_PER_SEC });
+
+ assert_animation_is_running_on_compositor(animation,
+ 'Animation reports that it is running on the compositor'
+ + ' when endDelay is changed');
+
+ animation.currentTime = 110 * MS_PER_SEC;
+ await waitForFrame();
+
+ assert_animation_is_not_running_on_compositor(animation,
+ 'Animation reports that it is NOT running on the compositor'
+ + ' when currentTime is during endDelay');
+}, 'animation is removed from compositor' +
+ ' when current time is made longer than the duration even during endDelay');
+
+promise_test(async t => {
+ var animation = addDivAndAnimate(t,
+ {},
+ { opacity: [ 0, 1 ] }, 100 * MS_PER_SEC);
+
+ await waitForPaints();
+
+ assert_animation_is_running_on_compositor(animation,
+ 'Animation reports that it is running on the compositor');
+
+ animation.effect.updateTiming({ endDelay: -200 * MS_PER_SEC });
+ await waitForFrame();
+
+ assert_animation_is_not_running_on_compositor(animation,
+ 'Animation reports that it is NOT running on the compositor'
+ + ' when endTime is negative value');
+}, 'animation is removed from compositor' +
+ ' when endTime is negative value');
+
+promise_test(async t => {
+ var animation = addDivAndAnimate(t,
+ {},
+ { opacity: [ 0, 1 ] }, 200 * MS_PER_SEC);
+
+ await waitForPaints();
+
+ assert_animation_is_running_on_compositor(animation,
+ 'Animation reports that it is running on the compositor');
+
+ animation.effect.updateTiming({ endDelay: -100 * MS_PER_SEC });
+ await waitForFrame();
+
+ assert_animation_is_running_on_compositor(animation,
+ 'Animation reports that it is running on the compositor'
+ + ' when endTime is positive and endDelay is negative');
+ animation.currentTime = 110 * MS_PER_SEC;
+ await waitForFrame();
+
+ assert_animation_is_not_running_on_compositor(animation,
+ 'Animation reports that it is NOT running on the compositor'
+ + ' when currentTime is after endTime');
+}, 'animation is NOT running on compositor' +
+ ' when endTime is positive and endDelay is negative');
+
+promise_test(async t => {
+ var effect = new KeyframeEffect(null,
+ { opacity: [ 0, 1 ] },
+ 100 * MS_PER_SEC);
+ var animation = new Animation(effect, document.timeline);
+ animation.play();
+
+ var div = addDiv(t);
+
+ await waitForPaints();
+
+ assert_animation_is_not_running_on_compositor(animation,
+ 'Animation with null target reports that it is not running ' +
+ 'on the compositor');
+
+ animation.effect.target = div;
+ await waitForFrame();
+
+ assert_animation_is_running_on_compositor(animation,
+ 'Animation reports that it is running on the compositor ' +
+ 'after setting a valid target');
+}, 'animation is added to the compositor when setting a valid target');
+
+promise_test(async t => {
+ var div = addDiv(t);
+ var animation = div.animate({ opacity: [ 0, 1 ] }, 100 * MS_PER_SEC);
+
+ await waitForPaints();
+
+ assert_animation_is_running_on_compositor(animation,
+ 'Animation reports that it is running on the compositor');
+
+ animation.effect.target = null;
+ assert_animation_is_not_running_on_compositor(animation,
+ 'Animation reports that it is NOT running on the ' +
+ 'compositor after setting null target');
+}, 'animation is removed from the compositor when setting null target');
+
+promise_test(async t => {
+ var div = addDiv(t);
+ var animation = div.animate({ opacity: [ 0, 1 ] },
+ { duration: 100 * MS_PER_SEC,
+ delay: 100 * MS_PER_SEC,
+ fill: 'backwards' });
+
+ await waitForPaints();
+
+ assert_animation_is_running_on_compositor(animation,
+ 'Animation with fill:backwards in delay phase reports ' +
+ 'that it is running on the compositor');
+
+ animation.currentTime = 100 * MS_PER_SEC;
+ await waitForFrame();
+
+ assert_animation_is_running_on_compositor(animation,
+ 'Animation with fill:backwards in delay phase reports ' +
+ 'that it is running on the compositor after delay phase');
+}, 'animation with fill:backwards in delay phase is running on the ' +
+ ' compositor while it is in delay phase');
+
+promise_test(async t => {
+ const animation = addDiv(t).animate({ opacity: [ 0, 1 ] }, 100 * MS_PER_SEC);
+ animation.playbackRate = -1;
+ animation.currentTime = 200 * MS_PER_SEC;
+
+ await waitForPaints();
+
+ assert_animation_is_running_on_compositor(animation,
+ 'Animation with negative playback rate is runnning on the'
+ + ' compositor even before it reaches the active interval');
+}, 'animation with negative playback rate is sent to the compositor even in'
+ + ' after phase');
+
+promise_test(async t => {
+ var div = addDiv(t);
+ var animation = div.animate([{ opacity: 1, offset: 0 },
+ { opacity: 1, offset: 0.99 },
+ { opacity: 0, offset: 1 }], 100 * MS_PER_SEC);
+
+ var another = addDiv(t);
+
+ await waitForPaints();
+
+ assert_animation_is_running_on_compositor(animation,
+ 'Opacity animation on a 100% opacity keyframe reports ' +
+ 'that it is running on the compositor from the begining');
+
+ animation.effect.target = another;
+ await waitForFrame();
+
+ assert_animation_is_running_on_compositor(animation,
+ 'Opacity animation on a 100% opacity keyframe keeps ' +
+ 'running on the compositor after changing the target ' +
+ 'element');
+}, '100% opacity animations with keeps running on the ' +
+ 'compositor after changing the target element');
+
+promise_test(async t => {
+ var div = addDiv(t);
+ var animation = div.animate({ color: ['red', 'black'] }, 100 * MS_PER_SEC);
+
+ await waitForPaints();
+
+ assert_animation_is_not_running_on_compositor(animation,
+ 'Color animation reports that it is not running on the ' +
+ 'compositor');
+
+ animation.effect.setKeyframes([{ opacity: 1, offset: 0 },
+ { opacity: 1, offset: 0.99 },
+ { opacity: 0, offset: 1 }]);
+ await waitForFrame();
+
+ assert_animation_is_running_on_compositor(animation,
+ '100% opacity animation set by using setKeyframes reports ' +
+ 'that it is running on the compositor');
+}, '100% opacity animation set up by converting an existing animation with ' +
+ 'cannot be run on the compositor, is running on the compositor');
+
+promise_test(async t => {
+ var div = addDiv(t);
+ var animation = div.animate({ color: ['red', 'black'] }, 100 * MS_PER_SEC);
+ var effect = new KeyframeEffect(div,
+ [{ opacity: 1, offset: 0 },
+ { opacity: 1, offset: 0.99 },
+ { opacity: 0, offset: 1 }],
+ 100 * MS_PER_SEC);
+
+ await waitForPaints();
+
+ assert_animation_is_not_running_on_compositor(animation,
+ 'Color animation reports that it is not running on the ' +
+ 'compositor');
+
+ animation.effect = effect;
+ await waitForFrame();
+
+ assert_animation_is_running_on_compositor(animation,
+ '100% opacity animation set up by changing effects reports ' +
+ 'that it is running on the compositor');
+}, '100% opacity animation set up by changing the effects on an existing ' +
+ 'animation which cannot be run on the compositor, is running on the ' +
+ 'compositor');
+
+promise_test(async t => {
+ var div = addDiv(t, { style: "opacity: 1 ! important" });
+
+ var animation = div.animate({ opacity: [0, 1] }, 100 * MS_PER_SEC);
+
+ await waitForPaints();
+
+ assert_animation_is_not_running_on_compositor(animation,
+ 'Opacity animation on an element which has 100% opacity style with ' +
+ '!important flag reports that it is not running on the compositor');
+ // Clear important flag from the opacity style on the target element.
+ div.style.setProperty("opacity", "1", "");
+ await waitForFrame();
+
+ assert_animation_is_running_on_compositor(animation,
+ 'Opacity animation reports that it is running on the compositor after '
+ + 'clearing the !important flag');
+}, 'Clearing *important* opacity style on the target element sends the ' +
+ 'animation to the compositor');
+
+promise_test(async t => {
+ var div = addDiv(t);
+ var lowerAnimation = div.animate({ opacity: [1, 0] }, 100 * MS_PER_SEC);
+ var higherAnimation = div.animate({ opacity: [0, 1] }, 100 * MS_PER_SEC);
+
+ await waitForPaints();
+
+ assert_animation_is_running_on_compositor(higherAnimation,
+ 'A higher-priority opacity animation on an element ' +
+ 'reports that it is running on the compositor');
+ assert_animation_is_running_on_compositor(lowerAnimation,
+ 'A lower-priority opacity animation on the same ' +
+ 'element also reports that it is running on the compositor');
+}, 'Opacity animations on the same element run on the compositor');
+
+promise_test(async t => {
+ var div = addDiv(t, { style: 'transition: opacity 100s; opacity: 1' });
+
+ getComputedStyle(div).opacity;
+
+ div.style.opacity = 0;
+ getComputedStyle(div).opacity;
+
+ var transition = div.getAnimations()[0];
+ var animation = div.animate({ opacity: [0, 1] }, 100 * MS_PER_SEC);
+
+ await waitForPaints();
+
+ assert_animation_is_running_on_compositor(animation,
+ 'An opacity animation on an element reports that' +
+ 'that it is running on the compositor');
+ assert_animation_is_running_on_compositor(transition,
+ 'An opacity transition on the same element reports that ' +
+ 'it is running on the compositor');
+}, 'Both of transition and script animation on the same element run on the ' +
+ 'compositor');
+
+promise_test(async t => {
+ var div = addDiv(t);
+ var importantOpacityElement = addDiv(t, { style: "opacity: 1 ! important" });
+
+ var animation = div.animate({ opacity: [1, 0] }, 100 * MS_PER_SEC);
+
+ await waitForPaints();
+
+ assert_animation_is_running_on_compositor(animation,
+ 'Opacity animation on an element reports ' +
+ 'that it is running on the compositor');
+
+ animation.effect.target = null;
+ await waitForFrame();
+
+ assert_animation_is_not_running_on_compositor(animation,
+ 'Animation is no longer running on the compositor after ' +
+ 'removing from the element');
+ animation.effect.target = importantOpacityElement;
+ await waitForFrame();
+
+ assert_animation_is_not_running_on_compositor(animation,
+ 'Animation is NOT running on the compositor even after ' +
+ 'being applied to a different element which has an ' +
+ '!important opacity declaration');
+}, 'Animation continues not running on the compositor after being ' +
+ 'applied to an element which has an important declaration and ' +
+ 'having previously been temporarily associated with no target element');
+
+promise_test(async t => {
+ var div = addDiv(t);
+ var another = addDiv(t);
+
+ var lowerAnimation = div.animate({ opacity: [1, 0] }, 100 * MS_PER_SEC);
+ var higherAnimation = another.animate({ opacity: [0, 1] }, 100 * MS_PER_SEC);
+
+ await waitForPaints();
+
+ assert_animation_is_running_on_compositor(lowerAnimation,
+ 'An opacity animation on an element reports that ' +
+ 'it is running on the compositor');
+ assert_animation_is_running_on_compositor(higherAnimation,
+ 'Opacity animation on a different element reports ' +
+ 'that it is running on the compositor');
+
+ lowerAnimation.effect.target = null;
+ await waitForFrame();
+
+ assert_animation_is_not_running_on_compositor(lowerAnimation,
+ 'Animation is no longer running on the compositor after ' +
+ 'being removed from the element');
+ lowerAnimation.effect.target = another;
+ await waitForFrame();
+
+ assert_animation_is_running_on_compositor(lowerAnimation,
+ 'A lower-priority animation begins running ' +
+ 'on the compositor after being applied to an element ' +
+ 'which has a higher-priority animation');
+ assert_animation_is_running_on_compositor(higherAnimation,
+ 'A higher-priority animation continues to run on the ' +
+ 'compositor even after a lower-priority animation is ' +
+ 'applied to the same element');
+}, 'Animation begins running on the compositor after being applied ' +
+ 'to an element which has a higher-priority animation and after ' +
+ 'being temporarily associated with no target element');
+
+promise_test(async t => {
+ var div = addDiv(t);
+ var another = addDiv(t);
+
+ var lowerAnimation = div.animate({ opacity: [1, 0] }, 100 * MS_PER_SEC);
+ var higherAnimation = another.animate({ opacity: [0, 1] }, 100 * MS_PER_SEC);
+
+ await waitForPaints();
+
+ assert_animation_is_running_on_compositor(lowerAnimation,
+ 'An opacity animation on an element reports that ' +
+ 'it is running on the compositor');
+ assert_animation_is_running_on_compositor(higherAnimation,
+ 'Opacity animation on a different element reports ' +
+ 'that it is running on the compositor');
+
+ higherAnimation.effect.target = null;
+ await waitForFrame();
+
+ assert_animation_is_not_running_on_compositor(higherAnimation,
+ 'Animation is no longer running on the compositor after ' +
+ 'being removed from the element');
+ higherAnimation.effect.target = div;
+ await waitForFrame();
+
+ assert_animation_is_running_on_compositor(lowerAnimation,
+ 'Animation continues running on the compositor after ' +
+ 'a higher-priority animation applied to the same element');
+ assert_animation_is_running_on_compositor(higherAnimation,
+ 'A higher-priority animation begins to running on the ' +
+ 'compositor after being applied to an element which has ' +
+ 'a lower-priority-animation');
+}, 'Animation begins running on the compositor after being applied ' +
+ 'to an element which has a lower-priority animation once after ' +
+ 'disassociating with an element');
+
+var delayPhaseTests = [
+ {
+ desc: 'script animation of opacity',
+ setupAnimation: t => {
+ return addDiv(t).animate(
+ { opacity: [0, 1] },
+ { delay: 100 * MS_PER_SEC, duration: 100 * MS_PER_SEC });
+ },
+ },
+ {
+ desc: 'script animation of transform',
+ setupAnimation: t => {
+ return addDiv(t).animate(
+ { transform: ['translateX(0px)', 'translateX(100px)'] },
+ { delay: 100 * MS_PER_SEC, duration: 100 * MS_PER_SEC });
+ },
+ },
+ {
+ desc: 'CSS animation of opacity',
+ setupAnimation: t => {
+ return addDiv(t, { style: 'animation: opacity 100s 100s' })
+ .getAnimations()[0];
+ },
+ },
+ {
+ desc: 'CSS animation of transform',
+ setupAnimation: t => {
+ return addDiv(t, { style: 'animation: anim 100s 100s' })
+ .getAnimations()[0];
+ },
+ },
+ {
+ desc: 'CSS transition of opacity',
+ setupAnimation: t => {
+ var div = addDiv(t, { style: 'transition: opacity 100s 100s' });
+ getComputedStyle(div).opacity;
+
+ div.style.opacity = 0;
+ return div.getAnimations()[0];
+ },
+ },
+ {
+ desc: 'CSS transition of transform',
+ setupAnimation: t => {
+ var div = addDiv(t, { style: 'transition: transform 100s 100s' });
+ getComputedStyle(div).transform;
+
+ div.style.transform = 'translateX(100px)';
+ return div.getAnimations()[0];
+ },
+ },
+];
+
+delayPhaseTests.forEach(test => {
+ promise_test(async t => {
+ var animation = test.setupAnimation(t);
+
+ await waitForPaints();
+
+ assert_animation_is_running_on_compositor(animation,
+ test.desc + ' reports that it is running on the '
+ + 'compositor even though it is in the delay phase');
+ }, 'isRunningOnCompositor for ' + test.desc + ' is true even though ' +
+ 'it is in the delay phase');
+});
+
+// The purpose of thie test cases is to check that
+// NS_FRAME_MAY_BE_TRANSFORMED flag on the associated nsIFrame persists
+// after transform style on the frame is removed.
+var delayPhaseWithTransformStyleTests = [
+ {
+ desc: 'script animation of transform with transform style',
+ setupAnimation: t => {
+ return addDiv(t, { style: 'transform: translateX(10px)' }).animate(
+ { transform: ['translateX(0px)', 'translateX(100px)'] },
+ { delay: 100 * MS_PER_SEC, duration: 100 * MS_PER_SEC });
+ },
+ },
+ {
+ desc: 'CSS animation of transform with transform style',
+ setupAnimation: t => {
+ return addDiv(t, { style: 'animation: anim 100s 100s;' +
+ 'transform: translateX(10px)' })
+ .getAnimations()[0];
+ },
+ },
+ {
+ desc: 'CSS transition of transform with transform style',
+ setupAnimation: t => {
+ var div = addDiv(t, { style: 'transition: transform 100s 100s;' +
+ 'transform: translateX(10px)'});
+ getComputedStyle(div).transform;
+
+ div.style.transform = 'translateX(100px)';
+ return div.getAnimations()[0];
+ },
+ },
+];
+
+delayPhaseWithTransformStyleTests.forEach(test => {
+ promise_test(async t => {
+ var animation = test.setupAnimation(t);
+
+ await waitForPaints();
+
+ assert_animation_is_running_on_compositor(animation,
+ test.desc + ' reports that it is running on the '
+ + 'compositor even though it is in the delay phase');
+
+ // Remove the initial transform style during delay phase.
+ animation.effect.target.style.transform = 'none';
+ await animation.ready;
+
+ assert_animation_is_running_on_compositor(animation,
+ test.desc + ' reports that it keeps running on the '
+ + 'compositor after removing the initial transform style');
+ }, 'isRunningOnCompositor for ' + test.desc + ' is true after removing ' +
+ 'the initial transform style during the delay phase');
+});
+
+var startsWithNoneTests = [
+ {
+ desc: 'script animation of transform starts with transform:none segment',
+ setupAnimation: t => {
+ return addDiv(t).animate(
+ { transform: ['none', 'none', 'translateX(100px)'] }, 100 * MS_PER_SEC);
+ },
+ },
+ {
+ desc: 'CSS animation of transform starts with transform:none segment',
+ setupAnimation: t => {
+ return addDiv(t,
+ { style: 'animation: transform-starts-with-none 100s 100s' })
+ .getAnimations()[0];
+ },
+ },
+];
+
+startsWithNoneTests.forEach(test => {
+ promise_test(async t => {
+ var animation = test.setupAnimation(t);
+
+ await waitForPaints();
+
+ assert_animation_is_running_on_compositor(animation,
+ test.desc + ' reports that it is running on the '
+ + 'compositor even though it is in transform:none segment');
+ }, 'isRunningOnCompositor for ' + test.desc + ' is true even though ' +
+ 'it is in transform:none segment');
+});
+
+promise_test(async t => {
+ var div = addDiv(t, { style: 'opacity: 1 ! important' });
+
+ var animation = div.animate(
+ { opacity: [0, 1] },
+ { delay: 100 * MS_PER_SEC, duration: 100 * MS_PER_SEC });
+
+ await waitForPaints();
+
+ assert_animation_is_not_running_on_compositor(animation,
+ 'Opacity animation on an element which has opacity:1 important style'
+ + 'reports that it is not running on the compositor');
+ // Clear the opacity style on the target element.
+ div.style.setProperty("opacity", "1", "");
+ await waitForFrame();
+
+ assert_animation_is_running_on_compositor(animation,
+ 'Opacity animations reports that it is running on the compositor after '
+ + 'clearing the opacity style on the element');
+}, 'Clearing *important* opacity style on the target element sends the ' +
+ 'animation to the compositor even if the animation is in the delay phase');
+
+promise_test(async t => {
+ var opaqueDiv = addDiv(t, { style: 'opacity: 1 ! important' });
+ var anotherDiv = addDiv(t);
+
+ var animation = opaqueDiv.animate(
+ { opacity: [0, 1] },
+ { delay: 100 * MS_PER_SEC, duration: 100 * MS_PER_SEC });
+
+ await waitForPaints();
+
+ assert_animation_is_not_running_on_compositor(animation,
+ 'Opacity animation on an element which has opacity:1 important style'
+ + 'reports that it is not running on the compositor');
+ // Changing target element to another element which has no opacity style.
+ animation.effect.target = anotherDiv;
+ await waitForFrame();
+
+ assert_animation_is_running_on_compositor(animation,
+ 'Opacity animations reports that it is running on the compositor after '
+ + 'changing the target element to another elemenent having no '
+ + 'opacity style');
+}, 'Changing target element of opacity animation sends the animation to the ' +
+ 'the compositor even if the animation is in the delay phase');
+
+promise_test(async t => {
+ var animation =
+ addDivAndAnimate(t,
+ {},
+ { width: ['100px', '200px'] },
+ { duration: 100 * MS_PER_SEC, delay: 100 * MS_PER_SEC });
+
+ await waitForPaints();
+
+ assert_animation_is_not_running_on_compositor(animation,
+ 'Width animation reports that it is not running on the compositor '
+ + 'in the delay phase');
+ // Changing to property runnable on the compositor.
+ animation.effect.setKeyframes({ opacity: [0, 1] });
+ await waitForFrame();
+
+ assert_animation_is_running_on_compositor(animation,
+ 'Opacity animation reports that it is running on the compositor '
+ + 'after changing the property from width property in the delay phase');
+}, 'Dynamic change to a property runnable on the compositor ' +
+ 'in the delay phase');
+
+promise_test(async t => {
+ var div = addDiv(t, { style: 'transition: opacity 100s; ' +
+ 'opacity: 0 !important' });
+ getComputedStyle(div).opacity;
+
+ div.style.setProperty('opacity', '1', 'important');
+ getComputedStyle(div).opacity;
+
+ var animation = div.getAnimations()[0];
+
+ await waitForPaints();
+
+ assert_animation_is_running_on_compositor(animation,
+ 'Transition reports that it is running on the compositor even if the ' +
+ 'property is overridden by an !important rule');
+}, 'Transitions override important rules');
+
+promise_test(async t => {
+ var div = addDiv(t, { style: 'transition: opacity 100s; ' +
+ 'opacity: 0 !important' });
+ getComputedStyle(div).opacity;
+
+ div.animate({ opacity: [ 0, 1 ] }, 100 * MS_PER_SEC);
+
+ div.style.setProperty('opacity', '1', 'important');
+ getComputedStyle(div).opacity;
+
+ var [transition, animation] = div.getAnimations();
+
+ await waitForPaints();
+
+ assert_animation_is_not_running_on_compositor(transition,
+ 'Transition suppressed by an animation which is overridden by an ' +
+ '!important rule reports that it is NOT running on the compositor');
+ assert_animation_is_not_running_on_compositor(animation,
+ 'Animation overridden by an !important rule reports that it is ' +
+ 'NOT running on the compositor');
+}, 'Neither transition nor animation does run on the compositor if the ' +
+ 'property is overridden by an !important rule');
+
+promise_test(async t => {
+ var div = addDiv(t, { style: 'display: table' });
+ var animation =
+ div.animate({ transform: ['rotate(0deg)', 'rotate(360deg)'] },
+ 100 * MS_PER_SEC);
+
+ await waitForAnimationReadyToRestyle(animation);
+
+ await waitForPaints();
+
+ assert_animation_is_running_on_compositor(animation,
+ 'Transform animation on display:table element should be running on the'
+ + ' compositor');
+}, 'Transform animation on display:table element runs on the compositor');
+
+promise_test(async t => {
+ const div = addDiv(t, { style: 'display: table' });
+ const animation = div.animate(null, 100 * MS_PER_SEC);
+ const effect = new KeyframeEffect(div,
+ { transform: ['none', 'none']},
+ 100 * MS_PER_SEC);
+
+ await waitForAnimationReadyToRestyle(animation);
+
+ animation.effect = effect;
+
+ await waitForNextFrame();
+ await waitForPaints();
+
+ assert_animation_is_running_on_compositor(
+ animation,
+ 'Transform animation on table element should be running on the compositor'
+ );
+}, 'Empty transform effect assigned after the fact to display:table content'
+ + ' runs on the compositor');
+
+promise_test(async t => {
+ const div = addDiv(t);
+ const animation = div.animate({ backgroundColor: ['blue', 'green'] },
+ 100 * MS_PER_SEC);
+
+ await waitForAnimationReadyToRestyle(animation);
+ await waitForPaints();
+
+ assert_animation_is_running_on_compositor(animation,
+ 'background-color animation should be running on the compositor');
+}, 'background-color animation runs on the compositor');
+
+promise_test(async t => {
+ const div = addDiv(t);
+ const animation = div.animate({ backgroundColor: ['blue', 'green'] },
+ 100 * MS_PER_SEC);
+
+ await waitForAnimationReadyToRestyle(animation);
+ await waitForPaints();
+
+ assert_animation_is_running_on_compositor(animation,
+ 'background-color animation should be running on the compositor');
+
+ // Add a red opaque background image covering the background color animation.
+ div.style.backgroundImage =
+ 'url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAIAAAAC64' +
+ 'paAAAAG0lEQVR42mP8z0A%2BYKJA76jmUc2jmkc1U0EzACKcASfOgGoMAAAAAElFTkSuQmCC)';
+
+ await waitForAnimationReadyToRestyle(animation);
+ await waitForPaints();
+
+ // Bug 1712246. We should optimize this case eventually.
+ //assert_animation_is_not_running_on_compositor(animation,
+ // 'Opaque background image stops background-color animations from running ' +
+ // 'on the compositor');
+}, 'Opaque background image stops background-color animations from running ' +
+ ' on the compositor');
+
+promise_test(async t => {
+ await SpecialPowers.pushPrefEnv({
+ set: [["gfx.omta.background-color", false]]
+ });
+
+ const div = addDiv(t);
+ const animation = div.animate({ backgroundColor: ['blue', 'green'] },
+ 100 * MS_PER_SEC);
+
+ await waitForAnimationReadyToRestyle(animation);
+ await waitForPaints();
+
+ assert_animation_is_not_running_on_compositor(animation,
+ 'background-color animation should NOT be running on the compositor ' +
+ 'if the pref is disabled');
+}, 'background-color animation does not run on the compositor if the pref ' +
+ 'is disabled');
+
+promise_test(async t => {
+ const div = addDiv(t);
+ const animation = div.animate({ translate: ['0px', '100px'] },
+ 100 * MS_PER_SEC);
+
+ await waitForAnimationReadyToRestyle(animation);
+ await waitForPaints();
+
+ assert_animation_is_running_on_compositor(animation,
+ 'translate animation should be running on the compositor');
+}, 'translate animation runs on the compositor');
+
+promise_test(async t => {
+ const div = addDiv(t);
+ const animation = div.animate({ rotate: ['0deg', '45deg'] },
+ 100 * MS_PER_SEC);
+
+ await waitForAnimationReadyToRestyle(animation);
+ await waitForPaints();
+
+ assert_animation_is_running_on_compositor(animation,
+ 'rotate animation should be running on the compositor');
+}, 'rotate animation runs on the compositor');
+
+promise_test(async t => {
+ const div = addDiv(t);
+ const animation = div.animate({ scale: ['1 1', '2 2'] },
+ 100 * MS_PER_SEC);
+
+ await waitForAnimationReadyToRestyle(animation);
+ await waitForPaints();
+
+ assert_animation_is_running_on_compositor(animation,
+ 'scale animation should be running on the compositor');
+}, 'scale animation runs on the compositor');
+
+promise_test(async t => {
+ const div = addDiv(t);
+ const animation = div.animate({ translate: ['0px', '100px'],
+ rotate: ['0deg', '45deg'],
+ transform: ['translate(20px)',
+ 'translate(30px)'] },
+ 100 * MS_PER_SEC);
+
+ await waitForAnimationReadyToRestyle(animation);
+ await waitForPaints();
+
+ assert_animation_is_running_on_compositor(animation,
+ 'multiple transform-like properties animation should be running on the ' +
+ 'compositor');
+
+ const properties = animation.effect.getProperties();
+ properties.forEach(property => {
+ assert_true(property.runningOnCompositor,
+ property.property + ' is running on the compositor');
+ });
+}, 'Multiple transform-like properties animation runs on the compositor');
+
+promise_test(async t => {
+ const div = addDiv(t);
+ const animation = div.animate({ offsetPath: ['none', 'none'] },
+ 100 * MS_PER_SEC);
+
+ await waitForAnimationReadyToRestyle(animation);
+ await waitForPaints();
+
+ assert_animation_is_running_on_compositor(animation,
+ 'offset-path animation should be running on the compositor even if ' +
+ 'it is always none');
+}, 'offset-path none animation runs on the compositor');
+
+promise_test(async t => {
+ const div = addDiv(t);
+ const animation = div.animate({ offsetPath: ['path("M0 0l100 100")',
+ 'path("M0 0l200 200")'] },
+ 100 * MS_PER_SEC);
+
+ await waitForAnimationReadyToRestyle(animation);
+ await waitForPaints();
+
+ assert_animation_is_running_on_compositor(animation,
+ 'offset-path:path() animation should be running on the compositor');
+}, 'offset-path:path() animation runs on the compositor');
+
+promise_test(async t => {
+ const div = addDiv(t);
+ const animation = div.animate({ offsetPath: ['ray(0deg)',
+ 'ray(180deg)'] },
+ 100 * MS_PER_SEC);
+
+ await waitForAnimationReadyToRestyle(animation);
+ await waitForPaints();
+
+ assert_animation_is_running_on_compositor(animation,
+ 'offset-path:ray() animation should be running on the compositor');
+}, 'offset-path:ray() animation runs on the compositor');
+
+promise_test(async t => {
+ const div = addDiv(t);
+ const animation = div.animate({ offsetPath: ['inset(0px)',
+ 'inset(10px)'] },
+ 100 * MS_PER_SEC);
+
+ await waitForAnimationReadyToRestyle(animation);
+ await waitForPaints();
+
+ assert_animation_is_running_on_compositor(animation,
+ 'offset-path:inset() animation should be running on the compositor');
+}, 'offset-path:inset() animation runs on the compositor');
+
+promise_test(async t => {
+ const div = addDiv(t);
+ const animation = div.animate({ offsetPath: ['circle(10px)',
+ 'circle(20px)'] },
+ 100 * MS_PER_SEC);
+
+ await waitForAnimationReadyToRestyle(animation);
+ await waitForPaints();
+
+ assert_animation_is_running_on_compositor(animation,
+ 'offset-path:circle() animation should be running on the compositor');
+}, 'offset-path:circle() animation runs on the compositor');
+
+promise_test(async t => {
+ const div = addDiv(t);
+ const animation = div.animate({ offsetPath: ['ellipse(10px 20px)',
+ 'ellipse(20px 40px)'] },
+ 100 * MS_PER_SEC);
+
+ await waitForAnimationReadyToRestyle(animation);
+ await waitForPaints();
+
+ assert_animation_is_running_on_compositor(animation,
+ 'offset-path:ellipse() animation should be running on the compositor');
+}, 'offset-path:ellipse() animation runs on the compositor');
+
+promise_test(async t => {
+ const div = addDiv(t);
+ const animation = div.animate({ offsetPath: ['polygon(0px 0px)',
+ 'polygon(50px 50px)'] },
+ 100 * MS_PER_SEC);
+
+ await waitForAnimationReadyToRestyle(animation);
+ await waitForPaints();
+
+ assert_animation_is_running_on_compositor(animation,
+ 'offset-path:polygon() animation should be running on the compositor');
+}, 'offset-path:polygon() animation runs on the compositor');
+
+promise_test(async t => {
+ const div = addDiv(t);
+ const animation = div.animate({ offsetPath: ['padding-box',
+ 'padding-box'] },
+ 100 * MS_PER_SEC);
+
+ await waitForAnimationReadyToRestyle(animation);
+ await waitForPaints();
+
+ assert_animation_is_running_on_compositor(animation,
+ 'offset-path:padding-box animation should be running on the compositor');
+}, 'offset-path:padding-box animation runs on the compositor');
+
+promise_test(async t => {
+ const div = addDiv(t);
+ const animation = div.animate({ offsetPath: ['content-box',
+ 'content-box'] },
+ 100 * MS_PER_SEC);
+
+ await waitForAnimationReadyToRestyle(animation);
+ await waitForPaints();
+
+ assert_animation_is_running_on_compositor(animation,
+ 'offset-path:content-box animation should be running on the compositor');
+}, 'offset-path:content-box animation runs on the compositor');
+
+promise_test(async t => {
+ const div = addDiv(t);
+ const animation = div.animate({ offsetPath: ['xywh(0% 0% 10px 10px)',
+ 'xywh(10% 10% 20px 20px)'] },
+ 100 * MS_PER_SEC);
+
+ await waitForAnimationReadyToRestyle(animation);
+ await waitForPaints();
+
+ assert_animation_is_running_on_compositor(animation,
+ 'offset-path:xywh() animation should be running on the compositor');
+}, 'offset-path:xywh() animation runs on the compositor');
+
+promise_test(async t => {
+ const div = addDiv(t);
+ const animation = div.animate({ offsetPath: ['rect(0% 0% 10px 10px)',
+ 'rect(10% 10% 20px 20px)'] },
+ 100 * MS_PER_SEC);
+
+ await waitForAnimationReadyToRestyle(animation);
+ await waitForPaints();
+
+ assert_animation_is_running_on_compositor(animation,
+ 'offset-path:rect() animation should be running on the compositor');
+}, 'offset-path:rect() animation runs on the compositor');
+
+promise_test(async t => {
+ const div = addDiv(t);
+ const animation = div.animate({ offsetDistance: ['0%', '100%'] },
+ 100 * MS_PER_SEC);
+
+ await waitForAnimationReadyToRestyle(animation);
+ await waitForPaints();
+
+ assert_animation_is_not_running_on_compositor(animation,
+ 'offset-distance animation is not running on the compositor because ' +
+ 'offset-path is none');
+
+ const newAnim = div.animate({ offsetPath: ['None', 'None'] },
+ 100 * MS_PER_SEC);
+ await waitForAnimationReadyToRestyle(newAnim);
+ await waitForPaints();
+
+ assert_animation_is_running_on_compositor(animation,
+ 'offset-distance animation should be running on the compositor');
+ assert_animation_is_running_on_compositor(newAnim,
+ 'new added offset-path animation should be running on the compositor');
+}, 'offset-distance animation runs on the compositor');
+
+promise_test(async t => {
+ const div = addDiv(t);
+ const animation = div.animate({ offsetRotate: ['0deg', '45deg'] },
+ 100 * MS_PER_SEC);
+
+ await waitForAnimationReadyToRestyle(animation);
+ await waitForPaints();
+
+ assert_animation_is_not_running_on_compositor(animation,
+ 'offset-rotate animation is not running on the compositor because ' +
+ 'offset-path is none');
+
+ const newAnim = div.animate({ offsetPath: ['None', 'None'] },
+ 100 * MS_PER_SEC);
+ await waitForAnimationReadyToRestyle(newAnim);
+ await waitForPaints();
+
+ assert_animation_is_running_on_compositor(animation,
+ 'offset-rotate animation should be running on the compositor');
+ assert_animation_is_running_on_compositor(newAnim,
+ 'new added offset-path animation should be running on the compositor');
+}, 'offset-rotate animation runs on the compositor');
+
+promise_test(async t => {
+ const div = addDiv(t);
+ const animation = div.animate({ offsetAnchor: ['0% 0%', '100% 100%'] },
+ 100 * MS_PER_SEC);
+
+ await waitForAnimationReadyToRestyle(animation);
+ await waitForPaints();
+
+ assert_animation_is_not_running_on_compositor(animation,
+ 'offset-anchor animation is not running on the compositor because ' +
+ 'offset-path is none');
+
+ const newAnim = div.animate({ offsetPath: ['None', 'None'] },
+ 100 * MS_PER_SEC);
+ await waitForAnimationReadyToRestyle(newAnim);
+ await waitForPaints();
+
+ assert_animation_is_running_on_compositor(animation,
+ 'offset-anchor animation should be running on the compositor');
+ assert_animation_is_running_on_compositor(newAnim,
+ 'new added offset-path animation should be running on the compositor');
+}, 'offset-anchor animation runs on the compositor');
+
+promise_test(async t => {
+ const div = addDiv(t);
+ const animation = div.animate({ offsetPosition: ['0% 0%', '100% 100%'] },
+ 100 * MS_PER_SEC);
+
+ await waitForAnimationReadyToRestyle(animation);
+ await waitForPaints();
+
+ assert_animation_is_not_running_on_compositor(animation,
+ 'offset-position animation is not running on the compositor because ' +
+ 'offset-path is none');
+
+ const newAnim = div.animate({ offsetPath: ['None', 'None'] },
+ 100 * MS_PER_SEC);
+ await waitForAnimationReadyToRestyle(newAnim);
+ await waitForPaints();
+
+ assert_animation_is_running_on_compositor(animation,
+ 'offset-position animation should be running on the compositor');
+ assert_animation_is_running_on_compositor(newAnim,
+ 'new added offset-path animation should be running on the compositor');
+}, 'offset-position animation runs on the compositor');
+
+promise_test(async t => {
+ const div = addDiv(t);
+ const animation = div.animate({ translate: ['0px', '100px'],
+ rotate: ['0deg', '45deg'],
+ transform: ['translate(0px)',
+ 'translate(100px)'],
+ offsetDistance: ['0%', '100%'] },
+ 100 * MS_PER_SEC);
+ await waitForAnimationReadyToRestyle(animation);
+ await waitForPaints();
+
+ assert_animation_is_running_on_compositor(animation,
+ 'Animation is running on the compositor even though we do not have ' +
+ 'offset-path');
+
+ div.style.offsetPath = 'path("M50 0v100")';
+ getComputedStyle(div).offsetPath;
+
+ await waitForPaints();
+
+ assert_animation_is_running_on_compositor(animation,
+ 'Animation is running on the compositor');
+
+}, 'Multiple transform-like properties (include motion-path) animation runs ' +
+ 'on the compositor');
+
+promise_test(async t => {
+ const div = addDiv(t);
+ const animation = div.animate({ translate: ['0px', '100px'],
+ rotate: ['0deg', '45deg'],
+ transform: ['translate(20px)',
+ 'translate(30px)'],
+ offsetDistance: ['0%', '100%'] },
+ 100 * MS_PER_SEC);
+
+ div.style.setProperty('translate', '50px', 'important');
+ getComputedStyle(div).translate;
+
+ await waitForAnimationReadyToRestyle(animation);
+ await waitForPaints();
+
+ assert_animation_is_not_running_on_compositor(animation,
+ 'Animation overridden by an !important rule reports that it is ' +
+ 'NOT running on the compositor');
+
+ const properties = animation.effect.getProperties();
+ properties.forEach(property => {
+ assert_true(!property.runningOnCompositor,
+ property.property + ' is not running on the compositor');
+ });
+}, 'Multiple transform-like properties animation does not runs on the ' +
+ 'compositor because one of the transform-like property is overridden ' +
+ 'by an !important rule');
+
+// FIXME: Bug 1593106: We should still run the animations on the compositor if
+// offset-* doesn't have any effect.
+promise_test(async t => {
+ const div = addDiv(t);
+ const animation = div.animate({ translate: ['0px', '100px'],
+ rotate: ['0deg', '45deg'],
+ transform: ['translate(0px)',
+ 'translate(100px)'],
+ offsetDistance: ['0%', '100%'] },
+ 100 * MS_PER_SEC);
+
+ div.style.setProperty('offset-distance', '50%', 'important');
+ getComputedStyle(div).offsetDistance;
+
+ await waitForAnimationReadyToRestyle(animation);
+ await waitForPaints();
+
+ assert_animation_is_not_running_on_compositor(animation,
+ 'Animation overridden by an !important rule reports that it is ' +
+ 'NOT running on the compositor');
+
+ const properties = animation.effect.getProperties();
+ properties.forEach(property => {
+ assert_true(!property.runningOnCompositor,
+ property.property + ' is not running on the compositor');
+ });
+}, 'Multiple transform-like properties animation does not runs on the ' +
+ 'compositor because one of the offset-* property is overridden ' +
+ 'by an !important rule');
+
+promise_test(async t => {
+ const div = addDiv(t);
+ const animation = div.animate({ rotate: ['0deg', '45deg'],
+ transform: ['translate(20px)',
+ 'translate(30px)'],
+ offsetDistance: ['0%', '100%'] },
+ 100 * MS_PER_SEC);
+
+ div.style.setProperty('translate', '50px', 'important');
+ getComputedStyle(div).translate;
+
+ await waitForAnimationReadyToRestyle(animation);
+ await waitForPaints();
+
+ assert_animation_is_running_on_compositor(animation,
+ 'Animation is still running on the compositor');
+
+ const properties = animation.effect.getProperties();
+ properties.forEach(property => {
+ assert_true(property.runningOnCompositor,
+ property.property + ' is running on the compositor');
+ });
+}, 'Multiple transform-like properties animation still runs on the ' +
+ 'compositor because the overridden-by-!important property does not have ' +
+ 'animation');
+
+promise_test(async t => {
+ // We should run the animations on the compositor for this case:
+ // 1. A transition of 'translate'
+ // 2. An !important rule on 'translate'
+ // 3. An animation of 'scale'
+ const div = addDiv(t, { style: 'translate: 100px !important;' });
+ const animation = div.animate({ rotate: ['0deg', '45deg'] },
+ 100 * MS_PER_SEC);
+ div.style.transition = 'translate 100s';
+ getComputedStyle(div).transition;
+
+ await waitForAnimationReadyToRestyle(animation);
+ await waitForPaints();
+
+ assert_animation_is_running_on_compositor(animation,
+ 'rotate animation should be running on the compositor');
+
+ div.style.setProperty('translate', '200px', 'important');
+ getComputedStyle(div).translate;
+
+ const anims = div.getAnimations();
+ await waitForPaints();
+
+ assert_animation_is_running_on_compositor(anims[0],
+ `${anims[0].effect.getProperties()[0].property} animation should be ` +
+ `running on the compositor`);
+ assert_animation_is_running_on_compositor(anims[1],
+ `${anims[1].effect.getProperties()[0].property} animation should be ` +
+ `running on the compositor`);
+}, 'Transform-like animations and transitions still runs on the compositor ' +
+ 'because the !important rule is overridden by a transition, and the ' +
+ 'transition property does not have animations');
+
+promise_test(async t => {
+ const container = addDiv(t, { style: 'transform-style: preserve-3d;' });
+ const targetA = addDiv(t, { style: 'transform-style: preserve-3d' });
+ const targetB = addDiv(t, { style: 'transform-style: preserve-3d' });
+ const targetC = addDiv(t);
+ container.appendChild(targetA);
+ targetA.append(targetB);
+ targetB.append(targetC);
+
+ const animation1 = targetA.animate({ rotate: ['0 0 1 0deg', '1 1 1 45deg'] },
+ 100 * MS_PER_SEC);
+
+ const animation2 = targetC.animate({ rotate: ['0 0 1 0deg', '0 1 1 100deg'] },
+ 100 * MS_PER_SEC);
+
+ await waitForAnimationReadyToRestyle(animation1);
+ await waitForAnimationReadyToRestyle(animation2);
+ await waitForPaints();
+
+ assert_animation_is_running_on_compositor(animation1,
+ 'rotate animation in the 3d rendering context should be running on the ' +
+ 'compositor');
+ assert_animation_is_running_on_compositor(animation2,
+ 'rotate animation in the 3d rendering context should be running on the ' +
+ 'compositor');
+}, 'Transform-like animations in the 3d rendering context should runs on the ' +
+ 'compositor');
+
+promise_test(async t => {
+ const container = addDiv(t, { style: 'transform-style: preserve-3d;' });
+ const target = addDiv(t, { style: 'transform-style: preserve-3d;' });
+ const innerA = addDiv(t, { style: 'width: 50px; height: 50px;' });
+ // The frame of innerB is too large, so this makes its ancenstors and children
+ // in the 3d context be not allowed the async animations.
+ const innerB = addDiv(t, { style: 'rotate: 0 1 1 100deg; ' +
+ 'transform-style: preserve-3d; ' +
+ 'text-indent: -9999em' });
+ const innerB2 = addDiv(t, { style: 'rotate: 0 1 1 45deg;' });
+ const innerBText = document.createTextNode("innerB");
+ container.appendChild(target);
+ target.appendChild(innerA);
+ target.appendChild(innerB);
+ innerB.appendChild(innerBText);
+ innerB.appendChild(innerB2);
+
+ const animation1 = target.animate({ rotate: ['0 0 1 0deg', '1 1 1 45deg'] },
+ 100 * MS_PER_SEC);
+
+ const animation2 = innerA.animate({ rotate: ['0 0 1 0deg', '0 1 1 100deg'] },
+ 100 * MS_PER_SEC);
+
+ const animation3 = innerB2.animate({ rotate: ['0 0 1 0deg', '0 1 1 90deg'] },
+ 100 * MS_PER_SEC);
+
+ await waitForAnimationReadyToRestyle(animation1);
+ await waitForAnimationReadyToRestyle(animation2);
+ await waitForAnimationReadyToRestyle(animation3);
+ await waitForPaints();
+
+ const isPartialPrerenderEnabled =
+ SpecialPowers.getBoolPref('layout.animation.prerender.partial');
+
+ if (isPartialPrerenderEnabled) {
+ assert_animation_is_running_on_compositor(animation1,
+ 'rotate animation in the 3d rendering context should be running on ' +
+ 'the compositor even if one of its inner frames is too large');
+ assert_animation_is_running_on_compositor(animation2,
+ 'rotate animation in the 3d rendering context is still running on ' +
+ 'the compositor because its display item is created earlier');
+ assert_animation_is_running_on_compositor(animation3,
+ 'rotate animation in the 3d rendering context should be running on ' +
+ 'the compositor even if one of its parent frames is too large');
+ } else {
+ assert_animation_is_not_running_on_compositor(animation1,
+ 'rotate animation in the 3d rendering context should not be running on ' +
+ 'the compositor because one of its inner frames is too large');
+ assert_animation_is_running_on_compositor(animation2,
+ 'rotate animation in the 3d rendering context is still running on ' +
+ 'the compositor because its display item is created earlier');
+ assert_animation_is_not_running_on_compositor(animation3,
+ 'rotate animation in the 3d rendering context should not be running on ' +
+ 'the compositor because one of its parent frames is too large');
+ }
+ innerBText.remove();
+}, 'Transform-like animations in the 3d rendering context should run on ' +
+ 'the compositor even if it is the ancestor of ones whose frames are too ' +
+ 'large or its ancestor is not allowed to run on the compositor');
+
+promise_test(async t => {
+ const container = addDiv(t, { style: 'transform-style: preserve-3d;' });
+ const target = addDiv(t, { style: 'transform-style: preserve-3d;' });
+ // The frame of innerA is too large, so this makes its ancenstors and children
+ // in the 3d context be not allowed the async animations.
+ const innerA = addDiv(t, { style: 'transform-style: preserve-3d; ' +
+ 'text-indent: -9999em' });
+ const innerAText = document.createTextNode("innerA");
+ const innerB = addDiv(t, { style: 'width: 50px; height: 50px;' });
+ container.appendChild(target);
+ target.appendChild(innerA);
+ target.appendChild(innerB);
+ innerA.appendChild(innerAText);
+
+ const animation1 = target.animate({ rotate: ['0 0 1 0deg', '1 1 1 45deg'] },
+ 100 * MS_PER_SEC);
+
+ const animation2 = innerA.animate({ rotate: ['0 0 1 0deg', '0 1 1 100deg'] },
+ 100 * MS_PER_SEC);
+
+ const animation3 = innerB.animate({ rotate: ['0 0 1 0deg', '0 1 1 90deg'] },
+ 100 * MS_PER_SEC);
+
+ await waitForAnimationReadyToRestyle(animation1);
+ await waitForAnimationReadyToRestyle(animation2);
+ await waitForAnimationReadyToRestyle(animation3);
+ await waitForPaints();
+
+ const isPartialPrerenderEnabled =
+ SpecialPowers.getBoolPref('layout.animation.prerender.partial');
+
+ if (isPartialPrerenderEnabled) {
+ assert_animation_is_running_on_compositor(animation1,
+ 'rotate animation in the 3d rendering context should be running on ' +
+ 'the compositor even if one of its inner frames is too large');
+ assert_animation_is_running_on_compositor(animation2,
+ 'rotate animation in the 3d rendering context should be running on ' +
+ 'the compositor even if its frame size is too large');
+ assert_animation_is_running_on_compositor(animation3,
+ 'rotate animation in the 3d rendering context should be running on ' +
+ 'the compositor even if its previous sibling frame is too large');
+ } else {
+ assert_animation_is_not_running_on_compositor(animation1,
+ 'rotate animation in the 3d rendering context should not be running on ' +
+ 'the compositor because one of its inner frames is too large');
+ assert_animation_is_not_running_on_compositor(animation2,
+ 'rotate animation in the 3d rendering context should not be running on ' +
+ 'the compositor because its frame size is too large');
+ assert_animation_is_not_running_on_compositor(animation3,
+ 'rotate animation in the 3d rendering context should not be running on ' +
+ 'the compositor because its previous sibling frame is too large');
+ }
+ innerAText.remove();
+}, 'Transform-like animations in the 3d rendering context should run on ' +
+ 'the compositor even if its previous sibling frame size is too large');
+
+</script>
+</body>
diff --git a/dom/animation/test/crashtests/1134538.html b/dom/animation/test/crashtests/1134538.html
new file mode 100644
index 0000000000..136d63deee
--- /dev/null
+++ b/dom/animation/test/crashtests/1134538.html
@@ -0,0 +1,8 @@
+<!doctype html>
+<div contenteditable=true style="transition-property: width;"></div>
+<style>
+html {
+ transition-delay: 18446744073709551584s;
+ transform: rotate(0deg);
+}
+</style>
diff --git a/dom/animation/test/crashtests/1216842-1.html b/dom/animation/test/crashtests/1216842-1.html
new file mode 100644
index 0000000000..6ac40b2fb8
--- /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 KeyframeEffect(
+ 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..7bae8a3116
--- /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 KeyframeEffect(
+ 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..1bc2179a86
--- /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 KeyframeEffect(
+ 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..eba13c396a
--- /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 KeyframeEffect(
+ 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..73b6f22c4b
--- /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 KeyframeEffect(
+ 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..aaf80eeec3
--- /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 KeyframeEffect(
+ 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..aa10ff3ab8
--- /dev/null
+++ b/dom/animation/test/crashtests/1239889-1.html
@@ -0,0 +1,16 @@
+<!doctype html>
+<html class="reftest-wait">
+ <head>
+ <title>Bug 1239889</title>
+ </head>
+ <body>
+ </body>
+ <script>
+ var div = document.createElement('div');
+ var effect = new KeyframeEffect(div, { opacity: [0, 1] });
+ requestAnimationFrame(() => {
+ document.body.appendChild(div);
+ document.documentElement.classList.remove("reftest-wait");
+ });
+ </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..2565aa6eb8
--- /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..71b6c24148
--- /dev/null
+++ b/dom/animation/test/crashtests/1277272-1.html
@@ -0,0 +1,25 @@
+<!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/1282691-1.html b/dom/animation/test/crashtests/1282691-1.html
new file mode 100644
index 0000000000..ab6e1a26c0
--- /dev/null
+++ b/dom/animation/test/crashtests/1282691-1.html
@@ -0,0 +1,23 @@
+<!DOCTYPE html>
+<html class=reftest-wait>
+<meta charset=utf-8>
+<script>
+
+function boom() {
+ const div = document.createElement('div');
+ const anim = div.animate([{}], {});
+ document.documentElement.appendChild(div);
+ anim.pause();
+ document.documentElement.removeChild(div);
+
+ requestAnimationFrame(() => {
+ document.documentElement.appendChild(div);
+ anim.play();
+ document.documentElement.className = '';
+ });
+}
+
+</script>
+</head>
+<body onload="boom();"></body>
+</html>
diff --git a/dom/animation/test/crashtests/1291413-1.html b/dom/animation/test/crashtests/1291413-1.html
new file mode 100644
index 0000000000..691a746c6e
--- /dev/null
+++ b/dom/animation/test/crashtests/1291413-1.html
@@ -0,0 +1,20 @@
+<!doctype html>
+<html class=reftest-wait>
+<script>
+window.onload = () => {
+ const div = document.createElement('div');
+
+ document.documentElement.appendChild(div);
+ const anim = div.animate([], 1000);
+
+ anim.ready.then(() => {
+ anim.pause();
+ anim.reverse();
+ anim.playbackRate = 0;
+ anim.ready.then(() => {
+ document.documentElement.className = '';
+ });
+ });
+};
+</script>
+</html>
diff --git a/dom/animation/test/crashtests/1291413-2.html b/dom/animation/test/crashtests/1291413-2.html
new file mode 100644
index 0000000000..fca3a93800
--- /dev/null
+++ b/dom/animation/test/crashtests/1291413-2.html
@@ -0,0 +1,21 @@
+<!doctype html>
+<html class=reftest-wait>
+<script>
+window.onload = () => {
+ const div = document.createElement('div');
+
+ document.documentElement.appendChild(div);
+ const anim = div.animate([], 1000);
+
+ anim.ready.then(() => {
+ anim.pause();
+ anim.reverse();
+ anim.playbackRate = 0;
+ anim.playbackRate = 1;
+ anim.ready.then(() => {
+ document.documentElement.className = '';
+ });
+ });
+};
+</script>
+</html>
diff --git a/dom/animation/test/crashtests/1304886-1.html b/dom/animation/test/crashtests/1304886-1.html
new file mode 100644
index 0000000000..703ef902b9
--- /dev/null
+++ b/dom/animation/test/crashtests/1304886-1.html
@@ -0,0 +1,14 @@
+<!DOCTYPE html>
+<html>
+<script>
+window.onload=function(){
+ var e = document.createElement("div");
+ document.documentElement.appendChild(e);
+ e.animate([{"font":"status-bar"},
+ {"font":"unset"}],
+ {duration:6,
+ iterationStart:4,
+ iterationComposite:"accumulate"});
+};
+</script>
+</html>
diff --git a/dom/animation/test/crashtests/1309198-1.html b/dom/animation/test/crashtests/1309198-1.html
new file mode 100644
index 0000000000..7fad5782c0
--- /dev/null
+++ b/dom/animation/test/crashtests/1309198-1.html
@@ -0,0 +1,40 @@
+<script>
+function start() {
+ o53=document.createElement('frameset');
+ o254=document.createElement('iframe');
+ o280=document.createElement('audio');
+ o317=document.documentElement;
+ o508=document.createElement('li');
+ o508.appendChild(o317);
+ o590=document.createElement('li');
+ o594=document.createElement('track');
+ o280.appendChild(o594);
+ o647=document.createElement('ol');
+ o654=document.createElement('li');
+ o647.appendChild(o654);
+ o654.insertAdjacentHTML('beforebegin','<iframe>');
+ document.write('<html><body></body></html>');
+ o955=document.documentElement;
+ document.documentElement.appendChild(o647);
+ o590.appendChild(o955);
+ document.close();
+ document.write('<html><body></body></html>');
+ document.documentElement.appendChild(o590);
+ document.documentElement.appendChild(o254);
+ o280.controls^=1;
+ SpecialPowers.forceGC();
+ o317.insertAdjacentHTML('afterend','<iframe>');
+ document.documentElement.appendChild(o280);
+ o2695=document.implementation.createHTMLDocument();
+ o2695.body.appendChild(o254);
+ o53.onerror=f0;
+ document.documentElement.appendChild(o508);
+ o2803=frames[1].document;
+ o2803.getAnimations();
+}
+function f0() {
+ o2803.write('<html><body></body></html>');
+ SpecialPowers.forceCC();
+}
+</script>
+<body onload="start()"></body>
diff --git a/dom/animation/test/crashtests/1322291-1.html b/dom/animation/test/crashtests/1322291-1.html
new file mode 100644
index 0000000000..87def99ba8
--- /dev/null
+++ b/dom/animation/test/crashtests/1322291-1.html
@@ -0,0 +1,24 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+<script>
+document.addEventListener("DOMContentLoaded", boom);
+function boom(){
+ let o1 = (function(){
+ let e=document.createElement("frameset");
+ document.documentElement.appendChild(e);
+ return e;
+ })();
+ let a1 = o1.animate({ "transform": "rotate3d(22,73,26,374grad)" },
+ { duration: 10, delay: 100 });
+
+ // We need to wait the end of the delay to ensure that the animation is
+ // composited on the compositor, but there is no event for script animation
+ // that fires after the delay phase finished. So we wait for finished promise
+ // instead.
+ a1.finished.then(function() {
+ document.documentElement.className = "";
+ });
+}
+</script>
+</body>
+</html>
diff --git a/dom/animation/test/crashtests/1322291-2.html b/dom/animation/test/crashtests/1322291-2.html
new file mode 100644
index 0000000000..b93d53224d
--- /dev/null
+++ b/dom/animation/test/crashtests/1322291-2.html
@@ -0,0 +1,31 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+<style>
+div {
+ width: 100px;
+ height: 100px;
+ background-color: red;
+}
+</style>
+<body>
+<div id=o_0></div>
+<script>
+function boom(){
+ var anim = o_0.animate([
+ {},
+ {"transform": "scale(2)"},
+ {"transform": "none"}
+ ], {
+ duration: 100,
+ iterationStart: 0.5,
+ });
+ // We need to wait for finished promise just like we do in 1322291-1.html.
+ anim.finished.then(function() {
+ document.documentElement.classList.remove("reftest-wait");
+ });
+}
+document.addEventListener("DOMContentLoaded", boom);
+</script>
+
+</body>
+</html>
diff --git a/dom/animation/test/crashtests/1322382-1.html b/dom/animation/test/crashtests/1322382-1.html
new file mode 100644
index 0000000000..6ca9c1b836
--- /dev/null
+++ b/dom/animation/test/crashtests/1322382-1.html
@@ -0,0 +1,16 @@
+<!DOCTYPE html>
+<style>
+details {
+ background-color: blue;
+ width: 100px;
+ height: 100px;
+}
+</style>
+<html>
+<details id=o1><div></div></details>
+<script>
+window.onload = function(){
+ o1.animate([{'transform': 'none'}], 100);
+};
+</script>
+</html>
diff --git a/dom/animation/test/crashtests/1323114-1.html b/dom/animation/test/crashtests/1323114-1.html
new file mode 100644
index 0000000000..344fd87db0
--- /dev/null
+++ b/dom/animation/test/crashtests/1323114-1.html
@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<html>
+<body>
+<div id=a />
+<script>
+addEventListener("DOMContentLoaded", function(){
+ a.animate([{"transform": "matrix3d(25,8788,-69,-24,-3,85,52,3,63,0,12,36810,-68,15,82,0) rotate(77rad)"}],
+ {fill: "both", iterationStart: 45, iterationComposite: "accumulate"});
+});
+</script>
+</body>
+</html>
diff --git a/dom/animation/test/crashtests/1323114-2.html b/dom/animation/test/crashtests/1323114-2.html
new file mode 100644
index 0000000000..527d05effa
--- /dev/null
+++ b/dom/animation/test/crashtests/1323114-2.html
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<style>
+div {
+ width: 100px;
+ height: 100px;
+ background-color: blue;
+ transform: rotate(45deg);
+}
+</style>
+<div id="div"></div>
+<script>
+addEventListener('DOMContentLoaded', function (){
+ var target = document.getElementById('div');
+ target.animate([{ transform: 'translateX(100px)', composite: 'accumulate' },
+ { transform: 'none' }],
+ 1000);
+});
+</script>
diff --git a/dom/animation/test/crashtests/1323119-1.html b/dom/animation/test/crashtests/1323119-1.html
new file mode 100644
index 0000000000..fd979822b8
--- /dev/null
+++ b/dom/animation/test/crashtests/1323119-1.html
@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+<body>
+<script>
+addEventListener("DOMContentLoaded", function() {
+ let a = document.createElement("th");
+ document.documentElement.appendChild(a);
+ a.animate([{"mask": "repeat-y "}], 484);
+ document.documentElement.classList.remove("reftest-wait");
+});
+</script>
+</body>
+</html>
diff --git a/dom/animation/test/crashtests/1324554-1.html b/dom/animation/test/crashtests/1324554-1.html
new file mode 100644
index 0000000000..b3f9435a18
--- /dev/null
+++ b/dom/animation/test/crashtests/1324554-1.html
@@ -0,0 +1,19 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="UTF-8">
+ <title>Bug 1324554 - missing final keyframes and zero-length segments together</title>
+ </head>
+ <script>
+ function go() {
+ var div = document.getElementById('target');
+ div.animate([ { "flex": "none" },
+ { "flex": "initial", offset: 0.5 },
+ { "flex": "0.0 ", offset: 0.5 },
+ {} ]);
+ }
+ </script>
+ <body onload="go()">
+ <div id='target' ></div>
+ </body>
+</html>
diff --git a/dom/animation/test/crashtests/1325193-1.html b/dom/animation/test/crashtests/1325193-1.html
new file mode 100644
index 0000000000..bd0666497c
--- /dev/null
+++ b/dom/animation/test/crashtests/1325193-1.html
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+<head>
+<meta charset="UTF-8">
+<script>
+function boom(){
+ o_0.animate([{"perspective": "262ex"}, {}], {duration: 1070, iterationStart: 68, iterationComposite: "accumulate"});
+ o_0.animate([{"color": "white", "flex": "inherit"}, {"color": "red", "flex": "66% "}], 3439);
+ o_0.animate([{"font": "icon"}], 1849);
+ setTimeout(function() {
+ document.documentElement.classList.remove("reftest-wait");
+ }, 500);
+}
+document.addEventListener("DOMContentLoaded", boom);
+</script>
+</head>
+<body><details id=o_0><q></q></details></body>
+</html>
diff --git a/dom/animation/test/crashtests/1330190-1.html b/dom/animation/test/crashtests/1330190-1.html
new file mode 100644
index 0000000000..fa14e0f741
--- /dev/null
+++ b/dom/animation/test/crashtests/1330190-1.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+<span id=a />
+<script>
+addEventListener("DOMContentLoaded", function(){
+ a.animate([{"left": "38%"}], 100);
+ a.appendChild(document.createElement("div"));
+ document.documentElement.classList.remove("reftest-wait");
+});
+</script>
+</html>
diff --git a/dom/animation/test/crashtests/1330190-2.html b/dom/animation/test/crashtests/1330190-2.html
new file mode 100644
index 0000000000..57e5d31b28
--- /dev/null
+++ b/dom/animation/test/crashtests/1330190-2.html
@@ -0,0 +1,36 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+<head>
+<style>
+@keyframes anim {
+}
+
+#o_0:before {
+ animation: anim 10s;
+ content: "";
+}
+</style>
+<meta charset="UTF-8">
+<script>
+function boom(){
+ function getPseudoElement() {
+ var anim = document.getAnimations()[0];
+ anim.cancel();
+ return anim.effect.target;
+ }
+
+ var target = getPseudoElement();
+ target.animate([{"perspective": "262ex"}, {}], {duration: 1070, iterationStart: 68, iterationComposite: "accumulate"});
+ target.animate([{"color": "white", "flex": "inherit"}, {"color": "red", "flex": "66% "}], 3439);
+ target.animate([{"font": "icon"}], 1849);
+ setTimeout(function() {
+ document.documentElement.classList.remove("reftest-wait");
+ }, 500);
+}
+document.addEventListener("DOMContentLoaded", boom);
+</script>
+</head>
+<body>
+<div id=o_0></div>
+</body>
+</html>
diff --git a/dom/animation/test/crashtests/1330513-1.html b/dom/animation/test/crashtests/1330513-1.html
new file mode 100644
index 0000000000..a497cc9e27
--- /dev/null
+++ b/dom/animation/test/crashtests/1330513-1.html
@@ -0,0 +1,8 @@
+<!DOCTYPE html>
+<html>
+<body id=a></body>
+<script>
+document.getElementById("a")
+ .animate([{"filter": "grayscale(28%)"}], {fill:"forwards", composite:"add"});
+</script>
+</html>
diff --git a/dom/animation/test/crashtests/1332588-1.html b/dom/animation/test/crashtests/1332588-1.html
new file mode 100644
index 0000000000..11719a86ca
--- /dev/null
+++ b/dom/animation/test/crashtests/1332588-1.html
@@ -0,0 +1,25 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+<head>
+<meta charset="UTF-8">
+<style>
+span {
+ width: 100px;
+ height: 100px;
+ background-color: red;
+}
+</style>
+<script>
+window.onload = function() {
+ let body = document.getElementsByTagName("body")[0];
+ let o_0 = document.createElement("span");
+ body.appendChild(o_0);
+ o_0.animate([{ "padding": "57pt", "transform": "none" },
+ { "padding": "57pt", "transform": "rotate(90deg)" }] , 10000);
+ body.appendChild(document.createElement("colgroup"));
+ document.documentElement.classList.remove("reftest-wait");
+};
+</script>
+</head>
+<body></body>
+</html>
diff --git a/dom/animation/test/crashtests/1333539-1.html b/dom/animation/test/crashtests/1333539-1.html
new file mode 100644
index 0000000000..c9111890b0
--- /dev/null
+++ b/dom/animation/test/crashtests/1333539-1.html
@@ -0,0 +1,30 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+<head>
+<meta charset="UTF-8">
+<script>
+window.onload = function(){
+ let body = document.getElementsByTagName("body")[0];
+ let target = document.createElement("div");
+ let anim1 = new Animation();
+ let anim2 = new Animation();
+ let effect = new KeyframeEffect(target, { opacity: [ 0, 1 ] }, 1000);
+ body.appendChild(target);
+ target.appendChild(document.createElement("meter"));
+ anim1.startTime = 88;
+ anim1.timeline = null;
+ anim1.pause();
+ anim1.effect = effect;
+ anim2.effect = effect;
+ anim1.effect = effect;
+
+ // anim1, since it doesn't have a timeline, will remain pause-pending,
+ // so just wait on anim2.
+ anim2.ready.then(() => {
+ document.documentElement.classList.remove("reftest-wait");
+ });
+};
+</script>
+</head>
+<body></body>
+</html>
diff --git a/dom/animation/test/crashtests/1333539-2.html b/dom/animation/test/crashtests/1333539-2.html
new file mode 100644
index 0000000000..b00700eccb
--- /dev/null
+++ b/dom/animation/test/crashtests/1333539-2.html
@@ -0,0 +1,38 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="UTF-8">
+<style>
+div {
+ width: 100px;
+ height: 100px;
+ background-color: blue;
+}
+</style>
+<script>
+window.onload = function(){
+ let body = document.getElementsByTagName("body")[0];
+ let target = document.createElement("div");
+ let anim1 = new Animation();
+ let anim2 = new Animation();
+ let effect = new KeyframeEffect(target, { opacity: [ 0, 1 ] }, 1000);
+ body.appendChild(target);
+ anim1.startTime = 88;
+ anim1.timeline = null;
+ anim1.pause();
+ anim1.effect = effect;
+ anim2.effect = effect;
+ anim1.effect = effect;
+ // Put another opacity animation on the top of the effect stack so that we
+ // try to send a lower priority animation that has no timeline to the
+ // compositor.
+ let anim3 = target.animate({ opacity : [ 1, 0 ] }, 1000);
+
+ Promise.all([anim1.ready, anim2.ready, anim2.ready]).then(function() {
+ document.documentElement.classList.remove("reftest-wait");
+ });
+};
+</script>
+</head>
+<body></body>
+</html>
diff --git a/dom/animation/test/crashtests/1334582-1.html b/dom/animation/test/crashtests/1334582-1.html
new file mode 100644
index 0000000000..d67ddc3c52
--- /dev/null
+++ b/dom/animation/test/crashtests/1334582-1.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="UTF-8">
+<script>
+window.onload = function(){
+ let a = document.documentElement.animate([], {"iterations": 1.7976931348623157e+308, "fill": "both"});
+};
+</script>
+</head>
+</html>
diff --git a/dom/animation/test/crashtests/1334582-2.html b/dom/animation/test/crashtests/1334582-2.html
new file mode 100644
index 0000000000..d3b223650d
--- /dev/null
+++ b/dom/animation/test/crashtests/1334582-2.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="UTF-8">
+<script>
+window.onload = function(){
+ let a = document.documentElement.animate([], {"iterationStart": 1.7976931348623157e+308, "fill": "both"});
+};
+</script>
+</head>
+</html>
diff --git a/dom/animation/test/crashtests/1334583-1.html b/dom/animation/test/crashtests/1334583-1.html
new file mode 100644
index 0000000000..b4d4109f0d
--- /dev/null
+++ b/dom/animation/test/crashtests/1334583-1.html
@@ -0,0 +1,9 @@
+<div style="width: 200px; height: 200px; background: purple" id=div>
+</div>
+<script>
+const animation = div.animate(
+ [ { transform: "scale(1)" },
+ { transform: "scale(2)" } ],
+ { iterations: Infinity, duration: 512 } );
+animation.currentTime = 2147483647;
+</script>
diff --git a/dom/animation/test/crashtests/1335998-1.html b/dom/animation/test/crashtests/1335998-1.html
new file mode 100644
index 0000000000..72353a9692
--- /dev/null
+++ b/dom/animation/test/crashtests/1335998-1.html
@@ -0,0 +1,28 @@
+<!doctype html>
+<html class="reftest-wait">
+ <head>
+ <title>
+ Bug 1335998 - Handle {Interpolate, Accumulate}Matrix of mismatched transform lists
+ </title>
+ <style>
+ #target {
+ width: 100px; height: 100px;
+ background: blue;
+ transform: rotate(45deg);
+ }
+ </style>
+ </head>
+ <body>
+ <div id="target"></div>
+ </body>
+ <script>
+ var div = document.getElementById("target");
+ var animation = div.animate([ { transform: 'translateX(200px) scale(2.0)',
+ composite: 'accumulate' },
+ { transform: 'rotate(-45deg)' } ],
+ 2000);
+ animation.finished.then(function() {
+ document.documentElement.className = "";
+ });
+ </script>
+</html>
diff --git a/dom/animation/test/crashtests/1343589-1.html b/dom/animation/test/crashtests/1343589-1.html
new file mode 100644
index 0000000000..a494b83da3
--- /dev/null
+++ b/dom/animation/test/crashtests/1343589-1.html
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+<head>
+<meta charset="UTF-8">
+<script>
+window.onload = function(){
+ let a = document.documentElement.animate(null,
+ { duration: 100, iterations: Number.POSITIVE_INFINITY });
+ a.startTime = 100000; // Set the start time far in the future
+ // Try reversing (this should throw because the target effect end is infinity)
+ try { a.reverse(); } catch(e) {}
+ // Do something that will trigger a timing update
+ a.effect.target = document.createElement("span");
+ document.documentElement.className = '';
+};
+</script>
+</head>
+</html>
diff --git a/dom/animation/test/crashtests/1359658-1.html b/dom/animation/test/crashtests/1359658-1.html
new file mode 100644
index 0000000000..972ec497fa
--- /dev/null
+++ b/dom/animation/test/crashtests/1359658-1.html
@@ -0,0 +1,33 @@
+<!doctype html>
+<html class="reftest-wait">
+ <head>
+ <meta charset=utf-8>
+ <title>Bug 1359658: Animation-only dirty descendants bit should be cleared
+ for display:none content</title>
+ </head>
+ <body>
+ <div id="ancestor">
+ <svg>
+ <rect id="target" width="100%" height="100%" fill="lime"/>
+ </svg>
+ </div>
+ </body>
+ <script>
+'use strict';
+
+const ancestor = document.getElementById('ancestor');
+const target = document.getElementById('target');
+
+document.addEventListener('DOMContentLoaded', () => {
+ const animation = target.animate({ color: [ 'red', 'lime' ] },
+ { duration: 1000, iterations: Infinity });
+ requestAnimationFrame(() => {
+ // Tweak animation to cause animation dirty bit to be set
+ animation.effect.updateTiming({ duration: 2000 });
+ ancestor.style.display = "none";
+ getComputedStyle(ancestor).display;
+ document.documentElement.className = '';
+ });
+});
+ </script>
+</html>
diff --git a/dom/animation/test/crashtests/1373712-1.html b/dom/animation/test/crashtests/1373712-1.html
new file mode 100644
index 0000000000..8b5c121c85
--- /dev/null
+++ b/dom/animation/test/crashtests/1373712-1.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>Bug 1373712 - Assertions of SpecifiedKeyframeArraysAreEqual(mKeyframes, keyframesCopy) with large color value
+</title>
+<meta charset="UTF-8">
+<script>
+document.documentElement.animate([{ "color": "hsl(63e292,41%,34%)" }]);
+</script>
+</head>
+</html>
diff --git a/dom/animation/test/crashtests/1379606-1.html b/dom/animation/test/crashtests/1379606-1.html
new file mode 100644
index 0000000000..89f756bf06
--- /dev/null
+++ b/dom/animation/test/crashtests/1379606-1.html
@@ -0,0 +1,21 @@
+<!doctype html>
+<style>
+div {
+ display: none;
+}
+</style>
+<div>
+<div>
+<div>
+<div>
+<div>
+<div id="target">
+</div>
+</div>
+</div>
+</div>
+</div>
+</div>
+<script>
+ target.animate({ color: "red" })
+</script>
diff --git a/dom/animation/test/crashtests/1393605-1.html b/dom/animation/test/crashtests/1393605-1.html
new file mode 100644
index 0000000000..9f282e58ba
--- /dev/null
+++ b/dom/animation/test/crashtests/1393605-1.html
@@ -0,0 +1,15 @@
+<!doctype html>
+<html>
+ <head>
+ <title>
+ Bug 1393605 - Still work if determinant of 2d matrix is not 1 or -1
+ </title>
+ </head>
+ <script>
+ document.documentElement.animate(
+ [ { 'transform': 'scale(4)' },
+ { 'transform': 'rotate(3grad) scaleX(0) ' +
+ 'translate(2mm) matrix(2,7,1,.32,7,0)' } ],
+ { fill: 'both' });
+ </script>
+</html>
diff --git a/dom/animation/test/crashtests/1400022-1.html b/dom/animation/test/crashtests/1400022-1.html
new file mode 100644
index 0000000000..8256091e1e
--- /dev/null
+++ b/dom/animation/test/crashtests/1400022-1.html
@@ -0,0 +1,10 @@
+<script>
+requestIdleCallback(function(){ location.reload() })
+a = document.createElement("x")
+document.documentElement.appendChild(a)
+b = document.createElement('link')
+b.setAttribute('rel', 'stylesheet')
+b.setAttribute('href', 'data:,*{border-block-start:solid}')
+document.head.appendChild(b)
+a.insertAdjacentHTML("afterBegin", "<d id='id0' style='transition-duration:1s'><svg filter='url(#id0)'>")
+</script>
diff --git a/dom/animation/test/crashtests/1401809.html b/dom/animation/test/crashtests/1401809.html
new file mode 100644
index 0000000000..7a3adcc60c
--- /dev/null
+++ b/dom/animation/test/crashtests/1401809.html
@@ -0,0 +1,14 @@
+<html>
+ <head>
+ <style></style>
+ <script>
+ o1 = document.createElement('t');
+ document.documentElement.appendChild(o1);
+ document.styleSheets[0].insertRule('* { will-change:an }', 0);
+ k = new KeyframeEffect(o1, [{'willChange':'s'}], {'':''});
+ k = null;
+ SpecialPowers.forceGC();
+ SpecialPowers.forceCC();
+ </script>
+ </head>
+</html>
diff --git a/dom/animation/test/crashtests/1411318-1.html b/dom/animation/test/crashtests/1411318-1.html
new file mode 100644
index 0000000000..5c8e7211c1
--- /dev/null
+++ b/dom/animation/test/crashtests/1411318-1.html
@@ -0,0 +1,15 @@
+<html>
+ <head>
+ <script>
+ o1 = (new DOMParser).parseFromString('', 'text/html');
+ o2 = document.createElement('canvas');
+ document.documentElement.appendChild(o2);
+ o3 = o2.animate([{'transform':'unset'}], {'delay':32});
+ o4 = o3.effect;
+ o5 = o1.createElement('d');
+ o6 = new Animation(o4, document.timeline);
+ o7 = o5.animate([], {});
+ o7.effect = o6.effect;
+ </script>
+ </head>
+</html>
diff --git a/dom/animation/test/crashtests/1467277-1.html b/dom/animation/test/crashtests/1467277-1.html
new file mode 100644
index 0000000000..c58fc64493
--- /dev/null
+++ b/dom/animation/test/crashtests/1467277-1.html
@@ -0,0 +1,6 @@
+<script>
+addEventListener("DOMContentLoaded", () => {
+ document.documentElement.animate(
+ [ { "transform": "rotate3d(1e58, 2, 6, 0turn)" } ], 1000)
+})
+</script>
diff --git a/dom/animation/test/crashtests/1468294-1.html b/dom/animation/test/crashtests/1468294-1.html
new file mode 100644
index 0000000000..e4092046ac
--- /dev/null
+++ b/dom/animation/test/crashtests/1468294-1.html
@@ -0,0 +1,7 @@
+<script>
+addEventListener("DOMContentLoaded", () => {
+ document.documentElement.animate([{ "transform": "matrix(2,1,1,5,2,8)" }],
+ { duration: 1000,
+ easing: "cubic-bezier(1,-15,.6,4)" });
+})
+</script>
diff --git a/dom/animation/test/crashtests/1524480-1.html b/dom/animation/test/crashtests/1524480-1.html
new file mode 100644
index 0000000000..89e5a412d9
--- /dev/null
+++ b/dom/animation/test/crashtests/1524480-1.html
@@ -0,0 +1,37 @@
+<!doctype html>
+<html class="reftest-wait">
+<meta charset=utf-8>
+<style>
+div {
+ display: none;
+ width: 100px;
+ height: 100px;
+ background: blue;
+}
+</style>
+<div id=div></div>
+<script>
+async function test() {
+ const animation = div.animate({ transform: ['none', 'none'] }, 1000);
+ animation.cancel();
+
+ await waitForFrame();
+
+ div.style.display = 'block';
+
+ await waitForFrame();
+ await waitForFrame();
+
+ animation.play();
+ await animation.finished;
+
+ document.documentElement.className = "";
+}
+
+function waitForFrame() {
+ return new Promise(resolve => requestAnimationFrame(resolve));
+}
+
+test();
+</script>
+</html>
diff --git a/dom/animation/test/crashtests/1575926.html b/dom/animation/test/crashtests/1575926.html
new file mode 100644
index 0000000000..cc37c94235
--- /dev/null
+++ b/dom/animation/test/crashtests/1575926.html
@@ -0,0 +1,24 @@
+<style>
+ @keyframes animation_0 {
+ 88% { }
+ }
+
+ * {
+ animation-name: animation_0;
+ animation-delay: 4s;
+ }
+</style>
+<script>
+ function start () {
+ const input = document.createElement('input')
+ document.documentElement.appendChild(input)
+ const animations = input.getAnimations({})
+ const animation = animations[(3782796448 % animations.length)]
+ const effect = animation.effect
+ effect.setKeyframes({ 'borderLeft': ['inherit'] })
+ effect.target = null
+ input.contentEditable = 'true'
+ }
+
+ document.addEventListener('DOMContentLoaded', start)
+</script>
diff --git a/dom/animation/test/crashtests/1585770.html b/dom/animation/test/crashtests/1585770.html
new file mode 100644
index 0000000000..018d688582
--- /dev/null
+++ b/dom/animation/test/crashtests/1585770.html
@@ -0,0 +1,22 @@
+<html class="reftest-wait">
+<script>
+function start () {
+ const kf_effect =
+ new KeyframeEffect(document.documentElement,
+ { opacity: ['', '1'] },
+ { easing: 'step-end',
+ duration: 10000 } );
+ const copy = new KeyframeEffect(kf_effect);
+ const animation = new Animation(copy);
+
+ animation.reverse();
+ document.documentElement.getBoundingClientRect();
+
+ requestAnimationFrame(() => {
+ document.documentElement.classList.remove("reftest-wait");
+ });
+}
+
+document.addEventListener('DOMContentLoaded', start);
+</script>
+</html>
diff --git a/dom/animation/test/crashtests/1604500-1.html b/dom/animation/test/crashtests/1604500-1.html
new file mode 100644
index 0000000000..01a6eafd1f
--- /dev/null
+++ b/dom/animation/test/crashtests/1604500-1.html
@@ -0,0 +1,24 @@
+<!doctype html>
+<html>
+<head>
+<script>
+function start () {
+ const keyframe = new KeyframeEffect(undefined, {});
+ const animation = new Animation(keyframe, undefined);
+ // Make animation run backwards...
+ animation.playbackRate = -100;
+ // But then set the current time to the future so it becomes "current"...
+ animation.currentTime = 2055;
+ // After updating the playback rate to zero, however, it should no longer
+ // be "current" (and this takes effect immediately because |animation| is
+ // paused)...
+ animation.updatePlaybackRate(0);
+ // Now update the target and hope nothing goes wrong...
+ keyframe.target = div;
+}
+
+document.addEventListener('DOMContentLoaded', start)
+</script>
+</head>
+<div id=div></div>
+</html>
diff --git a/dom/animation/test/crashtests/1611847.html b/dom/animation/test/crashtests/1611847.html
new file mode 100644
index 0000000000..720ce1179b
--- /dev/null
+++ b/dom/animation/test/crashtests/1611847.html
@@ -0,0 +1,23 @@
+<html>
+<head>
+ <style>
+ * {
+ transition-duration: 2s;
+ }
+ </style>
+ <script>
+ function start () {
+ const element = document.createElementNS('', 's');
+ const effect = new KeyframeEffect(document.documentElement, {}, 196);
+ document.documentElement.setAttribute('style', 'padding-left:3');
+ effect.updateTiming({ 'delay': 2723 });
+ const animations = document.getAnimations();
+ animations[0].effect = effect;
+ animations[0].updatePlaybackRate(-129);
+ effect.target = element;
+ }
+
+ document.addEventListener('DOMContentLoaded', start);
+ </script>
+</head>
+</html>
diff --git a/dom/animation/test/crashtests/1612891-1.html b/dom/animation/test/crashtests/1612891-1.html
new file mode 100644
index 0000000000..44cf022e88
--- /dev/null
+++ b/dom/animation/test/crashtests/1612891-1.html
@@ -0,0 +1,15 @@
+<html>
+<head>
+ <script>
+ function start() {
+ const element = document.createElement('img')
+ element.animate([
+ { 'easing': '' },
+ { 'offset': 'o' },
+ ], {})
+ }
+
+ window.addEventListener('load', start)
+ </script>
+</head>
+</html>
diff --git a/dom/animation/test/crashtests/1612891-2.html b/dom/animation/test/crashtests/1612891-2.html
new file mode 100644
index 0000000000..a9779ba70f
--- /dev/null
+++ b/dom/animation/test/crashtests/1612891-2.html
@@ -0,0 +1,15 @@
+<html>
+<head>
+ <script>
+ function start() {
+ const element = document.createElement('img')
+ element.animate([
+ { 'easing': '' },
+ 123,
+ ], {})
+ }
+
+ window.addEventListener('load', start)
+ </script>
+</head>
+</html>
diff --git a/dom/animation/test/crashtests/1612891-3.html b/dom/animation/test/crashtests/1612891-3.html
new file mode 100644
index 0000000000..89e71b6ca8
--- /dev/null
+++ b/dom/animation/test/crashtests/1612891-3.html
@@ -0,0 +1,10 @@
+<html>
+<head>
+ <script>
+ document.addEventListener('DOMContentLoaded', () => {
+ const keyframe = new KeyframeEffect(document.documentElement, [{}], {})
+ keyframe.setKeyframes([{ 'easing': '' }, { 'offset': 'o', }])
+ })
+ </script>
+</head>
+</html>
diff --git a/dom/animation/test/crashtests/1633442.html b/dom/animation/test/crashtests/1633442.html
new file mode 100644
index 0000000000..cb4beedebc
--- /dev/null
+++ b/dom/animation/test/crashtests/1633442.html
@@ -0,0 +1,15 @@
+<!doctype html>
+<html class="reftest-wait">
+<head>
+<script>
+ document.addEventListener('DOMContentLoaded', () => {
+ document.documentElement.style.setProperty('transition-duration', '3s', '')
+ document.documentElement.style.setProperty('rotate', '2deg', undefined)
+ document.documentElement.style.setProperty('border-radius', '2%', '')
+ const [anim_1, anim_0] = document.documentElement.getAnimations({})
+ anim_1.effect = anim_0.effect
+ document.documentElement.classList.remove("reftest-wait");
+ })
+</script>
+</head>
+</html>
diff --git a/dom/animation/test/crashtests/1633486.html b/dom/animation/test/crashtests/1633486.html
new file mode 100644
index 0000000000..20b88f6327
--- /dev/null
+++ b/dom/animation/test/crashtests/1633486.html
@@ -0,0 +1,20 @@
+<!doctype html>
+<html>
+<head>
+ <script>
+ window.addEventListener('load', async () => {
+ const element = document.getElementById('target');
+ element.animate({
+ 'all': ['initial']
+ }, {
+ 'duration': 500,
+ 'pseudoElement': '::marker',
+ })
+ element.hidden = false;
+ })
+ </script>
+</head>
+<ul>
+ <li id='target' hidden></li>
+</ul>
+</html>
diff --git a/dom/animation/test/crashtests/1656419.html b/dom/animation/test/crashtests/1656419.html
new file mode 100644
index 0000000000..4e76cb0a55
--- /dev/null
+++ b/dom/animation/test/crashtests/1656419.html
@@ -0,0 +1,23 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+<style>
+#target {
+ width: 200vw;
+ height: 200vh;
+}
+</style>
+<div id="target"></div>
+<script>
+const animA = target.animate(
+ { transform: 'translateX(100px)' },
+ { duration: 50 }
+);
+const animB = target.animate(
+ { transform: 'translateX(100px)', composite: 'add' },
+ { duration: 100 }
+);
+animB.finished.then(() => {
+ document.documentElement.classList.remove("reftest-wait");
+});
+</script>
+</html>
diff --git a/dom/animation/test/crashtests/1699890.html b/dom/animation/test/crashtests/1699890.html
new file mode 100644
index 0000000000..95aa1e190c
--- /dev/null
+++ b/dom/animation/test/crashtests/1699890.html
@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<html>
+<style>
+@keyframes anim {
+ from { background-color: rgba(0, 0, 0, 0); }
+ to { background-color: rgba(255, 0, 0, 255); }
+}
+body {
+ animation: anim 100s;
+ width: 100vw;
+ height: 100vh;
+}
+</style>
diff --git a/dom/animation/test/crashtests/1706157.html b/dom/animation/test/crashtests/1706157.html
new file mode 100644
index 0000000000..cfa7b70b56
--- /dev/null
+++ b/dom/animation/test/crashtests/1706157.html
@@ -0,0 +1,19 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <style>
+ @keyframes animation {
+ to {
+ left: 100px;
+ }
+ }
+ * {
+ animation: animation linear 1s;
+ }
+ #target {
+ animation-timing-function: steps(2965566999, jump-both);
+ }
+ </style>
+</head>
+<div id="target"></div>
+</html>
diff --git a/dom/animation/test/crashtests/1714421.html b/dom/animation/test/crashtests/1714421.html
new file mode 100644
index 0000000000..5408d70bc0
--- /dev/null
+++ b/dom/animation/test/crashtests/1714421.html
@@ -0,0 +1,8 @@
+<script>
+document.addEventListener('DOMContentLoaded', () => {
+ document.documentElement.style.setProperty('scale', '89%', undefined)
+ document.documentElement.style.setProperty('transition-duration', '2009216159ms', '')
+ document.getAnimations()[0].timeline = undefined
+ document.documentElement.animate({'scale': ['none', 'none', 'none']}, 1807)
+})
+</script>
diff --git a/dom/animation/test/crashtests/1807966.html b/dom/animation/test/crashtests/1807966.html
new file mode 100644
index 0000000000..d2759d2b5c
--- /dev/null
+++ b/dom/animation/test/crashtests/1807966.html
@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<style>
+ #target {
+ transition-timing-function: linear(calc(15 / 0), 10 10%);
+ transition-duration: 10s;
+ }
+</style>
+<script>
+ window.addEventListener('load', () => {
+ target.style.paddingBlock = "10px 10px";
+ })
+</script>
+<slot id="target"></slot>
diff --git a/dom/animation/test/crashtests/1875441.html b/dom/animation/test/crashtests/1875441.html
new file mode 100644
index 0000000000..d7a4ff911c
--- /dev/null
+++ b/dom/animation/test/crashtests/1875441.html
@@ -0,0 +1,12 @@
+<!doctype html>
+<style>
+@property --my-property {
+ syntax: "<length>",
+ inherits: false,
+ initial-value: 0,
+}
+</style>
+<div id="box"></div>
+<script>
+SpecialPowers.DOMWindowUtils.computeAnimationDistance(box, "--my-property", "red", "blue");
+</script>
diff --git a/dom/animation/test/crashtests/crashtests.list b/dom/animation/test/crashtests/crashtests.list
new file mode 100644
index 0000000000..be9b1c8ae9
--- /dev/null
+++ b/dom/animation/test/crashtests/crashtests.list
@@ -0,0 +1,62 @@
+load 1134538.html
+load 1239889-1.html
+load 1244595-1.html
+pref(dom.animations-api.timelines.enabled,true) load 1216842-1.html
+pref(dom.animations-api.timelines.enabled,true) load 1216842-2.html
+pref(dom.animations-api.timelines.enabled,true) load 1216842-3.html
+pref(dom.animations-api.timelines.enabled,true) load 1216842-4.html
+pref(dom.animations-api.timelines.enabled,true) load 1216842-5.html
+pref(dom.animations-api.timelines.enabled,true) load 1216842-6.html
+load 1272475-1.html
+load 1272475-2.html
+load 1278485-1.html
+pref(dom.animations-api.timelines.enabled,true) load 1277272-1.html
+load 1282691-1.html
+load 1291413-1.html
+load 1291413-2.html
+pref(dom.animations-api.compositing.enabled,true) load 1304886-1.html
+load 1309198-1.html
+load 1322382-1.html
+load 1322291-1.html
+load 1322291-2.html
+pref(dom.animations-api.compositing.enabled,true) load 1323114-1.html
+pref(dom.animations-api.compositing.enabled,true) load 1323114-2.html
+load 1323119-1.html
+load 1324554-1.html
+pref(dom.animations-api.compositing.enabled,true) load 1325193-1.html
+load 1332588-1.html
+load 1330190-1.html
+pref(dom.animations-api.compositing.enabled,true) load 1330190-2.html
+pref(dom.animations-api.compositing.enabled,true) load 1330513-1.html
+pref(dom.animations-api.timelines.enabled,true) load 1333539-1.html
+pref(dom.animations-api.timelines.enabled,true) load 1333539-2.html
+load 1334582-1.html
+load 1334582-2.html
+load 1334583-1.html
+pref(dom.animations-api.compositing.enabled,true) load 1335998-1.html
+load 1343589-1.html
+load 1359658-1.html
+load 1373712-1.html
+load 1379606-1.html
+load 1393605-1.html
+load 1400022-1.html
+load 1401809.html
+pref(dom.animations-api.timelines.enabled,true) load 1411318-1.html
+load 1468294-1.html
+load 1467277-1.html
+load 1524480-1.html
+load 1575926.html
+load 1585770.html
+load 1604500-1.html
+load 1611847.html
+load 1612891-1.html
+load 1612891-2.html
+load 1612891-3.html
+load 1633442.html
+load 1633486.html
+pref(layout.animation.prerender.partial,true) load 1656419.html
+load 1706157.html
+pref(gfx.omta.background-color,true) load 1699890.html
+pref(dom.animations-api.timelines.enabled,true) load 1714421.html
+load 1807966.html
+load 1875441.html
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..3a7d64af2c
--- /dev/null
+++ b/dom/animation/test/document-timeline/test_document-timeline.html
@@ -0,0 +1,147 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>Web Animations API: DocumentTimeline tests</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../testcommon.js"></script>
+<iframe srcdoc='<html><meta charset=utf-8></html>' width="10" height="10" id="iframe"></iframe>
+<iframe srcdoc='<html style="display:none"><meta charset=utf-8></html>' width="10" height="10" id="hidden-iframe"></iframe>
+<div id="log"></div>
+<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) {
+ const { AppConstants } = SpecialPowers.ChromeUtils.importESModule(
+ "resource://gre/modules/AppConstants.sys.mjs"
+ );
+
+ if (AppConstants.platform == "android") {
+ // Skip this test case on Android since it frequently fails on the
+ // environments. See bug 1761900.
+ t.done();
+ }
+
+ assert_greater_than_equal(document.timeline.currentTime, 0,
+ 'document.timeline.currentTime is positive or zero');
+ // 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';
+ getComputedStyle(hiddenIFrame).display;
+
+ window.requestAnimationFrame(t.step_func(function() {
+ assert_greater_than(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';
+ 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');
+
+</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..3da4e4deb2
--- /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 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.toml b/dom/animation/test/mochitest.toml
new file mode 100644
index 0000000000..1859525dfe
--- /dev/null
+++ b/dom/animation/test/mochitest.toml
@@ -0,0 +1,106 @@
+[DEFAULT]
+prefs = [
+ "dom.animations-api.compositing.enabled=true",
+ "dom.animations-api.timelines.enabled=true",
+ "gfx.omta.background-color=true",
+ "layout.css.individual-transform.enabled=true",
+ "layout.css.scroll-driven-animations.enabled=true",
+ "gfx.font_loader.delay=0",
+]
+# 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",
+ "mozilla/xhr_doc.html",
+ "mozilla/file_deferred_start.html",
+ "mozilla/file_disable_animations_api_compositing.html",
+ "mozilla/file_disable_animations_api_timelines.html",
+ "mozilla/file_discrete_animations.html",
+ "mozilla/file_transition_finish_on_compositor.html",
+ "../../../layout/style/test/property_database.js",
+ "testcommon.js",
+ "!/dom/events/test/event_leak_utils.js",
+]
+
+["document-timeline/test_document-timeline.html"]
+
+["document-timeline/test_request_animation_frame.html"]
+
+["mozilla/test_cascade.html"]
+
+["mozilla/test_cubic_bezier_limits.html"]
+
+["mozilla/test_deferred_start.html"]
+skip-if = ["os == 'win' && bits == 64"] # Bug 1363957
+
+["mozilla/test_disable_animations_api_compositing.html"]
+
+["mozilla/test_disable_animations_api_timelines.html"]
+
+["mozilla/test_disabled_properties.html"]
+
+["mozilla/test_discrete_animations.html"]
+
+["mozilla/test_distance_of_basic_shape.html"]
+
+["mozilla/test_distance_of_filter.html"]
+
+["mozilla/test_distance_of_path_function.html"]
+
+["mozilla/test_distance_of_transform.html"]
+
+["mozilla/test_document_timeline_origin_time_range.html"]
+
+["mozilla/test_event_listener_leaks.html"]
+
+["mozilla/test_get_animations_on_scroll_animations.html"]
+
+["mozilla/test_hide_and_show.html"]
+
+["mozilla/test_moz_prefixed_properties.html"]
+
+["mozilla/test_restyles.html"]
+support-files = [
+ "mozilla/file_restyles.html",
+ "mozilla/empty.html",
+]
+skip-if = [
+ "os == 'android' && debug", #Bug 1784931
+ "os == 'linux' && tsan", #Bug 1784931
+ "display == 'wayland' && os_version == '22.04' && debug", # Bug 1856969
+ "http3",
+ "http2",
+]
+
+["mozilla/test_restyling_xhr_doc.html"]
+
+["mozilla/test_set_easing.html"]
+
+["mozilla/test_style_after_finished_on_compositor.html"]
+
+["mozilla/test_transform_limits.html"]
+
+["mozilla/test_transition_finish_on_compositor.html"]
+skip-if = ["os == 'android'"]
+
+["mozilla/test_underlying_discrete_value.html"]
+
+["mozilla/test_unstyled.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_composite.html"]
+skip-if = ["xorigin"]
+
+["style/test_interpolation-from-interpolatematrix-to-none.html"]
+
+["style/test_missing-keyframe-on-compositor.html"]
+skip-if = ["fission && xorigin"] # Bug 1716403 - New fission platform triage
+
+["style/test_missing-keyframe.html"]
+
+["style/test_transform-non-normalizable-rotate3d.html"]
diff --git a/dom/animation/test/mozilla/empty.html b/dom/animation/test/mozilla/empty.html
new file mode 100644
index 0000000000..739422cbfa
--- /dev/null
+++ b/dom/animation/test/mozilla/empty.html
@@ -0,0 +1,2 @@
+<!DOCTYPE html>
+<script src="../testcommon.js"></script>
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..863fc80fec
--- /dev/null
+++ b/dom/animation/test/mozilla/file_deferred_start.html
@@ -0,0 +1,179 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="../testcommon.js"></script>
+<script src="/tests/SimpleTest/paint_listener.js"></script>
+<style>
+@keyframes empty { }
+.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((resolve, reject) => {
+ if (document.readyState === 'complete') {
+ resolve();
+ } else {
+ window.addEventListener('load', resolve);
+ }
+ });
+}
+
+function waitForPaints() {
+ return new Promise((resolve, reject) => {
+ waitForAllPaintsFlushed(resolve);
+ });
+}
+
+promise_test(async t => {
+ // 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, then create
+ // div element, and wait for painting. 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.
+ await waitForDocLoad();
+
+ const div = addDiv(t);
+
+ await waitForPaints();
+
+ div.style.animation = 'empty 1000s';
+ const animation = div.getAnimations()[0];
+
+ let promiseCallbackDone = false;
+ animation.ready.then(() => {
+ promiseCallbackDone = true;
+ }).catch(() => {
+ assert_unreached('ready promise was rejected');
+ });
+
+ // 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.
+ await waitForAnimationFrames(3);
+
+ 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(async t => {
+ assert_false(SpecialPowers.DOMWindowUtils.isTestControllingRefreshes,
+ 'Test should run without the refresh driver being under'
+ + ' test control');
+
+ // This test only applies to compositor animations
+ if (!isOMTAEnabled()) {
+ return;
+ }
+
+ const div = addDiv(t, { class: 'target' });
+
+ // As with the above test, any stray paints can cause this test to produce
+ // a false negative (that is, pass when it should fail). To avoid this we
+ // wait for paints and only then do we commence the test.
+ await waitForPaints();
+
+ const animation =
+ div.animate({ transform: [ 'translate(0px)', 'translate(100px)' ] },
+ { duration: 400 * MS_PER_SEC,
+ delay: -200 * MS_PER_SEC });
+
+ await waitForAnimationReadyToRestyle(animation);
+
+ await waitForPaints();
+
+ const transformStr =
+ SpecialPowers.DOMWindowUtils.getOMTAStyle(div, 'transform');
+ const translateX = getTranslateXFromTransform(transformStr);
+
+ // If the delay has been applied we should be about half-way through
+ // the animation. However, if we applied it twice we will be at the
+ // end of the animation already so check that we are roughly half way
+ // through.
+ assert_between_inclusive(translateX, 40, 75,
+ 'Animation is about half-way through on the compositor');
+}, 'Starting an animation with a delay starts from the correct point');
+
+// Test that compositor animations with a playback rate start at the
+// appropriate point.
+//
+// NOTE: As with the previous test, it is important that we DON'T use
+// SpecialPowers.DOMWindowUtils.advanceTimeAndRefresh here since that takes
+// us through a different code path.
+promise_test(async t => {
+ assert_false(SpecialPowers.DOMWindowUtils.isTestControllingRefreshes,
+ 'Test should run without the refresh driver being under'
+ + ' test control');
+
+ // This test only applies to compositor animations
+ if (!isOMTAEnabled()) {
+ return;
+ }
+
+ const div = addDiv(t, { class: 'target' });
+
+ // Wait for the document to load and painting (see notes in previous test).
+ await waitForPaints();
+
+ const animation =
+ div.animate({ transform: [ 'translate(0px)', 'translate(100px)' ] },
+ 200 * MS_PER_SEC);
+ animation.currentTime = 100 * MS_PER_SEC;
+ animation.playbackRate = 0.1;
+
+ await waitForPaints();
+
+ const transformStr =
+ SpecialPowers.DOMWindowUtils.getOMTAStyle(div, 'transform');
+ const translateX = getTranslateXFromTransform(transformStr);
+
+ // We pass the playback rate to the compositor independently and we have
+ // tests to ensure that it is correctly applied there. However, if, when
+ // we resolve the start time of the pending animation, we fail to
+ // incorporate the playback rate, we will end up starting from the wrong
+ // point and the current time calculated on the compositor will be wrong.
+ assert_between_inclusive(translateX, 25, 75,
+ 'Animation is about half-way through on the compositor');
+}, 'Starting an animation with a playbackRate starts from the correct point');
+
+function getTranslateXFromTransform(transformStr) {
+ const 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 + '")');
+
+ return matrixComponents[4];
+}
+
+done();
+</script>
+</body>
diff --git a/dom/animation/test/mozilla/file_disable_animations_api_compositing.html b/dom/animation/test/mozilla/file_disable_animations_api_compositing.html
new file mode 100644
index 0000000000..6d9ba35dc0
--- /dev/null
+++ b/dom/animation/test/mozilla/file_disable_animations_api_compositing.html
@@ -0,0 +1,137 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="../testcommon.js"></script>
+<body>
+<script>
+'use strict';
+
+test(t => {
+ const anim = addDiv(t).animate(
+ { marginLeft: ['0px', '10px'] },
+ {
+ duration: 100 * MS_PER_SEC,
+ iterations: 10,
+ iterationComposite: 'accumulate',
+ composite: 'add',
+ }
+ );
+ assert_false(
+ 'iterationComposite' in anim.effect,
+ 'The KeyframeEffect.iterationComposite member is not present'
+ );
+ assert_false(
+ 'composite' in anim.effect,
+ 'The KeyframeEffect.composite member is not present'
+ );
+}, 'The iterationComposite and composite members are not present on Animation'
+ + ' when the compositing pref is disabled');
+
+test(t => {
+ const div = addDiv(t);
+ const anim = div.animate(
+ { marginLeft: ['0px', '10px'] },
+ {
+ duration: 100 * MS_PER_SEC,
+ iterations: 10,
+ iterationComposite: 'accumulate',
+ }
+ );
+ anim.pause();
+ anim.currentTime = 200 * MS_PER_SEC;
+
+ assert_equals(
+ getComputedStyle(div).marginLeft,
+ '0px',
+ 'Animated style should NOT accumulate'
+ );
+}, 'KeyframeEffectOptions.iterationComposite should be ignored if the'
+ + ' compositing pref is disabled');
+
+test(t => {
+ const div = addDiv(t);
+ const anim1 = div.animate(
+ { marginLeft: ['0px', '100px'] },
+ { duration: 100 * MS_PER_SEC }
+ );
+ anim1.pause();
+ anim1.currentTime = 50 * MS_PER_SEC;
+
+ const anim2 = div.animate(
+ { marginLeft: ['0px', '100px'] },
+ { duration: 100 * MS_PER_SEC, composite: 'add' }
+ );
+ anim2.pause();
+ anim2.currentTime = 50 * MS_PER_SEC;
+
+ assert_equals(
+ getComputedStyle(div).marginLeft,
+ '50px',
+ 'Animations should NOT add together'
+ );
+}, 'KeyframeEffectOptions.composite should be ignored if the'
+ + ' compositing pref is disabled');
+
+test(t => {
+ const div = addDiv(t);
+ const anim1 = div.animate({ marginLeft: ['0px', '100px'] }, 100 * MS_PER_SEC);
+ anim1.pause();
+ anim1.currentTime = 50 * MS_PER_SEC;
+
+ const anim2 = div.animate(
+ [
+ { marginLeft: '0px', composite: 'add' },
+ { marginLeft: '100px', composite: 'add' },
+ ],
+ 100 * MS_PER_SEC
+ );
+ anim2.pause();
+ anim2.currentTime = 50 * MS_PER_SEC;
+
+ assert_equals(
+ getComputedStyle(div).marginLeft,
+ '50px',
+ 'Animations should NOT add together'
+ );
+}, 'composite member is ignored on keyframes when using array notation');
+
+test(t => {
+ const div = addDiv(t);
+ const anim1 = div.animate(
+ { marginLeft: ['0px', '100px'] },
+ 100 * MS_PER_SEC
+ );
+ anim1.pause();
+ anim1.currentTime = 50 * MS_PER_SEC;
+
+ const anim2 = div.animate(
+ { marginLeft: ['0px', '100px'], composite: ['add', 'add'] },
+ 100 * MS_PER_SEC
+ );
+ anim2.pause();
+ anim2.currentTime = 50 * MS_PER_SEC;
+
+ assert_equals(
+ getComputedStyle(div).marginLeft,
+ '50px',
+ 'Animations should NOT add together'
+ );
+}, 'composite member is ignored on keyframes when using object notation');
+
+test(t => {
+ const anim = addDiv(t).animate(
+ { marginLeft: ['0px', '10px'] },
+ 100 * MS_PER_SEC
+ );
+
+ for (let frame of anim.effect.getKeyframes()) {
+ assert_false(
+ 'composite' in frame,
+ 'The BaseComputedKeyframe.composite member is not present'
+ );
+ }
+}, 'composite member is hidden from the result of ' +
+ 'KeyframeEffect::getKeyframes()');
+
+done();
+</script>
+</body>
diff --git a/dom/animation/test/mozilla/file_disable_animations_api_timelines.html b/dom/animation/test/mozilla/file_disable_animations_api_timelines.html
new file mode 100644
index 0000000000..39fedb299a
--- /dev/null
+++ b/dom/animation/test/mozilla/file_disable_animations_api_timelines.html
@@ -0,0 +1,30 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="../testcommon.js"></script>
+<body>
+<script>
+'use strict';
+
+test(t => {
+ assert_false(
+ window.hasOwnProperty('DocumentTimeline'),
+ 'DocumentTimeline should not be exposed on the global'
+ );
+ assert_false(
+ window.hasOwnProperty('AnimationTimeline'),
+ 'AnimationTimeline should not be exposed on the global'
+ );
+ assert_false(
+ 'timeline' in document,
+ 'document should not have a timeline property'
+ );
+
+ const anim = addDiv(t).animate(null);
+ assert_false(
+ 'timeline' in anim,
+ 'Animation should not have a timeline property'
+ );
+}, 'Timeline-related interfaces and members are disabled');
+
+done();
+</script>
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..e0de609bc5
--- /dev/null
+++ b/dom/animation/test/mozilla/file_discrete_animations.html
@@ -0,0 +1,122 @@
+<!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-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: "0"
+ },
+ "-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 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_restyles.html b/dom/animation/test/mozilla/file_restyles.html
new file mode 100644
index 0000000000..0aba35cd0e
--- /dev/null
+++ b/dom/animation/test/mozilla/file_restyles.html
@@ -0,0 +1,2304 @@
+<!doctype html>
+<head>
+<meta name="viewport" content="width=device-width,minimum-scale=1,initial-scale=1">
+<meta charset=utf-8>
+<title>Tests restyles caused by animations</title>
+<script>
+const ok = opener.ok.bind(opener);
+const is = opener.is.bind(opener);
+const todo = opener.todo.bind(opener);
+const todo_is = opener.todo_is.bind(opener);
+const info = opener.info.bind(opener);
+const original_finish = opener.SimpleTest.finish;
+const SimpleTest = opener.SimpleTest;
+const add_task = opener.add_task;
+SimpleTest.finish = function finish() {
+ self.close();
+ original_finish();
+}
+</script>
+<script src="/tests/SimpleTest/EventUtils.js"></script>
+<script src="/tests/SimpleTest/paint_listener.js"></script>
+<script src="../testcommon.js"></script>
+<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css">
+<style>
+@keyframes background-position {
+ 0% {
+ background-position: -25px center;
+ }
+
+ 40%,
+ 100% {
+ background-position: 36px center;
+ }
+}
+@keyframes opacity {
+ from { opacity: 1; }
+ to { opacity: 0; }
+}
+@keyframes opacity-from-zero {
+ from { opacity: 0; }
+ to { opacity: 1; }
+}
+@keyframes opacity-without-end-value {
+ from { opacity: 0; }
+}
+@keyframes on-main-thread {
+ from { z-index: 0; }
+ to { z-index: 999; }
+}
+@keyframes rotate {
+ from { transform: rotate(0deg); }
+ to { transform: rotate(360deg); }
+}
+@keyframes move-in {
+ from { transform: translate(120%, 120%); }
+ to { transform: translate(0%, 0%); }
+}
+@keyframes background-color {
+ from { background-color: rgb(255, 0, 0,); }
+ to { background-color: rgb(0, 255, 0,); }
+}
+div {
+ /* Element needs geometry to be eligible for layerization */
+ width: 100px;
+ height: 100px;
+ background-color: white;
+}
+progress:not(.stop)::-moz-progress-bar {
+ animation: on-main-thread 100s;
+}
+body {
+ /*
+ * set overflow:hidden to avoid accidentally unthrottling animations to update
+ * the overflow region.
+ */
+ overflow: hidden;
+}
+</style>
+</head>
+<body>
+<script>
+'use strict';
+
+// Returns observed animation restyle markers when |funcToMakeRestyleHappen|
+// is called.
+// NOTE: This function is synchronous version of the above observeStyling().
+// Unlike the above observeStyling, this function takes a callback function,
+// |funcToMakeRestyleHappen|, which may be expected to trigger a synchronous
+// restyles, and returns any restyle markers produced by calling that function.
+function observeAnimSyncStyling(funcToMakeRestyleHappen) {
+
+ let priorAnimationTriggeredRestyles = SpecialPowers.DOMWindowUtils.animationTriggeredRestyles;
+
+ funcToMakeRestyleHappen();
+
+ const restyleCount = SpecialPowers.DOMWindowUtils.animationTriggeredRestyles - priorAnimationTriggeredRestyles;
+
+ return restyleCount;
+}
+
+function ensureElementRemoval(aElement) {
+ return new Promise(resolve => {
+ aElement.remove();
+ waitForAllPaintsFlushed(resolve);
+ });
+}
+
+function waitForWheelEvent(aTarget) {
+ return new Promise(resolve => {
+ // Get the scrollable target element position in this window coordinate
+ // system to send a wheel event to the element.
+ const targetRect = aTarget.getBoundingClientRect();
+ const centerX = targetRect.left + targetRect.width / 2;
+ const centerY = targetRect.top + targetRect.height / 2;
+
+ sendWheelAndPaintNoFlush(aTarget, centerX, centerY,
+ { deltaMode: WheelEvent.DOM_DELTA_PIXEL,
+ deltaY: targetRect.height },
+ resolve);
+ });
+}
+
+const omtaEnabled = isOMTAEnabled();
+
+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(() => {
+ add_task(async function () {
+ // Start vsync rate measurement in after a RAF callback.
+ await waitForNextFrame();
+
+ const timeAtStart = document.timeline.currentTime;
+ await waitForAnimationFrames(5);
+ const vsyncRate = (document.timeline.currentTime - timeAtStart) / 5;
+
+ // In this test we basically observe restyling counts in 5 frames, if it
+ // takes over 200ms during the 5 frames, this test will fail. So
+ // "200ms / 5 = 40ms" is a threshold whether the test works as expected or
+ // not. We'd take 5ms additional tolerance here.
+ // Note that the 200ms is a period we unthrottle throttled animations that
+ // at least one of the animating styles produces change hints causing
+ // overflow, the value is defined in
+ // KeyframeEffect::OverflowRegionRefreshInterval.
+ if (vsyncRate > 40 - 5) {
+ ok(true, `the vsync rate ${vsyncRate} on this machine is too slow to run this test`);
+ SimpleTest.finish();
+ }
+ });
+
+ add_task(async function restyling_for_main_thread_animations() {
+ const div = addDiv(null, { style: 'animation: on-main-thread 100s' });
+ const animation = div.getAnimations()[0];
+
+ await waitForAnimationReadyToRestyle(animation);
+
+ ok(!SpecialPowers.wrap(animation).isRunningOnCompositor);
+
+ const restyleCount = await observeStyling(5);
+ is(restyleCount, 5,
+ 'CSS animations running on the main-thread should update style ' +
+ 'on the main thread');
+ await ensureElementRemoval(div);
+ });
+
+ add_task(async function restyling_for_main_thread_animations_progress_bar_pseudo() {
+ const progress = document.createElement("progress");
+ document.body.appendChild(progress);
+
+ await waitForNextFrame();
+ await waitForNextFrame();
+
+ let restyleCount;
+ restyleCount = await observeStyling(5);
+ // TODO(bug 1784931): Figure out why we only see four markers sometimes.
+ // That's not the point of this test tho.
+ let maybe_todo_is = restyleCount == 4 ? todo_is : is;
+ maybe_todo_is(restyleCount, 5,
+ 'CSS animations running on the main-thread should update style ' +
+ 'on the main thread on ::-moz-progress-bar');
+ progress.classList.add("stop");
+ await waitForNextFrame();
+ await waitForNextFrame();
+
+ restyleCount = await observeStyling(5);
+ is(restyleCount, 0, 'Animation is correctly removed');
+ await ensureElementRemoval(progress);
+ });
+
+ add_task_if_omta_enabled(async function no_restyling_for_compositor_animations() {
+ const div = addDiv(null, { style: 'animation: opacity 100s' });
+ const animation = div.getAnimations()[0];
+
+ await waitForAnimationReadyToRestyle(animation);
+ ok(SpecialPowers.wrap(animation).isRunningOnCompositor);
+
+ const restyleCount = await observeStyling(5);
+ is(restyleCount, 0,
+ 'CSS animations running on the compositor should not update style ' +
+ 'on the main thread');
+ await ensureElementRemoval(div);
+ });
+
+ add_task_if_omta_enabled(async function no_restyling_for_compositor_transitions() {
+ const div = addDiv(null, { style: 'transition: opacity 100s; opacity: 0' });
+ getComputedStyle(div).opacity;
+ div.style.opacity = 1;
+
+ const animation = div.getAnimations()[0];
+
+ await waitForAnimationReadyToRestyle(animation);
+ ok(SpecialPowers.wrap(animation).isRunningOnCompositor);
+
+ const restyleCount = await observeStyling(5);
+ is(restyleCount, 0,
+ 'CSS transitions running on the compositor should not update style ' +
+ 'on the main thread');
+ await ensureElementRemoval(div);
+ });
+
+ add_task_if_omta_enabled(async function no_restyling_when_animation_duration_is_changed() {
+ const div = addDiv(null, { style: 'animation: opacity 100s' });
+ const animation = div.getAnimations()[0];
+
+ await waitForAnimationReadyToRestyle(animation);
+ ok(SpecialPowers.wrap(animation).isRunningOnCompositor);
+
+ div.animationDuration = '200s';
+
+ const restyleCount = await observeStyling(5);
+ is(restyleCount, 0,
+ 'Animations running on the compositor should not update style ' +
+ 'on the main thread');
+ await ensureElementRemoval(div);
+ });
+
+ add_task_if_omta_enabled(async function only_one_restyling_after_finish_is_called() {
+ const div = addDiv(null, { style: 'animation: opacity 100s' });
+ const animation = div.getAnimations()[0];
+
+ await waitForAnimationReadyToRestyle(animation);
+ ok(SpecialPowers.wrap(animation).isRunningOnCompositor);
+
+ animation.finish();
+
+ let restyleCount;
+ restyleCount = await observeStyling(1);
+ is(restyleCount, 1,
+ 'Animations running on the compositor should only update style once ' +
+ 'after finish() is called');
+
+ restyleCount = await observeStyling(1);
+ todo_is(restyleCount, 0,
+ 'Bug 1415457: Animations running on the compositor should only ' +
+ 'update style once after finish() is called');
+
+ restyleCount = await observeStyling(5);
+ is(restyleCount, 0,
+ 'Finished animations should never update style after one ' +
+ 'restyle happened for finish()');
+
+ await ensureElementRemoval(div);
+ });
+
+ add_task(async function no_restyling_mouse_movement_on_finished_transition() {
+ const div = addDiv(null, { style: 'transition: opacity 1ms; opacity: 0' });
+ getComputedStyle(div).opacity;
+ div.style.opacity = 1;
+
+ const animation = div.getAnimations()[0];
+ const initialRect = div.getBoundingClientRect();
+
+ await animation.finished;
+ let restyleCount;
+ restyleCount = await observeStyling(1);
+ is(restyleCount, 1,
+ 'Finished transitions should restyle once after Animation.finished ' +
+ 'was fulfilled');
+
+ let mouseX = initialRect.left + initialRect.width / 2;
+ let mouseY = initialRect.top + initialRect.height / 2;
+ restyleCount = await observeStyling(5, () => {
+ // We can't use synthesizeMouse here since synthesizeMouse causes
+ // layout flush.
+ synthesizeMouseAtPoint(mouseX++, mouseY++,
+ { type: 'mousemove' }, window);
+ });
+
+ is(restyleCount, 0,
+ 'Finished transitions should never cause restyles when mouse is moved ' +
+ 'on the transitions');
+ await ensureElementRemoval(div);
+ });
+
+ add_task(async function no_restyling_mouse_movement_on_finished_animation() {
+ const div = addDiv(null, { style: 'animation: opacity 1ms' });
+ const animation = div.getAnimations()[0];
+
+ const initialRect = div.getBoundingClientRect();
+
+ await animation.finished;
+ let restyleCount;
+ restyleCount = await observeStyling(1);
+ is(restyleCount, 1,
+ 'Finished animations should restyle once after Animation.finished ' +
+ 'was fulfilled');
+
+ let mouseX = initialRect.left + initialRect.width / 2;
+ let mouseY = initialRect.top + initialRect.height / 2;
+ restyleCount = await observeStyling(5, () => {
+ // We can't use synthesizeMouse here since synthesizeMouse causes
+ // layout flush.
+ synthesizeMouseAtPoint(mouseX++, mouseY++,
+ { type: 'mousemove' }, window);
+ });
+
+ is(restyleCount, 0,
+ 'Finished animations should never cause restyles when mouse is moved ' +
+ 'on the animations');
+ await ensureElementRemoval(div);
+ });
+
+ add_task_if_omta_enabled(async function no_restyling_compositor_animations_out_of_view_element() {
+ const div = addDiv(null,
+ { style: 'animation: opacity 100s; transform: translateY(-400px);' });
+ const animation = div.getAnimations()[0];
+
+ await waitForAnimationReadyToRestyle(animation);
+ ok(!SpecialPowers.wrap(animation).isRunningOnCompositor);
+
+ const restyleCount = await observeStyling(5);
+
+ is(restyleCount, 0,
+ 'Animations running on the compositor in an out-of-view element ' +
+ 'should never cause restyles');
+ await ensureElementRemoval(div);
+ });
+
+ add_task(async function no_restyling_main_thread_animations_out_of_view_element() {
+ const div = addDiv(null,
+ { style: 'animation: on-main-thread 100s; transform: translateY(-400px);' });
+ const animation = div.getAnimations()[0];
+
+ await waitForAnimationReadyToRestyle(animation);
+ const restyleCount = await observeStyling(5);
+
+ is(restyleCount, 0,
+ 'Animations running on the main-thread in an out-of-view element ' +
+ 'should never cause restyles');
+ await ensureElementRemoval(div);
+ });
+
+ add_task_if_omta_enabled(async function no_restyling_compositor_animations_in_scrolled_out_element() {
+ const parentElement = addDiv(null,
+ { style: 'overflow-y: scroll; height: 20px;' });
+ const div = addDiv(null,
+ { style: 'animation: opacity 100s; position: relative; top: 100px;' });
+ parentElement.appendChild(div);
+ const animation = div.getAnimations()[0];
+
+ await waitForAnimationReadyToRestyle(animation);
+
+ const restyleCount = await observeStyling(5);
+
+ is(restyleCount, 0,
+ 'Animations running on the compositor for elements ' +
+ 'which are scrolled out should never cause restyles');
+
+ await ensureElementRemoval(parentElement);
+ });
+
+ add_task(
+ async function no_restyling_missing_keyframe_opacity_animations_on_scrolled_out_element() {
+ const parentElement = addDiv(null,
+ { style: 'overflow-y: scroll; height: 20px;' });
+ const div = addDiv(null,
+ { style: 'animation: opacity-without-end-value 100s; ' +
+ 'position: relative; top: 100px;' });
+ parentElement.appendChild(div);
+ const animation = div.getAnimations()[0];
+ await waitForAnimationReadyToRestyle(animation);
+
+ const restyleCount = await observeStyling(5);
+
+ is(restyleCount, 0,
+ 'Opacity animations on scrolled out elements should never cause ' +
+ 'restyles even if the animation has missing keyframes');
+
+ await ensureElementRemoval(parentElement);
+ }
+ );
+
+ add_task(
+ async function restyling_transform_animations_in_scrolled_out_element() {
+ // Make sure we start from the state right after requestAnimationFrame.
+ await waitForFrame();
+
+ const parentElement = addDiv(null,
+ { style: 'overflow-y: scroll; height: 20px;' });
+ const div = addDiv(null,
+ { style: 'animation: rotate 100s infinite; position: relative; top: 100px;' });
+ parentElement.appendChild(div);
+ const animation = div.getAnimations()[0];
+ let timeAtStart = document.timeline.currentTime;
+
+ ok(!animation.isRunningOnCompositor,
+ 'The transform animation is not running on the compositor');
+
+ let restyleCount
+ let now;
+ let elapsed;
+ while (true) {
+ now = document.timeline.currentTime;
+ elapsed = (now - timeAtStart);
+ restyleCount = await observeStyling(1);
+ if (restyleCount) {
+ break;
+ }
+ }
+ // If the current time has elapsed over 200ms since the animation was
+ // created, it means that the animation should have already
+ // unthrottled in this tick, let's see what we observe in this tick's
+ // restyling process.
+ // We use toPrecision here and below so 199.99999999999977 will turn into 200.
+ ok(elapsed.toPrecision(10) >= 200,
+ 'Transform animation running on the element which is scrolled out ' +
+ 'should be throttled until 200ms is elapsed. now: ' +
+ now + ' start time: ' + timeAtStart + ' elapsed:' + elapsed);
+
+ timeAtStart = document.timeline.currentTime;
+ restyleCount = await observeStyling(1);
+ now = document.timeline.currentTime;
+ elapsed = (now - timeAtStart);
+
+ let expectedMarkersLengthValid;
+ // On the fence of 200 ms, we probably have 1 marker; but if we hit a bad rounding
+ // we might still have 0. But if it's > 200, we should have 1; and less we should have 0.
+ if (elapsed.toPrecision(10) == 200)
+ expectedMarkersLengthValid = restyleCount < 2;
+ else if (elapsed.toPrecision(10) > 200)
+ expectedMarkersLengthValid = restyleCount == 1;
+ else
+ expectedMarkersLengthValid = !restyleCount;
+ ok(expectedMarkersLengthValid,
+ 'Transform animation running on the element which is scrolled out ' +
+ 'should be unthrottled after around 200ms have elapsed. now: ' +
+ now + ' start time: ' + timeAtStart + ' elapsed: ' + elapsed);
+
+ await ensureElementRemoval(parentElement);
+ }
+ );
+
+ add_task(
+ async function restyling_out_of_view_transform_animations_in_another_element() {
+ // Make sure we start from the state right after requestAnimationFrame.
+ await waitForFrame();
+
+ const parentElement = addDiv(null,
+ { style: 'overflow: hidden;' });
+ const div = addDiv(null,
+ { style: 'animation: move-in 100s infinite;' });
+ parentElement.appendChild(div);
+ const animation = div.getAnimations()[0];
+ let timeAtStart = document.timeline.currentTime;
+
+ ok(!animation.isRunningOnCompositor,
+ 'The transform animation on out of view element ' +
+ 'is not running on the compositor');
+
+ // Structure copied from restyling_transform_animations_in_scrolled_out_element
+ let restyleCount
+ let now;
+ let elapsed;
+ while (true) {
+ now = document.timeline.currentTime;
+ elapsed = (now - timeAtStart);
+ restyleCount = await observeStyling(1);
+ if (restyleCount) {
+ break;
+ }
+ }
+
+ ok(elapsed.toPrecision(10) >= 200,
+ 'Transform animation running on out of view element ' +
+ 'should be throttled until 200ms is elapsed. now: ' +
+ now + ' start time: ' + timeAtStart + ' elapsed:' + elapsed);
+
+ timeAtStart = document.timeline.currentTime;
+ restyleCount = await observeStyling(1);
+ now = document.timeline.currentTime;
+ elapsed = (now - timeAtStart);
+
+ let expectedMarkersLengthValid;
+ // On the fence of 200 ms, we probably have 1 marker; but if we hit a bad rounding
+ // we might still have 0. But if it's > 200, we should have 1; and less we should have 0.
+ if (elapsed.toPrecision(10) == 200)
+ expectedMarkersLengthValid = restyleCount < 2;
+ else if (elapsed.toPrecision(10) > 200)
+ expectedMarkersLengthValid = restyleCount == 1;
+ else
+ expectedMarkersLengthValid = !restyleCount;
+ ok(expectedMarkersLengthValid,
+ 'Transform animation running on out of view element ' +
+ 'should be unthrottled after around 200ms have elapsed. now: ' +
+ now + ' start time: ' + timeAtStart + ' elapsed: ' + elapsed);
+
+ await ensureElementRemoval(parentElement);
+ }
+ );
+
+ add_task(async function finite_transform_animations_in_out_of_view_element() {
+ const parentElement = addDiv(null, { style: 'overflow: hidden;' });
+ const div = addDiv(null);
+ const animation =
+ div.animate({ transform: [ 'translateX(120%)', 'translateX(100%)' ] },
+ // This animation will move a bit but
+ // will remain out-of-view.
+ 100 * MS_PER_SEC);
+ parentElement.appendChild(div);
+
+ await waitForAnimationReadyToRestyle(animation);
+ ok(!SpecialPowers.wrap(animation).isRunningOnCompositor, "Should not be running in compositor");
+
+ const restyleCount = await observeStyling(20);
+ is(restyleCount, 20,
+ 'Finite transform animation in out-of-view element should never be ' +
+ 'throttled');
+
+ await ensureElementRemoval(parentElement);
+ });
+
+ add_task(async function restyling_main_thread_animations_in_scrolled_out_element() {
+ const parentElement = addDiv(null,
+ { style: 'overflow-y: scroll; height: 20px;' });
+ const div = addDiv(null,
+ { style: 'animation: on-main-thread 100s; position: relative; top: 20px;' });
+ parentElement.appendChild(div);
+ const animation = div.getAnimations()[0];
+
+ await waitForAnimationReadyToRestyle(animation);
+ let restyleCount;
+ restyleCount = await observeStyling(5);
+
+ is(restyleCount, 0,
+ 'Animations running on the main-thread for elements ' +
+ 'which are scrolled out should never cause restyles');
+
+ await waitForWheelEvent(parentElement);
+
+ // Make sure we are ready to restyle before counting restyles.
+ await waitForFrame();
+
+ restyleCount = await observeStyling(5);
+ is(restyleCount, 5,
+ 'Animations running on the main-thread which were in scrolled out ' +
+ 'elements should update restyling soon after the element moved in ' +
+ 'view by scrolling');
+
+ await ensureElementRemoval(parentElement);
+ });
+
+ add_task(async function restyling_main_thread_animations_in_nested_scrolled_out_element() {
+ const grandParent = addDiv(null,
+ { style: 'overflow-y: scroll; height: 20px;' });
+ const parentElement = addDiv(null,
+ { style: 'overflow-y: scroll; height: 100px;' });
+ const div = addDiv(null,
+ { style: 'animation: on-main-thread 100s; ' +
+ 'position: relative; ' +
+ 'top: 20px;' }); // This element is in-view in the parent, but
+ // out of view in the grandparent.
+ grandParent.appendChild(parentElement);
+ parentElement.appendChild(div);
+ const animation = div.getAnimations()[0];
+
+ await waitForAnimationReadyToRestyle(animation);
+ let restyleCount;
+ restyleCount = await observeStyling(5);
+
+ is(restyleCount, 0,
+ 'Animations running on the main-thread which are in nested elements ' +
+ 'which are scrolled out should never cause restyles');
+
+ await waitForWheelEvent(grandParent);
+
+ await waitForFrame();
+
+ restyleCount = await observeStyling(5);
+ is(restyleCount, 5,
+ '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');
+
+ await ensureElementRemoval(grandParent);
+ });
+
+ add_task_if_omta_enabled(async function no_restyling_compositor_animations_in_visibility_hidden_element() {
+ const div = addDiv(null,
+ { style: 'animation: opacity 100s; visibility: hidden' });
+ const animation = div.getAnimations()[0];
+
+ await waitForAnimationReadyToRestyle(animation);
+ ok(!SpecialPowers.wrap(animation).isRunningOnCompositor);
+
+ const restyleCount = await observeStyling(5);
+
+ is(restyleCount, 0,
+ 'Animations running on the compositor in visibility hidden element ' +
+ 'should never cause restyles');
+ await ensureElementRemoval(div);
+ });
+
+ add_task(async function restyling_main_thread_animations_move_out_of_view_by_scrolling() {
+ const parentElement = addDiv(null,
+ { style: 'overflow-y: scroll; height: 200px;' });
+ const div = addDiv(null,
+ { style: 'animation: on-main-thread 100s;' });
+ const pad = addDiv(null,
+ { style: 'height: 400px;' });
+ parentElement.appendChild(div);
+ parentElement.appendChild(pad);
+ const animation = div.getAnimations()[0];
+
+ await waitForAnimationReadyToRestyle(animation);
+
+ await waitForWheelEvent(parentElement);
+
+ await waitForFrame();
+
+ const restyleCount = await observeStyling(5);
+
+ // FIXME: We should reduce a redundant restyle here.
+ ok(restyleCount >= 0,
+ 'Animations running on the main-thread which are in scrolled out ' +
+ 'elements should throttle restyling');
+
+ await ensureElementRemoval(parentElement);
+ });
+
+ add_task(async function restyling_main_thread_animations_moved_in_view_by_resizing() {
+ const parentElement = addDiv(null,
+ { style: 'overflow-y: scroll; height: 20px;' });
+ const div = addDiv(null,
+ { style: 'animation: on-main-thread 100s; position: relative; top: 100px;' });
+ parentElement.appendChild(div);
+ const animation = div.getAnimations()[0];
+
+ await waitForAnimationReadyToRestyle(animation);
+
+ let restyleCount;
+ restyleCount = await observeStyling(5);
+ is(restyleCount, 0,
+ 'Animations running on the main-thread which is in scrolled out ' +
+ 'elements should not update restyling');
+
+ parentElement.style.height = '100px';
+ restyleCount = await observeStyling(1);
+
+ is(restyleCount, 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');
+
+ await ensureElementRemoval(parentElement);
+ });
+
+ add_task(
+ async function restyling_animations_on_visibility_changed_element_having_child() {
+ const div = addDiv(null,
+ { style: 'animation: on-main-thread 100s;' });
+ const childElement = addDiv(null);
+ div.appendChild(childElement);
+
+ const animation = div.getAnimations()[0];
+
+ await waitForAnimationReadyToRestyle(animation);
+
+ // We don't check the animation causes restyles here since we already
+ // check it in the first test case.
+
+ div.style.visibility = 'hidden';
+ await waitForNextFrame();
+
+ const restyleCount = await observeStyling(5);
+ todo_is(restyleCount, 0,
+ 'Animations running on visibility hidden element which ' +
+ 'has a child whose visiblity is inherited from the element and ' +
+ 'the element was initially visible');
+
+ await ensureElementRemoval(div);
+ }
+ );
+
+ add_task(
+ async function restyling_animations_on_visibility_hidden_element_which_gets_visible() {
+ const div = addDiv(null,
+ { style: 'animation: on-main-thread 100s; visibility: hidden' });
+ const animation = div.getAnimations()[0];
+
+
+ await waitForAnimationReadyToRestyle(animation);
+ let restyleCount;
+ restyleCount = await observeStyling(5);
+
+ is(restyleCount, 0,
+ 'Animations running on visibility hidden element should never ' +
+ 'cause restyles');
+
+ div.style.visibility = 'visible';
+ await waitForNextFrame();
+
+ restyleCount = await observeStyling(5);
+ is(restyleCount, 5,
+ 'Animations running that was on visibility hidden element which ' +
+ 'gets visible should not throttle restyling any more');
+
+ await ensureElementRemoval(div);
+ }
+ );
+
+ add_task(async function restyling_animations_in_visibility_changed_parent() {
+ const parentDiv = addDiv(null, { style: 'visibility: hidden' });
+ const div = addDiv(null, { style: 'animation: on-main-thread 100s;' });
+ parentDiv.appendChild(div);
+
+ const animation = div.getAnimations()[0];
+
+ await waitForAnimationReadyToRestyle(animation);
+ let restyleCount;
+ restyleCount = await observeStyling(5);
+
+ is(restyleCount, 0,
+ 'Animations running in visibility hidden parent should never cause ' +
+ 'restyles');
+
+ parentDiv.style.visibility = 'visible';
+ await waitForNextFrame();
+
+ restyleCount = await observeStyling(5);
+ is(restyleCount, 5,
+ 'Animations that was in visibility hidden parent should not ' +
+ 'throttle restyling any more');
+
+ parentDiv.style.visibility = 'hidden';
+ await waitForNextFrame();
+
+ restyleCount = await observeStyling(5);
+ is(restyleCount, 0,
+ 'Animations that the parent element became visible should throttle ' +
+ 'restyling again');
+
+ await ensureElementRemoval(parentDiv);
+ });
+
+ add_task(
+ async function restyling_animations_on_visibility_hidden_element_with_visibility_changed_children() {
+ const div = addDiv(null,
+ { style: 'animation: on-main-thread 100s; visibility: hidden' });
+ const animation = div.getAnimations()[0];
+
+ await waitForAnimationReadyToRestyle(animation);
+ let restyleCount;
+ restyleCount = await observeStyling(5);
+
+ is(restyleCount, 0,
+ 'Animations on visibility hidden element having no visible children ' +
+ 'should never cause restyles');
+
+ const childElement = addDiv(null, { style: 'visibility: visible' });
+ div.appendChild(childElement);
+ await waitForNextFrame();
+
+ restyleCount = await observeStyling(5);
+ is(restyleCount, 5,
+ 'Animations running on visibility hidden element but the element has ' +
+ 'a visible child should not throttle restyling');
+
+ childElement.style.visibility = 'hidden';
+ await waitForNextFrame();
+
+ restyleCount = await observeStyling(5);
+ todo_is(restyleCount, 0,
+ 'Animations running on visibility hidden element that a child ' +
+ 'has become invisible should throttle restyling');
+
+ childElement.style.visibility = 'visible';
+ await waitForNextFrame();
+
+ restyleCount = await observeStyling(5);
+ is(restyleCount, 5,
+ 'Animations running on visibility hidden element should not throttle ' +
+ 'restyling after the invisible element changed to visible');
+
+ childElement.remove();
+ await waitForNextFrame();
+
+ restyleCount = await observeStyling(5);
+ todo_is(restyleCount, 0,
+ 'Animations running on visibility hidden element should throttle ' +
+ 'restyling again after all visible descendants were removed');
+
+ await ensureElementRemoval(div);
+ }
+ );
+
+ add_task(
+ async function restyling_animations_on_visiblity_hidden_element_having_oof_child() {
+ const div = addDiv(null,
+ { style: 'animation: on-main-thread 100s; position: absolute' });
+ const childElement = addDiv(null,
+ { style: 'float: left; visibility: hidden' });
+ div.appendChild(childElement);
+
+ const animation = div.getAnimations()[0];
+
+ await waitForAnimationReadyToRestyle(animation);
+
+ // We don't check the animation causes restyles here since we already
+ // check it in the first test case.
+
+ div.style.visibility = 'hidden';
+ await waitForNextFrame();
+
+ const restyleCount = await observeStyling(5);
+ is(restyleCount, 0,
+ 'Animations running on visibility hidden element which has an ' +
+ 'out-of-flow child should throttle restyling');
+
+ await ensureElementRemoval(div);
+ }
+ );
+
+ add_task(
+ async function restyling_animations_on_visibility_hidden_element_having_grandchild() {
+ // element tree:
+ //
+ // root(visibility:hidden)
+ // / \
+ // childA childB
+ // / \ / \
+ // AA AB BA BB
+
+ const div = addDiv(null,
+ { style: 'animation: on-main-thread 100s; visibility: hidden' });
+
+ const childA = addDiv(null);
+ div.appendChild(childA);
+ const childB = addDiv(null);
+ div.appendChild(childB);
+
+ const grandchildAA = addDiv(null);
+ childA.appendChild(grandchildAA);
+ const grandchildAB = addDiv(null);
+ childA.appendChild(grandchildAB);
+
+ const grandchildBA = addDiv(null);
+ childB.appendChild(grandchildBA);
+ const grandchildBB = addDiv(null);
+ childB.appendChild(grandchildBB);
+
+ const animation = div.getAnimations()[0];
+
+ await waitForAnimationReadyToRestyle(animation);
+ let restyleCount;
+ restyleCount = await observeStyling(5);
+ is(restyleCount, 0,
+ 'Animations on visibility hidden element having no visible ' +
+ 'descendants should never cause restyles');
+
+ childA.style.visibility = 'visible';
+ grandchildAA.style.visibility = 'visible';
+ grandchildAB.style.visibility = 'visible';
+ await waitForNextFrame();
+
+ restyleCount = await observeStyling(5);
+ is(restyleCount, 5,
+ 'Animations running on visibility hidden element but the element has ' +
+ 'visible children should not throttle restyling');
+
+ // Make childA hidden again but both of grandchildAA and grandchildAB are
+ // still visible.
+ childA.style.visibility = 'hidden';
+ await waitForNextFrame();
+
+ restyleCount = await observeStyling(5);
+ is(restyleCount, 5,
+ 'Animations running on visibility hidden element that a child has ' +
+ 'become invisible again but there are still visible children should ' +
+ 'not throttle restyling');
+
+ // Make grandchildAA hidden but grandchildAB is still visible.
+ grandchildAA.style.visibility = 'hidden';
+ await waitForNextFrame();
+
+ restyleCount = await observeStyling(5);
+ is(restyleCount, 5,
+ 'Animations running on visibility hidden element that a grandchild ' +
+ 'become invisible again but another grandchild is still visible ' +
+ 'should not throttle restyling');
+
+
+ // Make childB and grandchildBA visible.
+ childB.style.visibility = 'visible';
+ grandchildBA.style.visibility = 'visible';
+ await waitForNextFrame();
+
+ restyleCount = await observeStyling(5);
+ is(restyleCount, 5,
+ 'Animations running on visibility hidden element but the element has ' +
+ 'visible descendants should not throttle restyling');
+
+ // Make childB hidden but grandchildAB and grandchildBA are still visible.
+ childB.style.visibility = 'hidden';
+ await waitForNextFrame();
+
+ restyleCount = await observeStyling(5);
+ is(restyleCount, 5,
+ 'Animations running on visibility hidden element but the element has ' +
+ 'visible grandchildren should not throttle restyling');
+
+ // Make grandchildAB hidden but grandchildBA is still visible.
+ grandchildAB.style.visibility = 'hidden';
+ await waitForNextFrame();
+
+ restyleCount = await observeStyling(5);
+ is(restyleCount, 5,
+ 'Animations running on visibility hidden element but the element has ' +
+ 'a visible grandchild should not throttle restyling');
+
+ // Make grandchildBA hidden. Now all descedants are invisible.
+ grandchildBA.style.visibility = 'hidden';
+ await waitForNextFrame();
+
+ restyleCount = await observeStyling(5);
+ todo_is(restyleCount, 0,
+ 'Animations on visibility hidden element that all descendants have ' +
+ 'become invisible again should never cause restyles');
+
+ // Make childB visible.
+ childB.style.visibility = 'visible';
+ await waitForNextFrame();
+
+ restyleCount = await observeStyling(5);
+ is(restyleCount, 5,
+ 'Animations on visibility hidden element that has a visible child ' +
+ 'should never cause restyles');
+
+ // Make childB invisible again
+ childB.style.visibility = 'hidden';
+ await waitForNextFrame();
+
+ restyleCount = await observeStyling(5);
+ todo_is(restyleCount, 0,
+ 'Animations on visibility hidden element that the visible child ' +
+ 'has become invisible again should never cause restyles');
+
+ await ensureElementRemoval(div);
+ }
+ );
+
+ add_task_if_omta_enabled(async function no_restyling_compositor_animations_after_pause_is_called() {
+ const div = addDiv(null, { style: 'animation: opacity 100s' });
+ const animation = div.getAnimations()[0];
+
+ await waitForAnimationReadyToRestyle(animation);
+ ok(SpecialPowers.wrap(animation).isRunningOnCompositor);
+
+ animation.pause();
+
+ await animation.ready;
+ let restyleCount;
+ restyleCount = await observeStyling(1);
+ is(restyleCount, 1,
+ 'Animations running on the compositor should restyle once after ' +
+ 'Animation.pause() was called');
+
+ restyleCount = await observeStyling(5);
+ is(restyleCount, 0,
+ 'Paused animations running on the compositor should never cause ' +
+ 'restyles');
+ await ensureElementRemoval(div);
+ });
+
+ add_task(async function no_restyling_main_thread_animations_after_pause_is_called() {
+ const div = addDiv(null, { style: 'animation: on-main-thread 100s' });
+ const animation = div.getAnimations()[0];
+
+ await waitForAnimationReadyToRestyle(animation);
+
+ animation.pause();
+
+ await animation.ready;
+ let restyleCount;
+ restyleCount = await observeStyling(1);
+ is(restyleCount, 1,
+ 'Animations running on the main-thread should restyle once after ' +
+ 'Animation.pause() was called');
+
+ restyleCount = await observeStyling(5);
+ is(restyleCount, 0,
+ 'Paused animations running on the main-thread should never cause ' +
+ 'restyles');
+ await ensureElementRemoval(div);
+ });
+
+ add_task_if_omta_enabled(async function only_one_restyling_when_current_time_is_set_to_middle_of_duration() {
+ const div = addDiv(null, { style: 'animation: opacity 100s' });
+ const animation = div.getAnimations()[0];
+
+ await waitForAnimationReadyToRestyle(animation);
+
+ animation.currentTime = 50 * MS_PER_SEC;
+
+ const restyleCount = await observeStyling(5);
+ is(restyleCount, 1,
+ 'Bug 1235478: Animations running on the compositor should only once ' +
+ 'update style when currentTime is set to middle of duration time');
+ await ensureElementRemoval(div);
+ });
+
+ add_task_if_omta_enabled(async function change_duration_and_currenttime() {
+ const div = addDiv(null);
+ const animation = div.animate({ opacity: [ 0, 1 ] }, 100 * MS_PER_SEC);
+
+ await waitForAnimationReadyToRestyle(animation);
+ ok(SpecialPowers.wrap(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(!SpecialPowers.wrap(animation).isRunningOnCompositor);
+
+ // Extend the duration.
+ animation.effect.updateTiming({ duration: 800 * MS_PER_SEC });
+ const restyleCount = await observeStyling(5);
+ is(restyleCount, 1,
+ 'Animations running on the compositor should update style ' +
+ 'when duration is made longer than the current time');
+
+ await ensureElementRemoval(div);
+ });
+
+ add_task(async function script_animation_on_display_none_element() {
+ const div = addDiv(null);
+ const animation = div.animate({ zIndex: [ '0', '999' ] },
+ 100 * MS_PER_SEC);
+
+ await waitForAnimationReadyToRestyle(animation);
+
+ div.style.display = 'none';
+
+ // We need to wait a frame to apply display:none style.
+ await waitForNextFrame();
+
+ is(animation.playState, 'running',
+ 'Script animations keep running even when the target element has ' +
+ '"display: none" style');
+
+ ok(!SpecialPowers.wrap(animation).isRunningOnCompositor,
+ 'Script animations on "display:none" element should not run on the ' +
+ 'compositor');
+
+ let restyleCount;
+ restyleCount = await observeStyling(5);
+ is(restyleCount, 0,
+ 'Script animations on "display: none" element should not update styles');
+
+ div.style.display = '';
+
+ restyleCount = await observeStyling(5);
+ is(restyleCount, 5,
+ 'Script animations restored from "display: none" state should update ' +
+ 'styles');
+
+ await ensureElementRemoval(div);
+ });
+
+ add_task_if_omta_enabled(async function compositable_script_animation_on_display_none_element() {
+ const div = addDiv(null);
+ const animation = div.animate({ opacity: [ 0, 1 ] }, 100 * MS_PER_SEC);
+
+ await waitForAnimationReadyToRestyle(animation);
+
+ div.style.display = 'none';
+
+ // We need to wait a frame to apply display:none style.
+ await waitForNextFrame();
+
+ is(animation.playState, 'running',
+ 'Opacity script animations keep running even when the target element ' +
+ 'has "display: none" style');
+
+ ok(!SpecialPowers.wrap(animation).isRunningOnCompositor,
+ 'Opacity script animations on "display:none" element should not ' +
+ 'run on the compositor');
+
+ let restyleCount;
+ restyleCount = await observeStyling(5);
+ is(restyleCount, 0,
+ 'Opacity script animations on "display: none" element should not ' +
+ 'update styles');
+
+ div.style.display = '';
+
+ restyleCount = await observeStyling(1);
+ is(restyleCount, 1,
+ 'Script animations restored from "display: none" state should update ' +
+ 'styles soon');
+
+ ok(SpecialPowers.wrap(animation).isRunningOnCompositor,
+ 'Opacity script animations restored from "display: none" should be ' +
+ 'run on the compositor in the next frame');
+
+ await ensureElementRemoval(div);
+ });
+
+ add_task(async function restyling_for_empty_keyframes() {
+ const div = addDiv(null);
+ const animation = div.animate({ }, 100 * MS_PER_SEC);
+
+ await waitForAnimationReadyToRestyle(animation);
+ let restyleCount;
+ restyleCount = await observeStyling(5);
+
+ is(restyleCount, 0,
+ 'Animations with no keyframes should not cause restyles');
+
+ animation.effect.setKeyframes({ zIndex: ['0', '999'] });
+ restyleCount = await observeStyling(5);
+
+ is(restyleCount, 5,
+ 'Setting valid keyframes should cause regular animation restyles to ' +
+ 'occur');
+
+ animation.effect.setKeyframes({ });
+ restyleCount = await observeStyling(5);
+
+ is(restyleCount, 1,
+ 'Setting an empty set of keyframes should trigger a single restyle ' +
+ 'to remove the previous animated style');
+
+ await ensureElementRemoval(div);
+ });
+
+ add_task_if_omta_enabled(async function no_restyling_when_animation_style_when_re_setting_same_animation_property() {
+ const div = addDiv(null, { style: 'animation: opacity 100s' });
+ const animation = div.getAnimations()[0];
+ await waitForAnimationReadyToRestyle(animation);
+ ok(SpecialPowers.wrap(animation).isRunningOnCompositor);
+ // Apply the same animation style
+ div.style.animation = 'opacity 100s';
+ const restyleCount = await observeStyling(5);
+ is(restyleCount, 0,
+ 'Applying same animation style ' +
+ 'should never cause restyles');
+ await ensureElementRemoval(div);
+ });
+
+ add_task(async function necessary_update_should_be_invoked() {
+ const div = addDiv(null, { style: 'animation: on-main-thread 100s' });
+ const animation = div.getAnimations()[0];
+ await waitForAnimationReadyToRestyle(animation);
+ await waitForAnimationFrames(5);
+ // Apply another animation style
+ div.style.animation = 'on-main-thread 110s';
+ const restyleCount = await observeStyling(1);
+ // There should be two restyles.
+ // 1) Animation-only restyle for before applying the new animation style
+ // 2) Animation-only restyle for after applying the new animation style
+ is(restyleCount, 2,
+ 'Applying animation style with different duration ' +
+ 'should restyle twice');
+ await ensureElementRemoval(div);
+ });
+
+ add_task_if_omta_enabled(
+ async function changing_cascading_result_for_main_thread_animation() {
+ const div = addDiv(null, { style: 'on-main-thread: blue' });
+ const animation = div.animate({ opacity: [0, 1],
+ zIndex: ['0', '999'] },
+ 100 * MS_PER_SEC);
+ await waitForAnimationReadyToRestyle(animation);
+ ok(SpecialPowers.wrap(animation).isRunningOnCompositor,
+ 'The opacity animation is running on the compositor');
+ // Make the z-index style as !important to cause an update
+ // to the cascade.
+ // Bug 1300982: The z-index animation should be no longer
+ // running on the main thread.
+ div.style.setProperty('z-index', '0', 'important');
+ const restyleCount = await observeStyling(5);
+ todo_is(restyleCount, 0,
+ 'Changing cascading result for the property running on the main ' +
+ 'thread does not cause synchronization layer of opacity animation ' +
+ 'running on the compositor');
+ await ensureElementRemoval(div);
+ }
+ );
+
+ add_task_if_omta_enabled(
+ async function animation_visibility_and_opacity() {
+ const div = addDiv(null);
+ const animation1 = div.animate({ opacity: [0, 1] }, 100 * MS_PER_SEC);
+ const animation2 = div.animate({ visibility: ['hidden', 'visible'] }, 100 * MS_PER_SEC);
+ await waitForAnimationReadyToRestyle(animation1);
+ await waitForAnimationReadyToRestyle(animation2);
+ const restyleCount = await observeStyling(5);
+ is(restyleCount, 5, 'The animation should not be throttled');
+ await ensureElementRemoval(div);
+ }
+ );
+
+ add_task(async function restyling_for_animation_on_orphaned_element() {
+ const div = addDiv(null);
+ const animation = div.animate({ marginLeft: [ '0px', '100px' ] },
+ 100 * MS_PER_SEC);
+
+ await waitForAnimationReadyToRestyle(animation);
+
+ div.remove();
+ let restyleCount;
+ restyleCount = await observeStyling(5);
+ is(restyleCount, 0,
+ 'Animation on orphaned element should not cause restyles');
+
+ document.body.appendChild(div);
+
+ await waitForNextFrame();
+ restyleCount = await observeStyling(5);
+
+ is(restyleCount, 5,
+ 'Animation on re-attached to the document begins to update style, got ' + restyleCount);
+
+ await 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.
+ async function restyling_for_opacity_animation_on_re_attached_element() {
+ const div = addDiv(null, { style: 'opacity: 1 ! important' });
+ const animation = div.animate({ opacity: [0, 1] }, 100 * MS_PER_SEC);
+
+ await waitForAnimationReadyToRestyle(animation);
+ ok(!SpecialPowers.wrap(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();
+
+ const restyleCount = await observeStyling(5);
+ is(restyleCount, 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.
+ await waitForNextFrame();
+
+ ok(SpecialPowers.wrap(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');
+
+ await ensureElementRemoval(div);
+ }
+ );
+
+ add_task(
+ async function no_throttling_additive_animations_out_of_view_element() {
+ const div = addDiv(null, { style: 'transform: translateY(-400px);' });
+ const animation =
+ div.animate([{ visibility: 'visible' }],
+ { duration: 100 * MS_PER_SEC, composite: 'add' });
+
+ await waitForAnimationReadyToRestyle(animation);
+
+ const restyleCount = await observeStyling(5);
+
+ is(restyleCount, 0,
+ 'Additive animation has no keyframe whose offset is 0 or 1 in an ' +
+ 'out-of-view element should be throttled');
+ await ensureElementRemoval(div);
+ }
+ );
+
+ // Tests that missing keyframes animations don't throttle at all.
+ add_task(async function no_throttling_animations_out_of_view_element() {
+ const div = addDiv(null, { style: 'transform: translateY(-400px);' });
+ const animation =
+ div.animate([{ visibility: 'visible' }], 100 * MS_PER_SEC);
+
+ await waitForAnimationReadyToRestyle(animation);
+
+ const restyleCount = await observeStyling(5);
+
+ is(restyleCount, 0,
+ 'Discrete animation has has no keyframe whose offset is 0 or 1 in an ' +
+ 'out-of-view element should be throttled');
+ await ensureElementRemoval(div);
+ });
+
+ // Tests that missing keyframes animation on scrolled out element that the
+ // animation is not able to be throttled.
+ add_task(
+ async function no_throttling_missing_keyframe_animations_out_of_view_element() {
+ const div =
+ addDiv(null, { style: 'transform: translateY(-400px);' +
+ 'visibility: collapse;' });
+ const animation =
+ div.animate([{ visibility: 'visible' }], 100 * MS_PER_SEC);
+ await waitForAnimationReadyToRestyle(animation);
+
+ const restyleCount = await observeStyling(5);
+ is(restyleCount, 0,
+ 'visibility animation has no keyframe whose offset is 0 or 1 in an ' +
+ 'out-of-view element should be throttled');
+ await ensureElementRemoval(div);
+ }
+ );
+
+ // Counter part of the above test.
+ add_task(async function no_restyling_discrete_animations_out_of_view_element() {
+ const div = addDiv(null, { style: 'transform: translateY(-400px);' });
+ const animation =
+ div.animate({ visibility: ['visible', 'hidden'] }, 100 * MS_PER_SEC);
+
+ await waitForAnimationReadyToRestyle(animation);
+
+ const restyleCount = await observeStyling(5);
+
+ is(restyleCount, 0,
+ 'Discrete animation running on the main-thread in an out-of-view ' +
+ 'element should never cause restyles');
+ await ensureElementRemoval(div);
+ });
+
+ add_task(async function no_restyling_while_computed_timing_is_not_changed() {
+ const div = addDiv(null);
+ const animation = div.animate({ zIndex: [ '0', '999' ] },
+ { duration: 100 * MS_PER_SEC,
+ easing: 'step-end' });
+
+ await waitForAnimationReadyToRestyle(animation);
+
+ const restyleCount = await observeStyling(5);
+
+ // We possibly expect one restyle from the initial animation compose, in
+ // order to update animations, but nothing else.
+ ok(restyleCount <= 1,
+ 'Animation running on the main-thread while computed timing is not ' +
+ 'changed should not cause extra restyles, got ' + restyleCount);
+ await ensureElementRemoval(div);
+ });
+
+ add_task(async function no_throttling_animations_in_view_svg() {
+ const div = addDiv(null, { style: 'overflow: scroll;' +
+ 'height: 100px; width: 100px;' });
+ const svg = addSVGElement(div, 'svg', { viewBox: '-10 -10 0.1 0.1',
+ width: '50px',
+ height: '50px' });
+ const rect = addSVGElement(svg, 'rect', { x: '-10',
+ y: '-10',
+ width: '10',
+ height: '10',
+ fill: 'red' });
+ const animation = rect.animate({ fill: ['blue', 'lime'] }, 100 * MS_PER_SEC);
+ await waitForAnimationReadyToRestyle(animation);
+
+ const restyleCount = await observeStyling(5);
+ is(restyleCount, 5,
+ 'CSS animations on an in-view svg element with post-transform should ' +
+ 'not be throttled.');
+
+ await ensureElementRemoval(div);
+ });
+
+ add_task(async function no_throttling_animations_in_transformed_parent() {
+ const div = addDiv(null, { style: 'overflow: scroll;' +
+ 'transform: translateX(50px);' });
+ const svg = addSVGElement(div, 'svg', { viewBox: '0 0 1250 1250',
+ width: '40px',
+ height: '40px' });
+ const rect = addSVGElement(svg, 'rect', { x: '0',
+ y: '0',
+ width: '1250',
+ height: '1250',
+ fill: 'red' });
+ const animation = rect.animate({ fill: ['blue', 'lime'] }, 100 * MS_PER_SEC);
+ await waitForAnimationReadyToRestyle(animation);
+
+ const restyleCount = await observeStyling(5);
+ is(restyleCount, 5,
+ 'CSS animations on an in-view svg element which is inside transformed ' +
+ 'parent should not be throttled.');
+
+ await ensureElementRemoval(div);
+ });
+
+ add_task(async function throttling_animations_out_of_view_svg() {
+ const div = addDiv(null, { style: 'overflow: scroll;' +
+ 'height: 100px; width: 100px;' });
+ const svg = addSVGElement(div, 'svg', { viewBox: '-10 -10 0.1 0.1',
+ width: '50px',
+ height: '50px' });
+ const rect = addSVGElement(svg, 'rect', { width: '10',
+ height: '10',
+ fill: 'red' });
+
+ const animation = rect.animate({ fill: ['blue', 'lime'] }, 100 * MS_PER_SEC);
+ await waitForAnimationReadyToRestyle(animation);
+
+ const restyleCount = await observeStyling(5);
+ is(restyleCount, 0,
+ 'CSS animations on an out-of-view svg element with post-transform ' +
+ 'should be throttled.');
+
+ await ensureElementRemoval(div);
+ });
+
+ add_task(async function no_throttling_animations_in_view_css_transform() {
+ const scrollDiv = addDiv(null, { style: 'overflow: scroll; ' +
+ 'height: 100px; width: 100px;' });
+ const targetDiv = addDiv(null,
+ { style: 'animation: on-main-thread 100s;' +
+ 'transform: translate(-50px, -50px);' });
+ scrollDiv.appendChild(targetDiv);
+
+ const animation = targetDiv.getAnimations()[0];
+ await waitForAnimationReadyToRestyle(animation);
+
+ const restyleCount = await observeStyling(5);
+ is(restyleCount, 5,
+ 'CSS animation on an in-view element with pre-transform should not ' +
+ 'be throttled.');
+
+ await ensureElementRemoval(scrollDiv);
+ });
+
+ add_task(async function throttling_animations_out_of_view_css_transform() {
+ const scrollDiv = addDiv(null, { style: 'overflow: scroll;' +
+ 'height: 100px; width: 100px;' });
+ const targetDiv = addDiv(null,
+ { style: 'animation: on-main-thread 100s;' +
+ 'transform: translate(100px, 100px);' });
+ scrollDiv.appendChild(targetDiv);
+
+ const animation = targetDiv.getAnimations()[0];
+ await waitForAnimationReadyToRestyle(animation);
+
+ const restyleCount = await observeStyling(5);
+ is(restyleCount, 0,
+ 'CSS animation on an out-of-view element with pre-transform should be ' +
+ 'throttled.');
+
+ await ensureElementRemoval(scrollDiv);
+ });
+
+ add_task(
+ async function throttling_animations_in_out_of_view_position_absolute_element() {
+ const parentDiv = addDiv(null,
+ { style: 'position: absolute; top: -1000px;' });
+ const targetDiv = addDiv(null,
+ { style: 'animation: on-main-thread 100s;' });
+ parentDiv.appendChild(targetDiv);
+
+ const animation = targetDiv.getAnimations()[0];
+ await waitForAnimationReadyToRestyle(animation);
+
+ const restyleCount = await observeStyling(5);
+ is(restyleCount, 0,
+ 'CSS animation in an out-of-view position absolute element should ' +
+ 'be throttled');
+
+ await ensureElementRemoval(parentDiv);
+ }
+ );
+
+ add_task(
+ async function throttling_animations_on_out_of_view_position_absolute_element() {
+ const div = addDiv(null,
+ { style: 'animation: on-main-thread 100s; ' +
+ 'position: absolute; top: -1000px;' });
+
+ const animation = div.getAnimations()[0];
+ await waitForAnimationReadyToRestyle(animation);
+
+ const restyleCount = await observeStyling(5);
+ is(restyleCount, 0,
+ 'CSS animation on an out-of-view position absolute element should ' +
+ 'be throttled');
+
+ await ensureElementRemoval(div);
+ }
+ );
+
+ add_task(
+ async function throttling_animations_in_out_of_view_position_fixed_element() {
+ const parentDiv = addDiv(null,
+ { style: 'position: fixed; top: -1000px;' });
+ const targetDiv = addDiv(null,
+ { style: 'animation: on-main-thread 100s;' });
+ parentDiv.appendChild(targetDiv);
+
+ const animation = targetDiv.getAnimations()[0];
+ await waitForAnimationReadyToRestyle(animation);
+
+ const restyleCount = await observeStyling(5);
+ is(restyleCount, 0,
+ 'CSS animation on an out-of-view position:fixed element should be ' +
+ 'throttled');
+
+ await ensureElementRemoval(parentDiv);
+ }
+ );
+
+ add_task(
+ async function throttling_animations_on_out_of_view_position_fixed_element() {
+ const div = addDiv(null,
+ { style: 'animation: on-main-thread 100s; ' +
+ 'position: fixed; top: -1000px;' });
+
+ const animation = div.getAnimations()[0];
+ await waitForAnimationReadyToRestyle(animation);
+
+ const restyleCount = await observeStyling(5);
+ is(restyleCount, 0,
+ 'CSS animation on an out-of-view position:fixed element should be ' +
+ 'throttled');
+
+ await ensureElementRemoval(div);
+ }
+ );
+
+ add_task(
+ async function no_throttling_animations_in_view_position_fixed_element() {
+ const iframe = document.createElement('iframe');
+ iframe.setAttribute('srcdoc', '<div id="target"></div>');
+ document.documentElement.appendChild(iframe);
+
+ await new Promise(resolve => {
+ iframe.addEventListener('load', () => {
+ resolve();
+ });
+ });
+
+ const target = iframe.contentDocument.getElementById('target');
+ target.style= 'position: fixed; top: 20px; width: 100px; height: 100px;';
+
+ const animation = target.animate({ zIndex: [ '0', '999' ] },
+ { duration: 100 * MS_PER_SEC,
+ iterations: Infinity });
+ await waitForAnimationReadyToRestyle(animation);
+
+ const restyleCount = await observeStylingInTargetWindow(iframe.contentWindow, 5);
+ is(restyleCount, 5,
+ 'CSS animation on an in-view position:fixed element should NOT be ' +
+ 'throttled');
+
+ await ensureElementRemoval(iframe);
+ }
+ );
+
+ add_task(
+ async function throttling_position_absolute_animations_in_collapsed_iframe() {
+ const iframe = document.createElement('iframe');
+ iframe.setAttribute('srcdoc', '<div id="target"></div>');
+ iframe.style.height = '0px';
+ document.documentElement.appendChild(iframe);
+
+ await new Promise(resolve => {
+ iframe.addEventListener('load', () => {
+ resolve();
+ });
+ });
+
+ const target = iframe.contentDocument.getElementById("target");
+ target.style= 'position: absolute; top: 50%; width: 100px; height: 100px';
+
+ const animation = target.animate({ opacity: [0, 1] },
+ { duration: 100 * MS_PER_SEC,
+ iterations: Infinity });
+ await waitForAnimationReadyToRestyle(animation);
+
+ const restyleCount = await observeStylingInTargetWindow(iframe.contentWindow, 5);
+ is(restyleCount, 0,
+ 'Animation on position:absolute element in collapsed iframe should ' +
+ 'be throttled');
+
+ await ensureElementRemoval(iframe);
+ }
+ );
+
+ add_task(
+ async function position_absolute_animations_in_collapsed_element() {
+ const parent = addDiv(null, { style: 'overflow: scroll; height: 0px;' });
+ const target = addDiv(null,
+ { style: 'animation: on-main-thread 100s infinite;' +
+ 'position: absolute; top: 50%;' +
+ 'width: 100px; height: 100px;' });
+ parent.appendChild(target);
+
+ const animation = target.getAnimations()[0];
+ await waitForAnimationReadyToRestyle(animation);
+
+ const restyleCount = await observeStyling(5);
+ is(restyleCount, 5,
+ 'Animation on position:absolute element in collapsed element ' +
+ 'should not be throttled');
+
+ await ensureElementRemoval(parent);
+ }
+ );
+
+ add_task(
+ async function throttling_position_absolute_animations_in_collapsed_element() {
+ const parent = addDiv(null, { style: 'overflow: scroll; height: 0px;' });
+ const target = addDiv(null,
+ { style: 'animation: on-main-thread 100s infinite;' +
+ 'position: absolute; top: 50%;' });
+ parent.appendChild(target);
+
+ const animation = target.getAnimations()[0];
+ await waitForAnimationReadyToRestyle(animation);
+
+ const restyleCount = await observeStyling(5);
+ todo_is(restyleCount, 0,
+ 'Animation on collapsed position:absolute element in collapsed ' +
+ 'element should be throttled');
+
+ await ensureElementRemoval(parent);
+ }
+ );
+
+ add_task_if_omta_enabled(
+ async function no_restyling_for_compositor_animation_on_unrelated_style_change() {
+ const div = addDiv(null);
+ const animation = div.animate({ opacity: [0, 1] }, 100 * MS_PER_SEC);
+
+ await waitForAnimationReadyToRestyle(animation);
+ ok(SpecialPowers.wrap(animation).isRunningOnCompositor,
+ 'The opacity animation is running on the compositor');
+
+ div.style.setProperty('color', 'blue', '');
+ const restyleCount = await observeStyling(5);
+ is(restyleCount, 0,
+ 'The opacity animation keeps running on the compositor when ' +
+ 'color style is changed');
+ await ensureElementRemoval(div);
+ }
+ );
+
+ add_task(
+ async function no_overflow_transform_animations_in_scrollable_element() {
+ const parentElement = addDiv(null,
+ { style: 'overflow-y: scroll; height: 100px;' });
+ const div = addDiv(null);
+ const animation =
+ div.animate({ transform: [ 'translateY(10px)', 'translateY(10px)' ] },
+ 100 * MS_PER_SEC);
+ parentElement.appendChild(div);
+
+ await waitForAnimationReadyToRestyle(animation);
+ ok(SpecialPowers.wrap(animation).isRunningOnCompositor);
+
+ const restyleCount = await observeStyling(20);
+ is(restyleCount, 0,
+ 'No-overflow transform animations running on the compositor should ' +
+ 'never update style on the main thread');
+
+ await ensureElementRemoval(parentElement);
+ }
+ );
+
+ add_task(async function no_flush_on_getAnimations() {
+ const div = addDiv(null);
+ const animation =
+ div.animate({ opacity: [ '0', '1' ] }, 100 * MS_PER_SEC);
+ await waitForAnimationReadyToRestyle(animation);
+
+ ok(SpecialPowers.wrap(animation).isRunningOnCompositor);
+
+ const restyleCount = observeAnimSyncStyling(() => {
+ is(div.getAnimations().length, 1, 'There should be one animation');
+ });
+ is(restyleCount, 0,
+ 'Element.getAnimations() should not flush throttled animation style');
+
+ await ensureElementRemoval(div);
+ });
+
+ add_task(async function restyling_for_throttled_animation_on_getAnimations() {
+ const div = addDiv(null, { style: 'animation: opacity 100s' });
+ const animation = div.getAnimations()[0];
+
+ await waitForAnimationReadyToRestyle(animation);
+ ok(SpecialPowers.wrap(animation).isRunningOnCompositor);
+
+ const restyleCount = observeAnimSyncStyling(() => {
+ div.style.animationDuration = '0s';
+ is(div.getAnimations().length, 0, 'There should be no animation');
+ });
+
+ is(restyleCount, 1, // For discarding the throttled animation.
+ 'Element.getAnimations() should flush throttled animation style so ' +
+ 'that the throttled animation is discarded');
+
+ await ensureElementRemoval(div);
+ });
+
+ add_task(
+ async function no_restyling_for_throttled_animation_on_querying_play_state() {
+ const div = addDiv(null, { style: 'animation: opacity 100s' });
+ const animation = div.getAnimations()[0];
+ const sibling = addDiv(null);
+
+ await waitForAnimationReadyToRestyle(animation);
+ ok(SpecialPowers.wrap(animation).isRunningOnCompositor);
+
+ const restyleCount = observeAnimSyncStyling(() => {
+ sibling.style.opacity = '0.5';
+ is(animation.playState, 'running',
+ 'Animation.playState should be running');
+ });
+ is(restyleCount, 0,
+ 'Animation.playState should not flush throttled animation in the ' +
+ 'case where there are only style changes that don\'t affect the ' +
+ 'throttled animation');
+
+ await ensureElementRemoval(div);
+ await ensureElementRemoval(sibling);
+ }
+ );
+
+ add_task(
+ async function restyling_for_throttled_animation_on_querying_play_state() {
+ const div = addDiv(null, { style: 'animation: opacity 100s' });
+ const animation = div.getAnimations()[0];
+
+ await waitForAnimationReadyToRestyle(animation);
+ ok(SpecialPowers.wrap(animation).isRunningOnCompositor);
+
+ const restyleCount = observeAnimSyncStyling(() => {
+ div.style.animationPlayState = 'paused';
+ is(animation.playState, 'paused',
+ 'Animation.playState should be reflected by pending style');
+ });
+
+ is(restyleCount, 1,
+ 'Animation.playState should flush throttled animation style that ' +
+ 'affects the throttled animation');
+
+ await ensureElementRemoval(div);
+ }
+ );
+
+ add_task(
+ async function no_restyling_for_throttled_transition_on_querying_play_state() {
+ const div = addDiv(null, { style: 'transition: opacity 100s; opacity: 0' });
+ const sibling = addDiv(null);
+
+ getComputedStyle(div).opacity;
+ div.style.opacity = 1;
+
+ const transition = div.getAnimations()[0];
+
+ await waitForAnimationReadyToRestyle(transition);
+ ok(SpecialPowers.wrap(transition).isRunningOnCompositor);
+
+ const restyleCount = observeAnimSyncStyling(() => {
+ sibling.style.opacity = '0.5';
+ is(transition.playState, 'running',
+ 'Animation.playState should be running');
+ });
+
+ is(restyleCount, 0,
+ 'Animation.playState should not flush throttled transition in the ' +
+ 'case where there are only style changes that don\'t affect the ' +
+ 'throttled animation');
+
+ await ensureElementRemoval(div);
+ await ensureElementRemoval(sibling);
+ }
+ );
+
+ add_task(
+ async function restyling_for_throttled_transition_on_querying_play_state() {
+ const div = addDiv(null, { style: 'transition: opacity 100s; opacity: 0' });
+ getComputedStyle(div).opacity;
+ div.style.opacity = '1';
+
+ const transition = div.getAnimations()[0];
+
+ await waitForAnimationReadyToRestyle(transition);
+ ok(SpecialPowers.wrap(transition).isRunningOnCompositor);
+
+ const restyleCount = observeAnimSyncStyling(() => {
+ div.style.transitionProperty = 'none';
+ is(transition.playState, 'idle',
+ 'Animation.playState should be reflected by pending style change ' +
+ 'which cancel the transition');
+ });
+
+ is(restyleCount, 1,
+ 'Animation.playState should flush throttled transition style that ' +
+ 'affects the throttled animation');
+
+ await ensureElementRemoval(div);
+ }
+ );
+
+ add_task(async function restyling_visibility_animations_on_in_view_element() {
+ const div = addDiv(null);
+ const animation =
+ div.animate({ visibility: ['hidden', 'visible'] }, 100 * MS_PER_SEC);
+
+ await waitForAnimationReadyToRestyle(animation);
+
+ const restyleCount = await observeStyling(5);
+
+ is(restyleCount, 5,
+ 'Visibility animation running on the main-thread on in-view element ' +
+ 'should not be throttled');
+ await ensureElementRemoval(div);
+ });
+
+ add_task(async function restyling_outline_offset_animations_on_invisible_element() {
+ const div = addDiv(null,
+ { style: 'visibility: hidden; ' +
+ 'outline-style: solid; ' +
+ 'outline-width: 1px;' });
+ const animation =
+ div.animate({ outlineOffset: [ '0px', '10px' ] },
+ { duration: 100 * MS_PER_SEC,
+ iterations: Infinity });
+
+ await waitForAnimationReadyToRestyle(animation);
+
+ const restyleCount = await observeStyling(5);
+
+ is(restyleCount, 0,
+ 'Outline offset animation running on the main-thread on invisible ' +
+ 'element should be throttled');
+ await ensureElementRemoval(div);
+ });
+
+ add_task(async function restyling_transform_animations_on_invisible_element() {
+ const div = addDiv(null, { style: 'visibility: hidden;' });
+
+ const animation =
+ div.animate({ transform: [ 'none', 'rotate(360deg)' ] },
+ { duration: 100 * MS_PER_SEC,
+ iterations: Infinity });
+
+ await waitForAnimationReadyToRestyle(animation);
+
+ ok(!SpecialPowers.wrap(animation).isRunningOnCompositor);
+
+ const restyleCount = await observeStyling(5);
+
+ is(restyleCount, 0,
+ 'Transform animations on visibility hidden element should be throttled');
+ await ensureElementRemoval(div);
+ });
+
+ add_task(async function restyling_transform_animations_on_invisible_element() {
+ const div = addDiv(null, { style: 'visibility: hidden;' });
+
+ const animation =
+ div.animate([ { transform: 'rotate(360deg)' } ],
+ { duration: 100 * MS_PER_SEC,
+ iterations: Infinity });
+
+ await waitForAnimationReadyToRestyle(animation);
+
+ ok(!SpecialPowers.wrap(animation).isRunningOnCompositor);
+
+ const restyleCount = await observeStyling(5);
+
+ is(restyleCount, 0,
+ 'Transform animations without 100% keyframe on visibility hidden ' +
+ 'element should be throttled');
+ await ensureElementRemoval(div);
+ });
+
+ add_task(async function restyling_translate_animations_on_invisible_element() {
+ const div = addDiv(null, { style: 'visibility: hidden;' });
+
+ const animation =
+ div.animate([ { translate: '100px' } ],
+ { duration: 100 * MS_PER_SEC,
+ iterations: Infinity });
+
+ await waitForAnimationReadyToRestyle(animation);
+
+ ok(!SpecialPowers.wrap(animation).isRunningOnCompositor);
+
+ const restyleCount = await observeStyling(5);
+
+ is(restyleCount, 0,
+ 'Translate animations without 100% keyframe on visibility hidden ' +
+ 'element should be throttled');
+ await ensureElementRemoval(div);
+ });
+
+ add_task(async function restyling_rotate_animations_on_invisible_element() {
+ const div = addDiv(null, { style: 'visibility: hidden;' });
+
+ const animation =
+ div.animate([ { rotate: '45deg' } ],
+ { duration: 100 * MS_PER_SEC,
+ iterations: Infinity });
+
+ await waitForAnimationReadyToRestyle(animation);
+
+ ok(!SpecialPowers.wrap(animation).isRunningOnCompositor);
+
+ const restyleCount = await observeStyling(5);
+
+ is(restyleCount, 0,
+ 'Rotate animations without 100% keyframe on visibility hidden ' +
+ 'element should be throttled');
+ await ensureElementRemoval(div);
+ });
+
+ add_task(async function restyling_scale_animations_on_invisible_element() {
+ const div = addDiv(null, { style: 'visibility: hidden;' });
+
+ const animation =
+ div.animate([ { scale: '2 2' } ],
+ { duration: 100 * MS_PER_SEC,
+ iterations: Infinity });
+
+ await waitForAnimationReadyToRestyle(animation);
+
+ ok(!SpecialPowers.wrap(animation).isRunningOnCompositor);
+
+ const restyleCount = await observeStyling(5);
+
+ is(restyleCount, 0,
+ 'Scale animations without 100% keyframe on visibility hidden ' +
+ 'element should be throttled');
+ await ensureElementRemoval(div);
+ });
+
+ add_task(
+ async function restyling_transform_animations_having_abs_pos_child_on_invisible_element() {
+ const div = addDiv(null, { style: 'visibility: hidden;' });
+ const child = addDiv(null, { style: 'position: absolute; top: 100px;' });
+ div.appendChild(child);
+
+ const animation =
+ div.animate({ transform: [ 'none', 'rotate(360deg)' ] },
+ { duration: 100 * MS_PER_SEC,
+ iterations: Infinity });
+
+ await waitForAnimationReadyToRestyle(animation);
+
+ ok(!SpecialPowers.wrap(animation).isRunningOnCompositor);
+
+ const restyleCount = await observeStyling(5);
+
+ is(restyleCount, 0,
+ 'Transform animation having an absolutely positioned child on ' +
+ 'visibility hidden element should be throttled');
+ await ensureElementRemoval(div);
+ });
+
+ add_task(async function no_restyling_animations_in_out_of_view_iframe() {
+ const div = addDiv(null, { style: 'overflow-y: scroll; height: 100px;' });
+
+ const iframe = document.createElement('iframe');
+ iframe.setAttribute(
+ 'srcdoc',
+ '<div style="height: 100px;"></div><div id="target"></div>');
+ div.appendChild(iframe);
+
+ await new Promise(resolve => {
+ iframe.addEventListener('load', () => {
+ resolve();
+ });
+ });
+
+ const target = iframe.contentDocument.getElementById("target");
+ target.style= 'width: 100px; height: 100px;';
+
+ const animation = target.animate({ zIndex: [ '0', '999' ] },
+ 100 * MS_PER_SEC);
+ await waitForAnimationReadyToRestyle(animation);
+
+ const restyleCount = await observeStylingInTargetWindow(iframe.contentWindow, 5);
+ is(restyleCount, 0,
+ 'Animation in out-of-view iframe should be throttled');
+
+ await ensureElementRemoval(div);
+ });
+
+ // Tests that transform animations are not able to run on the compositor due
+ // to layout restrictions (e.g. animations on a large size frame) doesn't
+ // flush layout at all.
+ add_task(async function flush_layout_for_transform_animations() {
+ // Set layout.animation.prerender.partial to disallow transform animations
+ // on large frames to be sent to the compositor.
+ await SpecialPowers.pushPrefEnv({
+ set: [['layout.animation.prerender.partial', false]] });
+ const div = addDiv(null, { style: 'width: 10000px; height: 10000px;' });
+
+ const animation = div.animate([ { transform: 'rotate(360deg)', } ],
+ { duration: 100 * MS_PER_SEC,
+ // Set step-end to skip further restyles.
+ easing: 'step-end' });
+
+ const FLUSH_LAYOUT = SpecialPowers.DOMWindowUtils.FLUSH_LAYOUT;
+ ok(SpecialPowers.DOMWindowUtils.needsFlush(FLUSH_LAYOUT),
+ 'Flush is needed for the appended div');
+
+ await waitForAnimationReadyToRestyle(animation);
+
+ ok(!SpecialPowers.wrap(animation).isRunningOnCompositor, "Shouldn't be running in the compositor");
+
+ // We expect one restyle from the initial animation compose.
+ await waitForNextFrame();
+
+ ok(!SpecialPowers.wrap(animation).isRunningOnCompositor, "Still shouldn't be running in the compositor");
+ ok(!SpecialPowers.DOMWindowUtils.needsFlush(FLUSH_LAYOUT),
+ 'No further layout flush needed');
+
+ await ensureElementRemoval(div);
+ });
+
+ add_task(async function partial_prerendered_transform_animations() {
+ await SpecialPowers.pushPrefEnv({
+ set: [['layout.animation.prerender.partial', true]] });
+ const div = addDiv(null, { style: 'width: 10000px; height: 10000px;' });
+
+ const animation = div.animate(
+ // Use the same value both for `from` and `to` to avoid jank on the
+ // compositor.
+ { transform: ['rotate(0deg)', 'rotate(0deg)'] },
+ 100 * MS_PER_SEC
+ );
+
+ await waitForAnimationReadyToRestyle(animation);
+
+ const restyleCount = await observeStyling(5)
+ is(restyleCount, 0,
+ 'Transform animation with partial pre-rendered should never cause ' +
+ 'restyles');
+
+ await ensureElementRemoval(div);
+ });
+
+ add_task(async function restyling_on_create_animation() {
+ const div = addDiv();
+ let priorAnimationTriggeredRestyles = SpecialPowers.DOMWindowUtils.animationTriggeredRestyles;
+
+ const animationA = div.animate(
+ { transform: ['none', 'rotate(360deg)'] },
+ 100 * MS_PER_SEC
+ );
+ const animationB = div.animate({ opacity: [0, 1] }, 100 * MS_PER_SEC);
+ const animationC = div.animate(
+ { color: ['blue', 'green'] },
+ 100 * MS_PER_SEC
+ );
+ const animationD = div.animate(
+ { width: ['100px', '200px'] },
+ 100 * MS_PER_SEC
+ );
+ const animationE = div.animate(
+ { height: ['100px', '200px'] },
+ 100 * MS_PER_SEC
+ );
+
+ const restyleCount = SpecialPowers.DOMWindowUtils.animationTriggeredRestyles - priorAnimationTriggeredRestyles;
+
+ is(restyleCount, 0, 'Creating animations should not flush styles');
+
+ await ensureElementRemoval(div);
+ });
+
+ add_task(async function out_of_view_background_position() {
+ const div = addDiv(null, {
+ style: `
+ background-image: linear-gradient(90deg, rgb(224, 224, 224), rgb(241, 241, 241) 30%, rgb(224, 224, 224) 60%);
+ background-size: 80px;
+ animation: background-position 100s infinite;
+ transform: translateY(-400px);
+ `,
+ })
+
+ const animation = div.getAnimations()[0];
+
+ await waitForAnimationReadyToRestyle(animation);
+
+ const restyleCount = await observeStyling(5);
+
+ is(restyleCount, 0, 'background-position animations can be throttled');
+ await ensureElementRemoval(div);
+ });
+
+ add_task_if_omta_enabled(async function no_restyling_animations_in_opacity_zero_element() {
+ const div = addDiv(null, { style: 'animation: on-main-thread 100s infinite; opacity: 0' });
+ const animation = div.getAnimations()[0];
+
+ await waitForAnimationReadyToRestyle(animation);
+ const restyleCount = await observeStyling(5);
+ is(restyleCount, 0,
+ 'Animations running on the main thread in opacity: 0 element ' +
+ 'should never cause restyles');
+ await ensureElementRemoval(div);
+ });
+
+ add_task_if_omta_enabled(async function no_restyling_compositor_animations_in_opacity_zero_descendant() {
+ const container = addDiv(null, { style: 'opacity: 0' });
+ const child = addDiv(null, { style: 'animation: background-color 100s infinite;' });
+ container.appendChild(child);
+
+ const animation = child.getAnimations()[0];
+ await waitForAnimationReadyToRestyle(animation);
+ ok(!SpecialPowers.wrap(animation).isRunningOnCompositor);
+
+ const restyleCount = await observeStyling(5);
+
+ is(restyleCount, 0,
+ 'Animations running on the compositor in opacity zero descendant element ' +
+ 'should never cause restyles');
+ await ensureElementRemoval(container);
+ });
+
+ add_task_if_omta_enabled(async function no_restyling_compositor_animations_in_opacity_zero_descendant_abspos() {
+ const container = addDiv(null, { style: 'opacity: 0' });
+ const child = addDiv(null, { style: 'position: absolute; animation: background-color 100s infinite;' });
+ container.appendChild(child);
+
+ const animation = child.getAnimations()[0];
+ await waitForAnimationReadyToRestyle(animation);
+ ok(!SpecialPowers.wrap(animation).isRunningOnCompositor);
+
+ const restyleCount = await observeStyling(5);
+
+ is(restyleCount, 0,
+ 'Animations running on the compositor in opacity zero abspos descendant element ' +
+ 'should never cause restyles');
+ await ensureElementRemoval(container);
+ });
+
+ add_task_if_omta_enabled(async function no_restyling_compositor_animations_in_opacity_zero_element() {
+ const child = addDiv(null, { style: 'animation: background-color 100s infinite; opacity: 0' });
+
+ const animation = child.getAnimations()[0];
+ await waitForAnimationReadyToRestyle(animation);
+ ok(!SpecialPowers.wrap(animation).isRunningOnCompositor);
+
+ const restyleCount = await observeStyling(5);
+
+ is(restyleCount, 0,
+ 'Animations running on the compositor in opacity zero element ' +
+ 'should never cause restyles');
+ await ensureElementRemoval(child);
+ });
+
+ add_task_if_omta_enabled(async function restyling_main_thread_animations_in_opacity_zero_descendant_after_root_opacity_animation() {
+ const container = addDiv(null, { style: 'opacity: 0' });
+
+ const child = addDiv(null, { style: 'animation: on-main-thread 100s infinite;' });
+ container.appendChild(child);
+
+ // Animate the container from 1 to zero opacity and ensure the child animation is throttled then.
+ const containerAnimation = container.animate({ opacity: [ '1', '0' ] }, 100);
+ await containerAnimation.finished;
+
+ const animation = child.getAnimations()[0];
+ await waitForAnimationReadyToRestyle(animation);
+
+ const restyleCount = await observeStyling(5);
+
+ is(restyleCount, 0,
+ 'Animations running on the compositor in opacity zero descendant element ' +
+ 'should never cause restyles after root animation has finished');
+ await ensureElementRemoval(container);
+ });
+
+ add_task_if_omta_enabled(async function restyling_main_thread_animations_in_opacity_zero_descendant_during_root_opacity_animation() {
+ const container = addDiv(null, { style: 'opacity: 0; animation: opacity-from-zero 100s infinite' });
+
+ const child = addDiv(null, { style: 'animation: on-main-thread 100s infinite;' });
+ container.appendChild(child);
+
+ const animation = child.getAnimations()[0];
+ await waitForAnimationReadyToRestyle(animation);
+
+ const restyleCount = await observeStyling(5);
+
+ is(restyleCount, 5,
+ 'Animations in opacity zero descendant element ' +
+ 'should not be throttled if root is animating opacity');
+ await ensureElementRemoval(container);
+ });
+
+ add_task_if_omta_enabled(async function restyling_omt_animations_in_opacity_zero_descendant_during_root_opacity_animation() {
+ const container = addDiv(null, { style: 'opacity: 0; animation: opacity-from-zero 100s infinite' });
+
+ const child = addDiv(null, { style: 'animation: rotate 100s infinite' });
+ container.appendChild(child);
+
+ const animation = child.getAnimations()[0];
+ await waitForAnimationReadyToRestyle(animation);
+ ok(SpecialPowers.wrap(animation).isRunningOnCompositor);
+
+ await ensureElementRemoval(container);
+ });
+
+ add_task_if_omta_enabled(async function transparent_background_color_animations() {
+ const div = addDiv(null);
+ const animation =
+ div.animate({ backgroundColor: [ 'rgb(0, 200, 0, 0)',
+ 'rgb(200, 0, 0, 0.1)' ] },
+ { duration: 100 * MS_PER_SEC,
+ // An easing function producing zero in the first half of
+ // the duration.
+ easing: 'cubic-bezier(1, 0, 1, 0)' });
+ await waitForAnimationReadyToRestyle(animation);
+
+ ok(SpecialPowers.wrap(animation).isRunningOnCompositor);
+
+ const restyleCount = await observeStyling(5);
+ is(restyleCount, 0,
+ 'transparent background-color animation should not update styles on ' +
+ 'the main thread');
+
+ await ensureElementRemoval(div);
+ });
+
+ add_task_if_omta_enabled(async function transform_animation_on_collapsed_element() {
+ const iframe = document.createElement('iframe');
+ document.body.appendChild(iframe);
+
+ // Load a cross origin iframe.
+ const targetURL = SimpleTest.getTestFileURL("empty.html")
+ .replace(window.location.origin, "http://example.com/");
+ iframe.src = targetURL;
+ await new Promise(resolve => {
+ iframe.onload = resolve;
+ });
+
+ await SpecialPowers.spawn(iframe, [MS_PER_SEC], async (MS_PER_SEC) => {
+ // Create a flex item with "preserve-3d" having an abs-pos child inside
+ // a grid container.
+ // These styles make the flex item size (0x0).
+ const gridContainer = content.document.createElement("div");
+ gridContainer.style.display = "grid";
+ gridContainer.style.placeItems = "center";
+
+ const target = content.document.createElement("div");
+ target.style.display = "flex";
+ target.style.transformStyle = "preserve-3d";
+ gridContainer.appendChild(target);
+
+ const child = content.document.createElement("div");
+ child.style.position = "absolute";
+ child.style.transform = "rotateY(0deg)";
+ child.style.width = "100px";
+ child.style.height = "100px";
+ child.style.backgroundColor = "green";
+ target.appendChild(child);
+
+ content.document.body.appendChild(gridContainer);
+
+ const animation =
+ target.animate({ transform: [ "rotateY(0deg)", "rotateY(360deg)" ] },
+ { duration: 100 * MS_PER_SEC,
+ id: "test",
+ easing: 'step-end' });
+ await content.wrappedJSObject.waitForAnimationReadyToRestyle(animation);
+ ok(SpecialPowers.wrap(animation).isRunningOnCompositor,
+ 'transform animation on a collapsed element should run on the ' +
+ 'compositor');
+
+ const restyleCount = await content.wrappedJSObject.observeStyling(5);
+ is(restyleCount, 0,
+ 'transform animation on a collapsed element animation should not ' +
+ 'update styles on the main thread');
+ });
+
+ await ensureElementRemoval(iframe);
+ });
+});
+
+</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..d5e7f4ffd7
--- /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(async 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();
+ await waitForPaints();
+
+ // 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/test_cascade.html b/dom/animation/test/mozilla/test_cascade.html
new file mode 100644
index 0000000000..4bdb07530e
--- /dev/null
+++ b/dom/animation/test/mozilla/test_cascade.html
@@ -0,0 +1,37 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../testcommon.js"></script>
+<style>
+@keyframes margin-left {
+ from { margin-left: 20px; }
+ to { margin-left: 80px; }
+}
+</style>
+<body>
+<div id="log"></div>
+<script>
+'use strict';
+
+test(function(t) {
+ var div = addDiv(t, { style: 'transition: margin-left 100s; ' +
+ 'margin-left: 80px' });
+ var cs = getComputedStyle(div);
+
+ assert_equals(cs.marginLeft, '80px', 'initial margin-left');
+
+ div.style.marginLeft = "20px";
+ assert_equals(cs.marginLeft, '80px', 'margin-left transition at 0s');
+
+ div.style.animation = "margin-left 2s";
+ assert_equals(cs.marginLeft, '20px',
+ 'margin-left animation overrides transition at 0s');
+
+ div.style.animation = "none";
+ assert_equals(cs.marginLeft, '80px',
+ 'margin-left animation stops overriding transition at 0s');
+}, 'Animation overrides/stops overriding transition immediately');
+
+</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..bdbc78654f
--- /dev/null
+++ b/dom/animation/test/mozilla/test_cubic_bezier_limits.html
@@ -0,0 +1,168 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../testcommon.js"></script>
+<body>
+<style>
+@keyframes anim {
+ to { margin-left: 100px; }
+}
+
+.transition-div {
+ margin-left: 100px;
+}
+</style>
+<div id="log"></div>
+<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.40282e38';
+
+test(function(t) {
+ var div = addDiv(t);
+ var anim = div.animate({ }, 100 * MS_PER_SEC);
+
+ anim.effect.updateTiming({ easing: 'cubic-bezier(0, 1e+39, 0, 0)' });
+ assert_equals(anim.effect.getComputedTiming().easing,
+ 'cubic-bezier(0, ' + max_float + ', 0, 0)',
+ 'y1 control point for effect easing is out of upper boundary');
+
+ anim.effect.updateTiming({ easing: 'cubic-bezier(0, 0, 0, 1e+39)' });
+ assert_equals(anim.effect.getComputedTiming().easing,
+ 'cubic-bezier(0, 0, 0, ' + max_float + ')',
+ 'y2 control point for effect easing is out of upper boundary');
+
+ anim.effect.updateTiming({ easing: 'cubic-bezier(0, -1e+39, 0, 0)' });
+ assert_equals(anim.effect.getComputedTiming().easing,
+ 'cubic-bezier(0, ' + '-' + max_float + ', 0, 0)',
+ 'y1 control point for effect easing is out of lower boundary');
+
+ anim.effect.updateTiming({ easing: 'cubic-bezier(0, 0, 0, -1e+39)' });
+ assert_equals(anim.effect.getComputedTiming().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.getTiming().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.getTiming().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.getTiming().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.getTiming().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.updateTiming({ 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.updateTiming({ 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');
+
+</script>
+</body>
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..8b3d293f02
--- /dev/null
+++ b/dom/animation/test/mozilla/test_deferred_start.html
@@ -0,0 +1,19 @@
+<!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.timelines.enabled", true],
+ ],
+ },
+ function() {
+ window.open("file_deferred_start.html");
+ }
+);
+</script>
diff --git a/dom/animation/test/mozilla/test_disable_animations_api_compositing.html b/dom/animation/test/mozilla/test_disable_animations_api_compositing.html
new file mode 100644
index 0000000000..94216ea62d
--- /dev/null
+++ b/dom/animation/test/mozilla/test_disable_animations_api_compositing.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.compositing.enabled", false]]},
+ function() {
+ window.open("file_disable_animations_api_compositing.html");
+ });
+</script>
diff --git a/dom/animation/test/mozilla/test_disable_animations_api_timelines.html b/dom/animation/test/mozilla/test_disable_animations_api_timelines.html
new file mode 100644
index 0000000000..a20adf4ea2
--- /dev/null
+++ b/dom/animation/test/mozilla/test_disable_animations_api_timelines.html
@@ -0,0 +1,16 @@
+<!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.timelines.enabled', false]] },
+ function() {
+ window.open('file_disable_animations_api_timelines.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..2244143ceb
--- /dev/null
+++ b/dom/animation/test/mozilla/test_disabled_properties.html
@@ -0,0 +1,73 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../testcommon.js"></script>
+<body>
+<div id="log"></div>
+<script>
+'use strict';
+
+function waitForSetPref(pref, value) {
+ return SpecialPowers.pushPrefEnv({ 'set': [[pref, value]] });
+}
+
+/*
+ * These tests rely on the fact that the overflow-clip-box property is
+ * disabled by the layout.css.overflow-clip-box.enabled 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.overflow-clip-box.enabled', true).then(() => {
+ var anim = addDiv(t).animate({ overflowClipBoxBlock: [ 'padding-box', 'content-box' ]});
+ assert_equals(anim.effect.getKeyframes().length, 2,
+ 'A property-indexed keyframe specifying only enabled'
+ + ' properties produces keyframes');
+ return waitForSetPref('layout.css.overflow-clip-box.enabled', false);
+ }).then(() => {
+ var anim = addDiv(t).animate({ overflowClipBoxBlock: [ 'padding-box', 'content-box' ]});
+ 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([ { overflowClipBoxBlock: 'padding-box' },
+ { overflowClipBoxBlock: 'content-box' } ]);
+ 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.overflow-clip-box.enabled', true).then(() => {
+ var anim = createAnim();
+ assert_has_property(anim, 0, 'Initial keyframe', 'overflowClipBoxBlock');
+ assert_has_property(anim, 1, 'Final keyframe', 'overflowClipBoxBlock');
+ return waitForSetPref('layout.css.overflow-clip-box.enabled', false);
+ }).then(() => {
+ var anim = createAnim();
+ assert_does_not_have_property(anim, 0, 'Initial keyframe',
+ 'overflowClipBoxBlock');
+ assert_does_not_have_property(anim, 1, 'Final keyframe',
+ 'overflowClipBoxBlock');
+ });
+}, 'Specifying a disabled property using a keyframe sequence');
+
+</script>
+</body>
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..d4826a74bd
--- /dev/null
+++ b/dom/animation/test/mozilla/test_discrete_animations.html
@@ -0,0 +1,16 @@
+<!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": [
+ ["layout.css.osx-font-smoothing.enabled", true],
+ ] },
+ function() {
+ window.open("file_discrete_animations.html");
+ });
+</script>
diff --git a/dom/animation/test/mozilla/test_distance_of_basic_shape.html b/dom/animation/test/mozilla/test_distance_of_basic_shape.html
new file mode 100644
index 0000000000..65e403bf06
--- /dev/null
+++ b/dom/animation/test/mozilla/test_distance_of_basic_shape.html
@@ -0,0 +1,91 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src='/resources/testharness.js'></script>
+<script src='/resources/testharnessreport.js'></script>
+<script src='../testcommon.js'></script>
+<div id='log'></div>
+<script type='text/javascript'>
+'use strict';
+
+// We don't have an official spec to define the distance between two basic
+// shapes, but we still need this for DevTools, so Gecko and Servo backends use
+// the similar rules to define the distance. If there is a spec for it, we have
+// to update this test file.
+// See https://github.com/w3c/csswg-drafts/issues/662.
+
+test(function(t) {
+ var target = addDiv(t);
+ var dist = getDistance(target, 'clip-path', 'none', 'none');
+ assert_equals(dist, 0, 'none and none');
+}, 'none and none');
+
+test(function(t) {
+ var target = addDiv(t);
+ var dist = getDistance(target, 'clip-path', 'circle(10px)', 'circle(20px)');
+ assert_equals(dist, 10, 'circle(10px) and circle(20px)');
+}, 'circles');
+
+test(function(t) {
+ var target = addDiv(t);
+ var circle1 = 'circle(calc(10px + 10px) at 20px 10px)';
+ var circle2 = 'circle(30px at 10px 10px)';
+ var dist = getDistance(target, 'clip-path', circle1, circle2);
+ assert_equals(dist,
+ Math.sqrt(10 * 10 + 10 * 10),
+ circle1 + ' and ' + circle2);
+}, 'circles with positions');
+
+test(function(t) {
+ var target = addDiv(t);
+ var ellipse1 = 'ellipse(20px calc(10px + 10px))';
+ var ellipse2 = 'ellipse(30px 30px)';
+ var dist = getDistance(target, 'clip-path', ellipse1, ellipse2);
+ assert_equals(dist,
+ Math.sqrt(10 * 10 + 10 * 10),
+ ellipse1 + ' and ' + ellipse2);
+}, 'ellipses');
+
+test(function(t) {
+ var target = addDiv(t);
+ var polygon1 = 'polygon(50px 0px, 100px 50px, 50px 100px, 0px 50px)';
+ var polygon2 = 'polygon(40px 0px, 100px 70px, 10px 100px, 0px 70px)';
+ var dist = getDistance(target, 'clip-path', polygon1, polygon2);
+ assert_equals(dist,
+ Math.sqrt(10 * 10 + 20 * 20 + 40 * 40 + 20 * 20),
+ polygon1 + ' and ' + polygon2);
+}, 'polygons');
+
+test(function(t) {
+ var target = addDiv(t);
+ var inset1 = 'inset(5px 5px 5px 5px round 40px 30px 20px 5px)';
+ var inset2 = 'inset(10px 5px round 50px 60px)';
+ var dist = getDistance(target, 'clip-path', inset1, inset2);
+
+ // if we have only two parameter in inset(), the first one means
+ // top and bottom edges, and the second one means left and right edges.
+ // and the definitions of inset is inset(top, right, bottom, left). Besides,
+ // the "round" part uses the shorthand of border radius for each corner, so
+ // each corner is a pair of (x, y). We are computing the distance between:
+ // 1. inset(5px 5px 5px 5px
+ // round (40px 40px) (30px 30px) (20px 20px) (5px 5px))
+ // 2. inset(10px 5px 10px 5px
+ // round (50px 50px) (60px 60px) (50px 50px) (60px 60px))
+ // That is why we need to multiply 2 for each border-radius corner.
+ assert_equals(dist,
+ Math.sqrt(5 * 5 + 5 * 5 +
+ (50 - 40) * (50 - 40) * 2 +
+ (60 - 30) * (60 - 30) * 2 +
+ (50 - 20) * (50 - 20) * 2 +
+ (60 - 5) * (60 - 5) * 2),
+ inset1 + ' and ' + inset2);
+}, 'insets');
+
+test(function(t) {
+ var target = addDiv(t);
+ var dist = getDistance(target, 'clip-path',
+ 'circle(20px)', 'ellipse(10px 20px)');
+ assert_equals(dist, 0, 'circle(20px) and ellipse(10px 20px)');
+}, 'Mismatched basic shapes');
+
+</script>
+</html>
diff --git a/dom/animation/test/mozilla/test_distance_of_filter.html b/dom/animation/test/mozilla/test_distance_of_filter.html
new file mode 100644
index 0000000000..33f772d983
--- /dev/null
+++ b/dom/animation/test/mozilla/test_distance_of_filter.html
@@ -0,0 +1,248 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src='/resources/testharness.js'></script>
+<script src='/resources/testharnessreport.js'></script>
+<script src='../testcommon.js'></script>
+<div id='log'></div>
+<script type='text/javascript'>
+'use strict';
+
+const EPSILON = 1e-6;
+
+// We don't have an official spec to define the distance between two filter
+// lists, but we still need this for DevTools, so Gecko and Servo backends use
+// the similar rules to define the distance. If there is a spec for it, we have
+// to update this test file.
+// See https://github.com/w3c/fxtf-drafts/issues/91.
+
+test(function(t) {
+ var target = addDiv(t);
+ var dist = getDistance(target, 'filter', 'none', 'none');
+ assert_equals(dist, 0, 'none and none');
+}, 'none and none');
+
+test(function(t) {
+ var target = addDiv(t);
+ var dist = getDistance(target, 'filter', 'blur(10px)', 'none');
+ // The default value of blur is 0px.
+ assert_equals(dist, 10, 'blur(10px) and none');
+}, 'blur and none');
+
+test(function(t) {
+ var target = addDiv(t);
+ var dist = getDistance(target, 'filter', 'blur(10px)', 'blur(1px)');
+ assert_equals(dist, 9, 'blur(10px) and blur(1px)');
+}, 'blurs');
+
+test(function(t) {
+ var target = addDiv(t);
+ var dist = getDistance(target, 'filter', 'brightness(75%)', 'none');
+ // The default value of brightness is 100%.
+ assert_equals(dist, (1 - 0.75), 'brightness(75%) and none');
+}, 'brightness and none');
+
+test(function(t) {
+ var target = addDiv(t);
+ var dist = getDistance(target, 'filter',
+ 'brightness(50%)', 'brightness(175%)');
+ assert_equals(dist, (1.75 - 0.5), 'brightness(50%) and brightness(175%)');
+}, 'brightnesses');
+
+test(function(t) {
+ var target = addDiv(t);
+ var dist = getDistance(target, 'filter', 'contrast(75%)', 'none');
+ // The default value of contrast is 100%.
+ assert_equals(dist, (1 - 0.75), 'contrast(75%) and none');
+}, 'contrast and none');
+
+test(function(t) {
+ var target = addDiv(t);
+ var dist = getDistance(target, 'filter', 'contrast(50%)', 'contrast(175%)');
+ assert_equals(dist, (1.75 - 0.5), 'contrast(50%) and contrast(175%)');
+}, 'contrasts');
+
+test(function(t) {
+ var target = addDiv(t);
+ var filter1 = 'drop-shadow(10px 10px 10px blue)';
+ var filter2 = 'none';
+ var dist = getDistance(target, 'filter', filter1, filter2);
+ // The rgba of Blue is rgba(0, 0, 255, 1.0) = rgba(0%, 0%, 100%, 100%).
+ // So we are try to compute the distance of
+ // 1. drop-shadow(10, 10, 10, rgba(0, 0, 1.0, 1.0)).
+ // 2. drop-shadow( 0, 0, 0, rgba(0, 0, 0, 0)).
+ assert_equals(dist,
+ Math.sqrt(10 * 10 * 3 + (1 * 1 + 1 * 1)),
+ filter1 + ' and ' + filter2);
+}, 'drop-shadow and none');
+
+test(function(t) {
+ var target = addDiv(t);
+ var filter1 = 'drop-shadow(10px 10px 10px blue)';
+ var filter2 = 'drop-shadow(5px 5px 1px yellow)';
+ var dist = getDistance(target, 'filter', filter1, filter2);
+ // Blue: rgba(0, 0, 255, 1.0) = rgba( 0%, 0%, 100%, 100%).
+ // Yellow: rgba(255, 255, 0, 1.0) = rgba(100%, 100%, 0%, 100%).
+ assert_equals(dist,
+ Math.sqrt(5 * 5 * 2 + 9 * 9 + (1 * 1 * 3)),
+ filter1 + ' and ' + filter2);
+}, 'drop-shadows');
+
+test(function(t) {
+ var target = addDiv(t);
+ var filter1 = 'drop-shadow(10px 10px 10px)';
+ var filter2 = 'drop-shadow(5px 5px 1px yellow)';
+ var dist = getDistance(target, 'filter', filter1, filter2);
+ // Yellow: rgba(255, 255, 0, 1.0) = rgba(100%, 100%, 0%, 100%)
+ // Transparent: rgba(0, 0, 0, 0) = rgba( 0%, 0%, 0%, 0%)
+ // Distance involving `currentcolor` is calculated as distance
+ // from `transparent`
+ assert_equals(dist,
+ Math.sqrt(5 * 5 * 2 + 9 * 9 + (1 * 1 * 3)),
+ filter1 + ' and ' + filter2);
+}, 'drop-shadows with color and non-color');
+
+test(function(t) {
+ var target = addDiv(t);
+ var dist = getDistance(target, 'filter', 'grayscale(25%)', 'none');
+ // The default value of grayscale is 0%.
+ assert_equals(dist, 0.25, 'grayscale(25%) and none');
+}, 'grayscale and none');
+
+test(function(t) {
+ var target = addDiv(t);
+ var dist = getDistance(target, 'filter', 'grayscale(50%)', 'grayscale(75%)');
+ assert_equals(dist, 0.25, 'grayscale(50%) and grayscale(75%)');
+}, 'grayscales');
+
+test(function(t) {
+ var target = addDiv(t);
+ var dist = getDistance(target, 'filter', 'grayscale(75%)', 'grayscale(175%)');
+ assert_equals(dist, 0.25, 'distance of grayscale(75%) and grayscale(175%)');
+}, 'grayscales where one has a value larger than 1.0');
+
+test(function(t) {
+ var target = addDiv(t);
+ var dist = getDistance(target, 'filter', 'hue-rotate(180deg)', 'none');
+ // The default value of hue-rotate is 0deg.
+ assert_approx_equals(dist, Math.PI, EPSILON, 'hue-rotate(180deg) and none');
+}, 'hue-rotate and none');
+
+test(function(t) {
+ var target = addDiv(t);
+ var dist = getDistance(target, 'filter',
+ 'hue-rotate(720deg)', 'hue-rotate(-180deg)');
+ assert_approx_equals(dist, 5 * Math.PI, EPSILON,
+ 'hue-rotate(720deg) and hue-rotate(-180deg)');
+}, 'hue-rotates');
+
+test(function(t) {
+ var target = addDiv(t);
+ var dist = getDistance(target, 'filter', 'invert(25%)', 'none');
+ // The default value of invert is 0%.
+ assert_equals(dist, 0.25, 'invert(25%) and none');
+}, 'invert and none');
+
+test(function(t) {
+ var target = addDiv(t);
+ var dist = getDistance(target, 'filter', 'invert(50%)', 'invert(75%)');
+ assert_equals(dist, 0.25, 'invert(50%) and invert(75%)');
+}, 'inverts');
+
+test(function(t) {
+ var target = addDiv(t);
+ var dist = getDistance(target, 'filter', 'invert(75%)', 'invert(175%)');
+ assert_equals(dist, 0.25, 'invert(75%) and invert(175%)');
+}, 'inverts where one has a value larger than 1.0');
+
+test(function(t) {
+ var target = addDiv(t);
+ var dist = getDistance(target, 'filter', 'opacity(75%)', 'none');
+ // The default value of opacity is 100%.
+ assert_equals(dist, (1 - 0.75), 'opacity(75%) and none');
+}, 'opacity and none');
+
+test(function(t) {
+ var target = addDiv(t);
+ var dist = getDistance(target, 'filter', 'opacity(50%)', 'opacity(75%)');
+ assert_equals(dist, 0.25, 'opacity(50%) and opacity(75%)');
+}, 'opacities');
+
+test(function(t) {
+ var target = addDiv(t);
+ var dist = getDistance(target, 'filter', 'opacity(75%)', 'opacity(175%)');
+ assert_equals(dist, 0.25, 'opacity(75%) and opacity(175%)');
+}, 'opacities where one has a value larger than 1.0');
+
+test(function(t) {
+ var target = addDiv(t);
+ var dist = getDistance(target, 'filter', 'saturate(75%)', 'none');
+ // The default value of saturate is 100%.
+ assert_equals(dist, (1 - 0.75), 'saturate(75%) and none');
+}, 'saturate and none');
+
+test(function(t) {
+ var target = addDiv(t);
+ var dist = getDistance(target, 'filter', 'saturate(50%)', 'saturate(175%)');
+ assert_equals(dist, (1.75 - 0.5), 'saturate(50%) and saturate(175%)');
+}, 'saturates');
+
+test(function(t) {
+ var target = addDiv(t);
+ var dist = getDistance(target, 'filter', 'sepia(25%)', 'none');
+ // The default value of sepia is 0%.
+ assert_equals(dist, 0.25, 'sepia(25%) and none');
+}, 'sepia and none');
+
+test(function(t) {
+ var target = addDiv(t);
+ var dist = getDistance(target, 'filter', 'sepia(50%)', 'sepia(75%)');
+ assert_equals(dist, 0.25, 'sepia(50%) and sepia(75%)');
+}, 'sepias');
+
+test(function(t) {
+ var target = addDiv(t);
+ var dist = getDistance(target, 'filter', 'sepia(75%)', 'sepia(175%)');
+ assert_equals(dist, 0.25, 'sepia(75%) and sepia(175%)');
+}, 'sepias where one has a value larger than 1.0');
+
+test(function(t) {
+ var target = addDiv(t);
+ var filter1 = 'grayscale(50%) opacity(100%) blur(5px)';
+ // none filter: 'grayscale(0) opacity(1) blur(0px)'
+ var filter2 = 'none';
+ var dist = getDistance(target, 'filter', filter1, filter2);
+ assert_equals(dist,
+ Math.sqrt(0.5 * 0.5 + 5 * 5),
+ filter1 + ' and ' + filter2);
+}, 'Filter list and none');
+
+test(function(t) {
+ var target = addDiv(t);
+ var filter1 = 'grayscale(50%) opacity(100%) blur(5px)';
+ var filter2 = 'grayscale(100%) opacity(50%) blur(1px)';
+ var dist = getDistance(target, 'filter', filter1, filter2);
+ assert_equals(dist,
+ Math.sqrt(0.5 * 0.5 + 0.5 * 0.5 + 4 * 4),
+ filter1 + ' and ' + filter2);
+}, 'Filter lists');
+
+test(function(t) {
+ var target = addDiv(t);
+ var filter1 = 'grayscale(50%) opacity(100%) blur(5px)';
+ var filter2 = 'grayscale(100%) opacity(50%) blur(1px) sepia(50%)';
+ var dist = getDistance(target, 'filter', filter1, filter2);
+ assert_equals(dist,
+ Math.sqrt(0.5 * 0.5 + 0.5 * 0.5 + 4 * 4 + 0.5 * 0.5),
+ filter1 + ' and ' + filter2);
+}, 'Filter lists where one has extra functions');
+
+test(function(t) {
+ var target = addDiv(t);
+ var filter1 = 'grayscale(50%) opacity(100%)';
+ var filter2 = 'opacity(100%) grayscale(50%)';
+ var dist = getDistance(target, 'filter', filter1, filter2);
+ assert_equals(dist, 0, filter1 + ' and ' + filter2);
+}, 'Mismatched filter lists');
+
+</script>
+</html>
diff --git a/dom/animation/test/mozilla/test_distance_of_path_function.html b/dom/animation/test/mozilla/test_distance_of_path_function.html
new file mode 100644
index 0000000000..af6592c892
--- /dev/null
+++ b/dom/animation/test/mozilla/test_distance_of_path_function.html
@@ -0,0 +1,140 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src='/resources/testharness.js'></script>
+<script src='/resources/testharnessreport.js'></script>
+<script src='../testcommon.js'></script>
+<div id="log"></div>
+<script type='text/javascript'>
+'use strict';
+
+test(function(t) {
+ var target = addDiv(t);
+ var dist = getDistance(target, 'offset-path', 'none', 'none');
+ assert_equals(dist, 0, 'none and none');
+}, 'none and none');
+
+test(function(t) {
+ var target = addDiv(t);
+ var dist = getDistance(target, 'offset-path', 'path("M 10 10")', 'none');
+ assert_equals(dist, 0, 'path("M 10 10") and none');
+}, 'Path and none');
+
+test(function(t) {
+ var target = addDiv(t);
+ var dist = getDistance(target, 'offset-path',
+ 'path("M 10 10 H 10")',
+ 'path("M 10 10 H 10 H 10")');
+ assert_equals(dist, 0, 'path("M 10 10 H 10") and ' +
+ 'path("M 10 10 H 10 H 10")');
+}, 'Mismatched path functions');
+
+test(function(t) {
+ var target = addDiv(t);
+ var dist = getDistance(target, 'offset-path',
+ 'path("M 10 10")',
+ 'path("M 20 20")');
+ assert_equals(dist,
+ Math.sqrt(10 * 10 * 2),
+ 'path("M 10 10") and path("M 30 30")');
+}, 'The moveto commands');
+
+test(function(t) {
+ var target = addDiv(t);
+ var dist = getDistance(target, 'offset-path',
+ 'path("M 0 0 L 10 10")',
+ 'path("M 0 0 L 20 20")');
+ assert_equals(dist,
+ Math.sqrt(10 * 10 * 2),
+ 'path("M 0 0 L 10 10") and path("M 0 0 L 20 20")');
+}, 'The lineto commands');
+
+test(function(t) {
+ var target = addDiv(t);
+ var dist = getDistance(target, 'offset-path',
+ 'path("M 0 0 H 10")',
+ 'path("M 0 0 H 20")');
+ assert_equals(dist, 10, 'path("M 0 0 H 10") and path("M 0 0 H 20")');
+}, 'The horizontal lineto commands');
+
+test(function(t) {
+ var target = addDiv(t);
+ var dist = getDistance(target, 'offset-path',
+ 'path("M 0 0 V 10")',
+ 'path("M 0 0 V 20")');
+ assert_equals(dist, 10, 'path("M 0 0 V 10") and path("M 0 0 V 20")');
+}, 'The vertical lineto commands');
+
+test(function(t) {
+ var target = addDiv(t);
+ var dist = getDistance(target, 'offset-path',
+ 'path("M 0 0 C 10 10 20 20 30 30")',
+ 'path("M 0 0 C 20 20 40 40 0 0")');
+ assert_equals(dist,
+ Math.sqrt(10 * 10 * 2 + 20 * 20 * 2 + 30 * 30 * 2),
+ 'path("M 0 0 C 10 10 20 20 30 30") and ' +
+ 'path("M 0 0 C 20 20 40 40 0 0")');
+}, 'The cubic Bézier curve commands');
+
+test(function(t) {
+ var target = addDiv(t);
+ var dist = getDistance(target, 'offset-path',
+ 'path("M 0 0 S 20 20 30 30")',
+ 'path("M 0 0 S 40 40 0 0")');
+ assert_equals(dist,
+ Math.sqrt(20 * 20 * 2 + 30 * 30 * 2),
+ 'path("M 0 0 S 20 20 30 30") and ' +
+ 'path("M 0 0 S 40 40 0 0")');
+}, 'The smooth cubic Bézier curve commands');
+
+test(function(t) {
+ var target = addDiv(t);
+ var dist = getDistance(target, 'offset-path',
+ 'path("M 0 0 Q 10 10 30 30")',
+ 'path("M 0 0 Q 20 20 0 0")');
+ assert_equals(dist,
+ Math.sqrt(10 * 10 * 2 + 30 * 30 * 2),
+ 'path("M 0 0 Q 10 10 30 30") and ' +
+ 'path("M 0 0 Q 20 20 0 0")');
+}, 'The quadratic cubic Bézier curve commands');
+
+test(function(t) {
+ var target = addDiv(t);
+ var dist = getDistance(target, 'offset-path',
+ 'path("M 0 0 T 30 30")',
+ 'path("M 0 0 T 0 0")');
+ assert_equals(dist,
+ Math.sqrt(30 * 30 * 2),
+ 'path("M 0 0 T 30 30") and ' +
+ 'path("M 0 0 T 0 0")');
+}, 'The smooth quadratic cubic Bézier curve commands');
+
+test(function(t) {
+ var target = addDiv(t);
+ var dist = getDistance(target, 'offset-path',
+ 'path("M 0 0 A 5 5 10 0 1 30 30")',
+ 'path("M 0 0 A 4 4 5 0 0 20 20")');
+ assert_equals(dist,
+ Math.sqrt(1 * 1 * 2 + // radii
+ 5 * 5 + // angle
+ 1 * 1 + // flag
+ 10 * 10 * 2),
+ 'path("M 0 0 A 5 5 10 0 1 30 30") and ' +
+ 'path("M 0 0 A 4 4 5 0 0 20 20")');
+}, 'The elliptical arc curve commands');
+
+test(function(t) {
+ var target = addDiv(t);
+ var dist = getDistance(target, 'offset-path',
+ 'path("m 10 20 h 30 v 60 h 10 v -10 l 110 60")',
+ // == 'path("M 10 20 H 40 V 80 H 50 V 70 L 160 130")'
+ 'path("M 130 140 H 120 V 160 H 130 V 150 L 200 170")');
+ assert_equals(dist,
+ Math.sqrt(120 * 120 * 2 +
+ 80 * 80 * 4 +
+ 40 * 40 * 2),
+ 'path("m 10 20 h 30 v 60 h 10 v -10 l 110 60") and ' +
+ 'path("M 130 140 H 120 V 160 H 130 V 150 L 200 170")');
+}, 'The distance of paths with absolute and relative coordinates');
+
+</script>
+</html>
diff --git a/dom/animation/test/mozilla/test_distance_of_transform.html b/dom/animation/test/mozilla/test_distance_of_transform.html
new file mode 100644
index 0000000000..96ff1eb66d
--- /dev/null
+++ b/dom/animation/test/mozilla/test_distance_of_transform.html
@@ -0,0 +1,404 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src='/resources/testharness.js'></script>
+<script src='/resources/testharnessreport.js'></script>
+<script src='../testcommon.js'></script>
+<div id='log'></div>
+<script type='text/javascript'>
+'use strict';
+
+// We don't have an official spec to define the distance between two transform
+// lists, but we still need this for DevTools, so Gecko and Servo backend use
+// the similar rules to define the distance. If there is a spec for it, we have
+// to update this test file.
+
+const EPSILON = 0.00001;
+
+// |v| should be a unit vector (i.e. having length 1)
+function getQuaternion(v, angle) {
+ return [
+ v[0] * Math.sin(angle / 2.0),
+ v[1] * Math.sin(angle / 2.0),
+ v[2] * Math.sin(angle / 2.0),
+ Math.cos(angle / 2.0)
+ ];
+}
+
+function computeRotateDistance(q1, q2) {
+ const dot = q1.reduce((sum, e, i) => sum + e * q2[i], 0);
+ return Math.acos(Math.min(Math.max(dot, -1.0), 1.0)) * 2.0;
+}
+
+function createMatrixFromArray(array) {
+ return (array.length === 16 ? 'matrix3d' : 'matrix') + `(${array.join()})`;
+}
+
+function rotate3dToMatrix(x, y, z, radian) {
+ var sc = Math.sin(radian / 2) * Math.cos(radian / 2);
+ var sq = Math.sin(radian / 2) * Math.sin(radian / 2);
+
+ // Normalize the vector.
+ var length = Math.sqrt(x*x + y*y + z*z);
+ x /= length;
+ y /= length;
+ z /= length;
+
+ return [
+ 1 - 2 * (y*y + z*z) * sq,
+ 2 * (x * y * sq + z * sc),
+ 2 * (x * z * sq - y * sc),
+ 0,
+ 2 * (x * y * sq - z * sc),
+ 1 - 2 * (x*x + z*z) * sq,
+ 2 * (y * z * sq + x * sc),
+ 0,
+ 2 * (x * z * sq + y * sc),
+ 2 * (y * z * sq - x * sc),
+ 1 - 2 * (x*x + y*y) * sq,
+ 0,
+ 0,
+ 0,
+ 0,
+ 1
+ ];
+}
+
+test(function(t) {
+ var target = addDiv(t);
+ var dist = getDistance(target, 'transform', 'none', 'none');
+ assert_equals(dist, 0, 'distance of translate');
+}, 'Test distance of none and none');
+
+test(function(t) {
+ var target = addDiv(t);
+ var dist = getDistance(target, 'transform', 'translate(100px)', 'none');
+ assert_equals(dist, 100, 'distance of translate');
+}, 'Test distance of translate function and none');
+
+test(function(t) {
+ var target = addDiv(t);
+ var dist =
+ getDistance(target, 'transform', 'translate(100px)', 'translate(200px)');
+ assert_equals(dist, 200 - 100, 'distance of translate');
+}, 'Test distance of translate functions');
+
+test(function(t) {
+ var target = addDiv(t);
+ var dist =
+ getDistance(target, 'transform', 'translate3d(100px, 0, 50px)', 'none');
+ assert_equals(dist, Math.sqrt(100 * 100 + 50 * 50),
+ 'distance of translate3d');
+}, 'Test distance of translate3d function and none');
+
+test(function(t) {
+ var target = addDiv(t);
+ var dist =
+ getDistance(target, 'transform',
+ 'translate3d(100px, 0, 50px)',
+ 'translate3d(200px, 80px, 0)');
+ assert_equals(dist, Math.sqrt(100 * 100 + 80 * 80 + 50 * 50),
+ 'distance of translate');
+}, 'Test distance of translate3d functions');
+
+test(function(t) {
+ var target = addDiv(t);
+ var dist = getDistance(target, 'transform', 'scale(1.5)', 'none');
+ assert_equals(dist, Math.sqrt(0.5 * 0.5 + 0.5 * 0.5), 'distance of scale');
+}, 'Test distance of scale function and none');
+
+test(function(t) {
+ var target = addDiv(t);
+ var dist = getDistance(target, 'transform', 'scale(1.5)', 'scale(2.0)');
+ assert_equals(dist, Math.sqrt(0.5 * 0.5 + 0.5 * 0.5), 'distance of scale');
+}, 'Test distance of scale functions');
+
+test(function(t) {
+ var target = addDiv(t);
+ var dist = getDistance(target, 'transform',
+ 'scale3d(1.5, 1.5, 1.5)',
+ 'none');
+ assert_equals(dist,
+ Math.sqrt(0.5 * 0.5 + 0.5 * 0.5 + 0.5 * 0.5),
+ 'distance of scale3d');
+}, 'Test distance of scale3d function and none');
+
+test(function(t) {
+ var target = addDiv(t);
+ var dist = getDistance(target, 'transform',
+ 'scale3d(1.5, 1.5, 1.5)',
+ 'scale3d(2.0, 2.0, 1.0)');
+ assert_equals(dist,
+ Math.sqrt(0.5 * 0.5 + 0.5 * 0.5 + 0.5 * 0.5),
+ 'distance of scale3d');
+}, 'Test distance of scale3d functions');
+
+test(function(t) {
+ var target = addDiv(t);
+ var dist =
+ getDistance(target, 'transform', 'rotate(45deg)', 'rotate(90deg)');
+ assert_approx_equals(dist, Math.PI / 2.0 - Math.PI / 4.0, EPSILON, 'distance of rotate');
+}, 'Test distance of rotate functions');
+
+test(function(t) {
+ var target = addDiv(t);
+ var dist =
+ getDistance(target, 'transform', 'rotate(45deg)', 'none');
+ assert_approx_equals(dist, Math.PI / 4.0, EPSILON, 'distance of rotate');
+}, 'Test distance of rotate function and none');
+
+test(function(t) {
+ var target = addDiv(t);
+ var dist = getDistance(target, 'transform',
+ 'rotate3d(0, 1, 0, 90deg)',
+ 'none');
+ assert_approx_equals(dist, Math.PI / 2, EPSILON, 'distance of rotate3d');
+}, 'Test distance of rotate3d function and none');
+
+test(function(t) {
+ var target = addDiv(t);
+ var dist = getDistance(target, 'transform',
+ 'rotate3d(0, 0, 1, 90deg)',
+ 'rotate3d(1, 0, 0, 90deg)');
+ let q1 = getQuaternion([0, 0, 1], Math.PI / 2.0);
+ let q2 = getQuaternion([1, 0, 0], Math.PI / 2.0);
+ assert_approx_equals(dist, computeRotateDistance(q1, q2), EPSILON, 'distance of rotate3d');
+}, 'Test distance of rotate3d functions');
+
+test(function(t) {
+ var target = addDiv(t);
+ var dist = getDistance(target, 'transform',
+ 'rotate3d(0, 0, 1, 90deg)',
+ 'rotate3d(0, 0, 0, 90deg)');
+ assert_approx_equals(dist, Math.PI / 2, EPSILON, 'distance of rotate3d');
+}, 'Test distance of rotate3d functions whose direction vector cannot be ' +
+ 'normalized');
+
+test(function(t) {
+ var target = addDiv(t);
+ var dist = getDistance(target, 'transform', 'skew(1rad, 0.5rad)', 'none');
+ assert_approx_equals(dist, Math.sqrt(1 * 1 + 0.5 * 0.5), EPSILON, 'distance of skew');
+}, 'Test distance of skew function and none');
+
+test(function(t) {
+ var target = addDiv(t);
+ var dist = getDistance(target, 'transform',
+ 'skew(1rad, 0.5rad)',
+ 'skew(-1rad, 0)');
+ assert_approx_equals(dist, Math.sqrt(2 * 2 + 0.5 * 0.5), EPSILON, 'distance of skew');
+}, 'Test distance of skew functions');
+
+test(function(t) {
+ var target = addDiv(t);
+ var dist = getDistance(target, 'transform',
+ 'perspective(128px)',
+ 'none');
+ assert_equals(dist, Infinity, 'distance of perspective');
+}, 'Test distance of perspective function and none');
+
+test(function(t) {
+ var target = addDiv(t);
+ // perspective(0) is treated as perspective(inf) because perspective length
+ // should be greater than or equal to zero.
+ var dist = getDistance(target, 'transform',
+ 'perspective(128px)',
+ 'perspective(0)');
+ assert_equals(dist, 128, 'distance of perspective');
+}, 'Test distance of perspective function and an invalid perspective');
+
+test(function(t) {
+ var target = addDiv(t);
+ var dist = getDistance(target, 'transform',
+ 'perspective(128px)',
+ 'perspective(1024px)');
+ assert_equals(dist, 1024 - 128, 'distance of perspective');
+}, 'Test distance of perspective functions');
+
+test(function(t) {
+ var target = addDiv(t);
+ var sin_30 = Math.sin(Math.PI / 6);
+ var cos_30 = Math.cos(Math.PI / 6);
+ // matrix => translate(100, 0) rotate(30deg).
+ var matrix = createMatrixFromArray([ cos_30, sin_30,
+ -sin_30, cos_30,
+ 100, 0 ]);
+ var dist = getDistance(target, 'transform', matrix, 'none');
+ assert_approx_equals(dist,
+ Math.sqrt(100 * 100 + (Math.PI / 6) * (Math.PI / 6)),
+ EPSILON,
+ 'distance of matrix');
+}, 'Test distance of matrix function and none');
+
+test(function(t) {
+ var target = addDiv(t);
+ var sin_30 = Math.sin(Math.PI / 6);
+ var cos_30 = Math.cos(Math.PI / 6);
+ // matrix1 => translate(100, 0) rotate(30deg).
+ var matrix1 = createMatrixFromArray([ cos_30, sin_30,
+ -sin_30, cos_30,
+ 100, 0 ]);
+ // matrix2 => translate(0, 100) scale(0.5).
+ var matrix2 = createMatrixFromArray([ 0.5, 0, 0, 0.5, 0, 100 ]);
+ var dist = getDistance(target, 'transform', matrix1, matrix2);
+ assert_approx_equals(dist,
+ Math.sqrt(100 * 100 + 100 * 100 + // translate
+ (Math.PI / 6) * (Math.PI / 6) + // rotate
+ 0.5 * 0.5 + 0.5 * 0.5), // scale
+ EPSILON,
+ 'distance of matrix');
+}, 'Test distance of matrix functions');
+
+test(function(t) {
+ var target = addDiv(t);
+ var matrix = createMatrixFromArray(rotate3dToMatrix(0, 1, 0, Math.PI / 6));
+ var dist = getDistance(target, 'transform', matrix, 'none');
+ assert_approx_equals(dist, Math.PI / 6, EPSILON, 'distance of matrix3d');
+}, 'Test distance of matrix3d function and none');
+
+test(function(t) {
+ var target = addDiv(t);
+ // matrix1 => rotate3d(0, 1, 0, 30deg).
+ var matrix1 = createMatrixFromArray(rotate3dToMatrix(0, 1, 0, Math.PI / 6));
+ // matrix1 => translate3d(100, 0, 0) scale3d(0.5, 0.5, 0.5).
+ var matrix2 = createMatrixFromArray([ 0.5, 0, 0, 0,
+ 0, 0.5, 0, 0,
+ 0, 0, 0.5, 0,
+ 100, 0, 0, 1 ]);
+ var dist = getDistance(target, 'transform', matrix1, matrix2);
+ assert_approx_equals(dist,
+ Math.sqrt(100 * 100 + // translate
+ 0.5 * 0.5 * 3 + // scale
+ (Math.PI / 6) * (Math.PI / 6)), // rotate
+ EPSILON,
+ 'distance of matrix');
+}, 'Test distance of matrix3d functions');
+
+test(function(t) {
+ var target = addDiv(t);
+ var cos_180 = Math.cos(Math.PI);
+ var sin_180 = Math.sin(Math.PI);
+ // matrix1 => translate3d(100px, 50px, -10px) skew(45deg).
+ var matrix1 = createMatrixFromArray([ 1, 0, 0, 0,
+ Math.tan(Math.PI/4.0), 1, 0, 0,
+ 0, 0, 1, 0,
+ 100, 50, -10, 1]);
+ // matrix2 => translate3d(1000px, 0, 0) rotate3d(1, 0, 0, 180deg).
+ var matrix2 = createMatrixFromArray([ 1, 0, 0, 0,
+ 0, cos_180, sin_180, 0,
+ 0, -sin_180, cos_180, 0,
+ 1000, 0, 0, 1 ]);
+ var dist = getDistance(target, 'transform', matrix1, matrix2);
+ assert_approx_equals(dist,
+ Math.sqrt(900 * 900 + 50 * 50 + 10 * 10 + // translate
+ Math.PI * Math.PI + // rotate
+ (Math.PI / 4) * (Math.PI / 4)), // skew angle
+ EPSILON,
+ 'distance of matrix');
+}, 'Test distance of matrix3d functions with skew factors');
+
+test(function(t) {
+ var target = addDiv(t);
+ var dist =
+ getDistance(target, 'transform',
+ 'rotate(180deg) translate(1000px)',
+ 'rotate(360deg) translate(0px)');
+ assert_approx_equals(dist, Math.sqrt(1000 * 1000 + Math.PI * Math.PI), EPSILON,
+ 'distance of transform lists');
+}, 'Test distance of transform lists');
+
+test(function(t) {
+ var target = addDiv(t);
+ var dist = getDistance(target, 'transform',
+ 'translate(100px) rotate(180deg)',
+ 'translate(50px) rotate(90deg) scale(5) skew(1rad)');
+ assert_approx_equals(dist,
+ Math.sqrt(50 * 50 +
+ Math.PI / 2 * Math.PI / 2 +
+ 4 * 4 * 2 +
+ 1 * 1),
+ EPSILON,
+ 'distance of transform lists');
+}, 'Test distance of transform lists where one has extra items');
+
+test(function(t) {
+ var target = addDiv(t);
+ var dist = getDistance(target, 'transform',
+ 'translate(1000px) rotate3d(1, 0, 0, 180deg)',
+ 'translate(1000px) scale3d(2.5, 0.5, 1)');
+ assert_equals(dist, Math.sqrt(Math.PI * Math.PI + 1.5 * 1.5 + 0.5 * 0.5),
+ 'distance of transform lists');
+}, 'Test distance of mismatched transform lists');
+
+test(function(t) {
+ var target = addDiv(t);
+ var dist = getDistance(target, 'transform',
+ 'translate(100px) skew(1rad)',
+ 'translate(1000px) rotate3d(0, 1, 0, -2rad)');
+ assert_approx_equals(dist,
+ Math.sqrt(900 * 900 + 1 * 1 + 2 * 2),
+ EPSILON,
+ 'distance of transform lists');
+}, 'Test distance of mismatched transform lists with skew function');
+
+
+// Individual transforms
+test(function(t) {
+ var target = addDiv(t);
+ var dist = getDistance(target, 'translate', '50px', 'none');
+ assert_equals(dist, Math.sqrt(50 * 50), 'distance of 2D translate and none');
+}, 'Test distance of 2D translate property with none');
+
+test(function(t) {
+ var target = addDiv(t);
+ var dist = getDistance(target, 'translate', '10px 30px', '50px');
+ assert_equals(dist, Math.sqrt(40 * 40 + 30 * 30), 'distance of 2D translate');
+}, 'Test distance of 2D translate property');
+
+test(function(t) {
+ var target = addDiv(t);
+ var dist = getDistance(target, 'translate', '10px 30px 50px', '50px');
+ assert_equals(dist, Math.sqrt(40 * 40 + 30 * 30 + 50 * 50),
+ 'distance of 3D translate');
+}, 'Test distance of 3D translate property');
+
+test(function(t) {
+ var target = addDiv(t);
+ var dist = getDistance(target, 'scale', '2', 'none');
+ assert_equals(dist, Math.sqrt(1 + 1), 'distance of 2D scale and none');
+}, 'Test distance of 2D scale property with none');
+
+test(function(t) {
+ var target = addDiv(t);
+ var dist = getDistance(target, 'scale', '3', '1 1');
+ assert_equals(dist, Math.sqrt(2 * 2 + 2 * 2), 'distance of 2D scale');
+}, 'Test distance of 2D scale property');
+
+test(function(t) {
+ var target = addDiv(t);
+ var dist = getDistance(target, 'scale', '3 2 2', '1 1');
+ assert_equals(dist, Math.sqrt(2 * 2 + 1 * 1 + 1 * 1),
+ 'distance of 3D scale');
+}, 'Test distance of 3D scale property');
+
+test(function(t) {
+ var target = addDiv(t);
+ var dist = getDistance(target, 'rotate', '180deg', 'none');
+ assert_equals(dist, Math.PI, 'distance of 2D rotate and none');
+}, 'Test distance of 2D rotate property with none');
+
+test(function(t) {
+ var target = addDiv(t);
+ var dist = getDistance(target, 'rotate', '180deg', '90deg');
+ assert_equals(dist, Math.PI / 2.0, 'distance of 2D rotate');
+}, 'Test distance of 2D rotate property');
+
+test(function(t) {
+ var target = addDiv(t);
+ var dist = getDistance(target, 'rotate', 'z 90deg', 'x 90deg');
+ let q1 = getQuaternion([0, 0, 1], Math.PI / 2.0);
+ let q2 = getQuaternion([1, 0, 0], Math.PI / 2.0);
+ assert_approx_equals(dist, computeRotateDistance(q1, q2), EPSILON,
+ 'distance of 3D rotate');
+}, 'Test distance of 3D rotate property');
+
+</script>
+</html>
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..b2aeef8a77
--- /dev/null
+++ b/dom/animation/test/mozilla/test_document_timeline_origin_time_range.html
@@ -0,0 +1,32 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../testcommon.js"></script>
+<body>
+<div id="log"></div>
+<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');
+
+</script>
+</body>
diff --git a/dom/animation/test/mozilla/test_event_listener_leaks.html b/dom/animation/test/mozilla/test_event_listener_leaks.html
new file mode 100644
index 0000000000..bcfadaf9e9
--- /dev/null
+++ b/dom/animation/test/mozilla/test_event_listener_leaks.html
@@ -0,0 +1,43 @@
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 1450271 - Test Animation event listener leak conditions</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/dom/events/test/event_leak_utils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<script class="testbody" type="text/javascript">
+// Manipulate Animation. Its important here that we create a
+// listener callback from the DOM objects back to the frame's global
+// in order to exercise the leak condition.
+async function useAnimation(contentWindow) {
+ let div = contentWindow.document.createElement("div");
+ contentWindow.document.body.appendChild(div);
+ let animation = div.animate({}, 100 * 1000);
+ is(animation.playState, "running", "animation should be running");
+ animation.onfinish = _ => {
+ contentWindow.finishCount += 1;
+ };
+}
+
+async function runTest() {
+ try {
+ await checkForEventListenerLeaks("Animation", useAnimation);
+ } catch (e) {
+ ok(false, e);
+ } finally {
+ SimpleTest.finish();
+ }
+}
+
+SimpleTest.waitForExplicitFinish();
+addEventListener("load", runTest, { once: true });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/animation/test/mozilla/test_get_animations_on_scroll_animations.html b/dom/animation/test/mozilla/test_get_animations_on_scroll_animations.html
new file mode 100644
index 0000000000..7d20e5b70b
--- /dev/null
+++ b/dom/animation/test/mozilla/test_get_animations_on_scroll_animations.html
@@ -0,0 +1,74 @@
+<!doctype html>
+<head>
+<meta charset=utf-8>
+<title>Test getAnimations() which doesn't return scroll animations</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../testcommon.js"></script>
+<style>
+ @keyframes animWidth {
+ from { width: 100px; }
+ to { width: 200px }
+ }
+ @keyframes animTop {
+ to { top: 100px }
+ }
+ .fill-vh {
+ width: 100px;
+ height: 100vh;
+ }
+</style>
+</head>
+<body>
+<div id="log"></div>
+<script>
+"use strict";
+
+test(function(t) {
+ const div = addDiv(t,
+ { style: "width: 10px; height: 100px; " +
+ "animation: animWidth 100s scroll(), animTop 200s;" });
+
+ // Sanity check to make sure the scroll animation is there.
+ addDiv(t, { class: "fill-vh" });
+ const scroller = document.scrollingElement;
+ const maxScroll = scroller.scrollHeight - scroller.clientHeight;
+ scroller.scrollTop = maxScroll;
+ assert_equals(getComputedStyle(div).width, "200px",
+ "The scroll animation is there");
+
+ const animations = div.getAnimations();
+ assert_equals(animations.length, 2,
+ 'getAnimations() should include scroll animations');
+ assert_equals(animations[0].animationName, "animWidth",
+ 'getAmimations() should return scroll animations');
+ // FIXME: Bug 1676794. Support ScrollTimeline interface.
+ assert_equals(animations[0].timeline, null,
+ 'scroll animation should not return scroll timeline');
+}, 'Element.getAnimation() should include scroll animations');
+
+test(function(t) {
+ const div = addDiv(t,
+ { style: "width: 10px; height: 100px; " +
+ "animation: animWidth 100s scroll(), animTop 100s;" });
+
+ // Sanity check to make sure the scroll animation is there.
+ addDiv(t, { class: "fill-vh" });
+ const scroller = document.scrollingElement;
+ const maxScroll = scroller.scrollHeight - scroller.clientHeight;
+ scroller.scrollTop = maxScroll;
+ assert_equals(getComputedStyle(div).width, "200px",
+ "The scroll animation is there");
+
+ const animations = document.getAnimations();
+ assert_equals(animations.length, 2,
+ 'getAnimations() should include scroll animations');
+ assert_equals(animations[0].animationName, "animWidth",
+ 'getAmimations() should return scroll animations');
+ // FIXME: Bug 1676794. Support ScrollTimeline interface.
+ assert_equals(animations[0].timeline, null,
+ 'scroll animation should not return scroll timeline');
+}, 'Document.getAnimation() should include scroll animations');
+
+</script>
+</body>
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..f36543bb1e
--- /dev/null
+++ b/dom/animation/test/mozilla/test_hide_and_show.html
@@ -0,0 +1,198 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../testcommon.js"></script>
+<style>
+@keyframes move {
+ 100% {
+ transform: translateX(100px);
+ }
+}
+
+div.pseudo::before {
+ animation: move 0.01s;
+ content: 'content';
+}
+
+</style>
+<body>
+<div id="log"></div>
+<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');
+
+promise_test(function(t) {
+ var div = addDiv(t, { 'class': 'pseudo' });
+ var eventWatcher = new EventWatcher(t, div, 'animationend');
+
+ assert_equals(document.getAnimations().length, 1,
+ 'CSS animation on pseudo element');
+
+ return eventWatcher.wait_for('animationend').then(function() {
+ assert_equals(document.getAnimations().length, 0,
+ 'No CSS animation on pseudo element after the animation ' +
+ 'finished');
+
+ // Remove the class which generated this pseudo element.
+ div.classList.remove('pseudo');
+
+ // We need to wait for two frames to process re-framing.
+ // The callback of 'animationend' is processed just before rAF callbacks,
+ // and rAF callbacks are processed before re-framing process, so waiting for
+ // one rAF callback is not sufficient.
+ return waitForAnimationFrames(2);
+ }).then(function() {
+ // Add the class again to re-generate pseudo element.
+ div.classList.add('pseudo');
+ assert_equals(document.getAnimations().length, 1,
+ 'A new CSS animation on pseudo element');
+ });
+}, 'CSS animation on pseudo element restarts after the pseudo element that ' +
+ 'had a finished CSS animation is re-generated');
+
+</script>
+</body>
diff --git a/dom/animation/test/mozilla/test_moz_prefixed_properties.html b/dom/animation/test/mozilla/test_moz_prefixed_properties.html
new file mode 100644
index 0000000000..af26f12931
--- /dev/null
+++ b/dom/animation/test/mozilla/test_moz_prefixed_properties.html
@@ -0,0 +1,90 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test animations of all properties that have -moz prefix</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="../testcommon.js"></script>
+ <script src="../property_database.js"></script>
+</head>
+<body>
+<div id="log"></div>
+<script>
+"use strict";
+
+const testcases = [
+ {
+ property: "-moz-box-align"
+ },
+ {
+ property: "-moz-box-direction"
+ },
+ {
+ property: "-moz-box-ordinal-group"
+ },
+ {
+ property: "-moz-box-orient",
+ },
+ {
+ property: "-moz-box-pack"
+ },
+ {
+ property: "-moz-float-edge"
+ },
+ {
+ property: "-moz-force-broken-image-icon"
+ },
+ {
+ property: "-moz-orient"
+ },
+ {
+ property: "-moz-osx-font-smoothing",
+ pref: "layout.css.osx-font-smoothing.enabled"
+ },
+ {
+ property: "-moz-text-size-adjust"
+ },
+ {
+ property: "-moz-user-input"
+ },
+ {
+ property: "-moz-user-modify"
+ },
+ {
+ property: "user-select"
+ },
+ {
+ property: "-moz-window-dragging"
+ },
+];
+
+testcases.forEach(testcase => {
+ if (testcase.pref && !IsCSSPropertyPrefEnabled(testcase.pref)) {
+ return;
+ }
+
+ const property = gCSSProperties[testcase.property];
+ const values = property.initial_values.concat(property.other_values);
+ values.forEach(value => {
+ test(function(t) {
+ const container = addDiv(t);
+ const target = document.createElement("div");
+ container.appendChild(target);
+
+ container.style[property.domProp] = value;
+
+ const animation =
+ target.animate({ [property.domProp]: [value, "inherit"] },
+ { duration: 1000, delay: -500 } );
+
+ const expectedValue = getComputedStyle(container)[property.domProp];
+ assert_equals(getComputedStyle(target)[property.domProp], expectedValue,
+ `Computed style shoud be "${ expectedValue }"`);
+ }, `Test inherit value for "${ testcase.property }" `
+ + `(Parent element style is "${ value }")`);
+ });
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/animation/test/mozilla/test_restyles.html b/dom/animation/test/mozilla/test_restyles.html
new file mode 100644
index 0000000000..bc1ab70c74
--- /dev/null
+++ b/dom/animation/test/mozilla/test_restyles.html
@@ -0,0 +1,22 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+<div id='log'></div>
+<script>
+'use strict';
+SimpleTest.waitForExplicitFinish();
+SimpleTest.expectAssertions(0, 1); // bug 1332970
+SpecialPowers.pushPrefEnv(
+ {
+ set: [
+ ['layout.reflow.synthMouseMove', false],
+ ['privacy.reduceTimerPrecision', false],
+ ],
+ },
+ function() {
+ window.open('file_restyles.html');
+ }
+);
+</script>
+</html>
diff --git a/dom/animation/test/mozilla/test_restyling_xhr_doc.html b/dom/animation/test/mozilla/test_restyling_xhr_doc.html
new file mode 100644
index 0000000000..67b6ac8845
--- /dev/null
+++ b/dom/animation/test/mozilla/test_restyling_xhr_doc.html
@@ -0,0 +1,106 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="../testcommon.js"></script>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+'use strict';
+
+// This test supplements the web-platform-tests in:
+//
+// web-animations/interfaces/Animatable/animate-no-browsing-context.html
+//
+// Specifically, it covers the case where we have a running animation
+// targetting an element in a document without a browsing context.
+//
+// Currently the behavior in this case is not well-defined. For example,
+// if we were to simply take an element from such a document, and do:
+//
+// const xdoc = xhr.responseXML;
+// const div = xdoc.getElementById('test');
+// div.style.opacity = '0';
+// alert(getComputedStyle(div).opacity);
+//
+// We'd get '0' in Firefox and Edge, but an empty string in Chrome.
+//
+// However, if instead of using the style attribute, we set style in a <style>
+// element in *either* the document we're calling from *or* the XHR doc and
+// do the same we get '1' in Firefox and Edge, but an empty string in Chrome.
+//
+// That is, no browser appears to apply styles to elements in a document without
+// a browsing context unless the styles are defined using the style attribute,
+// and even then Chrome does not.
+//
+// There is some prose in CSSOM which says,
+//
+// Note: This means that even if obj is in a different document (e.g. one
+// fetched via XMLHttpRequest) it will still use the style rules associated
+// with the document that is associated with the global object on which
+// getComputedStyle() was invoked to compute the CSS declaration block.[1]
+//
+// However, this text has been around since at least 2013 and does not appear
+// to be implemented.
+//
+// As a result, it's not really possible to write a cross-browser test for the
+// behavior for animations in this context since it's not clear what the result
+// should be. That said, we still want to exercise this particular code path so
+// we make this case a Mozilla-specific test. The other similar tests cases for
+// which the behavior is well-defined are covered by web-platform-tests.
+//
+// [1] https://drafts.csswg.org/cssom/#extensions-to-the-window-interface
+
+function getXHRDoc(t) {
+ return new Promise(resolve => {
+ const xhr = new XMLHttpRequest();
+ xhr.open('GET', 'xhr_doc.html');
+ xhr.responseType = 'document';
+ xhr.onload = t.step_func(() => {
+ assert_equals(xhr.readyState, xhr.DONE,
+ 'Request should complete successfully');
+ assert_equals(xhr.status, 200,
+ 'Response should be OK');
+ resolve(xhr.responseXML);
+ });
+ xhr.send();
+ });
+}
+
+promise_test(t => {
+ let anim;
+ return getXHRDoc(t).then(xhrdoc => {
+ const div = xhrdoc.getElementById('test');
+ anim = div.animate({ opacity: [ 0, 1 ] }, 1000);
+ // Give the animation an active timeline and kick-start it.
+ anim.timeline = document.timeline;
+ anim.startTime = document.timeline.currentTime;
+ assert_equals(anim.playState, 'running',
+ 'The animation should be running');
+ // Gecko currently skips applying animation styles to elements in documents
+ // without browsing contexts.
+ assert_not_equals(getComputedStyle(div).opacity, '0',
+ 'Style should NOT be updated');
+ });
+}, 'Forcing an animation targetting an element in a document without a'
+ + ' browsing context to play does not cause style to update');
+
+promise_test(t => {
+ let anim;
+ return getXHRDoc(t).then(xhrdoc => {
+ const div = addDiv(t);
+ anim = div.animate({ opacity: [ 0, 1 ] }, 1000);
+ assert_equals(getComputedStyle(div).opacity, '0',
+ 'Style should be updated');
+ // Trigger an animation restyle to be queued
+ anim.currentTime = 0.1;
+ // Adopt node into XHR doc
+ xhrdoc.body.appendChild(div);
+ // We should skip applying animation styles to elements in documents
+ // without a pres shell.
+ assert_equals(getComputedStyle(div).opacity, '1',
+ 'Style should NOT be updated');
+ });
+}, 'Moving an element with a pending animation restyle to a document without'
+ + ' a browsing context resets animation style');
+
+</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..55c77f0e8f
--- /dev/null
+++ b/dom/animation/test/mozilla/test_set_easing.html
@@ -0,0 +1,36 @@
+<!doctype html>
+<head>
+<meta charset=utf-8>
+<title>Test setting easing in sandbox</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../testcommon.js"></script>
+</head>
+<body>
+<div id="log"></div>
+<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.updateTiming({ 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.toString()})()`, sandbox);
+}, 'Setting easing should not throw any exceptions in sandbox');
+
+</script>
+</body>
diff --git a/dom/animation/test/mozilla/test_style_after_finished_on_compositor.html b/dom/animation/test/mozilla/test_style_after_finished_on_compositor.html
new file mode 100644
index 0000000000..bccae9e0d5
--- /dev/null
+++ b/dom/animation/test/mozilla/test_style_after_finished_on_compositor.html
@@ -0,0 +1,138 @@
+<!doctype html>
+<head>
+<meta charset=utf-8>
+<title>Test for styles after finished on the compositor</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../testcommon.js"></script>
+<style>
+.compositor {
+ /* Element needs geometry to be eligible for layerization */
+ width: 100px;
+ height: 100px;
+ background-color: green;
+}
+</style>
+</head>
+<body>
+<div id="log"></div>
+<script>
+"use strict";
+
+promise_test(async t => {
+ const div = addDiv(t, { 'class': 'compositor' });
+ const anim = div.animate([ { offset: 0, opacity: 1 },
+ { offset: 1, opacity: 0 } ],
+ { delay: 10,
+ duration: 100 });
+
+ await anim.finished;
+
+ await waitForNextFrame();
+
+ const opacity = SpecialPowers.DOMWindowUtils.getOMTAStyle(div, 'opacity');
+
+ assert_equals(opacity, '', 'No opacity animation runs on the compositor');
+}, 'Opacity animation with positive delay is removed from compositor when ' +
+ 'finished');
+
+promise_test(async t => {
+ const div = addDiv(t, { 'class': 'compositor' });
+ const anim = div.animate([ { offset: 0, opacity: 1 },
+ { offset: 0.9, opacity: 1 },
+ { offset: 1, opacity: 0 } ],
+ { duration: 100 });
+
+ await anim.finished;
+
+ await waitForNextFrame();
+
+ const opacity = SpecialPowers.DOMWindowUtils.getOMTAStyle(div, 'opacity');
+
+ assert_equals(opacity, '', 'No opacity animation runs on the compositor');
+}, 'Opacity animation initially opacity: 1 is removed from compositor when ' +
+ 'finished');
+
+promise_test(async t => {
+ const div = addDiv(t, { 'class': 'compositor' });
+ const anim = div.animate([ { offset: 0, opacity: 0 },
+ { offset: 0.5, opacity: 1 },
+ { offset: 0.51, opacity: 1 },
+ { offset: 1, opacity: 0 } ],
+ { delay: 10, duration: 100 });
+
+ await waitForAnimationFrames(2);
+
+ // Setting the current time at the offset generating opacity: 1.
+ anim.currentTime = 60;
+
+ await anim.finished;
+
+ await waitForNextFrame();
+
+ const opacity = SpecialPowers.DOMWindowUtils.getOMTAStyle(div, 'opacity');
+
+ assert_equals(opacity, '', 'No opacity animation runs on the compositor');
+}, 'Opacity animation is removed from compositor even when it only visits ' +
+ 'exactly the point where the opacity: 1 value was set');
+
+promise_test(async t => {
+ const div = addDiv(t, { 'class': 'compositor' });
+ const anim = div.animate([ { offset: 0, transform: 'none' },
+ { offset: 1, transform: 'translateX(100px)' } ],
+ { delay: 10,
+ duration: 100 });
+
+ await anim.finished;
+
+ await waitForNextFrame();
+
+ const transform = SpecialPowers.DOMWindowUtils.getOMTAStyle(div, 'transform');
+
+ assert_equals(transform, '', 'No transform animation runs on the compositor');
+}, 'Transform animation with positive delay is removed from compositor when ' +
+ 'finished');
+
+promise_test(async t => {
+ const div = addDiv(t, { 'class': 'compositor' });
+ const anim = div.animate([ { offset: 0, transform: 'none' },
+ { offset: 0.9, transform: 'none' },
+ { offset: 1, transform: 'translateX(100px)' } ],
+ { duration: 100 });
+
+ await anim.finished;
+
+ await waitForNextFrame();
+
+ const transform = SpecialPowers.DOMWindowUtils.getOMTAStyle(div, 'transform');
+
+ assert_equals(transform, '', 'No transform animation runs on the compositor');
+}, 'Transform animation initially transform: none is removed from compositor ' +
+ 'when finished');
+
+
+promise_test(async t => {
+ const div = addDiv(t, { 'class': 'compositor' });
+ const anim = div.animate([ { offset: 0, transform: 'translateX(100px)' },
+ { offset: 0.5, transform: 'none' },
+ { offset: 0.9, transform: 'none' },
+ { offset: 1, transform: 'translateX(100px)' } ],
+ { delay: 10, duration: 100 });
+
+ await waitForAnimationFrames(2);
+
+ // Setting the current time at the offset generating transform: none.
+ anim.currentTime = 60;
+
+ await anim.finished;
+
+ await waitForNextFrame();
+
+ const transform = SpecialPowers.DOMWindowUtils.getOMTAStyle(div, 'transform');
+
+ assert_equals(transform, '', 'No transform animation runs on the compositor');
+}, 'Transform animation is removed from compositor even when it only visits ' +
+ 'exactly the point where the transform: none value was set');
+
+</script>
+</body>
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..92d1b7e1ec
--- /dev/null
+++ b/dom/animation/test/mozilla/test_transform_limits.html
@@ -0,0 +1,56 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../testcommon.js"></script>
+<body>
+<div id="log"></div>
+<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' );
+
+</script>
+</body>
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..46a154b9af
--- /dev/null
+++ b/dom/animation/test/mozilla/test_transition_finish_on_compositor.html
@@ -0,0 +1,22 @@
+<!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});
+// This test appears like it might get racey and cause a timeout with too low of a
+// precision, so we hardcode it to something reasonable.
+SpecialPowers.pushPrefEnv(
+ {
+ set: [
+ ['privacy.reduceTimerPrecision', true],
+ ['privacy.resistFingerprinting.reduceTimerPrecision.microseconds', 2000],
+ ],
+ },
+ 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..3961305df3
--- /dev/null
+++ b/dom/animation/test/mozilla/test_underlying_discrete_value.html
@@ -0,0 +1,188 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../testcommon.js"></script>
+<body>
+<div id="log"></div>
+<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; }"
+ },
+ expectedKeyframes: [
+ { computedOffset: 0, alignContent: "flex-start" },
+ { computedOffset: 1, alignContent: "normal" }
+ ],
+ explanation: "Test for 0% keyframe only",
+ },
+ {
+ stylesheet: {
+ "@keyframes keyframes": "to { align-content: flex-end; }"
+ },
+ expectedKeyframes: [
+ { computedOffset: 0, alignContent: "normal" },
+ { 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;"
+ },
+ expectedKeyframes: [
+ { computedOffset: 0, alignContent: "normal" },
+ { computedOffset: 0.5, alignContent: "center" },
+ { computedOffset: 1, alignContent: "normal" }
+ ],
+ 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 => {
+ if (testcase.skip) {
+ return;
+ }
+ 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);
+});
+
+</script>
+</body>
diff --git a/dom/animation/test/mozilla/test_unstyled.html b/dom/animation/test/mozilla/test_unstyled.html
new file mode 100644
index 0000000000..4724979c11
--- /dev/null
+++ b/dom/animation/test/mozilla/test_unstyled.html
@@ -0,0 +1,54 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../testcommon.js"></script>
+<style>
+div.pseudo::before {
+ animation: animation 1s;
+ content: 'content';
+}
+@keyframes animation {
+ to { opacity: 0 }
+}
+</style>
+<body>
+<div id="log"></div>
+<script>
+'use strict';
+
+// Tests for cases where we may not have style data for an element
+
+promise_test(async t => {
+ // Get a CSSPseudoElement
+ const div = addDiv(t, { class: 'pseudo' });
+ const cssAnim = document.getAnimations()[0];
+ const pseudoElem = cssAnim.effect.target;
+
+ // Drop pseudo from styles and flush styles
+ div.classList.remove('pseudo');
+ getComputedStyle(div, '::before').content;
+
+ // Try animating the pseudo's content attribute
+ const contentAnim = pseudoElem.animate(
+ { content: ['none', '"content"'] },
+ { duration: 100 * MS_PER_SEC, fill: 'both' }
+ );
+
+ // Check that the initial value is as expected
+ await contentAnim.ready;
+ assert_equals(getComputedStyle(div, '::before').content, 'none');
+
+ contentAnim.finish();
+
+ // Animating an obsolete pseudo element should NOT cause the pseudo element
+ // to be re-generated. That behavior might change in which case this test
+ // will need to be updated. The most important part of this test, however,
+ // is simply checking that nothing explodes if we try to animate such a
+ // pseudo element.
+
+ assert_equals(getComputedStyle(div, '::before').content, 'none');
+}, 'Animation on an obsolete pseudo element produces expected results');
+
+</script>
+</body>
diff --git a/dom/animation/test/mozilla/xhr_doc.html b/dom/animation/test/mozilla/xhr_doc.html
new file mode 100644
index 0000000000..b9fa57e3f5
--- /dev/null
+++ b/dom/animation/test/mozilla/xhr_doc.html
@@ -0,0 +1,2 @@
+<!doctype html>
+<div id=test></div>
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..265de8f0f5
--- /dev/null
+++ b/dom/animation/test/style/test_animation-seeking-with-current-time.html
@@ -0,0 +1,123 @@
+<!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="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="../testcommon.js"></script>
+ </head>
+ <body>
+ <div id="log"></div>
+ <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() {
+ 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() {
+ // 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() {
+ // 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() {
+ // 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)');
+
+ </script>
+ </body>
+</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..e56db5f23d
--- /dev/null
+++ b/dom/animation/test/style/test_animation-seeking-with-start-time.html
@@ -0,0 +1,123 @@
+<!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="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="../testcommon.js"></script>
+ </head>
+ <body>
+ <div id="log"></div>
+ <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() {
+ 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() {
+ // 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() {
+ // 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() {
+ // 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)');
+
+ </script>
+ </body>
+</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..8712072a51
--- /dev/null
+++ b/dom/animation/test/style/test_animation-setting-effect.html
@@ -0,0 +1,127 @@
+<!doctype html>
+<html>
+ <head>
+ <meta charset=utf-8>
+ <title>Tests for setting effects by using Animation.effect</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src='../testcommon.js'></script>
+ </head>
+ <body>
+ <div id="log"></div>
+ <script type='text/javascript'>
+
+'use strict';
+
+test(function(t) {
+ var target = addDiv(t);
+ var anim = new Animation();
+ anim.effect = new KeyframeEffect(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');
+
+ </script>
+ </body>
+</html>
diff --git a/dom/animation/test/style/test_composite.html b/dom/animation/test/style/test_composite.html
new file mode 100644
index 0000000000..1383b1b1e6
--- /dev/null
+++ b/dom/animation/test/style/test_composite.html
@@ -0,0 +1,142 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../testcommon.js"></script>
+<script src="/tests/SimpleTest/paint_listener.js"></script>
+<style>
+div {
+ /* Element needs geometry to be eligible for layerization */
+ width: 20px;
+ height: 20px;
+ background-color: white;
+}
+</style>
+<body>
+<div id="log"></div>
+<script>
+'use strict';
+
+if (!SpecialPowers.DOMWindowUtils.layerManagerRemote ||
+ !SpecialPowers.getBoolPref(
+ 'layers.offmainthreadcomposition.async-animations')) {
+ // If OMTA is disabled, nothing to run.
+ done();
+}
+
+function waitForPaintsFlushed() {
+ return new Promise(function(resolve, reject) {
+ waitForAllPaintsFlushed(resolve);
+ });
+}
+
+promise_test(t => {
+ // Without this, the first test case fails on Android.
+ return waitForDocumentLoad();
+}, 'Ensure document has been loaded');
+
+promise_test(t => {
+ var div;
+ return useTestRefreshMode(t).then(() => {
+ div = addDiv(t, { style: 'transform: translateX(100px)' });
+ div.animate({ transform: ['translateX(0px)', 'translateX(200px)'],
+ composite: 'accumulate' },
+ 100 * MS_PER_SEC);
+
+ return waitForPaintsFlushed();
+ }).then(() => {
+ SpecialPowers.DOMWindowUtils.advanceTimeAndRefresh(50 * MS_PER_SEC);
+ var transform =
+ SpecialPowers.DOMWindowUtils.getOMTAStyle(div, 'transform');
+ assert_matrix_equals(transform, 'matrix(1, 0, 0, 1, 200, 0)',
+ 'Transform value at 50%');
+ });
+}, 'Accumulate onto the base value');
+
+promise_test(t => {
+ var div;
+ return useTestRefreshMode(t).then(() => {
+ div = addDiv(t);
+ div.animate({ transform: ['translateX(100px)', 'translateX(200px)'],
+ composite: 'replace' },
+ 100 * MS_PER_SEC);
+ div.animate({ transform: ['translateX(0px)', 'translateX(100px)'],
+ composite: 'accumulate' },
+ 100 * MS_PER_SEC);
+
+ return waitForPaintsFlushed();
+ }).then(() => {
+ SpecialPowers.DOMWindowUtils.advanceTimeAndRefresh(50 * MS_PER_SEC);
+ var transform =
+ SpecialPowers.DOMWindowUtils.getOMTAStyle(div, 'transform');
+ assert_matrix_equals(transform, 'matrix(1, 0, 0, 1, 200, 0)',
+ 'Transform value at 50%');
+ });
+}, 'Accumulate onto an underlying animation value');
+
+promise_test(t => {
+ var div;
+ return useTestRefreshMode(t).then(() => {
+ div = addDiv(t, { style: 'transform: translateX(100px)' });
+ div.animate([{ transform: 'translateX(100px)', composite: 'accumulate' },
+ { transform: 'translateX(300px)', composite: 'replace' }],
+ 100 * MS_PER_SEC);
+
+ return waitForPaintsFlushed();
+ }).then(() => {
+ SpecialPowers.DOMWindowUtils.advanceTimeAndRefresh(50 * MS_PER_SEC);
+ var transform =
+ SpecialPowers.DOMWindowUtils.getOMTAStyle(div, 'transform');
+ assert_matrix_equals(transform, 'matrix(1, 0, 0, 1, 250, 0)',
+ 'Transform value at 50s');
+ });
+}, 'Composite when mixing accumulate and replace');
+
+promise_test(t => {
+ var div;
+ return useTestRefreshMode(t).then(() => {
+ div = addDiv(t, { style: 'transform: translateX(100px)' });
+ div.animate([{ transform: 'translateX(100px)', composite: 'replace' },
+ { transform: 'translateX(300px)' }],
+ { duration: 100 * MS_PER_SEC, composite: 'accumulate' });
+ return waitForPaintsFlushed();
+ }).then(() => {
+ SpecialPowers.DOMWindowUtils.advanceTimeAndRefresh(50 * MS_PER_SEC);
+ var transform =
+ SpecialPowers.DOMWindowUtils.getOMTAStyle(div, 'transform');
+ assert_matrix_equals(transform, 'matrix(1, 0, 0, 1, 250, 0)',
+ 'Transform value at 50%');
+ });
+}, 'Composite specified on a keyframe overrides the composite mode of the ' +
+ 'effect');
+
+promise_test(t => {
+ var div;
+ var anim;
+ return useTestRefreshMode(t).then(() => {
+ div = addDiv(t);
+ div.animate({ transform: [ 'scale(2)', 'scale(2)' ] }, 100 * MS_PER_SEC);
+ anim = div.animate({ transform: [ 'scale(4)', 'scale(4)' ] },
+ { duration: 100 * MS_PER_SEC, composite: 'add' });
+
+ return waitForPaintsFlushed();
+ }).then(() => {
+ var transform =
+ SpecialPowers.DOMWindowUtils.getOMTAStyle(div, 'transform');
+ assert_matrix_equals(transform, 'matrix(8, 0, 0, 8, 0, 0)',
+ 'The additive scale value should be scale(8)'); // scale(2) scale(4)
+
+ anim.effect.composite = 'accumulate';
+ return waitForPaintsFlushed();
+ }).then(() => {
+ SpecialPowers.DOMWindowUtils.advanceTimeAndRefresh(1);
+ var transform =
+ SpecialPowers.DOMWindowUtils.getOMTAStyle(div, 'transform');
+ assert_matrix_equals(transform, 'matrix(5, 0, 0, 5, 0, 0)',
+ // (scale(2 - 1) + scale(4 - 1) + scale(1))
+ 'The accumulate scale value should be scale(5)');
+ });
+}, 'Composite operation change');
+
+</script>
+</body>
diff --git a/dom/animation/test/style/test_interpolation-from-interpolatematrix-to-none.html b/dom/animation/test/style/test_interpolation-from-interpolatematrix-to-none.html
new file mode 100644
index 0000000000..1da95392eb
--- /dev/null
+++ b/dom/animation/test/style/test_interpolation-from-interpolatematrix-to-none.html
@@ -0,0 +1,43 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src='/resources/testharness.js'></script>
+<script src='/resources/testharnessreport.js'></script>
+<script src='../testcommon.js'></script>
+<div id='log'></div>
+<script type='text/javascript'>
+'use strict';
+
+test(function(t) {
+ var target = addDiv(t);
+ target.style.transform = 'translateX(100px)';
+ target.style.transition = 'all 10s linear -5s';
+ getComputedStyle(target).transform;
+
+ target.style.transform = 'rotate(90deg)';
+ var interpolated_matrix = 'matrix(' + Math.cos(Math.PI / 4) + ',' +
+ Math.sin(Math.PI / 4) + ',' +
+ -Math.sin(Math.PI / 4) + ',' +
+ Math.cos(Math.PI / 4) + ',' +
+ '50, 0)';
+ assert_matrix_equals(getComputedStyle(target).transform,
+ interpolated_matrix,
+ 'the equivalent matrix of ' + 'interpolatematrix(' +
+ 'translateX(100px), rotate(90deg), 0.5)');
+
+ // Trigger a new transition from
+ // interpolatematrix(translateX(100px), rotate(90deg), 0.5) to none
+ // with 'all 10s linear -5s'.
+ target.style.transform = 'none';
+ interpolated_matrix = 'matrix(' + Math.cos(Math.PI / 8) + ',' +
+ Math.sin(Math.PI / 8) + ',' +
+ -Math.sin(Math.PI / 8) + ',' +
+ Math.cos(Math.PI / 8) + ',' +
+ '25, 0)';
+ assert_matrix_equals(getComputedStyle(target).transform,
+ interpolated_matrix,
+ 'the expected matrix from interpolatematrix(' +
+ 'translateX(100px), rotate(90deg), 0.5) to none at 50%');
+}, 'Test interpolation from interpolatematrix to none at 50%');
+
+</script>
+</html>
diff --git a/dom/animation/test/style/test_missing-keyframe-on-compositor.html b/dom/animation/test/style/test_missing-keyframe-on-compositor.html
new file mode 100644
index 0000000000..8b92a89168
--- /dev/null
+++ b/dom/animation/test/style/test_missing-keyframe-on-compositor.html
@@ -0,0 +1,577 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<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>
+<div id="log"></div>
+<script>
+'use strict';
+
+if (!SpecialPowers.DOMWindowUtils.layerManagerRemote ||
+ !SpecialPowers.getBoolPref(
+ 'layers.offmainthreadcomposition.async-animations')) {
+ // If OMTA is disabled, nothing to run.
+ done();
+}
+
+function waitForPaintsFlushed() {
+ return new Promise(function(resolve, reject) {
+ waitForAllPaintsFlushed(resolve);
+ });
+}
+
+// Note that promise tests run in sequence so this ensures the document is
+// loaded before any of the other tests run.
+promise_test(t => {
+ // Without this, the first test case fails on Android.
+ return waitForDocumentLoad();
+}, 'Ensure document has been loaded');
+
+promise_test(t => {
+ var div;
+ return useTestRefreshMode(t).then(() => {
+ div = addDiv(t, { style: 'opacity: 0.1' });
+ div.animate({ opacity: 1 }, 100 * MS_PER_SEC);
+ return waitForPaintsFlushed();
+ }).then(() => {
+ var opacity =
+ SpecialPowers.DOMWindowUtils.getOMTAStyle(div, 'opacity');
+ assert_equals(opacity, '0.1',
+ 'The initial opacity value should be the base value');
+ });
+}, 'Initial opacity value for animation with no no keyframe at offset 0');
+
+promise_test(t => {
+ var div;
+ return useTestRefreshMode(t).then(() => {
+ div = addDiv(t, { style: 'opacity: 0.1' });
+ div.animate({ opacity: [ 0.5, 1 ] }, 100 * MS_PER_SEC);
+ div.animate({ opacity: 1 }, 100 * MS_PER_SEC);
+
+ return waitForPaintsFlushed();
+ }).then(() => {
+ var opacity =
+ SpecialPowers.DOMWindowUtils.getOMTAStyle(div, 'opacity');
+ assert_equals(opacity, '0.5',
+ 'The initial opacity value should be the value of ' +
+ 'lower-priority animation value');
+ });
+}, 'Initial opacity value for animation with no keyframe at offset 0 when ' +
+ 'there is a lower-priority animation');
+
+promise_test(t => {
+ var div;
+ return useTestRefreshMode(t).then(() => {
+ div = addDiv(t, { style: 'opacity: 0.1; transition: opacity 100s linear' });
+ getComputedStyle(div).opacity;
+
+ div.style.opacity = '0.5';
+ getComputedStyle(div).opacity;
+
+ div.animate({ opacity: 1 }, 100 * MS_PER_SEC);
+
+ return waitForPaintsFlushed();
+ }).then(() => {
+ var opacity =
+ SpecialPowers.DOMWindowUtils.getOMTAStyle(div, 'opacity');
+ assert_equals(opacity, '0.1',
+ 'The initial opacity value should be the initial value of ' +
+ 'the transition');
+ });
+}, 'Initial opacity value for animation with no keyframe at offset 0 when ' +
+ 'there is a transition on the same property');
+
+promise_test(t => {
+ var div;
+ return useTestRefreshMode(t).then(() => {
+ div = addDiv(t, { style: 'opacity: 0' });
+ div.animate([{ offset: 0, opacity: 1 }], 100 * MS_PER_SEC);
+
+ return waitForPaintsFlushed();
+ }).then(() => {
+ SpecialPowers.DOMWindowUtils.advanceTimeAndRefresh(50 * MS_PER_SEC);
+
+ var opacity =
+ SpecialPowers.DOMWindowUtils.getOMTAStyle(div, 'opacity');
+ assert_equals(opacity, '0.5',
+ 'Opacity value at 50% should be composed onto the base ' +
+ 'value');
+ });
+}, 'Opacity value for animation with no keyframe at offset 1 at 50% ');
+
+promise_test(t => {
+ var div;
+ return useTestRefreshMode(t).then(() => {
+ div = addDiv(t, { style: 'opacity: 0' });
+ div.animate({ opacity: [ 0.5, 0.5 ] }, 100 * MS_PER_SEC);
+ div.animate([{ offset: 0, opacity: 1 }], 100 * MS_PER_SEC);
+
+ return waitForPaintsFlushed();
+ }).then(() => {
+ SpecialPowers.DOMWindowUtils.advanceTimeAndRefresh(50 * MS_PER_SEC);
+
+ var opacity =
+ SpecialPowers.DOMWindowUtils.getOMTAStyle(div, 'opacity');
+ assert_equals(opacity, '0.75', // (0.5 + 1) * 0.5
+ 'Opacity value at 50% should be composed onto the value ' +
+ 'of middle of lower-priority animation');
+ });
+}, 'Opacity value for animation with no keyframe at offset 1 at 50% when ' +
+ 'there is a lower-priority animation');
+
+promise_test(t => {
+ var div;
+ return useTestRefreshMode(t).then(() => {
+ div = addDiv(t, { style: 'opacity: 0; transition: opacity 100s linear' });
+ getComputedStyle(div).opacity;
+
+ div.style.opacity = '0.5';
+ getComputedStyle(div).opacity;
+
+ div.animate([{ offset: 0, opacity: 1 }], 100 * MS_PER_SEC);
+
+ return waitForPaintsFlushed();
+ }).then(() => {
+ SpecialPowers.DOMWindowUtils.advanceTimeAndRefresh(50 * MS_PER_SEC);
+
+ var opacity =
+ SpecialPowers.DOMWindowUtils.getOMTAStyle(div, 'opacity');
+ assert_equals(opacity, '0.625', // ((0 + 0.5) * 0.5 + 1) * 0.5
+ 'Opacity value at 50% should be composed onto the value ' +
+ 'of middle of transition');
+ });
+}, 'Opacity value for animation with no keyframe at offset 1 at 50% when ' +
+ 'there is a transition on the same property');
+
+promise_test(t => {
+ var div;
+ var lowerAnimation;
+ return useTestRefreshMode(t).then(() => {
+ div = addDiv(t);
+ lowerAnimation = div.animate({ opacity: [ 0.5, 1 ] }, 100 * MS_PER_SEC);
+ var higherAnimation = div.animate({ opacity: 1 }, 100 * MS_PER_SEC);
+
+ return waitForPaintsFlushed();
+ }).then(() => {
+ lowerAnimation.pause();
+ return waitForPaintsFlushed();
+ }).then(() => {
+ SpecialPowers.DOMWindowUtils.advanceTimeAndRefresh(50 * MS_PER_SEC);
+
+ var opacity =
+ SpecialPowers.DOMWindowUtils.getOMTAStyle(div, 'opacity');
+ // The underlying value is the value that is staying at 0ms of the
+ // lowerAnimation, that is 0.5.
+ // (0.5 + 1.0) * (50 * MS_PER_SEC / 100 * MS_PER_SEC) = 0.75.
+ assert_equals(opacity, '0.75',
+ 'Composed opacity value should be composed onto the value ' +
+ 'of lower-priority paused animation');
+ });
+}, 'Opacity value for animation with no keyframe at offset 0 at 50% when ' +
+ 'composed onto a paused underlying animation');
+
+promise_test(t => {
+ var div;
+ var lowerAnimation;
+ return useTestRefreshMode(t).then(() => {
+ div = addDiv(t);
+ lowerAnimation = div.animate({ opacity: [ 0.5, 1 ] }, 100 * MS_PER_SEC);
+ var higherAnimation = div.animate({ opacity: 1 }, 100 * MS_PER_SEC);
+
+ return waitForPaintsFlushed();
+ }).then(() => {
+ lowerAnimation.playbackRate = 0;
+ return waitForPaintsFlushed();
+ }).then(() => {
+ SpecialPowers.DOMWindowUtils.advanceTimeAndRefresh(50 * MS_PER_SEC);
+
+ var opacity =
+ SpecialPowers.DOMWindowUtils.getOMTAStyle(div, 'opacity');
+ // The underlying value is the value that is staying at 0ms of the
+ // lowerAnimation, that is 0.5.
+ // (0.5 + 1.0) * (50 * MS_PER_SEC / 100 * MS_PER_SEC) = 0.75.
+ assert_equals(opacity, '0.75',
+ 'Composed opacity value should be composed onto the value ' +
+ 'of lower-priority zero playback rate animation');
+ });
+}, 'Opacity value for animation with no keyframe at offset 0 at 50% when ' +
+ 'composed onto a zero playback rate underlying animation');
+
+promise_test(t => {
+ var div;
+ var lowerAnimation;
+ return useTestRefreshMode(t).then(() => {
+ div = addDiv(t);
+ lowerAnimation = div.animate({ opacity: [ 1, 0.5 ] }, 100 * MS_PER_SEC);
+ var higherAnimation = div.animate({ opacity: 1 }, 100 * MS_PER_SEC);
+
+ return waitForPaintsFlushed();
+ }).then(() => {
+ lowerAnimation.effect.updateTiming({
+ duration: 0,
+ fill: 'forwards',
+ });
+ return waitForPaintsFlushed();
+ }).then(() => {
+ SpecialPowers.DOMWindowUtils.advanceTimeAndRefresh(50 * MS_PER_SEC);
+
+ var opacity =
+ SpecialPowers.DOMWindowUtils.getOMTAStyle(div, 'opacity');
+ // The underlying value is the value that is filling forwards state of the
+ // lowerAnimation, that is 0.5.
+ // (0.5 + 1.0) * (50 * MS_PER_SEC / 100 * MS_PER_SEC) = 0.75.
+ assert_equals(opacity, '0.75',
+ 'Composed opacity value should be composed onto the value ' +
+ 'of lower-priority zero active duration animation');
+ });
+}, 'Opacity value for animation with no keyframe at offset 0 at 50% when ' +
+ 'composed onto a zero active duration underlying animation');
+
+promise_test(t => {
+ var div;
+ return useTestRefreshMode(t).then(() => {
+ div = addDiv(t, { style: 'transform: translateX(100px)' });
+ div.animate({ transform: 'translateX(200px)' }, 100 * MS_PER_SEC);
+
+ return waitForPaintsFlushed();
+ }).then(() => {
+ var transform =
+ SpecialPowers.DOMWindowUtils.getOMTAStyle(div, 'transform');
+ assert_matrix_equals(transform, 'matrix(1, 0, 0, 1, 100, 0)',
+ 'The initial transform value should be the base value');
+ });
+}, 'Initial transform value for animation with no keyframe at offset 0');
+
+promise_test(t => {
+ var div;
+ return useTestRefreshMode(t).then(() => {
+ div = addDiv(t, { style: 'transform: translateX(100px)' });
+ div.animate({ transform: [ 'translateX(200px)', 'translateX(300px)' ] },
+ 100 * MS_PER_SEC);
+ div.animate({ transform: 'translateX(400px)' }, 100 * MS_PER_SEC);
+
+ return waitForPaintsFlushed();
+ }).then(() => {
+ var transform =
+ SpecialPowers.DOMWindowUtils.getOMTAStyle(div, 'transform');
+ assert_matrix_equals(transform, 'matrix(1, 0, 0, 1, 200, 0)',
+ 'The initial transform value should be lower-priority animation value');
+ });
+}, 'Initial transform value for animation with no keyframe at offset 0 when ' +
+ 'there is a lower-priority animation');
+
+promise_test(t => {
+ var div;
+ return useTestRefreshMode(t).then(() => {
+ div = addDiv(t, { style: 'transform: translateX(100px);' +
+ 'transition: transform 100s linear' });
+ getComputedStyle(div).transform;
+
+ div.style.transform = 'translateX(200px)';
+ getComputedStyle(div).transform;
+
+ div.animate({ transform: 'translateX(400px)' }, 100 * MS_PER_SEC);
+
+ return waitForPaintsFlushed();
+ }).then(() => {
+ var transform =
+ SpecialPowers.DOMWindowUtils.getOMTAStyle(div, 'transform');
+ assert_matrix_equals(transform, 'matrix(1, 0, 0, 1, 100, 0)',
+ 'The initial transform value should be the initial value of the ' +
+ 'transition');
+ });
+}, 'Initial transform value for animation with no keyframe at offset 0 when ' +
+ 'there is a transition');
+
+promise_test(t => {
+ var div;
+ return useTestRefreshMode(t).then(() => {
+ div = addDiv(t, { style: 'transform: translateX(100px)' });
+ div.animate([{ offset: 0, transform: 'translateX(200pX)' }],
+ 100 * MS_PER_SEC);
+
+ return waitForPaintsFlushed();
+ }).then(() => {
+ SpecialPowers.DOMWindowUtils.advanceTimeAndRefresh(50 * MS_PER_SEC);
+
+ var transform =
+ SpecialPowers.DOMWindowUtils.getOMTAStyle(div, 'transform');
+ assert_matrix_equals(transform, 'matrix(1, 0, 0, 1, 150, 0)',
+ 'Transform value at 50% should be the base value');
+ });
+}, 'Transform value for animation with no keyframe at offset 1 at 50%');
+
+promise_test(t => {
+ var div;
+ return useTestRefreshMode(t).then(() => {
+ div = addDiv(t, { style: 'transform: translateX(100px)' });
+ div.animate({ transform: [ 'translateX(200px)', 'translateX(200px)' ] },
+ 100 * MS_PER_SEC);
+ div.animate([{ offset: 0, transform: 'translateX(300px)' }],
+ 100 * MS_PER_SEC);
+
+ return waitForPaintsFlushed();
+ }).then(() => {
+ SpecialPowers.DOMWindowUtils.advanceTimeAndRefresh(50 * MS_PER_SEC);
+
+ var transform =
+ SpecialPowers.DOMWindowUtils.getOMTAStyle(div, 'transform');
+ assert_matrix_equals(transform, 'matrix(1, 0, 0, 1, 250, 0)',
+ 'The final transform value should be the base value');
+ });
+}, 'Transform value for animation with no keyframe at offset 1 at 50% when ' +
+ 'there is a lower-priority animation');
+
+promise_test(t => {
+ var div;
+ return useTestRefreshMode(t).then(() => {
+ div = addDiv(t, { style: 'transform: translateX(100px);' +
+ 'transition: transform 100s linear' });
+ getComputedStyle(div).transform;
+
+ div.style.transform = 'translateX(200px)';
+ getComputedStyle(div).transform;
+
+ div.animate([{ offset: 0, transform: 'translateX(300px)' }],
+ 100 * MS_PER_SEC);
+
+ return waitForPaintsFlushed();
+ }).then(() => {
+ SpecialPowers.DOMWindowUtils.advanceTimeAndRefresh(50 * MS_PER_SEC);
+
+ var transform =
+ SpecialPowers.DOMWindowUtils.getOMTAStyle(div, 'transform');
+ // (150px + 300px) * 0.5
+ assert_matrix_equals(transform, 'matrix(1, 0, 0, 1, 225, 0)',
+ 'The final transform value should be the final value of the transition');
+ });
+}, 'Transform value for animation with no keyframe at offset 1 at 50% when ' +
+ 'there is a transition');
+
+promise_test(t => {
+ var div;
+ var lowerAnimation;
+ return useTestRefreshMode(t).then(() => {
+ div = addDiv(t);
+ lowerAnimation =
+ div.animate({ transform: [ 'translateX(100px)', 'translateX(200px)' ] },
+ 100 * MS_PER_SEC);
+ var higherAnimation = div.animate({ transform: 'translateX(300px)' },
+ 100 * MS_PER_SEC);
+
+ return waitForPaintsFlushed();
+ }).then(() => {
+ lowerAnimation.pause();
+ return waitForPaintsFlushed();
+ }).then(() => {
+ SpecialPowers.DOMWindowUtils.advanceTimeAndRefresh(50 * MS_PER_SEC);
+
+ var transform =
+ SpecialPowers.DOMWindowUtils.getOMTAStyle(div, 'transform');
+ // The underlying value is the value that is staying at 0ms of the
+ // lowerAnimation, that is 100px.
+ // (100px + 300px) * (50 * MS_PER_SEC / 100 * MS_PER_SEC) = 200px.
+ assert_matrix_equals(transform, 'matrix(1, 0, 0, 1, 200, 0)',
+ 'Composed transform value should be composed onto the value of ' +
+ 'lower-priority paused animation');
+ });
+}, 'Transform value for animation with no keyframe at offset 0 at 50% when ' +
+ 'composed onto a paused underlying animation');
+
+promise_test(t => {
+ var div;
+ var lowerAnimation;
+ return useTestRefreshMode(t).then(() => {
+ div = addDiv(t);
+ lowerAnimation =
+ div.animate({ transform: [ 'translateX(100px)', 'translateX(200px)' ] },
+ 100 * MS_PER_SEC);
+ var higherAnimation = div.animate({ transform: 'translateX(300px)' },
+ 100 * MS_PER_SEC);
+
+ return waitForPaintsFlushed();
+ }).then(() => {
+ lowerAnimation.playbackRate = 0;
+ return waitForPaintsFlushed();
+ }).then(() => {
+ SpecialPowers.DOMWindowUtils.advanceTimeAndRefresh(50 * MS_PER_SEC);
+
+ var transform =
+ SpecialPowers.DOMWindowUtils.getOMTAStyle(div, 'transform');
+ // The underlying value is the value that is staying at 0ms of the
+ // lowerAnimation, that is 100px.
+ // (100px + 300px) * (50 * MS_PER_SEC / 100 * MS_PER_SEC) = 200px.
+ assert_matrix_equals(transform, 'matrix(1, 0, 0, 1, 200, 0)',
+ 'Composed transform value should be composed onto the value of ' +
+ 'lower-priority zero playback rate animation');
+ });
+}, 'Transform value for animation with no keyframe at offset 0 at 50% when ' +
+ 'composed onto a zero playback rate underlying animation');
+
+promise_test(t => {
+ var div;
+ return useTestRefreshMode(t).then(() => {
+ div = addDiv(t);
+ var lowerAnimation =
+ div.animate({ transform: [ 'translateX(100px)', 'translateX(200px)' ] },
+ { duration: 10 * MS_PER_SEC,
+ fill: 'forwards' });
+ var higherAnimation = div.animate({ transform: 'translateX(300px)' },
+ 100 * MS_PER_SEC);
+
+ return waitForPaintsFlushed();
+ }).then(() => {
+ SpecialPowers.DOMWindowUtils.advanceTimeAndRefresh(50 * MS_PER_SEC);
+
+ // We need to wait for a paint so that we can send the state of the lower
+ // animation that is actually finished at this point.
+ return waitForPaintsFlushed();
+ }).then(() => {
+ var transform =
+ SpecialPowers.DOMWindowUtils.getOMTAStyle(div, 'transform');
+ // (200px + 300px) * (50 * MS_PER_SEC / 100 * MS_PER_SEC) = 250px.
+ assert_matrix_equals(transform, 'matrix(1, 0, 0, 1, 250, 0)',
+ 'Composed transform value should be composed onto the value of ' +
+ 'lower-priority animation with fill:forwards');
+ });
+}, 'Transform value for animation with no keyframe at offset 0 at 50% when ' +
+ 'composed onto a underlying animation with fill:forwards');
+
+promise_test(t => {
+ var div;
+ return useTestRefreshMode(t).then(() => {
+ div = addDiv(t);
+ var lowerAnimation =
+ div.animate({ transform: [ 'translateX(100px)', 'translateX(200px)' ] },
+ { duration: 10 * MS_PER_SEC,
+ endDelay: -5 * MS_PER_SEC,
+ fill: 'forwards' });
+ var higherAnimation = div.animate({ transform: 'translateX(300px)' },
+ 100 * MS_PER_SEC);
+
+ return waitForPaintsFlushed();
+ }).then(() => {
+ SpecialPowers.DOMWindowUtils.advanceTimeAndRefresh(50 * MS_PER_SEC);
+
+ // We need to wait for a paint just like the above test.
+ return waitForPaintsFlushed();
+ }).then(() => {
+ var transform =
+ SpecialPowers.DOMWindowUtils.getOMTAStyle(div, 'transform');
+ // (150px + 300px) * (50 * MS_PER_SEC / 100 * MS_PER_SEC) = 225px.
+ assert_matrix_equals(transform, 'matrix(1, 0, 0, 1, 225, 0)',
+ 'Composed transform value should be composed onto the value of ' +
+ 'lower-priority animation with fill:forwards and negative endDelay');
+ });
+}, 'Transform value for animation with no keyframe at offset 0 at 50% when ' +
+ 'composed onto a underlying animation with fill:forwards and negative ' +
+ 'endDelay');
+
+promise_test(t => {
+ var div;
+ return useTestRefreshMode(t).then(() => {
+ div = addDiv(t);
+ var lowerAnimation =
+ div.animate({ transform: [ 'translateX(100px)', 'translateX(200px)' ] },
+ { duration: 10 * MS_PER_SEC,
+ endDelay: 100 * MS_PER_SEC,
+ fill: 'forwards' });
+ var higherAnimation = div.animate({ transform: 'translateX(300px)' },
+ 100 * MS_PER_SEC);
+
+ return waitForPaintsFlushed();
+ }).then(() => {
+ SpecialPowers.DOMWindowUtils.advanceTimeAndRefresh(50 * MS_PER_SEC);
+
+ var transform =
+ SpecialPowers.DOMWindowUtils.getOMTAStyle(div, 'transform');
+ // (200px + 300px) * 0.5
+ assert_matrix_equals(transform, 'matrix(1, 0, 0, 1, 250, 0)',
+ 'Composed transform value should be composed onto the value of ' +
+ 'lower-priority animation with fill:forwards during positive endDelay');
+ });
+}, 'Transform value for animation with no keyframe at offset 0 at 50% when ' +
+ 'composed onto a underlying animation with fill:forwards during positive ' +
+ 'endDelay');
+
+promise_test(t => {
+ var div;
+ return useTestRefreshMode(t).then(() => {
+ div = addDiv(t, { style: 'transform: translateX(100px)' });
+ div.animate({ transform: 'translateX(200px)' },
+ { duration: 100 * MS_PER_SEC, delay: 50 * MS_PER_SEC });
+
+ return waitForPaintsFlushed();
+ }).then(() => {
+ SpecialPowers.DOMWindowUtils.advanceTimeAndRefresh(100 * MS_PER_SEC);
+
+ var transform =
+ SpecialPowers.DOMWindowUtils.getOMTAStyle(div, 'transform');
+ assert_matrix_equals(transform, 'matrix(1, 0, 0, 1, 150, 0)',
+ 'Transform value for animation with positive delay should be composed ' +
+ 'onto the base style');
+ });
+}, 'Transform value for animation with no keyframe at offset 0 and with ' +
+ 'positive delay');
+
+promise_test(t => {
+ var div;
+ return useTestRefreshMode(t).then(() => {
+ div = addDiv(t, { style: 'transform: translateX(100px)' });
+
+ div.animate([{ offset: 0, transform: 'translateX(200px)'}],
+ { duration: 100 * MS_PER_SEC,
+ iterationStart: 1,
+ iterationComposite: 'accumulate' });
+
+ return waitForPaintsFlushed();
+ }).then(() => {
+ var transform =
+ SpecialPowers.DOMWindowUtils.getOMTAStyle(div, 'transform');
+ assert_matrix_equals(transform, 'matrix(1, 0, 0, 1, 300, 0)',
+ 'Transform value for animation with no keyframe at offset 1 and its ' +
+ 'iterationComposite is accumulate');
+ });
+}, 'Transform value for animation with no keyframe at offset 1 and its ' +
+ 'iterationComposite is accumulate');
+
+promise_test(t => {
+ var div;
+ return useTestRefreshMode(t).then(() => {
+ div = addDiv(t);
+ var lowerAnimation =
+ div.animate({ transform: [ 'translateX(100px)', 'translateX(200px)' ] },
+ 100 * MS_PER_SEC);
+ var higherAnimation = div.animate({ transform: 'translateX(300px)' },
+ 100 * MS_PER_SEC);
+
+ lowerAnimation.timeline = null;
+ // Set current time at 50% duration.
+ lowerAnimation.currentTime = 50 * MS_PER_SEC;
+
+ return waitForPaintsFlushed();
+ }).then(() => {
+ SpecialPowers.DOMWindowUtils.advanceTimeAndRefresh(50 * MS_PER_SEC);
+
+ var transform =
+ SpecialPowers.DOMWindowUtils.getOMTAStyle(div, 'transform');
+ // (150px + 300px) * (50 * MS_PER_SEC / 100 * MS_PER_SEC) = 225px.
+ assert_matrix_equals(transform, 'matrix(1, 0, 0, 1, 225, 0)',
+ 'Composed transform value should be composed onto the value of ' +
+ 'lower-priority animation without timeline');
+ });
+}, 'Transform value for animation with no keyframe at offset 0 at 50% when ' +
+ 'composed onto an animation without timeline');
+
+</script>
+</body>
diff --git a/dom/animation/test/style/test_missing-keyframe.html b/dom/animation/test/style/test_missing-keyframe.html
new file mode 100644
index 0000000000..4047e62408
--- /dev/null
+++ b/dom/animation/test/style/test_missing-keyframe.html
@@ -0,0 +1,110 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../testcommon.js"></script>
+<body>
+<div id="log"></div>
+<script>
+'use strict';
+
+test(t => {
+ var div = addDiv(t, { style: 'margin-left: 100px' });
+ div.animate([{ marginLeft: '200px' }], 100 * MS_PER_SEC);
+
+ assert_equals(getComputedStyle(div).marginLeft, '100px',
+ 'The initial margin-left value should be the base value');
+}, 'Initial margin-left value for an animation with no keyframe at offset 0');
+
+test(t => {
+ var div = addDiv(t, { style: 'margin-left: 100px' });
+ div.animate([{ offset: 0, marginLeft: '200px' },
+ { offset: 1, marginLeft: '300px' }],
+ 100 * MS_PER_SEC);
+ div.animate([{ marginLeft: '200px' }], 100 * MS_PER_SEC);
+
+ assert_equals(getComputedStyle(div).marginLeft, '200px',
+ 'The initial margin-left value should be the initial value ' +
+ 'of lower-priority animation');
+}, 'Initial margin-left value for an animation with no keyframe at offset 0 ' +
+ 'is that of lower-priority animations');
+
+test(t => {
+ var div = addDiv(t, { style: 'margin-left: 100px;' +
+ 'transition: margin-left 100s -50s linear'});
+ flushComputedStyle(div);
+
+ div.style.marginLeft = '200px';
+ flushComputedStyle(div);
+
+ div.animate([{ marginLeft: '300px' }], 100 * MS_PER_SEC);
+
+ assert_equals(getComputedStyle(div).marginLeft, '150px',
+ 'The initial margin-left value should be the initial value ' +
+ 'of the transition');
+}, 'Initial margin-left value for an animation with no keyframe at offset 0 ' +
+ 'is that of transition');
+
+test(t => {
+ var div = addDiv(t, { style: 'margin-left: 100px' });
+ var animation = div.animate([{ offset: 0, marginLeft: '200px' }],
+ 100 * MS_PER_SEC);
+
+ animation.currentTime = 50 * MS_PER_SEC;
+ assert_equals(getComputedStyle(div).marginLeft, '150px',
+ 'The margin-left value at 50% should be the base value');
+}, 'margin-left value at 50% for an animation with no keyframe at offset 1');
+
+test(t => {
+ var div = addDiv(t, { style: 'margin-left: 100px' });
+ var lowerAnimation = div.animate([{ offset: 0, marginLeft: '200px' },
+ { offset: 1, marginLeft: '300px' }],
+ 100 * MS_PER_SEC);
+ var higherAnimation = div.animate([{ offset: 0, marginLeft: '400px' }],
+ 100 * MS_PER_SEC);
+
+ lowerAnimation.currentTime = 50 * MS_PER_SEC;
+ higherAnimation.currentTime = 50 * MS_PER_SEC;
+ // (250px + 400px) * 0.5
+ assert_equals(getComputedStyle(div).marginLeft, '325px',
+ 'The margin-left value at 50% should be additive value of ' +
+ 'lower-priority animation and higher-priority animation');
+}, 'margin-left value at 50% for an animation with no keyframe at offset 1 ' +
+ 'is that of lower-priority animations');
+
+test(t => {
+ var div = addDiv(t, { style: 'margin-left: 100px;' +
+ 'transition: margin-left 100s linear' });
+ flushComputedStyle(div);
+
+ div.style.marginLeft = '300px';
+ flushComputedStyle(div);
+
+ div.animate([{ offset: 0, marginLeft: '200px' }], 100 * MS_PER_SEC);
+
+ div.getAnimations().forEach(animation => {
+ animation.currentTime = 50 * MS_PER_SEC;
+ });
+ // (200px + 200px) * 0.5
+ assert_equals(getComputedStyle(div).marginLeft, '200px',
+ 'The margin-left value at 50% should be additive value of ' +
+ 'the transition and animation');
+}, 'margin-left value at 50% for an animation with no keyframe at offset 1 ' +
+ 'is that of transition');
+
+test(t => {
+ var div = addDiv(t, { style: 'margin-left: 100px' });
+
+ var animation = div.animate([{ offset: 0, marginLeft: '200px' }],
+ { duration: 100 * MS_PER_SEC,
+ iterationStart: 1,
+ iterationComposite: 'accumulate' });
+
+ assert_equals(getComputedStyle(div).marginLeft, '300px',
+ 'The margin-left value should be additive value of the ' +
+ 'accumulation of the initial value onto the base value ');
+}, 'margin-left value for an animation with no keyframe at offset 1 and its ' +
+ 'iterationComposite is accumulate');
+
+</script>
+</body>
diff --git a/dom/animation/test/style/test_transform-non-normalizable-rotate3d.html b/dom/animation/test/style/test_transform-non-normalizable-rotate3d.html
new file mode 100644
index 0000000000..ad2584ac40
--- /dev/null
+++ b/dom/animation/test/style/test_transform-non-normalizable-rotate3d.html
@@ -0,0 +1,28 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src='/resources/testharness.js'></script>
+<script src='/resources/testharnessreport.js'></script>
+<script src='../testcommon.js'></script>
+<div id='log'></div>
+<script type='text/javascript'>
+'use strict';
+
+test(function(t) {
+ var target = addDiv(t);
+ target.style.transform = 'rotate3d(0, 0, 1, 90deg)';
+ target.style.transition = 'all 10s linear -5s';
+ getComputedStyle(target).transform;
+
+ target.style.transform = 'rotate3d(0, 0, 0, 270deg)';
+ var interpolated_matrix = 'matrix(' + Math.cos(Math.PI / 4) + ',' +
+ Math.sin(Math.PI / 4) + ',' +
+ -Math.sin(Math.PI / 4) + ',' +
+ Math.cos(Math.PI / 4) + ',' +
+ '0, 0)';
+ assert_matrix_equals(getComputedStyle(target).transform, interpolated_matrix,
+ 'transition from a normal rotate3d to a ' +
+ 'non-normalizable rotate3d');
+}, 'Test interpolation on non-normalizable rotate3d function');
+
+</script>
+</html>
diff --git a/dom/animation/test/testcommon.js b/dom/animation/test/testcommon.js
new file mode 100644
index 0000000000..81232c5a81
--- /dev/null
+++ b/dom/animation/test/testcommon.js
@@ -0,0 +1,512 @@
+/* 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://drafts.csswg.org/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 * 2, description);
+}
+
+/*
+ * Compare a time value based on its precision requirements with a fixed value.
+ */
+function assert_time_equals_literal(actual, expected, description) {
+ assert_approx_equals(actual, expected, TIME_PRECISION, description);
+}
+
+/*
+ * Compare matrix string like 'matrix(1, 0, 0, 1, 100, 0)'.
+ * This function allows error, 0.01, because on Android when we are scaling down
+ * the document, it results in some errors.
+ */
+function assert_matrix_equals(actual, expected, description) {
+ var matrixRegExp = /^matrix\((.+),(.+),(.+),(.+),(.+),(.+)\)/;
+ assert_regexp_match(actual, matrixRegExp, "Actual value should be a matrix");
+ assert_regexp_match(
+ expected,
+ matrixRegExp,
+ "Expected value should be a matrix"
+ );
+
+ var actualMatrixArray = actual.match(matrixRegExp).slice(1).map(Number);
+ var expectedMatrixArray = expected.match(matrixRegExp).slice(1).map(Number);
+
+ assert_equals(
+ actualMatrixArray.length,
+ expectedMatrixArray.length,
+ "Array lengths should be equal (got '" +
+ expected +
+ "' and '" +
+ actual +
+ "'): " +
+ description
+ );
+ for (var i = 0; i < actualMatrixArray.length; i++) {
+ assert_approx_equals(
+ actualMatrixArray[i],
+ expectedMatrixArray[i],
+ 0.01,
+ "Matrix array should be equal (got '" +
+ expected +
+ "' and '" +
+ actual +
+ "'): " +
+ description
+ );
+ }
+}
+
+/**
+ * Compare given values which are same format of
+ * KeyframeEffectReadonly::GetProperties.
+ */
+function assert_properties_equal(actual, expected) {
+ assert_equals(actual.length, expected.length);
+
+ const compareProperties = (a, b) =>
+ a.property == b.property ? 0 : a.property < b.property ? -1 : 1;
+
+ const sortedActual = actual.sort(compareProperties);
+ const sortedExpected = expected.sort(compareProperties);
+
+ const serializeValues = values =>
+ values
+ .map(
+ value =>
+ "{ " +
+ ["offset", "value", "easing", "composite"]
+ .map(member => `${member}: ${value[member]}`)
+ .join(", ") +
+ " }"
+ )
+ .join(", ");
+
+ for (let 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`
+ );
+ }
+}
+
+/**
+ * Construct a object which is same to a value of
+ * KeyframeEffectReadonly::GetProperties().
+ * The method returns undefined as a value in case of missing keyframe.
+ * Therefor, we can use undefined for |value| and |easing| parameter.
+ * @param offset - keyframe offset. e.g. 0.1
+ * @param value - any keyframe value. e.g. undefined '1px', 'center', 0.5
+ * @param composite - 'replace', 'add', 'accumulate'
+ * @param easing - e.g. undefined, 'linear', 'ease' and so on
+ * @return Object -
+ * e.g. { offset: 0.1, value: '1px', composite: 'replace', easing: 'ease'}
+ */
+function valueFormat(offset, value, composite, easing) {
+ return { offset, value, easing, composite };
+}
+
+/**
+ * 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.remove();
+ }
+ });
+ }
+ 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();
+ });
+ }
+}
+
+/**
+ * Takes a CSS property (e.g. margin-left) and returns the equivalent IDL
+ * name (e.g. marginLeft).
+ */
+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();
+ });
+}
+
+/**
+ * Promise wrapper for requestAnimationFrame.
+ */
+function waitForFrame() {
+ return new Promise(function (resolve, reject) {
+ window.requestAnimationFrame(resolve);
+ });
+}
+
+/**
+ * Waits for a requestAnimationFrame callback in the next refresh driver tick.
+ */
+function waitForNextFrame(aWindow = window) {
+ const timeAtStart = aWindow.document.timeline.currentTime;
+ return new Promise(resolve => {
+ aWindow.requestAnimationFrame(() => {
+ if (timeAtStart === aWindow.document.timeline.currentTime) {
+ aWindow.requestAnimationFrame(resolve);
+ } else {
+ resolve();
+ }
+ });
+ });
+}
+
+/**
+ * Returns a Promise that is resolved after the given number of consecutive
+ * animation frames have occured (using requestAnimationFrame callbacks).
+ *
+ * @param aFrameCount The number of animation frames.
+ * @param aOnFrame An optional function to be processed in each animation frame.
+ * @param aWindow An optional window object to be used for requestAnimationFrame.
+ */
+function waitForAnimationFrames(aFrameCount, aOnFrame, aWindow = window) {
+ const timeAtStart = aWindow.document.timeline.currentTime;
+ return new Promise(function (resolve, reject) {
+ function handleFrame() {
+ if (aOnFrame && typeof aOnFrame === "function") {
+ aOnFrame();
+ }
+ if (
+ timeAtStart != aWindow.document.timeline.currentTime &&
+ --aFrameCount <= 0
+ ) {
+ resolve();
+ } else {
+ aWindow.requestAnimationFrame(handleFrame); // wait another frame
+ }
+ }
+ aWindow.requestAnimationFrame(handleFrame);
+ });
+}
+
+/**
+ * Promise wrapper for requestIdleCallback.
+ */
+function waitForIdle() {
+ return new Promise(resolve => {
+ requestIdleCallback(resolve);
+ });
+}
+
+/**
+ * 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 = 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",
+ "assert_regexp_match",
+ "promise_test",
+ "test",
+ ]) {
+ if (opener[funcName]) {
+ window[funcName] = opener[funcName].bind(opener);
+ }
+ }
+
+ window.EventWatcher = opener.EventWatcher;
+
+ function done() {
+ opener.add_completion_callback(function () {
+ self.close();
+ });
+ opener.done();
+ }
+}
+
+/*
+ * Returns a promise that is resolved when the document has finished loading.
+ */
+function waitForDocumentLoad() {
+ return new Promise(function (resolve, reject) {
+ if (document.readyState === "complete") {
+ resolve();
+ } else {
+ window.addEventListener("load", resolve);
+ }
+ });
+}
+
+/*
+ * Enters test refresh mode, and restores the mode when |t| finishes.
+ */
+function useTestRefreshMode(t) {
+ function ensureNoSuppressedPaints() {
+ return new Promise(resolve => {
+ function checkSuppressedPaints() {
+ if (!SpecialPowers.DOMWindowUtils.paintingSuppressed) {
+ resolve();
+ } else {
+ window.requestAnimationFrame(checkSuppressedPaints);
+ }
+ }
+ checkSuppressedPaints();
+ });
+ }
+
+ return ensureNoSuppressedPaints().then(() => {
+ SpecialPowers.DOMWindowUtils.advanceTimeAndRefresh(0);
+ t.add_cleanup(() => {
+ SpecialPowers.DOMWindowUtils.restoreNormalRefresh();
+ });
+ });
+}
+
+/**
+ * Returns true if off-main-thread animations.
+ */
+function isOMTAEnabled() {
+ const OMTAPrefKey = "layers.offmainthreadcomposition.async-animations";
+ return (
+ SpecialPowers.DOMWindowUtils.layerManagerRemote &&
+ SpecialPowers.getBoolPref(OMTAPrefKey)
+ );
+}
+
+/**
+ * Append an SVG element to the target element.
+ *
+ * @param target The element which want to append.
+ * @param attrs A array object with attribute name and values to set on
+ * the SVG element.
+ * @return An SVG outer element.
+ */
+function addSVGElement(target, tag, attrs) {
+ if (!target) {
+ return null;
+ }
+ var element = document.createElementNS("http://www.w3.org/2000/svg", tag);
+ if (attrs) {
+ for (var attrName in attrs) {
+ element.setAttributeNS(null, attrName, attrs[attrName]);
+ }
+ }
+ target.appendChild(element);
+ return element;
+}
+
+/*
+ * Get Animation distance between two specified values for a specific property.
+ *
+ * @param target The target element.
+ * @param prop The CSS property.
+ * @param v1 The first property value.
+ * @param v2 The Second property value.
+ *
+ * @return The distance between |v1| and |v2| for |prop| on |target|.
+ */
+function getDistance(target, prop, v1, v2) {
+ if (!target) {
+ return 0.0;
+ }
+ return SpecialPowers.DOMWindowUtils.computeAnimationDistance(
+ target,
+ prop,
+ v1,
+ v2
+ );
+}
+
+/*
+ * A promise wrapper for waiting MozAfterPaint.
+ */
+function waitForPaints() {
+ // FIXME: Bug 1415065. Instead waiting for two requestAnimationFrames, we
+ // should wait for MozAfterPaint once after MozAfterPaint is fired properly
+ // (bug 1341294).
+ return waitForAnimationFrames(2);
+}
+
+// Returns true if |aAnimation| begins at the current timeline time. We
+// sometimes need to detect this case because if we started an animation
+// asynchronously (e.g. using play()) and then ended up running the next frame
+// at precisely the time the animation started (due to aligning with vsync
+// refresh rate) then we won't end up restyling in that frame.
+function animationStartsRightNow(aAnimation) {
+ return (
+ aAnimation.startTime === aAnimation.timeline.currentTime &&
+ aAnimation.currentTime === 0
+ );
+}
+
+// Waits for a given animation being ready to restyle.
+async function waitForAnimationReadyToRestyle(aAnimation) {
+ await aAnimation.ready;
+ // If |aAnimation| begins at the current timeline time, we will not process
+ // restyling in the initial frame because of aligning with the refresh driver,
+ // the animation frame in which the ready promise is resolved happens to
+ // coincide perfectly with the start time of the animation. In this case no
+ // restyling is needed in the frame so we have to wait one more frame.
+ if (animationStartsRightNow(aAnimation)) {
+ await waitForNextFrame(aAnimation.ownerGlobal);
+ }
+}
+
+// Returns the animation restyle markers observed during |frameCount| refresh
+// driver ticks in this `window`. This function is typically used to count the
+// number of restyles that take place as part of the style update that happens
+// on each refresh driver tick, as opposed to synchronous restyles triggered by
+// script.
+//
+// For the latter observeAnimSyncStyling (below) should be used.
+function observeStyling(frameCount, onFrame) {
+ return observeStylingInTargetWindow(window, frameCount, onFrame);
+}
+
+// As with observeStyling but applied to target window |aWindow|.
+function observeStylingInTargetWindow(aWindow, aFrameCount, aOnFrame) {
+ let priorAnimationTriggeredRestyles =
+ SpecialPowers.wrap(aWindow).windowUtils.animationTriggeredRestyles;
+
+ return new Promise(resolve => {
+ return waitForAnimationFrames(aFrameCount, aOnFrame, aWindow).then(() => {
+ let restyleCount =
+ SpecialPowers.wrap(aWindow).windowUtils.animationTriggeredRestyles -
+ priorAnimationTriggeredRestyles;
+
+ resolve(restyleCount);
+ });
+ });
+}