LibWeb: Update style before getting animation play state

Pending style updates can influence this value
This commit is contained in:
Callum Law 2025-10-31 01:14:05 +13:00 committed by Alexander Kalenik
parent a95cde3660
commit fbcef936a9
Notes: github-actions[bot] 2025-11-02 22:55:11 +00:00
7 changed files with 286 additions and 1 deletions

View file

@ -277,6 +277,14 @@ WebIDL::ExceptionOr<void> Animation::set_playback_rate(double new_playback_rate)
}
// https://www.w3.org/TR/web-animations-1/#animation-play-state
Bindings::AnimationPlayState Animation::play_state_for_bindings() const
{
if (m_owning_element)
m_owning_element->document().update_style();
return play_state();
}
Bindings::AnimationPlayState Animation::play_state() const
{
// The play state of animation, animation, at a given moment is the state corresponding to the first matching

View file

@ -48,6 +48,7 @@ public:
double playback_rate() const { return m_playback_rate; }
WebIDL::ExceptionOr<void> set_playback_rate(double value);
Bindings::AnimationPlayState play_state_for_bindings() const;
Bindings::AnimationPlayState play_state() const;
bool is_relevant() const;

View file

@ -14,7 +14,7 @@ interface Animation : EventTarget {
attribute double? startTime;
attribute double? currentTime;
attribute double playbackRate;
readonly attribute AnimationPlayState playState;
[ImplementedAs=play_state_for_bindings] readonly attribute AnimationPlayState playState;
readonly attribute AnimationReplaceState replaceState;
readonly attribute boolean pending;
readonly attribute Promise<Animation> ready;

View file

@ -0,0 +1,15 @@
Harness status: OK
Found 9 tests
4 Pass
5 Fail
Fail Animated style is cleared after canceling a running CSS animation
Fail Animated style is cleared after canceling a filling CSS animation
Pass After canceling an animation, it can still be seeked
Fail After canceling an animation, it can still be re-used
Fail After canceling an animation, updating animation properties doesn't make it live again
Fail After canceling an animation, updating animation-play-state doesn't make it live again
Pass Setting animation-name to 'none' cancels the animation
Pass Setting display:none on an element cancel its animations
Pass Setting display:none on an ancestor element cancels animations on descendants

View file

@ -0,0 +1,10 @@
Harness status: OK
Found 5 tests
5 Pass
Pass A new CSS animation is initially play-pending
Pass Animation returns correct playState when paused
Pass Animation.playState updates when paused by script
Pass Animation.playState updates when resumed by setting style
Pass Animation returns correct playState when canceled

View file

@ -0,0 +1,196 @@
<!doctype html>
<meta charset=utf-8>
<title>Canceling a CSS animation</title>
<!-- TODO: Add a more specific link for this once it is specified. -->
<link rel="help" href="https://drafts.csswg.org/css-animations-2/">
<script src="../../resources/testharness.js"></script>
<script src="../../resources/testharnessreport.js"></script>
<script src="support/testcommon.js"></script>
<style>
@keyframes translateAnim {
to { transform: translate(100px) }
}
@keyframes marginLeftAnim {
to { margin-left: 100px }
}
@keyframes marginLeftAnim100To200 {
from { margin-left: 100px }
to { margin-left: 200px }
}
</style>
<div id="log"></div>
<script>
'use strict';
promise_test(async t => {
const div = addDiv(t, { style: 'animation: translateAnim 100s' });
const animation = div.getAnimations()[0];
await animation.ready;
assert_not_equals(getComputedStyle(div).transform, 'none',
'transform style is animated before canceling');
animation.cancel();
assert_equals(getComputedStyle(div).transform, 'none',
'transform style is no longer animated after canceling');
}, 'Animated style is cleared after canceling a running CSS animation');
promise_test(async t => {
const div = addDiv(t, { style: 'animation: translateAnim 100s forwards' });
const animation = div.getAnimations()[0];
animation.finish();
await animation.ready;
assert_not_equals(getComputedStyle(div).transform, 'none',
'transform style is filling before canceling');
animation.cancel();
assert_equals(getComputedStyle(div).transform, 'none',
'fill style is cleared after canceling');
}, 'Animated style is cleared after canceling a filling CSS animation');
test(t => {
const div = addDiv(t, { style: 'animation: marginLeftAnim 100s linear' });
const animation = div.getAnimations()[0];
animation.cancel();
assert_equals(getComputedStyle(div).marginLeft, '0px',
'margin-left style is not animated after canceling');
animation.currentTime = 50 * 1000;
assert_equals(getComputedStyle(div).marginLeft, '50px',
'margin-left style is updated when canceled animation is'
+ ' seeked');
}, 'After canceling an animation, it can still be seeked');
promise_test(async t => {
const div =
addDiv(t, { style: 'animation: marginLeftAnim100To200 100s linear' });
const animation = div.getAnimations()[0];
await animation.ready;
animation.cancel();
assert_equals(getComputedStyle(div).marginLeft, '0px',
'margin-left style is not animated after canceling');
animation.play();
assert_equals(getComputedStyle(div).marginLeft, '100px',
'margin-left style is animated after re-starting animation');
await animation.ready;
assert_equals(animation.playState, 'running',
'Animation succeeds in running after being re-started');
}, 'After canceling an animation, it can still be re-used');
test(t => {
const div =
addDiv(t, { style: 'animation: marginLeftAnim100To200 100s linear' });
const animation = div.getAnimations()[0];
animation.cancel();
assert_equals(getComputedStyle(div).marginLeft, '0px',
'margin-left style is not animated after canceling');
// Trigger a change to some animation properties and check that this
// doesn't cause the animation to become live again
div.style.animationDuration = '200s';
assert_equals(getComputedStyle(div).marginLeft, '0px',
'margin-left style is still not animated after updating'
+ ' animation-duration');
assert_equals(animation.playState, 'idle',
'Animation is still idle after updating animation-duration');
}, 'After canceling an animation, updating animation properties doesn\'t make'
+ ' it live again');
test(t => {
const div =
addDiv(t, { style: 'animation: marginLeftAnim100To200 100s linear' });
const animation = div.getAnimations()[0];
animation.cancel();
assert_equals(getComputedStyle(div).marginLeft, '0px',
'margin-left style is not animated after canceling');
// Make some changes to animation-play-state and check that the
// animation doesn't become live again. This is because it should be
// possible to cancel an animation from script such that all future
// changes to style are ignored.
// Redundant change
div.style.animationPlayState = 'running';
assert_equals(animation.playState, 'idle',
'Animation is still idle after a redundant change to'
+ ' animation-play-state');
// Pause
div.style.animationPlayState = 'paused';
assert_equals(animation.playState, 'idle',
'Animation is still idle after setting'
+ ' animation-play-state: paused');
// Play
div.style.animationPlayState = 'running';
assert_equals(animation.playState, 'idle',
'Animation is still idle after re-setting'
+ ' animation-play-state: running');
}, 'After canceling an animation, updating animation-play-state doesn\'t'
+ ' make it live again');
promise_test(async t => {
const div = addDiv(t, { style: 'animation: translateAnim 10s both' });
div.style.marginLeft = '0px';
const animation = div.getAnimations()[0];
await animation.ready;
assert_equals(animation.playState, 'running');
div.style.animationName = 'none';
flushComputedStyle(div);
await waitForFrame();
assert_equals(animation.playState, 'idle');
assert_equals(getComputedStyle(div).marginLeft, '0px');
}, 'Setting animation-name to \'none\' cancels the animation');
promise_test(async t => {
const div = addDiv(t, { style: 'animation: translateAnim 10s both' });
const animation = div.getAnimations()[0];
await animation.ready;
assert_equals(animation.playState, 'running');
div.style.display = 'none';
await waitForFrame();
assert_equals(animation.playState, 'idle');
assert_equals(getComputedStyle(div).marginLeft, '0px');
}, 'Setting display:none on an element cancel its animations');
promise_test(async t => {
const parentDiv = addDiv(t);
const childDiv = document.createElement('div');
parentDiv.appendChild(childDiv);
childDiv.setAttribute('style', 'animation: translateAnim 10s both');
flushComputedStyle(childDiv);
const animation = childDiv.getAnimations()[0];
await animation.ready;
assert_equals(animation.playState, 'running');
parentDiv.style.display = 'none';
await waitForFrame();
assert_equals(animation.playState, 'idle');
assert_equals(getComputedStyle(childDiv).marginLeft, '0px');
}, 'Setting display:none on an ancestor element cancels animations on ' +
'descendants');
</script>

View file

@ -0,0 +1,55 @@
<!doctype html>
<meta charset=utf-8>
<title>CSSAnimation.playState</title>
<!-- TODO: Add a more specific link for this once it is specified. -->
<link rel="help" href="https://drafts.csswg.org/css-animations-2/#cssanimation">
<script src="../../resources/testharness.js"></script>
<script src="../../resources/testharnessreport.js"></script>
<script src="support/testcommon.js"></script>
<style>
@keyframes anim { }
</style>
<div id="log"></div>
<script>
'use strict';
test(t => {
const div = addDiv(t, { 'style': 'animation: anim 100s' });
const animation = div.getAnimations()[0];
assert_true(animation.pending);
assert_equals(animation.playState, 'running');
assert_equals(animation.startTime, null);
}, 'A new CSS animation is initially play-pending');
test(t => {
const div = addDiv(t, { 'style': 'animation: anim 1000s paused' });
const animation = div.getAnimations()[0];
assert_equals(animation.playState, 'paused');
}, 'Animation returns correct playState when paused');
test(t => {
const div = addDiv(t, { 'style': 'animation: anim 1000s' });
const animation = div.getAnimations()[0];
animation.pause();
assert_equals(animation.playState, 'paused');
}, 'Animation.playState updates when paused by script');
test(t => {
const div = addDiv(t, { 'style': 'animation: anim 1000s paused' });
const animation = div.getAnimations()[0];
div.style.animationPlayState = 'running';
// This test also checks that calling playState flushes style
assert_equals(animation.playState, 'running',
'Animation.playState reports running after updating'
+ ' animation-play-state (got: ' + animation.playState + ')');
}, 'Animation.playState updates when resumed by setting style');
test(t => {
const div = addDiv(t, { 'style': 'animation: anim 1000s' });
const animation = div.getAnimations()[0];
animation.cancel();
assert_equals(animation.playState, 'idle');
}, 'Animation returns correct playState when canceled');
</script>