Bug 1228005 - 2 - Tests for the keyframes and animated properties panel; r=tromey
authorPatrick Brosset <pbrosset@mozilla.com>
Fri, 04 Dec 2015 12:14:43 +0100
changeset 275982 973264cc53e4e859f8c76b95c006a6aed16d0a8d
parent 275981 2a266b600cef638866608c9aa9ee25473f9ea86f
child 275983 a6a0a7cfd581709bb2b43c5bebb5e5fbccfa8584
push id18951
push usercbook@mozilla.com
push dateThu, 10 Dec 2015 11:45:16 +0000
treeherderb2g-inbound@c25d5a6ca04d [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerstromey
bugs1228005
milestone45.0a1
Bug 1228005 - 2 - Tests for the keyframes and animated properties panel; r=tromey
devtools/client/animationinspector/components.js
devtools/client/animationinspector/test/browser.ini
devtools/client/animationinspector/test/browser_animation_animated_properties_displayed.js
devtools/client/animationinspector/test/browser_animation_click_selects_animation.js
devtools/client/animationinspector/test/browser_animation_keyframe_click_to_set_time.js
devtools/client/animationinspector/test/browser_animation_keyframe_markers.js
devtools/client/animationinspector/test/doc_keyframes.html
devtools/client/animationinspector/test/head.js
devtools/client/animationinspector/test/unit/test_getCssPropertyName.js
devtools/client/animationinspector/test/unit/xpcshell.ini
--- a/devtools/client/animationinspector/components.js
+++ b/devtools/client/animationinspector/components.js
@@ -1341,8 +1341,9 @@ function getFormattedAnimationTitle({sta
  * Turn propertyName into property-name.
  * @param {String} jsPropertyName A camelcased CSS property name. Typically
  * something that comes out of computed styles. E.g. borderBottomColor
  * @return {String} The corresponding CSS property name: border-bottom-color
  */
 function getCssPropertyName(jsPropertyName) {
   return jsPropertyName.replace(/[A-Z]/g, "-$&").toLowerCase();
 }
+exports.getCssPropertyName = getCssPropertyName;
--- a/devtools/client/animationinspector/test/browser.ini
+++ b/devtools/client/animationinspector/test/browser.ini
@@ -1,22 +1,26 @@
 [DEFAULT]
 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
   head.js
 
+[browser_animation_animated_properties_displayed.js]
 [browser_animation_click_selects_animation.js]
 [browser_animation_controller_exposes_document_currentTime.js]
 [browser_animation_empty_on_invalid_nodes.js]
+[browser_animation_keyframe_click_to_set_time.js]
+[browser_animation_keyframe_markers.js]
 [browser_animation_mutations_with_same_names.js]
 [browser_animation_panel_exists.js]
 [browser_animation_participate_in_inspector_update.js]
 [browser_animation_playerFronts_are_refreshed.js]
 [browser_animation_playerWidgets_appear_on_panel_init.js]
 [browser_animation_playerWidgets_target_nodes.js]
 [browser_animation_refresh_on_added_animation.js]
 [browser_animation_refresh_on_removed_animation.js]
new file mode 100644
--- /dev/null
+++ b/devtools/client/animationinspector/test/browser_animation_animated_properties_displayed.js
@@ -0,0 +1,67 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that when an animation is selected, its list of animated properties is
+// displayed below it.
+
+const EXPECTED_PROPERTIES = [
+  "background-color",
+  "background-position",
+  "background-size",
+  "border-bottom-left-radius",
+  "border-bottom-right-radius",
+  "border-top-left-radius",
+  "border-top-right-radius",
+  "filter",
+  "height",
+  "transform",
+  "width"
+].sort();
+
+add_task(function*() {
+  yield addTab(TEST_URL_ROOT + "doc_keyframes.html");
+  let {panel} = yield openAnimationInspector();
+  let timeline = panel.animationsTimelineComponent;
+  let propertiesList = timeline.rootWrapperEl
+                               .querySelector(".animated-properties");
+
+  ok(!isNodeVisible(propertiesList),
+     "The list of properties panel is hidden by default");
+
+  info("Click to select the animation");
+  yield clickOnAnimation(panel, 0);
+
+  ok(isNodeVisible(propertiesList),
+     "The list of properties panel is shown");
+  ok(propertiesList.querySelectorAll(".property").length,
+     "The list of properties panel actually contains properties");
+  ok(hasExpectedProperties(propertiesList),
+     "The list of proeprties panel contains the right properties");
+
+  info("Click to unselect the animation");
+  yield clickOnAnimation(panel, 0, true);
+
+  ok(!isNodeVisible(propertiesList),
+     "The list of properties panel is hidden again");
+});
+
+function hasExpectedProperties(containerEl) {
+  let names = [...containerEl.querySelectorAll(".property .name")]
+              .map(n => n.textContent)
+              .sort();
+
+  if (names.length !== EXPECTED_PROPERTIES.length) {
+    return false;
+  }
+
+  for (let i = 0; i < names.length; i++) {
+    if (names[i] !== EXPECTED_PROPERTIES[i]) {
+      return false;
+    }
+  }
+
+  return true;
+}
--- a/devtools/client/animationinspector/test/browser_animation_click_selects_animation.js
+++ b/devtools/client/animationinspector/test/browser_animation_click_selects_animation.js
@@ -11,45 +11,34 @@ add_task(function*() {
   yield addTab(TEST_URL_ROOT + "doc_simple_animation.html");
   let {panel} = yield openAnimationInspector();
   let timeline = panel.animationsTimelineComponent;
 
   let selected = timeline.rootWrapperEl.querySelectorAll(".animation.selected");
   ok(!selected.length, "There are no animations selected by default");
 
   info("Click on the first animation, expect the right event and right class");
-  let animation0 = yield clickToChangeSelection(timeline, 0);
+  let animation0 = yield clickOnAnimation(panel, 0);
   is(animation0, timeline.animations[0],
      "The selected event was emitted with the right animation");
   ok(isTimeBlockSelected(timeline, 0),
      "The time block has the right selected class");
 
   info("Click on the second animation, expect it to be selected too");
-  let animation1 = yield clickToChangeSelection(timeline, 1);
+  let animation1 = yield clickOnAnimation(panel, 1);
   is(animation1, timeline.animations[1],
      "The selected event was emitted with the right animation");
   ok(isTimeBlockSelected(timeline, 1),
      "The second time block has the right selected class");
 
   info("Click again on the first animation and check if it unselects");
-  yield clickToChangeSelection(timeline, 0, true);
+  yield clickOnAnimation(panel, 0, true);
   ok(!isTimeBlockSelected(timeline, 0),
      "The first time block has been unselected");
 });
 
-function* clickToChangeSelection(timeline, index, isUnselect) {
-  info("Click on animation " + index + " in the timeline");
-  let onSelectionChanged = timeline.once(isUnselect
-                                         ? "animation-unselected"
-                                         : "animation-selected");
-  let timeBlock = timeline.rootWrapperEl.querySelectorAll(".time-block")[index];
-  EventUtils.sendMouseEvent({type: "click"}, timeBlock,
-                            timeBlock.ownerDocument.defaultView);
-  return yield onSelectionChanged;
-}
-
 function isTimeBlockSelected(timeline, index) {
   let animation = timeline.rootWrapperEl.querySelectorAll(".animation")[index];
   let animatedProperties = timeline.rootWrapperEl.querySelectorAll(
     ".animated-properties")[index];
   return animation.classList.contains("selected") &&
          animatedProperties.classList.contains("selected");
 }
new file mode 100644
--- /dev/null
+++ b/devtools/client/animationinspector/test/browser_animation_keyframe_click_to_set_time.js
@@ -0,0 +1,52 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that animated properties' keyframes can be clicked, and that doing so
+// sets the current time in the timeline.
+
+add_task(function*() {
+  yield addTab(TEST_URL_ROOT + "doc_keyframes.html");
+  let {panel} = yield openAnimationInspector();
+  let timeline = panel.animationsTimelineComponent;
+  let {scrubberEl} = timeline;
+
+  // XXX: The scrollbar is placed in the timeline in such a way that it causes
+  // the animations to be slightly offset with the header when it appears.
+  // So for now, let's hide the scrollbar. Bug 1229340 should fix this.
+  timeline.animationsEl.style.overflow = "hidden";
+
+  info("Expand the animation");
+  yield clickOnAnimation(panel, 0);
+
+  info("Click on the first keyframe of the first animated property");
+  yield clickKeyframe(panel, 0, "backgroundColor", 0);
+
+  info("Make sure the scrubber stopped moving and is at the right position");
+  yield assertScrubberMoving(panel, false);
+  checkScrubberPos(scrubberEl, 0);
+
+  info("Click on a keyframe in the middle");
+  yield clickKeyframe(panel, 0, "transform", 2);
+
+  info("Make sure the scrubber is at the right position");
+  checkScrubberPos(scrubberEl, 50);
+});
+
+function* clickKeyframe(panel, animIndex, property, index) {
+  let keyframeComponent = getKeyframeComponent(panel, animIndex, property);
+  let keyframeEl = getKeyframeEl(panel, animIndex, property, index);
+
+  let onSelect = keyframeComponent.once("frame-selected");
+  EventUtils.sendMouseEvent({type: "click"}, keyframeEl,
+                            keyframeEl.ownerDocument.defaultView);
+  yield onSelect;
+}
+
+function checkScrubberPos(scrubberEl, pos) {
+  let newPos = Math.round(parseFloat(scrubberEl.style.left));
+  let expectedPos = Math.round(pos);
+  is(newPos, expectedPos, `The scrubber is at ${pos}%`);
+}
new file mode 100644
--- /dev/null
+++ b/devtools/client/animationinspector/test/browser_animation_keyframe_markers.js
@@ -0,0 +1,74 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that when an animation is selected and its list of properties is shown,
+// there are keyframes markers next to each property being animated.
+
+const EXPECTED_PROPERTIES = [
+  "backgroundColor",
+  "backgroundPosition",
+  "backgroundSize",
+  "borderBottomLeftRadius",
+  "borderBottomRightRadius",
+  "borderTopLeftRadius",
+  "borderTopRightRadius",
+  "filter",
+  "height",
+  "transform",
+  "width"
+];
+
+add_task(function*() {
+  yield addTab(TEST_URL_ROOT + "doc_keyframes.html");
+  let {panel} = yield openAnimationInspector();
+  let timeline = panel.animationsTimelineComponent;
+
+  info("Expand the animation");
+  yield clickOnAnimation(panel, 0);
+
+  ok(timeline.rootWrapperEl.querySelectorAll(".frames .keyframes").length,
+     "There are container elements for displaying keyframes");
+
+  let data = yield getExpectedKeyframesData(timeline.animations[0]);
+  for (let propertyName in data) {
+    info("Check the keyframe markers for " + propertyName);
+    let widthMarkerSelector = ".frame[data-property=" + propertyName + "]";
+    let markers = timeline.rootWrapperEl.querySelectorAll(widthMarkerSelector);
+
+    is(markers.length, data[propertyName].length,
+       "The right number of keyframes was found for " + propertyName);
+
+    let offsets = [...markers].map(m => parseFloat(m.dataset.offset));
+    let values = [...markers].map(m => m.dataset.value);
+    for (let i = 0; i < markers.length; i++) {
+      is(markers[i].dataset.offset, offsets[i],
+         "Marker " + i + " for " + propertyName + " has the right offset");
+      is(markers[i].dataset.value, values[i],
+         "Marker " + i + " for " + propertyName + " has the right value");
+    }
+  }
+});
+
+function* getExpectedKeyframesData(animation) {
+  // We're testing the UI state here, so it's fine to get the list of expected
+  // keyframes from the animation actor.
+  let frames = yield animation.getFrames();
+  let data = {};
+
+  for (let property of EXPECTED_PROPERTIES) {
+    data[property] = [];
+    for (let frame of frames) {
+      if (typeof frame[property] !== "undefined") {
+        data[property].push({
+          offset: frame.computedOffset,
+          value: frame[property]
+        });
+      }
+    }
+  }
+
+  return data;
+}
new file mode 100644
--- /dev/null
+++ b/devtools/client/animationinspector/test/doc_keyframes.html
@@ -0,0 +1,55 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+  <meta charset="UTF-8">
+  <title>Yay! Keyframes!</title>
+  <style>
+    div {
+      animation: wow 10s forwards;
+    }
+    @keyframes wow {
+      0% {
+        width: 100px;
+        height: 100px;
+        border-radius: 0px;
+        background: #f06;
+      }
+      10% {
+        border-radius: 2px;
+      }
+      20% {
+        transform: rotate(13deg);
+      }
+      30% {
+        background: gold;
+      }
+      40% {
+        filter: blur(40px);
+      }
+      50% {
+        transform: rotate(720deg) translateX(300px) skew(-13deg);
+      }
+      60% {
+        width: 200px;
+        height: 200px;
+      }
+      70% {
+        border-radius: 10px;
+      }
+      80% {
+        background: #333;
+      }
+      90% {
+        border-radius: 50%;
+      }
+      100% {
+        width: 500px;
+        height: 500px;
+      }
+    }
+  </style>
+</head>
+<body>
+  <div></div>
+</body>
+</html>
--- a/devtools/client/animationinspector/test/head.js
+++ b/devtools/client/animationinspector/test/head.js
@@ -319,108 +319,16 @@ function executeInContent(name, data={},
   mm.sendAsyncMessage(name, data, objects);
   if (expectResponse) {
     return waitForContentMessage(name);
   }
 
   return promise.resolve();
 }
 
-function onceNextPlayerRefresh(player) {
-  let onRefresh = promise.defer();
-  player.once(player.AUTO_REFRESH_EVENT, onRefresh.resolve);
-  return onRefresh.promise;
-}
-
-/**
- * Simulate a click on the playPause button of a playerWidget.
- */
-var togglePlayPauseButton = Task.async(function*(widget) {
-  let nextState = widget.player.state.playState === "running"
-                  ? "paused"
-                  : "running";
-
-  // Note that instead of simulating a real event here, the callback is just
-  // called. This is better because the callback returns a promise, so we know
-  // when the player is paused, and we don't really care to test that simulating
-  // a DOM event actually works.
-  let onClicked = widget.onPlayPauseBtnClick();
-
-  // Verify that the button's state is changed immediately, even if it will be
-  // changed anyway with the next auto-refresh.
-  ok(widget.el.classList.contains(nextState),
-    "The button's state was changed in the UI before the request was sent");
-
-  yield onClicked;
-
-  // Wait until the state changes.
-  yield waitForPlayState(widget.player, nextState);
-});
-
-/**
- * Wait for a player's auto-refresh events and stop when a condition becomes
- * truthy.
- * @param {AnimationPlayerFront} player
- * @param {Function} conditionCheck Will be called over and over again when the
- * player state changes, passing the state as argument. This method must return
- * a truthy value to stop waiting.
- * @param {String} desc If provided, this will be logged with info(...) every
- * time the state is refreshed, until the condition passes.
- * @return {Promise} Resolves when the condition passes.
- */
-var waitForStateCondition = Task.async(function*(player, conditionCheck, desc="") {
-  if (desc) {
-    desc = "(" + desc + ")";
-  }
-  info("Waiting for a player's auto-refresh event " + desc);
-  let def = promise.defer();
-  player.on(player.AUTO_REFRESH_EVENT, function onNewState() {
-    info("State refreshed, checking condition ... " + desc);
-    if (conditionCheck(player.state)) {
-      player.off(player.AUTO_REFRESH_EVENT, onNewState);
-      def.resolve();
-    }
-  });
-  return def.promise;
-});
-
-/**
- * Wait for a player's auto-refresh events and stop when the playState is the
- * provided string.
- * @param {AnimationPlayerFront} player
- * @param {String} playState The playState to expect.
- * @return {Promise} Resolves when the playState has changed to the expected
- * value.
- */
-function waitForPlayState(player, playState) {
-  return waitForStateCondition(player, state => {
-    return state.playState === playState;
-  }, "Waiting for animation to be " + playState);
-}
-
-/**
- * Wait for the player's auto-refresh events until the animation is paused.
- * When done, check its currentTime.
- * @param {PlayerWidget} widget.
- * @param {Numer} time.
- * @return {Promise} Resolves when the animation is paused and tests have ran.
- */
-var checkPausedAt = Task.async(function*(widget, time) {
-  info("Wait for the next auto-refresh");
-
-  yield waitForStateCondition(widget.player, state => {
-    return state.playState === "paused" && state.currentTime === time;
-  }, "Waiting for animation to pause at " + time + "ms");
-
-  ok(widget.el.classList.contains("paused"), "The widget is in paused mode");
-  is(widget.player.state.currentTime, time,
-    "The player front's currentTime was set to " + time);
-  is(widget.currentTimeEl.value, time, "The input's value was set to " + time);
-});
-
 /**
  * Get the current playState of an animation player on a given node.
  */
 var getAnimationPlayerState = Task.async(function*(selector, animationIndex=0) {
   let playState = yield executeInContent("Test:GetAnimationPlayerState",
                                          {selector, animationIndex});
   return playState;
 });
@@ -551,8 +459,66 @@ function disableHighlighter(toolbox) {
     showBoxModel: () => new Promise(r => r()),
     hideBoxModel: () => new Promise(r => r()),
     pick: () => new Promise(r => r()),
     cancelPick: () => new Promise(r => r()),
     destroy: () => {},
     traits: {}
   };
 }
+
+/**
+ * Click on an animation in the timeline to select/unselect it.
+ * @param {AnimationsPanel} panel The panel instance.
+ * @param {Number} index The index of the animation to click on.
+ * @param {Boolean} shouldClose Set to true if clicking should close the
+ * animation.
+ * @return {Promise} resolves to the animation whose state has changed.
+ */
+function* clickOnAnimation(panel, index, shouldClose) {
+  let timeline = panel.animationsTimelineComponent;
+
+  // Expect a selection event.
+  let onSelectionChanged = timeline.once(shouldClose
+                                         ? "animation-unselected"
+                                         : "animation-selected");
+
+  // If we're opening the animation, also wait for the keyframes-retrieved
+  // event.
+  let onReady = shouldClose
+                ? Promise.resolve()
+                : timeline.details[index].once("keyframes-retrieved");
+
+  info("Click on animation " + index + " in the timeline");
+  let timeBlock = timeline.rootWrapperEl.querySelectorAll(".time-block")[index];
+  EventUtils.sendMouseEvent({type: "click"}, timeBlock,
+                            timeBlock.ownerDocument.defaultView);
+
+  yield onReady;
+  return yield onSelectionChanged;
+}
+
+/**
+ * Get an instance of the Keyframes component from the timeline.
+ * @param {AnimationsPanel} panel The panel instance.
+ * @param {Number} animationIndex The index of the animation in the timeline.
+ * @param {String} propertyName The name of the animated property.
+ * @return {Keyframes} The Keyframes component instance.
+ */
+function getKeyframeComponent(panel, animationIndex, propertyName) {
+  let timeline = panel.animationsTimelineComponent;
+  let detailsComponent = timeline.details[animationIndex];
+  return detailsComponent.keyframeComponents
+                         .find(c => c.propertyName === propertyName);
+}
+
+/**
+ * Get a keyframe element from the timeline.
+ * @param {AnimationsPanel} panel The panel instance.
+ * @param {Number} animationIndex The index of the animation in the timeline.
+ * @param {String} propertyName The name of the animated property.
+ * @param {Index} keyframeIndex The index of the keyframe.
+ * @return {DOMNode} The keyframe element.
+ */
+function getKeyframeEl(panel, animationIndex, propertyName, keyframeIndex) {
+  let keyframeComponent = getKeyframeComponent(panel, animationIndex, propertyName);
+  return keyframeComponent.keyframesEl.querySelectorAll(".frame")[keyframeIndex];
+}
new file mode 100644
--- /dev/null
+++ b/devtools/client/animationinspector/test/unit/test_getCssPropertyName.js
@@ -0,0 +1,27 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+var Cu = Components.utils;
+const {require} = Cu.import("resource://devtools/shared/Loader.jsm", {});
+const {getCssPropertyName} = require("devtools/client/animationinspector/components");
+
+const TEST_DATA = [{
+  jsName: "alllowercase",
+  cssName: "alllowercase"
+}, {
+  jsName: "borderWidth",
+  cssName: "border-width"
+}, {
+  jsName: "borderTopRightRadius",
+  cssName: "border-top-right-radius"
+}];
+
+function run_test() {
+  for (let {jsName, cssName} of TEST_DATA) {
+    equal(getCssPropertyName(jsName), cssName);
+  }
+}
--- a/devtools/client/animationinspector/test/unit/xpcshell.ini
+++ b/devtools/client/animationinspector/test/unit/xpcshell.ini
@@ -2,9 +2,10 @@
 tags = devtools
 head =
 tail =
 firefox-appdir = browser
 skip-if = toolkit == 'android' || toolkit == 'gonk'
 
 [test_findOptimalTimeInterval.js]
 [test_formatStopwatchTime.js]
+[test_getCssPropertyName.js]
 [test_timeScale.js]