Bug 1431573 - Part 12: Add tests. r?gl draft
authorDaisuke Akatsuka <dakatsuka@mozilla.com>
Tue, 13 Mar 2018 13:16:20 +0900
changeset 766596 d4b942a530b5b0d5614be9517c968d40f36315dc
parent 766122 7aaf68204a81fc0b7f8905fece374fe3a22b21f9
push id102366
push userbmo:dakatsuka@mozilla.com
push dateTue, 13 Mar 2018 04:20:41 +0000
reviewersgl
bugs1431573
milestone60.0a1
Bug 1431573 - Part 12: Add tests. r?gl MozReview-Commit-ID: E9WzhYeUm5R
devtools/client/inspector/animation/test/browser.ini
devtools/client/inspector/animation/test/browser_animation_current-time-label.js
devtools/client/inspector/animation/test/browser_animation_current-time-scrubber.js
devtools/client/inspector/animation/test/browser_animation_keyframes-progress-bar.js
devtools/client/inspector/animation/test/browser_animation_logic_auto-stop.js
devtools/client/inspector/animation/test/browser_animation_pause-resume-button.js
devtools/client/inspector/animation/test/browser_animation_pause-resume-button_spacebar.js
devtools/client/inspector/animation/test/browser_animation_playback-rate-selector.js
devtools/client/inspector/animation/test/browser_animation_rewind-button.js
devtools/client/inspector/animation/test/doc_custom_playback_rate.html
devtools/client/inspector/animation/test/head.js
--- a/devtools/client/inspector/animation/test/browser.ini
+++ b/devtools/client/inspector/animation/test/browser.ini
@@ -1,12 +1,13 @@
 [DEFAULT]
 tags = devtools
 subsuite = devtools
 support-files =
+  doc_custom_playback_rate.html
   doc_multi_easings.html
   doc_multi_keyframes.html
   doc_multi_timings.html
   doc_simple_animation.html
   head.js
   !/devtools/client/framework/test/shared-head.js
   !/devtools/client/inspector/test/head.js
   !/devtools/client/inspector/test/shared-head.js
@@ -16,21 +17,29 @@ support-files =
 [browser_animation_animated-property-list.js]
 [browser_animation_animated-property-name.js]
 [browser_animation_animation-detail_close-button.js]
 [browser_animation_animation-detail_title.js]
 [browser_animation_animation-detail_visibility.js]
 [browser_animation_animation-list.js]
 [browser_animation_animation-target.js]
 [browser_animation_animation-timeline-tick.js]
+[browser_animation_current-time-label.js]
+[browser_animation_current-time-scrubber.js]
 [browser_animation_empty_on_invalid_nodes.js]
 [browser_animation_inspector_exists.js]
 [browser_animation_keyframes-graph_computed-value-path.js]
 [browser_animation_keyframes-graph_computed-value-path_easing-hint.js]
 [browser_animation_keyframes-graph_keyframe-marker.js]
+[browser_animation_keyframes-progress-bar.js]
+[browser_animation_logic_auto-stop.js]
+[browser_animation_pause-resume-button.js]
+[browser_animation_pause-resume-button_spacebar.js]
+[browser_animation_playback-rate-selector.js]
+[browser_animation_rewind-button.js]
 [browser_animation_summary-graph_animation-name.js]
 [browser_animation_summary-graph_compositor.js]
 [browser_animation_summary-graph_computed-timing-path.js]
 [browser_animation_summary-graph_delay-sign.js]
 [browser_animation_summary-graph_end-delay-sign.js]
 [browser_animation_summary-graph_effect-timing-path.js]
 [browser_animation_summary-graph_negative-delay-path.js]
 [browser_animation_summary-graph_negative-end-delay-path.js]
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/animation/test/browser_animation_current-time-label.js
@@ -0,0 +1,61 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test for following CurrentTimeLabel component:
+// * element existence
+// * label content at plural timing
+
+add_task(async function () {
+  await addTab(URL_ROOT + "doc_multi_timings.html");
+  const { animationInspector, inspector, panel } = await openAnimationInspector();
+
+  info("Checking current time label existence");
+  const labelEl = panel.querySelector(".current-time-label");
+  ok(labelEl, "current time label should exist");
+
+  info("Checking current time label content");
+  await selectNodeAndWaitForAnimations(".keyframes-easing-step", inspector);
+  await clickOnCurrentTimeScrubberController(animationInspector, panel, 0.5);
+  assertLabelContent(labelEl, animationInspector.state.animations[0].state.currentTime);
+  await clickOnCurrentTimeScrubberController(animationInspector, panel, 0.2);
+  assertLabelContent(labelEl, animationInspector.state.animations[0].state.currentTime);
+
+  info("Checking current time label content during running");
+  // Resume
+  await clickOnPauseResumeButton(animationInspector, panel);
+  const previousContent = labelEl.textContent;
+  await wait(1000);
+  const currentContent = labelEl.textContent;
+  isnot(previousContent, currentContent, "Current time label should change");
+});
+
+function assertLabelContent(labelEl, time) {
+  const expected = formatStopwatchTime(time);
+  is(labelEl.textContent, expected, `Content of label should be ${ expected }`);
+}
+
+function formatStopwatchTime(time) {
+  // Format falsy values as 0
+  if (!time) {
+    return "00:00.000";
+  }
+
+  let milliseconds = parseInt(time % 1000, 10);
+  let seconds = parseInt((time / 1000) % 60, 10);
+  let minutes = parseInt((time / (1000 * 60)), 10);
+
+  let pad = (nb, max) => {
+    if (nb < max) {
+      return new Array((max + "").length - (nb + "").length + 1).join("0") + nb;
+    }
+    return nb;
+  };
+
+  minutes = pad(minutes, 10);
+  seconds = pad(seconds, 10);
+  milliseconds = pad(milliseconds, 100);
+
+  return `${minutes}:${seconds}.${milliseconds}`;
+}
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/animation/test/browser_animation_current-time-scrubber.js
@@ -0,0 +1,68 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test for following CurrentTimeScrubber and CurrentTimeScrubberController components:
+// * element existence
+// * scrubber position validity
+// * make animations currentTime to change by click on the controller
+// * mouse drag on the scrubber
+
+add_task(async function () {
+  await addTab(URL_ROOT + "doc_multi_timings.html");
+  const { animationInspector, inspector, panel } = await openAnimationInspector();
+
+  info("Checking scrubber controller existence");
+  const controllerEl = panel.querySelector(".current-time-scrubber-controller");
+  ok(controllerEl, "scrubber controller should exist");
+
+  info("Checking scrubber existence");
+  const scrubberEl = controllerEl.querySelector(".current-time-scrubber");
+  ok(scrubberEl, "scrubber should exist");
+
+  info("Checking scrubber changes current time of animation and the position");
+  await selectNodeAndWaitForAnimations(".enddelay-with-iterations-infinity", inspector);
+  const duration = animationInspector.state.timeScale.getDuration();
+  await clickOnCurrentTimeScrubberController(animationInspector, panel, 0);
+  assertAnimationsCurrentTime(animationInspector, 0);
+  assertPosition(scrubberEl, controllerEl, 0, animationInspector);
+
+  await clickOnCurrentTimeScrubberController(animationInspector, panel, 1);
+  assertAnimationsCurrentTime(animationInspector, duration);
+  assertPosition(scrubberEl, controllerEl, duration, animationInspector);
+
+  await clickOnCurrentTimeScrubberController(animationInspector, panel, 0.5);
+  assertAnimationsCurrentTime(animationInspector, duration * 0.5);
+  assertPosition(scrubberEl, controllerEl, duration * 0.5, animationInspector);
+
+  info("Checking current time scrubber position during running");
+  // Running again
+  await clickOnPauseResumeButton(animationInspector, panel);
+  let previousX = scrubberEl.getBoundingClientRect().x;
+  await wait(100);
+  let currentX = scrubberEl.getBoundingClientRect().x;
+  isnot(previousX, currentX, "Scrubber should be moved");
+
+  info("Checking draggable on scrubber over animation list");
+  await clickOnPauseResumeButton(animationInspector, panel);
+  previousX = scrubberEl.getBoundingClientRect().x;
+  await dragOnCurrentTimeScrubber(animationInspector, panel, 0.5, 2, 30);
+  currentX = scrubberEl.getBoundingClientRect().x;
+  isnot(previousX, currentX, "Scrubber should be draggable");
+
+  info("Checking a behavior which mouse out from animation inspector area " +
+       "during dragging from controller");
+  await dragOnCurrentTimeScrubberController(animationInspector, panel, 0.5, 2);
+  ok(!panel.querySelector(".animation-list-container")
+           .classList.contains("active-scrubber"), "Click and DnD should be inactive");
+});
+
+function assertPosition(scrubberEl, controllerEl, time, animationInspector) {
+  const controllerBounds = controllerEl.getBoundingClientRect();
+  const scrubberBounds = scrubberEl.getBoundingClientRect();
+  const scrubberX = scrubberBounds.x + scrubberBounds.width / 2 - controllerBounds.x;
+  const timeScale = animationInspector.state.timeScale;
+  const expected = Math.round(time / timeScale.getDuration() * controllerBounds.width);
+  is(scrubberX, expected, `Position should be ${ expected } at ${ time }ms`);
+}
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/animation/test/browser_animation_keyframes-progress-bar.js
@@ -0,0 +1,91 @@
+/* Any copyright is dedicated to the Public Domain.
+http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test for following KeyframesProgressBar:
+// * element existence
+// * progress bar position in multi effect timings
+// * progress bar position after changing playback rate
+// * progress bar position when select another animation
+
+const POSITION_TESTCASES = [
+  {
+    targetClassName: "cssanimation-linear",
+    scrubberPositions: [0, 0.25, 0.5, 0.75, 1],
+    expectedPositions: [0, 0.25, 0.5, 0.75, 0],
+  },
+  {
+    targetClassName: "easing-step",
+    scrubberPositions: [0, 0.49, 0.5, 0.99],
+    expectedPositions: [0, 0, 0.5, 0.5],
+  },
+  {
+    targetClassName: "delay-positive",
+    scrubberPositions: [0, 0.33, 0.5],
+    expectedPositions: [0, 0, 0.25],
+  },
+  {
+    targetClassName: "delay-negative",
+    scrubberPositions: [0, 0.49, 0.5, 0.75],
+    expectedPositions: [0, 0, 0.5, 0.75],
+  },
+  {
+    targetClassName: "enddelay-positive",
+    scrubberPositions: [0, 0.66, 0.67, 0.99],
+    expectedPositions: [0, 0.99, 0, 0],
+  },
+  {
+    targetClassName: "enddelay-negative",
+    scrubberPositions: [0, 0.49, 0.5, 0.99],
+    expectedPositions: [0, 0.49, 0, 0],
+  },
+  {
+    targetClassName: "direction-reverse-with-iterations-infinity",
+    scrubberPositions: [0, 0.25, 0.5, 0.75, 1],
+    expectedPositions: [1, 0.75, 0.5, 0.25, 1],
+  },
+  {
+    targetClassName: "fill-both-width-delay-iterationstart",
+    scrubberPositions: [0, 0.33, 0.66, 0.833, 1],
+    expectedPositions: [0.5, 0.5, 0.99, 0.25, 0.5],
+  },
+];
+
+add_task(async function () {
+  await addTab(URL_ROOT + "doc_multi_timings.html");
+  const { animationInspector, inspector, panel } = await openAnimationInspector();
+
+  info("Checking progress bar position in multi effect timings");
+  await clickOnPauseResumeButton(animationInspector, panel);
+
+  for (const testcase of POSITION_TESTCASES) {
+    info(`Checking progress bar position for ${ testcase.targetClassName }`);
+    await selectNodeAndWaitForAnimations(`.${ testcase.targetClassName }`, inspector);
+
+    info("Checking progress bar existence");
+    const areaEl = panel.querySelector(".keyframes-progress-bar-area");
+    ok(areaEl, "progress bar area should exist");
+    const barEl = areaEl.querySelector(".keyframes-progress-bar");
+    ok(barEl, "progress bar should exist");
+
+    const scrubberPositions = testcase.scrubberPositions;
+    const expectedPositions = testcase.expectedPositions;
+
+    for (let i = 0; i < scrubberPositions.length; i++) {
+      info(`Scrubber position is ${ scrubberPositions[i] }`);
+      await clickOnCurrentTimeScrubberController(animationInspector,
+                                                 panel, scrubberPositions[i]);
+      assertPosition(barEl, areaEl, expectedPositions[i], animationInspector);
+    }
+  }
+});
+
+function assertPosition(barEl, areaEl, expectedRate, animationInspector) {
+  const controllerBounds = areaEl.getBoundingClientRect();
+  const barBounds = barEl.getBoundingClientRect();
+  const barX = barBounds.x + barBounds.width / 2 - controllerBounds.x;
+  const expected = controllerBounds.width * expectedRate;
+  ok(expected - 1 < barX && barX < expected + 1,
+    `Position should apploximately be ${ expected } (x of bar is ${ barX })`);
+}
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/animation/test/browser_animation_logic_auto-stop.js
@@ -0,0 +1,63 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Animation inspector makes the current time to stop
+// after end of animation duration except iterations infinity.
+// Test followings:
+// * state of animations and UI components after end of animation duration
+// * state of animations and UI components after end of animation duration
+//   but iteration count is infinity
+
+add_task(async function () {
+  await addTab(URL_ROOT + "doc_multi_timings.html");
+  const { animationInspector, inspector, panel } = await openAnimationInspector();
+
+  info("Checking state after end of animation duration");
+  await selectNodeAndWaitForAnimations(".easing-step", inspector);
+  const pixelsData = getDurationAndRate(animationInspector, panel, 5);
+  await clickOnCurrentTimeScrubberController(animationInspector,
+                                             panel, 1 - pixelsData.rate);
+  await clickOnPauseResumeButton(animationInspector, panel);
+  // Must be able to catch rendering event after stopping the animation.
+  await waitForSummaryAndDetail(animationInspector);
+  await assertStates(animationInspector, panel, false);
+
+  info("Checking state after end of animation duration and infinity iterations");
+  await clickOnPauseResumeButton(animationInspector, panel);
+  await selectNodeAndWaitForAnimations(".enddelay-with-iterations-infinity", inspector);
+  await clickOnCurrentTimeScrubberController(animationInspector, panel, 1);
+  await clickOnPauseResumeButton(animationInspector, panel);
+  await assertStates(animationInspector, panel, true);
+});
+
+async function assertStates(animationInspector, panel, shouldRunning) {
+  const buttonEl = panel.querySelector(".pause-resume-button");
+  const labelEl = panel.querySelector(".current-time-label");
+  const scrubberEl = panel.querySelector(".current-time-scrubber");
+
+  const previousLabelContent = labelEl.textContent;
+  const previousScrubberX = scrubberEl.getBoundingClientRect().x;
+  await wait(100);
+  const currentLabelContent = labelEl.textContent;
+  const currentScrubberX = scrubberEl.getBoundingClientRect().x;
+
+  if (shouldRunning) {
+    isnot(previousLabelContent, currentLabelContent,
+      "Current time label content should change");
+    isnot(previousScrubberX, currentScrubberX,
+      "Current time scrubber position should change");
+    ok(!buttonEl.classList.contains("paused"),
+      "State of button should be running");
+    assertAnimationsRunning(animationInspector, panel);
+  } else {
+    is(previousLabelContent, currentLabelContent,
+      "Current time label Content should not change");
+    is(previousScrubberX, currentScrubberX,
+      "Current time scrubber position should not change");
+    ok(buttonEl.classList.contains("paused"),
+      "State of button should be paused");
+    assertAnimationsPausing(animationInspector, panel);
+  }
+}
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/animation/test/browser_animation_pause-resume-button.js
@@ -0,0 +1,33 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test for following PauseResumeButton component:
+// * element existence
+// * state during running animations
+// * state during pausing animations
+// * make animations to pause by push button
+// * make animations to resume by push button
+
+add_task(async function () {
+  await addTab(URL_ROOT + "doc_custom_playback_rate.html");
+  const { animationInspector, panel } = await openAnimationInspector();
+
+  info("Checking pause/resume button existence");
+  const buttonEl = panel.querySelector(".pause-resume-button");
+  ok(buttonEl, "pause/resume button should exist");
+
+  info("Checking state during running animations");
+  ok(!buttonEl.classList.contains("paused"), "State of button should be running");
+
+  info("Checking button makes animations to pause");
+  await clickOnPauseResumeButton(animationInspector, panel);
+  assertAnimationsPausing(animationInspector, panel);
+  ok(buttonEl.classList.contains("paused"), "State of button should be paused");
+
+  info("Checking button makes animations to resume");
+  await clickOnPauseResumeButton(animationInspector, panel);
+  assertAnimationsRunning(animationInspector, panel);
+  ok(!buttonEl.classList.contains("paused"), "State of button should be resumed");
+});
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/animation/test/browser_animation_pause-resume-button_spacebar.js
@@ -0,0 +1,33 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test for following PauseResumeButton component with spacebar:
+// * make animations to pause/resume by spacebar
+// * combination with other UI components
+
+add_task(async function () {
+  await addTab(URL_ROOT + "doc_custom_playback_rate.html");
+  const { animationInspector, panel } = await openAnimationInspector();
+
+  info("Checking spacebar makes animations to pause");
+  await sendSpaceKeyEvent(animationInspector, panel);
+  assertAnimationsPausing(animationInspector, panel);
+  await sendSpaceKeyEvent(animationInspector, panel);
+  assertAnimationsRunning(animationInspector, panel);
+
+  info("Checking spacebar works with other UI components");
+  // To pause
+  await clickOnPauseResumeButton(animationInspector, panel);
+  // To resume
+  await sendSpaceKeyEvent(animationInspector, panel);
+  assertAnimationsRunning(animationInspector, panel);
+  // To pause
+  await clickOnCurrentTimeScrubberController(animationInspector, panel, 0.5);
+  // To resume
+  await clickOnPauseResumeButton(animationInspector, panel);
+  // To pause
+  await sendSpaceKeyEvent(animationInspector, panel);
+  assertAnimationsPausing(animationInspector, panel);
+});
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/animation/test/browser_animation_playback-rate-selector.js
@@ -0,0 +1,54 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test for following PlaybackRateSelector component:
+// * element existence
+// * make playback rate of animations by the selector
+// * in case of animations have mixed playback rate
+// * in case of animations have playback rate which is not default selectable value
+
+add_task(async function () {
+  await addTab(URL_ROOT + "doc_custom_playback_rate.html");
+  const { animationInspector, inspector, panel } = await openAnimationInspector();
+
+  info("Checking playback rate selector existence");
+  const selectEl = panel.querySelector(".playback-rate-selector");
+  ok(selectEl, "scrubber controller should exist");
+
+  info("Checking playback rate existence which includes custom rate of animations");
+  const selectableRates = [0.1, 0.25, 0.5, 1, 1.5, 2, 5, 10];
+  is(selectEl.options.length, selectableRates.length,
+    `Length of options should be ${ selectableRates.length }`);
+  for (let i = 0; i < selectEl.options.length; i++) {
+    const optionEl = selectEl.options[i];
+    const selectableRate = selectableRates[i];
+    is(Number(optionEl.value), selectableRate,
+      `Option of index[${ i }] should be ${ selectableRate }`);
+  }
+
+  info("Checking selected playback rate");
+  is(Number(selectEl.value), 1.5, "Selected option should be 1.5");
+
+  info("Checking playback rate of animations");
+  await clickOnPlaybackRateSelector(animationInspector, panel, 0.5);
+  assertPlaybackRate(animationInspector, 0.5);
+
+  info("Checking mixed playback rate");
+  await selectNodeAndWaitForAnimations("div", inspector);
+  await clickOnPlaybackRateSelector(animationInspector, panel, 2);
+  assertPlaybackRate(animationInspector, 2);
+  await selectNodeAndWaitForAnimations("body", inspector);
+  is(selectEl.value, "", "Selected option should be empty");
+
+  info("Checking playback rate after re-setting");
+  await clickOnPlaybackRateSelector(animationInspector, panel, 1);
+  assertPlaybackRate(animationInspector, 1);
+});
+
+async function assertPlaybackRate(animationInspector, rate) {
+  const isRateEqual =
+    animationInspector.state.animations.every(({state}) => state.playbackRate === rate);
+  ok(isRateEqual, `Playback rate of animations should be ${ rate }`);
+}
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/animation/test/browser_animation_rewind-button.js
@@ -0,0 +1,28 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test for following RewindButton component:
+// * element existence
+// * make animations to rewind to zero
+// * the state should be always paused after rewinding
+
+add_task(async function () {
+  await addTab(URL_ROOT + "doc_custom_playback_rate.html");
+  const { animationInspector, panel } = await openAnimationInspector();
+
+  info("Checking button existence");
+  ok(panel.querySelector(".rewind-button"), "Rewind button should exist");
+
+  info("Checking rewind button makes animations to rewind to zero");
+  await clickOnRewindButton(animationInspector, panel);
+  assertAnimationsCurrentTime(animationInspector, 0);
+  assertAnimationsPausing(animationInspector, panel);
+
+  info("Checking rewind button makes animations after clicking scrubber");
+  await clickOnCurrentTimeScrubberController(animationInspector, panel, 0.5);
+  await clickOnRewindButton(animationInspector, panel);
+  assertAnimationsCurrentTime(animationInspector, 0);
+  assertAnimationsPausing(animationInspector, panel);
+});
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/animation/test/doc_custom_playback_rate.html
@@ -0,0 +1,29 @@
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8">
+    <style>
+    div {
+      background-color: lime;
+      height: 100px;
+    }
+    </style>
+  </head>
+  <body>
+    <script>
+    "use strict";
+
+    const duration = 100000;
+
+    function createAnimation() {
+      const div = document.createElement("div");
+      document.body.appendChild(div);
+      const animation = div.animate([{ opacity: 0 }], duration);
+      animation.playbackRate = 1.5;
+    }
+
+    createAnimation();
+    createAnimation();
+    </script>
+  </body>
+</html>
--- a/devtools/client/inspector/animation/test/head.js
+++ b/devtools/client/inspector/animation/test/head.js
@@ -119,16 +119,188 @@ const clickOnDetailCloseButton = functio
   const buttonEl = panel.querySelector(".animation-detail-close-button");
   const bounds = buttonEl.getBoundingClientRect();
   const x = bounds.width / 2;
   const y = bounds.height / 2;
   EventUtils.synthesizeMouse(buttonEl, x, y, {}, buttonEl.ownerGlobal);
 };
 
 /**
+ * Click on pause/resume button.
+ *
+ * @param {AnimationInspector} animationInspector
+ * @param {AnimationsPanel} panel
+ *        The panel instance.
+ */
+const clickOnPauseResumeButton = async function (animationInspector, panel) {
+  info("Click on pause/resume button");
+  const buttonEl = panel.querySelector(".pause-resume-button");
+  const bounds = buttonEl.getBoundingClientRect();
+  const x = bounds.width / 2;
+  const y = bounds.height / 2;
+  EventUtils.synthesizeMouse(buttonEl, x, y, {}, buttonEl.ownerGlobal);
+  await waitForSummaryAndDetail(animationInspector);
+};
+
+/**
+ * Click on rewind button.
+ *
+ * @param {AnimationInspector} animationInspector
+ * @param {AnimationsPanel} panel
+ *        The panel instance.
+ */
+const clickOnRewindButton = async function (animationInspector, panel) {
+  info("Click on rewind button");
+  const buttonEl = panel.querySelector(".rewind-button");
+  const bounds = buttonEl.getBoundingClientRect();
+  const x = bounds.width / 2;
+  const y = bounds.height / 2;
+  EventUtils.synthesizeMouse(buttonEl, x, y, {}, buttonEl.ownerGlobal);
+  await waitForSummaryAndDetail(animationInspector);
+};
+
+/**
+ * Click on the scrubber controller pane to update the animation current time.
+ *
+ * @param {AnimationsPanel} panel
+ * @param {Number} mouseDownPosition
+ *        rate on scrubber controller pane.
+ *        This method calculates
+ *        `mouseDownPosition * offsetWidth + offsetLeft of scrubber controller pane`
+ *        as the clientX of MouseEvent.
+ */
+const clickOnCurrentTimeScrubberController = async function (animationInspector,
+                                                             panel,
+                                                             mouseDownPosition,
+                                                             mouseMovePosition) {
+  const controllerEl = panel.querySelector(".current-time-scrubber-controller");
+  const bounds = controllerEl.getBoundingClientRect();
+  const mousedonwX = bounds.width * mouseDownPosition;
+
+  info(`Click ${ mousedonwX } on scrubber controller`);
+  EventUtils.synthesizeMouse(controllerEl, mousedonwX, 0, {}, controllerEl.ownerGlobal);
+  await waitForSummaryAndDetail(animationInspector);
+};
+
+/**
+ * Click on playback rate selector to select given rate.
+ *
+ * @param {AnimationInspector} animationInspector
+ * @param {AnimationsPanel} panel
+ * @param {Number} rate
+ */
+const clickOnPlaybackRateSelector = async function (animationInspector, panel, rate) {
+  info(`Click on playback rate selector to select ${rate}`);
+  const selectEl = panel.querySelector(".playback-rate-selector");
+  const optionEl = [...selectEl.options].filter(o => Number(o.value) === rate)[0];
+
+  if (!optionEl) {
+    ok(false, `Could not find an option for rate ${ rate } in the rate selector. ` +
+              `Values are: ${ [...selectEl.options].map(o => o.value) }`);
+    return;
+  }
+
+  const win = selectEl.ownerGlobal;
+  EventUtils.synthesizeMouseAtCenter(selectEl, { type: "mousedown" }, win);
+  EventUtils.synthesizeMouseAtCenter(optionEl, { type: "mouseup" }, win);
+  await waitForSummaryAndDetail(animationInspector);
+};
+
+/**
+ * Drag on the scrubber to update the animation current time.
+ *
+ * @param {AnimationsPanel} panel
+ * @param {Number} mouseDownPosition
+ *        rate on scrubber controller pane.
+ *        This method calculates
+ *        `mouseDownPosition * offsetWidth + offsetLeft of scrubber controller pane`
+ *        as the clientX of MouseEvent.
+ * @param {Number} mouseMovePosition
+ *        Dispatch mousemove event with mouseMovePosition after mousedown.
+ *        Calculation for clinetX is same to above.
+ * @param {Number} mouseYPixel
+ *        Y of mouse in pixel.
+ */
+const dragOnCurrentTimeScrubber = async function (animationInspector,
+                                                  panel,
+                                                  mouseDownPosition,
+                                                  mouseMovePosition,
+                                                  mouseYPixel) {
+  const controllerEl = panel.querySelector(".current-time-scrubber");
+  const bounds = controllerEl.getBoundingClientRect();
+  const mousedonwX = bounds.width * mouseDownPosition;
+  const mousemoveX = bounds.width * mouseMovePosition;
+
+  info(`Drag on scrubber from ${ mousedonwX } to ${ mousemoveX }`);
+  EventUtils.synthesizeMouse(controllerEl, mousedonwX, mouseYPixel,
+                             { type: "mousedown" }, controllerEl.ownerGlobal);
+  await waitForSummaryAndDetail(animationInspector);
+  EventUtils.synthesizeMouse(controllerEl, mousemoveX, mouseYPixel,
+                             { type: "mousemove" }, controllerEl.ownerGlobal);
+  EventUtils.synthesizeMouse(controllerEl, mousemoveX, mouseYPixel,
+                             { type: "mouseup" }, controllerEl.ownerGlobal);
+  await waitForSummaryAndDetail(animationInspector);
+};
+
+/**
+ * Drag on the scrubber controller pane to update the animation current time.
+ *
+ * @param {AnimationsPanel} panel
+ * @param {Number} mouseDownPosition
+ *        rate on scrubber controller pane.
+ *        This method calculates
+ *        `mouseDownPosition * offsetWidth + offsetLeft of scrubber controller pane`
+ *        as the clientX of MouseEvent.
+ * @param {Number} mouseMovePosition
+ *        Dispatch mousemove event with mouseMovePosition after mousedown.
+ *        Calculation for clinetX is same to above.
+ */
+const dragOnCurrentTimeScrubberController = async function (animationInspector,
+                                                            panel,
+                                                            mouseDownPosition,
+                                                            mouseMovePosition) {
+  const controllerEl = panel.querySelector(".current-time-scrubber-controller");
+  const bounds = controllerEl.getBoundingClientRect();
+  const mousedonwX = bounds.width * mouseDownPosition;
+  const mousemoveX = bounds.width * mouseMovePosition;
+
+  info(`Drag on scrubber controller from ${ mousedonwX } to ${ mousemoveX }`);
+  EventUtils.synthesizeMouse(controllerEl, mousedonwX, 0,
+                             { type: "mousedown" }, controllerEl.ownerGlobal);
+  await waitForSummaryAndDetail(animationInspector);
+  EventUtils.synthesizeMouse(controllerEl, mousemoveX, 0,
+                             { type: "mousemove" }, controllerEl.ownerGlobal);
+  EventUtils.synthesizeMouse(controllerEl, mousemoveX, 0,
+                             { type: "mouseup" }, controllerEl.ownerGlobal);
+  await waitForSummaryAndDetail(animationInspector);
+};
+
+/**
+ * Get current animation duration and rate of
+ * clickOrDragOnCurrentTimeScrubberController in given pixels.
+ *
+ * @param {AnimationInspector} animationInspector
+ * @param {AnimationsPanel} panel
+ * @param {Number} pixels
+ * @return {Object}
+ *         {
+ *           duration,
+ *           rate,
+ *         }
+ */
+const getDurationAndRate = function (animationInspector, panel, pixels) {
+  const controllerEl = panel.querySelector(".current-time-scrubber-controller");
+  const bounds = controllerEl.getBoundingClientRect();
+  const duration =
+    animationInspector.state.timeScale.getDuration() / bounds.width * pixels;
+  const rate = 1 / bounds.width * pixels;
+  return { duration, rate };
+};
+
+/**
  * Set the inspector's current selection to a node or to the first match of the
  * given css selector and wait for the animations to be displayed
  *
  * @param {String|NodeFront}
  *        data The node to select
  * @param {InspectorPanel} inspector
  *        The instance of InspectorPanel currently loaded in the toolbox
  * @param {String} reason
@@ -142,16 +314,28 @@ const selectNodeAndWaitForAnimations = a
   // be properly displayed (wait for all target DOM nodes to be previewed).
   const onUpdated = inspector.once("inspector-updated");
   await selectNode(data, inspector, reason);
   await onUpdated;
   await waitForRendering(inspector.animationinspector);
 };
 
 /**
+ * Send keyboard event of space to given panel.
+ *
+ * @param {AnimationInspector} animationInspector
+ * @param {AnimationsPanel} panel
+ */
+const sendSpaceKeyEvent = async function (animationInspector, panel) {
+  panel.focus();
+  EventUtils.sendKey("SPACE", panel.ownerGlobal);
+  await waitForSummaryAndDetail(animationInspector);
+};
+
+/**
  * Set the sidebar width by given parameter.
  *
  * @param {String} width
  *        Change sidebar width by given parameter.
  * @param {InspectorPanel} inspector
  *        The instance of InspectorPanel currently loaded in the toolbox
  * @return {Promise} Resolves when the sidebar size changed.
  */
@@ -175,17 +359,18 @@ const waitForRendering = async function 
 };
 
 /**
  * Wait for rendering of animation keyframes.
  *
  * @param {AnimationInspector} inspector
  */
 const waitForAnimationDetail = async function (animationInspector) {
-  if (animationInspector.state.animations.length === 1) {
+  if (animationInspector.state.selectedAnimation &&
+      animationInspector.state.detailVisibility) {
     await animationInspector.once("animation-keyframes-rendered");
   }
 };
 
 /**
  * Wait for all AnimationTarget components to be fully loaded
  * (fetched their related actor and rendered).
  *
@@ -204,16 +389,79 @@ const waitForAllAnimationTargets = async
  */
 const waitForAllSummaryGraph = async function (animationInspector) {
   for (let i = 0; i < animationInspector.state.animations.length; i++) {
     await animationInspector.once("animation-summary-graph-rendered");
   }
 };
 
 /**
+ * Wait for rendering of all summary graph and detail.
+ *
+ * @param {AnimationInspector} inspector
+ */
+const waitForSummaryAndDetail = async function (animationInspector) {
+  await Promise.all([
+    waitForAllSummaryGraph(animationInspector),
+    waitForAnimationDetail(animationInspector),
+  ]);
+};
+
+/**
+ * Check whether current time of all animations and UI are given specified time.
+ *
+ * @param {AnimationInspector} animationInspector
+ * @param {AnimationsPanel} panel
+ * @param {Number} time
+ */
+function assertAnimationsCurrentTime(animationInspector, time) {
+  const isTimeEqual =
+    animationInspector.state.animations.every(({state}) => state.currentTime === time);
+  ok(isTimeEqual, `Current time of animations should be ${ time }`);
+}
+
+/**
+ * Check whether the animations are pausing.
+ *
+ * @param {AnimationInspector} animationInspector
+ * @param {AnimationsPanel} panel
+ */
+function assertAnimationsPausing(animationInspector, panel) {
+  assertAnimationsPausingOrRunning(animationInspector, panel, true);
+}
+
+/**
+ * Check whether the animations are pausing/running.
+ *
+ * @param {AnimationInspector} animationInspector
+ * @param {AnimationsPanel} panel
+ * @param {boolean} shouldPause
+ */
+function assertAnimationsPausingOrRunning(animationInspector, panel, shouldPause) {
+  const hasRunningAnimation =
+    animationInspector.state.animations.some(({state}) => state.playState === "running");
+
+  if (shouldPause) {
+    is(hasRunningAnimation, false, "All animations should be paused");
+  } else {
+    is(hasRunningAnimation, true, "Animations should be running at least one");
+  }
+}
+
+/**
+ * Check whether the animations are running.
+ *
+ * @param {AnimationInspector} animationInspector
+ * @param {AnimationsPanel} panel
+ */
+function assertAnimationsRunning(animationInspector, panel) {
+  assertAnimationsPausingOrRunning(animationInspector, panel, false);
+}
+
+/**
  * Check the <stop> element in the given linearGradientEl for the correct offset
  * and color attributes.
  *
  * @param {Element} linearGradientEl
           <linearGradient> element which has <stop> element.
  * @param {Number} offset
  *        float which represents the "offset" attribute of <stop>.
  * @param {String} expectedColor