Bug 1232681 - Display script-generated animations correctly. r=pbro
authorNicolas Chevobbe <chevobbe.nicolas@gmail.com>
Wed, 03 Feb 2016 23:21:44 +0100
changeset 284131 61089f9d1f6d8a624e7cc902f7b4db85357cf4e3
parent 284100 0add7cd89394362d9458cc1a9c3a63d8ac4205e2
child 284132 d07600bebb4483a586d0f72b3d5209b84435ab5d
push id19537
push userkwierso@gmail.com
push dateWed, 17 Feb 2016 19:16:23 +0000
treeherderb2g-inbound@0c04a9efadab [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerspbro
bugs1232681
milestone47.0a1
Bug 1232681 - Display script-generated animations correctly. r=pbro MozReview-Commit-ID: 2pk7sxVTHTk
devtools/client/animationinspector/components/animation-time-block.js
devtools/client/animationinspector/test/browser.ini
devtools/client/animationinspector/test/browser_animation_playerWidgets_appear_on_panel_init.js
devtools/client/animationinspector/test/doc_multiple_animation_types.html
devtools/client/locales/en-US/animationinspector.properties
devtools/client/themes/animationinspector.css
devtools/server/actors/animation.js
devtools/server/tests/unit/test_animation_name.js
devtools/server/tests/unit/test_animation_type.js
--- a/devtools/client/animationinspector/components/animation-time-block.js
+++ b/devtools/client/animationinspector/components/animation-time-block.js
@@ -143,18 +143,29 @@ AnimationTimeBlock.prototype = {
   onClick: function(e) {
     e.stopPropagation();
     this.emit("selected", this.animation);
   }
 };
 
 /**
  * Get a formatted title for this animation. This will be either:
- * "some-name", "some-name : CSS Transition", or "some-name : CSS Animation",
- * depending if the server provides the type, and what type it is.
+ * "some-name", "some-name : CSS Transition", "some-name : CSS Animation",
+ * "some-name : Script Animation", or "Script Animation", depending
+ * if the server provides the type, what type it is and if the animation
+ * has a name
  * @param {AnimationPlayerFront} animation
  */
 function getFormattedAnimationTitle({state}) {
-  // Older servers don't send the type.
-  return state.type
-    ? L10N.getFormatStr("timeline." + state.type + ".nameLabel", state.name)
-    : state.name;
+  // Older servers don't send a type, and only know about
+  // CSSAnimations and CSSTransitions, so it's safe to use
+  // just the name.
+  if (!state.type) {
+    return state.name;
+  }
+
+  // Script-generated animations may not have a name.
+  if (state.type === "scriptanimation" && !state.name) {
+    return L10N.getStr("timeline.scriptanimation.unnamedLabel");
+  }
+
+  return L10N.getFormatStr(`timeline.${state.type}.nameLabel`, state.name);
 }
--- a/devtools/client/animationinspector/test/browser.ini
+++ b/devtools/client/animationinspector/test/browser.ini
@@ -3,16 +3,17 @@ tags = devtools
 subsuite = devtools
 support-files =
   doc_body_animation.html
   doc_frame_script.js
   doc_keyframes.html
   doc_modify_playbackRate.html
   doc_negative_animation.html
   doc_simple_animation.html
+  doc_multiple_animation_types.html
   head.js
 
 [browser_animation_animated_properties_displayed.js]
 [browser_animation_click_selects_animation.js]
 [browser_animation_controller_exposes_document_currentTime.js]
 skip-if = os == "linux" && !debug # Bug 1234567
 [browser_animation_empty_on_invalid_nodes.js]
 [browser_animation_keyframe_click_to_set_time.js]
--- a/devtools/client/animationinspector/test/browser_animation_playerWidgets_appear_on_panel_init.js
+++ b/devtools/client/animationinspector/test/browser_animation_playerWidgets_appear_on_panel_init.js
@@ -2,16 +2,46 @@
 /* Any copyright is dedicated to the Public Domain.
  http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
 // Test that player widgets are displayed right when the animation panel is
 // initialized, if the selected node (<body> by default) is animated.
 
+const { ANIMATION_TYPES } = require("devtools/server/actors/animation");
+
 add_task(function*() {
-  yield addTab(TEST_URL_ROOT + "doc_body_animation.html");
+  yield new Promise(resolve => {
+    SpecialPowers.pushPrefEnv({"set": [
+      ["dom.animations-api.core.enabled", true]
+    ]}, resolve);
+  });
+
+  yield addTab(TEST_URL_ROOT + "doc_multiple_animation_types.html");
 
   let {panel} = yield openAnimationInspector();
-  is(panel.animationsTimelineComponent.animations.length, 1,
-    "One animation is handled by the timeline after init");
-  assertAnimationsDisplayed(panel, 1, "One animation is displayed after init");
+  is(panel.animationsTimelineComponent.animations.length, 3,
+    "Three animations are handled by the timeline after init");
+  assertAnimationsDisplayed(panel, 3,
+    "Three animations are displayed after init");
+  is(
+    panel.animationsTimelineComponent
+         .animationsEl
+         .querySelectorAll(`.animation.${ANIMATION_TYPES.SCRIPT_ANIMATION}`)
+         .length,
+    1,
+    "One script-generated animation is displayed");
+  is(
+    panel.animationsTimelineComponent
+         .animationsEl
+         .querySelectorAll(`.animation.${ANIMATION_TYPES.CSS_ANIMATION}`)
+         .length,
+    1,
+    "One CSS animation is displayed");
+  is(
+    panel.animationsTimelineComponent
+         .animationsEl
+         .querySelectorAll(`.animation.${ANIMATION_TYPES.CSS_TRANSITION}`)
+         .length,
+    1,
+    "One CSS transition is displayed");
 });
new file mode 100644
--- /dev/null
+++ b/devtools/client/animationinspector/test/doc_multiple_animation_types.html
@@ -0,0 +1,58 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+  <meta charset="UTF-8">
+  <style>
+    .ball {
+      width: 80px;
+      height: 80px;
+      border-radius: 50%;
+    }
+
+    .script-animation {
+      background: #f06;
+    }
+
+    .css-transition {
+      background: #006;
+      transition: background-color 20s;
+    }
+
+    .css-animation {
+      background: #a06;
+      animation: flash 10s forwards;
+    }
+
+    @keyframes flash {
+      0% {
+        opacity: 1;
+      }
+      50% {
+        opacity: 0;
+      }
+      100% {
+        opacity: 1;
+      }
+    }
+  </style>
+</head>
+<body>
+  <div class="ball script-animation"></div>
+  <div class="ball css-animation"></div>
+  <div class="ball css-transition"></div>
+
+  <script>
+    setTimeout(function(){
+      document.querySelector(".css-transition").style.backgroundColor = "yellow";
+    }, 0);
+
+    document.querySelector(".script-animation").animate([
+      {  opacity: 1, offset: 0 },
+      {  opacity: .1, offset: 1 }
+    ], {
+      duration: 10000,
+      fill: "forwards"
+    });
+  </script>
+</body>
+</html>
--- a/devtools/client/locales/en-US/animationinspector.properties
+++ b/devtools/client/locales/en-US/animationinspector.properties
@@ -103,14 +103,25 @@ timeline.timeGraduationLabel=%Sms
 timeline.cssanimation.nameLabel=%S - CSS Animation
 
 # LOCALIZATION NOTE (timeline.csstransition.nameLabel):
 # This string is displayed in a tooltip of the animation panel that is shown
 # when hovering over the name of a CSS Transition in the timeline UI.
 # %S will be replaced by the name of the transition at run-time.
 timeline.csstransition.nameLabel=%S - CSS Transition
 
+# LOCALIZATION NOTE (timeline.scriptanimation.nameLabel):
+# This string is displayed in a tooltip of the animation panel that is shown
+# when hovering over the name of a script-generated animation in the timeline UI.
+# %S will be replaced by the name of the animation at run-time.
+timeline.scriptanimation.nameLabel=%S - Script Animation
+
+# LOCALIZATION NOTE (timeline.scriptanimation.unnamedLabel):
+# This string is displayed in a tooltip of the animation panel that is shown
+# when hovering over an unnamed script-generated animation in the timeline UI.
+timeline.scriptanimation.unnamedLabel=Script Animation
+
 # LOCALIZATION NOTE (timeline.unknown.nameLabel):
 # This string is displayed in a tooltip of the animation panel that is shown
 # when hovering over the name of an unknown animation type in the timeline UI.
 # This can happen if devtools couldn't figure out the type of the animation.
 # %S will be replaced by the name of the transition at run-time.
 timeline.unknown.nameLabel=%S
--- a/devtools/client/themes/animationinspector.css
+++ b/devtools/client/themes/animationinspector.css
@@ -37,16 +37,21 @@
   --timeline-background-color: var(--theme-contrast-background);
 }
 
 .animation.csstransition {
   --timeline-border-color: var(--theme-highlight-bluegrey);
   --timeline-background-color: var(--theme-highlight-blue);
 }
 
+.animation.scriptanimation {
+  --timeline-border-color: var(--theme-highlight-green);
+  --timeline-background-color: var(--theme-graphs-green);
+}
+
 html {
   height: 100%;
 }
 
 body {
   margin: 0;
   padding: 0;
   display : flex;
@@ -524,16 +529,20 @@ body {
 .keyframes.cssanimation {
   background-color: var(--theme-contrast-background);
 }
 
 .keyframes.csstransition {
   background-color: var(--theme-highlight-blue);
 }
 
+.keyframes.scriptanimation {
+  background-color: var(--theme-graphs-green);
+}
+
 .keyframes .frame {
   position: absolute;
   top: 0;
   width: 0;
   height: 0;
   background-color: inherit;
   cursor: pointer;
 }
--- a/devtools/server/actors/animation.js
+++ b/devtools/server/actors/animation.js
@@ -34,16 +34,17 @@ const {ActorClass, Actor, FrontClass, Fr
 // Make sure the nodeActor type is know here.
 const {NodeActor} = require("devtools/server/actors/inspector");
 const events = require("sdk/event/core");
 
 // Types of animations.
 const ANIMATION_TYPES = {
   CSS_ANIMATION: "cssanimation",
   CSS_TRANSITION: "csstransition",
+  SCRIPT_ANIMATION: "scriptanimation",
   UNKNOWN: "unknown"
 };
 exports.ANIMATION_TYPES = ANIMATION_TYPES;
 
 /**
  * The AnimationPlayerActor provides information about a given animation: its
  * startTime, currentTime, current state, etc.
  *
@@ -114,46 +115,55 @@ var AnimationPlayerActor = ActorClass({
     // return its corresponding NodeActor ID too.
     if (this.walker && this.walker.hasNode(this.node)) {
       data.animationTargetNodeActorID = this.walker.getNode(this.node).actorID;
     }
 
     return data;
   },
 
-  isAnimation: function(player = this.player) {
+  isCssAnimation: function(player = this.player) {
     return player instanceof this.window.CSSAnimation;
   },
 
-  isTransition: function(player = this.player) {
+  isCssTransition: function(player = this.player) {
     return player instanceof this.window.CSSTransition;
   },
 
+  isScriptAnimation: function(player = this.player) {
+    return player instanceof this.window.Animation && !(
+      player instanceof this.window.CSSAnimation ||
+      player instanceof this.window.CSSTransition
+    );
+  },
+
   getType: function() {
-    if (this.isAnimation()) {
+    if (this.isCssAnimation()) {
       return ANIMATION_TYPES.CSS_ANIMATION;
-    } else if (this.isTransition()) {
+    } else if (this.isCssTransition()) {
       return ANIMATION_TYPES.CSS_TRANSITION;
+    } else if (this.isScriptAnimation()) {
+      return ANIMATION_TYPES.SCRIPT_ANIMATION;
     }
 
     return ANIMATION_TYPES.UNKNOWN;
   },
 
   /**
    * Get the name of this animation. This can be either the animation.id
    * property if it was set, or the keyframe rule name or the transition
    * property.
    * @return {String}
    */
   getName: function() {
     if (this.player.id) {
       return this.player.id;
-    } else if (this.isAnimation()) {
+    } else if (this.isCssAnimation()) {
       return this.player.animationName;
-    } else if (this.isTransition()) {
+    } else if (this.isCssTransition()) {
       return this.player.transitionProperty;
     }
 
     return "";
   },
 
   /**
    * Get the animation duration from this player, in milliseconds.
@@ -621,19 +631,19 @@ var AnimationsActor = exports.Animations
         if (this.actors.find(a => a.player === player)) {
           continue;
         }
         // If the added player has the same name and target node as a player we
         // already have, it means it's a transition that's re-starting. So send
         // a "removed" event for the one we already have.
         let index = this.actors.findIndex(a => {
           let isSameType = a.player.constructor === player.constructor;
-          let isSameName = (a.isAnimation() &&
+          let isSameName = (a.isCssAnimation() &&
                             a.player.animationName === player.animationName) ||
-                           (a.isTransition() &&
+                           (a.isCssTransition() &&
                             a.player.transitionProperty === player.transitionProperty);
           let isSameNode = a.player.effect.target === player.effect.target;
 
           return isSameType && isSameNode && isSameName;
         });
         if (index !== -1) {
           eventData.push({
             type: "removed",
--- a/devtools/server/tests/unit/test_animation_name.js
+++ b/devtools/server/tests/unit/test_animation_name.js
@@ -19,16 +19,19 @@ function run_test() {
     CSSAnimation: function() {
       this.effect = {target: getMockNode()};
     },
     CSSTransition: function() {
       this.effect = {target: getMockNode()};
     }
   };
 
+  window.CSSAnimation.prototype = Object.create(window.Animation.prototype);
+  window.CSSTransition.prototype = Object.create(window.Animation.prototype);
+
   // Helper to get a mock DOM node.
   function getMockNode() {
     return {
       ownerDocument: {
         defaultView: window
       }
     };
   }
@@ -42,16 +45,21 @@ function run_test() {
   // - expectedName {String} The expected name returned by
   //   AnimationPlayerActor.getName.
   const TEST_DATA = [{
     desc: "Animation with an id",
     animation: new window.Animation(),
     props: { id: "animation-id" },
     expectedName: "animation-id"
   }, {
+    desc: "Animation without an id",
+    animation: new window.Animation(),
+    props: {},
+    expectedName: ""
+  }, {
     desc: "CSSTransition with an id",
     animation: new window.CSSTransition(),
     props: { id: "transition-with-id", transitionProperty: "width" },
     expectedName: "transition-with-id"
   }, {
     desc: "CSSAnimation with an id",
     animation: new window.CSSAnimation(),
     props: { id: "animation-with-id", animationName: "move" },
--- a/devtools/server/tests/unit/test_animation_type.js
+++ b/devtools/server/tests/unit/test_animation_type.js
@@ -19,16 +19,19 @@ function run_test() {
     CSSAnimation: function() {
       this.effect = {target: getMockNode()};
     },
     CSSTransition: function() {
       this.effect = {target: getMockNode()};
     }
   };
 
+  window.CSSAnimation.prototype = Object.create(window.Animation.prototype);
+  window.CSSTransition.prototype = Object.create(window.Animation.prototype);
+
   // Helper to get a mock DOM node.
   function getMockNode() {
     return {
       ownerDocument: {
         defaultView: window
       }
     };
   }
@@ -43,16 +46,20 @@ function run_test() {
     desc: "Test CSSAnimation type",
     animation: new window.CSSAnimation(),
     expectedType: ANIMATION_TYPES.CSS_ANIMATION
   }, {
     desc: "Test CSSTransition type",
     animation: new window.CSSTransition(),
     expectedType: ANIMATION_TYPES.CSS_TRANSITION
   }, {
+    desc: "Test ScriptAnimation type",
+    animation: new window.Animation(),
+    expectedType: ANIMATION_TYPES.SCRIPT_ANIMATION
+  }, {
     desc: "Test unknown type",
     animation: {effect: {target: getMockNode()}},
     expectedType: ANIMATION_TYPES.UNKNOWN
   }];
 
   for (let { desc, animation, expectedType } of TEST_DATA) {
     do_print(desc);
     let actor = AnimationPlayerActor({}, animation);