257 lines
8.5 KiB
HTML
257 lines
8.5 KiB
HTML
<!doctype html>
|
|
<meta charset=utf-8>
|
|
<title>Update animations and send events</title>
|
|
<meta name="timeout" content="long">
|
|
<link rel="help" href="https://drafts.csswg.org/web-animations/#update-animations-and-send-events">
|
|
<script src="/resources/testharness.js"></script>
|
|
<script src="/resources/testharnessreport.js"></script>
|
|
<script src="../../testcommon.js"></script>
|
|
<div id="log"></div>
|
|
<script>
|
|
'use strict';
|
|
|
|
promise_test(async t => {
|
|
const div = createDiv(t);
|
|
const animation = div.animate(null, 100 * MS_PER_SEC);
|
|
|
|
// The ready promise should be resolved as part of micro-task checkpoint
|
|
// after updating the current time of all timeslines in the procedure to
|
|
// "update animations and send events".
|
|
await animation.ready;
|
|
|
|
let rAFReceived = false;
|
|
requestAnimationFrame(() => rAFReceived = true);
|
|
|
|
const eventWatcher = new EventWatcher(t, animation, 'cancel');
|
|
animation.cancel();
|
|
|
|
await eventWatcher.wait_for('cancel');
|
|
|
|
assert_false(rAFReceived,
|
|
'cancel event should be fired before requestAnimationFrame');
|
|
}, 'Fires cancel event before requestAnimationFrame');
|
|
|
|
promise_test(async t => {
|
|
const div = createDiv(t);
|
|
const animation = div.animate(null, 100 * MS_PER_SEC);
|
|
|
|
// Like the above test, the ready promise should be resolved micro-task
|
|
// checkpoint after updating the current time of all timeslines in the
|
|
// procedure to "update animations and send events".
|
|
await animation.ready;
|
|
|
|
let rAFReceived = false;
|
|
requestAnimationFrame(() => rAFReceived = true);
|
|
|
|
const eventWatcher = new EventWatcher(t, animation, 'finish');
|
|
animation.finish();
|
|
|
|
await eventWatcher.wait_for('finish');
|
|
|
|
assert_false(rAFReceived,
|
|
'finish event should be fired before requestAnimationFrame');
|
|
}, 'Fires finish event before requestAnimationFrame');
|
|
|
|
function animationType(anim) {
|
|
if (anim instanceof CSSAnimation) {
|
|
return 'CSSAnimation';
|
|
} else if (anim instanceof CSSTransition) {
|
|
return 'CSSTransition';
|
|
} else {
|
|
return 'ScriptAnimation';
|
|
}
|
|
}
|
|
|
|
promise_test(async t => {
|
|
createStyle(t, { '@keyframes anim': '' });
|
|
const div = createDiv(t);
|
|
|
|
getComputedStyle(div).marginLeft;
|
|
div.style = 'animation: anim 100s; ' +
|
|
'transition: margin-left 100s; ' +
|
|
'margin-left: 100px;';
|
|
div.animate(null, 100 * MS_PER_SEC);
|
|
const animations = div.getAnimations();
|
|
|
|
let receivedEvents = [];
|
|
animations.forEach(anim => {
|
|
anim.onfinish = event => {
|
|
receivedEvents.push({
|
|
type: animationType(anim) + ':' + event.type,
|
|
timeStamp: event.timeStamp
|
|
});
|
|
};
|
|
});
|
|
|
|
await Promise.all(animations.map(anim => anim.ready));
|
|
|
|
// Setting current time to the time just before the effect end.
|
|
animations.forEach(anim => anim.currentTime = 100 * MS_PER_SEC - 1);
|
|
|
|
await waitForNextFrame();
|
|
|
|
assert_array_equals(receivedEvents.map(event => event.type),
|
|
[ 'CSSTransition:finish', 'CSSAnimation:finish',
|
|
'ScriptAnimation:finish' ],
|
|
'finish events for various animation type should be sorted by composite ' +
|
|
'order');
|
|
}, 'Sorts finish events by composite order');
|
|
|
|
promise_test(async t => {
|
|
createStyle(t, { '@keyframes anim': '' });
|
|
const div = createDiv(t);
|
|
|
|
let receivedEvents = [];
|
|
function receiveEvent(type, timeStamp) {
|
|
receivedEvents.push({ type, timeStamp });
|
|
}
|
|
|
|
div.onanimationcancel = event => receiveEvent(event.type, event.timeStamp);
|
|
div.ontransitioncancel = event => receiveEvent(event.type, event.timeStamp);
|
|
|
|
getComputedStyle(div).marginLeft;
|
|
div.style = 'animation: anim 100s; ' +
|
|
'transition: margin-left 100s; ' +
|
|
'margin-left: 100px;';
|
|
div.animate(null, 100 * MS_PER_SEC);
|
|
const animations = div.getAnimations();
|
|
|
|
animations.forEach(anim => {
|
|
anim.oncancel = event => {
|
|
receiveEvent(animationType(anim) + ':' + event.type, event.timeStamp);
|
|
};
|
|
});
|
|
|
|
await Promise.all(animations.map(anim => anim.ready));
|
|
|
|
const timeInAnimationReady = document.timeline.currentTime;
|
|
|
|
// Call cancel() in reverse composite order. I.e. canceling for script
|
|
// animation happen first, then for CSS animation and CSS transition.
|
|
// 'cancel' events for these animations should be sorted by composite
|
|
// order.
|
|
animations.reverse().forEach(anim => anim.cancel());
|
|
|
|
// requestAnimationFrame callback which is actually the _same_ frame since we
|
|
// are currently operating in the `ready` callbac of the animations which
|
|
// happens as part of the "Update animations and send events" procedure
|
|
// _before_ we run animation frame callbacks.
|
|
await waitForAnimationFrames(1);
|
|
|
|
assert_times_equal(timeInAnimationReady, document.timeline.currentTime,
|
|
'A rAF callback should happen in the same frame');
|
|
|
|
assert_array_equals(receivedEvents.map(event => event.type),
|
|
// This ordering needs more clarification in the spec, but the intention is
|
|
// that the cancel playback event fires before the equivalent CSS cancel
|
|
// event in each case.
|
|
[ 'CSSTransition:cancel', 'CSSAnimation:cancel', 'ScriptAnimation:cancel',
|
|
'transitioncancel', 'animationcancel' ],
|
|
'cancel events should be sorted by composite order');
|
|
}, 'Sorts cancel events by composite order');
|
|
|
|
promise_test(async t => {
|
|
const div = createDiv(t);
|
|
getComputedStyle(div).marginLeft;
|
|
div.style = 'transition: margin-left 100s; margin-left: 100px;';
|
|
const anim = div.getAnimations()[0];
|
|
|
|
let receivedEvents = [];
|
|
anim.oncancel = event => receivedEvents.push(event);
|
|
|
|
const eventWatcher = new EventWatcher(t, div, 'transitionstart');
|
|
await eventWatcher.wait_for('transitionstart');
|
|
|
|
const timeInEventCallback = document.timeline.currentTime;
|
|
|
|
// Calling cancel() queues a cancel event
|
|
anim.cancel();
|
|
|
|
await waitForAnimationFrames(1);
|
|
assert_times_equal(timeInEventCallback, document.timeline.currentTime,
|
|
'A rAF callback should happen in the same frame');
|
|
|
|
assert_array_equals(receivedEvents, [],
|
|
'The queued cancel event shouldn\'t be dispatched in the same frame');
|
|
|
|
await waitForAnimationFrames(1);
|
|
assert_array_equals(receivedEvents.map(event => event.type), ['cancel'],
|
|
'The cancel event should be dispatched in a later frame');
|
|
}, 'Queues a cancel event in transitionstart event callback');
|
|
|
|
promise_test(async t => {
|
|
const div = createDiv(t);
|
|
getComputedStyle(div).marginLeft;
|
|
div.style = 'transition: margin-left 100s; margin-left: 100px;';
|
|
const anim = div.getAnimations()[0];
|
|
|
|
let receivedEvents = [];
|
|
anim.oncancel = event => receivedEvents.push(event);
|
|
div.ontransitioncancel = event => receivedEvents.push(event);
|
|
|
|
await anim.ready;
|
|
|
|
anim.cancel();
|
|
|
|
await waitForAnimationFrames(1);
|
|
|
|
assert_array_equals(receivedEvents.map(event => event.type),
|
|
[ 'cancel', 'transitioncancel' ],
|
|
'Playback and CSS events for the same transition should be sorted by ' +
|
|
'schedule event time and composite order');
|
|
}, 'Sorts events for the same transition');
|
|
|
|
promise_test(async t => {
|
|
const div = createDiv(t);
|
|
const anim = div.animate(null, 100 * MS_PER_SEC);
|
|
|
|
let receivedEvents = [];
|
|
anim.oncancel = event => receivedEvents.push(event);
|
|
anim.onfinish = event => receivedEvents.push(event);
|
|
|
|
await anim.ready;
|
|
|
|
anim.finish();
|
|
anim.cancel();
|
|
|
|
await waitForAnimationFrames(1);
|
|
|
|
assert_array_equals(receivedEvents.map(event => event.type),
|
|
[ 'finish', 'cancel' ],
|
|
'Calling finish() synchronously queues a finish event when updating the ' +
|
|
'finish state so it should appear before the cancel event');
|
|
}, 'Playback events with the same timeline retain the order in which they are' +
|
|
'queued');
|
|
|
|
promise_test(async t => {
|
|
const div = createDiv(t);
|
|
|
|
// Create two animations with separate timelines
|
|
|
|
const timelineA = document.timeline;
|
|
const animA = div.animate(null, 100 * MS_PER_SEC);
|
|
|
|
const timelineB = new DocumentTimeline();
|
|
const animB = new Animation(
|
|
new KeyframeEffect(div, null, 100 * MS_PER_SEC),
|
|
timelineB
|
|
);
|
|
animB.play();
|
|
|
|
animA.currentTime = 99.9 * MS_PER_SEC;
|
|
animB.currentTime = 99.9 * MS_PER_SEC;
|
|
|
|
// When the next tick happens both animations should be updated, and we will
|
|
// notice that they are now finished. As a result their finished promise
|
|
// callbacks should be queued. All of that should happen before we run the
|
|
// next microtask checkpoint and actually run the promise callbacks and
|
|
// hence the calls to cancel should not stop the existing callbacks from
|
|
// being run.
|
|
|
|
animA.finished.then(() => { animB.cancel() });
|
|
animB.finished.then(() => { animA.cancel() });
|
|
|
|
await Promise.all([animA.finished, animB.finished]);
|
|
}, 'All timelines are updated before running microtasks');
|
|
|
|
</script>
|