зеркало из https://github.com/mozilla/gecko-dev.git
666 строки
24 KiB
HTML
666 строки
24 KiB
HTML
<!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 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(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.
|
|
return waitForFrame().then(() => {
|
|
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 ];
|
|
|
|
// 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: [] })
|
|
);
|
|
|
|
return await_event(div, "animationend");
|
|
}).then(() => {
|
|
// 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');
|
|
|
|
setup({explicit_done: true});
|
|
SpecialPowers.pushPrefEnv(
|
|
{
|
|
set: [
|
|
["dom.animations-api.autoremove.enabled", true],
|
|
["dom.animations-api.implicit-keyframes.enabled", true],
|
|
],
|
|
},
|
|
function() {
|
|
runTest();
|
|
done();
|
|
}
|
|
);
|
|
</script>
|