LibWeb: Correctly sort animations returned by getAnimations()

This commit is contained in:
Psychpsyo
2025-11-10 16:27:00 +01:00
committed by Jelle Raaijmakers
parent 2c389ae96c
commit b7a71ca950
Notes: github-actions[bot] 2025-11-10 17:30:34 +00:00
6 changed files with 284 additions and 14 deletions

View File

@@ -627,11 +627,12 @@ void KeyframeEffect::generate_initial_and_final_frames(RefPtr<KeyFrameSet> keyfr
// https://www.w3.org/TR/web-animations-1/#animation-composite-order
int KeyframeEffect::composite_order(GC::Ref<KeyframeEffect> a, GC::Ref<KeyframeEffect> b)
{
// 1. Let the associated animation of an animation effect be the animation associated with the animation effect.
// The relative composite order of any two keyframe effects A and B within an effect stack is established by
// comparing their properties as follows:
auto a_animation = a->associated_animation();
auto b_animation = b->associated_animation();
// 2. Sort A and B by applying the following conditions in turn until the order is resolved,
// 1. Sort A and B by applying the following conditions in turn until the order is resolved,
// 1. If A and Bs associated animations differ by class, sort by any inter-class composite order defined for
// the corresponding classes.

View File

@@ -5472,12 +5472,7 @@ void Document::remove_replaced_animations()
WebIDL::ExceptionOr<Vector<GC::Ref<Animations::Animation>>> Document::get_animations()
{
Vector<GC::Ref<Animations::Animation>> relevant_animations;
TRY(for_each_child_of_type_fallible<Element>([&](auto& child) -> WebIDL::ExceptionOr<IterationDecision> {
relevant_animations.extend(TRY(child.get_animations(Animations::GetAnimationsOptions { .subtree = true })));
return IterationDecision::Continue;
}));
return relevant_animations;
return calculate_get_animations(*this);
}
// https://html.spec.whatwg.org/multipage/dom.html#dom-document-nameditem-filter

View File

@@ -7,6 +7,7 @@
#pragma once
#include <LibWeb/Animations/Animation.h>
#include <LibWeb/DOM/Document.h>
#include <LibWeb/DOM/Utils.h>
@@ -47,4 +48,32 @@ GC::Ptr<Element> calculate_active_element(T& self)
return nullptr;
}
// https://drafts.csswg.org/web-animations-1/#dom-documentorshadowroot-getanimations
template<DocumentOrShadowRoot T>
WebIDL::ExceptionOr<Vector<GC::Ref<Animations::Animation>>> calculate_get_animations(T& self)
{
// Returns the set of relevant animations for a subtree for the document or shadow root on which this
// method is called.
Vector<GC::Ref<Animations::Animation>> relevant_animations;
TRY(self.template for_each_child_of_type_fallible<Element>([&](auto& child) -> WebIDL::ExceptionOr<IterationDecision> {
relevant_animations.extend(TRY(child.get_animations(Animations::GetAnimationsOptions { .subtree = true })));
return IterationDecision::Continue;
}));
// The returned list is sorted using the composite order described for the associated animations of
// effects in §5.4.2 The effect stack.
quick_sort(relevant_animations, [](GC::Ref<Animations::Animation>& a, GC::Ref<Animations::Animation>& b) {
auto& a_effect = as<Animations::KeyframeEffect>(*a->effect());
auto& b_effect = as<Animations::KeyframeEffect>(*b->effect());
return Animations::KeyframeEffect::composite_order(a_effect, b_effect) < 0;
});
// Calling this method triggers a style change event for the document. As a result, the returned list
// reflects the state after applying any pending style changes to animation such as changes to
// animation-related style properties that have yet to be processed.
// FIXME: Implement this.
return relevant_animations;
}
}

View File

@@ -198,12 +198,7 @@ void ShadowRoot::for_each_css_style_sheet(Function<void(CSS::CSSStyleSheet&)>&&
WebIDL::ExceptionOr<Vector<GC::Ref<Animations::Animation>>> ShadowRoot::get_animations()
{
Vector<GC::Ref<Animations::Animation>> relevant_animations;
TRY(for_each_child_of_type_fallible<Element>([&](auto& child) -> WebIDL::ExceptionOr<IterationDecision> {
relevant_animations.extend(TRY(child.get_animations(Animations::GetAnimationsOptions { .subtree = true })));
return IterationDecision::Continue;
}));
return relevant_animations;
return calculate_get_animations(*this);
}
ElementByIdMap& ShadowRoot::element_by_id() const

View File

@@ -0,0 +1,16 @@
Harness status: OK
Found 11 tests
11 Pass
Pass Document.getAnimations() returns an empty sequence for non-animated content
Pass Document.getAnimations() returns script-generated animations
Pass Document.getAnimations() returns script-generated animations in the order they were created
Pass Document.getAnimations() does not return a disconnected node
Pass Document.getAnimations() does not return an animation with a null target
Pass Document.getAnimations() returns animations on elements inside same-origin iframes
Pass iframe.contentDocument.getAnimations() returns animations on elements inside same-origin Document
Pass ShadowRoot.getAnimations() return all animations in the shadow tree
Pass Document.getAnimations() does NOT return animations in shadow trees
Pass ShadowRoot.getAnimations() does NOT return animations in parent document
Pass Document.getAnimations() triggers a style change event

View File

@@ -0,0 +1,234 @@
<!DOCTYPE html>
<meta charset=utf-8>
<title>DocumentOrShadowRoot.getAnimations</title>
<link rel="help" href="https://drafts.csswg.org/web-animations-1/#dom-documentorshadowroot-getanimations">
<script src="../../../resources/testharness.js"></script>
<script src="../../../resources/testharnessreport.js"></script>
<script src="../../testcommon.js"></script>
<body>
<div id="log"></div>
<div id="target"></div>
<script>
'use strict';
const gKeyFrames = { 'marginLeft': ['100px', '200px'] };
test(t => {
assert_equals(document.getAnimations().length, 0,
'getAnimations returns an empty sequence for a document ' +
'with no animations');
}, 'Document.getAnimations() returns an empty sequence for non-animated'
+ ' content');
test(t => {
const div = createDiv(t);
const anim1 = div.animate(gKeyFrames, 100 * MS_PER_SEC);
const anim2 = div.animate(gKeyFrames, 100 * MS_PER_SEC);
assert_equals(document.getAnimations().length, 2,
'getAnimation returns running animations');
anim1.finish();
anim2.finish();
assert_equals(document.getAnimations().length, 0,
'getAnimation only returns running animations');
}, 'Document.getAnimations() returns script-generated animations')
test(t => {
const div = createDiv(t);
const anim1 = div.animate(gKeyFrames, 100 * MS_PER_SEC);
const anim2 = div.animate(gKeyFrames, 100 * MS_PER_SEC);
assert_array_equals(document.getAnimations(),
[ anim1, anim2 ],
'getAnimations() returns running animations');
}, 'Document.getAnimations() returns script-generated animations in the order'
+ ' they were created')
test(t => {
// This element exists but is not a descendent of any document, so isn't
// picked up by getAnimations.
const div = document.createElement('div');
const anim = div.animate(gKeyFrames, 100 * MS_PER_SEC);
assert_equals(document.getAnimations().length, 0);
// Now connect the div; it should appear in the list of animations.
document.body.appendChild(div);
t.add_cleanup(() => { div.remove(); });
assert_equals(document.getAnimations().length, 1);
}, 'Document.getAnimations() does not return a disconnected node');
test(t => {
const effect = new KeyframeEffect(null, gKeyFrames, 100 * MS_PER_SEC);
const anim = new Animation(effect, document.timeline);
anim.play();
assert_equals(document.getAnimations().length, 0,
'document.getAnimations() only returns animations targeting ' +
'elements in this document');
}, 'Document.getAnimations() does not return an animation with a null target');
promise_test(async t => {
const iframe = document.createElement('iframe');
await insertFrameAndAwaitLoad(t, iframe, document)
const div = createDiv(t, iframe.contentDocument)
const effect = new KeyframeEffect(div, null, 100 * MS_PER_SEC);
const anim = new Animation(effect, document.timeline);
anim.play();
// The animation's timeline is from the main document, but the effect's
// target element is part of the iframe document and that is what matters
// for getAnimations.
assert_equals(document.getAnimations().length, 0);
assert_equals(iframe.contentDocument.getAnimations().length, 1);
anim.finish();
}, 'Document.getAnimations() returns animations on elements inside same-origin'
+ ' iframes');
promise_test(async t => {
const iframe1 = document.createElement('iframe');
const iframe2 = document.createElement('iframe');
await insertFrameAndAwaitLoad(t, iframe1, document);
await insertFrameAndAwaitLoad(t, iframe2, document);
const div_frame1 = createDiv(t, iframe1.contentDocument)
const div_main_frame = createDiv(t)
const effect1 = new KeyframeEffect(div_frame1, null, 100 * MS_PER_SEC);
const anim1 = new Animation(effect1, document.timeline);
anim1.play();
// Animation of div_frame1 is in iframe with main timeline.
// The animation's timeline is from the iframe, but the effect's target
// element is part of the iframe's document.
assert_equals(document.getAnimations().length, 0);
assert_equals(iframe1.contentDocument.getAnimations().length, 1);
anim1.finish();
// animation of div_frame1 in iframe1 with iframe timeline
const effect2 = new KeyframeEffect(div_frame1, null, 100 * MS_PER_SEC);
const anim2 = new Animation(effect2, iframe1.contentDocument.timeline);
anim2.play();
assert_equals(document.getAnimations().length, 0);
assert_equals(iframe1.contentDocument.getAnimations().length, 1);
anim2.finish();
//animation of div_main_frame in main frame with iframe timeline
const effect3 = new KeyframeEffect(div_main_frame, null, 100 * MS_PER_SEC);
const anim3 = new Animation(effect3, iframe1.contentDocument.timeline);
anim3.play();
assert_equals(document.getAnimations().length, 1);
assert_equals(iframe1.contentDocument.getAnimations().length, 0);
anim3.finish();
//animation of div_frame1 in iframe1 with another iframe's timeline
const effect4 = new KeyframeEffect(div_frame1, null, 100 * MS_PER_SEC);
const anim4 = new Animation(effect4, iframe2.contentDocument.timeline);
anim4.play();
assert_equals(document.getAnimations().length, 0);
assert_equals(iframe1.contentDocument.getAnimations().length, 1);
assert_equals(iframe2.contentDocument.getAnimations().length, 0);
anim4.finish();
}, 'iframe.contentDocument.getAnimations() returns animations on elements '
+ 'inside same-origin Document');
test(t => {
const div = createDiv(t);
const shadow = div.attachShadow({ mode: 'open' });
// Create a tree with the following structure
//
// div
// |
// (ShadowRoot)
// / \
// childA childB
// (*anim2) |
// grandChild
// (*anim1)
//
// This lets us test that:
//
// a) All children of the ShadowRoot are included
// b) Descendants of the children are included
// c) The result is sorted by composite order (since we fire anim1 before
// anim2 despite childA appearing first in tree order)
const childA = createDiv(t);
shadow.append(childA);
const childB = createDiv(t);
shadow.append(childB);
const grandChild = createDiv(t);
childB.append(grandChild);
const anim1 = grandChild.animate(gKeyFrames, 100 * MS_PER_SEC)
const anim2 = childA.animate(gKeyFrames, 100 * MS_PER_SEC)
assert_array_equals(
div.shadowRoot.getAnimations(),
[ anim1, anim2 ],
'getAnimations() called on ShadowRoot returns expected animations'
);
}, 'ShadowRoot.getAnimations() return all animations in the shadow tree');
test(t => {
const div = createDiv(t);
const shadow = div.attachShadow({ mode: 'open' });
const child = createDiv(t);
shadow.append(child);
child.animate(gKeyFrames, 100 * MS_PER_SEC)
assert_array_equals(
document.getAnimations(),
[],
'getAnimations() called on Document does not return animations from shadow'
+ ' trees'
);
}, 'Document.getAnimations() does NOT return animations in shadow trees');
test(t => {
const div = createDiv(t);
const shadow = div.attachShadow({ mode: 'open' });
div.animate(gKeyFrames, 100 * MS_PER_SEC)
assert_array_equals(
div.shadowRoot.getAnimations(),
[],
'getAnimations() called on ShadowRoot does not return animations from'
+ ' Document'
);
}, 'ShadowRoot.getAnimations() does NOT return animations in parent document');
promise_test(async t => {
const div = createDiv(t);
const watcher = EventWatcher(t, div, 'transitionrun');
// Create a covering animation to prevent transitions from firing after
// calling getAnimations().
const coveringAnimation = new Animation(
new KeyframeEffect(div, { opacity: [0, 1] }, 100 * MS_PER_SEC)
);
// Setup transition start point.
div.style.transition = 'opacity 100s';
getComputedStyle(div).opacity;
// Update specified style but don't flush style.
div.style.opacity = '0.5';
// Fetch animations
document.getAnimations();
// Play the covering animation to ensure that only the call to
// getAnimations() has a chance to trigger transitions.
coveringAnimation.play();
// If getAnimations() flushed style, we should get a transitionrun event.
await watcher.wait_for('transitionrun');
}, 'Document.getAnimations() triggers a style change event');
</script>
</body>