Bug 1129454 - 1 - Adds [play|pause]All methods to the AnimationsActor to pause/play all running animations; r=miker
authorPatrick Brosset <pbrosset@mozilla.com>
Thu, 12 Feb 2015 16:28:42 +0100
changeset 256004 b04a0da319b6101555ed58555fb10c1c52a54fc9
parent 256003 fef1003fe34b3dc64085e5897ab180778d40ada4
child 256005 ecd8e5e79346d2feaa22de8259beff4d4a5e8b86
push id4610
push userjlund@mozilla.com
push dateMon, 30 Mar 2015 18:32:55 +0000
treeherdermozilla-beta@4df54044d9ef [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmiker
bugs1129454
milestone38.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1129454 - 1 - Adds [play|pause]All methods to the AnimationsActor to pause/play all running animations; r=miker
toolkit/devtools/server/actors/animation.js
toolkit/devtools/server/tests/browser/browser.ini
toolkit/devtools/server/tests/browser/browser_animation_actors_01.js
toolkit/devtools/server/tests/browser/browser_animation_actors_08.js
toolkit/devtools/server/tests/browser/browser_animation_actors_09.js
--- a/toolkit/devtools/server/actors/animation.js
+++ b/toolkit/devtools/server/actors/animation.js
@@ -20,22 +20,24 @@
  * References:
  * - WebAnimation spec:
  *   http://w3c.github.io/web-animations/
  * - WebAnimation WebIDL files:
  *   /dom/webidl/Animation*.webidl
  */
 
 const {Cu} = require("chrome");
+const {Promise: promise} = Cu.import("resource://gre/modules/Promise.jsm", {});
 const {Task} = Cu.import("resource://gre/modules/Task.jsm", {});
 const {setInterval, clearInterval} = require("sdk/timers");
 const protocol = require("devtools/server/protocol");
 const {ActorClass, Actor, FrontClass, Front, Arg, method, RetVal} = protocol;
 const {NodeActor} = require("devtools/server/actors/inspector");
 const EventEmitter = require("devtools/toolkit/event-emitter");
+const events = require("sdk/event/core");
 
 const PLAYER_DEFAULT_AUTO_REFRESH_TIMEOUT = 500; // ms
 
 /**
  * The AnimationPlayerActor provides information about a given animation: its
  * startTime, currentTime, current state, etc.
  *
  * Since the state of a player changes as the animation progresses it is often
@@ -144,17 +146,19 @@ let AnimationPlayerActor = ActorClass({
    * @return {Number}
    */
   getIterationCount: function() {
     let iterationText = this.styles.animationIterationCount;
     if (iterationText.indexOf(",") !== -1) {
       iterationText = iterationText.split(",")[this.playerIndex];
     }
 
-    return parseInt(iterationText, 10);
+    return iterationText === "infinite"
+           ? null
+           : parseInt(iterationText, 10);
   },
 
   /**
    * Get the current state of the AnimationPlayer (currentTime, playState, ...).
    * Note that the initial state is returned as the form of this actor when it
    * is initialized.
    * @return {Object}
    */
@@ -374,20 +378,27 @@ let AnimationPlayerFront = FrontClass(An
 /**
  * The Animations actor lists animation players for a given node.
  */
 let AnimationsActor = exports.AnimationsActor = ActorClass({
   typeName: "animations",
 
   initialize: function(conn, tabActor) {
     Actor.prototype.initialize.call(this, conn);
+    this.tabActor = tabActor;
+
+    this.allAnimationsPaused = false;
+    this.onNavigate = this.onNavigate.bind(this);
+    events.on(this.tabActor, "navigate", this.onNavigate);
   },
 
   destroy: function() {
     Actor.prototype.destroy.call(this);
+    events.off(this.tabActor, "navigate", this.onNavigate);
+    this.tabActor = null;
   },
 
   /**
    * Since AnimationsActor doesn't have a protocol.js parent actor that takes
    * care of its lifetime, implementing disconnect is required to cleanup.
    */
   disconnect: function() {
     this.destroy();
@@ -412,16 +423,87 @@ let AnimationsActor = exports.Animations
     return actors;
   }, {
     request: {
       actorID: Arg(0, "domnode")
     },
     response: {
       players: RetVal("array:animationplayer")
     }
+  }),
+
+  /**
+   * Iterates through all nodes in all of the tabActor's window documents and
+   * finds all existing animation players.
+   * This is currently used to allow playing/pausing all animations at once
+   * until the WebAnimations API provides a way to play/pause via the document
+   * timeline (alternatively, when bug 1123524 is fixed, we will be able to
+   * only iterate once and then listen for changes).
+   */
+  getAllAnimationPlayers: function() {
+    let players = [];
+
+    // These loops shouldn't be as bad as they look.
+    // Typically, there will be very few windows, and getElementsByTagName is
+    // really fast even on large DOM trees.
+    for (let window of this.tabActor.windows) {
+      let root = window.document.body || window.document;
+      for (let element of root.getElementsByTagNameNS("*", "*")) {
+        players = [...players, ...element.getAnimationPlayers()];
+      }
+    }
+
+    return players;
+  },
+
+  onNavigate: function({isTopLevel}) {
+    if (isTopLevel) {
+      this.allAnimationsPaused = false;
+    }
+  },
+
+  /**
+   * Pause all animations in the current tabActor's frames.
+   */
+  pauseAll: method(function() {
+    for (let player of this.getAllAnimationPlayers()) {
+      player.pause();
+    }
+    this.allAnimationsPaused = true;
+  }, {
+    request: {},
+    response: {}
+  }),
+
+  /**
+   * Play all animations in the current tabActor's frames.
+   * This method only returns when the animations have left their pending states.
+   */
+  playAll: method(function() {
+    let readyPromises = [];
+    for (let player of this.getAllAnimationPlayers()) {
+      player.play();
+      readyPromises.push(player.ready);
+    }
+    this.allAnimationsPaused = false;
+    return promise.all(readyPromises);
+  }, {
+    request: {},
+    response: {}
+  }),
+
+  toggleAll: method(function() {
+    if (this.allAnimationsPaused) {
+      return this.playAll();
+    } else {
+      return this.pauseAll();
+    }
+  }, {
+    request: {},
+    response: {}
   })
 });
 
 let AnimationsFront = exports.AnimationsFront = FrontClass(AnimationsActor, {
   initialize: function(client, {animationsActor}) {
     Front.prototype.initialize.call(this, client, {actor: animationsActor});
     this.manage(this);
   },
--- a/toolkit/devtools/server/tests/browser/browser.ini
+++ b/toolkit/devtools/server/tests/browser/browser.ini
@@ -17,16 +17,18 @@ support-files =
 
 [browser_animation_actors_01.js]
 [browser_animation_actors_02.js]
 [browser_animation_actors_03.js]
 [browser_animation_actors_04.js]
 [browser_animation_actors_05.js]
 [browser_animation_actors_06.js]
 [browser_animation_actors_07.js]
+[browser_animation_actors_08.js]
+[browser_animation_actors_09.js]
 [browser_navigateEvents.js]
 [browser_storage_dynamic_windows.js]
 [browser_storage_listings.js]
 [browser_storage_updates.js]
 [browser_timeline.js]
 skip-if = buildapp == 'mulet'
 [browser_timeline_actors.js]
 skip-if = buildapp == 'mulet'
--- a/toolkit/devtools/server/tests/browser/browser_animation_actors_01.js
+++ b/toolkit/devtools/server/tests/browser/browser_animation_actors_01.js
@@ -16,16 +16,19 @@ add_task(function*() {
   let client = new DebuggerClient(DebuggerServer.connectPipe());
   let form = yield connectDebuggerClient(client);
   let inspector = InspectorFront(client, form);
   let walker = yield inspector.getWalker();
   let front = AnimationsFront(client, form);
 
   ok(front, "The AnimationsFront was created");
   ok(front.getAnimationPlayersForNode, "The getAnimationPlayersForNode method exists");
+  ok(front.toggleAll, "The toggleAll method exists");
+  ok(front.playAll, "The playAll method exists");
+  ok(front.pauseAll, "The pauseAll method exists");
 
   let didThrow = false;
   try {
     yield front.getAnimationPlayersForNode(null);
   } catch (e) {
     didThrow = true;
   }
   ok(didThrow, "An exception was thrown for a missing NodeActor");
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/server/tests/browser/browser_animation_actors_08.js
@@ -0,0 +1,55 @@
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Check that the AnimationsActor can pause/play all animations at once.
+
+const {AnimationsFront} = require("devtools/server/actors/animation");
+const {InspectorFront} = require("devtools/server/actors/inspector");
+
+add_task(function*() {
+  let doc = yield addTab(MAIN_DOMAIN + "animation.html");
+
+  initDebuggerServer();
+  let client = new DebuggerClient(DebuggerServer.connectPipe());
+  let form = yield connectDebuggerClient(client);
+  let inspector = InspectorFront(client, form);
+  let walker = yield inspector.getWalker();
+  let front = AnimationsFront(client, form);
+
+  info("Pause all animations in the test document");
+  yield front.pauseAll();
+  yield checkAllAnimationsStates(walker, front, "paused");
+
+  info("Play all animations in the test document");
+  yield front.playAll();
+  yield checkAllAnimationsStates(walker, front, "running");
+
+  info("Pause all animations in the test document using toggleAll");
+  yield front.toggleAll();
+  yield checkAllAnimationsStates(walker, front, "paused");
+
+  info("Play all animations in the test document using toggleAll");
+  yield front.toggleAll();
+  yield checkAllAnimationsStates(walker, front, "running");
+
+  yield closeDebuggerClient(client);
+  gBrowser.removeCurrentTab();
+});
+
+function* checkAllAnimationsStates(walker, front, playState) {
+  info("Checking the playState of all the nodes that have infinite running animations");
+
+  let selectors = [".simple-animation", ".multiple-animations", ".delayed-animation"];
+  for (let selector of selectors) {
+    info("Getting the AnimationPlayerFront for node " + selector);
+    let node = yield walker.querySelector(walker.rootNode, selector);
+    let [player] = yield front.getAnimationPlayersForNode(node);
+    yield player.ready;
+    let state = yield player.getCurrentState();
+    is(state.playState, playState,
+      "The playState of node " + selector + " is " + playState);
+  }
+}
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/server/tests/browser/browser_animation_actors_09.js
@@ -0,0 +1,58 @@
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Check that the AnimationsActor can pause/play all animations even those
+// within iframes.
+
+const {AnimationsFront} = require("devtools/server/actors/animation");
+const {InspectorFront} = require("devtools/server/actors/inspector");
+
+const URL = MAIN_DOMAIN + "animation.html";
+
+add_task(function*() {
+  info("Creating a test document with 2 iframes containing animated nodes");
+  let doc = yield addTab("data:text/html;charset=utf-8," +
+                         "<iframe id='i1' src='" + URL + "'></iframe>" +
+                         "<iframe id='i2' src='" + URL + "'></iframe>");
+
+  initDebuggerServer();
+  let client = new DebuggerClient(DebuggerServer.connectPipe());
+  let form = yield connectDebuggerClient(client);
+  let inspector = InspectorFront(client, form);
+  let walker = yield inspector.getWalker();
+  let front = AnimationsFront(client, form);
+
+  info("Getting the 2 iframe container nodes and animated nodes in them");
+  let nodeInFrame1 = yield getNodeInFrame(walker, "#i1", ".simple-animation");
+  let nodeInFrame2 = yield getNodeInFrame(walker, "#i2", ".simple-animation");
+
+  info("Pause all animations in the test document");
+  yield front.pauseAll();
+  yield checkState(front, nodeInFrame1, "paused");
+  yield checkState(front, nodeInFrame2, "paused");
+
+  info("Play all animations in the test document");
+  yield front.playAll();
+  yield checkState(front, nodeInFrame1, "running");
+  yield checkState(front, nodeInFrame2, "running");
+
+  yield closeDebuggerClient(client);
+  gBrowser.removeCurrentTab();
+});
+
+function* checkState(front, nodeFront, playState) {
+  info("Getting the AnimationPlayerFront for the test node");
+  let [player] = yield front.getAnimationPlayersForNode(nodeFront);
+  yield player.ready;
+  let state = yield player.getCurrentState();
+  is(state.playState, playState, "The playState of the test node is " + playState);
+}
+
+function* getNodeInFrame(walker, frameSelector, nodeSelector) {
+  let iframe = yield walker.querySelector(walker.rootNode, frameSelector);
+  let {nodes} = yield walker.children(iframe);
+  return walker.querySelector(nodes[0], nodeSelector);
+}