diff options
Diffstat (limited to 'dom/animation/test')
132 files changed, 15256 insertions, 0 deletions
diff --git a/dom/animation/test/chrome.ini b/dom/animation/test/chrome.ini new file mode 100644 index 0000000000..9026bcbd2e --- /dev/null +++ b/dom/animation/test/chrome.ini @@ -0,0 +1,17 @@ +[DEFAULT] +support-files = + testcommon.js + ../../imptests/testharness.js + ../../imptests/testharnessreport.js + !/dom/animation/test/chrome/file_animate_xrays.html + +[chrome/test_animate_xrays.html] +# file_animate_xrays.html needs to go in mochitest.ini since it is served +# over HTTP +[chrome/test_animation_observers.html] +[chrome/test_animation_performance_warning.html] +[chrome/test_animation_properties.html] +[chrome/test_generated_content_getAnimations.html] +[chrome/test_observers_for_sync_api.html] +[chrome/test_restyles.html] +[chrome/test_running_on_compositor.html] diff --git a/dom/animation/test/chrome/file_animate_xrays.html b/dom/animation/test/chrome/file_animate_xrays.html new file mode 100644 index 0000000000..8a68fc548f --- /dev/null +++ b/dom/animation/test/chrome/file_animate_xrays.html @@ -0,0 +1,19 @@ +<!doctype html> +<html> +<head> +<meta charset=utf-8> +<script> +Element.prototype.animate = function() { + throw 'Called animate() as defined in content document'; +} +// Bug 1211783: Use KeyframeEffect (not KeyframeEffectReadOnly) here +for (var obj of [KeyframeEffectReadOnly, Animation]) { + obj = function() { + throw 'Called overridden ' + String(obj) + ' constructor'; + }; +} +</script> +<body> +<div id="target"></div> +</body> +</html> diff --git a/dom/animation/test/chrome/test_animate_xrays.html b/dom/animation/test/chrome/test_animate_xrays.html new file mode 100644 index 0000000000..56b981bf15 --- /dev/null +++ b/dom/animation/test/chrome/test_animate_xrays.html @@ -0,0 +1,31 @@ +<!doctype html> +<head> +<meta charset=utf-8> +<script type="application/javascript" src="../testharness.js"></script> +<script type="application/javascript" src="../testharnessreport.js"></script> +<script type="application/javascript" src="../testcommon.js"></script> +</head> +<body> +<a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1045994" + target="_blank">Mozilla Bug 1045994</a> +<div id="log"></div> +<iframe id="iframe" + src="http://example.org/tests/dom/animation/test/chrome/file_animate_xrays.html"></iframe> +<script> +'use strict'; + +var win = document.getElementById('iframe').contentWindow; + +async_test(function(t) { + window.addEventListener('load', t.step_func(function() { + var target = win.document.getElementById('target'); + var anim = target.animate({ opacity: [ 0, 1 ] }, 100 * MS_PER_SEC); + // In the x-ray case, the frames object will be given an opaque wrapper + // so it won't be possible to fetch any frames from it. + assert_equals(anim.effect.getKeyframes().length, 0); + t.done(); + })); +}, 'Calling animate() across x-rays'); + +</script> +</body> diff --git a/dom/animation/test/chrome/test_animation_observers.html b/dom/animation/test/chrome/test_animation_observers.html new file mode 100644 index 0000000000..237128e041 --- /dev/null +++ b/dom/animation/test/chrome/test_animation_observers.html @@ -0,0 +1,1177 @@ +<!DOCTYPE html> +<meta charset=utf-8> +<title>Test chrome-only MutationObserver animation notifications</title> +<script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> +<script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> +<script src="../testcommon.js"></script> +<link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"> +<style> +@keyframes anim { + to { transform: translate(100px); } +} +@keyframes anotherAnim { + to { transform: translate(0px); } +} +#target { + width: 100px; + height: 100px; + background-color: yellow; + line-height: 16px; +} +</style> +<div id=container><div id=target></div></div> +<script> +var div = document.getElementById("target"); +var gRecords = []; +var gObserver = new MutationObserver(function(newRecords) { + gRecords.push(...newRecords); +}); + +// Asynchronous testing framework based on layout/style/test/animation_utils.js. + +var gTests = []; +var gCurrentTestName; + +function addAsyncAnimTest(aName, aOptions, aTestGenerator) { + aTestGenerator.testName = aName; + aTestGenerator.options = aOptions || {}; + gTests.push(aTestGenerator); +} + +function runAsyncTest(aTestGenerator) { + return waitForFrame().then(function() { + var generator; + + function step(arg) { + var next; + try { + next = generator.next(arg); + } catch (e) { + return Promise.reject(e); + } + if (next.done) { + return Promise.resolve(next.value); + } else { + return Promise.resolve(next.value).then(step); + } + } + + var subtree = aTestGenerator.options.subtree; + + gCurrentTestName = aTestGenerator.testName; + if (subtree) { + gCurrentTestName += ":subtree"; + } + + gRecords = []; + gObserver.disconnect(); + gObserver.observe(aTestGenerator.options.observe, + { animations: true, subtree: subtree}); + + generator = aTestGenerator(); + return step(); + }); +}; + +function runAllAsyncTests() { + return gTests.reduce(function(sequence, test) { + return sequence.then(() => runAsyncTest(test)); + }, Promise.resolve()); +} + +// Wrap is and ok with versions that prepend the current sub-test name +// to the assertion description. +var old_is = is, old_ok = ok; +is = function(a, b, message) { + if (gCurrentTestName && message) { + message = `[${gCurrentTestName}] ${message}`; + } + old_is(a, b, message); +} +ok = function(a, message) { + if (gCurrentTestName && message) { + message = `[${gCurrentTestName}] ${message}`; + } + old_ok(a, message); +} + +// Adds an event listener and returns a Promise that is resolved when the +// event listener is called. +function await_event(aElement, aEventName) { + return new Promise(function(aResolve) { + function listener(aEvent) { + aElement.removeEventListener(aEventName, listener); + aResolve(); + } + aElement.addEventListener(aEventName, listener, false); + }); +} + +function assert_record_list(actual, expected, desc, index, listName) { + is(actual.length, expected.length, `${desc} - record[${index}].${listName} length`); + if (actual.length != expected.length) { + return; + } + for (var i = 0; i < actual.length; i++) { + ok(actual.indexOf(expected[i]) != -1, + `${desc} - record[${index}].${listName} contains expected Animation`); + } +} + +function assert_records(expected, desc) { + var records = gRecords; + gRecords = []; + is(records.length, expected.length, `${desc} - number of records`); + if (records.length != expected.length) { + return; + } + for (var i = 0; i < records.length; i++) { + assert_record_list(records[i].addedAnimations, expected[i].added, desc, i, "addedAnimations"); + assert_record_list(records[i].changedAnimations, expected[i].changed, desc, i, "changedAnimations"); + assert_record_list(records[i].removedAnimations, expected[i].removed, desc, i, "removedAnimations"); + } +} + +// -- Tests ------------------------------------------------------------------ + +// We run all tests first targeting the div and observing the div, then again +// targeting the div and observing its parent while using the subtree:true +// MutationObserver option. + +[ + { observe: div, target: div, subtree: false }, + { observe: div.parentNode, target: div, subtree: true }, +].forEach(function(aOptions) { + + var e = aOptions.target; + + // Test that starting a single transition that completes normally + // dispatches an added notification and then a removed notification. + addAsyncAnimTest("single_transition", aOptions, function*() { + // Start a transition. + e.style = "transition: background-color 100s; background-color: lime;"; + + // Register for the end of the transition. + var transitionEnd = await_event(e, "transitionend"); + + // The transition should cause the creation of a single Animation. + var animations = e.getAnimations(); + is(animations.length, 1, "getAnimations().length after transition start"); + + // Wait for the single MutationRecord for the Animation addition to + // be delivered. + yield waitForFrame(); + assert_records([{ added: animations, changed: [], removed: [] }], + "records after transition start"); + + // Advance until near the end of the transition, then wait for it to finish. + animations[0].currentTime = 99900; + yield transitionEnd; + + // After the transition has finished, the Animation should disappear. + is(e.getAnimations().length, 0, + "getAnimations().length after transition end"); + + // Wait for the change MutationRecord for seeking the Animation to be + // delivered, followed by the the removal MutationRecord. + yield waitForFrame(); + assert_records([{ added: [], changed: animations, removed: [] }, + { added: [], changed: [], removed: animations }], + "records after transition end"); + + e.style = ""; + }); + + // Test that starting a single transition that is cancelled by resetting + // the transition-property property dispatches an added notification and + // then a removed notification. + addAsyncAnimTest("single_transition_cancelled_property", aOptions, function*() { + // Start a long transition. + e.style = "transition: background-color 100s; background-color: lime;"; + + // The transition should cause the creation of a single Animation. + var animations = e.getAnimations(); + is(animations.length, 1, "getAnimations().length after transition start"); + + // Wait for the single MutationRecord for the Animation addition to + // be delivered. + yield waitForFrame(); + assert_records([{ added: animations, changed: [], removed: [] }], + "records after transition start"); + + // Cancel the transition by setting transition-property. + e.style.transitionProperty = "none"; + + // Wait for the single MutationRecord for the Animation removal to + // be delivered. + yield waitForFrame(); + assert_records([{ added: [], changed: [], removed: animations }], + "records after transition end"); + + e.style = ""; + }); + + // Test that starting a single transition that is cancelled by setting + // style to the currently animated value dispatches an added + // notification and then a removed notification. + addAsyncAnimTest("single_transition_cancelled_value", aOptions, function*() { + // Start a long transition with a predictable value. + e.style = "transition: background-color 100s steps(2, end) -51s; background-color: lime;"; + + // The transition should cause the creation of a single Animation. + var animations = e.getAnimations(); + is(animations.length, 1, "getAnimations().length after transition start"); + + // Wait for the single MutationRecord for the Animation addition to + // be delivered. + yield waitForFrame(); + assert_records([{ added: animations, changed: [], removed: [] }], + "records after transition start"); + + // Cancel the transition by setting the current animation value. + var value = "rgb(128, 255, 0)"; + is(getComputedStyle(e).backgroundColor, value, "half-way transition value"); + e.style.backgroundColor = value; + + // Wait for the single MutationRecord for the Animation removal to + // be delivered. + yield waitForFrame(); + assert_records([{ added: [], changed: [], removed: animations }], + "records after transition end"); + + e.style = ""; + }); + + // Test that starting a single transition that is cancelled by setting + // style to a non-interpolable value dispatches an added notification + // and then a removed notification. + addAsyncAnimTest("single_transition_cancelled_noninterpolable", aOptions, function*() { + // Start a long transition. + e.style = "transition: line-height 100s; line-height: 100px;"; + + // The transition should cause the creation of a single Animation. + var animations = e.getAnimations(); + is(animations.length, 1, "getAnimations().length after transition start"); + + // Wait for the single MutationRecord for the Animation addition to + // be delivered. + yield waitForFrame(); + assert_records([{ added: animations, changed: [], removed: [] }], + "records after transition start"); + + // Cancel the transition by setting line-height to a non-interpolable value. + e.style.lineHeight = "normal"; + + // Wait for the single MutationRecord for the Animation removal to + // be delivered. + yield waitForFrame(); + assert_records([{ added: [], changed: [], removed: animations }], + "records after transition end"); + + e.style = ""; + }); + + // Test that starting a single transition and then reversing it + // dispatches an added notification, then a simultaneous removed and + // added notification, then a removed notification once finished. + addAsyncAnimTest("single_transition_reversed", aOptions, function*() { + // Start a long transition. + e.style = "transition: background-color 100s step-start; background-color: lime;"; + + // The transition should cause the creation of a single Animation. + var animations = e.getAnimations(); + is(animations.length, 1, "getAnimations().length after transition start"); + + var firstAnimation = animations[0]; + + // Wait for the single MutationRecord for the Animation addition to + // be delivered. + yield waitForFrame(); + assert_records([{ added: [firstAnimation], changed: [], removed: [] }], + "records after transition start"); + + // Wait for the Animation to be playing, then seek well into + // the transition. + yield firstAnimation.ready; + firstAnimation.currentTime = 50 * MS_PER_SEC; + + // Reverse the transition by setting the background-color back to its + // original value. + e.style.backgroundColor = "yellow"; + + // The reversal should cause the creation of a new Animation. + animations = e.getAnimations(); + is(animations.length, 1, "getAnimations().length after transition reversal"); + + var secondAnimation = animations[0]; + + ok(firstAnimation != secondAnimation, + "second Animation should be different from the first"); + + // Wait for the change Mutation record from seeking the first animation + // to be delivered, followed by a subsequent MutationRecord for the removal + // of the original Animation and the addition of the new Animation. + yield waitForFrame(); + assert_records([{ added: [], changed: [firstAnimation], removed: [] }, + { added: [secondAnimation], changed: [], + removed: [firstAnimation] }], + "records after transition reversal"); + + // Cancel the transition. + e.style.transitionProperty = "none"; + + // Wait for the single MutationRecord for the Animation removal to + // be delivered. + yield waitForFrame(); + assert_records([{ added: [], changed: [], removed: [secondAnimation] }], + "records after transition end"); + + e.style = ""; + }); + + // Test that multiple transitions starting and ending on an element + // at the same time get batched up into a single MutationRecord. + addAsyncAnimTest("multiple_transitions", aOptions, function*() { + // Start three long transitions. + e.style = "transition-duration: 100s; " + + "transition-property: color, background-color, line-height; " + + "color: blue; background-color: lime; line-height: 24px;"; + + // The transitions should cause the creation of three Animations. + var animations = e.getAnimations(); + is(animations.length, 3, "getAnimations().length after transition starts"); + + // Wait for the single MutationRecord for the Animation additions to + // be delivered. + yield waitForFrame(); + assert_records([{ added: animations, changed: [], removed: [] }], + "records after transition starts"); + + // Wait for the Animations to get going. + yield animations[0].ready; + is(animations.filter(p => p.playState == "running").length, 3, + "number of running Animations"); + + // Seek well into each animation. + animations.forEach(p => p.currentTime = 50 * MS_PER_SEC); + + // Prepare the set of expected change MutationRecords, one for each + // animation that was seeked. + var seekRecords = animations.map( + p => ({ added: [], changed: [p], removed: [] }) + ); + + // Cancel one of the transitions by setting transition-property. + e.style.transitionProperty = "background-color, line-height"; + + var colorAnimation = animations.filter(p => p.playState != "running"); + var otherAnimations = animations.filter(p => p.playState == "running"); + + is(colorAnimation.length, 1, + "number of non-running Animations after cancelling one"); + is(otherAnimations.length, 2, + "number of running Animations after cancelling one"); + + // Wait for the MutationRecords to be delivered: one for each animation + // that was seeked, followed by one for the removal of the color animation. + yield waitForFrame(); + assert_records(seekRecords.concat( + { added: [], changed: [], removed: colorAnimation }), + "records after color transition end"); + + // Cancel the remaining transitions. + e.style.transitionProperty = "none"; + + // Wait for the MutationRecord for the other two Animation + // removals to be delivered. + yield waitForFrame(); + assert_records([{ added: [], changed: [], removed: otherAnimations }], + "records after other transition ends"); + + e.style = ""; + }); + + // Test that starting a single animation that completes normally + // dispatches an added notification and then a removed notification. + addAsyncAnimTest("single_animation", aOptions, function*() { + // Start an animation. + e.style = "animation: anim 100s;"; + + // Register for the end of the animation. + var animationEnd = await_event(e, "animationend"); + + // The animation should cause the creation of a single Animation. + var animations = e.getAnimations(); + is(animations.length, 1, "getAnimations().length after animation start"); + + // Wait for the single MutationRecord for the Animation addition to + // be delivered. + yield waitForFrame(); + assert_records([{ added: animations, changed: [], removed: [] }], + "records after animation start"); + + // Advance until near the end of the animation, then wait for it to finish. + animations[0].currentTime = 99900; + yield animationEnd; + + // After the animation has finished, the Animation should disappear. + is(e.getAnimations().length, 0, + "getAnimations().length after animation end"); + + // Wait for the change MutationRecord from seeking the Animation to + // be delivered, followed by a further MutationRecord for the Animation + // removal. + yield waitForFrame(); + assert_records([{ added: [], changed: animations, removed: [] }, + { added: [], changed: [], removed: animations }], + "records after animation end"); + + e.style = ""; + }); + + // Test that starting a single animation that is cancelled by resetting + // the animation-name property dispatches an added notification and + // then a removed notification. + addAsyncAnimTest("single_animation_cancelled_name", aOptions, function*() { + // Start a long animation. + e.style = "animation: anim 100s;"; + + // The animation should cause the creation of a single Animation. + var animations = e.getAnimations(); + is(animations.length, 1, "getAnimations().length after animation start"); + + // Wait for the single MutationRecord for the Animation addition to + // be delivered. + yield waitForFrame(); + assert_records([{ added: animations, changed: [], removed: [] }], + "records after animation start"); + + // Cancel the animation by setting animation-name. + e.style.animationName = "none"; + + // Wait for the single MutationRecord for the Animation removal to + // be delivered. + yield waitForFrame(); + assert_records([{ added: [], changed: [], removed: animations }], + "records after animation end"); + + e.style = ""; + }); + + // Test that starting a single animation that is cancelled by updating + // the animation-duration property dispatches an added notification and + // then a removed notification. + addAsyncAnimTest("single_animation_cancelled_duration", aOptions, function*() { + // Start a long animation. + e.style = "animation: anim 100s;"; + + // The animation should cause the creation of a single Animation. + var animations = e.getAnimations(); + is(animations.length, 1, "getAnimations().length after animation start"); + + // Wait for the single MutationRecord for the Animation addition to + // be delivered. + yield waitForFrame(); + assert_records([{ added: animations, changed: [], removed: [] }], + "records after animation start"); + + // Advance the animation by a second. + animations[0].currentTime += 1 * MS_PER_SEC; + + // Cancel the animation by setting animation-duration to a value less + // than a second. + e.style.animationDuration = "0.1s"; + + // Wait for the change MutationRecord from seeking the Animation to + // be delivered, followed by a further MutationRecord for the Animation + // removal. + yield waitForFrame(); + assert_records([{ added: [], changed: animations, removed: [] }, + { added: [], changed: [], removed: animations }], + "records after animation end"); + + e.style = ""; + }); + + // Test that starting a single animation that is cancelled by updating + // the animation-delay property dispatches an added notification and + // then a removed notification. + addAsyncAnimTest("single_animation_cancelled_delay", aOptions, function*() { + // Start a long animation. + e.style = "animation: anim 100s;"; + + // The animation should cause the creation of a single Animation. + var animations = e.getAnimations(); + is(animations.length, 1, "getAnimations().length after animation start"); + + // Wait for the single MutationRecord for the Animation addition to + // be delivered. + yield waitForFrame(); + assert_records([{ added: animations, changed: [], removed: [] }], + "records after animation start"); + + // Cancel the animation by setting animation-delay. + e.style.animationDelay = "-200s"; + + // Wait for the single MutationRecord for the Animation removal to + // be delivered. + yield waitForFrame(); + assert_records([{ added: [], changed: [], removed: animations }], + "records after animation end"); + + e.style = ""; + }); + + // Test that starting a single animation that is cancelled by updating + // the animation-fill-mode property dispatches an added notification and + // then a removed notification. + addAsyncAnimTest("single_animation_cancelled_fill", aOptions, function*() { + // Start a short, filled animation. + e.style = "animation: anim 100s forwards;"; + + // Register for the end of the animation. + var animationEnd = await_event(e, "animationend"); + + // The animation should cause the creation of a single Animation. + var animations = e.getAnimations(); + is(animations.length, 1, "getAnimations().length after animation start"); + + // Wait for the single MutationRecord for the Animation addition to + // be delivered. + yield waitForFrame(); + assert_records([{ added: animations, changed: [], removed: [] }], + "records after animation start"); + + // Advance until near the end of the animation, then wait for it to finish. + animations[0].currentTime = 99900; + yield animationEnd; + + // The only MutationRecord at this point should be the change from + // seeking the Animation. + assert_records([{ added: [], changed: animations, removed: [] }], + "records after animation starts filling"); + + // Cancel the animation by setting animation-fill-mode. + e.style.animationFillMode = "none"; + + // Wait for the single MutationRecord for the Animation removal to + // be delivered. + yield waitForFrame(); + assert_records([{ added: [], changed: [], removed: animations }], + "records after animation end"); + + e.style = ""; + }); + + // Test that starting a single animation that is cancelled by updating + // the animation-iteration-count property dispatches an added notification + // and then a removed notification. + addAsyncAnimTest("single_animation_cancelled_iteration_count", + aOptions, function*() { + // Start a short, repeated animation. + e.style = "animation: anim 0.5s infinite;"; + + // The animation should cause the creation of a single Animation. + var animations = e.getAnimations(); + is(animations.length, 1, "getAnimations().length after animation start"); + + // Wait for the single MutationRecord for the Animation addition to + // be delivered. + yield waitForFrame(); + assert_records([{ added: animations, changed: [], removed: [] }], + "records after animation start"); + + // Advance the animation until we are past the first iteration. + animations[0].currentTime += 1 * MS_PER_SEC; + + // The only MutationRecord at this point should be the change from + // seeking the Animation. + yield waitForFrame(); + assert_records([{ added: [], changed: animations, removed: [] }], + "records after seeking animations"); + + // Cancel the animation by setting animation-iteration-count. + e.style.animationIterationCount = "1"; + + // Wait for the single MutationRecord for the Animation removal to + // be delivered. + yield waitForFrame(); + assert_records([{ added: [], changed: [], removed: animations }], + "records after animation end"); + + e.style = ""; + }); + + // Test that updating an animation property dispatches a changed notification. + [ + { name: "duration", prop: "animationDuration", val: "200s" }, + { name: "timing", prop: "animationTimingFunction", val: "linear" }, + { name: "iteration", prop: "animationIterationCount", val: "2" }, + { name: "direction", prop: "animationDirection", val: "reverse" }, + { name: "state", prop: "animationPlayState", val: "paused" }, + { name: "delay", prop: "animationDelay", val: "-1s" }, + { name: "fill", prop: "animationFillMode", val: "both" }, + ].forEach(function(aChangeTest) { + addAsyncAnimTest(`single_animation_change_${aChangeTest.name}`, aOptions, function*() { + // Start a long animation. + e.style = "animation: anim 100s;"; + + // The animation should cause the creation of a single Animation. + var animations = e.getAnimations(); + is(animations.length, 1, "getAnimations().length after animation start"); + + // Wait for the single MutationRecord for the Animation addition to + // be delivered. + yield waitForFrame(); + assert_records([{ added: animations, changed: [], removed: [] }], + "records after animation start"); + + // Change a property of the animation such that it keeps running. + e.style[aChangeTest.prop] = aChangeTest.val; + + // Wait for the single MutationRecord for the Animation change to + // be delivered. + yield waitForFrame(); + assert_records([{ added: [], changed: animations, removed: [] }], + "records after animation change"); + + // Cancel the animation. + e.style.animationName = "none"; + + // Wait for the addition, change and removal MutationRecords to be delivered. + yield waitForFrame(); + assert_records([{ added: [], changed: [], removed: animations }], + "records after animation end"); + + e.style = ""; + }); + }); + + // Test that calling finish() on a paused (but otherwise finished) animation + // dispatches a changed notification. + addAsyncAnimTest("finish_from_pause", aOptions, function*() { + // Start a long animation + e.style = "animation: anim 100s forwards"; + + // The animation should cause the creation of a single Animation. + var animations = e.getAnimations(); + is(animations.length, 1, "getAnimations().length after animation start"); + + // Wait for the single MutationRecord for the Animation addition to + // be delivered. + yield waitForFrame(); + assert_records([{ added: animations, changed: [], removed: [] }], + "records after animation start"); + + // Wait until the animation is playing. + yield animations[0].ready; + + // Finish and pause. + animations[0].finish(); + animations[0].pause(); + + // Wait for the pause to complete. + yield animations[0].ready; + is(animations[0].playState, "paused", + "playState after finishing and pausing"); + + // We should have two MutationRecords for the Animation changes: + // one for the finish, one for the pause. + assert_records([{ added: [], changed: animations, removed: [] }, + { added: [], changed: animations, removed: [] }], + "records after finish() and pause()"); + + // Call finish() again. + animations[0].finish(); + is(animations[0].playState, "finished", + "playState after finishing from paused state"); + + // Wait for the single MutationRecord for the Animation change to + // be delivered. Even though the currentTime does not change, the + // playState will change. + yield waitForFrame(); + assert_records([{ added: [], changed: animations, removed: [] }], + "records after finish() and pause()"); + + // Cancel the animation. + e.style = ""; + + // Wait for the single removal notification. + yield waitForFrame(); + assert_records([{ added: [], changed: [], removed: animations }], + "records after animation end"); + }); + + // Test that calling finish() on a pause-pending (but otherwise finished) + // animation dispatches a changed notification. + addAsyncAnimTest("finish_from_pause_pending", aOptions, function*() { + // Start a long animation + e.style = "animation: anim 100s forwards"; + + // The animation should cause the creation of a single Animation. + var animations = e.getAnimations(); + is(animations.length, 1, "getAnimations().length after animation start"); + + // Wait for the single MutationRecord for the Animation addition to + // be delivered. + yield waitForFrame(); + assert_records([{ added: animations, changed: [], removed: [] }], + "records after animation start"); + + // Wait until the animation is playing. + yield animations[0].ready; + + // Finish and pause. + animations[0].finish(); + animations[0].pause(); + is(animations[0].playState, "pending", + "playState after finishing and calling pause()"); + + // Call finish() again to abort the pause + animations[0].finish(); + is(animations[0].playState, "finished", + "playState after finishing and calling pause()"); + + // Wait for three MutationRecords for the Animation changes to + // be delivered: one for each finish(), pause(), finish() operation. + yield waitForFrame(); + assert_records([{ added: [], changed: animations, removed: [] }, + { added: [], changed: animations, removed: [] }, + { added: [], changed: animations, removed: [] }], + "records after finish(), pause(), finish()"); + + // Cancel the animation. + e.style = ""; + + // Wait for the single removal notification. + yield waitForFrame(); + assert_records([{ added: [], changed: [], removed: animations }], + "records after animation end"); + }); + + // Test that calling play() on a paused Animation dispatches a changed + // notification. + addAsyncAnimTest("play", aOptions, function*() { + // Start a long, paused animation + e.style = "animation: anim 100s paused"; + + // The animation should cause the creation of a single Animation. + var animations = e.getAnimations(); + is(animations.length, 1, "getAnimations().length after animation start"); + + // Wait for the single MutationRecord for the Animation addition to + // be delivered. + yield waitForFrame(); + assert_records([{ added: animations, changed: [], removed: [] }], + "records after animation start"); + + // Wait until the animation is ready + yield animations[0].ready; + + // Play + animations[0].play(); + + // Wait for the single MutationRecord for the Animation change to + // be delivered. + yield animations[0].ready; + assert_records([{ added: [], changed: animations, removed: [] }], + "records after play()"); + + // Redundant play + animations[0].play(); + + // Wait to ensure no change is dispatched + yield waitForFrame(); + assert_records([], "records after redundant play()"); + + // Cancel the animation. + e.style = ""; + + // Wait for the single removal notification. + yield waitForFrame(); + assert_records([{ added: [], changed: [], removed: animations }], + "records after animation end"); + }); + + // Test that calling play() on a finished Animation that fills forwards + // dispatches a changed notification. + addAsyncAnimTest("play_filling_forwards", aOptions, function*() { + // Start a long animation with a forwards fill + e.style = "animation: anim 100s forwards"; + + // The animation should cause the creation of a single Animation. + var animations = e.getAnimations(); + is(animations.length, 1, "getAnimations().length after animation start"); + + // Wait for the single MutationRecord for the Animation addition to + // be delivered. + yield waitForFrame(); + assert_records([{ added: animations, changed: [], removed: [] }], + "records after animation start"); + + // Wait until the animation is ready + yield animations[0].ready; + + // Seek to the end + animations[0].finish(); + + // Wait for the single MutationRecord for the Animation change to + // be delivered. + yield waitForFrame(); + assert_records([{ added: [], changed: animations, removed: [] }], + "records after finish()"); + + // Since we are filling forwards, calling play() should produce a + // change record since the animation remains relevant. + animations[0].play(); + + // Wait for the single MutationRecord for the Animation change to + // be delivered. + yield waitForFrame(); + assert_records([{ added: [], changed: animations, removed: [] }], + "records after play()"); + + // Cancel the animation. + e.style = ""; + + // Wait for the single removal notification. + yield waitForFrame(); + assert_records([{ added: [], changed: [], removed: animations }], + "records after animation end"); + }); + + // Test that calling play() on a finished Animation that does *not* fill + // forwards dispatches an addition notification. + addAsyncAnimTest("play_after_finish", aOptions, function*() { + // Start a long animation + e.style = "animation: anim 100s"; + + // The animation should cause the creation of a single Animation. + var animations = e.getAnimations(); + is(animations.length, 1, "getAnimations().length after animation start"); + + // Wait for the single MutationRecord for the Animation addition to + // be delivered. + yield waitForFrame(); + assert_records([{ added: animations, changed: [], removed: [] }], + "records after animation start"); + + // Wait until the animation is ready + yield animations[0].ready; + + // Seek to the end + animations[0].finish(); + + // Wait for the single MutationRecord for the Animation removal to + // be delivered. + yield waitForFrame(); + assert_records([{ added: [], changed: [], removed: animations }], + "records after finish()"); + + // Since we are *not* filling forwards, calling play() is equivalent + // to creating a new animation since it becomes relevant again. + animations[0].play(); + + // Wait for the single MutationRecord for the Animation addition to + // be delivered. + yield waitForFrame(); + assert_records([{ added: animations, changed: [], removed: [] }], + "records after play()"); + + // Cancel the animation. + e.style = ""; + + // Wait for the single removal notification. + yield waitForFrame(); + assert_records([{ added: [], changed: [], removed: animations }], + "records after animation end"); + }); + + // Test that calling pause() on an Animation dispatches a changed + // notification. + addAsyncAnimTest("pause", aOptions, function*() { + // Start a long animation + e.style = "animation: anim 100s"; + + // The animation should cause the creation of a single Animation. + var animations = e.getAnimations(); + is(animations.length, 1, "getAnimations().length after animation start"); + + // Wait for the single MutationRecord for the Animation addition to + // be delivered. + yield waitForFrame(); + assert_records([{ added: animations, changed: [], removed: [] }], + "records after animation start"); + + // Wait until the animation is ready + yield animations[0].ready; + + // Pause + animations[0].pause(); + + // Wait for the single MutationRecord for the Animation change to + // be delivered. + yield animations[0].ready; + assert_records([{ added: [], changed: animations, removed: [] }], + "records after pause()"); + + // Redundant pause + animations[0].pause(); + + // Wait to ensure no change is dispatched + yield animations[0].ready; + assert_records([], "records after redundant pause()"); + + // Cancel the animation. + e.style = ""; + + // Wait for the single removal notification. + yield waitForFrame(); + assert_records([{ added: [], changed: [], removed: animations }], + "records after animation end"); + }); + + // Test that calling pause() on an Animation that is pause-pending + // does not dispatch an additional changed notification. + addAsyncAnimTest("pause_while_pause_pending", aOptions, function*() { + // Start a long animation + e.style = "animation: anim 100s"; + + // The animation should cause the creation of a single Animation. + var animations = e.getAnimations(); + is(animations.length, 1, "getAnimations().length after animation start"); + + // Wait for the single MutationRecord for the Animation addition to + // be delivered. + yield waitForFrame(); + assert_records([{ added: animations, changed: [], removed: [] }], + "records after animation start"); + + // Wait until the animation is ready + yield animations[0].ready; + + // Pause + animations[0].pause(); + + // We are now pause-pending, but pause again + animations[0].pause(); + + // We should only get a single change record + yield animations[0].ready; + assert_records([{ added: [], changed: animations, removed: [] }], + "records after pause()"); + + // Cancel the animation. + e.style = ""; + + // Wait for the single removal notification. + yield waitForFrame(); + assert_records([{ added: [], changed: [], removed: animations }], + "records after animation end"); + }); + + // Test that calling play() on an Animation that is pause-pending + // dispatches a changed notification. + addAsyncAnimTest("aborted_pause", aOptions, function*() { + // Start a long animation + e.style = "animation: anim 100s"; + + // The animation should cause the creation of a single Animation. + var animations = e.getAnimations(); + is(animations.length, 1, "getAnimations().length after animation start"); + + // Wait for the single MutationRecord for the Animation addition to + // be delivered. + yield waitForFrame(); + assert_records([{ added: animations, changed: [], removed: [] }], + "records after animation start"); + + // Wait until the animation is ready + yield animations[0].ready; + + // Pause + animations[0].pause(); + + // We are now pause-pending. If we play() now, we will abort the pause + animations[0].play(); + + // We should get two change records + yield animations[0].ready; + assert_records([{ added: [], changed: animations, removed: [] }, + { added: [], changed: animations, removed: [] }], + "records after aborting a pause()"); + + // Cancel the animation. + e.style = ""; + + // Wait for the single removal notification. + yield waitForFrame(); + assert_records([{ added: [], changed: [], removed: animations }], + "records after animation end"); + }); + + // Test that a non-cancelling change to an animation followed immediately by a + // cancelling change will only send an animation removal notification. + addAsyncAnimTest("coalesce_change_cancel", aOptions, function*() { + // Start a long animation. + e.style = "animation: anim 100s;"; + + // The animation should cause the creation of a single Animation. + var animations = e.getAnimations(); + is(animations.length, 1, "getAnimations().length after animation start"); + + // Wait for the single MutationRecord for the Animation addition to + // be delivered. + yield waitForFrame(); + assert_records([{ added: animations, changed: [], removed: [] }], + "records after animation start"); + + // Update the animation's delay such that it is still running. + e.style.animationDelay = "-1s"; + + // Then cancel the animation by updating its duration. + e.style.animationDuration = "0.5s"; + + // We should get a single removal notification. + yield waitForFrame(); + assert_records([{ added: [], changed: [], removed: animations }], + "records after animation end"); + + e.style = ""; + }); + +}); + +addAsyncAnimTest("tree_ordering", { observe: div, subtree: true }, function*() { + // Add style for pseudo elements + var extraStyle = document.createElement('style'); + document.head.appendChild(extraStyle); + var sheet = extraStyle.sheet; + var rules = { ".before::before": "animation: anim 100s;", + ".after::after" : "animation: anim 100s, anim 100s;" }; + for (var selector in rules) { + sheet.insertRule(selector + '{' + rules[selector] + '}', + sheet.cssRules.length); + } + + // Create a tree with two children: + // + // div + // (::before) + // (::after) + // / \ + // childA childB(::before) + var childA = document.createElement("div"); + var childB = document.createElement("div"); + + div.appendChild(childA); + div.appendChild(childB); + + // Start an animation on each (using order: childB, div, childA) + // + // We include multiple animations on some nodes so that we can test batching + // works as expected later in this test. + childB.style = "animation: anim 100s"; + div.style = "animation: anim 100s, anim 100s, anim 100s"; + childA.style = "animation: anim 100s, anim 100s"; + + // Start animations targeting to pseudo element of div and childB. + childB.classList.add("before"); + div.classList.add("after"); + div.classList.add("before"); + + // Check all animations we have in this document + var docAnims = document.getAnimations(); + is(docAnims.length, 10, "total animations"); + + var divAnimations = div.getAnimations(); + var childAAnimations = childA.getAnimations(); + var childBAnimations = childB.getAnimations(); + var divBeforeAnimations = + [ for (x of docAnims) if (x.effect.target.parentElement == div && + x.effect.target.type == "::before") x ]; + var divAfterAnimations = + [ for (x of docAnims) if (x.effect.target.parentElement == div && + x.effect.target.type == "::after") x ]; + var childBPseudoAnimations = + [ for (x of docAnims) if (x.effect.target.parentElement == childB) x ]; + + // The order in which we get the corresponding records is currently + // based on the order we visit these nodes when updating styles. + // + // That is because we don't do any document-level batching of animation + // mutation records when we flush styles. We may introduce that in the + // future but for now all we are interested in testing here is that the order + // these records are dispatched is consistent between runs. + // + // We currently expect to get records in order div::after, childA, childB, + // childB::before, div, div::before + yield waitForFrame(); + assert_records([{ added: divAfterAnimations, changed: [], removed: [] }, + { added: childAAnimations, changed: [], removed: [] }, + { added: childBAnimations, changed: [], removed: [] }, + { added: childBPseudoAnimations, changed: [], removed: [] }, + { added: divAnimations, changed: [], removed: [] }, + { added: divBeforeAnimations, changed: [], removed: [] }], + "records after simultaneous animation start"); + + // The one case where we *do* currently perform document-level (or actually + // timeline-level) batching is when animations are updated from a refresh + // driver tick. In particular, this means that when animations finish + // naturally the removed records should be dispatched according to the + // position of the elements in the tree. + + // First, flatten the set of animations. we put the animations targeting to + // pseudo elements last. (Actually, we don't care the order in the list.) + var animations = [ ...divAnimations, + ...childAAnimations, + ...childBAnimations, + ...divBeforeAnimations, + ...divAfterAnimations, + ...childBPseudoAnimations ]; + + // Fast-forward to *just* before the end of the animation. + animations.forEach(animation => animation.currentTime = 99999); + + // Prepare the set of expected change MutationRecords, one for each + // animation that was seeked. + var seekRecords = animations.map( + p => ({ added: [], changed: [p], removed: [] }) + ); + + yield await_event(div, "animationend"); + + // After the changed notifications, which will be dispatched in the order that + // the animations were seeked, we should get removal MutationRecords in order + // (div, div::before, div::after), childA, (childB, childB::before). + // Note: The animations targeting to the pseudo element are appended after + // the animations of its parent element. + divAnimations = [ ...divAnimations, + ...divBeforeAnimations, + ...divAfterAnimations ]; + childBAnimations = [ ...childBAnimations, ...childBPseudoAnimations ]; + assert_records(seekRecords.concat( + { added: [], changed: [], removed: divAnimations }, + { added: [], changed: [], removed: childAAnimations }, + { added: [], changed: [], removed: childBAnimations }), + "records after finishing"); + + // Clean up + div.classList.remove("before"); + div.classList.remove("after"); + div.style = ""; + childA.remove(); + childB.remove(); + extraStyle.remove(); +}); + +// Run the tests. +SimpleTest.requestLongerTimeout(2); +SimpleTest.waitForExplicitFinish(); + +runAllAsyncTests().then(function() { + SimpleTest.finish(); +}, function(aError) { + ok(false, "Something failed: " + aError); +}); +</script> diff --git a/dom/animation/test/chrome/test_animation_performance_warning.html b/dom/animation/test/chrome/test_animation_performance_warning.html new file mode 100644 index 0000000000..a3bd63efcd --- /dev/null +++ b/dom/animation/test/chrome/test_animation_performance_warning.html @@ -0,0 +1,957 @@ +<!doctype html> +<head> +<meta charset=utf-8> +<title>Bug 1196114 - Test metadata related to which animation properties + are running on the compositor</title> +<script type="application/javascript" src="../testharness.js"></script> +<script type="application/javascript" src="../testharnessreport.js"></script> +<script type="application/javascript" src="../testcommon.js"></script> +<style> +.compositable { + /* Element needs geometry to be eligible for layerization */ + width: 100px; + height: 100px; + background-color: white; +} +@keyframes fade { + from { opacity: 1 } + to { opacity: 0 } +} +</style> +</head> +<body> +<a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1196114" + target="_blank">Mozilla Bug 1196114</a> +<div id="log"></div> +<script> +'use strict'; + +// This is used for obtaining localized strings. +var gStringBundle; + +W3CTest.runner.requestLongerTimeout(2); + +SpecialPowers.pushPrefEnv({ "set": [ + ["general.useragent.locale", "en-US"], + // Need to set devPixelsPerPx explicitly to gain + // consistent pixel values in warning messages + // regardless of platform DPIs. + ["layout.css.devPixelsPerPx", 1], + ] }, + start); + +function compare_property_state(a, b) { + if (a.property > b.property) { + return -1; + } else if (a.property < b.property) { + return 1; + } + if (a.runningOnCompositor != b.runningOnCompositor) { + return a.runningOnCompositor ? 1 : -1; + } + return a.warning > b.warning ? -1 : 1; +} + +function assert_animation_property_state_equals(actual, expected) { + assert_equals(actual.length, expected.length, 'Number of properties'); + + var sortedActual = actual.sort(compare_property_state); + var sortedExpected = expected.sort(compare_property_state); + + for (var i = 0; i < sortedActual.length; i++) { + assert_equals(sortedActual[i].property, + sortedExpected[i].property, + 'CSS property name should match'); + assert_equals(sortedActual[i].runningOnCompositor, + sortedExpected[i].runningOnCompositor, + 'runningOnCompositor property should match'); + if (sortedExpected[i].warning instanceof RegExp) { + assert_regexp_match(sortedActual[i].warning, + sortedExpected[i].warning, + 'warning message should match'); + } else if (sortedExpected[i].warning) { + assert_equals(sortedActual[i].warning, + gStringBundle.GetStringFromName(sortedExpected[i].warning), + 'warning message should match'); + } + } +} + +// Check that the animation is running on compositor and +// warning property is not set for the CSS property regardless +// expected values. +function assert_property_state_on_compositor(actual, expected) { + assert_equals(actual.length, expected.length); + + var sortedActual = actual.sort(compare_property_state); + var sortedExpected = expected.sort(compare_property_state); + + for (var i = 0; i < sortedActual.length; i++) { + assert_equals(sortedActual[i].property, + sortedExpected[i].property, + 'CSS property name should match'); + assert_true(sortedActual[i].runningOnCompositor, + 'runningOnCompositor property should be true on ' + + sortedActual[i].property); + assert_not_exists(sortedActual[i], 'warning', + 'warning property should not be set'); + } +} + +var gAnimationsTests = [ + { + desc: 'animations on compositor', + frames: { + opacity: [0, 1] + }, + expected: [ + { + property: 'opacity', + runningOnCompositor: true + } + ] + }, + { + desc: 'animations on main thread', + frames: { + backgroundColor: ['white', 'red'] + }, + expected: [ + { + property: 'background-color', + runningOnCompositor: false + } + ] + }, + { + desc: 'animations on both threads', + frames: { + backgroundColor: ['white', 'red'], + transform: ['translate(0px)', 'translate(100px)'] + }, + expected: [ + { + property: 'background-color', + runningOnCompositor: false + }, + { + property: 'transform', + runningOnCompositor: true + } + ] + }, + { + desc: 'two animation properties on compositor thread', + frames: { + opacity: [0, 1], + transform: ['translate(0px)', 'translate(100px)'] + }, + expected: [ + { + property: 'opacity', + runningOnCompositor: true + }, + { + property: 'transform', + runningOnCompositor: true + } + ] + }, + { + desc: 'opacity on compositor with animation of geometric properties', + frames: { + width: ['100px', '200px'], + opacity: [0, 1] + }, + expected: [ + { + property: 'width', + runningOnCompositor: false + }, + { + property: 'opacity', + runningOnCompositor: true + } + ] + }, +]; + +// Test cases that check results of adding/removing a 'width' property on the +// same animation object. +var gAnimationWithGeometricKeyframeTests = [ + { + desc: 'transform', + frames: { + transform: ['translate(0px)', 'translate(100px)'] + }, + expected: { + withoutGeometric: [ + { + property: 'transform', + runningOnCompositor: true + } + ], + withGeometric: [ + { + property: 'width', + runningOnCompositor: false + }, + { + property: 'transform', + runningOnCompositor: false, + warning: 'CompositorAnimationWarningTransformWithGeometricProperties' + } + ] + } + }, + { + desc: 'opacity and transform', + frames: { + opacity: [0, 1], + transform: ['translate(0px)', 'translate(100px)'] + }, + expected: { + withoutGeometric: [ + { + property: 'opacity', + runningOnCompositor: true + }, + { + property: 'transform', + runningOnCompositor: true + } + ], + withGeometric: [ + { + property: 'width', + runningOnCompositor: false + }, + { + property: 'opacity', + runningOnCompositor: true + }, + { + property: 'transform', + runningOnCompositor: false, + warning: 'CompositorAnimationWarningTransformWithGeometricProperties' + } + ] + } + }, +]; + +// Performance warning tests that set and clear a style property. +var gPerformanceWarningTestsStyle = [ + { + desc: 'preserve-3d transform', + frames: { + transform: ['translate(0px)', 'translate(100px)'] + }, + style: 'transform-style: preserve-3d', + expected: [ + { + property: 'transform', + runningOnCompositor: false, + warning: 'CompositorAnimationWarningTransformPreserve3D' + } + ] + }, + { + desc: 'transform with backface-visibility:hidden', + frames: { + transform: ['translate(0px)', 'translate(100px)'] + }, + style: 'backface-visibility: hidden;', + expected: [ + { + property: 'transform', + runningOnCompositor: false, + warning: 'CompositorAnimationWarningTransformBackfaceVisibilityHidden' + } + ] + }, + { + desc: 'opacity and transform with preserve-3d', + frames: { + opacity: [0, 1], + transform: ['translate(0px)', 'translate(100px)'] + }, + style: 'transform-style: preserve-3d', + expected: [ + { + property: 'opacity', + runningOnCompositor: true + }, + { + property: 'transform', + runningOnCompositor: false, + warning: 'CompositorAnimationWarningTransformPreserve3D' + } + ] + }, + { + desc: 'opacity and transform with backface-visibility:hidden', + frames: { + opacity: [0, 1], + transform: ['translate(0px)', 'translate(100px)'] + }, + style: 'backface-visibility: hidden;', + expected: [ + { + property: 'opacity', + runningOnCompositor: true + }, + { + property: 'transform', + runningOnCompositor: false, + warning: 'CompositorAnimationWarningTransformBackfaceVisibilityHidden' + } + ] + }, +]; + +// Performance warning tests that set and clear the id property +var gPerformanceWarningTestsId= [ + { + desc: 'moz-element referencing a transform', + frames: { + transform: ['translate(0px)', 'translate(100px)'] + }, + id: 'transformed', + createelement: 'width:100px; height:100px; background: -moz-element(#transformed)', + expected: [ + { + property: 'transform', + runningOnCompositor: false, + warning: 'CompositorAnimationWarningHasRenderingObserver' + } + ] + }, +]; + +var gMultipleAsyncAnimationsTests = [ + { + desc: 'opacity and transform with preserve-3d', + style: 'transform-style: preserve-3d', + animations: [ + { + frames: { + transform: ['translate(0px)', 'translate(100px)'] + }, + expected: [ + { + property: 'transform', + runningOnCompositor: false, + warning: 'CompositorAnimationWarningTransformPreserve3D' + } + ] + }, + { + frames: { + opacity: [0, 1] + }, + expected: [ + { + property: 'opacity', + runningOnCompositor: true, + } + ] + } + ], + }, + { + desc: 'opacity and transform with backface-visibility:hidden', + style: 'backface-visibility: hidden;', + animations: [ + { + frames: { + transform: ['translate(0px)', 'translate(100px)'] + }, + expected: [ + { + property: 'transform', + runningOnCompositor: false, + warning: 'CompositorAnimationWarningTransformBackfaceVisibilityHidden' + } + ] + }, + { + frames: { + opacity: [0, 1] + }, + expected: [ + { + property: 'opacity', + runningOnCompositor: true, + } + ] + } + ], + }, +]; + +// Test cases that check results of adding/removing a 'width' keyframe on the +// same animation object, where multiple animation objects belong to the same +// element. +// The 'width' property is added to animations[1]. +var gMultipleAsyncAnimationsWithGeometricKeyframeTests = [ + { + desc: 'transform and opacity with geometric keyframes', + animations: [ + { + frames: { + transform: ['translate(0px)', 'translate(100px)'] + }, + expected: { + withoutGeometric: [ + { + property: 'transform', + runningOnCompositor: true + } + ], + withGeometric: [ + { + property: 'transform', + runningOnCompositor: false, + warning: 'CompositorAnimationWarningTransformWithGeometricProperties' + } + ] + } + }, + { + frames: { + opacity: [0, 1] + }, + expected: { + withoutGeometric: [ + { + property: 'opacity', + runningOnCompositor: true, + } + ], + withGeometric: [ + { + property: 'width', + runningOnCompositor: false, + }, + { + property: 'opacity', + runningOnCompositor: true, + } + ] + } + } + ], + }, + { + desc: 'opacity and transform with geometric keyframes', + animations: [ + { + frames: { + opacity: [0, 1] + }, + expected: { + withoutGeometric: [ + { + property: 'opacity', + runningOnCompositor: true, + } + ], + withGeometric: [ + { + property: 'opacity', + runningOnCompositor: true, + } + ] + } + }, + { + frames: { + transform: ['translate(0px)', 'translate(100px)'] + }, + expected: { + withoutGeometric: [ + { + property: 'transform', + runningOnCompositor: true + } + ], + withGeometric: [ + { + property: 'width', + runningOnCompositor: false, + }, + { + property: 'transform', + runningOnCompositor: false, + warning: 'CompositorAnimationWarningTransformWithGeometricProperties' + } + ] + } + } + ] + }, +]; + +// Test cases that check results of adding/removing 'width' animation on the +// same element which has async animations. +var gMultipleAsyncAnimationsWithGeometricAnimationTests = [ + { + desc: 'transform', + animations: [ + { + frames: { + transform: ['translate(0px)', 'translate(100px)'] + }, + expected: [ + { + property: 'transform', + runningOnCompositor: false, + warning: 'CompositorAnimationWarningTransformWithGeometricProperties' + } + ] + }, + ] + }, + { + desc: 'opacity', + animations: [ + { + frames: { + opacity: [0, 1] + }, + expected: [ + { + property: 'opacity', + runningOnCompositor: true + } + ] + }, + ] + }, + { + desc: 'opacity and transform', + animations: [ + { + frames: { + transform: ['translate(0px)', 'translate(100px)'] + }, + expected: [ + { + property: 'transform', + runningOnCompositor: false, + warning: 'CompositorAnimationWarningTransformWithGeometricProperties' + } + ] + }, + { + frames: { + opacity: [0, 1] + }, + expected: [ + { + property: 'opacity', + runningOnCompositor: true, + } + ] + } + ], + }, +]; + +var gAnimationsOnTooSmallElementTests = [ + { + desc: 'opacity on too small element', + frames: { + opacity: [0, 1] + }, + style: { style: 'width: 8px; height: 8px; background-color: red;' + + // We need to set transform here to try creating an + // individual frame for this opacity element. + // Without this, this small element is created on the same + // nsIFrame of mochitest iframe, i.e. the document which are + // running this test, as a result the layer corresponding + // to the frame is sent to compositor. + 'transform: translateX(100px);' }, + expected: [ + { + property: 'opacity', + runningOnCompositor: false, + warning: /Animation cannot be run on the compositor because frame size \(8, 8\) is smaller than \(16, 16\)/ + } + ] + }, + { + desc: 'transform on too small element', + frames: { + transform: ['translate(0px)', 'translate(100px)'] + }, + style: { style: 'width: 8px; height: 8px; background-color: red;' }, + expected: [ + { + property: 'transform', + runningOnCompositor: false, + warning: /Animation cannot be run on the compositor because frame size \(8, 8\) is smaller than \(16, 16\)/ + } + ] + }, +]; + +function start() { + var bundleService = SpecialPowers.Cc['@mozilla.org/intl/stringbundle;1'] + .getService(SpecialPowers.Ci.nsIStringBundleService); + gStringBundle = bundleService + .createBundle("chrome://global/locale/layout_errors.properties"); + + gAnimationsTests.forEach(function(subtest) { + promise_test(function(t) { + var animation = addDivAndAnimate(t, + { class: 'compositable' }, + subtest.frames, 100 * MS_PER_SEC); + return animation.ready.then(function() { + assert_animation_property_state_equals( + animation.effect.getProperties(), + subtest.expected); + }); + }, subtest.desc); + }); + + gAnimationWithGeometricKeyframeTests.forEach(function(subtest) { + promise_test(function(t) { + var animation = addDivAndAnimate(t, + { class: 'compositable' }, + subtest.frames, 100 * MS_PER_SEC); + return animation.ready.then(function() { + // First, a transform animation is running on compositor. + assert_animation_property_state_equals( + animation.effect.getProperties(), + subtest.expected.withoutGeometric); + }).then(function() { + // Add a 'width' property. + var keyframes = animation.effect.getKeyframes(); + + keyframes[0].width = '100px'; + keyframes[1].width = '200px'; + + animation.effect.setKeyframes(keyframes); + return waitForFrame(); + }).then(function() { + // Now the transform animation is not running on compositor because of + // the 'width' property. + assert_animation_property_state_equals( + animation.effect.getProperties(), + subtest.expected.withGeometric); + }).then(function() { + // Remove the 'width' property. + var keyframes = animation.effect.getKeyframes(); + + delete keyframes[0].width; + delete keyframes[1].width; + + animation.effect.setKeyframes(keyframes); + return waitForFrame(); + }).then(function() { + // Finally, the transform animation is running on compositor. + assert_animation_property_state_equals( + animation.effect.getProperties(), + subtest.expected.withoutGeometric); + }); + }, 'An animation has: ' + subtest.desc); + }); + + gPerformanceWarningTestsStyle.forEach(function(subtest) { + promise_test(function(t) { + var animation = addDivAndAnimate(t, + { class: 'compositable' }, + subtest.frames, 100 * MS_PER_SEC); + return animation.ready.then(function() { + assert_property_state_on_compositor( + animation.effect.getProperties(), + subtest.expected); + animation.effect.target.style = subtest.style; + return waitForFrame(); + }).then(function() { + assert_animation_property_state_equals( + animation.effect.getProperties(), + subtest.expected); + animation.effect.target.style = ''; + return waitForFrame(); + }).then(function() { + assert_property_state_on_compositor( + animation.effect.getProperties(), + subtest.expected); + }); + }, subtest.desc); + }); + + gPerformanceWarningTestsId.forEach(function(subtest) { + promise_test(function(t) { + if (subtest.createelement) { + addDiv(t, { style: subtest.createelement }); + } + + var animation = addDivAndAnimate(t, + { class: 'compositable' }, + subtest.frames, 100 * MS_PER_SEC); + return animation.ready.then(function() { + assert_property_state_on_compositor( + animation.effect.getProperties(), + subtest.expected); + animation.effect.target.id = subtest.id; + return waitForFrame(); + }).then(function() { + assert_animation_property_state_equals( + animation.effect.getProperties(), + subtest.expected); + animation.effect.target.id = ''; + return waitForFrame(); + }).then(function() { + assert_property_state_on_compositor( + animation.effect.getProperties(), + subtest.expected); + }); + }, subtest.desc); + }); + + gMultipleAsyncAnimationsTests.forEach(function(subtest) { + promise_test(function(t) { + var div = addDiv(t, { class: 'compositable' }); + var animations = subtest.animations.map(function(anim) { + var animation = div.animate(anim.frames, 100 * MS_PER_SEC); + + // Bind expected values to animation object. + animation.expected = anim.expected; + return animation; + }); + return waitForAllAnimations(animations).then(function() { + animations.forEach(function(anim) { + assert_property_state_on_compositor( + anim.effect.getProperties(), + anim.expected); + }); + div.style = subtest.style; + return waitForFrame(); + }).then(function() { + animations.forEach(function(anim) { + assert_animation_property_state_equals( + anim.effect.getProperties(), + anim.expected); + }); + div.style = ''; + return waitForFrame(); + }).then(function() { + animations.forEach(function(anim) { + assert_property_state_on_compositor( + anim.effect.getProperties(), + anim.expected); + }); + }); + }, 'Multiple animations: ' + subtest.desc); + }); + + gMultipleAsyncAnimationsWithGeometricKeyframeTests.forEach(function(subtest) { + promise_test(function(t) { + var div = addDiv(t, { class: 'compositable' }); + var animations = subtest.animations.map(function(anim) { + var animation = div.animate(anim.frames, 100 * MS_PER_SEC); + + // Bind expected values to animation object. + animation.expected = anim.expected; + return animation; + }); + return waitForAllAnimations(animations).then(function() { + // First, all animations are running on compositor. + animations.forEach(function(anim) { + assert_animation_property_state_equals( + anim.effect.getProperties(), + anim.expected.withoutGeometric); + }); + }).then(function() { + // Add a 'width' property to animations[1]. + var keyframes = animations[1].effect.getKeyframes(); + + keyframes[0].width = '100px'; + keyframes[1].width = '200px'; + + animations[1].effect.setKeyframes(keyframes); + return waitForFrame(); + }).then(function() { + // Now the transform animation is not running on compositor because of + // the 'width' property. + animations.forEach(function(anim) { + assert_animation_property_state_equals( + anim.effect.getProperties(), + anim.expected.withGeometric); + }); + }).then(function() { + // Remove the 'width' property from animations[1]. + var keyframes = animations[1].effect.getKeyframes(); + + delete keyframes[0].width; + delete keyframes[1].width; + + animations[1].effect.setKeyframes(keyframes); + return waitForFrame(); + }).then(function() { + // Finally, all animations are running on compositor. + animations.forEach(function(anim) { + assert_animation_property_state_equals( + anim.effect.getProperties(), + anim.expected.withoutGeometric); + }); + }); + }, 'Multiple animations with geometric property: ' + subtest.desc); + }); + + gMultipleAsyncAnimationsWithGeometricAnimationTests.forEach(function(subtest) { + promise_test(function(t) { + var div = addDiv(t, { class: 'compositable' }); + var animations = subtest.animations.map(function(anim) { + var animation = div.animate(anim.frames, 100 * MS_PER_SEC); + + // Bind expected values to animation object. + animation.expected = anim.expected; + return animation; + }); + + var widthAnimation; + + return waitForAllAnimations(animations).then(function() { + animations.forEach(function(anim) { + assert_property_state_on_compositor( + anim.effect.getProperties(), + anim.expected); + }); + }).then(function() { + // Append 'width' animation on the same element. + widthAnimation = div.animate({ width: ['100px', '200px'] }, + 100 * MS_PER_SEC); + return waitForFrame(); + }).then(function() { + // Now transform animations are not running on compositor because of + // the 'width' animation. + animations.forEach(function(anim) { + assert_animation_property_state_equals( + anim.effect.getProperties(), + anim.expected); + }); + // Remove the 'width' animation. + widthAnimation.cancel(); + return waitForFrame(); + }).then(function() { + // Now all animations are running on compositor. + animations.forEach(function(anim) { + assert_property_state_on_compositor( + anim.effect.getProperties(), + anim.expected); + }); + }); + }, 'Multiple async animations and geometric animation: ' + subtest.desc); + }); + + gAnimationsOnTooSmallElementTests.forEach(function(subtest) { + promise_test(function(t) { + var div = addDiv(t, subtest.style); + var animation = div.animate(subtest.frames, 100 * MS_PER_SEC); + return animation.ready.then(function() { + assert_animation_property_state_equals( + animation.effect.getProperties(), + subtest.expected); + }); + }, subtest.desc); + }); + + promise_test(function(t) { + var animation = addDivAndAnimate(t, + { class: 'compositable' }, + { transform: [ 'translate(0px)', + 'translate(100px)'] }, + 100 * MS_PER_SEC); + return animation.ready.then(function() { + assert_animation_property_state_equals( + animation.effect.getProperties(), + [ { property: 'transform', runningOnCompositor: true } ]); + animation.effect.target.style = 'width: 10000px; height: 10000px'; + return waitForFrame(); + }).then(function() { + // viewport depends on test environment. + var expectedWarning = new RegExp( + "Animation cannot be run on the compositor because the frame size " + + "\\(10000, 10000\\) is bigger than the viewport \\(\\d+, \\d+\\) " + + "or the visual rectangle \\(10000, 10000\\) is larger than the " + + "maximum allowed value \\(\\d+\\)"); + assert_animation_property_state_equals( + animation.effect.getProperties(), + [ { + property: 'transform', + runningOnCompositor: false, + warning: expectedWarning + } ]); + animation.effect.target.style = 'width: 100px; height: 100px'; + return waitForFrame(); + }).then(function() { + // FIXME: Bug 1253164: the animation should get back on compositor. + assert_animation_property_state_equals( + animation.effect.getProperties(), + [ { property: 'transform', runningOnCompositor: false } ]); + }); + }, 'transform on too big element'); + + promise_test(function(t) { + var svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); + svg.setAttribute('width', '100'); + svg.setAttribute('height', '100'); + var rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect'); + rect.setAttribute('width', '100'); + rect.setAttribute('height', '100'); + rect.setAttribute('fill', 'red'); + svg.appendChild(rect); + document.body.appendChild(svg); + t.add_cleanup(function() { + svg.remove(); + }); + + var animation = svg.animate( + { transform: ['translate(0px)', 'translate(100px)'] }, 100 * MS_PER_SEC); + return animation.ready.then(function() { + assert_animation_property_state_equals( + animation.effect.getProperties(), + [ { property: 'transform', runningOnCompositor: true } ]); + svg.setAttribute('transform', 'translate(10, 20)'); + return waitForFrame(); + }).then(function() { + assert_animation_property_state_equals( + animation.effect.getProperties(), + [ { + property: 'transform', + runningOnCompositor: false, + warning: 'CompositorAnimationWarningTransformSVG' + } ]); + svg.removeAttribute('transform'); + return waitForFrame(); + }).then(function() { + assert_animation_property_state_equals( + animation.effect.getProperties(), + [ { property: 'transform', runningOnCompositor: true } ]); + }); + }, 'transform of nsIFrame with SVG transform'); + + promise_test(function(t) { + var div = addDiv(t, { class: 'compositable', + style: 'animation: fade 100s' }); + var cssAnimation = div.getAnimations()[0]; + var scriptAnimation = div.animate({ opacity: [ 1, 0 ] }, 100 * MS_PER_SEC); + return scriptAnimation.ready.then(function() { + assert_animation_property_state_equals( + cssAnimation.effect.getProperties(), + [ { property: 'opacity', runningOnCompositor: true } ]); + assert_animation_property_state_equals( + scriptAnimation.effect.getProperties(), + [ { property: 'opacity', runningOnCompositor: true } ]); + }); + }, 'overridden animation'); +} + +</script> + +</body> diff --git a/dom/animation/test/chrome/test_animation_properties.html b/dom/animation/test/chrome/test_animation_properties.html new file mode 100644 index 0000000000..5349013062 --- /dev/null +++ b/dom/animation/test/chrome/test_animation_properties.html @@ -0,0 +1,993 @@ +<!doctype html> +<head> +<meta charset=utf-8> +<title>Bug 1254419 - Test the values returned by + KeyframeEffectReadOnly.getProperties()</title> +<script type="application/javascript" src="../testharness.js"></script> +<script type="application/javascript" src="../testharnessreport.js"></script> +<script type="application/javascript" src="../testcommon.js"></script> +</head> +<body> +<a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1254419" + target="_blank">Mozilla Bug 1254419</a> +<div id="log"></div> +<style> + +:root { + --var-100px: 100px; + --var-100px-200px: 100px 200px; +} +div { + font-size: 10px; /* For calculating em-based units */ +} +</style> +<script> +'use strict'; + +function assert_properties_equal(actual, expected) { + assert_equals(actual.length, expected.length); + + var compareProperties = (a, b) => + a.property == b.property ? 0 : (a.property < b.property ? -1 : 1); + + var sortedActual = actual.sort(compareProperties); + var sortedExpected = expected.sort(compareProperties); + + // We want to serialize the values in the following form: + // + // { offset: 0, easing: linear, composite: replace, value: 5px }, ... + // + // So that we can just compare strings and, in the failure case, + // easily see where the differences lie. + var serializeMember = value => { + return typeof value === 'undefined' ? '<not set>' : value; + } + var serializeValues = values => + values.map(value => + '{ ' + + [ 'offset', 'value', 'easing', 'composite' ].map( + member => `${member}: ${serializeMember(value[member])}` + ).join(', ') + + ' }') + .join(', '); + + for (var i = 0; i < sortedActual.length; i++) { + assert_equals(sortedActual[i].property, + sortedExpected[i].property, + 'CSS property name should match'); + assert_equals(serializeValues(sortedActual[i].values), + serializeValues(sortedExpected[i].values), + `Values arrays do not match for ` + + `${sortedActual[i].property} property`); + } +} + +// Shorthand for constructing a value object +function value(offset, value, composite, easing) { + return { offset: offset, value: value, easing: easing, composite: composite }; +} + +var gTests = [ + + // --------------------------------------------------------------------- + // + // Tests for property-indexed specifications + // + // --------------------------------------------------------------------- + + { desc: 'a one-property two-value property-indexed specification', + frames: { left: ['10px', '20px'] }, + expected: [ { property: 'left', + values: [ value(0, '10px', 'replace', 'linear'), + value(1, '20px', 'replace') ] } ] + }, + { desc: 'a one-shorthand-property two-value property-indexed' + + ' specification', + frames: { margin: ['10px', '10px 20px 30px 40px'] }, + expected: [ { property: 'margin-top', + values: [ value(0, '10px', 'replace', 'linear'), + value(1, '10px', 'replace') ] }, + { property: 'margin-right', + values: [ value(0, '10px', 'replace', 'linear'), + value(1, '20px', 'replace') ] }, + { property: 'margin-bottom', + values: [ value(0, '10px', 'replace', 'linear'), + value(1, '30px', 'replace') ] }, + { property: 'margin-left', + values: [ value(0, '10px', 'replace', 'linear'), + value(1, '40px', 'replace') ] } ] + }, + { desc: 'a two-property (one shorthand and one of its longhand' + + ' components) two-value property-indexed specification', + frames: { marginTop: ['50px', '60px'], + margin: ['10px', '10px 20px 30px 40px'] }, + expected: [ { property: 'margin-top', + values: [ value(0, '50px', 'replace', 'linear'), + value(1, '60px', 'replace') ] }, + { property: 'margin-right', + values: [ value(0, '10px', 'replace', 'linear'), + value(1, '20px', 'replace') ] }, + { property: 'margin-bottom', + values: [ value(0, '10px', 'replace', 'linear'), + value(1, '30px', 'replace') ] }, + { property: 'margin-left', + values: [ value(0, '10px', 'replace', 'linear'), + value(1, '40px', 'replace') ] } ] + }, + { desc: 'a two-property property-indexed specification with different' + + ' numbers of values', + frames: { left: ['10px', '20px', '30px'], + top: ['40px', '50px'] }, + expected: [ { property: 'left', + values: [ value(0, '10px', 'replace', 'linear'), + value(0.5, '20px', 'replace', 'linear'), + value(1, '30px', 'replace') ] }, + { property: 'top', + values: [ value(0, '40px', 'replace', 'linear'), + value(1, '50px', 'replace') ] } ] + }, + { desc: 'a property-indexed specification with an invalid value', + frames: { left: ['10px', '20px', '30px', '40px', '50px'], + top: ['15px', '25px', 'invalid', '45px', '55px'] }, + expected: [ { property: 'left', + values: [ value(0, '10px', 'replace', 'linear'), + value(0.25, '20px', 'replace', 'linear'), + value(0.5, '30px', 'replace', 'linear'), + value(0.75, '40px', 'replace', 'linear'), + value(1, '50px', 'replace') ] }, + { property: 'top', + values: [ value(0, '15px', 'replace', 'linear'), + value(0.25, '25px', 'replace', 'linear'), + value(0.75, '45px', 'replace', 'linear'), + value(1, '55px', 'replace') ] } ] + }, + { desc: 'a one-property two-value property-indexed specification that' + + ' needs to stringify its values', + frames: { opacity: [0, 1] }, + expected: [ { property: 'opacity', + values: [ value(0, '0', 'replace', 'linear'), + value(1, '1', 'replace') ] } ] + }, + { desc: 'a property-indexed keyframe where a lesser shorthand precedes' + + ' a greater shorthand', + frames: { borderLeft: [ '1px solid rgb(1, 2, 3)', + '2px solid rgb(4, 5, 6)' ], + border: [ '3px dotted rgb(7, 8, 9)', + '4px dashed rgb(10, 11, 12)' ] }, + expected: [ { property: 'border-bottom-color', + values: [ value(0, 'rgb(7, 8, 9)', 'replace', 'linear'), + value(1, 'rgb(10, 11, 12)', 'replace') ] }, + { property: 'border-left-color', + values: [ value(0, 'rgb(1, 2, 3)', 'replace', 'linear'), + value(1, 'rgb(4, 5, 6)', 'replace') ] }, + { property: 'border-right-color', + values: [ value(0, 'rgb(7, 8, 9)', 'replace', 'linear'), + value(1, 'rgb(10, 11, 12)', 'replace') ] }, + { property: 'border-top-color', + values: [ value(0, 'rgb(7, 8, 9)', 'replace', 'linear'), + value(1, 'rgb(10, 11, 12)', 'replace') ] }, + { property: 'border-bottom-width', + values: [ value(0, '3px', 'replace', 'linear'), + value(1, '4px', 'replace') ] }, + { property: 'border-left-width', + values: [ value(0, '1px', 'replace', 'linear'), + value(1, '2px', 'replace') ] }, + { property: 'border-right-width', + values: [ value(0, '3px', 'replace', 'linear'), + value(1, '4px', 'replace') ] }, + { property: 'border-top-width', + values: [ value(0, '3px', 'replace', 'linear'), + value(1, '4px', 'replace') ] }, + { property: 'border-bottom-style', + values: [ value(0, 'dotted', 'replace', 'linear'), + value(1, 'dashed', 'replace') ] }, + { property: 'border-left-style', + values: [ value(0, 'solid', 'replace', 'linear'), + value(1, 'solid', 'replace') ] }, + { property: 'border-right-style', + values: [ value(0, 'dotted', 'replace', 'linear'), + value(1, 'dashed', 'replace') ] }, + { property: 'border-top-style', + values: [ value(0, 'dotted', 'replace', 'linear'), + value(1, 'dashed', 'replace') ] }, + { property: 'border-image-outset', + values: [ value(0, '0 0 0 0', 'replace', 'linear'), + value(1, '0 0 0 0', 'replace') ] }, + { property: 'border-image-repeat', + values: [ value(0, 'stretch stretch', 'replace', 'linear'), + value(1, 'stretch stretch', 'replace') ] }, + { property: 'border-image-slice', + values: [ value(0, '100% 100% 100% 100%', + 'replace', 'linear'), + value(1, '100% 100% 100% 100%', 'replace') ] }, + { property: 'border-image-source', + values: [ value(0, 'none', 'replace', 'linear'), + value(1, 'none', 'replace') ] }, + { property: 'border-image-width', + values: [ value(0, '1 1 1 1', 'replace', 'linear'), + value(1, '1 1 1 1', 'replace') ] }, + { property: '-moz-border-bottom-colors', + values: [ value(0, 'none', 'replace', 'linear'), + value(1, 'none', 'replace') ] }, + { property: '-moz-border-left-colors', + values: [ value(0, 'none', 'replace', 'linear'), + value(1, 'none', 'replace') ] }, + { property: '-moz-border-right-colors', + values: [ value(0, 'none', 'replace', 'linear'), + value(1, 'none', 'replace') ] }, + { property: '-moz-border-top-colors', + values: [ value(0, 'none', 'replace', 'linear'), + value(1, 'none', 'replace') ] } ] + }, + { desc: 'a property-indexed keyframe where a greater shorthand precedes' + + ' a lesser shorthand', + frames: { border: [ '3px dotted rgb(7, 8, 9)', + '4px dashed rgb(10, 11, 12)' ], + borderLeft: [ '1px solid rgb(1, 2, 3)', + '2px solid rgb(4, 5, 6)' ] }, + expected: [ { property: 'border-bottom-color', + values: [ value(0, 'rgb(7, 8, 9)', 'replace', 'linear'), + value(1, 'rgb(10, 11, 12)', 'replace') ] }, + { property: 'border-left-color', + values: [ value(0, 'rgb(1, 2, 3)', 'replace', 'linear'), + value(1, 'rgb(4, 5, 6)', 'replace') ] }, + { property: 'border-right-color', + values: [ value(0, 'rgb(7, 8, 9)', 'replace', 'linear'), + value(1, 'rgb(10, 11, 12)', 'replace') ] }, + { property: 'border-top-color', + values: [ value(0, 'rgb(7, 8, 9)', 'replace', 'linear'), + value(1, 'rgb(10, 11, 12)', 'replace') ] }, + { property: 'border-bottom-width', + values: [ value(0, '3px', 'replace', 'linear'), + value(1, '4px', 'replace') ] }, + { property: 'border-left-width', + values: [ value(0, '1px', 'replace', 'linear'), + value(1, '2px', 'replace') ] }, + { property: 'border-right-width', + values: [ value(0, '3px', 'replace', 'linear'), + value(1, '4px', 'replace') ] }, + { property: 'border-top-width', + values: [ value(0, '3px', 'replace', 'linear'), + value(1, '4px', 'replace') ] }, + { property: 'border-bottom-style', + values: [ value(0, 'dotted', 'replace', 'linear'), + value(1, 'dashed', 'replace') ] }, + { property: 'border-left-style', + values: [ value(0, 'solid', 'replace', 'linear'), + value(1, 'solid', 'replace') ] }, + { property: 'border-right-style', + values: [ value(0, 'dotted', 'replace', 'linear'), + value(1, 'dashed', 'replace') ] }, + { property: 'border-top-style', + values: [ value(0, 'dotted', 'replace', 'linear'), + value(1, 'dashed', 'replace') ] }, + { property: 'border-image-outset', + values: [ value(0, '0 0 0 0', 'replace', 'linear'), + value(1, '0 0 0 0', 'replace') ] }, + { property: 'border-image-repeat', + values: [ value(0, 'stretch stretch', 'replace', 'linear'), + value(1, 'stretch stretch', 'replace') ] }, + { property: 'border-image-slice', + values: [ value(0, '100% 100% 100% 100%', + 'replace', 'linear'), + value(1, '100% 100% 100% 100%', 'replace') ] }, + { property: 'border-image-source', + values: [ value(0, 'none', 'replace', 'linear'), + value(1, 'none', 'replace') ] }, + { property: 'border-image-width', + values: [ value(0, '1 1 1 1', 'replace', 'linear'), + value(1, '1 1 1 1', 'replace') ] }, + { property: '-moz-border-bottom-colors', + values: [ value(0, 'none', 'replace', 'linear'), + value(1, 'none', 'replace') ] }, + { property: '-moz-border-left-colors', + values: [ value(0, 'none', 'replace', 'linear'), + value(1, 'none', 'replace') ] }, + { property: '-moz-border-right-colors', + values: [ value(0, 'none', 'replace', 'linear'), + value(1, 'none', 'replace') ] }, + { property: '-moz-border-top-colors', + values: [ value(0, 'none', 'replace', 'linear'), + value(1, 'none', 'replace') ] } ] + }, + + // --------------------------------------------------------------------- + // + // Tests for keyframe sequences + // + // --------------------------------------------------------------------- + + { desc: 'a keyframe sequence specification with repeated values at' + + ' offset 0/1 with different easings', + frames: [ { offset: 0.0, left: '100px', easing: 'ease' }, + { offset: 0.0, left: '200px', easing: 'ease' }, + { offset: 0.5, left: '300px', easing: 'linear' }, + { offset: 1.0, left: '400px', easing: 'ease-out' }, + { offset: 1.0, left: '500px', easing: 'step-end' } ], + expected: [ { property: 'left', + values: [ value(0, '100px', 'replace'), + value(0, '200px', 'replace', 'ease'), + value(0.5, '300px', 'replace', 'linear'), + value(1, '400px', 'replace'), + value(1, '500px', 'replace') ] } ] + }, + { desc: 'a one-property two-keyframe sequence', + frames: [ { offset: 0, left: '10px' }, + { offset: 1, left: '20px' } ], + expected: [ { property: 'left', + values: [ value(0, '10px', 'replace', 'linear'), + value(1, '20px', 'replace') ] } ] + }, + { desc: 'a two-property two-keyframe sequence', + frames: [ { offset: 0, left: '10px', top: '30px' }, + { offset: 1, left: '20px', top: '40px' } ], + expected: [ { property: 'left', + values: [ value(0, '10px', 'replace', 'linear'), + value(1, '20px', 'replace') ] }, + { property: 'top', + values: [ value(0, '30px', 'replace', 'linear'), + value(1, '40px', 'replace') ] } ] + }, + { desc: 'a one shorthand property two-keyframe sequence', + frames: [ { offset: 0, margin: '10px' }, + { offset: 1, margin: '20px 30px 40px 50px' } ], + expected: [ { property: 'margin-top', + values: [ value(0, '10px', 'replace', 'linear'), + value(1, '20px', 'replace') ] }, + { property: 'margin-right', + values: [ value(0, '10px', 'replace', 'linear'), + value(1, '30px', 'replace') ] }, + { property: 'margin-bottom', + values: [ value(0, '10px', 'replace', 'linear'), + value(1, '40px', 'replace') ] }, + { property: 'margin-left', + values: [ value(0, '10px', 'replace', 'linear'), + value(1, '50px', 'replace') ] } ] + }, + { desc: 'a two-property (a shorthand and one of its component longhands)' + + ' two-keyframe sequence', + frames: [ { offset: 0, margin: '10px', marginTop: '20px' }, + { offset: 1, marginTop: '70px', + margin: '30px 40px 50px 60px' } ], + expected: [ { property: 'margin-top', + values: [ value(0, '20px', 'replace', 'linear'), + value(1, '70px', 'replace') ] }, + { property: 'margin-right', + values: [ value(0, '10px', 'replace', 'linear'), + value(1, '40px', 'replace') ] }, + { property: 'margin-bottom', + values: [ value(0, '10px', 'replace', 'linear'), + value(1, '50px', 'replace') ] }, + { property: 'margin-left', + values: [ value(0, '10px', 'replace', 'linear'), + value(1, '60px', 'replace') ] } ] + }, + { desc: 'a keyframe sequence with duplicate values for a given interior' + + ' offset', + frames: [ { offset: 0.0, left: '10px' }, + { offset: 0.5, left: '20px' }, + { offset: 0.5, left: '30px' }, + { offset: 0.5, left: '40px' }, + { offset: 1.0, left: '50px' } ], + expected: [ { property: 'left', + values: [ value(0, '10px', 'replace', 'linear'), + value(0.5, '20px', 'replace'), + value(0.5, '40px', 'replace', 'linear'), + value(1, '50px', 'replace') ] } ] + }, + { desc: 'a keyframe sequence with duplicate values for offsets 0 and 1', + frames: [ { offset: 0, left: '10px' }, + { offset: 0, left: '20px' }, + { offset: 0, left: '30px' }, + { offset: 1, left: '40px' }, + { offset: 1, left: '50px' }, + { offset: 1, left: '60px' } ], + expected: [ { property: 'left', + values: [ value(0, '10px', 'replace'), + value(0, '30px', 'replace', 'linear'), + value(1, '40px', 'replace'), + value(1, '60px', 'replace') ] } ] + }, + { desc: 'a two-property four-keyframe sequence', + frames: [ { offset: 0, left: '10px' }, + { offset: 0, top: '20px' }, + { offset: 1, top: '30px' }, + { offset: 1, left: '40px' } ], + expected: [ { property: 'left', + values: [ value(0, '10px', 'replace', 'linear'), + value(1, '40px', 'replace') ] }, + { property: 'top', + values: [ value(0, '20px', 'replace', 'linear'), + value(1, '30px', 'replace') ] } ] + }, + { desc: 'a one-property keyframe sequence with some omitted offsets', + frames: [ { offset: 0.00, left: '10px' }, + { offset: 0.25, left: '20px' }, + { left: '30px' }, + { left: '40px' }, + { offset: 1.00, left: '50px' } ], + expected: [ { property: 'left', + values: [ value(0, '10px', 'replace', 'linear'), + value(0.25, '20px', 'replace', 'linear'), + value(0.5, '30px', 'replace', 'linear'), + value(0.75, '40px', 'replace', 'linear'), + value(1, '50px', 'replace') ] } ] + }, + { desc: 'a two-property keyframe sequence with some omitted offsets', + frames: [ { offset: 0.00, left: '10px', top: '20px' }, + { offset: 0.25, left: '30px' }, + { left: '40px' }, + { left: '50px', top: '60px' }, + { offset: 1.00, left: '70px', top: '80px' } ], + expected: [ { property: 'left', + values: [ value(0, '10px', 'replace', 'linear'), + value(0.25, '30px', 'replace', 'linear'), + value(0.5, '40px', 'replace', 'linear'), + value(0.75, '50px', 'replace', 'linear'), + value(1, '70px', 'replace') ] }, + { property: 'top', + values: [ value(0, '20px', 'replace', 'linear'), + value(0.75, '60px', 'replace', 'linear'), + value(1, '80px', 'replace') ] } ] + }, + { desc: 'a one-property keyframe sequence with all omitted offsets', + frames: [ { left: '10px' }, + { left: '20px' }, + { left: '30px' }, + { left: '40px' }, + { left: '50px' } ], + expected: [ { property: 'left', + values: [ value(0, '10px', 'replace', 'linear'), + value(0.25, '20px', 'replace', 'linear'), + value(0.5, '30px', 'replace', 'linear'), + value(0.75, '40px', 'replace', 'linear'), + value(1, '50px', 'replace') ] } ] + }, + { desc: 'a keyframe sequence with different easing values, but the' + + ' same easing value for a given offset', + frames: [ { offset: 0.0, easing: 'ease', left: '10px'}, + { offset: 0.0, easing: 'ease', top: '20px'}, + { offset: 0.5, easing: 'linear', left: '30px' }, + { offset: 0.5, easing: 'linear', top: '40px' }, + { offset: 1.0, easing: 'step-end', left: '50px' }, + { offset: 1.0, easing: 'step-end', top: '60px' } ], + expected: [ { property: 'left', + values: [ value(0, '10px', 'replace', 'ease'), + value(0.5, '30px', 'replace', 'linear'), + value(1, '50px', 'replace') ] }, + { property: 'top', + values: [ value(0, '20px', 'replace', 'ease'), + value(0.5, '40px', 'replace', 'linear'), + value(1, '60px', 'replace') ] } ] + }, + { desc: 'a one-property two-keyframe sequence that needs to' + + ' stringify its values', + frames: [ { offset: 0, opacity: 0 }, + { offset: 1, opacity: 1 } ], + expected: [ { property: 'opacity', + values: [ value(0, '0', 'replace', 'linear'), + value(1, '1', 'replace') ] } ] + }, + { desc: 'a keyframe sequence where shorthand precedes longhand', + frames: [ { offset: 0, margin: '10px', marginRight: '20px' }, + { offset: 1, margin: '30px' } ], + expected: [ { property: 'margin-top', + values: [ value(0, '10px', 'replace', 'linear'), + value(1, '30px', 'replace') ] }, + { property: 'margin-right', + values: [ value(0, '20px', 'replace', 'linear'), + value(1, '30px', 'replace') ] }, + { property: 'margin-bottom', + values: [ value(0, '10px', 'replace', 'linear'), + value(1, '30px', 'replace') ] }, + { property: 'margin-left', + values: [ value(0, '10px', 'replace', 'linear'), + value(1, '30px', 'replace') ] } ] + }, + { desc: 'a keyframe sequence where longhand precedes shorthand', + frames: [ { offset: 0, marginRight: '20px', margin: '10px' }, + { offset: 1, margin: '30px' } ], + expected: [ { property: 'margin-top', + values: [ value(0, '10px', 'replace', 'linear'), + value(1, '30px', 'replace') ] }, + { property: 'margin-right', + values: [ value(0, '20px', 'replace', 'linear'), + value(1, '30px', 'replace') ] }, + { property: 'margin-bottom', + values: [ value(0, '10px', 'replace', 'linear'), + value(1, '30px', 'replace') ] }, + { property: 'margin-left', + values: [ value(0, '10px', 'replace', 'linear'), + value(1, '30px', 'replace') ] } ] + }, + { desc: 'a keyframe sequence where lesser shorthand precedes greater' + + ' shorthand', + frames: [ { offset: 0, borderLeft: '1px solid rgb(1, 2, 3)', + border: '2px dotted rgb(4, 5, 6)' }, + { offset: 1, border: '3px dashed rgb(7, 8, 9)' } ], + expected: [ { property: 'border-bottom-color', + values: [ value(0, 'rgb(4, 5, 6)', 'replace', 'linear'), + value(1, 'rgb(7, 8, 9)', 'replace') ] }, + { property: 'border-left-color', + values: [ value(0, 'rgb(1, 2, 3)', 'replace', 'linear'), + value(1, 'rgb(7, 8, 9)', 'replace') ] }, + { property: 'border-right-color', + values: [ value(0, 'rgb(4, 5, 6)', 'replace', 'linear'), + value(1, 'rgb(7, 8, 9)', 'replace') ] }, + { property: 'border-top-color', + values: [ value(0, 'rgb(4, 5, 6)', 'replace', 'linear'), + value(1, 'rgb(7, 8, 9)', 'replace') ] }, + { property: 'border-bottom-width', + values: [ value(0, '2px', 'replace', 'linear'), + value(1, '3px', 'replace') ] }, + { property: 'border-left-width', + values: [ value(0, '1px', 'replace', 'linear'), + value(1, '3px', 'replace') ] }, + { property: 'border-right-width', + values: [ value(0, '2px', 'replace', 'linear'), + value(1, '3px', 'replace') ] }, + { property: 'border-top-width', + values: [ value(0, '2px', 'replace', 'linear'), + value(1, '3px', 'replace') ] }, + { property: 'border-bottom-style', + values: [ value(0, 'dotted', 'replace', 'linear'), + value(1, 'dashed', 'replace') ] }, + { property: 'border-left-style', + values: [ value(0, 'solid', 'replace', 'linear'), + value(1, 'dashed', 'replace') ] }, + { property: 'border-right-style', + values: [ value(0, 'dotted', 'replace', 'linear'), + value(1, 'dashed', 'replace') ] }, + { property: 'border-top-style', + values: [ value(0, 'dotted', 'replace', 'linear'), + value(1, 'dashed', 'replace') ] }, + { property: 'border-image-outset', + values: [ value(0, '0 0 0 0', 'replace', 'linear'), + value(1, '0 0 0 0', 'replace') ] }, + { property: 'border-image-repeat', + values: [ value(0, 'stretch stretch', 'replace', 'linear'), + value(1, 'stretch stretch', 'replace') ] }, + { property: 'border-image-slice', + values: [ value(0, '100% 100% 100% 100%', + 'replace', 'linear'), + value(1, '100% 100% 100% 100%', 'replace') ] }, + { property: 'border-image-source', + values: [ value(0, 'none', 'replace', 'linear'), + value(1, 'none', 'replace') ] }, + { property: 'border-image-width', + values: [ value(0, '1 1 1 1', 'replace', 'linear'), + value(1, '1 1 1 1', 'replace') ] }, + { property: '-moz-border-bottom-colors', + values: [ value(0, 'none', 'replace', 'linear'), + value(1, 'none', 'replace') ] }, + { property: '-moz-border-left-colors', + values: [ value(0, 'none', 'replace', 'linear'), + value(1, 'none', 'replace') ] }, + { property: '-moz-border-right-colors', + values: [ value(0, 'none', 'replace', 'linear'), + value(1, 'none', 'replace') ] }, + { property: '-moz-border-top-colors', + values: [ value(0, 'none', 'replace', 'linear'), + value(1, 'none', 'replace') ] } ] + }, + { desc: 'a keyframe sequence where greater shorthand precedes' + + ' lesser shorthand', + frames: [ { offset: 0, border: '2px dotted rgb(4, 5, 6)', + borderLeft: '1px solid rgb(1, 2, 3)' }, + { offset: 1, border: '3px dashed rgb(7, 8, 9)' } ], + expected: [ { property: 'border-bottom-color', + values: [ value(0, 'rgb(4, 5, 6)', 'replace', 'linear'), + value(1, 'rgb(7, 8, 9)', 'replace') ] }, + { property: 'border-left-color', + values: [ value(0, 'rgb(1, 2, 3)', 'replace', 'linear'), + value(1, 'rgb(7, 8, 9)', 'replace') ] }, + { property: 'border-right-color', + values: [ value(0, 'rgb(4, 5, 6)', 'replace', 'linear'), + value(1, 'rgb(7, 8, 9)', 'replace') ] }, + { property: 'border-top-color', + values: [ value(0, 'rgb(4, 5, 6)', 'replace', 'linear'), + value(1, 'rgb(7, 8, 9)', 'replace') ] }, + { property: 'border-bottom-width', + values: [ value(0, '2px', 'replace', 'linear'), + value(1, '3px', 'replace') ] }, + { property: 'border-left-width', + values: [ value(0, '1px', 'replace', 'linear'), + value(1, '3px', 'replace') ] }, + { property: 'border-right-width', + values: [ value(0, '2px', 'replace', 'linear'), + value(1, '3px', 'replace') ] }, + { property: 'border-top-width', + values: [ value(0, '2px', 'replace', 'linear'), + value(1, '3px', 'replace') ] }, + { property: 'border-bottom-style', + values: [ value(0, 'dotted', 'replace', 'linear'), + value(1, 'dashed', 'replace') ] }, + { property: 'border-left-style', + values: [ value(0, 'solid', 'replace', 'linear'), + value(1, 'dashed', 'replace') ] }, + { property: 'border-right-style', + values: [ value(0, 'dotted', 'replace', 'linear'), + value(1, 'dashed', 'replace') ] }, + { property: 'border-top-style', + values: [ value(0, 'dotted', 'replace', 'linear'), + value(1, 'dashed', 'replace') ] }, + { property: 'border-image-outset', + values: [ value(0, '0 0 0 0', 'replace', 'linear'), + value(1, '0 0 0 0', 'replace') ] }, + { property: 'border-image-repeat', + values: [ value(0, 'stretch stretch', 'replace', 'linear'), + value(1, 'stretch stretch', 'replace') ] }, + { property: 'border-image-slice', + values: [ value(0, '100% 100% 100% 100%', + 'replace', 'linear'), + value(1, '100% 100% 100% 100%', 'replace') ] }, + { property: 'border-image-source', + values: [ value(0, 'none', 'replace', 'linear'), + value(1, 'none', 'replace') ] }, + { property: 'border-image-width', + values: [ value(0, '1 1 1 1', 'replace', 'linear'), + value(1, '1 1 1 1', 'replace') ] }, + { property: '-moz-border-bottom-colors', + values: [ value(0, 'none', 'replace', 'linear'), + value(1, 'none', 'replace') ] }, + { property: '-moz-border-left-colors', + values: [ value(0, 'none', 'replace', 'linear'), + value(1, 'none', 'replace') ] }, + { property: '-moz-border-right-colors', + values: [ value(0, 'none', 'replace', 'linear'), + value(1, 'none', 'replace') ] }, + { property: '-moz-border-top-colors', + values: [ value(0, 'none', 'replace', 'linear'), + value(1, 'none', 'replace') ] } ] + }, + + // --------------------------------------------------------------------- + // + // Tests for unit conversion + // + // --------------------------------------------------------------------- + + { desc: 'em units are resolved to px values', + frames: { left: ['10em', '20em'] }, + expected: [ { property: 'left', + values: [ value(0, '100px', 'replace', 'linear'), + value(1, '200px', 'replace') ] } ] + }, + { desc: 'calc() expressions are resolved to the equivalent units', + frames: { left: ['calc(10em + 10px)', 'calc(10em + 10%)'] }, + expected: [ { property: 'left', + values: [ value(0, 'calc(110px)', 'replace', 'linear'), + value(1, 'calc(100px + 10%)', 'replace') ] } ] + }, + + // --------------------------------------------------------------------- + // + // Tests for CSS variable handling conversion + // + // --------------------------------------------------------------------- + + { desc: 'CSS variables are resolved to their corresponding values', + frames: { left: ['10px', 'var(--var-100px)'] }, + expected: [ { property: 'left', + values: [ value(0, '10px', 'replace', 'linear'), + value(1, '100px', 'replace') ] } ] + }, + { desc: 'CSS variables in calc() expressions are resolved', + frames: { left: ['10px', 'calc(var(--var-100px) / 2 - 10%)'] }, + expected: [ { property: 'left', + values: [ value(0, '10px', 'replace', 'linear'), + value(1, 'calc(50px + -10%)', 'replace') ] } ] + }, + { desc: 'CSS variables in shorthands are resolved to their corresponding' + + ' values', + frames: { margin: ['10px', 'var(--var-100px-200px)'] }, + expected: [ { property: 'margin-top', + values: [ value(0, '10px', 'replace', 'linear'), + value(1, '100px', 'replace') ] }, + { property: 'margin-right', + values: [ value(0, '10px', 'replace', 'linear'), + value(1, '200px', 'replace') ] }, + { property: 'margin-bottom', + values: [ value(0, '10px', 'replace', 'linear'), + value(1, '100px', 'replace') ] }, + { property: 'margin-left', + values: [ value(0, '10px', 'replace', 'linear'), + value(1, '200px', 'replace') ] } ] + }, + + // --------------------------------------------------------------------- + // + // Tests for properties that parse correctly but which we fail to + // convert to computed values. + // + // --------------------------------------------------------------------- + + { desc: 'a property that can\'t be resolved to computed values in' + + ' initial keyframe', + frames: [ { margin: '5px', simulateComputeValuesFailure: true }, + { margin: '5px' } ], + expected: [ ] + }, + { desc: 'a property that can\'t be resolved to computed values in' + + ' initial keyframe where we have enough values to create' + + ' a final segment', + frames: [ { margin: '5px', simulateComputeValuesFailure: true }, + { margin: '5px' }, + { margin: '5px' } ], + expected: [ ] + }, + { desc: 'a property that can\'t be resolved to computed values in' + + ' initial overlapping keyframes (first in series of two)', + frames: [ { margin: '5px', offset: 0, + simulateComputeValuesFailure: true }, + { margin: '5px', offset: 0 }, + { margin: '5px' } ], + expected: [ { property: 'margin-top', + values: [ value(0, '5px', 'replace', 'linear'), + value(1, '5px', 'replace') ] }, + { property: 'margin-right', + values: [ value(0, '5px', 'replace', 'linear'), + value(1, '5px', 'replace') ] }, + { property: 'margin-bottom', + values: [ value(0, '5px', 'replace', 'linear'), + value(1, '5px', 'replace') ] }, + { property: 'margin-left', + values: [ value(0, '5px', 'replace', 'linear'), + value(1, '5px', 'replace') ] } ] + }, + { desc: 'a property that can\'t be resolved to computed values in' + + ' initial overlapping keyframes (second in series of two)', + frames: [ { margin: '5px', offset: 0 }, + { margin: '5px', offset: 0, + simulateComputeValuesFailure: true }, + { margin: '5px' } ], + expected: [ { property: 'margin-top', + values: [ value(0, '5px', 'replace', 'linear'), + value(1, '5px', 'replace') ] }, + { property: 'margin-right', + values: [ value(0, '5px', 'replace', 'linear'), + value(1, '5px', 'replace') ] }, + { property: 'margin-bottom', + values: [ value(0, '5px', 'replace', 'linear'), + value(1, '5px', 'replace') ] }, + { property: 'margin-left', + values: [ value(0, '5px', 'replace', 'linear'), + value(1, '5px', 'replace') ] } ] + }, + { desc: 'a property that can\'t be resolved to computed values in' + + ' initial overlapping keyframes (second in series of three)', + frames: [ { margin: '5px', offset: 0 }, + { margin: '5px', offset: 0, + simulateComputeValuesFailure: true }, + { margin: '5px', offset: 0 }, + { margin: '5px' } ], + expected: [ { property: 'margin-top', + values: [ value(0, '5px', 'replace'), + value(0, '5px', 'replace', 'linear'), + value(1, '5px', 'replace') ] }, + { property: 'margin-right', + values: [ value(0, '5px', 'replace'), + value(0, '5px', 'replace', 'linear'), + value(1, '5px', 'replace') ] }, + { property: 'margin-bottom', + values: [ value(0, '5px', 'replace'), + value(0, '5px', 'replace', 'linear'), + value(1, '5px', 'replace') ] }, + { property: 'margin-left', + values: [ value(0, '5px', 'replace'), + value(0, '5px', 'replace', 'linear'), + value(1, '5px', 'replace') ] } ] + }, + { desc: 'a property that can\'t be resolved to computed values in' + + ' final keyframe', + frames: [ { margin: '5px' }, + { margin: '5px', simulateComputeValuesFailure: true } ], + expected: [ ] + }, + { desc: 'a property that can\'t be resolved to computed values in' + + ' final keyframe where it forms the last segment in the series', + frames: [ { margin: '5px' }, + { margin: '5px', + marginLeft: '5px', + marginRight: '5px', + marginBottom: '5px', + // margin-top sorts last and only it will be missing since + // the other longhand components are specified + simulateComputeValuesFailure: true } ], + expected: [ { property: 'margin-bottom', + values: [ value(0, '5px', 'replace', 'linear'), + value(1, '5px', 'replace') ] }, + { property: 'margin-left', + values: [ value(0, '5px', 'replace', 'linear'), + value(1, '5px', 'replace') ] }, + { property: 'margin-right', + values: [ value(0, '5px', 'replace', 'linear'), + value(1, '5px', 'replace') ] } ] + }, + { desc: 'a property that can\'t be resolved to computed values in' + + ' final keyframe where we have enough values to create' + + ' an initial segment', + frames: [ { margin: '5px' }, + { margin: '5px' }, + { margin: '5px', simulateComputeValuesFailure: true } ], + expected: [ ] + }, + { desc: 'a property that can\'t be resolved to computed values in' + + ' final overlapping keyframes (first in series of two)', + frames: [ { margin: '5px' }, + { margin: '5px', offset: 1, + simulateComputeValuesFailure: true }, + { margin: '5px', offset: 1 } ], + expected: [ { property: 'margin-top', + values: [ value(0, '5px', 'replace', 'linear'), + value(1, '5px', 'replace') ] }, + { property: 'margin-right', + values: [ value(0, '5px', 'replace', 'linear'), + value(1, '5px', 'replace') ] }, + { property: 'margin-bottom', + values: [ value(0, '5px', 'replace', 'linear'), + value(1, '5px', 'replace') ] }, + { property: 'margin-left', + values: [ value(0, '5px', 'replace', 'linear'), + value(1, '5px', 'replace') ] } ] + }, + { desc: 'a property that can\'t be resolved to computed values in' + + ' final overlapping keyframes (second in series of two)', + frames: [ { margin: '5px' }, + { margin: '5px', offset: 1 }, + { margin: '5px', offset: 1, + simulateComputeValuesFailure: true } ], + expected: [ { property: 'margin-top', + values: [ value(0, '5px', 'replace', 'linear'), + value(1, '5px', 'replace') ] }, + { property: 'margin-right', + values: [ value(0, '5px', 'replace', 'linear'), + value(1, '5px', 'replace') ] }, + { property: 'margin-bottom', + values: [ value(0, '5px', 'replace', 'linear'), + value(1, '5px', 'replace') ] }, + { property: 'margin-left', + values: [ value(0, '5px', 'replace', 'linear'), + value(1, '5px', 'replace') ] } ] + }, + { desc: 'a property that can\'t be resolved to computed values in' + + ' final overlapping keyframes (second in series of three)', + frames: [ { margin: '5px' }, + { margin: '5px', offset: 1 }, + { margin: '5px', offset: 1, + simulateComputeValuesFailure: true }, + { margin: '5px', offset: 1 } ], + expected: [ { property: 'margin-top', + values: [ value(0, '5px', 'replace', 'linear'), + value(1, '5px', 'replace'), + value(1, '5px', 'replace') ] }, + { property: 'margin-right', + values: [ value(0, '5px', 'replace', 'linear'), + value(1, '5px', 'replace'), + value(1, '5px', 'replace') ] }, + { property: 'margin-bottom', + values: [ value(0, '5px', 'replace', 'linear'), + value(1, '5px', 'replace'), + value(1, '5px', 'replace') ] }, + { property: 'margin-left', + values: [ value(0, '5px', 'replace', 'linear'), + value(1, '5px', 'replace'), + value(1, '5px', 'replace') ] } ] + }, + { desc: 'a property that can\'t be resolved to computed values in' + + ' intermediate keyframe', + frames: [ { margin: '5px' }, + { margin: '5px', simulateComputeValuesFailure: true }, + { margin: '5px' } ], + expected: [ { property: 'margin-top', + values: [ value(0, '5px', 'replace', 'linear'), + value(1, '5px', 'replace') ] }, + { property: 'margin-right', + values: [ value(0, '5px', 'replace', 'linear'), + value(1, '5px', 'replace') ] }, + { property: 'margin-bottom', + values: [ value(0, '5px', 'replace', 'linear'), + value(1, '5px', 'replace') ] }, + { property: 'margin-left', + values: [ value(0, '5px', 'replace', 'linear'), + value(1, '5px', 'replace') ] } ] + }, + { desc: 'a property that can\'t be resolved to computed values in' + + ' initial keyframe along with other values', + // simulateComputeValuesFailure only applies to shorthands so we can set + // it on the same keyframe and it will only apply to |margin| and not + // |left|. + frames: [ { margin: '77%', left: '10px', + simulateComputeValuesFailure: true }, + { margin: '5px', left: '20px' } ], + expected: [ { property: 'left', + values: [ value(0, '10px', 'replace', 'linear'), + value(1, '20px', 'replace') ] } ], + }, + { desc: 'a property that can\'t be resolved to computed values in' + + ' initial keyframe along with other values where those' + + ' values sort after the property with missing values', + frames: [ { margin: '77%', right: '10px', + simulateComputeValuesFailure: true }, + { margin: '5px', right: '20px' } ], + expected: [ { property: 'right', + values: [ value(0, '10px', 'replace', 'linear'), + value(1, '20px', 'replace') ] } ], + }, + { desc: 'a property that can\'t be resolved to computed values in' + + ' final keyframe along with other values', + frames: [ { margin: '5px', left: '10px' }, + { margin: '5px', left: '20px', + simulateComputeValuesFailure: true } ], + expected: [ { property: 'left', + values: [ value(0, '10px', 'replace', 'linear'), + value(1, '20px', 'replace') ] } ], + }, + { desc: 'a property that can\'t be resolved to computed values in' + + ' final keyframe along with other values where those' + + ' values sort after the property with missing values', + frames: [ { margin: '5px', right: '10px' }, + { margin: '5px', right: '20px', + simulateComputeValuesFailure: true } ], + expected: [ { property: 'right', + values: [ value(0, '10px', 'replace', 'linear'), + value(1, '20px', 'replace') ] } ], + }, + { desc: 'a property that can\'t be resolved to computed values in' + + ' an intermediate keyframe along with other values', + frames: [ { margin: '5px', left: '10px' }, + { margin: '5px', left: '20px', + simulateComputeValuesFailure: true }, + { margin: '5px', left: '30px' } ], + expected: [ { property: 'margin-top', + values: [ value(0, '5px', 'replace', 'linear'), + value(1, '5px', 'replace') ] }, + { property: 'margin-right', + values: [ value(0, '5px', 'replace', 'linear'), + value(1, '5px', 'replace') ] }, + { property: 'margin-bottom', + values: [ value(0, '5px', 'replace', 'linear'), + value(1, '5px', 'replace') ] }, + { property: 'margin-left', + values: [ value(0, '5px', 'replace', 'linear'), + value(1, '5px', 'replace') ] }, + { property: 'left', + values: [ value(0, '10px', 'replace', 'linear'), + value(0.5, '20px', 'replace', 'linear'), + value(1, '30px', 'replace') ] } ] + }, + { desc: 'a property that can\'t be resolved to computed values in' + + ' an intermediate keyframe by itself', + frames: [ { margin: '5px', left: '10px' }, + { margin: '5px', + simulateComputeValuesFailure: true }, + { margin: '5px', left: '30px' } ], + expected: [ { property: 'margin-top', + values: [ value(0, '5px', 'replace', 'linear'), + value(1, '5px', 'replace') ] }, + { property: 'margin-right', + values: [ value(0, '5px', 'replace', 'linear'), + value(1, '5px', 'replace') ] }, + { property: 'margin-bottom', + values: [ value(0, '5px', 'replace', 'linear'), + value(1, '5px', 'replace') ] }, + { property: 'margin-left', + values: [ value(0, '5px', 'replace', 'linear'), + value(1, '5px', 'replace') ] }, + { property: 'left', + values: [ value(0, '10px', 'replace', 'linear'), + value(1, '30px', 'replace') ] } ] + }, +]; + +gTests.forEach(function(subtest) { + test(function(t) { + var div = addDiv(t); + var animation = div.animate(subtest.frames, 100 * MS_PER_SEC); + assert_properties_equal(animation.effect.getProperties(), + subtest.expected); + }, subtest.desc); +}); + +</script> +</body> diff --git a/dom/animation/test/chrome/test_generated_content_getAnimations.html b/dom/animation/test/chrome/test_generated_content_getAnimations.html new file mode 100644 index 0000000000..04e0f9bd09 --- /dev/null +++ b/dom/animation/test/chrome/test_generated_content_getAnimations.html @@ -0,0 +1,83 @@ +<!DOCTYPE html> +<head> +<meta charset=utf-8> +<title>Test getAnimations() for generated-content elements</title> +<script type="application/javascript" src="../testharness.js"></script> +<script type="application/javascript" src="../testharnessreport.js"></script> +<script type="application/javascript" src="../testcommon.js"></script> +<style> +@keyframes anim { } +@keyframes anim2 { } +.before::before { + content: ''; + animation: anim 100s; +} +.after::after { + content: ''; + animation: anim 100s, anim2 100s; +} +</style> +</head> +<body> +<div id='root' class='before after'> + <div class='before'></div> + <div></div> +</div> +<script> +'use strict'; + +const {Cc, Ci, Cu} = SpecialPowers; + +function getWalker(node) { + var walker = Cc["@mozilla.org/inspector/deep-tree-walker;1"]. + createInstance(Ci.inIDeepTreeWalker); + walker.showAnonymousContent = true; + walker.init(node.ownerDocument, Ci.nsIDOMNodeFilter.SHOW_ALL); + walker.currentNode = node; + return walker; +} + +test(function(t) { + var root = document.getElementById('root'); + // Flush first to make sure the generated-content elements are ready + // in the tree. + flushComputedStyle(root); + var before = getWalker(root).firstChild(); + var after = getWalker(root).lastChild(); + + // Sanity Checks + assert_equals(document.getAnimations().length, 4, + 'All animations in this document'); + assert_equals(before.tagName, '_moz_generated_content_before', + 'First child is ::before element'); + assert_equals(after.tagName, '_moz_generated_content_after', + 'Last child is ::after element'); + + // Test Element.getAnimations() for generated-content elements + assert_equals(before.getAnimations().length, 1, + 'Animations of ::before generated-content element'); + assert_equals(after.getAnimations().length, 2, + 'Animations of ::after generated-content element'); +}, 'Element.getAnimations() used on generated-content elements'); + +test(function(t) { + var root = document.getElementById('root'); + flushComputedStyle(root); + var walker = getWalker(root); + + var animations = []; + var element = walker.currentNode; + while (element) { + if (element.getAnimations) { + animations = [...animations, ...element.getAnimations()]; + } + element = walker.nextNode(); + } + + assert_equals(animations.length, document.getAnimations().length, + 'The number of animations got by DeepTreeWalker and ' + + 'document.getAnimations() should be the same'); +}, 'Element.getAnimations() used by traversing DeepTreeWalker'); + +</script> +</body> diff --git a/dom/animation/test/chrome/test_observers_for_sync_api.html b/dom/animation/test/chrome/test_observers_for_sync_api.html new file mode 100644 index 0000000000..20c3f3670d --- /dev/null +++ b/dom/animation/test/chrome/test_observers_for_sync_api.html @@ -0,0 +1,854 @@ +<!DOCTYPE html> +<meta charset=utf-8> +<title> +Test chrome-only MutationObserver animation notifications for sync APIs +</title> +<script type="application/javascript" src="../testharness.js"></script> +<script type="application/javascript" src="../testharnessreport.js"></script> +<script type="application/javascript" src="../testcommon.js"></script> +<div id="log"></div> +<style> +@keyframes anim { + to { transform: translate(100px); } +} +@keyframes anotherAnim { + to { transform: translate(0px); } +} +</style> +<script> + +function assert_record_list(actual, expected, desc, index, listName) { + assert_equals(actual.length, expected.length, + `${desc} - record[${index}].${listName} length`); + if (actual.length != expected.length) { + return; + } + for (var i = 0; i < actual.length; i++) { + assert_not_equals(actual.indexOf(expected[i]), -1, + `${desc} - record[${index}].${listName} contains expected Animation`); + } +} + +function assert_equals_records(actual, expected, desc) { + assert_equals(actual.length, expected.length, `${desc} - number of records`); + if (actual.length != expected.length) { + return; + } + for (var i = 0; i < actual.length; i++) { + assert_record_list(actual[i].addedAnimations, + expected[i].added, desc, i, "addedAnimations"); + assert_record_list(actual[i].changedAnimations, + expected[i].changed, desc, i, "changedAnimations"); + assert_record_list(actual[i].removedAnimations, + expected[i].removed, desc, i, "removedAnimations"); + } +} + +// Create a pseudo element +function createPseudo(test, element, type) { + addStyle(test, { '@keyframes anim': '', + ['.pseudo::' + type]: 'animation: anim 10s;' }); + element.classList.add('pseudo'); + var anims = document.getAnimations(); + assert_true(anims.length >= 1); + var anim = anims[anims.length - 1]; + assert_equals(anim.effect.target.parentElement, element); + assert_equals(anim.effect.target.type, '::' + type); + anim.cancel(); + return anim.effect.target; +} + +[ { subtree: false }, + { subtree: true } +].forEach(aOptions => { + test(t => { + var div = addDiv(t); + var observer = + setupSynchronousObserver(t, + aOptions.subtree ? div.parentNode : div, + aOptions.subtree); + + var anim = div.animate({ opacity: [ 0, 1 ] }, 200 * MS_PER_SEC); + + assert_equals_records(observer.takeRecords(), + [{ added: [anim], changed: [], removed: [] }], + "records after animation is added"); + + anim.effect.timing.duration = 100 * MS_PER_SEC; + assert_equals_records(observer.takeRecords(), + [{ added: [], changed: [anim], removed: [] }], + "records after duration is changed"); + + anim.effect.timing.duration = 100 * MS_PER_SEC; + assert_equals_records(observer.takeRecords(), + [], "records after assigning same value"); + + anim.currentTime = anim.effect.timing.duration * 2; + anim.finish(); + assert_equals_records(observer.takeRecords(), + [{ added: [], changed: [], removed: [anim] }], + "records after animation end"); + + anim.effect.timing.duration = anim.effect.timing.duration * 3; + assert_equals_records(observer.takeRecords(), + [{ added: [anim], changed: [], removed: [] }], + "records after animation restarted"); + + anim.effect.timing.duration = "auto"; + assert_equals_records(observer.takeRecords(), + [{ added: [], changed: [], removed: [anim] }], + "records after duration set \"auto\""); + + anim.effect.timing.duration = "auto"; + assert_equals_records(observer.takeRecords(), + [], "records after assigning same value \"auto\""); + }, "change_duration_and_currenttime"); + + test(t => { + var div = addDiv(t); + var observer = + setupSynchronousObserver(t, + aOptions.subtree ? div.parentNode : div, + aOptions.subtree); + + var anim = div.animate({ opacity: [ 0, 1 ] }, 100 * MS_PER_SEC); + + assert_equals_records(observer.takeRecords(), + [{ added: [anim], changed: [], removed: [] }], + "records after animation is added"); + + anim.effect.timing.endDelay = 10 * MS_PER_SEC; + assert_equals_records(observer.takeRecords(), + [{ added: [], changed: [anim], removed: [] }], + "records after endDelay is changed"); + + anim.effect.timing.endDelay = 10 * MS_PER_SEC; + assert_equals_records(observer.takeRecords(), + [], "records after assigning same value"); + + anim.currentTime = 109 * MS_PER_SEC; + assert_equals_records(observer.takeRecords(), + [{ added: [], changed: [], removed: [anim] }], + "records after currentTime during endDelay"); + + anim.effect.timing.endDelay = -110 * MS_PER_SEC; + assert_equals_records(observer.takeRecords(), + [], "records after assigning negative value"); + }, "change_enddelay_and_currenttime"); + + test(t => { + var div = addDiv(t); + var observer = + setupSynchronousObserver(t, + aOptions.subtree ? div.parentNode : div, + aOptions.subtree); + + var anim = div.animate({ opacity: [ 0, 1 ] }, + { duration: 100 * MS_PER_SEC, + endDelay: -100 * MS_PER_SEC }); + assert_equals_records(observer.takeRecords(), + [], "records after animation is added"); + }, "zero_end_time"); + + test(t => { + var div = addDiv(t); + var observer = + setupSynchronousObserver(t, + aOptions.subtree ? div.parentNode : div, + aOptions.subtree); + + var anim = div.animate({ opacity: [ 0, 1 ] }, 100 * MS_PER_SEC); + + assert_equals_records(observer.takeRecords(), + [{ added: [anim], changed: [], removed: [] }], + "records after animation is added"); + + anim.effect.timing.iterations = 2; + assert_equals_records(observer.takeRecords(), + [{ added: [], changed: [anim], removed: [] }], + "records after iterations is changed"); + + anim.effect.timing.iterations = 2; + assert_equals_records(observer.takeRecords(), + [], "records after assigning same value"); + + anim.effect.timing.iterations = 0; + assert_equals_records(observer.takeRecords(), + [{ added: [], changed: [], removed: [anim] }], + "records after animation end"); + + anim.effect.timing.iterations = Infinity; + assert_equals_records(observer.takeRecords(), + [{ added: [anim], changed: [], removed: [] }], + "records after animation restarted"); + }, "change_iterations"); + + test(t => { + var div = addDiv(t); + var observer = + setupSynchronousObserver(t, + aOptions.subtree ? div.parentNode : div, + aOptions.subtree); + + var anim = div.animate({ opacity: [ 0, 1 ] }, 100 * MS_PER_SEC); + + assert_equals_records(observer.takeRecords(), + [{ added: [anim], changed: [], removed: [] }], + "records after animation is added"); + + anim.effect.timing.delay = 100; + assert_equals_records(observer.takeRecords(), + [{ added: [], changed: [anim], removed: [] }], + "records after delay is changed"); + + anim.effect.timing.delay = 100; + assert_equals_records(observer.takeRecords(), + [], "records after assigning same value"); + + anim.effect.timing.delay = -100 * MS_PER_SEC; + assert_equals_records(observer.takeRecords(), + [{ added: [], changed: [], removed: [anim] }], + "records after animation end"); + + anim.effect.timing.delay = 0; + assert_equals_records(observer.takeRecords(), + [{ added: [anim], changed: [], removed: [] }], + "records after animation restarted"); + }, "change_delay"); + + test(t => { + var div = addDiv(t); + var observer = + setupSynchronousObserver(t, + aOptions.subtree ? div.parentNode : div, + aOptions.subtree); + + var anim = div.animate({ opacity: [ 0, 1 ] }, + { duration: 100 * MS_PER_SEC, + easing: "steps(2, start)" }); + + assert_equals_records(observer.takeRecords(), + [{ added: [anim], changed: [], removed: [] }], + "records after animation is added"); + + anim.effect.timing.easing = "steps(2, end)"; + assert_equals_records(observer.takeRecords(), + [{ added: [], changed: [anim], removed: [] }], + "records after easing is changed"); + + anim.effect.timing.easing = "steps(2, end)"; + assert_equals_records(observer.takeRecords(), + [], "records after assigning same value"); + }, "change_easing"); + + test(t => { + var div = addDiv(t); + var observer = + setupSynchronousObserver(t, + aOptions.subtree ? div.parentNode : div, + aOptions.subtree); + + var anim = div.animate({ opacity: [ 0, 1 ] }, + { duration: 100, delay: -100 }); + assert_equals_records(observer.takeRecords(), + [], "records after assigning negative value"); + }, "negative_delay_in_constructor"); + + test(t => { + var div = addDiv(t); + var observer = + setupSynchronousObserver(t, + aOptions.subtree ? div.parentNode : div, + aOptions.subtree); + + var effect = new KeyframeEffectReadOnly(null, + { opacity: [ 0, 1 ] }, + { duration: 100 * MS_PER_SEC }); + var anim = new Animation(effect, document.timeline); + anim.play(); + assert_equals_records(observer.takeRecords(), + [], "no records after animation is added"); + }, "create_animation_without_target"); + + test(t => { + var div = addDiv(t); + var observer = + setupSynchronousObserver(t, + aOptions.subtree ? div.parentNode : div, + aOptions.subtree); + + var anim = div.animate({ opacity: [ 0, 1 ] }, + { duration: 100 * MS_PER_SEC }); + assert_equals_records(observer.takeRecords(), + [{ added: [anim], changed: [], removed: [] }], + "records after animation is added"); + + anim.effect.target = div; + assert_equals_records(observer.takeRecords(), + [], "no records after setting the same target"); + + anim.effect.target = null; + assert_equals_records(observer.takeRecords(), + [{ added: [], changed: [], removed: [anim] }], + "records after setting null"); + + anim.effect.target = null; + assert_equals_records(observer.takeRecords(), + [], "records after setting redundant null"); + }, "set_redundant_animation_target"); + + test(t => { + var div = addDiv(t); + var observer = + setupSynchronousObserver(t, + aOptions.subtree ? div.parentNode : div, + aOptions.subtree); + + var anim = div.animate({ opacity: [ 0, 1 ] }, + { duration: 100 * MS_PER_SEC }); + assert_equals_records(observer.takeRecords(), + [{ added: [anim], changed: [], removed: [] }], + "records after animation is added"); + + anim.effect = null; + assert_equals_records(observer.takeRecords(), + [{ added: [], changed: [], removed: [anim] }], + "records after animation is removed"); + }, "set_null_animation_effect"); + + test(t => { + var div = addDiv(t); + var observer = + setupSynchronousObserver(t, + aOptions.subtree ? div.parentNode : div, + aOptions.subtree); + + var anim = new Animation(); + anim.play(); + anim.effect = new KeyframeEffect(div, { opacity: [ 0, 1 ] }, + 100 * MS_PER_SEC); + assert_equals_records(observer.takeRecords(), + [{ added: [anim], changed: [], removed: [] }], + "records after animation is added"); + }, "set_effect_on_null_effect_animation"); + + test(t => { + var div = addDiv(t); + var observer = + setupSynchronousObserver(t, + aOptions.subtree ? div.parentNode : div, + aOptions.subtree); + + var anim = div.animate({ marginLeft: [ "0px", "100px" ] }, + 100 * MS_PER_SEC); + assert_equals_records(observer.takeRecords(), + [{ added: [anim], changed: [], removed: [] }], + "records after animation is added"); + + anim.effect = new KeyframeEffect(div, { opacity: [ 0, 1 ] }, + 100 * MS_PER_SEC); + assert_equals_records(observer.takeRecords(), + [{ added: [], changed: [anim], removed: [] }], + "records after replace effects"); + }, "replace_effect_targeting_on_the_same_element"); + + test(t => { + var div = addDiv(t); + var observer = + setupSynchronousObserver(t, + aOptions.subtree ? div.parentNode : div, + aOptions.subtree); + + var anim = div.animate({ marginLeft: [ "0px", "100px" ] }, + 100 * MS_PER_SEC); + assert_equals_records(observer.takeRecords(), + [{ added: [anim], changed: [], removed: [] }], + "records after animation is added"); + + anim.currentTime = 60 * MS_PER_SEC; + assert_equals_records(observer.takeRecords(), + [{ added: [], changed: [anim], removed: [] }], + "records after animation is changed"); + + anim.effect = new KeyframeEffect(div, { opacity: [ 0, 1 ] }, + 50 * MS_PER_SEC); + assert_equals_records(observer.takeRecords(), + [{ added: [], changed: [], removed: [anim] }], + "records after replacing effects"); + }, "replace_effect_targeting_on_the_same_element_not_in_effect"); + + test(t => { + var div = addDiv(t); + var observer = + setupSynchronousObserver(t, + aOptions.subtree ? div.parentNode : div, + aOptions.subtree); + + var anim = div.animate([ { marginLeft: "0px" }, + { marginLeft: "-20px" }, + { marginLeft: "100px" }, + { marginLeft: "50px" } ], + { duration: 100 * MS_PER_SEC }); + assert_equals_records(observer.takeRecords(), + [{ added: [anim], changed: [], removed: [] }], + "records after animation is added"); + + anim.effect.spacing = "paced(margin-left)"; + assert_equals_records(observer.takeRecords(), + [{ added: [], changed: [anim], removed: [] }], + "records after animation is changed"); + }, "set_spacing"); + + test(t => { + var div = addDiv(t); + var observer = + setupSynchronousObserver(t, + aOptions.subtree ? div.parentNode : div, + aOptions.subtree); + + var anim = div.animate([ { marginLeft: "0px" }, + { marginLeft: "-20px" }, + { marginLeft: "100px" }, + { marginLeft: "50px" } ], + { duration: 100 * MS_PER_SEC, + spacing: "paced(margin-left)" }); + assert_equals_records(observer.takeRecords(), + [{ added: [anim], changed: [], removed: [] }], + "records after animation is added"); + + anim.effect.spacing = "paced(animation-duration)"; + assert_equals_records(observer.takeRecords(), + [{ added: [], changed: [anim], removed: [] }], + "records after setting a non-animatable paced property"); + }, "set_spacing_on_a_non-animatable_property"); + + test(t => { + var div = addDiv(t); + var observer = + setupSynchronousObserver(t, + aOptions.subtree ? div.parentNode : div, + aOptions.subtree); + + var anim = div.animate([ { marginLeft: "0px" }, + { marginLeft: "-20px" }, + { marginLeft: "100px" }, + { marginLeft: "50px" } ], + { duration: 100 * MS_PER_SEC, + spacing: "paced(margin-left)" }); + assert_equals_records(observer.takeRecords(), + [{ added: [anim], changed: [], removed: [] }], + "records after animation is added"); + + anim.effect.spacing = "paced(margin-left)"; + assert_equals_records(observer.takeRecords(), + [], "no record after setting the same spacing"); + }, "set_the_same_spacing"); + + // Test that starting a single animation that is cancelled by calling + // cancel() dispatches an added notification and then a removed + // notification. + test(t => { + var div = addDiv(t, { style: "animation: anim 100s forwards" }); + var observer = + setupSynchronousObserver(t, + aOptions.subtree ? div.parentNode : div, + aOptions.subtree); + + var animations = div.getAnimations(); + assert_equals(animations.length, 1, + "getAnimations().length after animation start"); + + assert_equals_records(observer.takeRecords(), + [{ added: animations, changed: [], removed: [] }], + "records after animation start"); + + animations[0].cancel(); + + assert_equals_records(observer.takeRecords(), + [{ added: [], changed: [], removed: animations }], + "records after animation end"); + + // Re-trigger the animation. + animations[0].play(); + + // Single MutationRecord for the Animation (re-)addition. + assert_equals_records(observer.takeRecords(), + [{ added: animations, changed: [], removed: [] }], + "records after animation start"); + }, "single_animation_cancelled_api"); + + // Test that updating a property on the Animation object dispatches a changed + // notification. + [ + { prop: "playbackRate", val: 0.5 }, + { prop: "startTime", val: 50 * MS_PER_SEC }, + { prop: "currentTime", val: 50 * MS_PER_SEC }, + ].forEach(function(aChangeTest) { + test(t => { + // We use a forwards fill mode so that even if the change we make causes + // the animation to become finished, it will still be "relevant" so we + // won't mark it as removed. + var div = addDiv(t, { style: "animation: anim 100s forwards" }); + var observer = + setupSynchronousObserver(t, + aOptions.subtree ? div.parentNode : div, + aOptions.subtree); + + var animations = div.getAnimations(); + assert_equals(animations.length, 1, + "getAnimations().length after animation start"); + + assert_equals_records(observer.takeRecords(), + [{ added: animations, changed: [], removed: [] }], + "records after animation start"); + + // Update the property. + animations[0][aChangeTest.prop] = aChangeTest.val; + + // Make a redundant change. + animations[0][aChangeTest.prop] = animations[0][aChangeTest.prop]; + + assert_equals_records(observer.takeRecords(), + [{ added: [], changed: animations, removed: [] }], + "records after animation property change"); + }, `single_animation_api_change_${aChangeTest.prop}`); + }); + + // Test that making a redundant change to currentTime while an Animation + // is pause-pending still generates a change MutationRecord since setting + // the currentTime to any value in this state aborts the pending pause. + test(t => { + var div = addDiv(t, { style: "animation: anim 100s" }); + var observer = + setupSynchronousObserver(t, + aOptions.subtree ? div.parentNode : div, + aOptions.subtree); + + var animations = div.getAnimations(); + assert_equals(animations.length, 1, + "getAnimations().length after animation start"); + + assert_equals_records(observer.takeRecords(), + [{ added: animations, changed: [], removed: [] }], + "records after animation start"); + + animations[0].pause(); + + // We are now pause-pending. Even if we make a redundant change to the + // currentTime, we should still get a change record because setting the + // currentTime while pause-pending has the effect of cancelling a pause. + animations[0].currentTime = animations[0].currentTime; + + // Two MutationRecords for the Animation changes: one for pausing, one + // for aborting the pause. + assert_equals_records(observer.takeRecords(), + [{ added: [], changed: animations, removed: [] }, + { added: [], changed: animations, removed: [] }], + "records after pausing then seeking"); + }, "change_currentTime_while_pause_pending"); + + // Test that calling finish() on a forwards-filling Animation dispatches + // a changed notification. + test(t => { + var div = addDiv(t, { style: "animation: anim 100s forwards" }); + var observer = + setupSynchronousObserver(t, + aOptions.subtree ? div.parentNode : div, + aOptions.subtree); + + var animations = div.getAnimations(); + assert_equals(animations.length, 1, + "getAnimations().length after animation start"); + + assert_equals_records(observer.takeRecords(), + [{ added: animations, changed: [], removed: [] }], + "records after animation start"); + + animations[0].finish(); + + assert_equals_records(observer.takeRecords(), + [{ added: [], changed: animations, removed: [] }], + "records after finish()"); + + // Redundant finish. + animations[0].finish(); + + // Ensure no change records. + assert_equals_records(observer.takeRecords(), + [], "records after redundant finish()"); + }, "finish_with_forwards_fill"); + + // Test that calling finish() on an Animation that does not fill forwards, + // dispatches a removal notification. + test(t => { + var div = addDiv(t, { style: "animation: anim 100s" }); + var observer = + setupSynchronousObserver(t, + aOptions.subtree ? div.parentNode : div, + aOptions.subtree); + + var animations = div.getAnimations(); + assert_equals(animations.length, 1, + "getAnimations().length after animation start"); + + assert_equals_records(observer.takeRecords(), + [{ added: animations, changed: [], removed: [] }], + "records after animation start"); + + animations[0].finish(); + + // Single MutationRecord for the Animation removal. + assert_equals_records(observer.takeRecords(), + [{ added: [], changed: [], removed: animations }], + "records after finishing"); + }, "finish_without_fill"); + + // Test that calling finish() on a forwards-filling Animation dispatches + test(t => { + var div = addDiv(t, { style: "animation: anim 100s" }); + var observer = + setupSynchronousObserver(t, + aOptions.subtree ? div.parentNode : div, + aOptions.subtree); + + var animation = div.getAnimations()[0]; + assert_equals_records(observer.takeRecords(), + [{ added: [animation], changed: [], removed: []}], + "records after creation"); + animation.id = "new id"; + assert_equals_records(observer.takeRecords(), + [{ added: [], changed: [animation], removed: []}], + "records after id is changed"); + + animation.id = "new id"; + assert_equals_records(observer.takeRecords(), + [], "records after assigning same value with id"); + }, "change_id"); + + // Test that calling reverse() dispatches a changed notification. + test(t => { + var div = addDiv(t, { style: "animation: anim 100s both" }); + var observer = + setupSynchronousObserver(t, + aOptions.subtree ? div.parentNode : div, + aOptions.subtree); + + var animations = div.getAnimations(); + assert_equals(animations.length, 1, + "getAnimations().length after animation start"); + + assert_equals_records(observer.takeRecords(), + [{ added: animations, changed: [], removed: [] }], + "records after animation start"); + + animations[0].reverse(); + + assert_equals_records(observer.takeRecords(), + [{ added: [], changed: animations, removed: [] }], + "records after calling reverse()"); + }, "reverse"); + + // Test that calling reverse() does *not* dispatch a changed notification + // when playbackRate == 0. + test(t => { + var div = addDiv(t, { style: "animation: anim 100s both" }); + var observer = + setupSynchronousObserver(t, + aOptions.subtree ? div.parentNode : div, + aOptions.subtree); + + var animations = div.getAnimations(); + assert_equals(animations.length, 1, + "getAnimations().length after animation start"); + + assert_equals_records(observer.takeRecords(), + [{ added: animations, changed: [], removed: [] }], + "records after animation start"); + + // Seek to the middle and set playbackRate to zero. + animations[0].currentTime = 50 * MS_PER_SEC; + animations[0].playbackRate = 0; + + // Two MutationRecords, one for each change. + assert_equals_records(observer.takeRecords(), + [{ added: [], changed: animations, removed: [] }, + { added: [], changed: animations, removed: [] }], + "records after seeking and setting playbackRate"); + + animations[0].reverse(); + + // We should get no notifications. + assert_equals_records(observer.takeRecords(), + [], "records after calling reverse()"); + }, "reverse_with_zero_playbackRate"); + + // Test that attempting to start an animation that should already be finished + // does not send any notifications. + test(t => { + // Start an animation that should already be finished. + var div = addDiv(t, { style: "animation: anim 1s -2s;" }); + var observer = + setupSynchronousObserver(t, + aOptions.subtree ? div.parentNode : div, + aOptions.subtree); + + // The animation should cause no Animations to be created. + var animations = div.getAnimations(); + assert_equals(animations.length, 0, + "getAnimations().length after animation start"); + + // And we should get no notifications. + assert_equals_records(observer.takeRecords(), + [], "records after attempted animation start"); + }, "already_finished"); + + test(t => { + var div = addDiv(t, { style: "animation: anim 100s, anotherAnim 100s" }); + var observer = + setupSynchronousObserver(t, + aOptions.subtree ? div.parentNode : div, + aOptions.subtree); + + var animations = div.getAnimations(); + + assert_equals_records(observer.takeRecords(), + [{ added: animations, changed: [], removed: []}], + "records after creation"); + + div.style.animation = "anotherAnim 100s, anim 100s"; + animations = div.getAnimations(); + + assert_equals_records(observer.takeRecords(), + [{ added: [], changed: animations, removed: []}], + "records after the order is changed"); + + div.style.animation = "anotherAnim 100s, anim 100s"; + + assert_equals_records(observer.takeRecords(), + [], "no records after applying the same order"); + }, "animtion_order_change"); + +}); + +test(t => { + var div = addDiv(t); + var observer = setupSynchronousObserver(t, div, true); + + var child = document.createElement("div"); + div.appendChild(child); + + var anim1 = div.animate({ marginLeft: [ "0px", "50px" ] }, + 100 * MS_PER_SEC); + var anim2 = child.animate({ marginLeft: [ "0px", "100px" ] }, + 50 * MS_PER_SEC); + assert_equals_records(observer.takeRecords(), + [{ added: [anim1], changed: [], removed: [] }, + { added: [anim2], changed: [], removed: [] }], + "records after animation is added"); + + // After setting a new effect, we remove the current animation, anim1, + // because it is no longer attached to |div|, and then remove the previous + // animation, anim2. Finally, add back the anim1 which is in effect on + // |child| now. In addition, we sort them by tree order and they are + // batched. + anim1.effect = anim2.effect; + assert_equals_records(observer.takeRecords(), + [{ added: [], changed: [], removed: [anim1] }, // div + { added: [anim1], changed: [], removed: [anim2] }], // child + "records after animation effects are changed"); +}, "set_effect_with_previous_animation"); + +test(t => { + var div = addDiv(t); + var observer = setupSynchronousObserver(t, document, true); + + var anim = div.animate({ opacity: [ 0, 1 ] }, + { duration: 100 * MS_PER_SEC }); + + var newTarget = document.createElement("div"); + + assert_equals_records(observer.takeRecords(), + [{ added: [anim], changed: [], removed: [] }], + "records after animation is added"); + + anim.effect.target = null; + assert_equals_records(observer.takeRecords(), + [{ added: [], changed: [], removed: [anim] }], + "records after setting null"); + + anim.effect.target = div; + assert_equals_records(observer.takeRecords(), + [{ added: [anim], changed: [], removed: [] }], + "records after setting a target"); + + anim.effect.target = addDiv(t); + assert_equals_records(observer.takeRecords(), + [{ added: [], changed: [], removed: [anim] }, + { added: [anim], changed: [], removed: [] }], + "records after setting a different target"); +}, "set_animation_target"); + +test(t => { + var div = addDiv(t); + var pseudoTarget = createPseudo(t, div, 'before'); + var observer = setupSynchronousObserver(t, div, true); + + var anim = pseudoTarget.animate({ opacity: [ 0, 1 ] }, 200 * MS_PER_SEC); + + assert_equals_records(observer.takeRecords(), + [{ added: [anim], changed: [], removed: [] }], + "records after animation is added"); + + anim.effect.timing.duration = 100 * MS_PER_SEC; + assert_equals_records(observer.takeRecords(), + [{ added: [], changed: [anim], removed: [] }], + "records after duration is changed"); + + anim.effect.timing.duration = 100 * MS_PER_SEC; + assert_equals_records(observer.takeRecords(), + [], "records after assigning same value"); + + anim.currentTime = anim.effect.timing.duration * 2; + anim.finish(); + assert_equals_records(observer.takeRecords(), + [{ added: [], changed: [], removed: [anim] }], + "records after animation end"); + + anim.effect.timing.duration = anim.effect.timing.duration * 3; + assert_equals_records(observer.takeRecords(), + [{ added: [anim], changed: [], removed: [] }], + "records after animation restarted"); + + anim.effect.timing.duration = "auto"; + assert_equals_records(observer.takeRecords(), + [{ added: [], changed: [], removed: [anim] }], + "records after duration set \"auto\""); + + anim.effect.timing.duration = "auto"; + assert_equals_records(observer.takeRecords(), + [], "records after assigning same value \"auto\""); +}, "change_duration_and_currenttime_on_pseudo_elements"); + +test(t => { + var div = addDiv(t); + var pseudoTarget = createPseudo(t, div, 'before'); + var observer = setupSynchronousObserver(t, div, false); + + var anim = div.animate({ opacity: [ 0, 1 ] }, + { duration: 100 * MS_PER_SEC }); + var pAnim = pseudoTarget.animate({ opacity: [ 0, 1 ] }, + { duration: 100 * MS_PER_SEC }); + + assert_equals_records(observer.takeRecords(), + [{ added: [anim], changed: [], removed: [] }], + "records after animation is added"); + + anim.finish(); + pAnim.finish(); + + assert_equals_records(observer.takeRecords(), + [{ added: [], changed: [], removed: [anim] }], + "records after animation is finished"); +}, "exclude_animations_targeting_pseudo_elements"); + +</script> diff --git a/dom/animation/test/chrome/test_restyles.html b/dom/animation/test/chrome/test_restyles.html new file mode 100644 index 0000000000..e59967c19d --- /dev/null +++ b/dom/animation/test/chrome/test_restyles.html @@ -0,0 +1,815 @@ +<!doctype html> +<head> +<meta charset=utf-8> +<title>Tests restyles caused by animations</title> +<script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> +<script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> +<script src="chrome://mochikit/content/tests/SimpleTest/SpawnTask.js"></script> +<script src="chrome://mochikit/content/tests/SimpleTest/paint_listener.js"></script> +<script src="../testcommon.js"></script> +<link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"> +<style> +@keyframes opacity { + from { opacity: 1; } + to { opacity: 0; } +} +@keyframes background-color { + from { background-color: red; } + to { background-color: blue; } +} +@keyframes rotate { + from { transform: rotate(0deg); } + to { transform: rotate(360deg); } +} +div { + /* Element needs geometry to be eligible for layerization */ + width: 100px; + height: 100px; + background-color: white; +} +</style> +</head> +<body> +<script> +'use strict'; + +function observeStyling(frameCount, onFrame) { + var docShell = window.QueryInterface(SpecialPowers.Ci.nsIInterfaceRequestor) + .getInterface(SpecialPowers.Ci.nsIWebNavigation) + .QueryInterface(SpecialPowers.Ci.nsIDocShell); + + docShell.recordProfileTimelineMarkers = true; + docShell.popProfileTimelineMarkers(); + + return new Promise(function(resolve) { + return waitForAnimationFrames(frameCount, onFrame).then(function() { + var markers = docShell.popProfileTimelineMarkers(); + docShell.recordProfileTimelineMarkers = false; + var stylingMarkers = markers.filter(function(marker, index) { + return marker.name == 'Styles' && + (marker.restyleHint == 'eRestyle_CSSAnimations' || + marker.restyleHint == 'eRestyle_CSSTransitions'); + }); + resolve(stylingMarkers); + }); + }); +} + +function ensureElementRemoval(aElement) { + return new Promise(function(resolve) { + aElement.remove(); + waitForAllPaintsFlushed(resolve); + }); +} + +SimpleTest.waitForExplicitFinish(); + +var omtaEnabled = isOMTAEnabled(); + +var isAndroid = !!navigator.userAgent.includes("Android"); + +function add_task_if_omta_enabled(test) { + if (!omtaEnabled) { + info(test.name + " is skipped because OMTA is disabled"); + return; + } + add_task(test); +} + +// We need to wait for all paints before running tests to avoid contaminations +// from styling of this document itself. +waitForAllPaints(function() { + add_task(function* restyling_for_main_thread_animations() { + var div = addDiv(null, { style: 'animation: background-color 100s' }); + var animation = div.getAnimations()[0]; + + yield animation.ready; + ok(!animation.isRunningOnCompositor); + + var markers = yield observeStyling(5); + is(markers.length, 5, + 'CSS animations running on the main-thread should update style ' + + 'on the main thread'); + yield ensureElementRemoval(div); + }); + + add_task_if_omta_enabled(function* no_restyling_for_compositor_animations() { + var div = addDiv(null, { style: 'animation: opacity 100s' }); + var animation = div.getAnimations()[0]; + + yield animation.ready; + ok(animation.isRunningOnCompositor); + + var markers = yield observeStyling(5); + is(markers.length, 0, + 'CSS animations running on the compositor should not update style ' + + 'on the main thread'); + yield ensureElementRemoval(div); + }); + + add_task_if_omta_enabled(function* no_restyling_for_compositor_transitions() { + var div = addDiv(null, { style: 'transition: opacity 100s; opacity: 0' }); + getComputedStyle(div).opacity; + div.style.opacity = 1; + + var animation = div.getAnimations()[0]; + + yield animation.ready; + ok(animation.isRunningOnCompositor); + + var markers = yield observeStyling(5); + is(markers.length, 0, + 'CSS transitions running on the compositor should not update style ' + + 'on the main thread'); + yield ensureElementRemoval(div); + }); + + add_task_if_omta_enabled(function* no_restyling_when_animation_duration_is_changed() { + var div = addDiv(null, { style: 'animation: opacity 100s' }); + var animation = div.getAnimations()[0]; + + yield animation.ready; + ok(animation.isRunningOnCompositor); + + div.animationDuration = '200s'; + + var markers = yield observeStyling(5); + is(markers.length, 0, + 'Animations running on the compositor should not update style ' + + 'on the main thread'); + yield ensureElementRemoval(div); + }); + + add_task_if_omta_enabled(function* only_one_restyling_after_finish_is_called() { + var div = addDiv(null, { style: 'animation: opacity 100s' }); + var animation = div.getAnimations()[0]; + + yield animation.ready; + ok(animation.isRunningOnCompositor); + + animation.finish(); + + var markers = yield observeStyling(5); + is(markers.length, 1, + 'Animations running on the compositor should only update style ' + + 'once after finish() is called'); + yield ensureElementRemoval(div); + }); + + add_task(function* no_restyling_mouse_movement_on_finished_transition() { + var div = addDiv(null, { style: 'transition: opacity 1ms; opacity: 0' }); + getComputedStyle(div).opacity; + div.style.opacity = 1; + + var animation = div.getAnimations()[0]; + var initialRect = div.getBoundingClientRect(); + + yield animation.finished; + + var mouseX = initialRect.left + initialRect.width / 2; + var mouseY = initialRect.top + initialRect.height / 2; + var markers = yield observeStyling(5, function() { + // We can't use synthesizeMouse here since synthesizeMouse causes + // layout flush. + synthesizeMouseAtPoint(mouseX++, mouseY++, + { type: 'mousemove' }, window); + }); + + is(markers.length, 0, + 'Bug 1219236: Finished transitions should never cause restyles ' + + 'when mouse is moved on the animations'); + yield ensureElementRemoval(div); + }); + + add_task(function* no_restyling_mouse_movement_on_finished_animation() { + var div = addDiv(null, { style: 'animation: opacity 1ms' }); + var animation = div.getAnimations()[0]; + + var initialRect = div.getBoundingClientRect(); + + yield animation.finished; + + var mouseX = initialRect.left + initialRect.width / 2; + var mouseY = initialRect.top + initialRect.height / 2; + var markers = yield observeStyling(5, function() { + // We can't use synthesizeMouse here since synthesizeMouse causes + // layout flush. + synthesizeMouseAtPoint(mouseX++, mouseY++, + { type: 'mousemove' }, window); + }); + + is(markers.length, 0, + 'Bug 1219236: Finished animations should never cause restyles ' + + 'when mouse is moved on the animations'); + yield ensureElementRemoval(div); + }); + + add_task_if_omta_enabled(function* no_restyling_compositor_animations_out_of_view_element() { + if (!SpecialPowers.getBoolPref('dom.animations.offscreen-throttling')) { + return; + } + + var div = addDiv(null, + { style: 'animation: opacity 100s; transform: translateY(-400px);' }); + var animation = div.getAnimations()[0]; + + yield animation.ready; + ok(!animation.isRunningOnCompositor); + + var markers = yield observeStyling(5); + + is(markers.length, 0, + 'Animations running on the compositor in an out-of-view element ' + + 'should never cause restyles'); + yield ensureElementRemoval(div); + }); + + add_task(function* no_restyling_main_thread_animations_out_of_view_element() { + if (!SpecialPowers.getBoolPref('dom.animations.offscreen-throttling')) { + return; + } + + var div = addDiv(null, + { style: 'animation: background-color 100s; transform: translateY(-400px);' }); + var animation = div.getAnimations()[0]; + + yield animation.ready; + var markers = yield observeStyling(5); + + is(markers.length, 0, + 'Animations running on the main-thread in an out-of-view element ' + + 'should never cause restyles'); + yield ensureElementRemoval(div); + }); + + add_task_if_omta_enabled(function* no_restyling_compositor_animations_in_scrolled_out_element() { + if (!SpecialPowers.getBoolPref('dom.animations.offscreen-throttling')) { + return; + } + + /* + On Android the opacity animation runs on the compositor even if it is + scrolled out of view. We will fix this in bug 1247800. + */ + if (isAndroid) { + return; + } + var parentElement = addDiv(null, + { style: 'overflow-y: scroll; height: 20px;' }); + var div = addDiv(null, + { style: 'animation: opacity 100s; position: relative; top: 100px;' }); + parentElement.appendChild(div); + var animation = div.getAnimations()[0]; + + yield animation.ready; + + var markers = yield observeStyling(5); + + is(markers.length, 0, + 'Animations running on the compositor for elements ' + + 'which are scrolled out should never cause restyles'); + + yield ensureElementRemoval(parentElement); + }); + + add_task(function* no_restyling_main_thread_animations_in_scrolled_out_element() { + if (!SpecialPowers.getBoolPref('dom.animations.offscreen-throttling')) { + return; + } + + /* + On Android throttled animations are left behind on the main thread in some + frames, We will fix this in bug 1247800. + */ + if (isAndroid) { + return; + } + + var parentElement = addDiv(null, + { style: 'overflow-y: scroll; height: 20px;' }); + var div = addDiv(null, + { style: 'animation: background-color 100s; position: relative; top: 100px;' }); + parentElement.appendChild(div); + var animation = div.getAnimations()[0]; + + yield animation.ready; + var markers = yield observeStyling(5); + + is(markers.length, 0, + 'Animations running on the main-thread for elements ' + + 'which are scrolled out should never cause restyles'); + + yield ensureElementRemoval(parentElement); + }); + + add_task(function* no_restyling_main_thread_animations_in_nested_scrolled_out_element() { + if (!SpecialPowers.getBoolPref('dom.animations.offscreen-throttling')) { + return; + } + + /* + On Android throttled animations are left behind on the main thread in some + frames, We will fix this in bug 1247800. + */ + if (isAndroid) { + return; + } + + var grandParent = addDiv(null, + { style: 'overflow-y: scroll; height: 20px;' }); + var parentElement = addDiv(null, + { style: 'overflow-y: scroll; height: 100px;' }); + var div = addDiv(null, + { style: 'animation: background-color 100s; position: relative; top: 100px;' }); + grandParent.appendChild(parentElement); + parentElement.appendChild(div); + var animation = div.getAnimations()[0]; + + yield animation.ready; + var markers = yield observeStyling(5); + + is(markers.length, 0, + 'Animations running on the main-thread which are in nested elements ' + + 'which are scrolled out should never cause restyles'); + + yield ensureElementRemoval(grandParent); + }); + + add_task_if_omta_enabled(function* no_restyling_compositor_animations_in_visiblily_hidden_element() { + var div = addDiv(null, + { style: 'animation: opacity 100s; visibility: hidden' }); + var animation = div.getAnimations()[0]; + + yield animation.ready; + ok(!animation.isRunningOnCompositor); + + var markers = yield observeStyling(5); + + todo_is(markers.length, 0, + 'Bug 1237454: Animations running on the compositor in ' + + 'visibility hidden element should never cause restyles'); + yield ensureElementRemoval(div); + }); + + add_task(function* restyling_main_thread_animations_moved_in_view_by_scrolling() { + if (!SpecialPowers.getBoolPref('dom.animations.offscreen-throttling')) { + return; + } + + /* + On Android throttled animations are left behind on the main thread in some + frames, We will fix this in bug 1247800. + */ + if (isAndroid) { + return; + } + + var parentElement = addDiv(null, + { style: 'overflow-y: scroll; height: 20px;' }); + var div = addDiv(null, + { style: 'animation: background-color 100s; position: relative; top: 100px;' }); + parentElement.appendChild(div); + var animation = div.getAnimations()[0]; + + var parentRect = parentElement.getBoundingClientRect(); + var centerX = parentRect.left + parentRect.width / 2; + var centerY = parentRect.top + parentRect.height / 2; + + yield animation.ready; + + var markers = yield observeStyling(1, function() { + // We can't use synthesizeWheel here since synthesizeWheel causes + // layout flush. + synthesizeWheelAtPoint(centerX, centerY, + { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaY: 100 }); + }); + + is(markers.length, 1, + 'Animations running on the main-thread which were in scrolled out ' + + 'elements should update restyling soon after the element moved in ' + + 'view by scrolling'); + + yield ensureElementRemoval(parentElement); + }); + + add_task(function* restyling_main_thread_animations_moved_in_view_by_scrolling() { + if (!SpecialPowers.getBoolPref('dom.animations.offscreen-throttling')) { + return; + } + + var grandParent = addDiv(null, + { style: 'overflow-y: scroll; height: 20px;' }); + var parentElement = addDiv(null, + { style: 'overflow-y: scroll; height: 200px;' }); + var div = addDiv(null, + { style: 'animation: background-color 100s; position: relative; top: 100px;' }); + grandParent.appendChild(parentElement); + parentElement.appendChild(div); + var animation = div.getAnimations()[0]; + + var parentRect = grandParent.getBoundingClientRect(); + var centerX = parentRect.left + parentRect.width / 2; + var centerY = parentRect.top + parentRect.height / 2; + + yield animation.ready; + + var markers = yield observeStyling(1, function() { + // We can't use synthesizeWheel here since synthesizeWheel causes + // layout flush. + synthesizeWheelAtPoint(centerX, centerY, + { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaY: 100 }); + }); + + // FIXME: We should reduce a redundant restyle here. + ok(markers.length >= 1, + 'Animations running on the main-thread which were in nested scrolled ' + + 'out elements should update restyle soon after the element moved ' + + 'in view by scrolling'); + + yield ensureElementRemoval(grandParent); + }); + + add_task(function* restyling_main_thread_animations_move_out_of_view_by_scrolling() { + if (!SpecialPowers.getBoolPref('dom.animations.offscreen-throttling')) { + return; + } + + /* + On Android throttled animations are left behind on the main thread in some + frames, We will fix this in bug 1247800. + */ + if (isAndroid) { + return; + } + + var parentElement = addDiv(null, + { style: 'overflow-y: scroll; height: 200px;' }); + var div = addDiv(null, + { style: 'animation: background-color 100s;' }); + var pad = addDiv(null, + { style: 'height: 400px;' }); + parentElement.appendChild(div); + parentElement.appendChild(pad); + var animation = div.getAnimations()[0]; + + var parentRect = parentElement.getBoundingClientRect(); + var centerX = parentRect.left + parentRect.width / 2; + var centerY = parentRect.top + parentRect.height / 2; + + yield animation.ready; + + // We can't use synthesizeWheel here since synthesizeWheel causes + // layout flush. + synthesizeWheelAtPoint(centerX, centerY, + { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaY: 200 }); + + var markers = yield observeStyling(5); + + // FIXME: We should reduce a redundant restyle here. + ok(markers.length >= 0, + 'Animations running on the main-thread which are in scrolled out ' + + 'elements should throttle restyling'); + + yield ensureElementRemoval(parentElement); + }); + + add_task(function* restyling_main_thread_animations_moved_in_view_by_resizing() { + if (!SpecialPowers.getBoolPref('dom.animations.offscreen-throttling')) { + return; + } + + var parentElement = addDiv(null, + { style: 'overflow-y: scroll; height: 20px;' }); + var div = addDiv(null, + { style: 'animation: background-color 100s; position: relative; top: 100px;' }); + parentElement.appendChild(div); + var animation = div.getAnimations()[0]; + + yield animation.ready; + + var markers = yield observeStyling(1, function() { + parentElement.style.height = '100px'; + }); + + is(markers.length, 1, + 'Animations running on the main-thread which was in scrolled out ' + + 'elements should update restyling soon after the element moved in ' + + 'view by resizing'); + + yield ensureElementRemoval(parentElement); + }); + + add_task(function* no_restyling_main_thread_animations_in_visiblily_hidden_element() { + var div = addDiv(null, + { style: 'animation: background-color 100s; visibility: hidden' }); + var animation = div.getAnimations()[0]; + + yield animation.ready; + var markers = yield observeStyling(5); + + todo_is(markers.length, 0, + 'Bug 1237454: Animations running on the main-thread in ' + + 'visibility hidden element should never cause restyles'); + yield ensureElementRemoval(div); + }); + + add_task_if_omta_enabled(function* no_restyling_compositor_animations_after_pause_is_called() { + var div = addDiv(null, { style: 'animation: opacity 100s' }); + var animation = div.getAnimations()[0]; + + yield animation.ready; + ok(animation.isRunningOnCompositor); + + animation.pause(); + + yield animation.ready; + + var markers = yield observeStyling(5); + is(markers.length, 0, + 'Bug 1232563: Paused animations running on the compositor should ' + + 'never cause restyles once after pause() is called'); + yield ensureElementRemoval(div); + }); + + add_task(function* no_restyling_main_thread_animations_after_pause_is_called() { + var div = addDiv(null, { style: 'animation: background-color 100s' }); + var animation = div.getAnimations()[0]; + + yield animation.ready; + + animation.pause(); + + yield animation.ready; + + var markers = yield observeStyling(5); + is(markers.length, 0, + 'Bug 1232563: Paused animations running on the main-thread should ' + + 'never cause restyles after pause() is called'); + yield ensureElementRemoval(div); + }); + + add_task_if_omta_enabled(function* only_one_restyling_when_current_time_is_set_to_middle_of_duration() { + var div = addDiv(null, { style: 'animation: opacity 100s' }); + var animation = div.getAnimations()[0]; + + yield animation.ready; + + animation.currentTime = 50 * MS_PER_SEC; + + var markers = yield observeStyling(5); + is(markers.length, 1, + 'Bug 1235478: Animations running on the compositor should only once ' + + 'update style when currentTime is set to middle of duration time'); + yield ensureElementRemoval(div); + }); + + add_task_if_omta_enabled(function* change_duration_and_currenttime() { + var div = addDiv(null); + var animation = div.animate({ opacity: [ 0, 1 ] }, 100 * MS_PER_SEC); + + yield animation.ready; + ok(animation.isRunningOnCompositor); + + // Set currentTime to a time longer than duration. + animation.currentTime = 500 * MS_PER_SEC; + + // Now the animation immediately get back from compositor. + ok(!animation.isRunningOnCompositor); + + // Extend the duration. + animation.effect.timing.duration = 800 * MS_PER_SEC; + var markers = yield observeStyling(5); + is(markers.length, 1, + 'Animations running on the compositor should update style ' + + 'when timing.duration is made longer than the current time'); + + yield ensureElementRemoval(div); + }); + + add_task(function* script_animation_on_display_none_element() { + var div = addDiv(null); + var animation = div.animate({ backgroundColor: [ 'red', 'blue' ] }, + 100 * MS_PER_SEC); + + yield animation.ready; + + div.style.display = 'none'; + + // We need to wait a frame to apply display:none style. + yield waitForFrame(); + + is(animation.playState, 'running', + 'Script animations keep running even when the target element has ' + + '"display: none" style'); + + ok(!animation.isRunningOnCompositor, + 'Script animations on "display:none" element should not run on the ' + + 'compositor'); + + var markers = yield observeStyling(5); + is(markers.length, 0, + 'Script animations on "display: none" element should not update styles'); + + div.style.display = ''; + + // We need to wait a frame to unapply display:none style. + yield waitForFrame(); + + var markers = yield observeStyling(5); + is(markers.length, 5, + 'Script animations restored from "display: none" state should update ' + + 'styles'); + + yield ensureElementRemoval(div); + }); + + add_task_if_omta_enabled(function* compositable_script_animation_on_display_none_element() { + var div = addDiv(null); + var animation = div.animate({ opacity: [ 0, 1 ] }, 100 * MS_PER_SEC); + + yield animation.ready; + + div.style.display = 'none'; + + // We need to wait a frame to apply display:none style. + yield waitForFrame(); + + is(animation.playState, 'running', + 'Opacity script animations keep running even when the target element ' + + 'has "display: none" style'); + + ok(!animation.isRunningOnCompositor, + 'Opacity script animations on "display:none" element should not ' + + 'run on the compositor'); + + var markers = yield observeStyling(5); + is(markers.length, 0, + 'Opacity script animations on "display: none" element should not ' + + 'update styles'); + + div.style.display = ''; + + // We need to wait a frame to unapply display:none style. + yield waitForFrame(); + + ok(animation.isRunningOnCompositor, + 'Opacity script animations restored from "display: none" should be ' + + 'run on the compositor'); + + yield ensureElementRemoval(div); + }); + + add_task(function* restyling_for_empty_keyframes() { + var div = addDiv(null); + var animation = div.animate({ }, 100 * MS_PER_SEC); + + yield animation.ready; + var markers = yield observeStyling(5); + + is(markers.length, 0, + 'Animations with no keyframes should not cause restyles'); + + animation.effect.setKeyframes({ backgroundColor: ['red', 'blue'] }); + markers = yield observeStyling(5); + + is(markers.length, 5, + 'Setting valid keyframes should cause regular animation restyles to ' + + 'occur'); + + animation.effect.setKeyframes({ }); + markers = yield observeStyling(5); + + is(markers.length, 1, + 'Setting an empty set of keyframes should trigger a single restyle ' + + 'to remove the previous animated style'); + + yield ensureElementRemoval(div); + }); + + add_task_if_omta_enabled(function* no_restyling_when_animation_style_when_re_setting_same_animation_property() { + var div = addDiv(null, { style: 'animation: opacity 100s' }); + var animation = div.getAnimations()[0]; + yield animation.ready; + ok(animation.isRunningOnCompositor); + // Apply the same animation style + div.style.animation = 'opacity 100s'; + var markers = yield observeStyling(5); + is(markers.length, 0, + 'Applying same animation style ' + + 'should never cause restyles'); + yield ensureElementRemoval(div); + }); + + add_task(function* necessary_update_should_be_invoked() { + var div = addDiv(null, { style: 'animation: background-color 100s' }); + var animation = div.getAnimations()[0]; + yield animation.ready; + yield waitForAnimationFrames(5); + // Apply another animation style + div.style.animation = 'background-color 110s'; + var animation = div.getAnimations()[0]; + var markers = yield observeStyling(5); + is(markers.length, 5, + 'Applying animation style with different duration ' + + 'should cause restyles on every frame.'); + yield ensureElementRemoval(div); + }); + + add_task_if_omta_enabled( + function* changing_cascading_result_for_main_thread_animation() { + var div = addDiv(null, { style: 'background-color: blue' }); + var animation = div.animate({ opacity: [0, 1], + backgroundColor: ['green', 'red'] }, + 100 * MS_PER_SEC); + yield animation.ready; + ok(animation.isRunningOnCompositor, + 'The opacity animation is running on the compositor'); + // Make the background-color style as !important to cause an update + // to the cascade. + // Bug 1300982: The background-color animation should be no longer + // running on the main thread. + div.style.setProperty('background-color', '1', 'important'); + var markers = yield observeStyling(5); + todo_is(markers.length, 0, + 'Changing cascading result for the property running on the main ' + + 'thread does not cause synchronization layer of opacity animation ' + + 'running on the compositor'); + yield ensureElementRemoval(div); + } + ); + + add_task(function* restyling_for_animation_on_orphaned_element() { + var div = addDiv(null); + var animation = div.animate({ marginLeft: [ '0px', '100px' ] }, + 100 * MS_PER_SEC); + + yield animation.ready; + + div.remove(); + + var markers = yield observeStyling(5); + is(markers.length, 0, + 'Animation on orphaned element should not cause restyles'); + + document.body.appendChild(div); + + markers = yield observeStyling(1); + // We are observing restyles in rAF callback which is processed before + // restyling process in each frame, so in the first frame there should be + // no observed restyle since we don't process restyle while the element + // is not attached to the document. + is(markers.length, 0, + 'We observe no restyle in the first frame right after re-atatching ' + + 'to the document'); + markers = yield observeStyling(5); + is(markers.length, 5, + 'Animation on re-attached to the document begins to update style'); + + yield ensureElementRemoval(div); + }); + + add_task_if_omta_enabled( + // Tests that if we remove an element from the document whose animation + // cascade needs recalculating, that it is correctly updated when it is + // re-attached to the document. + function* restyling_for_opacity_animation_on_re_attached_element() { + var div = addDiv(null, { style: 'opacity: 1 ! important' }); + var animation = div.animate({ opacity: [0, 1] }, 100 * MS_PER_SEC); + + yield animation.ready; + ok(!animation.isRunningOnCompositor, + 'The opacity animation overridden by an !important rule is NOT ' + + 'running on the compositor'); + + // Drop the !important rule to update the cascade. + div.style.setProperty('opacity', '1', ''); + + div.remove(); + + var markers = yield observeStyling(5); + is(markers.length, 0, + 'Opacity animation on orphaned element should not cause restyles'); + + document.body.appendChild(div); + + // Need a frame to give the animation a chance to be sent to the + // compositor. + yield waitForFrame(); + + ok(animation.isRunningOnCompositor, + 'The opacity animation which is no longer overridden by the ' + + '!important rule begins running on the compositor even if the ' + + '!important rule had been dropped before the target element was ' + + 'removed'); + + yield ensureElementRemoval(div); + } + ); + +}); + +</script> +</body> diff --git a/dom/animation/test/chrome/test_running_on_compositor.html b/dom/animation/test/chrome/test_running_on_compositor.html new file mode 100644 index 0000000000..cd6c679b8e --- /dev/null +++ b/dom/animation/test/chrome/test_running_on_compositor.html @@ -0,0 +1,966 @@ +<!doctype html> +<head> +<meta charset=utf-8> +<title>Bug 1045994 - Add a chrome-only property to inspect if an animation is + running on the compositor or not</title> +<script type="application/javascript" src="../testharness.js"></script> +<script type="application/javascript" src="../testharnessreport.js"></script> +<script type="application/javascript" src="../testcommon.js"></script> +<style> +@keyframes anim { + to { transform: translate(100px) } +} +@keyframes transform-starts-with-none { + 0% { transform: none } + 99% { transform: none } + 100% { transform: translate(100px) } +} +@keyframes opacity { + to { opacity: 0 } +} +@keyframes background_and_translate { + to { background-color: red; transform: translate(100px); } +} +@keyframes background { + to { background-color: red; } +} +@keyframes rotate { + from { transform: rotate(0deg); } + to { transform: rotate(360deg); } +} +@keyframes rotate-and-opacity { + from { transform: rotate(0deg); opacity: 1;} + to { transform: rotate(360deg); opacity: 0;} +} +div { + /* Element needs geometry to be eligible for layerization */ + width: 100px; + height: 100px; + background-color: white; +} +</style> +</head> +<body> +<a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1045994" + target="_blank">Mozilla Bug 1045994</a> +<div id="log"></div> +<script> +'use strict'; + +/** Test for bug 1045994 - Add a chrome-only property to inspect if an + animation is running on the compositor or not **/ + +var omtaEnabled = isOMTAEnabled(); + +function assert_animation_is_running_on_compositor(animation, desc) { + assert_equals(animation.isRunningOnCompositor, omtaEnabled, + desc + ' at ' + animation.currentTime + 'ms'); +} + +function assert_animation_is_not_running_on_compositor(animation, desc) { + assert_equals(animation.isRunningOnCompositor, false, + desc + ' at ' + animation.currentTime + 'ms'); +} + +promise_test(function(t) { + // FIXME: When we implement Element.animate, use that here instead of CSS + // so that we remove any dependency on the CSS mapping. + var div = addDiv(t, { style: 'animation: anim 100s' }); + var animation = div.getAnimations()[0]; + + return animation.ready.then(function() { + assert_animation_is_running_on_compositor(animation, + 'Animation reports that it is running on the compositor' + + ' during playback'); + + div.style.animationPlayState = 'paused'; + + return animation.ready; + }).then(function() { + assert_animation_is_not_running_on_compositor(animation, + 'Animation reports that it is NOT running on the compositor' + + ' when paused'); + }); +}, ''); + +promise_test(function(t) { + var div = addDiv(t, { style: 'animation: background 100s' }); + var animation = div.getAnimations()[0]; + + return animation.ready.then(function() { + assert_animation_is_not_running_on_compositor(animation, + 'Animation reports that it is NOT running on the compositor' + + ' for animation of "background"'); + }); +}, 'isRunningOnCompositor is false for animation of "background"'); + +promise_test(function(t) { + var div = addDiv(t, { style: 'animation: background_and_translate 100s' }); + var animation = div.getAnimations()[0]; + + return animation.ready.then(function() { + assert_animation_is_running_on_compositor(animation, + 'Animation reports that it is running on the compositor' + + ' when the animation has two properties, where one can run' + + ' on the compositor, the other cannot'); + }); +}, 'isRunningOnCompositor is true if the animation has at least one ' + + 'property can run on compositor'); + +promise_test(function(t) { + var div = addDiv(t, { style: 'animation: anim 100s' }); + var animation = div.getAnimations()[0]; + + return animation.ready.then(function() { + animation.pause(); + return animation.ready; + }).then(function() { + assert_animation_is_not_running_on_compositor(animation, + 'Animation reports that it is NOT running on the compositor' + + ' when animation.pause() is called'); + }); +}, 'isRunningOnCompositor is false when the animation.pause() is called'); + +promise_test(function(t) { + var div = addDiv(t, { style: 'animation: anim 100s' }); + var animation = div.getAnimations()[0]; + + return animation.ready.then(function() { + animation.finish(); + assert_animation_is_not_running_on_compositor(animation, + 'Animation reports that it is NOT running on the compositor' + + ' immediately after animation.finish() is called'); + // Check that we don't set the flag back again on the next tick. + return waitForFrame(); + }).then(function() { + assert_animation_is_not_running_on_compositor(animation, + 'Animation reports that it is NOT running on the compositor' + + ' on the next tick after animation.finish() is called'); + }); +}, 'isRunningOnCompositor is false when the animation.finish() is called'); + +promise_test(function(t) { + var div = addDiv(t, { style: 'animation: anim 100s' }); + var animation = div.getAnimations()[0]; + + return animation.ready.then(function() { + animation.currentTime = 100 * MS_PER_SEC; + assert_animation_is_not_running_on_compositor(animation, + 'Animation reports that it is NOT running on the compositor' + + ' immediately after manually seeking the animation to the end'); + // Check that we don't set the flag back again on the next tick. + return waitForFrame(); + }).then(function() { + assert_animation_is_not_running_on_compositor(animation, + 'Animation reports that it is NOT running on the compositor' + + ' on the next tick after manually seeking the animation to the end'); + }); +}, 'isRunningOnCompositor is false when manually seeking the animation to ' + + 'the end'); + +promise_test(function(t) { + var div = addDiv(t, { style: 'animation: anim 100s' }); + var animation = div.getAnimations()[0]; + + return animation.ready.then(function() { + animation.cancel(); + assert_animation_is_not_running_on_compositor(animation, + 'Animation reports that it is NOT running on the compositor' + + ' immediately after animation.cancel() is called'); + // Check that we don't set the flag back again on the next tick. + return waitForFrame(); + }).then(function() { + assert_animation_is_not_running_on_compositor(animation, + 'Animation reports that it is NOT running on the compositor' + + ' on the next tick after animation.cancel() is called'); + }); +}, 'isRunningOnCompositor is false when animation.cancel() is called'); + +// This is to test that we don't simply clobber the flag when ticking +// animations and then set it again during painting. +promise_test(function(t) { + var div = addDiv(t, { style: 'animation: anim 100s' }); + var animation = div.getAnimations()[0]; + + return animation.ready.then(function() { + return new Promise(function(resolve) { + window.requestAnimationFrame(function() { + t.step(function() { + assert_animation_is_running_on_compositor(animation, + 'Animation reports that it is running on the compositor' + + ' in requestAnimationFrame callback'); + }); + + resolve(); + }); + }); + }); +}, 'isRunningOnCompositor is true in requestAnimationFrame callback'); + +promise_test(function(t) { + var div = addDiv(t, { style: 'animation: anim 100s' }); + var animation = div.getAnimations()[0]; + + return animation.ready.then(function() { + return new Promise(function(resolve) { + var observer = new MutationObserver(function(records) { + var changedAnimation; + + records.forEach(function(record) { + changedAnimation = + record.changedAnimations.find(function(changedAnim) { + return changedAnim == animation; + }); + }); + + t.step(function() { + assert_true(!!changedAnimation, 'The animation should be recorded ' + + 'as one of the changedAnimations'); + + assert_animation_is_running_on_compositor(animation, + 'Animation reports that it is running on the compositor' + + ' in MutationObserver callback'); + }); + + resolve(); + }); + observer.observe(div, { animations: true, subtree: false }); + t.add_cleanup(function() { + observer.disconnect(); + }); + div.style.animationDuration = "200s"; + }); + }); +}, 'isRunningOnCompositor is true in MutationObserver callback'); + +// This is to test that we don't temporarily clear the flag when forcing +// an unthrottled sample. +promise_test(function(t) { + return new Promise(function(resolve) { + // Needs scrollbars to cause overflow. + SpecialPowers.pushPrefEnv({ set: [["ui.showHideScrollbars", 1]] }, + resolve); + }).then(function() { + var div = addDiv(t, { style: 'animation: rotate 100s' }); + var animation = div.getAnimations()[0]; + + return animation.ready.then(function() { + return new Promise(function(resolve) { + var timeAtStart = window.performance.now(); + function handleFrame() { + t.step(function() { + assert_animation_is_running_on_compositor(animation, + 'Animation reports that it is running on the compositor' + + ' in requestAnimationFrame callback'); + }); + + // we have to wait at least 200ms because this animation is + // unthrottled on every 200ms. + // See http://hg.mozilla.org/mozilla-central/file/cafb1c90f794/layout/style/AnimationCommon.cpp#l863 + if (window.performance.now() - timeAtStart > 200) { + resolve(); + return; + } + window.requestAnimationFrame(handleFrame); + } + window.requestAnimationFrame(handleFrame); + }); + }); + }); +}, 'isRunningOnCompositor remains true in requestAnimationFrameCallback for ' + + 'overflow animation'); + +promise_test(function(t) { + var div = addDiv(t, { style: 'transition: opacity 100s; opacity: 1' }); + + getComputedStyle(div).opacity; + + div.style.opacity = 0; + var animation = div.getAnimations()[0]; + + return animation.ready.then(function() { + assert_animation_is_running_on_compositor(animation, + 'Transition reports that it is running on the compositor' + + ' during playback for opacity transition'); + }); +}, 'isRunningOnCompositor for transitions'); + +promise_test(function(t) { + var div = addDiv(t, { style: 'animation: rotate-and-opacity 100s; ' + + 'backface-visibility: hidden; ' + + 'transform: none !important;' }); + var animation = div.getAnimations()[0]; + + return animation.ready.then(function() { + assert_animation_is_running_on_compositor(animation, + 'If an animation has a property that can run on the compositor and a ' + + 'property that cannot (due to Gecko limitations) but where the latter' + + 'property is overridden in the CSS cascade, the animation should ' + + 'still report that it is running on the compositor'); + }); +}, 'isRunningOnCompositor is true when a property that would otherwise block ' + + 'running on the compositor is overridden in the CSS cascade'); + +promise_test(function(t) { + var animation = addDivAndAnimate(t, + {}, + { opacity: [ 0, 1 ] }, 200 * MS_PER_SEC); + + return animation.ready.then(function() { + assert_animation_is_running_on_compositor(animation, + 'Animation reports that it is running on the compositor'); + + animation.currentTime = 150 * MS_PER_SEC; + animation.effect.timing.duration = 100 * MS_PER_SEC; + + assert_animation_is_not_running_on_compositor(animation, + 'Animation reports that it is NOT running on the compositor' + + ' when the animation is set a shorter duration than current time'); + }); +}, 'animation is immediately removed from compositor' + + 'when timing.duration is made shorter than the current time'); + +promise_test(function(t) { + var animation = addDivAndAnimate(t, + {}, + { opacity: [ 0, 1 ] }, 100 * MS_PER_SEC); + + return animation.ready.then(function() { + assert_animation_is_running_on_compositor(animation, + 'Animation reports that it is running on the compositor'); + + animation.currentTime = 500 * MS_PER_SEC; + + assert_animation_is_not_running_on_compositor(animation, + 'Animation reports that it is NOT running on the compositor' + + ' when finished'); + + animation.effect.timing.duration = 1000 * MS_PER_SEC; + return waitForFrame(); + }).then(function() { + assert_animation_is_running_on_compositor(animation, + 'Animation reports that it is running on the compositor' + + ' when restarted'); + }); +}, 'animation is added to compositor' + + ' when timing.duration is made longer than the current time'); + +promise_test(function(t) { + var animation = addDivAndAnimate(t, + {}, + { opacity: [ 0, 1 ] }, 100 * MS_PER_SEC); + + return animation.ready.then(function() { + assert_animation_is_running_on_compositor(animation, + 'Animation reports that it is running on the compositor'); + + animation.effect.timing.endDelay = 100 * MS_PER_SEC; + + assert_animation_is_running_on_compositor(animation, + 'Animation reports that it is running on the compositor' + + ' when endDelay is changed'); + + animation.currentTime = 110 * MS_PER_SEC; + return waitForFrame(); + }).then(function() { + assert_animation_is_not_running_on_compositor(animation, + 'Animation reports that it is NOT running on the compositor' + + ' when currentTime is during endDelay'); + }); +}, 'animation is removed from compositor' + + ' when current time is made longer than the duration even during endDelay'); + +promise_test(function(t) { + var animation = addDivAndAnimate(t, + {}, + { opacity: [ 0, 1 ] }, 100 * MS_PER_SEC); + + return animation.ready.then(function() { + assert_animation_is_running_on_compositor(animation, + 'Animation reports that it is running on the compositor'); + + animation.effect.timing.endDelay = -200 * MS_PER_SEC; + return waitForFrame(); + }).then(function() { + assert_animation_is_not_running_on_compositor(animation, + 'Animation reports that it is NOT running on the compositor' + + ' when endTime is negative value'); + }); +}, 'animation is removed from compositor' + + ' when endTime is negative value'); + +promise_test(function(t) { + var animation = addDivAndAnimate(t, + {}, + { opacity: [ 0, 1 ] }, 200 * MS_PER_SEC); + + return animation.ready.then(function() { + assert_animation_is_running_on_compositor(animation, + 'Animation reports that it is running on the compositor'); + + animation.effect.timing.endDelay = -100 * MS_PER_SEC; + return waitForFrame(); + }).then(function() { + assert_animation_is_running_on_compositor(animation, + 'Animation reports that it is running on the compositor' + + ' when endTime is positive and endDelay is negative'); + animation.currentTime = 110 * MS_PER_SEC; + return waitForFrame(); + }).then(function() { + assert_animation_is_not_running_on_compositor(animation, + 'Animation reports that it is NOT running on the compositor' + + ' when currentTime is after endTime'); + }); +}, 'animation is NOT running on compositor' + + ' when endTime is positive and endDelay is negative'); + +promise_test(function(t) { + var effect = new KeyframeEffect(null, + { opacity: [ 0, 1 ] }, + 100 * MS_PER_SEC); + var animation = new Animation(effect, document.timeline); + animation.play(); + + var div = addDiv(t); + + return animation.ready.then(function() { + assert_animation_is_not_running_on_compositor(animation, + 'Animation with null target reports that it is not running ' + + 'on the compositor'); + + animation.effect.target = div; + return waitForFrame(); + }).then(function() { + assert_animation_is_running_on_compositor(animation, + 'Animation reports that it is running on the compositor ' + + 'after setting a valid target'); + }); +}, 'animation is added to the compositor when setting a valid target'); + +promise_test(function(t) { + var div = addDiv(t); + var animation = div.animate({ opacity: [ 0, 1 ] }, 100 * MS_PER_SEC); + + return animation.ready.then(function() { + assert_animation_is_running_on_compositor(animation, + 'Animation reports that it is running on the compositor'); + + animation.effect.target = null; + assert_animation_is_not_running_on_compositor(animation, + 'Animation reports that it is NOT running on the ' + + 'compositor after setting null target'); + }); +}, 'animation is removed from the compositor when setting null target'); + +promise_test(function(t) { + var div = addDiv(t); + var animation = div.animate({ opacity: [ 0, 1 ] }, + { duration: 100 * MS_PER_SEC, + delay: 100 * MS_PER_SEC, + fill: 'backwards' }); + + return animation.ready.then(function() { + assert_animation_is_running_on_compositor(animation, + 'Animation with fill:backwards in delay phase reports ' + + 'that it is running on the compositor'); + + animation.currentTime = 100 * MS_PER_SEC; + return waitForFrame(); + }).then(function() { + assert_animation_is_running_on_compositor(animation, + 'Animation with fill:backwards in delay phase reports ' + + 'that it is running on the compositor after delay phase'); + }); +}, 'animation with fill:backwards in delay phase is running on the ' + + ' main-thread while it is in delay phase'); + +promise_test(function(t) { + var div = addDiv(t); + var animation = div.animate([{ opacity: 1, offset: 0 }, + { opacity: 1, offset: 0.99 }, + { opacity: 0, offset: 1 }], 100 * MS_PER_SEC); + + var another = addDiv(t); + + return animation.ready.then(function() { + assert_animation_is_running_on_compositor(animation, + 'Opacity animation on a 100% opacity keyframe reports ' + + 'that it is running on the compositor from the begining'); + + animation.effect.target = another; + return waitForFrame(); + }).then(function() { + assert_animation_is_running_on_compositor(animation, + 'Opacity animation on a 100% opacity keyframe keeps ' + + 'running on the compositor after changing the target ' + + 'element'); + }); +}, '100% opacity animations with keeps running on the ' + + 'compositor after changing the target element'); + +promise_test(function(t) { + var div = addDiv(t); + var animation = div.animate({ color: ['red', 'black'] }, 100 * MS_PER_SEC); + + return animation.ready.then(function() { + assert_animation_is_not_running_on_compositor(animation, + 'Color animation reports that it is not running on the ' + + 'compositor'); + + animation.effect.setKeyframes([{ opacity: 1, offset: 0 }, + { opacity: 1, offset: 0.99 }, + { opacity: 0, offset: 1 }]); + return waitForFrame(); + }).then(function() { + assert_animation_is_running_on_compositor(animation, + '100% opacity animation set by using setKeyframes reports ' + + 'that it is running on the compositor'); + }); +}, '100% opacity animation set up by converting an existing animation with ' + + 'cannot be run on the compositor, is running on the compositor'); + +promise_test(function(t) { + var div = addDiv(t); + var animation = div.animate({ color: ['red', 'black'] }, 100 * MS_PER_SEC); + var effect = new KeyframeEffect(div, + [{ opacity: 1, offset: 0 }, + { opacity: 1, offset: 0.99 }, + { opacity: 0, offset: 1 }], + 100 * MS_PER_SEC); + + return animation.ready.then(function() { + assert_animation_is_not_running_on_compositor(animation, + 'Color animation reports that it is not running on the ' + + 'compositor'); + + animation.effect = effect; + return waitForFrame(); + }).then(function() { + assert_animation_is_running_on_compositor(animation, + '100% opacity animation set up by changing effects reports ' + + 'that it is running on the compositor'); + }); +}, '100% opacity animation set up by changing the effects on an existing ' + + 'animation which cannot be run on the compositor, is running on the ' + + 'compositor'); + +promise_test(function(t) { + var div = addDiv(t, { style: "opacity: 1 ! important" }); + + var animation = div.animate({ opacity: [0, 1] }, 100 * MS_PER_SEC); + + return animation.ready.then(function() { + assert_animation_is_not_running_on_compositor(animation, + 'Opacity animation on an element which has 100% opacity style with ' + + '!important flag reports that it is not running on the compositor'); + // Clear important flag from the opacity style on the target element. + div.style.setProperty("opacity", "1", ""); + return waitForFrame(); + }).then(function() { + assert_animation_is_running_on_compositor(animation, + 'Opacity animation reports that it is running on the compositor after ' + + 'clearing the !important flag'); + }); +}, 'Clearing *important* opacity style on the target element sends the ' + + 'animation to the compositor'); + +promise_test(function(t) { + var div = addDiv(t); + var lowerAnimation = div.animate({ opacity: [1, 0] }, 100 * MS_PER_SEC); + var higherAnimation = div.animate({ opacity: [0, 1] }, 100 * MS_PER_SEC); + + return Promise.all([lowerAnimation.ready, higherAnimation.ready]).then(function() { + assert_animation_is_running_on_compositor(higherAnimation, + 'A higher-priority opacity animation on an element ' + + 'reports that it is running on the compositor'); + assert_animation_is_running_on_compositor(lowerAnimation, + 'A lower-priority opacity animation on the same ' + + 'element also reports that it is running on the compositor'); + }); +}, 'Opacity animations on the same element run on the compositor'); + +promise_test(function(t) { + var div = addDiv(t, { style: 'transition: opacity 100s; opacity: 1' }); + + getComputedStyle(div).opacity; + + div.style.opacity = 0; + getComputedStyle(div).opacity; + + var transition = div.getAnimations()[0]; + var animation = div.animate({ opacity: [0, 1] }, 100 * MS_PER_SEC); + + return Promise.all([transition.ready, animation.ready]).then(function() { + assert_animation_is_running_on_compositor(animation, + 'An opacity animation on an element reports that' + + 'that it is running on the compositor'); + assert_animation_is_running_on_compositor(transition, + 'An opacity transition on the same element reports that ' + + 'it is running on the compositor'); + }); +}, 'Both of transition and script animation on the same element run on the ' + + 'compositor'); + +promise_test(function(t) { + var div = addDiv(t); + var importantOpacityElement = addDiv(t, { style: "opacity: 1 ! important" }); + + var animation = div.animate({ opacity: [1, 0] }, 100 * MS_PER_SEC); + + return animation.ready.then(function() { + assert_animation_is_running_on_compositor(animation, + 'Opacity animation on an element reports ' + + 'that it is running on the compositor'); + + animation.effect.target = null; + return waitForFrame(); + }).then(function() { + assert_animation_is_not_running_on_compositor(animation, + 'Animation is no longer running on the compositor after ' + + 'removing from the element'); + animation.effect.target = importantOpacityElement; + return waitForFrame(); + }).then(function() { + assert_animation_is_not_running_on_compositor(animation, + 'Animation is NOT running on the compositor even after ' + + 'being applied to a different element which has an ' + + '!important opacity declaration'); + }); +}, 'Animation continues not running on the compositor after being ' + + 'applied to an element which has an important declaration and ' + + 'having previously been temporarily associated with no target element'); + +promise_test(function(t) { + var div = addDiv(t); + var another = addDiv(t); + + var lowerAnimation = div.animate({ opacity: [1, 0] }, 100 * MS_PER_SEC); + var higherAnimation = another.animate({ opacity: [0, 1] }, 100 * MS_PER_SEC); + + return Promise.all([lowerAnimation.ready, higherAnimation.ready]).then(function() { + assert_animation_is_running_on_compositor(lowerAnimation, + 'An opacity animation on an element reports that ' + + 'it is running on the compositor'); + assert_animation_is_running_on_compositor(higherAnimation, + 'Opacity animation on a different element reports ' + + 'that it is running on the compositor'); + + lowerAnimation.effect.target = null; + return waitForFrame(); + }).then(function() { + assert_animation_is_not_running_on_compositor(lowerAnimation, + 'Animation is no longer running on the compositor after ' + + 'being removed from the element'); + lowerAnimation.effect.target = another; + return waitForFrame(); + }).then(function() { + assert_animation_is_running_on_compositor(lowerAnimation, + 'A lower-priority animation begins running ' + + 'on the compositor after being applied to an element ' + + 'which has a higher-priority animation'); + assert_animation_is_running_on_compositor(higherAnimation, + 'A higher-priority animation continues to run on the ' + + 'compositor even after a lower-priority animation is ' + + 'applied to the same element'); + }); +}, 'Animation begins running on the compositor after being applied ' + + 'to an element which has a higher-priority animation and after ' + + 'being temporarily associated with no target element'); + +promise_test(function(t) { + var div = addDiv(t); + var another = addDiv(t); + + var lowerAnimation = div.animate({ opacity: [1, 0] }, 100 * MS_PER_SEC); + var higherAnimation = another.animate({ opacity: [0, 1] }, 100 * MS_PER_SEC); + + return Promise.all([lowerAnimation.ready, higherAnimation.ready]).then(function() { + assert_animation_is_running_on_compositor(lowerAnimation, + 'An opacity animation on an element reports that ' + + 'it is running on the compositor'); + assert_animation_is_running_on_compositor(higherAnimation, + 'Opacity animation on a different element reports ' + + 'that it is running on the compositor'); + + higherAnimation.effect.target = null; + return waitForFrame(); + }).then(function() { + assert_animation_is_not_running_on_compositor(higherAnimation, + 'Animation is no longer running on the compositor after ' + + 'being removed from the element'); + higherAnimation.effect.target = div; + return waitForFrame(); + }).then(function() { + assert_animation_is_running_on_compositor(lowerAnimation, + 'Animation continues running on the compositor after ' + + 'a higher-priority animation applied to the same element'); + assert_animation_is_running_on_compositor(higherAnimation, + 'A higher-priority animation begins to running on the ' + + 'compositor after being applied to an element which has ' + + 'a lower-priority-animation'); + }); +}, 'Animation begins running on the compositor after being applied ' + + 'to an element which has a lower-priority animation once after ' + + 'disassociating with an element'); + +var delayPhaseTests = [ + { + desc: 'script animation of opacity', + setupAnimation: function(t) { + return addDiv(t).animate( + { opacity: [0, 1] }, + { delay: 100 * MS_PER_SEC, duration: 100 * MS_PER_SEC }); + }, + }, + { + desc: 'script animation of transform', + setupAnimation: function(t) { + return addDiv(t).animate( + { transform: ['translateX(0px)', 'translateX(100px)'] }, + { delay: 100 * MS_PER_SEC, duration: 100 * MS_PER_SEC }); + }, + }, + { + desc: 'CSS animation of opacity', + setupAnimation: function(t) { + return addDiv(t, { style: 'animation: opacity 100s 100s' }) + .getAnimations()[0]; + }, + }, + { + desc: 'CSS animation of transform', + setupAnimation: function(t) { + return addDiv(t, { style: 'animation: anim 100s 100s' }) + .getAnimations()[0]; + }, + }, + { + desc: 'CSS transition of opacity', + setupAnimation: function(t) { + var div = addDiv(t, { style: 'transition: opacity 100s 100s' }); + getComputedStyle(div).opacity; + + div.style.opacity = 0; + return div.getAnimations()[0]; + }, + }, + { + desc: 'CSS transition of transform', + setupAnimation: function(t) { + var div = addDiv(t, { style: 'transition: transform 100s 100s' }); + getComputedStyle(div).transform; + + div.style.transform = 'translateX(100px)'; + return div.getAnimations()[0]; + }, + }, +]; + +delayPhaseTests.forEach(function(test) { + promise_test(function(t) { + var animation = test.setupAnimation(t); + + return animation.ready.then(function() { + assert_animation_is_running_on_compositor(animation, + test.desc + ' reports that it is running on the ' + + 'compositor even though it is in the delay phase'); + }); + }, 'isRunningOnCompositor for ' + test.desc + ' is true even though ' + + 'it is in the delay phase'); +}); + +// The purpose of thie test cases is to check that +// NS_FRAME_MAY_BE_TRANSFORMED flag on the associated nsIFrame persists +// after transform style on the frame is removed. +var delayPhaseWithTransformStyleTests = [ + { + desc: 'script animation of transform with transform style', + setupAnimation: function(t) { + return addDiv(t, { style: 'transform: translateX(10px)' }).animate( + { transform: ['translateX(0px)', 'translateX(100px)'] }, + { delay: 100 * MS_PER_SEC, duration: 100 * MS_PER_SEC }); + }, + }, + { + desc: 'CSS animation of transform with transform style', + setupAnimation: function(t) { + return addDiv(t, { style: 'animation: anim 100s 100s;' + + 'transform: translateX(10px)' }) + .getAnimations()[0]; + }, + }, + { + desc: 'CSS transition of transform with transform style', + setupAnimation: function(t) { + var div = addDiv(t, { style: 'transition: transform 100s 100s;' + + 'transform: translateX(10px)'}); + getComputedStyle(div).transform; + + div.style.transform = 'translateX(100px)'; + return div.getAnimations()[0]; + }, + }, +]; + +delayPhaseWithTransformStyleTests.forEach(function(test) { + promise_test(function(t) { + var animation = test.setupAnimation(t); + + return animation.ready.then(function() { + assert_animation_is_running_on_compositor(animation, + test.desc + ' reports that it is running on the ' + + 'compositor even though it is in the delay phase'); + }).then(function() { + // Remove the initial transform style during delay phase. + animation.effect.target.style.transform = 'none'; + return animation.ready; + }).then(function() { + assert_animation_is_running_on_compositor(animation, + test.desc + ' reports that it keeps running on the ' + + 'compositor after removing the initial transform style'); + }); + }, 'isRunningOnCompositor for ' + test.desc + ' is true after removing ' + + 'the initial transform style during the delay phase'); +}); + +var startsWithNoneTests = [ + { + desc: 'script animation of transform starts with transform:none segment', + setupAnimation: function(t) { + return addDiv(t).animate( + { transform: ['none', 'none', 'translateX(100px)'] }, 100 * MS_PER_SEC); + }, + }, + { + desc: 'CSS animation of transform starts with transform:none segment', + setupAnimation: function(t) { + return addDiv(t, + { style: 'animation: transform-starts-with-none 100s 100s' }) + .getAnimations()[0]; + }, + }, +]; + +startsWithNoneTests.forEach(function(test) { + promise_test(function(t) { + var animation = test.setupAnimation(t); + + return animation.ready.then(function() { + assert_animation_is_running_on_compositor(animation, + test.desc + ' reports that it is running on the ' + + 'compositor even though it is in transform:none segment'); + }); + }, 'isRunningOnCompositor for ' + test.desc + ' is true even though ' + + 'it is in transform:none segment'); +}); + +promise_test(function(t) { + var div = addDiv(t, { style: 'opacity: 1 ! important' }); + + var animation = div.animate( + { opacity: [0, 1] }, + { delay: 100 * MS_PER_SEC, duration: 100 * MS_PER_SEC }); + + return animation.ready.then(function() { + assert_animation_is_not_running_on_compositor(animation, + 'Opacity animation on an element which has opacity:1 important style' + + 'reports that it is not running on the compositor'); + // Clear the opacity style on the target element. + div.style.setProperty("opacity", "1", ""); + return waitForFrame(); + }).then(function() { + assert_animation_is_running_on_compositor(animation, + 'Opacity animations reports that it is running on the compositor after ' + + 'clearing the opacity style on the element'); + }); +}, 'Clearing *important* opacity style on the target element sends the ' + + 'animation to the compositor even if the animation is in the delay phase'); + +promise_test(function(t) { + var opaqueDiv = addDiv(t, { style: 'opacity: 1 ! important' }); + var anotherDiv = addDiv(t); + + var animation = opaqueDiv.animate( + { opacity: [0, 1] }, + { delay: 100 * MS_PER_SEC, duration: 100 * MS_PER_SEC }); + + return animation.ready.then(function() { + assert_animation_is_not_running_on_compositor(animation, + 'Opacity animation on an element which has opacity:1 important style' + + 'reports that it is not running on the compositor'); + // Changing target element to another element which has no opacity style. + animation.effect.target = anotherDiv; + return waitForFrame(); + }).then(function() { + assert_animation_is_running_on_compositor(animation, + 'Opacity animations reports that it is running on the compositor after ' + + 'changing the target element to another elemenent having no ' + + 'opacity style'); + }); +}, 'Changing target element of opacity animation sends the animation to the ' + + 'the compositor even if the animation is in the delay phase'); + +promise_test(function(t) { + var animation = + addDivAndAnimate(t, + {}, + { width: ['100px', '200px'] }, + { duration: 100 * MS_PER_SEC, delay: 100 * MS_PER_SEC }); + + return animation.ready.then(function() { + assert_animation_is_not_running_on_compositor(animation, + 'Width animation reports that it is not running on the compositor ' + + 'in the delay phase'); + // Changing to property runnable on the compositor. + animation.effect.setKeyframes({ opacity: [0, 1] }); + return waitForFrame(); + }).then(function() { + assert_animation_is_running_on_compositor(animation, + 'Opacity animation reports that it is running on the compositor ' + + 'after changing the property from width property in the delay phase'); + }); +}, 'Dynamic change to a property runnable on the compositor ' + + 'in the delay phase'); + +promise_test(function(t) { + var div = addDiv(t, { style: 'transition: opacity 100s; ' + + 'opacity: 0 !important' }); + getComputedStyle(div).opacity; + + div.style.setProperty('opacity', '1', 'important'); + getComputedStyle(div).opacity; + + var animation = div.getAnimations()[0]; + + return animation.ready.then(function() { + assert_animation_is_running_on_compositor(animation, + 'Transition reports that it is running on the compositor even if the ' + + 'property is overridden by an !important rule'); + }); +}, 'Transitions override important rules'); + +promise_test(function(t) { + var div = addDiv(t, { style: 'transition: opacity 100s; ' + + 'opacity: 0 !important' }); + getComputedStyle(div).opacity; + + div.animate({ opacity: [ 0, 1 ] }, 100 * MS_PER_SEC); + + div.style.setProperty('opacity', '1', 'important'); + getComputedStyle(div).opacity; + + var [transition, animation] = div.getAnimations(); + + return Promise.all([transition.ready, animation.ready]).then(function() { + assert_animation_is_not_running_on_compositor(transition, + 'Transition suppressed by an animation which is overridden by an ' + + '!important rule reports that it is NOT running on the compositor'); + assert_animation_is_not_running_on_compositor(animation, + 'Animation overridden by an !important rule reports that it is ' + + 'NOT running on the compositor'); + }); +}, 'Neither transition nor animation does run on the compositor if the ' + + 'property is overridden by an !important rule'); + +</script> +</body> diff --git a/dom/animation/test/crashtests/1216842-1.html b/dom/animation/test/crashtests/1216842-1.html new file mode 100644 index 0000000000..8df6808aed --- /dev/null +++ b/dom/animation/test/crashtests/1216842-1.html @@ -0,0 +1,35 @@ +<!doctype html> +<html class="reftest-wait"> + <head> + <title>Bug 1216842: effect-level easing function produces negative values (compositor thread)</title> + <style> + #target { + width: 100px; height: 100px; + background: blue; + } + </style> + </head> + <body> + <div id="target"></div> + </body> + <script> + var target = document.getElementById("target"); + var effect = + new KeyframeEffectReadOnly( + target, + { opacity: [0, 1] }, + { + fill: "forwards", + /* The function produces negative values in (0, 0.766312060) */ + easing: "cubic-bezier(0,-0.5,1,-0.5)", + duration: 100, + iterations: 0.75 /* To finish in the extraporation range */ + } + ); + var animation = new Animation(effect, document.timeline); + animation.play(); + animation.finished.then(function() { + document.documentElement.className = ""; + }); + </script> +</html> diff --git a/dom/animation/test/crashtests/1216842-2.html b/dom/animation/test/crashtests/1216842-2.html new file mode 100644 index 0000000000..ec2a2e167e --- /dev/null +++ b/dom/animation/test/crashtests/1216842-2.html @@ -0,0 +1,35 @@ +<!doctype html> +<html class="reftest-wait"> + <head> + <title>Bug 1216842: effect-level easing function produces values greater than 1 (compositor thread)</title> + <style> + #target { + width: 100px; height: 100px; + background: blue; + } + </style> + </head> + <body> + <div id="target"></div> + </body> + <script> + var target = document.getElementById("target"); + var effect = + new KeyframeEffectReadOnly( + target, + { opacity: [0, 1] }, + { + fill: "forwards", + /* The function produces values greater than 1 in (0.23368794, 1) */ + easing: "cubic-bezier(0,1.5,1,1.5)", + duration: 100, + iterations: 0.25 /* To finish in the extraporation range */ + } + ); + var animation = new Animation(effect, document.timeline); + animation.play(); + animation.finished.then(function() { + document.documentElement.className = ""; + }); + </script> +</html> diff --git a/dom/animation/test/crashtests/1216842-3.html b/dom/animation/test/crashtests/1216842-3.html new file mode 100644 index 0000000000..2e5a762aa0 --- /dev/null +++ b/dom/animation/test/crashtests/1216842-3.html @@ -0,0 +1,27 @@ +<!doctype html> +<html class="reftest-wait"> + <head> + <title>Bug 1216842: effect-level easing function produces values greater than 1 (main-thread)</title> + </head> + <body> + <div id="target"></div> + </body> + <script> + var target = document.getElementById("target"); + var effect = + new KeyframeEffectReadOnly( + target, + { color: ["red", "blue"] }, + { + fill: "forwards", + /* The function produces values greater than 1 in (0.23368794, 1) */ + easing: "cubic-bezier(0,1.5,1,1.5)", + duration: 100 + } + ); + var animation = new Animation(effect, document.timeline); + animation.pause(); + animation.currentTime = 250; + document.documentElement.className = ""; + </script> +</html> diff --git a/dom/animation/test/crashtests/1216842-4.html b/dom/animation/test/crashtests/1216842-4.html new file mode 100644 index 0000000000..2951adc959 --- /dev/null +++ b/dom/animation/test/crashtests/1216842-4.html @@ -0,0 +1,27 @@ +<!doctype html> +<html class="reftest-wait"> + <head> + <title>Bug 1216842: effect-level easing function produces negative values (main-thread)</title> + </head> + <body> + <div id="target"></div> + </body> + <script> + var target = document.getElementById("target"); + var effect = + new KeyframeEffectReadOnly( + target, + { color: ["red", "blue"] }, + { + fill: "forwards", + /* The function produces negative values in (0, 0.766312060) */ + easing: "cubic-bezier(0,-0.5,1,-0.5)", + duration: 100 + } + ); + var animation = new Animation(effect, document.timeline); + animation.pause(); + animation.currentTime = 250; + document.documentElement.className = ""; + </script> +</html> diff --git a/dom/animation/test/crashtests/1216842-5.html b/dom/animation/test/crashtests/1216842-5.html new file mode 100644 index 0000000000..65c64fa481 --- /dev/null +++ b/dom/animation/test/crashtests/1216842-5.html @@ -0,0 +1,38 @@ +<!doctype html> +<html class="reftest-wait"> + <head> + <title> + Bug 1216842: effect-level easing function produces negative values passed + to step-end function (compositor thread) + </title> + <style> + #target { + width: 100px; height: 100px; + background: blue; + } + </style> + </head> + <body> + <div id="target"></div> + </body> + <script> + var target = document.getElementById("target"); + var effect = + new KeyframeEffectReadOnly( + target, + { opacity: [0, 1], easing: "step-end" }, + { + fill: "forwards", + /* The function produces negative values in (0, 0.766312060) */ + easing: "cubic-bezier(0,-0.5,1,-0.5)", + duration: 100, + iterations: 0.75 /* To finish in the extraporation range */ + } + ); + var animation = new Animation(effect, document.timeline); + animation.play(); + animation.finished.then(function() { + document.documentElement.className = ""; + }); + </script> +</html> diff --git a/dom/animation/test/crashtests/1216842-6.html b/dom/animation/test/crashtests/1216842-6.html new file mode 100644 index 0000000000..a588c68f1e --- /dev/null +++ b/dom/animation/test/crashtests/1216842-6.html @@ -0,0 +1,38 @@ +<!doctype html> +<html class="reftest-wait"> + <head> + <title> + Bug 1216842: effect-level easing function produces values greater than 1 + which are passed to step-end function (compositor thread) + </title> + <style> + #target { + width: 100px; height: 100px; + background: blue; + } + </style> + </head> + <body> + <div id="target"></div> + </body> + <script> + var target = document.getElementById("target"); + var effect = + new KeyframeEffectReadOnly( + target, + { opacity: [0, 1], easing: "step-end" }, + { + fill: "forwards", + /* The function produces values greater than 1 in (0.23368794, 1) */ + easing: "cubic-bezier(0,1.5,1,1.5)", + duration: 100, + iterations: 0.25 /* To finish in the extraporation range */ + } + ); + var animation = new Animation(effect, document.timeline); + animation.play(); + animation.finished.then(function() { + document.documentElement.className = ""; + }); + </script> +</html> diff --git a/dom/animation/test/crashtests/1239889-1.html b/dom/animation/test/crashtests/1239889-1.html new file mode 100644 index 0000000000..476f3322b4 --- /dev/null +++ b/dom/animation/test/crashtests/1239889-1.html @@ -0,0 +1,12 @@ +<!doctype html> +<html> + <head> + <title>Bug 1239889</title> + </head> + <body> + </body> + <script> + var div = document.createElement('div'); + var effect = new KeyframeEffectReadOnly(div, { opacity: [0, 1] }); + </script> +</html> diff --git a/dom/animation/test/crashtests/1244595-1.html b/dom/animation/test/crashtests/1244595-1.html new file mode 100644 index 0000000000..13b2e2d7e7 --- /dev/null +++ b/dom/animation/test/crashtests/1244595-1.html @@ -0,0 +1,3 @@ +<div id=target><script> + var player = target.animate([{background: 'green'}, {background: 'green'}]); +</script> diff --git a/dom/animation/test/crashtests/1272475-1.html b/dom/animation/test/crashtests/1272475-1.html new file mode 100644 index 0000000000..e0b0495388 --- /dev/null +++ b/dom/animation/test/crashtests/1272475-1.html @@ -0,0 +1,20 @@ +<!doctype html> +<html> + <head> + <title>Bug 1272475 - scale function with an extreme large value</title> + <script> + function test() { + var div = document.createElement("div"); + div.setAttribute("style", "width: 1px; height: 1px; " + + "background: red;"); + document.body.appendChild(div); + div.animate([ { "transform": "scale(8)" }, + { "transform": "scale(9.5e+307)" }, + { "transform": "scale(32)" } ], + { "duration": 1000, "fill": "both" }); + } + </script> + </head> + <body onload="test()"> + </body> +</html> diff --git a/dom/animation/test/crashtests/1272475-2.html b/dom/animation/test/crashtests/1272475-2.html new file mode 100644 index 0000000000..da0e8605bd --- /dev/null +++ b/dom/animation/test/crashtests/1272475-2.html @@ -0,0 +1,20 @@ +<!doctype html> +<html> + <head> + <title>Bug 1272475 - rotate function with an extreme large value</title> + <script> + function test() { + var div = document.createElement("div"); + div.setAttribute("style", "width: 100px; height: 100px; " + + "background: red;"); + document.body.appendChild(div); + div.animate([ { "transform": "rotate(8rad)" }, + { "transform": "rotate(9.5e+307rad)" }, + { "transform": "rotate(32rad)" } ], + { "duration": 1000, "fill": "both" }); + } + </script> + </head> + <body onload="test()"> + </body> +</html> diff --git a/dom/animation/test/crashtests/1277272-1-inner.html b/dom/animation/test/crashtests/1277272-1-inner.html new file mode 100644 index 0000000000..2ba52174dd --- /dev/null +++ b/dom/animation/test/crashtests/1277272-1-inner.html @@ -0,0 +1,19 @@ +<!doctype html> +<head> +<script> +function start() { + var animation = document.body.animate([{marks: 'crop'},{marks: 'crop'}], 12); + document.write('<html><body></body></html>'); + + setTimeout(function() { animation.play(); }, 4); + setTimeout(function() { + animation.timeline = undefined; + SpecialPowers.Cu.forceGC(); + window.top.continueTest(); + }, 5); +} +</script> +</head> +<body onload="start()"></body> +</html> + diff --git a/dom/animation/test/crashtests/1277272-1.html b/dom/animation/test/crashtests/1277272-1.html new file mode 100644 index 0000000000..f398bcf6d9 --- /dev/null +++ b/dom/animation/test/crashtests/1277272-1.html @@ -0,0 +1,26 @@ +<!doctype html> +<html class="reftest-wait"> +<head> +<script> +var count = 0; + +function start() { + if (++count > 10) { + document.documentElement.className = ""; + return; + } + + var frame = document.getElementById("frame"); + frame.src = "./1277272-1-inner.html"; +} + +function continueTest() { + setTimeout(start.bind(window), 1); +} + +</script> +</head> +<body onload="start()"></body> +<iframe id="frame"> +</html> + diff --git a/dom/animation/test/crashtests/1278485-1.html b/dom/animation/test/crashtests/1278485-1.html new file mode 100644 index 0000000000..e7347f5d84 --- /dev/null +++ b/dom/animation/test/crashtests/1278485-1.html @@ -0,0 +1,26 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset="UTF-8"> +<script> + +function boom() +{ + document.body.animate([], + { duration: 6, + easing: "cubic-bezier(0, -1e+39, 0, 0)" }); + document.body.animate([], + { duration: 6, + easing: "cubic-bezier(0, 1e+39, 0, 0)" }); + document.body.animate([], + { duration: 6, + easing: "cubic-bezier(0, 0, 0, -1e+39)" }); + document.body.animate([], + { duration: 6, + easing: "cubic-bezier(0, 0, 0, 1e+39)" }); +} + +</script> +</head> +<body onload="boom();"></body> +</html> diff --git a/dom/animation/test/crashtests/1290535-1.html b/dom/animation/test/crashtests/1290535-1.html new file mode 100644 index 0000000000..20b44d8bfc --- /dev/null +++ b/dom/animation/test/crashtests/1290535-1.html @@ -0,0 +1,20 @@ +<!DOCTYPE html> +<html> +<head> + <title>Bug 1290535 - Sort paced subproperties of a shorthand property</title> +<meta charset="UTF-8"> +<script> + +function test() +{ + var div = document.createElement('div'); + document.documentElement.appendChild(div); + div.animate([ { borderRadius: "0", borderTopRightRadius: "0" }, + { borderRadius: "50%" } ], + { spacing:"paced(border-radius)" }); +} + +</script> +</head> +<body onload="test();"></body> +</html> diff --git a/dom/animation/test/crashtests/crashtests.list b/dom/animation/test/crashtests/crashtests.list new file mode 100644 index 0000000000..f61d7f876e --- /dev/null +++ b/dom/animation/test/crashtests/crashtests.list @@ -0,0 +1,13 @@ +pref(dom.animations-api.core.enabled,true) load 1239889-1.html +pref(dom.animations-api.core.enabled,true) load 1244595-1.html +pref(dom.animations-api.core.enabled,true) load 1216842-1.html +pref(dom.animations-api.core.enabled,true) load 1216842-2.html +pref(dom.animations-api.core.enabled,true) load 1216842-3.html +pref(dom.animations-api.core.enabled,true) load 1216842-4.html +pref(dom.animations-api.core.enabled,true) load 1216842-5.html +pref(dom.animations-api.core.enabled,true) load 1216842-6.html +pref(dom.animations-api.core.enabled,true) load 1272475-1.html +pref(dom.animations-api.core.enabled,true) load 1272475-2.html +pref(dom.animations-api.core.enabled,true) load 1278485-1.html +pref(dom.animations-api.core.enabled,true) load 1277272-1.html +pref(dom.animations-api.core.enabled,true) load 1290535-1.html diff --git a/dom/animation/test/css-animations/file_animation-cancel.html b/dom/animation/test/css-animations/file_animation-cancel.html new file mode 100644 index 0000000000..85499addfd --- /dev/null +++ b/dom/animation/test/css-animations/file_animation-cancel.html @@ -0,0 +1,154 @@ +<!doctype html> +<meta charset=utf-8> +<script src="../testcommon.js"></script> +<style> +@keyframes translateAnim { + to { transform: translate(100px) } +} +@keyframes marginLeftAnim { + to { margin-left: 100px } +} +@keyframes marginLeftAnim100To200 { + from { margin-left: 100px } + to { margin-left: 200px } +} +</style> +<body> +<script> +'use strict'; + +promise_test(function(t) { + var div = addDiv(t, { style: 'animation: translateAnim 100s' }); + var animation = div.getAnimations()[0]; + + return animation.ready.then(function() { + assert_not_equals(getComputedStyle(div).transform, 'none', + 'transform style is animated before cancelling'); + animation.cancel(); + assert_equals(getComputedStyle(div).transform, 'none', + 'transform style is no longer animated after cancelling'); + }); +}, 'Animated style is cleared after cancelling a running CSS animation'); + +promise_test(function(t) { + var div = addDiv(t, { style: 'animation: translateAnim 100s forwards' }); + var animation = div.getAnimations()[0]; + animation.finish(); + + return animation.ready.then(function() { + assert_not_equals(getComputedStyle(div).transform, 'none', + 'transform style is filling before cancelling'); + animation.cancel(); + assert_equals(getComputedStyle(div).transform, 'none', + 'fill style is cleared after cancelling'); + }); +}, 'Animated style is cleared after cancelling a filling CSS animation'); + +promise_test(function(t) { + var div = addDiv(t, { style: 'animation: translateAnim 100s' }); + var animation = div.getAnimations()[0]; + div.addEventListener('animationend', t.step_func(function() { + assert_unreached('Got unexpected end event on cancelled animation'); + })); + + return animation.ready.then(function() { + // Seek to just before the end then cancel + animation.currentTime = 99.9 * 1000; + animation.cancel(); + + // Then wait a couple of frames and check that no event was dispatched + return waitForAnimationFrames(2); + }); +}, 'Cancelled CSS animations do not dispatch events'); + +test(function(t) { + var div = addDiv(t, { style: 'animation: marginLeftAnim 100s linear' }); + var animation = div.getAnimations()[0]; + animation.cancel(); + + assert_equals(getComputedStyle(div).marginLeft, '0px', + 'margin-left style is not animated after cancelling'); + + animation.currentTime = 50 * 1000; + assert_equals(getComputedStyle(div).marginLeft, '50px', + 'margin-left style is updated when cancelled animation is' + + ' seeked'); +}, 'After cancelling an animation, it can still be seeked'); + +promise_test(function(t) { + var div = + addDiv(t, { style: 'animation: marginLeftAnim100To200 100s linear' }); + var animation = div.getAnimations()[0]; + + return animation.ready.then(function() { + animation.cancel(); + assert_equals(getComputedStyle(div).marginLeft, '0px', + 'margin-left style is not animated after cancelling'); + animation.play(); + assert_equals(getComputedStyle(div).marginLeft, '100px', + 'margin-left style is animated after re-starting animation'); + return animation.ready; + }).then(function() { + assert_equals(animation.playState, 'running', + 'Animation succeeds in running after being re-started'); + }); +}, 'After cancelling an animation, it can still be re-used'); + +test(function(t) { + var div = + addDiv(t, { style: 'animation: marginLeftAnim100To200 100s linear' }); + var animation = div.getAnimations()[0]; + animation.cancel(); + assert_equals(getComputedStyle(div).marginLeft, '0px', + 'margin-left style is not animated after cancelling'); + + // Trigger a change to some animation properties and check that this + // doesn't cause the animation to become live again + div.style.animationDuration = '200s'; + flushComputedStyle(div); + assert_equals(getComputedStyle(div).marginLeft, '0px', + 'margin-left style is still not animated after updating' + + ' animation-duration'); + assert_equals(animation.playState, 'idle', + 'Animation is still idle after updating animation-duration'); +}, 'After cancelling an animation, updating animation properties doesn\'t make' + + ' it live again'); + +test(function(t) { + var div = + addDiv(t, { style: 'animation: marginLeftAnim100To200 100s linear' }); + var animation = div.getAnimations()[0]; + animation.cancel(); + assert_equals(getComputedStyle(div).marginLeft, '0px', + 'margin-left style is not animated after cancelling'); + + // Make some changes to animation-play-state and check that the + // animation doesn't become live again. This is because it should be + // possible to cancel an animation from script such that all future + // changes to style are ignored. + + // Redundant change + div.style.animationPlayState = 'running'; + assert_equals(animation.playState, 'idle', + 'Animation is still idle after a redundant change to' + + ' animation-play-state'); + + // Pause + div.style.animationPlayState = 'paused'; + assert_equals(animation.playState, 'idle', + 'Animation is still idle after setting' + + ' animation-play-state: paused'); + + // Play + div.style.animationPlayState = 'running'; + assert_equals(animation.playState, 'idle', + 'Animation is still idle after re-setting' + + ' animation-play-state: running'); + +}, 'After cancelling an animation, updating animation-play-state doesn\'t' + + ' make it live again'); + +done(); +</script> +</body> +</html> diff --git a/dom/animation/test/css-animations/file_animation-computed-timing.html b/dom/animation/test/css-animations/file_animation-computed-timing.html new file mode 100644 index 0000000000..53597a473a --- /dev/null +++ b/dom/animation/test/css-animations/file_animation-computed-timing.html @@ -0,0 +1,566 @@ +<!doctype html> +<meta charset=utf-8> +<script src="../testcommon.js"></script> +<style> +@keyframes moveAnimation { + from { margin-left: 100px } + to { margin-left: 200px } +} +</style> +<body> +<script> + +'use strict'; + +// -------------------- +// delay +// -------------------- +test(function(t) { + var div = addDiv(t, {style: 'animation: moveAnimation 100s'}); + var effect = div.getAnimations()[0].effect; + assert_equals(effect.getComputedTiming().delay, 0, + 'Initial value of delay'); +}, 'delay of a new animation'); + +test(function(t) { + var div = addDiv(t, {style: 'animation: moveAnimation 100s -10s'}); + var effect = div.getAnimations()[0].effect; + assert_equals(effect.getComputedTiming().delay, -10 * MS_PER_SEC, + 'Initial value of delay'); +}, 'Negative delay of a new animation'); + +test(function(t) { + var div = addDiv(t, {style: 'animation: moveAnimation 100s 10s'}); + var effect = div.getAnimations()[0].effect; + assert_equals(effect.getComputedTiming().delay, 10 * MS_PER_SEC, + 'Initial value of delay'); +}, 'Positive delay of a new animation'); + + +// -------------------- +// endDelay +// -------------------- +test(function(t) { + var div = addDiv(t, {style: 'animation: moveAnimation 100s'}); + var effect = div.getAnimations()[0].effect; + assert_equals(effect.getComputedTiming().endDelay, 0, + 'Initial value of endDelay'); +}, 'endDelay of a new animation'); + + +// -------------------- +// fill +// -------------------- +test(function(t) { + var getEffectWithFill = function(fill) { + var div = addDiv(t, {style: 'animation: moveAnimation 100s ' + fill}); + return div.getAnimations()[0].effect; + }; + + var effect = getEffectWithFill(''); + assert_equals(effect.getComputedTiming().fill, 'none', + 'Initial value of fill'); + effect = getEffectWithFill('forwards'); + assert_equals(effect.getComputedTiming().fill, 'forwards', + 'Fill forwards'); + effect = getEffectWithFill('backwards'); + assert_equals(effect.getComputedTiming().fill, 'backwards', + 'Fill backwards'); + effect = getEffectWithFill('both'); + assert_equals(effect.getComputedTiming().fill, 'both', + 'Fill forwards and backwards'); +}, 'fill of a new animation'); + + +// -------------------- +// iterationStart +// -------------------- +test(function(t) { + var div = addDiv(t, {style: 'animation: moveAnimation 100s'}); + var effect = div.getAnimations()[0].effect; + assert_equals(effect.getComputedTiming().iterationStart, 0, + 'Initial value of iterationStart'); +}, 'iterationStart of a new animation'); + + +// -------------------- +// iterations +// -------------------- +test(function(t) { + var div = addDiv(t, {style: 'animation: moveAnimation 100s'}); + var effect = div.getAnimations()[0].effect; + assert_equals(effect.getComputedTiming().iterations, 1, + 'Initial value of iterations'); +}, 'iterations of a new animation'); + +test(function(t) { + var div = addDiv(t, {style: 'animation: moveAnimation 100s 2016.5'}); + var effect = div.getAnimations()[0].effect; + assert_equals(effect.getComputedTiming().iterations, 2016.5, + 'Initial value of iterations'); +}, 'iterations of a finitely repeating animation'); + +test(function(t) { + var div = addDiv(t, {style: 'animation: moveAnimation 100s infinite'}); + var effect = div.getAnimations()[0].effect; + assert_equals(effect.getComputedTiming().iterations, Infinity, + 'Initial value of iterations'); +}, 'iterations of an infinitely repeating animation'); + + +// -------------------- +// duration +// -------------------- +test(function(t) { + var div = addDiv(t, {style: 'animation: moveAnimation 100s -10s infinite'}); + var effect = div.getAnimations()[0].effect; + assert_equals(effect.getComputedTiming().duration, 100 * MS_PER_SEC, + 'Initial value of duration'); +}, 'duration of a new animation'); + + +// -------------------- +// direction +// -------------------- +test(function(t) { + var getEffectWithDir = function(dir) { + var div = addDiv(t, {style: 'animation: moveAnimation 100s ' + dir}); + return div.getAnimations()[0].effect; + }; + + var effect = getEffectWithDir(''); + assert_equals(effect.getComputedTiming().direction, 'normal', + 'Initial value of normal direction'); + effect = getEffectWithDir('reverse'); + assert_equals(effect.getComputedTiming().direction, 'reverse', + 'Initial value of reverse direction'); + effect = getEffectWithDir('alternate'); + assert_equals(effect.getComputedTiming().direction, 'alternate', + 'Initial value of alternate direction'); + effect = getEffectWithDir('alternate-reverse'); + assert_equals(effect.getComputedTiming().direction, 'alternate-reverse', + 'Initial value of alternate-reverse direction'); +}, 'direction of a new animation'); + + +// -------------------- +// easing +// -------------------- +test(function(t) { + var div = addDiv(t, {style: 'animation: moveAnimation 100s'}); + var effect = div.getAnimations()[0].effect; + assert_equals(effect.getComputedTiming().easing, 'linear', + 'Initial value of easing'); +}, 'easing of a new animation'); + + +// ------------------------------ +// endTime +// = max(start delay + active duration + end delay, 0) +// -------------------- +test(function(t) { + var div = addDiv(t, {style: 'animation: moveAnimation 100s'}); + var effect = div.getAnimations()[0].effect; + assert_equals(effect.getComputedTiming().endTime, 100 * MS_PER_SEC, + 'Initial value of endTime'); +}, 'endTime of an new animation'); + +test(function(t) { + var div = addDiv(t, {style: 'animation: moveAnimation 100s -5s'}); + var effect = div.getAnimations()[0].effect; + var answer = (100 - 5) * MS_PER_SEC; + assert_equals(effect.getComputedTiming().endTime, answer, + 'Initial value of endTime'); +}, 'endTime of an animation with a negative delay'); + +test(function(t) { + var div = addDiv(t, {style: 'animation: moveAnimation 10s -100s infinite'}); + var effect = div.getAnimations()[0].effect; + assert_equals(effect.getComputedTiming().endTime, Infinity, + 'Initial value of endTime'); +}, 'endTime of an infinitely repeating animation'); + +test(function(t) { + var div = addDiv(t, {style: 'animation: moveAnimation 0s 100s infinite'}); + var effect = div.getAnimations()[0].effect; + assert_equals(effect.getComputedTiming().endTime, 100 * MS_PER_SEC, + 'Initial value of endTime'); +}, 'endTime of an infinitely repeating zero-duration animation'); + +test(function(t) { + // Fill forwards so div.getAnimations()[0] won't return an + // undefined value. + var div = addDiv(t, {style: 'animation: moveAnimation 10s -100s forwards'}); + var effect = div.getAnimations()[0].effect; + assert_equals(effect.getComputedTiming().endTime, 0, + 'Initial value of endTime'); +}, 'endTime of an animation that finishes before its startTime'); + + +// -------------------- +// activeDuration +// = iteration duration * iteration count +// -------------------- +test(function(t) { + var div = addDiv(t, {style: 'animation: moveAnimation 100s 5'}); + var effect = div.getAnimations()[0].effect; + var answer = 100 * MS_PER_SEC * 5; + assert_equals(effect.getComputedTiming().activeDuration, answer, + 'Initial value of activeDuration'); +}, 'activeDuration of a new animation'); + +test(function(t) { + var div = addDiv(t, {style: 'animation: moveAnimation 100s infinite'}); + var effect = div.getAnimations()[0].effect; + assert_equals(effect.getComputedTiming().activeDuration, Infinity, + 'Initial value of activeDuration'); +}, 'activeDuration of an infinitely repeating animation'); + +test(function(t) { + var div = addDiv(t, {style: 'animation: moveAnimation 0s 1s infinite'}); + var effect = div.getAnimations()[0].effect; + // If either the iteration duration or iteration count are zero, + // the active duration is zero. + assert_equals(effect.getComputedTiming().activeDuration, 0, + 'Initial value of activeDuration'); +}, 'activeDuration of an infinitely repeating zero-duration animation'); + +test(function(t) { + var div = addDiv(t, {style: 'animation: moveAnimation 100s 1s 0'}); + var effect = div.getAnimations()[0].effect; + // If either the iteration duration or iteration count are zero, + // the active duration is zero. + assert_equals(effect.getComputedTiming().activeDuration, 0, + 'Initial value of activeDuration'); +}, 'activeDuration of an animation with zero iterations'); + + +// -------------------- +// localTime +// -------------------- +test(function(t) { + var div = addDiv(t, {style: 'animation: moveAnimation 100s'}); + var effect = div.getAnimations()[0].effect; + assert_equals(effect.getComputedTiming().localTime, 0, + 'Initial value of localTime'); +}, 'localTime of a new animation'); + +test(function(t) { + var div = addDiv(t, {style: 'animation: moveAnimation 100s'}); + var anim = div.getAnimations()[0]; + anim.currentTime = 5 * MS_PER_SEC; + assert_equals(anim.effect.getComputedTiming().localTime, anim.currentTime, + 'current localTime after setting currentTime'); +}, 'localTime of an animation is always equal to currentTime'); + +promise_test(function(t) { + var div = addDiv(t, {style: 'animation: moveAnimation 100s'}); + + var anim = div.getAnimations()[0]; + anim.playbackRate = 2; // 2 times faster + + return anim.ready.then(function() { + assert_equals(anim.effect.getComputedTiming().localTime, anim.currentTime, + 'localTime is equal to currentTime'); + return waitForFrame(); + }).then(function() { + assert_equals(anim.effect.getComputedTiming().localTime, anim.currentTime, + 'localTime is equal to currentTime'); + }); +}, 'localTime reflects playbackRate immediately'); + +test(function(t) { + var div = addDiv(t); + var effect = new KeyframeEffectReadOnly(div, {left: ["0px", "100px"]}); + + assert_equals(effect.getComputedTiming().localTime, null, + 'localTime for orphaned effect'); +}, 'localTime of an AnimationEffect without an Animation'); + + +// -------------------- +// progress +// Note: Default timing function is linear. +// -------------------- +test(function(t) { + [{fill: '', progress: [ null, null ]}, + {fill: 'none', progress: [ null, null ]}, + {fill: 'forwards', progress: [ null, 1.0 ]}, + {fill: 'backwards', progress: [ 0.0, null ]}, + {fill: 'both', progress: [ 0.0, 1.0 ]}] + .forEach(function(test) { + var div = + addDiv(t, {style: 'animation: moveAnimation 100s 10s ' + test.fill}); + var anim = div.getAnimations()[0]; + assert_true(anim.effect.getComputedTiming().progress === test.progress[0], + 'initial progress with "' + test.fill + '" fill'); + anim.finish(); + assert_true(anim.effect.getComputedTiming().progress === test.progress[1], + 'finished progress with "' + test.fill + '" fill'); + }); +}, 'progress of an animation with different fill modes'); + +test(function(t) { + var div = addDiv(t, {style: 'animation: moveAnimation 10s 10 both'}); + var anim = div.getAnimations()[0]; + + assert_equals(anim.effect.getComputedTiming().progress, 0.0, + 'Initial value of progress'); + anim.currentTime += 2.5 * MS_PER_SEC; + assert_equals(anim.effect.getComputedTiming().progress, 0.25, + 'Value of progress'); + anim.currentTime += 5 * MS_PER_SEC; + assert_equals(anim.effect.getComputedTiming().progress, 0.75, + 'Value of progress'); + anim.currentTime += 5 * MS_PER_SEC; + assert_equals(anim.effect.getComputedTiming().progress, 0.25, + 'Value of progress'); + anim.finish() + assert_equals(anim.effect.getComputedTiming().progress, 1.0, + 'Value of progress'); +}, 'progress of an integral repeating animation with normal direction'); + +test(function(t) { + var div = addDiv(t); + // Note: FillMode here is "both" because + // 1. Since this a zero-duration animation, it will already have finished + // so it won't be returned by getAnimations() unless it fills forwards. + // 2. Fill backwards, so the progress before phase wouldn't be + // unresolved (null value). + var div = addDiv(t, {style: 'animation: moveAnimation 0s infinite both'}); + var anim = div.getAnimations()[0]; + + assert_equals(anim.effect.getComputedTiming().progress, 1.0, + 'Initial value of progress in after phase'); + + // Seek backwards + anim.currentTime -= 1 * MS_PER_SEC; + assert_equals(anim.effect.getComputedTiming().progress, 0.0, + 'Value of progress before phase'); +}, 'progress of an infinitely repeating zero-duration animation'); + +test(function(t) { + // Default iterations = 1 + var div = addDiv(t, {style: 'animation: moveAnimation 0s both'}); + var anim = div.getAnimations()[0]; + + assert_equals(anim.effect.getComputedTiming().progress, 1.0, + 'Initial value of progress in after phase'); + + // Seek backwards + anim.currentTime -= 1 * MS_PER_SEC; + assert_equals(anim.effect.getComputedTiming().progress, 0.0, + 'Value of progress before phase'); +}, 'progress of a finitely repeating zero-duration animation'); + +test(function(t) { + var div = addDiv(t, {style: 'animation: moveAnimation 0s 5s 10.25 both'}); + var anim = div.getAnimations()[0]; + + assert_equals(anim.effect.getComputedTiming().progress, 0.0, + 'Initial value of progress (before phase)'); + + // Using iteration duration of 1 now. + // currentIteration now is floor(10.25) = 10, so progress should be 25%. + anim.finish(); + assert_equals(anim.effect.getComputedTiming().progress, 0.25, + 'Value of progress in after phase'); +}, 'progress of a non-integral repeating zero-duration animation'); + +test(function(t) { + var div = addDiv(t, {style: 'animation: moveAnimation 0s 5s 10.25 both reverse'}); + var anim = div.getAnimations()[0]; + + assert_equals(anim.effect.getComputedTiming().progress, 1.0, + 'Initial value of progress (before phase)'); + + // Seek forwards + anim.finish(); + assert_equals(anim.effect.getComputedTiming().progress, 0.75, + 'Value of progress in after phase'); +}, 'Progress of a non-integral repeating zero-duration animation ' + + 'with reversing direction'); + +test(function(t) { + var div = addDiv(t, {style: 'animation: moveAnimation 10s 10.25 both alternate'}); + var anim = div.getAnimations()[0]; + + assert_equals(anim.effect.getComputedTiming().progress, 0.0, + 'Initial value of progress'); + anim.currentTime += 2.5 * MS_PER_SEC; + assert_equals(anim.effect.getComputedTiming().progress, 0.25, + 'Value of progress'); + anim.currentTime += 5 * MS_PER_SEC; + assert_equals(anim.effect.getComputedTiming().progress, 0.75, + 'Value of progress'); + anim.currentTime += 5 * MS_PER_SEC; + assert_equals(anim.effect.getComputedTiming().progress, 0.75, + 'Value of progress'); + anim.finish() + assert_equals(anim.effect.getComputedTiming().progress, 0.25, + 'Value of progress'); +}, 'progress of a non-integral repeating animation ' + + 'with alternate direction'); + +test(function(t) { + var div = addDiv(t, {style: 'animation: moveAnimation 10s 10.25 both alternate-reverse'}); + var anim = div.getAnimations()[0]; + + assert_equals(anim.effect.getComputedTiming().progress, 1.0, + 'Initial value of progress'); + anim.currentTime += 2.5 * MS_PER_SEC; + assert_equals(anim.effect.getComputedTiming().progress, 0.75, + 'Value of progress'); + anim.currentTime += 5 * MS_PER_SEC; + assert_equals(anim.effect.getComputedTiming().progress, 0.25, + 'Value of progress'); + anim.currentTime += 5 * MS_PER_SEC; + assert_equals(anim.effect.getComputedTiming().progress, 0.25, + 'Value of progress'); + anim.finish() + assert_equals(anim.effect.getComputedTiming().progress, 0.75, + 'Value of progress'); +}, 'progress of a non-integral repeating animation ' + + 'with alternate-reversing direction'); + +test(function(t) { + var div = addDiv(t, {style: 'animation: moveAnimation 0s 10.25 both alternate'}); + var anim = div.getAnimations()[0]; + + assert_equals(anim.effect.getComputedTiming().progress, 0.25, + 'Initial value of progress'); + anim.currentTime += 2.5 * MS_PER_SEC; + assert_equals(anim.effect.getComputedTiming().progress, 0.25, + 'Value of progress'); + anim.currentTime -= 5 * MS_PER_SEC; + assert_equals(anim.effect.getComputedTiming().progress, 0.0, + 'Value of progress'); + anim.finish() + assert_equals(anim.effect.getComputedTiming().progress, 0.25, + 'Value of progress'); +}, 'progress of a non-integral repeating zero-duration animation ' + + 'with alternate direction'); + +test(function(t) { + var div = addDiv(t, {style: 'animation: moveAnimation 0s 10.25 both alternate-reverse'}); + var anim = div.getAnimations()[0]; + + assert_equals(anim.effect.getComputedTiming().progress, 0.75, + 'Initial value of progress'); + anim.currentTime += 2.5 * MS_PER_SEC; + assert_equals(anim.effect.getComputedTiming().progress, 0.75, + 'Value of progress'); + anim.currentTime -= 5 * MS_PER_SEC; + assert_equals(anim.effect.getComputedTiming().progress, 1.0, + 'Value of progress'); + anim.finish() + assert_equals(anim.effect.getComputedTiming().progress, 0.75, + 'Value of progress'); +}, 'progress of a non-integral repeating zero-duration animation ' + + 'with alternate-reverse direction'); + + +// -------------------- +// currentIteration +// -------------------- +test(function(t) { + var div = addDiv(t, {style: 'animation: moveAnimation 100s 2s'}); + var effect = div.getAnimations()[0].effect; + assert_equals(effect.getComputedTiming().currentIteration, null, + 'Initial value of currentIteration before phase'); +}, 'currentIteration of a new animation with no backwards fill is unresolved ' + + 'in before phase'); + +test(function(t) { + var div = addDiv(t, {style: 'animation: moveAnimation 100s'}); + var anim = div.getAnimations()[0]; + assert_equals(anim.effect.getComputedTiming().currentIteration, 0, + 'Initial value of currentIteration'); +}, 'currentIteration of a new animation is zero'); + +test(function(t) { + // Note: FillMode here is "both" because + // 1. Since this a zero-duration animation, it will already have finished + // so it won't be returned by getAnimations() unless it fills forwards. + // 2. Fill backwards, so the currentIteration (before phase) wouldn't be + // unresolved (null value). + var div = addDiv(t, {style: 'animation: moveAnimation 0s infinite both'}); + var anim = div.getAnimations()[0]; + + assert_equals(anim.effect.getComputedTiming().currentIteration, Infinity, + 'Initial value of currentIteration in after phase'); + + // Seek backwards + anim.currentTime -= 2 * MS_PER_SEC; + assert_equals(anim.effect.getComputedTiming().currentIteration, 0, + 'Value of currentIteration count during before phase'); +}, 'currentIteration of an infinitely repeating zero-duration animation'); + +test(function(t) { + var div = addDiv(t, {style: 'animation: moveAnimation 0s 10.5 both'}); + var anim = div.getAnimations()[0]; + + // Note: currentIteration = ceil(iteration start + iteration count) - 1 + assert_equals(anim.effect.getComputedTiming().currentIteration, 10, + 'Initial value of currentIteration'); + + // Seek backwards + anim.currentTime -= 2 * MS_PER_SEC; + assert_equals(anim.effect.getComputedTiming().currentIteration, 0, + 'Value of currentIteration count during before phase'); +}, 'currentIteration of a finitely repeating zero-duration animation'); + +test(function(t) { + var div = addDiv(t, {style: 'animation: moveAnimation 100s 5.5 forwards'}); + var anim = div.getAnimations()[0]; + + assert_equals(anim.effect.getComputedTiming().currentIteration, 0, + 'Initial value of currentIteration'); + // The 3rd iteration + // Note: currentIteration = floor(scaled active time / iteration duration) + anim.currentTime = 250 * MS_PER_SEC; + assert_equals(anim.effect.getComputedTiming().currentIteration, 2, + 'Value of currentIteration during the 3rd iteration'); + // Finish + anim.finish(); + assert_equals(anim.effect.getComputedTiming().currentIteration, 5, + 'Value of currentIteration in after phase'); +}, 'currentIteration of an animation with a non-integral iteration count'); + +test(function(t) { + var div = addDiv(t, {style: 'animation: moveAnimation 100s 2 forwards'}); + var anim = div.getAnimations()[0]; + + assert_equals(anim.effect.getComputedTiming().currentIteration, 0, + 'Initial value of currentIteration'); + // Finish + anim.finish(); + assert_equals(anim.effect.getComputedTiming().currentIteration, 1, + 'Value of currentIteration in after phase'); +}, 'currentIteration of an animation with an integral iteration count'); + +test(function(t) { + var div = addDiv(t, {style: 'animation: moveAnimation 100s forwards'}); + var anim = div.getAnimations()[0]; + assert_equals(anim.effect.getComputedTiming().currentIteration, 0, + 'Initial value of currentIteration'); + // Finish + anim.finish(); + assert_equals(anim.effect.getComputedTiming().currentIteration, 0, + 'Value of currentIteration in after phase'); +}, 'currentIteration of an animation with a default iteration count'); + +test(function(t) { + var div = addDiv(t); + var effect = new KeyframeEffectReadOnly(div, {left: ["0px", "100px"]}); + + assert_equals(effect.getComputedTiming().currentIteration, null, + 'currentIteration for orphaned effect'); +}, 'currentIteration of an AnimationEffect without an Animation'); + +// TODO: If iteration duration is Infinity, currentIteration is 0. +// However, we cannot set iteration duration to Infinity in CSS Animation now. + +done(); +</script> +</body> diff --git a/dom/animation/test/css-animations/file_animation-currenttime.html b/dom/animation/test/css-animations/file_animation-currenttime.html new file mode 100644 index 0000000000..ec6fb3f1a8 --- /dev/null +++ b/dom/animation/test/css-animations/file_animation-currenttime.html @@ -0,0 +1,345 @@ +<!doctype html> +<html> + <head> + <meta charset=utf-8> + <title>Tests for the effect of setting a CSS animation's + Animation.currentTime</title> + <style> + +.animated-div { + margin-left: 10px; + /* Make it easier to calculate expected values: */ + animation-timing-function: linear ! important; +} + +@keyframes anim { + from { margin-left: 100px; } + to { margin-left: 200px; } +} + + </style> + <script src="../testcommon.js"></script> + </head> + <body> + <script type="text/javascript"> + +'use strict'; + +// TODO: We should separate this test(Testing for CSS Animation events / +// Testing for currentTime of Web Animation). +// e.g: +// CSS Animation events test : +// - check the firing an event using Animation.currentTime +// The current Time of Web Animation test : +// - check an current time value on several situation(init / processing..) +// - Based on W3C Spec, check the behavior of setting current time. + +// TODO: Once the computedTiming property is implemented, add checks to the +// checker helpers to ensure that computedTiming's properties are updated as +// expected. +// See https://bugzilla.mozilla.org/show_bug.cgi?id=1108055 + +const CSS_ANIM_EVENTS = + ['animationstart', 'animationiteration', 'animationend']; + +test(function(t) +{ + var div = addDiv(t, {'class': 'animated-div'}); + div.style.animation = "anim 100s"; + var animation = div.getAnimations()[0]; + + // Animations shouldn't start until the next paint tick, so: + assert_equals(animation.currentTime, 0, + 'Animation.currentTime should be zero when an animation ' + + 'is initially created'); + + // Make sure the animation is running before we set the current time. + animation.startTime = animation.timeline.currentTime; + + animation.currentTime = 50 * MS_PER_SEC; + assert_times_equal(animation.currentTime, 50 * MS_PER_SEC, + 'Check setting of currentTime actually works'); +}, 'Sanity test to check round-tripping assigning to new animation\'s ' + + 'currentTime'); + +promise_test(function(t) { + var div = addDiv(t, {'class': 'animated-div'}); + var eventWatcher = new EventWatcher(t, div, CSS_ANIM_EVENTS); + div.style.animation = "anim 100s 100s"; + var animation = div.getAnimations()[0]; + + return animation.ready.then(function() { + // the 0.0001 here is for rounding error + assert_less_than_equal(animation.currentTime, + animation.timeline.currentTime - animation.startTime + 0.0001, + 'Animation.currentTime should be less than the local time ' + + 'equivalent of the timeline\'s currentTime on the first paint tick ' + + 'after animation creation'); + + animation.currentTime = 100 * MS_PER_SEC; + return eventWatcher.wait_for('animationstart'); + }).then(function() { + animation.currentTime = 200 * MS_PER_SEC; + return eventWatcher.wait_for('animationend'); + }); +}, 'Skipping forward through animation'); + +promise_test(function(t) { + var div = addDiv(t, {'class': 'animated-div'}); + var eventWatcher = new EventWatcher(t, div, CSS_ANIM_EVENTS); + div.style.animation = "anim 100s 100s"; + var animation = div.getAnimations()[0]; + animation.currentTime = 200 * MS_PER_SEC; + var previousTimelineTime = animation.timeline.currentTime; + + return eventWatcher.wait_for(['animationstart', + 'animationend']).then(function() { + assert_true(document.timeline.currentTime - previousTimelineTime < + 100 * MS_PER_SEC, + 'Sanity check that seeking worked rather than the events ' + + 'firing after normal playback through the very long ' + + 'animation duration'); + + animation.currentTime = 150 * MS_PER_SEC; + return eventWatcher.wait_for('animationstart'); + }).then(function() { + animation.currentTime = 0; + return eventWatcher.wait_for('animationend'); + }); +}, 'Skipping backwards through animation'); + +// Next we have multiple tests to check that redundant currentTime changes do +// NOT dispatch events. It's impossible to distinguish between events not being +// dispatched and events just taking an incredibly long time to dispatch +// without waiting an infinitely long time. Obviously we don't want to do that +// (block this test from finishing forever), so instead we just listen for +// events until two animation frames (i.e. requestAnimationFrame callbacks) +// have happened, then assume that no events will ever be dispatched for the +// redundant changes if no events were detected in that time. + +promise_test(function(t) { + var div = addDiv(t, {'class': 'animated-div'}); + var eventWatcher = new EventWatcher(t, div, CSS_ANIM_EVENTS); + div.style.animation = "anim 100s 100s"; + var animation = div.getAnimations()[0]; + + animation.currentTime = 150 * MS_PER_SEC; + animation.currentTime = 50 * MS_PER_SEC; + + return waitForAnimationFrames(2); +}, 'Redundant change, before -> active, then back'); + +promise_test(function(t) { + var div = addDiv(t, {'class': 'animated-div'}); + var eventWatcher = new EventWatcher(t, div, CSS_ANIM_EVENTS); + div.style.animation = "anim 100s 100s"; + var animation = div.getAnimations()[0]; + + animation.currentTime = 250 * MS_PER_SEC; + animation.currentTime = 50 * MS_PER_SEC; + + return waitForAnimationFrames(2); +}, 'Redundant change, before -> after, then back'); + +promise_test(function(t) { + var div = addDiv(t, {'class': 'animated-div'}); + var eventWatcher = new EventWatcher(t, div, CSS_ANIM_EVENTS); + div.style.animation = "anim 100s 100s"; + var animation = div.getAnimations()[0]; + + var retPromise = eventWatcher.wait_for('animationstart').then(function() { + animation.currentTime = 50 * MS_PER_SEC; + animation.currentTime = 150 * MS_PER_SEC; + + return waitForAnimationFrames(2); + }); + // get us into the initial state: + animation.currentTime = 150 * MS_PER_SEC; + + return retPromise; +}, 'Redundant change, active -> before, then back'); + +promise_test(function(t) { + var div = addDiv(t, {'class': 'animated-div'}); + var eventWatcher = new EventWatcher(t, div, CSS_ANIM_EVENTS); + div.style.animation = "anim 100s 100s"; + var animation = div.getAnimations()[0]; + + var retPromise = eventWatcher.wait_for('animationstart').then(function() { + animation.currentTime = 250 * MS_PER_SEC; + animation.currentTime = 150 * MS_PER_SEC; + + return waitForAnimationFrames(2); + }); + // get us into the initial state: + animation.currentTime = 150 * MS_PER_SEC; + + return retPromise; +}, 'Redundant change, active -> after, then back'); + +promise_test(function(t) { + var div = addDiv(t, {'class': 'animated-div'}); + var eventWatcher = new EventWatcher(t, div, CSS_ANIM_EVENTS); + div.style.animation = "anim 100s 100s"; + var animation = div.getAnimations()[0]; + + var retPromise = eventWatcher.wait_for(['animationstart', + 'animationend']).then(function() { + animation.currentTime = 50 * MS_PER_SEC; + animation.currentTime = 250 * MS_PER_SEC; + + return waitForAnimationFrames(2); + }); + // get us into the initial state: + animation.currentTime = 250 * MS_PER_SEC; + + return retPromise; +}, 'Redundant change, after -> before, then back'); + +promise_test(function(t) { + var div = addDiv(t, {'class': 'animated-div'}); + var eventWatcher = new EventWatcher(t, div, CSS_ANIM_EVENTS); + div.style.animation = "anim 100s 100s"; + var animation = div.getAnimations()[0]; + + var retPromise = eventWatcher.wait_for(['animationstart', + 'animationend']).then(function() { + animation.currentTime = 150 * MS_PER_SEC; + animation.currentTime = 250 * MS_PER_SEC; + + return waitForAnimationFrames(2); + }); + // get us into the initial state: + animation.currentTime = 250 * MS_PER_SEC; + + return retPromise; +}, 'Redundant change, after -> active, then back'); + +promise_test(function(t) { + var div = addDiv(t, {'class': 'animated-div'}); + var eventWatcher = new EventWatcher(t, div, CSS_ANIM_EVENTS); + div.style.animation = "anim 100s" + var animation = div.getAnimations()[0]; + + animation.pause(); + animation.currentTime = 150 * MS_PER_SEC; + + return eventWatcher.wait_for(['animationstart', + 'animationend']).then(function() { + animation.currentTime = 50 * MS_PER_SEC; + return eventWatcher.wait_for('animationstart'); + }); +}, 'Seeking finished -> paused dispatches animationstart'); + +promise_test(function(t) { + var div = addDiv(t, {'class': 'animated-div'}); + div.style.animation = "anim 100s"; + + var animation = div.getAnimations()[0]; + + return animation.ready.then(function() { + var exception; + try { + animation.currentTime = null; + } catch (e) { + exception = e; + } + assert_equals(exception.name, 'TypeError', + 'Expect TypeError exception on trying to set ' + + 'Animation.currentTime to null'); + }); +}, 'Setting currentTime to null'); + +promise_test(function(t) { + var div = addDiv(t, {'class': 'animated-div'}); + div.style.animation = 'anim 100s'; + + var animation = div.getAnimations()[0]; + var pauseTime; + + return animation.ready.then(function() { + assert_not_equals(animation.currentTime, null, + 'Animation.currentTime not null on ready Promise resolve'); + animation.pause(); + return animation.ready; + }).then(function() { + pauseTime = animation.currentTime; + return waitForFrame(); + }).then(function() { + assert_equals(animation.currentTime, pauseTime, + 'Animation.currentTime is unchanged after pausing'); + }); +}, 'Animation.currentTime after pausing'); + +promise_test(function(t) { + var div = addDiv(t, {'class': 'animated-div'}); + div.style.animation = "anim 100s"; + var animation = div.getAnimations()[0]; + + return animation.ready.then(function() { + // just before animation ends: + animation.currentTime = 100 * MS_PER_SEC - 1; + return waitForAnimationFrames(2); + }).then(function() { + assert_equals(animation.currentTime, 100 * MS_PER_SEC, + 'Animation.currentTime should not continue to increase after the ' + + 'animation has finished'); + }); +}, 'Animation.currentTime clamping'); + +promise_test(function(t) { + var div = addDiv(t, {'class': 'animated-div'}); + div.style.animation = "anim 100s"; + var animation = div.getAnimations()[0]; + + return animation.ready.then(function() { + // play backwards: + animation.playbackRate = -1; + + // just before animation ends (at the "start"): + animation.currentTime = 1; + + return waitForAnimationFrames(2); + }).then(function() { + assert_equals(animation.currentTime, 0, + 'Animation.currentTime should not continue to decrease after an ' + + 'animation running in reverse has finished and currentTime is zero'); + }); +}, 'Animation.currentTime clamping for reversed animation'); + +test(function(t) { + var div = addDiv(t, {'class': 'animated-div'}); + div.style.animation = 'anim 100s'; + var animation = div.getAnimations()[0]; + animation.cancel(); + + assert_equals(animation.currentTime, null, + 'The currentTime of a cancelled animation should be null'); +}, 'Animation.currentTime after cancelling'); + +promise_test(function(t) { + var div = addDiv(t, {'class': 'animated-div'}); + div.style.animation = 'anim 100s'; + var animation = div.getAnimations()[0]; + + return animation.ready.then(function() { + animation.finish(); + + // Initiate a pause then abort it + animation.pause(); + animation.play(); + + // Wait to return to running state + return animation.ready; + }).then(function() { + assert_true(animation.currentTime < 100 * 1000, + 'After aborting a pause when finished, the currentTime should' + + ' jump back towards the start of the animation'); + }); +}, 'After aborting a pause when finished, the call to play() should rewind' + + ' the current time'); + +done(); + </script> + </body> +</html> diff --git a/dom/animation/test/css-animations/file_animation-finish.html b/dom/animation/test/css-animations/file_animation-finish.html new file mode 100644 index 0000000000..996cb2ce7f --- /dev/null +++ b/dom/animation/test/css-animations/file_animation-finish.html @@ -0,0 +1,97 @@ +<!doctype html> +<meta charset=utf-8> +<script src="../testcommon.js"></script> +<style> +@keyframes anim { + from { margin-left: 100px; } + to { margin-left: 200px; } +} +</style> +<body> +<script> + +'use strict'; + +const ANIM_PROP_VAL = 'anim 100s'; +const ANIM_DURATION = 100000; // ms + +test(function(t) { + var div = addDiv(t); + div.style.animation = ANIM_PROP_VAL; + div.style.animationIterationCount = 'infinite'; + var animation = div.getAnimations()[0]; + + var threw = false; + try { + animation.finish(); + } catch (e) { + threw = true; + assert_equals(e.name, 'InvalidStateError', + 'Exception should be an InvalidStateError exception when ' + + 'trying to finish an infinite animation'); + } + assert_true(threw, + 'Expect InvalidStateError exception trying to finish an ' + + 'infinite animation'); +}, 'Test exceptions when finishing infinite animation'); + +async_test(function(t) { + var div = addDiv(t); + div.style.animation = ANIM_PROP_VAL + ' paused'; + var animation = div.getAnimations()[0]; + + animation.ready.then(t.step_func(function() { + animation.finish(); + assert_equals(animation.playState, 'finished', + 'The play state of a paused animation should become ' + + '"finished" after finish() is called'); + assert_approx_equals(animation.startTime, + animation.timeline.currentTime - ANIM_DURATION, + 0.0001, + 'The start time of a paused animation should be set ' + + 'after calling finish()'); + t.done(); + })); +}, 'Test finish() while paused'); + +test(function(t) { + var div = addDiv(t); + div.style.animation = ANIM_PROP_VAL + ' paused'; + var animation = div.getAnimations()[0]; + + // Update playbackRate so we can test that the calculated startTime + // respects it + animation.playbackRate = 2; + + // While animation is still pause-pending call finish() + animation.finish(); + + assert_equals(animation.playState, 'finished', + 'The play state of a pause-pending animation should become ' + + '"finished" after finish() is called'); + assert_approx_equals(animation.startTime, + animation.timeline.currentTime - ANIM_DURATION / 2, + 0.0001, + 'The start time of a pause-pending animation should ' + + 'be set after calling finish()'); +}, 'Test finish() while pause-pending with positive playbackRate'); + +test(function(t) { + var div = addDiv(t); + div.style.animation = ANIM_PROP_VAL + ' paused'; + var animation = div.getAnimations()[0]; + + animation.playbackRate = -2; + animation.finish(); + + assert_equals(animation.playState, 'finished', + 'The play state of a pause-pending animation should become ' + + '"finished" after finish() is called'); + assert_equals(animation.startTime, animation.timeline.currentTime, + 'The start time of a pause-pending animation should be ' + + 'set after calling finish()'); +}, 'Test finish() while pause-pending with negative playbackRate'); + +done(); +</script> +</body> diff --git a/dom/animation/test/css-animations/file_animation-finished.html b/dom/animation/test/css-animations/file_animation-finished.html new file mode 100644 index 0000000000..c296abb117 --- /dev/null +++ b/dom/animation/test/css-animations/file_animation-finished.html @@ -0,0 +1,93 @@ +<!doctype html> +<meta charset=utf-8> +<script src="../testcommon.js"></script> +<style> +@keyframes abc { + to { transform: translate(10px) } +} +@keyframes def {} +</style> +<body> +<script> +'use strict'; + +const ANIM_PROP_VAL = 'abc 100s'; +const ANIM_DURATION = 100 * MS_PER_SEC; + +promise_test(function(t) { + var div = addDiv(t); + // Set up pending animation + div.style.animation = ANIM_PROP_VAL; + var animation = div.getAnimations()[0]; + var previousFinishedPromise = animation.finished; + // Set up listeners on finished promise + var retPromise = animation.finished.then(function() { + assert_unreached('finished promise is fulfilled'); + }).catch(function(err) { + assert_equals(err.name, 'AbortError', + 'finished promise is rejected with AbortError'); + assert_not_equals(animation.finished, previousFinishedPromise, + 'Finished promise should change after the original is ' + + 'rejected'); + }); + + // Now cancel the animation and flush styles + div.style.animation = ''; + window.getComputedStyle(div).animation; + + return retPromise; +}, 'finished promise is rejected when an animation is cancelled by resetting ' + + 'the animation property'); + +promise_test(function(t) { + var div = addDiv(t); + // As before, but this time instead of removing all animations, simply update + // the list of animations. At least for Firefox, updating is a different + // code path. + + // Set up pending animation + div.style.animation = ANIM_PROP_VAL; + var animation = div.getAnimations()[0]; + var previousFinishedPromise = animation.finished; + + // Set up listeners on finished promise + var retPromise = animation.finished.then(function() { + assert_unreached('finished promise was fulfilled'); + }).catch(function(err) { + assert_equals(err.name, 'AbortError', + 'finished promise is rejected with AbortError'); + assert_not_equals(animation.finished, previousFinishedPromise, + 'Finished promise should change after the original is ' + + 'rejected'); + }); + + // Now update the animation and flush styles + div.style.animation = 'def 100s'; + window.getComputedStyle(div).animation; + + return retPromise; +}, 'finished promise is rejected when an animation is cancelled by changing ' + + 'the animation property'); + +promise_test(function(t) { + var div = addDiv(t); + div.style.animation = ANIM_PROP_VAL; + var animation = div.getAnimations()[0]; + var previousFinishedPromise = animation.finished; + animation.currentTime = ANIM_DURATION; + return animation.finished.then(function() { + div.style.animationPlayState = 'running'; + return waitForAnimationFrames(2); + }).then(function() { + assert_equals(animation.finished, previousFinishedPromise, + 'Should not replay when animation-play-state changes to ' + + '"running" on finished animation'); + assert_equals(animation.currentTime, ANIM_DURATION, + 'currentTime should not change when animation-play-state ' + + 'changes to "running" on finished animation'); + }); +}, 'Test finished promise changes when animationPlayState set to running'); + +done(); +</script> +</body> diff --git a/dom/animation/test/css-animations/file_animation-id.html b/dom/animation/test/css-animations/file_animation-id.html new file mode 100644 index 0000000000..dbd5ee0eef --- /dev/null +++ b/dom/animation/test/css-animations/file_animation-id.html @@ -0,0 +1,24 @@ +<!doctype html> +<meta charset=utf-8> +<script src="../testcommon.js"></script> +<style> +@keyframes abc { } +</style> +<body> +<script> +'use strict'; + +test(function(t) { + var div = addDiv(t); + div.style.animation = 'abc 100s'; + var animation = div.getAnimations()[0]; + assert_equals(animation.id, '', 'id for CSS Animation is initially empty'); + animation.id = 'anim' + + assert_equals(animation.id, 'anim', 'animation.id reflects the value set'); +}, 'Animation.id for CSS Animations'); + +done(); +</script> +</body> +</html> diff --git a/dom/animation/test/css-animations/file_animation-pausing.html b/dom/animation/test/css-animations/file_animation-pausing.html new file mode 100644 index 0000000000..7176a0c1d6 --- /dev/null +++ b/dom/animation/test/css-animations/file_animation-pausing.html @@ -0,0 +1,165 @@ +<!doctype html> +<meta charset=utf-8> +<script src="../testcommon.js"></script> +<style> +@keyframes anim { + 0% { margin-left: 0px } + 100% { margin-left: 10000px } +} +</style> +<body> +<script> +'use strict'; + +function getMarginLeft(cs) { + return parseFloat(cs.marginLeft); +} + +promise_test(function(t) { + var div = addDiv(t); + var cs = window.getComputedStyle(div); + div.style.animation = 'anim 1000s paused'; + var animation = div.getAnimations()[0]; + assert_equals(getMarginLeft(cs), 0, + 'Initial value of margin-left is zero'); + animation.play(); + + return animation.ready.then(waitForFrame).then(function() { + assert_true(getMarginLeft(cs) > 0, + 'Playing value of margin-left is greater than zero'); + }); +}, 'play() overrides animation-play-state'); + +promise_test(function(t) { + var div = addDiv(t); + var cs = window.getComputedStyle(div); + div.style.animation = 'anim 1000s paused'; + var animation = div.getAnimations()[0]; + assert_equals(getMarginLeft(cs), 0, + 'Initial value of margin-left is zero'); + + animation.pause(); + div.style.animationPlayState = 'running'; + + return animation.ready.then(waitForFrame).then(function() { + assert_equals(cs.animationPlayState, 'running', + 'animation-play-state is running'); + assert_equals(getMarginLeft(cs), 0, + 'Paused value of margin-left is zero'); + }); +}, 'pause() overrides animation-play-state'); + +promise_test(function(t) { + var div = addDiv(t); + var cs = window.getComputedStyle(div); + div.style.animation = 'anim 1000s paused'; + var animation = div.getAnimations()[0]; + assert_equals(getMarginLeft(cs), 0, + 'Initial value of margin-left is zero'); + animation.play(); + var previousAnimVal; + + return animation.ready.then(function() { + div.style.animationPlayState = 'running'; + cs.animationPlayState; // Trigger style resolution + return waitForFrame(); + }).then(function() { + assert_equals(cs.animationPlayState, 'running', + 'animation-play-state is running'); + div.style.animationPlayState = 'paused'; + return animation.ready; + }).then(function() { + assert_equals(cs.animationPlayState, 'paused', + 'animation-play-state is paused'); + previousAnimVal = getMarginLeft(cs); + return waitForFrame(); + }).then(function() { + assert_equals(getMarginLeft(cs), previousAnimVal, + 'Animated value of margin-left does not change when' + + ' paused by style'); + }); +}, 'play() is overridden by later setting "animation-play-state: paused"'); + +promise_test(function(t) { + var div = addDiv(t); + var cs = window.getComputedStyle(div); + div.style.animation = 'anim 1000s'; + var animation = div.getAnimations()[0]; + assert_equals(getMarginLeft(cs), 0, + 'Initial value of margin-left is zero'); + + // Set the specified style first. If implementations fail to + // apply the style changes first, they will ignore the redundant + // call to play() and fail to correctly override the pause style. + div.style.animationPlayState = 'paused'; + animation.play(); + var previousAnimVal = getMarginLeft(cs); + + return animation.ready.then(waitForFrame).then(function() { + assert_equals(cs.animationPlayState, 'paused', + 'animation-play-state is paused'); + assert_true(getMarginLeft(cs) > previousAnimVal, + 'Playing value of margin-left is increasing'); + }); +}, 'play() flushes pending changes to animation-play-state first'); + +promise_test(function(t) { + var div = addDiv(t); + var cs = window.getComputedStyle(div); + div.style.animation = 'anim 1000s paused'; + var animation = div.getAnimations()[0]; + assert_equals(getMarginLeft(cs), 0, + 'Initial value of margin-left is zero'); + + // Unlike the previous test for play(), since calling pause() is sticky, + // we'll apply it even if the underlying style also says we're paused. + // + // We would like to test that implementations flush styles before running + // pause() but actually there's no style we can currently set that will + // change the behavior of pause(). That may change in the future + // (e.g. if we introduce animation-timeline or animation-playback-rate etc.). + // + // For now this just serves as a sanity check that we do the same thing + // even if we set style before calling the API. + div.style.animationPlayState = 'running'; + animation.pause(); + var previousAnimVal = getMarginLeft(cs); + + return animation.ready.then(waitForFrame).then(function() { + assert_equals(cs.animationPlayState, 'running', + 'animation-play-state is running'); + assert_equals(getMarginLeft(cs), previousAnimVal, + 'Paused value of margin-left does not change'); + }); +}, 'pause() applies pending changes to animation-play-state first'); +// (Note that we can't actually test for this; see comment above, in test-body.) + +promise_test(function(t) { + var div = addDiv(t, { style: 'animation: anim 1000s' }); + var animation = div.getAnimations()[0]; + var readyPromiseRun = false; + + return animation.ready.then(function() { + div.style.animationPlayState = 'paused'; + assert_equals(animation.playState, 'pending', 'Animation is pause pending'); + + // Set current time + animation.currentTime = 5 * MS_PER_SEC; + assert_equals(animation.playState, 'paused', + 'Animation is paused immediately after setting currentTime'); + assert_equals(animation.startTime, null, + 'Animation startTime is unresolved immediately after ' + + 'setting currentTime'); + assert_equals(animation.currentTime, 5 * MS_PER_SEC, + 'Animation currentTime does not change when forcing a ' + + 'pause operation to complete'); + + // The ready promise should now be resolved. If it's not then test will + // probably time out before anything else happens that causes it to resolve. + return animation.ready; + }); +}, 'Setting the current time completes a pending pause'); + +done(); +</script> +</body> diff --git a/dom/animation/test/css-animations/file_animation-playstate.html b/dom/animation/test/css-animations/file_animation-playstate.html new file mode 100644 index 0000000000..ce9839f382 --- /dev/null +++ b/dom/animation/test/css-animations/file_animation-playstate.html @@ -0,0 +1,71 @@ +<!doctype html> +<meta charset=utf-8> +<script src="../testcommon.js"></script> +<style> +@keyframes anim { } +</style> +<body> +<script> +'use strict'; + +promise_test(function(t) { + var div = addDiv(t); + var cs = window.getComputedStyle(div); + div.style.animation = 'anim 1000s'; + var animation = div.getAnimations()[0]; + assert_equals(animation.playState, 'pending'); + + return animation.ready.then(function() { + assert_equals(animation.playState, 'running'); + }); +}, 'Animation returns correct playState when running'); + +promise_test(function(t) { + var div = addDiv(t); + var cs = window.getComputedStyle(div); + div.style.animation = 'anim 1000s paused'; + var animation = div.getAnimations()[0]; + assert_equals(animation.playState, 'pending'); + + return animation.ready.then(function() { + assert_equals(animation.playState, 'paused'); + }); +}, 'Animation returns correct playState when paused'); + +promise_test(function(t) { + var div = addDiv(t); + var cs = window.getComputedStyle(div); + div.style.animation = 'anim 1000s'; + var animation = div.getAnimations()[0]; + animation.pause(); + assert_equals(animation.playState, 'pending'); + + return animation.ready.then(function() { + assert_equals(animation.playState, 'paused'); + }); +}, 'Animation.playState updates when paused by script'); + +test(function(t) { + var div = addDiv(t); + var cs = window.getComputedStyle(div); + div.style.animation = 'anim 1000s paused'; + var animation = div.getAnimations()[0]; + div.style.animationPlayState = 'running'; + + // This test also checks that calling playState flushes style + assert_equals(animation.playState, 'pending', + 'Animation.playState reports pending after updating' + + ' animation-play-state (got: ' + animation.playState + ')'); +}, 'Animation.playState updates when resumed by setting style'); + +test(function(t) { + var div = addDiv(t); + div.style.animation = 'anim 1000s'; + var animation = div.getAnimations()[0]; + animation.cancel(); + assert_equals(animation.playState, 'idle'); +}, 'Animation returns correct playState when cancelled'); + +done(); +</script> +</body> diff --git a/dom/animation/test/css-animations/file_animation-ready.html b/dom/animation/test/css-animations/file_animation-ready.html new file mode 100644 index 0000000000..9318a1a182 --- /dev/null +++ b/dom/animation/test/css-animations/file_animation-ready.html @@ -0,0 +1,149 @@ +<!doctype html> +<meta charset=utf-8> +<script src="../testcommon.js"></script> +<style> +@keyframes abc { + to { transform: translate(10px) } +} +</style> +<body> +<script> +'use strict'; + +promise_test(function(t) { + var div = addDiv(t); + div.style.animation = 'abc 100s paused'; + var animation = div.getAnimations()[0]; + var originalReadyPromise = animation.ready; + + return animation.ready.then(function() { + div.style.animationPlayState = 'running'; + assert_not_equals(animation.ready, originalReadyPromise, + 'After updating animation-play-state a new ready promise' + + ' object is created'); + }); +}, 'A new ready promise is created when setting animation-play-state: running'); + +promise_test(function(t) { + var div = addDiv(t); + + // Set up pending animation + div.style.animation = 'abc 100s'; + var animation = div.getAnimations()[0]; + assert_equals(animation.playState, 'pending', + 'Animation is initially pending'); + + // Set up listeners on ready promise + var retPromise = animation.ready.then(function() { + assert_unreached('ready promise is fulfilled'); + }).catch(function(err) { + assert_equals(err.name, 'AbortError', + 'ready promise is rejected with AbortError'); + }); + + // Now cancel the animation and flush styles + div.style.animation = ''; + window.getComputedStyle(div).animation; + + return retPromise; +}, 'ready promise is rejected when an animation is cancelled by resetting' + + ' the animation property'); + +promise_test(function(t) { + var div = addDiv(t); + + // As before, but this time instead of removing all animations, simply update + // the list of animations. At least for Firefox, updating is a different + // code path. + + // Set up pending animation + div.style.animation = 'abc 100s'; + var animation = div.getAnimations()[0]; + assert_equals(animation.playState, 'pending', + 'Animation is initially pending'); + + // Set up listeners on ready promise + var retPromise = animation.ready.then(function() { + assert_unreached('ready promise was fulfilled'); + }).catch(function(err) { + assert_equals(err.name, 'AbortError', + 'ready promise is rejected with AbortError'); + }); + + // Now update the animation and flush styles + div.style.animation = 'def 100s'; + window.getComputedStyle(div).animation; + + return retPromise; +}, 'ready promise is rejected when an animation is cancelled by updating' + + ' the animation property'); + +promise_test(function(t) { + var div = addDiv(t, { style: 'animation: abc 100s' }); + var animation = div.getAnimations()[0]; + var originalReadyPromise = animation.ready; + + return animation.ready.then(function() { + div.style.animationPlayState = 'paused'; + assert_not_equals(animation.ready, originalReadyPromise, + 'A new Promise object is generated when setting' + + ' animation-play-state: paused'); + }); +}, 'A new ready promise is created when setting animation-play-state: paused'); + +promise_test(function(t) { + var div = addDiv(t, { style: 'animation: abc 100s' }); + var animation = div.getAnimations()[0]; + + return animation.ready.then(function() { + div.style.animationPlayState = 'paused'; + var firstReadyPromise = animation.ready; + animation.pause(); + assert_equals(animation.ready, firstReadyPromise, + 'Ready promise objects are identical after redundant pause'); + }); +}, 'Pausing twice re-uses the same Promise'); + +promise_test(function(t) { + var div = addDiv(t, { style: 'animation: abc 100s' }); + var animation = div.getAnimations()[0]; + + return animation.ready.then(function() { + div.style.animationPlayState = 'paused'; + + // Flush style and verify we're pending at the same time + assert_equals(animation.playState, 'pending', 'Animation is pending'); + var pauseReadyPromise = animation.ready; + + // Now play again immediately + div.style.animationPlayState = 'running'; + assert_equals(animation.playState, 'pending', 'Animation is still pending'); + assert_equals(animation.ready, pauseReadyPromise, + 'The pause Promise is re-used when playing while waiting' + + ' to pause'); + + return animation.ready; + }).then(function() { + assert_equals(animation.playState, 'running', + 'Animation is running after aborting a pause'); + }); +}, 'If a pause operation is interrupted, the ready promise is reused'); + +promise_test(function(t) { + var div = addDiv(t, { style: 'animation: abc 100s' }); + var animation = div.getAnimations()[0]; + + return animation.ready.then(function() { + div.style.animationPlayState = 'paused'; + return animation.ready; + }).then(function(resolvedAnimation) { + assert_equals(resolvedAnimation, animation, + 'Promise received when ready Promise for a pause operation' + + ' is completed is the animation on which the pause was' + + ' performed'); + }); +}, 'When a pause is complete the Promise callback gets the correct animation'); + +done(); +</script> +</body> diff --git a/dom/animation/test/css-animations/file_animation-reverse.html b/dom/animation/test/css-animations/file_animation-reverse.html new file mode 100644 index 0000000000..5060fa55f1 --- /dev/null +++ b/dom/animation/test/css-animations/file_animation-reverse.html @@ -0,0 +1,29 @@ +<!doctype html> +<meta charset=utf-8> +<script src="../testcommon.js"></script> +<style> +@keyframes anim { + to { transform: translate(100px) } +} +</style> +<body> +<script> +'use strict'; + +test(function(t) { + var div = addDiv(t, { style: 'animation: anim 100s' }); + var animation = div.getAnimations()[0]; + div.style.animation = ""; + flushComputedStyle(div); + + assert_equals(animation.currentTime, null); + animation.reverse(); + + assert_equals(animation.currentTime, 100 * MS_PER_SEC, + 'animation.currentTime should be its effect end'); +}, 'reverse() from idle state starts playing the animation'); + + +done(); +</script> +</body> diff --git a/dom/animation/test/css-animations/file_animation-starttime.html b/dom/animation/test/css-animations/file_animation-starttime.html new file mode 100644 index 0000000000..46144464c0 --- /dev/null +++ b/dom/animation/test/css-animations/file_animation-starttime.html @@ -0,0 +1,383 @@ +<!doctype html> +<html> + <head> + <meta charset=utf-8> + <title>Tests for the effect of setting a CSS animation's + Animation.startTime</title> + <style> + +.animated-div { + margin-left: 10px; + /* Make it easier to calculate expected values: */ + animation-timing-function: linear ! important; +} + +@keyframes anim { + from { margin-left: 100px; } + to { margin-left: 200px; } +} + + </style> + <script src="../testcommon.js"></script> + </head> + <body> + <script type="text/javascript"> + +'use strict'; + +// TODO: We should separate this test(Testing for CSS Animation events / +// Testing for start time of Web Animation). +// e.g: +// CSS Animation events test: +// - check the firing an event after setting an Animation.startTime +// The start time of Web Animation test: +// - check an start time value on several situation(init / processing..) +// - Based on W3C Spec, check the behavior of setting current time. + +// TODO: Once the computedTiming property is implemented, add checks to the +// checker helpers to ensure that computedTiming's properties are updated as +// expected. +// See https://bugzilla.mozilla.org/show_bug.cgi?id=1108055 + +const CSS_ANIM_EVENTS = + ['animationstart', 'animationiteration', 'animationend']; + +test(function(t) +{ + var div = addDiv(t, { 'style': 'animation: anim 100s' }); + var animation = div.getAnimations()[0]; + + assert_equals(animation.startTime, null, 'startTime is unresolved'); +}, 'startTime of a newly created (play-pending) animation is unresolved'); + +test(function(t) +{ + var div = addDiv(t, { 'style': 'animation: anim 100s paused' }); + var animation = div.getAnimations()[0]; + assert_equals(animation.startTime, null, 'startTime is unresolved'); +}, 'startTime of a newly created (pause-pending) animation is unresolved'); + +promise_test(function(t) +{ + var div = addDiv(t, { 'style': 'animation: anim 100s' }); + var animation = div.getAnimations()[0]; + + return animation.ready.then(function() { + assert_true(animation.startTime > 0, + 'startTime is resolved when running'); + }); +}, 'startTime is resolved when running'); + +promise_test(function(t) +{ + var div = addDiv(t, { 'style': 'animation: anim 100s paused' }); + var animation = div.getAnimations()[0]; + + return animation.ready.then(function() { + assert_equals(animation.startTime, null, + 'startTime is unresolved when paused'); + }); +}, 'startTime is unresolved when paused'); + +promise_test(function(t) +{ + var div = addDiv(t, { 'style': 'animation: anim 100s' }); + var animation = div.getAnimations()[0]; + + return animation.ready.then(function() { + div.style.animationPlayState = 'paused'; + getComputedStyle(div).animationPlayState; + + assert_not_equals(animation.startTime, null, + 'startTime is resolved when pause-pending'); + + div.style.animationPlayState = 'running'; + getComputedStyle(div).animationPlayState; + + assert_not_equals(animation.startTime, null, + 'startTime is preserved when a pause is aborted'); + }); +}, 'startTime while pause-pending and play-pending'); + +promise_test(function(t) { + var div = addDiv(t, { 'style': 'animation: anim 100s' }); + var animation = div.getAnimations()[0]; + // Seek to end to put us in the finished state + animation.currentTime = 100 * MS_PER_SEC; + + return animation.ready.then(function() { + // Call play() which puts us back in the running state + animation.play(); + + assert_equals(animation.startTime, null, 'startTime is unresolved'); + }); +}, 'startTime while play-pending from finished state'); + +test(function(t) { + var div = addDiv(t, { 'style': 'animation: anim 100s' }); + var animation = div.getAnimations()[0]; + animation.finish(); + // Call play() which puts us back in the running state + animation.play(); + + assert_equals(animation.startTime, null, 'startTime is unresolved'); +}, 'startTime while play-pending from finished state using finish()'); + +promise_test(function(t) { + var div = addDiv(t, { style: 'animation: anim 100s' }); + var animation = div.getAnimations()[0]; + + assert_equals(animation.startTime, null, 'The initial startTime is null'); + var initialTimelineTime = document.timeline.currentTime; + + return animation.ready.then(function() { + assert_true(animation.startTime > initialTimelineTime, + 'After the animation has started, startTime is greater than ' + + 'the time when it was started'); + var startTimeBeforePausing = animation.startTime; + + div.style.animationPlayState = 'paused'; + // Flush styles just in case querying animation.startTime doesn't flush + // styles (which would be a bug in of itself and could mask a further bug + // by causing startTime to appear to not change). + getComputedStyle(div).animationPlayState; + + assert_equals(animation.startTime, startTimeBeforePausing, + 'The startTime does not change when pausing-pending'); + return animation.ready; + }).then(function() { + assert_equals(animation.startTime, null, + 'After actually pausing, the startTime of an animation ' + + 'is null'); + }); +}, 'Pausing should make the startTime become null'); + +test(function(t) +{ + var div = addDiv(t, {'class': 'animated-div'}); + div.style.animation = 'anim 100s 100s'; + var animation = div.getAnimations()[0]; + var currentTime = animation.timeline.currentTime; + animation.startTime = currentTime; + + assert_times_equal(animation.startTime, currentTime, + 'Check setting of startTime actually works'); +}, 'Sanity test to check round-tripping assigning to a new animation\'s ' + + 'startTime'); + +promise_test(function(t) { + var div = addDiv(t, {'class': 'animated-div'}); + var eventWatcher = new EventWatcher(t, div, CSS_ANIM_EVENTS); + div.style.animation = 'anim 100s 100s'; + var animation = div.getAnimations()[0]; + + return animation.ready.then(function() { + assert_less_than_equal(animation.startTime, animation.timeline.currentTime, + 'Animation.startTime should be less than the timeline\'s ' + + 'currentTime on the first paint tick after animation creation'); + + animation.startTime = animation.timeline.currentTime - 100 * MS_PER_SEC; + return eventWatcher.wait_for('animationstart'); + }).then(function() { + animation.startTime = animation.timeline.currentTime - 200 * MS_PER_SEC; + return eventWatcher.wait_for('animationend'); + }); +}, 'Skipping forward through animation'); + +promise_test(function(t) { + var div = addDiv(t, {'class': 'animated-div'}); + var eventWatcher = new EventWatcher(t, div, CSS_ANIM_EVENTS); + div.style.animation = 'anim 100s 100s'; + var animation = div.getAnimations()[0]; + animation.startTime = animation.timeline.currentTime - 200 * MS_PER_SEC; + var previousTimelineTime = animation.timeline.currentTime; + + return eventWatcher.wait_for(['animationstart', + 'animationend']).then(function() { + assert_true(document.timeline.currentTime - previousTimelineTime < + 100 * MS_PER_SEC, + 'Sanity check that seeking worked rather than the events ' + + 'firing after normal playback through the very long ' + + 'animation duration'); + + animation.startTime = animation.timeline.currentTime - 150 * MS_PER_SEC; + + // Despite going backwards from after the end of the animation (to being + // in the active interval), we now expect an 'animationstart' event + // because the animation should go from being inactive to active. + return eventWatcher.wait_for('animationstart'); + }).then(function() { + animation.startTime = animation.timeline.currentTime; + + // Despite going backwards from just after the active interval starts to + // the animation start time, we now expect an animationend event + // because we went from inside to outside the active interval. + return eventWatcher.wait_for('animationend'); + }).then(function() { + assert_less_than_equal(animation.startTime, animation.timeline.currentTime, + 'Animation.startTime should be less than the timeline\'s ' + + 'currentTime on the first paint tick after animation creation'); + }); +}, 'Skipping backwards through animation'); + +// Next we have multiple tests to check that redundant startTime changes do NOT +// dispatch events. It's impossible to distinguish between events not being +// dispatched and events just taking an incredibly long time to dispatch +// without waiting an infinitely long time. Obviously we don't want to do that +// (block this test from finishing forever), so instead we just listen for +// events until two animation frames (i.e. requestAnimationFrame callbacks) +// have happened, then assume that no events will ever be dispatched for the +// redundant changes if no events were detected in that time. + +promise_test(function(t) { + var div = addDiv(t, {'class': 'animated-div'}); + var eventWatcher = new EventWatcher(t, div, CSS_ANIM_EVENTS); + div.style.animation = "anim 100s 100s"; + var animation = div.getAnimations()[0]; + + animation.startTime = animation.timeline.currentTime - 150 * MS_PER_SEC; + animation.startTime = animation.timeline.currentTime - 50 * MS_PER_SEC; + + return waitForAnimationFrames(2); +}, 'Redundant change, before -> active, then back'); + +promise_test(function(t) { + var div = addDiv(t, {'class': 'animated-div'}); + var eventWatcher = new EventWatcher(t, div, CSS_ANIM_EVENTS); + div.style.animation = "anim 100s 100s"; + var animation = div.getAnimations()[0]; + + animation.startTime = animation.timeline.currentTime - 250 * MS_PER_SEC; + animation.startTime = animation.timeline.currentTime - 50 * MS_PER_SEC; + + return waitForAnimationFrames(2); +}, 'Redundant change, before -> after, then back'); + +promise_test(function(t) { + var div = addDiv(t, {'class': 'animated-div'}); + var eventWatcher = new EventWatcher(t, div, CSS_ANIM_EVENTS); + div.style.animation = "anim 100s 100s"; + var animation = div.getAnimations()[0]; + + var retPromise = eventWatcher.wait_for('animationstart').then(function() { + animation.startTime = animation.timeline.currentTime - 50 * MS_PER_SEC; + animation.startTime = animation.timeline.currentTime - 150 * MS_PER_SEC; + + return waitForAnimationFrames(2); + }); + // get us into the initial state: + animation.startTime = animation.timeline.currentTime - 150 * MS_PER_SEC; + + return retPromise; +}, 'Redundant change, active -> before, then back'); + +promise_test(function(t) { + var div = addDiv(t, {'class': 'animated-div'}); + var eventWatcher = new EventWatcher(t, div, CSS_ANIM_EVENTS); + div.style.animation = "anim 100s 100s"; + var animation = div.getAnimations()[0]; + + var retPromise = eventWatcher.wait_for('animationstart').then(function() { + animation.startTime = animation.timeline.currentTime - 250 * MS_PER_SEC; + animation.startTime = animation.timeline.currentTime - 150 * MS_PER_SEC; + + return waitForAnimationFrames(2); + }); + // get us into the initial state: + animation.startTime = animation.timeline.currentTime - 150 * MS_PER_SEC; + + return retPromise; +}, 'Redundant change, active -> after, then back'); + +promise_test(function(t) { + var div = addDiv(t, {'class': 'animated-div'}); + var eventWatcher = new EventWatcher(t, div, CSS_ANIM_EVENTS); + div.style.animation = "anim 100s 100s"; + var animation = div.getAnimations()[0]; + + var retPromise = eventWatcher.wait_for(['animationstart', + 'animationend']).then(function() { + animation.startTime = animation.timeline.currentTime - 50 * MS_PER_SEC; + animation.startTime = animation.timeline.currentTime - 250 * MS_PER_SEC; + + return waitForAnimationFrames(2); + }); + // get us into the initial state: + animation.startTime = animation.timeline.currentTime - 250 * MS_PER_SEC; + + return retPromise; +}, 'Redundant change, after -> before, then back'); + +promise_test(function(t) { + var div = addDiv(t, {'class': 'animated-div'}); + var eventWatcher = new EventWatcher(t, div, CSS_ANIM_EVENTS); + div.style.animation = "anim 100s 100s"; + var animation = div.getAnimations()[0]; + + var retPromise = eventWatcher.wait_for(['animationstart', + 'animationend']).then(function() { + animation.startTime = animation.timeline.currentTime - 150 * MS_PER_SEC; + animation.startTime = animation.timeline.currentTime - 250 * MS_PER_SEC; + + return waitForAnimationFrames(2); + + }); + // get us into the initial state: + animation.startTime = animation.timeline.currentTime - 250 * MS_PER_SEC; + + return retPromise; +}, 'Redundant change, after -> active, then back'); + +promise_test(function(t) { + var div = addDiv(t, {'class': 'animated-div'}); + div.style.animation = 'anim 100s 100s'; + var animation = div.getAnimations()[0]; + var storedCurrentTime; + + return animation.ready.then(function() { + storedCurrentTime = animation.currentTime; + animation.startTime = null; + return animation.ready; + }).then(function() { + assert_equals(animation.currentTime, storedCurrentTime, + 'Test that hold time is correct'); + }); +}, 'Setting startTime to null'); + +promise_test(function(t) { + var div = addDiv(t, {'class': 'animated-div'}); + div.style.animation = 'anim 100s'; + var animation = div.getAnimations()[0]; + + return animation.ready.then(function() { + var savedStartTime = animation.startTime; + + assert_not_equals(animation.startTime, null, + 'Animation.startTime not null on ready Promise resolve'); + + animation.pause(); + return animation.ready; + }).then(function() { + assert_equals(animation.startTime, null, + 'Animation.startTime is null after paused'); + assert_equals(animation.playState, 'paused', + 'Animation.playState is "paused" after pause() call'); + }); +}, 'Animation.startTime after pausing'); + +promise_test(function(t) { + var div = addDiv(t, {'class': 'animated-div'}); + div.style.animation = 'anim 100s'; + var animation = div.getAnimations()[0]; + + return animation.ready.then(function() { + animation.cancel(); + assert_equals(animation.startTime, null, + 'The startTime of a cancelled animation should be null'); + }); +}, 'Animation.startTime after cancelling'); + +done(); + </script> + </body> +</html> diff --git a/dom/animation/test/css-animations/file_animations-dynamic-changes.html b/dom/animation/test/css-animations/file_animations-dynamic-changes.html new file mode 100644 index 0000000000..8f16536ae9 --- /dev/null +++ b/dom/animation/test/css-animations/file_animations-dynamic-changes.html @@ -0,0 +1,154 @@ +<!doctype html> +<meta charset=utf-8> +<script src="../testcommon.js"></script> +<style> +@keyframes anim1 { + to { left: 100px } +} +@keyframes anim2 { } +</style> +<body> +<script> +'use strict'; + +promise_test(function(t) { + var div = addDiv(t); + div.style.animation = 'anim1 100s'; + var originalAnimation = div.getAnimations()[0]; + var originalStartTime; + var originalCurrentTime; + + // Wait a moment so we can confirm the startTime doesn't change (and doesn't + // simply reflect the current time). + return originalAnimation.ready.then(function() { + originalStartTime = originalAnimation.startTime; + originalCurrentTime = originalAnimation.currentTime; + + // Wait a moment so we can confirm the startTime doesn't change (and + // doesn't simply reflect the current time). + return waitForFrame(); + }).then(function() { + div.style.animationDuration = '200s'; + var animation = div.getAnimations()[0]; + assert_equals(animation, originalAnimation, + 'The same Animation is returned after updating' + + ' animation duration'); + assert_equals(animation.startTime, originalStartTime, + 'Animations returned by getAnimations preserve' + + ' their startTime even when they are updated'); + // Sanity check + assert_not_equals(animation.currentTime, originalCurrentTime, + 'Animation.currentTime has updated in next' + + ' requestAnimationFrame callback'); + }); +}, 'Animations preserve their startTime when changed'); + +test(function(t) { + var div = addDiv(t); + div.style.animation = 'anim1 100s, anim1 100s'; + + // Store original state + var animations = div.getAnimations(); + var animation1 = animations[0]; + var animation2 = animations[1]; + + // Update first in list + div.style.animationDuration = '200s, 100s'; + animations = div.getAnimations(); + assert_equals(animations[0], animation1, + 'First Animation is in same position after update'); + assert_equals(animations[1], animation2, + 'Second Animation is in same position after update'); +}, 'Updated Animations maintain their order in the list'); + +promise_test(function(t) { + var div = addDiv(t); + div.style.animation = 'anim1 200s, anim1 100s'; + + // Store original state + var animations = div.getAnimations(); + var animation1 = animations[0]; + var animation2 = animations[1]; + + // Wait before continuing so we can compare start times (otherwise the + // new Animation objects and existing Animation objects will all have the same + // start time). + return waitForAllAnimations(animations).then(waitForFrame).then(function() { + // Swap duration of first and second in list and prepend animation at the + // same time + div.style.animation = 'anim1 100s, anim1 100s, anim1 200s'; + animations = div.getAnimations(); + assert_true(animations[0] !== animation1 && animations[0] !== animation2, + 'New Animation is prepended to start of list'); + assert_equals(animations[1], animation1, + 'First Animation is in second position after update'); + assert_equals(animations[2], animation2, + 'Second Animation is in third position after update'); + assert_equals(animations[1].startTime, animations[2].startTime, + 'Old Animations have the same start time'); + // TODO: Check that animations[0].startTime === null + return animations[0].ready; + }).then(function() { + assert_true(animations[0].startTime > animations[1].startTime, + 'New Animation has later start time'); + }); +}, 'Only the startTimes of existing animations are preserved'); + +promise_test(function(t) { + var div = addDiv(t); + div.style.animation = 'anim1 100s, anim1 100s'; + var secondAnimation = div.getAnimations()[1]; + + // Wait before continuing so we can compare start times + return secondAnimation.ready.then(waitForFrame).then(function() { + // Trim list of animations + div.style.animationName = 'anim1'; + var animations = div.getAnimations(); + assert_equals(animations.length, 1, 'List of Animations was trimmed'); + assert_equals(animations[0], secondAnimation, + 'Remaining Animation is the second one in the list'); + assert_equals(typeof(animations[0].startTime), 'number', + 'Remaining Animation has resolved startTime'); + assert_true(animations[0].startTime < animations[0].timeline.currentTime, + 'Remaining Animation preserves startTime'); + }); +}, 'Animations are removed from the start of the list while preserving' + + ' the state of existing Animations'); + +promise_test(function(t) { + var div = addDiv(t); + div.style.animation = 'anim1 100s'; + var firstAddedAnimation = div.getAnimations()[0], + secondAddedAnimation, + animations; + + // Wait and add second Animation + return firstAddedAnimation.ready.then(waitForFrame).then(function() { + div.style.animation = 'anim1 100s, anim1 100s'; + secondAddedAnimation = div.getAnimations()[0]; + + // Wait again and add another Animation + return secondAddedAnimation.ready.then(waitForFrame); + }).then(function() { + div.style.animation = 'anim1 100s, anim2 100s, anim1 100s'; + animations = div.getAnimations(); + assert_not_equals(firstAddedAnimation, secondAddedAnimation, + 'New Animations are added to start of the list'); + assert_equals(animations[0], secondAddedAnimation, + 'Second Animation remains in same position after' + + ' interleaving'); + assert_equals(animations[2], firstAddedAnimation, + 'First Animation remains in same position after' + + ' interleaving'); + return animations[1].ready; + }).then(function() { + assert_true(animations[1].startTime > animations[0].startTime, + 'Interleaved animation starts later than existing animations'); + assert_true(animations[0].startTime > animations[2].startTime, + 'Original animations retain their start time'); + }); +}, 'Animation state is preserved when interleaving animations in list'); + +done(); +</script> +</body> diff --git a/dom/animation/test/css-animations/file_cssanimation-animationname.html b/dom/animation/test/css-animations/file_cssanimation-animationname.html new file mode 100644 index 0000000000..fd69d85777 --- /dev/null +++ b/dom/animation/test/css-animations/file_cssanimation-animationname.html @@ -0,0 +1,37 @@ +<!doctype html> +<meta charset=utf-8> +<script src="../testcommon.js"></script> +<style> +@keyframes xyz { + to { left: 100px } +} +</style> +<body> +<script> +'use strict'; + +test(function(t) { + var div = addDiv(t); + div.style.animation = 'xyz 100s'; + assert_equals(div.getAnimations()[0].animationName, 'xyz', + 'Animation name matches keyframes rule name'); +}, 'Animation name makes keyframe rule'); + +test(function(t) { + var div = addDiv(t); + div.style.animation = 'x\\yz 100s'; + assert_equals(div.getAnimations()[0].animationName, 'xyz', + 'Escaped animation name matches keyframes rule name'); +}, 'Escaped animation name'); + +test(function(t) { + var div = addDiv(t); + div.style.animation = 'x\\79 z 100s'; + assert_equals(div.getAnimations()[0].animationName, 'xyz', + 'Hex-escaped animation name matches keyframes rule' + + ' name'); +}, 'Animation name with hex-escape'); + +done(); +</script> +</body> diff --git a/dom/animation/test/css-animations/file_document-get-animations.html b/dom/animation/test/css-animations/file_document-get-animations.html new file mode 100644 index 0000000000..abe02d7fc3 --- /dev/null +++ b/dom/animation/test/css-animations/file_document-get-animations.html @@ -0,0 +1,276 @@ +<!doctype html> +<meta charset=utf-8> +<script src="../testcommon.js"></script> +<style> +@keyframes animLeft { + to { left: 100px } +} +@keyframes animTop { + to { top: 100px } +} +@keyframes animBottom { + to { bottom: 100px } +} +@keyframes animRight { + to { right: 100px } +} +</style> +<body> +<script> +'use strict'; + +test(function(t) { + assert_equals(document.getAnimations().length, 0, + 'getAnimations returns an empty sequence for a document' + + ' with no animations'); +}, 'getAnimations for non-animated content'); + +test(function(t) { + var div = addDiv(t); + + // Add an animation + div.style.animation = 'animLeft 100s'; + assert_equals(document.getAnimations().length, 1, + 'getAnimations returns a running CSS Animation'); + + // Add another animation + div.style.animation = 'animLeft 100s, animTop 100s'; + assert_equals(document.getAnimations().length, 2, + 'getAnimations returns two running CSS Animations'); + + // Remove both + div.style.animation = ''; + assert_equals(document.getAnimations().length, 0, + 'getAnimations returns no running CSS Animations'); +}, 'getAnimations for CSS Animations'); + +test(function(t) { + var div = addDiv(t); + div.style.animation = 'animLeft 100s, animTop 100s, animRight 100s, ' + + 'animBottom 100s'; + + var animations = document.getAnimations(); + assert_equals(animations.length, 4, + 'getAnimations returns all running CSS Animations'); + assert_equals(animations[0].animationName, 'animLeft', + 'Order of first animation returned'); + assert_equals(animations[1].animationName, 'animTop', + 'Order of second animation returned'); + assert_equals(animations[2].animationName, 'animRight', + 'Order of third animation returned'); + assert_equals(animations[3].animationName, 'animBottom', + 'Order of fourth animation returned'); +}, 'Order of CSS Animations - within an element'); + +test(function(t) { + var div1 = addDiv(t, { style: 'animation: animLeft 100s' }); + var div2 = addDiv(t, { style: 'animation: animLeft 100s' }); + var div3 = addDiv(t, { style: 'animation: animLeft 100s' }); + var div4 = addDiv(t, { style: 'animation: animLeft 100s' }); + + var animations = document.getAnimations(); + assert_equals(animations.length, 4, + 'getAnimations returns all running CSS Animations'); + assert_equals(animations[0].effect.target, div1, + 'Order of first animation returned'); + assert_equals(animations[1].effect.target, div2, + 'Order of second animation returned'); + assert_equals(animations[2].effect.target, div3, + 'Order of third animation returned'); + assert_equals(animations[3].effect.target, div4, + 'Order of fourth animation returned'); + + // Order should be depth-first pre-order so add some depth as follows: + // + // <parent> + // / | + // 2 3 + // / \ + // 1 4 + // + // Which should give: 2, 1, 4, 3 + div2.appendChild(div1); + div2.appendChild(div4); + animations = document.getAnimations(); + assert_equals(animations[0].effect.target, div2, + 'Order of first animation returned after tree surgery'); + assert_equals(animations[1].effect.target, div1, + 'Order of second animation returned after tree surgery'); + assert_equals(animations[2].effect.target, div4, + 'Order of third animation returned after tree surgery'); + assert_equals(animations[3].effect.target, div3, + 'Order of fourth animation returned after tree surgery'); + +}, 'Order of CSS Animations - across elements'); + +test(function(t) { + var div1 = addDiv(t, { style: 'animation: animLeft 100s, animTop 100s' }); + var div2 = addDiv(t, { style: 'animation: animBottom 100s' }); + + var expectedResults = [ [ div1, 'animLeft' ], + [ div1, 'animTop' ], + [ div2, 'animBottom' ] ]; + var animations = document.getAnimations(); + assert_equals(animations.length, expectedResults.length, + 'getAnimations returns all running CSS Animations'); + animations.forEach(function(anim, i) { + assert_equals(anim.effect.target, expectedResults[i][0], + 'Target of animation in position ' + i); + assert_equals(anim.animationName, expectedResults[i][1], + 'Name of animation in position ' + i); + }); + + // Modify tree structure and animation list + div2.appendChild(div1); + div1.style.animation = 'animLeft 100s, animRight 100s, animTop 100s'; + + expectedResults = [ [ div2, 'animBottom' ], + [ div1, 'animLeft' ], + [ div1, 'animRight' ], + [ div1, 'animTop' ] ]; + animations = document.getAnimations(); + assert_equals(animations.length, expectedResults.length, + 'getAnimations returns all running CSS Animations after ' + + 'making changes'); + animations.forEach(function(anim, i) { + assert_equals(anim.effect.target, expectedResults[i][0], + 'Target of animation in position ' + i + ' after changes'); + assert_equals(anim.animationName, expectedResults[i][1], + 'Name of animation in position ' + i + ' after changes'); + }); +}, 'Order of CSS Animations - across and within elements'); + +test(function(t) { + var div = addDiv(t, { style: 'animation: animLeft 100s, animTop 100s' }); + var animLeft = document.getAnimations()[0]; + assert_equals(animLeft.animationName, 'animLeft', + 'Originally, animLeft animation comes first'); + + // Disassociate animLeft from markup and restart + div.style.animation = 'animTop 100s'; + animLeft.play(); + + var animations = document.getAnimations(); + assert_equals(animations.length, 2, + 'getAnimations returns markup-bound and free animations'); + assert_equals(animations[0].animationName, 'animTop', + 'Markup-bound animations come first'); + assert_equals(animations[1], animLeft, 'Free animations come last'); +}, 'Order of CSS Animations - markup-bound vs free animations'); + +test(function(t) { + var div = addDiv(t, { style: 'animation: animLeft 100s, animTop 100s' }); + var animLeft = document.getAnimations()[0]; + var animTop = document.getAnimations()[1]; + + // Disassociate both animations from markup and restart in opposite order + div.style.animation = ''; + animTop.play(); + animLeft.play(); + + var animations = document.getAnimations(); + assert_equals(animations.length, 2, + 'getAnimations returns free animations'); + assert_equals(animations[0], animTop, + 'Free animations are returned in the order they are started'); + assert_equals(animations[1], animLeft, + 'Animations started later are returned later'); + + // Restarting an animation should have no effect + animTop.cancel(); + animTop.play(); + assert_equals(document.getAnimations()[0], animTop, + 'After restarting, the ordering of free animations' + + ' does not change'); +}, 'Order of CSS Animations - free animations'); + +test(function(t) { + // Add an animation first + var div = addDiv(t, { style: 'animation: animLeft 100s' }); + div.style.top = '0px'; + div.style.transition = 'all 100s'; + flushComputedStyle(div); + + // *Then* add a transition + div.style.top = '100px'; + flushComputedStyle(div); + + // Although the transition was added later, it should come first in the list + var animations = document.getAnimations(); + assert_equals(animations.length, 2, + 'Both CSS animations and transitions are returned'); + assert_class_string(animations[0], 'CSSTransition', 'Transition comes first'); + assert_class_string(animations[1], 'CSSAnimation', 'Animation comes second'); +}, 'Order of CSS Animations and CSS Transitions'); + +test(function(t) { + var div = addDiv(t, { style: 'animation: animLeft 100s forwards' }); + div.getAnimations()[0].finish(); + assert_equals(document.getAnimations().length, 1, + 'Forwards-filling CSS animations are returned'); +}, 'Finished but filling CSS Animations are returned'); + +test(function(t) { + var div = addDiv(t, { style: 'animation: animLeft 100s' }); + div.getAnimations()[0].finish(); + assert_equals(document.getAnimations().length, 0, + 'Non-filling finished CSS animations are not returned'); +}, 'Finished but not filling CSS Animations are not returned'); + +test(function(t) { + var div = addDiv(t, { style: 'animation: animLeft 100s 100s' }); + assert_equals(document.getAnimations().length, 1, + 'Yet-to-start CSS animations are returned'); +}, 'Yet-to-start CSS Animations are returned'); + +test(function(t) { + var div = addDiv(t, { style: 'animation: animLeft 100s' }); + div.getAnimations()[0].cancel(); + assert_equals(document.getAnimations().length, 0, + 'CSS animations cancelled by the API are not returned'); +}, 'CSS Animations cancelled via the API are not returned'); + +test(function(t) { + var div = addDiv(t, { style: 'animation: animLeft 100s' }); + var anim = div.getAnimations()[0]; + anim.cancel(); + anim.play(); + assert_equals(document.getAnimations().length, 1, + 'CSS animations cancelled and restarted by the API are ' + + 'returned'); +}, 'CSS Animations cancelled and restarted via the API are returned'); + +test(function(t) { + addStyle(t, { '#parent::after': 'animation: animLeft 10s;', + '#parent::before': 'animation: animRight 10s;' }); + // create two divs with these arrangement: + // parent + // ::before, + // ::after + // | + // child + var parent = addDiv(t, { 'id': 'parent' }); + var child = addDiv(t); + parent.appendChild(child); + [parent, child].forEach((div) => { + div.setAttribute('style', 'animation: animBottom 10s'); + }); + + var anims = document.getAnimations(); + assert_equals(anims.length, 4, + 'CSS animations on both pseudo-elements and elements ' + + 'are returned'); + assert_equals(anims[0].effect.target, parent, + 'The animation targeting the parent element comes first'); + assert_equals(anims[1].effect.target.type, '::before', + 'The animation targeting the ::before element comes second'); + assert_equals(anims[2].effect.target.type, '::after', + 'The animation targeting the ::after element comes third'); + assert_equals(anims[3].effect.target, child, + 'The animation targeting the child element comes last'); +}, 'CSS Animations targetting (pseudo-)elements should have correct order ' + + 'after sorting'); + +done(); +</script> +</body> diff --git a/dom/animation/test/css-animations/file_effect-target.html b/dom/animation/test/css-animations/file_effect-target.html new file mode 100644 index 0000000000..006028e345 --- /dev/null +++ b/dom/animation/test/css-animations/file_effect-target.html @@ -0,0 +1,54 @@ +<!doctype html> +<meta charset=utf-8> +<script src="../testcommon.js"></script> +<style> +@keyframes anim { } +</style> +<body> +<script> +'use strict'; + +test(function(t) { + var div = addDiv(t); + div.style.animation = 'anim 100s'; + var animation = div.getAnimations()[0]; + assert_equals(animation.effect.target, div, + 'Animation.target is the animatable div'); +}, 'Returned CSS animations have the correct effect target'); + +test(function(t) { + addStyle(t, { '.after::after': 'animation: anim 10s, anim 100s;' }); + var div = addDiv(t, { class: 'after' }); + var anims = document.getAnimations(); + assert_equals(anims.length, 2, + 'Got animations running on ::after pseudo element'); + assert_equals(anims[0].effect.target, anims[1].effect.target, + 'Both animations return the same target object'); +}, 'effect.target should return the same CSSPseudoElement object each time'); + +test(function(t) { + addStyle(t, { '.after::after': 'animation: anim 10s;' }); + var div = addDiv(t, { class: 'after' }); + var pseudoTarget = document.getAnimations()[0].effect.target; + var effect = new KeyframeEffectReadOnly(pseudoTarget, + { background: ["blue", "red"] }, + 3 * MS_PER_SEC); + var newAnim = new Animation(effect, document.timeline); + newAnim.play(); + var anims = document.getAnimations(); + assert_equals(anims.length, 2, + 'Got animations running on ::after pseudo element'); + assert_not_equals(anims[0], newAnim, + 'The scriped-generated animation appears last'); + assert_equals(newAnim.effect.target, pseudoTarget, + 'The effect.target of the scripted-generated animation is ' + + 'the same as the one from the argument of ' + + 'KeyframeEffectReadOnly constructor'); + assert_equals(anims[0].effect.target, newAnim.effect.target, + 'Both animations return the same target object'); +}, 'effect.target from the script-generated animation should return the same ' + + 'CSSPseudoElement object as that from the CSS generated animation'); + +done(); +</script> +</body> diff --git a/dom/animation/test/css-animations/file_element-get-animations.html b/dom/animation/test/css-animations/file_element-get-animations.html new file mode 100644 index 0000000000..68386c98b0 --- /dev/null +++ b/dom/animation/test/css-animations/file_element-get-animations.html @@ -0,0 +1,445 @@ +<!doctype html> +<meta charset=utf-8> +<script src="../testcommon.js"></script> +<style> +@keyframes anim1 { + to { left: 100px } +} +@keyframes anim2 { + to { top: 100px } +} +@keyframes multiPropAnim { + to { background: green, opacity: 0.5, left: 100px, top: 100px } +} +@keyframes empty { } +</style> +<body> +<script> +'use strict'; + +test(function(t) { + var div = addDiv(t); + assert_equals(div.getAnimations().length, 0, + 'getAnimations returns an empty sequence for an element' + + ' with no animations'); +}, 'getAnimations for non-animated content'); + +promise_test(function(t) { + var div = addDiv(t); + + // FIXME: This test does too many things. It should be split up. + + // Add an animation + div.style.animation = 'anim1 100s'; + var animations = div.getAnimations(); + assert_equals(animations.length, 1, + 'getAnimations returns an Animation running CSS Animations'); + return animations[0].ready.then(function() { + var startTime = animations[0].startTime; + assert_true(startTime > 0 && startTime <= document.timeline.currentTime, + 'CSS animation has a sensible start time'); + + // Wait a moment then add a second animation. + // + // We wait for the next frame so that we can test that the start times of + // the animations differ. + return waitForFrame(); + }).then(function() { + div.style.animation = 'anim1 100s, anim2 100s'; + animations = div.getAnimations(); + assert_equals(animations.length, 2, + 'getAnimations returns one Animation for each value of' + + ' animation-name'); + // Wait until both Animations are ready + // (We don't make any assumptions about the order of the Animations since + // that is the purpose of the following test.) + return waitForAllAnimations(animations); + }).then(function() { + assert_true(animations[0].startTime < animations[1].startTime, + 'Additional Animations for CSS animations start after the original' + + ' animation and appear later in the list'); + }); +}, 'getAnimations for CSS Animations'); + +test(function(t) { + var div = addDiv(t, { style: 'animation: anim1 100s' }); + assert_class_string(div.getAnimations()[0], 'CSSAnimation', + 'Interface of returned animation is CSSAnimation'); +}, 'getAnimations returns CSSAnimation objects for CSS Animations'); + +test(function(t) { + var div = addDiv(t); + + // Add an animation that targets multiple properties + div.style.animation = 'multiPropAnim 100s'; + assert_equals(div.getAnimations().length, 1, + 'getAnimations returns only one Animation for a CSS Animation' + + ' that targets multiple properties'); +}, 'getAnimations for multi-property animations'); + +promise_test(function(t) { + var div = addDiv(t); + + // Add an animation + div.style.backgroundColor = 'red'; + div.style.animation = 'anim1 100s'; + window.getComputedStyle(div).backgroundColor; + + // Wait until a frame after the animation starts, then add a transition + var animations = div.getAnimations(); + return animations[0].ready.then(waitForFrame).then(function() { + div.style.transition = 'all 100s'; + div.style.backgroundColor = 'green'; + + animations = div.getAnimations(); + assert_equals(animations.length, 2, + 'getAnimations returns Animations for both animations and' + + ' transitions that run simultaneously'); + assert_class_string(animations[0], 'CSSTransition', + 'First-returned animation is the CSS Transition'); + assert_class_string(animations[1], 'CSSAnimation', + 'Second-returned animation is the CSS Animation'); + }); +}, 'getAnimations for both CSS Animations and CSS Transitions at once'); + +async_test(function(t) { + var div = addDiv(t); + + // Set up event listener + div.addEventListener('animationend', t.step_func(function() { + assert_equals(div.getAnimations().length, 0, + 'getAnimations does not return Animations for finished ' + + ' (and non-forwards-filling) CSS Animations'); + t.done(); + })); + + // Add a very short animation + div.style.animation = 'anim1 0.01s'; +}, 'getAnimations for CSS Animations that have finished'); + +async_test(function(t) { + var div = addDiv(t); + + // Set up event listener + div.addEventListener('animationend', t.step_func(function() { + assert_equals(div.getAnimations().length, 1, + 'getAnimations returns Animations for CSS Animations that have' + + ' finished but are filling forwards'); + t.done(); + })); + + // Add a very short animation + div.style.animation = 'anim1 0.01s forwards'; +}, 'getAnimations for CSS Animations that have finished but are' + + ' forwards filling'); + +test(function(t) { + var div = addDiv(t); + div.style.animation = 'none 100s'; + + var animations = div.getAnimations(); + assert_equals(animations.length, 0, + 'getAnimations returns an empty sequence for an element' + + ' with animation-name: none'); + + div.style.animation = 'none 100s, anim1 100s'; + animations = div.getAnimations(); + assert_equals(animations.length, 1, + 'getAnimations returns Animations only for those CSS Animations whose' + + ' animation-name is not none'); +}, 'getAnimations for CSS Animations with animation-name: none'); + +test(function(t) { + var div = addDiv(t); + div.style.animation = 'missing 100s'; + var animations = div.getAnimations(); + assert_equals(animations.length, 0, + 'getAnimations returns an empty sequence for an element' + + ' with animation-name: missing'); + + div.style.animation = 'anim1 100s, missing 100s'; + animations = div.getAnimations(); + assert_equals(animations.length, 1, + 'getAnimations returns Animations only for those CSS Animations whose' + + ' animation-name is found'); +}, 'getAnimations for CSS Animations with animation-name: missing'); + +promise_test(function(t) { + var div = addDiv(t); + div.style.animation = 'anim1 100s, notyet 100s'; + var animations = div.getAnimations(); + assert_equals(animations.length, 1, + 'getAnimations initally only returns Animations for CSS Animations whose' + + ' animation-name is found'); + + return animations[0].ready.then(waitForFrame).then(function() { + var keyframes = '@keyframes notyet { to { left: 100px; } }'; + document.styleSheets[0].insertRule(keyframes, 0); + animations = div.getAnimations(); + assert_equals(animations.length, 2, + 'getAnimations includes Animation when @keyframes rule is added' + + ' later'); + return waitForAllAnimations(animations); + }).then(function() { + assert_true(animations[0].startTime < animations[1].startTime, + 'Newly added animation has a later start time'); + document.styleSheets[0].deleteRule(0); + }); +}, 'getAnimations for CSS Animations where the @keyframes rule is added' + + ' later'); + +test(function(t) { + var div = addDiv(t); + div.style.animation = 'anim1 100s, anim1 100s'; + assert_equals(div.getAnimations().length, 2, + 'getAnimations returns one Animation for each CSS animation-name' + + ' even if the names are duplicated'); +}, 'getAnimations for CSS Animations with duplicated animation-name'); + +test(function(t) { + var div = addDiv(t); + div.style.animation = 'empty 100s'; + assert_equals(div.getAnimations().length, 1, + 'getAnimations returns Animations for CSS animations with an' + + ' empty keyframes rule'); +}, 'getAnimations for CSS Animations with empty keyframes rule'); + +promise_test(function(t) { + var div = addDiv(t); + div.style.animation = 'anim1 100s 100s'; + var animations = div.getAnimations(); + assert_equals(animations.length, 1, + 'getAnimations returns animations for CSS animations whose' + + ' delay makes them start later'); + return animations[0].ready.then(waitForFrame).then(function() { + assert_true(animations[0].startTime <= document.timeline.currentTime, + 'For CSS Animations in delay phase, the start time of the Animation is' + + ' not in the future'); + }); +}, 'getAnimations for CSS animations in delay phase'); + +test(function(t) { + var div = addDiv(t); + div.style.animation = 'anim1 0s 100s'; + assert_equals(div.getAnimations().length, 1, + 'getAnimations returns animations for CSS animations whose' + + ' duration is zero'); + div.remove(); +}, 'getAnimations for zero-duration CSS Animations'); + +test(function(t) { + var div = addDiv(t); + div.style.animation = 'anim1 100s'; + var originalAnimation = div.getAnimations()[0]; + + // Update pause state (an Animation change) + div.style.animationPlayState = 'paused'; + var pendingAnimation = div.getAnimations()[0]; + assert_equals(pendingAnimation.playState, 'pending', + 'animation\'s play state is updated'); + assert_equals(originalAnimation, pendingAnimation, + 'getAnimations returns the same objects even when their' + + ' play state changes'); + + // Update duration (an Animation change) + div.style.animationDuration = '200s'; + var extendedAnimation = div.getAnimations()[0]; + // FIXME: Check extendedAnimation.effect.timing.duration has changed once the + // API is available + assert_equals(originalAnimation, extendedAnimation, + 'getAnimations returns the same objects even when their' + + ' duration changes'); +}, 'getAnimations returns objects with the same identity'); + +test(function(t) { + var div = addDiv(t); + div.style.animation = 'anim1 100s'; + + assert_equals(div.getAnimations().length, 1, + 'getAnimations returns an animation before cancelling'); + + var animation = div.getAnimations()[0]; + + animation.cancel(); + assert_equals(div.getAnimations().length, 0, + 'getAnimations does not return cancelled animations'); + + animation.play(); + assert_equals(div.getAnimations().length, 1, + 'getAnimations returns cancelled animations that have been re-started'); + +}, 'getAnimations for CSS Animations that are cancelled'); + +promise_test(function(t) { + var div = addDiv(t); + div.style.animation = 'anim2 100s'; + + return div.getAnimations()[0].ready.then(function() { + // Prepend to the list and test that even though anim1 was triggered + // *after* anim2, it should come first because it appears first + // in the animation-name property. + div.style.animation = 'anim1 100s, anim2 100s'; + var anims = div.getAnimations(); + assert_equals(anims[0].animationName, 'anim1', + 'animation order after prepending to list'); + assert_equals(anims[1].animationName, 'anim2', + 'animation order after prepending to list'); + + // Normally calling cancel and play would this push anim1 to the top of + // the stack but it shouldn't for CSS animations that map an the + // animation-name property. + var anim1 = anims[0]; + anim1.cancel(); + anim1.play(); + anims = div.getAnimations(); + assert_equals(anims[0].animationName, 'anim1', + 'animation order after cancelling and restarting'); + assert_equals(anims[1].animationName, 'anim2', + 'animation order after cancelling and restarting'); + }); +}, 'getAnimations for CSS Animations follows animation-name order'); + +test(function(t) { + addStyle(t, { '#target::after': 'animation: anim1 10s;', + '#target::before': 'animation: anim1 10s;' }); + var target = addDiv(t, { 'id': 'target' }); + target.style.animation = 'anim1 100s'; + + var animations = target.getAnimations({ subtree: false }); + assert_equals(animations.length, 1, + 'Should find only the element'); + assert_equals(animations[0].effect.target, target, + 'Effect target should be the element'); +}, 'Test AnimationFilter{ subtree: false } with single element'); + +test(function(t) { + addStyle(t, { '#target::after': 'animation: anim1 10s;', + '#target::before': 'animation: anim1 10s;' }); + var target = addDiv(t, { 'id': 'target' }); + target.style.animation = 'anim1 100s'; + + var animations = target.getAnimations({ subtree: true }); + assert_equals(animations.length, 3, + 'getAnimations({ subtree: true }) ' + + 'should return animations on pseudo-elements'); + assert_equals(animations[0].effect.target, target, + 'The animation targeting the parent element ' + + 'should be returned first'); + assert_equals(animations[1].effect.target.type, '::before', + 'The animation targeting the ::before pseudo-element ' + + 'should be returned second'); + assert_equals(animations[2].effect.target.type, '::after', + 'The animation targeting the ::after pesudo-element ' + + 'should be returned last'); +}, 'Test AnimationFilter{ subtree: true } with single element'); + +test(function(t) { + addStyle(t, { '#parent::after': 'animation: anim1 10s;', + '#parent::before': 'animation: anim1 10s;', + '#child::after': 'animation: anim1 10s;', + '#child::before': 'animation: anim1 10s;' }); + var parent = addDiv(t, { 'id': 'parent' }); + parent.style.animation = 'anim1 100s'; + var child = addDiv(t, { 'id': 'child' }); + child.style.animation = 'anim1 100s'; + parent.appendChild(child); + + var animations = parent.getAnimations({ subtree: false }); + assert_equals(animations.length, 1, + 'Should find only the element even if it has a child'); + assert_equals(animations[0].effect.target, parent, + 'Effect target shuld be the element'); +}, 'Test AnimationFilter{ subtree: false } with element that has a child'); + +test(function(t) { + addStyle(t, { '#parent::after': 'animation: anim1 10s;', + '#parent::before': 'animation: anim1 10s;', + '#child::after': 'animation: anim1 10s;', + '#child::before': 'animation: anim1 10s;' }); + var parent = addDiv(t, { 'id': 'parent' }); + var child = addDiv(t, { 'id': 'child' }); + parent.style.animation = 'anim1 100s'; + child.style.animation = 'anim1 100s'; + parent.appendChild(child); + + var animations = parent.getAnimations({ subtree: true }); + assert_equals(animations.length, 6, + 'Should find all elements, pesudo-elements that parent has'); + + assert_equals(animations[0].effect.target, parent, + 'The animation targeting the parent element ' + + 'should be returned first'); + assert_equals(animations[1].effect.target.type, '::before', + 'The animation targeting the ::before pseudo-element ' + + 'should be returned second'); + assert_equals(animations[1].effect.target.parentElement, parent, + 'This ::before element should be child of parent element'); + assert_equals(animations[2].effect.target.type, '::after', + 'The animation targeting the ::after pesudo-element ' + + 'should be returned third'); + assert_equals(animations[2].effect.target.parentElement, parent, + 'This ::after element should be child of parent element'); + + assert_equals(animations[3].effect.target, child, + 'The animation targeting the child element ' + + 'should be returned fourth'); + assert_equals(animations[4].effect.target.type, '::before', + 'The animation targeting the ::before pseudo-element ' + + 'should be returned fifth'); + assert_equals(animations[4].effect.target.parentElement, child, + 'This ::before element should be child of child element'); + assert_equals(animations[5].effect.target.type, '::after', + 'The animation targeting the ::after pesudo-element ' + + 'should be returned last'); + assert_equals(animations[5].effect.target.parentElement, child, + 'This ::after element should be child of child element'); +}, 'Test AnimationFilter{ subtree: true } with element that has a child'); + +test(function(t) { + var parent = addDiv(t, { 'id': 'parent' }); + var child1 = addDiv(t, { 'id': 'child1' }); + var grandchild1 = addDiv(t, { 'id': 'grandchild1' }); + var grandchild2 = addDiv(t, { 'id': 'grandchild2' }); + var child2 = addDiv(t, { 'id': 'child2' }); + + parent.style.animation = 'anim1 100s'; + child1.style.animation = 'anim1 100s'; + grandchild1.style.animation = 'anim1 100s'; + grandchild2.style.animation = 'anim1 100s'; + child2.style.animation = 'anim1 100s'; + + parent.appendChild(child1); + child1.appendChild(grandchild1); + child1.appendChild(grandchild2); + parent.appendChild(child2); + + var animations = parent.getAnimations({ subtree: true }); + assert_equals( + parent.getAnimations({ subtree: true }).length, 5, + 'Should find all descendants of the element'); + + assert_equals(animations[0].effect.target, parent, + 'The animation targeting the parent element ' + + 'should be returned first'); + + assert_equals(animations[1].effect.target, child1, + 'The animation targeting the child1 element ' + + 'should be returned second'); + + assert_equals(animations[2].effect.target, grandchild1, + 'The animation targeting the grandchild1 element ' + + 'should be returned third'); + + assert_equals(animations[3].effect.target, grandchild2, + 'The animation targeting the grandchild2 element ' + + 'should be returned fourth'); + + assert_equals(animations[4].effect.target, child2, + 'The animation targeting the child2 element ' + + 'should be returned last'); + +}, 'Test AnimationFilter{ subtree: true } with element that has many descendant'); + +done(); +</script> +</body> diff --git a/dom/animation/test/css-animations/file_keyframeeffect-getkeyframes.html b/dom/animation/test/css-animations/file_keyframeeffect-getkeyframes.html new file mode 100644 index 0000000000..15e2d23f18 --- /dev/null +++ b/dom/animation/test/css-animations/file_keyframeeffect-getkeyframes.html @@ -0,0 +1,672 @@ +<!doctype html> +<meta charset=utf-8> +<script src="../testcommon.js"></script> +<style> +@keyframes anim-empty { } + +@keyframes anim-empty-frames { + from { } + to { } +} + +@keyframes anim-only-timing { + from { animation-timing-function: linear; } + to { } +} + +@keyframes anim-only-non-animatable { + from { animation-duration: 3s; } + to { animation-duration: 5s; } +} + +@keyframes anim-simple { + from { color: black; } + to { color: white; } +} + +@keyframes anim-simple-three { + from { color: black; } + 50% { color: blue; } + to { color: white; } +} + +@keyframes anim-simple-timing { + from { color: black; animation-timing-function: linear; } + 50% { color: blue; animation-timing-function: ease-in-out; } + to { color: white; animation-timing-function: step-end; } +} + +@keyframes anim-simple-timing-some { + from { color: black; animation-timing-function: linear; } + 50% { color: blue; } + to { color: white; } +} + +@keyframes anim-simple-shorthand { + from { margin: 8px; } + to { margin: 16px; } +} + +@keyframes anim-omit-to { + from { color: blue; } +} + +@keyframes anim-omit-from { + to { color: blue; } +} + +@keyframes anim-omit-from-to { + 50% { color: blue; } +} + +@keyframes anim-partially-omit-to { + from { margin-top: 50px; + margin-bottom: 100px; } + to { margin-top: 150px !important; /* ignored */ + margin-bottom: 200px; } +} + +@keyframes anim-different-props { + from { color: black; margin-top: 8px; } + 25% { color: blue; } + 75% { margin-top: 12px; } + to { color: white; margin-top: 16px } +} + +@keyframes anim-different-props-and-easing { + from { color: black; margin-top: 8px; animation-timing-function: linear; } + 25% { color: blue; animation-timing-function: step-end; } + 75% { margin-top: 12px; animation-timing-function: ease-in; } + to { color: white; margin-top: 16px } +} + +@keyframes anim-merge-offset { + from { color: black; } + to { color: white; } + from { margin-top: 8px; } + to { margin-top: 16px; } +} + +@keyframes anim-merge-offset-and-easing { + from { color: black; animation-timing-function: step-end; } + to { color: white; } + from { margin-top: 8px; animation-timing-function: linear; } + to { margin-top: 16px; } + from { font-size: 16px; animation-timing-function: step-end; } + to { font-size: 32px; } + from { padding-left: 2px; animation-timing-function: linear; } + to { padding-left: 4px; } +} + +@keyframes anim-no-merge-equiv-easing { + from { margin-top: 0px; animation-timing-function: steps(1, end); } + from { margin-right: 0px; animation-timing-function: step-end; } + from { margin-bottom: 0px; animation-timing-function: steps(1); } + 50% { margin-top: 10px; animation-timing-function: step-end; } + 50% { margin-right: 10px; animation-timing-function: step-end; } + 50% { margin-bottom: 10px; animation-timing-function: step-end; } + to { margin-top: 20px; margin-right: 20px; margin-bottom: 20px; } +} + +@keyframes anim-overriding { + from { padding-top: 50px } + 50%, from { padding-top: 30px } /* wins: 0% */ + 75%, 85%, 50% { padding-top: 20px } /* wins: 75%, 50% */ + 100%, 85% { padding-top: 70px } /* wins: 100% */ + 85.1% { padding-top: 60px } /* wins: 85.1% */ + 85% { padding-top: 30px } /* wins: 85% */ +} + +@keyframes anim-filter { + to { filter: blur(5px) sepia(60%) saturate(30%); } +} + +@keyframes anim-text-shadow { + to { text-shadow: none; } +} + +@keyframes anim-background-size { + to { background-size: 50%, 6px, contain } +} + +:root { + --var-100px: 100px; +} +@keyframes anim-variables { + to { transform: translate(var(--var-100px), 0) } +} +@keyframes anim-variables-shorthand { + to { margin: var(--var-100px) } +} +</style> +<body> +<script> +"use strict"; + +function getKeyframes(e) { + return e.getAnimations()[0].effect.getKeyframes(); +} + +function assert_frames_equal(a, b, name) { + assert_equals(Object.keys(a).sort().toString(), + Object.keys(b).sort().toString(), + "properties on " + name); + for (var p in a) { + if (p === 'offset' || p === 'computedOffset') { + assert_approx_equals(a[p], b[p], 0.00001, + "value for '" + p + "' on " + name); + } else { + assert_equals(a[p], b[p], "value for '" + p + "' on " + name); + } + } +} + +// animation-timing-function values to test with, where the value +// is exactly the same as its serialization, sorted by the order +// getKeyframes() will group frames with the same easing function +// together (by nsTimingFunction::Compare). +const kTimingFunctionValues = [ + "ease", + "linear", + "ease-in", + "ease-out", + "ease-in-out", + "steps(1, start)", + "steps(2, start)", + "steps(1)", + "steps(2)", + "cubic-bezier(0, 0, 1, 1)", + "cubic-bezier(0, 0.25, 0.75, 1)" +]; + +test(function(t) { + var div = addDiv(t); + + div.style.animation = 'anim-empty 100s'; + assert_equals(getKeyframes(div).length, 0, + "number of frames with empty @keyframes"); + + div.style.animation = 'anim-empty-frames 100s'; + assert_equals(getKeyframes(div).length, 0, + "number of frames when @keyframes has empty keyframes"); + + div.style.animation = 'anim-only-timing 100s'; + assert_equals(getKeyframes(div).length, 0, + "number of frames when @keyframes only has keyframes with " + + "animation-timing-function"); + + div.style.animation = 'anim-only-non-animatable 100s'; + assert_equals(getKeyframes(div).length, 0, + "number of frames when @keyframes only has frames with " + + "non-animatable properties"); +}, 'KeyframeEffectReadOnly.getKeyframes() returns no frames for various kinds' + + ' of empty enimations'); + +test(function(t) { + var div = addDiv(t); + + div.style.animation = 'anim-simple 100s'; + var frames = getKeyframes(div); + + assert_equals(frames.length, 2, "number of frames"); + + var expected = [ + { offset: 0, computedOffset: 0, easing: "ease", + color: "rgb(0, 0, 0)" }, + { offset: 1, computedOffset: 1, easing: "ease", + color: "rgb(255, 255, 255)" }, + ]; + + for (var i = 0; i < frames.length; i++) { + assert_frames_equal(frames[i], expected[i], "ComputedKeyframe #" + i); + } +}, 'KeyframeEffectReadOnly.getKeyframes() returns expected frames for a simple' + + 'animation'); + +test(function(t) { + kTimingFunctionValues.forEach(function(easing) { + var div = addDiv(t); + + div.style.animation = 'anim-simple-three 100s ' + easing; + var frames = getKeyframes(div); + + assert_equals(frames.length, 3, "number of frames"); + + for (var i = 0; i < frames.length; i++) { + assert_equals(frames[i].easing, easing, + "value for 'easing' on ComputedKeyframe #" + i); + } + }); +}, 'KeyframeEffectReadOnly.getKeyframes() returns frames with expected easing' + + ' values, when the easing comes from animation-timing-function on the' + + ' element'); + +test(function(t) { + var div = addDiv(t); + + div.style.animation = 'anim-simple-timing 100s'; + var frames = getKeyframes(div); + + assert_equals(frames.length, 3, "number of frames"); + assert_equals(frames[0].easing, "linear", + "value of 'easing' on ComputedKeyframe #0"); + assert_equals(frames[1].easing, "ease-in-out", + "value of 'easing' on ComputedKeyframe #1"); + assert_equals(frames[2].easing, "steps(1)", + "value of 'easing' on ComputedKeyframe #2"); +}, 'KeyframeEffectReadOnly.getKeyframes() returns frames with expected easing' + + ' values, when the easing is specified on each keyframe'); + +test(function(t) { + var div = addDiv(t); + + div.style.animation = 'anim-simple-timing-some 100s step-start'; + var frames = getKeyframes(div); + + assert_equals(frames.length, 3, "number of frames"); + assert_equals(frames[0].easing, "linear", + "value of 'easing' on ComputedKeyframe #0"); + assert_equals(frames[1].easing, "steps(1, start)", + "value of 'easing' on ComputedKeyframe #1"); + assert_equals(frames[2].easing, "steps(1, start)", + "value of 'easing' on ComputedKeyframe #2"); +}, 'KeyframeEffectReadOnly.getKeyframes() returns frames with expected easing' + + ' values, when the easing is specified on some keyframes'); + +test(function(t) { + var div = addDiv(t); + + div.style.animation = 'anim-simple-shorthand 100s'; + var frames = getKeyframes(div); + + assert_equals(frames.length, 2, "number of frames"); + + var expected = [ + { offset: 0, computedOffset: 0, easing: "ease", + marginBottom: "8px", marginLeft: "8px", + marginRight: "8px", marginTop: "8px" }, + { offset: 1, computedOffset: 1, easing: "ease", + marginBottom: "16px", marginLeft: "16px", + marginRight: "16px", marginTop: "16px" }, + ]; + + for (var i = 0; i < frames.length; i++) { + assert_frames_equal(frames[i], expected[i], "ComputedKeyframe #" + i); + } +}, 'KeyframeEffectReadOnly.getKeyframes() returns expected frames for a simple' + + ' animation that specifies a single shorthand property'); + +test(function(t) { + var div = addDiv(t); + + div.style.animation = 'anim-omit-to 100s'; + div.style.color = 'white'; + var frames = getKeyframes(div); + + assert_equals(frames.length, 2, "number of frames"); + + var expected = [ + { offset: 0, computedOffset: 0, easing: "ease", + color: "rgb(0, 0, 255)" }, + { offset: 1, computedOffset: 1, easing: "ease", + color: "rgb(255, 255, 255)" }, + ]; + + for (var i = 0; i < frames.length; i++) { + assert_frames_equal(frames[i], expected[i], "ComputedKeyframe #" + i); + } +}, 'KeyframeEffectReadOnly.getKeyframes() returns expected frames for an ' + + 'animation with a 0% keyframe and no 100% keyframe'); + +test(function(t) { + var div = addDiv(t); + + div.style.animation = 'anim-omit-from 100s'; + div.style.color = 'white'; + var frames = getKeyframes(div); + + assert_equals(frames.length, 2, "number of frames"); + + var expected = [ + { offset: 0, computedOffset: 0, easing: "ease", + color: "rgb(255, 255, 255)" }, + { offset: 1, computedOffset: 1, easing: "ease", + color: "rgb(0, 0, 255)" }, + ]; + + for (var i = 0; i < frames.length; i++) { + assert_frames_equal(frames[i], expected[i], "ComputedKeyframe #" + i); + } +}, 'KeyframeEffectReadOnly.getKeyframes() returns expected frames for an ' + + 'animation with a 100% keyframe and no 0% keyframe'); + +test(function(t) { + var div = addDiv(t); + + div.style.animation = 'anim-omit-from-to 100s'; + div.style.color = 'white'; + var frames = getKeyframes(div); + + assert_equals(frames.length, 3, "number of frames"); + + var expected = [ + { offset: 0, computedOffset: 0, easing: "ease", + color: "rgb(255, 255, 255)" }, + { offset: 0.5, computedOffset: 0.5, easing: "ease", + color: "rgb(0, 0, 255)" }, + { offset: 1, computedOffset: 1, easing: "ease", + color: "rgb(255, 255, 255)" }, + ]; + + for (var i = 0; i < frames.length; i++) { + assert_frames_equal(frames[i], expected[i], "ComputedKeyframe #" + i); + } +}, 'KeyframeEffectReadOnly.getKeyframes() returns expected frames for an ' + + 'animation with no 0% or 100% keyframe but with a 50% keyframe'); + +test(function(t) { + var div = addDiv(t); + + div.style.animation = 'anim-partially-omit-to 100s'; + div.style.marginTop = '250px'; + var frames = getKeyframes(div); + + assert_equals(frames.length, 2, "number of frames"); + + var expected = [ + { offset: 0, computedOffset: 0, easing: "ease", + marginTop: '50px', marginBottom: '100px' }, + { offset: 1, computedOffset: 1, easing: "ease", + marginTop: '250px', marginBottom: '200px' }, + ]; + + for (var i = 0; i < frames.length; i++) { + assert_frames_equal(frames[i], expected[i], "ComputedKeyframe #" + i); + } +}, 'KeyframeEffectReadOnly.getKeyframes() returns expected frames for an ' + + 'animation with a partially complete 100% keyframe (because the ' + + '!important rule is ignored)'); + +test(function(t) { + var div = addDiv(t); + + div.style.animation = 'anim-different-props 100s'; + var frames = getKeyframes(div); + + assert_equals(frames.length, 4, "number of frames"); + + var expected = [ + { offset: 0, computedOffset: 0, easing: "ease", + color: "rgb(0, 0, 0)", marginTop: "8px" }, + { offset: 0.25, computedOffset: 0.25, easing: "ease", + color: "rgb(0, 0, 255)" }, + { offset: 0.75, computedOffset: 0.75, easing: "ease", + marginTop: "12px" }, + { offset: 1, computedOffset: 1, easing: "ease", + color: "rgb(255, 255, 255)", marginTop: "16px" }, + ]; + + for (var i = 0; i < frames.length; i++) { + assert_frames_equal(frames[i], expected[i], "ComputedKeyframe #" + i); + } +}, 'KeyframeEffectReadOnly.getKeyframes() returns expected frames for an ' + + 'animation with different properties on different keyframes, all ' + + 'with the same easing function'); + +test(function(t) { + var div = addDiv(t); + + div.style.animation = 'anim-different-props-and-easing 100s'; + var frames = getKeyframes(div); + + assert_equals(frames.length, 4, "number of frames"); + + var expected = [ + { offset: 0, computedOffset: 0, easing: "linear", + color: "rgb(0, 0, 0)", marginTop: "8px" }, + { offset: 0.25, computedOffset: 0.25, easing: "steps(1)", + color: "rgb(0, 0, 255)" }, + { offset: 0.75, computedOffset: 0.75, easing: "ease-in", + marginTop: "12px" }, + { offset: 1, computedOffset: 1, easing: "ease", + color: "rgb(255, 255, 255)", marginTop: "16px" }, + ]; + + for (var i = 0; i < frames.length; i++) { + assert_frames_equal(frames[i], expected[i], "ComputedKeyframe #" + i); + } +}, 'KeyframeEffectReadOnly.getKeyframes() returns expected frames for an ' + + 'animation with different properties on different keyframes, with ' + + 'a different easing function on each'); + +test(function(t) { + var div = addDiv(t); + + div.style.animation = 'anim-merge-offset 100s'; + var frames = getKeyframes(div); + + assert_equals(frames.length, 2, "number of frames"); + + var expected = [ + { offset: 0, computedOffset: 0, easing: "ease", + color: "rgb(0, 0, 0)", marginTop: "8px" }, + { offset: 1, computedOffset: 1, easing: "ease", + color: "rgb(255, 255, 255)", marginTop: "16px" }, + ]; + + for (var i = 0; i < frames.length; i++) { + assert_frames_equal(frames[i], expected[i], "ComputedKeyframe #" + i); + } +}, 'KeyframeEffectReadOnly.getKeyframes() returns expected frames for an ' + + 'animation with multiple keyframes for the same time, and all with ' + + 'the same easing function'); + +test(function(t) { + var div = addDiv(t); + + div.style.animation = 'anim-merge-offset-and-easing 100s'; + var frames = getKeyframes(div); + + assert_equals(frames.length, 3, "number of frames"); + + var expected = [ + { offset: 0, computedOffset: 0, easing: "steps(1)", + color: "rgb(0, 0, 0)", fontSize: "16px" }, + { offset: 0, computedOffset: 0, easing: "linear", + marginTop: "8px", paddingLeft: "2px" }, + { offset: 1, computedOffset: 1, easing: "ease", + color: "rgb(255, 255, 255)", fontSize: "32px", marginTop: "16px", + paddingLeft: "4px" }, + ]; + + for (var i = 0; i < frames.length; i++) { + assert_frames_equal(frames[i], expected[i], "ComputedKeyframe #" + i); + } +}, 'KeyframeEffectReadOnly.getKeyframes() returns expected frames for an ' + + 'animation with multiple keyframes for the same time and with ' + + 'different easing functions'); + +test(function(t) { + var div = addDiv(t); + + div.style.animation = 'anim-no-merge-equiv-easing 100s'; + var frames = getKeyframes(div); + + assert_equals(frames.length, 3, "number of frames"); + + var expected = [ + { offset: 0, computedOffset: 0, easing: "steps(1)", + marginTop: "0px", marginRight: "0px", marginBottom: "0px" }, + { offset: 0.5, computedOffset: 0.5, easing: "steps(1)", + marginTop: "10px", marginRight: "10px", marginBottom: "10px" }, + { offset: 1, computedOffset: 1, easing: "ease", + marginTop: "20px", marginRight: "20px", marginBottom: "20px" }, + ]; + + for (var i = 0; i < frames.length; i++) { + assert_frames_equal(frames[i], expected[i], "ComputedKeyframe #" + i); + } +}, 'KeyframeEffectReadOnly.getKeyframes() returns expected frames for an ' + + 'animation with multiple keyframes for the same time and with ' + + 'different but equivalent easing functions'); + +test(function(t) { + var div = addDiv(t); + + div.style.animation = 'anim-overriding 100s'; + var frames = getKeyframes(div); + + assert_equals(frames.length, 6, "number of frames"); + + var expected = [ + { offset: 0, computedOffset: 0, easing: "ease", + paddingTop: "30px" }, + { offset: 0.5, computedOffset: 0.5, easing: "ease", + paddingTop: "20px" }, + { offset: 0.75, computedOffset: 0.75, easing: "ease", + paddingTop: "20px" }, + { offset: 0.85, computedOffset: 0.85, easing: "ease", + paddingTop: "30px" }, + { offset: 0.851, computedOffset: 0.851, easing: "ease", + paddingTop: "60px" }, + { offset: 1, computedOffset: 1, easing: "ease", + paddingTop: "70px" }, + ]; + + for (var i = 0; i < frames.length; i++) { + assert_frames_equal(frames[i], expected[i], "ComputedKeyframe #" + i); + } +}, 'KeyframeEffectReadOnly.getKeyframes() returns expected frames for ' + + 'overlapping keyframes'); + +// Gecko-specific test case: We are specifically concerned here that the +// computed value for filter, "none", is correctly represented. + +test(function(t) { + var div = addDiv(t); + + div.style.animation = 'anim-filter 100s'; + var frames = getKeyframes(div); + + assert_equals(frames.length, 2, "number of frames"); + + var expected = [ + { offset: 0, computedOffset: 0, easing: "ease", + filter: "none" }, + { offset: 1, computedOffset: 1, easing: "ease", + filter: "blur(5px) sepia(60%) saturate(30%)" }, + ]; + + for (var i = 0; i < frames.length; i++) { + assert_frames_equal(frames[i], expected[i], "ComputedKeyframe #" + i); + } +}, 'KeyframeEffectReadOnly.getKeyframes() returns expected values for ' + + 'animations with filter properties and missing keyframes'); + +// Gecko-specific test case: We are specifically concerned here that the +// computed value for text-shadow and a "none" specified on a keyframe +// are correctly represented. + +test(function(t) { + var div = addDiv(t); + + div.style.textShadow = '1px 1px 2px black, 0 0 16px blue, 0 0 3.2px blue'; + div.style.animation = 'anim-text-shadow 100s'; + var frames = getKeyframes(div); + + assert_equals(frames.length, 2, "number of frames"); + + var expected = [ + { offset: 0, computedOffset: 0, easing: "ease", + textShadow: "1px 1px 2px 0px rgb(0, 0, 0)," + + " 0px 0px 16px 0px rgb(0, 0, 255)," + + " 0px 0px 3.2px 0px rgb(0, 0, 255)" }, + { offset: 1, computedOffset: 1, easing: "ease", textShadow: "none" }, + ]; + + for (var i = 0; i < frames.length; i++) { + assert_frames_equal(frames[i], expected[i], "ComputedKeyframe #" + i); + } +}, 'KeyframeEffectReadOnly.getKeyframes() returns expected values for ' + + 'animations with text-shadow properties and missing keyframes'); + +// Gecko-specific test case: We are specifically concerned here that the +// initial value for background-size and the specified list are correctly +// represented. + +test(function(t) { + var div = addDiv(t); + + div.style.animation = 'anim-background-size 100s'; + var frames = getKeyframes(div); + + assert_equals(frames.length, 2, "number of frames"); + + var expected = [ + { offset: 0, computedOffset: 0, easing: "ease", + backgroundSize: "auto auto" }, + { offset: 1, computedOffset: 1, easing: "ease", + backgroundSize: "50% auto, 6px auto, contain" }, + ]; + + for (var i = 0; i < frames.length; i++) { + assert_frames_equal(frames[i], expected[i], "ComputedKeyframe #" + i); + } + + // Test inheriting a background-size value + + expected[0].backgroundSize = div.style.backgroundSize = + "30px auto, 40% auto, auto auto"; + frames = getKeyframes(div); + + for (var i = 0; i < frames.length; i++) { + assert_frames_equal(frames[i], expected[i], "ComputedKeyframe #" + i + + " after updating current style"); + } +}, 'KeyframeEffectReadOnly.getKeyframes() returns expected values for ' + + 'animations with background-size properties and missing keyframes'); + +test(function(t) { + var div = addDiv(t); + div.style.animation = 'anim-variables 100s'; + + var frames = getKeyframes(div); + + var expected = [ + { offset: 0, computedOffset: 0, easing: "ease", + transform: "none" }, + { offset: 1, computedOffset: 1, easing: "ease", + transform: "translate(100px, 0px)" }, + ]; + for (var i = 0; i < frames.length; i++) { + assert_frames_equal(frames[i], expected[i], "ComputedKeyframe #" + i); + } +}, 'KeyframeEffectReadOnly.getKeyframes() returns expected values for ' + + 'animations with CSS variables as keyframe values'); + +test(function(t) { + var div = addDiv(t); + div.style.animation = 'anim-variables-shorthand 100s'; + + var frames = getKeyframes(div); + + var expected = [ + { offset: 0, computedOffset: 0, easing: "ease", + marginBottom: "0px", + marginLeft: "0px", + marginRight: "0px", + marginTop: "0px" }, + { offset: 1, computedOffset: 1, easing: "ease", + marginBottom: "100px", + marginLeft: "100px", + marginRight: "100px", + marginTop: "100px" }, + ]; + for (var i = 0; i < frames.length; i++) { + assert_frames_equal(frames[i], expected[i], "ComputedKeyframe #" + i); + } +}, 'KeyframeEffectReadOnly.getKeyframes() returns expected values for ' + + 'animations with CSS variables as keyframe values in a shorthand property'); +done(); +</script> +</body> diff --git a/dom/animation/test/css-animations/file_pseudoElement-get-animations.html b/dom/animation/test/css-animations/file_pseudoElement-get-animations.html new file mode 100644 index 0000000000..bebe145334 --- /dev/null +++ b/dom/animation/test/css-animations/file_pseudoElement-get-animations.html @@ -0,0 +1,70 @@ +<!doctype html> +<meta charset=utf-8> +<script src="../testcommon.js"></script> +<style> +@keyframes anim1 { } +@keyframes anim2 { } +.before::before { + animation: anim1 10s; +} +.after-with-mix-anims-trans::after { + content: ''; + animation: anim1 10s, anim2 10s; + width: 0px; + height: 0px; + transition: all 100s; +} +.after-change::after { + width: 100px; + height: 100px; +} +</style> +<body> +<script> +'use strict'; + +test(function(t) { + var div = addDiv(t, { class: 'before' }); + var pseudoTarget = document.getAnimations()[0].effect.target; + assert_equals(pseudoTarget.getAnimations().length, 1, + 'Expected number of animations are returned'); + assert_equals(pseudoTarget.getAnimations()[0].animationName, 'anim1', + 'CSS animation name matches'); +}, 'getAnimations returns CSSAnimation objects'); + +test(function(t) { + var div = addDiv(t, { class: 'after-with-mix-anims-trans' }); + // Trigger transitions + flushComputedStyle(div); + div.classList.add('after-change'); + + // Create additional animation on the pseudo-element from script + var pseudoTarget = document.getAnimations()[0].effect.target; + var effect = new KeyframeEffectReadOnly(pseudoTarget, + { background: ["blue", "red"] }, + 3 * MS_PER_SEC); + var newAnimation = new Animation(effect, document.timeline); + newAnimation.id = 'scripted-anim'; + newAnimation.play(); + + // Check order - the script-generated animation should appear later + var anims = pseudoTarget.getAnimations(); + assert_equals(anims.length, 5, + 'Got expected number of animations/trnasitions running on ' + + '::after pseudo element'); + assert_equals(anims[0].transitionProperty, 'height', + '1st animation is the 1st transition sorted by name'); + assert_equals(anims[1].transitionProperty, 'width', + '2nd animation is the 2nd transition sorted by name '); + assert_equals(anims[2].animationName, 'anim1', + '3rd animation is the 1st animation in animation-name list'); + assert_equals(anims[3].animationName, 'anim2', + '4rd animation is the 2nd animation in animation-name list'); + assert_equals(anims[4].id, 'scripted-anim', + 'Animation added by script appears last'); +}, 'getAnimations returns css transitions/animations, and script-generated ' + + 'animations in the expected order'); + +done(); +</script> +</body> diff --git a/dom/animation/test/css-animations/test_animation-cancel.html b/dom/animation/test/css-animations/test_animation-cancel.html new file mode 100644 index 0000000000..15c9b482fe --- /dev/null +++ b/dom/animation/test/css-animations/test_animation-cancel.html @@ -0,0 +1,15 @@ +<!doctype html> +<meta charset=utf-8> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<div id="log"></div> +<script> +'use strict'; +setup({explicit_done: true}); +SpecialPowers.pushPrefEnv( + { "set": [["dom.animations-api.core.enabled", true]]}, + function() { + window.open("file_animation-cancel.html"); + }); +</script> +</html> diff --git a/dom/animation/test/css-animations/test_animation-computed-timing.html b/dom/animation/test/css-animations/test_animation-computed-timing.html new file mode 100644 index 0000000000..c1b40aaf36 --- /dev/null +++ b/dom/animation/test/css-animations/test_animation-computed-timing.html @@ -0,0 +1,16 @@ +<!doctype html> +<html> +<meta charset=utf-8> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<div id="log"></div> +<script> +'use strict'; +setup({explicit_done: true}); +SpecialPowers.pushPrefEnv( + { "set": [["dom.animations-api.core.enabled", true]]}, + function() { + window.open("file_animation-computed-timing.html"); + }); +</script> +</html> diff --git a/dom/animation/test/css-animations/test_animation-currenttime.html b/dom/animation/test/css-animations/test_animation-currenttime.html new file mode 100644 index 0000000000..7e3a8d74d6 --- /dev/null +++ b/dom/animation/test/css-animations/test_animation-currenttime.html @@ -0,0 +1,15 @@ +<!doctype html> +<meta charset=utf-8> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<div id="log"></div> +<script> +'use strict'; +setup({explicit_done: true}); +SpecialPowers.pushPrefEnv( + { "set": [["dom.animations-api.core.enabled", true]]}, + function() { + window.open("file_animation-currenttime.html"); + }); +</script> +</html> diff --git a/dom/animation/test/css-animations/test_animation-finish.html b/dom/animation/test/css-animations/test_animation-finish.html new file mode 100644 index 0000000000..abbd267d82 --- /dev/null +++ b/dom/animation/test/css-animations/test_animation-finish.html @@ -0,0 +1,15 @@ +<!doctype html> +<meta charset=utf-8> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<div id="log"></div> +<script> +'use strict'; +setup({explicit_done: true}); +SpecialPowers.pushPrefEnv( + { "set": [["dom.animations-api.core.enabled", true]]}, + function() { + window.open("file_animation-finish.html"); + }); +</script> +</html> diff --git a/dom/animation/test/css-animations/test_animation-finished.html b/dom/animation/test/css-animations/test_animation-finished.html new file mode 100644 index 0000000000..295ffe0af6 --- /dev/null +++ b/dom/animation/test/css-animations/test_animation-finished.html @@ -0,0 +1,15 @@ +<!doctype html> +<meta charset=utf-8> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<div id="log"></div> +<script> +'use strict'; +setup({explicit_done: true}); +SpecialPowers.pushPrefEnv( + { "set": [["dom.animations-api.core.enabled", true]]}, + function() { + window.open("file_animation-finished.html"); + }); +</script> +</html> diff --git a/dom/animation/test/css-animations/test_animation-id.html b/dom/animation/test/css-animations/test_animation-id.html new file mode 100644 index 0000000000..c23501b8d6 --- /dev/null +++ b/dom/animation/test/css-animations/test_animation-id.html @@ -0,0 +1,15 @@ +<!doctype html> +<meta charset=utf-8> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<div id="log"></div> +<script> +'use strict'; +setup({explicit_done: true}); +SpecialPowers.pushPrefEnv( + { "set": [["dom.animations-api.core.enabled", true]]}, + function() { + window.open("file_animation-id.html"); + }); +</script> +</html> diff --git a/dom/animation/test/css-animations/test_animation-pausing.html b/dom/animation/test/css-animations/test_animation-pausing.html new file mode 100644 index 0000000000..10be1ddf02 --- /dev/null +++ b/dom/animation/test/css-animations/test_animation-pausing.html @@ -0,0 +1,15 @@ +<!doctype html> +<meta charset=utf-8> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<div id="log"></div> +<script> +'use strict'; +setup({explicit_done: true}); +SpecialPowers.pushPrefEnv( + { "set": [["dom.animations-api.core.enabled", true]]}, + function() { + window.open("file_animation-pausing.html"); + }); +</script> +</html> diff --git a/dom/animation/test/css-animations/test_animation-playstate.html b/dom/animation/test/css-animations/test_animation-playstate.html new file mode 100644 index 0000000000..54c8e1f104 --- /dev/null +++ b/dom/animation/test/css-animations/test_animation-playstate.html @@ -0,0 +1,15 @@ +<!doctype html> +<meta charset=utf-8> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<div id="log"></div> +<script> +'use strict'; +setup({explicit_done: true}); +SpecialPowers.pushPrefEnv( + { "set": [["dom.animations-api.core.enabled", true]]}, + function() { + window.open("file_animation-playstate.html"); + }); +</script> +</html> diff --git a/dom/animation/test/css-animations/test_animation-ready.html b/dom/animation/test/css-animations/test_animation-ready.html new file mode 100644 index 0000000000..445f751b46 --- /dev/null +++ b/dom/animation/test/css-animations/test_animation-ready.html @@ -0,0 +1,15 @@ +<!doctype html> +<meta charset=utf-8> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<div id="log"></div> +<script> +'use strict'; +setup({explicit_done: true}); +SpecialPowers.pushPrefEnv( + { "set": [["dom.animations-api.core.enabled", true]]}, + function() { + window.open("file_animation-ready.html"); + }); +</script> +</html> diff --git a/dom/animation/test/css-animations/test_animation-reverse.html b/dom/animation/test/css-animations/test_animation-reverse.html new file mode 100644 index 0000000000..673b1e0d3a --- /dev/null +++ b/dom/animation/test/css-animations/test_animation-reverse.html @@ -0,0 +1,15 @@ +<!doctype html> +<meta charset=utf-8> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<div id="log"></div> +<script> +'use strict'; +setup({explicit_done: true}); +SpecialPowers.pushPrefEnv( + { "set": [["dom.animations-api.core.enabled", true]]}, + function() { + window.open("file_animation-reverse.html"); + }); +</script> +</html> diff --git a/dom/animation/test/css-animations/test_animation-starttime.html b/dom/animation/test/css-animations/test_animation-starttime.html new file mode 100644 index 0000000000..dfae89ffa0 --- /dev/null +++ b/dom/animation/test/css-animations/test_animation-starttime.html @@ -0,0 +1,15 @@ +<!doctype html> +<meta charset=utf-8> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<div id="log"></div> +<script> +'use strict'; +setup({explicit_done: true}); +SpecialPowers.pushPrefEnv( + { "set": [["dom.animations-api.core.enabled", true]]}, + function() { + window.open("file_animation-starttime.html"); + }); +</script> +</html> diff --git a/dom/animation/test/css-animations/test_animations-dynamic-changes.html b/dom/animation/test/css-animations/test_animations-dynamic-changes.html new file mode 100644 index 0000000000..ce4eb378dc --- /dev/null +++ b/dom/animation/test/css-animations/test_animations-dynamic-changes.html @@ -0,0 +1,15 @@ +<!doctype html> +<meta charset=utf-8> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<div id="log"></div> +<script> +'use strict'; +setup({explicit_done: true}); +SpecialPowers.pushPrefEnv( + { "set": [["dom.animations-api.core.enabled", true]]}, + function() { + window.open("file_animations-dynamic-changes.html"); + }); +</script> +</html> diff --git a/dom/animation/test/css-animations/test_cssanimation-animationname.html b/dom/animation/test/css-animations/test_cssanimation-animationname.html new file mode 100644 index 0000000000..ccddecc338 --- /dev/null +++ b/dom/animation/test/css-animations/test_cssanimation-animationname.html @@ -0,0 +1,15 @@ +<!doctype html> +<meta charset=utf-8> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<div id="log"></div> +<script> +'use strict'; +setup({explicit_done: true}); +SpecialPowers.pushPrefEnv( + { "set": [["dom.animations-api.core.enabled", true]]}, + function() { + window.open("file_cssanimation-animationname.html"); + }); +</script> +</html> diff --git a/dom/animation/test/css-animations/test_document-get-animations.html b/dom/animation/test/css-animations/test_document-get-animations.html new file mode 100644 index 0000000000..dc964e62c8 --- /dev/null +++ b/dom/animation/test/css-animations/test_document-get-animations.html @@ -0,0 +1,15 @@ +<!doctype html> +<meta charset=utf-8> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<div id="log"></div> +<script> +'use strict'; +setup({explicit_done: true}); +SpecialPowers.pushPrefEnv( + { "set": [["dom.animations-api.core.enabled", true]]}, + function() { + window.open("file_document-get-animations.html"); + }); +</script> +</html> diff --git a/dom/animation/test/css-animations/test_effect-target.html b/dom/animation/test/css-animations/test_effect-target.html new file mode 100644 index 0000000000..6c230c729f --- /dev/null +++ b/dom/animation/test/css-animations/test_effect-target.html @@ -0,0 +1,15 @@ +<!doctype html> +<meta charset=utf-8> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<div id="log"></div> +<script> +'use strict'; +setup({explicit_done: true}); +SpecialPowers.pushPrefEnv( + { "set": [["dom.animations-api.core.enabled", true]]}, + function() { + window.open("file_effect-target.html"); + }); +</script> +</html> diff --git a/dom/animation/test/css-animations/test_element-get-animations.html b/dom/animation/test/css-animations/test_element-get-animations.html new file mode 100644 index 0000000000..7b39e65ccb --- /dev/null +++ b/dom/animation/test/css-animations/test_element-get-animations.html @@ -0,0 +1,15 @@ +<!doctype html> +<meta charset=utf-8> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<div id="log"></div> +<script> +'use strict'; +setup({explicit_done: true}); +SpecialPowers.pushPrefEnv( + { "set": [["dom.animations-api.core.enabled", true]]}, + function() { + window.open("file_element-get-animations.html"); + }); +</script> +</html> diff --git a/dom/animation/test/css-animations/test_keyframeeffect-getkeyframes.html b/dom/animation/test/css-animations/test_keyframeeffect-getkeyframes.html new file mode 100644 index 0000000000..3cf2270084 --- /dev/null +++ b/dom/animation/test/css-animations/test_keyframeeffect-getkeyframes.html @@ -0,0 +1,15 @@ +<!doctype html> +<meta charset=utf-8> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<div id="log"></div> +<script> +'use strict'; +setup({explicit_done: true}); +SpecialPowers.pushPrefEnv( + { "set": [["dom.animations-api.core.enabled", true]]}, + function() { + window.open("file_keyframeeffect-getkeyframes.html"); + }); +</script> +</html> diff --git a/dom/animation/test/css-animations/test_pseudoElement-get-animations.html b/dom/animation/test/css-animations/test_pseudoElement-get-animations.html new file mode 100644 index 0000000000..1e0dc5c825 --- /dev/null +++ b/dom/animation/test/css-animations/test_pseudoElement-get-animations.html @@ -0,0 +1,14 @@ +<!doctype html> +<meta charset=utf-8> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<div id="log"></div> +<script> +'use strict'; +setup({explicit_done: true}); +SpecialPowers.pushPrefEnv( + { "set": [["dom.animations-api.core.enabled", true]]}, + function() { + window.open("file_pseudoElement-get-animations.html"); + }); +</script> diff --git a/dom/animation/test/css-transitions/file_animation-cancel.html b/dom/animation/test/css-transitions/file_animation-cancel.html new file mode 100644 index 0000000000..6094b383f3 --- /dev/null +++ b/dom/animation/test/css-transitions/file_animation-cancel.html @@ -0,0 +1,165 @@ +<!doctype html> +<meta charset=utf-8> +<script src="../testcommon.js"></script> +<body> +<script> +'use strict'; + +promise_test(function(t) { + var div = addDiv(t, { style: 'margin-left: 0px' }); + flushComputedStyle(div); + + div.style.transition = 'margin-left 100s'; + div.style.marginLeft = '1000px'; + flushComputedStyle(div); + + var animation = div.getAnimations()[0]; + return animation.ready.then(waitForFrame).then(function() { + assert_not_equals(getComputedStyle(div).marginLeft, '1000px', + 'transform style is animated before cancelling'); + animation.cancel(); + assert_equals(getComputedStyle(div).marginLeft, div.style.marginLeft, + 'transform style is no longer animated after cancelling'); + }); +}, 'Animated style is cleared after cancelling a running CSS transition'); + +promise_test(function(t) { + var div = addDiv(t, { style: 'margin-left: 0px' }); + flushComputedStyle(div); + + div.style.transition = 'margin-left 100s'; + div.style.marginLeft = '1000px'; + flushComputedStyle(div); + + div.addEventListener('transitionend', function() { + assert_unreached('Got unexpected end event on cancelled transition'); + }); + + var animation = div.getAnimations()[0]; + return animation.ready.then(function() { + // Seek to just before the end then cancel + animation.currentTime = 99.9 * 1000; + animation.cancel(); + + // Then wait a couple of frames and check that no event was dispatched + return waitForAnimationFrames(2); + }); +}, 'Cancelled CSS transitions do not dispatch events'); + +promise_test(function(t) { + var div = addDiv(t, { style: 'margin-left: 0px' }); + flushComputedStyle(div); + + div.style.transition = 'margin-left 100s'; + div.style.marginLeft = '1000px'; + flushComputedStyle(div); + + var animation = div.getAnimations()[0]; + return animation.ready.then(function() { + animation.cancel(); + assert_equals(getComputedStyle(div).marginLeft, '1000px', + 'margin-left style is not animated after cancelling'); + animation.play(); + assert_equals(getComputedStyle(div).marginLeft, '0px', + 'margin-left style is animated after re-starting transition'); + return animation.ready; + }).then(function() { + assert_equals(animation.playState, 'running', + 'Transition succeeds in running after being re-started'); + }); +}, 'After cancelling a transition, it can still be re-used'); + +promise_test(function(t) { + var div = addDiv(t, { style: 'margin-left: 0px' }); + flushComputedStyle(div); + + div.style.transition = 'margin-left 100s'; + div.style.marginLeft = '1000px'; + flushComputedStyle(div); + + var animation = div.getAnimations()[0]; + return animation.ready.then(function() { + animation.finish(); + animation.cancel(); + assert_equals(getComputedStyle(div).marginLeft, '1000px', + 'margin-left style is not animated after cancelling'); + animation.play(); + assert_equals(getComputedStyle(div).marginLeft, '0px', + 'margin-left style is animated after re-starting transition'); + return animation.ready; + }).then(function() { + assert_equals(animation.playState, 'running', + 'Transition succeeds in running after being re-started'); + }); +}, 'After cancelling a finished transition, it can still be re-used'); + +test(function(t) { + var div = addDiv(t, { style: 'margin-left: 0px' }); + flushComputedStyle(div); + + div.style.transition = 'margin-left 100s'; + div.style.marginLeft = '1000px'; + flushComputedStyle(div); + + var animation = div.getAnimations()[0]; + animation.cancel(); + assert_equals(getComputedStyle(div).marginLeft, '1000px', + 'margin-left style is not animated after cancelling'); + + // Trigger a change to a transition property and check that this + // doesn't cause the animation to become live again + div.style.transitionDuration = '200s'; + flushComputedStyle(div); + assert_equals(getComputedStyle(div).marginLeft, '1000px', + 'margin-left style is still not animated after updating' + + ' transition-duration'); + assert_equals(animation.playState, 'idle', + 'Transition is still idle after updating transition-duration'); +}, 'After cancelling a transition, updating transition properties doesn\'t make' + + ' it live again'); + +promise_test(function(t) { + var div = addDiv(t, { style: 'margin-left: 0px' }); + flushComputedStyle(div); + + div.style.transition = 'margin-left 100s'; + div.style.marginLeft = '1000px'; + flushComputedStyle(div); + + var animation = div.getAnimations()[0]; + return animation.ready.then(function() { + assert_equals(animation.playState, 'running'); + div.style.display = 'none'; + return waitForFrame(); + }).then(function() { + assert_equals(animation.playState, 'idle'); + assert_equals(getComputedStyle(div).marginLeft, '1000px'); + }); +}, 'Setting display:none on an element cancels its transitions'); + +promise_test(function(t) { + var parentDiv = addDiv(t); + var childDiv = document.createElement('div'); + parentDiv.appendChild(childDiv); + childDiv.setAttribute('style', 'margin-left: 0px'); + + flushComputedStyle(childDiv); + + childDiv.style.transition = 'margin-left 100s'; + childDiv.style.marginLeft = '1000px'; + flushComputedStyle(childDiv); + + var animation = childDiv.getAnimations()[0]; + return animation.ready.then(function() { + assert_equals(animation.playState, 'running'); + parentDiv.style.display = 'none'; + return waitForFrame(); + }).then(function() { + assert_equals(animation.playState, 'idle'); + assert_equals(getComputedStyle(childDiv).marginLeft, '1000px'); + }); +}, 'Setting display:none cancels transitions on a child element'); + +done(); +</script> +</body> diff --git a/dom/animation/test/css-transitions/file_animation-computed-timing.html b/dom/animation/test/css-transitions/file_animation-computed-timing.html new file mode 100644 index 0000000000..2dac82d753 --- /dev/null +++ b/dom/animation/test/css-transitions/file_animation-computed-timing.html @@ -0,0 +1,315 @@ +<!doctype html> +<meta charset=utf-8> +<script src="../testcommon.js"></script> +<style> + +.animated-div { + margin-left: 100px; +} + +</style> +<body> +<script> + +'use strict'; + +// -------------------- +// delay +// -------------------- +test(function(t) { + var div = addDiv(t, {'class': 'animated-div'}); + div.style.transition = 'margin-left 10s'; + flushComputedStyle(div); + div.style.marginLeft = '10px'; + + + var effect = div.getAnimations()[0].effect; + assert_equals(effect.getComputedTiming().delay, 0, + 'Initial value of delay'); +}, 'delay of a new tranisition'); + +test(function(t) { + var div = addDiv(t, {'class': 'animated-div'}); + div.style.transition = 'margin-left 10s 10s'; + flushComputedStyle(div); + div.style.marginLeft = '10px'; + + var effect = div.getAnimations()[0].effect; + assert_equals(effect.getComputedTiming().delay, 10000, + 'Initial value of delay'); +}, 'Positive delay of a new transition'); + +test(function(t) { + var div = addDiv(t, {'class': 'animated-div'}); + div.style.transition = 'margin-left 10s -5s'; + flushComputedStyle(div); + div.style.marginLeft = '10px'; + + var effect = div.getAnimations()[0].effect; + assert_equals(effect.getComputedTiming().delay, -5000, + 'Initial value of delay'); +}, 'Negative delay of a new transition'); + + +// -------------------- +// endDelay +// -------------------- +test(function(t) { + var div = addDiv(t, {'class': 'animated-div'}); + div.style.transition = 'margin-left 10s'; + flushComputedStyle(div); + div.style.marginLeft = '10px'; + + var effect = div.getAnimations()[0].effect; + assert_equals(effect.getComputedTiming().endDelay, 0, + 'Initial value of endDelay'); +}, 'endDelay of a new transition'); + + +// -------------------- +// fill +// -------------------- +test(function(t) { + var div = addDiv(t, {'class': 'animated-div'}); + div.style.transition = 'margin-left 10s'; + flushComputedStyle(div); + div.style.marginLeft = '10px'; + + var effect = div.getAnimations()[0].effect; + assert_equals(effect.getComputedTiming().fill, 'backwards', + 'Fill backwards'); +}, 'fill of a new transition'); + + +// -------------------- +// iterationStart +// -------------------- +test(function(t) { + var div = addDiv(t, {'class': 'animated-div'}); + div.style.transition = 'margin-left 10s'; + flushComputedStyle(div); + div.style.marginLeft = '10px'; + + var effect = div.getAnimations()[0].effect; + assert_equals(effect.getComputedTiming().iterationStart, 0, + 'Initial value of iterationStart'); +}, 'iterationStart of a new transition'); + + +// -------------------- +// iterations +// -------------------- +test(function(t) { + var div = addDiv(t, {'class': 'animated-div'}); + div.style.transition = 'margin-left 10s'; + flushComputedStyle(div); + div.style.marginLeft = '10px'; + + var effect = div.getAnimations()[0].effect; + assert_equals(effect.getComputedTiming().iterations, 1, + 'Initial value of iterations'); +}, 'iterations of a new transition'); + + +// -------------------- +// duration +// -------------------- +test(function(t) { + var div = addDiv(t, {'class': 'animated-div'}); + div.style.transition = 'margin-left 10s'; + flushComputedStyle(div); + div.style.marginLeft = '10px'; + + var effect = div.getAnimations()[0].effect; + assert_equals(effect.getComputedTiming().duration, 10000, + 'Initial value of duration'); +}, 'duration of a new transition'); + + +// -------------------- +// direction +// -------------------- +test(function(t) { + var div = addDiv(t, {'class': 'animated-div'}); + div.style.transition = 'margin-left 10s'; + flushComputedStyle(div); + div.style.marginLeft = '10px'; + + var effect = div.getAnimations()[0].effect; + assert_equals(effect.getComputedTiming().direction, 'normal', + 'Initial value of direction'); +}, 'direction of a new transition'); + + +// -------------------- +// easing +// -------------------- +test(function(t) { + var div = addDiv(t, {'class': 'animated-div'}); + div.style.transition = 'margin-left 10s'; + flushComputedStyle(div); + div.style.marginLeft = '10px'; + + var effect = div.getAnimations()[0].effect; + assert_equals(effect.getComputedTiming().easing, 'linear', + 'Initial value of easing'); +}, 'easing of a new transition'); + + +// ------------------------------ +// endTime +// = max(start delay + active duration + end delay, 0) +// -------------------- +test(function(t) { + var div = addDiv(t, {'class': 'animated-div'}); + div.style.transition = 'margin-left 100s -5s'; + flushComputedStyle(div); + div.style.marginLeft = '10px'; + + var effect = div.getAnimations()[0].effect; + var answer = 100000 - 5000; // ms + assert_equals(effect.getComputedTiming().endTime, answer, + 'Initial value of endTime'); +}, 'endTime of a new transition'); + + +// -------------------- +// activeDuration +// = iteration duration * iteration count(==1) +// -------------------- +test(function(t) { + var div = addDiv(t, {'class': 'animated-div'}); + div.style.transition = 'margin-left 100s -5s'; + flushComputedStyle(div); + div.style.marginLeft = '10px'; + + var effect = div.getAnimations()[0].effect; + assert_equals(effect.getComputedTiming().activeDuration, 100000, + 'Initial value of activeDuration'); +}, 'activeDuration of a new transition'); + + +// -------------------- +// localTime +// -------------------- +test(function(t) { + var div = addDiv(t, {'class': 'animated-div'}); + div.style.transition = 'margin-left 100s'; + flushComputedStyle(div); + div.style.marginLeft = '10px'; + + var effect = div.getAnimations()[0].effect; + assert_equals(effect.getComputedTiming().localTime, 0, + 'Initial value of localTime'); +}, 'localTime of a new transition'); + +test(function(t) { + var div = addDiv(t, {'class': 'animated-div'}); + div.style.transition = 'margin-left 100s'; + flushComputedStyle(div); + div.style.marginLeft = '10px'; + + var anim = div.getAnimations()[0]; + anim.currentTime = 5000; + assert_equals(anim.effect.getComputedTiming().localTime, anim.currentTime, + 'current localTime after setting currentTime'); +}, 'localTime is always equal to currentTime'); + +async_test(function(t) { + var div = addDiv(t, {'class': 'animated-div'}); + div.style.transition = 'margin-left 100s'; + flushComputedStyle(div); + div.style.marginLeft = '10px'; + + var anim = div.getAnimations()[0]; + anim.playbackRate = 2; // 2 times faster + + anim.ready.then(t.step_func(function() { + assert_equals(anim.effect.getComputedTiming().localTime, anim.currentTime, + 'localTime is equal to currentTime'); + return waitForFrame(); + })).then(t.step_func_done(function() { + assert_equals(anim.effect.getComputedTiming().localTime, anim.currentTime, + 'localTime is equal to currentTime'); + })); +}, 'localTime reflects playbackRate immediately'); + + +// -------------------- +// progress +// -------------------- +test(function(t) { + var div = addDiv(t, {'class': 'animated-div'}); + div.style.transition = 'margin-left 10.5s'; + flushComputedStyle(div); + div.style.marginLeft = '10px'; + + var effect = div.getAnimations()[0].effect; + assert_equals(effect.getComputedTiming().progress, 0.0, + 'Initial value of progress'); +}, 'progress of a new transition'); + +test(function(t) { + var div = addDiv(t, {'class': 'animated-div'}); + div.style.transition = 'margin-left 10.5s 2s'; + flushComputedStyle(div); + div.style.marginLeft = '10px'; + + var effect = div.getAnimations()[0].effect; + assert_equals(effect.getComputedTiming().progress, 0.0, + 'Initial value of progress'); +}, 'progress of a new transition with positive delay in before phase'); + +test(function(t) { + var div = addDiv(t, {'class': 'animated-div'}); + div.style.transition = 'margin-left 10.5s'; + flushComputedStyle(div); + div.style.marginLeft = '10px'; + + var anim = div.getAnimations()[0]; + anim.finish() + assert_equals(anim.effect.getComputedTiming().progress, null, + 'finished progress'); +}, 'progress of a finished transition'); + + +// -------------------- +// currentIteration +// -------------------- +test(function(t) { + var div = addDiv(t, {'class': 'animated-div'}); + div.style.transition = 'margin-left 10s'; + flushComputedStyle(div); + div.style.marginLeft = '10px'; + + var effect = div.getAnimations()[0].effect; + assert_equals(effect.getComputedTiming().currentIteration, 0, + 'Initial value of currentIteration'); +}, 'currentIteration of a new transition'); + +test(function(t) { + var div = addDiv(t, {'class': 'animated-div'}); + div.style.transition = 'margin-left 10s 2s'; + flushComputedStyle(div); + div.style.marginLeft = '10px'; + + var effect = div.getAnimations()[0].effect; + assert_equals(effect.getComputedTiming().currentIteration, 0, + 'Initial value of currentIteration'); +}, 'currentIteration of a new transition with positive delay in before phase'); + +test(function(t) { + var div = addDiv(t, {'class': 'animated-div'}); + div.style.transition = 'margin-left 10s'; + flushComputedStyle(div); + div.style.marginLeft = '10px'; + + var anim = div.getAnimations()[0]; + anim.finish(); + assert_equals(anim.effect.getComputedTiming().currentIteration, null, + 'finished currentIteration'); +}, 'currentIteration of a finished transition'); + +done(); +</script> +</body> diff --git a/dom/animation/test/css-transitions/file_animation-currenttime.html b/dom/animation/test/css-transitions/file_animation-currenttime.html new file mode 100644 index 0000000000..2a0f105d20 --- /dev/null +++ b/dom/animation/test/css-transitions/file_animation-currenttime.html @@ -0,0 +1,307 @@ +<!doctype html> +<html> + <head> + <meta charset=utf-8> + <title>Tests for the effect of setting a CSS transition's + Animation.currentTime</title> + <style> + +.animated-div { + margin-left: 100px; + transition: margin-left 1000s linear 1000s; +} + + </style> + <script src="../testcommon.js"></script> + </head> + <body> + <script type="text/javascript"> + +'use strict'; + +// TODO: Once the computedTiming property is implemented, add checks to the +// checker helpers to ensure that computedTiming's properties are updated as +// expected. +// See https://bugzilla.mozilla.org/show_bug.cgi?id=1108055 + + +const ANIM_DELAY_MS = 1000000; // 1000s +const ANIM_DUR_MS = 1000000; // 1000s + +/** + * These helpers get the value that the currentTime needs to be set to, to put + * an animation that uses the above ANIM_DELAY_MS and ANIM_DUR_MS values into + * the middle of various phases or points through the active duration. + */ +function currentTimeForBeforePhase() { + return ANIM_DELAY_MS / 2; +} +function currentTimeForActivePhase() { + return ANIM_DELAY_MS + ANIM_DUR_MS / 2; +} +function currentTimeForAfterPhase() { + return ANIM_DELAY_MS + ANIM_DUR_MS + ANIM_DELAY_MS / 2; +} +function currentTimeForStartOfActiveInterval() { + return ANIM_DELAY_MS; +} +function currentTimeForFiftyPercentThroughActiveInterval() { + return ANIM_DELAY_MS + ANIM_DUR_MS * 0.5; +} +function currentTimeForEndOfActiveInterval() { + return ANIM_DELAY_MS + ANIM_DUR_MS; +} + + +// Expected computed 'margin-left' values at points during the active interval: +// When we assert_between_inclusive using these values we could in theory cause +// intermittent failure due to very long delays between paints, but since the +// active duration is 1000s long, a delay would need to be around 100s to cause +// that. If that's happening then there are likely other issues that should be +// fixed, so a failure to make us look into that seems like a good thing. +const INITIAL_POSITION = 100; +const TEN_PCT_POSITION = 110; +const FIFTY_PCT_POSITION = 150; +const END_POSITION = 200; + + +// The terms used for the naming of the following helper functions refer to +// terms used in the Web Animations specification for specific phases of an +// animation. The terms can be found here: +// +// http://w3c.github.io/web-animations/#animation-effect-phases-and-states + +// Called when currentTime is set to zero (the beginning of the start delay). +function checkStateOnSettingCurrentTimeToZero(animation) +{ + // We don't test animation.currentTime since our caller just set it. + + assert_equals(animation.playState, 'running', + 'Animation.playState should be "running" at the start of ' + + 'the start delay'); + + assert_equals(animation.effect.target.style.animationPlayState, 'running', + 'Animation.effect.target.style.animationPlayState should be ' + + '"running" at the start of the start delay'); + + var div = animation.effect.target; + var marginLeft = parseFloat(getComputedStyle(div).marginLeft); + assert_equals(marginLeft, UNANIMATED_POSITION, + 'the computed value of margin-left should be unaffected ' + + 'at the beginning of the start delay'); +} + +// Called when the ready Promise's callbacks should happen +function checkStateOnReadyPromiseResolved(animation) +{ + // the 0.0001 here is for rounding error + assert_less_than_equal(animation.currentTime, + animation.timeline.currentTime - animation.startTime + 0.0001, + 'Animation.currentTime should be less than the local time ' + + 'equivalent of the timeline\'s currentTime on the first paint tick ' + + 'after animation creation'); + + assert_equals(animation.playState, 'running', + 'Animation.playState should be "running" on the first paint ' + + 'tick after animation creation'); + + var div = animation.effect.target; + var marginLeft = parseFloat(getComputedStyle(div).marginLeft); + assert_equals(marginLeft, INITIAL_POSITION, + 'the computed value of margin-left should be unaffected ' + + 'by an animation with a delay on ready Promise resolve'); +} + +// Called when currentTime is set to the time the active interval starts. +function checkStateAtActiveIntervalStartTime(animation) +{ + // We don't test animation.currentTime since our caller just set it. + + assert_equals(animation.playState, 'running', + 'Animation.playState should be "running" at the start of ' + + 'the active interval'); + + var div = animation.effect.target; + var marginLeft = parseFloat(getComputedStyle(div).marginLeft); + assert_between_inclusive(marginLeft, INITIAL_POSITION, TEN_PCT_POSITION, + 'the computed value of margin-left should be close to the value at the ' + + 'beginning of the animation'); +} + +function checkStateAtFiftyPctOfActiveInterval(animation) +{ + // We don't test animation.currentTime since our caller just set it. + + var div = animation.effect.target; + var marginLeft = parseFloat(getComputedStyle(div).marginLeft); + assert_equals(marginLeft, FIFTY_PCT_POSITION, + 'the computed value of margin-left should be half way through the ' + + 'animation at the midpoint of the active interval'); +} + +// Called when currentTime is set to the time the active interval ends. +function checkStateAtActiveIntervalEndTime(animation) +{ + // We don't test animation.currentTime since our caller just set it. + + assert_equals(animation.playState, 'finished', + 'Animation.playState should be "finished" at the end of ' + + 'the active interval'); + + var div = animation.effect.target; + var marginLeft = parseFloat(getComputedStyle(div).marginLeft); + assert_equals(marginLeft, END_POSITION, + 'the computed value of margin-left should be the final transitioned-to ' + + 'value at the end of the active duration'); +} + +test(function(t) +{ + var div = addDiv(t, {'class': 'animated-div'}); + flushComputedStyle(div); + div.style.marginLeft = '200px'; // initiate transition + + var animation = div.getAnimations()[0]; + assert_equals(animation.currentTime, 0, 'currentTime should be zero'); +}, 'currentTime of a newly created transition is zero'); + + +test(function(t) +{ + var div = addDiv(t, {'class': 'animated-div'}); + flushComputedStyle(div); + div.style.marginLeft = '200px'; // initiate transition + + var animation = div.getAnimations()[0]; + + // So that animation is running instead of paused when we set currentTime: + animation.startTime = animation.timeline.currentTime; + + animation.currentTime = 10; + assert_equals(animation.currentTime, 10, + 'Check setting of currentTime actually works'); +}, 'Sanity test to check round-tripping assigning to new animation\'s ' + + 'currentTime'); + + +async_test(function(t) { + var div = addDiv(t, {'class': 'animated-div'}); + var eventWatcher = new EventWatcher(t, div, 'transitionend'); + + flushComputedStyle(div); + div.style.marginLeft = '200px'; // initiate transition + + var animation = div.getAnimations()[0]; + + animation.ready.then(t.step_func(function() { + checkStateOnReadyPromiseResolved(animation); + + animation.currentTime = currentTimeForStartOfActiveInterval(); + checkStateAtActiveIntervalStartTime(animation); + + animation.currentTime = currentTimeForFiftyPercentThroughActiveInterval(); + checkStateAtFiftyPctOfActiveInterval(animation); + + animation.currentTime = currentTimeForEndOfActiveInterval(); + return eventWatcher.wait_for('transitionend'); + })).then(t.step_func(function() { + checkStateAtActiveIntervalEndTime(animation); + })).catch(t.step_func(function(reason) { + assert_unreached(reason); + })).then(function() { + t.done(); + }); +}, 'Skipping forward through transition'); + + +test(function(t) { + var div = addDiv(t, {'class': 'animated-div'}); + var eventWatcher = new EventWatcher(t, div, 'transitionend'); + + flushComputedStyle(div); + div.style.marginLeft = '200px'; // initiate transition + + var animation = div.getAnimations()[0]; + + // Unlike in the case of CSS animations, we cannot skip to the end and skip + // backwards since when we reach the end the transition effect is removed and + // changes to the Animation object no longer affect the element. For + // this reason we only skip forwards as far as the 50% through point. + + animation.ready.then(t.step_func(function() { + animation.currentTime = currentTimeForFiftyPercentThroughActiveInterval(); + checkStateAtFiftyPctOfActiveInterval(animation); + + animation.currentTime = currentTimeForStartOfActiveInterval(); + + // Despite going backwards from being in the active interval to being + // before it, we now expect a 'transitionend' event because the transition + // should go from being active to inactive. + // + // Calling checkStateAtActiveIntervalStartTime will check computed style, + // causing computed style to be updated and the 'transitionend' event to + // be dispatched synchronously. We need to call wait_for first + // otherwise eventWatcher will assert that the event was unexpected. + eventWatcher.wait_for('transitionend').then(function() { + t.done(); + }); + checkStateAtActiveIntervalStartTime(animation); + })); +}, 'Skipping backwards through transition'); + + +async_test(function(t) { + var div = addDiv(t, {'class': 'animated-div'}); + flushComputedStyle(div); + div.style.marginLeft = '200px'; // initiate transition + + var animation = div.getAnimations()[0]; + + animation.ready.then(t.step_func(function() { + var exception; + try { + animation.currentTime = null; + } catch (e) { + exception = e; + } + assert_equals(exception.name, 'TypeError', + 'Expect TypeError exception on trying to set ' + + 'Animation.currentTime to null'); + })).catch(t.step_func(function(reason) { + assert_unreached(reason); + })).then(function() { + t.done(); + }); +}, 'Setting currentTime to null'); + + +async_test(function(t) { + var div = addDiv(t, {'class': 'animated-div'}); + flushComputedStyle(div); + div.style.marginLeft = '200px'; // initiate transition + + var animation = div.getAnimations()[0]; + var pauseTime; + + animation.ready.then(t.step_func(function() { + assert_not_equals(animation.currentTime, null, + 'Animation.currentTime not null on ready Promise resolve'); + animation.pause(); + return animation.ready; + })).then(t.step_func(function() { + pauseTime = animation.currentTime; + return waitForFrame(); + })).then(t.step_func(function() { + assert_equals(animation.currentTime, pauseTime, + 'Animation.currentTime is unchanged after pausing'); + })).catch(t.step_func(function(reason) { + assert_unreached(reason); + })).then(function() { + t.done(); + }); +}, 'Animation.currentTime after pausing'); + +done(); + </script> + </body> +</html> diff --git a/dom/animation/test/css-transitions/file_animation-finished.html b/dom/animation/test/css-transitions/file_animation-finished.html new file mode 100644 index 0000000000..2f6bcf47d8 --- /dev/null +++ b/dom/animation/test/css-transitions/file_animation-finished.html @@ -0,0 +1,61 @@ +<!doctype html> +<meta charset=utf-8> +<script src="../testcommon.js"></script> +<style> + +.animated-div { + margin-left: 100px; + transition: margin-left 1000s linear 1000s; +} + +</style> +<body> +<script> + +'use strict'; + +const ANIM_DELAY_MS = 1000000; // 1000s +const ANIM_DUR_MS = 1000000; // 1000s + +async_test(function(t) { + var div = addDiv(t, {'class': 'animated-div'}); + flushComputedStyle(div); + div.style.marginLeft = '200px'; // initiate transition + + var animation = div.getAnimations()[0]; + + animation.finish(); + + animation.finished.then(t.step_func(function() { + animation.play(); + assert_equals(animation.currentTime, 0, + 'Replaying a finished transition should reset its ' + + 'currentTime'); + t.done(); + })); +}, 'Test restarting a finished transition'); + +async_test(function(t) { + var div = addDiv(t, {'class': 'animated-div'}); + flushComputedStyle(div); + div.style.marginLeft = '200px'; // initiate transition + + var animation = div.getAnimations()[0]; + + animation.ready.then(function() { + animation.playbackRate = -1; + return animation.finished; + }).then(t.step_func(function() { + animation.play(); + // FIXME: once animation.effect.computedTiming.endTime is available (bug + // 1108055) we should use that here. + assert_equals(animation.currentTime, ANIM_DELAY_MS + ANIM_DUR_MS, + 'Replaying a finished reversed transition should reset ' + + 'its currentTime to the end of the effect'); + t.done(); + })); +}, 'Test restarting a reversed finished transition'); + +done(); +</script> +</body> diff --git a/dom/animation/test/css-transitions/file_animation-pausing.html b/dom/animation/test/css-transitions/file_animation-pausing.html new file mode 100644 index 0000000000..b2f2d46188 --- /dev/null +++ b/dom/animation/test/css-transitions/file_animation-pausing.html @@ -0,0 +1,50 @@ +<!doctype html> +<meta charset=utf-8> +<script src="../testcommon.js"></script> +<body> +<script> +'use strict'; + +function getMarginLeft(cs) { + return parseFloat(cs.marginLeft); +} + +async_test(function(t) { + var div = addDiv(t); + var cs = window.getComputedStyle(div); + + div.style.marginLeft = '0px'; + cs.marginLeft; // Flush style to set up transition start point + div.style.transition = 'margin-left 100s'; + div.style.marginLeft = '10000px'; + cs.marginLeft; + + var animation = div.getAnimations()[0]; + assert_equals(getMarginLeft(cs), 0, + 'Initial value of margin-left is zero'); + var previousAnimVal = getMarginLeft(cs); + + animation.ready.then(waitForFrame).then(t.step_func(function() { + assert_true(getMarginLeft(cs) > previousAnimVal, + 'margin-left is initially increasing'); + animation.pause(); + return animation.ready; + })).then(t.step_func(function() { + previousAnimVal = getMarginLeft(cs); + return waitForFrame(); + })).then(t.step_func(function() { + assert_equals(getMarginLeft(cs), previousAnimVal, + 'margin-left does not increase after calling pause()'); + previousAnimVal = getMarginLeft(cs); + animation.play(); + return animation.ready.then(waitForFrame); + })).then(t.step_func(function() { + assert_true(getMarginLeft(cs) > previousAnimVal, + 'margin-left increases after calling play()'); + t.done(); + })); +}, 'pause() and play() a transition'); + +done(); +</script> +</body> diff --git a/dom/animation/test/css-transitions/file_animation-ready.html b/dom/animation/test/css-transitions/file_animation-ready.html new file mode 100644 index 0000000000..f141da7960 --- /dev/null +++ b/dom/animation/test/css-transitions/file_animation-ready.html @@ -0,0 +1,96 @@ +<!doctype html> +<meta charset=utf-8> +<script src="../testcommon.js"></script> +<body> +<script> +'use strict'; + +async_test(function(t) { + var div = addDiv(t); + div.style.transform = 'translate(0px)'; + window.getComputedStyle(div).transform; + div.style.transition = 'transform 100s'; + div.style.transform = 'translate(10px)'; + window.getComputedStyle(div).transform; + + var animation = div.getAnimations()[0]; + var originalReadyPromise = animation.ready; + + animation.ready.then(t.step_func(function() { + assert_equals(animation.ready, originalReadyPromise, + 'Ready promise is the same object when playing completes'); + animation.pause(); + assert_not_equals(animation.ready, originalReadyPromise, + 'Ready promise object identity differs when pausing'); + t.done(); + })); +}, 'A new ready promise is created each time play() is called' + + ' the animation property'); + +async_test(function(t) { + var div = addDiv(t); + + // Set up pending transition + div.style.transform = 'translate(0px)'; + window.getComputedStyle(div).transform; + div.style.transition = 'transform 100s'; + div.style.transform = 'translate(10px)'; + window.getComputedStyle(div).transform; + + var animation = div.getAnimations()[0]; + assert_equals(animation.playState, 'pending', 'Animation is initially pending'); + + // Set up listeners on ready promise + animation.ready.then(t.step_func(function() { + assert_unreached('ready promise was fulfilled'); + })).catch(t.step_func(function(err) { + assert_equals(err.name, 'AbortError', + 'ready promise is rejected with AbortError'); + assert_equals(animation.playState, 'idle', + 'Animation is idle after transition was cancelled'); + })).then(t.step_func(function() { + t.done(); + })); + + // Now remove transform from transition-property and flush styles + div.style.transitionProperty = 'none'; + window.getComputedStyle(div).transitionProperty; + +}, 'ready promise is rejected when a transition is cancelled by updating' + + ' transition-property'); + +async_test(function(t) { + var div = addDiv(t); + + // Set up pending transition + div.style.marginLeft = '0px'; + window.getComputedStyle(div).marginLeft; + div.style.transition = 'margin-left 100s'; + div.style.marginLeft = '100px'; + window.getComputedStyle(div).marginLeft; + + var animation = div.getAnimations()[0]; + assert_equals(animation.playState, 'pending', 'Animation is initially pending'); + + // Set up listeners on ready promise + animation.ready.then(t.step_func(function() { + assert_unreached('ready promise was fulfilled'); + })).catch(t.step_func(function(err) { + assert_equals(err.name, 'AbortError', + 'ready promise is rejected with AbortError'); + assert_equals(animation.playState, 'idle', + 'Animation is idle after transition was cancelled'); + })).then(t.step_func(function() { + t.done(); + })); + + // Now update the transition to animate to something not-interpolable + div.style.marginLeft = 'auto'; + window.getComputedStyle(div).marginLeft; + +}, 'ready promise is rejected when a transition is cancelled by changing' + + ' the transition property to something not interpolable'); + +done(); +</script> +</body> diff --git a/dom/animation/test/css-transitions/file_animation-starttime.html b/dom/animation/test/css-transitions/file_animation-starttime.html new file mode 100644 index 0000000000..a156ba0a08 --- /dev/null +++ b/dom/animation/test/css-transitions/file_animation-starttime.html @@ -0,0 +1,284 @@ +<!doctype html> +<html> + <head> + <meta charset=utf-8> + <title>Tests for the effect of setting a CSS transition's + Animation.startTime</title> + <style> + +.animated-div { + margin-left: 100px; + transition: margin-left 1000s linear 1000s; +} + + </style> + <script src="../testcommon.js"></script> + </head> + <body> + <script type="text/javascript"> + +'use strict'; + +// TODO: Once the computedTiming property is implemented, add checks to the +// checker helpers to ensure that computedTiming's properties are updated as +// expected. +// See https://bugzilla.mozilla.org/show_bug.cgi?id=1108055 + + +const ANIM_DELAY_MS = 1000000; // 1000s +const ANIM_DUR_MS = 1000000; // 1000s + +/** + * These helpers get the value that the startTime needs to be set to, to put an + * animation that uses the above ANIM_DELAY_MS and ANIM_DUR_MS values into the + * middle of various phases or points through the active duration. + */ +function startTimeForBeforePhase(timeline) { + return timeline.currentTime - ANIM_DELAY_MS / 2; +} +function startTimeForActivePhase(timeline) { + return timeline.currentTime - ANIM_DELAY_MS - ANIM_DUR_MS / 2; +} +function startTimeForAfterPhase(timeline) { + return timeline.currentTime - ANIM_DELAY_MS - ANIM_DUR_MS - ANIM_DELAY_MS / 2; +} +function startTimeForStartOfActiveInterval(timeline) { + return timeline.currentTime - ANIM_DELAY_MS; +} +function startTimeForFiftyPercentThroughActiveInterval(timeline) { + return timeline.currentTime - ANIM_DELAY_MS - ANIM_DUR_MS * 0.5; +} +function startTimeForEndOfActiveInterval(timeline) { + return timeline.currentTime - ANIM_DELAY_MS - ANIM_DUR_MS; +} + + +// Expected computed 'margin-left' values at points during the active interval: +// When we assert_between_inclusive using these values we could in theory cause +// intermittent failure due to very long delays between paints, but since the +// active duration is 1000s long, a delay would need to be around 100s to cause +// that. If that's happening then there are likely other issues that should be +// fixed, so a failure to make us look into that seems like a good thing. +const INITIAL_POSITION = 100; +const TEN_PCT_POSITION = 110; +const FIFTY_PCT_POSITION = 150; +const END_POSITION = 200; + +// The terms used for the naming of the following helper functions refer to +// terms used in the Web Animations specification for specific phases of an +// animation. The terms can be found here: +// +// https://w3c.github.io/web-animations/#animation-effect-phases-and-states +// +// Note the distinction between the "animation start time" which occurs before +// the start delay and the start of the active interval which occurs after it. + +// Called when the ready Promise's callbacks should happen +function checkStateOnReadyPromiseResolved(animation) +{ + assert_less_than_equal(animation.startTime, animation.timeline.currentTime, + 'Animation.startTime should be less than the timeline\'s ' + + 'currentTime on the first paint tick after animation creation'); + + assert_equals(animation.playState, 'running', + 'Animation.playState should be "running" on the first paint ' + + 'tick after animation creation'); + + var div = animation.effect.target; + var marginLeft = parseFloat(getComputedStyle(div).marginLeft); + assert_equals(marginLeft, INITIAL_POSITION, + 'the computed value of margin-left should be unaffected ' + + 'by an animation with a delay on ready Promise resolve'); +} + +// Called when startTime is set to the time the active interval starts. +function checkStateAtActiveIntervalStartTime(animation) +{ + // We don't test animation.startTime since our caller just set it. + + assert_equals(animation.playState, 'running', + 'Animation.playState should be "running" at the start of ' + + 'the active interval'); + + var div = animation.effect.target; + var marginLeft = parseFloat(getComputedStyle(div).marginLeft); + assert_between_inclusive(marginLeft, INITIAL_POSITION, TEN_PCT_POSITION, + 'the computed value of margin-left should be close to the value at the ' + + 'beginning of the animation'); +} + +function checkStateAtFiftyPctOfActiveInterval(animation) +{ + // We don't test animation.startTime since our caller just set it. + + var div = animation.effect.target; + var marginLeft = parseFloat(getComputedStyle(div).marginLeft); + assert_equals(marginLeft, FIFTY_PCT_POSITION, + 'the computed value of margin-left should be half way through the ' + + 'animation at the midpoint of the active interval'); +} + +// Called when startTime is set to the time the active interval ends. +function checkStateAtActiveIntervalEndTime(animation) +{ + // We don't test animation.startTime since our caller just set it. + + assert_equals(animation.playState, 'finished', + 'Animation.playState should be "finished" at the end of ' + + 'the active interval'); + + var div = animation.effect.target; + var marginLeft = parseFloat(getComputedStyle(div).marginLeft); + assert_equals(marginLeft, END_POSITION, + 'the computed value of margin-left should be the final transitioned-to ' + + 'value at the end of the active duration'); +} + +test(function(t) +{ + var div = addDiv(t, {'class': 'animated-div'}); + flushComputedStyle(div); + div.style.marginLeft = '200px'; // initiate transition + + var animation = div.getAnimations()[0]; + assert_equals(animation.startTime, null, 'startTime is unresolved'); +}, 'startTime of a newly created transition is unresolved'); + + +test(function(t) +{ + var div = addDiv(t, {'class': 'animated-div'}); + flushComputedStyle(div); + div.style.marginLeft = '200px'; // initiate transition + + var animation = div.getAnimations()[0]; + var currentTime = animation.timeline.currentTime; + animation.startTime = currentTime; + assert_approx_equals(animation.startTime, currentTime, 0.0001, // rounding error + 'Check setting of startTime actually works'); +}, 'Sanity test to check round-tripping assigning to new animation\'s ' + + 'startTime'); + + +async_test(function(t) { + var div = addDiv(t, {'class': 'animated-div'}); + var eventWatcher = new EventWatcher(t, div, 'transitionend'); + + flushComputedStyle(div); + div.style.marginLeft = '200px'; // initiate transition + + var animation = div.getAnimations()[0]; + + animation.ready.then(t.step_func(function() { + checkStateOnReadyPromiseResolved(animation); + + animation.startTime = startTimeForStartOfActiveInterval(animation.timeline); + checkStateAtActiveIntervalStartTime(animation); + + animation.startTime = + startTimeForFiftyPercentThroughActiveInterval(animation.timeline); + checkStateAtFiftyPctOfActiveInterval(animation); + + animation.startTime = startTimeForEndOfActiveInterval(animation.timeline); + return eventWatcher.wait_for('transitionend'); + })).then(t.step_func(function() { + checkStateAtActiveIntervalEndTime(animation); + })).catch(t.step_func(function(reason) { + assert_unreached(reason); + })).then(function() { + t.done(); + }); +}, 'Skipping forward through animation'); + + +test(function(t) { + var div = addDiv(t, {'class': 'animated-div'}); + var eventWatcher = new EventWatcher(t, div, 'transitionend'); + + flushComputedStyle(div); + div.style.marginLeft = '200px'; // initiate transition + + var animation = div.getAnimations()[0]; + + // Unlike in the case of CSS animations, we cannot skip to the end and skip + // backwards since when we reach the end the transition effect is removed and + // changes to the Animation object no longer affect the element. For + // this reason we only skip forwards as far as the 90% through point. + + animation.startTime = + startTimeForFiftyPercentThroughActiveInterval(animation.timeline); + checkStateAtFiftyPctOfActiveInterval(animation); + + animation.startTime = startTimeForStartOfActiveInterval(animation.timeline); + + // Despite going backwards from being in the active interval to being before + // it, we now expect an 'animationend' event because the animation should go + // from being active to inactive. + // + // Calling checkStateAtActiveIntervalStartTime will check computed style, + // causing computed style to be updated and the 'transitionend' event to + // be dispatched synchronously. We need to call waitForEvent first + // otherwise eventWatcher will assert that the event was unexpected. + eventWatcher.wait_for('transitionend').then(function() { + t.done(); + }); + checkStateAtActiveIntervalStartTime(animation); +}, 'Skipping backwards through transition'); + + +async_test(function(t) { + var div = addDiv(t, {'class': 'animated-div'}); + + flushComputedStyle(div); + div.style.marginLeft = '200px'; // initiate transition + + var animation = div.getAnimations()[0]; + + var storedCurrentTime; + + animation.ready.then(t.step_func(function() { + storedCurrentTime = animation.currentTime; + animation.startTime = null; + return animation.ready; + })).catch(t.step_func(function(reason) { + assert_unreached(reason); + })).then(t.step_func(function() { + assert_equals(animation.currentTime, storedCurrentTime, + 'Test that hold time is correct'); + t.done(); + })); +}, 'Setting startTime to null'); + + +async_test(function(t) { + var div = addDiv(t, {'class': 'animated-div'}); + + flushComputedStyle(div); + div.style.marginLeft = '200px'; // initiate transition + + var animation = div.getAnimations()[0]; + + animation.ready.then(t.step_func(function() { + var savedStartTime = animation.startTime; + + assert_not_equals(animation.startTime, null, + 'Animation.startTime not null on ready Promise resolve'); + + animation.pause(); + return animation.ready; + })).then(t.step_func(function() { + assert_equals(animation.startTime, null, + 'Animation.startTime is null after paused'); + assert_equals(animation.playState, 'paused', + 'Animation.playState is "paused" after pause() call'); + })).catch(t.step_func(function(reason) { + assert_unreached(reason); + })).then(function() { + t.done(); + }); +}, 'Animation.startTime after paused'); + +done(); + </script> + </body> +</html> diff --git a/dom/animation/test/css-transitions/file_csstransition-events.html b/dom/animation/test/css-transitions/file_csstransition-events.html new file mode 100644 index 0000000000..5011bc1309 --- /dev/null +++ b/dom/animation/test/css-transitions/file_csstransition-events.html @@ -0,0 +1,223 @@ +<!doctype html> +<meta charset=utf-8> +<title>Tests for CSS-Transition events</title> +<link rel="help" href="https://drafts.csswg.org/css-transitions-2/#transition-events"> +<script src="../testcommon.js"></script> +<body> +<script> +'use strict'; + +/** + * Helper class to record the elapsedTime member of each event. + * The EventWatcher class in testharness.js allows us to wait on + * multiple events in a certain order but only records the event + * parameters of the most recent event. + */ +function TransitionEventHandler(target) { + this.target = target; + this.target.ontransitionrun = function(evt) { + this.transitionrun = evt.elapsedTime; + }.bind(this); + this.target.ontransitionstart = function(evt) { + this.transitionstart = evt.elapsedTime; + }.bind(this); + this.target.ontransitionend = function(evt) { + this.transitionend = evt.elapsedTime; + }.bind(this); +} + +TransitionEventHandler.prototype.clear = function() { + this.transitionrun = undefined; + this.transitionstart = undefined; + this.transitionend = undefined; +}; + +function setupTransition(t, transitionStyle) { + var div, watcher, handler, transition; + transitionStyle = transitionStyle || 'transition: margin-left 100s 100s'; + div = addDiv(t, { style: transitionStyle }); + watcher = new EventWatcher(t, div, [ 'transitionrun', + 'transitionstart', + 'transitionend' ]); + handler = new TransitionEventHandler(div); + flushComputedStyle(div); + + div.style.marginLeft = '100px'; + flushComputedStyle(div); + + transition = div.getAnimations()[0]; + + return [transition, watcher, handler]; +} + +// On the next frame (i.e. when events are queued), whether or not the +// transition is still pending depends on the implementation. +promise_test(function(t) { + var [transition, watcher, handler] = setupTransition(t); + return watcher.wait_for('transitionrun').then(function(evt) { + assert_equals(evt.elapsedTime, 0.0); + }); +}, 'Idle -> Pending or Before'); + +promise_test(function(t) { + var [transition, watcher, handler] = setupTransition(t); + // Force the transition to leave the idle phase + transition.startTime = document.timeline.currentTime; + return watcher.wait_for('transitionrun').then(function(evt) { + assert_equals(evt.elapsedTime, 0.0); + }); +}, 'Idle -> Before'); + +promise_test(function(t) { + var [transition, watcher, handler] = setupTransition(t); + // Seek to Active phase. + transition.currentTime = 100 * MS_PER_SEC; + transition.pause(); + return watcher.wait_for([ 'transitionrun', + 'transitionstart' ]).then(function(evt) { + assert_equals(handler.transitionrun, 0.0); + assert_equals(handler.transitionstart, 0.0); + }); +}, 'Idle or Pending -> Active'); + +promise_test(function(t) { + var [transition, watcher, handler] = setupTransition(t); + // Seek to After phase. + transition.finish(); + return watcher.wait_for([ 'transitionrun', + 'transitionstart', + 'transitionend' ]).then(function(evt) { + assert_equals(handler.transitionrun, 0.0); + assert_equals(handler.transitionstart, 0.0); + assert_equals(handler.transitionend, 100.0); + }); +}, 'Idle or Pending -> After'); + +promise_test(function(t) { + var [transition, watcher, handler] = setupTransition(t); + + return Promise.all([ watcher.wait_for('transitionrun'), + transition.ready ]).then(function() { + transition.currentTime = 100 * MS_PER_SEC; + return watcher.wait_for('transitionstart'); + }).then(function() { + assert_equals(handler.transitionstart, 0.0); + }); +}, 'Before -> Active'); + +promise_test(function(t) { + var [transition, watcher, handler] = setupTransition(t); + return Promise.all([ watcher.wait_for('transitionrun'), + transition.ready ]).then(function() { + // Seek to After phase. + transition.currentTime = 200 * MS_PER_SEC; + return watcher.wait_for([ 'transitionstart', 'transitionend' ]); + }).then(function(evt) { + assert_equals(handler.transitionstart, 0.0); + assert_equals(handler.transitionend, 100.0); + }); +}, 'Before -> After'); + +promise_test(function(t) { + var [transition, watcher, handler] = setupTransition(t); + // Seek to Active phase. + transition.currentTime = 100 * MS_PER_SEC; + return watcher.wait_for([ 'transitionrun', + 'transitionstart' ]).then(function(evt) { + // Seek to Before phase. + transition.currentTime = 0; + return watcher.wait_for('transitionend'); + }).then(function(evt) { + assert_equals(evt.elapsedTime, 0.0); + }); +}, 'Active -> Before'); + +promise_test(function(t) { + var [transition, watcher, handler] = setupTransition(t); + // Seek to Active phase. + transition.currentTime = 100 * MS_PER_SEC; + return watcher.wait_for([ 'transitionrun', + 'transitionstart' ]).then(function(evt) { + // Seek to After phase. + transition.currentTime = 200 * MS_PER_SEC; + return watcher.wait_for('transitionend'); + }).then(function(evt) { + assert_equals(evt.elapsedTime, 100.0); + }); +}, 'Active -> After'); + +promise_test(function(t) { + var [transition, watcher, handler] = setupTransition(t); + // Seek to After phase. + transition.finish(); + return watcher.wait_for([ 'transitionrun', + 'transitionstart', + 'transitionend' ]).then(function(evt) { + // Seek to Before phase. + transition.currentTime = 0; + return watcher.wait_for([ 'transitionstart', 'transitionend' ]); + }).then(function(evt) { + assert_equals(handler.transitionstart, 100.0); + assert_equals(handler.transitionend, 0.0); + }); +}, 'After -> Before'); + +promise_test(function(t) { + var [transition, watcher, handler] = setupTransition(t); + // Seek to After phase. + transition.finish(); + return watcher.wait_for([ 'transitionrun', + 'transitionstart', + 'transitionend' ]).then(function(evt) { + // Seek to Active phase. + transition.currentTime = 100 * MS_PER_SEC; + return watcher.wait_for('transitionstart'); + }).then(function(evt) { + assert_equals(evt.elapsedTime, 100.0); + }); +}, 'After -> Active'); + +promise_test(function(t) { + var [transition, watcher, handler] = + setupTransition(t, 'transition: margin-left 100s -50s'); + + return watcher.wait_for([ 'transitionrun', + 'transitionstart' ]).then(function() { + assert_equals(handler.transitionrun, 50.0); + assert_equals(handler.transitionstart, 50.0); + transition.finish(); + return watcher.wait_for('transitionend'); + }).then(function(evt) { + assert_equals(evt.elapsedTime, 100.0); + }); +}, 'Calculating the interval start and end time with negative start delay.'); + +promise_test(function(t) { + var [transition, watcher, handler] = setupTransition(t); + + return watcher.wait_for('transitionrun').then(function(evt) { + // We can't set the end delay via generated effect timing. + // Because CSS-Transition use the AnimationEffectTimingReadOnly. + transition.effect = new KeyframeEffect(handler.target, + { marginleft: [ '0px', '100px' ]}, + { duration: 100 * MS_PER_SEC, + endDelay: -50 * MS_PER_SEC }); + // Seek to Before and play. + transition.cancel(); + transition.play(); + return watcher.wait_for('transitionstart'); + }).then(function() { + assert_equals(handler.transitionstart, 0.0); + + // Seek to After phase. + transition.finish(); + return watcher.wait_for('transitionend'); + }).then(function(evt) { + assert_equals(evt.elapsedTime, 50.0); + }); +}, 'Calculating the interval start and end time with negative end delay.'); + +done(); +</script> +</body> +</html> diff --git a/dom/animation/test/css-transitions/file_csstransition-transitionproperty.html b/dom/animation/test/css-transitions/file_csstransition-transitionproperty.html new file mode 100644 index 0000000000..176cc5a4df --- /dev/null +++ b/dom/animation/test/css-transitions/file_csstransition-transitionproperty.html @@ -0,0 +1,24 @@ +<!doctype html> +<meta charset=utf-8> +<script src="../testcommon.js"></script> +<body> +<script> +'use strict'; + +test(function(t) { + var div = addDiv(t); + + // Add a transition + div.style.left = '0px'; + window.getComputedStyle(div).transitionProperty; + div.style.transition = 'all 100s'; + div.style.left = '100px'; + + assert_equals(div.getAnimations()[0].transitionProperty, 'left', + 'The transitionProperty for the corresponds to the specific ' + + 'property being transitioned'); +}, 'CSSTransition.transitionProperty'); + +done(); +</script> +</body> diff --git a/dom/animation/test/css-transitions/file_document-get-animations.html b/dom/animation/test/css-transitions/file_document-get-animations.html new file mode 100644 index 0000000000..a5d55b76cd --- /dev/null +++ b/dom/animation/test/css-transitions/file_document-get-animations.html @@ -0,0 +1,93 @@ +<!doctype html> +<meta charset=utf-8> +<script src="../testcommon.js"></script> +<body> +<script> +'use strict'; + +test(function(t) { + assert_equals(document.getAnimations().length, 0, + 'getAnimations returns an empty sequence for a document' + + ' with no animations'); +}, 'getAnimations for non-animated content'); + +test(function(t) { + var div = addDiv(t); + + // Add a couple of transitions + div.style.left = '0px'; + div.style.top = '0px'; + getComputedStyle(div).transitionProperty; + + div.style.transition = 'all 100s'; + div.style.left = '100px'; + div.style.top = '100px'; + assert_equals(document.getAnimations().length, 2, + 'getAnimations returns two running CSS Transitions'); + + // Remove both + div.style.transitionProperty = 'none'; + assert_equals(document.getAnimations().length, 0, + 'getAnimations returns no running CSS Transitions'); +}, 'getAnimations for CSS Transitions'); + +test(function(t) { + addStyle(t, { '.init::after': 'content: ""; width: 0px; ' + + 'transition: all 100s;', + '.init::before': 'content: ""; width: 0px; ' + + 'transition: all 10s;', + '.change::after': 'width: 100px;', + '.change::before': 'width: 100px;' }); + // create two divs with these arrangement: + // parent + // ::before, + // ::after + // | + // child + var parent = addDiv(t); + var child = addDiv(t); + parent.appendChild(child); + + parent.style.left = '0px'; + parent.style.transition = 'left 10s'; + parent.classList.add('init'); + child.style.left = '0px'; + child.style.transition = 'left 10s'; + flushComputedStyle(parent); + + parent.style.left = '100px'; + parent.classList.add('change'); + child.style.left = '100px'; + + var anims = document.getAnimations(); + assert_equals(anims.length, 4, + 'CSS transition on both pseudo-elements and elements ' + + 'are returned'); + assert_equals(anims[0].effect.target, parent, + 'The animation targeting the parent element comes first'); + assert_equals(anims[1].effect.target.type, '::before', + 'The animation targeting the ::before element comes second'); + assert_equals(anims[2].effect.target.type, '::after', + 'The animation targeting the ::after element comes third'); + assert_equals(anims[3].effect.target, child, + 'The animation targeting the child element comes last'); +}, 'CSS Transitions targetting (pseudo-)elements should have correct order ' + + 'after sorting'); + +async_test(function(t) { + var div = addDiv(t, { style: 'left: 0px; transition: all 50ms' }); + flushComputedStyle(div); + + div.style.left = '100px'; + var animations = div.getAnimations(); + assert_equals(animations.length, 1, 'Got transition'); + animations[0].finished.then(t.step_func(function() { + assert_equals(document.getAnimations().length, 0, + 'No animations returned'); + t.done(); + })); +}, 'Transitions are not returned after they have finished'); + +done(); +</script> +</body> diff --git a/dom/animation/test/css-transitions/file_effect-target.html b/dom/animation/test/css-transitions/file_effect-target.html new file mode 100644 index 0000000000..0f67b0b9aa --- /dev/null +++ b/dom/animation/test/css-transitions/file_effect-target.html @@ -0,0 +1,66 @@ +<!doctype html> +<meta charset=utf-8> +<script src="../testcommon.js"></script> +<body> +<script> +'use strict'; + +test(function(t) { + var div = addDiv(t); + + div.style.left = '0px'; + window.getComputedStyle(div).transitionProperty; + div.style.transition = 'left 100s'; + div.style.left = '100px'; + + var animation = div.getAnimations()[0]; + assert_equals(animation.effect.target, div, + 'Animation.target is the animatable div'); +}, 'Returned CSS transitions have the correct Animation.target'); + +test(function(t) { + addStyle(t, { '.init::after': 'content: ""; width: 0px; height: 0px; ' + + 'transition: all 10s;', + '.change::after': 'width: 100px; height: 100px;' }); + var div = addDiv(t, { class: 'init' }); + flushComputedStyle(div); + div.classList.add('change'); + + var anims = document.getAnimations(); + assert_equals(anims.length, 2, + 'Got transitions running on ::after pseudo element'); + assert_equals(anims[0].effect.target, anims[1].effect.target, + 'Both transitions return the same target object'); +}, 'effect.target should return the same CSSPseudoElement object each time'); + +test(function(t) { + addStyle(t, { '.init::after': 'content: ""; width: 0px; transition: all 10s;', + '.change::after': 'width: 100px;' }); + var div = addDiv(t, { class: 'init' }); + flushComputedStyle(div); + div.classList.add('change'); + var pseudoTarget = document.getAnimations()[0].effect.target; + var effect = new KeyframeEffectReadOnly(pseudoTarget, + { background: ["blue", "red"] }, + 3000); + var newAnim = new Animation(effect, document.timeline); + newAnim.play(); + + var anims = document.getAnimations(); + assert_equals(anims.length, 2, + 'Got animations running on ::after pseudo element'); + assert_not_equals(anims[0], newAnim, + 'The scriped-generated animation appears last'); + assert_equals(newAnim.effect.target, pseudoTarget, + 'The effect.target of the scripted-generated animation is ' + + 'the same as the one from the argument of ' + + 'KeyframeEffectReadOnly constructor'); + assert_equals(anims[0].effect.target, newAnim.effect.target, + 'Both the transition and the scripted-generated animation ' + + 'return the same target object'); +}, 'effect.target from the script-generated animation should return the same ' + + 'CSSPseudoElement object as that from the CSS generated transition'); + +done(); +</script> +</body> diff --git a/dom/animation/test/css-transitions/file_element-get-animations.html b/dom/animation/test/css-transitions/file_element-get-animations.html new file mode 100644 index 0000000000..0ce145da04 --- /dev/null +++ b/dom/animation/test/css-transitions/file_element-get-animations.html @@ -0,0 +1,147 @@ +<!doctype html> +<meta charset=utf-8> +<script src="../testcommon.js"></script> +<body> +<script> +'use strict'; + +async_test(function(t) { + var div = addDiv(t); + + // FIXME: This test does too many things. It should be split up. + + // Add a couple of transitions + div.style.left = '0px'; + div.style.top = '0px'; + window.getComputedStyle(div).transitionProperty; + + div.style.transition = 'all 100s'; + div.style.left = '100px'; + div.style.top = '100px'; + + var animations = div.getAnimations(); + assert_equals(animations.length, 2, + 'getAnimations() returns one Animation per transitioning property'); + waitForAllAnimations(animations).then(t.step_func(function() { + var startTime = animations[0].startTime; + assert_true(startTime > 0 && startTime <= document.timeline.currentTime, + 'CSS transitions have sensible start times'); + assert_equals(animations[0].startTime, animations[1].startTime, + 'CSS transitions started together have the same start time'); + // Wait a moment then add a third transition + return waitForFrame(); + })).then(t.step_func(function() { + div.style.backgroundColor = 'green'; + animations = div.getAnimations(); + assert_equals(animations.length, 3, + 'getAnimations returns Animations for all running CSS Transitions'); + return waitForAllAnimations(animations); + })).then(t.step_func(function() { + assert_less_than(animations[1].startTime, animations[2].startTime, + 'Animation for additional CSS transition starts after the original' + + ' transitions and appears later in the list'); + t.done(); + })); +}, 'getAnimations for CSS Transitions'); + +test(function(t) { + var div = addDiv(t, { style: 'left: 0px; transition: all 100s' }); + + flushComputedStyle(div); + div.style.left = '100px'; + + assert_class_string(div.getAnimations()[0], 'CSSTransition', + 'Interface of returned animation is CSSTransition'); +}, 'getAnimations returns CSSTransition objects for CSS Transitions'); + +async_test(function(t) { + var div = addDiv(t); + + // Set up event listener + div.addEventListener('transitionend', t.step_func(function() { + assert_equals(div.getAnimations().length, 0, + 'getAnimations does not return finished CSS Transitions'); + t.done(); + })); + + // Add a very short transition + div.style.left = '0px'; + window.getComputedStyle(div).left; + + div.style.transition = 'all 0.01s'; + div.style.left = '100px'; + window.getComputedStyle(div).left; +}, 'getAnimations for CSS Transitions that have finished'); + +test(function(t) { + var div = addDiv(t); + + // Try to transition non-animatable property animation-duration + div.style.animationDuration = '10s'; + window.getComputedStyle(div).animationDuration; + + div.style.transition = 'all 100s'; + div.style.animationDuration = '100s'; + + assert_equals(div.getAnimations().length, 0, + 'getAnimations returns an empty sequence for a transition' + + ' of a non-animatable property'); +}, 'getAnimations for transition on non-animatable property'); + +test(function(t) { + var div = addDiv(t); + + div.style.setProperty('-vendor-unsupported', '0px', ''); + window.getComputedStyle(div).transitionProperty; + div.style.transition = 'all 100s'; + div.style.setProperty('-vendor-unsupported', '100px', ''); + + assert_equals(div.getAnimations().length, 0, + 'getAnimations returns an empty sequence for a transition' + + ' of an unsupported property'); +}, 'getAnimations for transition on unsupported property'); + +test(function(t) { + var div = addDiv(t, { style: 'transform: translate(0px); ' + + 'opacity: 0; ' + + 'border-width: 0px; ' + // Shorthand + 'border-style: solid' }); + getComputedStyle(div).transform; + + div.style.transition = 'all 100s'; + div.style.transform = 'translate(100px)'; + div.style.opacity = '1'; + div.style.borderWidth = '1px'; + + var animations = div.getAnimations(); + assert_equals(animations.length, 6, + 'Generated expected number of transitions'); + assert_equals(animations[0].transitionProperty, 'border-bottom-width'); + assert_equals(animations[1].transitionProperty, 'border-left-width'); + assert_equals(animations[2].transitionProperty, 'border-right-width'); + assert_equals(animations[3].transitionProperty, 'border-top-width'); + assert_equals(animations[4].transitionProperty, 'opacity'); + assert_equals(animations[5].transitionProperty, 'transform'); +}, 'getAnimations sorts simultaneous transitions by name'); + +test(function(t) { + var div = addDiv(t, { style: 'transform: translate(0px); ' + + 'opacity: 0' }); + getComputedStyle(div).transform; + + div.style.transition = 'all 100s'; + div.style.transform = 'translate(100px)'; + assert_equals(div.getAnimations().length, 1, + 'Initially there is only one (transform) transition'); + div.style.opacity = '1'; + assert_equals(div.getAnimations().length, 2, + 'Then a second (opacity) transition is added'); + + var animations = div.getAnimations(); + assert_equals(animations[0].transitionProperty, 'transform'); + assert_equals(animations[1].transitionProperty, 'opacity'); +}, 'getAnimations sorts transitions by when they were generated'); + +done(); +</script> +</body> diff --git a/dom/animation/test/css-transitions/file_keyframeeffect-getkeyframes.html b/dom/animation/test/css-transitions/file_keyframeeffect-getkeyframes.html new file mode 100644 index 0000000000..7bbf76fa7a --- /dev/null +++ b/dom/animation/test/css-transitions/file_keyframeeffect-getkeyframes.html @@ -0,0 +1,95 @@ +<!doctype html> +<meta charset=utf-8> +<script src="../testcommon.js"></script> +<style> +:root { + --var-100px: 100px; +} +</style> +<body> +<script> +'use strict'; + +function getKeyframes(e) { + return e.getAnimations()[0].effect.getKeyframes(); +} + +function assert_frames_equal(a, b, name) { + assert_equals(Object.keys(a).sort().toString(), + Object.keys(b).sort().toString(), + "properties on " + name); + for (var p in a) { + assert_equals(a[p], b[p], "value for '" + p + "' on " + name); + } +} + +test(function(t) { + var div = addDiv(t); + + div.style.left = '0px'; + window.getComputedStyle(div).transitionProperty; + div.style.transition = 'left 100s'; + div.style.left = '100px'; + + var frames = getKeyframes(div); + + assert_equals(frames.length, 2, "number of frames"); + + var expected = [ + { offset: 0, computedOffset: 0, easing: "ease", left: "0px" }, + { offset: 1, computedOffset: 1, easing: "linear", left: "100px" }, + ]; + + for (var i = 0; i < frames.length; i++) { + assert_frames_equal(frames[i], expected[i], "ComputedKeyframe #" + i); + } +}, 'KeyframeEffectReadOnly.getKeyframes() returns expected frames for a simple' + + ' transition'); + +test(function(t) { + var div = addDiv(t); + + div.style.left = '0px'; + window.getComputedStyle(div).transitionProperty; + div.style.transition = 'left 100s steps(2,end)'; + div.style.left = '100px'; + + var frames = getKeyframes(div); + + assert_equals(frames.length, 2, "number of frames"); + + var expected = [ + { offset: 0, computedOffset: 0, easing: "steps(2)", left: "0px" }, + { offset: 1, computedOffset: 1, easing: "linear", left: "100px" }, + ]; + + for (var i = 0; i < frames.length; i++) { + assert_frames_equal(frames[i], expected[i], "ComputedKeyframe #" + i); + } +}, 'KeyframeEffectReadOnly.getKeyframes() returns expected frames for a simple' + + ' transition with a non-default easing function'); + +test(function(t) { + var div = addDiv(t); + div.style.left = '0px'; + window.getComputedStyle(div).transitionProperty; + div.style.transition = 'left 100s'; + div.style.left = 'var(--var-100px)'; + + var frames = getKeyframes(div); + + // CSS transition endpoints are based on the computed value so we + // shouldn't see the variable reference + var expected = [ + { offset: 0, computedOffset: 0, easing: 'ease', left: '0px' }, + { offset: 1, computedOffset: 1, easing: 'linear', left: '100px' }, + ]; + for (var i = 0; i < frames.length; i++) { + assert_frames_equal(frames[i], expected[i], "ComputedKeyframe #" + i); + } +}, 'KeyframeEffectReadOnly.getKeyframes() returns expected frames for a' + + ' transition with a CSS variable endpoint'); + +done(); +</script> +</body> diff --git a/dom/animation/test/css-transitions/file_pseudoElement-get-animations.html b/dom/animation/test/css-transitions/file_pseudoElement-get-animations.html new file mode 100644 index 0000000000..5683a14a14 --- /dev/null +++ b/dom/animation/test/css-transitions/file_pseudoElement-get-animations.html @@ -0,0 +1,45 @@ +<!doctype html> +<meta charset=utf-8> +<script src="../testcommon.js"></script> +<style> +.init::before { + content: ''; + height: 0px; + width: 0px; + opacity: 0; + transition: all 100s; +} +.change::before { + height: 100px; + width: 100px; + opacity: 1; +} +</style> +<body> +<script> +'use strict'; + +test(function(t) { + var div = addDiv(t, { class: 'init' }); + flushComputedStyle(div); + div.classList.add('change'); + + // Sanity checks + assert_equals(document.getAnimations().length, 3, + 'Got expected number of animations on document'); + var pseudoTarget = document.getAnimations()[0].effect.target; + assert_class_string(pseudoTarget, 'CSSPseudoElement', + 'Got pseudo-element target'); + + // Check animations returned from the pseudo element are in correct order + var anims = pseudoTarget.getAnimations(); + assert_equals(anims.length, 3, + 'Got expected number of animations on pseudo-element'); + assert_equals(anims[0].transitionProperty, 'height'); + assert_equals(anims[1].transitionProperty, 'opacity'); + assert_equals(anims[2].transitionProperty, 'width'); +}, 'getAnimations sorts simultaneous transitions by name'); + +done(); +</script> +</body> diff --git a/dom/animation/test/css-transitions/file_setting-effect.html b/dom/animation/test/css-transitions/file_setting-effect.html new file mode 100644 index 0000000000..c61877194e --- /dev/null +++ b/dom/animation/test/css-transitions/file_setting-effect.html @@ -0,0 +1,91 @@ +<!doctype html> +<meta charset=utf-8> +<script src='../testcommon.js'></script> +<body> +<script> +'use strict'; + +promise_test(function(t) { + var div = addDiv(t); + div.style.left = '0px'; + + div.style.transition = 'left 100s'; + flushComputedStyle(div); + div.style.left = '100px'; + + var transition = div.getAnimations()[0]; + return transition.ready.then(function() { + transition.currentTime = 50 * MS_PER_SEC; + transition.effect = null; + assert_equals(transition.transitionProperty, 'left'); + assert_equals(transition.playState, 'finished'); + assert_equals(window.getComputedStyle(div).left, '100px'); + }); +}, 'Test for removing a transition effect'); + +promise_test(function(t) { + var div = addDiv(t); + div.style.left = '0px'; + + div.style.transition = 'left 100s'; + flushComputedStyle(div); + div.style.left = '100px'; + + var transition = div.getAnimations()[0]; + return transition.ready.then(function() { + transition.currentTime = 50 * MS_PER_SEC; + transition.effect = new KeyframeEffect(div, + { marginLeft: [ '0px' , '100px'] }, + 100 * MS_PER_SEC); + assert_equals(transition.transitionProperty, 'left'); + assert_equals(transition.playState, 'running'); + assert_equals(window.getComputedStyle(div).left, '100px'); + assert_equals(window.getComputedStyle(div).marginLeft, '50px'); + }); +}, 'Test for replacing the transition effect by a new keyframe effect'); + +promise_test(function(t) { + var div = addDiv(t); + div.style.left = '0px'; + div.style.width = '0px'; + + div.style.transition = 'left 100s'; + flushComputedStyle(div); + div.style.left = '100px'; + + var transition = div.getAnimations()[0]; + return transition.ready.then(function() { + transition.currentTime = 50 * MS_PER_SEC; + transition.effect = new KeyframeEffect(div, + { marginLeft: [ '0px' , '100px'] }, + 20 * MS_PER_SEC); + assert_equals(transition.playState, 'finished'); + }); +}, 'Test for setting a new keyframe effect with a shorter duration'); + +promise_test(function(t) { + var div = addDiv(t); + div.style.left = '0px'; + div.style.width = '0px'; + + div.style.transition = 'left 100s'; + flushComputedStyle(div); + div.style.left = '100px'; + + var transition = div.getAnimations()[0]; + assert_equals(transition.playState, 'pending'); + + transition.effect = new KeyframeEffect(div, + { marginLeft: [ '0px' , '100px'] }, + 100 * MS_PER_SEC); + assert_equals(transition.transitionProperty, 'left'); + assert_equals(transition.playState, 'pending'); + + return transition.ready.then(function() { + assert_equals(transition.playState, 'running'); + }); +}, 'Test for setting a new keyframe effect to a pending transition'); + +done(); +</script> +</body> diff --git a/dom/animation/test/css-transitions/test_animation-cancel.html b/dom/animation/test/css-transitions/test_animation-cancel.html new file mode 100644 index 0000000000..949e0843e4 --- /dev/null +++ b/dom/animation/test/css-transitions/test_animation-cancel.html @@ -0,0 +1,14 @@ +<!doctype html> +<meta charset=utf-8> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<div id="log"></div> +<script> +'use strict'; +setup({explicit_done: true}); +SpecialPowers.pushPrefEnv( + { "set": [["dom.animations-api.core.enabled", true]]}, + function() { + window.open("file_animation-cancel.html"); + }); +</script> diff --git a/dom/animation/test/css-transitions/test_animation-computed-timing.html b/dom/animation/test/css-transitions/test_animation-computed-timing.html new file mode 100644 index 0000000000..c1b40aaf36 --- /dev/null +++ b/dom/animation/test/css-transitions/test_animation-computed-timing.html @@ -0,0 +1,16 @@ +<!doctype html> +<html> +<meta charset=utf-8> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<div id="log"></div> +<script> +'use strict'; +setup({explicit_done: true}); +SpecialPowers.pushPrefEnv( + { "set": [["dom.animations-api.core.enabled", true]]}, + function() { + window.open("file_animation-computed-timing.html"); + }); +</script> +</html> diff --git a/dom/animation/test/css-transitions/test_animation-currenttime.html b/dom/animation/test/css-transitions/test_animation-currenttime.html new file mode 100644 index 0000000000..30b0ed030c --- /dev/null +++ b/dom/animation/test/css-transitions/test_animation-currenttime.html @@ -0,0 +1,14 @@ +<!doctype html> +<meta charset=utf-8> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<div id="log"></div> +<script> +'use strict'; +setup({explicit_done: true}); +SpecialPowers.pushPrefEnv( + { "set": [["dom.animations-api.core.enabled", true]]}, + function() { + window.open("file_animation-currenttime.html"); + }); +</script> diff --git a/dom/animation/test/css-transitions/test_animation-finished.html b/dom/animation/test/css-transitions/test_animation-finished.html new file mode 100644 index 0000000000..f2ed7f80b4 --- /dev/null +++ b/dom/animation/test/css-transitions/test_animation-finished.html @@ -0,0 +1,14 @@ +<!doctype html> +<meta charset=utf-8> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<div id="log"></div> +<script> +'use strict'; +setup({explicit_done: true}); +SpecialPowers.pushPrefEnv( + { "set": [["dom.animations-api.core.enabled", true]]}, + function() { + window.open("file_animation-finished.html"); + }); +</script> diff --git a/dom/animation/test/css-transitions/test_animation-pausing.html b/dom/animation/test/css-transitions/test_animation-pausing.html new file mode 100644 index 0000000000..67484a2a53 --- /dev/null +++ b/dom/animation/test/css-transitions/test_animation-pausing.html @@ -0,0 +1,14 @@ +<!doctype html> +<meta charset=utf-8> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<div id="log"></div> +<script> +'use strict'; +setup({explicit_done: true}); +SpecialPowers.pushPrefEnv( + { "set": [["dom.animations-api.core.enabled", true]]}, + function() { + window.open("file_animation-pausing.html"); + }); +</script> diff --git a/dom/animation/test/css-transitions/test_animation-ready.html b/dom/animation/test/css-transitions/test_animation-ready.html new file mode 100644 index 0000000000..a928ded641 --- /dev/null +++ b/dom/animation/test/css-transitions/test_animation-ready.html @@ -0,0 +1,14 @@ +<!doctype html> +<meta charset=utf-8> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<div id="log"></div> +<script> +'use strict'; +setup({explicit_done: true}); +SpecialPowers.pushPrefEnv( + { "set": [["dom.animations-api.core.enabled", true]]}, + function() { + window.open("file_animation-ready.html"); + }); +</script> diff --git a/dom/animation/test/css-transitions/test_animation-starttime.html b/dom/animation/test/css-transitions/test_animation-starttime.html new file mode 100644 index 0000000000..8a8c85f2dd --- /dev/null +++ b/dom/animation/test/css-transitions/test_animation-starttime.html @@ -0,0 +1,14 @@ +<!doctype html> +<meta charset=utf-8> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<div id="log"></div> +<script> +'use strict'; +setup({explicit_done: true}); +SpecialPowers.pushPrefEnv( + { "set": [["dom.animations-api.core.enabled", true]]}, + function() { + window.open("file_animation-starttime.html"); + }); +</script> diff --git a/dom/animation/test/css-transitions/test_csstransition-events.html b/dom/animation/test/css-transitions/test_csstransition-events.html new file mode 100644 index 0000000000..92559ad679 --- /dev/null +++ b/dom/animation/test/css-transitions/test_csstransition-events.html @@ -0,0 +1,14 @@ +<!doctype html> +<meta charset=utf-8> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<div id="log"></div> +<script> +'use strict'; +setup({explicit_done: true}); +SpecialPowers.pushPrefEnv( + { "set": [["dom.animations-api.core.enabled", true]]}, + function() { + window.open("file_csstransition-events.html"); + }); +</script> diff --git a/dom/animation/test/css-transitions/test_csstransition-transitionproperty.html b/dom/animation/test/css-transitions/test_csstransition-transitionproperty.html new file mode 100644 index 0000000000..0aa1912d96 --- /dev/null +++ b/dom/animation/test/css-transitions/test_csstransition-transitionproperty.html @@ -0,0 +1,14 @@ +<!doctype html> +<meta charset=utf-8> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<div id="log"></div> +<script> +'use strict'; +setup({explicit_done: true}); +SpecialPowers.pushPrefEnv( + { "set": [["dom.animations-api.core.enabled", true]]}, + function() { + window.open("file_csstransition-transitionproperty.html"); + }); +</script> diff --git a/dom/animation/test/css-transitions/test_document-get-animations.html b/dom/animation/test/css-transitions/test_document-get-animations.html new file mode 100644 index 0000000000..dc964e62c8 --- /dev/null +++ b/dom/animation/test/css-transitions/test_document-get-animations.html @@ -0,0 +1,15 @@ +<!doctype html> +<meta charset=utf-8> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<div id="log"></div> +<script> +'use strict'; +setup({explicit_done: true}); +SpecialPowers.pushPrefEnv( + { "set": [["dom.animations-api.core.enabled", true]]}, + function() { + window.open("file_document-get-animations.html"); + }); +</script> +</html> diff --git a/dom/animation/test/css-transitions/test_effect-target.html b/dom/animation/test/css-transitions/test_effect-target.html new file mode 100644 index 0000000000..f3ae72229c --- /dev/null +++ b/dom/animation/test/css-transitions/test_effect-target.html @@ -0,0 +1,14 @@ +<!doctype html> +<meta charset=utf-8> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<div id="log"></div> +<script> +'use strict'; +setup({explicit_done: true}); +SpecialPowers.pushPrefEnv( + { "set": [["dom.animations-api.core.enabled", true]]}, + function() { + window.open("file_effect-target.html"); + }); +</script> diff --git a/dom/animation/test/css-transitions/test_element-get-animations.html b/dom/animation/test/css-transitions/test_element-get-animations.html new file mode 100644 index 0000000000..87abdfa730 --- /dev/null +++ b/dom/animation/test/css-transitions/test_element-get-animations.html @@ -0,0 +1,14 @@ +<!doctype html> +<meta charset=utf-8> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<div id="log"></div> +<script> +'use strict'; +setup({explicit_done: true}); +SpecialPowers.pushPrefEnv( + { "set": [["dom.animations-api.core.enabled", true]]}, + function() { + window.open("file_element-get-animations.html"); + }); +</script> diff --git a/dom/animation/test/css-transitions/test_keyframeeffect-getkeyframes.html b/dom/animation/test/css-transitions/test_keyframeeffect-getkeyframes.html new file mode 100644 index 0000000000..dcc54255d5 --- /dev/null +++ b/dom/animation/test/css-transitions/test_keyframeeffect-getkeyframes.html @@ -0,0 +1,14 @@ +<!doctype html> +<meta charset=utf-8> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<div id="log"></div> +<script> +'use strict'; +setup({explicit_done: true}); +SpecialPowers.pushPrefEnv( + { "set": [["dom.animations-api.core.enabled", true]]}, + function() { + window.open("file_keyframeeffect-getkeyframes.html"); + }); +</script> diff --git a/dom/animation/test/css-transitions/test_pseudoElement-get-animations.html b/dom/animation/test/css-transitions/test_pseudoElement-get-animations.html new file mode 100644 index 0000000000..1e0dc5c825 --- /dev/null +++ b/dom/animation/test/css-transitions/test_pseudoElement-get-animations.html @@ -0,0 +1,14 @@ +<!doctype html> +<meta charset=utf-8> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<div id="log"></div> +<script> +'use strict'; +setup({explicit_done: true}); +SpecialPowers.pushPrefEnv( + { "set": [["dom.animations-api.core.enabled", true]]}, + function() { + window.open("file_pseudoElement-get-animations.html"); + }); +</script> diff --git a/dom/animation/test/css-transitions/test_setting-effect.html b/dom/animation/test/css-transitions/test_setting-effect.html new file mode 100644 index 0000000000..a9654ec556 --- /dev/null +++ b/dom/animation/test/css-transitions/test_setting-effect.html @@ -0,0 +1,14 @@ +<!doctype html> +<meta charset=utf-8> +<script src='/resources/testharness.js'></script> +<script src='/resources/testharnessreport.js'></script> +<div id='log'></div> +<script> +'use strict'; +setup({explicit_done: true}); +SpecialPowers.pushPrefEnv( + { 'set': [['dom.animations-api.core.enabled', true]]}, + function() { + window.open('file_setting-effect.html'); + }); +</script> diff --git a/dom/animation/test/document-timeline/file_document-timeline.html b/dom/animation/test/document-timeline/file_document-timeline.html new file mode 100644 index 0000000000..7ec48e2915 --- /dev/null +++ b/dom/animation/test/document-timeline/file_document-timeline.html @@ -0,0 +1,135 @@ +<!doctype html> +<meta charset=utf-8> +<title>Web Animations API: DocumentTimeline tests</title> +<script src="../testcommon.js"></script> +<iframe src="data:text/html;charset=utf-8," width="10" height="10" id="iframe"></iframe> +<iframe src="data:text/html;charset=utf-8,%3Chtml%20style%3D%22display%3Anone%22%3E%3C%2Fhtml%3E" width="10" height="10" id="hidden-iframe"></iframe> +<script> +'use strict'; + +test(function() { + assert_equals(document.timeline, document.timeline, + 'document.timeline returns the same object every time'); + var iframe = document.getElementById('iframe'); + assert_not_equals(document.timeline, iframe.contentDocument.timeline, + 'document.timeline returns a different object for each document'); + assert_not_equals(iframe.contentDocument.timeline, null, + 'document.timeline on an iframe is not null'); +}, +'document.timeline identity tests', +{ + help: 'http://dev.w3.org/fxtf/web-animations/#the-document-timeline', + assert: [ 'Each document has a timeline called the document timeline' ], + author: 'Brian Birtles' +}); + +async_test(function(t) { + assert_true(document.timeline.currentTime > 0, + 'document.timeline.currentTime is positive'); + // document.timeline.currentTime should be set even before document + // load fires. We expect this code to be run before document load and hence + // the above assertion is sufficient. + // If the following assertion fails, this test needs to be redesigned. + assert_true(document.readyState !== 'complete', + 'Test is running prior to document load'); + + // Test that the document timeline's current time is measured from + // navigationStart. + // + // We can't just compare document.timeline.currentTime to + // window.performance.now() because currentTime is only updated on a sample + // so we use requestAnimationFrame instead. + window.requestAnimationFrame(t.step_func(function(rafTime) { + assert_equals(document.timeline.currentTime, rafTime, + 'document.timeline.currentTime matches' + + ' requestAnimationFrame time'); + t.done(); + })); +}, +'document.timeline.currentTime value tests', +{ + help: [ + 'http://dev.w3.org/fxtf/web-animations/#the-global-clock', + 'http://dev.w3.org/fxtf/web-animations/#the-document-timeline' + ], + assert: [ + 'The global clock is a source of monotonically increasing time values', + 'The time values of the document timeline are calculated as a fixed' + + ' offset from the global clock', + 'the zero time corresponds to the navigationStart moment', + 'the time value of each document timeline must be equal to the time ' + + 'passed to animation frame request callbacks for that browsing context' + ], + author: 'Brian Birtles' +}); + +async_test(function(t) { + var valueAtStart = document.timeline.currentTime; + var timeAtStart = window.performance.now(); + while (window.performance.now() - timeAtStart < 100) { + // Wait 100ms + } + assert_equals(document.timeline.currentTime, valueAtStart, + 'document.timeline.currentTime does not change within a script block'); + window.requestAnimationFrame(t.step_func(function() { + assert_true(document.timeline.currentTime > valueAtStart, + 'document.timeline.currentTime increases between script blocks'); + t.done(); + })); +}, +'document.timeline.currentTime liveness tests', +{ + help: 'http://dev.w3.org/fxtf/web-animations/#script-execution-and-live-updates-to-the-model', + assert: [ 'The value returned by the currentTime attribute of a' + + ' document timeline will not change within a script block' ], + author: 'Brian Birtles' +}); + +test(function() { + var hiddenIFrame = document.getElementById('hidden-iframe'); + assert_equals(typeof hiddenIFrame.contentDocument.timeline.currentTime, + 'number', + 'currentTime of an initially hidden subframe\'s timeline is a number'); + assert_true(hiddenIFrame.contentDocument.timeline.currentTime >= 0, + 'currentTime of an initially hidden subframe\'s timeline is >= 0'); +}, 'document.timeline.currentTime hidden subframe test'); + +async_test(function(t) { + var hiddenIFrame = document.getElementById('hidden-iframe'); + + // Don't run the test until after the iframe has completed loading or else the + // contentDocument may change. + var testToRunOnLoad = t.step_func(function() { + // Remove display:none + hiddenIFrame.style.display = 'block'; + window.getComputedStyle(hiddenIFrame).display; + + window.requestAnimationFrame(t.step_func(function() { + assert_true(hiddenIFrame.contentDocument.timeline.currentTime > 0, + 'document.timeline.currentTime is positive after removing' + + ' display:none'); + var previousValue = hiddenIFrame.contentDocument.timeline.currentTime; + + // Re-introduce display:none + hiddenIFrame.style.display = 'none'; + window.getComputedStyle(hiddenIFrame).display; + + window.requestAnimationFrame(t.step_func(function() { + assert_true( + hiddenIFrame.contentDocument.timeline.currentTime >= previousValue, + 'document.timeline.currentTime does not go backwards after' + + ' re-setting display:none'); + t.done(); + })); + })); + }); + + if (hiddenIFrame.contentDocument.readyState === 'complete') { + testToRunOnLoad(); + } else { + hiddenIFrame.addEventListener("load", testToRunOnLoad); + } +}, 'document.timeline.currentTime hidden subframe dynamic test'); + +done(); +</script> diff --git a/dom/animation/test/document-timeline/test_document-timeline.html b/dom/animation/test/document-timeline/test_document-timeline.html new file mode 100644 index 0000000000..812d88ef2a --- /dev/null +++ b/dom/animation/test/document-timeline/test_document-timeline.html @@ -0,0 +1,14 @@ +<!doctype html> +<meta charset=utf-8> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<div id="log"></div> +<script> +'use strict'; +setup({explicit_done: true}); +SpecialPowers.pushPrefEnv( + { "set": [["dom.animations-api.core.enabled", true]]}, + function() { + window.open("file_document-timeline.html"); + }); +</script> diff --git a/dom/animation/test/document-timeline/test_request_animation_frame.html b/dom/animation/test/document-timeline/test_request_animation_frame.html new file mode 100644 index 0000000000..302a385b76 --- /dev/null +++ b/dom/animation/test/document-timeline/test_request_animation_frame.html @@ -0,0 +1,27 @@ +<!DOCTYPE html> +<meta charset=utf-8> +<title>Test RequestAnimationFrame Timestamps are monotonically increasing</title> +<script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> +<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +<script> + var lastRequestAnimationFrameTimestamp = 0; + var requestAnimationFrameCount = 20; + var currentCount = 0; + + // Test that all timestamps are always increasing + // and do not ever go backwards + function rafCallback(aTimestamp) { + SimpleTest.ok(aTimestamp > lastRequestAnimationFrameTimestamp, + "New RequestAnimationFrame timestamp should be later than the previous RequestAnimationFrame timestamp"); + lastRequestAnimationFrameTimestamp = aTimestamp; + if (currentCount == requestAnimationFrameCount) { + SimpleTest.finish(); + } else { + currentCount++; + window.requestAnimationFrame(rafCallback); + } + } + + window.requestAnimationFrame(rafCallback); + SimpleTest.waitForExplicitFinish(); +</script> diff --git a/dom/animation/test/mochitest.ini b/dom/animation/test/mochitest.ini new file mode 100644 index 0000000000..feb424518d --- /dev/null +++ b/dom/animation/test/mochitest.ini @@ -0,0 +1,111 @@ +[DEFAULT] +# Support files for chrome tests that we want to load over HTTP need +# to go in here, not chrome.ini. +support-files = + chrome/file_animate_xrays.html + css-animations/file_animation-cancel.html + css-animations/file_animation-computed-timing.html + css-animations/file_animation-currenttime.html + css-animations/file_animation-finish.html + css-animations/file_animation-finished.html + css-animations/file_animation-id.html + css-animations/file_animation-pausing.html + css-animations/file_animation-playstate.html + css-animations/file_animation-ready.html + css-animations/file_animation-reverse.html + css-animations/file_animation-starttime.html + css-animations/file_animations-dynamic-changes.html + css-animations/file_cssanimation-animationname.html + css-animations/file_document-get-animations.html + css-animations/file_effect-target.html + css-animations/file_element-get-animations.html + css-animations/file_keyframeeffect-getkeyframes.html + css-animations/file_pseudoElement-get-animations.html + css-transitions/file_animation-cancel.html + css-transitions/file_animation-computed-timing.html + css-transitions/file_animation-currenttime.html + css-transitions/file_animation-finished.html + css-transitions/file_animation-pausing.html + css-transitions/file_animation-ready.html + css-transitions/file_animation-starttime.html + css-transitions/file_csstransition-transitionproperty.html + css-transitions/file_document-get-animations.html + css-transitions/file_effect-target.html + css-transitions/file_element-get-animations.html + css-transitions/file_keyframeeffect-getkeyframes.html + css-transitions/file_pseudoElement-get-animations.html + css-transitions/file_setting-effect.html + document-timeline/file_document-timeline.html + mozilla/file_cubic_bezier_limits.html + mozilla/file_deferred_start.html + mozilla/file_disabled_properties.html + mozilla/file_disable_animations_api_core.html + mozilla/file_discrete-animations.html + mozilla/file_document-timeline-origin-time-range.html + mozilla/file_hide_and_show.html + mozilla/file_partial_keyframes.html + mozilla/file_spacing_property_order.html + mozilla/file_spacing_transform.html + mozilla/file_transform_limits.html + mozilla/file_transition_finish_on_compositor.html + mozilla/file_underlying-discrete-value.html + mozilla/file_set-easing.html + style/file_animation-seeking-with-current-time.html + style/file_animation-seeking-with-start-time.html + style/file_animation-setting-effect.html + style/file_animation-setting-spacing.html + testcommon.js + +[css-animations/test_animations-dynamic-changes.html] +[css-animations/test_animation-cancel.html] +[css-animations/test_animation-computed-timing.html] +[css-animations/test_animation-currenttime.html] +[css-animations/test_animation-finish.html] +[css-animations/test_animation-finished.html] +[css-animations/test_animation-id.html] +[css-animations/test_animation-pausing.html] +[css-animations/test_animation-playstate.html] +[css-animations/test_animation-ready.html] +[css-animations/test_animation-reverse.html] +[css-animations/test_animation-starttime.html] +[css-animations/test_cssanimation-animationname.html] +[css-animations/test_document-get-animations.html] +[css-animations/test_effect-target.html] +[css-animations/test_element-get-animations.html] +[css-animations/test_keyframeeffect-getkeyframes.html] +[css-animations/test_pseudoElement-get-animations.html] +[css-transitions/test_animation-cancel.html] +[css-transitions/test_animation-computed-timing.html] +[css-transitions/test_animation-currenttime.html] +[css-transitions/test_animation-finished.html] +[css-transitions/test_animation-pausing.html] +[css-transitions/test_animation-ready.html] +[css-transitions/test_animation-starttime.html] +[css-transitions/test_csstransition-transitionproperty.html] +[css-transitions/test_document-get-animations.html] +[css-transitions/test_effect-target.html] +[css-transitions/test_element-get-animations.html] +[css-transitions/test_keyframeeffect-getkeyframes.html] +[css-transitions/test_pseudoElement-get-animations.html] +[css-transitions/test_setting-effect.html] +[document-timeline/test_document-timeline.html] +[document-timeline/test_request_animation_frame.html] +[mozilla/test_cubic_bezier_limits.html] +[mozilla/test_deferred_start.html] +[mozilla/test_disable_animations_api_core.html] +[mozilla/test_disabled_properties.html] +[mozilla/test_discrete-animations.html] +[mozilla/test_document-timeline-origin-time-range.html] +[mozilla/test_hide_and_show.html] +[mozilla/test_partial_keyframes.html] +[mozilla/test_set-easing.html] +[mozilla/test_spacing_property_order.html] +[mozilla/test_spacing_transform.html] +[mozilla/test_transform_limits.html] +[mozilla/test_transition_finish_on_compositor.html] +skip-if = toolkit == 'android' +[mozilla/test_underlying-discrete-value.html] +[style/test_animation-seeking-with-current-time.html] +[style/test_animation-seeking-with-start-time.html] +[style/test_animation-setting-effect.html] +[style/test_animation-setting-spacing.html] diff --git a/dom/animation/test/mozilla/file_cubic_bezier_limits.html b/dom/animation/test/mozilla/file_cubic_bezier_limits.html new file mode 100644 index 0000000000..a0378f3953 --- /dev/null +++ b/dom/animation/test/mozilla/file_cubic_bezier_limits.html @@ -0,0 +1,167 @@ +<!doctype html> +<meta charset=utf-8> +<script src="../testcommon.js"></script> +<body> +<style> +@keyframes anim { + to { margin-left: 100px; } +} + +.transition-div { + margin-left: 100px; +} +</style> +<script> +'use strict'; + +// We clamp +infinity or -inifinity value in floating point to +// maximum floating point value or -maxinum floating point value. +const max_float = 3.40282e+38; + +test(function(t) { + var div = addDiv(t); + var anim = div.animate({ }, 100 * MS_PER_SEC); + + anim.effect.timing.easing = 'cubic-bezier(0, 1e+39, 0, 0)'; + assert_equals(anim.effect.timing.easing, + 'cubic-bezier(0, ' + max_float + ', 0, 0)', + 'y1 control point for effect easing is out of upper boundary'); + + anim.effect.timing.easing = 'cubic-bezier(0, 0, 0, 1e+39)'; + assert_equals(anim.effect.timing.easing, + 'cubic-bezier(0, 0, 0, ' + max_float + ')', + 'y2 control point for effect easing is out of upper boundary'); + + anim.effect.timing.easing = 'cubic-bezier(0, -1e+39, 0, 0)'; + assert_equals(anim.effect.timing.easing, + 'cubic-bezier(0, ' + -max_float + ', 0, 0)', + 'y1 control point for effect easing is out of lower boundary'); + + anim.effect.timing.easing = 'cubic-bezier(0, 0, 0, -1e+39)'; + assert_equals(anim.effect.timing.easing, + 'cubic-bezier(0, 0, 0, ' + -max_float + ')', + 'y2 control point for effect easing is out of lower boundary'); + +}, 'Clamp y1 and y2 control point out of boundaries for effect easing' ); + +test(function(t) { + var div = addDiv(t); + var anim = div.animate({ }, 100 * MS_PER_SEC); + + anim.effect.setKeyframes([ { easing: 'cubic-bezier(0, 1e+39, 0, 0)' }]); + assert_equals(anim.effect.getKeyframes()[0].easing, + 'cubic-bezier(0, ' + max_float + ', 0, 0)', + 'y1 control point for keyframe easing is out of upper boundary'); + + anim.effect.setKeyframes([ { easing: 'cubic-bezier(0, 0, 0, 1e+39)' }]); + assert_equals(anim.effect.getKeyframes()[0].easing, + 'cubic-bezier(0, 0, 0, ' + max_float + ')', + 'y2 control point for keyframe easing is out of upper boundary'); + + anim.effect.setKeyframes([ { easing: 'cubic-bezier(0, -1e+39, 0, 0)' }]); + assert_equals(anim.effect.getKeyframes()[0].easing, + 'cubic-bezier(0, ' + -max_float + ', 0, 0)', + 'y1 control point for keyframe easing is out of lower boundary'); + + anim.effect.setKeyframes([ { easing: 'cubic-bezier(0, 0, 0, -1e+39)' }]); + assert_equals(anim.effect.getKeyframes()[0].easing, + 'cubic-bezier(0, 0, 0, ' + -max_float + ')', + 'y2 control point for keyframe easing is out of lower boundary'); + +}, 'Clamp y1 and y2 control point out of boundaries for keyframe easing' ); + +test(function(t) { + var div = addDiv(t); + + div.style.animation = 'anim 100s cubic-bezier(0, 1e+39, 0, 0)'; + + assert_equals(div.getAnimations()[0].effect.getKeyframes()[0].easing, + 'cubic-bezier(0, ' + max_float + ', 0, 0)', + 'y1 control point for CSS animation is out of upper boundary'); + + div.style.animation = 'anim 100s cubic-bezier(0, 0, 0, 1e+39)'; + assert_equals(div.getAnimations()[0].effect.getKeyframes()[0].easing, + 'cubic-bezier(0, 0, 0, ' + max_float + ')', + 'y2 control point for CSS animation is out of upper boundary'); + + div.style.animation = 'anim 100s cubic-bezier(0, -1e+39, 0, 0)'; + assert_equals(div.getAnimations()[0].effect.getKeyframes()[0].easing, + 'cubic-bezier(0, ' + -max_float + ', 0, 0)', + 'y1 control point for CSS animation is out of lower boundary'); + + div.style.animation = 'anim 100s cubic-bezier(0, 0, 0, -1e+39)'; + assert_equals(div.getAnimations()[0].effect.getKeyframes()[0].easing, + 'cubic-bezier(0, 0, 0, ' + -max_float + ')', + 'y2 control point for CSS animation is out of lower boundary'); + +}, 'Clamp y1 and y2 control point out of boundaries for CSS animation' ); + +test(function(t) { + var div = addDiv(t, {'class': 'transition-div'}); + + div.style.transition = 'margin-left 100s cubic-bezier(0, 1e+39, 0, 0)'; + flushComputedStyle(div); + div.style.marginLeft = '0px'; + assert_equals(div.getAnimations()[0].effect.getKeyframes()[0].easing, + 'cubic-bezier(0, ' + max_float + ', 0, 0)', + 'y1 control point for CSS transition on upper boundary'); + div.style.transition = ''; + div.style.marginLeft = ''; + + div.style.transition = 'margin-left 100s cubic-bezier(0, 0, 0, 1e+39)'; + flushComputedStyle(div); + div.style.marginLeft = '0px'; + assert_equals(div.getAnimations()[0].effect.getKeyframes()[0].easing, + 'cubic-bezier(0, 0, 0, ' + max_float + ')', + 'y2 control point for CSS transition on upper boundary'); + div.style.transition = ''; + div.style.marginLeft = ''; + + div.style.transition = 'margin-left 100s cubic-bezier(0, -1e+39, 0, 0)'; + flushComputedStyle(div); + div.style.marginLeft = '0px'; + assert_equals(div.getAnimations()[0].effect.getKeyframes()[0].easing, + 'cubic-bezier(0, ' + -max_float + ', 0, 0)', + 'y1 control point for CSS transition on lower boundary'); + div.style.transition = ''; + div.style.marginLeft = ''; + + div.style.transition = 'margin-left 100s cubic-bezier(0, 0, 0, -1e+39)'; + flushComputedStyle(div); + div.style.marginLeft = '0px'; + assert_equals(div.getAnimations()[0].effect.getKeyframes()[0].easing, + 'cubic-bezier(0, 0, 0, ' + -max_float + ')', + 'y2 control point for CSS transition on lower boundary'); + +}, 'Clamp y1 and y2 control point out of boundaries for CSS transition' ); + +test(function(t) { + var div = addDiv(t); + var anim = div.animate({ }, { duration: 100 * MS_PER_SEC, fill: 'forwards' }); + + anim.pause(); + // The positive steepest function on both edges. + anim.effect.timing.easing = 'cubic-bezier(0, 1e+39, 0, 1e+39)'; + assert_equals(anim.effect.getComputedTiming().progress, 0.0, + 'progress on lower edge for the highest value of y1 and y2 control points'); + + anim.finish(); + assert_equals(anim.effect.getComputedTiming().progress, 1.0, + 'progress on upper edge for the highest value of y1 and y2 control points'); + + // The negative steepest function on both edges. + anim.effect.timing.easing = 'cubic-bezier(0, -1e+39, 0, -1e+39)'; + anim.currentTime = 0; + assert_equals(anim.effect.getComputedTiming().progress, 0.0, + 'progress on lower edge for the lowest value of y1 and y2 control points'); + + anim.finish(); + assert_equals(anim.effect.getComputedTiming().progress, 1.0, + 'progress on lower edge for the lowest value of y1 and y2 control points'); + +}, 'Calculated values on both edges'); + +done(); + +</script> +</body> diff --git a/dom/animation/test/mozilla/file_deferred_start.html b/dom/animation/test/mozilla/file_deferred_start.html new file mode 100644 index 0000000000..3be3f56aae --- /dev/null +++ b/dom/animation/test/mozilla/file_deferred_start.html @@ -0,0 +1,121 @@ +<!doctype html> +<meta charset=utf-8> +<script src="../testcommon.js"></script> +<script src="/tests/SimpleTest/paint_listener.js"></script> +<style> +@keyframes empty { } +@keyframes animTransform { + from { transform: translate(0px); } + to { transform: translate(100px); } +} +.target { + /* Element needs geometry to be eligible for layerization */ + width: 100px; + height: 100px; + background-color: white; +} +</style> +<body> +<script> +'use strict'; + +function waitForDocLoad() { + return new Promise(function(resolve, reject) { + if (document.readyState === 'complete') { + resolve(); + } else { + window.addEventListener('load', resolve); + } + }); +} + +function waitForPaints() { + return new Promise(function(resolve, reject) { + waitForAllPaintsFlushed(resolve); + }); +} + +promise_test(function(t) { + var div = addDiv(t); + var cs = window.getComputedStyle(div); + + // Test that empty animations actually start. + // + // Normally we tie the start of animations to when their first frame of + // the animation is rendered. However, for animations that don't actually + // trigger a paint (e.g. because they are empty, or are animating something + // that doesn't render or is offscreen) we want to make sure they still + // start. + // + // Before we start, wait for the document to finish loading. This is because + // during loading we will have other paint events taking place which might, + // by luck, happen to trigger animations that otherwise would not have been + // triggered, leading to false positives. + // + // As a result, it's better to wait until we have a more stable state before + // continuing. + var promiseCallbackDone = false; + return waitForDocLoad().then(function() { + div.style.animation = 'empty 1000s'; + var animation = div.getAnimations()[0]; + + return animation.ready.then(function() { + promiseCallbackDone = true; + }).catch(function() { + assert_unreached('ready promise was rejected'); + }); + }).then(function() { + // We need to wait for up to three frames. This is because in some + // cases it can take up to two frames for the initial layout + // to take place. Even after that happens we don't actually resolve the + // ready promise until the following tick. + return waitForAnimationFrames(3); + }).then(function() { + assert_true(promiseCallbackDone, + 'ready promise for an empty animation was resolved' + + ' within three animation frames'); + }); +}, 'Animation.ready is resolved for an empty animation'); + +// Test that compositor animations with delays get synced correctly +// +// NOTE: It is important that we DON'T use +// SpecialPowers.DOMWindowUtils.advanceTimeAndRefresh here since that takes +// us through a different code path. +promise_test(function(t) { + // This test only applies to compositor animations + if (!isOMTAEnabled()) { + return; + } + + // Setup animation + var div = addDiv(t); + div.classList.add('target'); + div.style.animation = 'animTransform 100s -50s forwards'; + var animation = div.getAnimations()[0]; + + return waitForPaints(function() { + var transformStr = + SpecialPowers.DOMWindowUtils.getOMTAStyle(div, 'transform'); + + var matrixComponents = + transformStr.startsWith('matrix(') + ? transformStr.substring('matrix('.length, transformStr.length-1) + .split(',') + .map(component => Number(component)) + : []; + assert_equals(matrixComponents.length, 6, + 'Got a valid transform matrix on the compositor' + + ' (got: "' + transformStr + '")'); + + // If the delay has been applied correctly we should be at least + // half-way through the animation + assert_true(matrixComponents[4] >= 50, + 'Animation is at least half-way through on the compositor' + + ' (got translation of ' + matrixComponents[4] + ')'); + }); +}, 'Starting an animation with a delay starts from the correct point'); + +done(); +</script> +</body> diff --git a/dom/animation/test/mozilla/file_disable_animations_api_core.html b/dom/animation/test/mozilla/file_disable_animations_api_core.html new file mode 100644 index 0000000000..ef77988d9a --- /dev/null +++ b/dom/animation/test/mozilla/file_disable_animations_api_core.html @@ -0,0 +1,30 @@ +<!doctype html> +<meta charset=utf-8> +<script src="../testcommon.js"></script> +<body> +<script> +'use strict'; + +test(function(t) { + var div = addDiv(t); + var anim = + div.animate({ marginLeft: ['0px', '10px'] }, + { duration: 100 * MS_PER_SEC, + easing: 'linear', + iterations: 10, + iterationComposite: 'accumulate' }); + anim.pause(); + + // NOTE: We can't check iterationComposite value itself though API since + // Animation.effect is also behind the the Web Animations API. So we just + // check that style value is not affected by iterationComposite. + anim.currentTime = 200 * MS_PER_SEC; + assert_equals(getComputedStyle(div).marginLeft, '0px', + 'Animated style should not be accumulated when the Web Animations API is ' + + 'not enabled even if accumulate is specified in the constructor'); +}, 'iterationComposite should not affect at all if the Web Animations API ' + + 'is not enabled'); + +done(); +</script> +</body> diff --git a/dom/animation/test/mozilla/file_disabled_properties.html b/dom/animation/test/mozilla/file_disabled_properties.html new file mode 100644 index 0000000000..f1b72973f4 --- /dev/null +++ b/dom/animation/test/mozilla/file_disabled_properties.html @@ -0,0 +1,73 @@ +<!doctype html> +<meta charset=utf-8> +<script src="../testcommon.js"></script> +<body> +<script> +'use strict'; + +function waitForSetPref(pref, value) { + return new Promise(function(resolve, reject) { + SpecialPowers.pushPrefEnv({ 'set': [[pref, value]] }, resolve); + }); +} + +/* + * These tests rely on the fact that the -webkit-text-fill-color property + * is disabled by the layout.css.prefixes.webkit pref. If we ever remove that + * pref we will need to substitute some other pref:property combination. + */ + +promise_test(function(t) { + return waitForSetPref('layout.css.prefixes.webkit', true).then(() => { + var anim = addDiv(t).animate({ webkitTextFillColor: [ 'green', 'blue' ]}); + assert_equals(anim.effect.getKeyframes().length, 2, + 'A property-indexed keyframe specifying only enabled' + + ' properties produces keyframes'); + return waitForSetPref('layout.css.prefixes.webkit', false); + }).then(() => { + var anim = addDiv(t).animate({ webkitTextFillColor: [ 'green', 'blue' ]}); + assert_equals(anim.effect.getKeyframes().length, 0, + 'A property-indexed keyframe specifying only disabled' + + ' properties produces no keyframes'); + }); +}, 'Specifying a disabled property using a property-indexed keyframe'); + +promise_test(function(t) { + var createAnim = () => { + var anim = addDiv(t).animate([ { webkitTextFillColor: 'green' }, + { webkitTextFillColor: 'blue' } ]); + assert_equals(anim.effect.getKeyframes().length, 2, + 'Animation specified using a keyframe sequence should' + + ' return the same number of keyframes regardless of' + + ' whether or not the specified properties are disabled'); + return anim; + }; + + var assert_has_property = (anim, index, descr, property) => { + assert_true( + anim.effect.getKeyframes()[index].hasOwnProperty(property), + `${descr} should have the '${property}' property`); + }; + var assert_does_not_have_property = (anim, index, descr, property) => { + assert_false( + anim.effect.getKeyframes()[index].hasOwnProperty(property), + `${descr} should NOT have the '${property}' property`); + }; + + return waitForSetPref('layout.css.prefixes.webkit', true).then(() => { + var anim = createAnim(); + assert_has_property(anim, 0, 'Initial keyframe', 'webkitTextFillColor'); + assert_has_property(anim, 1, 'Final keyframe', 'webkitTextFillColor'); + return waitForSetPref('layout.css.prefixes.webkit', false); + }).then(() => { + var anim = createAnim(); + assert_does_not_have_property(anim, 0, 'Initial keyframe', + 'webkitTextFillColor'); + assert_does_not_have_property(anim, 1, 'Final keyframe', + 'webkitTextFillColor'); + }); +}, 'Specifying a disabled property using a keyframe sequence'); + +done(); +</script> +</body> diff --git a/dom/animation/test/mozilla/file_discrete-animations.html b/dom/animation/test/mozilla/file_discrete-animations.html new file mode 100644 index 0000000000..35e818a909 --- /dev/null +++ b/dom/animation/test/mozilla/file_discrete-animations.html @@ -0,0 +1,170 @@ +<!doctype html> +<head> +<meta charset=utf-8> +<title>Test Mozilla-specific discrete animatable properties</title> +<script type="application/javascript" src="../testcommon.js"></script> +</head> +<body> +<script> +"use strict"; + +const gMozillaSpecificProperties = { + "-moz-appearance": { + // https://drafts.csswg.org/css-align/#propdef-align-content + from: "button", + to: "none" + }, + "-moz-border-bottom-colors": { + from: "rgb(255, 0, 0) rgb(255, 0, 0) rgb(255, 0, 0) rgb(255, 0, 0)", + to: "rgb(0, 255, 0) rgb(0, 255, 0) rgb(0, 255, 0) rgb(0, 255, 0)" + }, + "-moz-border-left-colors": { + from: "rgb(255, 0, 0) rgb(255, 0, 0) rgb(255, 0, 0) rgb(255, 0, 0)", + to: "rgb(0, 255, 0) rgb(0, 255, 0) rgb(0, 255, 0) rgb(0, 255, 0)" + }, + "-moz-border-right-colors": { + from: "rgb(255, 0, 0) rgb(255, 0, 0) rgb(255, 0, 0) rgb(255, 0, 0)", + to: "rgb(0, 255, 0) rgb(0, 255, 0) rgb(0, 255, 0) rgb(0, 255, 0)" + }, + "-moz-border-top-colors": { + from: "rgb(255, 0, 0) rgb(255, 0, 0) rgb(255, 0, 0) rgb(255, 0, 0)", + to: "rgb(0, 255, 0) rgb(0, 255, 0) rgb(0, 255, 0) rgb(0, 255, 0)" + }, + "-moz-box-align": { + // https://developer.mozilla.org/en/docs/Web/CSS/box-align + from: "center", + to: "stretch" + }, + "-moz-box-direction": { + // https://developer.mozilla.org/en/docs/Web/CSS/box-direction + from: "reverse", + to: "normal" + }, + "-moz-box-ordinal-group": { + // https://developer.mozilla.org/en/docs/Web/CSS/box-ordinal-group + from: "1", + to: "5" + }, + "-moz-box-orient": { + // https://www.w3.org/TR/css-flexbox-1/ + from: "horizontal", + to: "vertical" + }, + "-moz-box-pack": { + // https://www.w3.org/TR/2009/WD-css3-flexbox-20090723/#propdef-box-pack + from: "center", + to: "end" + }, + "-moz-float-edge": { + // https://developer.mozilla.org/en/docs/Web/CSS/-moz-float-edge + from: "margin-box", + to: "content-box" + }, + "-moz-force-broken-image-icon": { + // https://developer.mozilla.org/en/docs/Web/CSS/-moz-force-broken-image-icon + from: "1", + to: "5" + }, + "image-rendering": { + // https://drafts.csswg.org/css-images-3/#propdef-image-rendering + from: "-moz-crisp-edges", + to: "auto" + }, + "-moz-stack-sizing": { + // https://developer.mozilla.org/en/docs/Web/CSS/-moz-stack-sizing + from: "ignore", + to: "stretch-to-fit" + }, + "-moz-tab-size": { + // https://drafts.csswg.org/css-text-3/#propdef-tab-size + from: "1", + to: "5" + }, + "-moz-text-size-adjust": { + // https://drafts.csswg.org/css-size-adjust/#propdef-text-size-adjust + from: "none", + to: "auto" + }, + "-webkit-text-stroke-width": { + // https://compat.spec.whatwg.org/#propdef--webkit-text-stroke-width + from: "10px", + to: "50px" + } +} + +for (let property in gMozillaSpecificProperties) { + const testData = gMozillaSpecificProperties[property]; + const from = testData.from; + const to = testData.to; + const idlName = propertyToIDL(property); + const keyframes = {}; + keyframes[idlName] = [from, to]; + + test(t => { + const div = addDiv(t); + const animation = div.animate(keyframes, + { duration: 1000, fill: "both" }); + testAnimationSamples(animation, idlName, + [{ time: 0, expected: from.toLowerCase() }, + { time: 499, expected: from.toLowerCase() }, + { time: 500, expected: to.toLowerCase() }, + { time: 1000, expected: to.toLowerCase() }]); + }, property + " should animate between '" + + from + "' and '" + to + "' with linear easing"); + + test(function(t) { + // Easing: http://cubic-bezier.com/#.68,0,1,.01 + // With this curve, we don't reach the 50% point until about 95% of + // the time has expired. + const div = addDiv(t); + const animation = div.animate(keyframes, + { duration: 1000, fill: "both", + easing: "cubic-bezier(0.68,0,1,0.01)" }); + testAnimationSamples(animation, idlName, + [{ time: 0, expected: from.toLowerCase() }, + { time: 940, expected: from.toLowerCase() }, + { time: 960, expected: to.toLowerCase() }]); + }, property + " should animate between '" + + from + "' and '" + to + "' with effect easing"); + + test(function(t) { + // Easing: http://cubic-bezier.com/#.68,0,1,.01 + // With this curve, we don't reach the 50% point until about 95% of + // the time has expired. + keyframes.easing = "cubic-bezier(0.68,0,1,0.01)"; + const div = addDiv(t); + const animation = div.animate(keyframes, + { duration: 1000, fill: "both" }); + testAnimationSamples(animation, idlName, + [{ time: 0, expected: from.toLowerCase() }, + { time: 940, expected: from.toLowerCase() }, + { time: 960, expected: to.toLowerCase() }]); + }, property + " should animate between '" + + from + "' and '" + to + "' with keyframe easing"); +} + +function propertyToIDL(property) { + var prefixMatch = property.match(/^-(\w+)-/); + if (prefixMatch) { + var prefix = prefixMatch[1] === "moz" ? "Moz" : prefixMatch[1]; + property = prefix + property.substring(prefixMatch[0].length - 1); + } + // https://drafts.csswg.org/cssom/#css-property-to-idl-attribute + return property.replace(/-([a-z])/gi, function(str, group) { + return group.toUpperCase(); + }); +} + +function testAnimationSamples(animation, idlName, testSamples) { + const target = animation.effect.target; + testSamples.forEach(testSample => { + animation.currentTime = testSample.time; + assert_equals(getComputedStyle(target)[idlName], testSample.expected, + "The value should be " + testSample.expected + + " at " + testSample.time + "ms"); + }); +} + +done(); +</script> +</body> diff --git a/dom/animation/test/mozilla/file_document-timeline-origin-time-range.html b/dom/animation/test/mozilla/file_document-timeline-origin-time-range.html new file mode 100644 index 0000000000..083bf0903b --- /dev/null +++ b/dom/animation/test/mozilla/file_document-timeline-origin-time-range.html @@ -0,0 +1,30 @@ +<!doctype html> +<meta charset=utf-8> +<script src="../testcommon.js"></script> +<body> +<script> +'use strict'; + +// If the originTime parameter passed to the DocumentTimeline exceeds +// the range of the internal storage type (a signed 64-bit integer number +// of ticks--a platform-dependent unit) then we should throw. +// Infinity isn't allowed as an origin time value and clamping to just +// inside the allowed range will just mean we overflow elsewhere. + +test(function(t) { + assert_throws({ name: 'TypeError'}, + function() { + new DocumentTimeline({ originTime: Number.MAX_SAFE_INTEGER }); + }); +}, 'Calculated current time is positive infinity'); + +test(function(t) { + assert_throws({ name: 'TypeError'}, + function() { + new DocumentTimeline({ originTime: -1 * Number.MAX_SAFE_INTEGER }); + }); +}, 'Calculated current time is negative infinity'); + +done(); +</script> +</body> diff --git a/dom/animation/test/mozilla/file_hide_and_show.html b/dom/animation/test/mozilla/file_hide_and_show.html new file mode 100644 index 0000000000..0771fcce1f --- /dev/null +++ b/dom/animation/test/mozilla/file_hide_and_show.html @@ -0,0 +1,162 @@ +<!doctype html> +<meta charset=utf-8> +<script src="../testcommon.js"></script> +<style> +@keyframes move { + 100% { + transform: translateX(100px); + } +} + +</style> +<body> +<script> +'use strict'; + +test(function(t) { + var div = addDiv(t, { style: 'animation: move 100s infinite' }); + assert_equals(div.getAnimations().length, 1, + 'display:initial element has animations'); + + div.style.display = 'none'; + assert_equals(div.getAnimations().length, 0, + 'display:none element has no animations'); +}, 'Animation stops playing when the element style display is set to "none"'); + +test(function(t) { + var parentElement = addDiv(t); + var div = addDiv(t, { style: 'animation: move 100s infinite' }); + parentElement.appendChild(div); + assert_equals(div.getAnimations().length, 1, + 'display:initial element has animations'); + + parentElement.style.display = 'none'; + assert_equals(div.getAnimations().length, 0, + 'Element in display:none subtree has no animations'); +}, 'Animation stops playing when its parent element style display is set ' + + 'to "none"'); + +test(function(t) { + var div = addDiv(t, { style: 'animation: move 100s infinite' }); + assert_equals(div.getAnimations().length, 1, + 'display:initial element has animations'); + + div.style.display = 'none'; + assert_equals(div.getAnimations().length, 0, + 'display:none element has no animations'); + + div.style.display = ''; + assert_equals(div.getAnimations().length, 1, + 'Element which is no longer display:none has animations ' + + 'again'); +}, 'Animation starts playing when the element gets shown from ' + + '"display:none" state'); + +test(function(t) { + var parentElement = addDiv(t); + var div = addDiv(t, { style: 'animation: move 100s infinite' }); + parentElement.appendChild(div); + assert_equals(div.getAnimations().length, 1, + 'display:initial element has animations'); + + parentElement.style.display = 'none'; + assert_equals(div.getAnimations().length, 0, + 'Element in display:none subtree has no animations'); + + parentElement.style.display = ''; + assert_equals(div.getAnimations().length, 1, + 'Element which is no longer in display:none subtree has ' + + 'animations again'); +}, 'Animation starts playing when its parent element is shown from ' + + '"display:none" state'); + +test(function(t) { + var div = addDiv(t, { style: 'animation: move 100s forwards' }); + assert_equals(div.getAnimations().length, 1, + 'display:initial element has animations'); + + var animation = div.getAnimations()[0]; + animation.finish(); + assert_equals(div.getAnimations().length, 1, + 'Element has finished animation if the animation ' + + 'fill-mode is forwards'); + + div.style.display = 'none'; + assert_equals(animation.playState, 'idle', + 'The animation.playState should be idle'); + + assert_equals(div.getAnimations().length, 0, + 'display:none element has no animations'); + + div.style.display = ''; + assert_equals(div.getAnimations().length, 1, + 'Element which is no longer display:none has animations ' + + 'again'); + assert_not_equals(div.getAnimations()[0], animation, + 'Restarted animation is a newly-generated animation'); + +}, 'Animation which has already finished starts playing when the element ' + + 'gets shown from "display:none" state'); + +test(function(t) { + var parentElement = addDiv(t); + var div = addDiv(t, { style: 'animation: move 100s forwards' }); + parentElement.appendChild(div); + assert_equals(div.getAnimations().length, 1, + 'display:initial element has animations'); + + var animation = div.getAnimations()[0]; + animation.finish(); + assert_equals(div.getAnimations().length, 1, + 'Element has finished animation if the animation ' + + 'fill-mode is forwards'); + + parentElement.style.display = 'none'; + assert_equals(animation.playState, 'idle', + 'The animation.playState should be idle'); + assert_equals(div.getAnimations().length, 0, + 'Element in display:none subtree has no animations'); + + parentElement.style.display = ''; + assert_equals(div.getAnimations().length, 1, + 'Element which is no longer in display:none subtree has ' + + 'animations again'); + + assert_not_equals(div.getAnimations()[0], animation, + 'Restarted animation is a newly-generated animation'); + +}, 'Animation with fill:forwards which has already finished starts playing ' + + 'when its parent element is shown from "display:none" state'); + +test(function(t) { + var parentElement = addDiv(t); + var div = addDiv(t, { style: 'animation: move 100s' }); + parentElement.appendChild(div); + assert_equals(div.getAnimations().length, 1, + 'display:initial element has animations'); + + var animation = div.getAnimations()[0]; + animation.finish(); + assert_equals(div.getAnimations().length, 0, + 'Element does not have finished animations'); + + parentElement.style.display = 'none'; + assert_equals(animation.playState, 'idle', + 'The animation.playState should be idle'); + assert_equals(div.getAnimations().length, 0, + 'Element in display:none subtree has no animations'); + + parentElement.style.display = ''; + assert_equals(div.getAnimations().length, 1, + 'Element which is no longer in display:none subtree has ' + + 'animations again'); + + assert_not_equals(div.getAnimations()[0], animation, + 'Restarted animation is a newly-generated animation'); + +}, 'CSS Animation which has already finished starts playing when its parent ' + + 'element is shown from "display:none" state'); + +done(); +</script> +</body> diff --git a/dom/animation/test/mozilla/file_partial_keyframes.html b/dom/animation/test/mozilla/file_partial_keyframes.html new file mode 100644 index 0000000000..68832be7a0 --- /dev/null +++ b/dom/animation/test/mozilla/file_partial_keyframes.html @@ -0,0 +1,41 @@ +<!doctype html> +<meta charset=utf-8> +<script src="../testcommon.js"></script> +<body> +<script> +'use strict'; + +// Tests for cases we currently don't handle and should throw an exception for. +var gTests = [ + { desc: "single Keyframe with no offset", + keyframes: [{ left: "100px" }] }, + { desc: "multiple Keyframes with missing 0% Keyframe", + keyframes: [{ left: "100px", offset: 0.25 }, + { left: "200px", offset: 0.50 }, + { left: "300px", offset: 1.00 }] }, + { desc: "multiple Keyframes with missing 100% Keyframe", + keyframes: [{ left: "100px", offset: 0.00 }, + { left: "200px", offset: 0.50 }, + { left: "300px", offset: 0.75 }] }, + { desc: "multiple Keyframes with missing properties on first Keyframe", + keyframes: [{ left: "100px", offset: 0.0 }, + { left: "200px", top: "200px", offset: 0.5 }, + { left: "300px", top: "300px", offset: 1.0 }] }, + { desc: "multiple Keyframes with missing properties on last Keyframe", + keyframes: [{ left: "100px", top: "200px", offset: 0.0 }, + { left: "200px", top: "200px", offset: 0.5 }, + { left: "300px", offset: 1.0 }] }, +]; + +gTests.forEach(function(subtest) { + test(function(t) { + var div = addDiv(t); + assert_throws("NotSupportedError", function() { + new KeyframeEffectReadOnly(div, subtest.keyframes); + }); + }, "KeyframeEffectReadOnly constructor throws with " + subtest.desc); +}); + +done(); +</script> +</body> diff --git a/dom/animation/test/mozilla/file_set-easing.html b/dom/animation/test/mozilla/file_set-easing.html new file mode 100644 index 0000000000..072b125cb0 --- /dev/null +++ b/dom/animation/test/mozilla/file_set-easing.html @@ -0,0 +1,34 @@ +<!doctype html> +<head> +<meta charset=utf-8> +<title>Test setting easing in sandbox</title> +<script src="../testcommon.js"></script> +</head> +<body> +<script> +"use strict"; + +test(function(t) { + const div = document.createElement("div"); + document.body.appendChild(div); + div.animate({ opacity: [0, 1] }, 100000 ); + + const contentScript = function() { + try { + document.getAnimations()[0].effect.timing.easing = "linear"; + assert_true(true, 'Setting easing should not throw in sandbox'); + } catch (e) { + assert_unreached('Setting easing threw ' + e); + } + }; + + const sandbox = new SpecialPowers.Cu.Sandbox(window); + sandbox.importFunction(document, "document"); + sandbox.importFunction(assert_true, "assert_true"); + sandbox.importFunction(assert_unreached, "assert_unreached"); + SpecialPowers.Cu.evalInSandbox(`(${contentScript.toSource()})()`, sandbox); +}, 'Setting easing should not throw any exceptions in sandbox'); + +done(); +</script> +</body> diff --git a/dom/animation/test/mozilla/file_spacing_property_order.html b/dom/animation/test/mozilla/file_spacing_property_order.html new file mode 100644 index 0000000000..1338d60811 --- /dev/null +++ b/dom/animation/test/mozilla/file_spacing_property_order.html @@ -0,0 +1,33 @@ +<!doctype html> +<meta charset=utf-8> +<script src="../testcommon.js"></script> +<body> +<script> +'use strict'; + +test(function(t) { + var div = document.createElement('div'); + document.documentElement.appendChild(div); + var anim = div.animate([ { borderRadius: "0", borderTopRightRadius: "10%" }, + { borderTopLeftRadius: "20%", + borderTopRightRadius: "30%", + borderBottomRightRadius: "40%", + borderBottomLeftRadius: "50%" }, + { borderRadius: "50%" } ], + { spacing:"paced(border-radius)" }); + + var frames = anim.effect.getKeyframes(); + var dist = [ 0, + Math.sqrt(20 * 20 + (30 - 10) * (30 - 10) + 40 * 40 + 50 * 50), + Math.sqrt((50 - 20) * (50 - 20) + (50 - 30) * (50 - 30) + + (50 - 40) * (50 - 40) + (50 - 50) * (50 - 50)) ]; + var cumDist = []; + dist.reduce(function(prev, curr, i) { return cumDist[i] = prev + curr; }, 0); + assert_approx_equals(frames[1].computedOffset, cumDist[1] / cumDist[2], + 0.0001, 'frame offset'); +}, 'Test for the longhand components of the shorthand property surely sorted' ); + +done(); + +</script> +</body> diff --git a/dom/animation/test/mozilla/file_spacing_transform.html b/dom/animation/test/mozilla/file_spacing_transform.html new file mode 100644 index 0000000000..0de7737864 --- /dev/null +++ b/dom/animation/test/mozilla/file_spacing_transform.html @@ -0,0 +1,240 @@ +<!doctype html> +<meta charset=utf-8> +<script src="../testcommon.js"></script> +<body> +<script> +'use strict'; + +const pi = Math.PI; +const cos = Math.cos; +const sin = Math.sin; +const tan = Math.tan; +const sqrt = Math.sqrt; + +// Help function for testing the computed offsets by the distance array. +function assert_animation_offsets(anim, dist) { + const epsilon = 0.00000001; + const frames = anim.effect.getKeyframes(); + const cumDist = dist.reduce( (prev, curr) => { + prev.push(prev.length == 0 ? curr : curr + prev[prev.length - 1]); + return prev; + }, []); + + const total = cumDist[cumDist.length - 1]; + for (var i = 0; i < frames.length; ++i) { + assert_approx_equals(frames[i].computedOffset, cumDist[i] / total, + epsilon, 'computedOffset of frame ' + i); + } +} + +function getAngleDist(rotate1, rotate2) { + function quaternion(axis, angle) { + var x = axis[0] * sin(angle/2.0); + var y = axis[1] * sin(angle/2.0); + var z = axis[2] * sin(angle/2.0); + var w = cos(angle/2.0); + return { 'x': x, 'y': y, 'z': z, 'w': w }; + } + var q1 = quaternion(rotate1.axis, rotate1.angle); + var q2 = quaternion(rotate2.axis, rotate2.angle); + var dotProduct = q1.x * q2.x + q1.y * q2.y + q1.z * q2.z + q1.w * q2.w; + return 2.0 * Math.acos(dotProduct); +} + +function createMatrix(elements, Is3D) { + return (Is3D ? "matrix3d" : "matrix") + "(" + elements.join() + ")"; +} + +test(function(t) { + var anim = addDiv(t).animate([ { transform: "none" }, + { transform: "translate(-20px)" }, + { transform: "translate(100px)" }, + { transform: "translate(50px)"} ], + { spacing: "paced(transform)" }); + assert_animation_offsets(anim, [ 0, 20, 120, 50 ]); +}, 'Test spacing on translate' ); + +test(function(t) { + var anim = + addDiv(t).animate([ { transform: "none" }, + { transform: "translate3d(-20px, 10px, 100px)" }, + { transform: "translate3d(100px, 200px, 50px)" }, + { transform: "translate(50px, -10px)"} ], + { spacing: "paced(transform)" }); + var dist = [ 0, + Math.sqrt(20 * 20 + 10 * 10 + 100 * 100), + Math.sqrt(120 * 120 + 190 * 190 + 50 * 50), + Math.sqrt(50 * 50 + 210 * 210 + 50 * 50) ]; + assert_animation_offsets(anim, dist); +}, 'Test spacing on translate3d' ); + +test(function(t) { + var anim = addDiv(t).animate([ { transform: "scale(0.5)" }, + { transform: "scale(4.5)" }, + { transform: "scale(2.5)" }, + { transform: "none"} ], + { spacing: "paced(transform)" }); + assert_animation_offsets(anim, [ 0, 4.0, 2.0, 1.5 ]); +}, 'Test spacing on scale' ); + +test(function(t) { + var anim = addDiv(t).animate([ { transform: "scale(0.5, 0.5)" }, + { transform: "scale3d(4.5, 5.0, 2.5)" }, + { transform: "scale3d(2.5, 1.0, 2.0)" }, + { transform: "scale3d(1, 0.5, 1.0)"} ], + { spacing:"paced(transform)" }); + var dist = [ 0, + Math.sqrt(4.0 * 4.0 + 4.5 * 4.5 + 1.5 * 1.5), + Math.sqrt(2.0 * 2.0 + 4.0 * 4.0 + 0.5 * 0.5), + Math.sqrt(1.5 * 1.5 + 0.5 * 0.5 + 1.0 * 1.0) ]; + assert_animation_offsets(anim, dist); +}, 'Test spacing on scale3d' ); + +test(function(t) { + var anim = addDiv(t).animate([ { transform: "rotate(60deg)" }, + { transform: "none" }, + { transform: "rotate(720deg)" }, + { transform: "rotate(-360deg)"} ], + { spacing: "paced(transform)" }); + assert_animation_offsets(anim, [ 0, 60, 720, 1080 ]); +}, 'Test spacing on rotate' ); + +test(function(t) { + var anim = addDiv(t).animate([ { transform: "rotate3d(1,0,0,60deg)" }, + { transform: "rotate3d(1,0,0,70deg)" }, + { transform: "rotate3d(0,0,1,-110deg)" }, + { transform: "rotate3d(1,0,0,219deg)"} ], + { spacing: "paced(transform)" }); + var dist = [ 0, + getAngleDist({ axis: [1,0,0], angle: 60 * pi / 180 }, + { axis: [1,0,0], angle: 70 * pi / 180 }), + getAngleDist({ axis: [0,1,0], angle: 70 * pi / 180 }, + { axis: [0,0,1], angle: -110 * pi / 180 }), + getAngleDist({ axis: [0,0,1], angle: -110 * pi / 180 }, + { axis: [1,0,0], angle: 219 * pi / 180 }) ]; + assert_animation_offsets(anim, dist); +}, 'Test spacing on rotate3d' ); + +test(function(t) { + var anim = addDiv(t).animate([ { transform: "skew(60deg)" }, + { transform: "none" }, + { transform: "skew(-90deg)" }, + { transform: "skew(90deg)"} ], + { spacing: "paced(transform)" }); + assert_animation_offsets(anim, [ 0, 60, 90, 180 ]); +}, 'Test spacing on skew' ); + +test(function(t) { + var anim = addDiv(t).animate([ { transform: "skew(60deg, 30deg)" }, + { transform: "none" }, + { transform: "skew(-90deg, 60deg)" }, + { transform: "skew(90deg, 60deg)"} ], + { spacing: "paced(transform)" }); + var dist = [ 0, + sqrt(60 * 60 + 30 * 30), + sqrt(90 * 90 + 60 * 60), + sqrt(180 * 180 + 0) ]; + assert_animation_offsets(anim, dist); +}, 'Test spacing on skew along both X and Y' ); + +test(function(t) { + // We calculate the distance of two perspective functions by converting them + // into two matrix3ds, and then do matrix decomposition to get two + // perspective vectors, so the equivalent perspective vectors are: + // perspective 1: (0, 0, -1/128, 1); + // perspective 2: (0, 0, -1/infinity = 0, 1); + // perspective 3: (0, 0, -1/1024, 1); + // perspective 4: (0, 0, -1/32, 1); + var anim = addDiv(t).animate([ { transform: "perspective(128px)" }, + { transform: "none" }, + { transform: "perspective(1024px)" }, + { transform: "perspective(32px)"} ], + { spacing: "paced(transform)" }); + assert_animation_offsets(anim, + [ 0, 1/128, 1/1024, 1/32 - 1/1024 ]); +}, 'Test spacing on perspective' ); + +test(function(t) { + var anim = + addDiv(t).animate([ { transform: "none" }, + { transform: "rotate(180deg) translate(0px)" }, + { transform: "rotate(180deg) translate(1000px)" }, + { transform: "rotate(360deg) translate(1000px)"} ], + { spacing: "paced(transform)" }); + var dist = [ 0, + sqrt(pi * pi + 0), + sqrt(1000 * 1000), + sqrt(pi * pi + 0) ]; + assert_animation_offsets(anim, dist); +}, 'Test spacing on matched transform lists' ); + +test(function(t) { + // matrix1 => translate(100px, 50px), skewX(60deg). + // matrix2 => translate(1000px), rotate(180deg). + // matrix3 => translate(1000px), scale(1.5, 0.7). + const matrix1 = createMatrix([ 1, 0, tan(pi/4.0), 1, 100, 50 ]); + const matrix2 = createMatrix([ cos(pi), sin(pi), + -sin(pi), cos(pi), + 1000, 0 ]); + const matrix3 = createMatrix([ 1.5, 0, 0, 0.7, 1000, 0 ]); + var anim = addDiv(t).animate([ { transform: "none" }, + { transform: matrix1 }, + { transform: matrix2 }, + { transform: matrix3 } ], + { spacing: "paced(transform)" }); + var dist = [ 0, + sqrt(100 * 100 + 50 * 50 + pi/4 * pi/4), + sqrt(900 * 900 + 50 * 50 + pi * pi + pi/4 * pi/4), + sqrt(pi * pi + 0.5 * 0.5 + 0.3 * 0.3) ]; + assert_animation_offsets(anim, dist); +}, 'Test spacing on matrix' ); + +test(function(t) { + // matrix1 => translate3d(100px, 50px, -10px), skew(60deg). + // matrix2 => translate3d(1000px, 0, 0), rotate3d(1, 0, 0, 180deg). + // matrix3 => translate3d(1000px, 0, 0), scale3d(1.5, 0.7, 2.2). + const matrix1 = createMatrix([ 1, 0, 0, 0, + tan(pi/4.0), 1, 0, 0, + 0, 0, 1, 0, + 100, 50, -10, 1 ], true); + const matrix2 = createMatrix([ 1, 0, 0, 0, + 0, cos(pi), sin(pi), 0, + 0, -sin(pi), cos(pi), 0, + 1000, 0, 0, 1 ], true); + const matrix3 = createMatrix([ 1.5, 0, 0, 0, + 0, 0.7, 0, 0, + 0, 0, 2.2, 0, + 1000, 0, 0, 1 ], true); + var anim = addDiv(t).animate([ { transform: "none" }, + { transform: matrix1 }, + { transform: matrix2 }, + { transform: matrix3 } ], + { spacing: "paced(transform)" }); + var dist = [ 0, + sqrt(100 * 100 + 50 * 50 + 10 * 10 + pi/4 * pi/4), + sqrt(900 * 900 + 50 * 50 + 10 * 10 + pi/4 * pi/4 + pi * pi), + sqrt(0.5 * 0.5 + 0.3 * 0.3 + 1.2 * 1.2 + pi * pi) ]; + assert_animation_offsets(anim, dist); +}, 'Test spacing on matrix3d' ); + +test(function(t) { + var anim = + addDiv(t).animate([ { transform: "none" }, + { transform: "translate(100px, 50px) skew(45deg)" }, + { transform: "translate(1000px) " + + "rotate3d(1, 0, 0, 180deg)" }, + { transform: "translate(1000px) " + + "scale3d(2.5, 0.5, 0.7)" } ], + { spacing: "paced(transform)" }); + + var dist = [ 0, + sqrt(100 * 100 + 50 * 50 + pi/4 * pi/4), + sqrt(900 * 900 + 50 * 50 + pi/4 * pi/4 + pi * pi), + sqrt(1.5 * 1.5 + 0.5 * 0.5 + 0.3 * 0.3 + pi * pi) ]; + assert_animation_offsets(anim, dist); +}, 'Test spacing on mismatched transform list' ); + +done(); + +</script> +</body> diff --git a/dom/animation/test/mozilla/file_transform_limits.html b/dom/animation/test/mozilla/file_transform_limits.html new file mode 100644 index 0000000000..d4c813c67d --- /dev/null +++ b/dom/animation/test/mozilla/file_transform_limits.html @@ -0,0 +1,55 @@ +<!doctype html> +<meta charset=utf-8> +<script src="../testcommon.js"></script> +<body> +<script> +'use strict'; + +// We clamp +infinity or -inifinity value in floating point to +// maximum floating point value or -maximum floating point value. +const max_float = 3.40282e+38; + +test(function(t) { + var div = addDiv(t); + div.style = "width: 1px; height: 1px;"; + var anim = div.animate([ { transform: 'scale(1)' }, + { transform: 'scale(3.5e+38)'}, + { transform: 'scale(3)' } ], 100 * MS_PER_SEC); + + anim.pause(); + anim.currentTime = 50 * MS_PER_SEC; + assert_equals(getComputedStyle(div).transform, + 'matrix(' + max_float + ', 0, 0, ' + max_float + ', 0, 0)'); +}, 'Test that the parameter of transform scale is clamped' ); + +test(function(t) { + var div = addDiv(t); + div.style = "width: 1px; height: 1px;"; + var anim = div.animate([ { transform: 'translate(1px)' }, + { transform: 'translate(3.5e+38px)'}, + { transform: 'translate(3px)' } ], 100 * MS_PER_SEC); + + anim.pause(); + anim.currentTime = 50 * MS_PER_SEC; + assert_equals(getComputedStyle(div).transform, + 'matrix(1, 0, 0, 1, ' + max_float + ', 0)'); +}, 'Test that the parameter of transform translate is clamped' ); + +test(function(t) { + var div = addDiv(t); + div.style = "width: 1px; height: 1px;"; + var anim = div.animate([ { transform: 'matrix(0.5, 0, 0, 0.5, 0, 0)' }, + { transform: 'matrix(2, 0, 0, 2, 3.5e+38, 0)'}, + { transform: 'matrix(0, 2, 0, -2, 0, 0)' } ], + 100 * MS_PER_SEC); + + anim.pause(); + anim.currentTime = 50 * MS_PER_SEC; + assert_equals(getComputedStyle(div).transform, + 'matrix(2, 0, 0, 2, ' + max_float + ', 0)'); +}, 'Test that the parameter of transform matrix is clamped' ); + +done(); + +</script> +</body> diff --git a/dom/animation/test/mozilla/file_transition_finish_on_compositor.html b/dom/animation/test/mozilla/file_transition_finish_on_compositor.html new file mode 100644 index 0000000000..4912d05dd1 --- /dev/null +++ b/dom/animation/test/mozilla/file_transition_finish_on_compositor.html @@ -0,0 +1,67 @@ +<!doctype html> +<meta charset=utf-8> +<script src="../testcommon.js"></script> +<script src="/tests/SimpleTest/paint_listener.js"></script> +<style> +div { + /* Element needs geometry to be eligible for layerization */ + width: 100px; + height: 100px; + background-color: white; +} +</style> +<body> +<script> +'use strict'; + +function waitForPaints() { + return new Promise(function(resolve, reject) { + waitForAllPaintsFlushed(resolve); + }); +} + +promise_test(t => { + // This test only applies to compositor animations + if (!isOMTAEnabled()) { + return; + } + + var div = addDiv(t, { style: 'transition: transform 50ms; ' + + 'transform: translateX(0px)' }); + getComputedStyle(div).transform; + + div.style.transform = 'translateX(100px)'; + + var timeBeforeStart = window.performance.now(); + return waitForPaints().then(() => { + // If it took over 50ms to paint the transition, we have no luck + // to test it. This situation will happen if GC runs while waiting for the + // paint. + if (window.performance.now() - timeBeforeStart >= 50) { + return; + } + + var transform = + SpecialPowers.DOMWindowUtils.getOMTAStyle(div, 'transform'); + assert_not_equals(transform, '', + 'The transition style is applied on the compositor'); + + // Generate artificial busyness on the main thread for 100ms. + var timeAtStart = window.performance.now(); + while (window.performance.now() - timeAtStart < 100) {} + + // Now the transition on the compositor should finish but stay at the final + // position because there was no chance to pull the transition back from + // the compositor. + transform = + SpecialPowers.DOMWindowUtils.getOMTAStyle(div, 'transform'); + assert_equals(transform, 'matrix(1, 0, 0, 1, 100, 0)', + 'The final transition style is still applied on the ' + + 'compositor'); + }); +}, 'Transition on the compositor keeps the final style while the main thread ' + + 'is busy even if the transition finished on the compositor'); + +done(); +</script> +</body> diff --git a/dom/animation/test/mozilla/file_underlying-discrete-value.html b/dom/animation/test/mozilla/file_underlying-discrete-value.html new file mode 100644 index 0000000000..3be01b9041 --- /dev/null +++ b/dom/animation/test/mozilla/file_underlying-discrete-value.html @@ -0,0 +1,192 @@ +<!doctype html> +<meta charset=utf-8> +<script src="../testcommon.js"></script> +<body> +<script> +"use strict"; + +// Tests that we correctly extract the underlying value when the animation +// type is 'discrete'. +const discreteTests = [ + { + stylesheet: { + "@keyframes keyframes": + "from { align-content: flex-start; } to { align-content: flex-end; } " + }, + expectedKeyframes: [ + { computedOffset: 0, alignContent: "flex-start" }, + { computedOffset: 1, alignContent: "flex-end" } + ], + explanation: "Test for fully-specified keyframes" + }, + { + stylesheet: { + "@keyframes keyframes": "from { align-content: flex-start; }" + }, + // The value of 100% should be 'stretch', + // but we are not supporting underlying value. + // https://bugzilla.mozilla.org/show_bug.cgi?id=1295401 + expectedKeyframes: [ + { computedOffset: 0, alignContent: "flex-start" }, + { computedOffset: 1, alignContent: "unset" } + ], + explanation: "Test for 0% keyframe only" + }, + { + stylesheet: { + "@keyframes keyframes": "to { align-content: flex-end; }" + }, + // The value of 0% should be 'stretch', + // but we are not supporting underlying value. + // https://bugzilla.mozilla.org/show_bug.cgi?id=1295401 + expectedKeyframes: [ + { computedOffset: 0, alignContent: "unset" }, + { computedOffset: 1, alignContent: "flex-end" } + ], + explanation: "Test for 100% keyframe only" + }, + { + stylesheet: { + "@keyframes keyframes": "50% { align-content: center; }", + "#target": "align-content: space-between;" + }, + expectedKeyframes: [ + { computedOffset: 0, alignContent: "space-between" }, + { computedOffset: 0.5, alignContent: "center" }, + { computedOffset: 1, alignContent: "space-between" } + ], + explanation: "Test for no 0%/100% keyframes " + + "and specified style on target element" + }, + { + stylesheet: { + "@keyframes keyframes": "50% { align-content: center; }" + }, + attributes: { + style: "align-content: space-between" + }, + expectedKeyframes: [ + { computedOffset: 0, alignContent: "space-between" }, + { computedOffset: 0.5, alignContent: "center" }, + { computedOffset: 1, alignContent: "space-between" } + ], + explanation: "Test for no 0%/100% keyframes " + + "and specified style on target element using style attribute" + }, + { + stylesheet: { + "@keyframes keyframes": "50% { align-content: center; }", + "#target": "align-content: inherit;" + }, + // The value of 0%/100% should be 'stretch', + // but we are not supporting underlying value. + // https://bugzilla.mozilla.org/show_bug.cgi?id=1295401 + expectedKeyframes: [ + { computedOffset: 0, alignContent: "inherit" }, + { computedOffset: 0.5, alignContent: "center" }, + { computedOffset: 1, alignContent: "inherit" } + ], + explanation: "Test for no 0%/100% keyframes " + + "and 'inherit' specified on target element" + }, + { + stylesheet: { + "@keyframes keyframes": "50% { align-content: center; }", + ".target": "align-content: space-between;" + }, + attributes: { + class: "target" + }, + expectedKeyframes: [ + { computedOffset: 0, alignContent: "space-between" }, + { computedOffset: 0.5, alignContent: "center" }, + { computedOffset: 1, alignContent: "space-between" } + ], + explanation: "Test for no 0%/100% keyframes " + + "and specified style on target element using class selector" + }, + { + stylesheet: { + "@keyframes keyframes": "50% { align-content: center; }", + "div": "align-content: space-between;" + }, + expectedKeyframes: [ + { computedOffset: 0, alignContent: "space-between" }, + { computedOffset: 0.5, alignContent: "center" }, + { computedOffset: 1, alignContent: "space-between" } + ], + explanation: "Test for no 0%/100% keyframes " + + "and specified style on target element using type selector" + }, + { + stylesheet: { + "@keyframes keyframes": "50% { align-content: center; }", + "div": "align-content: space-between;", + ".target": "align-content: flex-start;", + "#target": "align-content: flex-end;" + }, + attributes: { + class: "target" + }, + expectedKeyframes: [ + { computedOffset: 0, alignContent: "flex-end" }, + { computedOffset: 0.5, alignContent: "center" }, + { computedOffset: 1, alignContent: "flex-end" } + ], + explanation: "Test for no 0%/100% keyframes " + + "and specified style on target element " + + "using ID selector that overrides class selector" + }, + { + stylesheet: { + "@keyframes keyframes": "50% { align-content: center; }", + "div": "align-content: space-between !important;", + ".target": "align-content: flex-start;", + "#target": "align-content: flex-end;" + }, + attributes: { + class: "target" + }, + expectedKeyframes: [ + { computedOffset: 0, alignContent: "space-between" }, + { computedOffset: 0.5, alignContent: "center" }, + { computedOffset: 1, alignContent: "space-between" } + ], + explanation: "Test for no 0%/100% keyframes " + + "and specified style on target element " + + "using important type selector that overrides other rules" + }, +]; + +discreteTests.forEach(testcase => { + test(t => { + addStyle(t, testcase.stylesheet); + + const div = addDiv(t, { "id": "target" }); + if (testcase.attributes) { + for (let attributeName in testcase.attributes) { + div.setAttribute(attributeName, testcase.attributes[attributeName]); + } + } + div.style.animation = "keyframes 100s"; + + const keyframes = div.getAnimations()[0].effect.getKeyframes(); + const expectedKeyframes = testcase.expectedKeyframes; + assert_equals(keyframes.length, expectedKeyframes.length, + `keyframes.length should be ${ expectedKeyframes.length }`); + + keyframes.forEach((keyframe, index) => { + const expectedKeyframe = expectedKeyframes[index]; + assert_equals(keyframe.computedOffset, expectedKeyframe.computedOffset, + `computedOffset of keyframes[${ index }] should be ` + + `${ expectedKeyframe.computedOffset }`); + assert_equals(keyframe.alignContent, expectedKeyframe.alignContent, + `alignContent of keyframes[${ index }] should be ` + + `${ expectedKeyframe.alignContent }`); + }); + }, testcase.explanation); +}); + +done(); +</script> +</body> diff --git a/dom/animation/test/mozilla/test_cubic_bezier_limits.html b/dom/animation/test/mozilla/test_cubic_bezier_limits.html new file mode 100644 index 0000000000..e67e5dbbb0 --- /dev/null +++ b/dom/animation/test/mozilla/test_cubic_bezier_limits.html @@ -0,0 +1,14 @@ +<!doctype html> +<meta charset=utf-8> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<div id="log"></div> +<script> +'use strict'; +setup({explicit_done: true}); +SpecialPowers.pushPrefEnv( + { "set": [["dom.animations-api.core.enabled", true]]}, + function() { + window.open("file_cubic_bezier_limits.html"); + }); +</script> diff --git a/dom/animation/test/mozilla/test_deferred_start.html b/dom/animation/test/mozilla/test_deferred_start.html new file mode 100644 index 0000000000..4db4bf6766 --- /dev/null +++ b/dom/animation/test/mozilla/test_deferred_start.html @@ -0,0 +1,14 @@ +<!doctype html> +<meta charset=utf-8> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<div id="log"></div> +<script> +'use strict'; +setup({explicit_done: true}); +SpecialPowers.pushPrefEnv( + { "set": [["dom.animations-api.core.enabled", true]]}, + function() { + window.open("file_deferred_start.html"); + }); +</script> diff --git a/dom/animation/test/mozilla/test_disable_animations_api_core.html b/dom/animation/test/mozilla/test_disable_animations_api_core.html new file mode 100644 index 0000000000..cfb64e5371 --- /dev/null +++ b/dom/animation/test/mozilla/test_disable_animations_api_core.html @@ -0,0 +1,14 @@ +<!doctype html> +<meta charset=utf-8> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<div id="log"></div> +<script> +'use strict'; +setup({explicit_done: true}); +SpecialPowers.pushPrefEnv( + { "set": [["dom.animations-api.core.enabled", false]]}, + function() { + window.open("file_disable_animations_api_core.html"); + }); +</script> diff --git a/dom/animation/test/mozilla/test_disabled_properties.html b/dom/animation/test/mozilla/test_disabled_properties.html new file mode 100644 index 0000000000..86d02e6b69 --- /dev/null +++ b/dom/animation/test/mozilla/test_disabled_properties.html @@ -0,0 +1,14 @@ +<!doctype html> +<meta charset=utf-8> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<div id="log"></div> +<script> +'use strict'; +setup({explicit_done: true}); +SpecialPowers.pushPrefEnv( + { "set": [["dom.animations-api.core.enabled", true]]}, + function() { + window.open("file_disabled_properties.html"); + }); +</script> diff --git a/dom/animation/test/mozilla/test_discrete-animations.html b/dom/animation/test/mozilla/test_discrete-animations.html new file mode 100644 index 0000000000..2a36bd50e0 --- /dev/null +++ b/dom/animation/test/mozilla/test_discrete-animations.html @@ -0,0 +1,18 @@ +<!doctype html> +<meta charset=utf-8> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<div id="log"></div> +<script> +'use strict'; +setup({explicit_done: true}); +SpecialPowers.pushPrefEnv( + { "set": [ + ["dom.animations-api.core.enabled", true], + ["layout.css.osx-font-smoothing.enabled", true], + ["layout.css.prefixes.webkit", true] + ] }, + function() { + window.open("file_discrete-animations.html"); + }); +</script> diff --git a/dom/animation/test/mozilla/test_document-timeline-origin-time-range.html b/dom/animation/test/mozilla/test_document-timeline-origin-time-range.html new file mode 100644 index 0000000000..f73c233d39 --- /dev/null +++ b/dom/animation/test/mozilla/test_document-timeline-origin-time-range.html @@ -0,0 +1,14 @@ +<!doctype html> +<meta charset=utf-8> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<div id="log"></div> +<script> +'use strict'; +setup({explicit_done: true}); +SpecialPowers.pushPrefEnv( + { "set": [["dom.animations-api.core.enabled", true]]}, + function() { + window.open("file_document-timeline-origin-time-range.html"); + }); +</script> diff --git a/dom/animation/test/mozilla/test_hide_and_show.html b/dom/animation/test/mozilla/test_hide_and_show.html new file mode 100644 index 0000000000..929a31bd47 --- /dev/null +++ b/dom/animation/test/mozilla/test_hide_and_show.html @@ -0,0 +1,14 @@ +<!doctype html> +<meta charset=utf-8> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<div id="log"></div> +<script> +'use strict'; +setup({explicit_done: true}); +SpecialPowers.pushPrefEnv( + { "set": [["dom.animations-api.core.enabled", true]]}, + function() { + window.open("file_hide_and_show.html"); + }); +</script> diff --git a/dom/animation/test/mozilla/test_partial_keyframes.html b/dom/animation/test/mozilla/test_partial_keyframes.html new file mode 100644 index 0000000000..28eb4c5881 --- /dev/null +++ b/dom/animation/test/mozilla/test_partial_keyframes.html @@ -0,0 +1,14 @@ +<!doctype html> +<meta charset=utf-8> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<div id="log"></div> +<script> +'use strict'; +setup({explicit_done: true}); +SpecialPowers.pushPrefEnv( + { "set": [["dom.animations-api.core.enabled", true]]}, + function() { + window.open("file_partial_keyframes.html"); + }); +</script> diff --git a/dom/animation/test/mozilla/test_set-easing.html b/dom/animation/test/mozilla/test_set-easing.html new file mode 100644 index 0000000000..e0069ff1c8 --- /dev/null +++ b/dom/animation/test/mozilla/test_set-easing.html @@ -0,0 +1,14 @@ +<!doctype html> +<meta charset=utf-8> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<div id="log"></div> +<script> +'use strict'; +setup({explicit_done: true}); +SpecialPowers.pushPrefEnv( + { "set": [["dom.animations-api.core.enabled", true]]}, + function() { + window.open("file_set-easing.html"); + }); +</script> diff --git a/dom/animation/test/mozilla/test_spacing_property_order.html b/dom/animation/test/mozilla/test_spacing_property_order.html new file mode 100644 index 0000000000..afcc12bedf --- /dev/null +++ b/dom/animation/test/mozilla/test_spacing_property_order.html @@ -0,0 +1,14 @@ +<!doctype html> +<meta charset=utf-8> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<div id="log"></div> +<script> +'use strict'; +setup({explicit_done: true}); +SpecialPowers.pushPrefEnv( + { "set": [["dom.animations-api.core.enabled", true]]}, + function() { + window.open("file_spacing_property_order.html"); + }); +</script> diff --git a/dom/animation/test/mozilla/test_spacing_transform.html b/dom/animation/test/mozilla/test_spacing_transform.html new file mode 100644 index 0000000000..38dce7e99c --- /dev/null +++ b/dom/animation/test/mozilla/test_spacing_transform.html @@ -0,0 +1,14 @@ +<!doctype html> +<meta charset=utf-8> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<div id="log"></div> +<script> +'use strict'; +setup({explicit_done: true}); +SpecialPowers.pushPrefEnv( + { "set": [["dom.animations-api.core.enabled", true]]}, + function() { + window.open("file_spacing_transform.html"); + }); +</script> diff --git a/dom/animation/test/mozilla/test_transform_limits.html b/dom/animation/test/mozilla/test_transform_limits.html new file mode 100644 index 0000000000..6c9b5e4fa9 --- /dev/null +++ b/dom/animation/test/mozilla/test_transform_limits.html @@ -0,0 +1,14 @@ +<!doctype html> +<meta charset=utf-8> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<div id="log"></div> +<script> +'use strict'; +setup({explicit_done: true}); +SpecialPowers.pushPrefEnv( + { "set": [["dom.animations-api.core.enabled", true]]}, + function() { + window.open("file_transform_limits.html"); + }); +</script> diff --git a/dom/animation/test/mozilla/test_transition_finish_on_compositor.html b/dom/animation/test/mozilla/test_transition_finish_on_compositor.html new file mode 100644 index 0000000000..357e5297e9 --- /dev/null +++ b/dom/animation/test/mozilla/test_transition_finish_on_compositor.html @@ -0,0 +1,14 @@ +<!doctype html> +<meta charset=utf-8> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<div id="log"></div> +<script> +'use strict'; +setup({explicit_done: true}); +SpecialPowers.pushPrefEnv( + { "set": [["dom.animations-api.core.enabled", true]]}, + function() { + window.open("file_transition_finish_on_compositor.html"); + }); +</script> diff --git a/dom/animation/test/mozilla/test_underlying-discrete-value.html b/dom/animation/test/mozilla/test_underlying-discrete-value.html new file mode 100644 index 0000000000..7feee53a15 --- /dev/null +++ b/dom/animation/test/mozilla/test_underlying-discrete-value.html @@ -0,0 +1,15 @@ +<!doctype html> +<meta charset=utf-8> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<div id="log"></div> +<script> +'use strict'; +setup({explicit_done: true}); +SpecialPowers.pushPrefEnv( + { "set": [["dom.animations-api.core.enabled", true]]}, + function() { + window.open("file_underlying-discrete-value.html"); + }); +</script> +</html> diff --git a/dom/animation/test/style/file_animation-seeking-with-current-time.html b/dom/animation/test/style/file_animation-seeking-with-current-time.html new file mode 100644 index 0000000000..c3a5903948 --- /dev/null +++ b/dom/animation/test/style/file_animation-seeking-with-current-time.html @@ -0,0 +1,121 @@ +<!doctype html> +<html> + <head> + <meta charset=utf-8> + <title>Tests for seeking using Animation.currentTime</title> + <style> +.animated-div { + margin-left: -10px; + animation-timing-function: linear ! important; +} + +@keyframes anim { + from { margin-left: 0px; } + to { margin-left: 100px; } +} + </style> + <script src="../testcommon.js"></script> + </head> + <body> + <script type="text/javascript"> +'use strict'; + +function assert_marginLeft_equals(target, expect, description) { + var marginLeft = parseFloat(getComputedStyle(target).marginLeft); + assert_equals(marginLeft, expect, description); +} + +promise_test(function(t) { + var div = addDiv(t, {'class': 'animated-div'}); + div.style.animation = "anim 100s"; + var animation = div.getAnimations()[0]; + + return animation.ready.then(function() { + animation.currentTime = 90 * MS_PER_SEC; + assert_marginLeft_equals(div, 90, + 'Computed style is updated when seeking forwards in active interval'); + + animation.currentTime = 10 * MS_PER_SEC; + assert_marginLeft_equals(div, 10, + 'Computed style is updated when seeking backwards in active interval'); + }); +}, 'Seeking forwards and backward in active interval'); + +promise_test(function(t) { + var div = addDiv(t, {'class': 'animated-div'}); + div.style.animation = "anim 100s 100s"; + var animation = div.getAnimations()[0]; + + return animation.ready.then(function(t) { + assert_marginLeft_equals(div, -10, + 'Computed style is unaffected in before phase with no backwards fill'); + + // before -> active (non-active -> active) + animation.currentTime = 150 * MS_PER_SEC; + assert_marginLeft_equals(div, 50, + 'Computed style is updated when seeking forwards from ' + + 'not \'in effect\' to \'in effect\' state'); + }); +}, 'Seeking to non-\'in effect\' from \'in effect\' (before -> active)'); + +promise_test(function(t) { + var div = addDiv(t, {'class': 'animated-div'}); + div.style.animation = "anim 100s 100s"; + var animation = div.getAnimations()[0]; + + return animation.ready.then(function(t) { + // move to after phase + animation.currentTime = 250 * MS_PER_SEC; + assert_marginLeft_equals(div, -10, + 'Computed style is unaffected in after phase with no forwards fill'); + + // after -> active (non-active -> active) + animation.currentTime = 150 * MS_PER_SEC; + assert_marginLeft_equals(div, 50, + 'Computed style is updated when seeking backwards from ' + + 'not \'in effect\' to \'in effect\' state'); + }); +}, 'Seeking to non-\'in effect\' from \'in effect\' (after -> active)'); + +promise_test(function(t) { + var div = addDiv(t, {'class': 'animated-div'}); + div.style.animation = "anim 100s 100s"; + var animation = div.getAnimations()[0]; + + return animation.ready.then(function(t) { + // move to active phase + animation.currentTime = 150 * MS_PER_SEC; + assert_marginLeft_equals(div, 50, + 'Computed value is set during active phase'); + + // active -> before + animation.currentTime = 50 * MS_PER_SEC; + assert_marginLeft_equals(div, -10, + 'Computed value is not effected after seeking backwards from ' + + '\'in effect\' to not \'in effect\' state'); + }); +}, 'Seeking to \'in effect\' from non-\'in effect\' (active -> before)'); + +promise_test(function(t) { + var div = addDiv(t, {'class': 'animated-div'}); + div.style.animation = "anim 100s 100s"; + var animation = div.getAnimations()[0]; + + return animation.ready.then(function(t) { + // move to active phase + animation.currentTime = 150 * MS_PER_SEC; + assert_marginLeft_equals(div, 50, + 'Computed value is set during active phase'); + + // active -> after + animation.currentTime = 250 * MS_PER_SEC; + assert_marginLeft_equals(div, -10, + 'Computed value is not affected after seeking forwards from ' + + '\'in effect\' to not \'in effect\' state'); + }); +}, 'Seeking to \'in effect\' from non-\'in effect\' (active -> after)'); + +done(); + </script> + </body> +</html> diff --git a/dom/animation/test/style/file_animation-seeking-with-start-time.html b/dom/animation/test/style/file_animation-seeking-with-start-time.html new file mode 100644 index 0000000000..ba09827c63 --- /dev/null +++ b/dom/animation/test/style/file_animation-seeking-with-start-time.html @@ -0,0 +1,121 @@ +<!doctype html> +<html> + <head> + <meta charset=utf-8> + <title>Tests for seeking using Animation.startTime</title> + <style> +.animated-div { + margin-left: -10px; + animation-timing-function: linear ! important; +} + +@keyframes anim { + from { margin-left: 0px; } + to { margin-left: 100px; } +} + </style> + <script src="../testcommon.js"></script> + </head> + <body> + <script type="text/javascript"> +'use strict'; + +function assert_marginLeft_equals(target, expect, description) { + var marginLeft = parseFloat(getComputedStyle(target).marginLeft); + assert_equals(marginLeft, expect, description); +} + +promise_test(function(t) { + var div = addDiv(t, {'class': 'animated-div'}); + div.style.animation = "anim 100s"; + var animation = div.getAnimations()[0]; + + return animation.ready.then(function() { + animation.startTime = animation.timeline.currentTime - 90 * MS_PER_SEC + assert_marginLeft_equals(div, 90, + 'Computed style is updated when seeking forwards in active interval'); + + animation.startTime = animation.timeline.currentTime - 10 * MS_PER_SEC; + assert_marginLeft_equals(div, 10, + 'Computed style is updated when seeking backwards in active interval'); + }); +}, 'Seeking forwards and backward in active interval'); + +promise_test(function(t) { + var div = addDiv(t, {'class': 'animated-div'}); + div.style.animation = "anim 100s 100s"; + var animation = div.getAnimations()[0]; + + return animation.ready.then(function(t) { + assert_marginLeft_equals(div, -10, + 'Computed style is unaffected in before phase with no backwards fill'); + + // before -> active (non-active -> active) + animation.startTime = animation.timeline.currentTime - 150 * MS_PER_SEC; + assert_marginLeft_equals(div, 50, + 'Computed style is updated when seeking forwards from ' + + 'not \'in effect\' to \'in effect\' state'); + }); +}, 'Seeking to non-\'in effect\' from \'in effect\' (before -> active)'); + +promise_test(function(t) { + var div = addDiv(t, {'class': 'animated-div'}); + div.style.animation = "anim 100s 100s"; + var animation = div.getAnimations()[0]; + + return animation.ready.then(function(t) { + // move to after phase + animation.startTime = animation.timeline.currentTime - 250 * MS_PER_SEC; + assert_marginLeft_equals(div, -10, + 'Computed style is unaffected in after phase with no forwards fill'); + + // after -> active (non-active -> active) + animation.startTime = animation.timeline.currentTime - 150 * MS_PER_SEC; + assert_marginLeft_equals(div, 50, + 'Computed style is updated when seeking backwards from ' + + 'not \'in effect\' to \'in effect\' state'); + }); +}, 'Seeking to non-\'in effect\' from \'in effect\' (after -> active)'); + +promise_test(function(t) { + var div = addDiv(t, {'class': 'animated-div'}); + div.style.animation = "anim 100s 100s"; + var animation = div.getAnimations()[0]; + + return animation.ready.then(function(t) { + // move to active phase + animation.startTime = animation.timeline.currentTime - 150 * MS_PER_SEC; + assert_marginLeft_equals(div, 50, + 'Computed value is set during active phase'); + + // active -> before + animation.startTime = animation.timeline.currentTime - 50 * MS_PER_SEC; + assert_marginLeft_equals(div, -10, + 'Computed value is not affected after seeking backwards from ' + + '\'in effect\' to not \'in effect\' state'); + }); +}, 'Seeking to \'in effect\' from non-\'in effect\' (active -> before)'); + +promise_test(function(t) { + var div = addDiv(t, {'class': 'animated-div'}); + div.style.animation = "anim 100s 100s"; + var animation = div.getAnimations()[0]; + + return animation.ready.then(function(t) { + // move to active phase + animation.startTime = animation.timeline.currentTime - 150 * MS_PER_SEC; + assert_marginLeft_equals(div, 50, + 'Computed value is set during active phase'); + + // active -> after + animation.startTime = animation.timeline.currentTime - 250 * MS_PER_SEC; + assert_marginLeft_equals(div, -10, + 'Computed value is not affected after seeking forwards from ' + + '\'in effect\' to not \'in effect\' state'); + }); +}, 'Seeking to \'in effect\' from non-\'in effect\' (active -> after)'); + +done(); + </script> + </body> +</html> diff --git a/dom/animation/test/style/file_animation-setting-effect.html b/dom/animation/test/style/file_animation-setting-effect.html new file mode 100644 index 0000000000..cf50cf2cee --- /dev/null +++ b/dom/animation/test/style/file_animation-setting-effect.html @@ -0,0 +1,125 @@ +<!doctype html> +<html> + <head> + <meta charset=utf-8> + <title>Tests for setting effects by using Animation.effect</title> + <script src='../testcommon.js'></script> + </head> + <body> + <script type='text/javascript'> + +'use strict'; + +test(function(t) { + var target = addDiv(t); + var anim = new Animation(); + anim.effect = new KeyframeEffectReadOnly(target, + { marginLeft: [ '0px', '100px' ] }, + 100 * MS_PER_SEC); + anim.currentTime = 50 * MS_PER_SEC; + assert_equals(getComputedStyle(target).marginLeft, '50px'); +}, 'After setting target effect on an animation with null effect, the ' + + 'animation still works'); + +test(function(t) { + var target = addDiv(t); + target.style.marginLeft = '10px'; + var anim = target.animate({ marginLeft: [ '0px', '100px' ] }, + 100 * MS_PER_SEC); + anim.currentTime = 50 * MS_PER_SEC; + assert_equals(getComputedStyle(target).marginLeft, '50px'); + + anim.effect = null; + assert_equals(getComputedStyle(target).marginLeft, '10px'); +}, 'After setting null target effect, the computed style of the target ' + + 'element becomes the initial value'); + +test(function(t) { + var target = addDiv(t); + var animA = target.animate({ marginLeft: [ '0px', '100px' ] }, + 100 * MS_PER_SEC); + var animB = new Animation(); + animA.currentTime = 50 * MS_PER_SEC; + animB.currentTime = 20 * MS_PER_SEC; + assert_equals(getComputedStyle(target).marginLeft, '50px', + 'original computed style of the target element'); + + animB.effect = animA.effect; + assert_equals(getComputedStyle(target).marginLeft, '20px', + 'new computed style of the target element'); +}, 'After setting the target effect from an existing animation, the computed ' + + 'style of the target effect should reflect the time of the updated ' + + 'animation.'); + +test(function(t) { + var target = addDiv(t); + target.style.marginTop = '-10px'; + var animA = target.animate({ marginLeft: [ '0px', '100px' ] }, + 100 * MS_PER_SEC); + var animB = target.animate({ marginTop: [ '0px', '100px' ] }, + 50 * MS_PER_SEC); + animA.currentTime = 50 * MS_PER_SEC; + animB.currentTime = 10 * MS_PER_SEC; + assert_equals(getComputedStyle(target).marginLeft, '50px', + 'original margin-left of the target element'); + assert_equals(getComputedStyle(target).marginTop, '20px', + 'original margin-top of the target element'); + + animB.effect = animA.effect; + assert_equals(getComputedStyle(target).marginLeft, '10px', + 'new margin-left of the target element'); + assert_equals(getComputedStyle(target).marginTop, '-10px', + 'new margin-top of the target element'); +}, 'After setting target effect with an animation to another animation which ' + + 'also has an target effect and both animation effects target to the same ' + + 'element, the computed style of this element should reflect the time and ' + + 'effect of the animation that was set'); + +test(function(t) { + var targetA = addDiv(t); + var targetB = addDiv(t); + targetB.style.marginLeft = '-10px'; + var animA = targetA.animate({ marginLeft: [ '0px', '100px' ] }, + 100 * MS_PER_SEC); + var animB = targetB.animate({ marginLeft: [ '0px', '100px' ] }, + 50 * MS_PER_SEC); + animA.currentTime = 50 * MS_PER_SEC; + animB.currentTime = 10 * MS_PER_SEC; + assert_equals(getComputedStyle(targetA).marginLeft, '50px', + 'original margin-left of the first element'); + assert_equals(getComputedStyle(targetB).marginLeft, '20px', + 'original margin-left of the second element'); + + animB.effect = animA.effect; + assert_equals(getComputedStyle(targetA).marginLeft, '10px', + 'new margin-left of the first element'); + assert_equals(getComputedStyle(targetB).marginLeft, '-10px', + 'new margin-left of the second element'); +}, 'After setting target effect with an animation to another animation which ' + + 'also has an target effect and these animation effects target to ' + + 'different elements, the computed styles of the two elements should ' + + 'reflect the time and effect of the animation that was set'); + +test(function(t) { + var target = addDiv(t); + var animA = target.animate({ marginLeft: [ '0px', '100px' ] }, + 50 * MS_PER_SEC); + var animB = target.animate({ marginTop: [ '0px', '50px' ] }, + 100 * MS_PER_SEC); + animA.currentTime = 20 * MS_PER_SEC; + animB.currentTime = 30 * MS_PER_SEC; + assert_equals(getComputedStyle(target).marginLeft, '40px'); + assert_equals(getComputedStyle(target).marginTop, '15px'); + + var effectA = animA.effect; + animA.effect = animB.effect; + animB.effect = effectA; + assert_equals(getComputedStyle(target).marginLeft, '60px'); + assert_equals(getComputedStyle(target).marginTop, '10px'); +}, 'After swapping effects of two playing animations, both animations are ' + + 'still running with the same current time'); + +done(); + </script> + </body> +</html> diff --git a/dom/animation/test/style/file_animation-setting-spacing.html b/dom/animation/test/style/file_animation-setting-spacing.html new file mode 100644 index 0000000000..6098b74336 --- /dev/null +++ b/dom/animation/test/style/file_animation-setting-spacing.html @@ -0,0 +1,111 @@ +<!doctype html> +<head> +<meta charset=utf-8> +<title>Tests for setting spacing by using KeyframeEffect.spacing</title> +<script src='../testcommon.js'></script> +</head> +<body> +<script> +'use strict'; + +function calculateInterpolation(pacedDistances, values, progress) { + if (progress == 0.0) { + return values[0]; + } else if (progress == 1.0) { + return values[valus.length - 1]; + } + + const cumDist = pacedDistances.reduce( (prev, curr) => { + prev.push(prev.length == 0 ? curr : curr + prev[prev.length - 1]); + return prev; + }, []); + + const last = cumDist[cumDist.length - 1]; + const offsets = cumDist.map( (curr) => { return curr / last; } ); + + let idx = 0; + for (let i = 0; i < offsets.length - 1; ++i) { + if (progress >= offsets[i] && progress < offsets[i + 1]) { + idx = i; + break; + } + } + + const ratio = (progress - offsets[idx]) / (offsets[idx + 1] - offsets[idx]); + return values[idx] + ratio * (values[idx + 1] - values[idx]) + 'px'; +} + +promise_test(function(t) { + var target = addDiv(t); + var anim = target.animate([ { marginLeft: '0px' }, + { marginLeft: '-20px' }, + { marginLeft: '100px' }, + { marginLeft: '50px' } ], + 100 * MS_PER_SEC); + + return anim.ready.then(function() { + anim.currentTime = 50 * MS_PER_SEC; + assert_equals(getComputedStyle(target).marginLeft, '40px', + 'computed value before setting a new spacing'); + + var dist = [0, 20, 120, 50]; + var marginLeftValues = [0, -20, 100, 50]; + anim.effect.spacing = 'paced(margin-left)'; + assert_equals(getComputedStyle(target).marginLeft, + calculateInterpolation(dist, marginLeftValues, 0.5), + 'computed value after setting a new spacing'); + }); +}, 'Test for setting spacing from distribute to paced'); + +promise_test(function(t) { + var target = addDiv(t); + var anim = target.animate([ { marginLeft: '0px' }, + { marginLeft: '-20px' }, + { marginLeft: '100px' }, + { marginLeft: '50px' } ], + { duration: 100 * MS_PER_SEC, + spacing: 'paced(margin-left)' }); + + return anim.ready.then(function() { + var dist = [0, 20, 120, 50]; + var marginLeftValues = [0, -20, 100, 50]; + anim.currentTime = 50 * MS_PER_SEC; + assert_equals(getComputedStyle(target).marginLeft, + calculateInterpolation(dist, marginLeftValues, 0.5), + 'computed value before setting a new spacing'); + + anim.effect.spacing = 'distribute'; + assert_equals(getComputedStyle(target).marginLeft, '40px', + 'computed value after setting a new spacing'); + }); +}, 'Test for setting spacing from paced to distribute'); + +promise_test(function(t) { + var target = addDiv(t); + var anim = + target.animate([ { marginLeft: '0px', borderRadius: '0%' }, + { marginLeft: '-20px', borderRadius: '50%' }, + { marginLeft: '100px', borderRadius: '25%' }, + { marginLeft: '50px', borderRadius: '100%' } ], + { duration: 100 * MS_PER_SEC, + spacing: 'paced(margin-left)' }); + + return anim.ready.then(function() { + var dist = [0, 20, 120, 50]; + var marginLeftValues = [0, -20, 100, 50]; + anim.currentTime = 50 * MS_PER_SEC; + assert_equals(getComputedStyle(target).marginLeft, + calculateInterpolation(dist, marginLeftValues, 0.5), + 'computed value before setting a new spacing'); + + dist = [0, 50, 25, 75]; + anim.effect.spacing = 'paced(border-radius)'; + assert_equals(getComputedStyle(target).marginLeft, + calculateInterpolation(dist, marginLeftValues, 0.5), + 'computed value after setting a new spacing'); + }); +}, 'Test for setting spacing from paced to a different paced'); + +done(); +</script> +</body> diff --git a/dom/animation/test/style/test_animation-seeking-with-current-time.html b/dom/animation/test/style/test_animation-seeking-with-current-time.html new file mode 100644 index 0000000000..386e577886 --- /dev/null +++ b/dom/animation/test/style/test_animation-seeking-with-current-time.html @@ -0,0 +1,15 @@ +<!doctype html> +<meta charset=utf-8> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<div id="log"></div> +<script> +'use strict'; +setup({explicit_done: true}); +SpecialPowers.pushPrefEnv( + { "set": [["dom.animations-api.core.enabled", true]]}, + function() { + window.open("file_animation-seeking-with-current-time.html"); + }); +</script> +</html> diff --git a/dom/animation/test/style/test_animation-seeking-with-start-time.html b/dom/animation/test/style/test_animation-seeking-with-start-time.html new file mode 100644 index 0000000000..0a1691a082 --- /dev/null +++ b/dom/animation/test/style/test_animation-seeking-with-start-time.html @@ -0,0 +1,15 @@ +<!doctype html> +<meta charset=utf-8> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<div id="log"></div> +<script> +'use strict'; +setup({explicit_done: true}); +SpecialPowers.pushPrefEnv( + { "set": [["dom.animations-api.core.enabled", true]]}, + function() { + window.open("file_animation-seeking-with-start-time.html"); + }); +</script> +</html> diff --git a/dom/animation/test/style/test_animation-setting-effect.html b/dom/animation/test/style/test_animation-setting-effect.html new file mode 100644 index 0000000000..1199b3e752 --- /dev/null +++ b/dom/animation/test/style/test_animation-setting-effect.html @@ -0,0 +1,15 @@ +<!doctype html> +<meta charset=utf-8> +<script src='/resources/testharness.js'></script> +<script src='/resources/testharnessreport.js'></script> +<div id='log'></div> +<script> +'use strict'; +setup({explicit_done: true}); +SpecialPowers.pushPrefEnv( + { 'set': [['dom.animations-api.core.enabled', true]]}, + function() { + window.open('file_animation-setting-effect.html'); + }); +</script> +</html> diff --git a/dom/animation/test/style/test_animation-setting-spacing.html b/dom/animation/test/style/test_animation-setting-spacing.html new file mode 100644 index 0000000000..1c703e2a3c --- /dev/null +++ b/dom/animation/test/style/test_animation-setting-spacing.html @@ -0,0 +1,14 @@ +<!doctype html> +<meta charset=utf-8> +<script src='/resources/testharness.js'></script> +<script src='/resources/testharnessreport.js'></script> +<div id='log'></div> +<script> +'use strict'; +setup({explicit_done: true}); +SpecialPowers.pushPrefEnv( + { 'set': [['dom.animations-api.core.enabled', true]]}, + function() { + window.open('file_animation-setting-spacing.html'); + }); +</script> diff --git a/dom/animation/test/testcommon.js b/dom/animation/test/testcommon.js new file mode 100644 index 0000000000..b9001e4f42 --- /dev/null +++ b/dom/animation/test/testcommon.js @@ -0,0 +1,216 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Use this variable if you specify duration or some other properties + * for script animation. + * E.g., div.animate({ opacity: [0, 1] }, 100 * MS_PER_SEC); + * + * NOTE: Creating animations with short duration may cause intermittent + * failures in asynchronous test. For example, the short duration animation + * might be finished when animation.ready has been fulfilled because of slow + * platforms or busyness of the main thread. + * Setting short duration to cancel its animation does not matter but + * if you don't want to cancel the animation, consider using longer duration. + */ +const MS_PER_SEC = 1000; + +/* The recommended minimum precision to use for time values[1]. + * + * [1] https://w3c.github.io/web-animations/#precision-of-time-values + */ +var TIME_PRECISION = 0.0005; // ms + +/* + * Allow implementations to substitute an alternative method for comparing + * times based on their precision requirements. + */ +function assert_times_equal(actual, expected, description) { + assert_approx_equals(actual, expected, TIME_PRECISION, description); +} + +/** + * Appends a div to the document body and creates an animation on the div. + * NOTE: This function asserts when trying to create animations with durations + * shorter than 100s because the shorter duration may cause intermittent + * failures. If you are not sure how long it is suitable, use 100s; it's + * long enough but shorter than our test framework timeout (330s). + * If you really need to use shorter durations, use animate() function directly. + * + * @param t The testharness.js Test object. If provided, this will be used + * to register a cleanup callback to remove the div when the test + * finishes. + * @param attrs A dictionary object with attribute names and values to set on + * the div. + * @param frames The keyframes passed to Element.animate(). + * @param options The options passed to Element.animate(). + */ +function addDivAndAnimate(t, attrs, frames, options) { + let animDur = (typeof options === 'object') ? + options.duration : options; + assert_greater_than_equal(animDur, 100 * MS_PER_SEC, + 'Clients of this addDivAndAnimate API must request a duration ' + + 'of at least 100s, to avoid intermittent failures from e.g.' + + 'the main thread being busy for an extended period'); + + return addDiv(t, attrs).animate(frames, options); +} + +/** + * Appends a div to the document body. + * + * @param t The testharness.js Test object. If provided, this will be used + * to register a cleanup callback to remove the div when the test + * finishes. + * + * @param attrs A dictionary object with attribute names and values to set on + * the div. + */ +function addDiv(t, attrs) { + var div = document.createElement('div'); + if (attrs) { + for (var attrName in attrs) { + div.setAttribute(attrName, attrs[attrName]); + } + } + document.body.appendChild(div); + if (t && typeof t.add_cleanup === 'function') { + t.add_cleanup(function() { + if (div.parentNode) { + div.parentNode.removeChild(div); + } + }); + } + return div; +} + +/** + * Appends a style div to the document head. + * + * @param t The testharness.js Test object. If provided, this will be used + * to register a cleanup callback to remove the style element + * when the test finishes. + * + * @param rules A dictionary object with selector names and rules to set on + * the style sheet. + */ +function addStyle(t, rules) { + var extraStyle = document.createElement('style'); + document.head.appendChild(extraStyle); + if (rules) { + var sheet = extraStyle.sheet; + for (var selector in rules) { + sheet.insertRule(selector + '{' + rules[selector] + '}', + sheet.cssRules.length); + } + } + + if (t && typeof t.add_cleanup === 'function') { + t.add_cleanup(function() { + extraStyle.remove(); + }); + } +} + +/** + * Promise wrapper for requestAnimationFrame. + */ +function waitForFrame() { + return new Promise(function(resolve, reject) { + window.requestAnimationFrame(resolve); + }); +} + +/** + * Returns a Promise that is resolved after the given number of consecutive + * animation frames have occured (using requestAnimationFrame callbacks). + * + * @param frameCount The number of animation frames. + * @param onFrame An optional function to be processed in each animation frame. + */ +function waitForAnimationFrames(frameCount, onFrame) { + return new Promise(function(resolve, reject) { + function handleFrame() { + if (onFrame && typeof onFrame === 'function') { + onFrame(); + } + if (--frameCount <= 0) { + resolve(); + } else { + window.requestAnimationFrame(handleFrame); // wait another frame + } + } + window.requestAnimationFrame(handleFrame); + }); +} + +/** + * Wrapper that takes a sequence of N animations and returns: + * + * Promise.all([animations[0].ready, animations[1].ready, ... animations[N-1].ready]); + */ +function waitForAllAnimations(animations) { + return Promise.all(animations.map(function(animation) { + return animation.ready; + })); +} + +/** + * Flush the computed style for the given element. This is useful, for example, + * when we are testing a transition and need the initial value of a property + * to be computed so that when we synchronouslyet set it to a different value + * we actually get a transition instead of that being the initial value. + */ +function flushComputedStyle(elem) { + var cs = window.getComputedStyle(elem); + cs.marginLeft; +} + +if (opener) { + for (var funcName of ["async_test", "assert_not_equals", "assert_equals", + "assert_approx_equals", "assert_less_than", + "assert_less_than_equal", "assert_greater_than", + "assert_between_inclusive", + "assert_true", "assert_false", + "assert_class_string", "assert_throws", + "assert_unreached", "promise_test", "test"]) { + window[funcName] = opener[funcName].bind(opener); + } + + window.EventWatcher = opener.EventWatcher; + + function done() { + opener.add_completion_callback(function() { + self.close(); + }); + opener.done(); + } +} + +/** + * Return a new MutaionObserver which started observing |target| element + * with { animations: true, subtree: |subtree| } option. + * NOTE: This observer should be used only with takeRecords(). If any of + * MutationRecords are observed in the callback of the MutationObserver, + * it will raise an assertion. + */ +function setupSynchronousObserver(t, target, subtree) { + var observer = new MutationObserver(records => { + assert_unreached("Any MutationRecords should not be observed in this " + + "callback"); + }); + t.add_cleanup(() => { + observer.disconnect(); + }); + observer.observe(target, { animations: true, subtree: subtree }); + return observer; +} + +/** + * Returns true if off-main-thread animations. + */ +function isOMTAEnabled() { + const OMTAPrefKey = 'layers.offmainthreadcomposition.async-animations'; + return SpecialPowers.DOMWindowUtils.layerManagerRemote && + SpecialPowers.getBoolPref(OMTAPrefKey); +} |