Tests: Import some more animation tests

This commit is contained in:
Callum Law
2025-12-18 17:22:16 +13:00
committed by Alexander Kalenik
parent 3a7fcde341
commit 16d1498bb0
6 changed files with 740 additions and 0 deletions

View File

@@ -0,0 +1,15 @@
Harness status: OK
Found 9 tests
2 Pass
7 Fail
Fail play() overrides animation-play-state
Pass play() does NOT override the animation-play-state if there was an error
Fail pause() overrides animation-play-state
Fail reverse() overrides animation-play-state when it starts playing the animation
Fail reverse() does NOT override animation-play-state if the animation is already running
Fail Setting the startTime to null overrides animation-play-state if the animation is already running
Fail Setting the startTime to non-null overrides animation-play-state if the animation is paused
Pass Setting the startTime to non-null does NOT override the animation-play-state if the animation is already running
Fail Setting the current time completes a pending pause

View File

@@ -0,0 +1,18 @@
Harness status: OK
Found 12 tests
11 Pass
1 Fail
Pass Playing a running animation leaves the current time unchanged
Pass Playing a finished animation seeks back to the start
Pass Playing a finished and reversed animation seeks to end
Pass Playing a pause-pending but previously finished animation seeks back to to the start
Pass Playing a finished animation clears the start time
Pass The ready promise should be replaced if the animation is not already pending
Pass A pending ready promise should be resolved and not replaced when the animation enters the running state
Pass Resuming an animation from paused calculates start time from hold time
Pass If a pause operation is interrupted, the ready promise is reused
Fail A pending playback rate is used when determining auto-rewind behavior
Pass Playing a canceled animation sets the start time
Pass Playing a canceled animation backwards sets the start time

View File

@@ -0,0 +1,14 @@
Harness status: OK
Found 8 tests
4 Pass
4 Fail
Pass Fires cancel event before requestAnimationFrame
Pass Fires finish event before requestAnimationFrame
Fail Sorts finish events by composite order
Fail Sorts cancel events by composite order
Pass Queues a cancel event in transitionstart event callback
Fail Sorts events for the same transition
Pass Playback events with the same timeline retain the order in which they arequeued
Fail All timelines are updated before running microtasks

View File

@@ -0,0 +1,259 @@
<!doctype html>
<meta charset=utf-8>
<title>Pausing a CSSAnimation</title>
<link rel="help"
href="https://drafts.csswg.org/css-animations-2/#animation-play-state">
<script src="../../resources/testharness.js"></script>
<script src="../../resources/testharnessreport.js"></script>
<script src="support/testcommon.js"></script>
<style>
@keyframes anim {
0% { margin-left: 0px }
100% { margin-left: 10000px }
}
</style>
<div id="log"></div>
<script>
'use strict';
promise_test(async t => {
const div = addDiv(t);
div.style.animation = 'anim 1000s paused';
const animation = div.getAnimations()[0];
animation.play();
await animation.ready;
await waitForNextFrame();
assert_equals(
animation.playState,
'running',
'Play state is running after calling play()'
);
// Flip the animation-play-state back and forth to check it has no effect
div.style.animationPlayState = 'running';
getComputedStyle(div).animationPlayState;
div.style.animationPlayState = 'paused';
getComputedStyle(div).animationPlayState;
assert_equals(
animation.playState,
'running',
'Should still be running even after flipping the animation-play-state'
);
}, 'play() overrides animation-play-state');
promise_test(async t => {
const div = addDiv(t);
div.style.animation = 'anim 100s infinite paused';
const animation = div.getAnimations()[0];
animation.playbackRate = -1;
animation.currentTime = -1;
assert_throws_dom('InvalidStateError', () => {
animation.play();
}, 'Trying to play a reversed infinite animation should throw');
assert_equals(
animation.playState,
'paused',
'Animation should still be paused'
);
animation.playbackRate = 1;
div.style.animationPlayState = 'running';
assert_equals(
animation.playState,
'running',
'Changing the animation-play-state should play the animation'
);
}, 'play() does NOT override the animation-play-state if there was an error');
promise_test(async t => {
const div = addDiv(t);
div.style.animation = 'anim 1000s paused';
const animation = div.getAnimations()[0];
animation.pause();
div.style.animationPlayState = 'running';
getComputedStyle(div).animationPlayState;
await animation.ready;
await waitForNextFrame();
assert_equals(animation.playState, 'paused', 'playState is paused ');
// Flip the animation-play-state back and forth to check it has no effect
div.style.animationPlayState = 'paused';
getComputedStyle(div).animationPlayState;
div.style.animationPlayState = 'running';
getComputedStyle(div).animationPlayState;
assert_equals(
animation.playState,
'paused',
'Should still be paused even after flipping the animation-play-state'
);
}, 'pause() overrides animation-play-state');
promise_test(async t => {
const div = addDiv(t);
div.style.animation = 'anim 100s paused';
const animation = div.getAnimations()[0];
animation.reverse();
assert_equals(
animation.playState,
'running',
'Play state is running after calling reverse()'
);
// Flip the animation-play-state back and forth to check it has no effect
div.style.animationPlayState = 'running';
getComputedStyle(div).animationPlayState;
div.style.animationPlayState = 'paused';
getComputedStyle(div).animationPlayState;
assert_equals(
animation.playState,
'running',
'Should still be running even after flipping the animation-play-state'
);
}, 'reverse() overrides animation-play-state when it starts playing the'
+ ' animation');
promise_test(async t => {
const div = addDiv(t);
div.style.animation = 'anim 100s';
const animation = div.getAnimations()[0];
animation.reverse();
assert_equals(
animation.playState,
'running',
'Play state is running after calling reverse()'
);
div.style.animationPlayState = 'paused';
getComputedStyle(div).animationPlayState;
assert_equals(
animation.playState,
'paused',
'Should be paused after changing the animation-play-state'
);
}, 'reverse() does NOT override animation-play-state if the animation is'
+ ' already running');
promise_test(async t => {
const div = addDiv(t);
div.style.animation = 'anim 100s';
const animation = div.getAnimations()[0];
animation.startTime = null;
assert_equals(
animation.playState,
'paused',
'Play state is paused after setting the start time to null'
);
// Flip the animation-play-state back and forth to check it has no effect
div.style.animationPlayState = 'paused';
getComputedStyle(div).animationPlayState;
div.style.animationPlayState = 'running';
getComputedStyle(div).animationPlayState;
assert_equals(
animation.playState,
'paused',
'Should still be paused even after flipping the animation-play-state'
);
}, 'Setting the startTime to null overrides animation-play-state if the'
+ ' animation is already running');
promise_test(async t => {
const div = addDiv(t);
div.style.animation = 'anim 100s paused';
const animation = div.getAnimations()[0];
animation.startTime = document.timeline.currentTime;
assert_equals(
animation.playState,
'running',
'Play state is running after setting the start time to non-null'
);
// Flip the animation-play-state back and forth to check it has no effect
div.style.animationPlayState = 'running';
getComputedStyle(div).animationPlayState;
div.style.animationPlayState = 'paused';
getComputedStyle(div).animationPlayState;
assert_equals(
animation.playState,
'running',
'Should still be running even after flipping the animation-play-state'
);
}, 'Setting the startTime to non-null overrides animation-play-state if the'
+ ' animation is paused');
promise_test(async t => {
const div = addDiv(t);
div.style.animation = 'anim 100s';
const animation = div.getAnimations()[0];
animation.startTime = document.timeline.currentTime;
div.style.animationPlayState = 'paused';
getComputedStyle(div).animationPlayState;
assert_equals(
animation.playState,
'paused',
'Should be paused after changing the animation-play-state'
);
}, 'Setting the startTime to non-null does NOT override the'
+ ' animation-play-state if the animation is already running');
promise_test(async t => {
const div = addDiv(t, { style: 'animation: anim 1000s' });
const animation = div.getAnimations()[0];
let readyPromiseRun = false;
await animation.ready;
div.style.animationPlayState = 'paused';
assert_true(animation.pending && animation.playState === 'paused',
'Animation is pause-pending');
// Set current time
animation.currentTime = 5 * MS_PER_SEC;
assert_equals(animation.playState, 'paused',
'Animation is paused immediately after setting currentTime');
assert_equals(animation.startTime, null,
'Animation startTime is unresolved immediately after ' +
'setting currentTime');
assert_equals(animation.currentTime, 5 * MS_PER_SEC,
'Animation currentTime does not change when forcing a ' +
'pause operation to complete');
// The ready promise should now be resolved. If it's not then test will
// probably time out before anything else happens that causes it to resolve.
await animation.ready;
}, 'Setting the current time completes a pending pause');
</script>

View File

@@ -0,0 +1,177 @@
<!DOCTYPE html>
<meta charset=utf-8>
<title>Playing an animation</title>
<link rel="help"
href="https://drafts.csswg.org/web-animations/#playing-an-animation-section">
<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 => {
const animation = createDiv(t).animate(null, 100 * MS_PER_SEC);
animation.currentTime = 1 * MS_PER_SEC;
assert_time_equals_literal(animation.currentTime, 1 * MS_PER_SEC);
animation.play();
assert_time_equals_literal(animation.currentTime, 1 * MS_PER_SEC);
}, 'Playing a running animation leaves the current time unchanged');
test(t => {
const animation = createDiv(t).animate(null, 100 * MS_PER_SEC);
animation.finish();
assert_time_equals_literal(animation.currentTime, 100 * MS_PER_SEC);
animation.play();
assert_time_equals_literal(animation.currentTime, 0);
}, 'Playing a finished animation seeks back to the start');
test(t => {
const animation = createDiv(t).animate(null, 100 * MS_PER_SEC);
animation.playbackRate = -1;
animation.currentTime = 0;
assert_time_equals_literal(animation.currentTime, 0);
animation.play();
assert_time_equals_literal(animation.currentTime, 100 * MS_PER_SEC);
}, 'Playing a finished and reversed animation seeks to end');
promise_test(async t => {
const animation = createDiv(t).animate(null, 100 * MS_PER_SEC);
animation.finish();
// Initiate a pause then abort it
animation.pause();
animation.play();
// Wait to return to running state
await animation.ready;
assert_true(animation.currentTime < 100 * 1000,
'After aborting a pause when finished, the current time should'
+ ' jump back to the start of the animation');
}, 'Playing a pause-pending but previously finished animation seeks back to'
+ ' to the start');
promise_test(async t => {
const animation = createDiv(t).animate(null, 100 * MS_PER_SEC);
animation.finish();
await animation.ready;
animation.play();
assert_equals(animation.startTime, null, 'start time is unresolved');
}, 'Playing a finished animation clears the start time');
test(t => {
const animation = createDiv(t).animate(null, 100 * MS_PER_SEC);
animation.cancel();
const promise = animation.ready;
animation.play();
assert_not_equals(animation.ready, promise);
}, 'The ready promise should be replaced if the animation is not already'
+ ' pending');
promise_test(async t => {
const animation = createDiv(t).animate(null, 100 * MS_PER_SEC);
const promise = animation.ready;
const promiseResult = await promise;
assert_equals(promiseResult, animation);
assert_equals(animation.ready, promise);
}, 'A pending ready promise should be resolved and not replaced when the'
+ ' animation enters the running state');
promise_test(async t => {
const animation = createDiv(t).animate(null, 100 * MS_PER_SEC);
animation.currentTime = 50 * MS_PER_SEC;
await animation.ready;
animation.pause();
await animation.ready;
const holdTime = animation.currentTime;
animation.play();
await animation.ready;
assert_less_than_equal(
animation.startTime,
animation.timeline.currentTime - holdTime + TIME_PRECISION
);
}, 'Resuming an animation from paused calculates start time from hold time');
promise_test(async t => {
const animation = createDiv(t).animate(null, 100 * MS_PER_SEC);
await animation.ready;
// Go to pause-pending state
animation.pause();
assert_true(animation.pending, 'Animation is pending');
const pauseReadyPromise = animation.ready;
// Now play again immediately (abort the pause)
animation.play();
assert_true(animation.pending, 'Animation is still pending');
assert_equals(animation.ready, pauseReadyPromise,
'The pause Promise is re-used when playing while waiting'
+ ' to pause');
// Sanity check: Animation proceeds to running state
await animation.ready;
assert_true(!animation.pending && animation.playState === 'running',
'Animation is running after aborting a pause');
}, 'If a pause operation is interrupted, the ready promise is reused');
promise_test(async t => {
// Seek animation beyond target end
const animation = createDiv(t).animate(null, 100 * MS_PER_SEC);
animation.currentTime = -100 * MS_PER_SEC;
await animation.ready;
// Set pending playback rate to the opposite direction
animation.updatePlaybackRate(-1);
assert_true(animation.pending);
assert_equals(animation.playbackRate, 1);
// When we play, we should seek to the target end, NOT to zero (which
// is where we would seek to if we used the playbackRate of 1.
animation.play();
assert_time_equals_literal(animation.currentTime, 100 * MS_PER_SEC);
}, 'A pending playback rate is used when determining auto-rewind behavior');
promise_test(async t => {
const animation = createDiv(t).animate(null, 100 * MS_PER_SEC);
animation.cancel();
assert_equals(animation.startTime, null,
'Start time should be unresolved');
const playTime = animation.timeline.currentTime;
animation.play();
assert_true(animation.pending, 'Animation should be play-pending');
await animation.ready;
assert_false(animation.pending, 'animation should no longer be pending');
assert_time_greater_than_equal(animation.startTime, playTime,
'The start time of the playing animation should be set');
}, 'Playing a canceled animation sets the start time');
promise_test(async t => {
const animation = createDiv(t).animate(null, 100 * MS_PER_SEC);
animation.playbackRate = -1;
animation.cancel();
assert_equals(animation.startTime, null,
'Start time should be unresolved');
const playTime = animation.timeline.currentTime;
animation.play();
assert_true(animation.pending, 'Animation should be play-pending');
await animation.ready;
assert_false(animation.pending, 'Animation should no longer be pending');
assert_time_greater_than_equal(animation.startTime, playTime + 100 * MS_PER_SEC,
'The start time of the playing animation should be set');
}, 'Playing a canceled animation backwards sets the start time');
</script>
</body>

View File

@@ -0,0 +1,257 @@
<!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>