Skip to content

Commit 8188dec

Browse files
committed
Bug 2001590 - [devtools] Filter out AnimationPlayerActor instances that aren't handled by AnimationsActor. r=devtools-reviewers,bomsy.
`AnimationActor` `pauseSome`, `playSome`, `setCurrentTimes` and `setPlaybackRates` all take an array of `AnimationPlayerActor` instances for which we'll perform some operation on their underlying animation. It can happen that some of those actors are not handled by the `AnimationsActor` anymore (e.g. the animation stopped, but the client wasn't updated yet). Performing any action on those unhandled player (pausing, setting time, …) might actually trigger a new, unwanted, mutation, which will cause both the server state and the client state to be out of date. This patch filters out actors which are not into `AnimationsActor#actors` to avoid such issue. Differential Revision: https://phabricator.services.mozilla.com/D273562
1 parent 92e5a93 commit 8188dec

File tree

3 files changed

+139
-17
lines changed

3 files changed

+139
-17
lines changed

devtools/server/actors/animation.js

Lines changed: 51 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -829,11 +829,20 @@ exports.AnimationsActor = class AnimationsActor extends Actor {
829829
* @param {Array} actors A list of AnimationPlayerActor.
830830
*/
831831
pauseSome(actors) {
832-
for (const { player } of actors) {
833-
this.pauseSync(player);
832+
const handledActors = [];
833+
for (const actor of actors) {
834+
// The client could call this with actors that we no longer handle, as it might
835+
// not have received the mutations event yet for removed animations.
836+
// In such case, ignore the actor, as pausing the animation again might trigger a
837+
// new mutation, which would cause problems here and on the client.
838+
if (!this.actors.includes(actor)) {
839+
continue;
840+
}
841+
this.pauseSync(actor.player);
842+
handledActors.push(actor);
834843
}
835844

836-
return this.waitForNextFrame(actors);
845+
return this.waitForNextFrame(handledActors);
837846
}
838847

839848
/**
@@ -842,22 +851,39 @@ exports.AnimationsActor = class AnimationsActor extends Actor {
842851
* @param {Array} actors A list of AnimationPlayerActor.
843852
*/
844853
playSome(actors) {
845-
for (const { player } of actors) {
846-
this.playSync(player);
854+
const handledActors = [];
855+
for (const actor of actors) {
856+
// The client could call this with actors that we no longer handle, as it might
857+
// not have received the mutations event yet for removed animations.
858+
// In such case, ignore the actor, as playing the animation again might trigger a
859+
// new mutation, which would cause problems here and on the client.
860+
if (!this.actors.includes(actor)) {
861+
continue;
862+
}
863+
this.playSync(actor.player);
864+
handledActors.push(actor);
847865
}
848866

849-
return this.waitForNextFrame(actors);
867+
return this.waitForNextFrame(handledActors);
850868
}
851869

852870
/**
853871
* Set the current time of several animations at the same time.
854872
*
855-
* @param {Array} players A list of AnimationPlayerActor.
873+
* @param {Array} actors A list of AnimationPlayerActor.
856874
* @param {number} time The new currentTime.
857875
* @param {boolean} shouldPause Should the players be paused too.
858876
*/
859-
setCurrentTimes(players, time, shouldPause) {
860-
for (const actor of players) {
877+
setCurrentTimes(actors, time, shouldPause) {
878+
const handledActors = [];
879+
for (const actor of actors) {
880+
// The client could call this with actors that we no longer handle, as it might
881+
// not have received the mutations event yet for removed animations.
882+
// In such case, ignore the actor, as setting the time might trigger a
883+
// new mutation, which would cause problems here and on the client.
884+
if (!this.actors.includes(actor)) {
885+
continue;
886+
}
861887
const player = actor.player;
862888

863889
if (shouldPause) {
@@ -869,9 +895,10 @@ exports.AnimationsActor = class AnimationsActor extends Actor {
869895
? time - actor.createdTime
870896
: actor.createdTime - time;
871897
player.currentTime = currentTime * Math.abs(player.playbackRate);
898+
handledActors.push(actor);
872899
}
873900

874-
return this.waitForNextFrame(players);
901+
return this.waitForNextFrame(handledActors);
875902
}
876903

877904
/**
@@ -880,13 +907,20 @@ exports.AnimationsActor = class AnimationsActor extends Actor {
880907
* @param {Array} actors A list of AnimationPlayerActor.
881908
* @param {number} rate The new rate.
882909
*/
883-
setPlaybackRates(players, rate) {
884-
return Promise.all(
885-
players.map(({ player }) => {
886-
player.updatePlaybackRate(rate);
887-
return player.ready;
888-
})
889-
);
910+
setPlaybackRates(actors, rate) {
911+
const readyPromises = [];
912+
for (const actor of actors) {
913+
// The client could call this with actors that we no longer handle, as it might
914+
// not have received the mutations event yet for removed animations.
915+
// In such case, ignore the actor, as setting the playback rate might trigger a
916+
// new mutation, which would cause problems here and on the client.
917+
if (!this.actors.includes(actor)) {
918+
continue;
919+
}
920+
actor.player.updatePlaybackRate(rate);
921+
readyPromises.push(actor.player.ready);
922+
}
923+
return Promise.all(readyPromises);
890924
}
891925

892926
/**

devtools/server/tests/browser/browser.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,8 @@ skip-if = [
112112

113113
["browser_animation_simple.js"]
114114

115+
["browser_animation_unhandledActorPlayers.js"]
116+
115117
["browser_animation_updatedState.js"]
116118

117119
["browser_application_manifest.js"]
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
/* Any copyright is dedicated to the Public Domain.
2+
http://creativecommons.org/publicdomain/zero/1.0/ */
3+
4+
"use strict";
5+
6+
// Check that calling AnimationsActor method taking AnimationPlayerActor arrays (pauseSome,
7+
// playSome, setCurrentTimes, setPlaybackRates) with instances that are not handled by
8+
// the AnimationsActor anymore doesn't throw nor trigger unexpected animations (see Bug 2001590).
9+
10+
add_task(async function () {
11+
const { target, walker, animations } = await initAnimationsFrontForUrl(
12+
`data:text/html,<meta charset=utf8>${encodeURIComponent(`
13+
<style>
14+
#target {
15+
animation: my-anim 1s infinite alternate;
16+
17+
&.still {
18+
animation: none;
19+
}
20+
}
21+
@keyframes my-anim {
22+
to {
23+
background-color: tomato;
24+
}
25+
}
26+
</style>
27+
<div id=target>Hello</div>`)}`
28+
);
29+
30+
info("Retrieve an animated node");
31+
const node = await walker.querySelector(walker.rootNode, "#target");
32+
33+
const getAnimationPlayersForTargetNode = () =>
34+
animations.getAnimationPlayersForNode(node);
35+
36+
info("Retrieve the animation player for the node");
37+
const players = await getAnimationPlayersForTargetNode();
38+
is(players.length, 1, "Got one animation player");
39+
const animationPlayer = players[0];
40+
41+
info("Stop the animation on the node");
42+
await node.modifyAttributes([
43+
{
44+
attributeName: "class",
45+
newValue: "still",
46+
},
47+
]);
48+
49+
// Wait until we're not getting the animation anymore
50+
await waitFor(async () => {
51+
return (await getAnimationPlayersForTargetNode()).length === 0;
52+
});
53+
54+
info("Call methodes with outdated animationplayer front");
55+
const onPause = animations.pauseSome([animationPlayer]);
56+
const onPlay = animations.playSome([animationPlayer]);
57+
const onCurrentTimeSet = animations.setCurrentTimes(
58+
[animationPlayer],
59+
1,
60+
true
61+
);
62+
const onPlaybackRateSet = animations.setPlaybackRates([animationPlayer], 10);
63+
64+
await onPause;
65+
ok(true, "pauseSome succeeded");
66+
67+
await onPlay;
68+
ok(true, "playSome succedded");
69+
70+
await onCurrentTimeSet;
71+
ok(true, "setCurrentTimes succedded");
72+
73+
await onPlaybackRateSet;
74+
ok(true, "setPlaybackRates succedded");
75+
76+
// wait for a bit so we would get notified about new animations
77+
await wait(500);
78+
is(
79+
(await getAnimationPlayersForTargetNode()).length,
80+
0,
81+
"No players were created after calling those methods"
82+
);
83+
84+
await target.destroy();
85+
gBrowser.removeCurrentTab();
86+
});

0 commit comments

Comments
 (0)