--- 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;
+}