Bug 1174060 - 6 - Tests for how delays are displayed in the timeline; r=ochameau
authorPatrick Brosset <pbrosset@mozilla.com>
Wed, 09 Sep 2015 11:07:29 +0200
changeset 294118 076c517796268734a28ff0b45adbd3118b6be428
parent 294117 6e64ea28389d117fcfd62e2902bbfa0770670e7f
child 294119 8968d387064d001fe3a8eac583d9b48a26c58140
push id5245
push userraliiev@mozilla.com
push dateThu, 29 Oct 2015 11:30:51 +0000
treeherdermozilla-beta@dac831dc1bd0 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersochameau
bugs1174060
milestone43.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 1174060 - 6 - Tests for how delays are displayed in the timeline; r=ochameau Added tests to ensure negative and positive delays are shown correctly and that the timeScale window is computed correctly. Also added a test to ensure that animations with the same name but different nodes don't override each others in the UI. This commit also cleans up a lot of exceptions that were thrown while tests were running. These exceptions were due to pending protocol requests when tests ended.
browser/devtools/animationinspector/components.js
browser/devtools/animationinspector/test/browser.ini
browser/devtools/animationinspector/test/browser_animation_controller_exposes_document_currentTime.js
browser/devtools/animationinspector/test/browser_animation_mutations_with_same_names.js
browser/devtools/animationinspector/test/browser_animation_playerWidgets_scrubber_enabled.js
browser/devtools/animationinspector/test/browser_animation_refresh_on_removed_animation.js
browser/devtools/animationinspector/test/browser_animation_timeline_scrubber_exists.js
browser/devtools/animationinspector/test/browser_animation_timeline_scrubber_movable.js
browser/devtools/animationinspector/test/browser_animation_timeline_scrubber_moves.js
browser/devtools/animationinspector/test/browser_animation_timeline_shows_delay.js
browser/devtools/animationinspector/test/browser_animation_timeline_shows_time_info.js
browser/devtools/animationinspector/test/browser_animation_timeline_takes_rate_into_account.js
browser/devtools/animationinspector/test/browser_animation_ui_updates_when_animation_data_changes.js
browser/devtools/animationinspector/test/doc_negative_animation.html
browser/devtools/animationinspector/test/doc_simple_animation.html
browser/devtools/animationinspector/test/head.js
browser/devtools/animationinspector/test/unit/test_timeScale.js
browser/themes/shared/devtools/animationinspector.css
toolkit/devtools/server/tests/browser/browser_animation_actors_03.js
--- a/browser/devtools/animationinspector/components.js
+++ b/browser/devtools/animationinspector/components.js
@@ -975,16 +975,16 @@ AnimationsTimeline.prototype = {
     // Delay.
     if (delay) {
       // Negative delays need to start at 0.
       let x = TimeScale.durationToDistance((delay < 0 ? 0 : delay) / rate, width);
       let w = TimeScale.durationToDistance(Math.abs(delay) / rate, width);
       createNode({
         parent: iterations,
         attributes: {
-          "class": "delay",
+          "class": "delay" + (delay < 0 ? " negative" : ""),
           "style": `left:-${x}px;
                     width:${w}px;`
         }
       });
     }
   }
 };
--- a/browser/devtools/animationinspector/test/browser.ini
+++ b/browser/devtools/animationinspector/test/browser.ini
@@ -1,20 +1,23 @@
 [DEFAULT]
 tags = devtools
 subsuite = devtools
 support-files =
   doc_body_animation.html
   doc_frame_script.js
   doc_modify_playbackRate.html
+  doc_negative_animation.html
   doc_simple_animation.html
   head.js
 
+[browser_animation_controller_exposes_document_currentTime.js]
 [browser_animation_empty_on_invalid_nodes.js]
 [browser_animation_iterationCount_hidden_by_default.js]
+[browser_animation_mutations_with_same_names.js]
 [browser_animation_panel_exists.js]
 [browser_animation_participate_in_inspector_update.js]
 [browser_animation_play_pause_button.js]
 [browser_animation_playerFronts_are_refreshed.js]
 [browser_animation_playerWidgets_appear_on_panel_init.js]
 [browser_animation_playerWidgets_compositor_icon.js]
 [browser_animation_playerWidgets_destroy.js]
 [browser_animation_playerWidgets_disables_on_finished.js]
new file mode 100644
--- /dev/null
+++ b/browser/devtools/animationinspector/test/browser_animation_controller_exposes_document_currentTime.js
@@ -0,0 +1,43 @@
+/* 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 the controller provides the document.timeline currentTime (at least
+// the last known version since new animations were added).
+
+add_task(function*() {
+  yield addTab(TEST_URL_ROOT + "doc_simple_animation.html");
+  let {panel, controller} = yield openAnimationInspectorNewUI();
+
+  ok(controller.documentCurrentTime, "The documentCurrentTime getter exists");
+  checkDocumentTimeIsCorrect(controller);
+  let time1 = controller.documentCurrentTime;
+
+  yield startNewAnimation(controller, panel);
+  checkDocumentTimeIsCorrect(controller);
+  let time2 = controller.documentCurrentTime;
+  ok(time2 > time1, "The new documentCurrentTime is higher than the old one");
+});
+
+function checkDocumentTimeIsCorrect(controller) {
+  let time = 0;
+  for (let {state} of controller.animationPlayers) {
+    time = Math.max(time, state.documentCurrentTime);
+  }
+  is(controller.documentCurrentTime, time,
+     "The documentCurrentTime is correct");
+}
+
+function* startNewAnimation(controller, panel) {
+  info("Add a new animation to the page and check the time again");
+  let onPlayerAdded = controller.once(controller.PLAYERS_UPDATED_EVENT);
+  yield executeInContent("devtools:test:setAttribute", {
+    selector: ".still",
+    attributeName: "class",
+    attributeValue: "ball still short"
+  });
+  yield onPlayerAdded;
+  yield waitForAllAnimationTargets(panel);
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/animationinspector/test/browser_animation_mutations_with_same_names.js
@@ -0,0 +1,31 @@
+/* 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";
+
+// Check that when animations are added later (through animation mutations) and
+// if these animations have the same names, then all of them are still being
+// displayed (which should be true as long as these animations apply to
+// different nodes).
+
+add_task(function*() {
+  yield addTab(TEST_URL_ROOT + "doc_negative_animation.html");
+  let {controller, panel} = yield openAnimationInspectorNewUI();
+
+  info("Wait until all animations have been added " +
+       "(they're added with setTimeout)");
+  while (controller.animationPlayers.length < 3) {
+    yield controller.once(controller.PLAYERS_UPDATED_EVENT);
+  }
+  yield waitForAllAnimationTargets(panel);
+
+  is(panel.animationsTimelineComponent.animations.length, 3,
+     "The timeline shows 3 animations too");
+
+  // Reduce the known nodeFronts to a set to make them unique.
+  let nodeFronts = new Set(panel.animationsTimelineComponent
+                                .targetNodes.map(n => n.nodeFront));
+  is(nodeFronts.size, 3,
+     "The animations are applied to 3 different node fronts");
+});
--- a/browser/devtools/animationinspector/test/browser_animation_playerWidgets_scrubber_enabled.js
+++ b/browser/devtools/animationinspector/test/browser_animation_playerWidgets_scrubber_enabled.js
@@ -15,27 +15,25 @@ add_task(function*() {
   yield selectNode(".animated", inspector);
 
   info("Get the player widget's timeline element");
   let widget = panel.playerWidgets[0];
   let timeline = widget.currentTimeEl;
 
   ok(!timeline.hasAttribute("disabled"), "The timeline input[range] is enabled");
   ok(widget.setCurrentTime, "The widget has the setCurrentTime method");
-  ok(widget.player.setCurrentTime, "The associated player front has the setCurrentTime method");
+  ok(widget.player.setCurrentTime,
+     "The associated player front has the setCurrentTime method");
 
   info("Faking an older server version by setting " +
-    "AnimationsController.traits.hasSetCurrentTime to false");
+       "AnimationsController.traits.hasSetCurrentTime to false");
 
   yield selectNode("body", inspector);
   controller.traits.hasSetCurrentTime = false;
 
   yield selectNode(".animated", inspector);
 
   info("Get the player widget's timeline element");
   widget = panel.playerWidgets[0];
   timeline = widget.currentTimeEl;
 
   ok(timeline.hasAttribute("disabled"), "The timeline input[range] is disabled");
-
-  yield selectNode("body", inspector);
-  controller.traits.hasSetCurrentTime = true;
 });
--- a/browser/devtools/animationinspector/test/browser_animation_refresh_on_removed_animation.js
+++ b/browser/devtools/animationinspector/test/browser_animation_refresh_on_removed_animation.js
@@ -42,16 +42,17 @@ function* testRefreshOnRemove(inspector,
   info("Add an finite animation on the node again, and wait for it to appear");
   onPanelUpdated = panel.once(panel.UI_UPDATED_EVENT);
   yield executeInContent("devtools:test:setAttribute", {
     selector: ".test-node",
     attributeName: "class",
     attributeValue: "ball short test-node"
   });
   yield onPanelUpdated;
+  yield waitForAllAnimationTargets(panel);
 
   assertAnimationsDisplayed(panel, 1);
 }
 
 function* testAddedAnimationWorks(inspector, panel) {
   info("Now wait until the animation finishes");
   let widget = panel.playerWidgets[0];
   yield waitForPlayState(widget.player, "finished");
--- a/browser/devtools/animationinspector/test/browser_animation_timeline_scrubber_exists.js
+++ b/browser/devtools/animationinspector/test/browser_animation_timeline_scrubber_exists.js
@@ -4,16 +4,15 @@
 
 "use strict";
 
 // Check that the timeline-based UI does have a scrubber element.
 
 add_task(function*() {
   yield addTab(TEST_URL_ROOT + "doc_simple_animation.html");
   let {panel} = yield openAnimationInspectorNewUI();
-  yield waitForAllAnimationTargets(panel);
 
   let timeline = panel.animationsTimelineComponent;
   let scrubberEl = timeline.scrubberEl;
 
   ok(scrubberEl, "The scrubber element exists");
   ok(scrubberEl.classList.contains("scrubber"), "It has the right classname");
 });
--- a/browser/devtools/animationinspector/test/browser_animation_timeline_scrubber_movable.js
+++ b/browser/devtools/animationinspector/test/browser_animation_timeline_scrubber_movable.js
@@ -6,31 +6,30 @@
 
 // Check that the scrubber in the timeline-based UI can be moved by clicking &
 // dragging in the header area.
 
 add_task(function*() {
   yield addTab(TEST_URL_ROOT + "doc_simple_animation.html");
 
   let {panel} = yield openAnimationInspectorNewUI();
-  yield waitForAllAnimationTargets(panel);
 
   let timeline = panel.animationsTimelineComponent;
   let win = timeline.win;
   let timeHeaderEl = timeline.timeHeaderEl;
   let scrubberEl = timeline.scrubberEl;
 
   info("Mousedown in the header to move the scrubber");
   EventUtils.synthesizeMouse(timeHeaderEl, 50, 1, {type: "mousedown"}, win);
-  let newPos = parseInt(scrubberEl.style.left);
+  let newPos = parseInt(scrubberEl.style.left, 10);
   is(newPos, 50, "The scrubber moved on mousedown");
 
   info("Continue moving the mouse and verify that the scrubber tracks it");
   EventUtils.synthesizeMouse(timeHeaderEl, 100, 1, {type: "mousemove"}, win);
-  newPos = parseInt(scrubberEl.style.left);
+  newPos = parseInt(scrubberEl.style.left, 10);
   is(newPos, 100, "The scrubber followed the mouse");
 
   info("Release the mouse and move again and verify that the scrubber stays");
   EventUtils.synthesizeMouse(timeHeaderEl, 100, 1, {type: "mouseup"}, win);
   EventUtils.synthesizeMouse(timeHeaderEl, 200, 1, {type: "mousemove"}, win);
-  newPos = parseInt(scrubberEl.style.left);
+  newPos = parseInt(scrubberEl.style.left, 10);
   is(newPos, 100, "The scrubber stopped following the mouse");
 });
--- a/browser/devtools/animationinspector/test/browser_animation_timeline_scrubber_moves.js
+++ b/browser/devtools/animationinspector/test/browser_animation_timeline_scrubber_moves.js
@@ -7,25 +7,20 @@
 // Check that the scrubber in the timeline-based UI moves when animations are
 // playing.
 // The animations in the test page last for a very long time, so the test just
 // measures the position of the scrubber once, then waits for some time to pass
 // and measures its position again.
 
 add_task(function*() {
   yield addTab(TEST_URL_ROOT + "doc_simple_animation.html");
-
   let {panel} = yield openAnimationInspectorNewUI();
-  yield waitForAllAnimationTargets(panel);
 
   let timeline = panel.animationsTimelineComponent;
-  let win = timeline.win;
-  let timeHeaderEl = timeline.timeHeaderEl;
   let scrubberEl = timeline.scrubberEl;
-
   let startPos = scrubberEl.getBoundingClientRect().left;
 
   info("Wait for some time to check that the scrubber moves");
   yield new Promise(r => setTimeout(r, 2000));
 
   let endPos = scrubberEl.getBoundingClientRect().left;
 
   ok(endPos > startPos, "The scrubber has moved");
--- a/browser/devtools/animationinspector/test/browser_animation_timeline_shows_delay.js
+++ b/browser/devtools/animationinspector/test/browser_animation_timeline_shows_delay.js
@@ -1,30 +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";
 
 // Check that animation delay is visualized in the timeline-based UI when the
 // animation is delayed.
+// Also check that negative delays do not overflow the UI, and are shown like
+// positive delays.
 
 add_task(function*() {
   yield addTab(TEST_URL_ROOT + "doc_simple_animation.html");
   let {inspector, panel} = yield openAnimationInspectorNewUI();
 
   info("Selecting a delayed animated node");
   yield selectNode(".delayed", inspector);
-
-  info("Getting the animation and delay elements from the panel");
   let timelineEl = panel.animationsTimelineComponent.rootWrapperEl;
-  let delay = timelineEl.querySelector(".delay");
-
-  ok(delay, "The animation timeline contains the delay element");
+  checkDelayAndName(timelineEl, true);
 
   info("Selecting a no-delay animated node");
   yield selectNode(".animated", inspector);
+  checkDelayAndName(timelineEl, false);
 
-  info("Getting the animation and delay elements from the panel again");
-  delay = timelineEl.querySelector(".delay");
+  info("Selecting a negative-delay animated node");
+  yield selectNode(".negative-delay", inspector);
+  checkDelayAndName(timelineEl, true);
+});
+
+function checkDelayAndName(timelineEl, hasDelay) {
+  let delay = timelineEl.querySelector(".delay");
+
+  is(!!delay, hasDelay, "The timeline " +
+                        (hasDelay ? "contains" : "does not contain") +
+                        " a delay element, as expected");
 
-  ok(!delay, "The animation timeline contains no delay element");
-});
+  if (hasDelay) {
+    let name = timelineEl.querySelector(".name");
+    let targetNode = timelineEl.querySelector(".target");
+
+    // Check that the delay element does not cause the timeline to overflow.
+    let delayRect = delay.getBoundingClientRect();
+    let sidebarWidth = targetNode.getBoundingClientRect().width;
+    ok(delayRect.x >= sidebarWidth,
+       "The delay element isn't displayed over the sidebar");
+
+    // Check that the delay is not displayed on top of the name.
+    let nameLeft = name.getBoundingClientRect().left;
+    ok(delayRect.right <= nameLeft,
+       "The delay element does not span over the name element");
+  }
+}
--- a/browser/devtools/animationinspector/test/browser_animation_timeline_shows_time_info.js
+++ b/browser/devtools/animationinspector/test/browser_animation_timeline_shows_time_info.js
@@ -5,25 +5,24 @@
 "use strict";
 
 // Check that the timeline-based UI displays animations' duration, delay and
 // iteration counts in tooltips.
 
 add_task(function*() {
   yield addTab(TEST_URL_ROOT + "doc_simple_animation.html");
   let {panel} = yield openAnimationInspectorNewUI();
-  yield waitForAllAnimationTargets(panel);
 
   info("Getting the animation element from the panel");
   let timelineEl = panel.animationsTimelineComponent.rootWrapperEl;
   let timeBlockNameEls = timelineEl.querySelectorAll(".time-block .name");
 
   // Verify that each time-block's name element has a tooltip that looks sort of
   // ok. We don't need to test the actual content.
   for (let el of timeBlockNameEls) {
     ok(el.hasAttribute("title"), "The tooltip is defined");
 
     let title = el.getAttribute("title");
-    ok(title.match(/Delay: [\d.]+s/), "The tooltip shows the delay");
-    ok(title.match(/Duration: [\d.]+s/), "The tooltip shows the delay");
+    ok(title.match(/Delay: [\d.-]+s/), "The tooltip shows the delay");
+    ok(title.match(/Duration: [\d.]+s/), "The tooltip shows the duration");
     ok(title.match(/Repeats: /), "The tooltip shows the iterations");
   }
 });
--- a/browser/devtools/animationinspector/test/browser_animation_timeline_takes_rate_into_account.js
+++ b/browser/devtools/animationinspector/test/browser_animation_timeline_takes_rate_into_account.js
@@ -10,17 +10,16 @@
 // because there might be multiple animations displayed at the same time, some
 // of which may have a different rate than others. Those that have had their
 // rate changed have a delay = delay/rate and a duration = duration/rate.
 
 add_task(function*() {
   yield addTab(TEST_URL_ROOT + "doc_modify_playbackRate.html");
 
   let {panel} = yield openAnimationInspectorNewUI();
-  yield waitForAllAnimationTargets(panel);
 
   let timelineEl = panel.animationsTimelineComponent.rootWrapperEl;
 
   let timeBlocks = timelineEl.querySelectorAll(".time-block");
   is(timeBlocks.length, 2, "2 animations are displayed");
 
   info("The first animation has its rate set to 1, let's measure it");
 
--- a/browser/devtools/animationinspector/test/browser_animation_ui_updates_when_animation_data_changes.js
+++ b/browser/devtools/animationinspector/test/browser_animation_ui_updates_when_animation_data_changes.js
@@ -18,19 +18,19 @@ add_task(function*() {
   yield testDataUpdates(ui, true);
 });
 
 function* testDataUpdates({panel, controller, inspector}, isNewUI=false) {
   info("Select the test node");
   yield selectNode(".animated", inspector);
 
   let animation = controller.animationPlayers[0];
-  yield setStyle(animation, "animationDuration", "5.5s", isNewUI);
-  yield setStyle(animation, "animationIterationCount", "300", isNewUI);
-  yield setStyle(animation, "animationDelay", "45s", isNewUI);
+  yield setStyle(animation, panel, "animationDuration", "5.5s", isNewUI);
+  yield setStyle(animation, panel, "animationIterationCount", "300", isNewUI);
+  yield setStyle(animation, panel, "animationDelay", "45s", isNewUI);
 
   if (isNewUI) {
     let animationsEl = panel.animationsTimelineComponent.animationsEl;
     let timeBlockEl = animationsEl.querySelector(".time-block");
 
     // 45s delay + (300 * 5.5)s duration
     let expectedTotalDuration = 1695 * 1000;
     let timeRatio = expectedTotalDuration / timeBlockEl.offsetWidth;
@@ -47,26 +47,30 @@ function* testDataUpdates({panel, contro
       "The widget shows the new duration");
     is(widget.metaDataComponent.iterationValue.textContent, "300",
       "The widget shows the new iteration count");
     is(widget.metaDataComponent.delayValue.textContent, "45s",
       "The widget shows the new delay");
   }
 }
 
-function* setStyle(animation, name, value, isNewUI=false) {
+function* setStyle(animation, panel, name, value, isNewUI=false) {
   info("Change the animation style via the content DOM. Setting " +
     name + " to " + value);
 
   let onAnimationChanged = once(animation, "changed");
   yield executeInContent("devtools:test:setStyle", {
     selector: ".animated",
     propertyName: name,
     propertyValue: value
   });
   yield onAnimationChanged;
 
+  // Also wait for the target node previews to be loaded if the panel got
+  // refreshed as a result of this animation mutation.
+  yield waitForAllAnimationTargets(panel);
+
   // If this is the playerWidget-based UI, wait for the auto-refresh event too
   // to make sure the UI has updated.
   if (!isNewUI) {
     yield once(animation, animation.AUTO_REFRESH_EVENT);
   }
 }
new file mode 100644
--- /dev/null
+++ b/browser/devtools/animationinspector/test/doc_negative_animation.html
@@ -0,0 +1,64 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+  <meta charset="UTF-8">
+  <style>
+  html, body {
+    margin: 0;
+    height: 100%;
+    overflow: hidden;
+  }
+
+  div {
+    position: absolute;
+    top: 0;
+    left: -500px;
+    height: 20px;
+    width: 500px;
+    color: red;
+    background: linear-gradient(to left, currentColor, currentColor 2px, transparent);
+  }
+
+  .zero {
+    color: blue;
+    top: 20px;
+  }
+
+  .positive {
+    color: green;
+    top: 40px;
+  }
+
+  .negative.move { animation: 5s -1s move linear forwards; }
+  .zero.move { animation: 5s 0s move linear forwards; }
+  .positive.move { animation: 5s 1s move linear forwards; }
+
+  @keyframes move {
+    to {
+      transform: translateX(500px);
+    }
+  }
+  </style>
+</head>
+<body>
+  <div class="negative"></div>
+  <div class="zero"></div>
+  <div class="positive"></div>
+  <script>
+    var negative = document.querySelector(".negative");
+    var zero = document.querySelector(".zero");
+    var positive = document.querySelector(".positive");
+
+    // The non-delayed animation starts now.
+    zero.classList.add("move");
+    // The negative-delayed animation starts in 1 second.
+    setTimeout(function() {
+      negative.classList.add("move");
+    }, 1000);
+    // The positive-delayed animation starts in 200 ms.
+    setTimeout(function() {
+      positive.classList.add("move");
+    }, 200);
+  </script>
+</body>
+</html>
\ No newline at end of file
--- a/browser/devtools/animationinspector/test/doc_simple_animation.html
+++ b/browser/devtools/animationinspector/test/doc_simple_animation.html
@@ -60,16 +60,25 @@
     .long {
       top: 600px;
       left: 10px;
       background: blue;
 
       animation: simple-animation 120s;
     }
 
+    .negative-delay {
+      top: 700px;
+      left: 10px;
+      background: gray;
+
+      animation: simple-animation 15s -10s;
+      animation-fill-mode: forwards;
+    }
+
     @keyframes simple-animation {
       100% {
         transform: translateX(300px);
       }
     }
 
     @keyframes other-animation {
       100% {
@@ -82,10 +91,11 @@
   <!-- Comment node -->
   <div class="ball still"></div>
   <div class="ball animated"></div>
   <div class="ball multi"></div>
   <div class="ball delayed"></div>
   <div class="ball multi-finite"></div>
   <div class="ball short"></div>
   <div class="ball long"></div>
+  <div class="ball negative-delay"></div>
 </body>
 </html>
--- a/browser/devtools/animationinspector/test/head.js
+++ b/browser/devtools/animationinspector/test/head.js
@@ -16,16 +16,17 @@ const DevToolsUtils = require("devtools/
 // All tests are asynchronous
 waitForExplicitFinish();
 
 const TEST_URL_ROOT = "http://example.com/browser/browser/devtools/animationinspector/test/";
 const ROOT_TEST_DIR = getRootDirectory(gTestPath);
 const FRAME_SCRIPT_URL = ROOT_TEST_DIR + "doc_frame_script.js";
 const COMMON_FRAME_SCRIPT_URL = "chrome://browser/content/devtools/frame-script-utils.js";
 const NEW_UI_PREF = "devtools.inspector.animationInspectorV3";
+const TAB_NAME = "animationinspector";
 
 // Auto clean-up when a test ends
 registerCleanupFunction(function*() {
   yield closeAnimationInspector();
 
   while (gBrowser.tabs.length > 1) {
     gBrowser.removeCurrentTab();
   }
@@ -124,16 +125,23 @@ let selectNode = Task.async(function*(da
   info("Selecting the node for '" + data + "'");
   let nodeFront = data;
   if (!data._form) {
     nodeFront = yield getNodeFront(data, inspector);
   }
   let updated = inspector.once("inspector-updated");
   inspector.selection.setNodeFront(nodeFront, reason);
   yield updated;
+
+  // 99% of the times, selectNode is called to select an animated node, and we
+  // want to make sure the rest of the test waits for the animations to be
+  // properly displayed (wait for all target DOM nodes to be previewed).
+  // Even if there are no animations, this is safe to do.
+  let {AnimationsPanel} = inspector.sidebar.getWindowForTab(TAB_NAME);
+  yield waitForAllAnimationTargets(AnimationsPanel);
 });
 
 /**
  * Check if there are the expected number of animations being displayed in the
  * panel right now.
  * @param {AnimationsPanel} panel
  * @param {Number} nbAnimations The expected number of animations.
  * @param {String} msg An optional string to be used as the assertion message.
@@ -154,17 +162,17 @@ function assertAnimationsDisplayed(panel
  * Takes an Inspector panel that was just created, and waits
  * for a "inspector-updated" event as well as the animation inspector
  * sidebar to be ready. Returns a promise once these are completed.
  *
  * @param {InspectorPanel} inspector
  * @return {Promise}
  */
 let waitForAnimationInspectorReady = Task.async(function*(inspector) {
-  let win = inspector.sidebar.getWindowForTab("animationinspector");
+  let win = inspector.sidebar.getWindowForTab(TAB_NAME);
   let updated = inspector.once("inspector-updated");
 
   // In e10s, if we wait for underlying toolbox actors to
   // load (by setting DevToolsUtils.testing to true), we miss the
   // "animationinspector-ready" event on the sidebar, so check to see if the
   // iframe is already loaded.
   let tabReady = win.document.readyState === "complete" ?
                  promise.resolve() :
@@ -187,31 +195,36 @@ let openAnimationInspector = Task.async(
   info("Switching to the animationinspector");
   let inspector = toolbox.getPanel("inspector");
 
   let panelReady = waitForAnimationInspectorReady(inspector);
 
   info("Waiting for toolbox focus");
   yield waitForToolboxFrameFocus(toolbox);
 
-  inspector.sidebar.select("animationinspector");
+  inspector.sidebar.select(TAB_NAME);
 
   info("Waiting for the inspector and sidebar to be ready");
   yield panelReady;
 
-  let win = inspector.sidebar.getWindowForTab("animationinspector");
+  let win = inspector.sidebar.getWindowForTab(TAB_NAME);
   let {AnimationsController, AnimationsPanel} = win;
 
   info("Waiting for the animation controller and panel to be ready");
   if (AnimationsPanel.initialized) {
     yield AnimationsPanel.initialized;
   } else {
     yield AnimationsPanel.once(AnimationsPanel.PANEL_INITIALIZED);
   }
 
+  // Make sure we wait for all animations to be loaded (especially their target
+  // nodes to be lazily displayed). This is safe to do even if there are no
+  // animations displayed.
+  yield waitForAllAnimationTargets(AnimationsPanel);
+
   return {
     toolbox: toolbox,
     inspector: inspector,
     controller: AnimationsController,
     panel: AnimationsPanel,
     window: win
   };
 });
@@ -245,21 +258,19 @@ let closeAnimationInspector = Task.async
  * @return a promise that resolves when the animation inspector is ready.
  */
 let closeAnimationInspectorAndRestartWithNewUI = Task.async(function*(reload) {
   info("Close the toolbox and test again with the new UI");
   yield closeAnimationInspector();
   if (reload) {
     yield reloadTab();
   }
-  enableNewUI();
-  return yield openAnimationInspector();
+  return yield openAnimationInspectorNewUI();
 });
 
-
 /**
  * Wait for the toolbox frame to receive focus after it loads
  * @param {Toolbox} toolbox
  * @return a promise that resolves when focus has been received
  */
 function waitForToolboxFrameFocus(toolbox) {
   info("Making sure that the toolbox's frame is focused");
   let def = promise.defer();
--- a/browser/devtools/animationinspector/test/unit/test_timeScale.js
+++ b/browser/devtools/animationinspector/test/unit/test_timeScale.js
@@ -5,36 +5,61 @@
 
 "use strict";
 
 const Cu = Components.utils;
 const {require} = Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
 const {TimeScale} = require("devtools/animationinspector/components");
 
 const TEST_ANIMATIONS = [{
-  startTime: 500,
-  delay: 0,
-  duration: 1000,
-  iterationCount: 1,
-  playbackRate: 1
+  desc: "Testing a few standard animations",
+  animations: [{
+    startTime: 500,
+    delay: 0,
+    duration: 1000,
+    iterationCount: 1,
+    playbackRate: 1
+  }, {
+    startTime: 400,
+    delay: 100,
+    duration: 10,
+    iterationCount: 100,
+    playbackRate: 1
+  }, {
+    startTime: 50,
+    delay: 1000,
+    duration: 100,
+    iterationCount: 20,
+    playbackRate: 1
+  }],
+  expectedMinStart: 50,
+  expectedMaxEnd: 3050
 }, {
-  startTime: 400,
-  delay: 100,
-  duration: 10,
-  iterationCount: 100,
-  playbackRate: 1
+  desc: "Testing a single negative-delay animation",
+  animations: [{
+    startTime: 100,
+    delay: -100,
+    duration: 100,
+    iterationCount: 1,
+    playbackRate: 1
+  }],
+  expectedMinStart: 0,
+  expectedMaxEnd: 100
 }, {
-  startTime: 50,
-  delay: 1000,
-  duration: 100,
-  iterationCount: 20,
-  playbackRate: 1
+  desc: "Testing a single negative-delay animation with a different rate",
+  animations: [{
+    startTime: 3500,
+    delay: -1000,
+    duration: 10000,
+    iterationCount: 2,
+    playbackRate: 2
+  }],
+  expectedMinStart: 3000,
+  expectedMaxEnd: 13000
 }];
-const EXPECTED_MIN_START = 50;
-const EXPECTED_MAX_END = 3050;
 
 const TEST_STARTTIME_TO_DISTANCE = [{
   time: 50,
   width: 100,
   expectedDistance: 0
 }, {
   time: 50,
   width: 0,
@@ -121,34 +146,37 @@ const TEST_FORMAT_TIME_S = [{
   expectedFormattedTime: "102.9s"
 }];
 
 function run_test() {
   do_print("Check the default min/max range values");
   equal(TimeScale.minStartTime, Infinity);
   equal(TimeScale.maxEndTime, 0);
 
-  do_print("Test adding a few animations");
-  for (let state of TEST_ANIMATIONS) {
+  for (let {desc, animations, expectedMinStart, expectedMaxEnd} of
+       TEST_ANIMATIONS) {
+    do_print("Test adding a few animations: " + desc);
+    for (let state of animations) {
+      TimeScale.addAnimation(state);
+    }
+
+    do_print("Checking the time scale range");
+    equal(TimeScale.minStartTime, expectedMinStart);
+    equal(TimeScale.maxEndTime, expectedMaxEnd);
+
+    do_print("Test reseting the animations");
+    TimeScale.reset();
+    equal(TimeScale.minStartTime, Infinity);
+    equal(TimeScale.maxEndTime, 0);
+  }
+
+  do_print("Add a set of animations again");
+  for (let state of TEST_ANIMATIONS[0].animations) {
     TimeScale.addAnimation(state);
   }
-  equal(TimeScale.minStartTime, EXPECTED_MIN_START);
-  equal(TimeScale.maxEndTime, EXPECTED_MAX_END);
-
-  do_print("Test reseting the animations");
-  TimeScale.reset();
-  equal(TimeScale.minStartTime, Infinity);
-  equal(TimeScale.maxEndTime, 0);
-
-  do_print("Test adding the animations again");
-  for (let state of TEST_ANIMATIONS) {
-    TimeScale.addAnimation(state);
-  }
-  equal(TimeScale.minStartTime, EXPECTED_MIN_START);
-  equal(TimeScale.maxEndTime, EXPECTED_MAX_END);
 
   do_print("Test converting start times to distances");
   for (let {time, width, expectedDistance} of TEST_STARTTIME_TO_DISTANCE) {
     let distance = TimeScale.startTimeToDistance(time, width);
     equal(distance, expectedDistance);
   }
 
   do_print("Test converting durations to distances");
--- a/browser/themes/shared/devtools/animationinspector.css
+++ b/browser/themes/shared/devtools/animationinspector.css
@@ -284,24 +284,35 @@ body {
   padding: 0 2px;
 }
 
 .animation-timeline .animation .delay {
   position: absolute;
   top: 0;
   /* Make sure the delay covers up the animation border */
   transform: translate(-1px, -1px);
-  height: 100%;
+  box-sizing: border-box;
+  height: calc(100% + 2px);
+
+  border: 1px solid var(--timelime-border-color);
+  border-width: 1px 0 1px 1px;
   background-image: repeating-linear-gradient(45deg,
                                               transparent,
                                               transparent 1px,
                                               var(--theme-selection-color) 1px,
                                               var(--theme-selection-color) 4px);
   background-color: var(--timelime-border-color);
-  border: 1px solid var(--timelime-border-color);
+}
+
+.animation-timeline .animation .delay.negative {
+  /* Negative delays are displayed on top of the animation, so they need a
+     right border. Whereas normal delays are displayed just before the
+     animation, so there's already the animation's left border that serves as
+     a separation. */
+  border-width: 1px;
 }
 
 /* Animation target node gutter, contains a preview of the dom node */
 
 .animation-target {
   background-color: var(--theme-toolbar-background);
   padding: 1px 4px;
   box-sizing: border-box;
--- a/toolkit/devtools/server/tests/browser/browser_animation_actors_03.js
+++ b/toolkit/devtools/server/tests/browser/browser_animation_actors_03.js
@@ -36,16 +36,17 @@ function* playerHasAnInitialState(walker
   ok("playState" in player.initialState, "Player's state has playState");
   ok("playbackRate" in player.initialState, "Player's state has playbackRate");
   ok("name" in player.initialState, "Player's state has name");
   ok("duration" in player.initialState, "Player's state has duration");
   ok("delay" in player.initialState, "Player's state has delay");
   ok("iterationCount" in player.initialState, "Player's state has iterationCount");
   ok("isRunningOnCompositor" in player.initialState, "Player's state has isRunningOnCompositor");
   ok("type" in player.initialState, "Player's state has type");
+  ok("documentCurrentTime" in player.initialState, "Player's state has documentCurrentTime");
 }
 
 function* playerStateIsCorrect(walker, front) {
   info("Checking the state of the simple animation");
 
   let state = yield getAnimationStateForNode(walker, front, ".simple-animation", 0);
   is(state.name, "move", "Name is correct");
   is(state.duration, 2000, "Duration is correct");