Bug 1416106 - Part 12: Add tests. r?gl draft
authorDaisuke Akatsuka <dakatsuka@mozilla.com>
Thu, 15 Feb 2018 13:29:12 +0900
changeset 755296 007e43e02fa7705d775bf09af43930c7aeb43359
parent 755236 7b245fa7c2ec0d7e1dbad53ba8fe83a74adc4781
child 755297 b2b19b960bb0a6c032d8caabd06811dc5a24eeb5
push id99144
push userbmo:dakatsuka@mozilla.com
push dateThu, 15 Feb 2018 04:31:26 +0000
reviewersgl
bugs1416106
milestone60.0a1
Bug 1416106 - Part 12: Add tests. r?gl MozReview-Commit-ID: 6kYdxqwPeKF
devtools/client/inspector/animation/test/browser.ini
devtools/client/inspector/animation/test/browser_animation_keyframes-graph_computed-value-path.js
devtools/client/inspector/animation/test/browser_animation_keyframes-graph_computed-value-path_easing-hint.js
devtools/client/inspector/animation/test/browser_animation_keyframes-graph_keyframe-marker.js
devtools/client/inspector/animation/test/doc_multi_easings.html
devtools/client/inspector/animation/test/doc_multi_keyframes.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,14 @@
 [DEFAULT]
 tags = devtools
 subsuite = devtools
 support-files =
+  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
   !/devtools/client/shared/test/test-actor-registry.js
   !/devtools/client/shared/test/test-actor.js
@@ -16,16 +18,19 @@ support-files =
 [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_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_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_keyframes-graph_computed-value-path.js
@@ -0,0 +1,461 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test for following ComputedValuePath component:
+// * element existence
+// * path segments
+// * fill color by animation type
+// * stop color if the animation type is color
+
+const TEST_DATA = [
+  {
+    targetName: "multi-types",
+    properties: [
+      {
+        name: "background-color",
+        computedValuePathClass: "color-path",
+        expectedPathSegments: [
+          { x: 0, y: 0 },
+          { x: 0, y: 100 },
+          { x: 1000, y: 100 },
+        ],
+        expectedStopColors: [
+          { offset: 0, color: "rgb(255, 0, 0)" },
+          { offset: 1, color: "rgb(0, 255, 0)" },
+        ]
+      },
+      {
+        name: "background-repeat",
+        computedValuePathClass: "discrete-path",
+        expectedPathSegments: [
+          { x: 0, y: 0 },
+          { x: 499.999, y: 0 },
+          { x: 500, y: 100 },
+          { x: 1000, y: 100 },
+        ],
+      },
+      {
+        name: "font-size",
+        computedValuePathClass: "distance-path",
+        expectedPathSegments: [
+          { x: 0, y: 0 },
+          { x: 500, y: 50 },
+          { x: 1000, y: 100 },
+        ],
+      },
+      {
+        name: "margin-left",
+        computedValuePathClass: "distance-path",
+        expectedPathSegments: [
+          { x: 0, y: 0 },
+          { x: 500, y: 50 },
+          { x: 1000, y: 100 },
+        ],
+      },
+      {
+        name: "opacity",
+        computedValuePathClass: "distance-path",
+        expectedPathSegments: [
+          { x: 0, y: 0 },
+          { x: 500, y: 50 },
+          { x: 1000, y: 100 },
+        ],
+      },
+      {
+        name: "text-align",
+        computedValuePathClass: "discrete-path",
+        expectedPathSegments: [
+          { x: 0, y: 0 },
+          { x: 499.999, y: 0 },
+          { x: 500, y: 100 },
+          { x: 1000, y: 100 },
+        ],
+      },
+      {
+        name: "transform",
+        computedValuePathClass: "distance-path",
+        expectedPathSegments: [
+          { x: 0, y: 0 },
+          { x: 500, y: 50 },
+          { x: 1000, y: 100 },
+        ],
+      },
+    ],
+  },
+  {
+    targetName: "multi-types-reverse",
+    properties: [
+      {
+        name: "background-color",
+        computedValuePathClass: "color-path",
+        expectedPathSegments: [
+          { x: 0, y: 0 },
+          { x: 0, y: 100 },
+          { x: 1000, y: 100 },
+        ],
+        expectedStopColors: [
+          { offset: 0, color: "rgb(0, 255, 0)" },
+          { offset: 1, color: "rgb(255, 0, 0)" },
+        ]
+      },
+      {
+        name: "background-repeat",
+        computedValuePathClass: "discrete-path",
+        expectedPathSegments: [
+          { x: 0, y: 0 },
+          { x: 499.999, y: 0 },
+          { x: 500, y: 100 },
+          { x: 1000, y: 100 },
+        ],
+      },
+      {
+        name: "font-size",
+        computedValuePathClass: "distance-path",
+        expectedPathSegments: [
+          { x: 0, y: 100 },
+          { x: 500, y: 50 },
+          { x: 1000, y: 0 },
+        ],
+      },
+      {
+        name: "margin-left",
+        computedValuePathClass: "distance-path",
+        expectedPathSegments: [
+          { x: 0, y: 100 },
+          { x: 500, y: 50 },
+          { x: 1000, y: 0 },
+        ],
+      },
+      {
+        name: "opacity",
+        computedValuePathClass: "distance-path",
+        expectedPathSegments: [
+          { x: 0, y: 100 },
+          { x: 500, y: 50 },
+          { x: 1000, y: 0 },
+        ],
+      },
+      {
+        name: "text-align",
+        computedValuePathClass: "discrete-path",
+        expectedPathSegments: [
+          { x: 0, y: 0 },
+          { x: 499.999, y: 0 },
+          { x: 500, y: 100 },
+          { x: 1000, y: 100 },
+        ],
+      },
+      {
+        name: "transform",
+        computedValuePathClass: "distance-path",
+        expectedPathSegments: [
+          { x: 0, y: 100 },
+          { x: 500, y: 50 },
+          { x: 1000, y: 0 },
+        ],
+      },
+    ],
+  },
+  {
+    targetName: "middle-keyframe",
+    properties: [
+      {
+        name: "background-color",
+        computedValuePathClass: "color-path",
+        expectedPathSegments: [
+          { x: 0, y: 0 },
+          { x: 0, y: 100 },
+          { x: 500, y: 100 },
+          { x: 1000, y: 100 },
+        ],
+        expectedStopColors: [
+          { offset: 0, color: "rgb(255, 0, 0)" },
+          { offset: 0.5, color: "rgb(0, 0, 255)" },
+          { offset: 1, color: "rgb(0, 255, 0)" },
+        ]
+      },
+      {
+        name: "background-repeat",
+        computedValuePathClass: "discrete-path",
+        expectedPathSegments: [
+          { x: 0, y: 0 },
+          { x: 249.999, y: 0 },
+          { x: 250, y: 100 },
+          { x: 749.999, y: 100 },
+          { x: 750, y: 0 },
+          { x: 1000, y: 0 },
+        ],
+      },
+      {
+        name: "font-size",
+        computedValuePathClass: "distance-path",
+        expectedPathSegments: [
+          { x: 0, y: 0 },
+          { x: 250, y: 50 },
+          { x: 500, y: 100 },
+          { x: 750, y: 50 },
+          { x: 1000, y: 0 },
+        ],
+      },
+      {
+        name: "margin-left",
+        computedValuePathClass: "distance-path",
+        expectedPathSegments: [
+          { x: 0, y: 0 },
+          { x: 250, y: 50 },
+          { x: 500, y: 100 },
+          { x: 750, y: 50 },
+          { x: 1000, y: 0 },
+        ],
+      },
+      {
+        name: "opacity",
+        computedValuePathClass: "distance-path",
+        expectedPathSegments: [
+          { x: 0, y: 0 },
+          { x: 250, y: 50 },
+          { x: 500, y: 100 },
+          { x: 750, y: 50 },
+          { x: 1000, y: 0 },
+        ],
+      },
+      {
+        name: "text-align",
+        computedValuePathClass: "discrete-path",
+        expectedPathSegments: [
+          { x: 0, y: 0 },
+          { x: 249.999, y: 0 },
+          { x: 250, y: 100 },
+          { x: 749.999, y: 100 },
+          { x: 750, y: 0 },
+          { x: 1000, y: 0 },
+        ],
+      },
+      {
+        name: "transform",
+        computedValuePathClass: "distance-path",
+        expectedPathSegments: [
+          { x: 0, y: 0 },
+          { x: 250, y: 50 },
+          { x: 500, y: 100 },
+          { x: 750, y: 50 },
+          { x: 1000, y: 0 },
+        ],
+      },
+    ],
+  },
+  {
+    targetName: "steps-keyframe",
+    properties: [
+      {
+        name: "background-color",
+        computedValuePathClass: "color-path",
+        expectedPathSegments: [
+          { x: 0, y: 0 },
+          { x: 0, y: 100 },
+          { x: 500, y: 100 },
+          { x: 1000, y: 100 },
+        ],
+        expectedStopColors: [
+          { offset: 0, color: "rgb(255, 0, 0)" },
+          { offset: 0.499, color: "rgb(255, 0, 0)" },
+          { offset: 0.5, color: "rgb(128, 128, 0)" },
+          { offset: 0.999, color: "rgb(128, 128, 0)" },
+          { offset: 1, color: "rgb(0, 255, 0)" },
+        ]
+      },
+      {
+        name: "background-repeat",
+        computedValuePathClass: "discrete-path",
+        expectedPathSegments: [
+          { x: 0, y: 0 },
+          { x: 499.999, y: 0 },
+          { x: 500, y: 100 },
+          { x: 1000, y: 100 },
+        ],
+      },
+      {
+        name: "font-size",
+        computedValuePathClass: "distance-path",
+        expectedPathSegments: [
+          { x: 0, y: 0 },
+          { x: 500, y: 0 },
+          { x: 500, y: 50 },
+          { x: 1000, y: 50 },
+          { x: 1000, y: 100 },
+        ],
+      },
+      {
+        name: "margin-left",
+        computedValuePathClass: "distance-path",
+        expectedPathSegments: [
+          { x: 0, y: 0 },
+          { x: 499.999, y: 0 },
+          { x: 500, y: 50 },
+          { x: 999.999, y: 50 },
+          { x: 1000, y: 100 },
+        ],
+      },
+      {
+        name: "opacity",
+        computedValuePathClass: "distance-path",
+        expectedPathSegments: [
+          { x: 0, y: 0 },
+          { x: 499.999, y: 0 },
+          { x: 500, y: 50 },
+          { x: 999.999, y: 50 },
+          { x: 1000, y: 100 },
+        ],
+      },
+      {
+        name: "text-align",
+        computedValuePathClass: "discrete-path",
+        expectedPathSegments: [
+          { x: 0, y: 0 },
+          { x: 499.999, y: 0 },
+          { x: 500, y: 100 },
+          { x: 1000, y: 100 },
+        ],
+      },
+      {
+        name: "transform",
+        computedValuePathClass: "distance-path",
+        expectedPathSegments: [
+          { x: 0, y: 0 },
+          { x: 500, y: 0 },
+          { x: 500, y: 50 },
+          { x: 1000, y: 50 },
+          { x: 1000, y: 100 },
+        ],
+      },
+    ],
+  },
+  {
+    targetName: "steps-effect",
+    properties: [
+      {
+        name: "opacity",
+        computedValuePathClass: "distance-path",
+        expectedPathSegments: [
+          { x: 0, y: 0 },
+          { x: 250, y: 25 },
+          { x: 500, y: 50 },
+          { x: 750, y: 75 },
+          { x: 1000, y: 100 },
+        ],
+      },
+    ],
+  },
+  {
+    targetName: "frames-keyframe",
+    properties: [
+      {
+        name: "opacity",
+        computedValuePathClass: "distance-path",
+        expectedPathSegments: [
+          { x: 0, y: 0 },
+          { x: 199, y: 0 },
+          { x: 200, y: 25 },
+          { x: 399, y: 25 },
+          { x: 400, y: 50 },
+          { x: 599, y: 50 },
+          { x: 600, y: 75 },
+          { x: 799, y: 75 },
+          { x: 800, y: 100 },
+          { x: 1000, y: 100 },
+        ],
+      },
+    ],
+  },
+  {
+    targetName: "narrow-offsets",
+    properties: [
+      {
+        name: "opacity",
+        computedValuePathClass: "distance-path",
+        expectedPathSegments: [
+          { x: 0, y: 0 },
+          { x: 100, y: 100 },
+          { x: 110, y: 100 },
+          { x: 114.9, y: 100 },
+          { x: 115, y: 50 },
+          { x: 129.9, y: 50 },
+          { x: 130, y: 0 },
+          { x: 1000, y: 100 },
+        ],
+      },
+    ],
+  },
+  {
+    targetName: "duplicate-offsets",
+    properties: [
+      {
+        name: "opacity",
+        computedValuePathClass: "distance-path",
+        expectedPathSegments: [
+          { x: 0, y: 100 },
+          { x: 250, y: 100 },
+          { x: 499, y: 100 },
+          { x: 500, y: 100 },
+          { x: 500, y: 0 },
+          { x: 750, y: 50 },
+          { x: 1000, y: 100 },
+        ],
+      },
+    ],
+  },
+];
+
+add_task(async function () {
+  await addTab(URL_ROOT + "doc_multi_keyframes.html");
+
+  const { inspector, panel } = await openAnimationInspector();
+
+  for (const { properties, targetName } of TEST_DATA) {
+    info(`Checking keyframes graph for ${ targetName }`);
+    await selectNodeAndWaitForAnimations(`#${ targetName }`, inspector);
+
+    for (const property of properties) {
+      const {
+        name,
+        computedValuePathClass,
+        expectedPathSegments,
+        expectedStopColors,
+      } = property;
+
+      const testTarget = `${ name } in ${ targetName }`;
+      info(`Checking keyframes graph for ${ testTarget }`);
+      info(`Checking keyframes graph path existence for ${ testTarget }`);
+      const keyframesGraphPathEl = panel.querySelector(`.${ name }`);
+      ok(keyframesGraphPathEl,
+        `The keyframes graph path element of ${ testTarget } should be existence`);
+
+      info(`Checking computed value path existence for ${ testTarget }`);
+      const computedValuePathEl =
+        keyframesGraphPathEl.querySelector(`.${ computedValuePathClass }`);
+      ok(computedValuePathEl,
+        `The computed value path element of ${ testTarget } should be existence`);
+
+      info(`Checking path segments for ${ testTarget }`);
+      const pathEl = computedValuePathEl.querySelector("path");
+      ok(pathEl, `The <path> element of ${ testTarget } should be existence`);
+      assertPathSegments(pathEl, true, expectedPathSegments);
+
+      if (!expectedStopColors) {
+        continue;
+      }
+
+      info(`Checking linearGradient for ${ testTarget }`);
+      const linearGradientEl = computedValuePathEl.querySelector("linearGradient");
+      ok(linearGradientEl,
+        `The <linearGradientEl> element of ${ testTarget } should be existence`);
+
+      for (const expectedStopColor of expectedStopColors) {
+        const { offset, color } = expectedStopColor;
+        assertLinearGradient(linearGradientEl, offset, color);
+      }
+    }
+  }
+});
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/animation/test/browser_animation_keyframes-graph_computed-value-path_easing-hint.js
@@ -0,0 +1,275 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test for following easing hint in ComputedValuePath.
+// * element existence
+// * path segments
+// * hint text
+
+const TEST_DATA = [
+  {
+    targetName: "no-easing",
+    properties: [
+      {
+        name: "opacity",
+        expectedHints: [
+          {
+            hint: "linear",
+            path: [
+              { x: 0, y: 100 },
+              { x: 500, y: 50 },
+              { x: 1000, y: 0 },
+            ],
+          },
+        ],
+      },
+    ],
+  },
+  {
+    targetName: "effect-easing",
+    properties: [
+      {
+        name: "opacity",
+        expectedHints: [
+          {
+            hint: "linear",
+            path: [
+              { x: 0, y: 100 },
+              { x: 500, y: 50 },
+              { x: 1000, y: 0 },
+            ],
+          },
+        ],
+      },
+    ],
+  },
+  {
+    targetName: "keyframe-easing",
+    properties: [
+      {
+        name: "opacity",
+        expectedHints: [
+          {
+            hint: "steps(2)",
+            path: [
+              { x: 0, y: 100 },
+              { x: 499, y: 100 },
+              { x: 500, y: 50 },
+              { x: 999, y: 50 },
+              { x: 1000, y: 0 },
+            ],
+          },
+        ],
+      },
+    ],
+  },
+  {
+    targetName: "both-easing",
+    properties: [
+      {
+        name: "margin-left",
+        expectedHints: [
+          {
+            hint: "steps(1)",
+            path: [
+              { x: 0, y: 0 },
+              { x: 999, y: 0 },
+              { x: 1000, y: 100 },
+            ],
+          },
+        ],
+      },
+      {
+        name: "opacity",
+        expectedHints: [
+          {
+            hint: "steps(2)",
+            path: [
+              { x: 0, y: 100 },
+              { x: 499, y: 100 },
+              { x: 500, y: 50 },
+              { x: 999, y: 50 },
+              { x: 1000, y: 0 },
+            ],
+          },
+        ],
+      },
+    ],
+  },
+  {
+    targetName: "narrow-keyframes",
+    properties: [
+      {
+        name: "opacity",
+        expectedHints: [
+          {
+            hint: "linear",
+            path: [
+              { x: 0, y: 0 },
+              { x: 100, y: 100 },
+            ],
+          },
+          {
+            hint: "steps(1)",
+            path: [
+              { x: 129, y: 100 },
+              { x: 130, y: 0 },
+            ],
+          },
+          {
+            hint: "linear",
+            path: [
+              { x: 130, y: 0 },
+              { x: 1000, y: 100 },
+            ],
+          },
+        ],
+      },
+    ],
+  },
+  {
+    targetName: "duplicate-keyframes",
+    properties: [
+      {
+        name: "opacity",
+        expectedHints: [
+          {
+            hint: "linear",
+            path: [
+              { x: 0, y: 0 },
+              { x: 500, y: 100 },
+            ],
+          },
+          {
+            hint: "",
+            path: [
+              { x: 500, y: 100 },
+              { x: 500, y: 0 },
+            ],
+          },
+          {
+            hint: "steps(1)",
+            path: [
+              { x: 500, y: 0 },
+              { x: 999, y: 0 },
+              { x: 1000, y: 100 },
+            ],
+          },
+        ],
+      },
+    ],
+  },
+  {
+    targetName: "color-keyframes",
+    properties: [
+      {
+        name: "color",
+        expectedHints: [
+          {
+            hint: "ease-in",
+            rect: {
+              x: 0,
+              height: 100,
+              width: 400,
+            },
+          },
+          {
+            hint: "ease-out",
+            rect: {
+              x: 400,
+              height: 100,
+              width: 600,
+            },
+          },
+        ],
+      },
+    ],
+  },
+];
+
+add_task(async function () {
+  await addTab(URL_ROOT + "doc_multi_easings.html");
+
+  const { inspector, panel } = await openAnimationInspector();
+
+  for (const { properties, targetName } of TEST_DATA) {
+    info(`Checking keyframes graph for ${ targetName }`);
+    await selectNodeAndWaitForAnimations(`#${ targetName }`, inspector);
+
+    for (const property of properties) {
+      const {
+        name,
+        expectedHints,
+      } = property;
+
+      const testTarget = `${ name } in ${ targetName }`;
+      info(`Checking easing hint for ${ testTarget }`);
+      info(`Checking easing hint existence for ${ testTarget }`);
+      const hintEls = panel.querySelectorAll(`.${ name } .hint`);
+      is(hintEls.length, expectedHints.length,
+        `Count of easing hint elements of ${ testTarget } `
+        + `should be ${ expectedHints.length }`);
+
+      for (let i = 0; i < expectedHints.length; i++) {
+        const hintTarget = `hint[${ i }] of ${ testTarget }`;
+
+        info(`Checking ${ hintTarget }`);
+        const hintEl = hintEls[i];
+        const expectedHint = expectedHints[i];
+
+        info(`Checking <title> in ${ hintTarget }`);
+        const titleEl = hintEl.querySelector("title");
+        ok(titleEl,
+          `<title> element in ${ hintTarget } should be existence`);
+        is(titleEl.textContent, expectedHint.hint,
+          `Content of <title> in ${ hintTarget } should be ${ expectedHint.hint }`);
+
+        let interactionEl = null;
+        if (expectedHint.path) {
+          info(`Checking <path> in ${ hintTarget }`);
+          interactionEl = hintEl.querySelector("path");
+          ok(interactionEl, `The <path> element  in ${ hintTarget } should be existence`);
+          assertPathSegments(interactionEl, false, expectedHint.path);
+        } else {
+          info(`Checking <rect> in ${ hintTarget }`);
+          interactionEl = hintEl.querySelector("rect");
+          ok(interactionEl, `The <rect> element  in ${ hintTarget } should be existence`);
+          is(interactionEl.getAttribute("x"), expectedHint.rect.x,
+            `x of <rect> in ${ hintTarget } should be ${ expectedHint.rect.x }`);
+          is(interactionEl.getAttribute("width"), expectedHint.rect.width,
+            `width of <rect> in ${ hintTarget } should be ${ expectedHint.rect.width }`);
+        }
+
+        info(`Checking interaction for ${ hintTarget }`);
+        interactionEl.scrollIntoView(false);
+        const win = hintEl.ownerGlobal;
+        // Mouse out once from pathEl.
+        EventUtils.synthesizeMouse(interactionEl, -1, -1, { type: "mouseout" }, win);
+        is(win.getComputedStyle(interactionEl).strokeOpacity, 0,
+          `stroke-opacity of hintEl for ${ hintTarget } should be 0`
+          + " while mouse is out from the element");
+        // Mouse over the pathEl.
+        ok(isStrokeChangedByMouseOver(interactionEl, win),
+          `stroke-opacity of hintEl for ${ hintTarget } should be 1`
+          + " while mouse is over the element");
+      }
+    }
+  }
+});
+
+function isStrokeChangedByMouseOver(pathEl, win) {
+  const boundingBox = pathEl.getBoundingClientRect();
+  const x = boundingBox.width / 2;
+
+  for (let y = 0; y < boundingBox.height; y++) {
+    EventUtils.synthesizeMouse(pathEl, x, y, { type: "mouseover" }, win);
+
+    if (win.getComputedStyle(pathEl).strokeOpacity == 1) {
+      return true;
+    }
+  }
+
+  return false;
+}
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/animation/test/browser_animation_keyframes-graph_keyframe-marker.js
@@ -0,0 +1,176 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test for following keyframe marker.
+// * element existence
+// * title
+// * and left style
+
+const TEST_DATA = [
+  {
+    targetName: "multi-types",
+    properties: [
+      {
+        name: "background-color",
+        expectedValues: [
+          {
+            title: "rgb(255, 0, 0)",
+            left: "0%",
+          },
+          {
+            title: "rgb(0, 255, 0)",
+            left: "100%",
+          }
+        ],
+      },
+      {
+        name: "background-repeat",
+        expectedValues: [
+          {
+            title: "space round",
+            left: "0%",
+          },
+          {
+            title: "round space",
+            left: "100%",
+          }
+        ],
+      },
+      {
+        name: "font-size",
+        expectedValues: [
+          {
+            title: "10px",
+            left: "0%",
+          },
+          {
+            title: "20px",
+            left: "100%",
+          }
+        ],
+      },
+      {
+        name: "margin-left",
+        expectedValues: [
+          {
+            title: "0px",
+            left: "0%",
+          },
+          {
+            title: "100px",
+            left: "100%",
+          }
+        ],
+      },
+      {
+        name: "opacity",
+        expectedValues: [
+          {
+            title: "0",
+            left: "0%",
+          },
+          {
+            title: "1",
+            left: "100%",
+          }
+        ],
+      },
+      {
+        name: "text-align",
+        expectedValues: [
+          {
+            title: "right",
+            left: "0%",
+          },
+          {
+            title: "center",
+            left: "100%",
+          }
+        ],
+      },
+      {
+        name: "transform",
+        expectedValues: [
+          {
+            title: "translate(0px)",
+            left: "0%",
+          },
+          {
+            title: "translate(100px)",
+            left: "100%",
+          }
+        ],
+      },
+    ],
+  },
+  {
+    targetName: "narrow-offsets",
+    properties: [
+      {
+        name: "opacity",
+        expectedValues: [
+          {
+            title: "0",
+            left: "0%",
+          },
+          {
+            title: "1",
+            left: "10%",
+          },
+          {
+            title: "0",
+            left: "13%",
+          },
+          {
+            title: "1",
+            left: "100%",
+          },
+        ],
+      },
+    ],
+  }
+];
+
+add_task(async function () {
+  await addTab(URL_ROOT + "doc_multi_keyframes.html");
+
+  const { inspector, panel } = await openAnimationInspector();
+
+  for (const { properties, targetName } of TEST_DATA) {
+    info(`Checking keyframe marker for ${ targetName }`);
+    await selectNodeAndWaitForAnimations(`#${ targetName }`, inspector);
+
+    for (const property of properties) {
+      const {
+        name,
+        expectedValues,
+      } = property;
+
+      const testTarget = `${ name } in ${ targetName }`;
+      info(`Checking keyframe marker for ${ testTarget }`);
+      info(`Checking keyframe marker existence for ${ testTarget }`);
+      const markerEls = panel.querySelectorAll(`.${ name } .keyframe-marker-item`);
+      is(markerEls.length, expectedValues.length,
+        `Count of keyframe marker elements of ${ testTarget } `
+        + `should be ${ expectedValues.length }`);
+
+      for (let i = 0; i < expectedValues.length; i++) {
+        const hintTarget = `.keyframe-marker-item[${ i }] of ${ testTarget }`;
+
+        info(`Checking ${ hintTarget }`);
+        const markerEl = markerEls[i];
+        const expectedValue = expectedValues[i];
+
+        info(`Checking title in ${ hintTarget }`);
+        is(markerEl.getAttribute("title"), expectedValue.title,
+          `title in ${ hintTarget } should be ${ expectedValue.title }`);
+
+        info(`Checking left style in ${ hintTarget }`);
+        is(markerEl.style.left, expectedValue.left,
+          `left in ${ hintTarget } should be ${ expectedValue.left }`);
+      }
+    }
+  }
+});
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/animation/test/doc_multi_easings.html
@@ -0,0 +1,97 @@
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8">
+    <style>
+    div {
+      background-color: lime;
+      height: 50px;
+    }
+    </style>
+  </head>
+  <body>
+    <script>
+    "use strict";
+
+    function createAnimation(name, keyframes, effectEasing) {
+      const div = document.createElement("div");
+      div.id = name;
+      document.body.appendChild(div);
+
+      const effect = {
+        duration: 100000,
+        fill: "forwards"
+      };
+
+      if (effectEasing) {
+        effect.easing = effectEasing;
+      }
+
+      div.animate(keyframes, effect);
+    }
+
+    createAnimation(
+      "no-easing",
+      [
+        { opacity: 1 },
+        { opacity: 0 },
+      ]
+    );
+
+    createAnimation(
+      "effect-easing",
+      [
+        { opacity: 1 },
+        { opacity: 0 },
+      ],
+      "frames(5)"
+    );
+
+    createAnimation(
+      "keyframe-easing",
+      [
+        { opacity: 1, easing: "steps(2)", },
+        { opacity: 0 },
+      ]
+    );
+
+    createAnimation(
+      "both-easing",
+      [
+        { offset: 0, opacity: 1, easing: "steps(2)", },
+        { offset: 0, marginLeft: "0px", easing: "steps(1)", },
+        { marginLeft: "100px", opacity: 0 },
+      ],
+      "steps(10)"
+    );
+
+    createAnimation(
+      "narrow-keyframes",
+      [
+        { opacity: 0, },
+        { offset: 0.1, opacity: 1, easing: "steps(1)", },
+        { offset: 0.13, opacity: 0, },
+      ]
+    );
+
+    createAnimation(
+      "duplicate-keyframes",
+      [
+        { opacity: 0 },
+        { offset: 0.5, opacity: 1, },
+        { offset: 0.5, opacity: 0, easing: "steps(1)", },
+        { opacity: 1, },
+      ]
+    );
+
+    createAnimation(
+      "color-keyframes",
+      [
+        { color: "red", easing: "ease-in", },
+        { offset: 0.4, color: "blue", easing: "ease-out", },
+        { color: "lime", },
+      ]
+    );
+    </script>
+  </body>
+</html>
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/animation/test/doc_multi_keyframes.html
@@ -0,0 +1,205 @@
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8">
+    <style>
+    div {
+      width: 50px;
+      height: 50px;
+    }
+    </style>
+  </head>
+  <body>
+    <script>
+    "use strict";
+
+    function createAnimation(name, keyframes, effectEasing) {
+      const div = document.createElement("div");
+      div.id = name;
+      document.body.appendChild(div);
+
+      const effect = {
+        duration: 100000,
+        fill: "forwards"
+      };
+
+      if (effectEasing) {
+        effect.easing = effectEasing;
+      }
+
+      div.animate(keyframes, effect);
+    }
+
+    createAnimation(
+      "multi-types",
+      [
+        {
+          backgroundColor: "red",
+          backgroundRepeat: "space round",
+          fontSize: "10px",
+          marginLeft: "0px",
+          opacity: 0,
+          textAlign: "right",
+          transform: "translate(0px)"
+        },
+        {
+          backgroundColor: "lime",
+          backgroundRepeat: "round space",
+          fontSize: "20px",
+          marginLeft: "100px",
+          opacity: 1,
+          textAlign: "center",
+          transform: "translate(100px)"
+        },
+      ]
+    );
+
+    createAnimation(
+      "multi-types-reverse",
+      [
+        {
+          backgroundColor: "lime",
+          backgroundRepeat: "space",
+          fontSize: "20px",
+          marginLeft: "100px",
+          opacity: 1,
+          textAlign: "center",
+          transform: "translate(100px)"
+        },
+        {
+          backgroundColor: "red",
+          backgroundRepeat: "round",
+          fontSize: "10px",
+          marginLeft: "0px",
+          opacity: 0,
+          textAlign: "right",
+          transform: "translate(0px)"
+        },
+      ]
+    );
+
+    createAnimation(
+      "middle-keyframe",
+      [
+        {
+          backgroundColor: "red",
+          backgroundRepeat: "space",
+          fontSize: "10px",
+          marginLeft: "0px",
+          opacity: 0,
+          textAlign: "right",
+          transform: "translate(0px)"
+        },
+        {
+          backgroundColor: "blue",
+          backgroundRepeat: "round",
+          fontSize: "20px",
+          marginLeft: "100px",
+          opacity: 1,
+          textAlign: "center",
+          transform: "translate(100px)"
+        },
+        {
+          backgroundColor: "lime",
+          backgroundRepeat: "space",
+          fontSize: "10px",
+          marginLeft: "0px",
+          opacity: 0,
+          textAlign: "right",
+          transform: "translate(0px)"
+        },
+      ]
+    );
+
+    createAnimation(
+      "steps-keyframe",
+      [
+        {
+          backgroundColor: "red",
+          backgroundRepeat: "space",
+          fontSize: "10px",
+          marginLeft: "0px",
+          opacity: 0,
+          textAlign: "right",
+          transform: "translate(0px)",
+          easing: "steps(2)"
+        },
+        {
+          backgroundColor: "lime",
+          backgroundRepeat: "round",
+          fontSize: "20px",
+          marginLeft: "100px",
+          opacity: 1,
+          textAlign: "center",
+          transform: "translate(100px)"
+        },
+      ]
+    );
+
+    createAnimation(
+      "steps-effect",
+      [
+        {
+          opacity: 0
+        },
+        {
+          opacity: 1
+        },
+      ],
+      "steps(2)"
+    );
+
+    createAnimation(
+      "frames-keyframe",
+      [
+        {
+          easing: "frames(5)",
+          opacity: 0,
+        },
+        {
+          opacity: 1
+        },
+      ]
+    );
+
+    createAnimation(
+      "narrow-offsets",
+      [
+        {
+          opacity: 0,
+        },
+        {
+          opacity: 1,
+          easing: "steps(2)",
+          offset: 0.1,
+        },
+        {
+          opacity: 0,
+          offset: 0.13,
+        },
+      ]
+    );
+
+    createAnimation(
+      "duplicate-offsets",
+      [
+        {
+          opacity: 1,
+        },
+        {
+          opacity: 1,
+          offset: 0.5,
+        },
+        {
+          opacity: 0,
+          offset: 0.5,
+        },
+        {
+          opacity: 1,
+          offset: 1,
+        },
+      ]
+    );
+    </script>
+  </body>
+</html>
--- a/devtools/client/inspector/animation/test/head.js
+++ b/devtools/client/inspector/animation/test/head.js
@@ -204,16 +204,34 @@ const waitForAllAnimationTargets = async
  */
 const waitForAllSummaryGraph = async function (animationInspector) {
   for (let i = 0; i < animationInspector.animations.length; i++) {
     await animationInspector.once("animation-summary-graph-rendered");
   }
 };
 
 /**
+ * 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
+ *        e.g. rgb(0, 0, 255)
+ */
+function assertLinearGradient(linearGradientEl, offset, expectedColor) {
+  const stopEl = findStopElement(linearGradientEl, offset);
+  ok(stopEl, `stop element at offset ${ offset } should exist`);
+  is(stopEl.getAttribute("stop-color"), expectedColor,
+    `stop-color of stop element at offset ${ offset } should be ${ expectedColor }`);
+}
+
+/**
  * SummaryGraph is constructed by <path> element.
  * This function checks the vertex of path segments.
  *
  * @param {Element} pathEl
  *        <path> element.
  * @param {boolean} hasClosePath
  *        Set true if the path shoud be closing.
  * @param {Object} expectedValues
@@ -289,8 +307,28 @@ function findAnimationItemElementsByTarg
 
     if (className === targetClassName) {
       return animationTargetEl.closest(".animation-item");
     }
   }
 
   return null;
 }
+
+/**
+ * Find the <stop> element which has the given offset in the given linearGradientEl.
+ *
+ * @param {Element} linearGradientEl
+ *        <linearGradient> element which has <stop> element.
+ * @param {Number} offset
+ *        Float which represents the "offset" attribute of <stop>.
+ * @return {Element}
+ *         If can't find suitable element, returns null.
+ */
+function findStopElement(linearGradientEl, offset) {
+  for (const stopEl of linearGradientEl.querySelectorAll("stop")) {
+    if (offset <= parseFloat(stopEl.getAttribute("offset"))) {
+      return stopEl;
+    }
+  }
+
+  return null;
+}