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 228754 b04a0da319b6101555ed58555fb10c1c52a54fc9
parent 228753 fef1003fe34b3dc64085e5897ab180778d40ada4
child 228755 ecd8e5e79346d2feaa22de8259beff4d4a5e8b86
push id11309
push userpbrosset@mozilla.com
push dateThu, 12 Feb 2015 15:29:01 +0000
treeherderfx-team@ecd8e5e79346 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmiker
bugs1129454
milestone38.0a1
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);
+}