summaryrefslogtreecommitdiffstats
path: root/dom/animation/test/mozilla
diff options
context:
space:
mode:
authorMatt A. Tobin <mattatobin@localhost.localdomain>2018-02-02 04:16:08 -0500
committerMatt A. Tobin <mattatobin@localhost.localdomain>2018-02-02 04:16:08 -0500
commit5f8de423f190bbb79a62f804151bc24824fa32d8 (patch)
tree10027f336435511475e392454359edea8e25895d /dom/animation/test/mozilla
parent49ee0794b5d912db1f95dce6eb52d781dc210db5 (diff)
downloadUXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.gz
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.lz
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.xz
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.zip
Add m-esr52 at 52.6.0
Diffstat (limited to 'dom/animation/test/mozilla')
-rw-r--r--dom/animation/test/mozilla/file_cubic_bezier_limits.html167
-rw-r--r--dom/animation/test/mozilla/file_deferred_start.html121
-rw-r--r--dom/animation/test/mozilla/file_disable_animations_api_core.html30
-rw-r--r--dom/animation/test/mozilla/file_disabled_properties.html73
-rw-r--r--dom/animation/test/mozilla/file_discrete-animations.html170
-rw-r--r--dom/animation/test/mozilla/file_document-timeline-origin-time-range.html30
-rw-r--r--dom/animation/test/mozilla/file_hide_and_show.html162
-rw-r--r--dom/animation/test/mozilla/file_partial_keyframes.html41
-rw-r--r--dom/animation/test/mozilla/file_set-easing.html34
-rw-r--r--dom/animation/test/mozilla/file_spacing_property_order.html33
-rw-r--r--dom/animation/test/mozilla/file_spacing_transform.html240
-rw-r--r--dom/animation/test/mozilla/file_transform_limits.html55
-rw-r--r--dom/animation/test/mozilla/file_transition_finish_on_compositor.html67
-rw-r--r--dom/animation/test/mozilla/file_underlying-discrete-value.html192
-rw-r--r--dom/animation/test/mozilla/test_cubic_bezier_limits.html14
-rw-r--r--dom/animation/test/mozilla/test_deferred_start.html14
-rw-r--r--dom/animation/test/mozilla/test_disable_animations_api_core.html14
-rw-r--r--dom/animation/test/mozilla/test_disabled_properties.html14
-rw-r--r--dom/animation/test/mozilla/test_discrete-animations.html18
-rw-r--r--dom/animation/test/mozilla/test_document-timeline-origin-time-range.html14
-rw-r--r--dom/animation/test/mozilla/test_hide_and_show.html14
-rw-r--r--dom/animation/test/mozilla/test_partial_keyframes.html14
-rw-r--r--dom/animation/test/mozilla/test_set-easing.html14
-rw-r--r--dom/animation/test/mozilla/test_spacing_property_order.html14
-rw-r--r--dom/animation/test/mozilla/test_spacing_transform.html14
-rw-r--r--dom/animation/test/mozilla/test_transform_limits.html14
-rw-r--r--dom/animation/test/mozilla/test_transition_finish_on_compositor.html14
-rw-r--r--dom/animation/test/mozilla/test_underlying-discrete-value.html15
28 files changed, 1616 insertions, 0 deletions
diff --git a/dom/animation/test/mozilla/file_cubic_bezier_limits.html b/dom/animation/test/mozilla/file_cubic_bezier_limits.html
new file mode 100644
index 000000000..a0378f395
--- /dev/null
+++ b/dom/animation/test/mozilla/file_cubic_bezier_limits.html
@@ -0,0 +1,167 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="../testcommon.js"></script>
+<body>
+<style>
+@keyframes anim {
+ to { margin-left: 100px; }
+}
+
+.transition-div {
+ margin-left: 100px;
+}
+</style>
+<script>
+'use strict';
+
+// We clamp +infinity or -inifinity value in floating point to
+// maximum floating point value or -maxinum floating point value.
+const max_float = 3.40282e+38;
+
+test(function(t) {
+ var div = addDiv(t);
+ var anim = div.animate({ }, 100 * MS_PER_SEC);
+
+ anim.effect.timing.easing = 'cubic-bezier(0, 1e+39, 0, 0)';
+ assert_equals(anim.effect.timing.easing,
+ 'cubic-bezier(0, ' + max_float + ', 0, 0)',
+ 'y1 control point for effect easing is out of upper boundary');
+
+ anim.effect.timing.easing = 'cubic-bezier(0, 0, 0, 1e+39)';
+ assert_equals(anim.effect.timing.easing,
+ 'cubic-bezier(0, 0, 0, ' + max_float + ')',
+ 'y2 control point for effect easing is out of upper boundary');
+
+ anim.effect.timing.easing = 'cubic-bezier(0, -1e+39, 0, 0)';
+ assert_equals(anim.effect.timing.easing,
+ 'cubic-bezier(0, ' + -max_float + ', 0, 0)',
+ 'y1 control point for effect easing is out of lower boundary');
+
+ anim.effect.timing.easing = 'cubic-bezier(0, 0, 0, -1e+39)';
+ assert_equals(anim.effect.timing.easing,
+ 'cubic-bezier(0, 0, 0, ' + -max_float + ')',
+ 'y2 control point for effect easing is out of lower boundary');
+
+}, 'Clamp y1 and y2 control point out of boundaries for effect easing' );
+
+test(function(t) {
+ var div = addDiv(t);
+ var anim = div.animate({ }, 100 * MS_PER_SEC);
+
+ anim.effect.setKeyframes([ { easing: 'cubic-bezier(0, 1e+39, 0, 0)' }]);
+ assert_equals(anim.effect.getKeyframes()[0].easing,
+ 'cubic-bezier(0, ' + max_float + ', 0, 0)',
+ 'y1 control point for keyframe easing is out of upper boundary');
+
+ anim.effect.setKeyframes([ { easing: 'cubic-bezier(0, 0, 0, 1e+39)' }]);
+ assert_equals(anim.effect.getKeyframes()[0].easing,
+ 'cubic-bezier(0, 0, 0, ' + max_float + ')',
+ 'y2 control point for keyframe easing is out of upper boundary');
+
+ anim.effect.setKeyframes([ { easing: 'cubic-bezier(0, -1e+39, 0, 0)' }]);
+ assert_equals(anim.effect.getKeyframes()[0].easing,
+ 'cubic-bezier(0, ' + -max_float + ', 0, 0)',
+ 'y1 control point for keyframe easing is out of lower boundary');
+
+ anim.effect.setKeyframes([ { easing: 'cubic-bezier(0, 0, 0, -1e+39)' }]);
+ assert_equals(anim.effect.getKeyframes()[0].easing,
+ 'cubic-bezier(0, 0, 0, ' + -max_float + ')',
+ 'y2 control point for keyframe easing is out of lower boundary');
+
+}, 'Clamp y1 and y2 control point out of boundaries for keyframe easing' );
+
+test(function(t) {
+ var div = addDiv(t);
+
+ div.style.animation = 'anim 100s cubic-bezier(0, 1e+39, 0, 0)';
+
+ assert_equals(div.getAnimations()[0].effect.getKeyframes()[0].easing,
+ 'cubic-bezier(0, ' + max_float + ', 0, 0)',
+ 'y1 control point for CSS animation is out of upper boundary');
+
+ div.style.animation = 'anim 100s cubic-bezier(0, 0, 0, 1e+39)';
+ assert_equals(div.getAnimations()[0].effect.getKeyframes()[0].easing,
+ 'cubic-bezier(0, 0, 0, ' + max_float + ')',
+ 'y2 control point for CSS animation is out of upper boundary');
+
+ div.style.animation = 'anim 100s cubic-bezier(0, -1e+39, 0, 0)';
+ assert_equals(div.getAnimations()[0].effect.getKeyframes()[0].easing,
+ 'cubic-bezier(0, ' + -max_float + ', 0, 0)',
+ 'y1 control point for CSS animation is out of lower boundary');
+
+ div.style.animation = 'anim 100s cubic-bezier(0, 0, 0, -1e+39)';
+ assert_equals(div.getAnimations()[0].effect.getKeyframes()[0].easing,
+ 'cubic-bezier(0, 0, 0, ' + -max_float + ')',
+ 'y2 control point for CSS animation is out of lower boundary');
+
+}, 'Clamp y1 and y2 control point out of boundaries for CSS animation' );
+
+test(function(t) {
+ var div = addDiv(t, {'class': 'transition-div'});
+
+ div.style.transition = 'margin-left 100s cubic-bezier(0, 1e+39, 0, 0)';
+ flushComputedStyle(div);
+ div.style.marginLeft = '0px';
+ assert_equals(div.getAnimations()[0].effect.getKeyframes()[0].easing,
+ 'cubic-bezier(0, ' + max_float + ', 0, 0)',
+ 'y1 control point for CSS transition on upper boundary');
+ div.style.transition = '';
+ div.style.marginLeft = '';
+
+ div.style.transition = 'margin-left 100s cubic-bezier(0, 0, 0, 1e+39)';
+ flushComputedStyle(div);
+ div.style.marginLeft = '0px';
+ assert_equals(div.getAnimations()[0].effect.getKeyframes()[0].easing,
+ 'cubic-bezier(0, 0, 0, ' + max_float + ')',
+ 'y2 control point for CSS transition on upper boundary');
+ div.style.transition = '';
+ div.style.marginLeft = '';
+
+ div.style.transition = 'margin-left 100s cubic-bezier(0, -1e+39, 0, 0)';
+ flushComputedStyle(div);
+ div.style.marginLeft = '0px';
+ assert_equals(div.getAnimations()[0].effect.getKeyframes()[0].easing,
+ 'cubic-bezier(0, ' + -max_float + ', 0, 0)',
+ 'y1 control point for CSS transition on lower boundary');
+ div.style.transition = '';
+ div.style.marginLeft = '';
+
+ div.style.transition = 'margin-left 100s cubic-bezier(0, 0, 0, -1e+39)';
+ flushComputedStyle(div);
+ div.style.marginLeft = '0px';
+ assert_equals(div.getAnimations()[0].effect.getKeyframes()[0].easing,
+ 'cubic-bezier(0, 0, 0, ' + -max_float + ')',
+ 'y2 control point for CSS transition on lower boundary');
+
+}, 'Clamp y1 and y2 control point out of boundaries for CSS transition' );
+
+test(function(t) {
+ var div = addDiv(t);
+ var anim = div.animate({ }, { duration: 100 * MS_PER_SEC, fill: 'forwards' });
+
+ anim.pause();
+ // The positive steepest function on both edges.
+ anim.effect.timing.easing = 'cubic-bezier(0, 1e+39, 0, 1e+39)';
+ assert_equals(anim.effect.getComputedTiming().progress, 0.0,
+ 'progress on lower edge for the highest value of y1 and y2 control points');
+
+ anim.finish();
+ assert_equals(anim.effect.getComputedTiming().progress, 1.0,
+ 'progress on upper edge for the highest value of y1 and y2 control points');
+
+ // The negative steepest function on both edges.
+ anim.effect.timing.easing = 'cubic-bezier(0, -1e+39, 0, -1e+39)';
+ anim.currentTime = 0;
+ assert_equals(anim.effect.getComputedTiming().progress, 0.0,
+ 'progress on lower edge for the lowest value of y1 and y2 control points');
+
+ anim.finish();
+ assert_equals(anim.effect.getComputedTiming().progress, 1.0,
+ 'progress on lower edge for the lowest value of y1 and y2 control points');
+
+}, 'Calculated values on both edges');
+
+done();
+
+</script>
+</body>
diff --git a/dom/animation/test/mozilla/file_deferred_start.html b/dom/animation/test/mozilla/file_deferred_start.html
new file mode 100644
index 000000000..3be3f56aa
--- /dev/null
+++ b/dom/animation/test/mozilla/file_deferred_start.html
@@ -0,0 +1,121 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="../testcommon.js"></script>
+<script src="/tests/SimpleTest/paint_listener.js"></script>
+<style>
+@keyframes empty { }
+@keyframes animTransform {
+ from { transform: translate(0px); }
+ to { transform: translate(100px); }
+}
+.target {
+ /* Element needs geometry to be eligible for layerization */
+ width: 100px;
+ height: 100px;
+ background-color: white;
+}
+</style>
+<body>
+<script>
+'use strict';
+
+function waitForDocLoad() {
+ return new Promise(function(resolve, reject) {
+ if (document.readyState === 'complete') {
+ resolve();
+ } else {
+ window.addEventListener('load', resolve);
+ }
+ });
+}
+
+function waitForPaints() {
+ return new Promise(function(resolve, reject) {
+ waitForAllPaintsFlushed(resolve);
+ });
+}
+
+promise_test(function(t) {
+ var div = addDiv(t);
+ var cs = window.getComputedStyle(div);
+
+ // Test that empty animations actually start.
+ //
+ // Normally we tie the start of animations to when their first frame of
+ // the animation is rendered. However, for animations that don't actually
+ // trigger a paint (e.g. because they are empty, or are animating something
+ // that doesn't render or is offscreen) we want to make sure they still
+ // start.
+ //
+ // Before we start, wait for the document to finish loading. This is because
+ // during loading we will have other paint events taking place which might,
+ // by luck, happen to trigger animations that otherwise would not have been
+ // triggered, leading to false positives.
+ //
+ // As a result, it's better to wait until we have a more stable state before
+ // continuing.
+ var promiseCallbackDone = false;
+ return waitForDocLoad().then(function() {
+ div.style.animation = 'empty 1000s';
+ var animation = div.getAnimations()[0];
+
+ return animation.ready.then(function() {
+ promiseCallbackDone = true;
+ }).catch(function() {
+ assert_unreached('ready promise was rejected');
+ });
+ }).then(function() {
+ // We need to wait for up to three frames. This is because in some
+ // cases it can take up to two frames for the initial layout
+ // to take place. Even after that happens we don't actually resolve the
+ // ready promise until the following tick.
+ return waitForAnimationFrames(3);
+ }).then(function() {
+ assert_true(promiseCallbackDone,
+ 'ready promise for an empty animation was resolved'
+ + ' within three animation frames');
+ });
+}, 'Animation.ready is resolved for an empty animation');
+
+// Test that compositor animations with delays get synced correctly
+//
+// NOTE: It is important that we DON'T use
+// SpecialPowers.DOMWindowUtils.advanceTimeAndRefresh here since that takes
+// us through a different code path.
+promise_test(function(t) {
+ // This test only applies to compositor animations
+ if (!isOMTAEnabled()) {
+ return;
+ }
+
+ // Setup animation
+ var div = addDiv(t);
+ div.classList.add('target');
+ div.style.animation = 'animTransform 100s -50s forwards';
+ var animation = div.getAnimations()[0];
+
+ return waitForPaints(function() {
+ var transformStr =
+ SpecialPowers.DOMWindowUtils.getOMTAStyle(div, 'transform');
+
+ var matrixComponents =
+ transformStr.startsWith('matrix(')
+ ? transformStr.substring('matrix('.length, transformStr.length-1)
+ .split(',')
+ .map(component => Number(component))
+ : [];
+ assert_equals(matrixComponents.length, 6,
+ 'Got a valid transform matrix on the compositor'
+ + ' (got: "' + transformStr + '")');
+
+ // If the delay has been applied correctly we should be at least
+ // half-way through the animation
+ assert_true(matrixComponents[4] >= 50,
+ 'Animation is at least half-way through on the compositor'
+ + ' (got translation of ' + matrixComponents[4] + ')');
+ });
+}, 'Starting an animation with a delay starts from the correct point');
+
+done();
+</script>
+</body>
diff --git a/dom/animation/test/mozilla/file_disable_animations_api_core.html b/dom/animation/test/mozilla/file_disable_animations_api_core.html
new file mode 100644
index 000000000..ef77988d9
--- /dev/null
+++ b/dom/animation/test/mozilla/file_disable_animations_api_core.html
@@ -0,0 +1,30 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="../testcommon.js"></script>
+<body>
+<script>
+'use strict';
+
+test(function(t) {
+ var div = addDiv(t);
+ var anim =
+ div.animate({ marginLeft: ['0px', '10px'] },
+ { duration: 100 * MS_PER_SEC,
+ easing: 'linear',
+ iterations: 10,
+ iterationComposite: 'accumulate' });
+ anim.pause();
+
+ // NOTE: We can't check iterationComposite value itself though API since
+ // Animation.effect is also behind the the Web Animations API. So we just
+ // check that style value is not affected by iterationComposite.
+ anim.currentTime = 200 * MS_PER_SEC;
+ assert_equals(getComputedStyle(div).marginLeft, '0px',
+ 'Animated style should not be accumulated when the Web Animations API is ' +
+ 'not enabled even if accumulate is specified in the constructor');
+}, 'iterationComposite should not affect at all if the Web Animations API ' +
+ 'is not enabled');
+
+done();
+</script>
+</body>
diff --git a/dom/animation/test/mozilla/file_disabled_properties.html b/dom/animation/test/mozilla/file_disabled_properties.html
new file mode 100644
index 000000000..f1b72973f
--- /dev/null
+++ b/dom/animation/test/mozilla/file_disabled_properties.html
@@ -0,0 +1,73 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="../testcommon.js"></script>
+<body>
+<script>
+'use strict';
+
+function waitForSetPref(pref, value) {
+ return new Promise(function(resolve, reject) {
+ SpecialPowers.pushPrefEnv({ 'set': [[pref, value]] }, resolve);
+ });
+}
+
+/*
+ * These tests rely on the fact that the -webkit-text-fill-color property
+ * is disabled by the layout.css.prefixes.webkit pref. If we ever remove that
+ * pref we will need to substitute some other pref:property combination.
+ */
+
+promise_test(function(t) {
+ return waitForSetPref('layout.css.prefixes.webkit', true).then(() => {
+ var anim = addDiv(t).animate({ webkitTextFillColor: [ 'green', 'blue' ]});
+ assert_equals(anim.effect.getKeyframes().length, 2,
+ 'A property-indexed keyframe specifying only enabled'
+ + ' properties produces keyframes');
+ return waitForSetPref('layout.css.prefixes.webkit', false);
+ }).then(() => {
+ var anim = addDiv(t).animate({ webkitTextFillColor: [ 'green', 'blue' ]});
+ assert_equals(anim.effect.getKeyframes().length, 0,
+ 'A property-indexed keyframe specifying only disabled'
+ + ' properties produces no keyframes');
+ });
+}, 'Specifying a disabled property using a property-indexed keyframe');
+
+promise_test(function(t) {
+ var createAnim = () => {
+ var anim = addDiv(t).animate([ { webkitTextFillColor: 'green' },
+ { webkitTextFillColor: 'blue' } ]);
+ assert_equals(anim.effect.getKeyframes().length, 2,
+ 'Animation specified using a keyframe sequence should'
+ + ' return the same number of keyframes regardless of'
+ + ' whether or not the specified properties are disabled');
+ return anim;
+ };
+
+ var assert_has_property = (anim, index, descr, property) => {
+ assert_true(
+ anim.effect.getKeyframes()[index].hasOwnProperty(property),
+ `${descr} should have the '${property}' property`);
+ };
+ var assert_does_not_have_property = (anim, index, descr, property) => {
+ assert_false(
+ anim.effect.getKeyframes()[index].hasOwnProperty(property),
+ `${descr} should NOT have the '${property}' property`);
+ };
+
+ return waitForSetPref('layout.css.prefixes.webkit', true).then(() => {
+ var anim = createAnim();
+ assert_has_property(anim, 0, 'Initial keyframe', 'webkitTextFillColor');
+ assert_has_property(anim, 1, 'Final keyframe', 'webkitTextFillColor');
+ return waitForSetPref('layout.css.prefixes.webkit', false);
+ }).then(() => {
+ var anim = createAnim();
+ assert_does_not_have_property(anim, 0, 'Initial keyframe',
+ 'webkitTextFillColor');
+ assert_does_not_have_property(anim, 1, 'Final keyframe',
+ 'webkitTextFillColor');
+ });
+}, 'Specifying a disabled property using a keyframe sequence');
+
+done();
+</script>
+</body>
diff --git a/dom/animation/test/mozilla/file_discrete-animations.html b/dom/animation/test/mozilla/file_discrete-animations.html
new file mode 100644
index 000000000..35e818a90
--- /dev/null
+++ b/dom/animation/test/mozilla/file_discrete-animations.html
@@ -0,0 +1,170 @@
+<!doctype html>
+<head>
+<meta charset=utf-8>
+<title>Test Mozilla-specific discrete animatable properties</title>
+<script type="application/javascript" src="../testcommon.js"></script>
+</head>
+<body>
+<script>
+"use strict";
+
+const gMozillaSpecificProperties = {
+ "-moz-appearance": {
+ // https://drafts.csswg.org/css-align/#propdef-align-content
+ from: "button",
+ to: "none"
+ },
+ "-moz-border-bottom-colors": {
+ from: "rgb(255, 0, 0) rgb(255, 0, 0) rgb(255, 0, 0) rgb(255, 0, 0)",
+ to: "rgb(0, 255, 0) rgb(0, 255, 0) rgb(0, 255, 0) rgb(0, 255, 0)"
+ },
+ "-moz-border-left-colors": {
+ from: "rgb(255, 0, 0) rgb(255, 0, 0) rgb(255, 0, 0) rgb(255, 0, 0)",
+ to: "rgb(0, 255, 0) rgb(0, 255, 0) rgb(0, 255, 0) rgb(0, 255, 0)"
+ },
+ "-moz-border-right-colors": {
+ from: "rgb(255, 0, 0) rgb(255, 0, 0) rgb(255, 0, 0) rgb(255, 0, 0)",
+ to: "rgb(0, 255, 0) rgb(0, 255, 0) rgb(0, 255, 0) rgb(0, 255, 0)"
+ },
+ "-moz-border-top-colors": {
+ from: "rgb(255, 0, 0) rgb(255, 0, 0) rgb(255, 0, 0) rgb(255, 0, 0)",
+ to: "rgb(0, 255, 0) rgb(0, 255, 0) rgb(0, 255, 0) rgb(0, 255, 0)"
+ },
+ "-moz-box-align": {
+ // https://developer.mozilla.org/en/docs/Web/CSS/box-align
+ from: "center",
+ to: "stretch"
+ },
+ "-moz-box-direction": {
+ // https://developer.mozilla.org/en/docs/Web/CSS/box-direction
+ from: "reverse",
+ to: "normal"
+ },
+ "-moz-box-ordinal-group": {
+ // https://developer.mozilla.org/en/docs/Web/CSS/box-ordinal-group
+ from: "1",
+ to: "5"
+ },
+ "-moz-box-orient": {
+ // https://www.w3.org/TR/css-flexbox-1/
+ from: "horizontal",
+ to: "vertical"
+ },
+ "-moz-box-pack": {
+ // https://www.w3.org/TR/2009/WD-css3-flexbox-20090723/#propdef-box-pack
+ from: "center",
+ to: "end"
+ },
+ "-moz-float-edge": {
+ // https://developer.mozilla.org/en/docs/Web/CSS/-moz-float-edge
+ from: "margin-box",
+ to: "content-box"
+ },
+ "-moz-force-broken-image-icon": {
+ // https://developer.mozilla.org/en/docs/Web/CSS/-moz-force-broken-image-icon
+ from: "1",
+ to: "5"
+ },
+ "image-rendering": {
+ // https://drafts.csswg.org/css-images-3/#propdef-image-rendering
+ from: "-moz-crisp-edges",
+ to: "auto"
+ },
+ "-moz-stack-sizing": {
+ // https://developer.mozilla.org/en/docs/Web/CSS/-moz-stack-sizing
+ from: "ignore",
+ to: "stretch-to-fit"
+ },
+ "-moz-tab-size": {
+ // https://drafts.csswg.org/css-text-3/#propdef-tab-size
+ from: "1",
+ to: "5"
+ },
+ "-moz-text-size-adjust": {
+ // https://drafts.csswg.org/css-size-adjust/#propdef-text-size-adjust
+ from: "none",
+ to: "auto"
+ },
+ "-webkit-text-stroke-width": {
+ // https://compat.spec.whatwg.org/#propdef--webkit-text-stroke-width
+ from: "10px",
+ to: "50px"
+ }
+}
+
+for (let property in gMozillaSpecificProperties) {
+ const testData = gMozillaSpecificProperties[property];
+ const from = testData.from;
+ const to = testData.to;
+ const idlName = propertyToIDL(property);
+ const keyframes = {};
+ keyframes[idlName] = [from, to];
+
+ test(t => {
+ const div = addDiv(t);
+ const animation = div.animate(keyframes,
+ { duration: 1000, fill: "both" });
+ testAnimationSamples(animation, idlName,
+ [{ time: 0, expected: from.toLowerCase() },
+ { time: 499, expected: from.toLowerCase() },
+ { time: 500, expected: to.toLowerCase() },
+ { time: 1000, expected: to.toLowerCase() }]);
+ }, property + " should animate between '"
+ + from + "' and '" + to + "' with linear easing");
+
+ test(function(t) {
+ // Easing: http://cubic-bezier.com/#.68,0,1,.01
+ // With this curve, we don't reach the 50% point until about 95% of
+ // the time has expired.
+ const div = addDiv(t);
+ const animation = div.animate(keyframes,
+ { duration: 1000, fill: "both",
+ easing: "cubic-bezier(0.68,0,1,0.01)" });
+ testAnimationSamples(animation, idlName,
+ [{ time: 0, expected: from.toLowerCase() },
+ { time: 940, expected: from.toLowerCase() },
+ { time: 960, expected: to.toLowerCase() }]);
+ }, property + " should animate between '"
+ + from + "' and '" + to + "' with effect easing");
+
+ test(function(t) {
+ // Easing: http://cubic-bezier.com/#.68,0,1,.01
+ // With this curve, we don't reach the 50% point until about 95% of
+ // the time has expired.
+ keyframes.easing = "cubic-bezier(0.68,0,1,0.01)";
+ const div = addDiv(t);
+ const animation = div.animate(keyframes,
+ { duration: 1000, fill: "both" });
+ testAnimationSamples(animation, idlName,
+ [{ time: 0, expected: from.toLowerCase() },
+ { time: 940, expected: from.toLowerCase() },
+ { time: 960, expected: to.toLowerCase() }]);
+ }, property + " should animate between '"
+ + from + "' and '" + to + "' with keyframe easing");
+}
+
+function propertyToIDL(property) {
+ var prefixMatch = property.match(/^-(\w+)-/);
+ if (prefixMatch) {
+ var prefix = prefixMatch[1] === "moz" ? "Moz" : prefixMatch[1];
+ property = prefix + property.substring(prefixMatch[0].length - 1);
+ }
+ // https://drafts.csswg.org/cssom/#css-property-to-idl-attribute
+ return property.replace(/-([a-z])/gi, function(str, group) {
+ return group.toUpperCase();
+ });
+}
+
+function testAnimationSamples(animation, idlName, testSamples) {
+ const target = animation.effect.target;
+ testSamples.forEach(testSample => {
+ animation.currentTime = testSample.time;
+ assert_equals(getComputedStyle(target)[idlName], testSample.expected,
+ "The value should be " + testSample.expected +
+ " at " + testSample.time + "ms");
+ });
+}
+
+done();
+</script>
+</body>
diff --git a/dom/animation/test/mozilla/file_document-timeline-origin-time-range.html b/dom/animation/test/mozilla/file_document-timeline-origin-time-range.html
new file mode 100644
index 000000000..083bf0903
--- /dev/null
+++ b/dom/animation/test/mozilla/file_document-timeline-origin-time-range.html
@@ -0,0 +1,30 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="../testcommon.js"></script>
+<body>
+<script>
+'use strict';
+
+// If the originTime parameter passed to the DocumentTimeline exceeds
+// the range of the internal storage type (a signed 64-bit integer number
+// of ticks--a platform-dependent unit) then we should throw.
+// Infinity isn't allowed as an origin time value and clamping to just
+// inside the allowed range will just mean we overflow elsewhere.
+
+test(function(t) {
+ assert_throws({ name: 'TypeError'},
+ function() {
+ new DocumentTimeline({ originTime: Number.MAX_SAFE_INTEGER });
+ });
+}, 'Calculated current time is positive infinity');
+
+test(function(t) {
+ assert_throws({ name: 'TypeError'},
+ function() {
+ new DocumentTimeline({ originTime: -1 * Number.MAX_SAFE_INTEGER });
+ });
+}, 'Calculated current time is negative infinity');
+
+done();
+</script>
+</body>
diff --git a/dom/animation/test/mozilla/file_hide_and_show.html b/dom/animation/test/mozilla/file_hide_and_show.html
new file mode 100644
index 000000000..0771fcce1
--- /dev/null
+++ b/dom/animation/test/mozilla/file_hide_and_show.html
@@ -0,0 +1,162 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="../testcommon.js"></script>
+<style>
+@keyframes move {
+ 100% {
+ transform: translateX(100px);
+ }
+}
+
+</style>
+<body>
+<script>
+'use strict';
+
+test(function(t) {
+ var div = addDiv(t, { style: 'animation: move 100s infinite' });
+ assert_equals(div.getAnimations().length, 1,
+ 'display:initial element has animations');
+
+ div.style.display = 'none';
+ assert_equals(div.getAnimations().length, 0,
+ 'display:none element has no animations');
+}, 'Animation stops playing when the element style display is set to "none"');
+
+test(function(t) {
+ var parentElement = addDiv(t);
+ var div = addDiv(t, { style: 'animation: move 100s infinite' });
+ parentElement.appendChild(div);
+ assert_equals(div.getAnimations().length, 1,
+ 'display:initial element has animations');
+
+ parentElement.style.display = 'none';
+ assert_equals(div.getAnimations().length, 0,
+ 'Element in display:none subtree has no animations');
+}, 'Animation stops playing when its parent element style display is set ' +
+ 'to "none"');
+
+test(function(t) {
+ var div = addDiv(t, { style: 'animation: move 100s infinite' });
+ assert_equals(div.getAnimations().length, 1,
+ 'display:initial element has animations');
+
+ div.style.display = 'none';
+ assert_equals(div.getAnimations().length, 0,
+ 'display:none element has no animations');
+
+ div.style.display = '';
+ assert_equals(div.getAnimations().length, 1,
+ 'Element which is no longer display:none has animations ' +
+ 'again');
+}, 'Animation starts playing when the element gets shown from ' +
+ '"display:none" state');
+
+test(function(t) {
+ var parentElement = addDiv(t);
+ var div = addDiv(t, { style: 'animation: move 100s infinite' });
+ parentElement.appendChild(div);
+ assert_equals(div.getAnimations().length, 1,
+ 'display:initial element has animations');
+
+ parentElement.style.display = 'none';
+ assert_equals(div.getAnimations().length, 0,
+ 'Element in display:none subtree has no animations');
+
+ parentElement.style.display = '';
+ assert_equals(div.getAnimations().length, 1,
+ 'Element which is no longer in display:none subtree has ' +
+ 'animations again');
+}, 'Animation starts playing when its parent element is shown from ' +
+ '"display:none" state');
+
+test(function(t) {
+ var div = addDiv(t, { style: 'animation: move 100s forwards' });
+ assert_equals(div.getAnimations().length, 1,
+ 'display:initial element has animations');
+
+ var animation = div.getAnimations()[0];
+ animation.finish();
+ assert_equals(div.getAnimations().length, 1,
+ 'Element has finished animation if the animation ' +
+ 'fill-mode is forwards');
+
+ div.style.display = 'none';
+ assert_equals(animation.playState, 'idle',
+ 'The animation.playState should be idle');
+
+ assert_equals(div.getAnimations().length, 0,
+ 'display:none element has no animations');
+
+ div.style.display = '';
+ assert_equals(div.getAnimations().length, 1,
+ 'Element which is no longer display:none has animations ' +
+ 'again');
+ assert_not_equals(div.getAnimations()[0], animation,
+ 'Restarted animation is a newly-generated animation');
+
+}, 'Animation which has already finished starts playing when the element ' +
+ 'gets shown from "display:none" state');
+
+test(function(t) {
+ var parentElement = addDiv(t);
+ var div = addDiv(t, { style: 'animation: move 100s forwards' });
+ parentElement.appendChild(div);
+ assert_equals(div.getAnimations().length, 1,
+ 'display:initial element has animations');
+
+ var animation = div.getAnimations()[0];
+ animation.finish();
+ assert_equals(div.getAnimations().length, 1,
+ 'Element has finished animation if the animation ' +
+ 'fill-mode is forwards');
+
+ parentElement.style.display = 'none';
+ assert_equals(animation.playState, 'idle',
+ 'The animation.playState should be idle');
+ assert_equals(div.getAnimations().length, 0,
+ 'Element in display:none subtree has no animations');
+
+ parentElement.style.display = '';
+ assert_equals(div.getAnimations().length, 1,
+ 'Element which is no longer in display:none subtree has ' +
+ 'animations again');
+
+ assert_not_equals(div.getAnimations()[0], animation,
+ 'Restarted animation is a newly-generated animation');
+
+}, 'Animation with fill:forwards which has already finished starts playing ' +
+ 'when its parent element is shown from "display:none" state');
+
+test(function(t) {
+ var parentElement = addDiv(t);
+ var div = addDiv(t, { style: 'animation: move 100s' });
+ parentElement.appendChild(div);
+ assert_equals(div.getAnimations().length, 1,
+ 'display:initial element has animations');
+
+ var animation = div.getAnimations()[0];
+ animation.finish();
+ assert_equals(div.getAnimations().length, 0,
+ 'Element does not have finished animations');
+
+ parentElement.style.display = 'none';
+ assert_equals(animation.playState, 'idle',
+ 'The animation.playState should be idle');
+ assert_equals(div.getAnimations().length, 0,
+ 'Element in display:none subtree has no animations');
+
+ parentElement.style.display = '';
+ assert_equals(div.getAnimations().length, 1,
+ 'Element which is no longer in display:none subtree has ' +
+ 'animations again');
+
+ assert_not_equals(div.getAnimations()[0], animation,
+ 'Restarted animation is a newly-generated animation');
+
+}, 'CSS Animation which has already finished starts playing when its parent ' +
+ 'element is shown from "display:none" state');
+
+done();
+</script>
+</body>
diff --git a/dom/animation/test/mozilla/file_partial_keyframes.html b/dom/animation/test/mozilla/file_partial_keyframes.html
new file mode 100644
index 000000000..68832be7a
--- /dev/null
+++ b/dom/animation/test/mozilla/file_partial_keyframes.html
@@ -0,0 +1,41 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="../testcommon.js"></script>
+<body>
+<script>
+'use strict';
+
+// Tests for cases we currently don't handle and should throw an exception for.
+var gTests = [
+ { desc: "single Keyframe with no offset",
+ keyframes: [{ left: "100px" }] },
+ { desc: "multiple Keyframes with missing 0% Keyframe",
+ keyframes: [{ left: "100px", offset: 0.25 },
+ { left: "200px", offset: 0.50 },
+ { left: "300px", offset: 1.00 }] },
+ { desc: "multiple Keyframes with missing 100% Keyframe",
+ keyframes: [{ left: "100px", offset: 0.00 },
+ { left: "200px", offset: 0.50 },
+ { left: "300px", offset: 0.75 }] },
+ { desc: "multiple Keyframes with missing properties on first Keyframe",
+ keyframes: [{ left: "100px", offset: 0.0 },
+ { left: "200px", top: "200px", offset: 0.5 },
+ { left: "300px", top: "300px", offset: 1.0 }] },
+ { desc: "multiple Keyframes with missing properties on last Keyframe",
+ keyframes: [{ left: "100px", top: "200px", offset: 0.0 },
+ { left: "200px", top: "200px", offset: 0.5 },
+ { left: "300px", offset: 1.0 }] },
+];
+
+gTests.forEach(function(subtest) {
+ test(function(t) {
+ var div = addDiv(t);
+ assert_throws("NotSupportedError", function() {
+ new KeyframeEffectReadOnly(div, subtest.keyframes);
+ });
+ }, "KeyframeEffectReadOnly constructor throws with " + subtest.desc);
+});
+
+done();
+</script>
+</body>
diff --git a/dom/animation/test/mozilla/file_set-easing.html b/dom/animation/test/mozilla/file_set-easing.html
new file mode 100644
index 000000000..072b125cb
--- /dev/null
+++ b/dom/animation/test/mozilla/file_set-easing.html
@@ -0,0 +1,34 @@
+<!doctype html>
+<head>
+<meta charset=utf-8>
+<title>Test setting easing in sandbox</title>
+<script src="../testcommon.js"></script>
+</head>
+<body>
+<script>
+"use strict";
+
+test(function(t) {
+ const div = document.createElement("div");
+ document.body.appendChild(div);
+ div.animate({ opacity: [0, 1] }, 100000 );
+
+ const contentScript = function() {
+ try {
+ document.getAnimations()[0].effect.timing.easing = "linear";
+ assert_true(true, 'Setting easing should not throw in sandbox');
+ } catch (e) {
+ assert_unreached('Setting easing threw ' + e);
+ }
+ };
+
+ const sandbox = new SpecialPowers.Cu.Sandbox(window);
+ sandbox.importFunction(document, "document");
+ sandbox.importFunction(assert_true, "assert_true");
+ sandbox.importFunction(assert_unreached, "assert_unreached");
+ SpecialPowers.Cu.evalInSandbox(`(${contentScript.toSource()})()`, sandbox);
+}, 'Setting easing should not throw any exceptions in sandbox');
+
+done();
+</script>
+</body>
diff --git a/dom/animation/test/mozilla/file_spacing_property_order.html b/dom/animation/test/mozilla/file_spacing_property_order.html
new file mode 100644
index 000000000..1338d6081
--- /dev/null
+++ b/dom/animation/test/mozilla/file_spacing_property_order.html
@@ -0,0 +1,33 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="../testcommon.js"></script>
+<body>
+<script>
+'use strict';
+
+test(function(t) {
+ var div = document.createElement('div');
+ document.documentElement.appendChild(div);
+ var anim = div.animate([ { borderRadius: "0", borderTopRightRadius: "10%" },
+ { borderTopLeftRadius: "20%",
+ borderTopRightRadius: "30%",
+ borderBottomRightRadius: "40%",
+ borderBottomLeftRadius: "50%" },
+ { borderRadius: "50%" } ],
+ { spacing:"paced(border-radius)" });
+
+ var frames = anim.effect.getKeyframes();
+ var dist = [ 0,
+ Math.sqrt(20 * 20 + (30 - 10) * (30 - 10) + 40 * 40 + 50 * 50),
+ Math.sqrt((50 - 20) * (50 - 20) + (50 - 30) * (50 - 30) +
+ (50 - 40) * (50 - 40) + (50 - 50) * (50 - 50)) ];
+ var cumDist = [];
+ dist.reduce(function(prev, curr, i) { return cumDist[i] = prev + curr; }, 0);
+ assert_approx_equals(frames[1].computedOffset, cumDist[1] / cumDist[2],
+ 0.0001, 'frame offset');
+}, 'Test for the longhand components of the shorthand property surely sorted' );
+
+done();
+
+</script>
+</body>
diff --git a/dom/animation/test/mozilla/file_spacing_transform.html b/dom/animation/test/mozilla/file_spacing_transform.html
new file mode 100644
index 000000000..0de773786
--- /dev/null
+++ b/dom/animation/test/mozilla/file_spacing_transform.html
@@ -0,0 +1,240 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="../testcommon.js"></script>
+<body>
+<script>
+'use strict';
+
+const pi = Math.PI;
+const cos = Math.cos;
+const sin = Math.sin;
+const tan = Math.tan;
+const sqrt = Math.sqrt;
+
+// Help function for testing the computed offsets by the distance array.
+function assert_animation_offsets(anim, dist) {
+ const epsilon = 0.00000001;
+ const frames = anim.effect.getKeyframes();
+ const cumDist = dist.reduce( (prev, curr) => {
+ prev.push(prev.length == 0 ? curr : curr + prev[prev.length - 1]);
+ return prev;
+ }, []);
+
+ const total = cumDist[cumDist.length - 1];
+ for (var i = 0; i < frames.length; ++i) {
+ assert_approx_equals(frames[i].computedOffset, cumDist[i] / total,
+ epsilon, 'computedOffset of frame ' + i);
+ }
+}
+
+function getAngleDist(rotate1, rotate2) {
+ function quaternion(axis, angle) {
+ var x = axis[0] * sin(angle/2.0);
+ var y = axis[1] * sin(angle/2.0);
+ var z = axis[2] * sin(angle/2.0);
+ var w = cos(angle/2.0);
+ return { 'x': x, 'y': y, 'z': z, 'w': w };
+ }
+ var q1 = quaternion(rotate1.axis, rotate1.angle);
+ var q2 = quaternion(rotate2.axis, rotate2.angle);
+ var dotProduct = q1.x * q2.x + q1.y * q2.y + q1.z * q2.z + q1.w * q2.w;
+ return 2.0 * Math.acos(dotProduct);
+}
+
+function createMatrix(elements, Is3D) {
+ return (Is3D ? "matrix3d" : "matrix") + "(" + elements.join() + ")";
+}
+
+test(function(t) {
+ var anim = addDiv(t).animate([ { transform: "none" },
+ { transform: "translate(-20px)" },
+ { transform: "translate(100px)" },
+ { transform: "translate(50px)"} ],
+ { spacing: "paced(transform)" });
+ assert_animation_offsets(anim, [ 0, 20, 120, 50 ]);
+}, 'Test spacing on translate' );
+
+test(function(t) {
+ var anim =
+ addDiv(t).animate([ { transform: "none" },
+ { transform: "translate3d(-20px, 10px, 100px)" },
+ { transform: "translate3d(100px, 200px, 50px)" },
+ { transform: "translate(50px, -10px)"} ],
+ { spacing: "paced(transform)" });
+ var dist = [ 0,
+ Math.sqrt(20 * 20 + 10 * 10 + 100 * 100),
+ Math.sqrt(120 * 120 + 190 * 190 + 50 * 50),
+ Math.sqrt(50 * 50 + 210 * 210 + 50 * 50) ];
+ assert_animation_offsets(anim, dist);
+}, 'Test spacing on translate3d' );
+
+test(function(t) {
+ var anim = addDiv(t).animate([ { transform: "scale(0.5)" },
+ { transform: "scale(4.5)" },
+ { transform: "scale(2.5)" },
+ { transform: "none"} ],
+ { spacing: "paced(transform)" });
+ assert_animation_offsets(anim, [ 0, 4.0, 2.0, 1.5 ]);
+}, 'Test spacing on scale' );
+
+test(function(t) {
+ var anim = addDiv(t).animate([ { transform: "scale(0.5, 0.5)" },
+ { transform: "scale3d(4.5, 5.0, 2.5)" },
+ { transform: "scale3d(2.5, 1.0, 2.0)" },
+ { transform: "scale3d(1, 0.5, 1.0)"} ],
+ { spacing:"paced(transform)" });
+ var dist = [ 0,
+ Math.sqrt(4.0 * 4.0 + 4.5 * 4.5 + 1.5 * 1.5),
+ Math.sqrt(2.0 * 2.0 + 4.0 * 4.0 + 0.5 * 0.5),
+ Math.sqrt(1.5 * 1.5 + 0.5 * 0.5 + 1.0 * 1.0) ];
+ assert_animation_offsets(anim, dist);
+}, 'Test spacing on scale3d' );
+
+test(function(t) {
+ var anim = addDiv(t).animate([ { transform: "rotate(60deg)" },
+ { transform: "none" },
+ { transform: "rotate(720deg)" },
+ { transform: "rotate(-360deg)"} ],
+ { spacing: "paced(transform)" });
+ assert_animation_offsets(anim, [ 0, 60, 720, 1080 ]);
+}, 'Test spacing on rotate' );
+
+test(function(t) {
+ var anim = addDiv(t).animate([ { transform: "rotate3d(1,0,0,60deg)" },
+ { transform: "rotate3d(1,0,0,70deg)" },
+ { transform: "rotate3d(0,0,1,-110deg)" },
+ { transform: "rotate3d(1,0,0,219deg)"} ],
+ { spacing: "paced(transform)" });
+ var dist = [ 0,
+ getAngleDist({ axis: [1,0,0], angle: 60 * pi / 180 },
+ { axis: [1,0,0], angle: 70 * pi / 180 }),
+ getAngleDist({ axis: [0,1,0], angle: 70 * pi / 180 },
+ { axis: [0,0,1], angle: -110 * pi / 180 }),
+ getAngleDist({ axis: [0,0,1], angle: -110 * pi / 180 },
+ { axis: [1,0,0], angle: 219 * pi / 180 }) ];
+ assert_animation_offsets(anim, dist);
+}, 'Test spacing on rotate3d' );
+
+test(function(t) {
+ var anim = addDiv(t).animate([ { transform: "skew(60deg)" },
+ { transform: "none" },
+ { transform: "skew(-90deg)" },
+ { transform: "skew(90deg)"} ],
+ { spacing: "paced(transform)" });
+ assert_animation_offsets(anim, [ 0, 60, 90, 180 ]);
+}, 'Test spacing on skew' );
+
+test(function(t) {
+ var anim = addDiv(t).animate([ { transform: "skew(60deg, 30deg)" },
+ { transform: "none" },
+ { transform: "skew(-90deg, 60deg)" },
+ { transform: "skew(90deg, 60deg)"} ],
+ { spacing: "paced(transform)" });
+ var dist = [ 0,
+ sqrt(60 * 60 + 30 * 30),
+ sqrt(90 * 90 + 60 * 60),
+ sqrt(180 * 180 + 0) ];
+ assert_animation_offsets(anim, dist);
+}, 'Test spacing on skew along both X and Y' );
+
+test(function(t) {
+ // We calculate the distance of two perspective functions by converting them
+ // into two matrix3ds, and then do matrix decomposition to get two
+ // perspective vectors, so the equivalent perspective vectors are:
+ // perspective 1: (0, 0, -1/128, 1);
+ // perspective 2: (0, 0, -1/infinity = 0, 1);
+ // perspective 3: (0, 0, -1/1024, 1);
+ // perspective 4: (0, 0, -1/32, 1);
+ var anim = addDiv(t).animate([ { transform: "perspective(128px)" },
+ { transform: "none" },
+ { transform: "perspective(1024px)" },
+ { transform: "perspective(32px)"} ],
+ { spacing: "paced(transform)" });
+ assert_animation_offsets(anim,
+ [ 0, 1/128, 1/1024, 1/32 - 1/1024 ]);
+}, 'Test spacing on perspective' );
+
+test(function(t) {
+ var anim =
+ addDiv(t).animate([ { transform: "none" },
+ { transform: "rotate(180deg) translate(0px)" },
+ { transform: "rotate(180deg) translate(1000px)" },
+ { transform: "rotate(360deg) translate(1000px)"} ],
+ { spacing: "paced(transform)" });
+ var dist = [ 0,
+ sqrt(pi * pi + 0),
+ sqrt(1000 * 1000),
+ sqrt(pi * pi + 0) ];
+ assert_animation_offsets(anim, dist);
+}, 'Test spacing on matched transform lists' );
+
+test(function(t) {
+ // matrix1 => translate(100px, 50px), skewX(60deg).
+ // matrix2 => translate(1000px), rotate(180deg).
+ // matrix3 => translate(1000px), scale(1.5, 0.7).
+ const matrix1 = createMatrix([ 1, 0, tan(pi/4.0), 1, 100, 50 ]);
+ const matrix2 = createMatrix([ cos(pi), sin(pi),
+ -sin(pi), cos(pi),
+ 1000, 0 ]);
+ const matrix3 = createMatrix([ 1.5, 0, 0, 0.7, 1000, 0 ]);
+ var anim = addDiv(t).animate([ { transform: "none" },
+ { transform: matrix1 },
+ { transform: matrix2 },
+ { transform: matrix3 } ],
+ { spacing: "paced(transform)" });
+ var dist = [ 0,
+ sqrt(100 * 100 + 50 * 50 + pi/4 * pi/4),
+ sqrt(900 * 900 + 50 * 50 + pi * pi + pi/4 * pi/4),
+ sqrt(pi * pi + 0.5 * 0.5 + 0.3 * 0.3) ];
+ assert_animation_offsets(anim, dist);
+}, 'Test spacing on matrix' );
+
+test(function(t) {
+ // matrix1 => translate3d(100px, 50px, -10px), skew(60deg).
+ // matrix2 => translate3d(1000px, 0, 0), rotate3d(1, 0, 0, 180deg).
+ // matrix3 => translate3d(1000px, 0, 0), scale3d(1.5, 0.7, 2.2).
+ const matrix1 = createMatrix([ 1, 0, 0, 0,
+ tan(pi/4.0), 1, 0, 0,
+ 0, 0, 1, 0,
+ 100, 50, -10, 1 ], true);
+ const matrix2 = createMatrix([ 1, 0, 0, 0,
+ 0, cos(pi), sin(pi), 0,
+ 0, -sin(pi), cos(pi), 0,
+ 1000, 0, 0, 1 ], true);
+ const matrix3 = createMatrix([ 1.5, 0, 0, 0,
+ 0, 0.7, 0, 0,
+ 0, 0, 2.2, 0,
+ 1000, 0, 0, 1 ], true);
+ var anim = addDiv(t).animate([ { transform: "none" },
+ { transform: matrix1 },
+ { transform: matrix2 },
+ { transform: matrix3 } ],
+ { spacing: "paced(transform)" });
+ var dist = [ 0,
+ sqrt(100 * 100 + 50 * 50 + 10 * 10 + pi/4 * pi/4),
+ sqrt(900 * 900 + 50 * 50 + 10 * 10 + pi/4 * pi/4 + pi * pi),
+ sqrt(0.5 * 0.5 + 0.3 * 0.3 + 1.2 * 1.2 + pi * pi) ];
+ assert_animation_offsets(anim, dist);
+}, 'Test spacing on matrix3d' );
+
+test(function(t) {
+ var anim =
+ addDiv(t).animate([ { transform: "none" },
+ { transform: "translate(100px, 50px) skew(45deg)" },
+ { transform: "translate(1000px) " +
+ "rotate3d(1, 0, 0, 180deg)" },
+ { transform: "translate(1000px) " +
+ "scale3d(2.5, 0.5, 0.7)" } ],
+ { spacing: "paced(transform)" });
+
+ var dist = [ 0,
+ sqrt(100 * 100 + 50 * 50 + pi/4 * pi/4),
+ sqrt(900 * 900 + 50 * 50 + pi/4 * pi/4 + pi * pi),
+ sqrt(1.5 * 1.5 + 0.5 * 0.5 + 0.3 * 0.3 + pi * pi) ];
+ assert_animation_offsets(anim, dist);
+}, 'Test spacing on mismatched transform list' );
+
+done();
+
+</script>
+</body>
diff --git a/dom/animation/test/mozilla/file_transform_limits.html b/dom/animation/test/mozilla/file_transform_limits.html
new file mode 100644
index 000000000..d4c813c67
--- /dev/null
+++ b/dom/animation/test/mozilla/file_transform_limits.html
@@ -0,0 +1,55 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="../testcommon.js"></script>
+<body>
+<script>
+'use strict';
+
+// We clamp +infinity or -inifinity value in floating point to
+// maximum floating point value or -maximum floating point value.
+const max_float = 3.40282e+38;
+
+test(function(t) {
+ var div = addDiv(t);
+ div.style = "width: 1px; height: 1px;";
+ var anim = div.animate([ { transform: 'scale(1)' },
+ { transform: 'scale(3.5e+38)'},
+ { transform: 'scale(3)' } ], 100 * MS_PER_SEC);
+
+ anim.pause();
+ anim.currentTime = 50 * MS_PER_SEC;
+ assert_equals(getComputedStyle(div).transform,
+ 'matrix(' + max_float + ', 0, 0, ' + max_float + ', 0, 0)');
+}, 'Test that the parameter of transform scale is clamped' );
+
+test(function(t) {
+ var div = addDiv(t);
+ div.style = "width: 1px; height: 1px;";
+ var anim = div.animate([ { transform: 'translate(1px)' },
+ { transform: 'translate(3.5e+38px)'},
+ { transform: 'translate(3px)' } ], 100 * MS_PER_SEC);
+
+ anim.pause();
+ anim.currentTime = 50 * MS_PER_SEC;
+ assert_equals(getComputedStyle(div).transform,
+ 'matrix(1, 0, 0, 1, ' + max_float + ', 0)');
+}, 'Test that the parameter of transform translate is clamped' );
+
+test(function(t) {
+ var div = addDiv(t);
+ div.style = "width: 1px; height: 1px;";
+ var anim = div.animate([ { transform: 'matrix(0.5, 0, 0, 0.5, 0, 0)' },
+ { transform: 'matrix(2, 0, 0, 2, 3.5e+38, 0)'},
+ { transform: 'matrix(0, 2, 0, -2, 0, 0)' } ],
+ 100 * MS_PER_SEC);
+
+ anim.pause();
+ anim.currentTime = 50 * MS_PER_SEC;
+ assert_equals(getComputedStyle(div).transform,
+ 'matrix(2, 0, 0, 2, ' + max_float + ', 0)');
+}, 'Test that the parameter of transform matrix is clamped' );
+
+done();
+
+</script>
+</body>
diff --git a/dom/animation/test/mozilla/file_transition_finish_on_compositor.html b/dom/animation/test/mozilla/file_transition_finish_on_compositor.html
new file mode 100644
index 000000000..4912d05dd
--- /dev/null
+++ b/dom/animation/test/mozilla/file_transition_finish_on_compositor.html
@@ -0,0 +1,67 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="../testcommon.js"></script>
+<script src="/tests/SimpleTest/paint_listener.js"></script>
+<style>
+div {
+ /* Element needs geometry to be eligible for layerization */
+ width: 100px;
+ height: 100px;
+ background-color: white;
+}
+</style>
+<body>
+<script>
+'use strict';
+
+function waitForPaints() {
+ return new Promise(function(resolve, reject) {
+ waitForAllPaintsFlushed(resolve);
+ });
+}
+
+promise_test(t => {
+ // This test only applies to compositor animations
+ if (!isOMTAEnabled()) {
+ return;
+ }
+
+ var div = addDiv(t, { style: 'transition: transform 50ms; ' +
+ 'transform: translateX(0px)' });
+ getComputedStyle(div).transform;
+
+ div.style.transform = 'translateX(100px)';
+
+ var timeBeforeStart = window.performance.now();
+ return waitForPaints().then(() => {
+ // If it took over 50ms to paint the transition, we have no luck
+ // to test it. This situation will happen if GC runs while waiting for the
+ // paint.
+ if (window.performance.now() - timeBeforeStart >= 50) {
+ return;
+ }
+
+ var transform =
+ SpecialPowers.DOMWindowUtils.getOMTAStyle(div, 'transform');
+ assert_not_equals(transform, '',
+ 'The transition style is applied on the compositor');
+
+ // Generate artificial busyness on the main thread for 100ms.
+ var timeAtStart = window.performance.now();
+ while (window.performance.now() - timeAtStart < 100) {}
+
+ // Now the transition on the compositor should finish but stay at the final
+ // position because there was no chance to pull the transition back from
+ // the compositor.
+ transform =
+ SpecialPowers.DOMWindowUtils.getOMTAStyle(div, 'transform');
+ assert_equals(transform, 'matrix(1, 0, 0, 1, 100, 0)',
+ 'The final transition style is still applied on the ' +
+ 'compositor');
+ });
+}, 'Transition on the compositor keeps the final style while the main thread ' +
+ 'is busy even if the transition finished on the compositor');
+
+done();
+</script>
+</body>
diff --git a/dom/animation/test/mozilla/file_underlying-discrete-value.html b/dom/animation/test/mozilla/file_underlying-discrete-value.html
new file mode 100644
index 000000000..3be01b904
--- /dev/null
+++ b/dom/animation/test/mozilla/file_underlying-discrete-value.html
@@ -0,0 +1,192 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="../testcommon.js"></script>
+<body>
+<script>
+"use strict";
+
+// Tests that we correctly extract the underlying value when the animation
+// type is 'discrete'.
+const discreteTests = [
+ {
+ stylesheet: {
+ "@keyframes keyframes":
+ "from { align-content: flex-start; } to { align-content: flex-end; } "
+ },
+ expectedKeyframes: [
+ { computedOffset: 0, alignContent: "flex-start" },
+ { computedOffset: 1, alignContent: "flex-end" }
+ ],
+ explanation: "Test for fully-specified keyframes"
+ },
+ {
+ stylesheet: {
+ "@keyframes keyframes": "from { align-content: flex-start; }"
+ },
+ // The value of 100% should be 'stretch',
+ // but we are not supporting underlying value.
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=1295401
+ expectedKeyframes: [
+ { computedOffset: 0, alignContent: "flex-start" },
+ { computedOffset: 1, alignContent: "unset" }
+ ],
+ explanation: "Test for 0% keyframe only"
+ },
+ {
+ stylesheet: {
+ "@keyframes keyframes": "to { align-content: flex-end; }"
+ },
+ // The value of 0% should be 'stretch',
+ // but we are not supporting underlying value.
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=1295401
+ expectedKeyframes: [
+ { computedOffset: 0, alignContent: "unset" },
+ { computedOffset: 1, alignContent: "flex-end" }
+ ],
+ explanation: "Test for 100% keyframe only"
+ },
+ {
+ stylesheet: {
+ "@keyframes keyframes": "50% { align-content: center; }",
+ "#target": "align-content: space-between;"
+ },
+ expectedKeyframes: [
+ { computedOffset: 0, alignContent: "space-between" },
+ { computedOffset: 0.5, alignContent: "center" },
+ { computedOffset: 1, alignContent: "space-between" }
+ ],
+ explanation: "Test for no 0%/100% keyframes " +
+ "and specified style on target element"
+ },
+ {
+ stylesheet: {
+ "@keyframes keyframes": "50% { align-content: center; }"
+ },
+ attributes: {
+ style: "align-content: space-between"
+ },
+ expectedKeyframes: [
+ { computedOffset: 0, alignContent: "space-between" },
+ { computedOffset: 0.5, alignContent: "center" },
+ { computedOffset: 1, alignContent: "space-between" }
+ ],
+ explanation: "Test for no 0%/100% keyframes " +
+ "and specified style on target element using style attribute"
+ },
+ {
+ stylesheet: {
+ "@keyframes keyframes": "50% { align-content: center; }",
+ "#target": "align-content: inherit;"
+ },
+ // The value of 0%/100% should be 'stretch',
+ // but we are not supporting underlying value.
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=1295401
+ expectedKeyframes: [
+ { computedOffset: 0, alignContent: "inherit" },
+ { computedOffset: 0.5, alignContent: "center" },
+ { computedOffset: 1, alignContent: "inherit" }
+ ],
+ explanation: "Test for no 0%/100% keyframes " +
+ "and 'inherit' specified on target element"
+ },
+ {
+ stylesheet: {
+ "@keyframes keyframes": "50% { align-content: center; }",
+ ".target": "align-content: space-between;"
+ },
+ attributes: {
+ class: "target"
+ },
+ expectedKeyframes: [
+ { computedOffset: 0, alignContent: "space-between" },
+ { computedOffset: 0.5, alignContent: "center" },
+ { computedOffset: 1, alignContent: "space-between" }
+ ],
+ explanation: "Test for no 0%/100% keyframes " +
+ "and specified style on target element using class selector"
+ },
+ {
+ stylesheet: {
+ "@keyframes keyframes": "50% { align-content: center; }",
+ "div": "align-content: space-between;"
+ },
+ expectedKeyframes: [
+ { computedOffset: 0, alignContent: "space-between" },
+ { computedOffset: 0.5, alignContent: "center" },
+ { computedOffset: 1, alignContent: "space-between" }
+ ],
+ explanation: "Test for no 0%/100% keyframes " +
+ "and specified style on target element using type selector"
+ },
+ {
+ stylesheet: {
+ "@keyframes keyframes": "50% { align-content: center; }",
+ "div": "align-content: space-between;",
+ ".target": "align-content: flex-start;",
+ "#target": "align-content: flex-end;"
+ },
+ attributes: {
+ class: "target"
+ },
+ expectedKeyframes: [
+ { computedOffset: 0, alignContent: "flex-end" },
+ { computedOffset: 0.5, alignContent: "center" },
+ { computedOffset: 1, alignContent: "flex-end" }
+ ],
+ explanation: "Test for no 0%/100% keyframes " +
+ "and specified style on target element " +
+ "using ID selector that overrides class selector"
+ },
+ {
+ stylesheet: {
+ "@keyframes keyframes": "50% { align-content: center; }",
+ "div": "align-content: space-between !important;",
+ ".target": "align-content: flex-start;",
+ "#target": "align-content: flex-end;"
+ },
+ attributes: {
+ class: "target"
+ },
+ expectedKeyframes: [
+ { computedOffset: 0, alignContent: "space-between" },
+ { computedOffset: 0.5, alignContent: "center" },
+ { computedOffset: 1, alignContent: "space-between" }
+ ],
+ explanation: "Test for no 0%/100% keyframes " +
+ "and specified style on target element " +
+ "using important type selector that overrides other rules"
+ },
+];
+
+discreteTests.forEach(testcase => {
+ test(t => {
+ addStyle(t, testcase.stylesheet);
+
+ const div = addDiv(t, { "id": "target" });
+ if (testcase.attributes) {
+ for (let attributeName in testcase.attributes) {
+ div.setAttribute(attributeName, testcase.attributes[attributeName]);
+ }
+ }
+ div.style.animation = "keyframes 100s";
+
+ const keyframes = div.getAnimations()[0].effect.getKeyframes();
+ const expectedKeyframes = testcase.expectedKeyframes;
+ assert_equals(keyframes.length, expectedKeyframes.length,
+ `keyframes.length should be ${ expectedKeyframes.length }`);
+
+ keyframes.forEach((keyframe, index) => {
+ const expectedKeyframe = expectedKeyframes[index];
+ assert_equals(keyframe.computedOffset, expectedKeyframe.computedOffset,
+ `computedOffset of keyframes[${ index }] should be ` +
+ `${ expectedKeyframe.computedOffset }`);
+ assert_equals(keyframe.alignContent, expectedKeyframe.alignContent,
+ `alignContent of keyframes[${ index }] should be ` +
+ `${ expectedKeyframe.alignContent }`);
+ });
+ }, testcase.explanation);
+});
+
+done();
+</script>
+</body>
diff --git a/dom/animation/test/mozilla/test_cubic_bezier_limits.html b/dom/animation/test/mozilla/test_cubic_bezier_limits.html
new file mode 100644
index 000000000..e67e5dbbb
--- /dev/null
+++ b/dom/animation/test/mozilla/test_cubic_bezier_limits.html
@@ -0,0 +1,14 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+'use strict';
+setup({explicit_done: true});
+SpecialPowers.pushPrefEnv(
+ { "set": [["dom.animations-api.core.enabled", true]]},
+ function() {
+ window.open("file_cubic_bezier_limits.html");
+ });
+</script>
diff --git a/dom/animation/test/mozilla/test_deferred_start.html b/dom/animation/test/mozilla/test_deferred_start.html
new file mode 100644
index 000000000..4db4bf676
--- /dev/null
+++ b/dom/animation/test/mozilla/test_deferred_start.html
@@ -0,0 +1,14 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+'use strict';
+setup({explicit_done: true});
+SpecialPowers.pushPrefEnv(
+ { "set": [["dom.animations-api.core.enabled", true]]},
+ function() {
+ window.open("file_deferred_start.html");
+ });
+</script>
diff --git a/dom/animation/test/mozilla/test_disable_animations_api_core.html b/dom/animation/test/mozilla/test_disable_animations_api_core.html
new file mode 100644
index 000000000..cfb64e537
--- /dev/null
+++ b/dom/animation/test/mozilla/test_disable_animations_api_core.html
@@ -0,0 +1,14 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+'use strict';
+setup({explicit_done: true});
+SpecialPowers.pushPrefEnv(
+ { "set": [["dom.animations-api.core.enabled", false]]},
+ function() {
+ window.open("file_disable_animations_api_core.html");
+ });
+</script>
diff --git a/dom/animation/test/mozilla/test_disabled_properties.html b/dom/animation/test/mozilla/test_disabled_properties.html
new file mode 100644
index 000000000..86d02e6b6
--- /dev/null
+++ b/dom/animation/test/mozilla/test_disabled_properties.html
@@ -0,0 +1,14 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+'use strict';
+setup({explicit_done: true});
+SpecialPowers.pushPrefEnv(
+ { "set": [["dom.animations-api.core.enabled", true]]},
+ function() {
+ window.open("file_disabled_properties.html");
+ });
+</script>
diff --git a/dom/animation/test/mozilla/test_discrete-animations.html b/dom/animation/test/mozilla/test_discrete-animations.html
new file mode 100644
index 000000000..2a36bd50e
--- /dev/null
+++ b/dom/animation/test/mozilla/test_discrete-animations.html
@@ -0,0 +1,18 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+'use strict';
+setup({explicit_done: true});
+SpecialPowers.pushPrefEnv(
+ { "set": [
+ ["dom.animations-api.core.enabled", true],
+ ["layout.css.osx-font-smoothing.enabled", true],
+ ["layout.css.prefixes.webkit", true]
+ ] },
+ function() {
+ window.open("file_discrete-animations.html");
+ });
+</script>
diff --git a/dom/animation/test/mozilla/test_document-timeline-origin-time-range.html b/dom/animation/test/mozilla/test_document-timeline-origin-time-range.html
new file mode 100644
index 000000000..f73c233d3
--- /dev/null
+++ b/dom/animation/test/mozilla/test_document-timeline-origin-time-range.html
@@ -0,0 +1,14 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+'use strict';
+setup({explicit_done: true});
+SpecialPowers.pushPrefEnv(
+ { "set": [["dom.animations-api.core.enabled", true]]},
+ function() {
+ window.open("file_document-timeline-origin-time-range.html");
+ });
+</script>
diff --git a/dom/animation/test/mozilla/test_hide_and_show.html b/dom/animation/test/mozilla/test_hide_and_show.html
new file mode 100644
index 000000000..929a31bd4
--- /dev/null
+++ b/dom/animation/test/mozilla/test_hide_and_show.html
@@ -0,0 +1,14 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+'use strict';
+setup({explicit_done: true});
+SpecialPowers.pushPrefEnv(
+ { "set": [["dom.animations-api.core.enabled", true]]},
+ function() {
+ window.open("file_hide_and_show.html");
+ });
+</script>
diff --git a/dom/animation/test/mozilla/test_partial_keyframes.html b/dom/animation/test/mozilla/test_partial_keyframes.html
new file mode 100644
index 000000000..28eb4c588
--- /dev/null
+++ b/dom/animation/test/mozilla/test_partial_keyframes.html
@@ -0,0 +1,14 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+'use strict';
+setup({explicit_done: true});
+SpecialPowers.pushPrefEnv(
+ { "set": [["dom.animations-api.core.enabled", true]]},
+ function() {
+ window.open("file_partial_keyframes.html");
+ });
+</script>
diff --git a/dom/animation/test/mozilla/test_set-easing.html b/dom/animation/test/mozilla/test_set-easing.html
new file mode 100644
index 000000000..e0069ff1c
--- /dev/null
+++ b/dom/animation/test/mozilla/test_set-easing.html
@@ -0,0 +1,14 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+'use strict';
+setup({explicit_done: true});
+SpecialPowers.pushPrefEnv(
+ { "set": [["dom.animations-api.core.enabled", true]]},
+ function() {
+ window.open("file_set-easing.html");
+ });
+</script>
diff --git a/dom/animation/test/mozilla/test_spacing_property_order.html b/dom/animation/test/mozilla/test_spacing_property_order.html
new file mode 100644
index 000000000..afcc12bed
--- /dev/null
+++ b/dom/animation/test/mozilla/test_spacing_property_order.html
@@ -0,0 +1,14 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+'use strict';
+setup({explicit_done: true});
+SpecialPowers.pushPrefEnv(
+ { "set": [["dom.animations-api.core.enabled", true]]},
+ function() {
+ window.open("file_spacing_property_order.html");
+ });
+</script>
diff --git a/dom/animation/test/mozilla/test_spacing_transform.html b/dom/animation/test/mozilla/test_spacing_transform.html
new file mode 100644
index 000000000..38dce7e99
--- /dev/null
+++ b/dom/animation/test/mozilla/test_spacing_transform.html
@@ -0,0 +1,14 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+'use strict';
+setup({explicit_done: true});
+SpecialPowers.pushPrefEnv(
+ { "set": [["dom.animations-api.core.enabled", true]]},
+ function() {
+ window.open("file_spacing_transform.html");
+ });
+</script>
diff --git a/dom/animation/test/mozilla/test_transform_limits.html b/dom/animation/test/mozilla/test_transform_limits.html
new file mode 100644
index 000000000..6c9b5e4fa
--- /dev/null
+++ b/dom/animation/test/mozilla/test_transform_limits.html
@@ -0,0 +1,14 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+'use strict';
+setup({explicit_done: true});
+SpecialPowers.pushPrefEnv(
+ { "set": [["dom.animations-api.core.enabled", true]]},
+ function() {
+ window.open("file_transform_limits.html");
+ });
+</script>
diff --git a/dom/animation/test/mozilla/test_transition_finish_on_compositor.html b/dom/animation/test/mozilla/test_transition_finish_on_compositor.html
new file mode 100644
index 000000000..357e5297e
--- /dev/null
+++ b/dom/animation/test/mozilla/test_transition_finish_on_compositor.html
@@ -0,0 +1,14 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+'use strict';
+setup({explicit_done: true});
+SpecialPowers.pushPrefEnv(
+ { "set": [["dom.animations-api.core.enabled", true]]},
+ function() {
+ window.open("file_transition_finish_on_compositor.html");
+ });
+</script>
diff --git a/dom/animation/test/mozilla/test_underlying-discrete-value.html b/dom/animation/test/mozilla/test_underlying-discrete-value.html
new file mode 100644
index 000000000..7feee53a1
--- /dev/null
+++ b/dom/animation/test/mozilla/test_underlying-discrete-value.html
@@ -0,0 +1,15 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+'use strict';
+setup({explicit_done: true});
+SpecialPowers.pushPrefEnv(
+ { "set": [["dom.animations-api.core.enabled", true]]},
+ function() {
+ window.open("file_underlying-discrete-value.html");
+ });
+</script>
+</html>