diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
commit | 26a029d407be480d791972afb5975cf62c9360a6 (patch) | |
tree | f435a8308119effd964b339f76abb83a57c29483 /dom/animation/test | |
parent | Initial commit. (diff) | |
download | firefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz firefox-26a029d407be480d791972afb5975cf62c9360a6.zip |
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'dom/animation/test')
121 files changed, 15309 insertions, 0 deletions
diff --git a/dom/animation/test/chrome.toml b/dom/animation/test/chrome.toml new file mode 100644 index 0000000000..fa1d21457c --- /dev/null +++ b/dom/animation/test/chrome.toml @@ -0,0 +1,42 @@ +[DEFAULT] +prefs = [ + "dom.animations-api.compositing.enabled=true", + "gfx.omta.background-color=true", + "layout.css.basic-shape-rect.enabled=true", + "layout.css.basic-shape-xywh.enabled=true", + "layout.css.individual-transform.enabled=true", + "layout.css.motion-path-basic-shapes.enabled=true", + "layout.css.motion-path-coord-box.enabled=true", + "layout.css.motion-path-offset-position.enabled=true", + "layout.css.motion-path-ray.enabled=true", +] +support-files = [ + "testcommon.js", + "../../imptests/testharness.js", + "../../imptests/testharnessreport.js", + "!/dom/animation/test/chrome/file_animate_xrays.html", +] + +["chrome/test_animate_xrays.html"] +# file_animate_xrays.html needs to go in mochitest.ini since it is served +# over HTTP + +["chrome/test_animation_observers_async.html"] + +["chrome/test_animation_observers_sync.html"] + +["chrome/test_animation_performance_warning.html"] + +["chrome/test_animation_properties.html"] + +["chrome/test_animation_properties_display.html"] + +["chrome/test_cssanimation_missing_keyframes.html"] + +["chrome/test_generated_content_getAnimations.html"] + +["chrome/test_keyframe_effect_xrays.html"] + +["chrome/test_mutation_observer_for_element_removal_in_shadow_tree.html"] + +["chrome/test_running_on_compositor.html"] diff --git a/dom/animation/test/chrome/file_animate_xrays.html b/dom/animation/test/chrome/file_animate_xrays.html new file mode 100644 index 0000000000..2fa15b1764 --- /dev/null +++ b/dom/animation/test/chrome/file_animate_xrays.html @@ -0,0 +1,18 @@ +<!doctype html> +<html> +<head> +<meta charset=utf-8> +<script> +Element.prototype.animate = function() { + throw 'Called animate() as defined in content document'; +} +for (let name of ["KeyframeEffect", "Animation"]) { + this[name] = function() { + throw `Called overridden ${name} constructor`; + }; +} +</script> +<body> +<div id="target"></div> +</body> +</html> diff --git a/dom/animation/test/chrome/test_animate_xrays.html b/dom/animation/test/chrome/test_animate_xrays.html new file mode 100644 index 0000000000..64df6db720 --- /dev/null +++ b/dom/animation/test/chrome/test_animate_xrays.html @@ -0,0 +1,40 @@ +<!doctype html> +<head> +<meta charset=utf-8> +<script type="application/javascript" src="../testharness.js"></script> +<script type="application/javascript" src="../testharnessreport.js"></script> +<script type="application/javascript" src="../testcommon.js"></script> +</head> +<body> +<a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1414674" + target="_blank">Mozilla Bug 1414674</a> +<div id="log"></div> +<iframe id="iframe" + src="http://example.org/tests/dom/animation/test/chrome/file_animate_xrays.html"></iframe> +<script> +'use strict'; + +var win = document.getElementById('iframe').contentWindow; + +async_test(function(t) { + window.addEventListener('load', t.step_func(function() { + var target = win.document.getElementById('target'); + var anim = target.animate({opacity: [ 1, 0 ]}, 100 * MS_PER_SEC); + // The frames object should be accessible via x-ray. + var frames = anim.effect.getKeyframes(); + assert_equals(frames.length, 2, + "frames for Element.animate should be non-zero"); + assert_equals(frames[0].opacity, "1", + "first frame opacity for Element.animate should be specified value"); + assert_equals(frames[0].computedOffset, 0, + "first frame offset for Element.animate should be 0"); + assert_equals(frames[1].opacity, "0", + "last frame opacity for Element.animate should be specified value"); + assert_equals(frames[1].computedOffset, 1, + "last frame offset for Element.animate should be 1"); + t.done(); + })); +}, 'Calling animate() across x-rays'); + +</script> +</body> diff --git a/dom/animation/test/chrome/test_animation_observers_async.html b/dom/animation/test/chrome/test_animation_observers_async.html new file mode 100644 index 0000000000..912d73a896 --- /dev/null +++ b/dom/animation/test/chrome/test_animation_observers_async.html @@ -0,0 +1,654 @@ +<!DOCTYPE html> +<meta charset=utf-8> +<title> +Test chrome-only MutationObserver animation notifications (async tests) +</title> +<!-- + + This file contains tests for animation mutation observers that require + some asynchronous steps (e.g. waiting for animation events). + + Where possible, however, we prefer to write synchronous tests since they are + less to timeout when run on automation. These synchronous tests are located + in test_animation_observers_sync.html. + +--> +<script type="application/javascript" src="../testharness.js"></script> +<script type="application/javascript" src="../testharnessreport.js"></script> +<script src="../testcommon.js"></script> +<div id="log"></div> +<style> +@keyframes anim { + to { transform: translate(100px); } +} +@keyframes anotherAnim { + to { transform: translate(0px); } +} +#target { + width: 100px; + height: 100px; + background-color: yellow; + line-height: 16px; +} +</style> +<div id=container><div id=target></div></div> +<script> +var div = document.getElementById("target"); +var gRecords = []; +var gObserver = new MutationObserver(newRecords => { + gRecords.push(...newRecords); +}); + +function setupAsynchronousObserver(t, options) { + + gRecords = []; + t.add_cleanup(() => { + gObserver.disconnect(); + }); + gObserver.observe(options.subtree ? div.parentNode : div, + { animations: true, subtree: options.subtree }); +} + +// Adds an event listener and returns a Promise that is resolved when the +// event listener is called. +function await_event(aElement, aEventName) { + return new Promise(aResolve => { + function listener(aEvent) { + aElement.removeEventListener(aEventName, listener); + aResolve(); + } + aElement.addEventListener(aEventName, listener); + }); +} + +function assert_record_list(actual, expected, desc, index, listName) { + assert_equals(actual.length, expected.length, + `${desc} - record[${index}].${listName} length`); + if (actual.length != expected.length) { + return; + } + for (var i = 0; i < actual.length; i++) { + assert_not_equals(actual.indexOf(expected[i]), -1, + `${desc} - record[${index}].${listName} contains expected Animation`); + } +} + +function assert_records(expected, desc) { + var records = gRecords; + gRecords = []; + assert_equals(records.length, expected.length, `${desc} - number of records`); + if (records.length != expected.length) { + return; + } + for (var i = 0; i < records.length; i++) { + assert_record_list(records[i].addedAnimations, expected[i].added, desc, i, "addedAnimations"); + assert_record_list(records[i].changedAnimations, expected[i].changed, desc, i, "changedAnimations"); + assert_record_list(records[i].removedAnimations, expected[i].removed, desc, i, "removedAnimations"); + } +} + +function assert_records_any_order(expected, desc) { + // Generate a unique label for each Animation object. + let animation_labels = new Map(); + let animation_counter = 0; + for (let record of gRecords) { + for (let a of [...record.addedAnimations, ...record.changedAnimations, ...record.removedAnimations]) { + if (!animation_labels.has(a)) { + animation_labels.set(a, ++animation_counter); + } + } + } + for (let record of expected) { + for (let a of [...record.added, ...record.changed, ...record.removed]) { + if (!animation_labels.has(a)) { + animation_labels.set(a, ++animation_counter); + } + } + } + + function record_label(record) { + // Generate a label of the form: + // + // <added-animations>:<changed-animations>:<removed-animations> + let added = record.addedAnimations || record.added; + let changed = record.changedAnimations || record.changed; + let removed = record.removedAnimations || record.removed; + return [added .map(a => animation_labels.get(a)).sort().join(), + changed.map(a => animation_labels.get(a)).sort().join(), + removed.map(a => animation_labels.get(a)).sort().join()] + .join(":"); + } + + // Sort records by their label. + gRecords.sort((a, b) => record_label(a) < record_label(b)); + expected.sort((a, b) => record_label(a) < record_label(b)); + + // Assert the sorted record lists are equal. + assert_records(expected, desc); +} + +// -- Tests ------------------------------------------------------------------ + +// We run all tests first targeting the div and observing the div, then again +// targeting the div and observing its parent while using the subtree:true +// MutationObserver option. + +function runTest() { + [ + { observe: div, target: div, subtree: false }, + { observe: div.parentNode, target: div, subtree: true }, + ].forEach(aOptions => { + + var e = aOptions.target; + + promise_test(t => { + setupAsynchronousObserver(t, aOptions); + // Clear all styles once test finished since we re-use the same element + // in all test cases. + t.add_cleanup(() => { + e.style = ""; + flushComputedStyle(e); + }); + + // Start a transition. + e.style = "transition: background-color 100s; background-color: lime;"; + + // Register for the end of the transition. + var transitionEnd = await_event(e, "transitionend"); + + // The transition should cause the creation of a single Animation. + var animations = e.getAnimations(); + assert_equals(animations.length, 1, + "getAnimations().length after transition start"); + + // Wait for the single MutationRecord for the Animation addition to + // be delivered. + return waitForFrame().then(() => { + assert_records([{ added: animations, changed: [], removed: [] }], + "records after transition start"); + + // Advance until near the end of the transition, then wait for it to + // finish. + animations[0].currentTime = 99900; + }).then(() => { + return transitionEnd; + }).then(() => { + // After the transition has finished, the Animation should disappear. + assert_equals(e.getAnimations().length, 0, + "getAnimations().length after transition end"); + + // Wait for the change MutationRecord for seeking the Animation to be + // delivered, followed by the removal MutationRecord. + return waitForFrame(); + }).then(() => { + assert_records([{ added: [], changed: animations, removed: [] }, + { added: [], changed: [], removed: animations }], + "records after transition end"); + }); + }, `single_transition ${aOptions.subtree ? ': subtree' : ''}`); + + // Test that starting a single animation that completes normally + // dispatches an added notification and then a removed notification. + promise_test(t => { + setupAsynchronousObserver(t, aOptions); + t.add_cleanup(() => { + e.style = ""; + flushComputedStyle(e); + }); + + // Start an animation. + e.style = "animation: anim 100s;"; + + // Register for the end of the animation. + var animationEnd = await_event(e, "animationend"); + + // The animation should cause the creation of a single Animation. + var animations = e.getAnimations(); + assert_equals(animations.length, 1, + "getAnimations().length after animation start"); + + // Wait for the single MutationRecord for the Animation addition to + // be delivered. + return waitForFrame().then(() => { + assert_records([{ added: animations, changed: [], removed: [] }], + "records after animation start"); + + // Advance until near the end of the animation, then wait for it to finish. + animations[0].currentTime = 99900; + return animationEnd; + }).then(() => { + // After the animation has finished, the Animation should disappear. + assert_equals(e.getAnimations().length, 0, + "getAnimations().length after animation end"); + + // Wait for the change MutationRecord from seeking the Animation to + // be delivered, followed by a further MutationRecord for the Animation + // removal. + return waitForFrame(); + }).then(() => { + assert_records([{ added: [], changed: animations, removed: [] }, + { added: [], changed: [], removed: animations }], + "records after animation end"); + }); + }, `single_animation ${aOptions.subtree ? ': subtree' : ''}`); + + // Test that starting a single animation that is cancelled by updating + // the animation-fill-mode property dispatches an added notification and + // then a removed notification. + promise_test(t => { + setupAsynchronousObserver(t, aOptions); + t.add_cleanup(() => { + e.style = ""; + flushComputedStyle(e); + }); + + // Start a short, filled animation. + e.style = "animation: anim 100s forwards;"; + + // Register for the end of the animation. + var animationEnd = await_event(e, "animationend"); + + // The animation should cause the creation of a single Animation. + var animations = e.getAnimations(); + assert_equals(animations.length, 1, + "getAnimations().length after animation start"); + + // Wait for the single MutationRecord for the Animation addition to + // be delivered. + return waitForFrame().then(() => { + assert_records([{ added: animations, changed: [], removed: [] }], + "records after animation start"); + + // Advance until near the end of the animation, then wait for it to finish. + animations[0].currentTime = 99900; + return animationEnd; + }).then(() => { + // The only MutationRecord at this point should be the change from + // seeking the Animation. + assert_records([{ added: [], changed: animations, removed: [] }], + "records after animation starts filling"); + + // Cancel the animation by setting animation-fill-mode. + e.style.animationFillMode = "none"; + // Explicitly flush style to make sure the above style change happens. + // Normally we don't need explicit style flush if there is a waitForFrame() + // call but in this particular case we are in the middle of animation events' + // callback handling and requestAnimationFrame handling so that we have no + // chance to process styling even after the requestAnimationFrame handling. + flushComputedStyle(e); + + // Wait for the single MutationRecord for the Animation removal to + // be delivered. + return waitForFrame(); + }).then(() => { + assert_records([{ added: [], changed: [], removed: animations }], + "records after animation end"); + }); + }, `single_animation_cancelled_fill ${aOptions.subtree ? ': subtree' : ''}`); + + // Test that calling finish() on a paused (but otherwise finished) animation + // dispatches a changed notification. + promise_test(t => { + setupAsynchronousObserver(t, aOptions); + t.add_cleanup(() => { + e.style = ""; + flushComputedStyle(e); + }); + + // Start a long animation + e.style = "animation: anim 100s forwards"; + + // The animation should cause the creation of a single Animation. + var animations = e.getAnimations(); + assert_equals(animations.length, 1, + "getAnimations().length after animation start"); + + // Wait for the single MutationRecord for the Animation addition to + // be delivered. + return waitForFrame().then(() => { + assert_records([{ added: animations, changed: [], removed: [] }], + "records after animation start"); + + // Wait until the animation is playing. + return animations[0].ready; + }).then(() => { + // Finish and pause. + animations[0].finish(); + animations[0].pause(); + + // Wait for the pause to complete. + return animations[0].ready; + }).then(() => { + assert_true( + !animations[0].pending && animations[0].playState === "paused", + "playState after finishing and pausing"); + + // We should have two MutationRecords for the Animation changes: + // one for the finish, one for the pause. + assert_records([{ added: [], changed: animations, removed: [] }, + { added: [], changed: animations, removed: [] }], + "records after finish() and pause()"); + + // Call finish() again. + animations[0].finish(); + assert_equals(animations[0].playState, "finished", + "playState after finishing from paused state"); + + // Wait for the single MutationRecord for the Animation change to + // be delivered. Even though the currentTime does not change, the + // playState will change. + return waitForFrame(); + }).then(() => { + assert_records([{ added: [], changed: animations, removed: [] }], + "records after finish() and pause()"); + + // Cancel the animation. + e.style = ""; + + // Wait for the single removal notification. + return waitForFrame(); + }).then(() => { + assert_records([{ added: [], changed: [], removed: animations }], + "records after animation end"); + }); + }, `finish_from_pause ${aOptions.subtree ? ': subtree' : ''}`); + + // Test that calling play() on a paused Animation dispatches a changed + // notification. + promise_test(t => { + setupAsynchronousObserver(t, aOptions); + t.add_cleanup(() => { + e.style = ""; + flushComputedStyle(e); + }); + + // Start a long, paused animation + e.style = "animation: anim 100s paused"; + + // The animation should cause the creation of a single Animation. + var animations = e.getAnimations(); + assert_equals(animations.length, 1, + "getAnimations().length after animation start"); + + // Wait for the single MutationRecord for the Animation addition to + // be delivered. + return waitForFrame().then(() => { + assert_records([{ added: animations, changed: [], removed: [] }], + "records after animation start"); + + // Wait until the animation is ready + return animations[0].ready; + }).then(() => { + // Play + animations[0].play(); + + // Wait for the single MutationRecord for the Animation change to + // be delivered. + return animations[0].ready; + }).then(() => { + assert_records([{ added: [], changed: animations, removed: [] }], + "records after play()"); + + // Redundant play + animations[0].play(); + + // Wait to ensure no change is dispatched + return waitForFrame(); + }).then(() => { + assert_records([], "records after redundant play()"); + + // Cancel the animation. + e.style = ""; + + // Wait for the single removal notification. + return waitForFrame(); + }).then(() => { + assert_records([{ added: [], changed: [], removed: animations }], + "records after animation end"); + }); + }, `play ${aOptions.subtree ? ': subtree' : ''}`); + + // Test that a non-cancelling change to an animation followed immediately by a + // cancelling change will only send an animation removal notification. + promise_test(t => { + setupAsynchronousObserver(t, aOptions); + t.add_cleanup(() => { + e.style = ""; + flushComputedStyle(e); + }); + + // Start a long animation. + e.style = "animation: anim 100s;"; + + // The animation should cause the creation of a single Animation. + var animations = e.getAnimations(); + assert_equals(animations.length, 1, + "getAnimations().length after animation start"); + + // Wait for the single MutationRecord for the Animation addition to + // be delivered. + return waitForFrame().then(() => {; + assert_records([{ added: animations, changed: [], removed: [] }], + "records after animation start"); + + // Update the animation's delay such that it is still running. + e.style.animationDelay = "-1s"; + + // Then cancel the animation by updating its duration. + e.style.animationDuration = "0.5s"; + + // We should get a single removal notification. + return waitForFrame(); + }).then(() => { + assert_records([{ added: [], changed: [], removed: animations }], + "records after animation end"); + }); + }, `coalesce_change_cancel ${aOptions.subtree ? ': subtree' : ''}`); + + }); +} + +promise_test(async t => { + setupAsynchronousObserver(t, { observe: div, subtree: true }); + t.add_cleanup(() => { + div.style = ""; + flushComputedStyle(div); + }); + + // Add style for pseudo elements + var extraStyle = document.createElement('style'); + document.head.appendChild(extraStyle); + var sheet = extraStyle.sheet; + var rules = { ".before::before": "animation: anim 100s; content: '';", + ".after::after" : "animation: anim 100s, anim 100s; " + + "content: '';"}; + for (var selector in rules) { + sheet.insertRule(selector + '{' + rules[selector] + '}', + sheet.cssRules.length); + } + + // Create a tree with two children: + // + // div + // (::before) + // (::after) + // / \ + // childA childB(::before) + var childA = document.createElement("div"); + var childB = document.createElement("div"); + + div.appendChild(childA); + div.appendChild(childB); + + // Start an animation on each (using order: childB, div, childA) + // + // We include multiple animations on some nodes so that we can test batching + // works as expected later in this test. + childB.style = "animation: anim 100s"; + div.style = "animation: anim 100s, anim 100s, anim 100s"; + childA.style = "animation: anim 100s, anim 100s"; + + // Start animations targeting to pseudo element of div and childB. + childB.classList.add("before"); + div.classList.add("after"); + div.classList.add("before"); + + // Check all animations we have in this document + var docAnims = document.getAnimations(); + assert_equals(docAnims.length, 10, "total animations"); + + var divAnimations = div.getAnimations(); + var childAAnimations = childA.getAnimations(); + var childBAnimations = childB.getAnimations(); + + var divBeforeAnimations = + docAnims.filter(x => (x.effect.target == div && + x.effect.pseudoElement == "::before")); + var divAfterAnimations = + docAnims.filter(x => (x.effect.target == div && + x.effect.pseudoElement == "::after")); + var childBPseudoAnimations = + docAnims.filter(x => (x.effect.target == childB && + x.effect.pseudoElement == "::before")); + + var seekRecords; + // The order in which we get the corresponding records is currently + // based on the order we visit these nodes when updating styles. + // + // That is because we don't do any document-level batching of animation + // mutation records when we flush styles. We may introduce that in the + // future but for now all we are interested in testing here is that the + // right records are generated, but we allow them to occur in any order. + await waitForFrame(); + + assert_records_any_order( + [{ added: divAfterAnimations, changed: [], removed: [] }, + { added: childAAnimations, changed: [], removed: [] }, + { added: childBAnimations, changed: [], removed: [] }, + { added: childBPseudoAnimations, changed: [], removed: [] }, + { added: divAnimations, changed: [], removed: [] }, + { added: divBeforeAnimations, changed: [], removed: [] }], + "records after simultaneous animation start"); + + // The one case where we *do* currently perform document-level (or actually + // timeline-level) batching is when animations are updated from a refresh + // driver tick. In particular, this means that when animations finish + // naturally the removed records should be dispatched according to the + // position of the elements in the tree. + + // First, flatten the set of animations. we put the animations targeting to + // pseudo elements last. (Actually, we don't care the order in the list.) + var animations = [ ...divAnimations, + ...childAAnimations, + ...childBAnimations, + ...divBeforeAnimations, + ...divAfterAnimations, + ...childBPseudoAnimations ]; + + await Promise.all(animations.map(animation => animation.ready)); + + // Fast-forward to *just* before the end of the animation. + animations.forEach(animation => animation.currentTime = 99999); + + // Prepare the set of expected change MutationRecords, one for each + // animation that was seeked. + seekRecords = animations.map( + p => ({ added: [], changed: [p], removed: [] }) + ); + + await Promise.all(animations.map(animation => animation.finished)); + + // After the changed notifications, which will be dispatched in the order that + // the animations were seeked, we should get removal MutationRecords in order + // (div, div::before, div::after), childA, (childB, childB::before). + // Note: The animations targeting to the pseudo element are appended after + // the animations of its parent element. + divAnimations = [ ...divAnimations, + ...divBeforeAnimations, + ...divAfterAnimations ]; + childBAnimations = [ ...childBAnimations, ...childBPseudoAnimations ]; + assert_records(seekRecords.concat( + { added: [], changed: [], removed: divAnimations }, + { added: [], changed: [], removed: childAAnimations }, + { added: [], changed: [], removed: childBAnimations }), + "records after finishing"); + + // Clean up + div.classList.remove("before"); + div.classList.remove("after"); + div.style = ""; + childA.remove(); + childB.remove(); + extraStyle.remove(); +}, "tree_ordering: subtree"); + +// Test that animations removed by auto-removal trigger an event +promise_test(async t => { + setupAsynchronousObserver(t, { observe: div, subtree: false }); + + // Start two animations such that one will be auto-removed + const animA = div.animate( + { opacity: 1 }, + { duration: 100 * MS_PER_SEC, fill: 'forwards' } + ); + const animB = div.animate( + { opacity: 1 }, + { duration: 100 * MS_PER_SEC, fill: 'forwards' } + ); + + // Wait for the MutationRecords corresponding to each addition. + await waitForNextFrame(); + + assert_records( + [ + { added: [animA], changed: [], removed: [] }, + { added: [animB], changed: [], removed: [] }, + ], + 'records after animation start' + ); + + // Finish the animations -- this should cause animA to be replaced, and + // automatically removed. + animA.finish(); + animB.finish(); + + // Wait for the MutationRecords corresponding to the timing changes and the + // subsequent removal to be delivered. + await waitForNextFrame(); + + assert_records( + [ + { added: [], changed: [animA], removed: [] }, + { added: [], changed: [animB], removed: [] }, + { added: [], changed: [], removed: [animA] }, + ], + 'records after finishing' + ); + + // Restore animA. + animA.persist(); + + // Wait for the MutationRecord corresponding to the re-addition of animA. + await waitForNextFrame(); + + assert_records( + [{ added: [animA], changed: [], removed: [] }], + 'records after persisting' + ); + + // Tidy up + animA.cancel(); + animB.cancel(); + + await waitForNextFrame(); + + assert_records( + [ + { added: [], changed: [], removed: [animA] }, + { added: [], changed: [], removed: [animB] }, + ], + 'records after tidying up end' + ); +}, 'Animations automatically removed are reported'); +runTest(); +</script> diff --git a/dom/animation/test/chrome/test_animation_observers_sync.html b/dom/animation/test/chrome/test_animation_observers_sync.html new file mode 100644 index 0000000000..ec760031e1 --- /dev/null +++ b/dom/animation/test/chrome/test_animation_observers_sync.html @@ -0,0 +1,1587 @@ +<!DOCTYPE html> +<meta charset=utf-8> +<title> +Test chrome-only MutationObserver animation notifications (sync tests) +</title> +<!-- + + This file contains synchronous tests for animation mutation observers. + + In general we prefer to write synchronous tests since they are less likely to + timeout when run on automation. Tests that require asynchronous steps (e.g. + waiting on events) should be added to test_animations_observers_async.html + instead. + +--> +<script type="application/javascript" src="../testharness.js"></script> +<script type="application/javascript" src="../testharnessreport.js"></script> +<script type="application/javascript" src="../testcommon.js"></script> +<div id="log"></div> +<style> +@keyframes anim { + to { transform: translate(100px); } +} +@keyframes anotherAnim { + to { transform: translate(0px); } +} +</style> +<script> + +/** + * Return a new MutationObserver which observing |target| element + * with { animations: true, subtree: |subtree| } option. + * + * NOTE: This observer should be used only with takeRecords(). If any of + * MutationRecords are observed in the callback of the MutationObserver, + * it will raise an assertion. + */ +function setupSynchronousObserver(t, target, subtree) { + var observer = new MutationObserver(records => { + assert_unreached("Any MutationRecords should not be observed in this " + + "callback"); + }); + t.add_cleanup(() => { + observer.disconnect(); + }); + observer.observe(target, { animations: true, subtree }); + return observer; +} + +function assert_record_list(actual, expected, desc, index, listName) { + assert_equals(actual.length, expected.length, + `${desc} - record[${index}].${listName} length`); + if (actual.length != expected.length) { + return; + } + for (var i = 0; i < actual.length; i++) { + assert_not_equals(actual.indexOf(expected[i]), -1, + `${desc} - record[${index}].${listName} contains expected Animation`); + } +} + +function assert_equals_records(actual, expected, desc) { + assert_equals(actual.length, expected.length, `${desc} - number of records`); + if (actual.length != expected.length) { + return; + } + for (var i = 0; i < actual.length; i++) { + assert_record_list(actual[i].addedAnimations, + expected[i].added, desc, i, "addedAnimations"); + assert_record_list(actual[i].changedAnimations, + expected[i].changed, desc, i, "changedAnimations"); + assert_record_list(actual[i].removedAnimations, + expected[i].removed, desc, i, "removedAnimations"); + } +} + +function runTest() { + [ { subtree: false }, + { subtree: true } + ].forEach(aOptions => { + test(t => { + var div = addDiv(t); + var observer = + setupSynchronousObserver(t, + aOptions.subtree ? div.parentNode : div, + aOptions.subtree); + + var anim = div.animate({ opacity: [ 0, 1 ] }, 200 * MS_PER_SEC); + + assert_equals_records(observer.takeRecords(), + [{ added: [anim], changed: [], removed: [] }], + "records after animation is added"); + + anim.effect.updateTiming({ duration: 100 * MS_PER_SEC }); + assert_equals_records(observer.takeRecords(), + [{ added: [], changed: [anim], removed: [] }], + "records after duration is changed"); + + anim.effect.updateTiming({ duration: 100 * MS_PER_SEC }); + assert_equals_records(observer.takeRecords(), + [], "records after assigning same value"); + + anim.currentTime = anim.effect.getComputedTiming().duration * 2; + anim.finish(); + assert_equals_records(observer.takeRecords(), + [{ added: [], changed: [], removed: [anim] }], + "records after animation end"); + + anim.effect.updateTiming({ + duration: anim.effect.getComputedTiming().duration * 3 + }); + assert_equals_records(observer.takeRecords(), + [{ added: [anim], changed: [], removed: [] }], + "records after animation restarted"); + + anim.effect.updateTiming({ duration: 'auto' }); + assert_equals_records(observer.takeRecords(), + [{ added: [], changed: [], removed: [anim] }], + "records after duration set \"auto\""); + + anim.effect.updateTiming({ duration: 'auto' }); + assert_equals_records(observer.takeRecords(), + [], "records after assigning same value \"auto\""); + }, "change_duration_and_currenttime"); + + test(t => { + var div = addDiv(t); + var observer = + setupSynchronousObserver(t, + aOptions.subtree ? div.parentNode : div, + aOptions.subtree); + + var anim = div.animate({ opacity: [ 0, 1 ] }, 100 * MS_PER_SEC); + + assert_equals_records(observer.takeRecords(), + [{ added: [anim], changed: [], removed: [] }], + "records after animation is added"); + + anim.effect.updateTiming({ endDelay: 10 * MS_PER_SEC }); + assert_equals_records(observer.takeRecords(), + [{ added: [], changed: [anim], removed: [] }], + "records after endDelay is changed"); + + anim.effect.updateTiming({ endDelay: 10 * MS_PER_SEC }); + assert_equals_records(observer.takeRecords(), + [], "records after assigning same value"); + + anim.currentTime = 109 * MS_PER_SEC; + assert_equals_records(observer.takeRecords(), + [{ added: [], changed: [], removed: [anim] }], + "records after currentTime during endDelay"); + + anim.effect.updateTiming({ endDelay: -110 * MS_PER_SEC }); + assert_equals_records(observer.takeRecords(), + [], "records after assigning negative value"); + }, "change_enddelay_and_currenttime"); + + test(t => { + var div = addDiv(t); + var observer = + setupSynchronousObserver(t, + aOptions.subtree ? div.parentNode : div, + aOptions.subtree); + + var anim = div.animate({ opacity: [ 0, 1 ] }, + { duration: 100 * MS_PER_SEC, + endDelay: -100 * MS_PER_SEC }); + assert_equals_records(observer.takeRecords(), + [], "records after animation is added"); + }, "zero_end_time"); + + test(t => { + var div = addDiv(t); + var observer = + setupSynchronousObserver(t, + aOptions.subtree ? div.parentNode : div, + aOptions.subtree); + + var anim = div.animate({ opacity: [ 0, 1 ] }, 100 * MS_PER_SEC); + + assert_equals_records(observer.takeRecords(), + [{ added: [anim], changed: [], removed: [] }], + "records after animation is added"); + + anim.effect.updateTiming({ iterations: 2 }); + assert_equals_records(observer.takeRecords(), + [{ added: [], changed: [anim], removed: [] }], + "records after iterations is changed"); + + anim.effect.updateTiming({ iterations: 2 }); + assert_equals_records(observer.takeRecords(), + [], "records after assigning same value"); + + anim.effect.updateTiming({ iterations: 0 }); + assert_equals_records(observer.takeRecords(), + [{ added: [], changed: [], removed: [anim] }], + "records after animation end"); + + anim.effect.updateTiming({ iterations: Infinity }); + assert_equals_records(observer.takeRecords(), + [{ added: [anim], changed: [], removed: [] }], + "records after animation restarted"); + }, "change_iterations"); + + test(t => { + var div = addDiv(t); + var observer = + setupSynchronousObserver(t, + aOptions.subtree ? div.parentNode : div, + aOptions.subtree); + + var anim = div.animate({ opacity: [ 0, 1 ] }, 100 * MS_PER_SEC); + + assert_equals_records(observer.takeRecords(), + [{ added: [anim], changed: [], removed: [] }], + "records after animation is added"); + + anim.effect.updateTiming({ delay: 100 }); + assert_equals_records(observer.takeRecords(), + [{ added: [], changed: [anim], removed: [] }], + "records after delay is changed"); + + anim.effect.updateTiming({ delay: 100 }); + assert_equals_records(observer.takeRecords(), + [], "records after assigning same value"); + + anim.effect.updateTiming({ delay: -100 * MS_PER_SEC }); + assert_equals_records(observer.takeRecords(), + [{ added: [], changed: [], removed: [anim] }], + "records after animation end"); + + anim.effect.updateTiming({ delay: 0 }); + assert_equals_records(observer.takeRecords(), + [{ added: [anim], changed: [], removed: [] }], + "records after animation restarted"); + }, "change_delay"); + + test(t => { + var div = addDiv(t); + var observer = + setupSynchronousObserver(t, + aOptions.subtree ? div.parentNode : div, + aOptions.subtree); + + var anim = div.animate({ opacity: [ 0, 1 ] }, + { duration: 100 * MS_PER_SEC, + easing: "steps(2, start)" }); + + assert_equals_records(observer.takeRecords(), + [{ added: [anim], changed: [], removed: [] }], + "records after animation is added"); + + anim.effect.updateTiming({ easing: "steps(2, end)" }); + assert_equals_records(observer.takeRecords(), + [{ added: [], changed: [anim], removed: [] }], + "records after easing is changed"); + + anim.effect.updateTiming({ easing: "steps(2, end)" }); + assert_equals_records(observer.takeRecords(), + [], "records after assigning same value"); + }, "change_easing"); + + test(t => { + var div = addDiv(t); + var observer = + setupSynchronousObserver(t, + aOptions.subtree ? div.parentNode : div, + aOptions.subtree); + + var anim = div.animate({ opacity: [ 0, 1 ] }, + { duration: 100, delay: -100 }); + assert_equals_records(observer.takeRecords(), + [], "records after assigning negative value"); + }, "negative_delay_in_constructor"); + + test(t => { + var div = addDiv(t); + var observer = + setupSynchronousObserver(t, + aOptions.subtree ? div.parentNode : div, + aOptions.subtree); + + var effect = new KeyframeEffect(null, + { opacity: [ 0, 1 ] }, + { duration: 100 * MS_PER_SEC }); + var anim = new Animation(effect, document.timeline); + anim.play(); + assert_equals_records(observer.takeRecords(), + [], "no records after animation is added"); + }, "create_animation_without_target"); + + test(t => { + var div = addDiv(t); + var observer = + setupSynchronousObserver(t, + aOptions.subtree ? div.parentNode : div, + aOptions.subtree); + + var anim = div.animate({ opacity: [ 0, 1 ] }, + { duration: 100 * MS_PER_SEC }); + assert_equals_records(observer.takeRecords(), + [{ added: [anim], changed: [], removed: [] }], + "records after animation is added"); + + anim.effect.target = div; + assert_equals_records(observer.takeRecords(), + [], "no records after setting the same target"); + + anim.effect.target = null; + assert_equals_records(observer.takeRecords(), + [{ added: [], changed: [], removed: [anim] }], + "records after setting null"); + + anim.effect.target = null; + assert_equals_records(observer.takeRecords(), + [], "records after setting redundant null"); + }, "set_redundant_animation_target"); + + test(t => { + var div = addDiv(t); + var observer = + setupSynchronousObserver(t, + aOptions.subtree ? div.parentNode : div, + aOptions.subtree); + + var anim = div.animate({ opacity: [ 0, 1 ] }, + { duration: 100 * MS_PER_SEC }); + assert_equals_records(observer.takeRecords(), + [{ added: [anim], changed: [], removed: [] }], + "records after animation is added"); + + anim.effect = null; + assert_equals_records(observer.takeRecords(), + [{ added: [], changed: [], removed: [anim] }], + "records after animation is removed"); + }, "set_null_animation_effect"); + + test(t => { + var div = addDiv(t); + var observer = + setupSynchronousObserver(t, + aOptions.subtree ? div.parentNode : div, + aOptions.subtree); + + var anim = new Animation(); + anim.play(); + anim.effect = new KeyframeEffect(div, { opacity: [ 0, 1 ] }, + 100 * MS_PER_SEC); + assert_equals_records(observer.takeRecords(), + [{ added: [anim], changed: [], removed: [] }], + "records after animation is added"); + }, "set_effect_on_null_effect_animation"); + + test(t => { + var div = addDiv(t); + var observer = + setupSynchronousObserver(t, + aOptions.subtree ? div.parentNode : div, + aOptions.subtree); + + var anim = div.animate({ marginLeft: [ "0px", "100px" ] }, + 100 * MS_PER_SEC); + assert_equals_records(observer.takeRecords(), + [{ added: [anim], changed: [], removed: [] }], + "records after animation is added"); + + anim.effect = new KeyframeEffect(div, { opacity: [ 0, 1 ] }, + 100 * MS_PER_SEC); + assert_equals_records(observer.takeRecords(), + [{ added: [], changed: [anim], removed: [] }], + "records after replace effects"); + }, "replace_effect_targeting_on_the_same_element"); + + test(t => { + var div = addDiv(t); + var observer = + setupSynchronousObserver(t, + aOptions.subtree ? div.parentNode : div, + aOptions.subtree); + + var anim = div.animate({ marginLeft: [ "0px", "100px" ] }, + 100 * MS_PER_SEC); + assert_equals_records(observer.takeRecords(), + [{ added: [anim], changed: [], removed: [] }], + "records after animation is added"); + + anim.currentTime = 60 * MS_PER_SEC; + assert_equals_records(observer.takeRecords(), + [{ added: [], changed: [anim], removed: [] }], + "records after animation is changed"); + + anim.effect = new KeyframeEffect(div, { opacity: [ 0, 1 ] }, + 50 * MS_PER_SEC); + assert_equals_records(observer.takeRecords(), + [{ added: [], changed: [], removed: [anim] }], + "records after replacing effects"); + }, "replace_effect_targeting_on_the_same_element_not_in_effect"); + + test(t => { + var div = addDiv(t); + var observer = + setupSynchronousObserver(t, + aOptions.subtree ? div.parentNode : div, + aOptions.subtree); + + var anim = div.animate({ }, 100 * MS_PER_SEC); + assert_equals_records(observer.takeRecords(), + [{ added: [anim], changed: [], removed: [] }], + "records after animation is added"); + + anim.effect.composite = "add"; + assert_equals_records(observer.takeRecords(), + [{ added: [], changed: [anim], removed: [] }], + "records after composite is changed"); + + anim.effect.composite = "add"; + assert_equals_records(observer.takeRecords(), + [], "no record after setting the same composite"); + + }, "set_composite"); + + // Test that starting a single animation that is cancelled by calling + // cancel() dispatches an added notification and then a removed + // notification. + test(t => { + var div = addDiv(t, { style: "animation: anim 100s forwards" }); + var observer = + setupSynchronousObserver(t, + aOptions.subtree ? div.parentNode : div, + aOptions.subtree); + + var animations = div.getAnimations(); + assert_equals(animations.length, 1, + "getAnimations().length after animation start"); + + assert_equals_records(observer.takeRecords(), + [{ added: animations, changed: [], removed: [] }], + "records after animation start"); + + animations[0].cancel(); + + assert_equals_records(observer.takeRecords(), + [{ added: [], changed: [], removed: animations }], + "records after animation end"); + + // Re-trigger the animation. + animations[0].play(); + + // Single MutationRecord for the Animation (re-)addition. + assert_equals_records(observer.takeRecords(), + [{ added: animations, changed: [], removed: [] }], + "records after animation start"); + }, "single_animation_cancelled_api"); + + // Test that updating a property on the Animation object dispatches a changed + // notification. + [ + { prop: "playbackRate", val: 0.5 }, + { prop: "startTime", val: 50 * MS_PER_SEC }, + { prop: "currentTime", val: 50 * MS_PER_SEC }, + ].forEach(aChangeTest => { + test(t => { + // We use a forwards fill mode so that even if the change we make causes + // the animation to become finished, it will still be "relevant" so we + // won't mark it as removed. + var div = addDiv(t, { style: "animation: anim 100s forwards" }); + var observer = + setupSynchronousObserver(t, + aOptions.subtree ? div.parentNode : div, + aOptions.subtree); + + var animations = div.getAnimations(); + assert_equals(animations.length, 1, + "getAnimations().length after animation start"); + + assert_equals_records(observer.takeRecords(), + [{ added: animations, changed: [], removed: [] }], + "records after animation start"); + + // Update the property. + animations[0][aChangeTest.prop] = aChangeTest.val; + + // Make a redundant change. + // eslint-disable-next-line no-self-assign + animations[0][aChangeTest.prop] = animations[0][aChangeTest.prop]; + + assert_equals_records(observer.takeRecords(), + [{ added: [], changed: animations, removed: [] }], + "records after animation property change"); + }, `single_animation_api_change_${aChangeTest.prop}`); + }); + + // Test that making a redundant change to currentTime while an Animation + // is pause-pending still generates a change MutationRecord since setting + // the currentTime to any value in this state aborts the pending pause. + test(t => { + var div = addDiv(t, { style: "animation: anim 100s" }); + var observer = + setupSynchronousObserver(t, + aOptions.subtree ? div.parentNode : div, + aOptions.subtree); + + var animations = div.getAnimations(); + assert_equals(animations.length, 1, + "getAnimations().length after animation start"); + + assert_equals_records(observer.takeRecords(), + [{ added: animations, changed: [], removed: [] }], + "records after animation start"); + + animations[0].pause(); + + // We are now pause-pending. Even if we make a redundant change to the + // currentTime, we should still get a change record because setting the + // currentTime while pause-pending has the effect of cancelling a pause. + // eslint-disable-next-line no-self-assign + animations[0].currentTime = animations[0].currentTime; + + // Two MutationRecords for the Animation changes: one for pausing, one + // for aborting the pause. + assert_equals_records(observer.takeRecords(), + [{ added: [], changed: animations, removed: [] }, + { added: [], changed: animations, removed: [] }], + "records after pausing then seeking"); + }, "change_currentTime_while_pause_pending"); + + // Test that calling finish() on a forwards-filling Animation dispatches + // a changed notification. + test(t => { + var div = addDiv(t, { style: "animation: anim 100s forwards" }); + var observer = + setupSynchronousObserver(t, + aOptions.subtree ? div.parentNode : div, + aOptions.subtree); + + var animations = div.getAnimations(); + assert_equals(animations.length, 1, + "getAnimations().length after animation start"); + + assert_equals_records(observer.takeRecords(), + [{ added: animations, changed: [], removed: [] }], + "records after animation start"); + + animations[0].finish(); + + assert_equals_records(observer.takeRecords(), + [{ added: [], changed: animations, removed: [] }], + "records after finish()"); + + // Redundant finish. + animations[0].finish(); + + // Ensure no change records. + assert_equals_records(observer.takeRecords(), + [], "records after redundant finish()"); + }, "finish_with_forwards_fill"); + + // Test that calling finish() on an Animation that does not fill forwards, + // dispatches a removal notification. + test(t => { + var div = addDiv(t, { style: "animation: anim 100s" }); + var observer = + setupSynchronousObserver(t, + aOptions.subtree ? div.parentNode : div, + aOptions.subtree); + + var animations = div.getAnimations(); + assert_equals(animations.length, 1, + "getAnimations().length after animation start"); + + assert_equals_records(observer.takeRecords(), + [{ added: animations, changed: [], removed: [] }], + "records after animation start"); + + animations[0].finish(); + + // Single MutationRecord for the Animation removal. + assert_equals_records(observer.takeRecords(), + [{ added: [], changed: [], removed: animations }], + "records after finishing"); + }, "finish_without_fill"); + + // Test that calling finish() on a forwards-filling Animation dispatches + test(t => { + var div = addDiv(t, { style: "animation: anim 100s" }); + var observer = + setupSynchronousObserver(t, + aOptions.subtree ? div.parentNode : div, + aOptions.subtree); + + var animation = div.getAnimations()[0]; + assert_equals_records(observer.takeRecords(), + [{ added: [animation], changed: [], removed: []}], + "records after creation"); + animation.id = "new id"; + assert_equals_records(observer.takeRecords(), + [{ added: [], changed: [animation], removed: []}], + "records after id is changed"); + + animation.id = "new id"; + assert_equals_records(observer.takeRecords(), + [], "records after assigning same value with id"); + }, "change_id"); + + // Test that calling reverse() dispatches a changed notification. + test(t => { + var div = addDiv(t, { style: "animation: anim 100s both" }); + var observer = + setupSynchronousObserver(t, + aOptions.subtree ? div.parentNode : div, + aOptions.subtree); + + var animations = div.getAnimations(); + assert_equals(animations.length, 1, + "getAnimations().length after animation start"); + + assert_equals_records(observer.takeRecords(), + [{ added: animations, changed: [], removed: [] }], + "records after animation start"); + + animations[0].reverse(); + + assert_equals_records(observer.takeRecords(), + [{ added: [], changed: animations, removed: [] }], + "records after calling reverse()"); + }, "reverse"); + + // Test that calling reverse() does *not* dispatch a changed notification + // when playbackRate == 0. + test(t => { + var div = addDiv(t, { style: "animation: anim 100s both" }); + var observer = + setupSynchronousObserver(t, + aOptions.subtree ? div.parentNode : div, + aOptions.subtree); + + var animations = div.getAnimations(); + assert_equals(animations.length, 1, + "getAnimations().length after animation start"); + + assert_equals_records(observer.takeRecords(), + [{ added: animations, changed: [], removed: [] }], + "records after animation start"); + + // Seek to the middle and set playbackRate to zero. + animations[0].currentTime = 50 * MS_PER_SEC; + animations[0].playbackRate = 0; + + // Two MutationRecords, one for each change. + assert_equals_records(observer.takeRecords(), + [{ added: [], changed: animations, removed: [] }, + { added: [], changed: animations, removed: [] }], + "records after seeking and setting playbackRate"); + + animations[0].reverse(); + + // We should get no notifications. + assert_equals_records(observer.takeRecords(), + [], "records after calling reverse()"); + }, "reverse_with_zero_playbackRate"); + + // Test that reverse() on an Animation does *not* dispatch a changed + // notification when it throws an exception. + test(t => { + // Start an infinite animation + var div = addDiv(t, { style: "animation: anim 10s infinite" }); + var observer = + setupSynchronousObserver(t, + aOptions.subtree ? div.parentNode : div, + aOptions.subtree); + + var animations = div.getAnimations(); + assert_equals(animations.length, 1, + "getAnimations().length after animation start"); + + assert_equals_records(observer.takeRecords(), + [{ added: animations, changed: [], removed: [] }], + "records after animation start"); + + // Shift the animation into the future such that when we call reverse + // it will try to seek to the (infinite) end. + animations[0].startTime = 100 * MS_PER_SEC; + + assert_equals_records(observer.takeRecords(), + [{ added: [], changed: animations, removed: [] }], + "records after adjusting startTime"); + + // Reverse: should throw + assert_throws('InvalidStateError', () => { + animations[0].reverse(); + }, 'reverse() on future infinite animation throws an exception'); + + // We should get no notifications. + assert_equals_records(observer.takeRecords(), + [], "records after calling reverse()"); + }, "reverse_with_exception"); + + // Test that attempting to start an animation that should already be finished + // does not send any notifications. + test(t => { + // Start an animation that should already be finished. + var div = addDiv(t, { style: "animation: anim 1s -2s;" }); + var observer = + setupSynchronousObserver(t, + aOptions.subtree ? div.parentNode : div, + aOptions.subtree); + + // The animation should cause no Animations to be created. + var animations = div.getAnimations(); + assert_equals(animations.length, 0, + "getAnimations().length after animation start"); + + // And we should get no notifications. + assert_equals_records(observer.takeRecords(), + [], "records after attempted animation start"); + }, "already_finished"); + + test(t => { + var div = addDiv(t, { style: "animation: anim 100s, anotherAnim 100s" }); + var observer = + setupSynchronousObserver(t, + aOptions.subtree ? div.parentNode : div, + aOptions.subtree); + + var animations = div.getAnimations(); + + assert_equals_records(observer.takeRecords(), + [{ added: animations, changed: [], removed: []}], + "records after creation"); + + div.style.animation = "anotherAnim 100s, anim 100s"; + animations = div.getAnimations(); + + assert_equals_records(observer.takeRecords(), + [{ added: [], changed: animations, removed: []}], + "records after the order is changed"); + + div.style.animation = "anotherAnim 100s, anim 100s"; + + assert_equals_records(observer.takeRecords(), + [], "no records after applying the same order"); + }, "animtion_order_change"); + + test(t => { + var div = addDiv(t); + var observer = + setupSynchronousObserver(t, + aOptions.subtree ? div.parentNode : div, + aOptions.subtree); + + var anim = div.animate({ opacity: [ 0, 1 ] }, + { duration: 100 * MS_PER_SEC, + iterationComposite: 'replace' }); + + assert_equals_records(observer.takeRecords(), + [{ added: [anim], changed: [], removed: [] }], + "records after animation is added"); + + anim.effect.iterationComposite = 'accumulate'; + assert_equals_records(observer.takeRecords(), + [{ added: [], changed: [anim], removed: [] }], + "records after iterationComposite is changed"); + + anim.effect.iterationComposite = 'accumulate'; + assert_equals_records(observer.takeRecords(), + [], "no record after setting the same iterationComposite"); + + }, "set_iterationComposite"); + + test(t => { + var div = addDiv(t); + var observer = + setupSynchronousObserver(t, + aOptions.subtree ? div.parentNode : div, + aOptions.subtree); + + var anim = div.animate({ opacity: [ 0, 1 ] }, + { duration: 100 * MS_PER_SEC }); + + assert_equals_records(observer.takeRecords(), + [{ added: [anim], changed: [], removed: [] }], + "records after animation is added"); + + anim.effect.setKeyframes({ opacity: 0.1 }); + assert_equals_records(observer.takeRecords(), + [{ added: [], changed: [anim], removed: [] }], + "records after keyframes are changed"); + + anim.effect.setKeyframes({ opacity: 0.1 }); + assert_equals_records(observer.takeRecords(), + [], "no record after setting the same keyframes"); + + anim.effect.setKeyframes(null); + assert_equals_records(observer.takeRecords(), + [{ added: [], changed: [anim], removed: [] }], + "records after keyframes are set to empty"); + + }, "set_keyframes"); + + // Test that starting a single transition that is cancelled by resetting + // the transition-property property dispatches an added notification and + // then a removed notification. + test(t => { + var div = + addDiv(t, { style: "transition: background-color 100s; " + + "background-color: yellow;" }); + var observer = + setupSynchronousObserver(t, + aOptions.subtree ? div.parentNode : div, + aOptions.subtree); + + getComputedStyle(div).transitionProperty; + div.style.backgroundColor = "lime"; + + // The transition should cause the creation of a single Animation. + var animations = div.getAnimations(); + assert_equals(animations.length, 1, + "getAnimations().length after transition start"); + + assert_equals_records(observer.takeRecords(), + [{ added: animations, changed: [], removed: [] }], + "records after transition start"); + + // Cancel the transition by setting transition-property. + div.style.transitionProperty = "none"; + getComputedStyle(div).transitionProperty; + + assert_equals_records(observer.takeRecords(), + [{ added: [], changed: [], removed: animations }], + "records after transition end"); + }, "single_transition_cancelled_property"); + + // Test that starting a single transition that is cancelled by setting + // style to the currently animated value dispatches an added + // notification and then a removed notification. + test(t => { + // A long transition with a predictable value. + var div = + addDiv(t, { style: "transition: z-index 100s -51s; " + + "z-index: 10;" }); + var observer = + setupSynchronousObserver(t, + aOptions.subtree ? div.parentNode : div, + aOptions.subtree); + getComputedStyle(div).transitionProperty; + div.style.zIndex = "100"; + + // The transition should cause the creation of a single Animation. + var animations = div.getAnimations(); + assert_equals(animations.length, 1, + "getAnimations().length after transition start"); + + assert_equals_records(observer.takeRecords(), + [{ added: animations, changed: [], removed: [] }], + "records after transition start"); + + // Cancel the transition by setting the current animation value. + let value = "83"; + assert_equals(getComputedStyle(div).zIndex, value, + "half-way transition value"); + div.style.zIndex = value; + getComputedStyle(div).transitionProperty; + + assert_equals_records(observer.takeRecords(), + [{ added: [], changed: [], removed: animations }], + "records after transition end"); + }, "single_transition_cancelled_value"); + + // Test that starting a single transition that is cancelled by setting + // style to a non-interpolable value dispatches an added notification + // and then a removed notification. + test(t => { + var div = + addDiv(t, { style: "transition: line-height 100s; " + + "line-height: 16px;" }); + var observer = + setupSynchronousObserver(t, + aOptions.subtree ? div.parentNode : div, + aOptions.subtree); + + getComputedStyle(div).transitionProperty; + div.style.lineHeight = "100px"; + + // The transition should cause the creation of a single Animation. + var animations = div.getAnimations(); + assert_equals(animations.length, 1, + "getAnimations().length after transition start"); + + assert_equals_records(observer.takeRecords(), + [{ added: animations, changed: [], removed: [] }], + "records after transition start"); + + // Cancel the transition by setting line-height to a non-interpolable value. + div.style.lineHeight = "normal"; + getComputedStyle(div).transitionProperty; + + assert_equals_records(observer.takeRecords(), + [{ added: [], changed: [], removed: animations }], + "records after transition end"); + }, "single_transition_cancelled_noninterpolable"); + + // Test that starting a single transition and then reversing it + // dispatches an added notification, then a simultaneous removed and + // added notification, then a removed notification once finished. + test(t => { + var div = + addDiv(t, { style: "transition: background-color 100s step-start; " + + "background-color: yellow;" }); + var observer = + setupSynchronousObserver(t, + aOptions.subtree ? div.parentNode : div, + aOptions.subtree); + + getComputedStyle(div).transitionProperty; + div.style.backgroundColor = "lime"; + + var animations = div.getAnimations(); + + // The transition should cause the creation of a single Animation. + assert_equals(animations.length, 1, + "getAnimations().length after transition start"); + + var firstAnimation = animations[0]; + assert_equals_records(observer.takeRecords(), + [{ added: [firstAnimation], changed: [], removed: [] }], + "records after transition start"); + + firstAnimation.currentTime = 50 * MS_PER_SEC; + + // Reverse the transition by setting the background-color back to its + // original value. + div.style.backgroundColor = "yellow"; + + // The reversal should cause the creation of a new Animation. + animations = div.getAnimations(); + assert_equals(animations.length, 1, + "getAnimations().length after transition reversal"); + + var secondAnimation = animations[0]; + + assert_true(firstAnimation != secondAnimation, + "second Animation should be different from the first"); + + assert_equals_records(observer.takeRecords(), + [{ added: [], changed: [firstAnimation], removed: [] }, + { added: [secondAnimation], changed: [], removed: [firstAnimation] }], + "records after transition reversal"); + + // Cancel the transition. + div.style.transitionProperty = "none"; + getComputedStyle(div).transitionProperty; + + assert_equals_records(observer.takeRecords(), + [{ added: [], changed: [], removed: [secondAnimation] }], + "records after transition end"); + }, "single_transition_reversed"); + + // Test that multiple transitions starting and ending on an element + // at the same time get batched up into a single MutationRecord. + test(t => { + var div = + addDiv(t, { style: "transition-duration: 100s; " + + "transition-property: color, background-color, line-height" + + "background-color: yellow; line-height: 16px" }); + var observer = + setupSynchronousObserver(t, + aOptions.subtree ? div.parentNode : div, + aOptions.subtree); + getComputedStyle(div).transitionProperty; + + div.style.backgroundColor = "lime"; + div.style.color = "blue"; + div.style.lineHeight = "24px"; + + // The transitions should cause the creation of three Animations. + var animations = div.getAnimations(); + assert_equals(animations.length, 3, + "getAnimations().length after transition starts"); + + assert_equals_records(observer.takeRecords(), + [{ added: animations, changed: [], removed: [] }], + "records after transition starts"); + + assert_equals(animations.filter(p => p.playState == "running").length, 3, + "number of running Animations"); + + // Seek well into each animation. + animations.forEach(p => p.currentTime = 50 * MS_PER_SEC); + + // Prepare the set of expected change MutationRecords, one for each + // animation that was seeked. + var seekRecords = animations.map( + p => ({ added: [], changed: [p], removed: [] }) + ); + + // Cancel one of the transitions by setting transition-property. + div.style.transitionProperty = "background-color, line-height"; + + var colorAnimation = animations.filter(p => p.playState != "running"); + var otherAnimations = animations.filter(p => p.playState == "running"); + + assert_equals(colorAnimation.length, 1, + "number of non-running Animations after cancelling one"); + assert_equals(otherAnimations.length, 2, + "number of running Animations after cancelling one"); + + assert_equals_records(observer.takeRecords(), + seekRecords.concat({ added: [], changed: [], removed: colorAnimation }), + "records after color transition end"); + + // Cancel the remaining transitions. + div.style.transitionProperty = "none"; + getComputedStyle(div).transitionProperty; + + assert_equals_records(observer.takeRecords(), + [{ added: [], changed: [], removed: otherAnimations }], + "records after other transition ends"); + }, "multiple_transitions"); + + // Test that starting a single animation that is cancelled by resetting + // the animation-name property dispatches an added notification and + // then a removed notification. + test(t => { + var div = + addDiv(t, { style: "animation: anim 100s" }); + var observer = + setupSynchronousObserver(t, + aOptions.subtree ? div.parentNode : div, + aOptions.subtree); + + // The animation should cause the creation of a single Animation. + var animations = div.getAnimations(); + assert_equals(animations.length, 1, + "getAnimations().length after animation start"); + + assert_equals_records(observer.takeRecords(), + [{ added: animations, changed: [], removed: [] }], + "records after animation start"); + + // Cancel the animation by setting animation-name. + div.style.animationName = "none"; + getComputedStyle(div).animationName; + + assert_equals_records(observer.takeRecords(), + [{ added: [], changed: [], removed: animations }], + "records after animation end"); + }, "single_animation_cancelled_name"); + + // Test that starting a single animation that is cancelled by updating + // the animation-duration property dispatches an added notification and + // then a removed notification. + test(t => { + var div = + addDiv(t, { style: "animation: anim 100s" }); + var observer = + setupSynchronousObserver(t, + aOptions.subtree ? div.parentNode : div, + aOptions.subtree); + + // The animation should cause the creation of a single Animation. + var animations = div.getAnimations(); + assert_equals(animations.length, 1, + "getAnimations().length after animation start"); + + assert_equals_records(observer.takeRecords(), + [{ added: animations, changed: [], removed: [] }], + "records after animation start"); + + // Advance the animation by a second. + animations[0].currentTime += 1 * MS_PER_SEC; + + // Cancel the animation by setting animation-duration to a value less + // than a second. + div.style.animationDuration = "0.1s"; + getComputedStyle(div).animationName; + + assert_equals_records(observer.takeRecords(), + [{ added: [], changed: animations, removed: [] }, + { added: [], changed: [], removed: animations }], + "records after animation end"); + }, "single_animation_cancelled_duration"); + + // Test that starting a single animation that is cancelled by updating + // the animation-delay property dispatches an added notification and + // then a removed notification. + test(t => { + var div = + addDiv(t, { style: "animation: anim 100s" }); + var observer = + setupSynchronousObserver(t, + aOptions.subtree ? div.parentNode : div, + aOptions.subtree); + + // The animation should cause the creation of a single Animation. + var animations = div.getAnimations(); + assert_equals(animations.length, 1, + "getAnimations().length after animation start"); + + assert_equals_records(observer.takeRecords(), + [{ added: animations, changed: [], removed: [] }], + "records after animation start"); + + // Cancel the animation by setting animation-delay. + div.style.animationDelay = "-200s"; + getComputedStyle(div).animationName; + + assert_equals_records(observer.takeRecords(), + [{ added: [], changed: [], removed: animations }], + "records after animation end"); + }, "single_animation_cancelled_delay"); + + // Test that starting a single animation that is cancelled by updating + // the animation-iteration-count property dispatches an added notification + // and then a removed notification. + test(t => { + // A short, repeated animation. + var div = + addDiv(t, { style: "animation: anim 0.5s infinite;" }); + var observer = + setupSynchronousObserver(t, + aOptions.subtree ? div.parentNode : div, + aOptions.subtree); + + // The animation should cause the creation of a single Animation. + var animations = div.getAnimations(); + assert_equals(animations.length, 1, + "getAnimations().length after animation start"); + + assert_equals_records(observer.takeRecords(), + [{ added: animations, changed: [], removed: [] }], + "records after animation start"); + + // Advance the animation until we are past the first iteration. + animations[0].currentTime += 1 * MS_PER_SEC; + + assert_equals_records(observer.takeRecords(), + [{ added: [], changed: animations, removed: [] }], + "records after seeking animations"); + + // Cancel the animation by setting animation-iteration-count. + div.style.animationIterationCount = "1"; + getComputedStyle(div).animationName; + + assert_equals_records(observer.takeRecords(), + [{ added: [], changed: [], removed: animations }], + "records after animation end"); + }, "single_animation_cancelled_iteration_count"); + + // Test that updating an animation property dispatches a changed notification. + [ + { name: "duration", prop: "animationDuration", val: "200s" }, + { name: "timing", prop: "animationTimingFunction", val: "linear" }, + { name: "iteration", prop: "animationIterationCount", val: "2" }, + { name: "direction", prop: "animationDirection", val: "reverse" }, + { name: "state", prop: "animationPlayState", val: "paused" }, + { name: "delay", prop: "animationDelay", val: "-1s" }, + { name: "fill", prop: "animationFillMode", val: "both" }, + ].forEach(aChangeTest => { + test(t => { + // Start a long animation. + var div = addDiv(t, { style: "animation: anim 100s;" }); + var observer = + setupSynchronousObserver(t, + aOptions.subtree ? div.parentNode : div, + aOptions.subtree); + + // The animation should cause the creation of a single Animation. + var animations = div.getAnimations(); + assert_equals(animations.length, 1, + "getAnimations().length after animation start"); + + assert_equals_records(observer.takeRecords(), + [{ added: animations, changed: [], removed: [] }], + "records after animation start"); + + // Change a property of the animation such that it keeps running. + div.style[aChangeTest.prop] = aChangeTest.val; + getComputedStyle(div).animationName; + + assert_equals_records(observer.takeRecords(), + [{ added: [], changed: animations, removed: [] }], + "records after animation change"); + + // Cancel the animation. + div.style.animationName = "none"; + getComputedStyle(div).animationName; + + assert_equals_records(observer.takeRecords(), + [{ added: [], changed: [], removed: animations }], + "records after animation end"); + }, `single_animation_change_${aChangeTest.name}`); + }); + + // Test that calling finish() on a pause-pending (but otherwise finished) + // animation dispatches a changed notification. + test(t => { + var div = + addDiv(t, { style: "animation: anim 100s forwards" }); + var observer = + setupSynchronousObserver(t, + aOptions.subtree ? div.parentNode : div, + aOptions.subtree); + + // The animation should cause the creation of a single Animation. + var animations = div.getAnimations(); + assert_equals(animations.length, 1, + "getAnimations().length after animation start"); + + assert_equals_records(observer.takeRecords(), + [{ added: animations, changed: [], removed: [] }], + "records after animation start"); + + // Finish and pause. + animations[0].finish(); + animations[0].pause(); + assert_true(animations[0].pending && animations[0].playState === "paused", + "playState after finishing and calling pause()"); + + // Call finish() again to abort the pause + animations[0].finish(); + assert_equals(animations[0].playState, "finished", + "playState after finishing again"); + + // Wait for three MutationRecords for the Animation changes to + // be delivered: one for each finish(), pause(), finish() operation. + assert_equals_records(observer.takeRecords(), + [{ added: [], changed: animations, removed: [] }, + { added: [], changed: animations, removed: [] }, + { added: [], changed: animations, removed: [] }], + "records after finish(), pause(), finish()"); + + // Cancel the animation. + div.style = ""; + getComputedStyle(div).animationName; + + assert_equals_records(observer.takeRecords(), + [{ added: [], changed: [], removed: animations }], + "records after animation end"); + }, "finish_from_pause_pending"); + + // Test that calling play() on a finished Animation that fills forwards + // dispatches a changed notification. + test(t => { + // Animation with a forwards fill + var div = + addDiv(t, { style: "animation: anim 100s forwards" }); + var observer = + setupSynchronousObserver(t, + aOptions.subtree ? div.parentNode : div, + aOptions.subtree); + + // The animation should cause the creation of a single Animation. + var animations = div.getAnimations(); + assert_equals(animations.length, 1, + "getAnimations().length after animation start"); + + assert_equals_records(observer.takeRecords(), + [{ added: animations, changed: [], removed: [] }], + "records after animation start"); + + // Seek to the end + animations[0].finish(); + + assert_equals_records(observer.takeRecords(), + [{ added: [], changed: animations, removed: [] }], + "records after finish()"); + + // Since we are filling forwards, calling play() should produce a + // change record since the animation remains relevant. + animations[0].play(); + + assert_equals_records(observer.takeRecords(), + [{ added: [], changed: animations, removed: [] }], + "records after play()"); + + // Cancel the animation. + div.style = ""; + getComputedStyle(div).animationName; + + assert_equals_records(observer.takeRecords(), + [{ added: [], changed: [], removed: animations }], + "records after animation end"); + }, "play_filling_forwards"); + + // Test that calling pause() on an Animation dispatches a changed + // notification. + test(t => { + var div = + addDiv(t, { style: "animation: anim 100s" }); + var observer = + setupSynchronousObserver(t, + aOptions.subtree ? div.parentNode : div, + aOptions.subtree); + + // The animation should cause the creation of a single Animation. + var animations = div.getAnimations(); + assert_equals(animations.length, 1, + "getAnimations().length after animation start"); + + assert_equals_records(observer.takeRecords(), + [{ added: animations, changed: [], removed: [] }], + "records after animation start"); + + // Pause + animations[0].pause(); + + assert_equals_records(observer.takeRecords(), + [{ added: [], changed: animations, removed: [] }], + "records after pause()"); + + // Redundant pause + animations[0].pause(); + + assert_equals_records(observer.takeRecords(), + [], "records after redundant pause()"); + + // Cancel the animation. + div.style = ""; + getComputedStyle(div).animationName; + + assert_equals_records(observer.takeRecords(), + [{ added: [], changed: [], removed: animations }], + "records after animation end"); + }, "pause"); + + // Test that calling pause() on an Animation that is pause-pending + // does not dispatch an additional changed notification. + test(t => { + var div = + addDiv(t, { style: "animation: anim 100s" }); + var observer = + setupSynchronousObserver(t, + aOptions.subtree ? div.parentNode : div, + aOptions.subtree); + + // The animation should cause the creation of a single Animation. + var animations = div.getAnimations(); + assert_equals(animations.length, 1, + "getAnimations().length after animation start"); + + assert_equals_records(observer.takeRecords(), + [{ added: animations, changed: [], removed: [] }], + "records after animation start"); + + // Pause + animations[0].pause(); + + // We are now pause-pending, but pause again + animations[0].pause(); + + assert_equals_records(observer.takeRecords(), + [{ added: [], changed: animations, removed: [] }], + "records after pause()"); + + // Cancel the animation. + div.style = ""; + getComputedStyle(div).animationName; + + assert_equals_records(observer.takeRecords(), + [{ added: [], changed: [], removed: animations }], + "records after animation end"); + }, "pause_while_pause_pending"); + + // Test that calling play() on an Animation that is pause-pending + // dispatches a changed notification. + test(t => { + var div = + addDiv(t, { style: "animation: anim 100s" }); + var observer = + setupSynchronousObserver(t, + aOptions.subtree ? div.parentNode : div, + aOptions.subtree); + + // The animation should cause the creation of a single Animation. + var animations = div.getAnimations(); + assert_equals(animations.length, 1, + "getAnimations().length after animation start"); + + assert_equals_records(observer.takeRecords(), + [{ added: animations, changed: [], removed: [] }], + "records after animation start"); + + // Pause + animations[0].pause(); + + // We are now pause-pending. If we play() now, we will abort the pause + animations[0].play(); + + assert_equals_records(observer.takeRecords(), + [{ added: [], changed: animations, removed: [] }, + { added: [], changed: animations, removed: [] }], + "records after aborting a pause()"); + + // Cancel the animation. + div.style = ""; + getComputedStyle(div).animationName; + + assert_equals_records(observer.takeRecords(), + [{ added: [], changed: [], removed: animations }], + "records after animation end"); + }, "aborted_pause"); + + // Test that calling play() on a finished Animation that does *not* fill + // forwards dispatches an addition notification. + test(t => { + var div = + addDiv(t, { style: "animation: anim 100s" }); + var observer = + setupSynchronousObserver(t, + aOptions.subtree ? div.parentNode : div, + aOptions.subtree); + + // The animation should cause the creation of a single Animation. + var animations = div.getAnimations(); + assert_equals(animations.length, 1, + "getAnimations().length after animation start"); + + assert_equals_records(observer.takeRecords(), + [{ added: animations, changed: [], removed: [] }], + "records after animation start"); + + // Seek to the end + animations[0].finish(); + + assert_equals_records(observer.takeRecords(), + [{ added: [], changed: [], removed: animations }], + "records after finish()"); + + // Since we are *not* filling forwards, calling play() is equivalent + // to creating a new animation since it becomes relevant again. + animations[0].play(); + + assert_equals_records(observer.takeRecords(), + [{ added: animations, changed: [], removed: [] }], + "records after play()"); + + // Cancel the animation. + div.style = ""; + getComputedStyle(div).animationName; + + assert_equals_records(observer.takeRecords(), + [{ added: [], changed: [], removed: animations }], + "records after animation end"); + }, "play_after_finish"); + + }); + + test(t => { + var div = addDiv(t); + var observer = setupSynchronousObserver(t, div, true); + + var child = document.createElement("div"); + div.appendChild(child); + + var anim1 = div.animate({ marginLeft: [ "0px", "50px" ] }, + 100 * MS_PER_SEC); + var anim2 = child.animate({ marginLeft: [ "0px", "100px" ] }, + 50 * MS_PER_SEC); + assert_equals_records(observer.takeRecords(), + [{ added: [anim1], changed: [], removed: [] }, + { added: [anim2], changed: [], removed: [] }], + "records after animation is added"); + + // After setting a new effect, we remove the current animation, anim1, + // because it is no longer attached to |div|, and then remove the previous + // animation, anim2. Finally, add back the anim1 which is in effect on + // |child| now. In addition, we sort them by tree order and they are + // batched. + anim1.effect = anim2.effect; + assert_equals_records(observer.takeRecords(), + [{ added: [], changed: [], removed: [anim1] }, // div + { added: [anim1], changed: [], removed: [anim2] }], // child + "records after animation effects are changed"); + }, "set_effect_with_previous_animation"); + + test(t => { + var div = addDiv(t); + var observer = setupSynchronousObserver(t, document, true); + + var anim = div.animate({ opacity: [ 0, 1 ] }, + { duration: 100 * MS_PER_SEC }); + + var newTarget = document.createElement("div"); + + assert_equals_records(observer.takeRecords(), + [{ added: [anim], changed: [], removed: [] }], + "records after animation is added"); + + anim.effect.target = null; + assert_equals_records(observer.takeRecords(), + [{ added: [], changed: [], removed: [anim] }], + "records after setting null"); + + anim.effect.target = div; + assert_equals_records(observer.takeRecords(), + [{ added: [anim], changed: [], removed: [] }], + "records after setting a target"); + + anim.effect.target = addDiv(t); + assert_equals_records(observer.takeRecords(), + [{ added: [], changed: [], removed: [anim] }, + { added: [anim], changed: [], removed: [] }], + "records after setting a different target"); + }, "set_animation_target"); + + test(t => { + var div = addDiv(t); + var observer = setupSynchronousObserver(t, div, true); + + var anim = div.animate({ opacity: [ 0, 1 ] }, + { duration: 200 * MS_PER_SEC, + pseudoElement: '::before' }); + + assert_equals_records(observer.takeRecords(), + [{ added: [anim], changed: [], removed: [] }], + "records after animation is added"); + + anim.effect.updateTiming({ duration: 100 * MS_PER_SEC }); + assert_equals_records(observer.takeRecords(), + [{ added: [], changed: [anim], removed: [] }], + "records after duration is changed"); + + anim.effect.updateTiming({ duration: 100 * MS_PER_SEC }); + assert_equals_records(observer.takeRecords(), + [], "records after assigning same value"); + + anim.currentTime = anim.effect.getComputedTiming().duration * 2; + anim.finish(); + assert_equals_records(observer.takeRecords(), + [{ added: [], changed: [], removed: [anim] }], + "records after animation end"); + + anim.effect.updateTiming({ + duration: anim.effect.getComputedTiming().duration * 3 + }); + assert_equals_records(observer.takeRecords(), + [{ added: [anim], changed: [], removed: [] }], + "records after animation restarted"); + + anim.effect.updateTiming({ duration: "auto" }); + assert_equals_records(observer.takeRecords(), + [{ added: [], changed: [], removed: [anim] }], + "records after duration set \"auto\""); + + anim.effect.updateTiming({ duration: "auto" }); + assert_equals_records(observer.takeRecords(), + [], "records after assigning same value \"auto\""); + }, "change_duration_and_currenttime_on_pseudo_elements"); + + test(t => { + var div = addDiv(t); + var observer = setupSynchronousObserver(t, div, false); + + var anim = div.animate({ opacity: [ 0, 1 ] }, + { duration: 100 * MS_PER_SEC }); + var pAnim = div.animate({ opacity: [ 0, 1 ] }, + { duration: 100 * MS_PER_SEC, + pseudoElement: "::before" }); + + assert_equals_records(observer.takeRecords(), + [{ added: [anim], changed: [], removed: [] }], + "records after animation is added"); + + anim.finish(); + pAnim.finish(); + + assert_equals_records(observer.takeRecords(), + [{ added: [], changed: [], removed: [anim] }], + "records after animation is finished"); + }, "exclude_animations_targeting_pseudo_elements"); +} + +W3CTest.runner.expectAssertions(0, 12); // bug 1189015 +setup({explicit_done: true}); +SpecialPowers.pushPrefEnv( + { + set: [ + ["dom.animations-api.timelines.enabled", true], + ], + }, + function() { + runTest(); + done(); + } +); + +</script> diff --git a/dom/animation/test/chrome/test_animation_performance_warning.html b/dom/animation/test/chrome/test_animation_performance_warning.html new file mode 100644 index 0000000000..13df5d7842 --- /dev/null +++ b/dom/animation/test/chrome/test_animation_performance_warning.html @@ -0,0 +1,1693 @@ +<!doctype html> +<head> +<meta charset=utf-8> +<title>Bug 1196114 - Test metadata related to which animation properties + are running on the compositor</title> +<script type="application/javascript" src="../testharness.js"></script> +<script type="application/javascript" src="../testharnessreport.js"></script> +<script type="application/javascript" src="../testcommon.js"></script> +<style> +.compositable { + /* Element needs geometry to be eligible for layerization */ + width: 100px; + height: 100px; + background-color: white; +} +@keyframes fade { + from { opacity: 1 } + to { opacity: 0 } +} +@keyframes translate { + from { transform: none } + to { transform: translate(100px) } +} +</style> +</head> +<body> +<a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1196114" + target="_blank">Mozilla Bug 1196114</a> +<div id="log"></div> +<script> +'use strict'; + +// This is used for obtaining localized strings. +var gStringBundle; + +W3CTest.runner.requestLongerTimeout(2); + +const Services = SpecialPowers.Services; +Services.locale.requestedLocales = ["en-US"]; + +SpecialPowers.pushPrefEnv({ "set": [ + // Need to set devPixelsPerPx explicitly to gain + // consistent pixel values in warning messages + // regardless of platform DPIs. + ["layout.css.devPixelsPerPx", 1], + ["layout.animation.prerender.partial", false], + ] }, + start); + +function compare_property_state(a, b) { + if (a.property > b.property) { + return -1; + } else if (a.property < b.property) { + return 1; + } + if (a.runningOnCompositor != b.runningOnCompositor) { + return a.runningOnCompositor ? 1 : -1; + } + return a.warning > b.warning ? -1 : 1; +} + +function assert_animation_property_state_equals(actual, expected) { + assert_equals(actual.length, expected.length, 'Number of properties'); + + var sortedActual = actual.sort(compare_property_state); + var sortedExpected = expected.sort(compare_property_state); + + for (var i = 0; i < sortedActual.length; i++) { + assert_equals(sortedActual[i].property, + sortedExpected[i].property, + 'CSS property name should match'); + assert_equals(sortedActual[i].runningOnCompositor, + sortedExpected[i].runningOnCompositor, + 'runningOnCompositor property should match'); + if (sortedExpected[i].warning instanceof RegExp) { + assert_regexp_match(sortedActual[i].warning, + sortedExpected[i].warning, + 'warning message should match'); + } else if (sortedExpected[i].warning) { + assert_equals(sortedActual[i].warning, + gStringBundle.GetStringFromName(sortedExpected[i].warning), + 'warning message should match'); + } + } +} + +// Check that the animation is running on compositor and +// warning property is not set for the CSS property regardless +// expected values. +function assert_all_properties_running_on_compositor(actual, expected) { + assert_equals(actual.length, expected.length); + + var sortedActual = actual.sort(compare_property_state); + var sortedExpected = expected.sort(compare_property_state); + + for (var i = 0; i < sortedActual.length; i++) { + assert_equals(sortedActual[i].property, + sortedExpected[i].property, + 'CSS property name should match'); + assert_true(sortedActual[i].runningOnCompositor, + 'runningOnCompositor property should be true on ' + + sortedActual[i].property); + assert_not_exists(sortedActual[i], 'warning', + 'warning property should not be set'); + } +} + +function testBasicOperation() { + [ + { + desc: 'animations on compositor', + frames: { + opacity: [0, 1] + }, + expected: [ + { + property: 'opacity', + runningOnCompositor: true + } + ] + }, + { + desc: 'animations on main thread', + frames: { + zIndex: ['0', '999'] + }, + expected: [ + { + property: 'z-index', + runningOnCompositor: false + } + ] + }, + { + desc: 'animations on both threads', + frames: { + zIndex: ['0', '999'], + transform: ['translate(0px)', 'translate(100px)'] + }, + expected: [ + { + property: 'z-index', + runningOnCompositor: false + }, + { + property: 'transform', + runningOnCompositor: true + } + ] + }, + { + desc: 'two animation properties on compositor thread', + frames: { + opacity: [0, 1], + transform: ['translate(0px)', 'translate(100px)'] + }, + expected: [ + { + property: 'opacity', + runningOnCompositor: true + }, + { + property: 'transform', + runningOnCompositor: true + } + ] + }, + { + desc: 'two transform-like animation properties on compositor thread', + frames: { + transform: ['translate(0px)', 'translate(100px)'], + translate: ['0px', '100px'] + }, + expected: [ + { + property: 'transform', + runningOnCompositor: true + }, + { + property: 'translate', + runningOnCompositor: true + } + ] + }, + { + desc: 'opacity on compositor with animation of geometric properties', + frames: { + width: ['100px', '200px'], + opacity: [0, 1] + }, + expected: [ + { + property: 'width', + runningOnCompositor: false + }, + { + property: 'opacity', + runningOnCompositor: true + } + ] + }, + ].forEach(subtest => { + promise_test(async t => { + var animation = addDivAndAnimate(t, { class: 'compositable' }, + subtest.frames, 100 * MS_PER_SEC); + await waitForPaints(); + assert_animation_property_state_equals( + animation.effect.getProperties(), + subtest.expected); + }, subtest.desc); + }); +} + +// Test adding/removing a 'width' property on the same animation object. +function testKeyframesWithGeometricProperties() { + [ + { + desc: 'transform', + frames: { + transform: ['translate(0px)', 'translate(100px)'] + }, + expected: { + withoutGeometric: [ + { + property: 'transform', + runningOnCompositor: true + } + ], + withGeometric: [ + { + property: 'width', + runningOnCompositor: false + }, + { + property: 'transform', + runningOnCompositor: true, + } + ] + } + }, + { + desc: 'translate', + frames: { + translate: ['0px', '100px'] + }, + expected: { + withoutGeometric: [ + { + property: 'translate', + runningOnCompositor: true + } + ], + withGeometric: [ + { + property: 'width', + runningOnCompositor: false + }, + { + property: 'translate', + runningOnCompositor: true, + } + ] + } + }, + { + desc: 'opacity and transform-like properties', + frames: { + opacity: [0, 1], + transform: ['translate(0px)', 'translate(100px)'], + translate: ['0px', '100px'] + }, + expected: { + withoutGeometric: [ + { + property: 'opacity', + runningOnCompositor: true + }, + { + property: 'transform', + runningOnCompositor: true + }, + { + property: 'translate', + runningOnCompositor: true + } + ], + withGeometric: [ + { + property: 'width', + runningOnCompositor: false + }, + { + property: 'opacity', + runningOnCompositor: true + }, + { + property: 'transform', + runningOnCompositor: true, + }, + { + property: 'translate', + runningOnCompositor: true, + } + ] + } + }, + ].forEach(subtest => { + promise_test(async t => { + var animation = addDivAndAnimate(t, { class: 'compositable' }, + subtest.frames, 100 * MS_PER_SEC); + await waitForPaints(); + + // First, a transform animation is running on compositor. + assert_animation_property_state_equals( + animation.effect.getProperties(), + subtest.expected.withoutGeometric); + + // Add a 'width' property. + var keyframes = animation.effect.getKeyframes(); + + keyframes[0].width = '100px'; + keyframes[1].width = '200px'; + + animation.effect.setKeyframes(keyframes); + await waitForFrame(); + + // Now the transform animation is not running on compositor because of + // the 'width' property. + assert_animation_property_state_equals( + animation.effect.getProperties(), + subtest.expected.withGeometric); + + // Remove the 'width' property. + var keyframes = animation.effect.getKeyframes(); + + delete keyframes[0].width; + delete keyframes[1].width; + + animation.effect.setKeyframes(keyframes); + await waitForFrame(); + + // Finally, the transform animation is running on compositor. + assert_animation_property_state_equals( + animation.effect.getProperties(), + subtest.expected.withoutGeometric); + }, 'An animation has: ' + subtest.desc); + }); +} + +// Test that the expected set of geometric properties all block transform +// animations. +function testSetOfGeometricProperties() { + const geometricProperties = [ + 'width', 'height', + 'top', 'right', 'bottom', 'left', + 'margin-top', 'margin-right', 'margin-bottom', 'margin-left', + 'padding-top', 'padding-right', 'padding-bottom', 'padding-left' + ]; + + geometricProperties.forEach(property => { + promise_test(async t => { + const keyframes = { + [propertyToIDL(property)]: [ '100px', '200px' ], + transform: [ 'translate(0px)', 'translate(100px)' ] + }; + var animation = addDivAndAnimate(t, { class: 'compositable' }, + keyframes, 100 * MS_PER_SEC); + + await waitForPaints(); + assert_animation_property_state_equals( + animation.effect.getProperties(), + [ + { + property, + runningOnCompositor: false + }, + { + property: 'transform', + runningOnCompositor: true, + } + ]); + }, `${property} is treated as a geometric property`); + }); +} + +// Performance warning tests that set and clear a style property. +function testStyleChanges() { + [ + { + desc: 'preserve-3d transform', + frames: { + transform: ['translate(0px)', 'translate(100px)'] + }, + style: 'transform-style: preserve-3d', + expected: [ + { + property: 'transform', + runningOnCompositor: true, + } + ] + }, + { + desc: 'preserve-3d translate', + frames: { + translate: ['0px', '100px'] + }, + style: 'transform-style: preserve-3d', + expected: [ + { + property: 'translate', + runningOnCompositor: true, + } + ] + }, + { + desc: 'transform with backface-visibility:hidden', + frames: { + transform: ['translate(0px)', 'translate(100px)'] + }, + style: 'backface-visibility: hidden;', + expected: [ + { + property: 'transform', + runningOnCompositor: true, + } + ] + }, + { + desc: 'translate with backface-visibility:hidden', + frames: { + translate: ['0px', '100px'] + }, + style: 'backface-visibility: hidden;', + expected: [ + { + property: 'translate', + runningOnCompositor: true, + } + ] + }, + { + desc: 'opacity and transform-like properties with preserve-3d', + frames: { + opacity: [0, 1], + transform: ['translate(0px)', 'translate(100px)'], + translate: ['0px', '100px'] + }, + style: 'transform-style: preserve-3d', + expected: [ + { + property: 'opacity', + runningOnCompositor: true + }, + { + property: 'transform', + runningOnCompositor: true, + }, + { + property: 'translate', + runningOnCompositor: true, + } + ] + }, + { + desc: 'opacity and transform-like properties with ' + + 'backface-visibility:hidden', + frames: { + opacity: [0, 1], + transform: ['translate(0px)', 'translate(100px)'], + translate: ['0px', '100px'] + }, + style: 'backface-visibility: hidden;', + expected: [ + { + property: 'opacity', + runningOnCompositor: true + }, + { + property: 'transform', + runningOnCompositor: true, + }, + { + property: 'translate', + runningOnCompositor: true, + } + ] + }, + ].forEach(subtest => { + promise_test(async t => { + var animation = addDivAndAnimate(t, { class: 'compositable' }, + subtest.frames, 100 * MS_PER_SEC); + await waitForPaints(); + assert_all_properties_running_on_compositor( + animation.effect.getProperties(), + subtest.expected); + animation.effect.target.style = subtest.style; + await waitForFrame(); + + assert_animation_property_state_equals( + animation.effect.getProperties(), + subtest.expected); + animation.effect.target.style = ''; + await waitForFrame(); + + assert_all_properties_running_on_compositor( + animation.effect.getProperties(), + subtest.expected); + }, subtest.desc); + }); +} + +// Performance warning tests that set and clear the id property +function testIdChanges() { + [ + { + desc: 'moz-element referencing a transform', + frames: { + transform: ['translate(0px)', 'translate(100px)'] + }, + id: 'transformed', + createelement: 'width:100px; height:100px; background: -moz-element(#transformed)', + expected: [ + { + property: 'transform', + runningOnCompositor: false, + warning: 'CompositorAnimationWarningHasRenderingObserver' + } + ] + }, + { + desc: 'moz-element referencing a translate', + frames: { + translate: ['0px', '100px'] + }, + id: 'transformed', + createelement: 'width:100px; height:100px; background: -moz-element(#transformed)', + expected: [ + { + property: 'translate', + runningOnCompositor: false, + warning: 'CompositorAnimationWarningHasRenderingObserver' + } + ] + }, + { + desc: 'moz-element referencing a translate and transform', + frames: { + transform: ['translate(0px)', 'translate(100px)'], + translate: ['0px', '100px'] + }, + id: 'transformed', + createelement: 'width:100px; height:100px; background: -moz-element(#transformed)', + expected: [ + { + property: 'translate', + runningOnCompositor: false, + warning: 'CompositorAnimationWarningHasRenderingObserver' + }, + { + property: 'transform', + runningOnCompositor: false, + warning: 'CompositorAnimationWarningHasRenderingObserver' + } + ] + }, + ].forEach(subtest => { + promise_test(async t => { + if (subtest.createelement) { + addDiv(t, { style: subtest.createelement }); + } + + var animation = addDivAndAnimate(t, { class: 'compositable' }, + subtest.frames, 100 * MS_PER_SEC); + await waitForPaints(); + + assert_all_properties_running_on_compositor( + animation.effect.getProperties(), + subtest.expected); + animation.effect.target.id = subtest.id; + await waitForFrame(); + + assert_animation_property_state_equals( + animation.effect.getProperties(), + subtest.expected); + animation.effect.target.id = ''; + await waitForFrame(); + + assert_all_properties_running_on_compositor( + animation.effect.getProperties(), + subtest.expected); + }, subtest.desc); + }); +} + +function testMultipleAnimations() { + [ + { + desc: 'opacity and transform-like properties with preserve-3d', + style: 'transform-style: preserve-3d', + animations: [ + { + frames: { + transform: ['translate(0px)', 'translate(100px)'] + }, + expected: [ + { + property: 'transform', + runningOnCompositor: true, + } + ] + }, + { + frames: { + translate: ['0px', '100px'] + }, + expected: [ + { + property: 'translate', + runningOnCompositor: true, + } + ] + }, + { + frames: { + opacity: [0, 1] + }, + expected: [ + { + property: 'opacity', + runningOnCompositor: true, + } + ] + } + ], + }, + { + desc: 'opacity and transform-like properties with ' + + 'backface-visibility:hidden', + style: 'backface-visibility: hidden;', + animations: [ + { + frames: { + transform: ['translate(0px)', 'translate(100px)'] + }, + expected: [ + { + property: 'transform', + runningOnCompositor: true, + } + ] + }, + { + frames: { + translate: ['0px', '100px'] + }, + expected: [ + { + property: 'translate', + runningOnCompositor: true, + } + ] + }, + { + frames: { + opacity: [0, 1] + }, + expected: [ + { + property: 'opacity', + runningOnCompositor: true, + } + ] + } + ], + }, + ].forEach(subtest => { + promise_test(async t => { + var div = addDiv(t, { class: 'compositable' }); + var animations = subtest.animations.map(anim => { + var animation = div.animate(anim.frames, 100 * MS_PER_SEC); + + // Bind expected values to animation object. + animation.expected = anim.expected; + return animation; + }); + await waitForPaints(); + + animations.forEach(anim => { + assert_all_properties_running_on_compositor( + anim.effect.getProperties(), + anim.expected); + }); + div.style = subtest.style; + await waitForFrame(); + + animations.forEach(anim => { + assert_animation_property_state_equals( + anim.effect.getProperties(), + anim.expected); + }); + div.style = ''; + await waitForFrame(); + + animations.forEach(anim => { + assert_all_properties_running_on_compositor( + anim.effect.getProperties(), + anim.expected); + }); + }, 'Multiple animations: ' + subtest.desc); + }); +} + +// Test adding/removing a 'width' keyframe on the same animation object, where +// multiple animation objects belong to the same element. +// The 'width' property is added to animations[1]. +function testMultipleAnimationsWithGeometricKeyframes() { + [ + { + desc: 'transform and opacity with geometric keyframes', + animations: [ + { + frames: { + transform: ['translate(0px)', 'translate(100px)'] + }, + expected: { + withoutGeometric: [ + { + property: 'transform', + runningOnCompositor: true + } + ], + withGeometric: [ + { + property: 'transform', + runningOnCompositor: true, + } + ] + } + }, + { + frames: { + opacity: [0, 1] + }, + expected: { + withoutGeometric: [ + { + property: 'opacity', + runningOnCompositor: true, + } + ], + withGeometric: [ + { + property: 'width', + runningOnCompositor: false, + }, + { + property: 'opacity', + runningOnCompositor: true, + } + ] + } + } + ], + }, + { + desc: 'opacity and transform with geometric keyframes', + animations: [ + { + frames: { + opacity: [0, 1] + }, + expected: { + withoutGeometric: [ + { + property: 'opacity', + runningOnCompositor: true, + } + ], + withGeometric: [ + { + property: 'opacity', + runningOnCompositor: true, + } + ] + } + }, + { + frames: { + transform: ['translate(0px)', 'translate(100px)'] + }, + expected: { + withoutGeometric: [ + { + property: 'transform', + runningOnCompositor: true + } + ], + withGeometric: [ + { + property: 'width', + runningOnCompositor: false, + }, + { + property: 'transform', + runningOnCompositor: true, + } + ] + } + } + ] + }, + { + desc: 'opacity and translate with geometric keyframes', + animations: [ + { + frames: { + opacity: [0, 1] + }, + expected: { + withoutGeometric: [ + { + property: 'opacity', + runningOnCompositor: true, + } + ], + withGeometric: [ + { + property: 'opacity', + runningOnCompositor: true, + } + ] + } + }, + { + frames: { + translate: ['0px', '100px'] + }, + expected: { + withoutGeometric: [ + { + property: 'translate', + runningOnCompositor: true + } + ], + withGeometric: [ + { + property: 'width', + runningOnCompositor: false, + }, + { + property: 'translate', + runningOnCompositor: true, + } + ] + } + } + ] + }, + ].forEach(subtest => { + promise_test(async t => { + var div = addDiv(t, { class: 'compositable' }); + var animations = subtest.animations.map(anim => { + var animation = div.animate(anim.frames, 100 * MS_PER_SEC); + + // Bind expected values to animation object. + animation.expected = anim.expected; + return animation; + }); + await waitForPaints(); + // First, all animations are running on compositor. + animations.forEach(anim => { + assert_animation_property_state_equals( + anim.effect.getProperties(), + anim.expected.withoutGeometric); + }); + + // Add a 'width' property to animations[1]. + var keyframes = animations[1].effect.getKeyframes(); + + keyframes[0].width = '100px'; + keyframes[1].width = '200px'; + + animations[1].effect.setKeyframes(keyframes); + await waitForFrame(); + + // Now the transform animation is not running on compositor because of + // the 'width' property. + animations.forEach(anim => { + assert_animation_property_state_equals( + anim.effect.getProperties(), + anim.expected.withGeometric); + }); + + // Remove the 'width' property from animations[1]. + var keyframes = animations[1].effect.getKeyframes(); + + delete keyframes[0].width; + delete keyframes[1].width; + + animations[1].effect.setKeyframes(keyframes); + await waitForFrame(); + + // Finally, all animations are running on compositor. + animations.forEach(anim => { + assert_animation_property_state_equals( + anim.effect.getProperties(), + anim.expected.withoutGeometric); + }); + }, 'Multiple animations with geometric property: ' + subtest.desc); + }); +} + +// Tests adding/removing 'width' animation on the same element which has async +// animations. +function testMultipleAnimationsWithGeometricAnimations() { + [ + { + desc: 'transform', + animations: [ + { + frames: { + transform: ['translate(0px)', 'translate(100px)'] + }, + expected: [ + { + property: 'transform', + runningOnCompositor: true, + } + ] + }, + ] + }, + { + desc: 'translate', + animations: [ + { + frames: { + translate: ['0px', '100px'] + }, + expected: [ + { + property: 'translate', + runningOnCompositor: true, + } + ] + }, + ] + }, + { + desc: 'opacity', + animations: [ + { + frames: { + opacity: [0, 1] + }, + expected: [ + { + property: 'opacity', + runningOnCompositor: true + } + ] + }, + ] + }, + { + desc: 'opacity, transform, and translate', + animations: [ + { + frames: { + transform: ['translate(0px)', 'translate(100px)'] + }, + expected: [ + { + property: 'transform', + runningOnCompositor: true, + } + ] + }, + { + frames: { + translate: ['0px', '100px'] + }, + expected: [ + { + property: 'translate', + runningOnCompositor: true, + } + ] + }, + { + frames: { + opacity: [0, 1] + }, + expected: [ + { + property: 'opacity', + runningOnCompositor: true, + } + ] + } + ], + }, + ].forEach(subtest => { + promise_test(async t => { + var div = addDiv(t, { class: 'compositable' }); + var animations = subtest.animations.map(anim => { + var animation = div.animate(anim.frames, 100 * MS_PER_SEC); + + // Bind expected values to animation object. + animation.expected = anim.expected; + return animation; + }); + + var widthAnimation; + + await waitForPaints(); + animations.forEach(anim => { + assert_all_properties_running_on_compositor( + anim.effect.getProperties(), + anim.expected); + }); + + // Append 'width' animation on the same element. + widthAnimation = div.animate({ width: ['100px', '200px'] }, + 100 * MS_PER_SEC); + await waitForFrame(); + + // Now transform animations are not running on compositor because of + // the 'width' animation. + animations.forEach(anim => { + assert_animation_property_state_equals( + anim.effect.getProperties(), + anim.expected); + }); + // Remove the 'width' animation. + widthAnimation.cancel(); + await waitForFrame(); + + // Now all animations are running on compositor. + animations.forEach(anim => { + assert_all_properties_running_on_compositor( + anim.effect.getProperties(), + anim.expected); + }); + }, 'Multiple async animations and geometric animation: ' + subtest.desc); + }); +} + +function testSmallElements() { + [ + { + desc: 'opacity on small element', + frames: { + opacity: [0, 1] + }, + style: { style: 'width: 8px; height: 8px; background-color: red;' + + // We need to set transform here to try creating an + // individual frame for this opacity element. + // Without this, this small element is created on the same + // nsIFrame of mochitest iframe, i.e. the document which are + // running this test, as a result the layer corresponding + // to the frame is sent to compositor. + 'transform: translateX(100px);' }, + expected: [ + { + property: 'opacity', + runningOnCompositor: true + } + ] + }, + { + desc: 'transform on small element', + frames: { + transform: ['translate(0px)', 'translate(100px)'] + }, + style: { style: 'width: 8px; height: 8px; background-color: red;' }, + expected: [ + { + property: 'transform', + runningOnCompositor: true + } + ] + }, + { + desc: 'translate on small element', + frames: { + translate: ['0px', '100px'] + }, + style: { style: 'width: 8px; height: 8px; background-color: red;' }, + expected: [ + { + property: 'translate', + runningOnCompositor: true + } + ] + }, + ].forEach(subtest => { + promise_test(async t => { + var div = addDiv(t, subtest.style); + var animation = div.animate(subtest.frames, 100 * MS_PER_SEC); + await waitForPaints(); + + assert_animation_property_state_equals( + animation.effect.getProperties(), + subtest.expected); + }, subtest.desc); + }); +} + +function testSynchronizedAnimations() { + promise_test(async t => { + const elemA = addDiv(t, { class: 'compositable' }); + const elemB = addDiv(t, { class: 'compositable' }); + + const animA = elemA.animate({ transform: [ 'translate(0px)', + 'translate(100px)' ] }, + 100 * MS_PER_SEC); + const animB = elemB.animate({ marginLeft: [ '0px', '100px' ] }, + 100 * MS_PER_SEC); + + await Promise.all([animA.ready, animB.ready]); + await waitForPaints(); + + assert_animation_property_state_equals( + animA.effect.getProperties(), + [ { property: 'transform', + runningOnCompositor: true, + } ]); + }, 'Animations created within the same tick are synchronized' + + ' (compositor animation created first)'); + + promise_test(async t => { + const elemA = addDiv(t, { class: 'compositable' }); + const elemB = addDiv(t, { class: 'compositable' }); + const elemC = addDiv(t, { class: 'compositable' }); + + const animA = elemA.animate({ transform: [ 'translate(0px)', + 'translate(100px)' ] }, + 100 * MS_PER_SEC); + const animB = elemB.animate({ translate: [ '0px', '100px' ] }, + 100 * MS_PER_SEC); + const animC = elemC.animate({ marginLeft: [ '0px', '100px' ] }, + 100 * MS_PER_SEC); + + await Promise.all([animA.ready, animB.ready, animC.ready]); + await waitForPaints(); + + assert_animation_property_state_equals( + animA.effect.getProperties(), + [ + { property: 'transform', + runningOnCompositor: true, + } ]); + assert_animation_property_state_equals( + animB.effect.getProperties(), + [ + { property: 'translate', + runningOnCompositor: true, + } ]); + }, 'Animations created within the same tick are synchronized' + + ' (compositor animation created first/second)'); + + promise_test(async t => { + const elemA = addDiv(t, { class: 'compositable' }); + const elemB = addDiv(t, { class: 'compositable' }); + const elemC = addDiv(t, { class: 'compositable' }); + + const animA = elemA.animate({ marginLeft: [ '0px', '100px' ] }, + 100 * MS_PER_SEC); + const animB = elemB.animate({ transform: [ 'translate(0px)', + 'translate(100px)' ] }, + 100 * MS_PER_SEC); + const animC = elemC.animate({ translate: [ '0px', '100px' ] }, + 100 * MS_PER_SEC); + + await Promise.all([animA.ready, animB.ready, animC.ready]); + await waitForPaints(); + + assert_animation_property_state_equals( + animB.effect.getProperties(), + [ { property: 'transform', + runningOnCompositor: true, + } ]); + assert_animation_property_state_equals( + animC.effect.getProperties(), + [ + { property: 'translate', + runningOnCompositor: true, + } ]); + }, 'Animations created within the same tick are synchronized' + + ' (compositor animation created second/third)'); + + promise_test(async t => { + const attrs = { class: 'compositable', + style: 'transition: all 100s' }; + const elemA = addDiv(t, attrs); + const elemB = addDiv(t, attrs); + elemA.style.transform = 'translate(0px)'; + elemB.style.marginLeft = '0px'; + getComputedStyle(elemA).transform; + getComputedStyle(elemB).marginLeft; + + // Generally the sequence of steps is as follows: + // + // Tick -> requestAnimationFrame -> Style -> Paint -> Events (-> Tick...) + // + // In this test we want to set up two transitions during the "Events" + // stage but only flush style for one such that the second one is actually + // generated during the "Style" stage of the *next* tick. + // + // Web content often generates transitions in this way (that is, it doesn't + // pay regard to when style is flushed and nor should it). However, we + // still want transitions generated in this way to be synchronized. + let timeForFirstFrame; + await waitForIdle(); + + timeForFirstFrame = document.timeline.currentTime; + elemA.style.transform = 'translate(100px)'; + // Flush style to trigger first transition + getComputedStyle(elemA).transform; + elemB.style.marginLeft = '100px'; + // DON'T flush style here (this includes calling getAnimations!) + await waitForFrame(); + + assert_not_equals(timeForFirstFrame, document.timeline.currentTime, + 'Should be on the other side of a tick'); + // Wait another tick so we can let the transition be started + // by regular style resolution. + await waitForFrame(); + + const transitionA = elemA.getAnimations()[0]; + assert_animation_property_state_equals( + transitionA.effect.getProperties(), + [ { property: 'transform', + runningOnCompositor: true, + } ]); + }, 'Transitions created before and after a tick are synchronized'); + + promise_test(async t => { + const elemA = addDiv(t, { class: 'compositable' }); + const elemB = addDiv(t, { class: 'compositable' }); + + const animA = elemA.animate({ transform: [ 'translate(0px)', + 'translate(100px)' ], + opacity: [ 0, 1 ] }, + 100 * MS_PER_SEC); + const animB = elemB.animate({ marginLeft: [ '0px', '100px' ] }, + 100 * MS_PER_SEC); + + await Promise.all([animA.ready, animB.ready]); + await waitForPaints(); + + assert_animation_property_state_equals( + animA.effect.getProperties(), + [ { property: 'transform', + runningOnCompositor: true, + }, + { property: 'opacity', + runningOnCompositor: true + } ]); + }, 'Opacity animations on the same element continue running on the' + + ' compositor when transform animations are synchronized with geometric' + + ' animations'); + + promise_test(async t => { + const transitionElem = addDiv(t, { + style: 'margin-left: 0px; transition: margin-left 100s', + }); + getComputedStyle(transitionElem).marginLeft; + + await waitForFrame(); + + transitionElem.style.marginLeft = '100px'; + const cssTransition = transitionElem.getAnimations()[0]; + + const animationElem = addDiv(t, { + class: 'compositable', + style: 'animation: translate 100s', + }); + const cssAnimation = animationElem.getAnimations()[0]; + + await Promise.all([cssTransition.ready, cssAnimation.ready]); + await waitForPaints(); + + assert_animation_property_state_equals(cssAnimation.effect.getProperties(), + [{ property: 'transform', + runningOnCompositor: true }]); + }, 'CSS Animations are NOT synchronized with CSS Transitions'); + + promise_test(async t => { + const elemA = addDiv(t, { class: 'compositable' }); + const elemB = addDiv(t, { class: 'compositable' }); + + const animA = elemA.animate({ marginLeft: [ '0px', '100px' ] }, + 100 * MS_PER_SEC); + await animA.ready; + await waitForPaints(); + + let animB = elemB.animate({ transform: [ 'translate(0px)', + 'translate(100px)' ] }, + 100 * MS_PER_SEC); + await animB.ready; + await waitForPaints(); + + assert_animation_property_state_equals( + animB.effect.getProperties(), + [ { property: 'transform', + runningOnCompositor: true } ]); + }, 'Transform animations are NOT synchronized with geometric animations' + + ' started in the previous frame'); + + promise_test(async t => { + const elemA = addDiv(t, { class: 'compositable' }); + const elemB = addDiv(t, { class: 'compositable' }); + + const animA = elemA.animate({ transform: [ 'translate(0px)', + 'translate(100px)' ] }, + 100 * MS_PER_SEC); + await animA.ready; + await waitForPaints(); + + let animB = elemB.animate({ marginLeft: [ '0px', '100px' ] }, + 100 * MS_PER_SEC); + await animB.ready; + await waitForPaints(); + + assert_animation_property_state_equals( + animA.effect.getProperties(), + [ { property: 'transform', + runningOnCompositor: true } ]); + }, 'Transform animations are NOT synchronized with geometric animations' + + ' started in the next frame'); + + promise_test(async t => { + const elemA = addDiv(t, { class: 'compositable' }); + const elemB = addDiv(t, { class: 'compositable' }); + + const animA = elemA.animate({ transform: [ 'translate(0px)', + 'translate(100px)' ] }, + 100 * MS_PER_SEC); + const animB = elemB.animate({ marginLeft: [ '0px', '100px' ] }, + 100 * MS_PER_SEC); + animB.pause(); + + await animA.ready; + await waitForPaints(); + + assert_animation_property_state_equals( + animA.effect.getProperties(), + [ { property: 'transform', runningOnCompositor: true } ]); + }, 'Paused animations are not synchronized'); + + promise_test(async t => { + const elemA = addDiv(t, { class: 'compositable' }); + const elemB = addDiv(t, { class: 'compositable' }); + + const animA = elemA.animate({ transform: [ 'translate(0px)', + 'translate(100px)' ] }, + 100 * MS_PER_SEC); + const animB = elemB.animate({ marginLeft: [ '0px', '100px' ] }, + 100 * MS_PER_SEC); + + // Seek one of the animations so that their start times will differ + animA.currentTime = 5000; + + await Promise.all([animA.ready, animB.ready]); + await waitForPaints(); + + assert_not_equals(animA.startTime, animB.startTime, + 'Animations should have different start times'); + assert_animation_property_state_equals( + animA.effect.getProperties(), + [ { property: 'transform', + runningOnCompositor: true, + } ]); + }, 'Animations are synchronized based on when they are started' + + ' and NOT their start time'); + + promise_test(async t => { + const elemA = addDiv(t, { class: 'compositable' }); + const elemB = addDiv(t, { class: 'compositable' }); + + const animA = elemA.animate({ transform: [ 'translate(0px)', + 'translate(100px)' ] }, + 100 * MS_PER_SEC); + const animB = elemB.animate({ marginLeft: [ '0px', '100px' ] }, + 100 * MS_PER_SEC); + + await Promise.all([animA.ready, animB.ready]); + await waitForPaints(); + + assert_animation_property_state_equals( + animA.effect.getProperties(), + [ { property: 'transform', + runningOnCompositor: true } ]); + // Restart animation + animA.pause(); + animA.play(); + await animA.ready; + await waitForPaints(); + + assert_animation_property_state_equals( + animA.effect.getProperties(), + [ { property: 'transform', + runningOnCompositor: true } ]); + }, 'An initially synchronized animation may be unsynchronized if restarted'); + + promise_test(async t => { + const elemA = addDiv(t, { class: 'compositable' }); + const elemB = addDiv(t, { class: 'compositable' }); + + const animA = elemA.animate({ transform: [ 'translate(0px)', + 'translate(100px)' ] }, + 100 * MS_PER_SEC); + const animB = elemB.animate({ marginLeft: [ '0px', '100px' ] }, + 100 * MS_PER_SEC); + + // Clear target effect + animB.effect.target = null; + + await Promise.all([animA.ready, animB.ready]); + await waitForPaints(); + + assert_animation_property_state_equals( + animA.effect.getProperties(), + [ { property: 'transform', + runningOnCompositor: true } ]); + }, 'A geometric animation with no target element is not synchronized'); +} + +function testTooLargeFrame() { + [ + { + property: 'transform', + frames: { transform: ['translate(0px)', 'translate(100px)'] }, + }, + { + property: 'translate', + frames: { translate: ['0px', '100px'] }, + }, + ].forEach(subtest => { + promise_test(async t => { + var animation = addDivAndAnimate(t, + { class: 'compositable' }, + subtest.frames, + 100 * MS_PER_SEC); + await waitForPaints(); + + assert_animation_property_state_equals( + animation.effect.getProperties(), + [ { property: subtest.property, runningOnCompositor: true } ]); + animation.effect.target.style = 'width: 10000px; height: 10000px'; + await waitForFrame(); + + // viewport depends on test environment. + var expectedWarning = new RegExp( + "Animation cannot be run on the compositor because the area of the frame " + + "\\(\\d+\\) is too large relative to the viewport " + + "\\(larger than \\d+\\)"); + assert_animation_property_state_equals( + animation.effect.getProperties(), + [ { + property: subtest.property, + runningOnCompositor: false, + warning: expectedWarning + } ]); + animation.effect.target.style = 'width: 100px; height: 100px'; + await waitForFrame(); + + // With WebRender we appear to stick to the previous layerization decision + // after changing the bounds back to a smaller object. + const isWebRender = + SpecialPowers.DOMWindowUtils.layerManagerType.startsWith('WebRender'); + assert_animation_property_state_equals( + animation.effect.getProperties(), + [ { property: subtest.property, runningOnCompositor: !isWebRender } ]); + }, subtest.property + ' on too big element - area'); + + promise_test(async t => { + var animation = addDivAndAnimate(t, + { class: 'compositable' }, + subtest.frames, + 100 * MS_PER_SEC); + await waitForPaints(); + + assert_animation_property_state_equals( + animation.effect.getProperties(), + [ { property: subtest.property, runningOnCompositor: true } ]); + animation.effect.target.style = 'width: 20000px; height: 1px'; + await waitForFrame(); + + // viewport depends on test environment. + var expectedWarning = new RegExp( + "Animation cannot be run on the compositor because the frame size " + + "\\(20000, 1\\) is too large relative to the viewport " + + "\\(larger than \\(\\d+, \\d+\\)\\) or larger than the " + + "maximum allowed value \\(\\d+, \\d+\\)"); + assert_animation_property_state_equals( + animation.effect.getProperties(), + [ { + property: subtest.property, + runningOnCompositor: false, + warning: expectedWarning + } ]); + animation.effect.target.style = 'width: 100px; height: 100px'; + await waitForFrame(); + + const isWebRender = + SpecialPowers.DOMWindowUtils.layerManagerType.startsWith('WebRender'); + assert_animation_property_state_equals( + animation.effect.getProperties(), + [ { property: subtest.property, runningOnCompositor: !isWebRender } ]); + }, subtest.property + ' on too big element - dimensions'); + }); +} + +function testTransformSVG() { + [ + { + property: 'transform', + frames: { transform: ['translate(0px)', 'translate(100px)'] }, + }, + { + property: 'translate', + frames: { translate: ['0px', '100px'] }, + }, + { + property: 'rotate', + frames: { rotate: ['0deg', '45deg'] }, + }, + { + property: 'scale', + frames: { scale: ['1', '2'] }, + }, + ].forEach(subtest => { + promise_test(async t => { + var svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); + svg.setAttribute('width', '100'); + svg.setAttribute('height', '100'); + var rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect'); + rect.setAttribute('width', '100'); + rect.setAttribute('height', '100'); + rect.setAttribute('fill', 'red'); + svg.appendChild(rect); + document.body.appendChild(svg); + t.add_cleanup(() => { + svg.remove(); + }); + + var animation = svg.animate(subtest.frames, 100 * MS_PER_SEC); + await waitForPaints(); + + assert_animation_property_state_equals( + animation.effect.getProperties(), + [ { property: subtest.property, runningOnCompositor: true } ]); + svg.setAttribute('transform', 'translate(10, 20)'); + await waitForFrame(); + + assert_animation_property_state_equals( + animation.effect.getProperties(), + [ { + property: subtest.property, + runningOnCompositor: false, + warning: 'CompositorAnimationWarningTransformSVG' + } ]); + svg.removeAttribute('transform'); + await waitForFrame(); + + assert_animation_property_state_equals( + animation.effect.getProperties(), + [ { property: subtest.property, runningOnCompositor: true } ]); + }, subtest.property + ' of nsIFrame with SVG transform'); + }); +} + +function testImportantRuleOverride() { + promise_test(async t => { + const elem = addDiv(t, { class: 'compositable' }); + const anim = elem.animate({ translate: [ '0px', '100px' ], + rotate: ['0deg', '90deg'] }, + 100 * MS_PER_SEC); + + await waitForAnimationReadyToRestyle(anim); + await waitForPaints(); + + assert_animation_property_state_equals( + anim.effect.getProperties(), + [ { property: 'translate', runningOnCompositor: true }, + { property: 'rotate', runningOnCompositor: true } ] + ); + + elem.style.setProperty('rotate', '45deg', 'important'); + getComputedStyle(elem).rotate; + + await waitForFrame(); + + assert_animation_property_state_equals( + anim.effect.getProperties(), + [ + { + property: 'translate', + runningOnCompositor: false, + warning: + 'CompositorAnimationWarningTransformIsBlockedByImportantRules' + }, + { + property: 'rotate', + runningOnCompositor: false, + warning: + 'CompositorAnimationWarningTransformIsBlockedByImportantRules' + }, + ] + ); + }, 'The animations of transform-like properties are not running on the ' + + 'compositor because any of the properties has important rules'); +} + +function testCurrentColor() { + if (SpecialPowers.DOMWindowUtils.layerManagerType.startsWith('WebRender')) { + return; // skip this test until bug 1510030 landed. + } + promise_test(async t => { + const animation = addDivAndAnimate(t, { class: 'compositable' }, + { backgroundColor: [ 'currentColor', + 'red' ] }, + 100 * MS_PER_SEC); + await waitForPaints(); + + assert_animation_property_state_equals( + animation.effect.getProperties(), + [ { property: 'background-color', + runningOnCompositor: false, + warning: 'CompositorAnimationWarningHasCurrentColor' + } ]); + }, 'Background color animations with `current-color` don\'t run on the ' + + 'compositor'); +} + +function start() { + var bundleService = SpecialPowers.Cc['@mozilla.org/intl/stringbundle;1'] + .getService(SpecialPowers.Ci.nsIStringBundleService); + gStringBundle = bundleService + .createBundle("chrome://global/locale/layout_errors.properties"); + + testBasicOperation(); + testKeyframesWithGeometricProperties(); + testSetOfGeometricProperties(); + testStyleChanges(); + testIdChanges(); + testMultipleAnimations(); + testMultipleAnimationsWithGeometricKeyframes(); + testMultipleAnimationsWithGeometricAnimations(); + testSmallElements(); + testSynchronizedAnimations(); + testTooLargeFrame(); + testTransformSVG(); + testImportantRuleOverride(); + testCurrentColor(); + + promise_test(async t => { + var div = addDiv(t, { class: 'compositable', + style: 'animation: fade 100s' }); + var cssAnimation = div.getAnimations()[0]; + var scriptAnimation = div.animate({ opacity: [ 1, 0 ] }, 100 * MS_PER_SEC); + + await waitForPaints(); + assert_animation_property_state_equals( + cssAnimation.effect.getProperties(), + [ { property: 'opacity', runningOnCompositor: true } ]); + assert_animation_property_state_equals( + scriptAnimation.effect.getProperties(), + [ { property: 'opacity', runningOnCompositor: true } ]); + }, 'overridden animation'); + + promise_test(async t => { + const keyframes = { + width: [ '100px', '200px' ], + transform: [ 'translate(0px)', 'translate(100px)' ], + "--foo": ["--bar", "--baz"], + }; + const animation = addDivAndAnimate(t, { class: 'compositable' }, + keyframes, 100 * MS_PER_SEC); + await waitForPaints(); + + assert_true(true, "Didn't crash"); + }, 'Warning with custom props'); + + done(); +} + +</script> + +</body> diff --git a/dom/animation/test/chrome/test_animation_properties.html b/dom/animation/test/chrome/test_animation_properties.html new file mode 100644 index 0000000000..497b04e068 --- /dev/null +++ b/dom/animation/test/chrome/test_animation_properties.html @@ -0,0 +1,837 @@ +<!doctype html> +<head> +<meta charset=utf-8> +<title>Bug 1254419 - Test the values returned by + KeyframeEffect.getProperties()</title> +<script type="application/javascript" src="../testharness.js"></script> +<script type="application/javascript" src="../testharnessreport.js"></script> +<script type="application/javascript" src="../testcommon.js"></script> +</head> +<body> +<a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1254419" + target="_blank">Mozilla Bug 1254419</a> +<div id="log"></div> +<style> +@property --my-color { + syntax: "<color>"; + inherits: true; + initial-value: "gold"; +} + +:root { + --var-100px: 100px; + --var-100px-200px: 100px 200px; +} +div { + font-size: 10px; /* For calculating em-based units */ +} +</style> +<script> +'use strict'; + +var gTests = [ + + // --------------------------------------------------------------------- + // + // Tests for property-indexed specifications + // + // --------------------------------------------------------------------- + + { desc: 'a one-property two-value property-indexed specification', + frames: { left: ['10px', '20px'] }, + expected: [ { property: 'left', + values: [ valueFormat(0, '10px', 'replace', 'linear'), + valueFormat(1, '20px', 'replace') ] } ] + }, + { desc: 'a one-shorthand-property two-value property-indexed' + + ' specification', + frames: { margin: ['10px', '10px 20px 30px 40px'] }, + expected: [ { property: 'margin-top', + values: [ valueFormat(0, '10px', 'replace', 'linear'), + valueFormat(1, '10px', 'replace') ] }, + { property: 'margin-right', + values: [ valueFormat(0, '10px', 'replace', 'linear'), + valueFormat(1, '20px', 'replace') ] }, + { property: 'margin-bottom', + values: [ valueFormat(0, '10px', 'replace', 'linear'), + valueFormat(1, '30px', 'replace') ] }, + { property: 'margin-left', + values: [ valueFormat(0, '10px', 'replace', 'linear'), + valueFormat(1, '40px', 'replace') ] } ] + }, + { desc: 'a two-property (one shorthand and one of its longhand' + + ' components) two-value property-indexed specification', + frames: { marginTop: ['50px', '60px'], + margin: ['10px', '10px 20px 30px 40px'] }, + expected: [ { property: 'margin-top', + values: [ valueFormat(0, '50px', 'replace', 'linear'), + valueFormat(1, '60px', 'replace') ] }, + { property: 'margin-right', + values: [ valueFormat(0, '10px', 'replace', 'linear'), + valueFormat(1, '20px', 'replace') ] }, + { property: 'margin-bottom', + values: [ valueFormat(0, '10px', 'replace', 'linear'), + valueFormat(1, '30px', 'replace') ] }, + { property: 'margin-left', + values: [ valueFormat(0, '10px', 'replace', 'linear'), + valueFormat(1, '40px', 'replace') ] } ] + }, + { desc: 'a two-property property-indexed specification with different' + + ' numbers of values', + frames: { left: ['10px', '20px', '30px'], + top: ['40px', '50px'] }, + expected: [ { property: 'left', + values: [ valueFormat(0, '10px', 'replace', 'linear'), + valueFormat(0.5, '20px', 'replace', 'linear'), + valueFormat(1, '30px', 'replace') ] }, + { property: 'top', + values: [ valueFormat(0, '40px', 'replace', 'linear'), + valueFormat(1, '50px', 'replace') ] } ] + }, + { desc: 'a property-indexed specification with an invalid value', + frames: { left: ['10px', '20px', '30px', '40px', '50px'], + top: ['15px', '25px', 'invalid', '45px', '55px'] }, + expected: [ { property: 'left', + values: [ valueFormat(0, '10px', 'replace', 'linear'), + valueFormat(0.25, '20px', 'replace', 'linear'), + valueFormat(0.5, '30px', 'replace', 'linear'), + valueFormat(0.75, '40px', 'replace', 'linear'), + valueFormat(1, '50px', 'replace') ] }, + { property: 'top', + values: [ valueFormat(0, '15px', 'replace', 'linear'), + valueFormat(0.25, '25px', 'replace', 'linear'), + valueFormat(0.75, '45px', 'replace', 'linear'), + valueFormat(1, '55px', 'replace') ] } ] + }, + { desc: 'a one-property two-value property-indexed specification that' + + ' needs to stringify its values', + frames: { opacity: [0, 1] }, + expected: [ { property: 'opacity', + values: [ valueFormat(0, '0', 'replace', 'linear'), + valueFormat(1, '1', 'replace') ] } ] + }, + { desc: 'a property-indexed keyframe where a lesser shorthand precedes' + + ' a greater shorthand', + frames: { borderLeft: [ '1px solid rgb(1, 2, 3)', + '2px solid rgb(4, 5, 6)' ], + border: [ '3px dotted rgb(7, 8, 9)', + '4px dashed rgb(10, 11, 12)' ] }, + expected: [ { property: 'border-bottom-color', + values: [ valueFormat(0, 'rgb(7, 8, 9)', 'replace', 'linear'), + valueFormat(1, 'rgb(10, 11, 12)', 'replace') ] }, + { property: 'border-left-color', + values: [ valueFormat(0, 'rgb(1, 2, 3)', 'replace', 'linear'), + valueFormat(1, 'rgb(4, 5, 6)', 'replace') ] }, + { property: 'border-right-color', + values: [ valueFormat(0, 'rgb(7, 8, 9)', 'replace', 'linear'), + valueFormat(1, 'rgb(10, 11, 12)', 'replace') ] }, + { property: 'border-top-color', + values: [ valueFormat(0, 'rgb(7, 8, 9)', 'replace', 'linear'), + valueFormat(1, 'rgb(10, 11, 12)', 'replace') ] }, + { property: 'border-bottom-width', + values: [ valueFormat(0, '3px', 'replace', 'linear'), + valueFormat(1, '4px', 'replace') ] }, + { property: 'border-left-width', + values: [ valueFormat(0, '1px', 'replace', 'linear'), + valueFormat(1, '2px', 'replace') ] }, + { property: 'border-right-width', + values: [ valueFormat(0, '3px', 'replace', 'linear'), + valueFormat(1, '4px', 'replace') ] }, + { property: 'border-top-width', + values: [ valueFormat(0, '3px', 'replace', 'linear'), + valueFormat(1, '4px', 'replace') ] }, + { property: 'border-bottom-style', + values: [ valueFormat(0, 'dotted', 'replace', 'linear'), + valueFormat(1, 'dashed', 'replace') ] }, + { property: 'border-left-style', + values: [ valueFormat(0, 'solid', 'replace', 'linear'), + valueFormat(1, 'solid', 'replace') ] }, + { property: 'border-right-style', + values: [ valueFormat(0, 'dotted', 'replace', 'linear'), + valueFormat(1, 'dashed', 'replace') ] }, + { property: 'border-top-style', + values: [ valueFormat(0, 'dotted', 'replace', 'linear'), + valueFormat(1, 'dashed', 'replace') ] }, + { property: 'border-image-outset', + values: [ valueFormat(0, '0', 'replace', 'linear'), + valueFormat(1, '0', 'replace') ] }, + { property: 'border-image-repeat', + values: [ valueFormat(0, 'stretch', 'replace', 'linear'), + valueFormat(1, 'stretch', 'replace') ] }, + { property: 'border-image-slice', + values: [ valueFormat(0, '100%', 'replace', 'linear'), + valueFormat(1, '100%', 'replace') ] }, + { property: 'border-image-source', + values: [ valueFormat(0, 'none', 'replace', 'linear'), + valueFormat(1, 'none', 'replace') ] }, + { property: 'border-image-width', + values: [ valueFormat(0, '1', 'replace', 'linear'), + valueFormat(1, '1', 'replace') ] }, + ] + }, + { desc: 'a property-indexed keyframe where a greater shorthand precedes' + + ' a lesser shorthand', + frames: { border: [ '3px dotted rgb(7, 8, 9)', + '4px dashed rgb(10, 11, 12)' ], + borderLeft: [ '1px solid rgb(1, 2, 3)', + '2px solid rgb(4, 5, 6)' ] }, + expected: [ { property: 'border-bottom-color', + values: [ valueFormat(0, 'rgb(7, 8, 9)', 'replace', 'linear'), + valueFormat(1, 'rgb(10, 11, 12)', 'replace') ] }, + { property: 'border-left-color', + values: [ valueFormat(0, 'rgb(1, 2, 3)', 'replace', 'linear'), + valueFormat(1, 'rgb(4, 5, 6)', 'replace') ] }, + { property: 'border-right-color', + values: [ valueFormat(0, 'rgb(7, 8, 9)', 'replace', 'linear'), + valueFormat(1, 'rgb(10, 11, 12)', 'replace') ] }, + { property: 'border-top-color', + values: [ valueFormat(0, 'rgb(7, 8, 9)', 'replace', 'linear'), + valueFormat(1, 'rgb(10, 11, 12)', 'replace') ] }, + { property: 'border-bottom-width', + values: [ valueFormat(0, '3px', 'replace', 'linear'), + valueFormat(1, '4px', 'replace') ] }, + { property: 'border-left-width', + values: [ valueFormat(0, '1px', 'replace', 'linear'), + valueFormat(1, '2px', 'replace') ] }, + { property: 'border-right-width', + values: [ valueFormat(0, '3px', 'replace', 'linear'), + valueFormat(1, '4px', 'replace') ] }, + { property: 'border-top-width', + values: [ valueFormat(0, '3px', 'replace', 'linear'), + valueFormat(1, '4px', 'replace') ] }, + { property: 'border-bottom-style', + values: [ valueFormat(0, 'dotted', 'replace', 'linear'), + valueFormat(1, 'dashed', 'replace') ] }, + { property: 'border-left-style', + values: [ valueFormat(0, 'solid', 'replace', 'linear'), + valueFormat(1, 'solid', 'replace') ] }, + { property: 'border-right-style', + values: [ valueFormat(0, 'dotted', 'replace', 'linear'), + valueFormat(1, 'dashed', 'replace') ] }, + { property: 'border-top-style', + values: [ valueFormat(0, 'dotted', 'replace', 'linear'), + valueFormat(1, 'dashed', 'replace') ] }, + { property: 'border-image-outset', + values: [ valueFormat(0, '0', 'replace', 'linear'), + valueFormat(1, '0', 'replace') ] }, + { property: 'border-image-repeat', + values: [ valueFormat(0, 'stretch', 'replace', 'linear'), + valueFormat(1, 'stretch', 'replace') ] }, + { property: 'border-image-slice', + values: [ valueFormat(0, '100%', 'replace', 'linear'), + valueFormat(1, '100%', 'replace') ] }, + { property: 'border-image-source', + values: [ valueFormat(0, 'none', 'replace', 'linear'), + valueFormat(1, 'none', 'replace') ] }, + { property: 'border-image-width', + values: [ valueFormat(0, '1', 'replace', 'linear'), + valueFormat(1, '1', 'replace') ] }, + ] + }, + { desc: 'custom registered property', + frames: { "--my-color": ['red', 'blue'] }, + expected: [ { property: '--my-color', + values: [ valueFormat(0, 'red', 'replace', 'linear'), + valueFormat(1, 'blue', 'replace') ] } ] + }, + + // --------------------------------------------------------------------- + // + // Tests for keyframe sequences + // + // --------------------------------------------------------------------- + + { desc: 'a keyframe sequence specification with repeated values at' + + ' offset 0/1 with different easings', + frames: [ { offset: 0.0, left: '100px', easing: 'ease' }, + { offset: 0.0, left: '200px', easing: 'ease' }, + { offset: 0.5, left: '300px', easing: 'linear' }, + { offset: 1.0, left: '400px', easing: 'ease-out' }, + { offset: 1.0, left: '500px', easing: 'step-end' } ], + expected: [ { property: 'left', + values: [ valueFormat(0, '100px', 'replace'), + valueFormat(0, '200px', 'replace', 'ease'), + valueFormat(0.5, '300px', 'replace', 'linear'), + valueFormat(1, '400px', 'replace'), + valueFormat(1, '500px', 'replace') ] } ] + }, + { desc: 'a one-property two-keyframe sequence', + frames: [ { offset: 0, left: '10px' }, + { offset: 1, left: '20px' } ], + expected: [ { property: 'left', + values: [ valueFormat(0, '10px', 'replace', 'linear'), + valueFormat(1, '20px', 'replace') ] } ] + }, + { desc: 'a two-property two-keyframe sequence', + frames: [ { offset: 0, left: '10px', top: '30px' }, + { offset: 1, left: '20px', top: '40px' } ], + expected: [ { property: 'left', + values: [ valueFormat(0, '10px', 'replace', 'linear'), + valueFormat(1, '20px', 'replace') ] }, + { property: 'top', + values: [ valueFormat(0, '30px', 'replace', 'linear'), + valueFormat(1, '40px', 'replace') ] } ] + }, + { desc: 'a one shorthand property two-keyframe sequence', + frames: [ { offset: 0, margin: '10px' }, + { offset: 1, margin: '20px 30px 40px 50px' } ], + expected: [ { property: 'margin-top', + values: [ valueFormat(0, '10px', 'replace', 'linear'), + valueFormat(1, '20px', 'replace') ] }, + { property: 'margin-right', + values: [ valueFormat(0, '10px', 'replace', 'linear'), + valueFormat(1, '30px', 'replace') ] }, + { property: 'margin-bottom', + values: [ valueFormat(0, '10px', 'replace', 'linear'), + valueFormat(1, '40px', 'replace') ] }, + { property: 'margin-left', + values: [ valueFormat(0, '10px', 'replace', 'linear'), + valueFormat(1, '50px', 'replace') ] } ] + }, + { desc: 'a two-property (a shorthand and one of its component longhands)' + + ' two-keyframe sequence', + frames: [ { offset: 0, margin: '10px', marginTop: '20px' }, + { offset: 1, marginTop: '70px', + margin: '30px 40px 50px 60px' } ], + expected: [ { property: 'margin-top', + values: [ valueFormat(0, '20px', 'replace', 'linear'), + valueFormat(1, '70px', 'replace') ] }, + { property: 'margin-right', + values: [ valueFormat(0, '10px', 'replace', 'linear'), + valueFormat(1, '40px', 'replace') ] }, + { property: 'margin-bottom', + values: [ valueFormat(0, '10px', 'replace', 'linear'), + valueFormat(1, '50px', 'replace') ] }, + { property: 'margin-left', + values: [ valueFormat(0, '10px', 'replace', 'linear'), + valueFormat(1, '60px', 'replace') ] } ] + }, + { desc: 'a keyframe sequence with duplicate values for a given interior' + + ' offset', + frames: [ { offset: 0.0, left: '10px' }, + { offset: 0.5, left: '20px' }, + { offset: 0.5, left: '30px' }, + { offset: 0.5, left: '40px' }, + { offset: 1.0, left: '50px' } ], + expected: [ { property: 'left', + values: [ valueFormat(0, '10px', 'replace', 'linear'), + valueFormat(0.5, '20px', 'replace'), + valueFormat(0.5, '40px', 'replace', 'linear'), + valueFormat(1, '50px', 'replace') ] } ] + }, + { desc: 'a keyframe sequence with duplicate values for offsets 0 and 1', + frames: [ { offset: 0, left: '10px' }, + { offset: 0, left: '20px' }, + { offset: 0, left: '30px' }, + { offset: 1, left: '40px' }, + { offset: 1, left: '50px' }, + { offset: 1, left: '60px' } ], + expected: [ { property: 'left', + values: [ valueFormat(0, '10px', 'replace'), + valueFormat(0, '30px', 'replace', 'linear'), + valueFormat(1, '40px', 'replace'), + valueFormat(1, '60px', 'replace') ] } ] + }, + { desc: 'a two-property four-keyframe sequence', + frames: [ { offset: 0, left: '10px' }, + { offset: 0, top: '20px' }, + { offset: 1, top: '30px' }, + { offset: 1, left: '40px' } ], + expected: [ { property: 'left', + values: [ valueFormat(0, '10px', 'replace', 'linear'), + valueFormat(1, '40px', 'replace') ] }, + { property: 'top', + values: [ valueFormat(0, '20px', 'replace', 'linear'), + valueFormat(1, '30px', 'replace') ] } ] + }, + { desc: 'a one-property keyframe sequence with some omitted offsets', + frames: [ { offset: 0.00, left: '10px' }, + { offset: 0.25, left: '20px' }, + { left: '30px' }, + { left: '40px' }, + { offset: 1.00, left: '50px' } ], + expected: [ { property: 'left', + values: [ valueFormat(0, '10px', 'replace', 'linear'), + valueFormat(0.25, '20px', 'replace', 'linear'), + valueFormat(0.5, '30px', 'replace', 'linear'), + valueFormat(0.75, '40px', 'replace', 'linear'), + valueFormat(1, '50px', 'replace') ] } ] + }, + { desc: 'a two-property keyframe sequence with some omitted offsets', + frames: [ { offset: 0.00, left: '10px', top: '20px' }, + { offset: 0.25, left: '30px' }, + { left: '40px' }, + { left: '50px', top: '60px' }, + { offset: 1.00, left: '70px', top: '80px' } ], + expected: [ { property: 'left', + values: [ valueFormat(0, '10px', 'replace', 'linear'), + valueFormat(0.25, '30px', 'replace', 'linear'), + valueFormat(0.5, '40px', 'replace', 'linear'), + valueFormat(0.75, '50px', 'replace', 'linear'), + valueFormat(1, '70px', 'replace') ] }, + { property: 'top', + values: [ valueFormat(0, '20px', 'replace', 'linear'), + valueFormat(0.75, '60px', 'replace', 'linear'), + valueFormat(1, '80px', 'replace') ] } ] + }, + { desc: 'a one-property keyframe sequence with all omitted offsets', + frames: [ { left: '10px' }, + { left: '20px' }, + { left: '30px' }, + { left: '40px' }, + { left: '50px' } ], + expected: [ { property: 'left', + values: [ valueFormat(0, '10px', 'replace', 'linear'), + valueFormat(0.25, '20px', 'replace', 'linear'), + valueFormat(0.5, '30px', 'replace', 'linear'), + valueFormat(0.75, '40px', 'replace', 'linear'), + valueFormat(1, '50px', 'replace') ] } ] + }, + { desc: 'a keyframe sequence with different easing values, but the' + + ' same easing value for a given offset', + frames: [ { offset: 0.0, easing: 'ease', left: '10px'}, + { offset: 0.0, easing: 'ease', top: '20px'}, + { offset: 0.5, easing: 'linear', left: '30px' }, + { offset: 0.5, easing: 'linear', top: '40px' }, + { offset: 1.0, easing: 'step-end', left: '50px' }, + { offset: 1.0, easing: 'step-end', top: '60px' } ], + expected: [ { property: 'left', + values: [ valueFormat(0, '10px', 'replace', 'ease'), + valueFormat(0.5, '30px', 'replace', 'linear'), + valueFormat(1, '50px', 'replace') ] }, + { property: 'top', + values: [ valueFormat(0, '20px', 'replace', 'ease'), + valueFormat(0.5, '40px', 'replace', 'linear'), + valueFormat(1, '60px', 'replace') ] } ] + }, + { desc: 'a one-property two-keyframe sequence that needs to' + + ' stringify its values', + frames: [ { offset: 0, opacity: 0 }, + { offset: 1, opacity: 1 } ], + expected: [ { property: 'opacity', + values: [ valueFormat(0, '0', 'replace', 'linear'), + valueFormat(1, '1', 'replace') ] } ] + }, + { desc: 'a keyframe sequence where shorthand precedes longhand', + frames: [ { offset: 0, margin: '10px', marginRight: '20px' }, + { offset: 1, margin: '30px' } ], + expected: [ { property: 'margin-top', + values: [ valueFormat(0, '10px', 'replace', 'linear'), + valueFormat(1, '30px', 'replace') ] }, + { property: 'margin-right', + values: [ valueFormat(0, '20px', 'replace', 'linear'), + valueFormat(1, '30px', 'replace') ] }, + { property: 'margin-bottom', + values: [ valueFormat(0, '10px', 'replace', 'linear'), + valueFormat(1, '30px', 'replace') ] }, + { property: 'margin-left', + values: [ valueFormat(0, '10px', 'replace', 'linear'), + valueFormat(1, '30px', 'replace') ] } ] + }, + { desc: 'a keyframe sequence where longhand precedes shorthand', + frames: [ { offset: 0, marginRight: '20px', margin: '10px' }, + { offset: 1, margin: '30px' } ], + expected: [ { property: 'margin-top', + values: [ valueFormat(0, '10px', 'replace', 'linear'), + valueFormat(1, '30px', 'replace') ] }, + { property: 'margin-right', + values: [ valueFormat(0, '20px', 'replace', 'linear'), + valueFormat(1, '30px', 'replace') ] }, + { property: 'margin-bottom', + values: [ valueFormat(0, '10px', 'replace', 'linear'), + valueFormat(1, '30px', 'replace') ] }, + { property: 'margin-left', + values: [ valueFormat(0, '10px', 'replace', 'linear'), + valueFormat(1, '30px', 'replace') ] } ] + }, + { desc: 'a keyframe sequence where lesser shorthand precedes greater' + + ' shorthand', + frames: [ { offset: 0, borderLeft: '1px solid rgb(1, 2, 3)', + border: '2px dotted rgb(4, 5, 6)' }, + { offset: 1, border: '3px dashed rgb(7, 8, 9)' } ], + expected: [ { property: 'border-bottom-color', + values: [ valueFormat(0, 'rgb(4, 5, 6)', 'replace', 'linear'), + valueFormat(1, 'rgb(7, 8, 9)', 'replace') ] }, + { property: 'border-left-color', + values: [ valueFormat(0, 'rgb(1, 2, 3)', 'replace', 'linear'), + valueFormat(1, 'rgb(7, 8, 9)', 'replace') ] }, + { property: 'border-right-color', + values: [ valueFormat(0, 'rgb(4, 5, 6)', 'replace', 'linear'), + valueFormat(1, 'rgb(7, 8, 9)', 'replace') ] }, + { property: 'border-top-color', + values: [ valueFormat(0, 'rgb(4, 5, 6)', 'replace', 'linear'), + valueFormat(1, 'rgb(7, 8, 9)', 'replace') ] }, + { property: 'border-bottom-width', + values: [ valueFormat(0, '2px', 'replace', 'linear'), + valueFormat(1, '3px', 'replace') ] }, + { property: 'border-left-width', + values: [ valueFormat(0, '1px', 'replace', 'linear'), + valueFormat(1, '3px', 'replace') ] }, + { property: 'border-right-width', + values: [ valueFormat(0, '2px', 'replace', 'linear'), + valueFormat(1, '3px', 'replace') ] }, + { property: 'border-top-width', + values: [ valueFormat(0, '2px', 'replace', 'linear'), + valueFormat(1, '3px', 'replace') ] }, + { property: 'border-bottom-style', + values: [ valueFormat(0, 'dotted', 'replace', 'linear'), + valueFormat(1, 'dashed', 'replace') ] }, + { property: 'border-left-style', + values: [ valueFormat(0, 'solid', 'replace', 'linear'), + valueFormat(1, 'dashed', 'replace') ] }, + { property: 'border-right-style', + values: [ valueFormat(0, 'dotted', 'replace', 'linear'), + valueFormat(1, 'dashed', 'replace') ] }, + { property: 'border-top-style', + values: [ valueFormat(0, 'dotted', 'replace', 'linear'), + valueFormat(1, 'dashed', 'replace') ] }, + { property: 'border-image-outset', + values: [ valueFormat(0, '0', 'replace', 'linear'), + valueFormat(1, '0', 'replace') ] }, + { property: 'border-image-repeat', + values: [ valueFormat(0, 'stretch', 'replace', 'linear'), + valueFormat(1, 'stretch', 'replace') ] }, + { property: 'border-image-slice', + values: [ valueFormat(0, '100%', 'replace', 'linear'), + valueFormat(1, '100%', 'replace') ] }, + { property: 'border-image-source', + values: [ valueFormat(0, 'none', 'replace', 'linear'), + valueFormat(1, 'none', 'replace') ] }, + { property: 'border-image-width', + values: [ valueFormat(0, '1', 'replace', 'linear'), + valueFormat(1, '1', 'replace') ] }, + ] + }, + { desc: 'a keyframe sequence where greater shorthand precedes' + + ' lesser shorthand', + frames: [ { offset: 0, border: '2px dotted rgb(4, 5, 6)', + borderLeft: '1px solid rgb(1, 2, 3)' }, + { offset: 1, border: '3px dashed rgb(7, 8, 9)' } ], + expected: [ { property: 'border-bottom-color', + values: [ valueFormat(0, 'rgb(4, 5, 6)', 'replace', 'linear'), + valueFormat(1, 'rgb(7, 8, 9)', 'replace') ] }, + { property: 'border-left-color', + values: [ valueFormat(0, 'rgb(1, 2, 3)', 'replace', 'linear'), + valueFormat(1, 'rgb(7, 8, 9)', 'replace') ] }, + { property: 'border-right-color', + values: [ valueFormat(0, 'rgb(4, 5, 6)', 'replace', 'linear'), + valueFormat(1, 'rgb(7, 8, 9)', 'replace') ] }, + { property: 'border-top-color', + values: [ valueFormat(0, 'rgb(4, 5, 6)', 'replace', 'linear'), + valueFormat(1, 'rgb(7, 8, 9)', 'replace') ] }, + { property: 'border-bottom-width', + values: [ valueFormat(0, '2px', 'replace', 'linear'), + valueFormat(1, '3px', 'replace') ] }, + { property: 'border-left-width', + values: [ valueFormat(0, '1px', 'replace', 'linear'), + valueFormat(1, '3px', 'replace') ] }, + { property: 'border-right-width', + values: [ valueFormat(0, '2px', 'replace', 'linear'), + valueFormat(1, '3px', 'replace') ] }, + { property: 'border-top-width', + values: [ valueFormat(0, '2px', 'replace', 'linear'), + valueFormat(1, '3px', 'replace') ] }, + { property: 'border-bottom-style', + values: [ valueFormat(0, 'dotted', 'replace', 'linear'), + valueFormat(1, 'dashed', 'replace') ] }, + { property: 'border-left-style', + values: [ valueFormat(0, 'solid', 'replace', 'linear'), + valueFormat(1, 'dashed', 'replace') ] }, + { property: 'border-right-style', + values: [ valueFormat(0, 'dotted', 'replace', 'linear'), + valueFormat(1, 'dashed', 'replace') ] }, + { property: 'border-top-style', + values: [ valueFormat(0, 'dotted', 'replace', 'linear'), + valueFormat(1, 'dashed', 'replace') ] }, + { property: 'border-image-outset', + values: [ valueFormat(0, '0', 'replace', 'linear'), + valueFormat(1, '0', 'replace') ] }, + { property: 'border-image-repeat', + values: [ valueFormat(0, 'stretch', 'replace', 'linear'), + valueFormat(1, 'stretch', 'replace') ] }, + { property: 'border-image-slice', + values: [ valueFormat(0, '100%', 'replace', 'linear'), + valueFormat(1, '100%', 'replace') ] }, + { property: 'border-image-source', + values: [ valueFormat(0, 'none', 'replace', 'linear'), + valueFormat(1, 'none', 'replace') ] }, + { property: 'border-image-width', + values: [ valueFormat(0, '1', 'replace', 'linear'), + valueFormat(1, '1', 'replace') ] }, + ] + }, + + // --------------------------------------------------------------------- + // + // Tests for unit conversion + // + // --------------------------------------------------------------------- + + { desc: 'em units are resolved to px values', + frames: { left: ['10em', '20em'] }, + expected: [ { property: 'left', + values: [ valueFormat(0, '100px', 'replace', 'linear'), + valueFormat(1, '200px', 'replace') ] } ] + }, + { desc: 'calc() expressions are resolved to the equivalent units', + frames: { left: ['calc(10em + 10px)', 'calc(10em + 10%)'] }, + expected: [ { property: 'left', + values: [ valueFormat(0, '110px', 'replace', 'linear'), + valueFormat(1, 'calc(10% + 100px)', 'replace') ] } ] + }, + + // --------------------------------------------------------------------- + // + // Tests for CSS variable handling conversion + // + // --------------------------------------------------------------------- + + { desc: 'CSS variables are resolved to their corresponding values', + frames: { left: ['10px', 'var(--var-100px)'] }, + expected: [ { property: 'left', + values: [ valueFormat(0, '10px', 'replace', 'linear'), + valueFormat(1, '100px', 'replace') ] } ] + }, + { desc: 'CSS variables in calc() expressions are resolved', + frames: { left: ['10px', 'calc(var(--var-100px) / 2 - 10%)'] }, + expected: [ { property: 'left', + values: [ valueFormat(0, '10px', 'replace', 'linear'), + valueFormat(1, 'calc(-10% + 50px)', 'replace') ] } ] + }, + { desc: 'CSS variables in shorthands are resolved to their corresponding' + + ' values', + frames: { margin: ['10px', 'var(--var-100px-200px)'] }, + expected: [ { property: 'margin-top', + values: [ valueFormat(0, '10px', 'replace', 'linear'), + valueFormat(1, '100px', 'replace') ] }, + { property: 'margin-right', + values: [ valueFormat(0, '10px', 'replace', 'linear'), + valueFormat(1, '200px', 'replace') ] }, + { property: 'margin-bottom', + values: [ valueFormat(0, '10px', 'replace', 'linear'), + valueFormat(1, '100px', 'replace') ] }, + { property: 'margin-left', + values: [ valueFormat(0, '10px', 'replace', 'linear'), + valueFormat(1, '200px', 'replace') ] } ] + }, + + // --------------------------------------------------------------------- + // + // Tests for properties that parse correctly but which we fail to + // convert to computed values. + // + // --------------------------------------------------------------------- + + { desc: 'a missing property in initial keyframe', + frames: [ { }, + { margin: '5px' } ], + expected: [ { property: 'margin-top', + values: [ valueFormat(0, undefined, 'replace', 'linear'), + valueFormat(1, '5px', 'replace') ] }, + { property: 'margin-right', + values: [ valueFormat(0, undefined, 'replace', 'linear'), + valueFormat(1, '5px', 'replace') ] }, + { property: 'margin-bottom', + values: [ valueFormat(0, undefined, 'replace', 'linear'), + valueFormat(1, '5px', 'replace') ] }, + { property: 'margin-left', + values: [ valueFormat(0, undefined, 'replace', 'linear'), + valueFormat(1, '5px', 'replace') ] } ] + }, + { desc: 'a missing property in initial keyframe and there are some ' + + 'keyframes with the same offset', + frames: [ { }, + { margin: '10px', offset: 0.5 }, + { margin: '20px', offset: 0.5 }, + { margin: '30px'} ], + expected: [ { property: 'margin-top', + values: [ valueFormat(0, undefined, 'replace', 'linear'), + valueFormat(0.5, '10px', 'replace'), + valueFormat(0.5, '20px', 'replace', 'linear'), + valueFormat(1, '30px', 'replace') ] }, + { property: 'margin-right', + values: [ valueFormat(0, undefined, 'replace', 'linear'), + valueFormat(0.5, '10px', 'replace'), + valueFormat(0.5, '20px', 'replace', 'linear'), + valueFormat(1, '30px', 'replace') ] }, + { property: 'margin-bottom', + values: [ valueFormat(0, undefined, 'replace', 'linear'), + valueFormat(0.5, '10px', 'replace'), + valueFormat(0.5, '20px', 'replace', 'linear'), + valueFormat(1, '30px', 'replace') ] }, + { property: 'margin-left', + values: [ valueFormat(0, undefined, 'replace', 'linear'), + valueFormat(0.5, '10px', 'replace'), + valueFormat(0.5, '20px', 'replace', 'linear'), + valueFormat(1, '30px', 'replace') ] } ] + }, + { desc: 'a missing property in final keyframe', + frames: [ { margin: '5px' }, + { } ], + expected: [ { property: 'margin-top', + values: [ valueFormat(0, '5px', 'replace', 'linear'), + valueFormat(1, undefined, 'replace') ] }, + { property: 'margin-right', + values: [ valueFormat(0, '5px', 'replace', 'linear'), + valueFormat(1, undefined, 'replace') ] }, + { property: 'margin-bottom', + values: [ valueFormat(0, '5px', 'replace', 'linear'), + valueFormat(1, undefined, 'replace') ] }, + { property: 'margin-left', + values: [ valueFormat(0, '5px', 'replace', 'linear'), + valueFormat(1, undefined, 'replace') ] } ] + }, + { desc: 'a missing property in final keyframe and there are some ' + + 'keyframes with the same offsets', + frames: [ { margin: '5px' }, + { margin: '10px', offset: 0.5 }, + { margin: '20px', offset: 0.5 }, + { } ], + expected: [ { property: 'margin-top', + values: [ valueFormat(0, '5px', 'replace', 'linear'), + valueFormat(0.5, '10px', 'replace'), + valueFormat(0.5, '20px', 'replace', 'linear'), + valueFormat(1, undefined, 'replace') ] }, + { property: 'margin-right', + values: [ valueFormat(0, '5px', 'replace', 'linear'), + valueFormat(0.5, '10px', 'replace'), + valueFormat(0.5, '20px', 'replace', 'linear'), + valueFormat(1, undefined, 'replace') ] }, + { property: 'margin-bottom', + values: [ valueFormat(0, '5px', 'replace', 'linear'), + valueFormat(0.5, '10px', 'replace'), + valueFormat(0.5, '20px', 'replace', 'linear'), + valueFormat(1, undefined, 'replace') ] }, + { property: 'margin-left', + values: [ valueFormat(0, '5px', 'replace', 'linear'), + valueFormat(0.5, '10px', 'replace'), + valueFormat(0.5, '20px', 'replace', 'linear'), + valueFormat(1, undefined, 'replace') ] } ] + }, + { desc: 'a missing property in final keyframe where it forms the last' + + ' segment in the series', + frames: [ { margin: '5px' }, + { marginLeft: '5px', + marginRight: '5px', + marginBottom: '5px' } ], + expected: [ { property: 'margin-bottom', + values: [ valueFormat(0, '5px', 'replace', 'linear'), + valueFormat(1, '5px', 'replace') ] }, + { property: 'margin-left', + values: [ valueFormat(0, '5px', 'replace', 'linear'), + valueFormat(1, '5px', 'replace') ] }, + { property: 'margin-right', + values: [ valueFormat(0, '5px', 'replace', 'linear'), + valueFormat(1, '5px', 'replace') ] }, + { property: 'margin-top', + values: [ valueFormat(0, '5px', 'replace', 'linear'), + valueFormat(1, undefined, 'replace') ] } ] + }, + { desc: 'a missing property in initial keyframe along with other values', + frames: [ { left: '10px' }, + { margin: '5px', left: '20px' } ], + expected: [ { property: 'left', + values: [ valueFormat(0, '10px', 'replace', 'linear'), + valueFormat(1, '20px', 'replace') ] }, + { property: 'margin-top', + values: [ valueFormat(0, undefined, 'replace', 'linear'), + valueFormat(1, '5px', 'replace') ] }, + { property: 'margin-right', + values: [ valueFormat(0, undefined, 'replace', 'linear'), + valueFormat(1, '5px', 'replace') ] }, + { property: 'margin-bottom', + values: [ valueFormat(0, undefined, 'replace', 'linear'), + valueFormat(1, '5px', 'replace') ] }, + { property: 'margin-left', + values: [ valueFormat(0, undefined, 'replace', 'linear'), + valueFormat(1, '5px', 'replace') ] } ] + }, + { desc: 'a missing property in final keyframe along with other values', + frames: [ { margin: '5px', left: '10px' }, + { left: '20px' } ], + expected: [ { property: 'left', + values: [ valueFormat(0, '10px', 'replace', 'linear'), + valueFormat(1, '20px', 'replace') ] }, + { property: 'margin-top', + values: [ valueFormat(0, '5px', 'replace', 'linear'), + valueFormat(1, undefined, 'replace') ] }, + { property: 'margin-right', + values: [ valueFormat(0, '5px', 'replace', 'linear'), + valueFormat(1, undefined, 'replace') ] }, + { property: 'margin-bottom', + values: [ valueFormat(0, '5px', 'replace', 'linear'), + valueFormat(1, undefined, 'replace') ] }, + { property: 'margin-left', + values: [ valueFormat(0, '5px', 'replace', 'linear'), + valueFormat(1, undefined, 'replace') ] } ] + }, + { desc: 'missing properties in both of initial and final keyframe', + frames: [ { left: '5px', offset: 0.5 } ], + expected: [ { property: 'left', + values: [ valueFormat(0, undefined, 'replace', 'linear'), + valueFormat(0.5, '5px', 'replace', 'linear'), + valueFormat(1, undefined, 'replace') ] } ] + }, + { desc: 'missing propertes in both of initial and final keyframe along ' + + 'with other values', + frames: [ { left: '5px', offset: 0 }, + { right: '5px', offset: 0.5 }, + { left: '10px', offset: 1 } ], + expected: [ { property: 'left', + values: [ valueFormat(0, '5px', 'replace', 'linear'), + valueFormat(1, '10px', 'replace') ] }, + { property: 'right', + values: [ valueFormat(0, undefined, 'replace', 'linear'), + valueFormat(0.5, '5px', 'replace', 'linear'), + valueFormat(1, undefined, 'replace') ] } ] + }, + + { desc: 'a missing property in final keyframe with duplicate offset ' + + + 'along with other values', + frames: [ { left: '5px', right: '5px', offset: 0 }, + { left: '8px', right: '8px', offset: 0 }, + { left: '10px', offset: 1 } ], + expected: [ { property: 'left', + values: [ valueFormat(0, '5px', 'replace'), + valueFormat(0, '8px', 'replace', 'linear'), + valueFormat(1, '10px', 'replace') ] }, + { property: 'right', + values: [ valueFormat(0, '5px', 'replace'), + valueFormat(0, '8px', 'replace', 'linear'), + valueFormat(1, undefined, 'replace') ] } ] + }, + + { desc: 'a missing property in initial keyframe with duplicate offset ' + + 'along with other values', + frames: [ { left: '10px', offset: 0 }, + { left: '8px', right: '8px', offset: 1 }, + { left: '5px', right: '5px', offset: 1 } ], + expected: [ { property: 'left', + values: [ valueFormat(0, '10px', 'replace', 'linear'), + valueFormat(1, '8px', 'replace'), + valueFormat(1, '5px', 'replace') ] }, + { property: 'right', + values: [ valueFormat(0, undefined, 'replace', 'linear'), + valueFormat(1, '8px', 'replace'), + valueFormat(1, '5px', 'replace') ] } ] + }, +]; + +gTests.forEach(function(subtest) { + test(function(t) { + var div = addDiv(t); + var animation = div.animate(subtest.frames, 100 * MS_PER_SEC); + // Flush styles since getProperties currently does not. Rather, it + // returns the actual properties in use at the current time. + // However, we want to test what these properties will look like + // after the next restyle. + getComputedStyle(div).opacity; + assert_properties_equal( + animation.effect.getProperties(), + subtest.expected + ); + }, subtest.desc); +}); + +</script> +</body> diff --git a/dom/animation/test/chrome/test_animation_properties_display.html b/dom/animation/test/chrome/test_animation_properties_display.html new file mode 100644 index 0000000000..1d558d2114 --- /dev/null +++ b/dom/animation/test/chrome/test_animation_properties_display.html @@ -0,0 +1,34 @@ +<!doctype html> +<head> +<meta charset=utf-8> +<title>Bug 1536688 - Test that 'display' is not included in + KeyframeEffect.getProperties() when using shorthand 'all'</title> +<script type="application/javascript" src="../testharness.js"></script> +<script type="application/javascript" src="../testharnessreport.js"></script> +<script type="application/javascript" src="../testcommon.js"></script> +</head> +<body> +<a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1536688" + target="_blank">Mozilla Bug 1536688</a> +<div id="log"></div> +<script> +'use strict'; + +test(t => { + const div = addDiv(t); + const animation = div.animate( + { all: ['unset', 'unset'] }, + 100 * MS_PER_SEC + ); + // Flush styles since getProperties does not. + getComputedStyle(div).opacity; + + const properties = animation.effect.getProperties(); + assert_false( + properties.some(property => property.property === 'display'), + 'Should not have a property for display' + ); +}); + +</script> +</body> diff --git a/dom/animation/test/chrome/test_cssanimation_missing_keyframes.html b/dom/animation/test/chrome/test_cssanimation_missing_keyframes.html new file mode 100644 index 0000000000..8c599655b7 --- /dev/null +++ b/dom/animation/test/chrome/test_cssanimation_missing_keyframes.html @@ -0,0 +1,66 @@ +<!doctype html> +<head> +<meta charset=utf-8> +<title>Bug 1339332 - Test for missing keyframes in CSS Animation</title> +<script type="application/javascript" src="../testharness.js"></script> +<script type="application/javascript" src="../testharnessreport.js"></script> +<script type="application/javascript" src="../testcommon.js"></script> +</head> +<body> +<a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1339332" + target="_blank">Mozilla Bug 1339332</a> +<div id="log"></div> +<style> +@keyframes missingFrom { + to { + text-align: right; + } +} +@keyframes missingBoth { + 50% { + text-align: right; + } +} +@keyframes missingTo { + from { + text-align: right; + } +} +</style> +<script> +'use strict'; + +const gTests = [ + { desc: 'missing "from" keyframe', + animationName: 'missingFrom', + expected: [{ property: 'text-align', + values: [valueFormat(0, undefined, 'replace', 'ease'), + valueFormat(1, 'right', 'replace')] } ] + }, + { desc: 'missing "to" keyframe', + animationName: 'missingTo', + expected: [{ property: 'text-align', + values: [valueFormat(0, 'right', 'replace', 'ease'), + valueFormat(1, undefined, 'replace')] } ] + }, + { desc: 'missing "from" and "to" keyframes', + animationName: 'missingBoth', + expected: [{ property: 'text-align', + values: [valueFormat(0, undefined, 'replace', 'ease'), + valueFormat(.5, 'right', 'replace', 'ease'), + valueFormat(1, undefined, 'replace')] } ] + }, +]; + +gTests.forEach(function(subtest) { + test(function(t) { + const div = addDiv(t); + div.style.animation = `${ subtest.animationName } 1000s`; + const animation = div.getAnimations()[0]; + assert_properties_equal(animation.effect.getProperties(), + subtest.expected); + }, subtest.desc); +}); + +</script> +</body> diff --git a/dom/animation/test/chrome/test_generated_content_getAnimations.html b/dom/animation/test/chrome/test_generated_content_getAnimations.html new file mode 100644 index 0000000000..41010ca917 --- /dev/null +++ b/dom/animation/test/chrome/test_generated_content_getAnimations.html @@ -0,0 +1,81 @@ +<!DOCTYPE html> +<head> +<meta charset=utf-8> +<title>Test getAnimations() for generated-content elements</title> +<script type="application/javascript" src="../testharness.js"></script> +<script type="application/javascript" src="../testharnessreport.js"></script> +<script type="application/javascript" src="../testcommon.js"></script> +<style> +@keyframes anim { } +@keyframes anim2 { } +.before::before { + content: ''; + animation: anim 100s; +} +.after::after { + content: ''; + animation: anim 100s, anim2 100s; +} +</style> +</head> +<body> +<div id='root' class='before after'> + <div class='before'></div> + <div></div> +</div> +<script> +'use strict'; + +function getWalker(node) { + var walker = Cc["@mozilla.org/inspector/deep-tree-walker;1"]. + createInstance(Ci.inIDeepTreeWalker); + walker.showAnonymousContent = true; + walker.init(node.ownerDocument, NodeFilter.SHOW_ALL); + walker.currentNode = node; + return walker; +} + +test(function(t) { + var root = document.getElementById('root'); + // Flush first to make sure the generated-content elements are ready + // in the tree. + flushComputedStyle(root); + var before = getWalker(root).firstChild(); + var after = getWalker(root).lastChild(); + + // Sanity Checks + assert_equals(document.getAnimations().length, 4, + 'All animations in this document'); + assert_equals(before.tagName, '_moz_generated_content_before', + 'First child is ::before element'); + assert_equals(after.tagName, '_moz_generated_content_after', + 'Last child is ::after element'); + + // Test Element.getAnimations() for generated-content elements + assert_equals(before.getAnimations().length, 1, + 'Animations of ::before generated-content element'); + assert_equals(after.getAnimations().length, 2, + 'Animations of ::after generated-content element'); +}, 'Element.getAnimations() used on generated-content elements'); + +test(function(t) { + var root = document.getElementById('root'); + flushComputedStyle(root); + var walker = getWalker(root); + + var animations = []; + var element = walker.currentNode; + while (element) { + if (element.getAnimations) { + animations = [...animations, ...element.getAnimations()]; + } + element = walker.nextNode(); + } + + assert_equals(animations.length, document.getAnimations().length, + 'The number of animations got by DeepTreeWalker and ' + + 'document.getAnimations() should be the same'); +}, 'Element.getAnimations() used by traversing DeepTreeWalker'); + +</script> +</body> diff --git a/dom/animation/test/chrome/test_keyframe_effect_xrays.html b/dom/animation/test/chrome/test_keyframe_effect_xrays.html new file mode 100644 index 0000000000..ca3e712ac5 --- /dev/null +++ b/dom/animation/test/chrome/test_keyframe_effect_xrays.html @@ -0,0 +1,45 @@ +<!doctype html> +<head> +<meta charset=utf-8> +<script type="application/javascript" src="../testharness.js"></script> +<script type="application/javascript" src="../testharnessreport.js"></script> +<script type="application/javascript" src="../testcommon.js"></script> +</head> +<body> +<a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1414674" + target="_blank">Mozilla Bug 1414674</a> +<div id="log"></div> +<iframe id="iframe" + src="http://example.org/tests/dom/animation/test/chrome/file_animate_xrays.html"></iframe> +<script> +'use strict'; + +var win = document.getElementById('iframe').contentWindow; + +async_test(function(t) { + window.addEventListener('load', t.step_func(function() { + var target = win.document.getElementById('target'); + var effect = new win.KeyframeEffect(target, [ + {opacity: 1, offset: 0}, + {opacity: 0, offset: 1}, + ], {duration: 100 * MS_PER_SEC, fill: "forwards"}); + // The frames object should be accessible via x-ray. + var frames = effect.getKeyframes(); + assert_equals(frames.length, 2, + "frames for KeyframeEffect ctor should be non-zero"); + assert_equals(frames[0].opacity, "1", + "first frame opacity for KeyframeEffect ctor should be specified value"); + assert_equals(frames[0].computedOffset, 0, + "first frame offset for KeyframeEffect ctor should be 0"); + assert_equals(frames[1].opacity, "0", + "last frame opacity for KeyframeEffect ctor should be specified value"); + assert_equals(frames[1].computedOffset, 1, + "last frame offset for KeyframeEffect ctor should be 1"); + var animation = new win.Animation(effect, document.timeline); + animation.play(); + t.done(); + })); +}, 'Calling KeyframeEffect() ctor across x-rays'); + +</script> +</body> diff --git a/dom/animation/test/chrome/test_mutation_observer_for_element_removal_in_shadow_tree.html b/dom/animation/test/chrome/test_mutation_observer_for_element_removal_in_shadow_tree.html new file mode 100644 index 0000000000..f8efaa6baf --- /dev/null +++ b/dom/animation/test/chrome/test_mutation_observer_for_element_removal_in_shadow_tree.html @@ -0,0 +1,45 @@ +<!DOCTYPE html> +<meta charset=utf-8> +<script type="application/javascript" src="../testharness.js"></script> +<script type="application/javascript" src="../testharnessreport.js"></script> +<script type="application/javascript" src="../testcommon.js"></script> +<div id="log"></div> +<script> + +promise_test(async t => { + // Set up a MutationObserver for animations. + const observer = new MutationObserver(() => {}); + observer.observe(document.documentElement, { + animations: true, + subtree: true, + }); + + // Create a CSS transition in a shadow tree. + let s = document.createElement('shadow-test'); + document.documentElement.appendChild(s); + s.attachShadow({mode:"open"}); + + let property = 'opacity'; + let initial = '1'; + let finalValue = '0'; + + let div = document.createElement('div'); + div.style = `${property}:${initial};transition:${property} 2s;` + + s.shadowRoot.appendChild(div); + div.offsetWidth; + + div.style[property] = finalValue; + + const eventWatcher = new EventWatcher(t, div, ['transitionstart']); + + // Trigger a CSS transition. + getComputedStyle(div)[property]; + + // Wait for a transitionend event to make sure the transition has been started. + await eventWatcher.wait_for('transitionstart'); + + // Now remove the element to notify it to the observer + div.parentNode.removeChild(div); +}); +</script> diff --git a/dom/animation/test/chrome/test_running_on_compositor.html b/dom/animation/test/chrome/test_running_on_compositor.html new file mode 100644 index 0000000000..d8c1d0573e --- /dev/null +++ b/dom/animation/test/chrome/test_running_on_compositor.html @@ -0,0 +1,1656 @@ +<!doctype html> +<head> +<meta charset=utf-8> +<title>Bug 1045994 - Add a chrome-only property to inspect if an animation is + running on the compositor or not</title> +<script type="application/javascript" src="../testharness.js"></script> +<script type="application/javascript" src="../testharnessreport.js"></script> +<script type="application/javascript" src="../testcommon.js"></script> +<style> +@keyframes anim { + to { transform: translate(100px) } +} +@keyframes transform-starts-with-none { + 0% { transform: none } + 99% { transform: none } + 100% { transform: translate(100px) } +} +@keyframes opacity { + to { opacity: 0 } +} +@keyframes zIndex_and_translate { + to { z-index: 999; transform: translate(100px); } +} +@keyframes z-index { + to { z-index: 999; } +} +@keyframes rotate { + from { transform: rotate(0deg); } + to { transform: rotate(360deg); } +} +@keyframes rotate-and-opacity { + from { transform: rotate(0deg); opacity: 1;} + to { transform: rotate(360deg); opacity: 0;} +} +div { + /* Element needs geometry to be eligible for layerization */ + width: 100px; + height: 100px; + background-color: white; +} +</style> +</head> +<body> +<a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1045994" + target="_blank">Mozilla Bug 1045994</a> +<div id="log"></div> +<script> +'use strict'; + +/** Test for bug 1045994 - Add a chrome-only property to inspect if an + animation is running on the compositor or not **/ + +const omtaEnabled = isOMTAEnabled(); + +function assert_animation_is_running_on_compositor(animation, desc) { + assert_equals(animation.isRunningOnCompositor, omtaEnabled, + desc + ' at ' + animation.currentTime + 'ms'); +} + +function assert_animation_is_not_running_on_compositor(animation, desc) { + assert_equals(animation.isRunningOnCompositor, false, + desc + ' at ' + animation.currentTime + 'ms'); +} + +promise_test(async t => { + // FIXME: When we implement Element.animate, use that here instead of CSS + // so that we remove any dependency on the CSS mapping. + var div = addDiv(t, { style: 'animation: anim 100s' }); + var animation = div.getAnimations()[0]; + + // If the animation starts at the current timeline time, we need to wait for + // one more frame to avoid receiving the fake timer-based MozAfterPaint event. + // FIXME: Bug 1419226: Drop this 'animation.ready' and 'waitForFrame'. Once + // MozAfterPaint is fired reliably, we just need to wait for a MozAfterPaint + // here. + await animation.ready; + + if (animationStartsRightNow(animation)) { + await waitForNextFrame(); + } + + await waitForPaints(); + + assert_animation_is_running_on_compositor(animation, + 'Animation reports that it is running on the compositor' + + ' during playback'); + + div.style.animationPlayState = 'paused'; + + await animation.ready; + + assert_animation_is_not_running_on_compositor(animation, + 'Animation reports that it is NOT running on the compositor' + + ' when paused'); +}, ''); + +promise_test(async t => { + var div = addDiv(t, { style: 'animation: z-index 100s' }); + var animation = div.getAnimations()[0]; + + await waitForPaints(); + + assert_animation_is_not_running_on_compositor(animation, + 'Animation reports that it is NOT running on the compositor' + + ' for animation of "z-index"'); +}, 'isRunningOnCompositor is false for animation of "z-index"'); + +promise_test(async t => { + var div = addDiv(t, { style: 'animation: zIndex_and_translate 100s' }); + var animation = div.getAnimations()[0]; + + await waitForPaints(); + + assert_animation_is_running_on_compositor(animation, + 'Animation reports that it is running on the compositor' + + ' when the animation has two properties, where one can run' + + ' on the compositor, the other cannot'); +}, 'isRunningOnCompositor is true if the animation has at least one ' + + 'property can run on compositor'); + +promise_test(async t => { + var div = addDiv(t, { style: 'animation: anim 100s' }); + var animation = div.getAnimations()[0]; + + await waitForPaints(); + + animation.pause(); + await animation.ready; + + assert_animation_is_not_running_on_compositor(animation, + 'Animation reports that it is NOT running on the compositor' + + ' when animation.pause() is called'); +}, 'isRunningOnCompositor is false when the animation.pause() is called'); + +promise_test(async t => { + var div = addDiv(t, { style: 'animation: anim 100s' }); + var animation = div.getAnimations()[0]; + + await waitForPaints(); + + animation.finish(); + assert_animation_is_not_running_on_compositor(animation, + 'Animation reports that it is NOT running on the compositor' + + ' immediately after animation.finish() is called'); + // Check that we don't set the flag back again on the next tick. + await waitForFrame(); + + assert_animation_is_not_running_on_compositor(animation, + 'Animation reports that it is NOT running on the compositor' + + ' on the next tick after animation.finish() is called'); +}, 'isRunningOnCompositor is false when the animation.finish() is called'); + +promise_test(async t => { + var div = addDiv(t, { style: 'animation: anim 100s' }); + var animation = div.getAnimations()[0]; + + await waitForPaints(); + + animation.currentTime = 100 * MS_PER_SEC; + assert_animation_is_not_running_on_compositor(animation, + 'Animation reports that it is NOT running on the compositor' + + ' immediately after manually seeking the animation to the end'); + // Check that we don't set the flag back again on the next tick. + await waitForFrame(); + + assert_animation_is_not_running_on_compositor(animation, + 'Animation reports that it is NOT running on the compositor' + + ' on the next tick after manually seeking the animation to the end'); +}, 'isRunningOnCompositor is false when manually seeking the animation to ' + + 'the end'); + +promise_test(async t => { + var div = addDiv(t, { style: 'animation: anim 100s' }); + var animation = div.getAnimations()[0]; + + await waitForPaints(); + + animation.cancel(); + assert_animation_is_not_running_on_compositor(animation, + 'Animation reports that it is NOT running on the compositor' + + ' immediately after animation.cancel() is called'); + // Check that we don't set the flag back again on the next tick. + await waitForFrame(); + + assert_animation_is_not_running_on_compositor(animation, + 'Animation reports that it is NOT running on the compositor' + + ' on the next tick after animation.cancel() is called'); +}, 'isRunningOnCompositor is false when animation.cancel() is called'); + +// This is to test that we don't simply clobber the flag when ticking +// animations and then set it again during painting. +promise_test(async t => { + var div = addDiv(t, { style: 'animation: anim 100s' }); + var animation = div.getAnimations()[0]; + + await waitForPaints(); + + await new Promise(resolve => { + window.requestAnimationFrame(() => { + t.step(() => { + assert_animation_is_running_on_compositor(animation, + 'Animation reports that it is running on the compositor' + + ' in requestAnimationFrame callback'); + }); + + resolve(); + }); + }); +}, 'isRunningOnCompositor is true in requestAnimationFrame callback'); + +promise_test(async t => { + var div = addDiv(t, { style: 'animation: anim 100s' }); + var animation = div.getAnimations()[0]; + + await waitForPaints(); + + await new Promise(resolve => { + var observer = new MutationObserver(records => { + var changedAnimation; + + records.forEach(record => { + changedAnimation = + record.changedAnimations.find(changedAnim => { + return changedAnim == animation; + }); + }); + + t.step(() => { + assert_true(!!changedAnimation, 'The animation should be recorded ' + + 'as one of the changedAnimations'); + + assert_animation_is_running_on_compositor(animation, + 'Animation reports that it is running on the compositor' + + ' in MutationObserver callback'); + }); + + resolve(); + }); + observer.observe(div, { animations: true, subtree: false }); + t.add_cleanup(() => { + observer.disconnect(); + }); + div.style.animationDuration = "200s"; + }); +}, 'isRunningOnCompositor is true in MutationObserver callback'); + +// This is to test that we don't temporarily clear the flag when forcing +// an unthrottled sample. +promise_test(async t => { + var div = addDiv(t, { style: 'animation: rotate 100s' }); + var animation = div.getAnimations()[0]; + + await waitForPaints(); + + await new Promise(resolve => { + var timeAtStart = window.performance.now(); + function handleFrame() { + t.step(() => { + assert_animation_is_running_on_compositor(animation, + 'Animation reports that it is running on the compositor' + + ' in requestAnimationFrame callback'); + }); + + // we have to wait at least 200ms because this animation is + // unthrottled on every 200ms. + // See https://hg.mozilla.org/mozilla-central/file/cafb1c90f794/layout/style/AnimationCommon.cpp#l863 + if (window.performance.now() - timeAtStart > 200) { + resolve(); + return; + } + window.requestAnimationFrame(handleFrame); + } + window.requestAnimationFrame(handleFrame); + }); +}, 'isRunningOnCompositor remains true in requestAnimationFrameCallback for ' + + 'overflow animation'); + +promise_test(async t => { + var div = addDiv(t, { style: 'transition: opacity 100s; opacity: 1' }); + + getComputedStyle(div).opacity; + + div.style.opacity = 0; + var animation = div.getAnimations()[0]; + + await waitForPaints(); + + assert_animation_is_running_on_compositor(animation, + 'Transition reports that it is running on the compositor' + + ' during playback for opacity transition'); +}, 'isRunningOnCompositor for transitions'); + +promise_test(async t => { + var div = addDiv(t, { style: 'animation: rotate-and-opacity 100s; ' + + 'backface-visibility: hidden; ' + + 'transform: none !important;' }); + var animation = div.getAnimations()[0]; + + await waitForPaints(); + + assert_animation_is_running_on_compositor(animation, + 'If an animation has a property that can run on the compositor and a ' + + 'property that cannot (due to Gecko limitations) but where the latter' + + 'property is overridden in the CSS cascade, the animation should ' + + 'still report that it is running on the compositor'); +}, 'isRunningOnCompositor is true when a property that would otherwise block ' + + 'running on the compositor is overridden in the CSS cascade'); + +promise_test(async t => { + var animation = addDivAndAnimate(t, + {}, + { opacity: [ 0, 1 ] }, 200 * MS_PER_SEC); + + await waitForPaints(); + + assert_animation_is_running_on_compositor(animation, + 'Animation reports that it is running on the compositor'); + + animation.currentTime = 150 * MS_PER_SEC; + animation.effect.updateTiming({ duration: 100 * MS_PER_SEC }); + + assert_animation_is_not_running_on_compositor(animation, + 'Animation reports that it is NOT running on the compositor' + + ' when the animation is set a shorter duration than current time'); +}, 'animation is immediately removed from compositor' + + 'when the duration is made shorter than the current time'); + +promise_test(async t => { + var animation = addDivAndAnimate(t, + {}, + { opacity: [ 0, 1 ] }, 100 * MS_PER_SEC); + + await waitForPaints(); + + assert_animation_is_running_on_compositor(animation, + 'Animation reports that it is running on the compositor'); + + animation.currentTime = 500 * MS_PER_SEC; + + assert_animation_is_not_running_on_compositor(animation, + 'Animation reports that it is NOT running on the compositor' + + ' when finished'); + + animation.effect.updateTiming({ duration: 1000 * MS_PER_SEC }); + await waitForFrame(); + + assert_animation_is_running_on_compositor(animation, + 'Animation reports that it is running on the compositor' + + ' when restarted'); +}, 'animation is added to compositor' + + ' when the duration is made longer than the current time'); + +promise_test(async t => { + var animation = addDivAndAnimate(t, + {}, + { opacity: [ 0, 1 ] }, 100 * MS_PER_SEC); + + await waitForPaints(); + + assert_animation_is_running_on_compositor(animation, + 'Animation reports that it is running on the compositor'); + + animation.effect.updateTiming({ endDelay: 100 * MS_PER_SEC }); + + assert_animation_is_running_on_compositor(animation, + 'Animation reports that it is running on the compositor' + + ' when endDelay is changed'); + + animation.currentTime = 110 * MS_PER_SEC; + await waitForFrame(); + + assert_animation_is_not_running_on_compositor(animation, + 'Animation reports that it is NOT running on the compositor' + + ' when currentTime is during endDelay'); +}, 'animation is removed from compositor' + + ' when current time is made longer than the duration even during endDelay'); + +promise_test(async t => { + var animation = addDivAndAnimate(t, + {}, + { opacity: [ 0, 1 ] }, 100 * MS_PER_SEC); + + await waitForPaints(); + + assert_animation_is_running_on_compositor(animation, + 'Animation reports that it is running on the compositor'); + + animation.effect.updateTiming({ endDelay: -200 * MS_PER_SEC }); + await waitForFrame(); + + assert_animation_is_not_running_on_compositor(animation, + 'Animation reports that it is NOT running on the compositor' + + ' when endTime is negative value'); +}, 'animation is removed from compositor' + + ' when endTime is negative value'); + +promise_test(async t => { + var animation = addDivAndAnimate(t, + {}, + { opacity: [ 0, 1 ] }, 200 * MS_PER_SEC); + + await waitForPaints(); + + assert_animation_is_running_on_compositor(animation, + 'Animation reports that it is running on the compositor'); + + animation.effect.updateTiming({ endDelay: -100 * MS_PER_SEC }); + await waitForFrame(); + + assert_animation_is_running_on_compositor(animation, + 'Animation reports that it is running on the compositor' + + ' when endTime is positive and endDelay is negative'); + animation.currentTime = 110 * MS_PER_SEC; + await waitForFrame(); + + assert_animation_is_not_running_on_compositor(animation, + 'Animation reports that it is NOT running on the compositor' + + ' when currentTime is after endTime'); +}, 'animation is NOT running on compositor' + + ' when endTime is positive and endDelay is negative'); + +promise_test(async t => { + var effect = new KeyframeEffect(null, + { opacity: [ 0, 1 ] }, + 100 * MS_PER_SEC); + var animation = new Animation(effect, document.timeline); + animation.play(); + + var div = addDiv(t); + + await waitForPaints(); + + assert_animation_is_not_running_on_compositor(animation, + 'Animation with null target reports that it is not running ' + + 'on the compositor'); + + animation.effect.target = div; + await waitForFrame(); + + assert_animation_is_running_on_compositor(animation, + 'Animation reports that it is running on the compositor ' + + 'after setting a valid target'); +}, 'animation is added to the compositor when setting a valid target'); + +promise_test(async t => { + var div = addDiv(t); + var animation = div.animate({ opacity: [ 0, 1 ] }, 100 * MS_PER_SEC); + + await waitForPaints(); + + assert_animation_is_running_on_compositor(animation, + 'Animation reports that it is running on the compositor'); + + animation.effect.target = null; + assert_animation_is_not_running_on_compositor(animation, + 'Animation reports that it is NOT running on the ' + + 'compositor after setting null target'); +}, 'animation is removed from the compositor when setting null target'); + +promise_test(async t => { + var div = addDiv(t); + var animation = div.animate({ opacity: [ 0, 1 ] }, + { duration: 100 * MS_PER_SEC, + delay: 100 * MS_PER_SEC, + fill: 'backwards' }); + + await waitForPaints(); + + assert_animation_is_running_on_compositor(animation, + 'Animation with fill:backwards in delay phase reports ' + + 'that it is running on the compositor'); + + animation.currentTime = 100 * MS_PER_SEC; + await waitForFrame(); + + assert_animation_is_running_on_compositor(animation, + 'Animation with fill:backwards in delay phase reports ' + + 'that it is running on the compositor after delay phase'); +}, 'animation with fill:backwards in delay phase is running on the ' + + ' compositor while it is in delay phase'); + +promise_test(async t => { + const animation = addDiv(t).animate({ opacity: [ 0, 1 ] }, 100 * MS_PER_SEC); + animation.playbackRate = -1; + animation.currentTime = 200 * MS_PER_SEC; + + await waitForPaints(); + + assert_animation_is_running_on_compositor(animation, + 'Animation with negative playback rate is runnning on the' + + ' compositor even before it reaches the active interval'); +}, 'animation with negative playback rate is sent to the compositor even in' + + ' after phase'); + +promise_test(async t => { + var div = addDiv(t); + var animation = div.animate([{ opacity: 1, offset: 0 }, + { opacity: 1, offset: 0.99 }, + { opacity: 0, offset: 1 }], 100 * MS_PER_SEC); + + var another = addDiv(t); + + await waitForPaints(); + + assert_animation_is_running_on_compositor(animation, + 'Opacity animation on a 100% opacity keyframe reports ' + + 'that it is running on the compositor from the begining'); + + animation.effect.target = another; + await waitForFrame(); + + assert_animation_is_running_on_compositor(animation, + 'Opacity animation on a 100% opacity keyframe keeps ' + + 'running on the compositor after changing the target ' + + 'element'); +}, '100% opacity animations with keeps running on the ' + + 'compositor after changing the target element'); + +promise_test(async t => { + var div = addDiv(t); + var animation = div.animate({ color: ['red', 'black'] }, 100 * MS_PER_SEC); + + await waitForPaints(); + + assert_animation_is_not_running_on_compositor(animation, + 'Color animation reports that it is not running on the ' + + 'compositor'); + + animation.effect.setKeyframes([{ opacity: 1, offset: 0 }, + { opacity: 1, offset: 0.99 }, + { opacity: 0, offset: 1 }]); + await waitForFrame(); + + assert_animation_is_running_on_compositor(animation, + '100% opacity animation set by using setKeyframes reports ' + + 'that it is running on the compositor'); +}, '100% opacity animation set up by converting an existing animation with ' + + 'cannot be run on the compositor, is running on the compositor'); + +promise_test(async t => { + var div = addDiv(t); + var animation = div.animate({ color: ['red', 'black'] }, 100 * MS_PER_SEC); + var effect = new KeyframeEffect(div, + [{ opacity: 1, offset: 0 }, + { opacity: 1, offset: 0.99 }, + { opacity: 0, offset: 1 }], + 100 * MS_PER_SEC); + + await waitForPaints(); + + assert_animation_is_not_running_on_compositor(animation, + 'Color animation reports that it is not running on the ' + + 'compositor'); + + animation.effect = effect; + await waitForFrame(); + + assert_animation_is_running_on_compositor(animation, + '100% opacity animation set up by changing effects reports ' + + 'that it is running on the compositor'); +}, '100% opacity animation set up by changing the effects on an existing ' + + 'animation which cannot be run on the compositor, is running on the ' + + 'compositor'); + +promise_test(async t => { + var div = addDiv(t, { style: "opacity: 1 ! important" }); + + var animation = div.animate({ opacity: [0, 1] }, 100 * MS_PER_SEC); + + await waitForPaints(); + + assert_animation_is_not_running_on_compositor(animation, + 'Opacity animation on an element which has 100% opacity style with ' + + '!important flag reports that it is not running on the compositor'); + // Clear important flag from the opacity style on the target element. + div.style.setProperty("opacity", "1", ""); + await waitForFrame(); + + assert_animation_is_running_on_compositor(animation, + 'Opacity animation reports that it is running on the compositor after ' + + 'clearing the !important flag'); +}, 'Clearing *important* opacity style on the target element sends the ' + + 'animation to the compositor'); + +promise_test(async t => { + var div = addDiv(t); + var lowerAnimation = div.animate({ opacity: [1, 0] }, 100 * MS_PER_SEC); + var higherAnimation = div.animate({ opacity: [0, 1] }, 100 * MS_PER_SEC); + + await waitForPaints(); + + assert_animation_is_running_on_compositor(higherAnimation, + 'A higher-priority opacity animation on an element ' + + 'reports that it is running on the compositor'); + assert_animation_is_running_on_compositor(lowerAnimation, + 'A lower-priority opacity animation on the same ' + + 'element also reports that it is running on the compositor'); +}, 'Opacity animations on the same element run on the compositor'); + +promise_test(async t => { + var div = addDiv(t, { style: 'transition: opacity 100s; opacity: 1' }); + + getComputedStyle(div).opacity; + + div.style.opacity = 0; + getComputedStyle(div).opacity; + + var transition = div.getAnimations()[0]; + var animation = div.animate({ opacity: [0, 1] }, 100 * MS_PER_SEC); + + await waitForPaints(); + + assert_animation_is_running_on_compositor(animation, + 'An opacity animation on an element reports that' + + 'that it is running on the compositor'); + assert_animation_is_running_on_compositor(transition, + 'An opacity transition on the same element reports that ' + + 'it is running on the compositor'); +}, 'Both of transition and script animation on the same element run on the ' + + 'compositor'); + +promise_test(async t => { + var div = addDiv(t); + var importantOpacityElement = addDiv(t, { style: "opacity: 1 ! important" }); + + var animation = div.animate({ opacity: [1, 0] }, 100 * MS_PER_SEC); + + await waitForPaints(); + + assert_animation_is_running_on_compositor(animation, + 'Opacity animation on an element reports ' + + 'that it is running on the compositor'); + + animation.effect.target = null; + await waitForFrame(); + + assert_animation_is_not_running_on_compositor(animation, + 'Animation is no longer running on the compositor after ' + + 'removing from the element'); + animation.effect.target = importantOpacityElement; + await waitForFrame(); + + assert_animation_is_not_running_on_compositor(animation, + 'Animation is NOT running on the compositor even after ' + + 'being applied to a different element which has an ' + + '!important opacity declaration'); +}, 'Animation continues not running on the compositor after being ' + + 'applied to an element which has an important declaration and ' + + 'having previously been temporarily associated with no target element'); + +promise_test(async t => { + var div = addDiv(t); + var another = addDiv(t); + + var lowerAnimation = div.animate({ opacity: [1, 0] }, 100 * MS_PER_SEC); + var higherAnimation = another.animate({ opacity: [0, 1] }, 100 * MS_PER_SEC); + + await waitForPaints(); + + assert_animation_is_running_on_compositor(lowerAnimation, + 'An opacity animation on an element reports that ' + + 'it is running on the compositor'); + assert_animation_is_running_on_compositor(higherAnimation, + 'Opacity animation on a different element reports ' + + 'that it is running on the compositor'); + + lowerAnimation.effect.target = null; + await waitForFrame(); + + assert_animation_is_not_running_on_compositor(lowerAnimation, + 'Animation is no longer running on the compositor after ' + + 'being removed from the element'); + lowerAnimation.effect.target = another; + await waitForFrame(); + + assert_animation_is_running_on_compositor(lowerAnimation, + 'A lower-priority animation begins running ' + + 'on the compositor after being applied to an element ' + + 'which has a higher-priority animation'); + assert_animation_is_running_on_compositor(higherAnimation, + 'A higher-priority animation continues to run on the ' + + 'compositor even after a lower-priority animation is ' + + 'applied to the same element'); +}, 'Animation begins running on the compositor after being applied ' + + 'to an element which has a higher-priority animation and after ' + + 'being temporarily associated with no target element'); + +promise_test(async t => { + var div = addDiv(t); + var another = addDiv(t); + + var lowerAnimation = div.animate({ opacity: [1, 0] }, 100 * MS_PER_SEC); + var higherAnimation = another.animate({ opacity: [0, 1] }, 100 * MS_PER_SEC); + + await waitForPaints(); + + assert_animation_is_running_on_compositor(lowerAnimation, + 'An opacity animation on an element reports that ' + + 'it is running on the compositor'); + assert_animation_is_running_on_compositor(higherAnimation, + 'Opacity animation on a different element reports ' + + 'that it is running on the compositor'); + + higherAnimation.effect.target = null; + await waitForFrame(); + + assert_animation_is_not_running_on_compositor(higherAnimation, + 'Animation is no longer running on the compositor after ' + + 'being removed from the element'); + higherAnimation.effect.target = div; + await waitForFrame(); + + assert_animation_is_running_on_compositor(lowerAnimation, + 'Animation continues running on the compositor after ' + + 'a higher-priority animation applied to the same element'); + assert_animation_is_running_on_compositor(higherAnimation, + 'A higher-priority animation begins to running on the ' + + 'compositor after being applied to an element which has ' + + 'a lower-priority-animation'); +}, 'Animation begins running on the compositor after being applied ' + + 'to an element which has a lower-priority animation once after ' + + 'disassociating with an element'); + +var delayPhaseTests = [ + { + desc: 'script animation of opacity', + setupAnimation: t => { + return addDiv(t).animate( + { opacity: [0, 1] }, + { delay: 100 * MS_PER_SEC, duration: 100 * MS_PER_SEC }); + }, + }, + { + desc: 'script animation of transform', + setupAnimation: t => { + return addDiv(t).animate( + { transform: ['translateX(0px)', 'translateX(100px)'] }, + { delay: 100 * MS_PER_SEC, duration: 100 * MS_PER_SEC }); + }, + }, + { + desc: 'CSS animation of opacity', + setupAnimation: t => { + return addDiv(t, { style: 'animation: opacity 100s 100s' }) + .getAnimations()[0]; + }, + }, + { + desc: 'CSS animation of transform', + setupAnimation: t => { + return addDiv(t, { style: 'animation: anim 100s 100s' }) + .getAnimations()[0]; + }, + }, + { + desc: 'CSS transition of opacity', + setupAnimation: t => { + var div = addDiv(t, { style: 'transition: opacity 100s 100s' }); + getComputedStyle(div).opacity; + + div.style.opacity = 0; + return div.getAnimations()[0]; + }, + }, + { + desc: 'CSS transition of transform', + setupAnimation: t => { + var div = addDiv(t, { style: 'transition: transform 100s 100s' }); + getComputedStyle(div).transform; + + div.style.transform = 'translateX(100px)'; + return div.getAnimations()[0]; + }, + }, +]; + +delayPhaseTests.forEach(test => { + promise_test(async t => { + var animation = test.setupAnimation(t); + + await waitForPaints(); + + assert_animation_is_running_on_compositor(animation, + test.desc + ' reports that it is running on the ' + + 'compositor even though it is in the delay phase'); + }, 'isRunningOnCompositor for ' + test.desc + ' is true even though ' + + 'it is in the delay phase'); +}); + +// The purpose of thie test cases is to check that +// NS_FRAME_MAY_BE_TRANSFORMED flag on the associated nsIFrame persists +// after transform style on the frame is removed. +var delayPhaseWithTransformStyleTests = [ + { + desc: 'script animation of transform with transform style', + setupAnimation: t => { + return addDiv(t, { style: 'transform: translateX(10px)' }).animate( + { transform: ['translateX(0px)', 'translateX(100px)'] }, + { delay: 100 * MS_PER_SEC, duration: 100 * MS_PER_SEC }); + }, + }, + { + desc: 'CSS animation of transform with transform style', + setupAnimation: t => { + return addDiv(t, { style: 'animation: anim 100s 100s;' + + 'transform: translateX(10px)' }) + .getAnimations()[0]; + }, + }, + { + desc: 'CSS transition of transform with transform style', + setupAnimation: t => { + var div = addDiv(t, { style: 'transition: transform 100s 100s;' + + 'transform: translateX(10px)'}); + getComputedStyle(div).transform; + + div.style.transform = 'translateX(100px)'; + return div.getAnimations()[0]; + }, + }, +]; + +delayPhaseWithTransformStyleTests.forEach(test => { + promise_test(async t => { + var animation = test.setupAnimation(t); + + await waitForPaints(); + + assert_animation_is_running_on_compositor(animation, + test.desc + ' reports that it is running on the ' + + 'compositor even though it is in the delay phase'); + + // Remove the initial transform style during delay phase. + animation.effect.target.style.transform = 'none'; + await animation.ready; + + assert_animation_is_running_on_compositor(animation, + test.desc + ' reports that it keeps running on the ' + + 'compositor after removing the initial transform style'); + }, 'isRunningOnCompositor for ' + test.desc + ' is true after removing ' + + 'the initial transform style during the delay phase'); +}); + +var startsWithNoneTests = [ + { + desc: 'script animation of transform starts with transform:none segment', + setupAnimation: t => { + return addDiv(t).animate( + { transform: ['none', 'none', 'translateX(100px)'] }, 100 * MS_PER_SEC); + }, + }, + { + desc: 'CSS animation of transform starts with transform:none segment', + setupAnimation: t => { + return addDiv(t, + { style: 'animation: transform-starts-with-none 100s 100s' }) + .getAnimations()[0]; + }, + }, +]; + +startsWithNoneTests.forEach(test => { + promise_test(async t => { + var animation = test.setupAnimation(t); + + await waitForPaints(); + + assert_animation_is_running_on_compositor(animation, + test.desc + ' reports that it is running on the ' + + 'compositor even though it is in transform:none segment'); + }, 'isRunningOnCompositor for ' + test.desc + ' is true even though ' + + 'it is in transform:none segment'); +}); + +promise_test(async t => { + var div = addDiv(t, { style: 'opacity: 1 ! important' }); + + var animation = div.animate( + { opacity: [0, 1] }, + { delay: 100 * MS_PER_SEC, duration: 100 * MS_PER_SEC }); + + await waitForPaints(); + + assert_animation_is_not_running_on_compositor(animation, + 'Opacity animation on an element which has opacity:1 important style' + + 'reports that it is not running on the compositor'); + // Clear the opacity style on the target element. + div.style.setProperty("opacity", "1", ""); + await waitForFrame(); + + assert_animation_is_running_on_compositor(animation, + 'Opacity animations reports that it is running on the compositor after ' + + 'clearing the opacity style on the element'); +}, 'Clearing *important* opacity style on the target element sends the ' + + 'animation to the compositor even if the animation is in the delay phase'); + +promise_test(async t => { + var opaqueDiv = addDiv(t, { style: 'opacity: 1 ! important' }); + var anotherDiv = addDiv(t); + + var animation = opaqueDiv.animate( + { opacity: [0, 1] }, + { delay: 100 * MS_PER_SEC, duration: 100 * MS_PER_SEC }); + + await waitForPaints(); + + assert_animation_is_not_running_on_compositor(animation, + 'Opacity animation on an element which has opacity:1 important style' + + 'reports that it is not running on the compositor'); + // Changing target element to another element which has no opacity style. + animation.effect.target = anotherDiv; + await waitForFrame(); + + assert_animation_is_running_on_compositor(animation, + 'Opacity animations reports that it is running on the compositor after ' + + 'changing the target element to another elemenent having no ' + + 'opacity style'); +}, 'Changing target element of opacity animation sends the animation to the ' + + 'the compositor even if the animation is in the delay phase'); + +promise_test(async t => { + var animation = + addDivAndAnimate(t, + {}, + { width: ['100px', '200px'] }, + { duration: 100 * MS_PER_SEC, delay: 100 * MS_PER_SEC }); + + await waitForPaints(); + + assert_animation_is_not_running_on_compositor(animation, + 'Width animation reports that it is not running on the compositor ' + + 'in the delay phase'); + // Changing to property runnable on the compositor. + animation.effect.setKeyframes({ opacity: [0, 1] }); + await waitForFrame(); + + assert_animation_is_running_on_compositor(animation, + 'Opacity animation reports that it is running on the compositor ' + + 'after changing the property from width property in the delay phase'); +}, 'Dynamic change to a property runnable on the compositor ' + + 'in the delay phase'); + +promise_test(async t => { + var div = addDiv(t, { style: 'transition: opacity 100s; ' + + 'opacity: 0 !important' }); + getComputedStyle(div).opacity; + + div.style.setProperty('opacity', '1', 'important'); + getComputedStyle(div).opacity; + + var animation = div.getAnimations()[0]; + + await waitForPaints(); + + assert_animation_is_running_on_compositor(animation, + 'Transition reports that it is running on the compositor even if the ' + + 'property is overridden by an !important rule'); +}, 'Transitions override important rules'); + +promise_test(async t => { + var div = addDiv(t, { style: 'transition: opacity 100s; ' + + 'opacity: 0 !important' }); + getComputedStyle(div).opacity; + + div.animate({ opacity: [ 0, 1 ] }, 100 * MS_PER_SEC); + + div.style.setProperty('opacity', '1', 'important'); + getComputedStyle(div).opacity; + + var [transition, animation] = div.getAnimations(); + + await waitForPaints(); + + assert_animation_is_not_running_on_compositor(transition, + 'Transition suppressed by an animation which is overridden by an ' + + '!important rule reports that it is NOT running on the compositor'); + assert_animation_is_not_running_on_compositor(animation, + 'Animation overridden by an !important rule reports that it is ' + + 'NOT running on the compositor'); +}, 'Neither transition nor animation does run on the compositor if the ' + + 'property is overridden by an !important rule'); + +promise_test(async t => { + var div = addDiv(t, { style: 'display: table' }); + var animation = + div.animate({ transform: ['rotate(0deg)', 'rotate(360deg)'] }, + 100 * MS_PER_SEC); + + await waitForAnimationReadyToRestyle(animation); + + await waitForPaints(); + + assert_animation_is_running_on_compositor(animation, + 'Transform animation on display:table element should be running on the' + + ' compositor'); +}, 'Transform animation on display:table element runs on the compositor'); + +promise_test(async t => { + const div = addDiv(t, { style: 'display: table' }); + const animation = div.animate(null, 100 * MS_PER_SEC); + const effect = new KeyframeEffect(div, + { transform: ['none', 'none']}, + 100 * MS_PER_SEC); + + await waitForAnimationReadyToRestyle(animation); + + animation.effect = effect; + + await waitForNextFrame(); + await waitForPaints(); + + assert_animation_is_running_on_compositor( + animation, + 'Transform animation on table element should be running on the compositor' + ); +}, 'Empty transform effect assigned after the fact to display:table content' + + ' runs on the compositor'); + +promise_test(async t => { + const div = addDiv(t); + const animation = div.animate({ backgroundColor: ['blue', 'green'] }, + 100 * MS_PER_SEC); + + await waitForAnimationReadyToRestyle(animation); + await waitForPaints(); + + assert_animation_is_running_on_compositor(animation, + 'background-color animation should be running on the compositor'); +}, 'background-color animation runs on the compositor'); + +promise_test(async t => { + const div = addDiv(t); + const animation = div.animate({ backgroundColor: ['blue', 'green'] }, + 100 * MS_PER_SEC); + + await waitForAnimationReadyToRestyle(animation); + await waitForPaints(); + + assert_animation_is_running_on_compositor(animation, + 'background-color animation should be running on the compositor'); + + // Add a red opaque background image covering the background color animation. + div.style.backgroundImage = + 'url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAIAAAAC64' + + 'paAAAAG0lEQVR42mP8z0A%2BYKJA76jmUc2jmkc1U0EzACKcASfOgGoMAAAAAElFTkSuQmCC)'; + + await waitForAnimationReadyToRestyle(animation); + await waitForPaints(); + + // Bug 1712246. We should optimize this case eventually. + //assert_animation_is_not_running_on_compositor(animation, + // 'Opaque background image stops background-color animations from running ' + + // 'on the compositor'); +}, 'Opaque background image stops background-color animations from running ' + + ' on the compositor'); + +promise_test(async t => { + await SpecialPowers.pushPrefEnv({ + set: [["gfx.omta.background-color", false]] + }); + + const div = addDiv(t); + const animation = div.animate({ backgroundColor: ['blue', 'green'] }, + 100 * MS_PER_SEC); + + await waitForAnimationReadyToRestyle(animation); + await waitForPaints(); + + assert_animation_is_not_running_on_compositor(animation, + 'background-color animation should NOT be running on the compositor ' + + 'if the pref is disabled'); +}, 'background-color animation does not run on the compositor if the pref ' + + 'is disabled'); + +promise_test(async t => { + const div = addDiv(t); + const animation = div.animate({ translate: ['0px', '100px'] }, + 100 * MS_PER_SEC); + + await waitForAnimationReadyToRestyle(animation); + await waitForPaints(); + + assert_animation_is_running_on_compositor(animation, + 'translate animation should be running on the compositor'); +}, 'translate animation runs on the compositor'); + +promise_test(async t => { + const div = addDiv(t); + const animation = div.animate({ rotate: ['0deg', '45deg'] }, + 100 * MS_PER_SEC); + + await waitForAnimationReadyToRestyle(animation); + await waitForPaints(); + + assert_animation_is_running_on_compositor(animation, + 'rotate animation should be running on the compositor'); +}, 'rotate animation runs on the compositor'); + +promise_test(async t => { + const div = addDiv(t); + const animation = div.animate({ scale: ['1 1', '2 2'] }, + 100 * MS_PER_SEC); + + await waitForAnimationReadyToRestyle(animation); + await waitForPaints(); + + assert_animation_is_running_on_compositor(animation, + 'scale animation should be running on the compositor'); +}, 'scale animation runs on the compositor'); + +promise_test(async t => { + const div = addDiv(t); + const animation = div.animate({ translate: ['0px', '100px'], + rotate: ['0deg', '45deg'], + transform: ['translate(20px)', + 'translate(30px)'] }, + 100 * MS_PER_SEC); + + await waitForAnimationReadyToRestyle(animation); + await waitForPaints(); + + assert_animation_is_running_on_compositor(animation, + 'multiple transform-like properties animation should be running on the ' + + 'compositor'); + + const properties = animation.effect.getProperties(); + properties.forEach(property => { + assert_true(property.runningOnCompositor, + property.property + ' is running on the compositor'); + }); +}, 'Multiple transform-like properties animation runs on the compositor'); + +promise_test(async t => { + const div = addDiv(t); + const animation = div.animate({ offsetPath: ['none', 'none'] }, + 100 * MS_PER_SEC); + + await waitForAnimationReadyToRestyle(animation); + await waitForPaints(); + + assert_animation_is_running_on_compositor(animation, + 'offset-path animation should be running on the compositor even if ' + + 'it is always none'); +}, 'offset-path none animation runs on the compositor'); + +promise_test(async t => { + const div = addDiv(t); + const animation = div.animate({ offsetPath: ['path("M0 0l100 100")', + 'path("M0 0l200 200")'] }, + 100 * MS_PER_SEC); + + await waitForAnimationReadyToRestyle(animation); + await waitForPaints(); + + assert_animation_is_running_on_compositor(animation, + 'offset-path:path() animation should be running on the compositor'); +}, 'offset-path:path() animation runs on the compositor'); + +promise_test(async t => { + const div = addDiv(t); + const animation = div.animate({ offsetPath: ['ray(0deg)', + 'ray(180deg)'] }, + 100 * MS_PER_SEC); + + await waitForAnimationReadyToRestyle(animation); + await waitForPaints(); + + assert_animation_is_running_on_compositor(animation, + 'offset-path:ray() animation should be running on the compositor'); +}, 'offset-path:ray() animation runs on the compositor'); + +promise_test(async t => { + const div = addDiv(t); + const animation = div.animate({ offsetPath: ['inset(0px)', + 'inset(10px)'] }, + 100 * MS_PER_SEC); + + await waitForAnimationReadyToRestyle(animation); + await waitForPaints(); + + assert_animation_is_running_on_compositor(animation, + 'offset-path:inset() animation should be running on the compositor'); +}, 'offset-path:inset() animation runs on the compositor'); + +promise_test(async t => { + const div = addDiv(t); + const animation = div.animate({ offsetPath: ['circle(10px)', + 'circle(20px)'] }, + 100 * MS_PER_SEC); + + await waitForAnimationReadyToRestyle(animation); + await waitForPaints(); + + assert_animation_is_running_on_compositor(animation, + 'offset-path:circle() animation should be running on the compositor'); +}, 'offset-path:circle() animation runs on the compositor'); + +promise_test(async t => { + const div = addDiv(t); + const animation = div.animate({ offsetPath: ['ellipse(10px 20px)', + 'ellipse(20px 40px)'] }, + 100 * MS_PER_SEC); + + await waitForAnimationReadyToRestyle(animation); + await waitForPaints(); + + assert_animation_is_running_on_compositor(animation, + 'offset-path:ellipse() animation should be running on the compositor'); +}, 'offset-path:ellipse() animation runs on the compositor'); + +promise_test(async t => { + const div = addDiv(t); + const animation = div.animate({ offsetPath: ['polygon(0px 0px)', + 'polygon(50px 50px)'] }, + 100 * MS_PER_SEC); + + await waitForAnimationReadyToRestyle(animation); + await waitForPaints(); + + assert_animation_is_running_on_compositor(animation, + 'offset-path:polygon() animation should be running on the compositor'); +}, 'offset-path:polygon() animation runs on the compositor'); + +promise_test(async t => { + const div = addDiv(t); + const animation = div.animate({ offsetPath: ['padding-box', + 'padding-box'] }, + 100 * MS_PER_SEC); + + await waitForAnimationReadyToRestyle(animation); + await waitForPaints(); + + assert_animation_is_running_on_compositor(animation, + 'offset-path:padding-box animation should be running on the compositor'); +}, 'offset-path:padding-box animation runs on the compositor'); + +promise_test(async t => { + const div = addDiv(t); + const animation = div.animate({ offsetPath: ['content-box', + 'content-box'] }, + 100 * MS_PER_SEC); + + await waitForAnimationReadyToRestyle(animation); + await waitForPaints(); + + assert_animation_is_running_on_compositor(animation, + 'offset-path:content-box animation should be running on the compositor'); +}, 'offset-path:content-box animation runs on the compositor'); + +promise_test(async t => { + const div = addDiv(t); + const animation = div.animate({ offsetPath: ['xywh(0% 0% 10px 10px)', + 'xywh(10% 10% 20px 20px)'] }, + 100 * MS_PER_SEC); + + await waitForAnimationReadyToRestyle(animation); + await waitForPaints(); + + assert_animation_is_running_on_compositor(animation, + 'offset-path:xywh() animation should be running on the compositor'); +}, 'offset-path:xywh() animation runs on the compositor'); + +promise_test(async t => { + const div = addDiv(t); + const animation = div.animate({ offsetPath: ['rect(0% 0% 10px 10px)', + 'rect(10% 10% 20px 20px)'] }, + 100 * MS_PER_SEC); + + await waitForAnimationReadyToRestyle(animation); + await waitForPaints(); + + assert_animation_is_running_on_compositor(animation, + 'offset-path:rect() animation should be running on the compositor'); +}, 'offset-path:rect() animation runs on the compositor'); + +promise_test(async t => { + const div = addDiv(t); + const animation = div.animate({ offsetDistance: ['0%', '100%'] }, + 100 * MS_PER_SEC); + + await waitForAnimationReadyToRestyle(animation); + await waitForPaints(); + + assert_animation_is_not_running_on_compositor(animation, + 'offset-distance animation is not running on the compositor because ' + + 'offset-path is none'); + + const newAnim = div.animate({ offsetPath: ['None', 'None'] }, + 100 * MS_PER_SEC); + await waitForAnimationReadyToRestyle(newAnim); + await waitForPaints(); + + assert_animation_is_running_on_compositor(animation, + 'offset-distance animation should be running on the compositor'); + assert_animation_is_running_on_compositor(newAnim, + 'new added offset-path animation should be running on the compositor'); +}, 'offset-distance animation runs on the compositor'); + +promise_test(async t => { + const div = addDiv(t); + const animation = div.animate({ offsetRotate: ['0deg', '45deg'] }, + 100 * MS_PER_SEC); + + await waitForAnimationReadyToRestyle(animation); + await waitForPaints(); + + assert_animation_is_not_running_on_compositor(animation, + 'offset-rotate animation is not running on the compositor because ' + + 'offset-path is none'); + + const newAnim = div.animate({ offsetPath: ['None', 'None'] }, + 100 * MS_PER_SEC); + await waitForAnimationReadyToRestyle(newAnim); + await waitForPaints(); + + assert_animation_is_running_on_compositor(animation, + 'offset-rotate animation should be running on the compositor'); + assert_animation_is_running_on_compositor(newAnim, + 'new added offset-path animation should be running on the compositor'); +}, 'offset-rotate animation runs on the compositor'); + +promise_test(async t => { + const div = addDiv(t); + const animation = div.animate({ offsetAnchor: ['0% 0%', '100% 100%'] }, + 100 * MS_PER_SEC); + + await waitForAnimationReadyToRestyle(animation); + await waitForPaints(); + + assert_animation_is_not_running_on_compositor(animation, + 'offset-anchor animation is not running on the compositor because ' + + 'offset-path is none'); + + const newAnim = div.animate({ offsetPath: ['None', 'None'] }, + 100 * MS_PER_SEC); + await waitForAnimationReadyToRestyle(newAnim); + await waitForPaints(); + + assert_animation_is_running_on_compositor(animation, + 'offset-anchor animation should be running on the compositor'); + assert_animation_is_running_on_compositor(newAnim, + 'new added offset-path animation should be running on the compositor'); +}, 'offset-anchor animation runs on the compositor'); + +promise_test(async t => { + const div = addDiv(t); + const animation = div.animate({ offsetPosition: ['0% 0%', '100% 100%'] }, + 100 * MS_PER_SEC); + + await waitForAnimationReadyToRestyle(animation); + await waitForPaints(); + + assert_animation_is_not_running_on_compositor(animation, + 'offset-position animation is not running on the compositor because ' + + 'offset-path is none'); + + const newAnim = div.animate({ offsetPath: ['None', 'None'] }, + 100 * MS_PER_SEC); + await waitForAnimationReadyToRestyle(newAnim); + await waitForPaints(); + + assert_animation_is_running_on_compositor(animation, + 'offset-position animation should be running on the compositor'); + assert_animation_is_running_on_compositor(newAnim, + 'new added offset-path animation should be running on the compositor'); +}, 'offset-position animation runs on the compositor'); + +promise_test(async t => { + const div = addDiv(t); + const animation = div.animate({ translate: ['0px', '100px'], + rotate: ['0deg', '45deg'], + transform: ['translate(0px)', + 'translate(100px)'], + offsetDistance: ['0%', '100%'] }, + 100 * MS_PER_SEC); + await waitForAnimationReadyToRestyle(animation); + await waitForPaints(); + + assert_animation_is_running_on_compositor(animation, + 'Animation is running on the compositor even though we do not have ' + + 'offset-path'); + + div.style.offsetPath = 'path("M50 0v100")'; + getComputedStyle(div).offsetPath; + + await waitForPaints(); + + assert_animation_is_running_on_compositor(animation, + 'Animation is running on the compositor'); + +}, 'Multiple transform-like properties (include motion-path) animation runs ' + + 'on the compositor'); + +promise_test(async t => { + const div = addDiv(t); + const animation = div.animate({ translate: ['0px', '100px'], + rotate: ['0deg', '45deg'], + transform: ['translate(20px)', + 'translate(30px)'], + offsetDistance: ['0%', '100%'] }, + 100 * MS_PER_SEC); + + div.style.setProperty('translate', '50px', 'important'); + getComputedStyle(div).translate; + + await waitForAnimationReadyToRestyle(animation); + await waitForPaints(); + + assert_animation_is_not_running_on_compositor(animation, + 'Animation overridden by an !important rule reports that it is ' + + 'NOT running on the compositor'); + + const properties = animation.effect.getProperties(); + properties.forEach(property => { + assert_true(!property.runningOnCompositor, + property.property + ' is not running on the compositor'); + }); +}, 'Multiple transform-like properties animation does not runs on the ' + + 'compositor because one of the transform-like property is overridden ' + + 'by an !important rule'); + +// FIXME: Bug 1593106: We should still run the animations on the compositor if +// offset-* doesn't have any effect. +promise_test(async t => { + const div = addDiv(t); + const animation = div.animate({ translate: ['0px', '100px'], + rotate: ['0deg', '45deg'], + transform: ['translate(0px)', + 'translate(100px)'], + offsetDistance: ['0%', '100%'] }, + 100 * MS_PER_SEC); + + div.style.setProperty('offset-distance', '50%', 'important'); + getComputedStyle(div).offsetDistance; + + await waitForAnimationReadyToRestyle(animation); + await waitForPaints(); + + assert_animation_is_not_running_on_compositor(animation, + 'Animation overridden by an !important rule reports that it is ' + + 'NOT running on the compositor'); + + const properties = animation.effect.getProperties(); + properties.forEach(property => { + assert_true(!property.runningOnCompositor, + property.property + ' is not running on the compositor'); + }); +}, 'Multiple transform-like properties animation does not runs on the ' + + 'compositor because one of the offset-* property is overridden ' + + 'by an !important rule'); + +promise_test(async t => { + const div = addDiv(t); + const animation = div.animate({ rotate: ['0deg', '45deg'], + transform: ['translate(20px)', + 'translate(30px)'], + offsetDistance: ['0%', '100%'] }, + 100 * MS_PER_SEC); + + div.style.setProperty('translate', '50px', 'important'); + getComputedStyle(div).translate; + + await waitForAnimationReadyToRestyle(animation); + await waitForPaints(); + + assert_animation_is_running_on_compositor(animation, + 'Animation is still running on the compositor'); + + const properties = animation.effect.getProperties(); + properties.forEach(property => { + assert_true(property.runningOnCompositor, + property.property + ' is running on the compositor'); + }); +}, 'Multiple transform-like properties animation still runs on the ' + + 'compositor because the overridden-by-!important property does not have ' + + 'animation'); + +promise_test(async t => { + // We should run the animations on the compositor for this case: + // 1. A transition of 'translate' + // 2. An !important rule on 'translate' + // 3. An animation of 'scale' + const div = addDiv(t, { style: 'translate: 100px !important;' }); + const animation = div.animate({ rotate: ['0deg', '45deg'] }, + 100 * MS_PER_SEC); + div.style.transition = 'translate 100s'; + getComputedStyle(div).transition; + + await waitForAnimationReadyToRestyle(animation); + await waitForPaints(); + + assert_animation_is_running_on_compositor(animation, + 'rotate animation should be running on the compositor'); + + div.style.setProperty('translate', '200px', 'important'); + getComputedStyle(div).translate; + + const anims = div.getAnimations(); + await waitForPaints(); + + assert_animation_is_running_on_compositor(anims[0], + `${anims[0].effect.getProperties()[0].property} animation should be ` + + `running on the compositor`); + assert_animation_is_running_on_compositor(anims[1], + `${anims[1].effect.getProperties()[0].property} animation should be ` + + `running on the compositor`); +}, 'Transform-like animations and transitions still runs on the compositor ' + + 'because the !important rule is overridden by a transition, and the ' + + 'transition property does not have animations'); + +promise_test(async t => { + const container = addDiv(t, { style: 'transform-style: preserve-3d;' }); + const targetA = addDiv(t, { style: 'transform-style: preserve-3d' }); + const targetB = addDiv(t, { style: 'transform-style: preserve-3d' }); + const targetC = addDiv(t); + container.appendChild(targetA); + targetA.append(targetB); + targetB.append(targetC); + + const animation1 = targetA.animate({ rotate: ['0 0 1 0deg', '1 1 1 45deg'] }, + 100 * MS_PER_SEC); + + const animation2 = targetC.animate({ rotate: ['0 0 1 0deg', '0 1 1 100deg'] }, + 100 * MS_PER_SEC); + + await waitForAnimationReadyToRestyle(animation1); + await waitForAnimationReadyToRestyle(animation2); + await waitForPaints(); + + assert_animation_is_running_on_compositor(animation1, + 'rotate animation in the 3d rendering context should be running on the ' + + 'compositor'); + assert_animation_is_running_on_compositor(animation2, + 'rotate animation in the 3d rendering context should be running on the ' + + 'compositor'); +}, 'Transform-like animations in the 3d rendering context should runs on the ' + + 'compositor'); + +promise_test(async t => { + const container = addDiv(t, { style: 'transform-style: preserve-3d;' }); + const target = addDiv(t, { style: 'transform-style: preserve-3d;' }); + const innerA = addDiv(t, { style: 'width: 50px; height: 50px;' }); + // The frame of innerB is too large, so this makes its ancenstors and children + // in the 3d context be not allowed the async animations. + const innerB = addDiv(t, { style: 'rotate: 0 1 1 100deg; ' + + 'transform-style: preserve-3d; ' + + 'text-indent: -9999em' }); + const innerB2 = addDiv(t, { style: 'rotate: 0 1 1 45deg;' }); + const innerBText = document.createTextNode("innerB"); + container.appendChild(target); + target.appendChild(innerA); + target.appendChild(innerB); + innerB.appendChild(innerBText); + innerB.appendChild(innerB2); + + const animation1 = target.animate({ rotate: ['0 0 1 0deg', '1 1 1 45deg'] }, + 100 * MS_PER_SEC); + + const animation2 = innerA.animate({ rotate: ['0 0 1 0deg', '0 1 1 100deg'] }, + 100 * MS_PER_SEC); + + const animation3 = innerB2.animate({ rotate: ['0 0 1 0deg', '0 1 1 90deg'] }, + 100 * MS_PER_SEC); + + await waitForAnimationReadyToRestyle(animation1); + await waitForAnimationReadyToRestyle(animation2); + await waitForAnimationReadyToRestyle(animation3); + await waitForPaints(); + + const isPartialPrerenderEnabled = + SpecialPowers.getBoolPref('layout.animation.prerender.partial'); + + if (isPartialPrerenderEnabled) { + assert_animation_is_running_on_compositor(animation1, + 'rotate animation in the 3d rendering context should be running on ' + + 'the compositor even if one of its inner frames is too large'); + assert_animation_is_running_on_compositor(animation2, + 'rotate animation in the 3d rendering context is still running on ' + + 'the compositor because its display item is created earlier'); + assert_animation_is_running_on_compositor(animation3, + 'rotate animation in the 3d rendering context should be running on ' + + 'the compositor even if one of its parent frames is too large'); + } else { + assert_animation_is_not_running_on_compositor(animation1, + 'rotate animation in the 3d rendering context should not be running on ' + + 'the compositor because one of its inner frames is too large'); + assert_animation_is_running_on_compositor(animation2, + 'rotate animation in the 3d rendering context is still running on ' + + 'the compositor because its display item is created earlier'); + assert_animation_is_not_running_on_compositor(animation3, + 'rotate animation in the 3d rendering context should not be running on ' + + 'the compositor because one of its parent frames is too large'); + } + innerBText.remove(); +}, 'Transform-like animations in the 3d rendering context should run on ' + + 'the compositor even if it is the ancestor of ones whose frames are too ' + + 'large or its ancestor is not allowed to run on the compositor'); + +promise_test(async t => { + const container = addDiv(t, { style: 'transform-style: preserve-3d;' }); + const target = addDiv(t, { style: 'transform-style: preserve-3d;' }); + // The frame of innerA is too large, so this makes its ancenstors and children + // in the 3d context be not allowed the async animations. + const innerA = addDiv(t, { style: 'transform-style: preserve-3d; ' + + 'text-indent: -9999em' }); + const innerAText = document.createTextNode("innerA"); + const innerB = addDiv(t, { style: 'width: 50px; height: 50px;' }); + container.appendChild(target); + target.appendChild(innerA); + target.appendChild(innerB); + innerA.appendChild(innerAText); + + const animation1 = target.animate({ rotate: ['0 0 1 0deg', '1 1 1 45deg'] }, + 100 * MS_PER_SEC); + + const animation2 = innerA.animate({ rotate: ['0 0 1 0deg', '0 1 1 100deg'] }, + 100 * MS_PER_SEC); + + const animation3 = innerB.animate({ rotate: ['0 0 1 0deg', '0 1 1 90deg'] }, + 100 * MS_PER_SEC); + + await waitForAnimationReadyToRestyle(animation1); + await waitForAnimationReadyToRestyle(animation2); + await waitForAnimationReadyToRestyle(animation3); + await waitForPaints(); + + const isPartialPrerenderEnabled = + SpecialPowers.getBoolPref('layout.animation.prerender.partial'); + + if (isPartialPrerenderEnabled) { + assert_animation_is_running_on_compositor(animation1, + 'rotate animation in the 3d rendering context should be running on ' + + 'the compositor even if one of its inner frames is too large'); + assert_animation_is_running_on_compositor(animation2, + 'rotate animation in the 3d rendering context should be running on ' + + 'the compositor even if its frame size is too large'); + assert_animation_is_running_on_compositor(animation3, + 'rotate animation in the 3d rendering context should be running on ' + + 'the compositor even if its previous sibling frame is too large'); + } else { + assert_animation_is_not_running_on_compositor(animation1, + 'rotate animation in the 3d rendering context should not be running on ' + + 'the compositor because one of its inner frames is too large'); + assert_animation_is_not_running_on_compositor(animation2, + 'rotate animation in the 3d rendering context should not be running on ' + + 'the compositor because its frame size is too large'); + assert_animation_is_not_running_on_compositor(animation3, + 'rotate animation in the 3d rendering context should not be running on ' + + 'the compositor because its previous sibling frame is too large'); + } + innerAText.remove(); +}, 'Transform-like animations in the 3d rendering context should run on ' + + 'the compositor even if its previous sibling frame size is too large'); + +</script> +</body> diff --git a/dom/animation/test/crashtests/1134538.html b/dom/animation/test/crashtests/1134538.html new file mode 100644 index 0000000000..136d63deee --- /dev/null +++ b/dom/animation/test/crashtests/1134538.html @@ -0,0 +1,8 @@ +<!doctype html> +<div contenteditable=true style="transition-property: width;"></div> +<style> +html { + transition-delay: 18446744073709551584s; + transform: rotate(0deg); +} +</style> diff --git a/dom/animation/test/crashtests/1216842-1.html b/dom/animation/test/crashtests/1216842-1.html new file mode 100644 index 0000000000..6ac40b2fb8 --- /dev/null +++ b/dom/animation/test/crashtests/1216842-1.html @@ -0,0 +1,35 @@ +<!doctype html> +<html class="reftest-wait"> + <head> + <title>Bug 1216842: effect-level easing function produces negative values (compositor thread)</title> + <style> + #target { + width: 100px; height: 100px; + background: blue; + } + </style> + </head> + <body> + <div id="target"></div> + </body> + <script> + var target = document.getElementById("target"); + var effect = + new KeyframeEffect( + target, + { opacity: [0, 1] }, + { + fill: "forwards", + /* The function produces negative values in (0, 0.766312060) */ + easing: "cubic-bezier(0,-0.5,1,-0.5)", + duration: 100, + iterations: 0.75 /* To finish in the extraporation range */ + } + ); + var animation = new Animation(effect, document.timeline); + animation.play(); + animation.finished.then(function() { + document.documentElement.className = ""; + }); + </script> +</html> diff --git a/dom/animation/test/crashtests/1216842-2.html b/dom/animation/test/crashtests/1216842-2.html new file mode 100644 index 0000000000..7bae8a3116 --- /dev/null +++ b/dom/animation/test/crashtests/1216842-2.html @@ -0,0 +1,35 @@ +<!doctype html> +<html class="reftest-wait"> + <head> + <title>Bug 1216842: effect-level easing function produces values greater than 1 (compositor thread)</title> + <style> + #target { + width: 100px; height: 100px; + background: blue; + } + </style> + </head> + <body> + <div id="target"></div> + </body> + <script> + var target = document.getElementById("target"); + var effect = + new KeyframeEffect( + target, + { opacity: [0, 1] }, + { + fill: "forwards", + /* The function produces values greater than 1 in (0.23368794, 1) */ + easing: "cubic-bezier(0,1.5,1,1.5)", + duration: 100, + iterations: 0.25 /* To finish in the extraporation range */ + } + ); + var animation = new Animation(effect, document.timeline); + animation.play(); + animation.finished.then(function() { + document.documentElement.className = ""; + }); + </script> +</html> diff --git a/dom/animation/test/crashtests/1216842-3.html b/dom/animation/test/crashtests/1216842-3.html new file mode 100644 index 0000000000..1bc2179a86 --- /dev/null +++ b/dom/animation/test/crashtests/1216842-3.html @@ -0,0 +1,27 @@ +<!doctype html> +<html class="reftest-wait"> + <head> + <title>Bug 1216842: effect-level easing function produces values greater than 1 (main-thread)</title> + </head> + <body> + <div id="target"></div> + </body> + <script> + var target = document.getElementById("target"); + var effect = + new KeyframeEffect( + target, + { color: ["red", "blue"] }, + { + fill: "forwards", + /* The function produces values greater than 1 in (0.23368794, 1) */ + easing: "cubic-bezier(0,1.5,1,1.5)", + duration: 100 + } + ); + var animation = new Animation(effect, document.timeline); + animation.pause(); + animation.currentTime = 250; + document.documentElement.className = ""; + </script> +</html> diff --git a/dom/animation/test/crashtests/1216842-4.html b/dom/animation/test/crashtests/1216842-4.html new file mode 100644 index 0000000000..eba13c396a --- /dev/null +++ b/dom/animation/test/crashtests/1216842-4.html @@ -0,0 +1,27 @@ +<!doctype html> +<html class="reftest-wait"> + <head> + <title>Bug 1216842: effect-level easing function produces negative values (main-thread)</title> + </head> + <body> + <div id="target"></div> + </body> + <script> + var target = document.getElementById("target"); + var effect = + new KeyframeEffect( + target, + { color: ["red", "blue"] }, + { + fill: "forwards", + /* The function produces negative values in (0, 0.766312060) */ + easing: "cubic-bezier(0,-0.5,1,-0.5)", + duration: 100 + } + ); + var animation = new Animation(effect, document.timeline); + animation.pause(); + animation.currentTime = 250; + document.documentElement.className = ""; + </script> +</html> diff --git a/dom/animation/test/crashtests/1216842-5.html b/dom/animation/test/crashtests/1216842-5.html new file mode 100644 index 0000000000..73b6f22c4b --- /dev/null +++ b/dom/animation/test/crashtests/1216842-5.html @@ -0,0 +1,38 @@ +<!doctype html> +<html class="reftest-wait"> + <head> + <title> + Bug 1216842: effect-level easing function produces negative values passed + to step-end function (compositor thread) + </title> + <style> + #target { + width: 100px; height: 100px; + background: blue; + } + </style> + </head> + <body> + <div id="target"></div> + </body> + <script> + var target = document.getElementById("target"); + var effect = + new KeyframeEffect( + target, + { opacity: [0, 1], easing: "step-end" }, + { + fill: "forwards", + /* The function produces negative values in (0, 0.766312060) */ + easing: "cubic-bezier(0,-0.5,1,-0.5)", + duration: 100, + iterations: 0.75 /* To finish in the extraporation range */ + } + ); + var animation = new Animation(effect, document.timeline); + animation.play(); + animation.finished.then(function() { + document.documentElement.className = ""; + }); + </script> +</html> diff --git a/dom/animation/test/crashtests/1216842-6.html b/dom/animation/test/crashtests/1216842-6.html new file mode 100644 index 0000000000..aaf80eeec3 --- /dev/null +++ b/dom/animation/test/crashtests/1216842-6.html @@ -0,0 +1,38 @@ +<!doctype html> +<html class="reftest-wait"> + <head> + <title> + Bug 1216842: effect-level easing function produces values greater than 1 + which are passed to step-end function (compositor thread) + </title> + <style> + #target { + width: 100px; height: 100px; + background: blue; + } + </style> + </head> + <body> + <div id="target"></div> + </body> + <script> + var target = document.getElementById("target"); + var effect = + new KeyframeEffect( + target, + { opacity: [0, 1], easing: "step-end" }, + { + fill: "forwards", + /* The function produces values greater than 1 in (0.23368794, 1) */ + easing: "cubic-bezier(0,1.5,1,1.5)", + duration: 100, + iterations: 0.25 /* To finish in the extraporation range */ + } + ); + var animation = new Animation(effect, document.timeline); + animation.play(); + animation.finished.then(function() { + document.documentElement.className = ""; + }); + </script> +</html> diff --git a/dom/animation/test/crashtests/1239889-1.html b/dom/animation/test/crashtests/1239889-1.html new file mode 100644 index 0000000000..aa10ff3ab8 --- /dev/null +++ b/dom/animation/test/crashtests/1239889-1.html @@ -0,0 +1,16 @@ +<!doctype html> +<html class="reftest-wait"> + <head> + <title>Bug 1239889</title> + </head> + <body> + </body> + <script> + var div = document.createElement('div'); + var effect = new KeyframeEffect(div, { opacity: [0, 1] }); + requestAnimationFrame(() => { + document.body.appendChild(div); + document.documentElement.classList.remove("reftest-wait"); + }); + </script> +</html> diff --git a/dom/animation/test/crashtests/1244595-1.html b/dom/animation/test/crashtests/1244595-1.html new file mode 100644 index 0000000000..13b2e2d7e7 --- /dev/null +++ b/dom/animation/test/crashtests/1244595-1.html @@ -0,0 +1,3 @@ +<div id=target><script> + var player = target.animate([{background: 'green'}, {background: 'green'}]); +</script> diff --git a/dom/animation/test/crashtests/1272475-1.html b/dom/animation/test/crashtests/1272475-1.html new file mode 100644 index 0000000000..e0b0495388 --- /dev/null +++ b/dom/animation/test/crashtests/1272475-1.html @@ -0,0 +1,20 @@ +<!doctype html> +<html> + <head> + <title>Bug 1272475 - scale function with an extreme large value</title> + <script> + function test() { + var div = document.createElement("div"); + div.setAttribute("style", "width: 1px; height: 1px; " + + "background: red;"); + document.body.appendChild(div); + div.animate([ { "transform": "scale(8)" }, + { "transform": "scale(9.5e+307)" }, + { "transform": "scale(32)" } ], + { "duration": 1000, "fill": "both" }); + } + </script> + </head> + <body onload="test()"> + </body> +</html> diff --git a/dom/animation/test/crashtests/1272475-2.html b/dom/animation/test/crashtests/1272475-2.html new file mode 100644 index 0000000000..da0e8605bd --- /dev/null +++ b/dom/animation/test/crashtests/1272475-2.html @@ -0,0 +1,20 @@ +<!doctype html> +<html> + <head> + <title>Bug 1272475 - rotate function with an extreme large value</title> + <script> + function test() { + var div = document.createElement("div"); + div.setAttribute("style", "width: 100px; height: 100px; " + + "background: red;"); + document.body.appendChild(div); + div.animate([ { "transform": "rotate(8rad)" }, + { "transform": "rotate(9.5e+307rad)" }, + { "transform": "rotate(32rad)" } ], + { "duration": 1000, "fill": "both" }); + } + </script> + </head> + <body onload="test()"> + </body> +</html> diff --git a/dom/animation/test/crashtests/1277272-1-inner.html b/dom/animation/test/crashtests/1277272-1-inner.html new file mode 100644 index 0000000000..2565aa6eb8 --- /dev/null +++ b/dom/animation/test/crashtests/1277272-1-inner.html @@ -0,0 +1,19 @@ +<!doctype html> +<head> +<script> +function start() { + var animation = document.body.animate([{marks: 'crop'},{marks: 'crop'}], 12); + document.write('<html><body></body></html>'); + + setTimeout(function() { animation.play(); }, 4); + setTimeout(function() { + animation.timeline = undefined; + + SpecialPowers.Cu.forceGC(); + window.top.continueTest(); + }, 5); +} +</script> +</head> +<body onload="start()"></body> +</html> diff --git a/dom/animation/test/crashtests/1277272-1.html b/dom/animation/test/crashtests/1277272-1.html new file mode 100644 index 0000000000..71b6c24148 --- /dev/null +++ b/dom/animation/test/crashtests/1277272-1.html @@ -0,0 +1,25 @@ +<!doctype html> +<html class="reftest-wait"> +<head> +<script> +var count = 0; + +function start() { + if (++count > 10) { + document.documentElement.className = ""; + return; + } + + var frame = document.getElementById("frame"); + frame.src = "./1277272-1-inner.html"; +} + +function continueTest() { + setTimeout(start.bind(window), 1); +} + +</script> +</head> +<body onload="start()"></body> +<iframe id="frame"> +</html> diff --git a/dom/animation/test/crashtests/1278485-1.html b/dom/animation/test/crashtests/1278485-1.html new file mode 100644 index 0000000000..e7347f5d84 --- /dev/null +++ b/dom/animation/test/crashtests/1278485-1.html @@ -0,0 +1,26 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset="UTF-8"> +<script> + +function boom() +{ + document.body.animate([], + { duration: 6, + easing: "cubic-bezier(0, -1e+39, 0, 0)" }); + document.body.animate([], + { duration: 6, + easing: "cubic-bezier(0, 1e+39, 0, 0)" }); + document.body.animate([], + { duration: 6, + easing: "cubic-bezier(0, 0, 0, -1e+39)" }); + document.body.animate([], + { duration: 6, + easing: "cubic-bezier(0, 0, 0, 1e+39)" }); +} + +</script> +</head> +<body onload="boom();"></body> +</html> diff --git a/dom/animation/test/crashtests/1282691-1.html b/dom/animation/test/crashtests/1282691-1.html new file mode 100644 index 0000000000..ab6e1a26c0 --- /dev/null +++ b/dom/animation/test/crashtests/1282691-1.html @@ -0,0 +1,23 @@ +<!DOCTYPE html> +<html class=reftest-wait> +<meta charset=utf-8> +<script> + +function boom() { + const div = document.createElement('div'); + const anim = div.animate([{}], {}); + document.documentElement.appendChild(div); + anim.pause(); + document.documentElement.removeChild(div); + + requestAnimationFrame(() => { + document.documentElement.appendChild(div); + anim.play(); + document.documentElement.className = ''; + }); +} + +</script> +</head> +<body onload="boom();"></body> +</html> diff --git a/dom/animation/test/crashtests/1291413-1.html b/dom/animation/test/crashtests/1291413-1.html new file mode 100644 index 0000000000..691a746c6e --- /dev/null +++ b/dom/animation/test/crashtests/1291413-1.html @@ -0,0 +1,20 @@ +<!doctype html> +<html class=reftest-wait> +<script> +window.onload = () => { + const div = document.createElement('div'); + + document.documentElement.appendChild(div); + const anim = div.animate([], 1000); + + anim.ready.then(() => { + anim.pause(); + anim.reverse(); + anim.playbackRate = 0; + anim.ready.then(() => { + document.documentElement.className = ''; + }); + }); +}; +</script> +</html> diff --git a/dom/animation/test/crashtests/1291413-2.html b/dom/animation/test/crashtests/1291413-2.html new file mode 100644 index 0000000000..fca3a93800 --- /dev/null +++ b/dom/animation/test/crashtests/1291413-2.html @@ -0,0 +1,21 @@ +<!doctype html> +<html class=reftest-wait> +<script> +window.onload = () => { + const div = document.createElement('div'); + + document.documentElement.appendChild(div); + const anim = div.animate([], 1000); + + anim.ready.then(() => { + anim.pause(); + anim.reverse(); + anim.playbackRate = 0; + anim.playbackRate = 1; + anim.ready.then(() => { + document.documentElement.className = ''; + }); + }); +}; +</script> +</html> diff --git a/dom/animation/test/crashtests/1304886-1.html b/dom/animation/test/crashtests/1304886-1.html new file mode 100644 index 0000000000..703ef902b9 --- /dev/null +++ b/dom/animation/test/crashtests/1304886-1.html @@ -0,0 +1,14 @@ +<!DOCTYPE html> +<html> +<script> +window.onload=function(){ + var e = document.createElement("div"); + document.documentElement.appendChild(e); + e.animate([{"font":"status-bar"}, + {"font":"unset"}], + {duration:6, + iterationStart:4, + iterationComposite:"accumulate"}); +}; +</script> +</html> diff --git a/dom/animation/test/crashtests/1309198-1.html b/dom/animation/test/crashtests/1309198-1.html new file mode 100644 index 0000000000..7fad5782c0 --- /dev/null +++ b/dom/animation/test/crashtests/1309198-1.html @@ -0,0 +1,40 @@ +<script> +function start() { + o53=document.createElement('frameset'); + o254=document.createElement('iframe'); + o280=document.createElement('audio'); + o317=document.documentElement; + o508=document.createElement('li'); + o508.appendChild(o317); + o590=document.createElement('li'); + o594=document.createElement('track'); + o280.appendChild(o594); + o647=document.createElement('ol'); + o654=document.createElement('li'); + o647.appendChild(o654); + o654.insertAdjacentHTML('beforebegin','<iframe>'); + document.write('<html><body></body></html>'); + o955=document.documentElement; + document.documentElement.appendChild(o647); + o590.appendChild(o955); + document.close(); + document.write('<html><body></body></html>'); + document.documentElement.appendChild(o590); + document.documentElement.appendChild(o254); + o280.controls^=1; + SpecialPowers.forceGC(); + o317.insertAdjacentHTML('afterend','<iframe>'); + document.documentElement.appendChild(o280); + o2695=document.implementation.createHTMLDocument(); + o2695.body.appendChild(o254); + o53.onerror=f0; + document.documentElement.appendChild(o508); + o2803=frames[1].document; + o2803.getAnimations(); +} +function f0() { + o2803.write('<html><body></body></html>'); + SpecialPowers.forceCC(); +} +</script> +<body onload="start()"></body> diff --git a/dom/animation/test/crashtests/1322291-1.html b/dom/animation/test/crashtests/1322291-1.html new file mode 100644 index 0000000000..87def99ba8 --- /dev/null +++ b/dom/animation/test/crashtests/1322291-1.html @@ -0,0 +1,24 @@ +<!DOCTYPE html> +<html class="reftest-wait"> +<script> +document.addEventListener("DOMContentLoaded", boom); +function boom(){ + let o1 = (function(){ + let e=document.createElement("frameset"); + document.documentElement.appendChild(e); + return e; + })(); + let a1 = o1.animate({ "transform": "rotate3d(22,73,26,374grad)" }, + { duration: 10, delay: 100 }); + + // We need to wait the end of the delay to ensure that the animation is + // composited on the compositor, but there is no event for script animation + // that fires after the delay phase finished. So we wait for finished promise + // instead. + a1.finished.then(function() { + document.documentElement.className = ""; + }); +} +</script> +</body> +</html> diff --git a/dom/animation/test/crashtests/1322291-2.html b/dom/animation/test/crashtests/1322291-2.html new file mode 100644 index 0000000000..b93d53224d --- /dev/null +++ b/dom/animation/test/crashtests/1322291-2.html @@ -0,0 +1,31 @@ +<!DOCTYPE html> +<html class="reftest-wait"> +<style> +div { + width: 100px; + height: 100px; + background-color: red; +} +</style> +<body> +<div id=o_0></div> +<script> +function boom(){ + var anim = o_0.animate([ + {}, + {"transform": "scale(2)"}, + {"transform": "none"} + ], { + duration: 100, + iterationStart: 0.5, + }); + // We need to wait for finished promise just like we do in 1322291-1.html. + anim.finished.then(function() { + document.documentElement.classList.remove("reftest-wait"); + }); +} +document.addEventListener("DOMContentLoaded", boom); +</script> + +</body> +</html> diff --git a/dom/animation/test/crashtests/1322382-1.html b/dom/animation/test/crashtests/1322382-1.html new file mode 100644 index 0000000000..6ca9c1b836 --- /dev/null +++ b/dom/animation/test/crashtests/1322382-1.html @@ -0,0 +1,16 @@ +<!DOCTYPE html> +<style> +details { + background-color: blue; + width: 100px; + height: 100px; +} +</style> +<html> +<details id=o1><div></div></details> +<script> +window.onload = function(){ + o1.animate([{'transform': 'none'}], 100); +}; +</script> +</html> diff --git a/dom/animation/test/crashtests/1323114-1.html b/dom/animation/test/crashtests/1323114-1.html new file mode 100644 index 0000000000..344fd87db0 --- /dev/null +++ b/dom/animation/test/crashtests/1323114-1.html @@ -0,0 +1,12 @@ +<!DOCTYPE html> +<html> +<body> +<div id=a /> +<script> +addEventListener("DOMContentLoaded", function(){ + a.animate([{"transform": "matrix3d(25,8788,-69,-24,-3,85,52,3,63,0,12,36810,-68,15,82,0) rotate(77rad)"}], + {fill: "both", iterationStart: 45, iterationComposite: "accumulate"}); +}); +</script> +</body> +</html> diff --git a/dom/animation/test/crashtests/1323114-2.html b/dom/animation/test/crashtests/1323114-2.html new file mode 100644 index 0000000000..527d05effa --- /dev/null +++ b/dom/animation/test/crashtests/1323114-2.html @@ -0,0 +1,18 @@ +<!DOCTYPE html> +<style> +div { + width: 100px; + height: 100px; + background-color: blue; + transform: rotate(45deg); +} +</style> +<div id="div"></div> +<script> +addEventListener('DOMContentLoaded', function (){ + var target = document.getElementById('div'); + target.animate([{ transform: 'translateX(100px)', composite: 'accumulate' }, + { transform: 'none' }], + 1000); +}); +</script> diff --git a/dom/animation/test/crashtests/1323119-1.html b/dom/animation/test/crashtests/1323119-1.html new file mode 100644 index 0000000000..fd979822b8 --- /dev/null +++ b/dom/animation/test/crashtests/1323119-1.html @@ -0,0 +1,13 @@ +<!DOCTYPE html> +<html class="reftest-wait"> +<body> +<script> +addEventListener("DOMContentLoaded", function() { + let a = document.createElement("th"); + document.documentElement.appendChild(a); + a.animate([{"mask": "repeat-y "}], 484); + document.documentElement.classList.remove("reftest-wait"); +}); +</script> +</body> +</html> diff --git a/dom/animation/test/crashtests/1324554-1.html b/dom/animation/test/crashtests/1324554-1.html new file mode 100644 index 0000000000..b3f9435a18 --- /dev/null +++ b/dom/animation/test/crashtests/1324554-1.html @@ -0,0 +1,19 @@ +<!DOCTYPE html> +<html> + <head> + <meta charset="UTF-8"> + <title>Bug 1324554 - missing final keyframes and zero-length segments together</title> + </head> + <script> + function go() { + var div = document.getElementById('target'); + div.animate([ { "flex": "none" }, + { "flex": "initial", offset: 0.5 }, + { "flex": "0.0 ", offset: 0.5 }, + {} ]); + } + </script> + <body onload="go()"> + <div id='target' ></div> + </body> +</html> diff --git a/dom/animation/test/crashtests/1325193-1.html b/dom/animation/test/crashtests/1325193-1.html new file mode 100644 index 0000000000..bd0666497c --- /dev/null +++ b/dom/animation/test/crashtests/1325193-1.html @@ -0,0 +1,18 @@ +<!DOCTYPE html> +<html class="reftest-wait"> +<head> +<meta charset="UTF-8"> +<script> +function boom(){ + o_0.animate([{"perspective": "262ex"}, {}], {duration: 1070, iterationStart: 68, iterationComposite: "accumulate"}); + o_0.animate([{"color": "white", "flex": "inherit"}, {"color": "red", "flex": "66% "}], 3439); + o_0.animate([{"font": "icon"}], 1849); + setTimeout(function() { + document.documentElement.classList.remove("reftest-wait"); + }, 500); +} +document.addEventListener("DOMContentLoaded", boom); +</script> +</head> +<body><details id=o_0><q></q></details></body> +</html> diff --git a/dom/animation/test/crashtests/1330190-1.html b/dom/animation/test/crashtests/1330190-1.html new file mode 100644 index 0000000000..fa14e0f741 --- /dev/null +++ b/dom/animation/test/crashtests/1330190-1.html @@ -0,0 +1,11 @@ +<!DOCTYPE html> +<html class="reftest-wait"> +<span id=a /> +<script> +addEventListener("DOMContentLoaded", function(){ + a.animate([{"left": "38%"}], 100); + a.appendChild(document.createElement("div")); + document.documentElement.classList.remove("reftest-wait"); +}); +</script> +</html> diff --git a/dom/animation/test/crashtests/1330190-2.html b/dom/animation/test/crashtests/1330190-2.html new file mode 100644 index 0000000000..57e5d31b28 --- /dev/null +++ b/dom/animation/test/crashtests/1330190-2.html @@ -0,0 +1,36 @@ +<!DOCTYPE html> +<html class="reftest-wait"> +<head> +<style> +@keyframes anim { +} + +#o_0:before { + animation: anim 10s; + content: ""; +} +</style> +<meta charset="UTF-8"> +<script> +function boom(){ + function getPseudoElement() { + var anim = document.getAnimations()[0]; + anim.cancel(); + return anim.effect.target; + } + + var target = getPseudoElement(); + target.animate([{"perspective": "262ex"}, {}], {duration: 1070, iterationStart: 68, iterationComposite: "accumulate"}); + target.animate([{"color": "white", "flex": "inherit"}, {"color": "red", "flex": "66% "}], 3439); + target.animate([{"font": "icon"}], 1849); + setTimeout(function() { + document.documentElement.classList.remove("reftest-wait"); + }, 500); +} +document.addEventListener("DOMContentLoaded", boom); +</script> +</head> +<body> +<div id=o_0></div> +</body> +</html> diff --git a/dom/animation/test/crashtests/1330513-1.html b/dom/animation/test/crashtests/1330513-1.html new file mode 100644 index 0000000000..a497cc9e27 --- /dev/null +++ b/dom/animation/test/crashtests/1330513-1.html @@ -0,0 +1,8 @@ +<!DOCTYPE html> +<html> +<body id=a></body> +<script> +document.getElementById("a") + .animate([{"filter": "grayscale(28%)"}], {fill:"forwards", composite:"add"}); +</script> +</html> diff --git a/dom/animation/test/crashtests/1332588-1.html b/dom/animation/test/crashtests/1332588-1.html new file mode 100644 index 0000000000..11719a86ca --- /dev/null +++ b/dom/animation/test/crashtests/1332588-1.html @@ -0,0 +1,25 @@ +<!DOCTYPE html> +<html class="reftest-wait"> +<head> +<meta charset="UTF-8"> +<style> +span { + width: 100px; + height: 100px; + background-color: red; +} +</style> +<script> +window.onload = function() { + let body = document.getElementsByTagName("body")[0]; + let o_0 = document.createElement("span"); + body.appendChild(o_0); + o_0.animate([{ "padding": "57pt", "transform": "none" }, + { "padding": "57pt", "transform": "rotate(90deg)" }] , 10000); + body.appendChild(document.createElement("colgroup")); + document.documentElement.classList.remove("reftest-wait"); +}; +</script> +</head> +<body></body> +</html> diff --git a/dom/animation/test/crashtests/1333539-1.html b/dom/animation/test/crashtests/1333539-1.html new file mode 100644 index 0000000000..c9111890b0 --- /dev/null +++ b/dom/animation/test/crashtests/1333539-1.html @@ -0,0 +1,30 @@ +<!DOCTYPE html> +<html class="reftest-wait"> +<head> +<meta charset="UTF-8"> +<script> +window.onload = function(){ + let body = document.getElementsByTagName("body")[0]; + let target = document.createElement("div"); + let anim1 = new Animation(); + let anim2 = new Animation(); + let effect = new KeyframeEffect(target, { opacity: [ 0, 1 ] }, 1000); + body.appendChild(target); + target.appendChild(document.createElement("meter")); + anim1.startTime = 88; + anim1.timeline = null; + anim1.pause(); + anim1.effect = effect; + anim2.effect = effect; + anim1.effect = effect; + + // anim1, since it doesn't have a timeline, will remain pause-pending, + // so just wait on anim2. + anim2.ready.then(() => { + document.documentElement.classList.remove("reftest-wait"); + }); +}; +</script> +</head> +<body></body> +</html> diff --git a/dom/animation/test/crashtests/1333539-2.html b/dom/animation/test/crashtests/1333539-2.html new file mode 100644 index 0000000000..b00700eccb --- /dev/null +++ b/dom/animation/test/crashtests/1333539-2.html @@ -0,0 +1,38 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset="UTF-8"> +<style> +div { + width: 100px; + height: 100px; + background-color: blue; +} +</style> +<script> +window.onload = function(){ + let body = document.getElementsByTagName("body")[0]; + let target = document.createElement("div"); + let anim1 = new Animation(); + let anim2 = new Animation(); + let effect = new KeyframeEffect(target, { opacity: [ 0, 1 ] }, 1000); + body.appendChild(target); + anim1.startTime = 88; + anim1.timeline = null; + anim1.pause(); + anim1.effect = effect; + anim2.effect = effect; + anim1.effect = effect; + // Put another opacity animation on the top of the effect stack so that we + // try to send a lower priority animation that has no timeline to the + // compositor. + let anim3 = target.animate({ opacity : [ 1, 0 ] }, 1000); + + Promise.all([anim1.ready, anim2.ready, anim2.ready]).then(function() { + document.documentElement.classList.remove("reftest-wait"); + }); +}; +</script> +</head> +<body></body> +</html> diff --git a/dom/animation/test/crashtests/1334582-1.html b/dom/animation/test/crashtests/1334582-1.html new file mode 100644 index 0000000000..d67ddc3c52 --- /dev/null +++ b/dom/animation/test/crashtests/1334582-1.html @@ -0,0 +1,11 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset="UTF-8"> +<script> +window.onload = function(){ + let a = document.documentElement.animate([], {"iterations": 1.7976931348623157e+308, "fill": "both"}); +}; +</script> +</head> +</html> diff --git a/dom/animation/test/crashtests/1334582-2.html b/dom/animation/test/crashtests/1334582-2.html new file mode 100644 index 0000000000..d3b223650d --- /dev/null +++ b/dom/animation/test/crashtests/1334582-2.html @@ -0,0 +1,11 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset="UTF-8"> +<script> +window.onload = function(){ + let a = document.documentElement.animate([], {"iterationStart": 1.7976931348623157e+308, "fill": "both"}); +}; +</script> +</head> +</html> diff --git a/dom/animation/test/crashtests/1334583-1.html b/dom/animation/test/crashtests/1334583-1.html new file mode 100644 index 0000000000..b4d4109f0d --- /dev/null +++ b/dom/animation/test/crashtests/1334583-1.html @@ -0,0 +1,9 @@ +<div style="width: 200px; height: 200px; background: purple" id=div> +</div> +<script> +const animation = div.animate( + [ { transform: "scale(1)" }, + { transform: "scale(2)" } ], + { iterations: Infinity, duration: 512 } ); +animation.currentTime = 2147483647; +</script> diff --git a/dom/animation/test/crashtests/1335998-1.html b/dom/animation/test/crashtests/1335998-1.html new file mode 100644 index 0000000000..72353a9692 --- /dev/null +++ b/dom/animation/test/crashtests/1335998-1.html @@ -0,0 +1,28 @@ +<!doctype html> +<html class="reftest-wait"> + <head> + <title> + Bug 1335998 - Handle {Interpolate, Accumulate}Matrix of mismatched transform lists + </title> + <style> + #target { + width: 100px; height: 100px; + background: blue; + transform: rotate(45deg); + } + </style> + </head> + <body> + <div id="target"></div> + </body> + <script> + var div = document.getElementById("target"); + var animation = div.animate([ { transform: 'translateX(200px) scale(2.0)', + composite: 'accumulate' }, + { transform: 'rotate(-45deg)' } ], + 2000); + animation.finished.then(function() { + document.documentElement.className = ""; + }); + </script> +</html> diff --git a/dom/animation/test/crashtests/1343589-1.html b/dom/animation/test/crashtests/1343589-1.html new file mode 100644 index 0000000000..a494b83da3 --- /dev/null +++ b/dom/animation/test/crashtests/1343589-1.html @@ -0,0 +1,18 @@ +<!DOCTYPE html> +<html class="reftest-wait"> +<head> +<meta charset="UTF-8"> +<script> +window.onload = function(){ + let a = document.documentElement.animate(null, + { duration: 100, iterations: Number.POSITIVE_INFINITY }); + a.startTime = 100000; // Set the start time far in the future + // Try reversing (this should throw because the target effect end is infinity) + try { a.reverse(); } catch(e) {} + // Do something that will trigger a timing update + a.effect.target = document.createElement("span"); + document.documentElement.className = ''; +}; +</script> +</head> +</html> diff --git a/dom/animation/test/crashtests/1359658-1.html b/dom/animation/test/crashtests/1359658-1.html new file mode 100644 index 0000000000..972ec497fa --- /dev/null +++ b/dom/animation/test/crashtests/1359658-1.html @@ -0,0 +1,33 @@ +<!doctype html> +<html class="reftest-wait"> + <head> + <meta charset=utf-8> + <title>Bug 1359658: Animation-only dirty descendants bit should be cleared + for display:none content</title> + </head> + <body> + <div id="ancestor"> + <svg> + <rect id="target" width="100%" height="100%" fill="lime"/> + </svg> + </div> + </body> + <script> +'use strict'; + +const ancestor = document.getElementById('ancestor'); +const target = document.getElementById('target'); + +document.addEventListener('DOMContentLoaded', () => { + const animation = target.animate({ color: [ 'red', 'lime' ] }, + { duration: 1000, iterations: Infinity }); + requestAnimationFrame(() => { + // Tweak animation to cause animation dirty bit to be set + animation.effect.updateTiming({ duration: 2000 }); + ancestor.style.display = "none"; + getComputedStyle(ancestor).display; + document.documentElement.className = ''; + }); +}); + </script> +</html> diff --git a/dom/animation/test/crashtests/1373712-1.html b/dom/animation/test/crashtests/1373712-1.html new file mode 100644 index 0000000000..8b5c121c85 --- /dev/null +++ b/dom/animation/test/crashtests/1373712-1.html @@ -0,0 +1,11 @@ +<!DOCTYPE html> +<html> +<head> +<title>Bug 1373712 - Assertions of SpecifiedKeyframeArraysAreEqual(mKeyframes, keyframesCopy) with large color value +</title> +<meta charset="UTF-8"> +<script> +document.documentElement.animate([{ "color": "hsl(63e292,41%,34%)" }]); +</script> +</head> +</html> diff --git a/dom/animation/test/crashtests/1379606-1.html b/dom/animation/test/crashtests/1379606-1.html new file mode 100644 index 0000000000..89f756bf06 --- /dev/null +++ b/dom/animation/test/crashtests/1379606-1.html @@ -0,0 +1,21 @@ +<!doctype html> +<style> +div { + display: none; +} +</style> +<div> +<div> +<div> +<div> +<div> +<div id="target"> +</div> +</div> +</div> +</div> +</div> +</div> +<script> + target.animate({ color: "red" }) +</script> diff --git a/dom/animation/test/crashtests/1393605-1.html b/dom/animation/test/crashtests/1393605-1.html new file mode 100644 index 0000000000..9f282e58ba --- /dev/null +++ b/dom/animation/test/crashtests/1393605-1.html @@ -0,0 +1,15 @@ +<!doctype html> +<html> + <head> + <title> + Bug 1393605 - Still work if determinant of 2d matrix is not 1 or -1 + </title> + </head> + <script> + document.documentElement.animate( + [ { 'transform': 'scale(4)' }, + { 'transform': 'rotate(3grad) scaleX(0) ' + + 'translate(2mm) matrix(2,7,1,.32,7,0)' } ], + { fill: 'both' }); + </script> +</html> diff --git a/dom/animation/test/crashtests/1400022-1.html b/dom/animation/test/crashtests/1400022-1.html new file mode 100644 index 0000000000..8256091e1e --- /dev/null +++ b/dom/animation/test/crashtests/1400022-1.html @@ -0,0 +1,10 @@ +<script> +requestIdleCallback(function(){ location.reload() }) +a = document.createElement("x") +document.documentElement.appendChild(a) +b = document.createElement('link') +b.setAttribute('rel', 'stylesheet') +b.setAttribute('href', 'data:,*{border-block-start:solid}') +document.head.appendChild(b) +a.insertAdjacentHTML("afterBegin", "<d id='id0' style='transition-duration:1s'><svg filter='url(#id0)'>") +</script> diff --git a/dom/animation/test/crashtests/1401809.html b/dom/animation/test/crashtests/1401809.html new file mode 100644 index 0000000000..7a3adcc60c --- /dev/null +++ b/dom/animation/test/crashtests/1401809.html @@ -0,0 +1,14 @@ +<html> + <head> + <style></style> + <script> + o1 = document.createElement('t'); + document.documentElement.appendChild(o1); + document.styleSheets[0].insertRule('* { will-change:an }', 0); + k = new KeyframeEffect(o1, [{'willChange':'s'}], {'':''}); + k = null; + SpecialPowers.forceGC(); + SpecialPowers.forceCC(); + </script> + </head> +</html> diff --git a/dom/animation/test/crashtests/1411318-1.html b/dom/animation/test/crashtests/1411318-1.html new file mode 100644 index 0000000000..5c8e7211c1 --- /dev/null +++ b/dom/animation/test/crashtests/1411318-1.html @@ -0,0 +1,15 @@ +<html> + <head> + <script> + o1 = (new DOMParser).parseFromString('', 'text/html'); + o2 = document.createElement('canvas'); + document.documentElement.appendChild(o2); + o3 = o2.animate([{'transform':'unset'}], {'delay':32}); + o4 = o3.effect; + o5 = o1.createElement('d'); + o6 = new Animation(o4, document.timeline); + o7 = o5.animate([], {}); + o7.effect = o6.effect; + </script> + </head> +</html> diff --git a/dom/animation/test/crashtests/1467277-1.html b/dom/animation/test/crashtests/1467277-1.html new file mode 100644 index 0000000000..c58fc64493 --- /dev/null +++ b/dom/animation/test/crashtests/1467277-1.html @@ -0,0 +1,6 @@ +<script> +addEventListener("DOMContentLoaded", () => { + document.documentElement.animate( + [ { "transform": "rotate3d(1e58, 2, 6, 0turn)" } ], 1000) +}) +</script> diff --git a/dom/animation/test/crashtests/1468294-1.html b/dom/animation/test/crashtests/1468294-1.html new file mode 100644 index 0000000000..e4092046ac --- /dev/null +++ b/dom/animation/test/crashtests/1468294-1.html @@ -0,0 +1,7 @@ +<script> +addEventListener("DOMContentLoaded", () => { + document.documentElement.animate([{ "transform": "matrix(2,1,1,5,2,8)" }], + { duration: 1000, + easing: "cubic-bezier(1,-15,.6,4)" }); +}) +</script> diff --git a/dom/animation/test/crashtests/1524480-1.html b/dom/animation/test/crashtests/1524480-1.html new file mode 100644 index 0000000000..89e5a412d9 --- /dev/null +++ b/dom/animation/test/crashtests/1524480-1.html @@ -0,0 +1,37 @@ +<!doctype html> +<html class="reftest-wait"> +<meta charset=utf-8> +<style> +div { + display: none; + width: 100px; + height: 100px; + background: blue; +} +</style> +<div id=div></div> +<script> +async function test() { + const animation = div.animate({ transform: ['none', 'none'] }, 1000); + animation.cancel(); + + await waitForFrame(); + + div.style.display = 'block'; + + await waitForFrame(); + await waitForFrame(); + + animation.play(); + await animation.finished; + + document.documentElement.className = ""; +} + +function waitForFrame() { + return new Promise(resolve => requestAnimationFrame(resolve)); +} + +test(); +</script> +</html> diff --git a/dom/animation/test/crashtests/1575926.html b/dom/animation/test/crashtests/1575926.html new file mode 100644 index 0000000000..cc37c94235 --- /dev/null +++ b/dom/animation/test/crashtests/1575926.html @@ -0,0 +1,24 @@ +<style> + @keyframes animation_0 { + 88% { } + } + + * { + animation-name: animation_0; + animation-delay: 4s; + } +</style> +<script> + function start () { + const input = document.createElement('input') + document.documentElement.appendChild(input) + const animations = input.getAnimations({}) + const animation = animations[(3782796448 % animations.length)] + const effect = animation.effect + effect.setKeyframes({ 'borderLeft': ['inherit'] }) + effect.target = null + input.contentEditable = 'true' + } + + document.addEventListener('DOMContentLoaded', start) +</script> diff --git a/dom/animation/test/crashtests/1585770.html b/dom/animation/test/crashtests/1585770.html new file mode 100644 index 0000000000..018d688582 --- /dev/null +++ b/dom/animation/test/crashtests/1585770.html @@ -0,0 +1,22 @@ +<html class="reftest-wait"> +<script> +function start () { + const kf_effect = + new KeyframeEffect(document.documentElement, + { opacity: ['', '1'] }, + { easing: 'step-end', + duration: 10000 } ); + const copy = new KeyframeEffect(kf_effect); + const animation = new Animation(copy); + + animation.reverse(); + document.documentElement.getBoundingClientRect(); + + requestAnimationFrame(() => { + document.documentElement.classList.remove("reftest-wait"); + }); +} + +document.addEventListener('DOMContentLoaded', start); +</script> +</html> diff --git a/dom/animation/test/crashtests/1604500-1.html b/dom/animation/test/crashtests/1604500-1.html new file mode 100644 index 0000000000..01a6eafd1f --- /dev/null +++ b/dom/animation/test/crashtests/1604500-1.html @@ -0,0 +1,24 @@ +<!doctype html> +<html> +<head> +<script> +function start () { + const keyframe = new KeyframeEffect(undefined, {}); + const animation = new Animation(keyframe, undefined); + // Make animation run backwards... + animation.playbackRate = -100; + // But then set the current time to the future so it becomes "current"... + animation.currentTime = 2055; + // After updating the playback rate to zero, however, it should no longer + // be "current" (and this takes effect immediately because |animation| is + // paused)... + animation.updatePlaybackRate(0); + // Now update the target and hope nothing goes wrong... + keyframe.target = div; +} + +document.addEventListener('DOMContentLoaded', start) +</script> +</head> +<div id=div></div> +</html> diff --git a/dom/animation/test/crashtests/1611847.html b/dom/animation/test/crashtests/1611847.html new file mode 100644 index 0000000000..720ce1179b --- /dev/null +++ b/dom/animation/test/crashtests/1611847.html @@ -0,0 +1,23 @@ +<html> +<head> + <style> + * { + transition-duration: 2s; + } + </style> + <script> + function start () { + const element = document.createElementNS('', 's'); + const effect = new KeyframeEffect(document.documentElement, {}, 196); + document.documentElement.setAttribute('style', 'padding-left:3'); + effect.updateTiming({ 'delay': 2723 }); + const animations = document.getAnimations(); + animations[0].effect = effect; + animations[0].updatePlaybackRate(-129); + effect.target = element; + } + + document.addEventListener('DOMContentLoaded', start); + </script> +</head> +</html> diff --git a/dom/animation/test/crashtests/1612891-1.html b/dom/animation/test/crashtests/1612891-1.html new file mode 100644 index 0000000000..44cf022e88 --- /dev/null +++ b/dom/animation/test/crashtests/1612891-1.html @@ -0,0 +1,15 @@ +<html> +<head> + <script> + function start() { + const element = document.createElement('img') + element.animate([ + { 'easing': '' }, + { 'offset': 'o' }, + ], {}) + } + + window.addEventListener('load', start) + </script> +</head> +</html> diff --git a/dom/animation/test/crashtests/1612891-2.html b/dom/animation/test/crashtests/1612891-2.html new file mode 100644 index 0000000000..a9779ba70f --- /dev/null +++ b/dom/animation/test/crashtests/1612891-2.html @@ -0,0 +1,15 @@ +<html> +<head> + <script> + function start() { + const element = document.createElement('img') + element.animate([ + { 'easing': '' }, + 123, + ], {}) + } + + window.addEventListener('load', start) + </script> +</head> +</html> diff --git a/dom/animation/test/crashtests/1612891-3.html b/dom/animation/test/crashtests/1612891-3.html new file mode 100644 index 0000000000..89e71b6ca8 --- /dev/null +++ b/dom/animation/test/crashtests/1612891-3.html @@ -0,0 +1,10 @@ +<html> +<head> + <script> + document.addEventListener('DOMContentLoaded', () => { + const keyframe = new KeyframeEffect(document.documentElement, [{}], {}) + keyframe.setKeyframes([{ 'easing': '' }, { 'offset': 'o', }]) + }) + </script> +</head> +</html> diff --git a/dom/animation/test/crashtests/1633442.html b/dom/animation/test/crashtests/1633442.html new file mode 100644 index 0000000000..cb4beedebc --- /dev/null +++ b/dom/animation/test/crashtests/1633442.html @@ -0,0 +1,15 @@ +<!doctype html> +<html class="reftest-wait"> +<head> +<script> + document.addEventListener('DOMContentLoaded', () => { + document.documentElement.style.setProperty('transition-duration', '3s', '') + document.documentElement.style.setProperty('rotate', '2deg', undefined) + document.documentElement.style.setProperty('border-radius', '2%', '') + const [anim_1, anim_0] = document.documentElement.getAnimations({}) + anim_1.effect = anim_0.effect + document.documentElement.classList.remove("reftest-wait"); + }) +</script> +</head> +</html> diff --git a/dom/animation/test/crashtests/1633486.html b/dom/animation/test/crashtests/1633486.html new file mode 100644 index 0000000000..20b88f6327 --- /dev/null +++ b/dom/animation/test/crashtests/1633486.html @@ -0,0 +1,20 @@ +<!doctype html> +<html> +<head> + <script> + window.addEventListener('load', async () => { + const element = document.getElementById('target'); + element.animate({ + 'all': ['initial'] + }, { + 'duration': 500, + 'pseudoElement': '::marker', + }) + element.hidden = false; + }) + </script> +</head> +<ul> + <li id='target' hidden></li> +</ul> +</html> diff --git a/dom/animation/test/crashtests/1656419.html b/dom/animation/test/crashtests/1656419.html new file mode 100644 index 0000000000..4e76cb0a55 --- /dev/null +++ b/dom/animation/test/crashtests/1656419.html @@ -0,0 +1,23 @@ +<!DOCTYPE html> +<html class="reftest-wait"> +<style> +#target { + width: 200vw; + height: 200vh; +} +</style> +<div id="target"></div> +<script> +const animA = target.animate( + { transform: 'translateX(100px)' }, + { duration: 50 } +); +const animB = target.animate( + { transform: 'translateX(100px)', composite: 'add' }, + { duration: 100 } +); +animB.finished.then(() => { + document.documentElement.classList.remove("reftest-wait"); +}); +</script> +</html> diff --git a/dom/animation/test/crashtests/1699890.html b/dom/animation/test/crashtests/1699890.html new file mode 100644 index 0000000000..95aa1e190c --- /dev/null +++ b/dom/animation/test/crashtests/1699890.html @@ -0,0 +1,13 @@ +<!DOCTYPE html> +<html> +<style> +@keyframes anim { + from { background-color: rgba(0, 0, 0, 0); } + to { background-color: rgba(255, 0, 0, 255); } +} +body { + animation: anim 100s; + width: 100vw; + height: 100vh; +} +</style> diff --git a/dom/animation/test/crashtests/1706157.html b/dom/animation/test/crashtests/1706157.html new file mode 100644 index 0000000000..cfa7b70b56 --- /dev/null +++ b/dom/animation/test/crashtests/1706157.html @@ -0,0 +1,19 @@ +<!DOCTYPE html> +<html> +<head> + <style> + @keyframes animation { + to { + left: 100px; + } + } + * { + animation: animation linear 1s; + } + #target { + animation-timing-function: steps(2965566999, jump-both); + } + </style> +</head> +<div id="target"></div> +</html> diff --git a/dom/animation/test/crashtests/1714421.html b/dom/animation/test/crashtests/1714421.html new file mode 100644 index 0000000000..5408d70bc0 --- /dev/null +++ b/dom/animation/test/crashtests/1714421.html @@ -0,0 +1,8 @@ +<script> +document.addEventListener('DOMContentLoaded', () => { + document.documentElement.style.setProperty('scale', '89%', undefined) + document.documentElement.style.setProperty('transition-duration', '2009216159ms', '') + document.getAnimations()[0].timeline = undefined + document.documentElement.animate({'scale': ['none', 'none', 'none']}, 1807) +}) +</script> diff --git a/dom/animation/test/crashtests/1807966.html b/dom/animation/test/crashtests/1807966.html new file mode 100644 index 0000000000..d2759d2b5c --- /dev/null +++ b/dom/animation/test/crashtests/1807966.html @@ -0,0 +1,13 @@ +<!DOCTYPE html> +<style> + #target { + transition-timing-function: linear(calc(15 / 0), 10 10%); + transition-duration: 10s; + } +</style> +<script> + window.addEventListener('load', () => { + target.style.paddingBlock = "10px 10px"; + }) +</script> +<slot id="target"></slot> diff --git a/dom/animation/test/crashtests/1875441.html b/dom/animation/test/crashtests/1875441.html new file mode 100644 index 0000000000..d7a4ff911c --- /dev/null +++ b/dom/animation/test/crashtests/1875441.html @@ -0,0 +1,12 @@ +<!doctype html> +<style> +@property --my-property { + syntax: "<length>", + inherits: false, + initial-value: 0, +} +</style> +<div id="box"></div> +<script> +SpecialPowers.DOMWindowUtils.computeAnimationDistance(box, "--my-property", "red", "blue"); +</script> diff --git a/dom/animation/test/crashtests/crashtests.list b/dom/animation/test/crashtests/crashtests.list new file mode 100644 index 0000000000..be9b1c8ae9 --- /dev/null +++ b/dom/animation/test/crashtests/crashtests.list @@ -0,0 +1,62 @@ +load 1134538.html +load 1239889-1.html +load 1244595-1.html +pref(dom.animations-api.timelines.enabled,true) load 1216842-1.html +pref(dom.animations-api.timelines.enabled,true) load 1216842-2.html +pref(dom.animations-api.timelines.enabled,true) load 1216842-3.html +pref(dom.animations-api.timelines.enabled,true) load 1216842-4.html +pref(dom.animations-api.timelines.enabled,true) load 1216842-5.html +pref(dom.animations-api.timelines.enabled,true) load 1216842-6.html +load 1272475-1.html +load 1272475-2.html +load 1278485-1.html +pref(dom.animations-api.timelines.enabled,true) load 1277272-1.html +load 1282691-1.html +load 1291413-1.html +load 1291413-2.html +pref(dom.animations-api.compositing.enabled,true) load 1304886-1.html +load 1309198-1.html +load 1322382-1.html +load 1322291-1.html +load 1322291-2.html +pref(dom.animations-api.compositing.enabled,true) load 1323114-1.html +pref(dom.animations-api.compositing.enabled,true) load 1323114-2.html +load 1323119-1.html +load 1324554-1.html +pref(dom.animations-api.compositing.enabled,true) load 1325193-1.html +load 1332588-1.html +load 1330190-1.html +pref(dom.animations-api.compositing.enabled,true) load 1330190-2.html +pref(dom.animations-api.compositing.enabled,true) load 1330513-1.html +pref(dom.animations-api.timelines.enabled,true) load 1333539-1.html +pref(dom.animations-api.timelines.enabled,true) load 1333539-2.html +load 1334582-1.html +load 1334582-2.html +load 1334583-1.html +pref(dom.animations-api.compositing.enabled,true) load 1335998-1.html +load 1343589-1.html +load 1359658-1.html +load 1373712-1.html +load 1379606-1.html +load 1393605-1.html +load 1400022-1.html +load 1401809.html +pref(dom.animations-api.timelines.enabled,true) load 1411318-1.html +load 1468294-1.html +load 1467277-1.html +load 1524480-1.html +load 1575926.html +load 1585770.html +load 1604500-1.html +load 1611847.html +load 1612891-1.html +load 1612891-2.html +load 1612891-3.html +load 1633442.html +load 1633486.html +pref(layout.animation.prerender.partial,true) load 1656419.html +load 1706157.html +pref(gfx.omta.background-color,true) load 1699890.html +pref(dom.animations-api.timelines.enabled,true) load 1714421.html +load 1807966.html +load 1875441.html diff --git a/dom/animation/test/document-timeline/test_document-timeline.html b/dom/animation/test/document-timeline/test_document-timeline.html new file mode 100644 index 0000000000..3a7d64af2c --- /dev/null +++ b/dom/animation/test/document-timeline/test_document-timeline.html @@ -0,0 +1,147 @@ +<!doctype html> +<meta charset=utf-8> +<title>Web Animations API: DocumentTimeline tests</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../testcommon.js"></script> +<iframe srcdoc='<html><meta charset=utf-8></html>' width="10" height="10" id="iframe"></iframe> +<iframe srcdoc='<html style="display:none"><meta charset=utf-8></html>' width="10" height="10" id="hidden-iframe"></iframe> +<div id="log"></div> +<script> +'use strict'; + +test(function() { + assert_equals(document.timeline, document.timeline, + 'document.timeline returns the same object every time'); + var iframe = document.getElementById('iframe'); + assert_not_equals(document.timeline, iframe.contentDocument.timeline, + 'document.timeline returns a different object for each document'); + assert_not_equals(iframe.contentDocument.timeline, null, + 'document.timeline on an iframe is not null'); +}, +'document.timeline identity tests', +{ + help: 'http://dev.w3.org/fxtf/web-animations/#the-document-timeline', + assert: [ 'Each document has a timeline called the document timeline' ], + author: 'Brian Birtles' +}); + +async_test(function(t) { + const { AppConstants } = SpecialPowers.ChromeUtils.importESModule( + "resource://gre/modules/AppConstants.sys.mjs" + ); + + if (AppConstants.platform == "android") { + // Skip this test case on Android since it frequently fails on the + // environments. See bug 1761900. + t.done(); + } + + assert_greater_than_equal(document.timeline.currentTime, 0, + 'document.timeline.currentTime is positive or zero'); + // document.timeline.currentTime should be set even before document + // load fires. We expect this code to be run before document load and hence + // the above assertion is sufficient. + // If the following assertion fails, this test needs to be redesigned. + assert_true(document.readyState !== 'complete', + 'Test is running prior to document load'); + + // Test that the document timeline's current time is measured from + // navigationStart. + // + // We can't just compare document.timeline.currentTime to + // window.performance.now() because currentTime is only updated on a sample + // so we use requestAnimationFrame instead. + window.requestAnimationFrame(t.step_func(function(rafTime) { + assert_equals(document.timeline.currentTime, rafTime, + 'document.timeline.currentTime matches' + + ' requestAnimationFrame time'); + t.done(); + })); +}, +'document.timeline.currentTime value tests', +{ + help: [ + 'http://dev.w3.org/fxtf/web-animations/#the-global-clock', + 'http://dev.w3.org/fxtf/web-animations/#the-document-timeline' + ], + assert: [ + 'The global clock is a source of monotonically increasing time values', + 'The time values of the document timeline are calculated as a fixed' + + ' offset from the global clock', + 'the zero time corresponds to the navigationStart moment', + 'the time value of each document timeline must be equal to the time ' + + 'passed to animation frame request callbacks for that browsing context' + ], + author: 'Brian Birtles' +}); + +async_test(function(t) { + var valueAtStart = document.timeline.currentTime; + var timeAtStart = window.performance.now(); + while (window.performance.now() - timeAtStart < 100) { + // Wait 100ms + } + assert_equals(document.timeline.currentTime, valueAtStart, + 'document.timeline.currentTime does not change within a script block'); + window.requestAnimationFrame(t.step_func(function() { + assert_true(document.timeline.currentTime > valueAtStart, + 'document.timeline.currentTime increases between script blocks'); + t.done(); + })); +}, +'document.timeline.currentTime liveness tests', +{ + help: 'http://dev.w3.org/fxtf/web-animations/#script-execution-and-live-updates-to-the-model', + assert: [ 'The value returned by the currentTime attribute of a' + + ' document timeline will not change within a script block' ], + author: 'Brian Birtles' +}); + +test(function() { + var hiddenIFrame = document.getElementById('hidden-iframe'); + assert_equals(typeof hiddenIFrame.contentDocument.timeline.currentTime, + 'number', + 'currentTime of an initially hidden subframe\'s timeline is a number'); + assert_true(hiddenIFrame.contentDocument.timeline.currentTime >= 0, + 'currentTime of an initially hidden subframe\'s timeline is >= 0'); +}, 'document.timeline.currentTime hidden subframe test'); + +async_test(function(t) { + var hiddenIFrame = document.getElementById('hidden-iframe'); + + // Don't run the test until after the iframe has completed loading or else the + // contentDocument may change. + var testToRunOnLoad = t.step_func(function() { + // Remove display:none + hiddenIFrame.style.display = 'block'; + getComputedStyle(hiddenIFrame).display; + + window.requestAnimationFrame(t.step_func(function() { + assert_greater_than(hiddenIFrame.contentDocument.timeline.currentTime, 0, + 'document.timeline.currentTime is positive after removing' + + ' display:none'); + var previousValue = hiddenIFrame.contentDocument.timeline.currentTime; + + // Re-introduce display:none + hiddenIFrame.style.display = 'none'; + getComputedStyle(hiddenIFrame).display; + + window.requestAnimationFrame(t.step_func(function() { + assert_true( + hiddenIFrame.contentDocument.timeline.currentTime >= previousValue, + 'document.timeline.currentTime does not go backwards after' + + ' re-setting display:none'); + t.done(); + })); + })); + }); + + if (hiddenIFrame.contentDocument.readyState === 'complete') { + testToRunOnLoad(); + } else { + hiddenIFrame.addEventListener("load", testToRunOnLoad); + } +}, 'document.timeline.currentTime hidden subframe dynamic test'); + +</script> diff --git a/dom/animation/test/document-timeline/test_request_animation_frame.html b/dom/animation/test/document-timeline/test_request_animation_frame.html new file mode 100644 index 0000000000..3da4e4deb2 --- /dev/null +++ b/dom/animation/test/document-timeline/test_request_animation_frame.html @@ -0,0 +1,27 @@ +<!DOCTYPE html> +<meta charset=utf-8> +<title>Test RequestAnimationFrame Timestamps are monotonically increasing</title> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +<script> + var lastRequestAnimationFrameTimestamp = 0; + var requestAnimationFrameCount = 20; + var currentCount = 0; + + // Test that all timestamps are always increasing + // and do not ever go backwards + function rafCallback(aTimestamp) { + SimpleTest.ok(aTimestamp > lastRequestAnimationFrameTimestamp, + "New RequestAnimationFrame timestamp should be later than the previous RequestAnimationFrame timestamp"); + lastRequestAnimationFrameTimestamp = aTimestamp; + if (currentCount == requestAnimationFrameCount) { + SimpleTest.finish(); + } else { + currentCount++; + window.requestAnimationFrame(rafCallback); + } + } + + window.requestAnimationFrame(rafCallback); + SimpleTest.waitForExplicitFinish(); +</script> diff --git a/dom/animation/test/mochitest.toml b/dom/animation/test/mochitest.toml new file mode 100644 index 0000000000..1859525dfe --- /dev/null +++ b/dom/animation/test/mochitest.toml @@ -0,0 +1,106 @@ +[DEFAULT] +prefs = [ + "dom.animations-api.compositing.enabled=true", + "dom.animations-api.timelines.enabled=true", + "gfx.omta.background-color=true", + "layout.css.individual-transform.enabled=true", + "layout.css.scroll-driven-animations.enabled=true", + "gfx.font_loader.delay=0", +] +# Support files for chrome tests that we want to load over HTTP need +# to go in here, not chrome.ini. +support-files = [ + "chrome/file_animate_xrays.html", + "mozilla/xhr_doc.html", + "mozilla/file_deferred_start.html", + "mozilla/file_disable_animations_api_compositing.html", + "mozilla/file_disable_animations_api_timelines.html", + "mozilla/file_discrete_animations.html", + "mozilla/file_transition_finish_on_compositor.html", + "../../../layout/style/test/property_database.js", + "testcommon.js", + "!/dom/events/test/event_leak_utils.js", +] + +["document-timeline/test_document-timeline.html"] + +["document-timeline/test_request_animation_frame.html"] + +["mozilla/test_cascade.html"] + +["mozilla/test_cubic_bezier_limits.html"] + +["mozilla/test_deferred_start.html"] +skip-if = ["os == 'win' && bits == 64"] # Bug 1363957 + +["mozilla/test_disable_animations_api_compositing.html"] + +["mozilla/test_disable_animations_api_timelines.html"] + +["mozilla/test_disabled_properties.html"] + +["mozilla/test_discrete_animations.html"] + +["mozilla/test_distance_of_basic_shape.html"] + +["mozilla/test_distance_of_filter.html"] + +["mozilla/test_distance_of_path_function.html"] + +["mozilla/test_distance_of_transform.html"] + +["mozilla/test_document_timeline_origin_time_range.html"] + +["mozilla/test_event_listener_leaks.html"] + +["mozilla/test_get_animations_on_scroll_animations.html"] + +["mozilla/test_hide_and_show.html"] + +["mozilla/test_moz_prefixed_properties.html"] + +["mozilla/test_restyles.html"] +support-files = [ + "mozilla/file_restyles.html", + "mozilla/empty.html", +] +skip-if = [ + "os == 'android' && debug", #Bug 1784931 + "os == 'linux' && tsan", #Bug 1784931 + "display == 'wayland' && os_version == '22.04' && debug", # Bug 1856969 + "http3", + "http2", +] + +["mozilla/test_restyling_xhr_doc.html"] + +["mozilla/test_set_easing.html"] + +["mozilla/test_style_after_finished_on_compositor.html"] + +["mozilla/test_transform_limits.html"] + +["mozilla/test_transition_finish_on_compositor.html"] +skip-if = ["os == 'android'"] + +["mozilla/test_underlying_discrete_value.html"] + +["mozilla/test_unstyled.html"] + +["style/test_animation-seeking-with-current-time.html"] + +["style/test_animation-seeking-with-start-time.html"] + +["style/test_animation-setting-effect.html"] + +["style/test_composite.html"] +skip-if = ["xorigin"] + +["style/test_interpolation-from-interpolatematrix-to-none.html"] + +["style/test_missing-keyframe-on-compositor.html"] +skip-if = ["fission && xorigin"] # Bug 1716403 - New fission platform triage + +["style/test_missing-keyframe.html"] + +["style/test_transform-non-normalizable-rotate3d.html"] diff --git a/dom/animation/test/mozilla/empty.html b/dom/animation/test/mozilla/empty.html new file mode 100644 index 0000000000..739422cbfa --- /dev/null +++ b/dom/animation/test/mozilla/empty.html @@ -0,0 +1,2 @@ +<!DOCTYPE html> +<script src="../testcommon.js"></script> diff --git a/dom/animation/test/mozilla/file_deferred_start.html b/dom/animation/test/mozilla/file_deferred_start.html new file mode 100644 index 0000000000..863fc80fec --- /dev/null +++ b/dom/animation/test/mozilla/file_deferred_start.html @@ -0,0 +1,179 @@ +<!doctype html> +<meta charset=utf-8> +<script src="../testcommon.js"></script> +<script src="/tests/SimpleTest/paint_listener.js"></script> +<style> +@keyframes empty { } +.target { + /* Element needs geometry to be eligible for layerization */ + width: 100px; + height: 100px; + background-color: white; +} +</style> +<body> +<script> +'use strict'; + +function waitForDocLoad() { + return new Promise((resolve, reject) => { + if (document.readyState === 'complete') { + resolve(); + } else { + window.addEventListener('load', resolve); + } + }); +} + +function waitForPaints() { + return new Promise((resolve, reject) => { + waitForAllPaintsFlushed(resolve); + }); +} + +promise_test(async t => { + // Test that empty animations actually start. + // + // Normally we tie the start of animations to when their first frame of + // the animation is rendered. However, for animations that don't actually + // trigger a paint (e.g. because they are empty, or are animating something + // that doesn't render or is offscreen) we want to make sure they still + // start. + // + // Before we start, wait for the document to finish loading, then create + // div element, and wait for painting. This is because during loading we will + // have other paint events taking place which might, by luck, happen to + // trigger animations that otherwise would not have been triggered, leading to + // false positives. + // + // As a result, it's better to wait until we have a more stable state before + // continuing. + await waitForDocLoad(); + + const div = addDiv(t); + + await waitForPaints(); + + div.style.animation = 'empty 1000s'; + const animation = div.getAnimations()[0]; + + let promiseCallbackDone = false; + animation.ready.then(() => { + promiseCallbackDone = true; + }).catch(() => { + assert_unreached('ready promise was rejected'); + }); + + // We need to wait for up to three frames. This is because in some + // cases it can take up to two frames for the initial layout + // to take place. Even after that happens we don't actually resolve the + // ready promise until the following tick. + await waitForAnimationFrames(3); + + assert_true(promiseCallbackDone, + 'ready promise for an empty animation was resolved' + + ' within three animation frames'); +}, 'Animation.ready is resolved for an empty animation'); + +// Test that compositor animations with delays get synced correctly +// +// NOTE: It is important that we DON'T use +// SpecialPowers.DOMWindowUtils.advanceTimeAndRefresh here since that takes +// us through a different code path. +promise_test(async t => { + assert_false(SpecialPowers.DOMWindowUtils.isTestControllingRefreshes, + 'Test should run without the refresh driver being under' + + ' test control'); + + // This test only applies to compositor animations + if (!isOMTAEnabled()) { + return; + } + + const div = addDiv(t, { class: 'target' }); + + // As with the above test, any stray paints can cause this test to produce + // a false negative (that is, pass when it should fail). To avoid this we + // wait for paints and only then do we commence the test. + await waitForPaints(); + + const animation = + div.animate({ transform: [ 'translate(0px)', 'translate(100px)' ] }, + { duration: 400 * MS_PER_SEC, + delay: -200 * MS_PER_SEC }); + + await waitForAnimationReadyToRestyle(animation); + + await waitForPaints(); + + const transformStr = + SpecialPowers.DOMWindowUtils.getOMTAStyle(div, 'transform'); + const translateX = getTranslateXFromTransform(transformStr); + + // If the delay has been applied we should be about half-way through + // the animation. However, if we applied it twice we will be at the + // end of the animation already so check that we are roughly half way + // through. + assert_between_inclusive(translateX, 40, 75, + 'Animation is about half-way through on the compositor'); +}, 'Starting an animation with a delay starts from the correct point'); + +// Test that compositor animations with a playback rate start at the +// appropriate point. +// +// NOTE: As with the previous test, it is important that we DON'T use +// SpecialPowers.DOMWindowUtils.advanceTimeAndRefresh here since that takes +// us through a different code path. +promise_test(async t => { + assert_false(SpecialPowers.DOMWindowUtils.isTestControllingRefreshes, + 'Test should run without the refresh driver being under' + + ' test control'); + + // This test only applies to compositor animations + if (!isOMTAEnabled()) { + return; + } + + const div = addDiv(t, { class: 'target' }); + + // Wait for the document to load and painting (see notes in previous test). + await waitForPaints(); + + const animation = + div.animate({ transform: [ 'translate(0px)', 'translate(100px)' ] }, + 200 * MS_PER_SEC); + animation.currentTime = 100 * MS_PER_SEC; + animation.playbackRate = 0.1; + + await waitForPaints(); + + const transformStr = + SpecialPowers.DOMWindowUtils.getOMTAStyle(div, 'transform'); + const translateX = getTranslateXFromTransform(transformStr); + + // We pass the playback rate to the compositor independently and we have + // tests to ensure that it is correctly applied there. However, if, when + // we resolve the start time of the pending animation, we fail to + // incorporate the playback rate, we will end up starting from the wrong + // point and the current time calculated on the compositor will be wrong. + assert_between_inclusive(translateX, 25, 75, + 'Animation is about half-way through on the compositor'); +}, 'Starting an animation with a playbackRate starts from the correct point'); + +function getTranslateXFromTransform(transformStr) { + const matrixComponents = + transformStr.startsWith('matrix(') + ? transformStr.substring('matrix('.length, transformStr.length-1) + .split(',') + .map(component => Number(component)) + : []; + assert_equals(matrixComponents.length, 6, + 'Got a valid transform matrix on the compositor' + + ' (got: "' + transformStr + '")'); + + return matrixComponents[4]; +} + +done(); +</script> +</body> diff --git a/dom/animation/test/mozilla/file_disable_animations_api_compositing.html b/dom/animation/test/mozilla/file_disable_animations_api_compositing.html new file mode 100644 index 0000000000..6d9ba35dc0 --- /dev/null +++ b/dom/animation/test/mozilla/file_disable_animations_api_compositing.html @@ -0,0 +1,137 @@ +<!doctype html> +<meta charset=utf-8> +<script src="../testcommon.js"></script> +<body> +<script> +'use strict'; + +test(t => { + const anim = addDiv(t).animate( + { marginLeft: ['0px', '10px'] }, + { + duration: 100 * MS_PER_SEC, + iterations: 10, + iterationComposite: 'accumulate', + composite: 'add', + } + ); + assert_false( + 'iterationComposite' in anim.effect, + 'The KeyframeEffect.iterationComposite member is not present' + ); + assert_false( + 'composite' in anim.effect, + 'The KeyframeEffect.composite member is not present' + ); +}, 'The iterationComposite and composite members are not present on Animation' + + ' when the compositing pref is disabled'); + +test(t => { + const div = addDiv(t); + const anim = div.animate( + { marginLeft: ['0px', '10px'] }, + { + duration: 100 * MS_PER_SEC, + iterations: 10, + iterationComposite: 'accumulate', + } + ); + anim.pause(); + anim.currentTime = 200 * MS_PER_SEC; + + assert_equals( + getComputedStyle(div).marginLeft, + '0px', + 'Animated style should NOT accumulate' + ); +}, 'KeyframeEffectOptions.iterationComposite should be ignored if the' + + ' compositing pref is disabled'); + +test(t => { + const div = addDiv(t); + const anim1 = div.animate( + { marginLeft: ['0px', '100px'] }, + { duration: 100 * MS_PER_SEC } + ); + anim1.pause(); + anim1.currentTime = 50 * MS_PER_SEC; + + const anim2 = div.animate( + { marginLeft: ['0px', '100px'] }, + { duration: 100 * MS_PER_SEC, composite: 'add' } + ); + anim2.pause(); + anim2.currentTime = 50 * MS_PER_SEC; + + assert_equals( + getComputedStyle(div).marginLeft, + '50px', + 'Animations should NOT add together' + ); +}, 'KeyframeEffectOptions.composite should be ignored if the' + + ' compositing pref is disabled'); + +test(t => { + const div = addDiv(t); + const anim1 = div.animate({ marginLeft: ['0px', '100px'] }, 100 * MS_PER_SEC); + anim1.pause(); + anim1.currentTime = 50 * MS_PER_SEC; + + const anim2 = div.animate( + [ + { marginLeft: '0px', composite: 'add' }, + { marginLeft: '100px', composite: 'add' }, + ], + 100 * MS_PER_SEC + ); + anim2.pause(); + anim2.currentTime = 50 * MS_PER_SEC; + + assert_equals( + getComputedStyle(div).marginLeft, + '50px', + 'Animations should NOT add together' + ); +}, 'composite member is ignored on keyframes when using array notation'); + +test(t => { + const div = addDiv(t); + const anim1 = div.animate( + { marginLeft: ['0px', '100px'] }, + 100 * MS_PER_SEC + ); + anim1.pause(); + anim1.currentTime = 50 * MS_PER_SEC; + + const anim2 = div.animate( + { marginLeft: ['0px', '100px'], composite: ['add', 'add'] }, + 100 * MS_PER_SEC + ); + anim2.pause(); + anim2.currentTime = 50 * MS_PER_SEC; + + assert_equals( + getComputedStyle(div).marginLeft, + '50px', + 'Animations should NOT add together' + ); +}, 'composite member is ignored on keyframes when using object notation'); + +test(t => { + const anim = addDiv(t).animate( + { marginLeft: ['0px', '10px'] }, + 100 * MS_PER_SEC + ); + + for (let frame of anim.effect.getKeyframes()) { + assert_false( + 'composite' in frame, + 'The BaseComputedKeyframe.composite member is not present' + ); + } +}, 'composite member is hidden from the result of ' + + 'KeyframeEffect::getKeyframes()'); + +done(); +</script> +</body> diff --git a/dom/animation/test/mozilla/file_disable_animations_api_timelines.html b/dom/animation/test/mozilla/file_disable_animations_api_timelines.html new file mode 100644 index 0000000000..39fedb299a --- /dev/null +++ b/dom/animation/test/mozilla/file_disable_animations_api_timelines.html @@ -0,0 +1,30 @@ +<!doctype html> +<meta charset=utf-8> +<script src="../testcommon.js"></script> +<body> +<script> +'use strict'; + +test(t => { + assert_false( + window.hasOwnProperty('DocumentTimeline'), + 'DocumentTimeline should not be exposed on the global' + ); + assert_false( + window.hasOwnProperty('AnimationTimeline'), + 'AnimationTimeline should not be exposed on the global' + ); + assert_false( + 'timeline' in document, + 'document should not have a timeline property' + ); + + const anim = addDiv(t).animate(null); + assert_false( + 'timeline' in anim, + 'Animation should not have a timeline property' + ); +}, 'Timeline-related interfaces and members are disabled'); + +done(); +</script> diff --git a/dom/animation/test/mozilla/file_discrete_animations.html b/dom/animation/test/mozilla/file_discrete_animations.html new file mode 100644 index 0000000000..e0de609bc5 --- /dev/null +++ b/dom/animation/test/mozilla/file_discrete_animations.html @@ -0,0 +1,122 @@ +<!doctype html> +<head> +<meta charset=utf-8> +<title>Test Mozilla-specific discrete animatable properties</title> +<script type="application/javascript" src="../testcommon.js"></script> +</head> +<body> +<script> +"use strict"; + +const gMozillaSpecificProperties = { + "-moz-box-align": { + // https://developer.mozilla.org/en/docs/Web/CSS/box-align + from: "center", + to: "stretch" + }, + "-moz-box-direction": { + // https://developer.mozilla.org/en/docs/Web/CSS/box-direction + from: "reverse", + to: "normal" + }, + "-moz-box-ordinal-group": { + // https://developer.mozilla.org/en/docs/Web/CSS/box-ordinal-group + from: "1", + to: "5" + }, + "-moz-box-orient": { + // https://www.w3.org/TR/css-flexbox-1/ + from: "horizontal", + to: "vertical" + }, + "-moz-box-pack": { + // https://www.w3.org/TR/2009/WD-css3-flexbox-20090723/#propdef-box-pack + from: "center", + to: "end" + }, + "-moz-float-edge": { + // https://developer.mozilla.org/en/docs/Web/CSS/-moz-float-edge + from: "margin-box", + to: "content-box" + }, + "-moz-force-broken-image-icon": { + // https://developer.mozilla.org/en/docs/Web/CSS/-moz-force-broken-image-icon + from: "1", + to: "0" + }, + "-moz-text-size-adjust": { + // https://drafts.csswg.org/css-size-adjust/#propdef-text-size-adjust + from: "none", + to: "auto" + }, + "-webkit-text-stroke-width": { + // https://compat.spec.whatwg.org/#propdef--webkit-text-stroke-width + from: "10px", + to: "50px" + } +} + +for (let property in gMozillaSpecificProperties) { + const testData = gMozillaSpecificProperties[property]; + const from = testData.from; + const to = testData.to; + const idlName = propertyToIDL(property); + const keyframes = {}; + keyframes[idlName] = [from, to]; + + test(t => { + const div = addDiv(t); + const animation = div.animate(keyframes, + { duration: 1000, fill: "both" }); + testAnimationSamples(animation, idlName, + [{ time: 0, expected: from.toLowerCase() }, + { time: 499, expected: from.toLowerCase() }, + { time: 500, expected: to.toLowerCase() }, + { time: 1000, expected: to.toLowerCase() }]); + }, property + " should animate between '" + + from + "' and '" + to + "' with linear easing"); + + test(function(t) { + // Easing: http://cubic-bezier.com/#.68,0,1,.01 + // With this curve, we don't reach the 50% point until about 95% of + // the time has expired. + const div = addDiv(t); + const animation = div.animate(keyframes, + { duration: 1000, fill: "both", + easing: "cubic-bezier(0.68,0,1,0.01)" }); + testAnimationSamples(animation, idlName, + [{ time: 0, expected: from.toLowerCase() }, + { time: 940, expected: from.toLowerCase() }, + { time: 960, expected: to.toLowerCase() }]); + }, property + " should animate between '" + + from + "' and '" + to + "' with effect easing"); + + test(function(t) { + // Easing: http://cubic-bezier.com/#.68,0,1,.01 + // With this curve, we don't reach the 50% point until about 95% of + // the time has expired. + keyframes.easing = "cubic-bezier(0.68,0,1,0.01)"; + const div = addDiv(t); + const animation = div.animate(keyframes, + { duration: 1000, fill: "both" }); + testAnimationSamples(animation, idlName, + [{ time: 0, expected: from.toLowerCase() }, + { time: 940, expected: from.toLowerCase() }, + { time: 960, expected: to.toLowerCase() }]); + }, property + " should animate between '" + + from + "' and '" + to + "' with keyframe easing"); +} + +function testAnimationSamples(animation, idlName, testSamples) { + const target = animation.effect.target; + testSamples.forEach(testSample => { + animation.currentTime = testSample.time; + assert_equals(getComputedStyle(target)[idlName], testSample.expected, + "The value should be " + testSample.expected + + " at " + testSample.time + "ms"); + }); +} + +done(); +</script> +</body> diff --git a/dom/animation/test/mozilla/file_restyles.html b/dom/animation/test/mozilla/file_restyles.html new file mode 100644 index 0000000000..0aba35cd0e --- /dev/null +++ b/dom/animation/test/mozilla/file_restyles.html @@ -0,0 +1,2304 @@ +<!doctype html> +<head> +<meta name="viewport" content="width=device-width,minimum-scale=1,initial-scale=1"> +<meta charset=utf-8> +<title>Tests restyles caused by animations</title> +<script> +const ok = opener.ok.bind(opener); +const is = opener.is.bind(opener); +const todo = opener.todo.bind(opener); +const todo_is = opener.todo_is.bind(opener); +const info = opener.info.bind(opener); +const original_finish = opener.SimpleTest.finish; +const SimpleTest = opener.SimpleTest; +const add_task = opener.add_task; +SimpleTest.finish = function finish() { + self.close(); + original_finish(); +} +</script> +<script src="/tests/SimpleTest/EventUtils.js"></script> +<script src="/tests/SimpleTest/paint_listener.js"></script> +<script src="../testcommon.js"></script> +<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"> +<style> +@keyframes background-position { + 0% { + background-position: -25px center; + } + + 40%, + 100% { + background-position: 36px center; + } +} +@keyframes opacity { + from { opacity: 1; } + to { opacity: 0; } +} +@keyframes opacity-from-zero { + from { opacity: 0; } + to { opacity: 1; } +} +@keyframes opacity-without-end-value { + from { opacity: 0; } +} +@keyframes on-main-thread { + from { z-index: 0; } + to { z-index: 999; } +} +@keyframes rotate { + from { transform: rotate(0deg); } + to { transform: rotate(360deg); } +} +@keyframes move-in { + from { transform: translate(120%, 120%); } + to { transform: translate(0%, 0%); } +} +@keyframes background-color { + from { background-color: rgb(255, 0, 0,); } + to { background-color: rgb(0, 255, 0,); } +} +div { + /* Element needs geometry to be eligible for layerization */ + width: 100px; + height: 100px; + background-color: white; +} +progress:not(.stop)::-moz-progress-bar { + animation: on-main-thread 100s; +} +body { + /* + * set overflow:hidden to avoid accidentally unthrottling animations to update + * the overflow region. + */ + overflow: hidden; +} +</style> +</head> +<body> +<script> +'use strict'; + +// Returns observed animation restyle markers when |funcToMakeRestyleHappen| +// is called. +// NOTE: This function is synchronous version of the above observeStyling(). +// Unlike the above observeStyling, this function takes a callback function, +// |funcToMakeRestyleHappen|, which may be expected to trigger a synchronous +// restyles, and returns any restyle markers produced by calling that function. +function observeAnimSyncStyling(funcToMakeRestyleHappen) { + + let priorAnimationTriggeredRestyles = SpecialPowers.DOMWindowUtils.animationTriggeredRestyles; + + funcToMakeRestyleHappen(); + + const restyleCount = SpecialPowers.DOMWindowUtils.animationTriggeredRestyles - priorAnimationTriggeredRestyles; + + return restyleCount; +} + +function ensureElementRemoval(aElement) { + return new Promise(resolve => { + aElement.remove(); + waitForAllPaintsFlushed(resolve); + }); +} + +function waitForWheelEvent(aTarget) { + return new Promise(resolve => { + // Get the scrollable target element position in this window coordinate + // system to send a wheel event to the element. + const targetRect = aTarget.getBoundingClientRect(); + const centerX = targetRect.left + targetRect.width / 2; + const centerY = targetRect.top + targetRect.height / 2; + + sendWheelAndPaintNoFlush(aTarget, centerX, centerY, + { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaY: targetRect.height }, + resolve); + }); +} + +const omtaEnabled = isOMTAEnabled(); + +function add_task_if_omta_enabled(test) { + if (!omtaEnabled) { + info(test.name + " is skipped because OMTA is disabled"); + return; + } + add_task(test); +} + +// We need to wait for all paints before running tests to avoid contaminations +// from styling of this document itself. +waitForAllPaints(() => { + add_task(async function () { + // Start vsync rate measurement in after a RAF callback. + await waitForNextFrame(); + + const timeAtStart = document.timeline.currentTime; + await waitForAnimationFrames(5); + const vsyncRate = (document.timeline.currentTime - timeAtStart) / 5; + + // In this test we basically observe restyling counts in 5 frames, if it + // takes over 200ms during the 5 frames, this test will fail. So + // "200ms / 5 = 40ms" is a threshold whether the test works as expected or + // not. We'd take 5ms additional tolerance here. + // Note that the 200ms is a period we unthrottle throttled animations that + // at least one of the animating styles produces change hints causing + // overflow, the value is defined in + // KeyframeEffect::OverflowRegionRefreshInterval. + if (vsyncRate > 40 - 5) { + ok(true, `the vsync rate ${vsyncRate} on this machine is too slow to run this test`); + SimpleTest.finish(); + } + }); + + add_task(async function restyling_for_main_thread_animations() { + const div = addDiv(null, { style: 'animation: on-main-thread 100s' }); + const animation = div.getAnimations()[0]; + + await waitForAnimationReadyToRestyle(animation); + + ok(!SpecialPowers.wrap(animation).isRunningOnCompositor); + + const restyleCount = await observeStyling(5); + is(restyleCount, 5, + 'CSS animations running on the main-thread should update style ' + + 'on the main thread'); + await ensureElementRemoval(div); + }); + + add_task(async function restyling_for_main_thread_animations_progress_bar_pseudo() { + const progress = document.createElement("progress"); + document.body.appendChild(progress); + + await waitForNextFrame(); + await waitForNextFrame(); + + let restyleCount; + restyleCount = await observeStyling(5); + // TODO(bug 1784931): Figure out why we only see four markers sometimes. + // That's not the point of this test tho. + let maybe_todo_is = restyleCount == 4 ? todo_is : is; + maybe_todo_is(restyleCount, 5, + 'CSS animations running on the main-thread should update style ' + + 'on the main thread on ::-moz-progress-bar'); + progress.classList.add("stop"); + await waitForNextFrame(); + await waitForNextFrame(); + + restyleCount = await observeStyling(5); + is(restyleCount, 0, 'Animation is correctly removed'); + await ensureElementRemoval(progress); + }); + + add_task_if_omta_enabled(async function no_restyling_for_compositor_animations() { + const div = addDiv(null, { style: 'animation: opacity 100s' }); + const animation = div.getAnimations()[0]; + + await waitForAnimationReadyToRestyle(animation); + ok(SpecialPowers.wrap(animation).isRunningOnCompositor); + + const restyleCount = await observeStyling(5); + is(restyleCount, 0, + 'CSS animations running on the compositor should not update style ' + + 'on the main thread'); + await ensureElementRemoval(div); + }); + + add_task_if_omta_enabled(async function no_restyling_for_compositor_transitions() { + const div = addDiv(null, { style: 'transition: opacity 100s; opacity: 0' }); + getComputedStyle(div).opacity; + div.style.opacity = 1; + + const animation = div.getAnimations()[0]; + + await waitForAnimationReadyToRestyle(animation); + ok(SpecialPowers.wrap(animation).isRunningOnCompositor); + + const restyleCount = await observeStyling(5); + is(restyleCount, 0, + 'CSS transitions running on the compositor should not update style ' + + 'on the main thread'); + await ensureElementRemoval(div); + }); + + add_task_if_omta_enabled(async function no_restyling_when_animation_duration_is_changed() { + const div = addDiv(null, { style: 'animation: opacity 100s' }); + const animation = div.getAnimations()[0]; + + await waitForAnimationReadyToRestyle(animation); + ok(SpecialPowers.wrap(animation).isRunningOnCompositor); + + div.animationDuration = '200s'; + + const restyleCount = await observeStyling(5); + is(restyleCount, 0, + 'Animations running on the compositor should not update style ' + + 'on the main thread'); + await ensureElementRemoval(div); + }); + + add_task_if_omta_enabled(async function only_one_restyling_after_finish_is_called() { + const div = addDiv(null, { style: 'animation: opacity 100s' }); + const animation = div.getAnimations()[0]; + + await waitForAnimationReadyToRestyle(animation); + ok(SpecialPowers.wrap(animation).isRunningOnCompositor); + + animation.finish(); + + let restyleCount; + restyleCount = await observeStyling(1); + is(restyleCount, 1, + 'Animations running on the compositor should only update style once ' + + 'after finish() is called'); + + restyleCount = await observeStyling(1); + todo_is(restyleCount, 0, + 'Bug 1415457: Animations running on the compositor should only ' + + 'update style once after finish() is called'); + + restyleCount = await observeStyling(5); + is(restyleCount, 0, + 'Finished animations should never update style after one ' + + 'restyle happened for finish()'); + + await ensureElementRemoval(div); + }); + + add_task(async function no_restyling_mouse_movement_on_finished_transition() { + const div = addDiv(null, { style: 'transition: opacity 1ms; opacity: 0' }); + getComputedStyle(div).opacity; + div.style.opacity = 1; + + const animation = div.getAnimations()[0]; + const initialRect = div.getBoundingClientRect(); + + await animation.finished; + let restyleCount; + restyleCount = await observeStyling(1); + is(restyleCount, 1, + 'Finished transitions should restyle once after Animation.finished ' + + 'was fulfilled'); + + let mouseX = initialRect.left + initialRect.width / 2; + let mouseY = initialRect.top + initialRect.height / 2; + restyleCount = await observeStyling(5, () => { + // We can't use synthesizeMouse here since synthesizeMouse causes + // layout flush. + synthesizeMouseAtPoint(mouseX++, mouseY++, + { type: 'mousemove' }, window); + }); + + is(restyleCount, 0, + 'Finished transitions should never cause restyles when mouse is moved ' + + 'on the transitions'); + await ensureElementRemoval(div); + }); + + add_task(async function no_restyling_mouse_movement_on_finished_animation() { + const div = addDiv(null, { style: 'animation: opacity 1ms' }); + const animation = div.getAnimations()[0]; + + const initialRect = div.getBoundingClientRect(); + + await animation.finished; + let restyleCount; + restyleCount = await observeStyling(1); + is(restyleCount, 1, + 'Finished animations should restyle once after Animation.finished ' + + 'was fulfilled'); + + let mouseX = initialRect.left + initialRect.width / 2; + let mouseY = initialRect.top + initialRect.height / 2; + restyleCount = await observeStyling(5, () => { + // We can't use synthesizeMouse here since synthesizeMouse causes + // layout flush. + synthesizeMouseAtPoint(mouseX++, mouseY++, + { type: 'mousemove' }, window); + }); + + is(restyleCount, 0, + 'Finished animations should never cause restyles when mouse is moved ' + + 'on the animations'); + await ensureElementRemoval(div); + }); + + add_task_if_omta_enabled(async function no_restyling_compositor_animations_out_of_view_element() { + const div = addDiv(null, + { style: 'animation: opacity 100s; transform: translateY(-400px);' }); + const animation = div.getAnimations()[0]; + + await waitForAnimationReadyToRestyle(animation); + ok(!SpecialPowers.wrap(animation).isRunningOnCompositor); + + const restyleCount = await observeStyling(5); + + is(restyleCount, 0, + 'Animations running on the compositor in an out-of-view element ' + + 'should never cause restyles'); + await ensureElementRemoval(div); + }); + + add_task(async function no_restyling_main_thread_animations_out_of_view_element() { + const div = addDiv(null, + { style: 'animation: on-main-thread 100s; transform: translateY(-400px);' }); + const animation = div.getAnimations()[0]; + + await waitForAnimationReadyToRestyle(animation); + const restyleCount = await observeStyling(5); + + is(restyleCount, 0, + 'Animations running on the main-thread in an out-of-view element ' + + 'should never cause restyles'); + await ensureElementRemoval(div); + }); + + add_task_if_omta_enabled(async function no_restyling_compositor_animations_in_scrolled_out_element() { + const parentElement = addDiv(null, + { style: 'overflow-y: scroll; height: 20px;' }); + const div = addDiv(null, + { style: 'animation: opacity 100s; position: relative; top: 100px;' }); + parentElement.appendChild(div); + const animation = div.getAnimations()[0]; + + await waitForAnimationReadyToRestyle(animation); + + const restyleCount = await observeStyling(5); + + is(restyleCount, 0, + 'Animations running on the compositor for elements ' + + 'which are scrolled out should never cause restyles'); + + await ensureElementRemoval(parentElement); + }); + + add_task( + async function no_restyling_missing_keyframe_opacity_animations_on_scrolled_out_element() { + const parentElement = addDiv(null, + { style: 'overflow-y: scroll; height: 20px;' }); + const div = addDiv(null, + { style: 'animation: opacity-without-end-value 100s; ' + + 'position: relative; top: 100px;' }); + parentElement.appendChild(div); + const animation = div.getAnimations()[0]; + await waitForAnimationReadyToRestyle(animation); + + const restyleCount = await observeStyling(5); + + is(restyleCount, 0, + 'Opacity animations on scrolled out elements should never cause ' + + 'restyles even if the animation has missing keyframes'); + + await ensureElementRemoval(parentElement); + } + ); + + add_task( + async function restyling_transform_animations_in_scrolled_out_element() { + // Make sure we start from the state right after requestAnimationFrame. + await waitForFrame(); + + const parentElement = addDiv(null, + { style: 'overflow-y: scroll; height: 20px;' }); + const div = addDiv(null, + { style: 'animation: rotate 100s infinite; position: relative; top: 100px;' }); + parentElement.appendChild(div); + const animation = div.getAnimations()[0]; + let timeAtStart = document.timeline.currentTime; + + ok(!animation.isRunningOnCompositor, + 'The transform animation is not running on the compositor'); + + let restyleCount + let now; + let elapsed; + while (true) { + now = document.timeline.currentTime; + elapsed = (now - timeAtStart); + restyleCount = await observeStyling(1); + if (restyleCount) { + break; + } + } + // If the current time has elapsed over 200ms since the animation was + // created, it means that the animation should have already + // unthrottled in this tick, let's see what we observe in this tick's + // restyling process. + // We use toPrecision here and below so 199.99999999999977 will turn into 200. + ok(elapsed.toPrecision(10) >= 200, + 'Transform animation running on the element which is scrolled out ' + + 'should be throttled until 200ms is elapsed. now: ' + + now + ' start time: ' + timeAtStart + ' elapsed:' + elapsed); + + timeAtStart = document.timeline.currentTime; + restyleCount = await observeStyling(1); + now = document.timeline.currentTime; + elapsed = (now - timeAtStart); + + let expectedMarkersLengthValid; + // On the fence of 200 ms, we probably have 1 marker; but if we hit a bad rounding + // we might still have 0. But if it's > 200, we should have 1; and less we should have 0. + if (elapsed.toPrecision(10) == 200) + expectedMarkersLengthValid = restyleCount < 2; + else if (elapsed.toPrecision(10) > 200) + expectedMarkersLengthValid = restyleCount == 1; + else + expectedMarkersLengthValid = !restyleCount; + ok(expectedMarkersLengthValid, + 'Transform animation running on the element which is scrolled out ' + + 'should be unthrottled after around 200ms have elapsed. now: ' + + now + ' start time: ' + timeAtStart + ' elapsed: ' + elapsed); + + await ensureElementRemoval(parentElement); + } + ); + + add_task( + async function restyling_out_of_view_transform_animations_in_another_element() { + // Make sure we start from the state right after requestAnimationFrame. + await waitForFrame(); + + const parentElement = addDiv(null, + { style: 'overflow: hidden;' }); + const div = addDiv(null, + { style: 'animation: move-in 100s infinite;' }); + parentElement.appendChild(div); + const animation = div.getAnimations()[0]; + let timeAtStart = document.timeline.currentTime; + + ok(!animation.isRunningOnCompositor, + 'The transform animation on out of view element ' + + 'is not running on the compositor'); + + // Structure copied from restyling_transform_animations_in_scrolled_out_element + let restyleCount + let now; + let elapsed; + while (true) { + now = document.timeline.currentTime; + elapsed = (now - timeAtStart); + restyleCount = await observeStyling(1); + if (restyleCount) { + break; + } + } + + ok(elapsed.toPrecision(10) >= 200, + 'Transform animation running on out of view element ' + + 'should be throttled until 200ms is elapsed. now: ' + + now + ' start time: ' + timeAtStart + ' elapsed:' + elapsed); + + timeAtStart = document.timeline.currentTime; + restyleCount = await observeStyling(1); + now = document.timeline.currentTime; + elapsed = (now - timeAtStart); + + let expectedMarkersLengthValid; + // On the fence of 200 ms, we probably have 1 marker; but if we hit a bad rounding + // we might still have 0. But if it's > 200, we should have 1; and less we should have 0. + if (elapsed.toPrecision(10) == 200) + expectedMarkersLengthValid = restyleCount < 2; + else if (elapsed.toPrecision(10) > 200) + expectedMarkersLengthValid = restyleCount == 1; + else + expectedMarkersLengthValid = !restyleCount; + ok(expectedMarkersLengthValid, + 'Transform animation running on out of view element ' + + 'should be unthrottled after around 200ms have elapsed. now: ' + + now + ' start time: ' + timeAtStart + ' elapsed: ' + elapsed); + + await ensureElementRemoval(parentElement); + } + ); + + add_task(async function finite_transform_animations_in_out_of_view_element() { + const parentElement = addDiv(null, { style: 'overflow: hidden;' }); + const div = addDiv(null); + const animation = + div.animate({ transform: [ 'translateX(120%)', 'translateX(100%)' ] }, + // This animation will move a bit but + // will remain out-of-view. + 100 * MS_PER_SEC); + parentElement.appendChild(div); + + await waitForAnimationReadyToRestyle(animation); + ok(!SpecialPowers.wrap(animation).isRunningOnCompositor, "Should not be running in compositor"); + + const restyleCount = await observeStyling(20); + is(restyleCount, 20, + 'Finite transform animation in out-of-view element should never be ' + + 'throttled'); + + await ensureElementRemoval(parentElement); + }); + + add_task(async function restyling_main_thread_animations_in_scrolled_out_element() { + const parentElement = addDiv(null, + { style: 'overflow-y: scroll; height: 20px;' }); + const div = addDiv(null, + { style: 'animation: on-main-thread 100s; position: relative; top: 20px;' }); + parentElement.appendChild(div); + const animation = div.getAnimations()[0]; + + await waitForAnimationReadyToRestyle(animation); + let restyleCount; + restyleCount = await observeStyling(5); + + is(restyleCount, 0, + 'Animations running on the main-thread for elements ' + + 'which are scrolled out should never cause restyles'); + + await waitForWheelEvent(parentElement); + + // Make sure we are ready to restyle before counting restyles. + await waitForFrame(); + + restyleCount = await observeStyling(5); + is(restyleCount, 5, + 'Animations running on the main-thread which were in scrolled out ' + + 'elements should update restyling soon after the element moved in ' + + 'view by scrolling'); + + await ensureElementRemoval(parentElement); + }); + + add_task(async function restyling_main_thread_animations_in_nested_scrolled_out_element() { + const grandParent = addDiv(null, + { style: 'overflow-y: scroll; height: 20px;' }); + const parentElement = addDiv(null, + { style: 'overflow-y: scroll; height: 100px;' }); + const div = addDiv(null, + { style: 'animation: on-main-thread 100s; ' + + 'position: relative; ' + + 'top: 20px;' }); // This element is in-view in the parent, but + // out of view in the grandparent. + grandParent.appendChild(parentElement); + parentElement.appendChild(div); + const animation = div.getAnimations()[0]; + + await waitForAnimationReadyToRestyle(animation); + let restyleCount; + restyleCount = await observeStyling(5); + + is(restyleCount, 0, + 'Animations running on the main-thread which are in nested elements ' + + 'which are scrolled out should never cause restyles'); + + await waitForWheelEvent(grandParent); + + await waitForFrame(); + + restyleCount = await observeStyling(5); + is(restyleCount, 5, + 'Animations running on the main-thread which were in nested scrolled ' + + 'out elements should update restyle soon after the element moved ' + + 'in view by scrolling'); + + await ensureElementRemoval(grandParent); + }); + + add_task_if_omta_enabled(async function no_restyling_compositor_animations_in_visibility_hidden_element() { + const div = addDiv(null, + { style: 'animation: opacity 100s; visibility: hidden' }); + const animation = div.getAnimations()[0]; + + await waitForAnimationReadyToRestyle(animation); + ok(!SpecialPowers.wrap(animation).isRunningOnCompositor); + + const restyleCount = await observeStyling(5); + + is(restyleCount, 0, + 'Animations running on the compositor in visibility hidden element ' + + 'should never cause restyles'); + await ensureElementRemoval(div); + }); + + add_task(async function restyling_main_thread_animations_move_out_of_view_by_scrolling() { + const parentElement = addDiv(null, + { style: 'overflow-y: scroll; height: 200px;' }); + const div = addDiv(null, + { style: 'animation: on-main-thread 100s;' }); + const pad = addDiv(null, + { style: 'height: 400px;' }); + parentElement.appendChild(div); + parentElement.appendChild(pad); + const animation = div.getAnimations()[0]; + + await waitForAnimationReadyToRestyle(animation); + + await waitForWheelEvent(parentElement); + + await waitForFrame(); + + const restyleCount = await observeStyling(5); + + // FIXME: We should reduce a redundant restyle here. + ok(restyleCount >= 0, + 'Animations running on the main-thread which are in scrolled out ' + + 'elements should throttle restyling'); + + await ensureElementRemoval(parentElement); + }); + + add_task(async function restyling_main_thread_animations_moved_in_view_by_resizing() { + const parentElement = addDiv(null, + { style: 'overflow-y: scroll; height: 20px;' }); + const div = addDiv(null, + { style: 'animation: on-main-thread 100s; position: relative; top: 100px;' }); + parentElement.appendChild(div); + const animation = div.getAnimations()[0]; + + await waitForAnimationReadyToRestyle(animation); + + let restyleCount; + restyleCount = await observeStyling(5); + is(restyleCount, 0, + 'Animations running on the main-thread which is in scrolled out ' + + 'elements should not update restyling'); + + parentElement.style.height = '100px'; + restyleCount = await observeStyling(1); + + is(restyleCount, 1, + 'Animations running on the main-thread which was in scrolled out ' + + 'elements should update restyling soon after the element moved in ' + + 'view by resizing'); + + await ensureElementRemoval(parentElement); + }); + + add_task( + async function restyling_animations_on_visibility_changed_element_having_child() { + const div = addDiv(null, + { style: 'animation: on-main-thread 100s;' }); + const childElement = addDiv(null); + div.appendChild(childElement); + + const animation = div.getAnimations()[0]; + + await waitForAnimationReadyToRestyle(animation); + + // We don't check the animation causes restyles here since we already + // check it in the first test case. + + div.style.visibility = 'hidden'; + await waitForNextFrame(); + + const restyleCount = await observeStyling(5); + todo_is(restyleCount, 0, + 'Animations running on visibility hidden element which ' + + 'has a child whose visiblity is inherited from the element and ' + + 'the element was initially visible'); + + await ensureElementRemoval(div); + } + ); + + add_task( + async function restyling_animations_on_visibility_hidden_element_which_gets_visible() { + const div = addDiv(null, + { style: 'animation: on-main-thread 100s; visibility: hidden' }); + const animation = div.getAnimations()[0]; + + + await waitForAnimationReadyToRestyle(animation); + let restyleCount; + restyleCount = await observeStyling(5); + + is(restyleCount, 0, + 'Animations running on visibility hidden element should never ' + + 'cause restyles'); + + div.style.visibility = 'visible'; + await waitForNextFrame(); + + restyleCount = await observeStyling(5); + is(restyleCount, 5, + 'Animations running that was on visibility hidden element which ' + + 'gets visible should not throttle restyling any more'); + + await ensureElementRemoval(div); + } + ); + + add_task(async function restyling_animations_in_visibility_changed_parent() { + const parentDiv = addDiv(null, { style: 'visibility: hidden' }); + const div = addDiv(null, { style: 'animation: on-main-thread 100s;' }); + parentDiv.appendChild(div); + + const animation = div.getAnimations()[0]; + + await waitForAnimationReadyToRestyle(animation); + let restyleCount; + restyleCount = await observeStyling(5); + + is(restyleCount, 0, + 'Animations running in visibility hidden parent should never cause ' + + 'restyles'); + + parentDiv.style.visibility = 'visible'; + await waitForNextFrame(); + + restyleCount = await observeStyling(5); + is(restyleCount, 5, + 'Animations that was in visibility hidden parent should not ' + + 'throttle restyling any more'); + + parentDiv.style.visibility = 'hidden'; + await waitForNextFrame(); + + restyleCount = await observeStyling(5); + is(restyleCount, 0, + 'Animations that the parent element became visible should throttle ' + + 'restyling again'); + + await ensureElementRemoval(parentDiv); + }); + + add_task( + async function restyling_animations_on_visibility_hidden_element_with_visibility_changed_children() { + const div = addDiv(null, + { style: 'animation: on-main-thread 100s; visibility: hidden' }); + const animation = div.getAnimations()[0]; + + await waitForAnimationReadyToRestyle(animation); + let restyleCount; + restyleCount = await observeStyling(5); + + is(restyleCount, 0, + 'Animations on visibility hidden element having no visible children ' + + 'should never cause restyles'); + + const childElement = addDiv(null, { style: 'visibility: visible' }); + div.appendChild(childElement); + await waitForNextFrame(); + + restyleCount = await observeStyling(5); + is(restyleCount, 5, + 'Animations running on visibility hidden element but the element has ' + + 'a visible child should not throttle restyling'); + + childElement.style.visibility = 'hidden'; + await waitForNextFrame(); + + restyleCount = await observeStyling(5); + todo_is(restyleCount, 0, + 'Animations running on visibility hidden element that a child ' + + 'has become invisible should throttle restyling'); + + childElement.style.visibility = 'visible'; + await waitForNextFrame(); + + restyleCount = await observeStyling(5); + is(restyleCount, 5, + 'Animations running on visibility hidden element should not throttle ' + + 'restyling after the invisible element changed to visible'); + + childElement.remove(); + await waitForNextFrame(); + + restyleCount = await observeStyling(5); + todo_is(restyleCount, 0, + 'Animations running on visibility hidden element should throttle ' + + 'restyling again after all visible descendants were removed'); + + await ensureElementRemoval(div); + } + ); + + add_task( + async function restyling_animations_on_visiblity_hidden_element_having_oof_child() { + const div = addDiv(null, + { style: 'animation: on-main-thread 100s; position: absolute' }); + const childElement = addDiv(null, + { style: 'float: left; visibility: hidden' }); + div.appendChild(childElement); + + const animation = div.getAnimations()[0]; + + await waitForAnimationReadyToRestyle(animation); + + // We don't check the animation causes restyles here since we already + // check it in the first test case. + + div.style.visibility = 'hidden'; + await waitForNextFrame(); + + const restyleCount = await observeStyling(5); + is(restyleCount, 0, + 'Animations running on visibility hidden element which has an ' + + 'out-of-flow child should throttle restyling'); + + await ensureElementRemoval(div); + } + ); + + add_task( + async function restyling_animations_on_visibility_hidden_element_having_grandchild() { + // element tree: + // + // root(visibility:hidden) + // / \ + // childA childB + // / \ / \ + // AA AB BA BB + + const div = addDiv(null, + { style: 'animation: on-main-thread 100s; visibility: hidden' }); + + const childA = addDiv(null); + div.appendChild(childA); + const childB = addDiv(null); + div.appendChild(childB); + + const grandchildAA = addDiv(null); + childA.appendChild(grandchildAA); + const grandchildAB = addDiv(null); + childA.appendChild(grandchildAB); + + const grandchildBA = addDiv(null); + childB.appendChild(grandchildBA); + const grandchildBB = addDiv(null); + childB.appendChild(grandchildBB); + + const animation = div.getAnimations()[0]; + + await waitForAnimationReadyToRestyle(animation); + let restyleCount; + restyleCount = await observeStyling(5); + is(restyleCount, 0, + 'Animations on visibility hidden element having no visible ' + + 'descendants should never cause restyles'); + + childA.style.visibility = 'visible'; + grandchildAA.style.visibility = 'visible'; + grandchildAB.style.visibility = 'visible'; + await waitForNextFrame(); + + restyleCount = await observeStyling(5); + is(restyleCount, 5, + 'Animations running on visibility hidden element but the element has ' + + 'visible children should not throttle restyling'); + + // Make childA hidden again but both of grandchildAA and grandchildAB are + // still visible. + childA.style.visibility = 'hidden'; + await waitForNextFrame(); + + restyleCount = await observeStyling(5); + is(restyleCount, 5, + 'Animations running on visibility hidden element that a child has ' + + 'become invisible again but there are still visible children should ' + + 'not throttle restyling'); + + // Make grandchildAA hidden but grandchildAB is still visible. + grandchildAA.style.visibility = 'hidden'; + await waitForNextFrame(); + + restyleCount = await observeStyling(5); + is(restyleCount, 5, + 'Animations running on visibility hidden element that a grandchild ' + + 'become invisible again but another grandchild is still visible ' + + 'should not throttle restyling'); + + + // Make childB and grandchildBA visible. + childB.style.visibility = 'visible'; + grandchildBA.style.visibility = 'visible'; + await waitForNextFrame(); + + restyleCount = await observeStyling(5); + is(restyleCount, 5, + 'Animations running on visibility hidden element but the element has ' + + 'visible descendants should not throttle restyling'); + + // Make childB hidden but grandchildAB and grandchildBA are still visible. + childB.style.visibility = 'hidden'; + await waitForNextFrame(); + + restyleCount = await observeStyling(5); + is(restyleCount, 5, + 'Animations running on visibility hidden element but the element has ' + + 'visible grandchildren should not throttle restyling'); + + // Make grandchildAB hidden but grandchildBA is still visible. + grandchildAB.style.visibility = 'hidden'; + await waitForNextFrame(); + + restyleCount = await observeStyling(5); + is(restyleCount, 5, + 'Animations running on visibility hidden element but the element has ' + + 'a visible grandchild should not throttle restyling'); + + // Make grandchildBA hidden. Now all descedants are invisible. + grandchildBA.style.visibility = 'hidden'; + await waitForNextFrame(); + + restyleCount = await observeStyling(5); + todo_is(restyleCount, 0, + 'Animations on visibility hidden element that all descendants have ' + + 'become invisible again should never cause restyles'); + + // Make childB visible. + childB.style.visibility = 'visible'; + await waitForNextFrame(); + + restyleCount = await observeStyling(5); + is(restyleCount, 5, + 'Animations on visibility hidden element that has a visible child ' + + 'should never cause restyles'); + + // Make childB invisible again + childB.style.visibility = 'hidden'; + await waitForNextFrame(); + + restyleCount = await observeStyling(5); + todo_is(restyleCount, 0, + 'Animations on visibility hidden element that the visible child ' + + 'has become invisible again should never cause restyles'); + + await ensureElementRemoval(div); + } + ); + + add_task_if_omta_enabled(async function no_restyling_compositor_animations_after_pause_is_called() { + const div = addDiv(null, { style: 'animation: opacity 100s' }); + const animation = div.getAnimations()[0]; + + await waitForAnimationReadyToRestyle(animation); + ok(SpecialPowers.wrap(animation).isRunningOnCompositor); + + animation.pause(); + + await animation.ready; + let restyleCount; + restyleCount = await observeStyling(1); + is(restyleCount, 1, + 'Animations running on the compositor should restyle once after ' + + 'Animation.pause() was called'); + + restyleCount = await observeStyling(5); + is(restyleCount, 0, + 'Paused animations running on the compositor should never cause ' + + 'restyles'); + await ensureElementRemoval(div); + }); + + add_task(async function no_restyling_main_thread_animations_after_pause_is_called() { + const div = addDiv(null, { style: 'animation: on-main-thread 100s' }); + const animation = div.getAnimations()[0]; + + await waitForAnimationReadyToRestyle(animation); + + animation.pause(); + + await animation.ready; + let restyleCount; + restyleCount = await observeStyling(1); + is(restyleCount, 1, + 'Animations running on the main-thread should restyle once after ' + + 'Animation.pause() was called'); + + restyleCount = await observeStyling(5); + is(restyleCount, 0, + 'Paused animations running on the main-thread should never cause ' + + 'restyles'); + await ensureElementRemoval(div); + }); + + add_task_if_omta_enabled(async function only_one_restyling_when_current_time_is_set_to_middle_of_duration() { + const div = addDiv(null, { style: 'animation: opacity 100s' }); + const animation = div.getAnimations()[0]; + + await waitForAnimationReadyToRestyle(animation); + + animation.currentTime = 50 * MS_PER_SEC; + + const restyleCount = await observeStyling(5); + is(restyleCount, 1, + 'Bug 1235478: Animations running on the compositor should only once ' + + 'update style when currentTime is set to middle of duration time'); + await ensureElementRemoval(div); + }); + + add_task_if_omta_enabled(async function change_duration_and_currenttime() { + const div = addDiv(null); + const animation = div.animate({ opacity: [ 0, 1 ] }, 100 * MS_PER_SEC); + + await waitForAnimationReadyToRestyle(animation); + ok(SpecialPowers.wrap(animation).isRunningOnCompositor); + + // Set currentTime to a time longer than duration. + animation.currentTime = 500 * MS_PER_SEC; + + // Now the animation immediately get back from compositor. + ok(!SpecialPowers.wrap(animation).isRunningOnCompositor); + + // Extend the duration. + animation.effect.updateTiming({ duration: 800 * MS_PER_SEC }); + const restyleCount = await observeStyling(5); + is(restyleCount, 1, + 'Animations running on the compositor should update style ' + + 'when duration is made longer than the current time'); + + await ensureElementRemoval(div); + }); + + add_task(async function script_animation_on_display_none_element() { + const div = addDiv(null); + const animation = div.animate({ zIndex: [ '0', '999' ] }, + 100 * MS_PER_SEC); + + await waitForAnimationReadyToRestyle(animation); + + div.style.display = 'none'; + + // We need to wait a frame to apply display:none style. + await waitForNextFrame(); + + is(animation.playState, 'running', + 'Script animations keep running even when the target element has ' + + '"display: none" style'); + + ok(!SpecialPowers.wrap(animation).isRunningOnCompositor, + 'Script animations on "display:none" element should not run on the ' + + 'compositor'); + + let restyleCount; + restyleCount = await observeStyling(5); + is(restyleCount, 0, + 'Script animations on "display: none" element should not update styles'); + + div.style.display = ''; + + restyleCount = await observeStyling(5); + is(restyleCount, 5, + 'Script animations restored from "display: none" state should update ' + + 'styles'); + + await ensureElementRemoval(div); + }); + + add_task_if_omta_enabled(async function compositable_script_animation_on_display_none_element() { + const div = addDiv(null); + const animation = div.animate({ opacity: [ 0, 1 ] }, 100 * MS_PER_SEC); + + await waitForAnimationReadyToRestyle(animation); + + div.style.display = 'none'; + + // We need to wait a frame to apply display:none style. + await waitForNextFrame(); + + is(animation.playState, 'running', + 'Opacity script animations keep running even when the target element ' + + 'has "display: none" style'); + + ok(!SpecialPowers.wrap(animation).isRunningOnCompositor, + 'Opacity script animations on "display:none" element should not ' + + 'run on the compositor'); + + let restyleCount; + restyleCount = await observeStyling(5); + is(restyleCount, 0, + 'Opacity script animations on "display: none" element should not ' + + 'update styles'); + + div.style.display = ''; + + restyleCount = await observeStyling(1); + is(restyleCount, 1, + 'Script animations restored from "display: none" state should update ' + + 'styles soon'); + + ok(SpecialPowers.wrap(animation).isRunningOnCompositor, + 'Opacity script animations restored from "display: none" should be ' + + 'run on the compositor in the next frame'); + + await ensureElementRemoval(div); + }); + + add_task(async function restyling_for_empty_keyframes() { + const div = addDiv(null); + const animation = div.animate({ }, 100 * MS_PER_SEC); + + await waitForAnimationReadyToRestyle(animation); + let restyleCount; + restyleCount = await observeStyling(5); + + is(restyleCount, 0, + 'Animations with no keyframes should not cause restyles'); + + animation.effect.setKeyframes({ zIndex: ['0', '999'] }); + restyleCount = await observeStyling(5); + + is(restyleCount, 5, + 'Setting valid keyframes should cause regular animation restyles to ' + + 'occur'); + + animation.effect.setKeyframes({ }); + restyleCount = await observeStyling(5); + + is(restyleCount, 1, + 'Setting an empty set of keyframes should trigger a single restyle ' + + 'to remove the previous animated style'); + + await ensureElementRemoval(div); + }); + + add_task_if_omta_enabled(async function no_restyling_when_animation_style_when_re_setting_same_animation_property() { + const div = addDiv(null, { style: 'animation: opacity 100s' }); + const animation = div.getAnimations()[0]; + await waitForAnimationReadyToRestyle(animation); + ok(SpecialPowers.wrap(animation).isRunningOnCompositor); + // Apply the same animation style + div.style.animation = 'opacity 100s'; + const restyleCount = await observeStyling(5); + is(restyleCount, 0, + 'Applying same animation style ' + + 'should never cause restyles'); + await ensureElementRemoval(div); + }); + + add_task(async function necessary_update_should_be_invoked() { + const div = addDiv(null, { style: 'animation: on-main-thread 100s' }); + const animation = div.getAnimations()[0]; + await waitForAnimationReadyToRestyle(animation); + await waitForAnimationFrames(5); + // Apply another animation style + div.style.animation = 'on-main-thread 110s'; + const restyleCount = await observeStyling(1); + // There should be two restyles. + // 1) Animation-only restyle for before applying the new animation style + // 2) Animation-only restyle for after applying the new animation style + is(restyleCount, 2, + 'Applying animation style with different duration ' + + 'should restyle twice'); + await ensureElementRemoval(div); + }); + + add_task_if_omta_enabled( + async function changing_cascading_result_for_main_thread_animation() { + const div = addDiv(null, { style: 'on-main-thread: blue' }); + const animation = div.animate({ opacity: [0, 1], + zIndex: ['0', '999'] }, + 100 * MS_PER_SEC); + await waitForAnimationReadyToRestyle(animation); + ok(SpecialPowers.wrap(animation).isRunningOnCompositor, + 'The opacity animation is running on the compositor'); + // Make the z-index style as !important to cause an update + // to the cascade. + // Bug 1300982: The z-index animation should be no longer + // running on the main thread. + div.style.setProperty('z-index', '0', 'important'); + const restyleCount = await observeStyling(5); + todo_is(restyleCount, 0, + 'Changing cascading result for the property running on the main ' + + 'thread does not cause synchronization layer of opacity animation ' + + 'running on the compositor'); + await ensureElementRemoval(div); + } + ); + + add_task_if_omta_enabled( + async function animation_visibility_and_opacity() { + const div = addDiv(null); + const animation1 = div.animate({ opacity: [0, 1] }, 100 * MS_PER_SEC); + const animation2 = div.animate({ visibility: ['hidden', 'visible'] }, 100 * MS_PER_SEC); + await waitForAnimationReadyToRestyle(animation1); + await waitForAnimationReadyToRestyle(animation2); + const restyleCount = await observeStyling(5); + is(restyleCount, 5, 'The animation should not be throttled'); + await ensureElementRemoval(div); + } + ); + + add_task(async function restyling_for_animation_on_orphaned_element() { + const div = addDiv(null); + const animation = div.animate({ marginLeft: [ '0px', '100px' ] }, + 100 * MS_PER_SEC); + + await waitForAnimationReadyToRestyle(animation); + + div.remove(); + let restyleCount; + restyleCount = await observeStyling(5); + is(restyleCount, 0, + 'Animation on orphaned element should not cause restyles'); + + document.body.appendChild(div); + + await waitForNextFrame(); + restyleCount = await observeStyling(5); + + is(restyleCount, 5, + 'Animation on re-attached to the document begins to update style, got ' + restyleCount); + + await ensureElementRemoval(div); + }); + + add_task_if_omta_enabled( + // Tests that if we remove an element from the document whose animation + // cascade needs recalculating, that it is correctly updated when it is + // re-attached to the document. + async function restyling_for_opacity_animation_on_re_attached_element() { + const div = addDiv(null, { style: 'opacity: 1 ! important' }); + const animation = div.animate({ opacity: [0, 1] }, 100 * MS_PER_SEC); + + await waitForAnimationReadyToRestyle(animation); + ok(!SpecialPowers.wrap(animation).isRunningOnCompositor, + 'The opacity animation overridden by an !important rule is NOT ' + + 'running on the compositor'); + + // Drop the !important rule to update the cascade. + div.style.setProperty('opacity', '1', ''); + + div.remove(); + + const restyleCount = await observeStyling(5); + is(restyleCount, 0, + 'Opacity animation on orphaned element should not cause restyles'); + + document.body.appendChild(div); + + // Need a frame to give the animation a chance to be sent to the + // compositor. + await waitForNextFrame(); + + ok(SpecialPowers.wrap(animation).isRunningOnCompositor, + 'The opacity animation which is no longer overridden by the ' + + '!important rule begins running on the compositor even if the ' + + '!important rule had been dropped before the target element was ' + + 'removed'); + + await ensureElementRemoval(div); + } + ); + + add_task( + async function no_throttling_additive_animations_out_of_view_element() { + const div = addDiv(null, { style: 'transform: translateY(-400px);' }); + const animation = + div.animate([{ visibility: 'visible' }], + { duration: 100 * MS_PER_SEC, composite: 'add' }); + + await waitForAnimationReadyToRestyle(animation); + + const restyleCount = await observeStyling(5); + + is(restyleCount, 0, + 'Additive animation has no keyframe whose offset is 0 or 1 in an ' + + 'out-of-view element should be throttled'); + await ensureElementRemoval(div); + } + ); + + // Tests that missing keyframes animations don't throttle at all. + add_task(async function no_throttling_animations_out_of_view_element() { + const div = addDiv(null, { style: 'transform: translateY(-400px);' }); + const animation = + div.animate([{ visibility: 'visible' }], 100 * MS_PER_SEC); + + await waitForAnimationReadyToRestyle(animation); + + const restyleCount = await observeStyling(5); + + is(restyleCount, 0, + 'Discrete animation has has no keyframe whose offset is 0 or 1 in an ' + + 'out-of-view element should be throttled'); + await ensureElementRemoval(div); + }); + + // Tests that missing keyframes animation on scrolled out element that the + // animation is not able to be throttled. + add_task( + async function no_throttling_missing_keyframe_animations_out_of_view_element() { + const div = + addDiv(null, { style: 'transform: translateY(-400px);' + + 'visibility: collapse;' }); + const animation = + div.animate([{ visibility: 'visible' }], 100 * MS_PER_SEC); + await waitForAnimationReadyToRestyle(animation); + + const restyleCount = await observeStyling(5); + is(restyleCount, 0, + 'visibility animation has no keyframe whose offset is 0 or 1 in an ' + + 'out-of-view element should be throttled'); + await ensureElementRemoval(div); + } + ); + + // Counter part of the above test. + add_task(async function no_restyling_discrete_animations_out_of_view_element() { + const div = addDiv(null, { style: 'transform: translateY(-400px);' }); + const animation = + div.animate({ visibility: ['visible', 'hidden'] }, 100 * MS_PER_SEC); + + await waitForAnimationReadyToRestyle(animation); + + const restyleCount = await observeStyling(5); + + is(restyleCount, 0, + 'Discrete animation running on the main-thread in an out-of-view ' + + 'element should never cause restyles'); + await ensureElementRemoval(div); + }); + + add_task(async function no_restyling_while_computed_timing_is_not_changed() { + const div = addDiv(null); + const animation = div.animate({ zIndex: [ '0', '999' ] }, + { duration: 100 * MS_PER_SEC, + easing: 'step-end' }); + + await waitForAnimationReadyToRestyle(animation); + + const restyleCount = await observeStyling(5); + + // We possibly expect one restyle from the initial animation compose, in + // order to update animations, but nothing else. + ok(restyleCount <= 1, + 'Animation running on the main-thread while computed timing is not ' + + 'changed should not cause extra restyles, got ' + restyleCount); + await ensureElementRemoval(div); + }); + + add_task(async function no_throttling_animations_in_view_svg() { + const div = addDiv(null, { style: 'overflow: scroll;' + + 'height: 100px; width: 100px;' }); + const svg = addSVGElement(div, 'svg', { viewBox: '-10 -10 0.1 0.1', + width: '50px', + height: '50px' }); + const rect = addSVGElement(svg, 'rect', { x: '-10', + y: '-10', + width: '10', + height: '10', + fill: 'red' }); + const animation = rect.animate({ fill: ['blue', 'lime'] }, 100 * MS_PER_SEC); + await waitForAnimationReadyToRestyle(animation); + + const restyleCount = await observeStyling(5); + is(restyleCount, 5, + 'CSS animations on an in-view svg element with post-transform should ' + + 'not be throttled.'); + + await ensureElementRemoval(div); + }); + + add_task(async function no_throttling_animations_in_transformed_parent() { + const div = addDiv(null, { style: 'overflow: scroll;' + + 'transform: translateX(50px);' }); + const svg = addSVGElement(div, 'svg', { viewBox: '0 0 1250 1250', + width: '40px', + height: '40px' }); + const rect = addSVGElement(svg, 'rect', { x: '0', + y: '0', + width: '1250', + height: '1250', + fill: 'red' }); + const animation = rect.animate({ fill: ['blue', 'lime'] }, 100 * MS_PER_SEC); + await waitForAnimationReadyToRestyle(animation); + + const restyleCount = await observeStyling(5); + is(restyleCount, 5, + 'CSS animations on an in-view svg element which is inside transformed ' + + 'parent should not be throttled.'); + + await ensureElementRemoval(div); + }); + + add_task(async function throttling_animations_out_of_view_svg() { + const div = addDiv(null, { style: 'overflow: scroll;' + + 'height: 100px; width: 100px;' }); + const svg = addSVGElement(div, 'svg', { viewBox: '-10 -10 0.1 0.1', + width: '50px', + height: '50px' }); + const rect = addSVGElement(svg, 'rect', { width: '10', + height: '10', + fill: 'red' }); + + const animation = rect.animate({ fill: ['blue', 'lime'] }, 100 * MS_PER_SEC); + await waitForAnimationReadyToRestyle(animation); + + const restyleCount = await observeStyling(5); + is(restyleCount, 0, + 'CSS animations on an out-of-view svg element with post-transform ' + + 'should be throttled.'); + + await ensureElementRemoval(div); + }); + + add_task(async function no_throttling_animations_in_view_css_transform() { + const scrollDiv = addDiv(null, { style: 'overflow: scroll; ' + + 'height: 100px; width: 100px;' }); + const targetDiv = addDiv(null, + { style: 'animation: on-main-thread 100s;' + + 'transform: translate(-50px, -50px);' }); + scrollDiv.appendChild(targetDiv); + + const animation = targetDiv.getAnimations()[0]; + await waitForAnimationReadyToRestyle(animation); + + const restyleCount = await observeStyling(5); + is(restyleCount, 5, + 'CSS animation on an in-view element with pre-transform should not ' + + 'be throttled.'); + + await ensureElementRemoval(scrollDiv); + }); + + add_task(async function throttling_animations_out_of_view_css_transform() { + const scrollDiv = addDiv(null, { style: 'overflow: scroll;' + + 'height: 100px; width: 100px;' }); + const targetDiv = addDiv(null, + { style: 'animation: on-main-thread 100s;' + + 'transform: translate(100px, 100px);' }); + scrollDiv.appendChild(targetDiv); + + const animation = targetDiv.getAnimations()[0]; + await waitForAnimationReadyToRestyle(animation); + + const restyleCount = await observeStyling(5); + is(restyleCount, 0, + 'CSS animation on an out-of-view element with pre-transform should be ' + + 'throttled.'); + + await ensureElementRemoval(scrollDiv); + }); + + add_task( + async function throttling_animations_in_out_of_view_position_absolute_element() { + const parentDiv = addDiv(null, + { style: 'position: absolute; top: -1000px;' }); + const targetDiv = addDiv(null, + { style: 'animation: on-main-thread 100s;' }); + parentDiv.appendChild(targetDiv); + + const animation = targetDiv.getAnimations()[0]; + await waitForAnimationReadyToRestyle(animation); + + const restyleCount = await observeStyling(5); + is(restyleCount, 0, + 'CSS animation in an out-of-view position absolute element should ' + + 'be throttled'); + + await ensureElementRemoval(parentDiv); + } + ); + + add_task( + async function throttling_animations_on_out_of_view_position_absolute_element() { + const div = addDiv(null, + { style: 'animation: on-main-thread 100s; ' + + 'position: absolute; top: -1000px;' }); + + const animation = div.getAnimations()[0]; + await waitForAnimationReadyToRestyle(animation); + + const restyleCount = await observeStyling(5); + is(restyleCount, 0, + 'CSS animation on an out-of-view position absolute element should ' + + 'be throttled'); + + await ensureElementRemoval(div); + } + ); + + add_task( + async function throttling_animations_in_out_of_view_position_fixed_element() { + const parentDiv = addDiv(null, + { style: 'position: fixed; top: -1000px;' }); + const targetDiv = addDiv(null, + { style: 'animation: on-main-thread 100s;' }); + parentDiv.appendChild(targetDiv); + + const animation = targetDiv.getAnimations()[0]; + await waitForAnimationReadyToRestyle(animation); + + const restyleCount = await observeStyling(5); + is(restyleCount, 0, + 'CSS animation on an out-of-view position:fixed element should be ' + + 'throttled'); + + await ensureElementRemoval(parentDiv); + } + ); + + add_task( + async function throttling_animations_on_out_of_view_position_fixed_element() { + const div = addDiv(null, + { style: 'animation: on-main-thread 100s; ' + + 'position: fixed; top: -1000px;' }); + + const animation = div.getAnimations()[0]; + await waitForAnimationReadyToRestyle(animation); + + const restyleCount = await observeStyling(5); + is(restyleCount, 0, + 'CSS animation on an out-of-view position:fixed element should be ' + + 'throttled'); + + await ensureElementRemoval(div); + } + ); + + add_task( + async function no_throttling_animations_in_view_position_fixed_element() { + const iframe = document.createElement('iframe'); + iframe.setAttribute('srcdoc', '<div id="target"></div>'); + document.documentElement.appendChild(iframe); + + await new Promise(resolve => { + iframe.addEventListener('load', () => { + resolve(); + }); + }); + + const target = iframe.contentDocument.getElementById('target'); + target.style= 'position: fixed; top: 20px; width: 100px; height: 100px;'; + + const animation = target.animate({ zIndex: [ '0', '999' ] }, + { duration: 100 * MS_PER_SEC, + iterations: Infinity }); + await waitForAnimationReadyToRestyle(animation); + + const restyleCount = await observeStylingInTargetWindow(iframe.contentWindow, 5); + is(restyleCount, 5, + 'CSS animation on an in-view position:fixed element should NOT be ' + + 'throttled'); + + await ensureElementRemoval(iframe); + } + ); + + add_task( + async function throttling_position_absolute_animations_in_collapsed_iframe() { + const iframe = document.createElement('iframe'); + iframe.setAttribute('srcdoc', '<div id="target"></div>'); + iframe.style.height = '0px'; + document.documentElement.appendChild(iframe); + + await new Promise(resolve => { + iframe.addEventListener('load', () => { + resolve(); + }); + }); + + const target = iframe.contentDocument.getElementById("target"); + target.style= 'position: absolute; top: 50%; width: 100px; height: 100px'; + + const animation = target.animate({ opacity: [0, 1] }, + { duration: 100 * MS_PER_SEC, + iterations: Infinity }); + await waitForAnimationReadyToRestyle(animation); + + const restyleCount = await observeStylingInTargetWindow(iframe.contentWindow, 5); + is(restyleCount, 0, + 'Animation on position:absolute element in collapsed iframe should ' + + 'be throttled'); + + await ensureElementRemoval(iframe); + } + ); + + add_task( + async function position_absolute_animations_in_collapsed_element() { + const parent = addDiv(null, { style: 'overflow: scroll; height: 0px;' }); + const target = addDiv(null, + { style: 'animation: on-main-thread 100s infinite;' + + 'position: absolute; top: 50%;' + + 'width: 100px; height: 100px;' }); + parent.appendChild(target); + + const animation = target.getAnimations()[0]; + await waitForAnimationReadyToRestyle(animation); + + const restyleCount = await observeStyling(5); + is(restyleCount, 5, + 'Animation on position:absolute element in collapsed element ' + + 'should not be throttled'); + + await ensureElementRemoval(parent); + } + ); + + add_task( + async function throttling_position_absolute_animations_in_collapsed_element() { + const parent = addDiv(null, { style: 'overflow: scroll; height: 0px;' }); + const target = addDiv(null, + { style: 'animation: on-main-thread 100s infinite;' + + 'position: absolute; top: 50%;' }); + parent.appendChild(target); + + const animation = target.getAnimations()[0]; + await waitForAnimationReadyToRestyle(animation); + + const restyleCount = await observeStyling(5); + todo_is(restyleCount, 0, + 'Animation on collapsed position:absolute element in collapsed ' + + 'element should be throttled'); + + await ensureElementRemoval(parent); + } + ); + + add_task_if_omta_enabled( + async function no_restyling_for_compositor_animation_on_unrelated_style_change() { + const div = addDiv(null); + const animation = div.animate({ opacity: [0, 1] }, 100 * MS_PER_SEC); + + await waitForAnimationReadyToRestyle(animation); + ok(SpecialPowers.wrap(animation).isRunningOnCompositor, + 'The opacity animation is running on the compositor'); + + div.style.setProperty('color', 'blue', ''); + const restyleCount = await observeStyling(5); + is(restyleCount, 0, + 'The opacity animation keeps running on the compositor when ' + + 'color style is changed'); + await ensureElementRemoval(div); + } + ); + + add_task( + async function no_overflow_transform_animations_in_scrollable_element() { + const parentElement = addDiv(null, + { style: 'overflow-y: scroll; height: 100px;' }); + const div = addDiv(null); + const animation = + div.animate({ transform: [ 'translateY(10px)', 'translateY(10px)' ] }, + 100 * MS_PER_SEC); + parentElement.appendChild(div); + + await waitForAnimationReadyToRestyle(animation); + ok(SpecialPowers.wrap(animation).isRunningOnCompositor); + + const restyleCount = await observeStyling(20); + is(restyleCount, 0, + 'No-overflow transform animations running on the compositor should ' + + 'never update style on the main thread'); + + await ensureElementRemoval(parentElement); + } + ); + + add_task(async function no_flush_on_getAnimations() { + const div = addDiv(null); + const animation = + div.animate({ opacity: [ '0', '1' ] }, 100 * MS_PER_SEC); + await waitForAnimationReadyToRestyle(animation); + + ok(SpecialPowers.wrap(animation).isRunningOnCompositor); + + const restyleCount = observeAnimSyncStyling(() => { + is(div.getAnimations().length, 1, 'There should be one animation'); + }); + is(restyleCount, 0, + 'Element.getAnimations() should not flush throttled animation style'); + + await ensureElementRemoval(div); + }); + + add_task(async function restyling_for_throttled_animation_on_getAnimations() { + const div = addDiv(null, { style: 'animation: opacity 100s' }); + const animation = div.getAnimations()[0]; + + await waitForAnimationReadyToRestyle(animation); + ok(SpecialPowers.wrap(animation).isRunningOnCompositor); + + const restyleCount = observeAnimSyncStyling(() => { + div.style.animationDuration = '0s'; + is(div.getAnimations().length, 0, 'There should be no animation'); + }); + + is(restyleCount, 1, // For discarding the throttled animation. + 'Element.getAnimations() should flush throttled animation style so ' + + 'that the throttled animation is discarded'); + + await ensureElementRemoval(div); + }); + + add_task( + async function no_restyling_for_throttled_animation_on_querying_play_state() { + const div = addDiv(null, { style: 'animation: opacity 100s' }); + const animation = div.getAnimations()[0]; + const sibling = addDiv(null); + + await waitForAnimationReadyToRestyle(animation); + ok(SpecialPowers.wrap(animation).isRunningOnCompositor); + + const restyleCount = observeAnimSyncStyling(() => { + sibling.style.opacity = '0.5'; + is(animation.playState, 'running', + 'Animation.playState should be running'); + }); + is(restyleCount, 0, + 'Animation.playState should not flush throttled animation in the ' + + 'case where there are only style changes that don\'t affect the ' + + 'throttled animation'); + + await ensureElementRemoval(div); + await ensureElementRemoval(sibling); + } + ); + + add_task( + async function restyling_for_throttled_animation_on_querying_play_state() { + const div = addDiv(null, { style: 'animation: opacity 100s' }); + const animation = div.getAnimations()[0]; + + await waitForAnimationReadyToRestyle(animation); + ok(SpecialPowers.wrap(animation).isRunningOnCompositor); + + const restyleCount = observeAnimSyncStyling(() => { + div.style.animationPlayState = 'paused'; + is(animation.playState, 'paused', + 'Animation.playState should be reflected by pending style'); + }); + + is(restyleCount, 1, + 'Animation.playState should flush throttled animation style that ' + + 'affects the throttled animation'); + + await ensureElementRemoval(div); + } + ); + + add_task( + async function no_restyling_for_throttled_transition_on_querying_play_state() { + const div = addDiv(null, { style: 'transition: opacity 100s; opacity: 0' }); + const sibling = addDiv(null); + + getComputedStyle(div).opacity; + div.style.opacity = 1; + + const transition = div.getAnimations()[0]; + + await waitForAnimationReadyToRestyle(transition); + ok(SpecialPowers.wrap(transition).isRunningOnCompositor); + + const restyleCount = observeAnimSyncStyling(() => { + sibling.style.opacity = '0.5'; + is(transition.playState, 'running', + 'Animation.playState should be running'); + }); + + is(restyleCount, 0, + 'Animation.playState should not flush throttled transition in the ' + + 'case where there are only style changes that don\'t affect the ' + + 'throttled animation'); + + await ensureElementRemoval(div); + await ensureElementRemoval(sibling); + } + ); + + add_task( + async function restyling_for_throttled_transition_on_querying_play_state() { + const div = addDiv(null, { style: 'transition: opacity 100s; opacity: 0' }); + getComputedStyle(div).opacity; + div.style.opacity = '1'; + + const transition = div.getAnimations()[0]; + + await waitForAnimationReadyToRestyle(transition); + ok(SpecialPowers.wrap(transition).isRunningOnCompositor); + + const restyleCount = observeAnimSyncStyling(() => { + div.style.transitionProperty = 'none'; + is(transition.playState, 'idle', + 'Animation.playState should be reflected by pending style change ' + + 'which cancel the transition'); + }); + + is(restyleCount, 1, + 'Animation.playState should flush throttled transition style that ' + + 'affects the throttled animation'); + + await ensureElementRemoval(div); + } + ); + + add_task(async function restyling_visibility_animations_on_in_view_element() { + const div = addDiv(null); + const animation = + div.animate({ visibility: ['hidden', 'visible'] }, 100 * MS_PER_SEC); + + await waitForAnimationReadyToRestyle(animation); + + const restyleCount = await observeStyling(5); + + is(restyleCount, 5, + 'Visibility animation running on the main-thread on in-view element ' + + 'should not be throttled'); + await ensureElementRemoval(div); + }); + + add_task(async function restyling_outline_offset_animations_on_invisible_element() { + const div = addDiv(null, + { style: 'visibility: hidden; ' + + 'outline-style: solid; ' + + 'outline-width: 1px;' }); + const animation = + div.animate({ outlineOffset: [ '0px', '10px' ] }, + { duration: 100 * MS_PER_SEC, + iterations: Infinity }); + + await waitForAnimationReadyToRestyle(animation); + + const restyleCount = await observeStyling(5); + + is(restyleCount, 0, + 'Outline offset animation running on the main-thread on invisible ' + + 'element should be throttled'); + await ensureElementRemoval(div); + }); + + add_task(async function restyling_transform_animations_on_invisible_element() { + const div = addDiv(null, { style: 'visibility: hidden;' }); + + const animation = + div.animate({ transform: [ 'none', 'rotate(360deg)' ] }, + { duration: 100 * MS_PER_SEC, + iterations: Infinity }); + + await waitForAnimationReadyToRestyle(animation); + + ok(!SpecialPowers.wrap(animation).isRunningOnCompositor); + + const restyleCount = await observeStyling(5); + + is(restyleCount, 0, + 'Transform animations on visibility hidden element should be throttled'); + await ensureElementRemoval(div); + }); + + add_task(async function restyling_transform_animations_on_invisible_element() { + const div = addDiv(null, { style: 'visibility: hidden;' }); + + const animation = + div.animate([ { transform: 'rotate(360deg)' } ], + { duration: 100 * MS_PER_SEC, + iterations: Infinity }); + + await waitForAnimationReadyToRestyle(animation); + + ok(!SpecialPowers.wrap(animation).isRunningOnCompositor); + + const restyleCount = await observeStyling(5); + + is(restyleCount, 0, + 'Transform animations without 100% keyframe on visibility hidden ' + + 'element should be throttled'); + await ensureElementRemoval(div); + }); + + add_task(async function restyling_translate_animations_on_invisible_element() { + const div = addDiv(null, { style: 'visibility: hidden;' }); + + const animation = + div.animate([ { translate: '100px' } ], + { duration: 100 * MS_PER_SEC, + iterations: Infinity }); + + await waitForAnimationReadyToRestyle(animation); + + ok(!SpecialPowers.wrap(animation).isRunningOnCompositor); + + const restyleCount = await observeStyling(5); + + is(restyleCount, 0, + 'Translate animations without 100% keyframe on visibility hidden ' + + 'element should be throttled'); + await ensureElementRemoval(div); + }); + + add_task(async function restyling_rotate_animations_on_invisible_element() { + const div = addDiv(null, { style: 'visibility: hidden;' }); + + const animation = + div.animate([ { rotate: '45deg' } ], + { duration: 100 * MS_PER_SEC, + iterations: Infinity }); + + await waitForAnimationReadyToRestyle(animation); + + ok(!SpecialPowers.wrap(animation).isRunningOnCompositor); + + const restyleCount = await observeStyling(5); + + is(restyleCount, 0, + 'Rotate animations without 100% keyframe on visibility hidden ' + + 'element should be throttled'); + await ensureElementRemoval(div); + }); + + add_task(async function restyling_scale_animations_on_invisible_element() { + const div = addDiv(null, { style: 'visibility: hidden;' }); + + const animation = + div.animate([ { scale: '2 2' } ], + { duration: 100 * MS_PER_SEC, + iterations: Infinity }); + + await waitForAnimationReadyToRestyle(animation); + + ok(!SpecialPowers.wrap(animation).isRunningOnCompositor); + + const restyleCount = await observeStyling(5); + + is(restyleCount, 0, + 'Scale animations without 100% keyframe on visibility hidden ' + + 'element should be throttled'); + await ensureElementRemoval(div); + }); + + add_task( + async function restyling_transform_animations_having_abs_pos_child_on_invisible_element() { + const div = addDiv(null, { style: 'visibility: hidden;' }); + const child = addDiv(null, { style: 'position: absolute; top: 100px;' }); + div.appendChild(child); + + const animation = + div.animate({ transform: [ 'none', 'rotate(360deg)' ] }, + { duration: 100 * MS_PER_SEC, + iterations: Infinity }); + + await waitForAnimationReadyToRestyle(animation); + + ok(!SpecialPowers.wrap(animation).isRunningOnCompositor); + + const restyleCount = await observeStyling(5); + + is(restyleCount, 0, + 'Transform animation having an absolutely positioned child on ' + + 'visibility hidden element should be throttled'); + await ensureElementRemoval(div); + }); + + add_task(async function no_restyling_animations_in_out_of_view_iframe() { + const div = addDiv(null, { style: 'overflow-y: scroll; height: 100px;' }); + + const iframe = document.createElement('iframe'); + iframe.setAttribute( + 'srcdoc', + '<div style="height: 100px;"></div><div id="target"></div>'); + div.appendChild(iframe); + + await new Promise(resolve => { + iframe.addEventListener('load', () => { + resolve(); + }); + }); + + const target = iframe.contentDocument.getElementById("target"); + target.style= 'width: 100px; height: 100px;'; + + const animation = target.animate({ zIndex: [ '0', '999' ] }, + 100 * MS_PER_SEC); + await waitForAnimationReadyToRestyle(animation); + + const restyleCount = await observeStylingInTargetWindow(iframe.contentWindow, 5); + is(restyleCount, 0, + 'Animation in out-of-view iframe should be throttled'); + + await ensureElementRemoval(div); + }); + + // Tests that transform animations are not able to run on the compositor due + // to layout restrictions (e.g. animations on a large size frame) doesn't + // flush layout at all. + add_task(async function flush_layout_for_transform_animations() { + // Set layout.animation.prerender.partial to disallow transform animations + // on large frames to be sent to the compositor. + await SpecialPowers.pushPrefEnv({ + set: [['layout.animation.prerender.partial', false]] }); + const div = addDiv(null, { style: 'width: 10000px; height: 10000px;' }); + + const animation = div.animate([ { transform: 'rotate(360deg)', } ], + { duration: 100 * MS_PER_SEC, + // Set step-end to skip further restyles. + easing: 'step-end' }); + + const FLUSH_LAYOUT = SpecialPowers.DOMWindowUtils.FLUSH_LAYOUT; + ok(SpecialPowers.DOMWindowUtils.needsFlush(FLUSH_LAYOUT), + 'Flush is needed for the appended div'); + + await waitForAnimationReadyToRestyle(animation); + + ok(!SpecialPowers.wrap(animation).isRunningOnCompositor, "Shouldn't be running in the compositor"); + + // We expect one restyle from the initial animation compose. + await waitForNextFrame(); + + ok(!SpecialPowers.wrap(animation).isRunningOnCompositor, "Still shouldn't be running in the compositor"); + ok(!SpecialPowers.DOMWindowUtils.needsFlush(FLUSH_LAYOUT), + 'No further layout flush needed'); + + await ensureElementRemoval(div); + }); + + add_task(async function partial_prerendered_transform_animations() { + await SpecialPowers.pushPrefEnv({ + set: [['layout.animation.prerender.partial', true]] }); + const div = addDiv(null, { style: 'width: 10000px; height: 10000px;' }); + + const animation = div.animate( + // Use the same value both for `from` and `to` to avoid jank on the + // compositor. + { transform: ['rotate(0deg)', 'rotate(0deg)'] }, + 100 * MS_PER_SEC + ); + + await waitForAnimationReadyToRestyle(animation); + + const restyleCount = await observeStyling(5) + is(restyleCount, 0, + 'Transform animation with partial pre-rendered should never cause ' + + 'restyles'); + + await ensureElementRemoval(div); + }); + + add_task(async function restyling_on_create_animation() { + const div = addDiv(); + let priorAnimationTriggeredRestyles = SpecialPowers.DOMWindowUtils.animationTriggeredRestyles; + + const animationA = div.animate( + { transform: ['none', 'rotate(360deg)'] }, + 100 * MS_PER_SEC + ); + const animationB = div.animate({ opacity: [0, 1] }, 100 * MS_PER_SEC); + const animationC = div.animate( + { color: ['blue', 'green'] }, + 100 * MS_PER_SEC + ); + const animationD = div.animate( + { width: ['100px', '200px'] }, + 100 * MS_PER_SEC + ); + const animationE = div.animate( + { height: ['100px', '200px'] }, + 100 * MS_PER_SEC + ); + + const restyleCount = SpecialPowers.DOMWindowUtils.animationTriggeredRestyles - priorAnimationTriggeredRestyles; + + is(restyleCount, 0, 'Creating animations should not flush styles'); + + await ensureElementRemoval(div); + }); + + add_task(async function out_of_view_background_position() { + const div = addDiv(null, { + style: ` + background-image: linear-gradient(90deg, rgb(224, 224, 224), rgb(241, 241, 241) 30%, rgb(224, 224, 224) 60%); + background-size: 80px; + animation: background-position 100s infinite; + transform: translateY(-400px); + `, + }) + + const animation = div.getAnimations()[0]; + + await waitForAnimationReadyToRestyle(animation); + + const restyleCount = await observeStyling(5); + + is(restyleCount, 0, 'background-position animations can be throttled'); + await ensureElementRemoval(div); + }); + + add_task_if_omta_enabled(async function no_restyling_animations_in_opacity_zero_element() { + const div = addDiv(null, { style: 'animation: on-main-thread 100s infinite; opacity: 0' }); + const animation = div.getAnimations()[0]; + + await waitForAnimationReadyToRestyle(animation); + const restyleCount = await observeStyling(5); + is(restyleCount, 0, + 'Animations running on the main thread in opacity: 0 element ' + + 'should never cause restyles'); + await ensureElementRemoval(div); + }); + + add_task_if_omta_enabled(async function no_restyling_compositor_animations_in_opacity_zero_descendant() { + const container = addDiv(null, { style: 'opacity: 0' }); + const child = addDiv(null, { style: 'animation: background-color 100s infinite;' }); + container.appendChild(child); + + const animation = child.getAnimations()[0]; + await waitForAnimationReadyToRestyle(animation); + ok(!SpecialPowers.wrap(animation).isRunningOnCompositor); + + const restyleCount = await observeStyling(5); + + is(restyleCount, 0, + 'Animations running on the compositor in opacity zero descendant element ' + + 'should never cause restyles'); + await ensureElementRemoval(container); + }); + + add_task_if_omta_enabled(async function no_restyling_compositor_animations_in_opacity_zero_descendant_abspos() { + const container = addDiv(null, { style: 'opacity: 0' }); + const child = addDiv(null, { style: 'position: absolute; animation: background-color 100s infinite;' }); + container.appendChild(child); + + const animation = child.getAnimations()[0]; + await waitForAnimationReadyToRestyle(animation); + ok(!SpecialPowers.wrap(animation).isRunningOnCompositor); + + const restyleCount = await observeStyling(5); + + is(restyleCount, 0, + 'Animations running on the compositor in opacity zero abspos descendant element ' + + 'should never cause restyles'); + await ensureElementRemoval(container); + }); + + add_task_if_omta_enabled(async function no_restyling_compositor_animations_in_opacity_zero_element() { + const child = addDiv(null, { style: 'animation: background-color 100s infinite; opacity: 0' }); + + const animation = child.getAnimations()[0]; + await waitForAnimationReadyToRestyle(animation); + ok(!SpecialPowers.wrap(animation).isRunningOnCompositor); + + const restyleCount = await observeStyling(5); + + is(restyleCount, 0, + 'Animations running on the compositor in opacity zero element ' + + 'should never cause restyles'); + await ensureElementRemoval(child); + }); + + add_task_if_omta_enabled(async function restyling_main_thread_animations_in_opacity_zero_descendant_after_root_opacity_animation() { + const container = addDiv(null, { style: 'opacity: 0' }); + + const child = addDiv(null, { style: 'animation: on-main-thread 100s infinite;' }); + container.appendChild(child); + + // Animate the container from 1 to zero opacity and ensure the child animation is throttled then. + const containerAnimation = container.animate({ opacity: [ '1', '0' ] }, 100); + await containerAnimation.finished; + + const animation = child.getAnimations()[0]; + await waitForAnimationReadyToRestyle(animation); + + const restyleCount = await observeStyling(5); + + is(restyleCount, 0, + 'Animations running on the compositor in opacity zero descendant element ' + + 'should never cause restyles after root animation has finished'); + await ensureElementRemoval(container); + }); + + add_task_if_omta_enabled(async function restyling_main_thread_animations_in_opacity_zero_descendant_during_root_opacity_animation() { + const container = addDiv(null, { style: 'opacity: 0; animation: opacity-from-zero 100s infinite' }); + + const child = addDiv(null, { style: 'animation: on-main-thread 100s infinite;' }); + container.appendChild(child); + + const animation = child.getAnimations()[0]; + await waitForAnimationReadyToRestyle(animation); + + const restyleCount = await observeStyling(5); + + is(restyleCount, 5, + 'Animations in opacity zero descendant element ' + + 'should not be throttled if root is animating opacity'); + await ensureElementRemoval(container); + }); + + add_task_if_omta_enabled(async function restyling_omt_animations_in_opacity_zero_descendant_during_root_opacity_animation() { + const container = addDiv(null, { style: 'opacity: 0; animation: opacity-from-zero 100s infinite' }); + + const child = addDiv(null, { style: 'animation: rotate 100s infinite' }); + container.appendChild(child); + + const animation = child.getAnimations()[0]; + await waitForAnimationReadyToRestyle(animation); + ok(SpecialPowers.wrap(animation).isRunningOnCompositor); + + await ensureElementRemoval(container); + }); + + add_task_if_omta_enabled(async function transparent_background_color_animations() { + const div = addDiv(null); + const animation = + div.animate({ backgroundColor: [ 'rgb(0, 200, 0, 0)', + 'rgb(200, 0, 0, 0.1)' ] }, + { duration: 100 * MS_PER_SEC, + // An easing function producing zero in the first half of + // the duration. + easing: 'cubic-bezier(1, 0, 1, 0)' }); + await waitForAnimationReadyToRestyle(animation); + + ok(SpecialPowers.wrap(animation).isRunningOnCompositor); + + const restyleCount = await observeStyling(5); + is(restyleCount, 0, + 'transparent background-color animation should not update styles on ' + + 'the main thread'); + + await ensureElementRemoval(div); + }); + + add_task_if_omta_enabled(async function transform_animation_on_collapsed_element() { + const iframe = document.createElement('iframe'); + document.body.appendChild(iframe); + + // Load a cross origin iframe. + const targetURL = SimpleTest.getTestFileURL("empty.html") + .replace(window.location.origin, "http://example.com/"); + iframe.src = targetURL; + await new Promise(resolve => { + iframe.onload = resolve; + }); + + await SpecialPowers.spawn(iframe, [MS_PER_SEC], async (MS_PER_SEC) => { + // Create a flex item with "preserve-3d" having an abs-pos child inside + // a grid container. + // These styles make the flex item size (0x0). + const gridContainer = content.document.createElement("div"); + gridContainer.style.display = "grid"; + gridContainer.style.placeItems = "center"; + + const target = content.document.createElement("div"); + target.style.display = "flex"; + target.style.transformStyle = "preserve-3d"; + gridContainer.appendChild(target); + + const child = content.document.createElement("div"); + child.style.position = "absolute"; + child.style.transform = "rotateY(0deg)"; + child.style.width = "100px"; + child.style.height = "100px"; + child.style.backgroundColor = "green"; + target.appendChild(child); + + content.document.body.appendChild(gridContainer); + + const animation = + target.animate({ transform: [ "rotateY(0deg)", "rotateY(360deg)" ] }, + { duration: 100 * MS_PER_SEC, + id: "test", + easing: 'step-end' }); + await content.wrappedJSObject.waitForAnimationReadyToRestyle(animation); + ok(SpecialPowers.wrap(animation).isRunningOnCompositor, + 'transform animation on a collapsed element should run on the ' + + 'compositor'); + + const restyleCount = await content.wrappedJSObject.observeStyling(5); + is(restyleCount, 0, + 'transform animation on a collapsed element animation should not ' + + 'update styles on the main thread'); + }); + + await ensureElementRemoval(iframe); + }); +}); + +</script> +</body> diff --git a/dom/animation/test/mozilla/file_transition_finish_on_compositor.html b/dom/animation/test/mozilla/file_transition_finish_on_compositor.html new file mode 100644 index 0000000000..d5e7f4ffd7 --- /dev/null +++ b/dom/animation/test/mozilla/file_transition_finish_on_compositor.html @@ -0,0 +1,67 @@ +<!doctype html> +<meta charset=utf-8> +<script src="../testcommon.js"></script> +<script src="/tests/SimpleTest/paint_listener.js"></script> +<style> +div { + /* Element needs geometry to be eligible for layerization */ + width: 100px; + height: 100px; + background-color: white; +} +</style> +<body> +<script> +'use strict'; + +function waitForPaints() { + return new Promise(function(resolve, reject) { + waitForAllPaintsFlushed(resolve); + }); +} + +promise_test(async t => { + // This test only applies to compositor animations + if (!isOMTAEnabled()) { + return; + } + + var div = addDiv(t, { style: 'transition: transform 50ms; ' + + 'transform: translateX(0px)' }); + getComputedStyle(div).transform; + + div.style.transform = 'translateX(100px)'; + + var timeBeforeStart = window.performance.now(); + await waitForPaints(); + + // If it took over 50ms to paint the transition, we have no luck + // to test it. This situation will happen if GC runs while waiting for the + // paint. + if (window.performance.now() - timeBeforeStart >= 50) { + return; + } + + var transform = + SpecialPowers.DOMWindowUtils.getOMTAStyle(div, 'transform'); + assert_not_equals(transform, '', + 'The transition style is applied on the compositor'); + + // Generate artificial busyness on the main thread for 100ms. + var timeAtStart = window.performance.now(); + while (window.performance.now() - timeAtStart < 100) {} + + // Now the transition on the compositor should finish but stay at the final + // position because there was no chance to pull the transition back from + // the compositor. + transform = + SpecialPowers.DOMWindowUtils.getOMTAStyle(div, 'transform'); + assert_equals(transform, 'matrix(1, 0, 0, 1, 100, 0)', + 'The final transition style is still applied on the ' + + 'compositor'); +}, 'Transition on the compositor keeps the final style while the main thread ' + + 'is busy even if the transition finished on the compositor'); + +done(); +</script> +</body> diff --git a/dom/animation/test/mozilla/test_cascade.html b/dom/animation/test/mozilla/test_cascade.html new file mode 100644 index 0000000000..4bdb07530e --- /dev/null +++ b/dom/animation/test/mozilla/test_cascade.html @@ -0,0 +1,37 @@ +<!doctype html> +<meta charset=utf-8> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../testcommon.js"></script> +<style> +@keyframes margin-left { + from { margin-left: 20px; } + to { margin-left: 80px; } +} +</style> +<body> +<div id="log"></div> +<script> +'use strict'; + +test(function(t) { + var div = addDiv(t, { style: 'transition: margin-left 100s; ' + + 'margin-left: 80px' }); + var cs = getComputedStyle(div); + + assert_equals(cs.marginLeft, '80px', 'initial margin-left'); + + div.style.marginLeft = "20px"; + assert_equals(cs.marginLeft, '80px', 'margin-left transition at 0s'); + + div.style.animation = "margin-left 2s"; + assert_equals(cs.marginLeft, '20px', + 'margin-left animation overrides transition at 0s'); + + div.style.animation = "none"; + assert_equals(cs.marginLeft, '80px', + 'margin-left animation stops overriding transition at 0s'); +}, 'Animation overrides/stops overriding transition immediately'); + +</script> +</body> diff --git a/dom/animation/test/mozilla/test_cubic_bezier_limits.html b/dom/animation/test/mozilla/test_cubic_bezier_limits.html new file mode 100644 index 0000000000..bdbc78654f --- /dev/null +++ b/dom/animation/test/mozilla/test_cubic_bezier_limits.html @@ -0,0 +1,168 @@ +<!doctype html> +<meta charset=utf-8> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../testcommon.js"></script> +<body> +<style> +@keyframes anim { + to { margin-left: 100px; } +} + +.transition-div { + margin-left: 100px; +} +</style> +<div id="log"></div> +<script> +'use strict'; + +// We clamp +infinity or -inifinity value in floating point to +// maximum floating point value or -maxinum floating point value. +const max_float = '3.40282e38'; + +test(function(t) { + var div = addDiv(t); + var anim = div.animate({ }, 100 * MS_PER_SEC); + + anim.effect.updateTiming({ easing: 'cubic-bezier(0, 1e+39, 0, 0)' }); + assert_equals(anim.effect.getComputedTiming().easing, + 'cubic-bezier(0, ' + max_float + ', 0, 0)', + 'y1 control point for effect easing is out of upper boundary'); + + anim.effect.updateTiming({ easing: 'cubic-bezier(0, 0, 0, 1e+39)' }); + assert_equals(anim.effect.getComputedTiming().easing, + 'cubic-bezier(0, 0, 0, ' + max_float + ')', + 'y2 control point for effect easing is out of upper boundary'); + + anim.effect.updateTiming({ easing: 'cubic-bezier(0, -1e+39, 0, 0)' }); + assert_equals(anim.effect.getComputedTiming().easing, + 'cubic-bezier(0, ' + '-' + max_float + ', 0, 0)', + 'y1 control point for effect easing is out of lower boundary'); + + anim.effect.updateTiming({ easing: 'cubic-bezier(0, 0, 0, -1e+39)' }); + assert_equals(anim.effect.getComputedTiming().easing, + 'cubic-bezier(0, 0, 0, ' + '-' + max_float + ')', + 'y2 control point for effect easing is out of lower boundary'); + +}, 'Clamp y1 and y2 control point out of boundaries for effect easing' ); + +test(function(t) { + var div = addDiv(t); + var anim = div.animate({ }, 100 * MS_PER_SEC); + + anim.effect.setKeyframes([ { easing: 'cubic-bezier(0, 1e+39, 0, 0)' }]); + assert_equals(anim.effect.getKeyframes()[0].easing, + 'cubic-bezier(0, ' + max_float + ', 0, 0)', + 'y1 control point for keyframe easing is out of upper boundary'); + + anim.effect.setKeyframes([ { easing: 'cubic-bezier(0, 0, 0, 1e+39)' }]); + assert_equals(anim.effect.getKeyframes()[0].easing, + 'cubic-bezier(0, 0, 0, ' + max_float + ')', + 'y2 control point for keyframe easing is out of upper boundary'); + + anim.effect.setKeyframes([ { easing: 'cubic-bezier(0, -1e+39, 0, 0)' }]); + assert_equals(anim.effect.getKeyframes()[0].easing, + 'cubic-bezier(0, ' + '-' + max_float + ', 0, 0)', + 'y1 control point for keyframe easing is out of lower boundary'); + + anim.effect.setKeyframes([ { easing: 'cubic-bezier(0, 0, 0, -1e+39)' }]); + assert_equals(anim.effect.getKeyframes()[0].easing, + 'cubic-bezier(0, 0, 0, ' + '-' + max_float + ')', + 'y2 control point for keyframe easing is out of lower boundary'); + +}, 'Clamp y1 and y2 control point out of boundaries for keyframe easing' ); + +test(function(t) { + var div = addDiv(t); + + div.style.animation = 'anim 100s cubic-bezier(0, 1e+39, 0, 0)'; + + assert_equals(div.getAnimations()[0].effect.getKeyframes()[0].easing, + 'cubic-bezier(0, ' + max_float + ', 0, 0)', + 'y1 control point for CSS animation is out of upper boundary'); + + div.style.animation = 'anim 100s cubic-bezier(0, 0, 0, 1e+39)'; + assert_equals(div.getAnimations()[0].effect.getKeyframes()[0].easing, + 'cubic-bezier(0, 0, 0, ' + max_float + ')', + 'y2 control point for CSS animation is out of upper boundary'); + + div.style.animation = 'anim 100s cubic-bezier(0, -1e+39, 0, 0)'; + assert_equals(div.getAnimations()[0].effect.getKeyframes()[0].easing, + 'cubic-bezier(0, ' + '-' + max_float + ', 0, 0)', + 'y1 control point for CSS animation is out of lower boundary'); + + div.style.animation = 'anim 100s cubic-bezier(0, 0, 0, -1e+39)'; + assert_equals(div.getAnimations()[0].effect.getKeyframes()[0].easing, + 'cubic-bezier(0, 0, 0, ' + '-' + max_float + ')', + 'y2 control point for CSS animation is out of lower boundary'); + +}, 'Clamp y1 and y2 control point out of boundaries for CSS animation' ); + +test(function(t) { + var div = addDiv(t, {'class': 'transition-div'}); + + div.style.transition = 'margin-left 100s cubic-bezier(0, 1e+39, 0, 0)'; + flushComputedStyle(div); + div.style.marginLeft = '0px'; + assert_equals(div.getAnimations()[0].effect.getTiming().easing, + 'cubic-bezier(0, ' + max_float + ', 0, 0)', + 'y1 control point for CSS transition on upper boundary'); + div.style.transition = ''; + div.style.marginLeft = ''; + + div.style.transition = 'margin-left 100s cubic-bezier(0, 0, 0, 1e+39)'; + flushComputedStyle(div); + div.style.marginLeft = '0px'; + assert_equals(div.getAnimations()[0].effect.getTiming().easing, + 'cubic-bezier(0, 0, 0, ' + max_float + ')', + 'y2 control point for CSS transition on upper boundary'); + div.style.transition = ''; + div.style.marginLeft = ''; + + div.style.transition = 'margin-left 100s cubic-bezier(0, -1e+39, 0, 0)'; + flushComputedStyle(div); + div.style.marginLeft = '0px'; + assert_equals(div.getAnimations()[0].effect.getTiming().easing, + 'cubic-bezier(0, ' + '-' + max_float + ', 0, 0)', + 'y1 control point for CSS transition on lower boundary'); + div.style.transition = ''; + div.style.marginLeft = ''; + + div.style.transition = 'margin-left 100s cubic-bezier(0, 0, 0, -1e+39)'; + flushComputedStyle(div); + div.style.marginLeft = '0px'; + assert_equals(div.getAnimations()[0].effect.getTiming().easing, + 'cubic-bezier(0, 0, 0, ' + '-' + max_float + ')', + 'y2 control point for CSS transition on lower boundary'); + +}, 'Clamp y1 and y2 control point out of boundaries for CSS transition' ); + +test(function(t) { + var div = addDiv(t); + var anim = div.animate({ }, { duration: 100 * MS_PER_SEC, fill: 'forwards' }); + + anim.pause(); + // The positive steepest function on both edges. + anim.effect.updateTiming({ easing: 'cubic-bezier(0, 1e+39, 0, 1e+39)' }); + assert_equals(anim.effect.getComputedTiming().progress, 0.0, + 'progress on lower edge for the highest value of y1 and y2 control points'); + + anim.finish(); + assert_equals(anim.effect.getComputedTiming().progress, 1.0, + 'progress on upper edge for the highest value of y1 and y2 control points'); + + // The negative steepest function on both edges. + anim.effect.updateTiming({ easing: 'cubic-bezier(0, -1e+39, 0, -1e+39)' }); + anim.currentTime = 0; + assert_equals(anim.effect.getComputedTiming().progress, 0.0, + 'progress on lower edge for the lowest value of y1 and y2 control points'); + + anim.finish(); + assert_equals(anim.effect.getComputedTiming().progress, 1.0, + 'progress on lower edge for the lowest value of y1 and y2 control points'); + +}, 'Calculated values on both edges'); + +</script> +</body> diff --git a/dom/animation/test/mozilla/test_deferred_start.html b/dom/animation/test/mozilla/test_deferred_start.html new file mode 100644 index 0000000000..8b3d293f02 --- /dev/null +++ b/dom/animation/test/mozilla/test_deferred_start.html @@ -0,0 +1,19 @@ +<!doctype html> +<meta charset=utf-8> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<div id="log"></div> +<script> +'use strict'; +setup({explicit_done: true}); +SpecialPowers.pushPrefEnv( + { + set: [ + ["dom.animations-api.timelines.enabled", true], + ], + }, + function() { + window.open("file_deferred_start.html"); + } +); +</script> diff --git a/dom/animation/test/mozilla/test_disable_animations_api_compositing.html b/dom/animation/test/mozilla/test_disable_animations_api_compositing.html new file mode 100644 index 0000000000..94216ea62d --- /dev/null +++ b/dom/animation/test/mozilla/test_disable_animations_api_compositing.html @@ -0,0 +1,14 @@ +<!doctype html> +<meta charset=utf-8> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<div id="log"></div> +<script> +'use strict'; +setup({explicit_done: true}); +SpecialPowers.pushPrefEnv( + { "set": [["dom.animations-api.compositing.enabled", false]]}, + function() { + window.open("file_disable_animations_api_compositing.html"); + }); +</script> diff --git a/dom/animation/test/mozilla/test_disable_animations_api_timelines.html b/dom/animation/test/mozilla/test_disable_animations_api_timelines.html new file mode 100644 index 0000000000..a20adf4ea2 --- /dev/null +++ b/dom/animation/test/mozilla/test_disable_animations_api_timelines.html @@ -0,0 +1,16 @@ +<!doctype html> +<meta charset=utf-8> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<div id="log"></div> +<script> +'use strict'; + +setup({ explicit_done: true }); +SpecialPowers.pushPrefEnv( + { set: [['dom.animations-api.timelines.enabled', false]] }, + function() { + window.open('file_disable_animations_api_timelines.html'); + } +); +</script> diff --git a/dom/animation/test/mozilla/test_disabled_properties.html b/dom/animation/test/mozilla/test_disabled_properties.html new file mode 100644 index 0000000000..2244143ceb --- /dev/null +++ b/dom/animation/test/mozilla/test_disabled_properties.html @@ -0,0 +1,73 @@ +<!doctype html> +<meta charset=utf-8> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../testcommon.js"></script> +<body> +<div id="log"></div> +<script> +'use strict'; + +function waitForSetPref(pref, value) { + return SpecialPowers.pushPrefEnv({ 'set': [[pref, value]] }); +} + +/* + * These tests rely on the fact that the overflow-clip-box property is + * disabled by the layout.css.overflow-clip-box.enabled pref. If we ever remove + * that pref we will need to substitute some other pref:property combination. + */ + +promise_test(function(t) { + return waitForSetPref('layout.css.overflow-clip-box.enabled', true).then(() => { + var anim = addDiv(t).animate({ overflowClipBoxBlock: [ 'padding-box', 'content-box' ]}); + assert_equals(anim.effect.getKeyframes().length, 2, + 'A property-indexed keyframe specifying only enabled' + + ' properties produces keyframes'); + return waitForSetPref('layout.css.overflow-clip-box.enabled', false); + }).then(() => { + var anim = addDiv(t).animate({ overflowClipBoxBlock: [ 'padding-box', 'content-box' ]}); + assert_equals(anim.effect.getKeyframes().length, 0, + 'A property-indexed keyframe specifying only disabled' + + ' properties produces no keyframes'); + }); +}, 'Specifying a disabled property using a property-indexed keyframe'); + +promise_test(function(t) { + var createAnim = () => { + var anim = addDiv(t).animate([ { overflowClipBoxBlock: 'padding-box' }, + { overflowClipBoxBlock: 'content-box' } ]); + assert_equals(anim.effect.getKeyframes().length, 2, + 'Animation specified using a keyframe sequence should' + + ' return the same number of keyframes regardless of' + + ' whether or not the specified properties are disabled'); + return anim; + }; + + var assert_has_property = (anim, index, descr, property) => { + assert_true( + anim.effect.getKeyframes()[index].hasOwnProperty(property), + `${descr} should have the '${property}' property`); + }; + var assert_does_not_have_property = (anim, index, descr, property) => { + assert_false( + anim.effect.getKeyframes()[index].hasOwnProperty(property), + `${descr} should NOT have the '${property}' property`); + }; + + return waitForSetPref('layout.css.overflow-clip-box.enabled', true).then(() => { + var anim = createAnim(); + assert_has_property(anim, 0, 'Initial keyframe', 'overflowClipBoxBlock'); + assert_has_property(anim, 1, 'Final keyframe', 'overflowClipBoxBlock'); + return waitForSetPref('layout.css.overflow-clip-box.enabled', false); + }).then(() => { + var anim = createAnim(); + assert_does_not_have_property(anim, 0, 'Initial keyframe', + 'overflowClipBoxBlock'); + assert_does_not_have_property(anim, 1, 'Final keyframe', + 'overflowClipBoxBlock'); + }); +}, 'Specifying a disabled property using a keyframe sequence'); + +</script> +</body> diff --git a/dom/animation/test/mozilla/test_discrete_animations.html b/dom/animation/test/mozilla/test_discrete_animations.html new file mode 100644 index 0000000000..d4826a74bd --- /dev/null +++ b/dom/animation/test/mozilla/test_discrete_animations.html @@ -0,0 +1,16 @@ +<!doctype html> +<meta charset=utf-8> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<div id="log"></div> +<script> +'use strict'; +setup({explicit_done: true}); +SpecialPowers.pushPrefEnv( + { "set": [ + ["layout.css.osx-font-smoothing.enabled", true], + ] }, + function() { + window.open("file_discrete_animations.html"); + }); +</script> diff --git a/dom/animation/test/mozilla/test_distance_of_basic_shape.html b/dom/animation/test/mozilla/test_distance_of_basic_shape.html new file mode 100644 index 0000000000..65e403bf06 --- /dev/null +++ b/dom/animation/test/mozilla/test_distance_of_basic_shape.html @@ -0,0 +1,91 @@ +<!doctype html> +<meta charset=utf-8> +<script src='/resources/testharness.js'></script> +<script src='/resources/testharnessreport.js'></script> +<script src='../testcommon.js'></script> +<div id='log'></div> +<script type='text/javascript'> +'use strict'; + +// We don't have an official spec to define the distance between two basic +// shapes, but we still need this for DevTools, so Gecko and Servo backends use +// the similar rules to define the distance. If there is a spec for it, we have +// to update this test file. +// See https://github.com/w3c/csswg-drafts/issues/662. + +test(function(t) { + var target = addDiv(t); + var dist = getDistance(target, 'clip-path', 'none', 'none'); + assert_equals(dist, 0, 'none and none'); +}, 'none and none'); + +test(function(t) { + var target = addDiv(t); + var dist = getDistance(target, 'clip-path', 'circle(10px)', 'circle(20px)'); + assert_equals(dist, 10, 'circle(10px) and circle(20px)'); +}, 'circles'); + +test(function(t) { + var target = addDiv(t); + var circle1 = 'circle(calc(10px + 10px) at 20px 10px)'; + var circle2 = 'circle(30px at 10px 10px)'; + var dist = getDistance(target, 'clip-path', circle1, circle2); + assert_equals(dist, + Math.sqrt(10 * 10 + 10 * 10), + circle1 + ' and ' + circle2); +}, 'circles with positions'); + +test(function(t) { + var target = addDiv(t); + var ellipse1 = 'ellipse(20px calc(10px + 10px))'; + var ellipse2 = 'ellipse(30px 30px)'; + var dist = getDistance(target, 'clip-path', ellipse1, ellipse2); + assert_equals(dist, + Math.sqrt(10 * 10 + 10 * 10), + ellipse1 + ' and ' + ellipse2); +}, 'ellipses'); + +test(function(t) { + var target = addDiv(t); + var polygon1 = 'polygon(50px 0px, 100px 50px, 50px 100px, 0px 50px)'; + var polygon2 = 'polygon(40px 0px, 100px 70px, 10px 100px, 0px 70px)'; + var dist = getDistance(target, 'clip-path', polygon1, polygon2); + assert_equals(dist, + Math.sqrt(10 * 10 + 20 * 20 + 40 * 40 + 20 * 20), + polygon1 + ' and ' + polygon2); +}, 'polygons'); + +test(function(t) { + var target = addDiv(t); + var inset1 = 'inset(5px 5px 5px 5px round 40px 30px 20px 5px)'; + var inset2 = 'inset(10px 5px round 50px 60px)'; + var dist = getDistance(target, 'clip-path', inset1, inset2); + + // if we have only two parameter in inset(), the first one means + // top and bottom edges, and the second one means left and right edges. + // and the definitions of inset is inset(top, right, bottom, left). Besides, + // the "round" part uses the shorthand of border radius for each corner, so + // each corner is a pair of (x, y). We are computing the distance between: + // 1. inset(5px 5px 5px 5px + // round (40px 40px) (30px 30px) (20px 20px) (5px 5px)) + // 2. inset(10px 5px 10px 5px + // round (50px 50px) (60px 60px) (50px 50px) (60px 60px)) + // That is why we need to multiply 2 for each border-radius corner. + assert_equals(dist, + Math.sqrt(5 * 5 + 5 * 5 + + (50 - 40) * (50 - 40) * 2 + + (60 - 30) * (60 - 30) * 2 + + (50 - 20) * (50 - 20) * 2 + + (60 - 5) * (60 - 5) * 2), + inset1 + ' and ' + inset2); +}, 'insets'); + +test(function(t) { + var target = addDiv(t); + var dist = getDistance(target, 'clip-path', + 'circle(20px)', 'ellipse(10px 20px)'); + assert_equals(dist, 0, 'circle(20px) and ellipse(10px 20px)'); +}, 'Mismatched basic shapes'); + +</script> +</html> diff --git a/dom/animation/test/mozilla/test_distance_of_filter.html b/dom/animation/test/mozilla/test_distance_of_filter.html new file mode 100644 index 0000000000..33f772d983 --- /dev/null +++ b/dom/animation/test/mozilla/test_distance_of_filter.html @@ -0,0 +1,248 @@ +<!doctype html> +<meta charset=utf-8> +<script src='/resources/testharness.js'></script> +<script src='/resources/testharnessreport.js'></script> +<script src='../testcommon.js'></script> +<div id='log'></div> +<script type='text/javascript'> +'use strict'; + +const EPSILON = 1e-6; + +// We don't have an official spec to define the distance between two filter +// lists, but we still need this for DevTools, so Gecko and Servo backends use +// the similar rules to define the distance. If there is a spec for it, we have +// to update this test file. +// See https://github.com/w3c/fxtf-drafts/issues/91. + +test(function(t) { + var target = addDiv(t); + var dist = getDistance(target, 'filter', 'none', 'none'); + assert_equals(dist, 0, 'none and none'); +}, 'none and none'); + +test(function(t) { + var target = addDiv(t); + var dist = getDistance(target, 'filter', 'blur(10px)', 'none'); + // The default value of blur is 0px. + assert_equals(dist, 10, 'blur(10px) and none'); +}, 'blur and none'); + +test(function(t) { + var target = addDiv(t); + var dist = getDistance(target, 'filter', 'blur(10px)', 'blur(1px)'); + assert_equals(dist, 9, 'blur(10px) and blur(1px)'); +}, 'blurs'); + +test(function(t) { + var target = addDiv(t); + var dist = getDistance(target, 'filter', 'brightness(75%)', 'none'); + // The default value of brightness is 100%. + assert_equals(dist, (1 - 0.75), 'brightness(75%) and none'); +}, 'brightness and none'); + +test(function(t) { + var target = addDiv(t); + var dist = getDistance(target, 'filter', + 'brightness(50%)', 'brightness(175%)'); + assert_equals(dist, (1.75 - 0.5), 'brightness(50%) and brightness(175%)'); +}, 'brightnesses'); + +test(function(t) { + var target = addDiv(t); + var dist = getDistance(target, 'filter', 'contrast(75%)', 'none'); + // The default value of contrast is 100%. + assert_equals(dist, (1 - 0.75), 'contrast(75%) and none'); +}, 'contrast and none'); + +test(function(t) { + var target = addDiv(t); + var dist = getDistance(target, 'filter', 'contrast(50%)', 'contrast(175%)'); + assert_equals(dist, (1.75 - 0.5), 'contrast(50%) and contrast(175%)'); +}, 'contrasts'); + +test(function(t) { + var target = addDiv(t); + var filter1 = 'drop-shadow(10px 10px 10px blue)'; + var filter2 = 'none'; + var dist = getDistance(target, 'filter', filter1, filter2); + // The rgba of Blue is rgba(0, 0, 255, 1.0) = rgba(0%, 0%, 100%, 100%). + // So we are try to compute the distance of + // 1. drop-shadow(10, 10, 10, rgba(0, 0, 1.0, 1.0)). + // 2. drop-shadow( 0, 0, 0, rgba(0, 0, 0, 0)). + assert_equals(dist, + Math.sqrt(10 * 10 * 3 + (1 * 1 + 1 * 1)), + filter1 + ' and ' + filter2); +}, 'drop-shadow and none'); + +test(function(t) { + var target = addDiv(t); + var filter1 = 'drop-shadow(10px 10px 10px blue)'; + var filter2 = 'drop-shadow(5px 5px 1px yellow)'; + var dist = getDistance(target, 'filter', filter1, filter2); + // Blue: rgba(0, 0, 255, 1.0) = rgba( 0%, 0%, 100%, 100%). + // Yellow: rgba(255, 255, 0, 1.0) = rgba(100%, 100%, 0%, 100%). + assert_equals(dist, + Math.sqrt(5 * 5 * 2 + 9 * 9 + (1 * 1 * 3)), + filter1 + ' and ' + filter2); +}, 'drop-shadows'); + +test(function(t) { + var target = addDiv(t); + var filter1 = 'drop-shadow(10px 10px 10px)'; + var filter2 = 'drop-shadow(5px 5px 1px yellow)'; + var dist = getDistance(target, 'filter', filter1, filter2); + // Yellow: rgba(255, 255, 0, 1.0) = rgba(100%, 100%, 0%, 100%) + // Transparent: rgba(0, 0, 0, 0) = rgba( 0%, 0%, 0%, 0%) + // Distance involving `currentcolor` is calculated as distance + // from `transparent` + assert_equals(dist, + Math.sqrt(5 * 5 * 2 + 9 * 9 + (1 * 1 * 3)), + filter1 + ' and ' + filter2); +}, 'drop-shadows with color and non-color'); + +test(function(t) { + var target = addDiv(t); + var dist = getDistance(target, 'filter', 'grayscale(25%)', 'none'); + // The default value of grayscale is 0%. + assert_equals(dist, 0.25, 'grayscale(25%) and none'); +}, 'grayscale and none'); + +test(function(t) { + var target = addDiv(t); + var dist = getDistance(target, 'filter', 'grayscale(50%)', 'grayscale(75%)'); + assert_equals(dist, 0.25, 'grayscale(50%) and grayscale(75%)'); +}, 'grayscales'); + +test(function(t) { + var target = addDiv(t); + var dist = getDistance(target, 'filter', 'grayscale(75%)', 'grayscale(175%)'); + assert_equals(dist, 0.25, 'distance of grayscale(75%) and grayscale(175%)'); +}, 'grayscales where one has a value larger than 1.0'); + +test(function(t) { + var target = addDiv(t); + var dist = getDistance(target, 'filter', 'hue-rotate(180deg)', 'none'); + // The default value of hue-rotate is 0deg. + assert_approx_equals(dist, Math.PI, EPSILON, 'hue-rotate(180deg) and none'); +}, 'hue-rotate and none'); + +test(function(t) { + var target = addDiv(t); + var dist = getDistance(target, 'filter', + 'hue-rotate(720deg)', 'hue-rotate(-180deg)'); + assert_approx_equals(dist, 5 * Math.PI, EPSILON, + 'hue-rotate(720deg) and hue-rotate(-180deg)'); +}, 'hue-rotates'); + +test(function(t) { + var target = addDiv(t); + var dist = getDistance(target, 'filter', 'invert(25%)', 'none'); + // The default value of invert is 0%. + assert_equals(dist, 0.25, 'invert(25%) and none'); +}, 'invert and none'); + +test(function(t) { + var target = addDiv(t); + var dist = getDistance(target, 'filter', 'invert(50%)', 'invert(75%)'); + assert_equals(dist, 0.25, 'invert(50%) and invert(75%)'); +}, 'inverts'); + +test(function(t) { + var target = addDiv(t); + var dist = getDistance(target, 'filter', 'invert(75%)', 'invert(175%)'); + assert_equals(dist, 0.25, 'invert(75%) and invert(175%)'); +}, 'inverts where one has a value larger than 1.0'); + +test(function(t) { + var target = addDiv(t); + var dist = getDistance(target, 'filter', 'opacity(75%)', 'none'); + // The default value of opacity is 100%. + assert_equals(dist, (1 - 0.75), 'opacity(75%) and none'); +}, 'opacity and none'); + +test(function(t) { + var target = addDiv(t); + var dist = getDistance(target, 'filter', 'opacity(50%)', 'opacity(75%)'); + assert_equals(dist, 0.25, 'opacity(50%) and opacity(75%)'); +}, 'opacities'); + +test(function(t) { + var target = addDiv(t); + var dist = getDistance(target, 'filter', 'opacity(75%)', 'opacity(175%)'); + assert_equals(dist, 0.25, 'opacity(75%) and opacity(175%)'); +}, 'opacities where one has a value larger than 1.0'); + +test(function(t) { + var target = addDiv(t); + var dist = getDistance(target, 'filter', 'saturate(75%)', 'none'); + // The default value of saturate is 100%. + assert_equals(dist, (1 - 0.75), 'saturate(75%) and none'); +}, 'saturate and none'); + +test(function(t) { + var target = addDiv(t); + var dist = getDistance(target, 'filter', 'saturate(50%)', 'saturate(175%)'); + assert_equals(dist, (1.75 - 0.5), 'saturate(50%) and saturate(175%)'); +}, 'saturates'); + +test(function(t) { + var target = addDiv(t); + var dist = getDistance(target, 'filter', 'sepia(25%)', 'none'); + // The default value of sepia is 0%. + assert_equals(dist, 0.25, 'sepia(25%) and none'); +}, 'sepia and none'); + +test(function(t) { + var target = addDiv(t); + var dist = getDistance(target, 'filter', 'sepia(50%)', 'sepia(75%)'); + assert_equals(dist, 0.25, 'sepia(50%) and sepia(75%)'); +}, 'sepias'); + +test(function(t) { + var target = addDiv(t); + var dist = getDistance(target, 'filter', 'sepia(75%)', 'sepia(175%)'); + assert_equals(dist, 0.25, 'sepia(75%) and sepia(175%)'); +}, 'sepias where one has a value larger than 1.0'); + +test(function(t) { + var target = addDiv(t); + var filter1 = 'grayscale(50%) opacity(100%) blur(5px)'; + // none filter: 'grayscale(0) opacity(1) blur(0px)' + var filter2 = 'none'; + var dist = getDistance(target, 'filter', filter1, filter2); + assert_equals(dist, + Math.sqrt(0.5 * 0.5 + 5 * 5), + filter1 + ' and ' + filter2); +}, 'Filter list and none'); + +test(function(t) { + var target = addDiv(t); + var filter1 = 'grayscale(50%) opacity(100%) blur(5px)'; + var filter2 = 'grayscale(100%) opacity(50%) blur(1px)'; + var dist = getDistance(target, 'filter', filter1, filter2); + assert_equals(dist, + Math.sqrt(0.5 * 0.5 + 0.5 * 0.5 + 4 * 4), + filter1 + ' and ' + filter2); +}, 'Filter lists'); + +test(function(t) { + var target = addDiv(t); + var filter1 = 'grayscale(50%) opacity(100%) blur(5px)'; + var filter2 = 'grayscale(100%) opacity(50%) blur(1px) sepia(50%)'; + var dist = getDistance(target, 'filter', filter1, filter2); + assert_equals(dist, + Math.sqrt(0.5 * 0.5 + 0.5 * 0.5 + 4 * 4 + 0.5 * 0.5), + filter1 + ' and ' + filter2); +}, 'Filter lists where one has extra functions'); + +test(function(t) { + var target = addDiv(t); + var filter1 = 'grayscale(50%) opacity(100%)'; + var filter2 = 'opacity(100%) grayscale(50%)'; + var dist = getDistance(target, 'filter', filter1, filter2); + assert_equals(dist, 0, filter1 + ' and ' + filter2); +}, 'Mismatched filter lists'); + +</script> +</html> diff --git a/dom/animation/test/mozilla/test_distance_of_path_function.html b/dom/animation/test/mozilla/test_distance_of_path_function.html new file mode 100644 index 0000000000..af6592c892 --- /dev/null +++ b/dom/animation/test/mozilla/test_distance_of_path_function.html @@ -0,0 +1,140 @@ +<!doctype html> +<meta charset=utf-8> +<script src='/resources/testharness.js'></script> +<script src='/resources/testharnessreport.js'></script> +<script src='../testcommon.js'></script> +<div id="log"></div> +<script type='text/javascript'> +'use strict'; + +test(function(t) { + var target = addDiv(t); + var dist = getDistance(target, 'offset-path', 'none', 'none'); + assert_equals(dist, 0, 'none and none'); +}, 'none and none'); + +test(function(t) { + var target = addDiv(t); + var dist = getDistance(target, 'offset-path', 'path("M 10 10")', 'none'); + assert_equals(dist, 0, 'path("M 10 10") and none'); +}, 'Path and none'); + +test(function(t) { + var target = addDiv(t); + var dist = getDistance(target, 'offset-path', + 'path("M 10 10 H 10")', + 'path("M 10 10 H 10 H 10")'); + assert_equals(dist, 0, 'path("M 10 10 H 10") and ' + + 'path("M 10 10 H 10 H 10")'); +}, 'Mismatched path functions'); + +test(function(t) { + var target = addDiv(t); + var dist = getDistance(target, 'offset-path', + 'path("M 10 10")', + 'path("M 20 20")'); + assert_equals(dist, + Math.sqrt(10 * 10 * 2), + 'path("M 10 10") and path("M 30 30")'); +}, 'The moveto commands'); + +test(function(t) { + var target = addDiv(t); + var dist = getDistance(target, 'offset-path', + 'path("M 0 0 L 10 10")', + 'path("M 0 0 L 20 20")'); + assert_equals(dist, + Math.sqrt(10 * 10 * 2), + 'path("M 0 0 L 10 10") and path("M 0 0 L 20 20")'); +}, 'The lineto commands'); + +test(function(t) { + var target = addDiv(t); + var dist = getDistance(target, 'offset-path', + 'path("M 0 0 H 10")', + 'path("M 0 0 H 20")'); + assert_equals(dist, 10, 'path("M 0 0 H 10") and path("M 0 0 H 20")'); +}, 'The horizontal lineto commands'); + +test(function(t) { + var target = addDiv(t); + var dist = getDistance(target, 'offset-path', + 'path("M 0 0 V 10")', + 'path("M 0 0 V 20")'); + assert_equals(dist, 10, 'path("M 0 0 V 10") and path("M 0 0 V 20")'); +}, 'The vertical lineto commands'); + +test(function(t) { + var target = addDiv(t); + var dist = getDistance(target, 'offset-path', + 'path("M 0 0 C 10 10 20 20 30 30")', + 'path("M 0 0 C 20 20 40 40 0 0")'); + assert_equals(dist, + Math.sqrt(10 * 10 * 2 + 20 * 20 * 2 + 30 * 30 * 2), + 'path("M 0 0 C 10 10 20 20 30 30") and ' + + 'path("M 0 0 C 20 20 40 40 0 0")'); +}, 'The cubic Bézier curve commands'); + +test(function(t) { + var target = addDiv(t); + var dist = getDistance(target, 'offset-path', + 'path("M 0 0 S 20 20 30 30")', + 'path("M 0 0 S 40 40 0 0")'); + assert_equals(dist, + Math.sqrt(20 * 20 * 2 + 30 * 30 * 2), + 'path("M 0 0 S 20 20 30 30") and ' + + 'path("M 0 0 S 40 40 0 0")'); +}, 'The smooth cubic Bézier curve commands'); + +test(function(t) { + var target = addDiv(t); + var dist = getDistance(target, 'offset-path', + 'path("M 0 0 Q 10 10 30 30")', + 'path("M 0 0 Q 20 20 0 0")'); + assert_equals(dist, + Math.sqrt(10 * 10 * 2 + 30 * 30 * 2), + 'path("M 0 0 Q 10 10 30 30") and ' + + 'path("M 0 0 Q 20 20 0 0")'); +}, 'The quadratic cubic Bézier curve commands'); + +test(function(t) { + var target = addDiv(t); + var dist = getDistance(target, 'offset-path', + 'path("M 0 0 T 30 30")', + 'path("M 0 0 T 0 0")'); + assert_equals(dist, + Math.sqrt(30 * 30 * 2), + 'path("M 0 0 T 30 30") and ' + + 'path("M 0 0 T 0 0")'); +}, 'The smooth quadratic cubic Bézier curve commands'); + +test(function(t) { + var target = addDiv(t); + var dist = getDistance(target, 'offset-path', + 'path("M 0 0 A 5 5 10 0 1 30 30")', + 'path("M 0 0 A 4 4 5 0 0 20 20")'); + assert_equals(dist, + Math.sqrt(1 * 1 * 2 + // radii + 5 * 5 + // angle + 1 * 1 + // flag + 10 * 10 * 2), + 'path("M 0 0 A 5 5 10 0 1 30 30") and ' + + 'path("M 0 0 A 4 4 5 0 0 20 20")'); +}, 'The elliptical arc curve commands'); + +test(function(t) { + var target = addDiv(t); + var dist = getDistance(target, 'offset-path', + 'path("m 10 20 h 30 v 60 h 10 v -10 l 110 60")', + // == 'path("M 10 20 H 40 V 80 H 50 V 70 L 160 130")' + 'path("M 130 140 H 120 V 160 H 130 V 150 L 200 170")'); + assert_equals(dist, + Math.sqrt(120 * 120 * 2 + + 80 * 80 * 4 + + 40 * 40 * 2), + 'path("m 10 20 h 30 v 60 h 10 v -10 l 110 60") and ' + + 'path("M 130 140 H 120 V 160 H 130 V 150 L 200 170")'); +}, 'The distance of paths with absolute and relative coordinates'); + +</script> +</html> diff --git a/dom/animation/test/mozilla/test_distance_of_transform.html b/dom/animation/test/mozilla/test_distance_of_transform.html new file mode 100644 index 0000000000..96ff1eb66d --- /dev/null +++ b/dom/animation/test/mozilla/test_distance_of_transform.html @@ -0,0 +1,404 @@ +<!doctype html> +<meta charset=utf-8> +<script src='/resources/testharness.js'></script> +<script src='/resources/testharnessreport.js'></script> +<script src='../testcommon.js'></script> +<div id='log'></div> +<script type='text/javascript'> +'use strict'; + +// We don't have an official spec to define the distance between two transform +// lists, but we still need this for DevTools, so Gecko and Servo backend use +// the similar rules to define the distance. If there is a spec for it, we have +// to update this test file. + +const EPSILON = 0.00001; + +// |v| should be a unit vector (i.e. having length 1) +function getQuaternion(v, angle) { + return [ + v[0] * Math.sin(angle / 2.0), + v[1] * Math.sin(angle / 2.0), + v[2] * Math.sin(angle / 2.0), + Math.cos(angle / 2.0) + ]; +} + +function computeRotateDistance(q1, q2) { + const dot = q1.reduce((sum, e, i) => sum + e * q2[i], 0); + return Math.acos(Math.min(Math.max(dot, -1.0), 1.0)) * 2.0; +} + +function createMatrixFromArray(array) { + return (array.length === 16 ? 'matrix3d' : 'matrix') + `(${array.join()})`; +} + +function rotate3dToMatrix(x, y, z, radian) { + var sc = Math.sin(radian / 2) * Math.cos(radian / 2); + var sq = Math.sin(radian / 2) * Math.sin(radian / 2); + + // Normalize the vector. + var length = Math.sqrt(x*x + y*y + z*z); + x /= length; + y /= length; + z /= length; + + return [ + 1 - 2 * (y*y + z*z) * sq, + 2 * (x * y * sq + z * sc), + 2 * (x * z * sq - y * sc), + 0, + 2 * (x * y * sq - z * sc), + 1 - 2 * (x*x + z*z) * sq, + 2 * (y * z * sq + x * sc), + 0, + 2 * (x * z * sq + y * sc), + 2 * (y * z * sq - x * sc), + 1 - 2 * (x*x + y*y) * sq, + 0, + 0, + 0, + 0, + 1 + ]; +} + +test(function(t) { + var target = addDiv(t); + var dist = getDistance(target, 'transform', 'none', 'none'); + assert_equals(dist, 0, 'distance of translate'); +}, 'Test distance of none and none'); + +test(function(t) { + var target = addDiv(t); + var dist = getDistance(target, 'transform', 'translate(100px)', 'none'); + assert_equals(dist, 100, 'distance of translate'); +}, 'Test distance of translate function and none'); + +test(function(t) { + var target = addDiv(t); + var dist = + getDistance(target, 'transform', 'translate(100px)', 'translate(200px)'); + assert_equals(dist, 200 - 100, 'distance of translate'); +}, 'Test distance of translate functions'); + +test(function(t) { + var target = addDiv(t); + var dist = + getDistance(target, 'transform', 'translate3d(100px, 0, 50px)', 'none'); + assert_equals(dist, Math.sqrt(100 * 100 + 50 * 50), + 'distance of translate3d'); +}, 'Test distance of translate3d function and none'); + +test(function(t) { + var target = addDiv(t); + var dist = + getDistance(target, 'transform', + 'translate3d(100px, 0, 50px)', + 'translate3d(200px, 80px, 0)'); + assert_equals(dist, Math.sqrt(100 * 100 + 80 * 80 + 50 * 50), + 'distance of translate'); +}, 'Test distance of translate3d functions'); + +test(function(t) { + var target = addDiv(t); + var dist = getDistance(target, 'transform', 'scale(1.5)', 'none'); + assert_equals(dist, Math.sqrt(0.5 * 0.5 + 0.5 * 0.5), 'distance of scale'); +}, 'Test distance of scale function and none'); + +test(function(t) { + var target = addDiv(t); + var dist = getDistance(target, 'transform', 'scale(1.5)', 'scale(2.0)'); + assert_equals(dist, Math.sqrt(0.5 * 0.5 + 0.5 * 0.5), 'distance of scale'); +}, 'Test distance of scale functions'); + +test(function(t) { + var target = addDiv(t); + var dist = getDistance(target, 'transform', + 'scale3d(1.5, 1.5, 1.5)', + 'none'); + assert_equals(dist, + Math.sqrt(0.5 * 0.5 + 0.5 * 0.5 + 0.5 * 0.5), + 'distance of scale3d'); +}, 'Test distance of scale3d function and none'); + +test(function(t) { + var target = addDiv(t); + var dist = getDistance(target, 'transform', + 'scale3d(1.5, 1.5, 1.5)', + 'scale3d(2.0, 2.0, 1.0)'); + assert_equals(dist, + Math.sqrt(0.5 * 0.5 + 0.5 * 0.5 + 0.5 * 0.5), + 'distance of scale3d'); +}, 'Test distance of scale3d functions'); + +test(function(t) { + var target = addDiv(t); + var dist = + getDistance(target, 'transform', 'rotate(45deg)', 'rotate(90deg)'); + assert_approx_equals(dist, Math.PI / 2.0 - Math.PI / 4.0, EPSILON, 'distance of rotate'); +}, 'Test distance of rotate functions'); + +test(function(t) { + var target = addDiv(t); + var dist = + getDistance(target, 'transform', 'rotate(45deg)', 'none'); + assert_approx_equals(dist, Math.PI / 4.0, EPSILON, 'distance of rotate'); +}, 'Test distance of rotate function and none'); + +test(function(t) { + var target = addDiv(t); + var dist = getDistance(target, 'transform', + 'rotate3d(0, 1, 0, 90deg)', + 'none'); + assert_approx_equals(dist, Math.PI / 2, EPSILON, 'distance of rotate3d'); +}, 'Test distance of rotate3d function and none'); + +test(function(t) { + var target = addDiv(t); + var dist = getDistance(target, 'transform', + 'rotate3d(0, 0, 1, 90deg)', + 'rotate3d(1, 0, 0, 90deg)'); + let q1 = getQuaternion([0, 0, 1], Math.PI / 2.0); + let q2 = getQuaternion([1, 0, 0], Math.PI / 2.0); + assert_approx_equals(dist, computeRotateDistance(q1, q2), EPSILON, 'distance of rotate3d'); +}, 'Test distance of rotate3d functions'); + +test(function(t) { + var target = addDiv(t); + var dist = getDistance(target, 'transform', + 'rotate3d(0, 0, 1, 90deg)', + 'rotate3d(0, 0, 0, 90deg)'); + assert_approx_equals(dist, Math.PI / 2, EPSILON, 'distance of rotate3d'); +}, 'Test distance of rotate3d functions whose direction vector cannot be ' + + 'normalized'); + +test(function(t) { + var target = addDiv(t); + var dist = getDistance(target, 'transform', 'skew(1rad, 0.5rad)', 'none'); + assert_approx_equals(dist, Math.sqrt(1 * 1 + 0.5 * 0.5), EPSILON, 'distance of skew'); +}, 'Test distance of skew function and none'); + +test(function(t) { + var target = addDiv(t); + var dist = getDistance(target, 'transform', + 'skew(1rad, 0.5rad)', + 'skew(-1rad, 0)'); + assert_approx_equals(dist, Math.sqrt(2 * 2 + 0.5 * 0.5), EPSILON, 'distance of skew'); +}, 'Test distance of skew functions'); + +test(function(t) { + var target = addDiv(t); + var dist = getDistance(target, 'transform', + 'perspective(128px)', + 'none'); + assert_equals(dist, Infinity, 'distance of perspective'); +}, 'Test distance of perspective function and none'); + +test(function(t) { + var target = addDiv(t); + // perspective(0) is treated as perspective(inf) because perspective length + // should be greater than or equal to zero. + var dist = getDistance(target, 'transform', + 'perspective(128px)', + 'perspective(0)'); + assert_equals(dist, 128, 'distance of perspective'); +}, 'Test distance of perspective function and an invalid perspective'); + +test(function(t) { + var target = addDiv(t); + var dist = getDistance(target, 'transform', + 'perspective(128px)', + 'perspective(1024px)'); + assert_equals(dist, 1024 - 128, 'distance of perspective'); +}, 'Test distance of perspective functions'); + +test(function(t) { + var target = addDiv(t); + var sin_30 = Math.sin(Math.PI / 6); + var cos_30 = Math.cos(Math.PI / 6); + // matrix => translate(100, 0) rotate(30deg). + var matrix = createMatrixFromArray([ cos_30, sin_30, + -sin_30, cos_30, + 100, 0 ]); + var dist = getDistance(target, 'transform', matrix, 'none'); + assert_approx_equals(dist, + Math.sqrt(100 * 100 + (Math.PI / 6) * (Math.PI / 6)), + EPSILON, + 'distance of matrix'); +}, 'Test distance of matrix function and none'); + +test(function(t) { + var target = addDiv(t); + var sin_30 = Math.sin(Math.PI / 6); + var cos_30 = Math.cos(Math.PI / 6); + // matrix1 => translate(100, 0) rotate(30deg). + var matrix1 = createMatrixFromArray([ cos_30, sin_30, + -sin_30, cos_30, + 100, 0 ]); + // matrix2 => translate(0, 100) scale(0.5). + var matrix2 = createMatrixFromArray([ 0.5, 0, 0, 0.5, 0, 100 ]); + var dist = getDistance(target, 'transform', matrix1, matrix2); + assert_approx_equals(dist, + Math.sqrt(100 * 100 + 100 * 100 + // translate + (Math.PI / 6) * (Math.PI / 6) + // rotate + 0.5 * 0.5 + 0.5 * 0.5), // scale + EPSILON, + 'distance of matrix'); +}, 'Test distance of matrix functions'); + +test(function(t) { + var target = addDiv(t); + var matrix = createMatrixFromArray(rotate3dToMatrix(0, 1, 0, Math.PI / 6)); + var dist = getDistance(target, 'transform', matrix, 'none'); + assert_approx_equals(dist, Math.PI / 6, EPSILON, 'distance of matrix3d'); +}, 'Test distance of matrix3d function and none'); + +test(function(t) { + var target = addDiv(t); + // matrix1 => rotate3d(0, 1, 0, 30deg). + var matrix1 = createMatrixFromArray(rotate3dToMatrix(0, 1, 0, Math.PI / 6)); + // matrix1 => translate3d(100, 0, 0) scale3d(0.5, 0.5, 0.5). + var matrix2 = createMatrixFromArray([ 0.5, 0, 0, 0, + 0, 0.5, 0, 0, + 0, 0, 0.5, 0, + 100, 0, 0, 1 ]); + var dist = getDistance(target, 'transform', matrix1, matrix2); + assert_approx_equals(dist, + Math.sqrt(100 * 100 + // translate + 0.5 * 0.5 * 3 + // scale + (Math.PI / 6) * (Math.PI / 6)), // rotate + EPSILON, + 'distance of matrix'); +}, 'Test distance of matrix3d functions'); + +test(function(t) { + var target = addDiv(t); + var cos_180 = Math.cos(Math.PI); + var sin_180 = Math.sin(Math.PI); + // matrix1 => translate3d(100px, 50px, -10px) skew(45deg). + var matrix1 = createMatrixFromArray([ 1, 0, 0, 0, + Math.tan(Math.PI/4.0), 1, 0, 0, + 0, 0, 1, 0, + 100, 50, -10, 1]); + // matrix2 => translate3d(1000px, 0, 0) rotate3d(1, 0, 0, 180deg). + var matrix2 = createMatrixFromArray([ 1, 0, 0, 0, + 0, cos_180, sin_180, 0, + 0, -sin_180, cos_180, 0, + 1000, 0, 0, 1 ]); + var dist = getDistance(target, 'transform', matrix1, matrix2); + assert_approx_equals(dist, + Math.sqrt(900 * 900 + 50 * 50 + 10 * 10 + // translate + Math.PI * Math.PI + // rotate + (Math.PI / 4) * (Math.PI / 4)), // skew angle + EPSILON, + 'distance of matrix'); +}, 'Test distance of matrix3d functions with skew factors'); + +test(function(t) { + var target = addDiv(t); + var dist = + getDistance(target, 'transform', + 'rotate(180deg) translate(1000px)', + 'rotate(360deg) translate(0px)'); + assert_approx_equals(dist, Math.sqrt(1000 * 1000 + Math.PI * Math.PI), EPSILON, + 'distance of transform lists'); +}, 'Test distance of transform lists'); + +test(function(t) { + var target = addDiv(t); + var dist = getDistance(target, 'transform', + 'translate(100px) rotate(180deg)', + 'translate(50px) rotate(90deg) scale(5) skew(1rad)'); + assert_approx_equals(dist, + Math.sqrt(50 * 50 + + Math.PI / 2 * Math.PI / 2 + + 4 * 4 * 2 + + 1 * 1), + EPSILON, + 'distance of transform lists'); +}, 'Test distance of transform lists where one has extra items'); + +test(function(t) { + var target = addDiv(t); + var dist = getDistance(target, 'transform', + 'translate(1000px) rotate3d(1, 0, 0, 180deg)', + 'translate(1000px) scale3d(2.5, 0.5, 1)'); + assert_equals(dist, Math.sqrt(Math.PI * Math.PI + 1.5 * 1.5 + 0.5 * 0.5), + 'distance of transform lists'); +}, 'Test distance of mismatched transform lists'); + +test(function(t) { + var target = addDiv(t); + var dist = getDistance(target, 'transform', + 'translate(100px) skew(1rad)', + 'translate(1000px) rotate3d(0, 1, 0, -2rad)'); + assert_approx_equals(dist, + Math.sqrt(900 * 900 + 1 * 1 + 2 * 2), + EPSILON, + 'distance of transform lists'); +}, 'Test distance of mismatched transform lists with skew function'); + + +// Individual transforms +test(function(t) { + var target = addDiv(t); + var dist = getDistance(target, 'translate', '50px', 'none'); + assert_equals(dist, Math.sqrt(50 * 50), 'distance of 2D translate and none'); +}, 'Test distance of 2D translate property with none'); + +test(function(t) { + var target = addDiv(t); + var dist = getDistance(target, 'translate', '10px 30px', '50px'); + assert_equals(dist, Math.sqrt(40 * 40 + 30 * 30), 'distance of 2D translate'); +}, 'Test distance of 2D translate property'); + +test(function(t) { + var target = addDiv(t); + var dist = getDistance(target, 'translate', '10px 30px 50px', '50px'); + assert_equals(dist, Math.sqrt(40 * 40 + 30 * 30 + 50 * 50), + 'distance of 3D translate'); +}, 'Test distance of 3D translate property'); + +test(function(t) { + var target = addDiv(t); + var dist = getDistance(target, 'scale', '2', 'none'); + assert_equals(dist, Math.sqrt(1 + 1), 'distance of 2D scale and none'); +}, 'Test distance of 2D scale property with none'); + +test(function(t) { + var target = addDiv(t); + var dist = getDistance(target, 'scale', '3', '1 1'); + assert_equals(dist, Math.sqrt(2 * 2 + 2 * 2), 'distance of 2D scale'); +}, 'Test distance of 2D scale property'); + +test(function(t) { + var target = addDiv(t); + var dist = getDistance(target, 'scale', '3 2 2', '1 1'); + assert_equals(dist, Math.sqrt(2 * 2 + 1 * 1 + 1 * 1), + 'distance of 3D scale'); +}, 'Test distance of 3D scale property'); + +test(function(t) { + var target = addDiv(t); + var dist = getDistance(target, 'rotate', '180deg', 'none'); + assert_equals(dist, Math.PI, 'distance of 2D rotate and none'); +}, 'Test distance of 2D rotate property with none'); + +test(function(t) { + var target = addDiv(t); + var dist = getDistance(target, 'rotate', '180deg', '90deg'); + assert_equals(dist, Math.PI / 2.0, 'distance of 2D rotate'); +}, 'Test distance of 2D rotate property'); + +test(function(t) { + var target = addDiv(t); + var dist = getDistance(target, 'rotate', 'z 90deg', 'x 90deg'); + let q1 = getQuaternion([0, 0, 1], Math.PI / 2.0); + let q2 = getQuaternion([1, 0, 0], Math.PI / 2.0); + assert_approx_equals(dist, computeRotateDistance(q1, q2), EPSILON, + 'distance of 3D rotate'); +}, 'Test distance of 3D rotate property'); + +</script> +</html> diff --git a/dom/animation/test/mozilla/test_document_timeline_origin_time_range.html b/dom/animation/test/mozilla/test_document_timeline_origin_time_range.html new file mode 100644 index 0000000000..b2aeef8a77 --- /dev/null +++ b/dom/animation/test/mozilla/test_document_timeline_origin_time_range.html @@ -0,0 +1,32 @@ +<!doctype html> +<meta charset=utf-8> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../testcommon.js"></script> +<body> +<div id="log"></div> +<script> +'use strict'; + +// If the originTime parameter passed to the DocumentTimeline exceeds +// the range of the internal storage type (a signed 64-bit integer number +// of ticks--a platform-dependent unit) then we should throw. +// Infinity isn't allowed as an origin time value and clamping to just +// inside the allowed range will just mean we overflow elsewhere. + +test(function(t) { + assert_throws({ name: 'TypeError'}, + function() { + new DocumentTimeline({ originTime: Number.MAX_SAFE_INTEGER }); + }); +}, 'Calculated current time is positive infinity'); + +test(function(t) { + assert_throws({ name: 'TypeError'}, + function() { + new DocumentTimeline({ originTime: -1 * Number.MAX_SAFE_INTEGER }); + }); +}, 'Calculated current time is negative infinity'); + +</script> +</body> diff --git a/dom/animation/test/mozilla/test_event_listener_leaks.html b/dom/animation/test/mozilla/test_event_listener_leaks.html new file mode 100644 index 0000000000..bcfadaf9e9 --- /dev/null +++ b/dom/animation/test/mozilla/test_event_listener_leaks.html @@ -0,0 +1,43 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1450271 - Test Animation event listener leak conditions</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/dom/events/test/event_leak_utils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<script class="testbody" type="text/javascript"> +// Manipulate Animation. Its important here that we create a +// listener callback from the DOM objects back to the frame's global +// in order to exercise the leak condition. +async function useAnimation(contentWindow) { + let div = contentWindow.document.createElement("div"); + contentWindow.document.body.appendChild(div); + let animation = div.animate({}, 100 * 1000); + is(animation.playState, "running", "animation should be running"); + animation.onfinish = _ => { + contentWindow.finishCount += 1; + }; +} + +async function runTest() { + try { + await checkForEventListenerLeaks("Animation", useAnimation); + } catch (e) { + ok(false, e); + } finally { + SimpleTest.finish(); + } +} + +SimpleTest.waitForExplicitFinish(); +addEventListener("load", runTest, { once: true }); +</script> +</pre> +</body> +</html> diff --git a/dom/animation/test/mozilla/test_get_animations_on_scroll_animations.html b/dom/animation/test/mozilla/test_get_animations_on_scroll_animations.html new file mode 100644 index 0000000000..7d20e5b70b --- /dev/null +++ b/dom/animation/test/mozilla/test_get_animations_on_scroll_animations.html @@ -0,0 +1,74 @@ +<!doctype html> +<head> +<meta charset=utf-8> +<title>Test getAnimations() which doesn't return scroll animations</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../testcommon.js"></script> +<style> + @keyframes animWidth { + from { width: 100px; } + to { width: 200px } + } + @keyframes animTop { + to { top: 100px } + } + .fill-vh { + width: 100px; + height: 100vh; + } +</style> +</head> +<body> +<div id="log"></div> +<script> +"use strict"; + +test(function(t) { + const div = addDiv(t, + { style: "width: 10px; height: 100px; " + + "animation: animWidth 100s scroll(), animTop 200s;" }); + + // Sanity check to make sure the scroll animation is there. + addDiv(t, { class: "fill-vh" }); + const scroller = document.scrollingElement; + const maxScroll = scroller.scrollHeight - scroller.clientHeight; + scroller.scrollTop = maxScroll; + assert_equals(getComputedStyle(div).width, "200px", + "The scroll animation is there"); + + const animations = div.getAnimations(); + assert_equals(animations.length, 2, + 'getAnimations() should include scroll animations'); + assert_equals(animations[0].animationName, "animWidth", + 'getAmimations() should return scroll animations'); + // FIXME: Bug 1676794. Support ScrollTimeline interface. + assert_equals(animations[0].timeline, null, + 'scroll animation should not return scroll timeline'); +}, 'Element.getAnimation() should include scroll animations'); + +test(function(t) { + const div = addDiv(t, + { style: "width: 10px; height: 100px; " + + "animation: animWidth 100s scroll(), animTop 100s;" }); + + // Sanity check to make sure the scroll animation is there. + addDiv(t, { class: "fill-vh" }); + const scroller = document.scrollingElement; + const maxScroll = scroller.scrollHeight - scroller.clientHeight; + scroller.scrollTop = maxScroll; + assert_equals(getComputedStyle(div).width, "200px", + "The scroll animation is there"); + + const animations = document.getAnimations(); + assert_equals(animations.length, 2, + 'getAnimations() should include scroll animations'); + assert_equals(animations[0].animationName, "animWidth", + 'getAmimations() should return scroll animations'); + // FIXME: Bug 1676794. Support ScrollTimeline interface. + assert_equals(animations[0].timeline, null, + 'scroll animation should not return scroll timeline'); +}, 'Document.getAnimation() should include scroll animations'); + +</script> +</body> diff --git a/dom/animation/test/mozilla/test_hide_and_show.html b/dom/animation/test/mozilla/test_hide_and_show.html new file mode 100644 index 0000000000..f36543bb1e --- /dev/null +++ b/dom/animation/test/mozilla/test_hide_and_show.html @@ -0,0 +1,198 @@ +<!doctype html> +<meta charset=utf-8> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../testcommon.js"></script> +<style> +@keyframes move { + 100% { + transform: translateX(100px); + } +} + +div.pseudo::before { + animation: move 0.01s; + content: 'content'; +} + +</style> +<body> +<div id="log"></div> +<script> +'use strict'; + +test(function(t) { + var div = addDiv(t, { style: 'animation: move 100s infinite' }); + assert_equals(div.getAnimations().length, 1, + 'display:initial element has animations'); + + div.style.display = 'none'; + assert_equals(div.getAnimations().length, 0, + 'display:none element has no animations'); +}, 'Animation stops playing when the element style display is set to "none"'); + +test(function(t) { + var parentElement = addDiv(t); + var div = addDiv(t, { style: 'animation: move 100s infinite' }); + parentElement.appendChild(div); + assert_equals(div.getAnimations().length, 1, + 'display:initial element has animations'); + + parentElement.style.display = 'none'; + assert_equals(div.getAnimations().length, 0, + 'Element in display:none subtree has no animations'); +}, 'Animation stops playing when its parent element style display is set ' + + 'to "none"'); + +test(function(t) { + var div = addDiv(t, { style: 'animation: move 100s infinite' }); + assert_equals(div.getAnimations().length, 1, + 'display:initial element has animations'); + + div.style.display = 'none'; + assert_equals(div.getAnimations().length, 0, + 'display:none element has no animations'); + + div.style.display = ''; + assert_equals(div.getAnimations().length, 1, + 'Element which is no longer display:none has animations ' + + 'again'); +}, 'Animation starts playing when the element gets shown from ' + + '"display:none" state'); + +test(function(t) { + var parentElement = addDiv(t); + var div = addDiv(t, { style: 'animation: move 100s infinite' }); + parentElement.appendChild(div); + assert_equals(div.getAnimations().length, 1, + 'display:initial element has animations'); + + parentElement.style.display = 'none'; + assert_equals(div.getAnimations().length, 0, + 'Element in display:none subtree has no animations'); + + parentElement.style.display = ''; + assert_equals(div.getAnimations().length, 1, + 'Element which is no longer in display:none subtree has ' + + 'animations again'); +}, 'Animation starts playing when its parent element is shown from ' + + '"display:none" state'); + +test(function(t) { + var div = addDiv(t, { style: 'animation: move 100s forwards' }); + assert_equals(div.getAnimations().length, 1, + 'display:initial element has animations'); + + var animation = div.getAnimations()[0]; + animation.finish(); + assert_equals(div.getAnimations().length, 1, + 'Element has finished animation if the animation ' + + 'fill-mode is forwards'); + + div.style.display = 'none'; + assert_equals(animation.playState, 'idle', + 'The animation.playState should be idle'); + + assert_equals(div.getAnimations().length, 0, + 'display:none element has no animations'); + + div.style.display = ''; + assert_equals(div.getAnimations().length, 1, + 'Element which is no longer display:none has animations ' + + 'again'); + assert_not_equals(div.getAnimations()[0], animation, + 'Restarted animation is a newly-generated animation'); + +}, 'Animation which has already finished starts playing when the element ' + + 'gets shown from "display:none" state'); + +test(function(t) { + var parentElement = addDiv(t); + var div = addDiv(t, { style: 'animation: move 100s forwards' }); + parentElement.appendChild(div); + assert_equals(div.getAnimations().length, 1, + 'display:initial element has animations'); + + var animation = div.getAnimations()[0]; + animation.finish(); + assert_equals(div.getAnimations().length, 1, + 'Element has finished animation if the animation ' + + 'fill-mode is forwards'); + + parentElement.style.display = 'none'; + assert_equals(animation.playState, 'idle', + 'The animation.playState should be idle'); + assert_equals(div.getAnimations().length, 0, + 'Element in display:none subtree has no animations'); + + parentElement.style.display = ''; + assert_equals(div.getAnimations().length, 1, + 'Element which is no longer in display:none subtree has ' + + 'animations again'); + + assert_not_equals(div.getAnimations()[0], animation, + 'Restarted animation is a newly-generated animation'); + +}, 'Animation with fill:forwards which has already finished starts playing ' + + 'when its parent element is shown from "display:none" state'); + +test(function(t) { + var parentElement = addDiv(t); + var div = addDiv(t, { style: 'animation: move 100s' }); + parentElement.appendChild(div); + assert_equals(div.getAnimations().length, 1, + 'display:initial element has animations'); + + var animation = div.getAnimations()[0]; + animation.finish(); + assert_equals(div.getAnimations().length, 0, + 'Element does not have finished animations'); + + parentElement.style.display = 'none'; + assert_equals(animation.playState, 'idle', + 'The animation.playState should be idle'); + assert_equals(div.getAnimations().length, 0, + 'Element in display:none subtree has no animations'); + + parentElement.style.display = ''; + assert_equals(div.getAnimations().length, 1, + 'Element which is no longer in display:none subtree has ' + + 'animations again'); + + assert_not_equals(div.getAnimations()[0], animation, + 'Restarted animation is a newly-generated animation'); + +}, 'CSS Animation which has already finished starts playing when its parent ' + + 'element is shown from "display:none" state'); + +promise_test(function(t) { + var div = addDiv(t, { 'class': 'pseudo' }); + var eventWatcher = new EventWatcher(t, div, 'animationend'); + + assert_equals(document.getAnimations().length, 1, + 'CSS animation on pseudo element'); + + return eventWatcher.wait_for('animationend').then(function() { + assert_equals(document.getAnimations().length, 0, + 'No CSS animation on pseudo element after the animation ' + + 'finished'); + + // Remove the class which generated this pseudo element. + div.classList.remove('pseudo'); + + // We need to wait for two frames to process re-framing. + // The callback of 'animationend' is processed just before rAF callbacks, + // and rAF callbacks are processed before re-framing process, so waiting for + // one rAF callback is not sufficient. + return waitForAnimationFrames(2); + }).then(function() { + // Add the class again to re-generate pseudo element. + div.classList.add('pseudo'); + assert_equals(document.getAnimations().length, 1, + 'A new CSS animation on pseudo element'); + }); +}, 'CSS animation on pseudo element restarts after the pseudo element that ' + + 'had a finished CSS animation is re-generated'); + +</script> +</body> diff --git a/dom/animation/test/mozilla/test_moz_prefixed_properties.html b/dom/animation/test/mozilla/test_moz_prefixed_properties.html new file mode 100644 index 0000000000..af26f12931 --- /dev/null +++ b/dom/animation/test/mozilla/test_moz_prefixed_properties.html @@ -0,0 +1,90 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test animations of all properties that have -moz prefix</title> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="../testcommon.js"></script> + <script src="../property_database.js"></script> +</head> +<body> +<div id="log"></div> +<script> +"use strict"; + +const testcases = [ + { + property: "-moz-box-align" + }, + { + property: "-moz-box-direction" + }, + { + property: "-moz-box-ordinal-group" + }, + { + property: "-moz-box-orient", + }, + { + property: "-moz-box-pack" + }, + { + property: "-moz-float-edge" + }, + { + property: "-moz-force-broken-image-icon" + }, + { + property: "-moz-orient" + }, + { + property: "-moz-osx-font-smoothing", + pref: "layout.css.osx-font-smoothing.enabled" + }, + { + property: "-moz-text-size-adjust" + }, + { + property: "-moz-user-input" + }, + { + property: "-moz-user-modify" + }, + { + property: "user-select" + }, + { + property: "-moz-window-dragging" + }, +]; + +testcases.forEach(testcase => { + if (testcase.pref && !IsCSSPropertyPrefEnabled(testcase.pref)) { + return; + } + + const property = gCSSProperties[testcase.property]; + const values = property.initial_values.concat(property.other_values); + values.forEach(value => { + test(function(t) { + const container = addDiv(t); + const target = document.createElement("div"); + container.appendChild(target); + + container.style[property.domProp] = value; + + const animation = + target.animate({ [property.domProp]: [value, "inherit"] }, + { duration: 1000, delay: -500 } ); + + const expectedValue = getComputedStyle(container)[property.domProp]; + assert_equals(getComputedStyle(target)[property.domProp], expectedValue, + `Computed style shoud be "${ expectedValue }"`); + }, `Test inherit value for "${ testcase.property }" ` + + `(Parent element style is "${ value }")`); + }); +}); +</script> +</pre> +</body> +</html> diff --git a/dom/animation/test/mozilla/test_restyles.html b/dom/animation/test/mozilla/test_restyles.html new file mode 100644 index 0000000000..bc1ab70c74 --- /dev/null +++ b/dom/animation/test/mozilla/test_restyles.html @@ -0,0 +1,22 @@ +<!doctype html> +<meta charset=utf-8> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +<div id='log'></div> +<script> +'use strict'; +SimpleTest.waitForExplicitFinish(); +SimpleTest.expectAssertions(0, 1); // bug 1332970 +SpecialPowers.pushPrefEnv( + { + set: [ + ['layout.reflow.synthMouseMove', false], + ['privacy.reduceTimerPrecision', false], + ], + }, + function() { + window.open('file_restyles.html'); + } +); +</script> +</html> diff --git a/dom/animation/test/mozilla/test_restyling_xhr_doc.html b/dom/animation/test/mozilla/test_restyling_xhr_doc.html new file mode 100644 index 0000000000..67b6ac8845 --- /dev/null +++ b/dom/animation/test/mozilla/test_restyling_xhr_doc.html @@ -0,0 +1,106 @@ +<!doctype html> +<meta charset=utf-8> +<script src="../testcommon.js"></script> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<div id="log"></div> +<script> +'use strict'; + +// This test supplements the web-platform-tests in: +// +// web-animations/interfaces/Animatable/animate-no-browsing-context.html +// +// Specifically, it covers the case where we have a running animation +// targetting an element in a document without a browsing context. +// +// Currently the behavior in this case is not well-defined. For example, +// if we were to simply take an element from such a document, and do: +// +// const xdoc = xhr.responseXML; +// const div = xdoc.getElementById('test'); +// div.style.opacity = '0'; +// alert(getComputedStyle(div).opacity); +// +// We'd get '0' in Firefox and Edge, but an empty string in Chrome. +// +// However, if instead of using the style attribute, we set style in a <style> +// element in *either* the document we're calling from *or* the XHR doc and +// do the same we get '1' in Firefox and Edge, but an empty string in Chrome. +// +// That is, no browser appears to apply styles to elements in a document without +// a browsing context unless the styles are defined using the style attribute, +// and even then Chrome does not. +// +// There is some prose in CSSOM which says, +// +// Note: This means that even if obj is in a different document (e.g. one +// fetched via XMLHttpRequest) it will still use the style rules associated +// with the document that is associated with the global object on which +// getComputedStyle() was invoked to compute the CSS declaration block.[1] +// +// However, this text has been around since at least 2013 and does not appear +// to be implemented. +// +// As a result, it's not really possible to write a cross-browser test for the +// behavior for animations in this context since it's not clear what the result +// should be. That said, we still want to exercise this particular code path so +// we make this case a Mozilla-specific test. The other similar tests cases for +// which the behavior is well-defined are covered by web-platform-tests. +// +// [1] https://drafts.csswg.org/cssom/#extensions-to-the-window-interface + +function getXHRDoc(t) { + return new Promise(resolve => { + const xhr = new XMLHttpRequest(); + xhr.open('GET', 'xhr_doc.html'); + xhr.responseType = 'document'; + xhr.onload = t.step_func(() => { + assert_equals(xhr.readyState, xhr.DONE, + 'Request should complete successfully'); + assert_equals(xhr.status, 200, + 'Response should be OK'); + resolve(xhr.responseXML); + }); + xhr.send(); + }); +} + +promise_test(t => { + let anim; + return getXHRDoc(t).then(xhrdoc => { + const div = xhrdoc.getElementById('test'); + anim = div.animate({ opacity: [ 0, 1 ] }, 1000); + // Give the animation an active timeline and kick-start it. + anim.timeline = document.timeline; + anim.startTime = document.timeline.currentTime; + assert_equals(anim.playState, 'running', + 'The animation should be running'); + // Gecko currently skips applying animation styles to elements in documents + // without browsing contexts. + assert_not_equals(getComputedStyle(div).opacity, '0', + 'Style should NOT be updated'); + }); +}, 'Forcing an animation targetting an element in a document without a' + + ' browsing context to play does not cause style to update'); + +promise_test(t => { + let anim; + return getXHRDoc(t).then(xhrdoc => { + const div = addDiv(t); + anim = div.animate({ opacity: [ 0, 1 ] }, 1000); + assert_equals(getComputedStyle(div).opacity, '0', + 'Style should be updated'); + // Trigger an animation restyle to be queued + anim.currentTime = 0.1; + // Adopt node into XHR doc + xhrdoc.body.appendChild(div); + // We should skip applying animation styles to elements in documents + // without a pres shell. + assert_equals(getComputedStyle(div).opacity, '1', + 'Style should NOT be updated'); + }); +}, 'Moving an element with a pending animation restyle to a document without' + + ' a browsing context resets animation style'); + +</script> diff --git a/dom/animation/test/mozilla/test_set_easing.html b/dom/animation/test/mozilla/test_set_easing.html new file mode 100644 index 0000000000..55c77f0e8f --- /dev/null +++ b/dom/animation/test/mozilla/test_set_easing.html @@ -0,0 +1,36 @@ +<!doctype html> +<head> +<meta charset=utf-8> +<title>Test setting easing in sandbox</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../testcommon.js"></script> +</head> +<body> +<div id="log"></div> +<script> +"use strict"; + +test(function(t) { + const div = document.createElement("div"); + document.body.appendChild(div); + div.animate({ opacity: [0, 1] }, 100000 ); + + const contentScript = function() { + try { + document.getAnimations()[0].effect.updateTiming({ easing: 'linear' }); + assert_true(true, 'Setting easing should not throw in sandbox'); + } catch (e) { + assert_unreached('Setting easing threw ' + e); + } + }; + + const sandbox = new SpecialPowers.Cu.Sandbox(window); + sandbox.importFunction(document, "document"); + sandbox.importFunction(assert_true, "assert_true"); + sandbox.importFunction(assert_unreached, "assert_unreached"); + SpecialPowers.Cu.evalInSandbox(`(${contentScript.toString()})()`, sandbox); +}, 'Setting easing should not throw any exceptions in sandbox'); + +</script> +</body> diff --git a/dom/animation/test/mozilla/test_style_after_finished_on_compositor.html b/dom/animation/test/mozilla/test_style_after_finished_on_compositor.html new file mode 100644 index 0000000000..bccae9e0d5 --- /dev/null +++ b/dom/animation/test/mozilla/test_style_after_finished_on_compositor.html @@ -0,0 +1,138 @@ +<!doctype html> +<head> +<meta charset=utf-8> +<title>Test for styles after finished on the compositor</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../testcommon.js"></script> +<style> +.compositor { + /* Element needs geometry to be eligible for layerization */ + width: 100px; + height: 100px; + background-color: green; +} +</style> +</head> +<body> +<div id="log"></div> +<script> +"use strict"; + +promise_test(async t => { + const div = addDiv(t, { 'class': 'compositor' }); + const anim = div.animate([ { offset: 0, opacity: 1 }, + { offset: 1, opacity: 0 } ], + { delay: 10, + duration: 100 }); + + await anim.finished; + + await waitForNextFrame(); + + const opacity = SpecialPowers.DOMWindowUtils.getOMTAStyle(div, 'opacity'); + + assert_equals(opacity, '', 'No opacity animation runs on the compositor'); +}, 'Opacity animation with positive delay is removed from compositor when ' + + 'finished'); + +promise_test(async t => { + const div = addDiv(t, { 'class': 'compositor' }); + const anim = div.animate([ { offset: 0, opacity: 1 }, + { offset: 0.9, opacity: 1 }, + { offset: 1, opacity: 0 } ], + { duration: 100 }); + + await anim.finished; + + await waitForNextFrame(); + + const opacity = SpecialPowers.DOMWindowUtils.getOMTAStyle(div, 'opacity'); + + assert_equals(opacity, '', 'No opacity animation runs on the compositor'); +}, 'Opacity animation initially opacity: 1 is removed from compositor when ' + + 'finished'); + +promise_test(async t => { + const div = addDiv(t, { 'class': 'compositor' }); + const anim = div.animate([ { offset: 0, opacity: 0 }, + { offset: 0.5, opacity: 1 }, + { offset: 0.51, opacity: 1 }, + { offset: 1, opacity: 0 } ], + { delay: 10, duration: 100 }); + + await waitForAnimationFrames(2); + + // Setting the current time at the offset generating opacity: 1. + anim.currentTime = 60; + + await anim.finished; + + await waitForNextFrame(); + + const opacity = SpecialPowers.DOMWindowUtils.getOMTAStyle(div, 'opacity'); + + assert_equals(opacity, '', 'No opacity animation runs on the compositor'); +}, 'Opacity animation is removed from compositor even when it only visits ' + + 'exactly the point where the opacity: 1 value was set'); + +promise_test(async t => { + const div = addDiv(t, { 'class': 'compositor' }); + const anim = div.animate([ { offset: 0, transform: 'none' }, + { offset: 1, transform: 'translateX(100px)' } ], + { delay: 10, + duration: 100 }); + + await anim.finished; + + await waitForNextFrame(); + + const transform = SpecialPowers.DOMWindowUtils.getOMTAStyle(div, 'transform'); + + assert_equals(transform, '', 'No transform animation runs on the compositor'); +}, 'Transform animation with positive delay is removed from compositor when ' + + 'finished'); + +promise_test(async t => { + const div = addDiv(t, { 'class': 'compositor' }); + const anim = div.animate([ { offset: 0, transform: 'none' }, + { offset: 0.9, transform: 'none' }, + { offset: 1, transform: 'translateX(100px)' } ], + { duration: 100 }); + + await anim.finished; + + await waitForNextFrame(); + + const transform = SpecialPowers.DOMWindowUtils.getOMTAStyle(div, 'transform'); + + assert_equals(transform, '', 'No transform animation runs on the compositor'); +}, 'Transform animation initially transform: none is removed from compositor ' + + 'when finished'); + + +promise_test(async t => { + const div = addDiv(t, { 'class': 'compositor' }); + const anim = div.animate([ { offset: 0, transform: 'translateX(100px)' }, + { offset: 0.5, transform: 'none' }, + { offset: 0.9, transform: 'none' }, + { offset: 1, transform: 'translateX(100px)' } ], + { delay: 10, duration: 100 }); + + await waitForAnimationFrames(2); + + // Setting the current time at the offset generating transform: none. + anim.currentTime = 60; + + await anim.finished; + + await waitForNextFrame(); + + const transform = SpecialPowers.DOMWindowUtils.getOMTAStyle(div, 'transform'); + + assert_equals(transform, '', 'No transform animation runs on the compositor'); +}, 'Transform animation is removed from compositor even when it only visits ' + + 'exactly the point where the transform: none value was set'); + +</script> +</body> diff --git a/dom/animation/test/mozilla/test_transform_limits.html b/dom/animation/test/mozilla/test_transform_limits.html new file mode 100644 index 0000000000..92d1b7e1ec --- /dev/null +++ b/dom/animation/test/mozilla/test_transform_limits.html @@ -0,0 +1,56 @@ +<!doctype html> +<meta charset=utf-8> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../testcommon.js"></script> +<body> +<div id="log"></div> +<script> +'use strict'; + +// We clamp +infinity or -inifinity value in floating point to +// maximum floating point value or -maximum floating point value. +const MAX_FLOAT = 3.40282e+38; + +test(function(t) { + var div = addDiv(t); + div.style = "width: 1px; height: 1px;"; + var anim = div.animate([ { transform: 'scale(1)' }, + { transform: 'scale(3.5e+38)'}, + { transform: 'scale(3)' } ], 100 * MS_PER_SEC); + + anim.pause(); + anim.currentTime = 50 * MS_PER_SEC; + assert_equals(getComputedStyle(div).transform, + 'matrix(' + MAX_FLOAT + ', 0, 0, ' + MAX_FLOAT + ', 0, 0)'); +}, 'Test that the parameter of transform scale is clamped' ); + +test(function(t) { + var div = addDiv(t); + div.style = "width: 1px; height: 1px;"; + var anim = div.animate([ { transform: 'translate(1px)' }, + { transform: 'translate(3.5e+38px)'}, + { transform: 'translate(3px)' } ], 100 * MS_PER_SEC); + + anim.pause(); + anim.currentTime = 50 * MS_PER_SEC; + assert_equals(getComputedStyle(div).transform, + 'matrix(1, 0, 0, 1, ' + MAX_FLOAT + ', 0)'); +}, 'Test that the parameter of transform translate is clamped' ); + +test(function(t) { + var div = addDiv(t); + div.style = "width: 1px; height: 1px;"; + var anim = div.animate([ { transform: 'matrix(0.5, 0, 0, 0.5, 0, 0)' }, + { transform: 'matrix(2, 0, 0, 2, 3.5e+38, 0)'}, + { transform: 'matrix(0, 2, 0, -2, 0, 0)' } ], + 100 * MS_PER_SEC); + + anim.pause(); + anim.currentTime = 50 * MS_PER_SEC; + assert_equals(getComputedStyle(div).transform, + 'matrix(2, 0, 0, 2, ' + MAX_FLOAT + ', 0)'); +}, 'Test that the parameter of transform matrix is clamped' ); + +</script> +</body> diff --git a/dom/animation/test/mozilla/test_transition_finish_on_compositor.html b/dom/animation/test/mozilla/test_transition_finish_on_compositor.html new file mode 100644 index 0000000000..46a154b9af --- /dev/null +++ b/dom/animation/test/mozilla/test_transition_finish_on_compositor.html @@ -0,0 +1,22 @@ +<!doctype html> +<meta charset=utf-8> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<div id="log"></div> +<script> +'use strict'; +setup({explicit_done: true}); +// This test appears like it might get racey and cause a timeout with too low of a +// precision, so we hardcode it to something reasonable. +SpecialPowers.pushPrefEnv( + { + set: [ + ['privacy.reduceTimerPrecision', true], + ['privacy.resistFingerprinting.reduceTimerPrecision.microseconds', 2000], + ], + }, + function() { + window.open('file_transition_finish_on_compositor.html'); + } +); +</script> diff --git a/dom/animation/test/mozilla/test_underlying_discrete_value.html b/dom/animation/test/mozilla/test_underlying_discrete_value.html new file mode 100644 index 0000000000..3961305df3 --- /dev/null +++ b/dom/animation/test/mozilla/test_underlying_discrete_value.html @@ -0,0 +1,188 @@ +<!doctype html> +<meta charset=utf-8> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../testcommon.js"></script> +<body> +<div id="log"></div> +<script> +"use strict"; + +// Tests that we correctly extract the underlying value when the animation +// type is 'discrete'. +const discreteTests = [ + { + stylesheet: { + "@keyframes keyframes": + "from { align-content: flex-start; } to { align-content: flex-end; } " + }, + expectedKeyframes: [ + { computedOffset: 0, alignContent: "flex-start" }, + { computedOffset: 1, alignContent: "flex-end" } + ], + explanation: "Test for fully-specified keyframes" + }, + { + stylesheet: { + "@keyframes keyframes": "from { align-content: flex-start; }" + }, + expectedKeyframes: [ + { computedOffset: 0, alignContent: "flex-start" }, + { computedOffset: 1, alignContent: "normal" } + ], + explanation: "Test for 0% keyframe only", + }, + { + stylesheet: { + "@keyframes keyframes": "to { align-content: flex-end; }" + }, + expectedKeyframes: [ + { computedOffset: 0, alignContent: "normal" }, + { computedOffset: 1, alignContent: "flex-end" } + ], + explanation: "Test for 100% keyframe only", + }, + { + stylesheet: { + "@keyframes keyframes": "50% { align-content: center; }", + "#target": "align-content: space-between;" + }, + expectedKeyframes: [ + { computedOffset: 0, alignContent: "space-between" }, + { computedOffset: 0.5, alignContent: "center" }, + { computedOffset: 1, alignContent: "space-between" } + ], + explanation: "Test for no 0%/100% keyframes " + + "and specified style on target element" + }, + { + stylesheet: { + "@keyframes keyframes": "50% { align-content: center; }" + }, + attributes: { + style: "align-content: space-between" + }, + expectedKeyframes: [ + { computedOffset: 0, alignContent: "space-between" }, + { computedOffset: 0.5, alignContent: "center" }, + { computedOffset: 1, alignContent: "space-between" } + ], + explanation: "Test for no 0%/100% keyframes " + + "and specified style on target element using style attribute" + }, + { + stylesheet: { + "@keyframes keyframes": "50% { align-content: center; }", + "#target": "align-content: inherit;" + }, + expectedKeyframes: [ + { computedOffset: 0, alignContent: "normal" }, + { computedOffset: 0.5, alignContent: "center" }, + { computedOffset: 1, alignContent: "normal" } + ], + explanation: "Test for no 0%/100% keyframes " + + "and 'inherit' specified on target element", + }, + { + stylesheet: { + "@keyframes keyframes": "50% { align-content: center; }", + ".target": "align-content: space-between;" + }, + attributes: { + class: "target" + }, + expectedKeyframes: [ + { computedOffset: 0, alignContent: "space-between" }, + { computedOffset: 0.5, alignContent: "center" }, + { computedOffset: 1, alignContent: "space-between" } + ], + explanation: "Test for no 0%/100% keyframes " + + "and specified style on target element using class selector" + }, + { + stylesheet: { + "@keyframes keyframes": "50% { align-content: center; }", + "div": "align-content: space-between;" + }, + expectedKeyframes: [ + { computedOffset: 0, alignContent: "space-between" }, + { computedOffset: 0.5, alignContent: "center" }, + { computedOffset: 1, alignContent: "space-between" } + ], + explanation: "Test for no 0%/100% keyframes " + + "and specified style on target element using type selector" + }, + { + stylesheet: { + "@keyframes keyframes": "50% { align-content: center; }", + "div": "align-content: space-between;", + ".target": "align-content: flex-start;", + "#target": "align-content: flex-end;" + }, + attributes: { + class: "target" + }, + expectedKeyframes: [ + { computedOffset: 0, alignContent: "flex-end" }, + { computedOffset: 0.5, alignContent: "center" }, + { computedOffset: 1, alignContent: "flex-end" } + ], + explanation: "Test for no 0%/100% keyframes " + + "and specified style on target element " + + "using ID selector that overrides class selector" + }, + { + stylesheet: { + "@keyframes keyframes": "50% { align-content: center; }", + "div": "align-content: space-between !important;", + ".target": "align-content: flex-start;", + "#target": "align-content: flex-end;" + }, + attributes: { + class: "target" + }, + expectedKeyframes: [ + { computedOffset: 0, alignContent: "space-between" }, + { computedOffset: 0.5, alignContent: "center" }, + { computedOffset: 1, alignContent: "space-between" } + ], + explanation: "Test for no 0%/100% keyframes " + + "and specified style on target element " + + "using important type selector that overrides other rules" + }, +]; + +discreteTests.forEach(testcase => { + test(t => { + if (testcase.skip) { + return; + } + addStyle(t, testcase.stylesheet); + + const div = addDiv(t, { "id": "target" }); + if (testcase.attributes) { + for (let attributeName in testcase.attributes) { + div.setAttribute(attributeName, testcase.attributes[attributeName]); + } + } + div.style.animation = "keyframes 100s"; + + const keyframes = div.getAnimations()[0].effect.getKeyframes(); + const expectedKeyframes = testcase.expectedKeyframes; + assert_equals(keyframes.length, expectedKeyframes.length, + `keyframes.length should be ${ expectedKeyframes.length }`); + + keyframes.forEach((keyframe, index) => { + const expectedKeyframe = expectedKeyframes[index]; + assert_equals(keyframe.computedOffset, expectedKeyframe.computedOffset, + `computedOffset of keyframes[${ index }] should be ` + + `${ expectedKeyframe.computedOffset }`); + assert_equals(keyframe.alignContent, expectedKeyframe.alignContent, + `alignContent of keyframes[${ index }] should be ` + + `${ expectedKeyframe.alignContent }`); + }); + }, testcase.explanation); +}); + +</script> +</body> diff --git a/dom/animation/test/mozilla/test_unstyled.html b/dom/animation/test/mozilla/test_unstyled.html new file mode 100644 index 0000000000..4724979c11 --- /dev/null +++ b/dom/animation/test/mozilla/test_unstyled.html @@ -0,0 +1,54 @@ +<!doctype html> +<meta charset=utf-8> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../testcommon.js"></script> +<style> +div.pseudo::before { + animation: animation 1s; + content: 'content'; +} +@keyframes animation { + to { opacity: 0 } +} +</style> +<body> +<div id="log"></div> +<script> +'use strict'; + +// Tests for cases where we may not have style data for an element + +promise_test(async t => { + // Get a CSSPseudoElement + const div = addDiv(t, { class: 'pseudo' }); + const cssAnim = document.getAnimations()[0]; + const pseudoElem = cssAnim.effect.target; + + // Drop pseudo from styles and flush styles + div.classList.remove('pseudo'); + getComputedStyle(div, '::before').content; + + // Try animating the pseudo's content attribute + const contentAnim = pseudoElem.animate( + { content: ['none', '"content"'] }, + { duration: 100 * MS_PER_SEC, fill: 'both' } + ); + + // Check that the initial value is as expected + await contentAnim.ready; + assert_equals(getComputedStyle(div, '::before').content, 'none'); + + contentAnim.finish(); + + // Animating an obsolete pseudo element should NOT cause the pseudo element + // to be re-generated. That behavior might change in which case this test + // will need to be updated. The most important part of this test, however, + // is simply checking that nothing explodes if we try to animate such a + // pseudo element. + + assert_equals(getComputedStyle(div, '::before').content, 'none'); +}, 'Animation on an obsolete pseudo element produces expected results'); + +</script> +</body> diff --git a/dom/animation/test/mozilla/xhr_doc.html b/dom/animation/test/mozilla/xhr_doc.html new file mode 100644 index 0000000000..b9fa57e3f5 --- /dev/null +++ b/dom/animation/test/mozilla/xhr_doc.html @@ -0,0 +1,2 @@ +<!doctype html> +<div id=test></div> diff --git a/dom/animation/test/style/test_animation-seeking-with-current-time.html b/dom/animation/test/style/test_animation-seeking-with-current-time.html new file mode 100644 index 0000000000..265de8f0f5 --- /dev/null +++ b/dom/animation/test/style/test_animation-seeking-with-current-time.html @@ -0,0 +1,123 @@ +<!doctype html> +<html> + <head> + <meta charset=utf-8> + <title>Tests for seeking using Animation.currentTime</title> + <style> +.animated-div { + margin-left: -10px; + animation-timing-function: linear ! important; +} + +@keyframes anim { + from { margin-left: 0px; } + to { margin-left: 100px; } +} + </style> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="../testcommon.js"></script> + </head> + <body> + <div id="log"></div> + <script type="text/javascript"> +'use strict'; + +function assert_marginLeft_equals(target, expect, description) { + var marginLeft = parseFloat(getComputedStyle(target).marginLeft); + assert_equals(marginLeft, expect, description); +} + +promise_test(function(t) { + var div = addDiv(t, {'class': 'animated-div'}); + div.style.animation = "anim 100s"; + var animation = div.getAnimations()[0]; + + return animation.ready.then(function() { + animation.currentTime = 90 * MS_PER_SEC; + assert_marginLeft_equals(div, 90, + 'Computed style is updated when seeking forwards in active interval'); + + animation.currentTime = 10 * MS_PER_SEC; + assert_marginLeft_equals(div, 10, + 'Computed style is updated when seeking backwards in active interval'); + }); +}, 'Seeking forwards and backward in active interval'); + +promise_test(function(t) { + var div = addDiv(t, {'class': 'animated-div'}); + div.style.animation = "anim 100s 100s"; + var animation = div.getAnimations()[0]; + + return animation.ready.then(function() { + assert_marginLeft_equals(div, -10, + 'Computed style is unaffected in before phase with no backwards fill'); + + // before -> active (non-active -> active) + animation.currentTime = 150 * MS_PER_SEC; + assert_marginLeft_equals(div, 50, + 'Computed style is updated when seeking forwards from ' + + 'not \'in effect\' to \'in effect\' state'); + }); +}, 'Seeking to non-\'in effect\' from \'in effect\' (before -> active)'); + +promise_test(function(t) { + var div = addDiv(t, {'class': 'animated-div'}); + div.style.animation = "anim 100s 100s"; + var animation = div.getAnimations()[0]; + + return animation.ready.then(function() { + // move to after phase + animation.currentTime = 250 * MS_PER_SEC; + assert_marginLeft_equals(div, -10, + 'Computed style is unaffected in after phase with no forwards fill'); + + // after -> active (non-active -> active) + animation.currentTime = 150 * MS_PER_SEC; + assert_marginLeft_equals(div, 50, + 'Computed style is updated when seeking backwards from ' + + 'not \'in effect\' to \'in effect\' state'); + }); +}, 'Seeking to non-\'in effect\' from \'in effect\' (after -> active)'); + +promise_test(function(t) { + var div = addDiv(t, {'class': 'animated-div'}); + div.style.animation = "anim 100s 100s"; + var animation = div.getAnimations()[0]; + + return animation.ready.then(function() { + // move to active phase + animation.currentTime = 150 * MS_PER_SEC; + assert_marginLeft_equals(div, 50, + 'Computed value is set during active phase'); + + // active -> before + animation.currentTime = 50 * MS_PER_SEC; + assert_marginLeft_equals(div, -10, + 'Computed value is not effected after seeking backwards from ' + + '\'in effect\' to not \'in effect\' state'); + }); +}, 'Seeking to \'in effect\' from non-\'in effect\' (active -> before)'); + +promise_test(function(t) { + var div = addDiv(t, {'class': 'animated-div'}); + div.style.animation = "anim 100s 100s"; + var animation = div.getAnimations()[0]; + + return animation.ready.then(function() { + // move to active phase + animation.currentTime = 150 * MS_PER_SEC; + assert_marginLeft_equals(div, 50, + 'Computed value is set during active phase'); + + // active -> after + animation.currentTime = 250 * MS_PER_SEC; + assert_marginLeft_equals(div, -10, + 'Computed value is not affected after seeking forwards from ' + + '\'in effect\' to not \'in effect\' state'); + }); +}, 'Seeking to \'in effect\' from non-\'in effect\' (active -> after)'); + + </script> + </body> +</html> diff --git a/dom/animation/test/style/test_animation-seeking-with-start-time.html b/dom/animation/test/style/test_animation-seeking-with-start-time.html new file mode 100644 index 0000000000..e56db5f23d --- /dev/null +++ b/dom/animation/test/style/test_animation-seeking-with-start-time.html @@ -0,0 +1,123 @@ +<!doctype html> +<html> + <head> + <meta charset=utf-8> + <title>Tests for seeking using Animation.startTime</title> + <style> +.animated-div { + margin-left: -10px; + animation-timing-function: linear ! important; +} + +@keyframes anim { + from { margin-left: 0px; } + to { margin-left: 100px; } +} + </style> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="../testcommon.js"></script> + </head> + <body> + <div id="log"></div> + <script type="text/javascript"> +'use strict'; + +function assert_marginLeft_equals(target, expect, description) { + var marginLeft = parseFloat(getComputedStyle(target).marginLeft); + assert_equals(marginLeft, expect, description); +} + +promise_test(function(t) { + var div = addDiv(t, {'class': 'animated-div'}); + div.style.animation = "anim 100s"; + var animation = div.getAnimations()[0]; + + return animation.ready.then(function() { + animation.startTime = animation.timeline.currentTime - 90 * MS_PER_SEC + assert_marginLeft_equals(div, 90, + 'Computed style is updated when seeking forwards in active interval'); + + animation.startTime = animation.timeline.currentTime - 10 * MS_PER_SEC; + assert_marginLeft_equals(div, 10, + 'Computed style is updated when seeking backwards in active interval'); + }); +}, 'Seeking forwards and backward in active interval'); + +promise_test(function(t) { + var div = addDiv(t, {'class': 'animated-div'}); + div.style.animation = "anim 100s 100s"; + var animation = div.getAnimations()[0]; + + return animation.ready.then(function() { + assert_marginLeft_equals(div, -10, + 'Computed style is unaffected in before phase with no backwards fill'); + + // before -> active (non-active -> active) + animation.startTime = animation.timeline.currentTime - 150 * MS_PER_SEC; + assert_marginLeft_equals(div, 50, + 'Computed style is updated when seeking forwards from ' + + 'not \'in effect\' to \'in effect\' state'); + }); +}, 'Seeking to non-\'in effect\' from \'in effect\' (before -> active)'); + +promise_test(function(t) { + var div = addDiv(t, {'class': 'animated-div'}); + div.style.animation = "anim 100s 100s"; + var animation = div.getAnimations()[0]; + + return animation.ready.then(function() { + // move to after phase + animation.startTime = animation.timeline.currentTime - 250 * MS_PER_SEC; + assert_marginLeft_equals(div, -10, + 'Computed style is unaffected in after phase with no forwards fill'); + + // after -> active (non-active -> active) + animation.startTime = animation.timeline.currentTime - 150 * MS_PER_SEC; + assert_marginLeft_equals(div, 50, + 'Computed style is updated when seeking backwards from ' + + 'not \'in effect\' to \'in effect\' state'); + }); +}, 'Seeking to non-\'in effect\' from \'in effect\' (after -> active)'); + +promise_test(function(t) { + var div = addDiv(t, {'class': 'animated-div'}); + div.style.animation = "anim 100s 100s"; + var animation = div.getAnimations()[0]; + + return animation.ready.then(function() { + // move to active phase + animation.startTime = animation.timeline.currentTime - 150 * MS_PER_SEC; + assert_marginLeft_equals(div, 50, + 'Computed value is set during active phase'); + + // active -> before + animation.startTime = animation.timeline.currentTime - 50 * MS_PER_SEC; + assert_marginLeft_equals(div, -10, + 'Computed value is not affected after seeking backwards from ' + + '\'in effect\' to not \'in effect\' state'); + }); +}, 'Seeking to \'in effect\' from non-\'in effect\' (active -> before)'); + +promise_test(function(t) { + var div = addDiv(t, {'class': 'animated-div'}); + div.style.animation = "anim 100s 100s"; + var animation = div.getAnimations()[0]; + + return animation.ready.then(function() { + // move to active phase + animation.startTime = animation.timeline.currentTime - 150 * MS_PER_SEC; + assert_marginLeft_equals(div, 50, + 'Computed value is set during active phase'); + + // active -> after + animation.startTime = animation.timeline.currentTime - 250 * MS_PER_SEC; + assert_marginLeft_equals(div, -10, + 'Computed value is not affected after seeking forwards from ' + + '\'in effect\' to not \'in effect\' state'); + }); +}, 'Seeking to \'in effect\' from non-\'in effect\' (active -> after)'); + + </script> + </body> +</html> diff --git a/dom/animation/test/style/test_animation-setting-effect.html b/dom/animation/test/style/test_animation-setting-effect.html new file mode 100644 index 0000000000..8712072a51 --- /dev/null +++ b/dom/animation/test/style/test_animation-setting-effect.html @@ -0,0 +1,127 @@ +<!doctype html> +<html> + <head> + <meta charset=utf-8> + <title>Tests for setting effects by using Animation.effect</title> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src='../testcommon.js'></script> + </head> + <body> + <div id="log"></div> + <script type='text/javascript'> + +'use strict'; + +test(function(t) { + var target = addDiv(t); + var anim = new Animation(); + anim.effect = new KeyframeEffect(target, + { marginLeft: [ '0px', '100px' ] }, + 100 * MS_PER_SEC); + anim.currentTime = 50 * MS_PER_SEC; + assert_equals(getComputedStyle(target).marginLeft, '50px'); +}, 'After setting target effect on an animation with null effect, the ' + + 'animation still works'); + +test(function(t) { + var target = addDiv(t); + target.style.marginLeft = '10px'; + var anim = target.animate({ marginLeft: [ '0px', '100px' ] }, + 100 * MS_PER_SEC); + anim.currentTime = 50 * MS_PER_SEC; + assert_equals(getComputedStyle(target).marginLeft, '50px'); + + anim.effect = null; + assert_equals(getComputedStyle(target).marginLeft, '10px'); +}, 'After setting null target effect, the computed style of the target ' + + 'element becomes the initial value'); + +test(function(t) { + var target = addDiv(t); + var animA = target.animate({ marginLeft: [ '0px', '100px' ] }, + 100 * MS_PER_SEC); + var animB = new Animation(); + animA.currentTime = 50 * MS_PER_SEC; + animB.currentTime = 20 * MS_PER_SEC; + assert_equals(getComputedStyle(target).marginLeft, '50px', + 'original computed style of the target element'); + + animB.effect = animA.effect; + assert_equals(getComputedStyle(target).marginLeft, '20px', + 'new computed style of the target element'); +}, 'After setting the target effect from an existing animation, the computed ' + + 'style of the target effect should reflect the time of the updated ' + + 'animation.'); + +test(function(t) { + var target = addDiv(t); + target.style.marginTop = '-10px'; + var animA = target.animate({ marginLeft: [ '0px', '100px' ] }, + 100 * MS_PER_SEC); + var animB = target.animate({ marginTop: [ '0px', '100px' ] }, + 50 * MS_PER_SEC); + animA.currentTime = 50 * MS_PER_SEC; + animB.currentTime = 10 * MS_PER_SEC; + assert_equals(getComputedStyle(target).marginLeft, '50px', + 'original margin-left of the target element'); + assert_equals(getComputedStyle(target).marginTop, '20px', + 'original margin-top of the target element'); + + animB.effect = animA.effect; + assert_equals(getComputedStyle(target).marginLeft, '10px', + 'new margin-left of the target element'); + assert_equals(getComputedStyle(target).marginTop, '-10px', + 'new margin-top of the target element'); +}, 'After setting target effect with an animation to another animation which ' + + 'also has an target effect and both animation effects target to the same ' + + 'element, the computed style of this element should reflect the time and ' + + 'effect of the animation that was set'); + +test(function(t) { + var targetA = addDiv(t); + var targetB = addDiv(t); + targetB.style.marginLeft = '-10px'; + var animA = targetA.animate({ marginLeft: [ '0px', '100px' ] }, + 100 * MS_PER_SEC); + var animB = targetB.animate({ marginLeft: [ '0px', '100px' ] }, + 50 * MS_PER_SEC); + animA.currentTime = 50 * MS_PER_SEC; + animB.currentTime = 10 * MS_PER_SEC; + assert_equals(getComputedStyle(targetA).marginLeft, '50px', + 'original margin-left of the first element'); + assert_equals(getComputedStyle(targetB).marginLeft, '20px', + 'original margin-left of the second element'); + + animB.effect = animA.effect; + assert_equals(getComputedStyle(targetA).marginLeft, '10px', + 'new margin-left of the first element'); + assert_equals(getComputedStyle(targetB).marginLeft, '-10px', + 'new margin-left of the second element'); +}, 'After setting target effect with an animation to another animation which ' + + 'also has an target effect and these animation effects target to ' + + 'different elements, the computed styles of the two elements should ' + + 'reflect the time and effect of the animation that was set'); + +test(function(t) { + var target = addDiv(t); + var animA = target.animate({ marginLeft: [ '0px', '100px' ] }, + 50 * MS_PER_SEC); + var animB = target.animate({ marginTop: [ '0px', '50px' ] }, + 100 * MS_PER_SEC); + animA.currentTime = 20 * MS_PER_SEC; + animB.currentTime = 30 * MS_PER_SEC; + assert_equals(getComputedStyle(target).marginLeft, '40px'); + assert_equals(getComputedStyle(target).marginTop, '15px'); + + var effectA = animA.effect; + animA.effect = animB.effect; + animB.effect = effectA; + assert_equals(getComputedStyle(target).marginLeft, '60px'); + assert_equals(getComputedStyle(target).marginTop, '10px'); +}, 'After swapping effects of two playing animations, both animations are ' + + 'still running with the same current time'); + + </script> + </body> +</html> diff --git a/dom/animation/test/style/test_composite.html b/dom/animation/test/style/test_composite.html new file mode 100644 index 0000000000..1383b1b1e6 --- /dev/null +++ b/dom/animation/test/style/test_composite.html @@ -0,0 +1,142 @@ +<!doctype html> +<meta charset=utf-8> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../testcommon.js"></script> +<script src="/tests/SimpleTest/paint_listener.js"></script> +<style> +div { + /* Element needs geometry to be eligible for layerization */ + width: 20px; + height: 20px; + background-color: white; +} +</style> +<body> +<div id="log"></div> +<script> +'use strict'; + +if (!SpecialPowers.DOMWindowUtils.layerManagerRemote || + !SpecialPowers.getBoolPref( + 'layers.offmainthreadcomposition.async-animations')) { + // If OMTA is disabled, nothing to run. + done(); +} + +function waitForPaintsFlushed() { + return new Promise(function(resolve, reject) { + waitForAllPaintsFlushed(resolve); + }); +} + +promise_test(t => { + // Without this, the first test case fails on Android. + return waitForDocumentLoad(); +}, 'Ensure document has been loaded'); + +promise_test(t => { + var div; + return useTestRefreshMode(t).then(() => { + div = addDiv(t, { style: 'transform: translateX(100px)' }); + div.animate({ transform: ['translateX(0px)', 'translateX(200px)'], + composite: 'accumulate' }, + 100 * MS_PER_SEC); + + return waitForPaintsFlushed(); + }).then(() => { + SpecialPowers.DOMWindowUtils.advanceTimeAndRefresh(50 * MS_PER_SEC); + var transform = + SpecialPowers.DOMWindowUtils.getOMTAStyle(div, 'transform'); + assert_matrix_equals(transform, 'matrix(1, 0, 0, 1, 200, 0)', + 'Transform value at 50%'); + }); +}, 'Accumulate onto the base value'); + +promise_test(t => { + var div; + return useTestRefreshMode(t).then(() => { + div = addDiv(t); + div.animate({ transform: ['translateX(100px)', 'translateX(200px)'], + composite: 'replace' }, + 100 * MS_PER_SEC); + div.animate({ transform: ['translateX(0px)', 'translateX(100px)'], + composite: 'accumulate' }, + 100 * MS_PER_SEC); + + return waitForPaintsFlushed(); + }).then(() => { + SpecialPowers.DOMWindowUtils.advanceTimeAndRefresh(50 * MS_PER_SEC); + var transform = + SpecialPowers.DOMWindowUtils.getOMTAStyle(div, 'transform'); + assert_matrix_equals(transform, 'matrix(1, 0, 0, 1, 200, 0)', + 'Transform value at 50%'); + }); +}, 'Accumulate onto an underlying animation value'); + +promise_test(t => { + var div; + return useTestRefreshMode(t).then(() => { + div = addDiv(t, { style: 'transform: translateX(100px)' }); + div.animate([{ transform: 'translateX(100px)', composite: 'accumulate' }, + { transform: 'translateX(300px)', composite: 'replace' }], + 100 * MS_PER_SEC); + + return waitForPaintsFlushed(); + }).then(() => { + SpecialPowers.DOMWindowUtils.advanceTimeAndRefresh(50 * MS_PER_SEC); + var transform = + SpecialPowers.DOMWindowUtils.getOMTAStyle(div, 'transform'); + assert_matrix_equals(transform, 'matrix(1, 0, 0, 1, 250, 0)', + 'Transform value at 50s'); + }); +}, 'Composite when mixing accumulate and replace'); + +promise_test(t => { + var div; + return useTestRefreshMode(t).then(() => { + div = addDiv(t, { style: 'transform: translateX(100px)' }); + div.animate([{ transform: 'translateX(100px)', composite: 'replace' }, + { transform: 'translateX(300px)' }], + { duration: 100 * MS_PER_SEC, composite: 'accumulate' }); + return waitForPaintsFlushed(); + }).then(() => { + SpecialPowers.DOMWindowUtils.advanceTimeAndRefresh(50 * MS_PER_SEC); + var transform = + SpecialPowers.DOMWindowUtils.getOMTAStyle(div, 'transform'); + assert_matrix_equals(transform, 'matrix(1, 0, 0, 1, 250, 0)', + 'Transform value at 50%'); + }); +}, 'Composite specified on a keyframe overrides the composite mode of the ' + + 'effect'); + +promise_test(t => { + var div; + var anim; + return useTestRefreshMode(t).then(() => { + div = addDiv(t); + div.animate({ transform: [ 'scale(2)', 'scale(2)' ] }, 100 * MS_PER_SEC); + anim = div.animate({ transform: [ 'scale(4)', 'scale(4)' ] }, + { duration: 100 * MS_PER_SEC, composite: 'add' }); + + return waitForPaintsFlushed(); + }).then(() => { + var transform = + SpecialPowers.DOMWindowUtils.getOMTAStyle(div, 'transform'); + assert_matrix_equals(transform, 'matrix(8, 0, 0, 8, 0, 0)', + 'The additive scale value should be scale(8)'); // scale(2) scale(4) + + anim.effect.composite = 'accumulate'; + return waitForPaintsFlushed(); + }).then(() => { + SpecialPowers.DOMWindowUtils.advanceTimeAndRefresh(1); + var transform = + SpecialPowers.DOMWindowUtils.getOMTAStyle(div, 'transform'); + assert_matrix_equals(transform, 'matrix(5, 0, 0, 5, 0, 0)', + // (scale(2 - 1) + scale(4 - 1) + scale(1)) + 'The accumulate scale value should be scale(5)'); + }); +}, 'Composite operation change'); + +</script> +</body> diff --git a/dom/animation/test/style/test_interpolation-from-interpolatematrix-to-none.html b/dom/animation/test/style/test_interpolation-from-interpolatematrix-to-none.html new file mode 100644 index 0000000000..1da95392eb --- /dev/null +++ b/dom/animation/test/style/test_interpolation-from-interpolatematrix-to-none.html @@ -0,0 +1,43 @@ +<!doctype html> +<meta charset=utf-8> +<script src='/resources/testharness.js'></script> +<script src='/resources/testharnessreport.js'></script> +<script src='../testcommon.js'></script> +<div id='log'></div> +<script type='text/javascript'> +'use strict'; + +test(function(t) { + var target = addDiv(t); + target.style.transform = 'translateX(100px)'; + target.style.transition = 'all 10s linear -5s'; + getComputedStyle(target).transform; + + target.style.transform = 'rotate(90deg)'; + var interpolated_matrix = 'matrix(' + Math.cos(Math.PI / 4) + ',' + + Math.sin(Math.PI / 4) + ',' + + -Math.sin(Math.PI / 4) + ',' + + Math.cos(Math.PI / 4) + ',' + + '50, 0)'; + assert_matrix_equals(getComputedStyle(target).transform, + interpolated_matrix, + 'the equivalent matrix of ' + 'interpolatematrix(' + + 'translateX(100px), rotate(90deg), 0.5)'); + + // Trigger a new transition from + // interpolatematrix(translateX(100px), rotate(90deg), 0.5) to none + // with 'all 10s linear -5s'. + target.style.transform = 'none'; + interpolated_matrix = 'matrix(' + Math.cos(Math.PI / 8) + ',' + + Math.sin(Math.PI / 8) + ',' + + -Math.sin(Math.PI / 8) + ',' + + Math.cos(Math.PI / 8) + ',' + + '25, 0)'; + assert_matrix_equals(getComputedStyle(target).transform, + interpolated_matrix, + 'the expected matrix from interpolatematrix(' + + 'translateX(100px), rotate(90deg), 0.5) to none at 50%'); +}, 'Test interpolation from interpolatematrix to none at 50%'); + +</script> +</html> diff --git a/dom/animation/test/style/test_missing-keyframe-on-compositor.html b/dom/animation/test/style/test_missing-keyframe-on-compositor.html new file mode 100644 index 0000000000..8b92a89168 --- /dev/null +++ b/dom/animation/test/style/test_missing-keyframe-on-compositor.html @@ -0,0 +1,577 @@ +<!doctype html> +<meta charset=utf-8> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../testcommon.js"></script> +<script src="/tests/SimpleTest/paint_listener.js"></script> +<style> +div { + /* Element needs geometry to be eligible for layerization */ + width: 100px; + height: 100px; + background-color: white; +} +</style> +<body> +<div id="log"></div> +<script> +'use strict'; + +if (!SpecialPowers.DOMWindowUtils.layerManagerRemote || + !SpecialPowers.getBoolPref( + 'layers.offmainthreadcomposition.async-animations')) { + // If OMTA is disabled, nothing to run. + done(); +} + +function waitForPaintsFlushed() { + return new Promise(function(resolve, reject) { + waitForAllPaintsFlushed(resolve); + }); +} + +// Note that promise tests run in sequence so this ensures the document is +// loaded before any of the other tests run. +promise_test(t => { + // Without this, the first test case fails on Android. + return waitForDocumentLoad(); +}, 'Ensure document has been loaded'); + +promise_test(t => { + var div; + return useTestRefreshMode(t).then(() => { + div = addDiv(t, { style: 'opacity: 0.1' }); + div.animate({ opacity: 1 }, 100 * MS_PER_SEC); + return waitForPaintsFlushed(); + }).then(() => { + var opacity = + SpecialPowers.DOMWindowUtils.getOMTAStyle(div, 'opacity'); + assert_equals(opacity, '0.1', + 'The initial opacity value should be the base value'); + }); +}, 'Initial opacity value for animation with no no keyframe at offset 0'); + +promise_test(t => { + var div; + return useTestRefreshMode(t).then(() => { + div = addDiv(t, { style: 'opacity: 0.1' }); + div.animate({ opacity: [ 0.5, 1 ] }, 100 * MS_PER_SEC); + div.animate({ opacity: 1 }, 100 * MS_PER_SEC); + + return waitForPaintsFlushed(); + }).then(() => { + var opacity = + SpecialPowers.DOMWindowUtils.getOMTAStyle(div, 'opacity'); + assert_equals(opacity, '0.5', + 'The initial opacity value should be the value of ' + + 'lower-priority animation value'); + }); +}, 'Initial opacity value for animation with no keyframe at offset 0 when ' + + 'there is a lower-priority animation'); + +promise_test(t => { + var div; + return useTestRefreshMode(t).then(() => { + div = addDiv(t, { style: 'opacity: 0.1; transition: opacity 100s linear' }); + getComputedStyle(div).opacity; + + div.style.opacity = '0.5'; + getComputedStyle(div).opacity; + + div.animate({ opacity: 1 }, 100 * MS_PER_SEC); + + return waitForPaintsFlushed(); + }).then(() => { + var opacity = + SpecialPowers.DOMWindowUtils.getOMTAStyle(div, 'opacity'); + assert_equals(opacity, '0.1', + 'The initial opacity value should be the initial value of ' + + 'the transition'); + }); +}, 'Initial opacity value for animation with no keyframe at offset 0 when ' + + 'there is a transition on the same property'); + +promise_test(t => { + var div; + return useTestRefreshMode(t).then(() => { + div = addDiv(t, { style: 'opacity: 0' }); + div.animate([{ offset: 0, opacity: 1 }], 100 * MS_PER_SEC); + + return waitForPaintsFlushed(); + }).then(() => { + SpecialPowers.DOMWindowUtils.advanceTimeAndRefresh(50 * MS_PER_SEC); + + var opacity = + SpecialPowers.DOMWindowUtils.getOMTAStyle(div, 'opacity'); + assert_equals(opacity, '0.5', + 'Opacity value at 50% should be composed onto the base ' + + 'value'); + }); +}, 'Opacity value for animation with no keyframe at offset 1 at 50% '); + +promise_test(t => { + var div; + return useTestRefreshMode(t).then(() => { + div = addDiv(t, { style: 'opacity: 0' }); + div.animate({ opacity: [ 0.5, 0.5 ] }, 100 * MS_PER_SEC); + div.animate([{ offset: 0, opacity: 1 }], 100 * MS_PER_SEC); + + return waitForPaintsFlushed(); + }).then(() => { + SpecialPowers.DOMWindowUtils.advanceTimeAndRefresh(50 * MS_PER_SEC); + + var opacity = + SpecialPowers.DOMWindowUtils.getOMTAStyle(div, 'opacity'); + assert_equals(opacity, '0.75', // (0.5 + 1) * 0.5 + 'Opacity value at 50% should be composed onto the value ' + + 'of middle of lower-priority animation'); + }); +}, 'Opacity value for animation with no keyframe at offset 1 at 50% when ' + + 'there is a lower-priority animation'); + +promise_test(t => { + var div; + return useTestRefreshMode(t).then(() => { + div = addDiv(t, { style: 'opacity: 0; transition: opacity 100s linear' }); + getComputedStyle(div).opacity; + + div.style.opacity = '0.5'; + getComputedStyle(div).opacity; + + div.animate([{ offset: 0, opacity: 1 }], 100 * MS_PER_SEC); + + return waitForPaintsFlushed(); + }).then(() => { + SpecialPowers.DOMWindowUtils.advanceTimeAndRefresh(50 * MS_PER_SEC); + + var opacity = + SpecialPowers.DOMWindowUtils.getOMTAStyle(div, 'opacity'); + assert_equals(opacity, '0.625', // ((0 + 0.5) * 0.5 + 1) * 0.5 + 'Opacity value at 50% should be composed onto the value ' + + 'of middle of transition'); + }); +}, 'Opacity value for animation with no keyframe at offset 1 at 50% when ' + + 'there is a transition on the same property'); + +promise_test(t => { + var div; + var lowerAnimation; + return useTestRefreshMode(t).then(() => { + div = addDiv(t); + lowerAnimation = div.animate({ opacity: [ 0.5, 1 ] }, 100 * MS_PER_SEC); + var higherAnimation = div.animate({ opacity: 1 }, 100 * MS_PER_SEC); + + return waitForPaintsFlushed(); + }).then(() => { + lowerAnimation.pause(); + return waitForPaintsFlushed(); + }).then(() => { + SpecialPowers.DOMWindowUtils.advanceTimeAndRefresh(50 * MS_PER_SEC); + + var opacity = + SpecialPowers.DOMWindowUtils.getOMTAStyle(div, 'opacity'); + // The underlying value is the value that is staying at 0ms of the + // lowerAnimation, that is 0.5. + // (0.5 + 1.0) * (50 * MS_PER_SEC / 100 * MS_PER_SEC) = 0.75. + assert_equals(opacity, '0.75', + 'Composed opacity value should be composed onto the value ' + + 'of lower-priority paused animation'); + }); +}, 'Opacity value for animation with no keyframe at offset 0 at 50% when ' + + 'composed onto a paused underlying animation'); + +promise_test(t => { + var div; + var lowerAnimation; + return useTestRefreshMode(t).then(() => { + div = addDiv(t); + lowerAnimation = div.animate({ opacity: [ 0.5, 1 ] }, 100 * MS_PER_SEC); + var higherAnimation = div.animate({ opacity: 1 }, 100 * MS_PER_SEC); + + return waitForPaintsFlushed(); + }).then(() => { + lowerAnimation.playbackRate = 0; + return waitForPaintsFlushed(); + }).then(() => { + SpecialPowers.DOMWindowUtils.advanceTimeAndRefresh(50 * MS_PER_SEC); + + var opacity = + SpecialPowers.DOMWindowUtils.getOMTAStyle(div, 'opacity'); + // The underlying value is the value that is staying at 0ms of the + // lowerAnimation, that is 0.5. + // (0.5 + 1.0) * (50 * MS_PER_SEC / 100 * MS_PER_SEC) = 0.75. + assert_equals(opacity, '0.75', + 'Composed opacity value should be composed onto the value ' + + 'of lower-priority zero playback rate animation'); + }); +}, 'Opacity value for animation with no keyframe at offset 0 at 50% when ' + + 'composed onto a zero playback rate underlying animation'); + +promise_test(t => { + var div; + var lowerAnimation; + return useTestRefreshMode(t).then(() => { + div = addDiv(t); + lowerAnimation = div.animate({ opacity: [ 1, 0.5 ] }, 100 * MS_PER_SEC); + var higherAnimation = div.animate({ opacity: 1 }, 100 * MS_PER_SEC); + + return waitForPaintsFlushed(); + }).then(() => { + lowerAnimation.effect.updateTiming({ + duration: 0, + fill: 'forwards', + }); + return waitForPaintsFlushed(); + }).then(() => { + SpecialPowers.DOMWindowUtils.advanceTimeAndRefresh(50 * MS_PER_SEC); + + var opacity = + SpecialPowers.DOMWindowUtils.getOMTAStyle(div, 'opacity'); + // The underlying value is the value that is filling forwards state of the + // lowerAnimation, that is 0.5. + // (0.5 + 1.0) * (50 * MS_PER_SEC / 100 * MS_PER_SEC) = 0.75. + assert_equals(opacity, '0.75', + 'Composed opacity value should be composed onto the value ' + + 'of lower-priority zero active duration animation'); + }); +}, 'Opacity value for animation with no keyframe at offset 0 at 50% when ' + + 'composed onto a zero active duration underlying animation'); + +promise_test(t => { + var div; + return useTestRefreshMode(t).then(() => { + div = addDiv(t, { style: 'transform: translateX(100px)' }); + div.animate({ transform: 'translateX(200px)' }, 100 * MS_PER_SEC); + + return waitForPaintsFlushed(); + }).then(() => { + var transform = + SpecialPowers.DOMWindowUtils.getOMTAStyle(div, 'transform'); + assert_matrix_equals(transform, 'matrix(1, 0, 0, 1, 100, 0)', + 'The initial transform value should be the base value'); + }); +}, 'Initial transform value for animation with no keyframe at offset 0'); + +promise_test(t => { + var div; + return useTestRefreshMode(t).then(() => { + div = addDiv(t, { style: 'transform: translateX(100px)' }); + div.animate({ transform: [ 'translateX(200px)', 'translateX(300px)' ] }, + 100 * MS_PER_SEC); + div.animate({ transform: 'translateX(400px)' }, 100 * MS_PER_SEC); + + return waitForPaintsFlushed(); + }).then(() => { + var transform = + SpecialPowers.DOMWindowUtils.getOMTAStyle(div, 'transform'); + assert_matrix_equals(transform, 'matrix(1, 0, 0, 1, 200, 0)', + 'The initial transform value should be lower-priority animation value'); + }); +}, 'Initial transform value for animation with no keyframe at offset 0 when ' + + 'there is a lower-priority animation'); + +promise_test(t => { + var div; + return useTestRefreshMode(t).then(() => { + div = addDiv(t, { style: 'transform: translateX(100px);' + + 'transition: transform 100s linear' }); + getComputedStyle(div).transform; + + div.style.transform = 'translateX(200px)'; + getComputedStyle(div).transform; + + div.animate({ transform: 'translateX(400px)' }, 100 * MS_PER_SEC); + + return waitForPaintsFlushed(); + }).then(() => { + var transform = + SpecialPowers.DOMWindowUtils.getOMTAStyle(div, 'transform'); + assert_matrix_equals(transform, 'matrix(1, 0, 0, 1, 100, 0)', + 'The initial transform value should be the initial value of the ' + + 'transition'); + }); +}, 'Initial transform value for animation with no keyframe at offset 0 when ' + + 'there is a transition'); + +promise_test(t => { + var div; + return useTestRefreshMode(t).then(() => { + div = addDiv(t, { style: 'transform: translateX(100px)' }); + div.animate([{ offset: 0, transform: 'translateX(200pX)' }], + 100 * MS_PER_SEC); + + return waitForPaintsFlushed(); + }).then(() => { + SpecialPowers.DOMWindowUtils.advanceTimeAndRefresh(50 * MS_PER_SEC); + + var transform = + SpecialPowers.DOMWindowUtils.getOMTAStyle(div, 'transform'); + assert_matrix_equals(transform, 'matrix(1, 0, 0, 1, 150, 0)', + 'Transform value at 50% should be the base value'); + }); +}, 'Transform value for animation with no keyframe at offset 1 at 50%'); + +promise_test(t => { + var div; + return useTestRefreshMode(t).then(() => { + div = addDiv(t, { style: 'transform: translateX(100px)' }); + div.animate({ transform: [ 'translateX(200px)', 'translateX(200px)' ] }, + 100 * MS_PER_SEC); + div.animate([{ offset: 0, transform: 'translateX(300px)' }], + 100 * MS_PER_SEC); + + return waitForPaintsFlushed(); + }).then(() => { + SpecialPowers.DOMWindowUtils.advanceTimeAndRefresh(50 * MS_PER_SEC); + + var transform = + SpecialPowers.DOMWindowUtils.getOMTAStyle(div, 'transform'); + assert_matrix_equals(transform, 'matrix(1, 0, 0, 1, 250, 0)', + 'The final transform value should be the base value'); + }); +}, 'Transform value for animation with no keyframe at offset 1 at 50% when ' + + 'there is a lower-priority animation'); + +promise_test(t => { + var div; + return useTestRefreshMode(t).then(() => { + div = addDiv(t, { style: 'transform: translateX(100px);' + + 'transition: transform 100s linear' }); + getComputedStyle(div).transform; + + div.style.transform = 'translateX(200px)'; + getComputedStyle(div).transform; + + div.animate([{ offset: 0, transform: 'translateX(300px)' }], + 100 * MS_PER_SEC); + + return waitForPaintsFlushed(); + }).then(() => { + SpecialPowers.DOMWindowUtils.advanceTimeAndRefresh(50 * MS_PER_SEC); + + var transform = + SpecialPowers.DOMWindowUtils.getOMTAStyle(div, 'transform'); + // (150px + 300px) * 0.5 + assert_matrix_equals(transform, 'matrix(1, 0, 0, 1, 225, 0)', + 'The final transform value should be the final value of the transition'); + }); +}, 'Transform value for animation with no keyframe at offset 1 at 50% when ' + + 'there is a transition'); + +promise_test(t => { + var div; + var lowerAnimation; + return useTestRefreshMode(t).then(() => { + div = addDiv(t); + lowerAnimation = + div.animate({ transform: [ 'translateX(100px)', 'translateX(200px)' ] }, + 100 * MS_PER_SEC); + var higherAnimation = div.animate({ transform: 'translateX(300px)' }, + 100 * MS_PER_SEC); + + return waitForPaintsFlushed(); + }).then(() => { + lowerAnimation.pause(); + return waitForPaintsFlushed(); + }).then(() => { + SpecialPowers.DOMWindowUtils.advanceTimeAndRefresh(50 * MS_PER_SEC); + + var transform = + SpecialPowers.DOMWindowUtils.getOMTAStyle(div, 'transform'); + // The underlying value is the value that is staying at 0ms of the + // lowerAnimation, that is 100px. + // (100px + 300px) * (50 * MS_PER_SEC / 100 * MS_PER_SEC) = 200px. + assert_matrix_equals(transform, 'matrix(1, 0, 0, 1, 200, 0)', + 'Composed transform value should be composed onto the value of ' + + 'lower-priority paused animation'); + }); +}, 'Transform value for animation with no keyframe at offset 0 at 50% when ' + + 'composed onto a paused underlying animation'); + +promise_test(t => { + var div; + var lowerAnimation; + return useTestRefreshMode(t).then(() => { + div = addDiv(t); + lowerAnimation = + div.animate({ transform: [ 'translateX(100px)', 'translateX(200px)' ] }, + 100 * MS_PER_SEC); + var higherAnimation = div.animate({ transform: 'translateX(300px)' }, + 100 * MS_PER_SEC); + + return waitForPaintsFlushed(); + }).then(() => { + lowerAnimation.playbackRate = 0; + return waitForPaintsFlushed(); + }).then(() => { + SpecialPowers.DOMWindowUtils.advanceTimeAndRefresh(50 * MS_PER_SEC); + + var transform = + SpecialPowers.DOMWindowUtils.getOMTAStyle(div, 'transform'); + // The underlying value is the value that is staying at 0ms of the + // lowerAnimation, that is 100px. + // (100px + 300px) * (50 * MS_PER_SEC / 100 * MS_PER_SEC) = 200px. + assert_matrix_equals(transform, 'matrix(1, 0, 0, 1, 200, 0)', + 'Composed transform value should be composed onto the value of ' + + 'lower-priority zero playback rate animation'); + }); +}, 'Transform value for animation with no keyframe at offset 0 at 50% when ' + + 'composed onto a zero playback rate underlying animation'); + +promise_test(t => { + var div; + return useTestRefreshMode(t).then(() => { + div = addDiv(t); + var lowerAnimation = + div.animate({ transform: [ 'translateX(100px)', 'translateX(200px)' ] }, + { duration: 10 * MS_PER_SEC, + fill: 'forwards' }); + var higherAnimation = div.animate({ transform: 'translateX(300px)' }, + 100 * MS_PER_SEC); + + return waitForPaintsFlushed(); + }).then(() => { + SpecialPowers.DOMWindowUtils.advanceTimeAndRefresh(50 * MS_PER_SEC); + + // We need to wait for a paint so that we can send the state of the lower + // animation that is actually finished at this point. + return waitForPaintsFlushed(); + }).then(() => { + var transform = + SpecialPowers.DOMWindowUtils.getOMTAStyle(div, 'transform'); + // (200px + 300px) * (50 * MS_PER_SEC / 100 * MS_PER_SEC) = 250px. + assert_matrix_equals(transform, 'matrix(1, 0, 0, 1, 250, 0)', + 'Composed transform value should be composed onto the value of ' + + 'lower-priority animation with fill:forwards'); + }); +}, 'Transform value for animation with no keyframe at offset 0 at 50% when ' + + 'composed onto a underlying animation with fill:forwards'); + +promise_test(t => { + var div; + return useTestRefreshMode(t).then(() => { + div = addDiv(t); + var lowerAnimation = + div.animate({ transform: [ 'translateX(100px)', 'translateX(200px)' ] }, + { duration: 10 * MS_PER_SEC, + endDelay: -5 * MS_PER_SEC, + fill: 'forwards' }); + var higherAnimation = div.animate({ transform: 'translateX(300px)' }, + 100 * MS_PER_SEC); + + return waitForPaintsFlushed(); + }).then(() => { + SpecialPowers.DOMWindowUtils.advanceTimeAndRefresh(50 * MS_PER_SEC); + + // We need to wait for a paint just like the above test. + return waitForPaintsFlushed(); + }).then(() => { + var transform = + SpecialPowers.DOMWindowUtils.getOMTAStyle(div, 'transform'); + // (150px + 300px) * (50 * MS_PER_SEC / 100 * MS_PER_SEC) = 225px. + assert_matrix_equals(transform, 'matrix(1, 0, 0, 1, 225, 0)', + 'Composed transform value should be composed onto the value of ' + + 'lower-priority animation with fill:forwards and negative endDelay'); + }); +}, 'Transform value for animation with no keyframe at offset 0 at 50% when ' + + 'composed onto a underlying animation with fill:forwards and negative ' + + 'endDelay'); + +promise_test(t => { + var div; + return useTestRefreshMode(t).then(() => { + div = addDiv(t); + var lowerAnimation = + div.animate({ transform: [ 'translateX(100px)', 'translateX(200px)' ] }, + { duration: 10 * MS_PER_SEC, + endDelay: 100 * MS_PER_SEC, + fill: 'forwards' }); + var higherAnimation = div.animate({ transform: 'translateX(300px)' }, + 100 * MS_PER_SEC); + + return waitForPaintsFlushed(); + }).then(() => { + SpecialPowers.DOMWindowUtils.advanceTimeAndRefresh(50 * MS_PER_SEC); + + var transform = + SpecialPowers.DOMWindowUtils.getOMTAStyle(div, 'transform'); + // (200px + 300px) * 0.5 + assert_matrix_equals(transform, 'matrix(1, 0, 0, 1, 250, 0)', + 'Composed transform value should be composed onto the value of ' + + 'lower-priority animation with fill:forwards during positive endDelay'); + }); +}, 'Transform value for animation with no keyframe at offset 0 at 50% when ' + + 'composed onto a underlying animation with fill:forwards during positive ' + + 'endDelay'); + +promise_test(t => { + var div; + return useTestRefreshMode(t).then(() => { + div = addDiv(t, { style: 'transform: translateX(100px)' }); + div.animate({ transform: 'translateX(200px)' }, + { duration: 100 * MS_PER_SEC, delay: 50 * MS_PER_SEC }); + + return waitForPaintsFlushed(); + }).then(() => { + SpecialPowers.DOMWindowUtils.advanceTimeAndRefresh(100 * MS_PER_SEC); + + var transform = + SpecialPowers.DOMWindowUtils.getOMTAStyle(div, 'transform'); + assert_matrix_equals(transform, 'matrix(1, 0, 0, 1, 150, 0)', + 'Transform value for animation with positive delay should be composed ' + + 'onto the base style'); + }); +}, 'Transform value for animation with no keyframe at offset 0 and with ' + + 'positive delay'); + +promise_test(t => { + var div; + return useTestRefreshMode(t).then(() => { + div = addDiv(t, { style: 'transform: translateX(100px)' }); + + div.animate([{ offset: 0, transform: 'translateX(200px)'}], + { duration: 100 * MS_PER_SEC, + iterationStart: 1, + iterationComposite: 'accumulate' }); + + return waitForPaintsFlushed(); + }).then(() => { + var transform = + SpecialPowers.DOMWindowUtils.getOMTAStyle(div, 'transform'); + assert_matrix_equals(transform, 'matrix(1, 0, 0, 1, 300, 0)', + 'Transform value for animation with no keyframe at offset 1 and its ' + + 'iterationComposite is accumulate'); + }); +}, 'Transform value for animation with no keyframe at offset 1 and its ' + + 'iterationComposite is accumulate'); + +promise_test(t => { + var div; + return useTestRefreshMode(t).then(() => { + div = addDiv(t); + var lowerAnimation = + div.animate({ transform: [ 'translateX(100px)', 'translateX(200px)' ] }, + 100 * MS_PER_SEC); + var higherAnimation = div.animate({ transform: 'translateX(300px)' }, + 100 * MS_PER_SEC); + + lowerAnimation.timeline = null; + // Set current time at 50% duration. + lowerAnimation.currentTime = 50 * MS_PER_SEC; + + return waitForPaintsFlushed(); + }).then(() => { + SpecialPowers.DOMWindowUtils.advanceTimeAndRefresh(50 * MS_PER_SEC); + + var transform = + SpecialPowers.DOMWindowUtils.getOMTAStyle(div, 'transform'); + // (150px + 300px) * (50 * MS_PER_SEC / 100 * MS_PER_SEC) = 225px. + assert_matrix_equals(transform, 'matrix(1, 0, 0, 1, 225, 0)', + 'Composed transform value should be composed onto the value of ' + + 'lower-priority animation without timeline'); + }); +}, 'Transform value for animation with no keyframe at offset 0 at 50% when ' + + 'composed onto an animation without timeline'); + +</script> +</body> diff --git a/dom/animation/test/style/test_missing-keyframe.html b/dom/animation/test/style/test_missing-keyframe.html new file mode 100644 index 0000000000..4047e62408 --- /dev/null +++ b/dom/animation/test/style/test_missing-keyframe.html @@ -0,0 +1,110 @@ +<!doctype html> +<meta charset=utf-8> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../testcommon.js"></script> +<body> +<div id="log"></div> +<script> +'use strict'; + +test(t => { + var div = addDiv(t, { style: 'margin-left: 100px' }); + div.animate([{ marginLeft: '200px' }], 100 * MS_PER_SEC); + + assert_equals(getComputedStyle(div).marginLeft, '100px', + 'The initial margin-left value should be the base value'); +}, 'Initial margin-left value for an animation with no keyframe at offset 0'); + +test(t => { + var div = addDiv(t, { style: 'margin-left: 100px' }); + div.animate([{ offset: 0, marginLeft: '200px' }, + { offset: 1, marginLeft: '300px' }], + 100 * MS_PER_SEC); + div.animate([{ marginLeft: '200px' }], 100 * MS_PER_SEC); + + assert_equals(getComputedStyle(div).marginLeft, '200px', + 'The initial margin-left value should be the initial value ' + + 'of lower-priority animation'); +}, 'Initial margin-left value for an animation with no keyframe at offset 0 ' + + 'is that of lower-priority animations'); + +test(t => { + var div = addDiv(t, { style: 'margin-left: 100px;' + + 'transition: margin-left 100s -50s linear'}); + flushComputedStyle(div); + + div.style.marginLeft = '200px'; + flushComputedStyle(div); + + div.animate([{ marginLeft: '300px' }], 100 * MS_PER_SEC); + + assert_equals(getComputedStyle(div).marginLeft, '150px', + 'The initial margin-left value should be the initial value ' + + 'of the transition'); +}, 'Initial margin-left value for an animation with no keyframe at offset 0 ' + + 'is that of transition'); + +test(t => { + var div = addDiv(t, { style: 'margin-left: 100px' }); + var animation = div.animate([{ offset: 0, marginLeft: '200px' }], + 100 * MS_PER_SEC); + + animation.currentTime = 50 * MS_PER_SEC; + assert_equals(getComputedStyle(div).marginLeft, '150px', + 'The margin-left value at 50% should be the base value'); +}, 'margin-left value at 50% for an animation with no keyframe at offset 1'); + +test(t => { + var div = addDiv(t, { style: 'margin-left: 100px' }); + var lowerAnimation = div.animate([{ offset: 0, marginLeft: '200px' }, + { offset: 1, marginLeft: '300px' }], + 100 * MS_PER_SEC); + var higherAnimation = div.animate([{ offset: 0, marginLeft: '400px' }], + 100 * MS_PER_SEC); + + lowerAnimation.currentTime = 50 * MS_PER_SEC; + higherAnimation.currentTime = 50 * MS_PER_SEC; + // (250px + 400px) * 0.5 + assert_equals(getComputedStyle(div).marginLeft, '325px', + 'The margin-left value at 50% should be additive value of ' + + 'lower-priority animation and higher-priority animation'); +}, 'margin-left value at 50% for an animation with no keyframe at offset 1 ' + + 'is that of lower-priority animations'); + +test(t => { + var div = addDiv(t, { style: 'margin-left: 100px;' + + 'transition: margin-left 100s linear' }); + flushComputedStyle(div); + + div.style.marginLeft = '300px'; + flushComputedStyle(div); + + div.animate([{ offset: 0, marginLeft: '200px' }], 100 * MS_PER_SEC); + + div.getAnimations().forEach(animation => { + animation.currentTime = 50 * MS_PER_SEC; + }); + // (200px + 200px) * 0.5 + assert_equals(getComputedStyle(div).marginLeft, '200px', + 'The margin-left value at 50% should be additive value of ' + + 'the transition and animation'); +}, 'margin-left value at 50% for an animation with no keyframe at offset 1 ' + + 'is that of transition'); + +test(t => { + var div = addDiv(t, { style: 'margin-left: 100px' }); + + var animation = div.animate([{ offset: 0, marginLeft: '200px' }], + { duration: 100 * MS_PER_SEC, + iterationStart: 1, + iterationComposite: 'accumulate' }); + + assert_equals(getComputedStyle(div).marginLeft, '300px', + 'The margin-left value should be additive value of the ' + + 'accumulation of the initial value onto the base value '); +}, 'margin-left value for an animation with no keyframe at offset 1 and its ' + + 'iterationComposite is accumulate'); + +</script> +</body> diff --git a/dom/animation/test/style/test_transform-non-normalizable-rotate3d.html b/dom/animation/test/style/test_transform-non-normalizable-rotate3d.html new file mode 100644 index 0000000000..ad2584ac40 --- /dev/null +++ b/dom/animation/test/style/test_transform-non-normalizable-rotate3d.html @@ -0,0 +1,28 @@ +<!doctype html> +<meta charset=utf-8> +<script src='/resources/testharness.js'></script> +<script src='/resources/testharnessreport.js'></script> +<script src='../testcommon.js'></script> +<div id='log'></div> +<script type='text/javascript'> +'use strict'; + +test(function(t) { + var target = addDiv(t); + target.style.transform = 'rotate3d(0, 0, 1, 90deg)'; + target.style.transition = 'all 10s linear -5s'; + getComputedStyle(target).transform; + + target.style.transform = 'rotate3d(0, 0, 0, 270deg)'; + var interpolated_matrix = 'matrix(' + Math.cos(Math.PI / 4) + ',' + + Math.sin(Math.PI / 4) + ',' + + -Math.sin(Math.PI / 4) + ',' + + Math.cos(Math.PI / 4) + ',' + + '0, 0)'; + assert_matrix_equals(getComputedStyle(target).transform, interpolated_matrix, + 'transition from a normal rotate3d to a ' + + 'non-normalizable rotate3d'); +}, 'Test interpolation on non-normalizable rotate3d function'); + +</script> +</html> diff --git a/dom/animation/test/testcommon.js b/dom/animation/test/testcommon.js new file mode 100644 index 0000000000..81232c5a81 --- /dev/null +++ b/dom/animation/test/testcommon.js @@ -0,0 +1,512 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Use this variable if you specify duration or some other properties + * for script animation. + * E.g., div.animate({ opacity: [0, 1] }, 100 * MS_PER_SEC); + * + * NOTE: Creating animations with short duration may cause intermittent + * failures in asynchronous test. For example, the short duration animation + * might be finished when animation.ready has been fulfilled because of slow + * platforms or busyness of the main thread. + * Setting short duration to cancel its animation does not matter but + * if you don't want to cancel the animation, consider using longer duration. + */ +const MS_PER_SEC = 1000; + +/* The recommended minimum precision to use for time values[1]. + * + * [1] https://drafts.csswg.org/web-animations/#precision-of-time-values + */ +var TIME_PRECISION = 0.0005; // ms + +/* + * Allow implementations to substitute an alternative method for comparing + * times based on their precision requirements. + */ +function assert_times_equal(actual, expected, description) { + assert_approx_equals(actual, expected, TIME_PRECISION * 2, description); +} + +/* + * Compare a time value based on its precision requirements with a fixed value. + */ +function assert_time_equals_literal(actual, expected, description) { + assert_approx_equals(actual, expected, TIME_PRECISION, description); +} + +/* + * Compare matrix string like 'matrix(1, 0, 0, 1, 100, 0)'. + * This function allows error, 0.01, because on Android when we are scaling down + * the document, it results in some errors. + */ +function assert_matrix_equals(actual, expected, description) { + var matrixRegExp = /^matrix\((.+),(.+),(.+),(.+),(.+),(.+)\)/; + assert_regexp_match(actual, matrixRegExp, "Actual value should be a matrix"); + assert_regexp_match( + expected, + matrixRegExp, + "Expected value should be a matrix" + ); + + var actualMatrixArray = actual.match(matrixRegExp).slice(1).map(Number); + var expectedMatrixArray = expected.match(matrixRegExp).slice(1).map(Number); + + assert_equals( + actualMatrixArray.length, + expectedMatrixArray.length, + "Array lengths should be equal (got '" + + expected + + "' and '" + + actual + + "'): " + + description + ); + for (var i = 0; i < actualMatrixArray.length; i++) { + assert_approx_equals( + actualMatrixArray[i], + expectedMatrixArray[i], + 0.01, + "Matrix array should be equal (got '" + + expected + + "' and '" + + actual + + "'): " + + description + ); + } +} + +/** + * Compare given values which are same format of + * KeyframeEffectReadonly::GetProperties. + */ +function assert_properties_equal(actual, expected) { + assert_equals(actual.length, expected.length); + + const compareProperties = (a, b) => + a.property == b.property ? 0 : a.property < b.property ? -1 : 1; + + const sortedActual = actual.sort(compareProperties); + const sortedExpected = expected.sort(compareProperties); + + const serializeValues = values => + values + .map( + value => + "{ " + + ["offset", "value", "easing", "composite"] + .map(member => `${member}: ${value[member]}`) + .join(", ") + + " }" + ) + .join(", "); + + for (let i = 0; i < sortedActual.length; i++) { + assert_equals( + sortedActual[i].property, + sortedExpected[i].property, + "CSS property name should match" + ); + assert_equals( + serializeValues(sortedActual[i].values), + serializeValues(sortedExpected[i].values), + `Values arrays do not match for ` + `${sortedActual[i].property} property` + ); + } +} + +/** + * Construct a object which is same to a value of + * KeyframeEffectReadonly::GetProperties(). + * The method returns undefined as a value in case of missing keyframe. + * Therefor, we can use undefined for |value| and |easing| parameter. + * @param offset - keyframe offset. e.g. 0.1 + * @param value - any keyframe value. e.g. undefined '1px', 'center', 0.5 + * @param composite - 'replace', 'add', 'accumulate' + * @param easing - e.g. undefined, 'linear', 'ease' and so on + * @return Object - + * e.g. { offset: 0.1, value: '1px', composite: 'replace', easing: 'ease'} + */ +function valueFormat(offset, value, composite, easing) { + return { offset, value, easing, composite }; +} + +/** + * Appends a div to the document body and creates an animation on the div. + * NOTE: This function asserts when trying to create animations with durations + * shorter than 100s because the shorter duration may cause intermittent + * failures. If you are not sure how long it is suitable, use 100s; it's + * long enough but shorter than our test framework timeout (330s). + * If you really need to use shorter durations, use animate() function directly. + * + * @param t The testharness.js Test object. If provided, this will be used + * to register a cleanup callback to remove the div when the test + * finishes. + * @param attrs A dictionary object with attribute names and values to set on + * the div. + * @param frames The keyframes passed to Element.animate(). + * @param options The options passed to Element.animate(). + */ +function addDivAndAnimate(t, attrs, frames, options) { + let animDur = typeof options === "object" ? options.duration : options; + assert_greater_than_equal( + animDur, + 100 * MS_PER_SEC, + "Clients of this addDivAndAnimate API must request a duration " + + "of at least 100s, to avoid intermittent failures from e.g." + + "the main thread being busy for an extended period" + ); + + return addDiv(t, attrs).animate(frames, options); +} + +/** + * Appends a div to the document body. + * + * @param t The testharness.js Test object. If provided, this will be used + * to register a cleanup callback to remove the div when the test + * finishes. + * + * @param attrs A dictionary object with attribute names and values to set on + * the div. + */ +function addDiv(t, attrs) { + var div = document.createElement("div"); + if (attrs) { + for (var attrName in attrs) { + div.setAttribute(attrName, attrs[attrName]); + } + } + document.body.appendChild(div); + if (t && typeof t.add_cleanup === "function") { + t.add_cleanup(function () { + if (div.parentNode) { + div.remove(); + } + }); + } + return div; +} + +/** + * Appends a style div to the document head. + * + * @param t The testharness.js Test object. If provided, this will be used + * to register a cleanup callback to remove the style element + * when the test finishes. + * + * @param rules A dictionary object with selector names and rules to set on + * the style sheet. + */ +function addStyle(t, rules) { + var extraStyle = document.createElement("style"); + document.head.appendChild(extraStyle); + if (rules) { + var sheet = extraStyle.sheet; + for (var selector in rules) { + sheet.insertRule( + selector + "{" + rules[selector] + "}", + sheet.cssRules.length + ); + } + } + + if (t && typeof t.add_cleanup === "function") { + t.add_cleanup(function () { + extraStyle.remove(); + }); + } +} + +/** + * Takes a CSS property (e.g. margin-left) and returns the equivalent IDL + * name (e.g. marginLeft). + */ +function propertyToIDL(property) { + var prefixMatch = property.match(/^-(\w+)-/); + if (prefixMatch) { + var prefix = prefixMatch[1] === "moz" ? "Moz" : prefixMatch[1]; + property = prefix + property.substring(prefixMatch[0].length - 1); + } + // https://drafts.csswg.org/cssom/#css-property-to-idl-attribute + return property.replace(/-([a-z])/gi, function (str, group) { + return group.toUpperCase(); + }); +} + +/** + * Promise wrapper for requestAnimationFrame. + */ +function waitForFrame() { + return new Promise(function (resolve, reject) { + window.requestAnimationFrame(resolve); + }); +} + +/** + * Waits for a requestAnimationFrame callback in the next refresh driver tick. + */ +function waitForNextFrame(aWindow = window) { + const timeAtStart = aWindow.document.timeline.currentTime; + return new Promise(resolve => { + aWindow.requestAnimationFrame(() => { + if (timeAtStart === aWindow.document.timeline.currentTime) { + aWindow.requestAnimationFrame(resolve); + } else { + resolve(); + } + }); + }); +} + +/** + * Returns a Promise that is resolved after the given number of consecutive + * animation frames have occured (using requestAnimationFrame callbacks). + * + * @param aFrameCount The number of animation frames. + * @param aOnFrame An optional function to be processed in each animation frame. + * @param aWindow An optional window object to be used for requestAnimationFrame. + */ +function waitForAnimationFrames(aFrameCount, aOnFrame, aWindow = window) { + const timeAtStart = aWindow.document.timeline.currentTime; + return new Promise(function (resolve, reject) { + function handleFrame() { + if (aOnFrame && typeof aOnFrame === "function") { + aOnFrame(); + } + if ( + timeAtStart != aWindow.document.timeline.currentTime && + --aFrameCount <= 0 + ) { + resolve(); + } else { + aWindow.requestAnimationFrame(handleFrame); // wait another frame + } + } + aWindow.requestAnimationFrame(handleFrame); + }); +} + +/** + * Promise wrapper for requestIdleCallback. + */ +function waitForIdle() { + return new Promise(resolve => { + requestIdleCallback(resolve); + }); +} + +/** + * Wrapper that takes a sequence of N animations and returns: + * + * Promise.all([animations[0].ready, animations[1].ready, ... animations[N-1].ready]); + */ +function waitForAllAnimations(animations) { + return Promise.all( + animations.map(function (animation) { + return animation.ready; + }) + ); +} + +/** + * Flush the computed style for the given element. This is useful, for example, + * when we are testing a transition and need the initial value of a property + * to be computed so that when we synchronouslyet set it to a different value + * we actually get a transition instead of that being the initial value. + */ +function flushComputedStyle(elem) { + var cs = getComputedStyle(elem); + cs.marginLeft; +} + +if (opener) { + for (var funcName of [ + "async_test", + "assert_not_equals", + "assert_equals", + "assert_approx_equals", + "assert_less_than", + "assert_less_than_equal", + "assert_greater_than", + "assert_between_inclusive", + "assert_true", + "assert_false", + "assert_class_string", + "assert_throws", + "assert_unreached", + "assert_regexp_match", + "promise_test", + "test", + ]) { + if (opener[funcName]) { + window[funcName] = opener[funcName].bind(opener); + } + } + + window.EventWatcher = opener.EventWatcher; + + function done() { + opener.add_completion_callback(function () { + self.close(); + }); + opener.done(); + } +} + +/* + * Returns a promise that is resolved when the document has finished loading. + */ +function waitForDocumentLoad() { + return new Promise(function (resolve, reject) { + if (document.readyState === "complete") { + resolve(); + } else { + window.addEventListener("load", resolve); + } + }); +} + +/* + * Enters test refresh mode, and restores the mode when |t| finishes. + */ +function useTestRefreshMode(t) { + function ensureNoSuppressedPaints() { + return new Promise(resolve => { + function checkSuppressedPaints() { + if (!SpecialPowers.DOMWindowUtils.paintingSuppressed) { + resolve(); + } else { + window.requestAnimationFrame(checkSuppressedPaints); + } + } + checkSuppressedPaints(); + }); + } + + return ensureNoSuppressedPaints().then(() => { + SpecialPowers.DOMWindowUtils.advanceTimeAndRefresh(0); + t.add_cleanup(() => { + SpecialPowers.DOMWindowUtils.restoreNormalRefresh(); + }); + }); +} + +/** + * Returns true if off-main-thread animations. + */ +function isOMTAEnabled() { + const OMTAPrefKey = "layers.offmainthreadcomposition.async-animations"; + return ( + SpecialPowers.DOMWindowUtils.layerManagerRemote && + SpecialPowers.getBoolPref(OMTAPrefKey) + ); +} + +/** + * Append an SVG element to the target element. + * + * @param target The element which want to append. + * @param attrs A array object with attribute name and values to set on + * the SVG element. + * @return An SVG outer element. + */ +function addSVGElement(target, tag, attrs) { + if (!target) { + return null; + } + var element = document.createElementNS("http://www.w3.org/2000/svg", tag); + if (attrs) { + for (var attrName in attrs) { + element.setAttributeNS(null, attrName, attrs[attrName]); + } + } + target.appendChild(element); + return element; +} + +/* + * Get Animation distance between two specified values for a specific property. + * + * @param target The target element. + * @param prop The CSS property. + * @param v1 The first property value. + * @param v2 The Second property value. + * + * @return The distance between |v1| and |v2| for |prop| on |target|. + */ +function getDistance(target, prop, v1, v2) { + if (!target) { + return 0.0; + } + return SpecialPowers.DOMWindowUtils.computeAnimationDistance( + target, + prop, + v1, + v2 + ); +} + +/* + * A promise wrapper for waiting MozAfterPaint. + */ +function waitForPaints() { + // FIXME: Bug 1415065. Instead waiting for two requestAnimationFrames, we + // should wait for MozAfterPaint once after MozAfterPaint is fired properly + // (bug 1341294). + return waitForAnimationFrames(2); +} + +// Returns true if |aAnimation| begins at the current timeline time. We +// sometimes need to detect this case because if we started an animation +// asynchronously (e.g. using play()) and then ended up running the next frame +// at precisely the time the animation started (due to aligning with vsync +// refresh rate) then we won't end up restyling in that frame. +function animationStartsRightNow(aAnimation) { + return ( + aAnimation.startTime === aAnimation.timeline.currentTime && + aAnimation.currentTime === 0 + ); +} + +// Waits for a given animation being ready to restyle. +async function waitForAnimationReadyToRestyle(aAnimation) { + await aAnimation.ready; + // If |aAnimation| begins at the current timeline time, we will not process + // restyling in the initial frame because of aligning with the refresh driver, + // the animation frame in which the ready promise is resolved happens to + // coincide perfectly with the start time of the animation. In this case no + // restyling is needed in the frame so we have to wait one more frame. + if (animationStartsRightNow(aAnimation)) { + await waitForNextFrame(aAnimation.ownerGlobal); + } +} + +// Returns the animation restyle markers observed during |frameCount| refresh +// driver ticks in this `window`. This function is typically used to count the +// number of restyles that take place as part of the style update that happens +// on each refresh driver tick, as opposed to synchronous restyles triggered by +// script. +// +// For the latter observeAnimSyncStyling (below) should be used. +function observeStyling(frameCount, onFrame) { + return observeStylingInTargetWindow(window, frameCount, onFrame); +} + +// As with observeStyling but applied to target window |aWindow|. +function observeStylingInTargetWindow(aWindow, aFrameCount, aOnFrame) { + let priorAnimationTriggeredRestyles = + SpecialPowers.wrap(aWindow).windowUtils.animationTriggeredRestyles; + + return new Promise(resolve => { + return waitForAnimationFrames(aFrameCount, aOnFrame, aWindow).then(() => { + let restyleCount = + SpecialPowers.wrap(aWindow).windowUtils.animationTriggeredRestyles - + priorAnimationTriggeredRestyles; + + resolve(restyleCount); + }); + }); +} |