Bug 1210796 - Part 13: Add tests. r=pbro draft
authorDaisuke Akatsuka <daisuke@mozilla-japan.org>
Tue, 18 Apr 2017 12:15:56 +0900
changeset 564126 254d333f555fc715a166670cb2d89d7031017b32
parent 564125 153619c923b9a15576610f1eef813c1651fc0e2d
child 564127 24990eab85d69b26128d05a2523b5241b82d69ba
push id54524
push userbmo:dakatsuka@mozilla.com
push dateTue, 18 Apr 2017 09:24:06 +0000
reviewerspbro
bugs1210796
milestone55.0a1
Bug 1210796 - Part 13: Add tests. r=pbro MozReview-Commit-ID: DqcIrW5Hy7E
devtools/client/animationinspector/test/browser.ini
devtools/client/animationinspector/test/browser_animation_animated_properties_for_delayed_starttime_animations.js
devtools/client/animationinspector/test/browser_animation_animated_properties_path.js
devtools/client/animationinspector/test/browser_animation_animated_properties_progress_indicator.js
devtools/client/animationinspector/test/browser_animation_detail_displayed.js
devtools/client/animationinspector/test/browser_animation_timeline_ui.js
devtools/client/animationinspector/test/browser_animation_ui_updates_when_animation_data_changes.js
devtools/client/animationinspector/test/doc_delayed_starttime_animations.html
devtools/client/animationinspector/test/doc_multiple_property_types.html
devtools/client/animationinspector/test/head.js
--- a/devtools/client/animationinspector/test/browser.ini
+++ b/devtools/client/animationinspector/test/browser.ini
@@ -1,35 +1,41 @@
 [DEFAULT]
 tags = devtools
 subsuite = devtools
 support-files =
   doc_body_animation.html
+  doc_delayed_starttime_animations.html
   doc_end_delay.html
   doc_frame_script.js
   doc_keyframes.html
   doc_modify_playbackRate.html
   doc_negative_animation.html
   doc_pseudo_elements.html
   doc_script_animation.html
   doc_short_duration_animation.html
   doc_simple_animation.html
   doc_multiple_animation_types.html
+  doc_multiple_property_types.html
   doc_timing_combination_animation.html
   head.js
   !/devtools/client/commandline/test/helpers.js
   !/devtools/client/framework/test/shared-head.js
   !/devtools/client/inspector/test/head.js
   !/devtools/client/inspector/test/shared-head.js
   !/devtools/client/shared/test/test-actor-registry.js
   !/devtools/client/shared/test/test-actor.js
 
 [browser_animation_animated_properties_displayed.js]
+[browser_animation_animated_properties_for_delayed_starttime_animations.js]
+[browser_animation_animated_properties_path.js]
+[browser_animation_animated_properties_progress_indicator.js]
 [browser_animation_click_selects_animation.js]
 [browser_animation_controller_exposes_document_currentTime.js]
+[browser_animation_detail_displayed.js]
 skip-if = os == "linux" && !debug # Bug 1234567
 [browser_animation_empty_on_invalid_nodes.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]
new file mode 100644
--- /dev/null
+++ b/devtools/client/animationinspector/test/browser_animation_animated_properties_for_delayed_starttime_animations.js
@@ -0,0 +1,38 @@
+/* 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 for animations that have different starting time.
+// We should check progress indicator working well even if the start time is not zero.
+
+add_task(function* () {
+  yield addTab(URL_ROOT + "doc_delayed_starttime_animations.html");
+  const { panel } = yield openAnimationInspector();
+  yield setStyle(null, panel, "animation", "anim 100s", "#target2");
+  yield setStyle(null, panel, "animation", "anim 100s", "#target3");
+  yield setStyle(null, panel, "animation", "anim 100s", "#target4");
+  yield setStyle(null, panel, "animation", "anim 100s", "#target5");
+
+  yield clickOnAnimation(panel, 1);
+  const timelineComponent = panel.animationsTimelineComponent;
+  const detailsComponent = timelineComponent.details;
+  const progressIndicatorEl = detailsComponent.progressIndicatorEl;
+  const startTime = detailsComponent.animation.state.previousStartTime;
+  detailsComponent.indicateProgress(0);
+  is(progressIndicatorEl.style.left, "0%",
+     "The progress indicator position should be 0% at 0ms");
+  detailsComponent.indicateProgress(startTime);
+  is(progressIndicatorEl.style.left, "0%",
+     "The progress indicator position should be 0% at start time");
+  detailsComponent.indicateProgress(startTime + 50 * 1000);
+  is(progressIndicatorEl.style.left, "50%",
+     "The progress indicator position should be 50% at half time of animation");
+  detailsComponent.indicateProgress(startTime + 99 * 1000);
+  is(progressIndicatorEl.style.left, "99%",
+     "The progress indicator position should be 99% at 99s");
+  detailsComponent.indicateProgress(startTime + 100 * 1000);
+  is(progressIndicatorEl.style.left, "0%",
+     "The progress indicator position should be 0% at end of animation");
+});
new file mode 100644
--- /dev/null
+++ b/devtools/client/animationinspector/test/browser_animation_animated_properties_path.js
@@ -0,0 +1,306 @@
+/* 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 animated properties's graph.
+// The graph constructs from SVG, also uses path (for shape), linearGradient,
+// stop (for color) element and so on.
+// We test followings.
+// 1. class name - which represents the animation type.
+// 2. coordinates of the path - x is time, y is graph y value which should be 0 - 1.
+//    The path of animation types 'color', 'coord', 'opacity' or 'discrete' are created by
+//    createPathSegments. Other types are created by createKeyframesPathSegments.
+// 3. color - animation type 'color' has linearGradient element.
+
+requestLongerTimeout(5);
+
+const TEST_CASES = [
+  {
+    "background-color": {
+      expectedClass: "color",
+      expectedValues: [
+        { x: 0, y: 1, color: "rgb(255, 0, 0)" },
+        { x: 1000, y: 1, color: "rgb(0, 255, 0)" }
+      ]
+    },
+    "font-size": {
+      expectedClass: "length",
+      expectedValues: [
+        { x: 0, y: 0 },
+        { x: 1000, y: 1 },
+      ]
+    },
+    "margin-left": {
+      expectedClass: "coord",
+      expectedValues: [
+        { x: 0, y: 0 },
+        { x: 1000, y: 1 },
+      ]
+    },
+    "opacity": {
+      expectedClass: "opacity",
+      expectedValues: [
+        { x: 0, y: 0 },
+        { x: 1000, y: 1 },
+      ]
+    },
+    "text-align": {
+      expectedClass: "discrete",
+      expectedValues: [
+        { x: 0, y: 0 },
+        { x: 499.999, y: 0 },
+        { x: 500, y: 1 },
+        { x: 1000, y: 1 },
+      ]
+    },
+    "transform": {
+      expectedClass: "transform",
+      expectedValues: [
+        { x: 0, y: 0 },
+        { x: 1000, y: 1 },
+      ]
+    }
+  },
+
+  {
+    "background-color": {
+      expectedClass: "color",
+      expectedValues: [
+        { x: 0, y: 1, color: "rgb(0, 255, 0)" },
+        { x: 1000, y: 1, color: "rgb(255, 0, 0)" }
+      ]
+    },
+    "font-size": {
+      expectedClass: "length",
+      expectedValues: [
+        { x: 0, y: 1 },
+        { x: 1000, y: 0 },
+      ]
+    },
+    "margin-left": {
+      expectedClass: "coord",
+      expectedValues: [
+        { x: 0, y: 1 },
+        { x: 1000, y: 0 },
+      ]
+    },
+    "opacity": {
+      expectedClass: "opacity",
+      expectedValues: [
+        { x: 0, y: 1 },
+        { x: 1000, y: 0 },
+      ]
+    },
+    "text-align": {
+      expectedClass: "discrete",
+      expectedValues: [
+        { x: 0, y: 0 },
+        { x: 499.999, y: 0 },
+        { x: 500, y: 1 },
+        { x: 1000, y: 1 },
+      ]
+    },
+    "transform": {
+      expectedClass: "transform",
+      expectedValues: [
+        { x: 0, y: 1 },
+        { x: 1000, y: 0 },
+      ]
+    }
+  },
+
+  {
+    "background-color": {
+      expectedClass: "color",
+      expectedValues: [
+        { x: 0, y: 1, color: "rgb(255, 0, 0)" },
+        { x: 500, y: 1, color: "rgb(0, 0, 255)" },
+        { x: 1000, y: 1, color: "rgb(0, 255, 0)" }
+      ]
+    },
+    "font-size": {
+      expectedClass: "length",
+      expectedValues: [
+        { x: 0, y: 0 },
+        { x: 500, y: 1 },
+        { x: 1000, y: 0 },
+      ]
+    },
+    "margin-left": {
+      expectedClass: "coord",
+      expectedValues: [
+        { x: 0, y: 0 },
+        { x: 500, y: 1 },
+        { x: 1000, y: 0 },
+      ]
+    },
+    "opacity": {
+      expectedClass: "opacity",
+      expectedValues: [
+        { x: 0, y: 0 },
+        { x: 500, y: 1 },
+        { x: 1000, y: 0 },
+      ]
+    },
+    "text-align": {
+      expectedClass: "discrete",
+      expectedValues: [
+        { x: 0, y: 0 },
+        { x: 249.999, y: 0 },
+        { x: 250, y: 1 },
+        { x: 749.999, y: 1 },
+        { x: 750, y: 0 },
+        { x: 1000, y: 0 },
+      ]
+    },
+    "transform": {
+      expectedClass: "transform",
+      expectedValues: [
+        { x: 0, y: 0 },
+        { x: 500, y: 1 },
+        { x: 1000, y: 0 },
+      ]
+    }
+  },
+  {
+    "background-color": {
+      expectedClass: "color",
+      expectedValues: [
+        { x: 0, y: 1, color: "rgb(255, 0, 0)" },
+        { x: 499.999, y: 1, color: "rgb(255, 0, 0)" },
+        { x: 500, y: 1, color: "rgb(128, 128, 0)" },
+        { x: 999.999, y: 1, color: "rgb(128, 128, 0)" },
+        { x: 1000, y: 1, color: "rgb(0, 255, 0)" }
+      ]
+    },
+    "font-size": {
+      expectedClass: "length",
+      expectedValues: [
+        { x: 0, y: 0 },
+        { x: 500, y: 0 },
+        { x: 500, y: 0.5 },
+        { x: 1000, y: 0.5 },
+        { x: 1000, y: 1 },
+      ]
+    },
+    "margin-left": {
+      expectedClass: "coord",
+      expectedValues: [
+        { x: 0, y: 0 },
+        { x: 499.999, y: 0 },
+        { x: 500, y: 0.5 },
+        { x: 999.999, y: 0.5 },
+        { x: 1000, y: 1 },
+      ]
+    },
+    "opacity": {
+      expectedClass: "opacity",
+      expectedValues: [
+        { x: 0, y: 0 },
+        { x: 499.999, y: 0 },
+        { x: 500, y: 0.5 },
+        { x: 999.999, y: 0.5 },
+        { x: 1000, y: 1 },
+      ]
+    },
+    "text-align": {
+      expectedClass: "discrete",
+      expectedValues: [
+        { x: 0, y: 0 },
+        { x: 499.999, y: 0 },
+        { x: 500, y: 1 },
+        { x: 1000, y: 1 },
+      ]
+    },
+    "transform": {
+      expectedClass: "transform",
+      expectedValues: [
+        { x: 0, y: 0 },
+        { x: 500, y: 0 },
+        { x: 500, y: 0.5 },
+        { x: 1000, y: 0.5 },
+        { x: 1000, y: 1 },
+      ]
+    }
+  },
+  {
+    "opacity": {
+      expectedClass: "opacity",
+      expectedValues: [
+        { x: 0, y: 0 },
+        { x: 500, y: 0.5 },
+        { x: 1000, y: 1 },
+      ]
+    }
+  }
+];
+
+add_task(function* () {
+  yield addTab(URL_ROOT + "doc_multiple_property_types.html");
+  const {panel} = yield openAnimationInspector();
+  const timelineComponent = panel.animationsTimelineComponent;
+  const detailEl = timelineComponent.details.containerEl;
+
+  for (let i = 0; i < TEST_CASES.length; i++) {
+    info(`Click to select the animation[${ i }]`);
+    yield clickOnAnimation(panel, i);
+    const timeBlock = timelineComponent.timeBlocks[0];
+    const state = timeBlock.animation.state;
+    const properties = TEST_CASES[i];
+    for (let property in properties) {
+      const testcase = properties[property];
+      info(`Test path of ${ property }`);
+      const className = testcase.expectedClass;
+      const pathEl = detailEl.querySelector(`path.${ className }`);
+      ok(pathEl, `Path element with class '${ className }' should exis`);
+      checkPathSegments(pathEl, state, testcase.expectedValues);
+    }
+  }
+});
+
+function checkPathSegments(pathEl, { duration }, expectedValues) {
+  const pathSegList = pathEl.pathSegList;
+
+  const firstPathSeg = pathSegList.getItem(0);
+  is(firstPathSeg.x, 0, "The x of first segment should be 0");
+  is(firstPathSeg.y, 0, "The y of first segment should be 0");
+
+  expectedValues.forEach(expectedValue => {
+    ok(hasSegment(pathSegList, expectedValue.x, expectedValue.y),
+       `The path segment of x ${ expectedValue.x }, y ${ expectedValue.y } should exist`);
+
+    if (expectedValue.color) {
+      checkColor(pathEl.closest("svg"), expectedValue.x / duration, expectedValue.color);
+    }
+  });
+
+  const closePathSeg = pathSegList.getItem(pathSegList.numberOfItems - 1);
+  is(closePathSeg.pathSegType, closePathSeg.PATHSEG_CLOSEPATH,
+     "The actual last segment should be close path");
+}
+
+function checkColor(svgEl, offset, color) {
+  const stopEl = findStopElement(svgEl, offset);
+  ok(stopEl, `stop element at offset ${ offset } should exist`);
+  is(stopEl.getAttribute("stop-color"), color,
+     `stop-color of stop element at offset ${ offset } should be ${ color }`);
+}
+
+function hasSegment(pathSegList, x, y) {
+  for (let i = 1; i < pathSegList.numberOfItems - 1; i++) {
+    const pathSeg = pathSegList.getItem(i);
+    if (parseFloat(pathSeg.x.toFixed(3)) === x &&
+        parseFloat(pathSeg.y.toFixed(6)) === y) {
+      return true;
+    }
+  }
+  return false;
+}
+
+function findStopElement(svgEl, offset) {
+  return [...svgEl.querySelectorAll("stop")].find(stopEl => {
+    return stopEl.getAttribute("offset") == offset;
+  });
+}
new file mode 100644
--- /dev/null
+++ b/devtools/client/animationinspector/test/browser_animation_animated_properties_progress_indicator.js
@@ -0,0 +1,86 @@
+/* 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 progress indicator in animated properties.
+// Since this indicator works with the timeline, after selecting each animation,
+// click the timeline header to change the current time and check the change.
+
+add_task(function* () {
+  yield addTab(URL_ROOT + "doc_multiple_property_types.html");
+  const { panel } = yield openAnimationInspector();
+  const timelineComponent = panel.animationsTimelineComponent;
+  const detailsComponent = timelineComponent.details;
+
+  info("Click to select the animation");
+  yield clickOnAnimation(panel, 0);
+  let progressIndicatorEl = detailsComponent.progressIndicatorEl;
+  ok(progressIndicatorEl, "The progress indicator should be exist");
+  yield clickOnTimelineHeader(panel, 0);
+  is(progressIndicatorEl.style.left, "0%",
+     "The left style of progress indicator element should be 0% at 0ms");
+  yield clickOnTimelineHeader(panel, 0.5);
+  approximate(progressIndicatorEl.style.left, "50%",
+              "The left style of progress indicator element should be "
+              + "approximately 50% at 500ms");
+  yield clickOnTimelineHeader(panel, 1);
+  is(progressIndicatorEl.style.left, "100%",
+     "The left style of progress indicator element should be 100% at 1000ms");
+
+  info("Click to select the steps animation");
+  yield clickOnAnimation(panel, 4);
+  // Re-get progressIndicatorEl since this element re-create
+  // in case of select the animation.
+  progressIndicatorEl = detailsComponent.progressIndicatorEl;
+  // Use indicateProgess directly from here since
+  // MouseEvent.clientX may not be able to indicate finely
+  // in case of the width of header element * xPositionRate has a fraction.
+  detailsComponent.indicateProgress(499);
+  is(progressIndicatorEl.style.left, "0%",
+     "The left style of progress indicator element should be 0% at 0ms");
+  detailsComponent.indicateProgress(499);
+  is(progressIndicatorEl.style.left, "0%",
+     "The left style of progress indicator element should be 0% at 499ms");
+  detailsComponent.indicateProgress(500);
+  is(progressIndicatorEl.style.left, "50%",
+     "The left style of progress indicator element should be 50% at 500ms");
+  detailsComponent.indicateProgress(999);
+  is(progressIndicatorEl.style.left, "50%",
+     "The left style of progress indicator element should be 50% at 999ms");
+  yield clickOnTimelineHeader(panel, 1);
+  is(progressIndicatorEl.style.left, "100%",
+     "The left style of progress indicator element should be 100% at 1000ms");
+
+  info("Change the playback rate");
+  yield changeTimelinePlaybackRate(panel, 2);
+  yield clickOnAnimation(panel, 0);
+  progressIndicatorEl = detailsComponent.progressIndicatorEl;
+  yield clickOnTimelineHeader(panel, 0);
+  is(progressIndicatorEl.style.left, "0%",
+     "The left style of progress indicator element should be 0% "
+     + "at 0ms and playback rate 2");
+  detailsComponent.indicateProgress(250);
+  is(progressIndicatorEl.style.left, "50%",
+     "The left style of progress indicator element should be 50% "
+     + "at 250ms and playback rate 2");
+  detailsComponent.indicateProgress(500);
+  is(progressIndicatorEl.style.left, "100%",
+     "The left style of progress indicator element should be 100% "
+     + "at 500ms and playback rate 2");
+
+  info("Check the progress indicator position after select another animation");
+  yield changeTimelinePlaybackRate(panel, 1);
+  yield clickOnTimelineHeader(panel, 0.5);
+  const originalIndicatorPosition = progressIndicatorEl.style.left;
+  yield clickOnAnimation(panel, 1);
+  is(progressIndicatorEl.style.left, originalIndicatorPosition,
+     "The animation time should be continued even if another animation selects");
+});
+
+function approximate(percentageString1, percentageString2, message) {
+  const val1 = Math.round(parseFloat(percentageString1));
+  const val2 = Math.round(parseFloat(percentageString2));
+  is(val1, val2, message);
+}
new file mode 100644
--- /dev/null
+++ b/devtools/client/animationinspector/test/browser_animation_detail_displayed.js
@@ -0,0 +1,41 @@
+/* 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";
+
+// Tests the behavior of animation-detail container.
+// We test following cases.
+// 1. Existance of animation-detail element.
+// 2. Hidden at first if multiple animations were displayed.
+// 3. Display after click on an animation.
+// 4. Display from first time if displayed animation is only one.
+
+requestLongerTimeout(5);
+
+add_task(function* () {
+  yield addTab(URL_ROOT + "doc_multiple_property_types.html");
+  const { panel, inspector } = yield openAnimationInspector();
+  const timelineComponent = panel.animationsTimelineComponent;
+  const animationDetailEl =
+    timelineComponent.rootWrapperEl.querySelector(".animation-detail");
+
+  // 1. Existance of animation-detail element.
+  ok(animationDetailEl, "The animation-detail element should exist");
+
+  // 2. Hidden at first if multiple animations were displayed.
+  const win = animationDetailEl.ownerDocument.defaultView;
+  is(win.getComputedStyle(animationDetailEl).display, "none",
+     "The animation-detail element should be hidden at first "
+     + "if multiple animations were displayed");
+
+  // 3. Display after click on an animation.
+  yield clickOnAnimation(panel, 0);
+  isnot(win.getComputedStyle(animationDetailEl).display, "none",
+        "The animation-detail element should be displayed after clicked on an animation");
+
+  // 4. Display from first time if displayed animation is only one.
+  yield selectNodeAndWaitForAnimations("#target1", inspector);
+  ok(animationDetailEl.querySelector(".property"),
+     "The property in animation-detail element should be displayed");
+});
--- a/devtools/client/animationinspector/test/browser_animation_timeline_ui.js
+++ b/devtools/client/animationinspector/test/browser_animation_timeline_ui.js
@@ -34,10 +34,17 @@ add_task(function* () {
        "The animated node target element is in the DOM");
     ok(animationEl.querySelector(".time-block"),
        "The timeline element is in the DOM");
     is(animationEl.querySelector(".name").textContent,
        animation.state.name,
        "The name on the timeline is correct");
     ok(animationEl.querySelector("svg path"),
        "The timeline has svg and path element as summary graph");
+
+    const expectedBackgroundColor =
+      i % 2 === 0 ? "rgba(128, 128, 128, 0.03)" : "rgba(0, 0, 0, 0)";
+    const backgroundColor =
+      animationEl.ownerDocument.defaultView.getComputedStyle(animationEl).backgroundColor;
+    is(backgroundColor, expectedBackgroundColor,
+       "The background-color shoud be changed to alternate");
   }
 });
--- a/devtools/client/animationinspector/test/browser_animation_ui_updates_when_animation_data_changes.js
+++ b/devtools/client/animationinspector/test/browser_animation_ui_updates_when_animation_data_changes.js
@@ -12,43 +12,25 @@ requestLongerTimeout(2);
 add_task(function* () {
   yield addTab(URL_ROOT + "doc_simple_animation.html");
   let {panel, controller, inspector} = yield openAnimationInspector();
 
   info("Select the test node");
   yield selectNodeAndWaitForAnimations(".animated", inspector);
 
   let animation = controller.animationPlayers[0];
-  yield setStyle(animation, panel, "animationDuration", "5.5s");
-  yield setStyle(animation, panel, "animationIterationCount", "300");
-  yield setStyle(animation, panel, "animationDelay", "45s");
+  yield setStyle(animation, panel, "animationDuration", "5.5s", ".animated");
+  yield setStyle(animation, panel, "animationIterationCount", "300", ".animated");
+  yield setStyle(animation, panel, "animationDelay", "45s", ".animated");
 
   let animationsEl = panel.animationsTimelineComponent.animationsEl;
   let timeBlockEl = animationsEl.querySelector(".time-block");
 
   // 45s delay + (300 * 5.5)s duration
   let expectedTotalDuration = 1695 * 1000;
 
   // XXX: the nb and size of each iteration cannot be tested easily (displayed
   // using a linear-gradient background and capped at 2px wide). They should
   // be tested in bug 1173761.
   let delayWidth = parseFloat(timeBlockEl.querySelector(".delay").style.width);
   is(Math.round(delayWidth * expectedTotalDuration / 100), 45 * 1000,
     "The timeline has the right delay");
 });
-
-function* setStyle(animation, panel, name, value) {
-  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;
-  yield waitForAnimationSelecting(panel);
-
-  // 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);
-}
new file mode 100644
--- /dev/null
+++ b/devtools/client/animationinspector/test/doc_delayed_starttime_animations.html
@@ -0,0 +1,33 @@
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8">
+    <style>
+    div {
+      width: 100px;
+      height: 100px;
+      border: 1px solid gray;
+    }
+
+    #target1 {
+      animation: anim 100s;
+    }
+
+    @keyframes anim {
+      from {
+        transform: translate(-50%, 100%);
+      }
+      to {
+        transform: translateX(-50%);
+      }
+    }
+    </style>
+  </head>
+  <body>
+    <div id="target1"></div>
+    <div id="target2"></div>
+    <div id="target3"></div>
+    <div id="target4"></div>
+    <div id="target5"></div>
+  </body>
+</html>
new file mode 100644
--- /dev/null
+++ b/devtools/client/animationinspector/test/doc_multiple_property_types.html
@@ -0,0 +1,95 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+  <meta charset="UTF-8">
+  <style>
+  div {
+    width: 50px;
+    height: 50px;
+  }
+  </style>
+</head>
+<body>
+  <div id=target1>1</div>
+  <div id=target2>2</div>
+  <div id=target3>3</div>
+  <div id=target4>4</div>
+  <div id=target5>5</div>
+
+  <script>
+  "use strict";
+
+  const timing = {
+    duration: 1000,
+    fill: "forwards"
+  };
+
+  document.querySelector("#target1").animate(
+    [{ backgroundColor: "red",
+       fontSize: "10px",
+       marginLeft: "0px",
+       opacity: 0,
+       textAlign: "right",
+       transform: "translate(0px)" },
+    { backgroundColor: "lime",
+      fontSize: "20px",
+      marginLeft: "100px",
+      opacity: 1,
+      textAlign: "center",
+      transform: "translate(100px)" }], timing).pause();
+
+  document.querySelector("#target2").animate(
+    [{ backgroundColor: "lime",
+       fontSize: "20px",
+       marginLeft: "100px",
+       opacity: 1,
+       textAlign: "center",
+       transform: "translate(100px)" },
+    { backgroundColor: "red",
+      fontSize: "10px",
+      marginLeft: "0px",
+      opacity: 0,
+      textAlign: "right",
+      transform: "translate(0px)" }], timing).pause();
+
+  document.querySelector("#target3").animate(
+    [{ backgroundColor: "red",
+       fontSize: "10px",
+       marginLeft: "0px",
+       opacity: 0,
+       textAlign: "right",
+       transform: "translate(0px)" },
+    { backgroundColor: "blue",
+      fontSize: "20px",
+      marginLeft: "100px",
+      opacity: 1,
+      textAlign: "center",
+      transform: "translate(100px)" },
+    { backgroundColor: "lime",
+      fontSize: "10px",
+      marginLeft: "0px",
+      opacity: 0,
+      textAlign: "right",
+      transform: "translate(0px)" }], timing).pause();
+
+  document.querySelector("#target4").animate(
+    [{ backgroundColor: "red",
+       fontSize: "10px",
+       marginLeft: "0px",
+       opacity: 0,
+       textAlign: "right",
+       transform: "translate(0px)",
+       easing: "steps(2)" },
+    { backgroundColor: "lime",
+      fontSize: "20px",
+      marginLeft: "100px",
+      opacity: 1,
+      textAlign: "center",
+      transform: "translate(100px)" }], timing).pause();
+
+  timing.easing = "steps(2)";
+  document.querySelector("#target5").animate(
+    [{ opacity: 0 }, { opacity: 1 }], timing).pause();
+  </script>
+</body>
+</html>
--- a/devtools/client/animationinspector/test/head.js
+++ b/devtools/client/animationinspector/test/head.js
@@ -364,16 +364,39 @@ function* changeTimelinePlaybackRate(pan
  * Wait for animation selecting.
  * @param {AnimationsPanel} panel
  */
 function* waitForAnimationSelecting(panel) {
   yield panel.animationsTimelineComponent.once("animation-selected");
 }
 
 /**
+   + * Click the timeline header to update the animation current time.
+   + * @param {AnimationsPanel} panel
+   + * @param {Number} x position rate on timeline header.
+   + *                 This method calculates
+   + *                 `position * offsetWidth + offsetLeft of timeline header`
+   + *                 as the clientX of MouseEvent.
+   + *                 This parameter should be from 0.0 to 1.0.
+   + */
+function* clickOnTimelineHeader(panel, position) {
+  const timeline = panel.animationsTimelineComponent;
+  const onTimelineDataChanged = timeline.once("timeline-data-changed");
+
+  const header = timeline.timeHeaderEl;
+  const clientX = header.offsetLeft + header.offsetWidth * position;
+  EventUtils.sendMouseEvent({ type: "mousedown", clientX: clientX },
+                            header, header.ownerDocument.defaultView);
+  info(`Click at (${ clientX }, 0) on timeline header`);
+  EventUtils.sendMouseEvent({ type: "mouseup", clientX: clientX }, header,
+                            header.ownerDocument.defaultView);
+  return yield onTimelineDataChanged;
+}
+
+/**
  * Prevent the toolbox common highlighter from making backend requests.
  * @param {Toolbox} toolbox
  */
 function disableHighlighter(toolbox) {
   toolbox._highlighter = {
     showBoxModel: () => new Promise(r => r()),
     hideBoxModel: () => new Promise(r => r()),
     pick: () => new Promise(r => r()),
@@ -427,8 +450,35 @@ function getKeyframeComponent(panel, pro
  * @param {Index} keyframeIndex The index of the keyframe.
  * @return {DOMNode} The keyframe element.
  */
 function getKeyframeEl(panel, propertyName, keyframeIndex) {
   let keyframeComponent = getKeyframeComponent(panel, propertyName);
   return keyframeComponent.keyframesEl
                           .querySelectorAll(".frame")[keyframeIndex];
 }
+
+/**
+ * Set style to test document.
+ * @param {Animation} animation - animation object.
+ * @param {AnimationsPanel} panel - The panel instance.
+ * @param {String} name - property name.
+ * @param {String} value - property value.
+ * @param {String} selector - selector for test document.
+ */
+function* setStyle(animation, panel, name, value, selector) {
+  info("Change the animation style via the content DOM. Setting " +
+       name + " to " + value + " of " + selector);
+
+  const onAnimationChanged = animation ? once(animation, "changed") : Promise.resolve();
+  yield executeInContent("devtools:test:setStyle", {
+    selector: selector,
+    propertyName: name,
+    propertyValue: value
+  });
+  yield onAnimationChanged;
+  const onSelected = animation ? waitForAnimationSelecting(panel) : Promise.resolve();
+  yield onSelected;
+
+  // 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);
+}