summaryrefslogtreecommitdiffstats
path: root/dom/animation/test/mozilla
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
commit26a029d407be480d791972afb5975cf62c9360a6 (patch)
treef435a8308119effd964b339f76abb83a57c29483 /dom/animation/test/mozilla
parentInitial commit. (diff)
downloadfirefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz
firefox-26a029d407be480d791972afb5975cf62c9360a6.zip
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'dom/animation/test/mozilla')
-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
32 files changed, 5128 insertions, 0 deletions
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>