Skip to content

Commit b7a71ca

Browse files
Psychpsyogmta
authored andcommitted
LibWeb: Correctly sort animations returned by getAnimations()
1 parent 2c389ae commit b7a71ca

File tree

6 files changed

+284
-14
lines changed

6 files changed

+284
-14
lines changed

Libraries/LibWeb/Animations/KeyframeEffect.cpp

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -627,11 +627,12 @@ void KeyframeEffect::generate_initial_and_final_frames(RefPtr<KeyFrameSet> keyfr
627627
// https://www.w3.org/TR/web-animations-1/#animation-composite-order
628628
int KeyframeEffect::composite_order(GC::Ref<KeyframeEffect> a, GC::Ref<KeyframeEffect> b)
629629
{
630-
// 1. Let the associated animation of an animation effect be the animation associated with the animation effect.
630+
// The relative composite order of any two keyframe effects A and B within an effect stack is established by
631+
// comparing their properties as follows:
631632
auto a_animation = a->associated_animation();
632633
auto b_animation = b->associated_animation();
633634

634-
// 2. Sort A and B by applying the following conditions in turn until the order is resolved,
635+
// 1. Sort A and B by applying the following conditions in turn until the order is resolved,
635636

636637
// 1. If A and B’s associated animations differ by class, sort by any inter-class composite order defined for
637638
// the corresponding classes.

Libraries/LibWeb/DOM/Document.cpp

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5472,12 +5472,7 @@ void Document::remove_replaced_animations()
54725472

54735473
WebIDL::ExceptionOr<Vector<GC::Ref<Animations::Animation>>> Document::get_animations()
54745474
{
5475-
Vector<GC::Ref<Animations::Animation>> relevant_animations;
5476-
TRY(for_each_child_of_type_fallible<Element>([&](auto& child) -> WebIDL::ExceptionOr<IterationDecision> {
5477-
relevant_animations.extend(TRY(child.get_animations(Animations::GetAnimationsOptions { .subtree = true })));
5478-
return IterationDecision::Continue;
5479-
}));
5480-
return relevant_animations;
5475+
return calculate_get_animations(*this);
54815476
}
54825477

54835478
// https://html.spec.whatwg.org/multipage/dom.html#dom-document-nameditem-filter

Libraries/LibWeb/DOM/DocumentOrShadowRoot.h

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
#pragma once
99

10+
#include <LibWeb/Animations/Animation.h>
1011
#include <LibWeb/DOM/Document.h>
1112
#include <LibWeb/DOM/Utils.h>
1213

@@ -47,4 +48,32 @@ GC::Ptr<Element> calculate_active_element(T& self)
4748
return nullptr;
4849
}
4950

51+
// https://drafts.csswg.org/web-animations-1/#dom-documentorshadowroot-getanimations
52+
template<DocumentOrShadowRoot T>
53+
WebIDL::ExceptionOr<Vector<GC::Ref<Animations::Animation>>> calculate_get_animations(T& self)
54+
{
55+
// Returns the set of relevant animations for a subtree for the document or shadow root on which this
56+
// method is called.
57+
Vector<GC::Ref<Animations::Animation>> relevant_animations;
58+
TRY(self.template for_each_child_of_type_fallible<Element>([&](auto& child) -> WebIDL::ExceptionOr<IterationDecision> {
59+
relevant_animations.extend(TRY(child.get_animations(Animations::GetAnimationsOptions { .subtree = true })));
60+
return IterationDecision::Continue;
61+
}));
62+
63+
// The returned list is sorted using the composite order described for the associated animations of
64+
// effects in § 5.4.2 The effect stack.
65+
quick_sort(relevant_animations, [](GC::Ref<Animations::Animation>& a, GC::Ref<Animations::Animation>& b) {
66+
auto& a_effect = as<Animations::KeyframeEffect>(*a->effect());
67+
auto& b_effect = as<Animations::KeyframeEffect>(*b->effect());
68+
return Animations::KeyframeEffect::composite_order(a_effect, b_effect) < 0;
69+
});
70+
71+
// Calling this method triggers a style change event for the document. As a result, the returned list
72+
// reflects the state after applying any pending style changes to animation such as changes to
73+
// animation-related style properties that have yet to be processed.
74+
// FIXME: Implement this.
75+
76+
return relevant_animations;
77+
}
78+
5079
}

Libraries/LibWeb/DOM/ShadowRoot.cpp

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -198,12 +198,7 @@ void ShadowRoot::for_each_css_style_sheet(Function<void(CSS::CSSStyleSheet&)>&&
198198

199199
WebIDL::ExceptionOr<Vector<GC::Ref<Animations::Animation>>> ShadowRoot::get_animations()
200200
{
201-
Vector<GC::Ref<Animations::Animation>> relevant_animations;
202-
TRY(for_each_child_of_type_fallible<Element>([&](auto& child) -> WebIDL::ExceptionOr<IterationDecision> {
203-
relevant_animations.extend(TRY(child.get_animations(Animations::GetAnimationsOptions { .subtree = true })));
204-
return IterationDecision::Continue;
205-
}));
206-
return relevant_animations;
201+
return calculate_get_animations(*this);
207202
}
208203

209204
ElementByIdMap& ShadowRoot::element_by_id() const
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
Harness status: OK
2+
3+
Found 11 tests
4+
5+
11 Pass
6+
Pass Document.getAnimations() returns an empty sequence for non-animated content
7+
Pass Document.getAnimations() returns script-generated animations
8+
Pass Document.getAnimations() returns script-generated animations in the order they were created
9+
Pass Document.getAnimations() does not return a disconnected node
10+
Pass Document.getAnimations() does not return an animation with a null target
11+
Pass Document.getAnimations() returns animations on elements inside same-origin iframes
12+
Pass iframe.contentDocument.getAnimations() returns animations on elements inside same-origin Document
13+
Pass ShadowRoot.getAnimations() return all animations in the shadow tree
14+
Pass Document.getAnimations() does NOT return animations in shadow trees
15+
Pass ShadowRoot.getAnimations() does NOT return animations in parent document
16+
Pass Document.getAnimations() triggers a style change event
Lines changed: 234 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,234 @@
1+
<!DOCTYPE html>
2+
<meta charset=utf-8>
3+
<title>DocumentOrShadowRoot.getAnimations</title>
4+
<link rel="help" href="https://drafts.csswg.org/web-animations-1/#dom-documentorshadowroot-getanimations">
5+
<script src="../../../resources/testharness.js"></script>
6+
<script src="../../../resources/testharnessreport.js"></script>
7+
<script src="../../testcommon.js"></script>
8+
<body>
9+
<div id="log"></div>
10+
<div id="target"></div>
11+
<script>
12+
'use strict';
13+
14+
const gKeyFrames = { 'marginLeft': ['100px', '200px'] };
15+
16+
test(t => {
17+
assert_equals(document.getAnimations().length, 0,
18+
'getAnimations returns an empty sequence for a document ' +
19+
'with no animations');
20+
}, 'Document.getAnimations() returns an empty sequence for non-animated'
21+
+ ' content');
22+
23+
test(t => {
24+
const div = createDiv(t);
25+
const anim1 = div.animate(gKeyFrames, 100 * MS_PER_SEC);
26+
const anim2 = div.animate(gKeyFrames, 100 * MS_PER_SEC);
27+
assert_equals(document.getAnimations().length, 2,
28+
'getAnimation returns running animations');
29+
30+
anim1.finish();
31+
anim2.finish();
32+
assert_equals(document.getAnimations().length, 0,
33+
'getAnimation only returns running animations');
34+
}, 'Document.getAnimations() returns script-generated animations')
35+
36+
test(t => {
37+
const div = createDiv(t);
38+
const anim1 = div.animate(gKeyFrames, 100 * MS_PER_SEC);
39+
const anim2 = div.animate(gKeyFrames, 100 * MS_PER_SEC);
40+
assert_array_equals(document.getAnimations(),
41+
[ anim1, anim2 ],
42+
'getAnimations() returns running animations');
43+
}, 'Document.getAnimations() returns script-generated animations in the order'
44+
+ ' they were created')
45+
46+
test(t => {
47+
// This element exists but is not a descendent of any document, so isn't
48+
// picked up by getAnimations.
49+
const div = document.createElement('div');
50+
const anim = div.animate(gKeyFrames, 100 * MS_PER_SEC);
51+
assert_equals(document.getAnimations().length, 0);
52+
53+
// Now connect the div; it should appear in the list of animations.
54+
document.body.appendChild(div);
55+
t.add_cleanup(() => { div.remove(); });
56+
assert_equals(document.getAnimations().length, 1);
57+
}, 'Document.getAnimations() does not return a disconnected node');
58+
59+
test(t => {
60+
const effect = new KeyframeEffect(null, gKeyFrames, 100 * MS_PER_SEC);
61+
const anim = new Animation(effect, document.timeline);
62+
anim.play();
63+
64+
assert_equals(document.getAnimations().length, 0,
65+
'document.getAnimations() only returns animations targeting ' +
66+
'elements in this document');
67+
}, 'Document.getAnimations() does not return an animation with a null target');
68+
69+
promise_test(async t => {
70+
const iframe = document.createElement('iframe');
71+
await insertFrameAndAwaitLoad(t, iframe, document)
72+
73+
const div = createDiv(t, iframe.contentDocument)
74+
const effect = new KeyframeEffect(div, null, 100 * MS_PER_SEC);
75+
const anim = new Animation(effect, document.timeline);
76+
anim.play();
77+
78+
// The animation's timeline is from the main document, but the effect's
79+
// target element is part of the iframe document and that is what matters
80+
// for getAnimations.
81+
assert_equals(document.getAnimations().length, 0);
82+
assert_equals(iframe.contentDocument.getAnimations().length, 1);
83+
anim.finish();
84+
}, 'Document.getAnimations() returns animations on elements inside same-origin'
85+
+ ' iframes');
86+
87+
promise_test(async t => {
88+
const iframe1 = document.createElement('iframe');
89+
const iframe2 = document.createElement('iframe');
90+
91+
await insertFrameAndAwaitLoad(t, iframe1, document);
92+
await insertFrameAndAwaitLoad(t, iframe2, document);
93+
94+
const div_frame1 = createDiv(t, iframe1.contentDocument)
95+
const div_main_frame = createDiv(t)
96+
const effect1 = new KeyframeEffect(div_frame1, null, 100 * MS_PER_SEC);
97+
const anim1 = new Animation(effect1, document.timeline);
98+
anim1.play();
99+
// Animation of div_frame1 is in iframe with main timeline.
100+
// The animation's timeline is from the iframe, but the effect's target
101+
// element is part of the iframe's document.
102+
assert_equals(document.getAnimations().length, 0);
103+
assert_equals(iframe1.contentDocument.getAnimations().length, 1);
104+
anim1.finish();
105+
106+
// animation of div_frame1 in iframe1 with iframe timeline
107+
const effect2 = new KeyframeEffect(div_frame1, null, 100 * MS_PER_SEC);
108+
const anim2 = new Animation(effect2, iframe1.contentDocument.timeline);
109+
anim2.play();
110+
assert_equals(document.getAnimations().length, 0);
111+
assert_equals(iframe1.contentDocument.getAnimations().length, 1);
112+
anim2.finish();
113+
114+
//animation of div_main_frame in main frame with iframe timeline
115+
const effect3 = new KeyframeEffect(div_main_frame, null, 100 * MS_PER_SEC);
116+
const anim3 = new Animation(effect3, iframe1.contentDocument.timeline);
117+
anim3.play();
118+
assert_equals(document.getAnimations().length, 1);
119+
assert_equals(iframe1.contentDocument.getAnimations().length, 0);
120+
anim3.finish();
121+
122+
//animation of div_frame1 in iframe1 with another iframe's timeline
123+
const effect4 = new KeyframeEffect(div_frame1, null, 100 * MS_PER_SEC);
124+
const anim4 = new Animation(effect4, iframe2.contentDocument.timeline);
125+
anim4.play();
126+
assert_equals(document.getAnimations().length, 0);
127+
assert_equals(iframe1.contentDocument.getAnimations().length, 1);
128+
assert_equals(iframe2.contentDocument.getAnimations().length, 0);
129+
anim4.finish();
130+
}, 'iframe.contentDocument.getAnimations() returns animations on elements '
131+
+ 'inside same-origin Document');
132+
133+
test(t => {
134+
const div = createDiv(t);
135+
const shadow = div.attachShadow({ mode: 'open' });
136+
137+
// Create a tree with the following structure
138+
//
139+
// div
140+
// |
141+
// (ShadowRoot)
142+
// / \
143+
// childA childB
144+
// (*anim2) |
145+
// grandChild
146+
// (*anim1)
147+
//
148+
// This lets us test that:
149+
//
150+
// a) All children of the ShadowRoot are included
151+
// b) Descendants of the children are included
152+
// c) The result is sorted by composite order (since we fire anim1 before
153+
// anim2 despite childA appearing first in tree order)
154+
155+
const childA = createDiv(t);
156+
shadow.append(childA);
157+
158+
const childB = createDiv(t);
159+
shadow.append(childB);
160+
161+
const grandChild = createDiv(t);
162+
childB.append(grandChild);
163+
164+
const anim1 = grandChild.animate(gKeyFrames, 100 * MS_PER_SEC)
165+
const anim2 = childA.animate(gKeyFrames, 100 * MS_PER_SEC)
166+
167+
assert_array_equals(
168+
div.shadowRoot.getAnimations(),
169+
[ anim1, anim2 ],
170+
'getAnimations() called on ShadowRoot returns expected animations'
171+
);
172+
}, 'ShadowRoot.getAnimations() return all animations in the shadow tree');
173+
174+
test(t => {
175+
const div = createDiv(t);
176+
const shadow = div.attachShadow({ mode: 'open' });
177+
178+
const child = createDiv(t);
179+
shadow.append(child);
180+
181+
child.animate(gKeyFrames, 100 * MS_PER_SEC)
182+
183+
assert_array_equals(
184+
document.getAnimations(),
185+
[],
186+
'getAnimations() called on Document does not return animations from shadow'
187+
+ ' trees'
188+
);
189+
}, 'Document.getAnimations() does NOT return animations in shadow trees');
190+
191+
test(t => {
192+
const div = createDiv(t);
193+
const shadow = div.attachShadow({ mode: 'open' });
194+
195+
div.animate(gKeyFrames, 100 * MS_PER_SEC)
196+
197+
assert_array_equals(
198+
div.shadowRoot.getAnimations(),
199+
[],
200+
'getAnimations() called on ShadowRoot does not return animations from'
201+
+ ' Document'
202+
);
203+
}, 'ShadowRoot.getAnimations() does NOT return animations in parent document');
204+
205+
promise_test(async t => {
206+
const div = createDiv(t);
207+
const watcher = EventWatcher(t, div, 'transitionrun');
208+
209+
// Create a covering animation to prevent transitions from firing after
210+
// calling getAnimations().
211+
const coveringAnimation = new Animation(
212+
new KeyframeEffect(div, { opacity: [0, 1] }, 100 * MS_PER_SEC)
213+
);
214+
215+
// Setup transition start point.
216+
div.style.transition = 'opacity 100s';
217+
getComputedStyle(div).opacity;
218+
219+
// Update specified style but don't flush style.
220+
div.style.opacity = '0.5';
221+
222+
// Fetch animations
223+
document.getAnimations();
224+
225+
// Play the covering animation to ensure that only the call to
226+
// getAnimations() has a chance to trigger transitions.
227+
coveringAnimation.play();
228+
229+
// If getAnimations() flushed style, we should get a transitionrun event.
230+
await watcher.wait_for('transitionrun');
231+
}, 'Document.getAnimations() triggers a style change event');
232+
233+
</script>
234+
</body>

0 commit comments

Comments
 (0)