tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

commit 8188dec434a7b65ed44c8caf35967479754113b6
parent 92e5a93352e46c7d019cd79f362336116e7cd543
Author: Nicolas Chevobbe <nchevobbe@mozilla.com>
Date:   Sun, 23 Nov 2025 22:45:24 +0000

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

Diffstat:
Mdevtools/server/actors/animation.js | 68+++++++++++++++++++++++++++++++++++++++++++++++++++-----------------
Mdevtools/server/tests/browser/browser.toml | 2++
Adevtools/server/tests/browser/browser_animation_unhandledActorPlayers.js | 86+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
3 files changed, 139 insertions(+), 17 deletions(-)

diff --git a/devtools/server/actors/animation.js b/devtools/server/actors/animation.js @@ -829,11 +829,20 @@ exports.AnimationsActor = class AnimationsActor extends Actor { * @param {Array} actors A list of AnimationPlayerActor. */ pauseSome(actors) { - for (const { player } of actors) { - this.pauseSync(player); + const handledActors = []; + for (const actor of actors) { + // The client could call this with actors that we no longer handle, as it might + // not have received the mutations event yet for removed animations. + // In such case, ignore the actor, as pausing the animation again might trigger a + // new mutation, which would cause problems here and on the client. + if (!this.actors.includes(actor)) { + continue; + } + this.pauseSync(actor.player); + handledActors.push(actor); } - return this.waitForNextFrame(actors); + return this.waitForNextFrame(handledActors); } /** @@ -842,22 +851,39 @@ exports.AnimationsActor = class AnimationsActor extends Actor { * @param {Array} actors A list of AnimationPlayerActor. */ playSome(actors) { - for (const { player } of actors) { - this.playSync(player); + const handledActors = []; + for (const actor of actors) { + // The client could call this with actors that we no longer handle, as it might + // not have received the mutations event yet for removed animations. + // In such case, ignore the actor, as playing the animation again might trigger a + // new mutation, which would cause problems here and on the client. + if (!this.actors.includes(actor)) { + continue; + } + this.playSync(actor.player); + handledActors.push(actor); } - return this.waitForNextFrame(actors); + return this.waitForNextFrame(handledActors); } /** * Set the current time of several animations at the same time. * - * @param {Array} players A list of AnimationPlayerActor. + * @param {Array} actors A list of AnimationPlayerActor. * @param {number} time The new currentTime. * @param {boolean} shouldPause Should the players be paused too. */ - setCurrentTimes(players, time, shouldPause) { - for (const actor of players) { + setCurrentTimes(actors, time, shouldPause) { + const handledActors = []; + for (const actor of actors) { + // The client could call this with actors that we no longer handle, as it might + // not have received the mutations event yet for removed animations. + // In such case, ignore the actor, as setting the time might trigger a + // new mutation, which would cause problems here and on the client. + if (!this.actors.includes(actor)) { + continue; + } const player = actor.player; if (shouldPause) { @@ -869,9 +895,10 @@ exports.AnimationsActor = class AnimationsActor extends Actor { ? time - actor.createdTime : actor.createdTime - time; player.currentTime = currentTime * Math.abs(player.playbackRate); + handledActors.push(actor); } - return this.waitForNextFrame(players); + return this.waitForNextFrame(handledActors); } /** @@ -880,13 +907,20 @@ exports.AnimationsActor = class AnimationsActor extends Actor { * @param {Array} actors A list of AnimationPlayerActor. * @param {number} rate The new rate. */ - setPlaybackRates(players, rate) { - return Promise.all( - players.map(({ player }) => { - player.updatePlaybackRate(rate); - return player.ready; - }) - ); + setPlaybackRates(actors, rate) { + const readyPromises = []; + for (const actor of actors) { + // The client could call this with actors that we no longer handle, as it might + // not have received the mutations event yet for removed animations. + // In such case, ignore the actor, as setting the playback rate might trigger a + // new mutation, which would cause problems here and on the client. + if (!this.actors.includes(actor)) { + continue; + } + actor.player.updatePlaybackRate(rate); + readyPromises.push(actor.player.ready); + } + return Promise.all(readyPromises); } /** diff --git a/devtools/server/tests/browser/browser.toml b/devtools/server/tests/browser/browser.toml @@ -112,6 +112,8 @@ skip-if = [ ["browser_animation_simple.js"] +["browser_animation_unhandledActorPlayers.js"] + ["browser_animation_updatedState.js"] ["browser_application_manifest.js"] diff --git a/devtools/server/tests/browser/browser_animation_unhandledActorPlayers.js b/devtools/server/tests/browser/browser_animation_unhandledActorPlayers.js @@ -0,0 +1,86 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Check that calling AnimationsActor method taking AnimationPlayerActor arrays (pauseSome, +// playSome, setCurrentTimes, setPlaybackRates) with instances that are not handled by +// the AnimationsActor anymore doesn't throw nor trigger unexpected animations (see Bug 2001590). + +add_task(async function () { + const { target, walker, animations } = await initAnimationsFrontForUrl( + `data:text/html,<meta charset=utf8>${encodeURIComponent(` + <style> + #target { + animation: my-anim 1s infinite alternate; + + &.still { + animation: none; + } + } + @keyframes my-anim { + to { + background-color: tomato; + } + } + </style> + <div id=target>Hello</div>`)}` + ); + + info("Retrieve an animated node"); + const node = await walker.querySelector(walker.rootNode, "#target"); + + const getAnimationPlayersForTargetNode = () => + animations.getAnimationPlayersForNode(node); + + info("Retrieve the animation player for the node"); + const players = await getAnimationPlayersForTargetNode(); + is(players.length, 1, "Got one animation player"); + const animationPlayer = players[0]; + + info("Stop the animation on the node"); + await node.modifyAttributes([ + { + attributeName: "class", + newValue: "still", + }, + ]); + + // Wait until we're not getting the animation anymore + await waitFor(async () => { + return (await getAnimationPlayersForTargetNode()).length === 0; + }); + + info("Call methodes with outdated animationplayer front"); + const onPause = animations.pauseSome([animationPlayer]); + const onPlay = animations.playSome([animationPlayer]); + const onCurrentTimeSet = animations.setCurrentTimes( + [animationPlayer], + 1, + true + ); + const onPlaybackRateSet = animations.setPlaybackRates([animationPlayer], 10); + + await onPause; + ok(true, "pauseSome succeeded"); + + await onPlay; + ok(true, "playSome succedded"); + + await onCurrentTimeSet; + ok(true, "setCurrentTimes succedded"); + + await onPlaybackRateSet; + ok(true, "setPlaybackRates succedded"); + + // wait for a bit so we would get notified about new animations + await wait(500); + is( + (await getAnimationPlayersForTargetNode()).length, + 0, + "No players were created after calling those methods" + ); + + await target.destroy(); + gBrowser.removeCurrentTab(); +});