diff options
Diffstat (limited to 'dom/animation/test/chrome/test_animation_observers_sync.html')
-rw-r--r-- | dom/animation/test/chrome/test_animation_observers_sync.html | 1587 |
1 files changed, 1587 insertions, 0 deletions
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..7a890fd2f4 --- /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. + animations[0][aChangeTest.prop] = animations[0][aChangeTest.prop]; + + assert_equals_records(observer.takeRecords(), + [{ added: [], changed: animations, removed: [] }], + "records after animation property change"); + }, `single_animation_api_change_${aChangeTest.prop}`); + }); + + // Test that making a redundant change to currentTime while an Animation + // is pause-pending still generates a change MutationRecord since setting + // the currentTime to any value in this state aborts the pending pause. + test(t => { + var div = addDiv(t, { style: "animation: anim 100s" }); + var observer = + setupSynchronousObserver(t, + aOptions.subtree ? div.parentNode : div, + aOptions.subtree); + + var animations = div.getAnimations(); + assert_equals(animations.length, 1, + "getAnimations().length after animation start"); + + assert_equals_records(observer.takeRecords(), + [{ added: animations, changed: [], removed: [] }], + "records after animation start"); + + animations[0].pause(); + + // We are now pause-pending. Even if we make a redundant change to the + // currentTime, we should still get a change record because setting the + // currentTime while pause-pending has the effect of cancelling a pause. + animations[0].currentTime = animations[0].currentTime; + + // Two MutationRecords for the Animation changes: one for pausing, one + // for aborting the pause. + assert_equals_records(observer.takeRecords(), + [{ added: [], changed: animations, removed: [] }, + { added: [], changed: animations, removed: [] }], + "records after pausing then seeking"); + }, "change_currentTime_while_pause_pending"); + + // Test that calling finish() on a forwards-filling Animation dispatches + // a changed notification. + test(t => { + var div = addDiv(t, { style: "animation: anim 100s forwards" }); + var observer = + setupSynchronousObserver(t, + aOptions.subtree ? div.parentNode : div, + aOptions.subtree); + + var animations = div.getAnimations(); + assert_equals(animations.length, 1, + "getAnimations().length after animation start"); + + assert_equals_records(observer.takeRecords(), + [{ added: animations, changed: [], removed: [] }], + "records after animation start"); + + animations[0].finish(); + + assert_equals_records(observer.takeRecords(), + [{ added: [], changed: animations, removed: [] }], + "records after finish()"); + + // Redundant finish. + animations[0].finish(); + + // Ensure no change records. + assert_equals_records(observer.takeRecords(), + [], "records after redundant finish()"); + }, "finish_with_forwards_fill"); + + // Test that calling finish() on an Animation that does not fill forwards, + // dispatches a removal notification. + test(t => { + var div = addDiv(t, { style: "animation: anim 100s" }); + var observer = + setupSynchronousObserver(t, + aOptions.subtree ? div.parentNode : div, + aOptions.subtree); + + var animations = div.getAnimations(); + assert_equals(animations.length, 1, + "getAnimations().length after animation start"); + + assert_equals_records(observer.takeRecords(), + [{ added: animations, changed: [], removed: [] }], + "records after animation start"); + + animations[0].finish(); + + // Single MutationRecord for the Animation removal. + assert_equals_records(observer.takeRecords(), + [{ added: [], changed: [], removed: animations }], + "records after finishing"); + }, "finish_without_fill"); + + // Test that calling finish() on a forwards-filling Animation dispatches + test(t => { + var div = addDiv(t, { style: "animation: anim 100s" }); + var observer = + setupSynchronousObserver(t, + aOptions.subtree ? div.parentNode : div, + aOptions.subtree); + + var animation = div.getAnimations()[0]; + assert_equals_records(observer.takeRecords(), + [{ added: [animation], changed: [], removed: []}], + "records after creation"); + animation.id = "new id"; + assert_equals_records(observer.takeRecords(), + [{ added: [], changed: [animation], removed: []}], + "records after id is changed"); + + animation.id = "new id"; + assert_equals_records(observer.takeRecords(), + [], "records after assigning same value with id"); + }, "change_id"); + + // Test that calling reverse() dispatches a changed notification. + test(t => { + var div = addDiv(t, { style: "animation: anim 100s both" }); + var observer = + setupSynchronousObserver(t, + aOptions.subtree ? div.parentNode : div, + aOptions.subtree); + + var animations = div.getAnimations(); + assert_equals(animations.length, 1, + "getAnimations().length after animation start"); + + assert_equals_records(observer.takeRecords(), + [{ added: animations, changed: [], removed: [] }], + "records after animation start"); + + animations[0].reverse(); + + assert_equals_records(observer.takeRecords(), + [{ added: [], changed: animations, removed: [] }], + "records after calling reverse()"); + }, "reverse"); + + // Test that calling reverse() does *not* dispatch a changed notification + // when playbackRate == 0. + test(t => { + var div = addDiv(t, { style: "animation: anim 100s both" }); + var observer = + setupSynchronousObserver(t, + aOptions.subtree ? div.parentNode : div, + aOptions.subtree); + + var animations = div.getAnimations(); + assert_equals(animations.length, 1, + "getAnimations().length after animation start"); + + assert_equals_records(observer.takeRecords(), + [{ added: animations, changed: [], removed: [] }], + "records after animation start"); + + // Seek to the middle and set playbackRate to zero. + animations[0].currentTime = 50 * MS_PER_SEC; + animations[0].playbackRate = 0; + + // Two MutationRecords, one for each change. + assert_equals_records(observer.takeRecords(), + [{ added: [], changed: animations, removed: [] }, + { added: [], changed: animations, removed: [] }], + "records after seeking and setting playbackRate"); + + animations[0].reverse(); + + // We should get no notifications. + assert_equals_records(observer.takeRecords(), + [], "records after calling reverse()"); + }, "reverse_with_zero_playbackRate"); + + // Test that 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.core.enabled", true], + ["dom.animations-api.implicit-keyframes.enabled", true], + ["dom.animations-api.timelines.enabled", true], + ], + }, + function() { + runTest(); + done(); + } +); + +</script> |