summaryrefslogtreecommitdiffstats
path: root/devtools/client/inspector/animation/test
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/client/inspector/animation/test')
-rw-r--r--devtools/client/inspector/animation/test/browser.toml221
-rw-r--r--devtools/client/inspector/animation/test/browser_animation_animated-property-list.js52
-rw-r--r--devtools/client/inspector/animation/test/browser_animation_animated-property-list_unchanged-items.js58
-rw-r--r--devtools/client/inspector/animation/test/browser_animation_animated-property-name.js127
-rw-r--r--devtools/client/inspector/animation/test/browser_animation_animation-detail_close-button.js27
-rw-r--r--devtools/client/inspector/animation/test/browser_animation_animation-detail_title.js44
-rw-r--r--devtools/client/inspector/animation/test/browser_animation_animation-detail_visibility.js52
-rw-r--r--devtools/client/inspector/animation/test/browser_animation_animation-list.js36
-rw-r--r--devtools/client/inspector/animation/test/browser_animation_animation-list_one-animation-select.js30
-rw-r--r--devtools/client/inspector/animation/test/browser_animation_animation-list_select.js38
-rw-r--r--devtools/client/inspector/animation/test/browser_animation_animation-target.js61
-rw-r--r--devtools/client/inspector/animation/test/browser_animation_animation-target_highlight.js118
-rw-r--r--devtools/client/inspector/animation/test/browser_animation_animation-target_select.js64
-rw-r--r--devtools/client/inspector/animation/test/browser_animation_animation-timeline-tick.js109
-rw-r--r--devtools/client/inspector/animation/test/browser_animation_css-transition-with-playstate-idle.js81
-rw-r--r--devtools/client/inspector/animation/test/browser_animation_current-time-label.js73
-rw-r--r--devtools/client/inspector/animation/test/browser_animation_current-time-scrubber-rtl.js18
-rw-r--r--devtools/client/inspector/animation/test/browser_animation_current-time-scrubber-with-negative-delay.js48
-rw-r--r--devtools/client/inspector/animation/test/browser_animation_current-time-scrubber.js14
-rw-r--r--devtools/client/inspector/animation/test/browser_animation_current-time-scrubber_each-different-creation-time-animations.js34
-rw-r--r--devtools/client/inspector/animation/test/browser_animation_empty_on_invalid_nodes.js49
-rw-r--r--devtools/client/inspector/animation/test/browser_animation_fission_switch-target.js99
-rw-r--r--devtools/client/inspector/animation/test/browser_animation_indication-bar.js42
-rw-r--r--devtools/client/inspector/animation/test/browser_animation_infinity-duration_current-time-scrubber.js40
-rw-r--r--devtools/client/inspector/animation/test/browser_animation_infinity-duration_summary-graph.js147
-rw-r--r--devtools/client/inspector/animation/test/browser_animation_infinity-duration_tick-label.js29
-rw-r--r--devtools/client/inspector/animation/test/browser_animation_keyframes-graph_computed-value-path-01.js164
-rw-r--r--devtools/client/inspector/animation/test/browser_animation_keyframes-graph_computed-value-path-02.js90
-rw-r--r--devtools/client/inspector/animation/test/browser_animation_keyframes-graph_computed-value-path-03.js190
-rw-r--r--devtools/client/inspector/animation/test/browser_animation_keyframes-graph_computed-value-path_easing-hint.js380
-rw-r--r--devtools/client/inspector/animation/test/browser_animation_keyframes-graph_keyframe-marker-rtl.js14
-rw-r--r--devtools/client/inspector/animation/test/browser_animation_keyframes-graph_keyframe-marker.js13
-rw-r--r--devtools/client/inspector/animation/test/browser_animation_keyframes-graph_special-colors.js40
-rw-r--r--devtools/client/inspector/animation/test/browser_animation_keyframes-progress-bar.js103
-rw-r--r--devtools/client/inspector/animation/test/browser_animation_keyframes-progress-bar_after-resuming.js64
-rw-r--r--devtools/client/inspector/animation/test/browser_animation_logic_adjust-time-with-playback-rate.js50
-rw-r--r--devtools/client/inspector/animation/test/browser_animation_logic_adjust-time.js50
-rw-r--r--devtools/client/inspector/animation/test/browser_animation_logic_auto-stop.js94
-rw-r--r--devtools/client/inspector/animation/test/browser_animation_logic_avoid-updating-during-hiding.js92
-rw-r--r--devtools/client/inspector/animation/test/browser_animation_logic_created-time.js57
-rw-r--r--devtools/client/inspector/animation/test/browser_animation_logic_mutations.js113
-rw-r--r--devtools/client/inspector/animation/test/browser_animation_logic_mutations_add_remove_immediately.js29
-rw-r--r--devtools/client/inspector/animation/test/browser_animation_logic_mutations_fast.js24
-rw-r--r--devtools/client/inspector/animation/test/browser_animation_logic_mutations_properties.js103
-rw-r--r--devtools/client/inspector/animation/test/browser_animation_logic_overflowed_delay_end-delay.js29
-rw-r--r--devtools/client/inspector/animation/test/browser_animation_logic_scroll-amount.js70
-rw-r--r--devtools/client/inspector/animation/test/browser_animation_pause-resume-button.js41
-rw-r--r--devtools/client/inspector/animation/test/browser_animation_pause-resume-button_end-time.js79
-rw-r--r--devtools/client/inspector/animation/test/browser_animation_pause-resume-button_respectively.js96
-rw-r--r--devtools/client/inspector/animation/test/browser_animation_pause-resume-button_spacebar.js49
-rw-r--r--devtools/client/inspector/animation/test/browser_animation_playback-rate-selector.js81
-rw-r--r--devtools/client/inspector/animation/test/browser_animation_pseudo-element.js129
-rw-r--r--devtools/client/inspector/animation/test/browser_animation_rewind-button.js33
-rw-r--r--devtools/client/inspector/animation/test/browser_animation_short-duration.js43
-rw-r--r--devtools/client/inspector/animation/test/browser_animation_summary-graph_animation-name.js63
-rw-r--r--devtools/client/inspector/animation/test/browser_animation_summary-graph_compositor.js121
-rw-r--r--devtools/client/inspector/animation/test/browser_animation_summary-graph_computed-timing-path_1.js208
-rw-r--r--devtools/client/inspector/animation/test/browser_animation_summary-graph_computed-timing-path_2.js192
-rw-r--r--devtools/client/inspector/animation/test/browser_animation_summary-graph_computed-timing-path_different-timescale.js43
-rw-r--r--devtools/client/inspector/animation/test/browser_animation_summary-graph_delay-sign-rtl.js14
-rw-r--r--devtools/client/inspector/animation/test/browser_animation_summary-graph_delay-sign.js13
-rw-r--r--devtools/client/inspector/animation/test/browser_animation_summary-graph_effect-timing-path.js64
-rw-r--r--devtools/client/inspector/animation/test/browser_animation_summary-graph_end-delay-sign-rtl.js15
-rw-r--r--devtools/client/inspector/animation/test/browser_animation_summary-graph_end-delay-sign.js13
-rw-r--r--devtools/client/inspector/animation/test/browser_animation_summary-graph_layout-by-seek.js150
-rw-r--r--devtools/client/inspector/animation/test/browser_animation_summary-graph_negative-delay-path.js128
-rw-r--r--devtools/client/inspector/animation/test/browser_animation_summary-graph_negative-end-delay-path.js56
-rw-r--r--devtools/client/inspector/animation/test/browser_animation_summary-graph_tooltip.js294
-rw-r--r--devtools/client/inspector/animation/test/browser_animation_timing_negative-playback-rate_current-time-scrubber.js47
-rw-r--r--devtools/client/inspector/animation/test/browser_animation_timing_negative-playback-rate_summary-graph.js235
-rw-r--r--devtools/client/inspector/animation/test/current-time-scrubber_head.js101
-rw-r--r--devtools/client/inspector/animation/test/doc_custom_playback_rate.html30
-rw-r--r--devtools/client/inspector/animation/test/doc_infinity_duration.html41
-rw-r--r--devtools/client/inspector/animation/test/doc_multi_easings.html121
-rw-r--r--devtools/client/inspector/animation/test/doc_multi_keyframes.html229
-rw-r--r--devtools/client/inspector/animation/test/doc_multi_timings.html169
-rw-r--r--devtools/client/inspector/animation/test/doc_mutations_add_remove_immediately.html22
-rw-r--r--devtools/client/inspector/animation/test/doc_mutations_fast.html53
-rw-r--r--devtools/client/inspector/animation/test/doc_negative_playback_rate.html38
-rw-r--r--devtools/client/inspector/animation/test/doc_overflowed_delay_end_delay.html75
-rw-r--r--devtools/client/inspector/animation/test/doc_pseudo.html91
-rw-r--r--devtools/client/inspector/animation/test/doc_short_duration.html26
-rw-r--r--devtools/client/inspector/animation/test/doc_simple_animation.html193
-rw-r--r--devtools/client/inspector/animation/test/doc_special_colors.html28
-rw-r--r--devtools/client/inspector/animation/test/head.js1028
-rw-r--r--devtools/client/inspector/animation/test/keyframes-graph_keyframe-marker_head.js237
-rw-r--r--devtools/client/inspector/animation/test/summary-graph_computed-timing-path_head.js103
-rw-r--r--devtools/client/inspector/animation/test/summary-graph_delay-sign_head.js106
-rw-r--r--devtools/client/inspector/animation/test/summary-graph_end-delay-sign_head.js100
89 files changed, 8478 insertions, 0 deletions
diff --git a/devtools/client/inspector/animation/test/browser.toml b/devtools/client/inspector/animation/test/browser.toml
new file mode 100644
index 0000000000..a13e259df3
--- /dev/null
+++ b/devtools/client/inspector/animation/test/browser.toml
@@ -0,0 +1,221 @@
+[DEFAULT]
+prefs = [
+ "dom.svg.pathSeg.enabled=true",
+ "layout.css.properties-and-values.enabled=true"
+]
+tags = "devtools"
+subsuite = "devtools"
+support-files = [
+ "current-time-scrubber_head.js",
+ "doc_custom_playback_rate.html",
+ "doc_infinity_duration.html",
+ "doc_multi_easings.html",
+ "doc_multi_keyframes.html",
+ "doc_multi_timings.html",
+ "doc_mutations_add_remove_immediately.html",
+ "doc_mutations_fast.html",
+ "doc_negative_playback_rate.html",
+ "doc_overflowed_delay_end_delay.html",
+ "doc_pseudo.html",
+ "doc_short_duration.html",
+ "doc_simple_animation.html",
+ "doc_special_colors.html",
+ "head.js",
+ "keyframes-graph_keyframe-marker_head.js",
+ "summary-graph_computed-timing-path_head.js",
+ "summary-graph_delay-sign_head.js",
+ "summary-graph_end-delay-sign_head.js",
+ "!/devtools/client/inspector/test/head.js",
+ "!/devtools/client/inspector/test/shared-head.js",
+ "!/devtools/client/shared/test/shared-head.js",
+ "!/devtools/client/shared/test/telemetry-test-helpers.js",
+ "!/devtools/client/shared/test/highlighter-test-actor.js",
+]
+
+["browser_animation_animated-property-list.js"]
+fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled
+
+["browser_animation_animated-property-list_unchanged-items.js"]
+
+["browser_animation_animated-property-name.js"]
+
+["browser_animation_animation-detail_close-button.js"]
+fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled
+
+["browser_animation_animation-detail_title.js"]
+fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled
+
+["browser_animation_animation-detail_visibility.js"]
+fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled
+
+["browser_animation_animation-list.js"]
+
+["browser_animation_animation-list_one-animation-select.js"]
+fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled
+
+["browser_animation_animation-list_select.js"]
+fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled
+
+["browser_animation_animation-target.js"]
+
+["browser_animation_animation-target_highlight.js"]
+fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled
+skip-if = [
+ "apple_catalina && !debug", # Disabled in Bug 1713158. Intemittent bug: Bug 1665011
+ "os == 'linux' && !debug && !asan && !swgl && !ccov", # Bug 1665011
+ "win11_2009", # Bug 1798331
+ "a11y_checks && debug", # Bugs 1849028 and 1858041 for causing intermittent test results
+]
+
+["browser_animation_animation-target_select.js"]
+fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled
+
+["browser_animation_animation-timeline-tick.js"]
+
+["browser_animation_css-transition-with-playstate-idle.js"]
+
+["browser_animation_current-time-label.js"]
+fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled
+
+["browser_animation_current-time-scrubber-rtl.js"]
+fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled
+skip-if = ["os == 'linux' && debug"] # Bug 1721716
+
+["browser_animation_current-time-scrubber-with-negative-delay.js"]
+fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled
+
+["browser_animation_current-time-scrubber.js"]
+fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled
+
+["browser_animation_current-time-scrubber_each-different-creation-time-animations.js"]
+fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled
+
+["browser_animation_empty_on_invalid_nodes.js"]
+
+["browser_animation_fission_switch-target.js"]
+
+["browser_animation_indication-bar.js"]
+skip-if = ["a11y_checks"] # Bugs 1858041 and 1849028 for causing intermittent a11y_checks results
+
+["browser_animation_infinity-duration_current-time-scrubber.js"]
+fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled
+
+["browser_animation_infinity-duration_summary-graph.js"]
+
+["browser_animation_infinity-duration_tick-label.js"]
+
+["browser_animation_keyframes-graph_computed-value-path-01.js"]
+fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled
+
+["browser_animation_keyframes-graph_computed-value-path-02.js"]
+fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled
+
+["browser_animation_keyframes-graph_computed-value-path-03.js"]
+fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled
+
+["browser_animation_keyframes-graph_computed-value-path_easing-hint.js"]
+fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled
+skip-if = ["verify && !debug"]
+
+["browser_animation_keyframes-graph_keyframe-marker-rtl.js"]
+fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled
+
+["browser_animation_keyframes-graph_keyframe-marker.js"]
+fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled
+
+["browser_animation_keyframes-graph_special-colors.js"]
+
+["browser_animation_keyframes-progress-bar.js"]
+fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled
+skip-if = ["os == 'win' && ccov"] # Bug 1490981
+
+["browser_animation_keyframes-progress-bar_after-resuming.js"]
+fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled
+
+["browser_animation_logic_adjust-time-with-playback-rate.js"]
+fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled
+
+["browser_animation_logic_adjust-time.js"]
+fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled
+
+["browser_animation_logic_auto-stop.js"]
+fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled
+
+["browser_animation_logic_avoid-updating-during-hiding.js"]
+
+["browser_animation_logic_created-time.js"]
+
+["browser_animation_logic_mutations.js"]
+
+["browser_animation_logic_mutations_add_remove_immediately.js"]
+
+["browser_animation_logic_mutations_fast.js"]
+skip-if = [
+ "debug",
+ "win11_2009' && bits == 32", # Bug 1567800
+ "os == 'linux' && !asan && !debug && !swgl && !ccov", # Bug 1567800
+]
+
+["browser_animation_logic_mutations_properties.js"]
+
+["browser_animation_logic_overflowed_delay_end-delay.js"]
+skip-if = ["debug"] #bug 1480027
+
+["browser_animation_logic_scroll-amount.js"]
+fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled
+
+["browser_animation_pause-resume-button.js"]
+
+["browser_animation_pause-resume-button_end-time.js"]
+fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled
+skip-if = ["os == 'linux' && bits == 64 && debug"] # Bug 1767699
+
+["browser_animation_pause-resume-button_respectively.js"]
+
+["browser_animation_pause-resume-button_spacebar.js"]
+fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled
+
+["browser_animation_playback-rate-selector.js"]
+
+["browser_animation_pseudo-element.js"]
+fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled
+
+["browser_animation_rewind-button.js"]
+fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled
+
+["browser_animation_short-duration.js"]
+
+["browser_animation_summary-graph_animation-name.js"]
+
+["browser_animation_summary-graph_compositor.js"]
+fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled
+
+["browser_animation_summary-graph_computed-timing-path_1.js"]
+
+["browser_animation_summary-graph_computed-timing-path_2.js"]
+
+["browser_animation_summary-graph_computed-timing-path_different-timescale.js"]
+
+["browser_animation_summary-graph_delay-sign-rtl.js"]
+
+["browser_animation_summary-graph_delay-sign.js"]
+
+["browser_animation_summary-graph_effect-timing-path.js"]
+
+["browser_animation_summary-graph_end-delay-sign-rtl.js"]
+
+["browser_animation_summary-graph_end-delay-sign.js"]
+
+["browser_animation_summary-graph_layout-by-seek.js"]
+fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled
+
+["browser_animation_summary-graph_negative-delay-path.js"]
+
+["browser_animation_summary-graph_negative-end-delay-path.js"]
+
+["browser_animation_summary-graph_tooltip.js"]
+
+["browser_animation_timing_negative-playback-rate_current-time-scrubber.js"]
+fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled
+
+["browser_animation_timing_negative-playback-rate_summary-graph.js"]
diff --git a/devtools/client/inspector/animation/test/browser_animation_animated-property-list.js b/devtools/client/inspector/animation/test/browser_animation_animated-property-list.js
new file mode 100644
index 0000000000..47661d00fc
--- /dev/null
+++ b/devtools/client/inspector/animation/test/browser_animation_animated-property-list.js
@@ -0,0 +1,52 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test following animated property list test.
+// 1. Existence for animated property list.
+// 2. Number of animated property item.
+
+const TEST_DATA = [
+ {
+ targetClass: "animated",
+ expectedNumber: 1,
+ },
+ {
+ targetClass: "compositor-notall",
+ expectedNumber: 4,
+ },
+];
+
+add_task(async function () {
+ await addTab(URL_ROOT + "doc_simple_animation.html");
+ await removeAnimatedElementsExcept(TEST_DATA.map(t => `.${t.targetClass}`));
+ const { animationInspector, panel } = await openAnimationInspector();
+
+ info("Checking animated property list and items existence at initial");
+ ok(
+ !panel.querySelector(".animated-property-list"),
+ "The animated-property-list should not be in the DOM at initial"
+ );
+
+ for (const { targetClass, expectedNumber } of TEST_DATA) {
+ info(
+ `Checking animated-property-list and items existence at ${targetClass}`
+ );
+ await clickOnAnimationByTargetSelector(
+ animationInspector,
+ panel,
+ `.${targetClass}`
+ );
+
+ await waitUntil(
+ () =>
+ panel.querySelectorAll(".animated-property-item").length ===
+ expectedNumber
+ );
+ ok(
+ true,
+ `The number of animated-property-list should be ${expectedNumber} at ${targetClass}`
+ );
+ }
+});
diff --git a/devtools/client/inspector/animation/test/browser_animation_animated-property-list_unchanged-items.js b/devtools/client/inspector/animation/test/browser_animation_animated-property-list_unchanged-items.js
new file mode 100644
index 0000000000..22b889297f
--- /dev/null
+++ b/devtools/client/inspector/animation/test/browser_animation_animated-property-list_unchanged-items.js
@@ -0,0 +1,58 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test the position and the class of unchanged animated property items.
+
+const TEST_DATA = [
+ { property: "background-color", isUnchanged: false },
+ { property: "padding-left", isUnchanged: false },
+ { property: "background-attachment", isUnchanged: true },
+ { property: "background-clip", isUnchanged: true },
+ { property: "background-image", isUnchanged: true },
+ { property: "background-origin", isUnchanged: true },
+ { property: "background-position-x", isUnchanged: true },
+ { property: "background-position-y", isUnchanged: true },
+ { property: "background-repeat", isUnchanged: true },
+ { property: "background-size", isUnchanged: true },
+ { property: "padding-bottom", isUnchanged: true },
+ { property: "padding-right", isUnchanged: true },
+ { property: "padding-top", isUnchanged: true },
+];
+
+add_task(async function () {
+ await addTab(URL_ROOT + "doc_simple_animation.html");
+ await removeAnimatedElementsExcept([".longhand"]);
+ const { panel } = await openAnimationInspector();
+
+ info("Checking unchanged animated property item");
+ const itemEls = panel.querySelectorAll(".animated-property-item");
+ is(
+ itemEls.length,
+ TEST_DATA.length,
+ `Count of animated property item should be ${TEST_DATA.length}`
+ );
+
+ for (let i = 0; i < TEST_DATA.length; i++) {
+ const { property, isUnchanged } = TEST_DATA[i];
+ const itemEl = itemEls[i];
+
+ ok(
+ itemEl.querySelector(`.keyframes-graph.${property}`),
+ `Item of ${property} should display at here`
+ );
+
+ if (isUnchanged) {
+ ok(
+ itemEl.classList.contains("unchanged"),
+ "Animated property item should have 'unchanged' class"
+ );
+ } else {
+ ok(
+ !itemEl.classList.contains("unchanged"),
+ "Animated property item should not have 'unchanged' class"
+ );
+ }
+ }
+});
diff --git a/devtools/client/inspector/animation/test/browser_animation_animated-property-name.js b/devtools/client/inspector/animation/test/browser_animation_animated-property-name.js
new file mode 100644
index 0000000000..a1505125f6
--- /dev/null
+++ b/devtools/client/inspector/animation/test/browser_animation_animated-property-name.js
@@ -0,0 +1,127 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test the following animated property name component features:
+// * name of property
+// * display compositor sign when the property was running on compositor.
+// * display warning when the property is runnable on compositor but was not.
+
+async function test_element(className, data) {
+ await addTab(URL_ROOT + "doc_simple_animation.html");
+ await removeAnimatedElementsExcept([className]);
+ const { panel } = await openAnimationInspector();
+
+ info("Checking animated property name component");
+ const animatedPropertyNameEls = panel.querySelectorAll(
+ ".animated-property-name"
+ );
+ is(
+ animatedPropertyNameEls.length,
+ data.length,
+ `Number of animated property name elements should be ${data.length}`
+ );
+
+ for (const [
+ index,
+ animatedPropertyNameEl,
+ ] of animatedPropertyNameEls.entries()) {
+ const { property, isOnCompositor, isWarning } = data[index];
+
+ info(`Checking text content for ${property}`);
+
+ const spanEl = animatedPropertyNameEl.querySelector("span");
+ ok(
+ spanEl,
+ `<span> element should be in animated-property-name of ${property}`
+ );
+ is(spanEl.textContent, property, `textContent should be ${property}`);
+
+ info(`Checking compositor sign for ${property}`);
+
+ if (isOnCompositor) {
+ ok(
+ animatedPropertyNameEl.classList.contains("compositor"),
+ "animatedPropertyNameEl should has .compositor class"
+ );
+ isnot(
+ getComputedStyle(spanEl, "::before").width,
+ "auto",
+ "width of ::before pseud should not be auto"
+ );
+ } else {
+ ok(
+ !animatedPropertyNameEl.classList.contains("compositor"),
+ "animatedPropertyNameEl should not have .compositor class"
+ );
+ is(
+ getComputedStyle(spanEl, "::before").width,
+ "auto",
+ "width of ::before pseud should be auto"
+ );
+ }
+
+ info(`Checking warning for ${property}`);
+
+ if (isWarning) {
+ ok(
+ animatedPropertyNameEl.classList.contains("warning"),
+ "animatedPropertyNameEl should has .warning class"
+ );
+ is(
+ getComputedStyle(spanEl).textDecorationStyle,
+ "dotted",
+ "text-decoration-style of spanEl should be 'dotted'"
+ );
+ is(
+ getComputedStyle(spanEl).textDecorationLine,
+ "underline",
+ "text-decoration-line of spanEl should be 'underline'"
+ );
+ } else {
+ ok(
+ !animatedPropertyNameEl.classList.contains("warning"),
+ "animatedPropertyNameEl should not have .warning class"
+ );
+ is(
+ getComputedStyle(spanEl).textDecorationStyle,
+ "solid",
+ "text-decoration-style of spanEl should be 'solid'"
+ );
+ is(
+ getComputedStyle(spanEl).textDecorationLine,
+ "none",
+ "text-decoration-line of spanEl should be 'none'"
+ );
+ }
+ }
+}
+
+add_task(async function compositor_notall() {
+ await test_element(".compositor-notall", [
+ {
+ property: "--ball-color",
+ },
+ {
+ property: "opacity",
+ isOnCompositor: true,
+ },
+ {
+ property: "transform",
+ isOnCompositor: true,
+ },
+ {
+ property: "width",
+ },
+ ]);
+});
+
+add_task(async function compositor_warning() {
+ await test_element(".compositor-warning", [
+ {
+ property: "opacity",
+ isWarning: true,
+ },
+ ]);
+});
diff --git a/devtools/client/inspector/animation/test/browser_animation_animation-detail_close-button.js b/devtools/client/inspector/animation/test/browser_animation_animation-detail_close-button.js
new file mode 100644
index 0000000000..f7fa4cee70
--- /dev/null
+++ b/devtools/client/inspector/animation/test/browser_animation_animation-detail_close-button.js
@@ -0,0 +1,27 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that whether close button in header of animation detail works.
+
+add_task(async function () {
+ await addTab(URL_ROOT + "doc_custom_playback_rate.html");
+ const { animationInspector, panel } = await openAnimationInspector();
+
+ info("Checking close button in header of animation detail");
+ await clickOnAnimation(animationInspector, panel, 0);
+ const detailEl = panel.querySelector("#animation-container .controlled");
+ const win = panel.ownerGlobal;
+ isnot(
+ win.getComputedStyle(detailEl).display,
+ "none",
+ "detailEl should be visibled before clicking close button"
+ );
+ clickOnDetailCloseButton(panel);
+ is(
+ win.getComputedStyle(detailEl).display,
+ "none",
+ "detailEl should be unvisibled after clicking close button"
+ );
+});
diff --git a/devtools/client/inspector/animation/test/browser_animation_animation-detail_title.js b/devtools/client/inspector/animation/test/browser_animation_animation-detail_title.js
new file mode 100644
index 0000000000..91dfd2e50f
--- /dev/null
+++ b/devtools/client/inspector/animation/test/browser_animation_animation-detail_title.js
@@ -0,0 +1,44 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that whether title in header of animations detail.
+
+const TEST_DATA = [
+ {
+ targetClass: "cssanimation-normal",
+ expectedTitle: "cssanimation — CSS Animation",
+ },
+ {
+ targetClass: "delay-positive",
+ expectedTitle: "test-delay-animation — Script Animation",
+ },
+ {
+ targetClass: "easing-step",
+ expectedTitle: "Script Animation",
+ },
+];
+
+add_task(async function () {
+ await addTab(URL_ROOT + "doc_multi_timings.html");
+ await removeAnimatedElementsExcept(TEST_DATA.map(t => `.${t.targetClass}`));
+ const { animationInspector, panel } = await openAnimationInspector();
+
+ info("Checking title in each header of animation detail");
+
+ for (const { targetClass, expectedTitle } of TEST_DATA) {
+ info(`Checking title at ${targetClass}`);
+ await clickOnAnimationByTargetSelector(
+ animationInspector,
+ panel,
+ `.${targetClass}`
+ );
+ const titleEl = panel.querySelector(".animation-detail-title");
+ is(
+ titleEl.textContent,
+ expectedTitle,
+ `Title of "${targetClass}" should be "${expectedTitle}"`
+ );
+ }
+});
diff --git a/devtools/client/inspector/animation/test/browser_animation_animation-detail_visibility.js b/devtools/client/inspector/animation/test/browser_animation_animation-detail_visibility.js
new file mode 100644
index 0000000000..14f406a1a3
--- /dev/null
+++ b/devtools/client/inspector/animation/test/browser_animation_animation-detail_visibility.js
@@ -0,0 +1,52 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that whether animations detail could be displayed if there is selected animation.
+
+add_task(async function () {
+ await addTab(URL_ROOT + "doc_custom_playback_rate.html");
+ const { animationInspector, inspector, panel } =
+ await openAnimationInspector();
+
+ info("Checking animation detail visibility if animation was unselected");
+ const detailEl = panel.querySelector("#animation-container .controlled");
+ ok(detailEl, "The detail pane should be in the DOM");
+ await assertDisplayStyle(detailEl, true, "detailEl should be unvisibled");
+
+ info(
+ "Checking animation detail visibility if animation was selected by click"
+ );
+ await clickOnAnimation(animationInspector, panel, 0);
+ await assertDisplayStyle(detailEl, false, "detailEl should be visibled");
+
+ info(
+ "Checking animation detail visibility when choose node which has animations"
+ );
+ await selectNode("html", inspector);
+ await assertDisplayStyle(
+ detailEl,
+ true,
+ "detailEl should be unvisibled after choose html node"
+ );
+
+ info(
+ "Checking animation detail visibility when choose node which has an animation"
+ );
+ await selectNode("div", inspector);
+ await assertDisplayStyle(
+ detailEl,
+ false,
+ "detailEl should be visibled after choose .cssanimation-normal node"
+ );
+});
+
+async function assertDisplayStyle(detailEl, isNoneExpected, description) {
+ const win = detailEl.ownerGlobal;
+ await waitUntil(() => {
+ const isNone = win.getComputedStyle(detailEl).display === "none";
+ return isNone === isNoneExpected;
+ });
+ ok(true, description);
+}
diff --git a/devtools/client/inspector/animation/test/browser_animation_animation-list.js b/devtools/client/inspector/animation/test/browser_animation_animation-list.js
new file mode 100644
index 0000000000..4f2c4419b3
--- /dev/null
+++ b/devtools/client/inspector/animation/test/browser_animation_animation-list.js
@@ -0,0 +1,36 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that whether animations ui could be displayed
+
+add_task(async function () {
+ await addTab(URL_ROOT + "doc_simple_animation.html");
+ await removeAnimatedElementsExcept([".animated", ".long"]);
+ const { animationInspector, inspector, panel } =
+ await openAnimationInspector();
+
+ info("Checking animation list and items existence");
+ ok(
+ panel.querySelector(".animation-list"),
+ "The animation-list is in the DOM"
+ );
+ is(
+ panel.querySelectorAll(".animation-list .animation-item").length,
+ animationInspector.state.animations.length,
+ "The number of animations displayed matches the number of animations"
+ );
+
+ info(
+ "Checking list and items existence after select a element which has an animation"
+ );
+ await selectNode(".animated", inspector);
+ await waitUntil(
+ () => panel.querySelectorAll(".animation-list .animation-item").length === 1
+ );
+ ok(
+ true,
+ "The number of animations displayed should be 1 for .animated element"
+ );
+});
diff --git a/devtools/client/inspector/animation/test/browser_animation_animation-list_one-animation-select.js b/devtools/client/inspector/animation/test/browser_animation_animation-list_one-animation-select.js
new file mode 100644
index 0000000000..d84750385c
--- /dev/null
+++ b/devtools/client/inspector/animation/test/browser_animation_animation-list_one-animation-select.js
@@ -0,0 +1,30 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test whether the animation item has been selected from first time
+// if count of the animations is one.
+
+add_task(async function () {
+ await addTab(URL_ROOT + "doc_simple_animation.html");
+ await removeAnimatedElementsExcept([".animated"]);
+ const { panel } = await openAnimationInspector();
+
+ info("Checking whether an item element has been selected");
+ is(
+ panel.querySelector(".animation-item").classList.contains("selected"),
+ true,
+ "The animation item should have 'selected' class"
+ );
+
+ info(
+ "Checking whether the element will be unselected after closing the detail pane"
+ );
+ clickOnDetailCloseButton(panel);
+ is(
+ panel.querySelector(".animation-item").classList.contains("selected"),
+ false,
+ "The animation item should not have 'selected' class"
+ );
+});
diff --git a/devtools/client/inspector/animation/test/browser_animation_animation-list_select.js b/devtools/client/inspector/animation/test/browser_animation_animation-list_select.js
new file mode 100644
index 0000000000..0d8901ef46
--- /dev/null
+++ b/devtools/client/inspector/animation/test/browser_animation_animation-list_select.js
@@ -0,0 +1,38 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test whether the animation items in the list were selectable.
+
+add_task(async function () {
+ await addTab(URL_ROOT + "doc_simple_animation.html");
+ await removeAnimatedElementsExcept([".animated", ".long"]);
+ const { animationInspector, panel } = await openAnimationInspector();
+
+ info("Checking whether 1st element will be selected");
+ await clickOnAnimation(animationInspector, panel, 0);
+ assertSelection(panel, [true, false]);
+
+ info("Checking whether 2nd element will be selected");
+ await clickOnAnimation(animationInspector, panel, 1);
+ assertSelection(panel, [false, true]);
+
+ info(
+ "Checking whether all elements will be unselected after closing the detail pane"
+ );
+ clickOnDetailCloseButton(panel);
+ assertSelection(panel, [false, false]);
+});
+
+function assertSelection(panel, expectedResult) {
+ panel.querySelectorAll(".animation-item").forEach((item, index) => {
+ const shouldSelected = expectedResult[index];
+ is(
+ item.classList.contains("selected"),
+ shouldSelected,
+ `Animation item[${index}] should ` +
+ `${shouldSelected ? "" : "not"} have 'selected' class`
+ );
+ });
+}
diff --git a/devtools/client/inspector/animation/test/browser_animation_animation-target.js b/devtools/client/inspector/animation/test/browser_animation_animation-target.js
new file mode 100644
index 0000000000..e38ae18755
--- /dev/null
+++ b/devtools/client/inspector/animation/test/browser_animation_animation-target.js
@@ -0,0 +1,61 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test for following AnimationTarget component works.
+// * element existance
+// * number of elements
+// * content of element
+// * title of inspect icon
+
+const TEST_DATA = [
+ { expectedTextContent: "div.ball.animated" },
+ { expectedTextContent: "div.ball.long" },
+];
+
+add_task(async function () {
+ await addTab(URL_ROOT + "doc_simple_animation.html");
+ await removeAnimatedElementsExcept([".animated", ".long"]);
+ const { animationInspector, panel } = await openAnimationInspector();
+
+ info("Checking the animation target elements existance");
+ const animationItemEls = panel.querySelectorAll(
+ ".animation-list .animation-item"
+ );
+ is(
+ animationItemEls.length,
+ animationInspector.state.animations.length,
+ "Number of animation target element should be same to number of animations " +
+ "that displays"
+ );
+
+ for (let i = 0; i < animationItemEls.length; i++) {
+ const animationItemEl = animationItemEls[i];
+ animationItemEl.scrollIntoView(false);
+ await waitUntil(() => animationItemEl.querySelector(".animation-target"));
+
+ const animationTargetEl =
+ animationItemEl.querySelector(".animation-target");
+ ok(
+ animationTargetEl,
+ "The animation target element should be in each animation item element"
+ );
+
+ info("Checking the content of animation target");
+ const testData = TEST_DATA[i];
+ is(
+ animationTargetEl.textContent,
+ testData.expectedTextContent,
+ "The target element's content is correct"
+ );
+ ok(
+ animationTargetEl.querySelector(".objectBox"),
+ "objectBox is in the page exists"
+ );
+ ok(
+ animationTargetEl.querySelector(".highlight-node").title,
+ INSPECTOR_L10N.getStr("inspector.nodePreview.highlightNodeLabel")
+ );
+ }
+});
diff --git a/devtools/client/inspector/animation/test/browser_animation_animation-target_highlight.js b/devtools/client/inspector/animation/test/browser_animation_animation-target_highlight.js
new file mode 100644
index 0000000000..0ff5b08018
--- /dev/null
+++ b/devtools/client/inspector/animation/test/browser_animation_animation-target_highlight.js
@@ -0,0 +1,118 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test for following highlighting related.
+// * highlight when mouse over on a target node
+// * unhighlight when mouse out from the above element
+// * lock highlighting when click on the inspect icon in animation target component
+// * add 'highlighting' class to animation target component during locking
+// * mouseover locked target node
+// * unlock highlighting when click on the above icon
+// * lock highlighting when click on the other inspect icon
+// * if the locked node has multi animations,
+// the class will add to those animation target as well
+
+add_task(async function () {
+ await addTab(URL_ROOT + "doc_simple_animation.html");
+ await removeAnimatedElementsExcept([".animated", ".multi"]);
+ const { animationInspector, inspector, panel } =
+ await openAnimationInspector();
+ const { waitForHighlighterTypeShown, waitForHighlighterTypeHidden } =
+ getHighlighterTestHelpers(inspector);
+
+ info("Check highlighting when mouse over on a target node");
+ const onHighlight = waitForHighlighterTypeShown(
+ inspector.highlighters.TYPES.BOXMODEL
+ );
+ mouseOverOnTargetNode(animationInspector, panel, 0);
+ let data = await onHighlight;
+ assertNodeFront(data.nodeFront, "DIV", "ball animated");
+
+ info("Check unhighlighting when mouse out on a target node");
+ const onUnhighlight = waitForHighlighterTypeHidden(
+ inspector.highlighters.TYPES.BOXMODEL
+ );
+ mouseOutOnTargetNode(animationInspector, panel, 0);
+ await onUnhighlight;
+ ok(true, "Unhighlighted the targe node");
+
+ info("Check node is highlighted when the inspect icon is clicked");
+ const onHighlighterShown = waitForHighlighterTypeShown(
+ inspector.highlighters.TYPES.BOXMODEL
+ );
+ await clickOnInspectIcon(animationInspector, panel, 0);
+ data = await onHighlighterShown;
+ assertNodeFront(data.nodeFront, "DIV", "ball animated");
+ await assertHighlight(panel, 0, true);
+
+ info("Check if the animation target is still highlighted on mouse out");
+ mouseOutOnTargetNode(animationInspector, panel, 0);
+ await wait(500);
+ await assertHighlight(panel, 0, true);
+
+ info("Check no highlight event occur by mouse over locked target");
+ let highlightEventCount = 0;
+ function onHighlighterHidden({ type }) {
+ if (type === inspector.highlighters.TYPES.BOXMODEL) {
+ highlightEventCount += 1;
+ }
+ }
+ inspector.highlighters.on("highlighter-hidden", onHighlighterHidden);
+ mouseOverOnTargetNode(animationInspector, panel, 0);
+ await wait(500);
+ is(highlightEventCount, 0, "Highlight event should not occur");
+ inspector.highlighters.off("highlighter-hidden", onHighlighterHidden);
+
+ info("Show persistent highlighter on an animation target");
+ const onPersistentHighlighterShown = waitForHighlighterTypeShown(
+ inspector.highlighters.TYPES.SELECTOR
+ );
+ await clickOnInspectIcon(animationInspector, panel, 1);
+ data = await onPersistentHighlighterShown;
+ assertNodeFront(data.nodeFront, "DIV", "ball multi");
+
+ info("Check the highlighted state of the animation targets");
+ await assertHighlight(panel, 0, false);
+ await assertHighlight(panel, 1, true);
+ await assertHighlight(panel, 2, true);
+
+ info("Hide persistent highlighter");
+ const onPersistentHighlighterHidden = waitForHighlighterTypeHidden(
+ inspector.highlighters.TYPES.SELECTOR
+ );
+ await clickOnInspectIcon(animationInspector, panel, 1);
+ await onPersistentHighlighterHidden;
+
+ info("Check the highlighted state of the animation targets");
+ await assertHighlight(panel, 0, false);
+ await assertHighlight(panel, 1, false);
+ await assertHighlight(panel, 2, false);
+});
+
+async function assertHighlight(panel, index, isHighlightExpected) {
+ const animationItemEl = await findAnimationItemByIndex(panel, index);
+ const animationTargetEl = animationItemEl.querySelector(".animation-target");
+
+ await waitUntil(
+ () =>
+ animationTargetEl.classList.contains("highlighting") ===
+ isHighlightExpected
+ );
+ ok(true, `Highlighting class of animation target[${index}] is correct`);
+}
+
+function assertNodeFront(nodeFront, tagName, classValue) {
+ is(nodeFront.tagName, "DIV", "The highlighted node has the correct tagName");
+ is(
+ nodeFront.attributes[0].name,
+ "class",
+ "The highlighted node has the correct attributes"
+ );
+ is(
+ nodeFront.attributes[0].value,
+ classValue,
+ "The highlighted node has the correct class"
+ );
+}
diff --git a/devtools/client/inspector/animation/test/browser_animation_animation-target_select.js b/devtools/client/inspector/animation/test/browser_animation_animation-target_select.js
new file mode 100644
index 0000000000..970778c4c6
--- /dev/null
+++ b/devtools/client/inspector/animation/test/browser_animation_animation-target_select.js
@@ -0,0 +1,64 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test for following selection feature related AnimationTarget component works:
+// * select selected node by clicking on target node
+
+add_task(async function () {
+ await addTab(URL_ROOT + "doc_simple_animation.html");
+ await removeAnimatedElementsExcept([".multi", ".long"]);
+ const { animationInspector, panel } = await openAnimationInspector();
+
+ info("Check initial status");
+ is(
+ panel.querySelectorAll(".animation-item").length,
+ 3,
+ "The length of animations should be 3. Two .multi animations and one .long animation"
+ );
+
+ info("Check selecting an animated node by clicking on the target node");
+ await clickOnTargetNode(animationInspector, panel, 0);
+ assertNodeFront(
+ animationInspector.inspector.selection.nodeFront,
+ "DIV",
+ "ball multi"
+ );
+ is(
+ panel.querySelectorAll(".animation-item").length,
+ 2,
+ "The length of animations should be 2"
+ );
+
+ info("Check if the both target nodes refer to the same node");
+ await clickOnTargetNode(animationInspector, panel, 1);
+ assertNodeFront(
+ animationInspector.inspector.selection.nodeFront,
+ "DIV",
+ "ball multi"
+ );
+ is(
+ panel.querySelectorAll(".animation-item").length,
+ 2,
+ "The length of animations should be 2"
+ );
+});
+
+function assertNodeFront(nodeFront, tagName, classValue) {
+ is(
+ nodeFront.tagName,
+ tagName,
+ "The highlighted node has the correct tagName"
+ );
+ is(
+ nodeFront.attributes[0].name,
+ "class",
+ "The highlighted node has the correct attributes"
+ );
+ is(
+ nodeFront.attributes[0].value,
+ classValue,
+ "The highlighted node has the correct class"
+ );
+}
diff --git a/devtools/client/inspector/animation/test/browser_animation_animation-timeline-tick.js b/devtools/client/inspector/animation/test/browser_animation_animation-timeline-tick.js
new file mode 100644
index 0000000000..03e9535558
--- /dev/null
+++ b/devtools/client/inspector/animation/test/browser_animation_animation-timeline-tick.js
@@ -0,0 +1,109 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test for following timeline tick items.
+// * animation list header elements existence
+// * tick labels elements existence
+// * count and text of tick label elements changing by the sidebar width
+
+const TimeScale = require("resource://devtools/client/inspector/animation/utils/timescale.js");
+const {
+ findOptimalTimeInterval,
+} = require("resource://devtools/client/inspector/animation/utils/utils.js");
+
+// Should be kept in sync with TIME_GRADUATION_MIN_SPACING in
+// AnimationTimeTickList component.
+const TIME_GRADUATION_MIN_SPACING = 40;
+
+add_task(async function () {
+ await pushPref("devtools.inspector.three-pane-enabled", false);
+ await addTab(URL_ROOT + "doc_simple_animation.html");
+ await removeAnimatedElementsExcept([".end-delay", ".negative-delay"]);
+ const { animationInspector, inspector, panel } =
+ await openAnimationInspector();
+ const timeScale = new TimeScale(animationInspector.state.animations);
+
+ info("Checking animation list header element existence");
+ const listContainerEl = panel.querySelector(".animation-list-container");
+ const listHeaderEl = listContainerEl.querySelector(".devtools-toolbar");
+ ok(
+ listHeaderEl,
+ "The header element should be in animation list container element"
+ );
+
+ info("Checking time tick item elements existence");
+ await assertTickLabels(timeScale, listContainerEl);
+ const timelineTickItemLength =
+ listContainerEl.querySelectorAll(".tick-label").length;
+
+ info("Checking timeline tick item elements after enlarge sidebar width");
+ await setSidebarWidth("100%", inspector);
+ await assertTickLabels(timeScale, listContainerEl);
+ Assert.less(
+ timelineTickItemLength,
+ listContainerEl.querySelectorAll(".tick-label").length,
+ "The timeline tick item elements should increase"
+ );
+});
+
+/**
+ * Assert tick label's position and label.
+ *
+ * @param {TimeScale} - timeScale
+ * @param {Element} - listContainerEl
+ */
+async function assertTickLabels(timeScale, listContainerEl) {
+ const timelineTickListEl = listContainerEl.querySelector(".tick-labels");
+ ok(
+ timelineTickListEl,
+ "The animation timeline tick list element should be in header"
+ );
+
+ const width = timelineTickListEl.offsetWidth;
+ const animationDuration = timeScale.getDuration();
+ const minTimeInterval =
+ (TIME_GRADUATION_MIN_SPACING * animationDuration) / width;
+ const interval = findOptimalTimeInterval(minTimeInterval);
+ const shiftWidth = timeScale.zeroPositionTime % interval;
+ const expectedTickItem =
+ Math.ceil(animationDuration / interval) + (shiftWidth !== 0 ? 1 : 0);
+
+ await waitUntil(
+ () =>
+ timelineTickListEl.querySelectorAll(".tick-label").length ===
+ expectedTickItem
+ );
+ ok(true, "The expected number of timeline ticks were found");
+
+ const timelineTickItemEls =
+ timelineTickListEl.querySelectorAll(".tick-label");
+
+ info("Make sure graduations are evenly distributed and show the right times");
+ for (const [index, tickEl] of timelineTickItemEls.entries()) {
+ const left = parseFloat(tickEl.style.marginInlineStart);
+ let expectedPos =
+ (((index - 1) * interval + shiftWidth) / animationDuration) * 100;
+ if (shiftWidth !== 0 && index === 0) {
+ expectedPos = 0;
+ }
+ is(
+ Math.round(left),
+ Math.round(expectedPos),
+ `Graduation ${index} is positioned correctly`
+ );
+
+ // Note that the distancetoRelativeTime and formatTime functions are tested
+ // separately in xpcshell test test_timeScale.js, so we assume that they
+ // work here.
+ const formattedTime = timeScale.formatTime(
+ timeScale.distanceToRelativeTime(expectedPos, width)
+ );
+ is(
+ tickEl.textContent,
+ formattedTime,
+ `Graduation ${index} has the right text content`
+ );
+ }
+}
diff --git a/devtools/client/inspector/animation/test/browser_animation_css-transition-with-playstate-idle.js b/devtools/client/inspector/animation/test/browser_animation_css-transition-with-playstate-idle.js
new file mode 100644
index 0000000000..1970884623
--- /dev/null
+++ b/devtools/client/inspector/animation/test/browser_animation_css-transition-with-playstate-idle.js
@@ -0,0 +1,81 @@
+/* Any copyright is dedicated to the Public Domain.
+http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that animation inspector does not fail when rendering an animation that
+// transitions from the playState "idle".
+
+const PAGE_URL = `data:text/html;charset=utf-8,
+<!DOCTYPE html>
+<html>
+<head>
+ <style type="text/css">
+ div {
+ opacity: 0;
+ transition-duration: 5000ms;
+ transition-property: opacity;
+ }
+
+ div.visible {
+ opacity: 1;
+ }
+ </style>
+</head>
+<body>
+ <div>test</div>
+</body>
+</html>`;
+
+add_task(async function () {
+ const tab = await addTab(PAGE_URL);
+ const { animationInspector, panel } = await openAnimationInspector();
+
+ info("Toggle the visible class to start the animation");
+ await toggleVisibleClass(tab);
+
+ info("Wait until the scrubber is displayed");
+ await waitUntil(() => panel.querySelector(".current-time-scrubber"));
+ const scrubberEl = panel.querySelector(".current-time-scrubber");
+
+ info("Wait until animations are paused");
+ await waitUntilAnimationsPaused(animationInspector);
+
+ // Check the initial position of the scrubber to detect the animation.
+ const scrubberX = scrubberEl.getBoundingClientRect().x;
+
+ info("Toggle the visible class to start the animation");
+ await toggleVisibleClass(tab);
+
+ info("Wait until the scrubber starts moving");
+ await waitUntil(() => scrubberEl.getBoundingClientRect().x != scrubberX);
+
+ info("Wait until animations are paused");
+ await waitUntilAnimationsPaused(animationInspector);
+
+ // Query the scrubber element again to check that the UI is still rendered.
+ ok(
+ !!panel.querySelector(".current-time-scrubber"),
+ "The scrubber element is still rendered in the animation inspector panel"
+ );
+});
+
+/**
+ * Local helper to toggle the "visible" class on the element with a transition defined.
+ */
+async function toggleVisibleClass(tab) {
+ await SpecialPowers.spawn(tab.linkedBrowser, [], async function () {
+ const win = content.wrappedJSObject;
+ win.document.querySelector("div").classList.toggle("visible");
+ });
+}
+
+async function waitUntilAnimationsPaused(animationInspector) {
+ await waitUntil(() => {
+ const animations = animationInspector.state.animations;
+ return animations.every(animation => {
+ const state = animation.state.playState;
+ return state === "paused" || state === "finished";
+ });
+ });
+}
diff --git a/devtools/client/inspector/animation/test/browser_animation_current-time-label.js b/devtools/client/inspector/animation/test/browser_animation_current-time-label.js
new file mode 100644
index 0000000000..0cff5b1f53
--- /dev/null
+++ b/devtools/client/inspector/animation/test/browser_animation_current-time-label.js
@@ -0,0 +1,73 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test for following CurrentTimeLabel component:
+// * element existence
+// * label content at plural timing
+
+add_task(async function () {
+ await addTab(URL_ROOT + "doc_multi_timings.html");
+ await removeAnimatedElementsExcept([".keyframes-easing-step"]);
+ const { animationInspector, panel } = await openAnimationInspector();
+
+ info("Checking current time label existence");
+ const labelEl = panel.querySelector(".current-time-label");
+ ok(labelEl, "current time label should exist");
+
+ info("Checking current time label content");
+ const duration = animationInspector.state.timeScale.getDuration();
+ clickOnCurrentTimeScrubberController(animationInspector, panel, 0.5);
+ await waitUntilAnimationsPlayState(animationInspector, "paused");
+ await waitUntilCurrentTimeChangedAt(animationInspector, duration * 0.5);
+ const targetAnimation = animationInspector.state.animations[0];
+ assertLabelContent(labelEl, targetAnimation.state.currentTime);
+
+ clickOnCurrentTimeScrubberController(animationInspector, panel, 0.2);
+ await waitUntilCurrentTimeChangedAt(animationInspector, duration * 0.2);
+ assertLabelContent(labelEl, targetAnimation.state.currentTime);
+
+ info("Checking current time label content during running");
+ // Resume
+ clickOnPauseResumeButton(animationInspector, panel);
+ const previousContent = labelEl.textContent;
+
+ info("Wait until the time label changes");
+ await waitFor(() => labelEl.textContent != previousContent);
+ isnot(
+ previousContent,
+ labelEl.textContent,
+ "Current time label should change"
+ );
+});
+
+function assertLabelContent(labelEl, time) {
+ const expected = formatStopwatchTime(time);
+ is(labelEl.textContent, expected, `Content of label should be ${expected}`);
+}
+
+function formatStopwatchTime(time) {
+ // Format falsy values as 0
+ if (!time) {
+ return "00:00.000";
+ }
+
+ let milliseconds = parseInt(time % 1000, 10);
+ let seconds = parseInt((time / 1000) % 60, 10);
+ let minutes = parseInt(time / (1000 * 60), 10);
+
+ const pad = (nb, max) => {
+ if (nb < max) {
+ return new Array((max + "").length - (nb + "").length + 1).join("0") + nb;
+ }
+
+ return nb;
+ };
+
+ minutes = pad(minutes, 10);
+ seconds = pad(seconds, 10);
+ milliseconds = pad(milliseconds, 100);
+
+ return `${minutes}:${seconds}.${milliseconds}`;
+}
diff --git a/devtools/client/inspector/animation/test/browser_animation_current-time-scrubber-rtl.js b/devtools/client/inspector/animation/test/browser_animation_current-time-scrubber-rtl.js
new file mode 100644
index 0000000000..1da1c56e10
--- /dev/null
+++ b/devtools/client/inspector/animation/test/browser_animation_current-time-scrubber-rtl.js
@@ -0,0 +1,18 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/* import-globals-from current-time-scrubber_head.js */
+
+// Test for CurrentTimeScrubber on RTL environment.
+
+add_task(async function () {
+ Services.scriptloader.loadSubScript(
+ CHROME_URL_ROOT + "current-time-scrubber_head.js",
+ this
+ );
+ await pushPref("intl.l10n.pseudo", "bidi");
+ // eslint-disable-next-line no-undef
+ await testCurrentTimeScrubber(true);
+});
diff --git a/devtools/client/inspector/animation/test/browser_animation_current-time-scrubber-with-negative-delay.js b/devtools/client/inspector/animation/test/browser_animation_current-time-scrubber-with-negative-delay.js
new file mode 100644
index 0000000000..8b2e177079
--- /dev/null
+++ b/devtools/client/inspector/animation/test/browser_animation_current-time-scrubber-with-negative-delay.js
@@ -0,0 +1,48 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test whether the most left position means negative current time.
+
+add_task(async function () {
+ await addTab(URL_ROOT + "doc_multi_timings.html");
+ await removeAnimatedElementsExcept([
+ ".cssanimation-normal",
+ ".delay-negative",
+ ]);
+ const { animationInspector, panel, inspector } =
+ await openAnimationInspector();
+
+ info("Checking scrubber controller existence");
+ const controllerEl = panel.querySelector(".current-time-scrubber-area");
+ ok(controllerEl, "scrubber controller should exist");
+
+ info("Checking the current time of most left scrubber position");
+ const timeScale = animationInspector.state.timeScale;
+ clickOnCurrentTimeScrubberController(animationInspector, panel, 0);
+ await waitUntilAnimationsPlayState(animationInspector, "paused");
+ await waitUntilCurrentTimeChangedAt(
+ animationInspector,
+ -1 * timeScale.zeroPositionTime
+ );
+ ok(true, "Current time is correct");
+
+ info("Select negative current time animation");
+ await selectNode(".cssanimation-normal", inspector);
+ await waitUntilCurrentTimeChangedAt(
+ animationInspector,
+ -1 * timeScale.zeroPositionTime
+ );
+ ok(true, "Current time is correct");
+
+ info("Back to 'body' and rewind the animation");
+ await selectNode("body", inspector);
+ await waitUntil(
+ () =>
+ panel.querySelectorAll(".animation-item").length ===
+ animationInspector.state.animations.length
+ );
+ clickOnRewindButton(animationInspector, panel);
+ await waitUntilCurrentTimeChangedAt(animationInspector, 0);
+});
diff --git a/devtools/client/inspector/animation/test/browser_animation_current-time-scrubber.js b/devtools/client/inspector/animation/test/browser_animation_current-time-scrubber.js
new file mode 100644
index 0000000000..76cd42f282
--- /dev/null
+++ b/devtools/client/inspector/animation/test/browser_animation_current-time-scrubber.js
@@ -0,0 +1,14 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/* import-globals-from current-time-scrubber_head.js */
+
+add_task(async function () {
+ Services.scriptloader.loadSubScript(
+ CHROME_URL_ROOT + "current-time-scrubber_head.js",
+ this
+ );
+ await testCurrentTimeScrubber();
+});
diff --git a/devtools/client/inspector/animation/test/browser_animation_current-time-scrubber_each-different-creation-time-animations.js b/devtools/client/inspector/animation/test/browser_animation_current-time-scrubber_each-different-creation-time-animations.js
new file mode 100644
index 0000000000..6761bacb19
--- /dev/null
+++ b/devtools/client/inspector/animation/test/browser_animation_current-time-scrubber_each-different-creation-time-animations.js
@@ -0,0 +1,34 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test whether proper currentTime was set for each animations.
+
+const WAIT_TIME = 3000;
+
+add_task(async function () {
+ await addTab(URL_ROOT + "doc_simple_animation.html");
+ await removeAnimatedElementsExcept([".animated", ".still"]);
+ const { animationInspector, panel } = await openAnimationInspector();
+
+ info(
+ "Add an animation to make a situation which has different creation time"
+ );
+ await wait(WAIT_TIME);
+ await setClassAttribute(animationInspector, ".still", "ball compositor-all");
+ await waitUntil(() => panel.querySelectorAll(".animation-item").length === 2);
+
+ info("Move the scrubber");
+ clickOnCurrentTimeScrubberController(animationInspector, panel, 0.5);
+ await waitUntilAnimationsPlayState(animationInspector, "paused");
+
+ info("Check existed animations have different currentTime");
+ const animations = animationInspector.state.animations;
+ Assert.greater(
+ animations[0].state.currentTime + WAIT_TIME,
+ animations[1].state.currentTime,
+ `The currentTime of added animation shold be ${WAIT_TIME}ms less than ` +
+ "at least that currentTime of first animation"
+ );
+});
diff --git a/devtools/client/inspector/animation/test/browser_animation_empty_on_invalid_nodes.js b/devtools/client/inspector/animation/test/browser_animation_empty_on_invalid_nodes.js
new file mode 100644
index 0000000000..d3fa9166a1
--- /dev/null
+++ b/devtools/client/inspector/animation/test/browser_animation_empty_on_invalid_nodes.js
@@ -0,0 +1,49 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test that the panel shows no animation data for invalid or not animated nodes
+
+add_task(async function () {
+ await addTab(URL_ROOT + "doc_simple_animation.html");
+ await removeAnimatedElementsExcept([".animated", ".long", ".still"]);
+ const { inspector, panel } = await openAnimationInspector();
+
+ info("Checking animation list and error message existence for a still node");
+ const stillNode = await getNodeFront(".still", inspector);
+ await selectNode(stillNode, inspector);
+
+ await waitUntil(() => panel.querySelector(".animation-error-message"));
+ ok(
+ true,
+ "Element which has animation-error-message class should exist for a still node"
+ );
+ is(
+ panel.querySelector(".animation-error-message > p").textContent,
+ ANIMATION_L10N.getStr("panel.noAnimation"),
+ "The correct error message is displayed"
+ );
+ ok(
+ !panel.querySelector(".animation-list"),
+ "Element which has animations class should not exist for a still node"
+ );
+
+ info(
+ "Show animations once to confirm if there is no animations on the comment node"
+ );
+ await selectNode(".long", inspector);
+ await waitUntil(() => !panel.querySelector(".animation-error-message"));
+
+ info("Checking animation list and error message existence for a text node");
+ const commentNode = await inspector.walker.previousSibling(stillNode);
+ await selectNode(commentNode, inspector);
+ await waitUntil(() => panel.querySelector(".animation-error-message"));
+ ok(
+ panel.querySelector(".animation-error-message"),
+ "Element which has animation-error-message class should exist for a text node"
+ );
+ ok(
+ !panel.querySelector(".animation-list"),
+ "Element which has animations class should not exist for a text node"
+ );
+});
diff --git a/devtools/client/inspector/animation/test/browser_animation_fission_switch-target.js b/devtools/client/inspector/animation/test/browser_animation_fission_switch-target.js
new file mode 100644
index 0000000000..5a50e4098f
--- /dev/null
+++ b/devtools/client/inspector/animation/test/browser_animation_fission_switch-target.js
@@ -0,0 +1,99 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that the animation inspector works after switching targets.
+
+const PAGE_ON_CONTENT = `data:text/html;charset=utf-8,
+<!DOCTYPE html>
+<style type="text/css">
+ div {
+ opacity: 0;
+ transition-duration: 5000ms;
+ transition-property: opacity;
+ }
+
+ div:hover {
+ opacity: 1;
+ }
+</style>
+<div class="anim">animation</div>
+`;
+const PAGE_ON_MAIN = "about:networking";
+
+add_task(async function () {
+ await pushPref("devtools.inspector.three-pane-enabled", false);
+
+ info("Open a page that runs on the content process and has animations");
+ const tab = await addTab(PAGE_ON_CONTENT);
+ const { animationInspector, inspector } = await openAnimationInspector();
+
+ info("Check the length of the initial animations of the content process");
+ is(
+ animationInspector.state.animations.length,
+ 0,
+ "The length of the initial animation is correct"
+ );
+
+ info("Check whether the mutation on content process page is worked or not");
+ await assertAnimationsMutation(tab, "div", animationInspector, 1);
+
+ info("Load a page that runs on the main process");
+ await navigateTo(
+ PAGE_ON_MAIN,
+ tab.linkedBrowser,
+ animationInspector,
+ inspector
+ );
+ await waitUntil(() => animationInspector.state.animations.length === 0);
+ ok(true, "The animations are replaced");
+
+ info("Check whether the mutation on main process page is worked or not");
+ await assertAnimationsMutation(tab, "#category-http", animationInspector, 1);
+
+ info("Load a content process page again");
+ await navigateTo(
+ PAGE_ON_CONTENT,
+ tab.linkedBrowser,
+ animationInspector,
+ inspector
+ );
+ await waitUntil(() => animationInspector.state.animations.length === 0);
+ ok(true, "The animations are replaced again");
+
+ info("Check the mutation on content process again");
+ await assertAnimationsMutation(tab, "div", animationInspector, 1);
+});
+
+async function assertAnimationsMutation(
+ tab,
+ selector,
+ animationInspector,
+ expectedAnimationCount
+) {
+ await hover(tab, selector);
+ await waitUntil(
+ () => animationInspector.state.animations.length === expectedAnimationCount
+ );
+ ok(true, "Animations mutation is worked");
+}
+
+async function navigateTo(uri, browser, animationInspector, inspector) {
+ const previousAnimationsFront = animationInspector.animationsFront;
+ const onReloaded = inspector.once("reloaded");
+ const onUpdated = inspector.once("inspector-updated");
+ BrowserTestUtils.startLoadingURIString(browser, uri);
+ await waitUntil(
+ () => previousAnimationsFront !== animationInspector.animationsFront
+ );
+ ok(true, "Target is switched correctly");
+ await Promise.all([onReloaded, onUpdated]);
+}
+
+async function hover(tab, selector) {
+ await SpecialPowers.spawn(tab.linkedBrowser, [selector], async s => {
+ const element = content.wrappedJSObject.document.querySelector(s);
+ InspectorUtils.addPseudoClassLock(element, ":hover", true);
+ });
+}
diff --git a/devtools/client/inspector/animation/test/browser_animation_indication-bar.js b/devtools/client/inspector/animation/test/browser_animation_indication-bar.js
new file mode 100644
index 0000000000..829054178a
--- /dev/null
+++ b/devtools/client/inspector/animation/test/browser_animation_indication-bar.js
@@ -0,0 +1,42 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test whether the indication bar of both scrubber and progress bar indicates correct
+// progress after resizing animation inspector.
+
+add_task(async function () {
+ await pushPref("devtools.inspector.three-pane-enabled", false);
+ await addTab(URL_ROOT + "doc_simple_animation.html");
+ await removeAnimatedElementsExcept([".animated"]);
+ const { animationInspector, inspector, panel } =
+ await openAnimationInspector();
+
+ info("Checking timeline tick item elements after enlarge sidebar width");
+ clickOnCurrentTimeScrubberController(animationInspector, panel, 0.5);
+ await waitUntilAnimationsPlayState(animationInspector, "paused");
+ await setSidebarWidth("100%", inspector);
+ assertPosition(".current-time-scrubber", panel, 0.5);
+ assertPosition(".keyframes-progress-bar", panel, 0.5);
+});
+
+/**
+ * Assert indication bar position.
+ *
+ * @param {String} indicationBarSelector
+ * @param {Element} panel
+ * @param {Number} expectedPositionRate
+ */
+function assertPosition(indicationBarSelector, panel, expectedPositionRate) {
+ const barEl = panel.querySelector(indicationBarSelector);
+ const parentEl = barEl.parentNode;
+ const rectBar = barEl.getBoundingClientRect();
+ const rectParent = parentEl.getBoundingClientRect();
+ const barX = rectBar.x + rectBar.width * 0.5 - rectParent.x;
+ const expectedPosition = rectParent.width * expectedPositionRate;
+ ok(
+ expectedPosition - 1 <= barX && barX <= expectedPosition + 1,
+ `Indication bar position should be approximately ${expectedPosition}`
+ );
+}
diff --git a/devtools/client/inspector/animation/test/browser_animation_infinity-duration_current-time-scrubber.js b/devtools/client/inspector/animation/test/browser_animation_infinity-duration_current-time-scrubber.js
new file mode 100644
index 0000000000..b4f82a950e
--- /dev/null
+++ b/devtools/client/inspector/animation/test/browser_animation_infinity-duration_current-time-scrubber.js
@@ -0,0 +1,40 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test whether the scrubber was working for even the animation of infinity duration.
+
+add_task(async function () {
+ await addTab(URL_ROOT + "doc_infinity_duration.html");
+ await removeAnimatedElementsExcept([".infinity-delay-iteration-start"]);
+ const { animationInspector, panel } = await openAnimationInspector();
+
+ info("Set initial state");
+ clickOnCurrentTimeScrubberController(animationInspector, panel, 0);
+ await waitUntilAnimationsPlayState(animationInspector, "paused");
+ const initialCurrentTime =
+ animationInspector.state.animations[0].state.currentTime;
+
+ info("Check whether the animation currentTime was increased");
+ clickOnCurrentTimeScrubberController(animationInspector, panel, 1);
+ await waitUntil(
+ () =>
+ initialCurrentTime <
+ animationInspector.state.animations[0].state.currentTime
+ );
+ ok(true, "currentTime should be increased");
+
+ info("Check whether the progress bar was moved");
+ const areaEl = panel.querySelector(".keyframes-progress-bar-area");
+ const barEl = areaEl.querySelector(".keyframes-progress-bar");
+ const controllerBounds = areaEl.getBoundingClientRect();
+ const barBounds = barEl.getBoundingClientRect();
+ const barX = barBounds.x + barBounds.width / 2 - controllerBounds.x;
+ const expectedBarX = controllerBounds.width * 0.5;
+ Assert.less(
+ Math.abs(barX - expectedBarX),
+ 1,
+ "Progress bar should indicate at progress of 0.5"
+ );
+});
diff --git a/devtools/client/inspector/animation/test/browser_animation_infinity-duration_summary-graph.js b/devtools/client/inspector/animation/test/browser_animation_infinity-duration_summary-graph.js
new file mode 100644
index 0000000000..44343c3aa8
--- /dev/null
+++ b/devtools/client/inspector/animation/test/browser_animation_infinity-duration_summary-graph.js
@@ -0,0 +1,147 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test for following summary graph with the animation which has infinity duration.
+// * Tooltips
+// * Graph path
+// * Delay sign
+
+const TEST_DATA = [
+ {
+ targetClass: "infinity",
+ expectedIterationPath: [
+ { x: 0, y: 0 },
+ { x: 200000, y: 0 },
+ ],
+ expectedTooltip: {
+ duration: "\u221E",
+ },
+ },
+ {
+ targetClass: "infinity-delay-iteration-start",
+ expectedDelayPath: [
+ { x: 0, y: 0 },
+ { x: 100000, y: 0 },
+ ],
+ expectedDelaySign: {
+ marginInlineStart: "0%",
+ width: "50%",
+ },
+ expectedIterationPath: [
+ { x: 100000, y: 50 },
+ { x: 200000, y: 50 },
+ ],
+ expectedTooltip: {
+ delay: "100s",
+ duration: "\u221E",
+ iterationStart: "0.5 (\u221E)",
+ },
+ },
+ {
+ targetClass: "limited",
+ expectedIterationPath: [
+ { x: 0, y: 0 },
+ { x: 100000, y: 100 },
+ ],
+ expectedTooltip: {
+ duration: "100s",
+ },
+ },
+];
+
+add_task(async function () {
+ await addTab(URL_ROOT + "doc_infinity_duration.html");
+ const { panel } = await openAnimationInspector();
+
+ for (const testData of TEST_DATA) {
+ const {
+ targetClass,
+ expectedDelayPath,
+ expectedDelaySign,
+ expectedIterationPath,
+ expectedTooltip,
+ } = testData;
+
+ const animationItemEl = await findAnimationItemByTargetSelector(
+ panel,
+ `.${targetClass}`
+ );
+ const summaryGraphEl = animationItemEl.querySelector(
+ ".animation-summary-graph"
+ );
+
+ info(`Check tooltip for the animation of .${targetClass}`);
+ assertTooltip(summaryGraphEl, expectedTooltip);
+
+ if (expectedDelayPath) {
+ info(`Check delay path for the animation of .${targetClass}`);
+ assertDelayPath(summaryGraphEl, expectedDelayPath);
+ }
+
+ if (expectedDelaySign) {
+ info(`Check delay sign for the animation of .${targetClass}`);
+ assertDelaySign(summaryGraphEl, expectedDelaySign);
+ }
+
+ info(`Check iteration path for the animation of .${targetClass}`);
+ assertIterationPath(summaryGraphEl, expectedIterationPath);
+ }
+});
+
+function assertDelayPath(summaryGraphEl, expectedPath) {
+ assertPath(
+ summaryGraphEl,
+ ".animation-computed-timing-path .animation-delay-path",
+ expectedPath
+ );
+}
+
+function assertDelaySign(summaryGraphEl, expectedSign) {
+ const signEl = summaryGraphEl.querySelector(".animation-delay-sign");
+
+ is(
+ signEl.style.marginInlineStart,
+ expectedSign.marginInlineStart,
+ `marginInlineStart position should be ${expectedSign.marginInlineStart}`
+ );
+ is(
+ signEl.style.width,
+ expectedSign.width,
+ `Width should be ${expectedSign.width}`
+ );
+}
+
+function assertIterationPath(summaryGraphEl, expectedPath) {
+ assertPath(
+ summaryGraphEl,
+ ".animation-computed-timing-path .animation-iteration-path",
+ expectedPath
+ );
+}
+
+function assertPath(summaryGraphEl, pathSelector, expectedPath) {
+ const pathEl = summaryGraphEl.querySelector(pathSelector);
+ assertPathSegments(pathEl, true, expectedPath);
+}
+
+function assertTooltip(summaryGraphEl, expectedTooltip) {
+ const tooltip = summaryGraphEl.getAttribute("title");
+ const { delay, duration, iterationStart } = expectedTooltip;
+
+ if (delay) {
+ const expected = `Delay: ${delay}`;
+ ok(tooltip.includes(expected), `Tooltip should include '${expected}'`);
+ }
+
+ if (duration) {
+ const expected = `Duration: ${duration}`;
+ ok(tooltip.includes(expected), `Tooltip should include '${expected}'`);
+ }
+
+ if (iterationStart) {
+ const expected = `Iteration start: ${iterationStart}`;
+ ok(tooltip.includes(expected), `Tooltip should include '${expected}'`);
+ }
+}
diff --git a/devtools/client/inspector/animation/test/browser_animation_infinity-duration_tick-label.js b/devtools/client/inspector/animation/test/browser_animation_infinity-duration_tick-label.js
new file mode 100644
index 0000000000..2a554267c4
--- /dev/null
+++ b/devtools/client/inspector/animation/test/browser_animation_infinity-duration_tick-label.js
@@ -0,0 +1,29 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test of the content of tick label on timeline header
+// with the animation which has infinity duration.
+
+add_task(async function () {
+ await addTab(URL_ROOT + "doc_infinity_duration.html");
+ const { inspector, panel } = await openAnimationInspector();
+
+ info("Check the tick label content with limited duration animation");
+ isnot(
+ panel.querySelector(".animation-list-container .tick-label:last-child")
+ .textContent,
+ "\u221E",
+ "The content should not be \u221E"
+ );
+
+ info("Check the tick label content with infinity duration animation only");
+ await selectNode(".infinity", inspector);
+ await waitUntil(
+ () =>
+ panel.querySelector(".animation-list-container .tick-label:last-child")
+ .textContent === "\u221E"
+ );
+ ok(true, "The content should be \u221E");
+});
diff --git a/devtools/client/inspector/animation/test/browser_animation_keyframes-graph_computed-value-path-01.js b/devtools/client/inspector/animation/test/browser_animation_keyframes-graph_computed-value-path-01.js
new file mode 100644
index 0000000000..b893626bda
--- /dev/null
+++ b/devtools/client/inspector/animation/test/browser_animation_keyframes-graph_computed-value-path-01.js
@@ -0,0 +1,164 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test for ComputedValuePath of animations that consist by multi types of animated
+// properties.
+
+requestLongerTimeout(2);
+
+const TEST_DATA = [
+ {
+ targetClass: "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 },
+ ],
+ },
+ ],
+ },
+ {
+ targetClass: "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 },
+ ],
+ },
+ ],
+ },
+];
+
+add_task(async function () {
+ await testKeyframesGraphComputedValuePath(TEST_DATA);
+});
diff --git a/devtools/client/inspector/animation/test/browser_animation_keyframes-graph_computed-value-path-02.js b/devtools/client/inspector/animation/test/browser_animation_keyframes-graph_computed-value-path-02.js
new file mode 100644
index 0000000000..c36bd22628
--- /dev/null
+++ b/devtools/client/inspector/animation/test/browser_animation_keyframes-graph_computed-value-path-02.js
@@ -0,0 +1,90 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test for ComputedValuePath of animations that consist by one animated property
+// on complexed keyframes.
+
+requestLongerTimeout(2);
+
+const TEST_DATA = [
+ {
+ targetClass: "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 },
+ ],
+ },
+ ],
+ },
+ {
+ targetClass: "steps-jump-none-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 },
+ ],
+ },
+ ],
+ },
+ {
+ targetClass: "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 },
+ ],
+ },
+ ],
+ },
+ {
+ targetClass: "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 testKeyframesGraphComputedValuePath(TEST_DATA);
+});
diff --git a/devtools/client/inspector/animation/test/browser_animation_keyframes-graph_computed-value-path-03.js b/devtools/client/inspector/animation/test/browser_animation_keyframes-graph_computed-value-path-03.js
new file mode 100644
index 0000000000..957a693a31
--- /dev/null
+++ b/devtools/client/inspector/animation/test/browser_animation_keyframes-graph_computed-value-path-03.js
@@ -0,0 +1,190 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test for ComputedValuePath of animations that consist by multi types of animated
+// properties on complexed keyframes.
+
+requestLongerTimeout(2);
+
+const TEST_DATA = [
+ {
+ targetClass: "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 },
+ ],
+ },
+ ],
+ },
+ {
+ targetClass: "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 },
+ ],
+ },
+ ],
+ },
+];
+
+add_task(async function () {
+ await testKeyframesGraphComputedValuePath(TEST_DATA);
+});
diff --git a/devtools/client/inspector/animation/test/browser_animation_keyframes-graph_computed-value-path_easing-hint.js b/devtools/client/inspector/animation/test/browser_animation_keyframes-graph_computed-value-path_easing-hint.js
new file mode 100644
index 0000000000..b95c8d5fe4
--- /dev/null
+++ b/devtools/client/inspector/animation/test/browser_animation_keyframes-graph_computed-value-path_easing-hint.js
@@ -0,0 +1,380 @@
+/* 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 = [
+ {
+ targetClass: "no-easing",
+ properties: [
+ {
+ name: "opacity",
+ expectedHints: [
+ {
+ hint: "linear",
+ path: [
+ { x: 0, y: 100 },
+ { x: 500, y: 50 },
+ { x: 1000, y: 0 },
+ ],
+ },
+ ],
+ },
+ ],
+ },
+ {
+ targetClass: "effect-easing",
+ properties: [
+ {
+ name: "opacity",
+ expectedHints: [
+ {
+ hint: "linear",
+ path: [
+ { x: 0, y: 100 },
+ { x: 199, y: 81 },
+ { x: 200, y: 80 },
+ { x: 399, y: 61 },
+ { x: 400, y: 60 },
+ { x: 599, y: 41 },
+ { x: 600, y: 40 },
+ { x: 799, y: 21 },
+ { x: 800, y: 20 },
+ { x: 1000, y: 0 },
+ ],
+ },
+ ],
+ },
+ ],
+ },
+ {
+ targetClass: "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 },
+ ],
+ },
+ ],
+ },
+ ],
+ },
+ {
+ targetClass: "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 },
+ ],
+ },
+ ],
+ },
+ ],
+ },
+ {
+ targetClass: "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 },
+ ],
+ },
+ ],
+ },
+ ],
+ },
+ {
+ targetClass: "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 },
+ ],
+ },
+ ],
+ },
+ ],
+ },
+ {
+ targetClass: "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,
+ },
+ },
+ ],
+ },
+ ],
+ },
+ {
+ targetClass: "jump-start",
+ properties: [
+ {
+ name: "opacity",
+ expectedHints: [
+ {
+ hint: "steps(2, jump-start)",
+ path: [
+ { x: 0, y: 50 },
+ { x: 499, y: 50 },
+ { x: 500, y: 0 },
+ { x: 1000, y: 0 },
+ ],
+ },
+ ],
+ },
+ ],
+ },
+ {
+ targetClass: "jump-end",
+ 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 },
+ ],
+ },
+ ],
+ },
+ ],
+ },
+ {
+ targetClass: "jump-both",
+ properties: [
+ {
+ name: "opacity",
+ expectedHints: [
+ {
+ hint: "steps(3, jump-both)",
+ path: [
+ { x: 0, y: 75 },
+ { x: 330, y: 75 },
+ { x: 340, y: 50 },
+ { x: 660, y: 50 },
+ { x: 670, y: 25 },
+ { x: 999, y: 25 },
+ { x: 1000, y: 0 },
+ ],
+ },
+ ],
+ },
+ ],
+ },
+];
+
+// Prevent test timeout's on windows code coverage: Bug 1470757
+requestLongerTimeout(2);
+
+add_task(async function () {
+ await addTab(URL_ROOT + "doc_multi_easings.html");
+ await removeAnimatedElementsExcept(TEST_DATA.map(t => `.${t.targetClass}`));
+ const { animationInspector, panel } = await openAnimationInspector();
+
+ for (const { properties, targetClass } of TEST_DATA) {
+ info(`Checking keyframes graph for ${targetClass}`);
+ const onDetailRendered = animationInspector.once(
+ "animation-keyframes-rendered"
+ );
+ await clickOnAnimationByTargetSelector(
+ animationInspector,
+ panel,
+ `.${targetClass}`
+ );
+ await onDetailRendered;
+
+ for (const { name, expectedHints } of properties) {
+ const testTarget = `${name} in ${targetClass}`;
+ 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;
+ let displayedEl = null;
+ if (expectedHint.path) {
+ info(`Checking <path> in ${hintTarget}`);
+ interactionEl = hintEl.querySelector("path");
+ displayedEl = interactionEl;
+ 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");
+ displayedEl = hintEl.querySelector("line");
+ ok(
+ interactionEl,
+ `The <rect> element in ${hintTarget} should be existence`
+ );
+ is(
+ parseInt(interactionEl.getAttribute("x"), 10),
+ expectedHint.rect.x,
+ `x of <rect> in ${hintTarget} should be ${expectedHint.rect.x}`
+ );
+ is(
+ parseInt(interactionEl.getAttribute("width"), 10),
+ 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 over the pathEl.
+ ok(
+ isStrokeChangedByMouseOver(interactionEl, displayedEl, win),
+ `stroke-opacity of hintEl for ${hintTarget} should be 1 ` +
+ "while mouse is over the element"
+ );
+ // Mouse out from pathEl.
+ EventUtils.synthesizeMouse(
+ panel.querySelector(".animation-toolbar"),
+ 0,
+ 0,
+ { type: "mouseover" },
+ win
+ );
+ is(
+ parseInt(win.getComputedStyle(displayedEl).strokeOpacity, 10),
+ 0,
+ `stroke-opacity of hintEl for ${hintTarget} should be 0 ` +
+ "while mouse is out from the element"
+ );
+ }
+ }
+ }
+});
+
+function isStrokeChangedByMouseOver(mouseoverEl, displayedEl, win) {
+ const boundingBox = mouseoverEl.getBoundingClientRect();
+ const x = boundingBox.width / 2;
+
+ for (let y = 0; y < boundingBox.height; y++) {
+ EventUtils.synthesizeMouse(mouseoverEl, x, y, { type: "mouseover" }, win);
+
+ if (win.getComputedStyle(displayedEl).strokeOpacity == 1) {
+ return true;
+ }
+ }
+
+ return false;
+}
diff --git a/devtools/client/inspector/animation/test/browser_animation_keyframes-graph_keyframe-marker-rtl.js b/devtools/client/inspector/animation/test/browser_animation_keyframes-graph_keyframe-marker-rtl.js
new file mode 100644
index 0000000000..c90b231f0a
--- /dev/null
+++ b/devtools/client/inspector/animation/test/browser_animation_keyframes-graph_keyframe-marker-rtl.js
@@ -0,0 +1,14 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function () {
+ Services.scriptloader.loadSubScript(
+ CHROME_URL_ROOT + "keyframes-graph_keyframe-marker_head.js",
+ this
+ );
+ await pushPref("intl.l10n.pseudo", "bidi");
+ // eslint-disable-next-line no-undef
+ await testKeyframesGraphKeyframesMarker();
+});
diff --git a/devtools/client/inspector/animation/test/browser_animation_keyframes-graph_keyframe-marker.js b/devtools/client/inspector/animation/test/browser_animation_keyframes-graph_keyframe-marker.js
new file mode 100644
index 0000000000..a19c2993f2
--- /dev/null
+++ b/devtools/client/inspector/animation/test/browser_animation_keyframes-graph_keyframe-marker.js
@@ -0,0 +1,13 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function () {
+ Services.scriptloader.loadSubScript(
+ CHROME_URL_ROOT + "keyframes-graph_keyframe-marker_head.js",
+ this
+ );
+ // eslint-disable-next-line no-undef
+ await testKeyframesGraphKeyframesMarker();
+});
diff --git a/devtools/client/inspector/animation/test/browser_animation_keyframes-graph_special-colors.js b/devtools/client/inspector/animation/test/browser_animation_keyframes-graph_special-colors.js
new file mode 100644
index 0000000000..6f46b44332
--- /dev/null
+++ b/devtools/client/inspector/animation/test/browser_animation_keyframes-graph_special-colors.js
@@ -0,0 +1,40 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_DATA = [
+ {
+ propertyName: "caret-color",
+ expectedMarkers: ["auto", "rgb(0, 255, 0)"],
+ },
+ {
+ propertyName: "scrollbar-color",
+ expectedMarkers: ["rgb(0, 255, 0) rgb(255, 0, 0)", "auto"],
+ },
+];
+
+// Test for animatable property which can specify the non standard CSS color value.
+add_task(async function () {
+ await addTab(URL_ROOT + "doc_special_colors.html");
+ const { panel } = await openAnimationInspector();
+
+ for (const { propertyName, expectedMarkers } of TEST_DATA) {
+ const animatedPropertyEl = panel.querySelector(`.${propertyName}`);
+ ok(animatedPropertyEl, `Animated property ${propertyName} exists`);
+
+ const markerEls = animatedPropertyEl.querySelectorAll(
+ ".keyframe-marker-item"
+ );
+ is(
+ markerEls.length,
+ expectedMarkers.length,
+ `The length of keyframe markers should ${expectedMarkers.length}`
+ );
+ for (let i = 0; i < expectedMarkers.length; i++) {
+ const actualTitle = markerEls[i].title;
+ const expectedTitle = expectedMarkers[i];
+ is(actualTitle, expectedTitle, `Value of keyframes[${i}] is correct`);
+ }
+ }
+});
diff --git a/devtools/client/inspector/animation/test/browser_animation_keyframes-progress-bar.js b/devtools/client/inspector/animation/test/browser_animation_keyframes-progress-bar.js
new file mode 100644
index 0000000000..a7051d9a01
--- /dev/null
+++ b/devtools/client/inspector/animation/test/browser_animation_keyframes-progress-bar.js
@@ -0,0 +1,103 @@
+/* Any copyright is dedicated to the Public Domain.
+http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test for following KeyframesProgressBar:
+// * element existence
+// * progress bar position in multi effect timings
+// * progress bar position after changing playback rate
+// * progress bar position when select another animation
+
+requestLongerTimeout(3);
+
+const TEST_DATA = [
+ {
+ targetClass: "cssanimation-linear",
+ scrubberPositions: [0, 0.25, 0.5, 0.75, 1],
+ expectedPositions: [0, 0.25, 0.5, 0.75, 0],
+ },
+ {
+ targetClass: "easing-step",
+ scrubberPositions: [0, 0.49, 0.5, 0.99],
+ expectedPositions: [0, 0, 0.5, 0.5],
+ },
+ {
+ targetClass: "delay-positive",
+ scrubberPositions: [0, 0.33, 0.5],
+ expectedPositions: [0, 0, 0.25],
+ },
+ {
+ targetClass: "delay-negative",
+ scrubberPositions: [0, 0.49, 0.5, 0.75],
+ expectedPositions: [0, 0, 0.5, 0.75],
+ },
+ {
+ targetClass: "enddelay-positive",
+ scrubberPositions: [0, 0.66, 0.67, 0.99],
+ expectedPositions: [0, 0.99, 0, 0],
+ },
+ {
+ targetClass: "enddelay-negative",
+ scrubberPositions: [0, 0.49, 0.5, 0.99],
+ expectedPositions: [0, 0.49, 0, 0],
+ },
+ {
+ targetClass: "direction-reverse-with-iterations-infinity",
+ scrubberPositions: [0, 0.25, 0.5, 0.75, 1],
+ expectedPositions: [1, 0.75, 0.5, 0.25, 1],
+ },
+ {
+ targetClass: "fill-both-width-delay-iterationstart",
+ scrubberPositions: [0, 0.33, 0.66, 0.833, 1],
+ expectedPositions: [0.5, 0.5, 0.99, 0.25, 0.5],
+ },
+];
+
+add_task(async function () {
+ await addTab(URL_ROOT + "doc_multi_timings.html");
+ await removeAnimatedElementsExcept(TEST_DATA.map(t => `.${t.targetClass}`));
+ const { animationInspector, inspector, panel } =
+ await openAnimationInspector();
+
+ info("Checking progress bar position in multi effect timings");
+
+ for (const testdata of TEST_DATA) {
+ const { targetClass, scrubberPositions, expectedPositions } = testdata;
+
+ info(`Checking progress bar position for ${targetClass}`);
+ const onDetailRendered = animationInspector.once(
+ "animation-keyframes-rendered"
+ );
+ await selectNode(`.${targetClass}`, inspector);
+ await onDetailRendered;
+
+ info("Checking progress bar existence");
+ const areaEl = panel.querySelector(".keyframes-progress-bar-area");
+ ok(areaEl, "progress bar area should exist");
+ const barEl = areaEl.querySelector(".keyframes-progress-bar");
+ ok(barEl, "progress bar should exist");
+
+ for (let i = 0; i < scrubberPositions.length; i++) {
+ info(`Scrubber position is ${scrubberPositions[i]}`);
+ clickOnCurrentTimeScrubberController(
+ animationInspector,
+ panel,
+ scrubberPositions[i]
+ );
+ await waitUntilAnimationsPlayState(animationInspector, "paused");
+ assertPosition(barEl, areaEl, expectedPositions[i], animationInspector);
+ }
+ }
+});
+
+function assertPosition(barEl, areaEl, expectedRate, animationInspector) {
+ const controllerBounds = areaEl.getBoundingClientRect();
+ const barBounds = barEl.getBoundingClientRect();
+ const barX = barBounds.x + barBounds.width / 2 - controllerBounds.x;
+ const expected = controllerBounds.width * expectedRate;
+ ok(
+ expected - 1 < barX && barX < expected + 1,
+ `Position should apploximately be ${expected} (x of bar is ${barX})`
+ );
+}
diff --git a/devtools/client/inspector/animation/test/browser_animation_keyframes-progress-bar_after-resuming.js b/devtools/client/inspector/animation/test/browser_animation_keyframes-progress-bar_after-resuming.js
new file mode 100644
index 0000000000..8b91026799
--- /dev/null
+++ b/devtools/client/inspector/animation/test/browser_animation_keyframes-progress-bar_after-resuming.js
@@ -0,0 +1,64 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test whether keyframes progress bar moves correctly after resuming the animation.
+
+add_task(async function () {
+ await addTab(URL_ROOT + "doc_simple_animation.html");
+ await removeAnimatedElementsExcept([".animated"]);
+ const { animationInspector, panel } = await openAnimationInspector();
+
+ const scrubberPositions = [0, 0.25, 0.5, 0.75];
+ const expectedPositions = [0, 0.25, 0.5, 0.75];
+
+ info("Check whether the keyframes progress bar position was correct");
+ await assertPosition(
+ panel,
+ scrubberPositions,
+ expectedPositions,
+ animationInspector
+ );
+
+ info(
+ "Check whether the keyframes progress bar position was correct " +
+ "after a bit time passed and resuming"
+ );
+ await wait(500);
+ clickOnPauseResumeButton(animationInspector, panel);
+ await assertPosition(
+ panel,
+ scrubberPositions,
+ expectedPositions,
+ animationInspector
+ );
+});
+
+async function assertPosition(
+ panel,
+ scrubberPositions,
+ expectedPositions,
+ animationInspector
+) {
+ const areaEl = panel.querySelector(".keyframes-progress-bar-area");
+ const barEl = areaEl.querySelector(".keyframes-progress-bar");
+ const controllerBounds = areaEl.getBoundingClientRect();
+
+ for (let i = 0; i < scrubberPositions.length; i++) {
+ info(`Scrubber position is ${scrubberPositions[i]}`);
+ clickOnCurrentTimeScrubberController(
+ animationInspector,
+ panel,
+ scrubberPositions[i]
+ );
+ await waitUntilAnimationsPlayState(animationInspector, "paused");
+ const barBounds = barEl.getBoundingClientRect();
+ const barX = barBounds.x + barBounds.width / 2 - controllerBounds.x;
+ const expected = controllerBounds.width * expectedPositions[i];
+ ok(
+ expected - 1 < barX && barX < expected + 1,
+ `Position should apploximately be ${expected} (x of bar is ${barX})`
+ );
+ }
+}
diff --git a/devtools/client/inspector/animation/test/browser_animation_logic_adjust-time-with-playback-rate.js b/devtools/client/inspector/animation/test/browser_animation_logic_adjust-time-with-playback-rate.js
new file mode 100644
index 0000000000..ad356cadd2
--- /dev/null
+++ b/devtools/client/inspector/animation/test/browser_animation_logic_adjust-time-with-playback-rate.js
@@ -0,0 +1,50 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test adjusting the created time with different playback rate of animation.
+
+add_task(async function () {
+ await addTab(URL_ROOT + "doc_custom_playback_rate.html");
+ const { animationInspector, inspector, panel } =
+ await openAnimationInspector();
+
+ info(
+ "Pause the all animation and set current time to middle in order to check " +
+ "the adjusting time"
+ );
+ clickOnPauseResumeButton(animationInspector, panel);
+ await waitUntilAnimationsPlayState(animationInspector, "paused");
+ clickOnCurrentTimeScrubberController(animationInspector, panel, 0.5);
+
+ info("Check the created times of all animation are same");
+ checkAdjustingTheTime(
+ animationInspector.state.animations[0].state,
+ animationInspector.state.animations[1].state
+ );
+
+ info("Change the playback rate to x10 after selecting '.div2'");
+ await selectNode(".div2", inspector);
+ await waitUntil(() => panel.querySelectorAll(".animation-item").length === 1);
+ await changePlaybackRateSelector(animationInspector, panel, 10);
+
+ info("Check each adjusted result of animations after selecting 'body' again");
+ await selectNode("body", inspector);
+ await waitUntil(() => panel.querySelectorAll(".animation-item").length === 2);
+
+ checkAdjustingTheTime(
+ animationInspector.state.animations[0].state,
+ animationInspector.state.animations[1].state
+ );
+
+ await waitUntil(
+ () => animationInspector.state.animations[0].state.currentTime === 50000
+ );
+ ok(true, "The current time of '.div1' animation is 50%");
+
+ await waitUntil(
+ () => animationInspector.state.animations[1].state.currentTime === 50000
+ );
+ ok(true, "The current time of '.div2' animation is 50%");
+});
diff --git a/devtools/client/inspector/animation/test/browser_animation_logic_adjust-time.js b/devtools/client/inspector/animation/test/browser_animation_logic_adjust-time.js
new file mode 100644
index 0000000000..44769ea055
--- /dev/null
+++ b/devtools/client/inspector/animation/test/browser_animation_logic_adjust-time.js
@@ -0,0 +1,50 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test adjusting the created time with different current times of animation.
+
+add_task(async function () {
+ await addTab(URL_ROOT + "doc_custom_playback_rate.html");
+ const { animationInspector, inspector, panel } =
+ await openAnimationInspector();
+
+ info(
+ "Pause the all animation and set current time to middle time in order to " +
+ "check the adjusting time"
+ );
+ clickOnPauseResumeButton(animationInspector, panel);
+ await waitUntilAnimationsPlayState(animationInspector, "paused");
+ clickOnCurrentTimeScrubberController(animationInspector, panel, 0.5);
+
+ info("Check the created times of all animation are same");
+ checkAdjustingTheTime(
+ animationInspector.state.animations[0].state,
+ animationInspector.state.animations[1].state
+ );
+
+ info("Change the current time to 75% after selecting '.div2'");
+ await selectNode(".div2", inspector);
+ await waitUntil(() => panel.querySelectorAll(".animation-item").length === 1);
+ clickOnCurrentTimeScrubberController(animationInspector, panel, 0.75);
+
+ info("Check each adjusted result of animations after selecting 'body' again");
+ await selectNode("body", inspector);
+ await waitUntil(() => panel.querySelectorAll(".animation-item").length === 2);
+
+ checkAdjustingTheTime(
+ animationInspector.state.animations[0].state,
+ animationInspector.state.animations[1].state
+ );
+ is(
+ animationInspector.state.animations[0].state.currentTime,
+ 50000,
+ "The current time of '.div1' animation is 50%"
+ );
+ is(
+ animationInspector.state.animations[1].state.currentTime,
+ 75000,
+ "The current time of '.div2' animation is 75%"
+ );
+});
diff --git a/devtools/client/inspector/animation/test/browser_animation_logic_auto-stop.js b/devtools/client/inspector/animation/test/browser_animation_logic_auto-stop.js
new file mode 100644
index 0000000000..fdf1867ffa
--- /dev/null
+++ b/devtools/client/inspector/animation/test/browser_animation_logic_auto-stop.js
@@ -0,0 +1,94 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Animation inspector makes the current time to stop
+// after end of animation duration except iterations infinity.
+// Test followings:
+// * state of animations and UI components after end of animation duration
+// * state of animations and UI components after end of animation duration
+// but iteration count is infinity
+
+add_task(async function () {
+ await addTab(URL_ROOT + "doc_simple_animation.html");
+ await removeAnimatedElementsExcept([".compositor-all", ".long"]);
+ const { animationInspector, inspector, panel } =
+ await openAnimationInspector();
+
+ info("Checking state after end of animation duration");
+ await selectNode(".long", inspector);
+ await waitUntil(() => panel.querySelectorAll(".animation-item").length === 1);
+ const pixelsData = getDurationAndRate(animationInspector, panel, 5);
+ clickOnCurrentTimeScrubberController(
+ animationInspector,
+ panel,
+ 1 - pixelsData.rate
+ );
+ await waitUntilAnimationsPlayState(animationInspector, "paused");
+ clickOnPauseResumeButton(animationInspector, panel);
+ await assertStates(animationInspector, panel, false);
+
+ info(
+ "Checking state after end of animation duration and infinity iterations"
+ );
+ clickOnPauseResumeButton(animationInspector, panel);
+ await waitUntilAnimationsPlayState(animationInspector, "paused");
+ await selectNode(".compositor-all", inspector);
+ await waitUntil(() => panel.querySelectorAll(".animation-item").length === 1);
+ clickOnCurrentTimeScrubberController(animationInspector, panel, 1);
+ await waitUntilAnimationsPlayState(animationInspector, "paused");
+ clickOnPauseResumeButton(animationInspector, panel);
+ await assertStates(animationInspector, panel, true);
+});
+
+async function assertStates(animationInspector, panel, shouldRunning) {
+ const buttonEl = panel.querySelector(".pause-resume-button");
+ const labelEl = panel.querySelector(".current-time-label");
+ const scrubberEl = panel.querySelector(".current-time-scrubber");
+
+ const previousLabelContent = labelEl.textContent;
+ const previousScrubberX = scrubberEl.getBoundingClientRect().x;
+
+ await waitUntilAnimationsPlayState(
+ animationInspector,
+ shouldRunning ? "running" : "paused"
+ );
+
+ const currentLabelContent = labelEl.textContent;
+ const currentScrubberX = scrubberEl.getBoundingClientRect().x;
+
+ if (shouldRunning) {
+ isnot(
+ previousLabelContent,
+ currentLabelContent,
+ "Current time label content should change"
+ );
+ isnot(
+ previousScrubberX,
+ currentScrubberX,
+ "Current time scrubber position should change"
+ );
+ ok(
+ !buttonEl.classList.contains("paused"),
+ "State of button should be running"
+ );
+ assertAnimationsRunning(animationInspector);
+ } else {
+ is(
+ previousLabelContent,
+ currentLabelContent,
+ "Current time label Content should not change"
+ );
+ is(
+ previousScrubberX,
+ currentScrubberX,
+ "Current time scrubber position should not change"
+ );
+ ok(
+ buttonEl.classList.contains("paused"),
+ "State of button should be paused"
+ );
+ assertAnimationsPausing(animationInspector);
+ }
+}
diff --git a/devtools/client/inspector/animation/test/browser_animation_logic_avoid-updating-during-hiding.js b/devtools/client/inspector/animation/test/browser_animation_logic_avoid-updating-during-hiding.js
new file mode 100644
index 0000000000..cfdd111ec9
--- /dev/null
+++ b/devtools/client/inspector/animation/test/browser_animation_logic_avoid-updating-during-hiding.js
@@ -0,0 +1,92 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Animation inspector should not update when hidden.
+// Test for followings:
+// * whether the UIs update after selecting another inspector
+// * whether the UIs update after selecting another tool
+// * whether the UIs update after selecting animation inspector again
+
+add_task(async function () {
+ info(
+ "Switch to 2 pane inspector to see if the animation only refreshes when visible"
+ );
+ await pushPref("devtools.inspector.three-pane-enabled", false);
+ await addTab(URL_ROOT + "doc_custom_playback_rate.html");
+ const { animationInspector, inspector, panel } =
+ await openAnimationInspector();
+
+ info("Checking the UIs update after selecting another inspector");
+ await selectNode("head", inspector);
+ inspector.sidebar.select("ruleview");
+ await selectNode("div", inspector);
+ await waitUntil(() => !animationInspector.state.animations.length);
+ ok(true, "Should not update after selecting another inspector");
+
+ await selectAnimationInspector(inspector);
+ await waitUntil(() => animationInspector.state.animations.length);
+ ok(true, "Should update after selecting animation inspector");
+
+ await assertCurrentTimeUpdated(animationInspector, panel, true);
+ inspector.sidebar.select("ruleview");
+ is(
+ animationInspector.state.animations.length,
+ 1,
+ "Should not update after selecting another inspector again"
+ );
+ await assertCurrentTimeUpdated(animationInspector, panel, false);
+
+ info("Checking the UIs update after selecting another tool");
+ await selectAnimationInspector(inspector);
+ await selectNode("head", inspector);
+ await waitUntil(() => !animationInspector.state.animations.length);
+ await inspector.toolbox.selectTool("webconsole");
+ await selectNode("div", inspector);
+ is(
+ animationInspector.state.animations.length,
+ 0,
+ "Should not update after selecting another tool"
+ );
+ await selectAnimationInspector(inspector);
+ await waitUntil(() => animationInspector.state.animations.length);
+ is(
+ animationInspector.state.animations.length,
+ 1,
+ "Should update after selecting animation inspector"
+ );
+ await assertCurrentTimeUpdated(animationInspector, panel, true);
+ await inspector.toolbox.selectTool("webconsole");
+ await waitUntil(() => animationInspector.state.animations.length);
+ is(
+ animationInspector.state.animations.length,
+ 1,
+ "Should not update after selecting another tool again"
+ );
+ await assertCurrentTimeUpdated(animationInspector, panel, false);
+});
+
+async function assertCurrentTimeUpdated(
+ animationInspector,
+ panel,
+ shouldRunning
+) {
+ let count = 0;
+
+ const listener = () => {
+ count++;
+ };
+
+ animationInspector.addAnimationsCurrentTimeListener(listener);
+ await new Promise(resolve =>
+ panel.ownerGlobal.requestAnimationFrame(resolve)
+ );
+ animationInspector.removeAnimationsCurrentTimeListener(listener);
+
+ if (shouldRunning) {
+ isnot(count, 0, "Should forward current time");
+ } else {
+ is(count, 0, "Should not forward current time");
+ }
+}
diff --git a/devtools/client/inspector/animation/test/browser_animation_logic_created-time.js b/devtools/client/inspector/animation/test/browser_animation_logic_created-time.js
new file mode 100644
index 0000000000..59e0f4df52
--- /dev/null
+++ b/devtools/client/inspector/animation/test/browser_animation_logic_created-time.js
@@ -0,0 +1,57 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test whether the created time of animation unchanged even if change node.
+
+add_task(async function () {
+ await addTab(URL_ROOT + "doc_custom_playback_rate.html");
+ const { animationInspector, inspector } = await openAnimationInspector();
+
+ info("Check both the created time of animation are same");
+ const baseCreatedTime =
+ animationInspector.state.animations[0].state.createdTime;
+ is(
+ animationInspector.state.animations[1].state.createdTime,
+ baseCreatedTime,
+ "Both created time of animations should be same"
+ );
+
+ info("Check created time after selecting '.div1'");
+ await selectNode(".div1", inspector);
+ await waitUntil(
+ () =>
+ animationInspector.state.animations[0].state.createdTime ===
+ baseCreatedTime
+ );
+ ok(
+ true,
+ "The created time of animation on element of .div1 should unchanged"
+ );
+
+ info("Check created time after selecting '.div2'");
+ await selectNode(".div2", inspector);
+ await waitUntil(
+ () =>
+ animationInspector.state.animations[0].state.createdTime ===
+ baseCreatedTime
+ );
+ ok(
+ true,
+ "The created time of animation on element of .div2 should unchanged"
+ );
+
+ info("Check created time after selecting 'body' again");
+ await selectNode("body", inspector);
+ is(
+ animationInspector.state.animations[0].state.createdTime,
+ baseCreatedTime,
+ "The created time of animation[0] should unchanged"
+ );
+ is(
+ animationInspector.state.animations[1].state.createdTime,
+ baseCreatedTime,
+ "The created time of animation[1] should unchanged"
+ );
+});
diff --git a/devtools/client/inspector/animation/test/browser_animation_logic_mutations.js b/devtools/client/inspector/animation/test/browser_animation_logic_mutations.js
new file mode 100644
index 0000000000..56228a60c2
--- /dev/null
+++ b/devtools/client/inspector/animation/test/browser_animation_logic_mutations.js
@@ -0,0 +1,113 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test for following mutations:
+// * add animation
+// * remove animation
+// * modify animation
+
+add_task(async function () {
+ await addTab(URL_ROOT + "doc_simple_animation.html");
+ await removeAnimatedElementsExcept([
+ ".compositor-all",
+ ".compositor-notall",
+ ".no-compositor",
+ ".still",
+ ]);
+ const { animationInspector, inspector, panel } =
+ await openAnimationInspector();
+
+ info("Checking the mutation for add an animation");
+ const originalAnimationCount = animationInspector.state.animations.length;
+ await setClassAttribute(animationInspector, ".still", "ball no-compositor");
+ await waitUntil(
+ () =>
+ animationInspector.state.animations.length === originalAnimationCount + 1
+ );
+ ok(true, "Count of animation should be plus one to original count");
+
+ info(
+ "Checking added animation existence even the animation name is duplicated"
+ );
+ is(
+ getAnimationNameCount(panel, "no-compositor"),
+ 2,
+ "Count of animation should be plus one to original count"
+ );
+
+ info("Checking the mutation for remove an animation");
+ await setClassAttribute(
+ animationInspector,
+ ".compositor-notall",
+ "ball still"
+ );
+ await waitUntil(
+ () => animationInspector.state.animations.length === originalAnimationCount
+ );
+ ok(
+ true,
+ "Count of animation should be same to original count since we remove an animation"
+ );
+
+ info("Checking the mutation for modify an animation");
+ await selectNode(".compositor-all", inspector);
+ await setStyle(
+ animationInspector,
+ ".compositor-all",
+ "animationDuration",
+ "100s"
+ );
+ await setStyle(
+ animationInspector,
+ ".compositor-all",
+ "animationIterationCount",
+ 1
+ );
+ const summaryGraphPathEl = getSummaryGraphPathElement(
+ panel,
+ "compositor-all"
+ );
+ await waitUntil(() => summaryGraphPathEl.viewBox.baseVal.width === 100000);
+ ok(
+ true,
+ "Width of summary graph path should be 100000 " +
+ "after modifing the duration and iteration count"
+ );
+ await setStyle(
+ animationInspector,
+ ".compositor-all",
+ "animationDelay",
+ "100s"
+ );
+ await waitUntil(() => summaryGraphPathEl.viewBox.baseVal.width === 200000);
+ ok(
+ true,
+ "Width of summary graph path should be 200000 after modifing the delay"
+ );
+ ok(
+ summaryGraphPathEl.parentElement.querySelector(".animation-delay-sign"),
+ "Delay sign element shoud exist"
+ );
+});
+
+function getAnimationNameCount(panel, animationName) {
+ return [...panel.querySelectorAll(".animation-name")].reduce(
+ (count, element) =>
+ element.textContent === animationName ? count + 1 : count,
+ 0
+ );
+}
+
+function getSummaryGraphPathElement(panel, animationName) {
+ for (const animationNameEl of panel.querySelectorAll(".animation-name")) {
+ if (animationNameEl.textContent === animationName) {
+ return animationNameEl
+ .closest(".animation-summary-graph")
+ .querySelector(".animation-summary-graph-path");
+ }
+ }
+
+ return null;
+}
diff --git a/devtools/client/inspector/animation/test/browser_animation_logic_mutations_add_remove_immediately.js b/devtools/client/inspector/animation/test/browser_animation_logic_mutations_add_remove_immediately.js
new file mode 100644
index 0000000000..c57f3c7b3b
--- /dev/null
+++ b/devtools/client/inspector/animation/test/browser_animation_logic_mutations_add_remove_immediately.js
@@ -0,0 +1,29 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test whether the animation inspector will not crash when add animation then remove
+// immediately.
+
+add_task(async function () {
+ const tab = await addTab(
+ URL_ROOT + "doc_mutations_add_remove_immediately.html"
+ );
+ const { inspector, panel } = await openAnimationInspector();
+
+ info("Check state of the animation inspector after fast mutations");
+ const onDispatch = waitForDispatch(inspector.store, "UPDATE_ANIMATIONS");
+ await startMutation(tab);
+ await onDispatch;
+ ok(
+ panel.querySelector(".animation-error-message"),
+ "No animations message should display"
+ );
+});
+
+async function startMutation(tab) {
+ await SpecialPowers.spawn(tab.linkedBrowser, [], async function () {
+ await content.wrappedJSObject.startMutation();
+ });
+}
diff --git a/devtools/client/inspector/animation/test/browser_animation_logic_mutations_fast.js b/devtools/client/inspector/animation/test/browser_animation_logic_mutations_fast.js
new file mode 100644
index 0000000000..516a150e42
--- /dev/null
+++ b/devtools/client/inspector/animation/test/browser_animation_logic_mutations_fast.js
@@ -0,0 +1,24 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test whether the animation inspector will not crash when remove/add animations faster.
+
+add_task(async function () {
+ const tab = await addTab(URL_ROOT + "doc_mutations_fast.html");
+ const { inspector } = await openAnimationInspector();
+
+ info("Check state of the animation inspector after fast mutations");
+ await startFastMutations(tab);
+ ok(
+ inspector.panelWin.document.getElementById("animation-container"),
+ "Animation inspector should be live"
+ );
+});
+
+async function startFastMutations(tab) {
+ await SpecialPowers.spawn(tab.linkedBrowser, [], async function () {
+ await content.wrappedJSObject.startFastMutations();
+ });
+}
diff --git a/devtools/client/inspector/animation/test/browser_animation_logic_mutations_properties.js b/devtools/client/inspector/animation/test/browser_animation_logic_mutations_properties.js
new file mode 100644
index 0000000000..9ec3d58be9
--- /dev/null
+++ b/devtools/client/inspector/animation/test/browser_animation_logic_mutations_properties.js
@@ -0,0 +1,103 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test whether animation was changed after altering following properties.
+// * delay
+// * direction
+// * duration
+// * easing (animationTimingFunction in case of CSS Animationns)
+// * fill
+// * iterations
+// * endDelay (script animation only)
+// * iterationStart (script animation only)
+// * playbackRate (script animation only)
+
+const SEC = 1000;
+const TEST_EFFECT_TIMING = {
+ delay: 20 * SEC,
+ direction: "reverse",
+ duration: 20 * SEC,
+ easing: "steps(1)",
+ endDelay: 20 * SEC,
+ fill: "backwards",
+ iterations: 20,
+ iterationStart: 20 * SEC,
+};
+const TEST_PLAYBACK_RATE = 0.1;
+
+add_task(async function () {
+ await addTab(URL_ROOT + "doc_simple_animation.html");
+ await removeAnimatedElementsExcept([".animated", ".end-delay"]);
+ const { animationInspector } = await openAnimationInspector();
+ await setCSSAnimationProperties(animationInspector);
+ await assertProperties(animationInspector.state.animations[0], false);
+ await setScriptAnimationProperties(animationInspector);
+ await assertProperties(animationInspector.state.animations[1], true);
+});
+
+async function setCSSAnimationProperties(animationInspector) {
+ const properties = {
+ animationDelay: `${TEST_EFFECT_TIMING.delay}ms`,
+ animationDirection: TEST_EFFECT_TIMING.direction,
+ animationDuration: `${TEST_EFFECT_TIMING.duration}ms`,
+ animationFillMode: TEST_EFFECT_TIMING.fill,
+ animationIterationCount: TEST_EFFECT_TIMING.iterations,
+ animationTimingFunction: TEST_EFFECT_TIMING.easing,
+ };
+
+ await setStyles(animationInspector, ".animated", properties);
+}
+
+async function setScriptAnimationProperties(animationInspector) {
+ await setEffectTimingAndPlayback(
+ animationInspector,
+ ".end-delay",
+ TEST_EFFECT_TIMING,
+ TEST_PLAYBACK_RATE
+ );
+}
+
+async function assertProperties(animation, isScriptAnimation) {
+ await waitUntil(() => animation.state.delay === TEST_EFFECT_TIMING.delay);
+ ok(true, `Delay should be ${TEST_EFFECT_TIMING.delay}`);
+
+ await waitUntil(
+ () => animation.state.direction === TEST_EFFECT_TIMING.direction
+ );
+ ok(true, `Direction should be ${TEST_EFFECT_TIMING.direction}`);
+
+ await waitUntil(
+ () => animation.state.duration === TEST_EFFECT_TIMING.duration
+ );
+ ok(true, `Duration should be ${TEST_EFFECT_TIMING.duration}`);
+
+ await waitUntil(() => animation.state.fill === TEST_EFFECT_TIMING.fill);
+ ok(true, `Fill should be ${TEST_EFFECT_TIMING.fill}`);
+
+ await waitUntil(
+ () => animation.state.iterationCount === TEST_EFFECT_TIMING.iterations
+ );
+ ok(true, `Iterations should be ${TEST_EFFECT_TIMING.iterations}`);
+
+ if (isScriptAnimation) {
+ await waitUntil(() => animation.state.easing === TEST_EFFECT_TIMING.easing);
+ ok(true, `Easing should be ${TEST_EFFECT_TIMING.easing}`);
+
+ await waitUntil(
+ () => animation.state.iterationStart === TEST_EFFECT_TIMING.iterationStart
+ );
+ ok(true, `IterationStart should be ${TEST_EFFECT_TIMING.iterationStart}`);
+
+ await waitUntil(() => animation.state.playbackRate === TEST_PLAYBACK_RATE);
+ ok(true, `PlaybackRate should be ${TEST_PLAYBACK_RATE}`);
+ } else {
+ await waitUntil(
+ () =>
+ animation.state.animationTimingFunction === TEST_EFFECT_TIMING.easing
+ );
+
+ ok(true, `AnimationTimingFunction should be ${TEST_EFFECT_TIMING.easing}`);
+ }
+}
diff --git a/devtools/client/inspector/animation/test/browser_animation_logic_overflowed_delay_end-delay.js b/devtools/client/inspector/animation/test/browser_animation_logic_overflowed_delay_end-delay.js
new file mode 100644
index 0000000000..3d1c71b6c3
--- /dev/null
+++ b/devtools/client/inspector/animation/test/browser_animation_logic_overflowed_delay_end-delay.js
@@ -0,0 +1,29 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that animations with an overflowed delay and end delay are not displayed.
+
+add_task(async function () {
+ await addTab(URL_ROOT + "doc_overflowed_delay_end_delay.html");
+ const { panel } = await openAnimationInspector();
+
+ info("Check the number of animation item");
+ const animationItemEls = panel.querySelectorAll(
+ ".animation-list .animation-item"
+ );
+ is(
+ animationItemEls.length,
+ 1,
+ "The number of animations displayed should be 1"
+ );
+
+ info("Check the id of animation displayed");
+ const animationNameEl = animationItemEls[0].querySelector(".animation-name");
+ is(
+ animationNameEl.textContent,
+ "big-iteration-start",
+ "The animation name should be 'big-iteration-start'"
+ );
+});
diff --git a/devtools/client/inspector/animation/test/browser_animation_logic_scroll-amount.js b/devtools/client/inspector/animation/test/browser_animation_logic_scroll-amount.js
new file mode 100644
index 0000000000..11835cd880
--- /dev/null
+++ b/devtools/client/inspector/animation/test/browser_animation_logic_scroll-amount.js
@@ -0,0 +1,70 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test whether the scroll amount of animation and animated property re-calculate after
+// changing selected node.
+
+add_task(async function () {
+ await addTab(URL_ROOT + "doc_simple_animation.html");
+ await removeAnimatedElementsExcept([
+ ".animated",
+ ".multi",
+ ".longhand",
+ ".negative-delay",
+ ]);
+ const { animationInspector, inspector, panel } =
+ await openAnimationInspector();
+
+ info(
+ "Set the scroll amount of animation and animated property to the bottom"
+ );
+ const onDetailRendered = animationInspector.once(
+ "animation-keyframes-rendered"
+ );
+ await clickOnAnimationByTargetSelector(
+ animationInspector,
+ panel,
+ ".longhand"
+ );
+ await onDetailRendered;
+
+ await waitUntil(() => panel.querySelectorAll(".animation-item").length === 5);
+ const bottomAnimationEl = await findAnimationItemByIndex(panel, 4);
+ const bottomAnimatedPropertyEl = panel.querySelector(
+ ".animated-property-item:last-child"
+ );
+ bottomAnimationEl.scrollIntoView(false);
+ bottomAnimatedPropertyEl.scrollIntoView(false);
+
+ info("Hold the scroll amount");
+ const animationInspectionPanel = bottomAnimationEl.closest(
+ ".progress-inspection-panel"
+ );
+ const animatedPropertyInspectionPanel = bottomAnimatedPropertyEl.closest(
+ ".progress-inspection-panel"
+ );
+ const initialScrollTopOfAnimation = animationInspectionPanel.scrollTop;
+ const initialScrollTopOfAnimatedProperty =
+ animatedPropertyInspectionPanel.scrollTop;
+
+ info(
+ "Check whether the scroll amount re-calculate after changing the count of items"
+ );
+ await selectNode(".negative-delay", inspector);
+ await waitUntil(
+ () =>
+ initialScrollTopOfAnimation > animationInspectionPanel.scrollTop &&
+ initialScrollTopOfAnimatedProperty >
+ animatedPropertyInspectionPanel.scrollTop
+ );
+ ok(
+ true,
+ "Scroll amount for animation list should be less than previous state"
+ );
+ ok(
+ true,
+ "Scroll amount for animated property list should be less than previous state"
+ );
+});
diff --git a/devtools/client/inspector/animation/test/browser_animation_pause-resume-button.js b/devtools/client/inspector/animation/test/browser_animation_pause-resume-button.js
new file mode 100644
index 0000000000..8a8d9f848b
--- /dev/null
+++ b/devtools/client/inspector/animation/test/browser_animation_pause-resume-button.js
@@ -0,0 +1,41 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test for following PauseResumeButton component:
+// * element existence
+// * state during running animations
+// * state during pausing animations
+// * make animations to pause by push button
+// * make animations to resume by push button
+
+add_task(async function () {
+ await addTab(URL_ROOT + "doc_custom_playback_rate.html");
+ const { animationInspector, panel } = await openAnimationInspector();
+
+ info("Checking pause/resume button existence");
+ const buttonEl = panel.querySelector(".pause-resume-button");
+ ok(buttonEl, "pause/resume button should exist");
+
+ info("Checking state during running animations");
+ ok(
+ !buttonEl.classList.contains("paused"),
+ "State of button should be running"
+ );
+
+ info("Checking button makes animations to pause");
+ clickOnPauseResumeButton(animationInspector, panel);
+ await waitUntilAnimationsPlayState(animationInspector, "paused");
+ ok(true, "All of animtion are paused");
+ ok(buttonEl.classList.contains("paused"), "State of button should be paused");
+
+ info("Checking button makes animations to resume");
+ clickOnPauseResumeButton(animationInspector, panel);
+ await waitUntilAnimationsPlayState(animationInspector, "running");
+ ok(true, "All of animtion are running");
+ ok(
+ !buttonEl.classList.contains("paused"),
+ "State of button should be resumed"
+ );
+});
diff --git a/devtools/client/inspector/animation/test/browser_animation_pause-resume-button_end-time.js b/devtools/client/inspector/animation/test/browser_animation_pause-resume-button_end-time.js
new file mode 100644
index 0000000000..896ae5d2ef
--- /dev/null
+++ b/devtools/client/inspector/animation/test/browser_animation_pause-resume-button_end-time.js
@@ -0,0 +1,79 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test whether the animation can rewind if the current time is over end time when
+// the resume button clicked.
+
+add_task(async function () {
+ await addTab(URL_ROOT + "doc_simple_animation.html");
+ await removeAnimatedElementsExcept([
+ ".animated",
+ ".end-delay",
+ ".long",
+ ".negative-delay",
+ ]);
+ const { animationInspector, panel } = await openAnimationInspector();
+
+ info("Check animations state after resuming with infinite animation");
+ info("Make the current time of animation to be over its end time");
+ clickOnCurrentTimeScrubberController(animationInspector, panel, 1);
+ await waitUntilAnimationsPlayState(animationInspector, "paused");
+ info("Resume animations");
+ clickOnPauseResumeButton(animationInspector, panel);
+ await wait(1000);
+ assertPlayState(animationInspector.state.animations, [
+ "running",
+ "finished",
+ "finished",
+ "finished",
+ ]);
+ clickOnCurrentTimeScrubberController(animationInspector, panel, 0);
+ await waitUntilAnimationsPlayState(animationInspector, "paused");
+
+ info("Check animations state after resuming without infinite animation");
+ info("Remove infinite animation");
+ await setClassAttribute(animationInspector, ".animated", "ball still");
+ await waitUntil(() => panel.querySelectorAll(".animation-item").length === 3);
+
+ info("Make the current time of animation to be over its end time");
+ clickOnCurrentTimeScrubberController(animationInspector, panel, 1.1);
+ await waitUntilAnimationsPlayState(animationInspector, "paused");
+ await changePlaybackRateSelector(animationInspector, panel, 0.1);
+ info("Resume animations");
+ clickOnPauseResumeButton(animationInspector, panel);
+ await waitUntilAnimationsPlayState(animationInspector, "running");
+ assertCurrentTimeLessThanDuration(animationInspector.state.animations);
+ assertScrubberPosition(panel);
+});
+
+function assertPlayState(animations, expectedState) {
+ animations.forEach((animation, index) => {
+ is(
+ animation.state.playState,
+ expectedState[index],
+ `The playState of animation [${index}] should be ${expectedState[index]}`
+ );
+ });
+}
+
+function assertCurrentTimeLessThanDuration(animations) {
+ animations.forEach((animation, index) => {
+ Assert.less(
+ animation.state.currentTime,
+ animation.state.duration,
+ `The current time of animation[${index}] should be less than its duration`
+ );
+ });
+}
+
+function assertScrubberPosition(panel) {
+ const scrubberEl = panel.querySelector(".current-time-scrubber");
+ const marginInlineStart = parseFloat(scrubberEl.style.marginInlineStart);
+ Assert.greaterOrEqual(
+ marginInlineStart,
+ 0,
+ "The translateX of scrubber position should be zero or more"
+ );
+}
diff --git a/devtools/client/inspector/animation/test/browser_animation_pause-resume-button_respectively.js b/devtools/client/inspector/animation/test/browser_animation_pause-resume-button_respectively.js
new file mode 100644
index 0000000000..ad84e4c257
--- /dev/null
+++ b/devtools/client/inspector/animation/test/browser_animation_pause-resume-button_respectively.js
@@ -0,0 +1,96 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test whether pausing/resuming the each animations correctly.
+
+add_task(async function () {
+ await addTab(URL_ROOT + "doc_simple_animation.html");
+ await removeAnimatedElementsExcept([".animated", ".compositor-all"]);
+ const { animationInspector, inspector, panel } =
+ await openAnimationInspector();
+ const buttonEl = panel.querySelector(".pause-resume-button");
+
+ info(
+ "Check '.compositor-all' animation is still running " +
+ "after even pausing '.animated' animation"
+ );
+ await selectNode(".animated", inspector);
+ clickOnPauseResumeButton(animationInspector, panel);
+ await waitUntilAnimationsPlayState(animationInspector, "paused");
+ ok(buttonEl.classList.contains("paused"), "State of button should be paused");
+ await selectNode("body", inspector);
+ await assertStatus(
+ animationInspector.state.animations,
+ buttonEl,
+ ["paused", "running"],
+ false
+ );
+
+ info(
+ "Check both animations are paused after clicking pause/resume " +
+ "while displaying both animations"
+ );
+ clickOnPauseResumeButton(animationInspector, panel);
+ await assertStatus(
+ animationInspector.state.animations,
+ buttonEl,
+ ["paused", "paused"],
+ true
+ );
+
+ info(
+ "Check '.animated' animation is still paused " +
+ "after even resuming '.compositor-all' animation"
+ );
+ await selectNode(".compositor-all", inspector);
+ clickOnPauseResumeButton(animationInspector, panel);
+ await waitUntil(() =>
+ animationInspector.state.animations.some(
+ a => a.state.playState === "running"
+ )
+ );
+ ok(
+ !buttonEl.classList.contains("paused"),
+ "State of button should be running"
+ );
+ await selectNode("body", inspector);
+ await assertStatus(
+ animationInspector.state.animations,
+ buttonEl,
+ ["paused", "running"],
+ false
+ );
+});
+
+async function assertStatus(
+ animations,
+ buttonEl,
+ expectedAnimationStates,
+ shouldButtonPaused
+) {
+ await waitUntil(() => {
+ for (let i = 0; i < expectedAnimationStates.length; i++) {
+ const animation = animations[i];
+ const state = expectedAnimationStates[i];
+ if (animation.state.playState !== state) {
+ return false;
+ }
+ }
+ return true;
+ });
+ expectedAnimationStates.forEach((state, index) => {
+ is(
+ animations[index].state.playState,
+ state,
+ `Animation ${index} should be ${state}`
+ );
+ });
+
+ is(
+ buttonEl.classList.contains("paused"),
+ shouldButtonPaused,
+ "State of button is correct"
+ );
+}
diff --git a/devtools/client/inspector/animation/test/browser_animation_pause-resume-button_spacebar.js b/devtools/client/inspector/animation/test/browser_animation_pause-resume-button_spacebar.js
new file mode 100644
index 0000000000..7a27b1bd07
--- /dev/null
+++ b/devtools/client/inspector/animation/test/browser_animation_pause-resume-button_spacebar.js
@@ -0,0 +1,49 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test for following PauseResumeButton component with spacebar:
+// * make animations to pause/resume by spacebar
+// * combination with other UI components
+
+add_task(async function () {
+ await addTab(URL_ROOT + "doc_custom_playback_rate.html");
+ const { animationInspector, panel } = await openAnimationInspector();
+
+ info("Checking spacebar makes animations to pause");
+ await testPauseAndResumeBySpacebar(animationInspector, panel);
+
+ info(
+ "Checking spacebar makes animations to pause when the button has the focus"
+ );
+ const pauseResumeButton = panel.querySelector(".pause-resume-button");
+ await testPauseAndResumeBySpacebar(animationInspector, pauseResumeButton);
+
+ info("Checking spacebar works with other UI components");
+ // To pause
+ clickOnPauseResumeButton(animationInspector, panel);
+ await waitUntilAnimationsPlayState(animationInspector, "paused");
+ // To resume
+ sendSpaceKeyEvent(animationInspector, panel);
+ await waitUntilAnimationsPlayState(animationInspector, "running");
+ // To pause
+ clickOnCurrentTimeScrubberController(animationInspector, panel, 0.5);
+ await waitUntilAnimationsPlayState(animationInspector, "paused");
+ // To resume
+ clickOnPauseResumeButton(animationInspector, panel);
+ await waitUntilAnimationsPlayState(animationInspector, "running");
+ // To pause
+ sendSpaceKeyEvent(animationInspector, panel);
+ await waitUntilAnimationsPlayState(animationInspector, "paused");
+ ok(true, "All components that can make animations pause/resume works fine");
+});
+
+async function testPauseAndResumeBySpacebar(animationInspector, element) {
+ await sendSpaceKeyEvent(animationInspector, element);
+ await waitUntilAnimationsPlayState(animationInspector, "paused");
+ ok(true, "Space key can pause animations");
+ await sendSpaceKeyEvent(animationInspector, element);
+ await waitUntilAnimationsPlayState(animationInspector, "running");
+ ok(true, "Space key can resume animations");
+}
diff --git a/devtools/client/inspector/animation/test/browser_animation_playback-rate-selector.js b/devtools/client/inspector/animation/test/browser_animation_playback-rate-selector.js
new file mode 100644
index 0000000000..8552eae138
--- /dev/null
+++ b/devtools/client/inspector/animation/test/browser_animation_playback-rate-selector.js
@@ -0,0 +1,81 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test for following PlaybackRateSelector component:
+// * element existence
+// * make playback rate of animations by the selector
+// * in case of animations have mixed playback rate
+// * in case of animations have playback rate which is not default selectable value
+
+add_task(async function () {
+ await addTab(URL_ROOT + "doc_custom_playback_rate.html");
+ const { animationInspector, inspector, panel } =
+ await openAnimationInspector();
+
+ info("Checking playback rate selector existence");
+ const selectEl = panel.querySelector(".playback-rate-selector");
+ ok(selectEl, "scrubber controller should exist");
+
+ info(
+ "Checking playback rate existence which includes custom rate of animations"
+ );
+ const expectedPlaybackRates = [0.1, 0.25, 0.5, 1, 1.5, 2, 5, 10];
+ await assertPlaybackRateOptions(selectEl, expectedPlaybackRates);
+
+ info("Checking selected playback rate");
+ is(Number(selectEl.value), 1.5, "Selected option should be 1.5");
+
+ info("Checking playback rate of animations");
+ await changePlaybackRateSelector(animationInspector, panel, 0.5);
+ await assertPlaybackRate(animationInspector, 0.5);
+
+ info("Checking mixed playback rate");
+ await selectNode("div", inspector);
+ await waitUntil(() => panel.querySelectorAll(".animation-item").length === 1);
+ await changePlaybackRateSelector(animationInspector, panel, 2);
+ await assertPlaybackRate(animationInspector, 2);
+ await selectNode("body", inspector);
+ await waitUntil(() => panel.querySelectorAll(".animation-item").length === 2);
+ await waitUntil(() => selectEl.value === "");
+ ok(true, "Selected option should be empty");
+
+ info("Checking playback rate after re-setting");
+ await changePlaybackRateSelector(animationInspector, panel, 1);
+ await assertPlaybackRate(animationInspector, 1);
+
+ info(
+ "Checking whether custom playback rate exist " +
+ "after selecting another playback rate"
+ );
+ await assertPlaybackRateOptions(selectEl, expectedPlaybackRates);
+});
+
+async function assertPlaybackRate(animationInspector, rate) {
+ await waitUntil(() =>
+ animationInspector.state?.animations.every(
+ ({ state }) => state.playbackRate === rate
+ )
+ );
+ ok(true, `Playback rate of animations should be ${rate}`);
+}
+
+async function assertPlaybackRateOptions(selectEl, expectedPlaybackRates) {
+ await waitUntil(() => {
+ if (selectEl.options.length !== expectedPlaybackRates.length) {
+ return false;
+ }
+
+ for (let i = 0; i < selectEl.options.length; i++) {
+ const optionEl = selectEl.options[i];
+ const expectedPlaybackRate = expectedPlaybackRates[i];
+ if (Number(optionEl.value) !== expectedPlaybackRate) {
+ return false;
+ }
+ }
+
+ return true;
+ });
+ ok(true, "Content of playback rate options are correct");
+}
diff --git a/devtools/client/inspector/animation/test/browser_animation_pseudo-element.js b/devtools/client/inspector/animation/test/browser_animation_pseudo-element.js
new file mode 100644
index 0000000000..00e267f9f8
--- /dev/null
+++ b/devtools/client/inspector/animation/test/browser_animation_pseudo-element.js
@@ -0,0 +1,129 @@
+/* Any copyright is dedicated to the Public Domain.
+http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test for pseudo element.
+
+const TEST_DATA = [
+ {
+ expectedTargetLabel: "::before",
+ expectedAnimationNameLabel: "body",
+ expectedKeyframsGraphPathSegments: [
+ { x: 0, y: 0 },
+ { x: 1000, y: 100 },
+ ],
+ },
+ {
+ expectedTargetLabel: "::before",
+ expectedAnimationNameLabel: "div-before",
+ expectedKeyframsGraphPathSegments: [
+ { x: 0, y: 100 },
+ { x: 1000, y: 0 },
+ ],
+ },
+ {
+ expectedTargetLabel: "::after",
+ expectedAnimationNameLabel: "div-after",
+ },
+ {
+ expectedTargetLabel: "::marker",
+ expectedAnimationNameLabel: "div-marker",
+ },
+];
+
+add_task(async function () {
+ await addTab(URL_ROOT + "doc_pseudo.html");
+ const { animationInspector, inspector, panel } =
+ await openAnimationInspector();
+
+ info("Checking count of animation item for pseudo elements");
+ is(
+ panel.querySelectorAll(".animation-list .animation-item").length,
+ TEST_DATA.length,
+ `Count of animation item should be ${TEST_DATA.length}`
+ );
+
+ info("Checking content of each animation item");
+ for (let i = 0; i < TEST_DATA.length; i++) {
+ const testData = TEST_DATA[i];
+ info(`Checking pseudo element for ${testData.expectedTargetLabel}`);
+ const animationItemEl = await findAnimationItemByIndex(panel, i);
+
+ info("Checking text content of animation target");
+ const animationTargetEl = animationItemEl.querySelector(
+ ".animation-list .animation-item .animation-target"
+ );
+ is(
+ animationTargetEl.textContent,
+ testData.expectedTargetLabel,
+ `Text content of animation target[${i}] should be ${testData.expectedTarget}`
+ );
+
+ info("Checking text content of animation name");
+ const animationNameEl = animationItemEl.querySelector(".animation-name");
+ is(
+ animationNameEl.textContent,
+ testData.expectedAnimationNameLabel,
+ `The animation name should be ${testData.expectedAnimationNameLabel}`
+ );
+ }
+
+ info(
+ "Checking whether node is selected correctly " +
+ "when click on the first inspector icon on Reps component"
+ );
+ let onDetailRendered = animationInspector.once(
+ "animation-keyframes-rendered"
+ );
+ await clickOnTargetNode(animationInspector, panel, 0);
+ await onDetailRendered;
+ assertAnimationCount(panel, 1);
+ assertAnimationNameLabel(panel, TEST_DATA[0].expectedAnimationNameLabel);
+ assertKeyframesGraphPathSegments(
+ panel,
+ TEST_DATA[0].expectedKeyframsGraphPathSegments
+ );
+
+ info("Select <body> again to reset the animation list");
+ await selectNode("body", inspector);
+
+ info(
+ "Checking whether node is selected correctly " +
+ "when click on the second inspector icon on Reps component"
+ );
+ onDetailRendered = animationInspector.once("animation-keyframes-rendered");
+ await clickOnTargetNode(animationInspector, panel, 1);
+ await onDetailRendered;
+ assertAnimationCount(panel, 1);
+ assertAnimationNameLabel(panel, TEST_DATA[1].expectedAnimationNameLabel);
+ assertKeyframesGraphPathSegments(
+ panel,
+ TEST_DATA[1].expectedKeyframsGraphPathSegments
+ );
+});
+
+function assertAnimationCount(panel, expectedCount) {
+ info("Checking count of animation item");
+ is(
+ panel.querySelectorAll(".animation-list .animation-item").length,
+ expectedCount,
+ `Count of animation item should be ${expectedCount}`
+ );
+}
+
+function assertAnimationNameLabel(panel, expectedAnimationNameLabel) {
+ info("Checking the animation name label");
+ is(
+ panel.querySelector(".animation-list .animation-item .animation-name")
+ .textContent,
+ expectedAnimationNameLabel,
+ `The animation name should be ${expectedAnimationNameLabel}`
+ );
+}
+
+function assertKeyframesGraphPathSegments(panel, expectedPathSegments) {
+ info("Checking the keyframes graph path segments");
+ const pathEl = panel.querySelector(".keyframes-graph-path path");
+ assertPathSegments(pathEl, true, expectedPathSegments);
+}
diff --git a/devtools/client/inspector/animation/test/browser_animation_rewind-button.js b/devtools/client/inspector/animation/test/browser_animation_rewind-button.js
new file mode 100644
index 0000000000..f74518095b
--- /dev/null
+++ b/devtools/client/inspector/animation/test/browser_animation_rewind-button.js
@@ -0,0 +1,33 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+requestLongerTimeout(2);
+
+// Test for following RewindButton component:
+// * element existence
+// * make animations to rewind to zero
+// * the state should be always paused after rewinding
+
+add_task(async function () {
+ await addTab(URL_ROOT + "doc_multi_timings.html");
+ await removeAnimatedElementsExcept([".delay-negative", ".delay-positive"]);
+ const { animationInspector, panel } = await openAnimationInspector();
+
+ info("Checking button existence");
+ ok(panel.querySelector(".rewind-button"), "Rewind button should exist");
+
+ info("Checking rewind button makes animations to rewind to zero");
+ clickOnRewindButton(animationInspector, panel);
+ await waitUntilAnimationsPlayState(animationInspector, "paused");
+ await waitUntilCurrentTimeChangedAt(animationInspector, 0);
+ ok(true, "Rewind button make current time 0");
+
+ info("Checking rewind button makes animations after clicking scrubber");
+ clickOnCurrentTimeScrubberController(animationInspector, panel, 0.5);
+ clickOnRewindButton(animationInspector, panel);
+ await waitUntilCurrentTimeChangedAt(animationInspector, 0);
+ await waitUntilAnimationsPlayState(animationInspector, "paused");
+ ok(true, "Rewind button make current time 0 even after clicking scrubber");
+});
diff --git a/devtools/client/inspector/animation/test/browser_animation_short-duration.js b/devtools/client/inspector/animation/test/browser_animation_short-duration.js
new file mode 100644
index 0000000000..c953d886ff
--- /dev/null
+++ b/devtools/client/inspector/animation/test/browser_animation_short-duration.js
@@ -0,0 +1,43 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test tooltips and iteration path of summary graph with short duration animation.
+
+add_task(async function () {
+ await addTab(URL_ROOT + "doc_short_duration.html");
+ const { panel } = await openAnimationInspector();
+
+ const animationItemEl = await findAnimationItemByTargetSelector(
+ panel,
+ ".short"
+ );
+ const summaryGraphEl = animationItemEl.querySelector(
+ ".animation-summary-graph"
+ );
+
+ info("Check tooltip");
+ assertTooltip(summaryGraphEl);
+
+ info("Check iteration path");
+ assertIterationPath(summaryGraphEl);
+});
+
+function assertTooltip(summaryGraphEl) {
+ const tooltip = summaryGraphEl.getAttribute("title");
+ const expected = "Duration: 0s";
+ ok(tooltip.includes(expected), `Tooltip should include '${expected}'`);
+}
+
+function assertIterationPath(summaryGraphEl) {
+ const pathEl = summaryGraphEl.querySelector(
+ ".animation-computed-timing-path .animation-iteration-path"
+ );
+ const expected = [
+ { x: 0, y: 0 },
+ { x: 0.999, y: 99.9 },
+ { x: 1, y: 0 },
+ ];
+ assertPathSegments(pathEl, true, expected);
+}
diff --git a/devtools/client/inspector/animation/test/browser_animation_summary-graph_animation-name.js b/devtools/client/inspector/animation/test/browser_animation_summary-graph_animation-name.js
new file mode 100644
index 0000000000..0e9c52449d
--- /dev/null
+++ b/devtools/client/inspector/animation/test/browser_animation_summary-graph_animation-name.js
@@ -0,0 +1,63 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test for following AnimationName component works.
+// * element existance
+// * name text
+
+const TEST_DATA = [
+ {
+ targetClass: "cssanimation-normal",
+ expectedLabel: "cssanimation",
+ },
+ {
+ targetClass: "cssanimation-linear",
+ expectedLabel: "cssanimation",
+ },
+ {
+ targetClass: "delay-positive",
+ expectedLabel: "test-delay-animation",
+ },
+ {
+ targetClass: "delay-negative",
+ expectedLabel: "test-negative-delay-animation",
+ },
+ {
+ targetClass: "easing-step",
+ },
+];
+
+add_task(async function () {
+ await addTab(URL_ROOT + "doc_multi_timings.html");
+ await removeAnimatedElementsExcept(TEST_DATA.map(t => `.${t.targetClass}`));
+ const { panel } = await openAnimationInspector();
+
+ for (const { targetClass, expectedLabel } of TEST_DATA) {
+ const animationItemEl = await findAnimationItemByTargetSelector(
+ panel,
+ `.${targetClass}`
+ );
+
+ info(`Checking animation name element existance for ${targetClass}`);
+ const animationNameEl = animationItemEl.querySelector(".animation-name");
+
+ if (expectedLabel) {
+ ok(
+ animationNameEl,
+ "The animation name element should be in animation item element"
+ );
+ is(
+ animationNameEl.textContent,
+ expectedLabel,
+ `The animation name should be ${expectedLabel}`
+ );
+ } else {
+ ok(
+ !animationNameEl,
+ "The animation name element should not be in animation item element"
+ );
+ }
+ }
+});
diff --git a/devtools/client/inspector/animation/test/browser_animation_summary-graph_compositor.js b/devtools/client/inspector/animation/test/browser_animation_summary-graph_compositor.js
new file mode 100644
index 0000000000..186c54cba6
--- /dev/null
+++ b/devtools/client/inspector/animation/test/browser_animation_summary-graph_compositor.js
@@ -0,0 +1,121 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that when animations displayed in the timeline are running on the
+// compositor, they get a special icon and information in the tooltip.
+
+requestLongerTimeout(2);
+
+add_task(async function () {
+ await addTab(URL_ROOT + "doc_simple_animation.html");
+ await removeAnimatedElementsExcept([
+ ".compositor-all",
+ ".compositor-notall",
+ ".no-compositor",
+ ]);
+ const { animationInspector, inspector, panel } =
+ await openAnimationInspector();
+
+ info("Check animation whose all properties are running on compositor");
+ const summaryGraphAllEl = await findSummaryGraph(".compositor-all", panel);
+ ok(
+ summaryGraphAllEl.classList.contains("compositor"),
+ "The element has the compositor css class"
+ );
+ ok(
+ hasTooltip(
+ summaryGraphAllEl,
+ ANIMATION_L10N.getStr("player.allPropertiesOnCompositorTooltip")
+ ),
+ "The element has the right tooltip content"
+ );
+
+ info("Check animation is not running on compositor");
+ const summaryGraphNoEl = await findSummaryGraph(".no-compositor", panel);
+ ok(
+ !summaryGraphNoEl.classList.contains("compositor"),
+ "The element does not have the compositor css class"
+ );
+ ok(
+ !hasTooltip(
+ summaryGraphNoEl,
+ ANIMATION_L10N.getStr("player.allPropertiesOnCompositorTooltip")
+ ),
+ "The element does not have oncompositor tooltip content"
+ );
+ ok(
+ !hasTooltip(
+ summaryGraphNoEl,
+ ANIMATION_L10N.getStr("player.somePropertiesOnCompositorTooltip")
+ ),
+ "The element does not have oncompositor tooltip content"
+ );
+
+ info(
+ "Select a node has animation whose some properties are running on compositor"
+ );
+ await selectNode(".compositor-notall", inspector);
+ const summaryGraphEl = await findSummaryGraph(".compositor-notall", panel);
+ ok(
+ summaryGraphEl.classList.contains("compositor"),
+ "The element has the compositor css class"
+ );
+ ok(
+ hasTooltip(
+ summaryGraphEl,
+ ANIMATION_L10N.getStr("player.somePropertiesOnCompositorTooltip")
+ ),
+ "The element has the right tooltip content"
+ );
+
+ info("Check compositor sign after pausing");
+ clickOnPauseResumeButton(animationInspector, panel);
+ await waitUntil(() => !summaryGraphEl.classList.contains("compositor"));
+ ok(
+ true,
+ "The element should not have the compositor css class after pausing"
+ );
+
+ info("Check compositor sign after resuming");
+ clickOnPauseResumeButton(animationInspector, panel);
+ await waitUntil(() => summaryGraphEl.classList.contains("compositor"));
+ ok(true, "The element should have the compositor css class after resuming");
+
+ info("Check compositor sign after rewind");
+ clickOnRewindButton(animationInspector, panel);
+ await waitUntil(() => !summaryGraphEl.classList.contains("compositor"));
+ ok(
+ true,
+ "The element should not have the compositor css class after rewinding"
+ );
+ clickOnPauseResumeButton(animationInspector, panel);
+ await waitUntil(() => summaryGraphEl.classList.contains("compositor"));
+ ok(true, "The element should have the compositor css class after resuming");
+
+ info("Check compositor sign after setting the current time");
+ clickOnCurrentTimeScrubberController(animationInspector, panel, 0.5);
+ await waitUntil(() => !summaryGraphEl.classList.contains("compositor"));
+ ok(
+ true,
+ "The element should not have the compositor css class " +
+ "after setting the current time"
+ );
+ clickOnPauseResumeButton(animationInspector, panel);
+ await waitUntil(() => summaryGraphEl.classList.contains("compositor"));
+ ok(true, "The element should have the compositor css class after resuming");
+});
+
+async function findSummaryGraph(selector, panel) {
+ const animationItemEl = await findAnimationItemByTargetSelector(
+ panel,
+ selector
+ );
+ return animationItemEl.querySelector(".animation-summary-graph");
+}
+
+function hasTooltip(summaryGraphEl, expected) {
+ const tooltip = summaryGraphEl.getAttribute("title");
+ return tooltip.includes(expected);
+}
diff --git a/devtools/client/inspector/animation/test/browser_animation_summary-graph_computed-timing-path_1.js b/devtools/client/inspector/animation/test/browser_animation_summary-graph_computed-timing-path_1.js
new file mode 100644
index 0000000000..a81b971559
--- /dev/null
+++ b/devtools/client/inspector/animation/test/browser_animation_summary-graph_computed-timing-path_1.js
@@ -0,0 +1,208 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test for following ComputedTimingPath component works.
+// * element existance
+// * iterations: path, count
+// * delay: path
+// * fill: path
+// * endDelay: path
+
+/* import-globals-from summary-graph_computed-timing-path_head.js */
+Services.scriptloader.loadSubScript(
+ CHROME_URL_ROOT + "summary-graph_computed-timing-path_head.js",
+ this
+);
+
+const TEST_DATA = [
+ {
+ targetClass: "cssanimation-normal",
+ expectedIterationPathList: [
+ [
+ { x: 0, y: 0 },
+ { x: 250000, y: 40.851 },
+ { x: 500000, y: 80.24 },
+ { x: 750000, y: 96.05 },
+ { x: 1000000, y: 100 },
+ { x: 1000000, y: 0 },
+ ],
+ ],
+ },
+ {
+ targetClass: "cssanimation-linear",
+ expectedIterationPathList: [
+ [
+ { x: 0, y: 0 },
+ { x: 250000, y: 25 },
+ { x: 500000, y: 50 },
+ { x: 750000, y: 75 },
+ { x: 1000000, y: 100 },
+ { x: 1000000, y: 0 },
+ ],
+ ],
+ },
+ {
+ targetClass: "delay-positive",
+ expectedDelayPath: [
+ { x: 0, y: 0 },
+ { x: 500000, y: 0 },
+ ],
+ expectedIterationPathList: [
+ [
+ { x: 500000, y: 0 },
+ { x: 750000, y: 25 },
+ { x: 1000000, y: 50 },
+ { x: 1250000, y: 75 },
+ { x: 1500000, y: 100 },
+ { x: 1500000, y: 0 },
+ ],
+ ],
+ },
+ {
+ targetClass: "easing-step",
+ expectedIterationPathList: [
+ [
+ { x: 0, y: 0 },
+ { x: 499999, y: 0 },
+ { x: 500000, y: 50 },
+ { x: 999999, y: 50 },
+ { x: 1000000, y: 0 },
+ ],
+ ],
+ },
+ {
+ targetClass: "enddelay-positive",
+ expectedIterationPathList: [
+ [
+ { x: 0, y: 0 },
+ { x: 250000, y: 25 },
+ { x: 500000, y: 50 },
+ { x: 750000, y: 75 },
+ { x: 1000000, y: 100 },
+ { x: 1000000, y: 0 },
+ ],
+ ],
+ expectedEndDelayPath: [
+ { x: 1000000, y: 0 },
+ { x: 1500000, y: 0 },
+ ],
+ },
+ {
+ targetClass: "enddelay-negative",
+ expectedIterationPathList: [
+ [
+ { x: 0, y: 0 },
+ { x: 250000, y: 25 },
+ { x: 500000, y: 50 },
+ { x: 500000, y: 0 },
+ ],
+ ],
+ },
+ {
+ targetClass: "enddelay-with-fill-forwards",
+ expectedIterationPathList: [
+ [
+ { x: 0, y: 0 },
+ { x: 250000, y: 25 },
+ { x: 500000, y: 50 },
+ { x: 750000, y: 75 },
+ { x: 1000000, y: 100 },
+ { x: 1000000, y: 0 },
+ ],
+ ],
+ expectedEndDelayPath: [
+ { x: 1000000, y: 0 },
+ { x: 1000000, y: 100 },
+ { x: 1500000, y: 100 },
+ { x: 1500000, y: 0 },
+ ],
+ expectedForwardsPath: [
+ { x: 1500000, y: 0 },
+ { x: 1500000, y: 100 },
+ ],
+ },
+ {
+ targetClass: "enddelay-with-iterations-infinity",
+ expectedIterationPathList: [
+ [
+ { x: 0, y: 0 },
+ { x: 250000, y: 25 },
+ { x: 500000, y: 50 },
+ { x: 750000, y: 75 },
+ { x: 1000000, y: 100 },
+ { x: 1000000, y: 0 },
+ ],
+ [
+ { x: 1000000, y: 0 },
+ { x: 1250000, y: 25 },
+ { x: 1500000, y: 50 },
+ ],
+ ],
+ isInfinity: true,
+ },
+ {
+ targetClass: "direction-alternate-with-iterations-infinity",
+ expectedIterationPathList: [
+ [
+ { x: 0, y: 0 },
+ { x: 250000, y: 25 },
+ { x: 500000, y: 50 },
+ { x: 750000, y: 75 },
+ { x: 1000000, y: 100 },
+ { x: 1000000, y: 0 },
+ ],
+ [
+ { x: 1000000, y: 0 },
+ { x: 1000000, y: 100 },
+ { x: 1250000, y: 75 },
+ { x: 1500000, y: 50 },
+ ],
+ ],
+ isInfinity: true,
+ },
+ {
+ targetClass: "direction-alternate-reverse-with-iterations-infinity",
+ expectedIterationPathList: [
+ [
+ { x: 0, y: 0 },
+ { x: 0, y: 100 },
+ { x: 250000, y: 75 },
+ { x: 500000, y: 50 },
+ { x: 750000, y: 25 },
+ { x: 1000000, y: 0 },
+ ],
+ [
+ { x: 1000000, y: 0 },
+ { x: 1250000, y: 25 },
+ { x: 1500000, y: 50 },
+ ],
+ ],
+ isInfinity: true,
+ },
+ {
+ targetClass: "direction-reverse-with-iterations-infinity",
+ expectedIterationPathList: [
+ [
+ { x: 0, y: 0 },
+ { x: 0, y: 100 },
+ { x: 250000, y: 75 },
+ { x: 500000, y: 50 },
+ { x: 750000, y: 25 },
+ { x: 1000000, y: 0 },
+ ],
+ [
+ { x: 1000000, y: 0 },
+ { x: 1000000, y: 100 },
+ { x: 1250000, y: 75 },
+ { x: 1500000, y: 50 },
+ ],
+ ],
+ isInfinity: true,
+ },
+];
+
+add_task(async function () {
+ await testComputedTimingPath(TEST_DATA);
+});
diff --git a/devtools/client/inspector/animation/test/browser_animation_summary-graph_computed-timing-path_2.js b/devtools/client/inspector/animation/test/browser_animation_summary-graph_computed-timing-path_2.js
new file mode 100644
index 0000000000..e1e4c52ba6
--- /dev/null
+++ b/devtools/client/inspector/animation/test/browser_animation_summary-graph_computed-timing-path_2.js
@@ -0,0 +1,192 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test for following ComputedTimingPath component works.
+// * element existance
+// * iterations: path, count
+// * delay: path
+// * fill: path
+// * endDelay: path
+
+/* import-globals-from summary-graph_computed-timing-path_head.js */
+Services.scriptloader.loadSubScript(
+ CHROME_URL_ROOT + "summary-graph_computed-timing-path_head.js",
+ this
+);
+
+const TEST_DATA = [
+ {
+ targetClass: "fill-backwards",
+ expectedIterationPathList: [
+ [
+ { x: 0, y: 0 },
+ { x: 250000, y: 25 },
+ { x: 500000, y: 50 },
+ { x: 750000, y: 75 },
+ { x: 1000000, y: 100 },
+ { x: 1000000, y: 0 },
+ ],
+ ],
+ },
+ {
+ targetClass: "fill-backwards-with-delay-iterationstart",
+ expectedDelayPath: [
+ { x: 0, y: 0 },
+ { x: 0, y: 50 },
+ { x: 500000, y: 50 },
+ { x: 500000, y: 0 },
+ ],
+ expectedIterationPathList: [
+ [
+ { x: 500000, y: 50 },
+ { x: 750000, y: 75 },
+ { x: 1000000, y: 100 },
+ { x: 1000000, y: 0 },
+ ],
+ [
+ { x: 1000000, y: 0 },
+ { x: 1250000, y: 25 },
+ { x: 1500000, y: 50 },
+ { x: 1500000, y: 0 },
+ ],
+ ],
+ },
+ {
+ targetClass: "fill-both",
+ expectedIterationPathList: [
+ [
+ { x: 0, y: 0 },
+ { x: 250000, y: 25 },
+ { x: 500000, y: 50 },
+ { x: 750000, y: 75 },
+ { x: 1000000, y: 100 },
+ { x: 1000000, y: 0 },
+ ],
+ ],
+ expectedForwardsPath: [
+ { x: 1000000, y: 0 },
+ { x: 1000000, y: 100 },
+ { x: 1500000, y: 100 },
+ { x: 1500000, y: 0 },
+ ],
+ },
+ {
+ targetClass: "fill-both-width-delay-iterationstart",
+ expectedDelayPath: [
+ { x: 0, y: 0 },
+ { x: 0, y: 50 },
+ { x: 500000, y: 50 },
+ { x: 500000, y: 0 },
+ ],
+ expectedIterationPathList: [
+ [
+ { x: 500000, y: 50 },
+ { x: 750000, y: 75 },
+ { x: 1000000, y: 100 },
+ { x: 1000000, y: 0 },
+ ],
+ [
+ { x: 1000000, y: 0 },
+ { x: 1250000, y: 25 },
+ { x: 1500000, y: 50 },
+ { x: 1500000, y: 0 },
+ ],
+ ],
+ expectedForwardsPath: [
+ { x: 1500000, y: 0 },
+ { x: 1500000, y: 50 },
+ ],
+ },
+ {
+ targetClass: "fill-forwards",
+ expectedIterationPathList: [
+ [
+ { x: 0, y: 0 },
+ { x: 250000, y: 25 },
+ { x: 500000, y: 50 },
+ { x: 750000, y: 75 },
+ { x: 1000000, y: 100 },
+ { x: 1000000, y: 0 },
+ ],
+ ],
+ expectedForwardsPath: [
+ { x: 1000000, y: 0 },
+ { x: 1000000, y: 100 },
+ { x: 1500000, y: 100 },
+ { x: 1500000, y: 0 },
+ ],
+ },
+ {
+ targetClass: "iterationstart",
+ expectedIterationPathList: [
+ [
+ { x: 0, y: 50 },
+ { x: 250000, y: 75 },
+ { x: 500000, y: 100 },
+ { x: 500000, y: 0 },
+ ],
+ [
+ { x: 500000, y: 0 },
+ { x: 750000, y: 25 },
+ { x: 1000000, y: 50 },
+ { x: 1000000, y: 0 },
+ ],
+ ],
+ },
+ {
+ targetClass: "no-compositor",
+ expectedIterationPathList: [
+ [
+ { x: 0, y: 0 },
+ { x: 250000, y: 25 },
+ { x: 500000, y: 50 },
+ { x: 750000, y: 75 },
+ { x: 1000000, y: 100 },
+ { x: 1000000, y: 0 },
+ ],
+ ],
+ },
+ {
+ targetClass: "keyframes-easing-step",
+ expectedIterationPathList: [
+ [
+ { x: 0, y: 0 },
+ { x: 499999, y: 0 },
+ { x: 500000, y: 50 },
+ { x: 999999, y: 50 },
+ { x: 1000000, y: 0 },
+ ],
+ ],
+ },
+ {
+ targetClass: "narrow-keyframes",
+ expectedIterationPathList: [
+ [
+ { x: 0, y: 0 },
+ { x: 100000, y: 10 },
+ { x: 110000, y: 10 },
+ { x: 115000, y: 10 },
+ { x: 129999, y: 10 },
+ { x: 130000, y: 13 },
+ { x: 135000, y: 13.5 },
+ ],
+ ],
+ },
+ {
+ targetClass: "duplicate-offsets",
+ expectedIterationPathList: [
+ [
+ { x: 0, y: 0 },
+ { x: 250000, y: 25 },
+ { x: 500000, y: 50 },
+ { x: 999999, y: 50 },
+ ],
+ ],
+ },
+];
+
+add_task(async function () {
+ await testComputedTimingPath(TEST_DATA);
+});
diff --git a/devtools/client/inspector/animation/test/browser_animation_summary-graph_computed-timing-path_different-timescale.js b/devtools/client/inspector/animation/test/browser_animation_summary-graph_computed-timing-path_different-timescale.js
new file mode 100644
index 0000000000..0b9bc79def
--- /dev/null
+++ b/devtools/client/inspector/animation/test/browser_animation_summary-graph_computed-timing-path_different-timescale.js
@@ -0,0 +1,43 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test the Computed Timing Path component for different time scales.
+
+add_task(async function () {
+ await addTab(URL_ROOT + "doc_simple_animation.html");
+ await removeAnimatedElementsExcept([".animated", ".end-delay"]);
+ const { animationInspector, inspector, panel } =
+ await openAnimationInspector();
+
+ info("Checking the path for different time scale");
+ let onDetailRendered = animationInspector.once(
+ "animation-keyframes-rendered"
+ );
+ await selectNode(".animated", inspector);
+ await onDetailRendered;
+ const itemA = await findAnimationItemByTargetSelector(panel, ".animated");
+ const pathStringA = itemA
+ .querySelector(".animation-iteration-path")
+ .getAttribute("d");
+
+ info("Select animation which has different time scale from no-compositor");
+ onDetailRendered = animationInspector.once("animation-keyframes-rendered");
+ await selectNode(".end-delay", inspector);
+ await onDetailRendered;
+
+ info("Select no-compositor again");
+ onDetailRendered = animationInspector.once("animation-keyframes-rendered");
+ await selectNode(".animated", inspector);
+ await onDetailRendered;
+ const itemB = await findAnimationItemByTargetSelector(panel, ".animated");
+ const pathStringB = itemB
+ .querySelector(".animation-iteration-path")
+ .getAttribute("d");
+ is(
+ pathStringA,
+ pathStringB,
+ "Path string should be same even change the time scale"
+ );
+});
diff --git a/devtools/client/inspector/animation/test/browser_animation_summary-graph_delay-sign-rtl.js b/devtools/client/inspector/animation/test/browser_animation_summary-graph_delay-sign-rtl.js
new file mode 100644
index 0000000000..591fc5f3fe
--- /dev/null
+++ b/devtools/client/inspector/animation/test/browser_animation_summary-graph_delay-sign-rtl.js
@@ -0,0 +1,14 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function () {
+ Services.scriptloader.loadSubScript(
+ CHROME_URL_ROOT + "summary-graph_delay-sign_head.js",
+ this
+ );
+ await pushPref("intl.l10n.pseudo", "bidi");
+ // eslint-disable-next-line no-undef
+ await testSummaryGraphDelaySign();
+});
diff --git a/devtools/client/inspector/animation/test/browser_animation_summary-graph_delay-sign.js b/devtools/client/inspector/animation/test/browser_animation_summary-graph_delay-sign.js
new file mode 100644
index 0000000000..891d9fd90e
--- /dev/null
+++ b/devtools/client/inspector/animation/test/browser_animation_summary-graph_delay-sign.js
@@ -0,0 +1,13 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function () {
+ Services.scriptloader.loadSubScript(
+ CHROME_URL_ROOT + "summary-graph_delay-sign_head.js",
+ this
+ );
+ // eslint-disable-next-line no-undef
+ await testSummaryGraphDelaySign();
+});
diff --git a/devtools/client/inspector/animation/test/browser_animation_summary-graph_effect-timing-path.js b/devtools/client/inspector/animation/test/browser_animation_summary-graph_effect-timing-path.js
new file mode 100644
index 0000000000..6974eab6c6
--- /dev/null
+++ b/devtools/client/inspector/animation/test/browser_animation_summary-graph_effect-timing-path.js
@@ -0,0 +1,64 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test for following EffectTimingPath component works.
+// * element existance
+// * path
+
+const TEST_DATA = [
+ {
+ targetClass: "cssanimation-linear",
+ },
+ {
+ targetClass: "delay-negative",
+ },
+ {
+ targetClass: "easing-step",
+ expectedPath: [
+ { x: 0, y: 0 },
+ { x: 499999, y: 0 },
+ { x: 500000, y: 50 },
+ { x: 999999, y: 50 },
+ { x: 1000000, y: 0 },
+ ],
+ },
+ {
+ targetClass: "keyframes-easing-step",
+ },
+];
+
+add_task(async function () {
+ await addTab(URL_ROOT + "doc_multi_timings.html");
+ await removeAnimatedElementsExcept(TEST_DATA.map(t => `.${t.targetClass}`));
+ const { panel } = await openAnimationInspector();
+
+ for (const { targetClass, expectedPath } of TEST_DATA) {
+ const animationItemEl = await findAnimationItemByTargetSelector(
+ panel,
+ `.${targetClass}`
+ );
+
+ info(`Checking effect timing path existance for ${targetClass}`);
+ const effectTimingPathEl = animationItemEl.querySelector(
+ ".animation-effect-timing-path"
+ );
+
+ if (expectedPath) {
+ ok(
+ effectTimingPathEl,
+ "The effect timing path element should be in animation item element"
+ );
+ const pathEl = effectTimingPathEl.querySelector(
+ ".animation-iteration-path"
+ );
+ assertPathSegments(pathEl, false, expectedPath);
+ } else {
+ ok(
+ !effectTimingPathEl,
+ "The effect timing path element should not be in animation item element"
+ );
+ }
+ }
+});
diff --git a/devtools/client/inspector/animation/test/browser_animation_summary-graph_end-delay-sign-rtl.js b/devtools/client/inspector/animation/test/browser_animation_summary-graph_end-delay-sign-rtl.js
new file mode 100644
index 0000000000..084e4acf1d
--- /dev/null
+++ b/devtools/client/inspector/animation/test/browser_animation_summary-graph_end-delay-sign-rtl.js
@@ -0,0 +1,15 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/* import-globals-from summary-graph_end-delay-sign_head.js */
+
+add_task(async function () {
+ Services.scriptloader.loadSubScript(
+ CHROME_URL_ROOT + "summary-graph_end-delay-sign_head.js",
+ this
+ );
+ await pushPref("intl.l10n.pseudo", "bidi");
+ await testSummaryGraphEndDelaySign();
+});
diff --git a/devtools/client/inspector/animation/test/browser_animation_summary-graph_end-delay-sign.js b/devtools/client/inspector/animation/test/browser_animation_summary-graph_end-delay-sign.js
new file mode 100644
index 0000000000..4382ed4c2d
--- /dev/null
+++ b/devtools/client/inspector/animation/test/browser_animation_summary-graph_end-delay-sign.js
@@ -0,0 +1,13 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function () {
+ Services.scriptloader.loadSubScript(
+ CHROME_URL_ROOT + "summary-graph_end-delay-sign_head.js",
+ this
+ );
+ // eslint-disable-next-line no-undef
+ await testSummaryGraphEndDelaySign();
+});
diff --git a/devtools/client/inspector/animation/test/browser_animation_summary-graph_layout-by-seek.js b/devtools/client/inspector/animation/test/browser_animation_summary-graph_layout-by-seek.js
new file mode 100644
index 0000000000..5f1c808728
--- /dev/null
+++ b/devtools/client/inspector/animation/test/browser_animation_summary-graph_layout-by-seek.js
@@ -0,0 +1,150 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test whether the layout of graphs were broken by seek and resume.
+
+add_task(async function () {
+ await addTab(URL_ROOT + "doc_multi_timings.html");
+ await removeAnimatedElementsExcept([
+ ".delay-positive",
+ ".delay-negative",
+ ".enddelay-positive",
+ ".enddelay-negative",
+ ]);
+ const { animationInspector, panel } = await openAnimationInspector();
+
+ info("Get initial coordinates result as test data");
+ const initialCoordinatesResult = [];
+
+ for (let i = 0; i < animationInspector.state.animations.length; i++) {
+ const itemEl = await findAnimationItemByIndex(panel, i);
+ const svgEl = itemEl.querySelector("svg");
+ const svgViewBoxX = svgEl.viewBox.baseVal.x;
+ const svgViewBoxWidth = svgEl.viewBox.baseVal.width;
+
+ const pathEl = svgEl.querySelector(".animation-computed-timing-path");
+ const pathX = pathEl.transform.baseVal[0].matrix.e;
+
+ const delayEl = itemEl.querySelector(".animation-delay-sign");
+ let delayX = null;
+ let delayWidth = null;
+
+ if (delayEl) {
+ const computedStyle = delayEl.ownerGlobal.getComputedStyle(delayEl);
+ delayX = computedStyle.left;
+ delayWidth = computedStyle.width;
+ }
+
+ const endDelayEl = itemEl.querySelector(".animation-end-delay-sign");
+ let endDelayX = null;
+ let endDelayWidth = null;
+
+ if (endDelayEl) {
+ const computedStyle = endDelayEl.ownerGlobal.getComputedStyle(endDelayEl);
+ endDelayX = computedStyle.left;
+ endDelayWidth = computedStyle.width;
+ }
+
+ const coordinates = {
+ svgViewBoxX,
+ svgViewBoxWidth,
+ pathX,
+ delayX,
+ delayWidth,
+ endDelayX,
+ endDelayWidth,
+ };
+ initialCoordinatesResult.push(coordinates);
+ }
+
+ info("Set currentTime to rear of the end of animation of .delay-negative.");
+ clickOnCurrentTimeScrubberController(animationInspector, panel, 0.75);
+ await waitUntilAnimationsPlayState(animationInspector, "paused");
+ info("Resume animations");
+ clickOnPauseResumeButton(animationInspector, panel);
+ // As some animations may be finished, we check if some animations will be running.
+ await waitUntil(() =>
+ animationInspector.state.animations.some(
+ a => a.state.playState === "running"
+ )
+ );
+
+ info("Check the layout");
+ const itemEls = panel.querySelectorAll(".animation-item");
+ is(
+ itemEls.length,
+ initialCoordinatesResult.length,
+ "Count of animation item should be same to initial items"
+ );
+
+ info("Check the coordinates");
+ checkExpectedCoordinates(itemEls, initialCoordinatesResult);
+});
+
+function checkExpectedCoordinates(itemEls, initialCoordinatesResult) {
+ for (let i = 0; i < itemEls.length; i++) {
+ const expectedCoordinates = initialCoordinatesResult[i];
+ const itemEl = itemEls[i];
+ const svgEl = itemEl.querySelector("svg");
+ is(
+ svgEl.viewBox.baseVal.x,
+ expectedCoordinates.svgViewBoxX,
+ "X of viewBox of svg should be same"
+ );
+ is(
+ svgEl.viewBox.baseVal.width,
+ expectedCoordinates.svgViewBoxWidth,
+ "Width of viewBox of svg should be same"
+ );
+
+ const pathEl = svgEl.querySelector(".animation-computed-timing-path");
+ is(
+ pathEl.transform.baseVal[0].matrix.e,
+ expectedCoordinates.pathX,
+ "X of tansform of path element should be same"
+ );
+
+ const delayEl = itemEl.querySelector(".animation-delay-sign");
+
+ if (delayEl) {
+ const computedStyle = delayEl.ownerGlobal.getComputedStyle(delayEl);
+ is(
+ computedStyle.left,
+ expectedCoordinates.delayX,
+ "X of delay sign should be same"
+ );
+ is(
+ computedStyle.width,
+ expectedCoordinates.delayWidth,
+ "Width of delay sign should be same"
+ );
+ } else {
+ ok(!expectedCoordinates.delayX, "X of delay sign should exist");
+ ok(!expectedCoordinates.delayWidth, "Width of delay sign should exist");
+ }
+
+ const endDelayEl = itemEl.querySelector(".animation-end-delay-sign");
+
+ if (endDelayEl) {
+ const computedStyle = endDelayEl.ownerGlobal.getComputedStyle(endDelayEl);
+ is(
+ computedStyle.left,
+ expectedCoordinates.endDelayX,
+ "X of endDelay sign should be same"
+ );
+ is(
+ computedStyle.width,
+ expectedCoordinates.endDelayWidth,
+ "Width of endDelay sign should be same"
+ );
+ } else {
+ ok(!expectedCoordinates.endDelayX, "X of endDelay sign should exist");
+ ok(
+ !expectedCoordinates.endDelayWidth,
+ "Width of endDelay sign should exist"
+ );
+ }
+ }
+}
diff --git a/devtools/client/inspector/animation/test/browser_animation_summary-graph_negative-delay-path.js b/devtools/client/inspector/animation/test/browser_animation_summary-graph_negative-delay-path.js
new file mode 100644
index 0000000000..8ed638c443
--- /dev/null
+++ b/devtools/client/inspector/animation/test/browser_animation_summary-graph_negative-delay-path.js
@@ -0,0 +1,128 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test for following NegativeDelayPath component works.
+// * element existance
+// * path
+
+const TEST_DATA = [
+ {
+ targetClass: "delay-positive",
+ },
+ {
+ targetClass: "delay-negative",
+ expectedIterationPathList: [
+ [
+ { x: 0, y: 0 },
+ { x: 0, y: 50 },
+ { x: 250000, y: 75 },
+ { x: 500000, y: 100 },
+ { x: 500000, y: 0 },
+ ],
+ ],
+ expectedNegativePath: [
+ { x: -500000, y: 0 },
+ { x: -250000, y: 25 },
+ { x: 0, y: 50 },
+ { x: 0, y: 0 },
+ ],
+ },
+ {
+ targetClass: "delay-negative-25",
+ expectedIterationPathList: [
+ [
+ { x: 0, y: 0 },
+ { x: 0, y: 25 },
+ { x: 750000, y: 100 },
+ { x: 750000, y: 0 },
+ ],
+ ],
+ expectedNegativePath: [
+ { x: -250000, y: 0 },
+ { x: 0, y: 25 },
+ { x: 0, y: 0 },
+ ],
+ },
+ {
+ targetClass: "delay-negative-75",
+ expectedIterationPathList: [
+ [
+ { x: 0, y: 0 },
+ { x: 0, y: 75 },
+ { x: 250000, y: 100 },
+ { x: 250000, y: 0 },
+ ],
+ ],
+ expectedNegativePath: [
+ { x: -750000, y: 0 },
+ { x: 0, y: 75 },
+ { x: 0, y: 0 },
+ ],
+ },
+];
+
+add_task(async function () {
+ await addTab(URL_ROOT + "doc_multi_timings.html");
+ await removeAnimatedElementsExcept(TEST_DATA.map(t => `.${t.targetClass}`));
+ const { panel } = await openAnimationInspector();
+
+ for (const {
+ targetClass,
+ expectedIterationPathList,
+ expectedNegativePath,
+ } of TEST_DATA) {
+ const animationItemEl = await findAnimationItemByTargetSelector(
+ panel,
+ `.${targetClass}`
+ );
+
+ info(`Checking negative delay path existence for ${targetClass}`);
+ const negativeDelayPathEl = animationItemEl.querySelector(
+ ".animation-negative-delay-path"
+ );
+
+ if (expectedNegativePath) {
+ ok(
+ negativeDelayPathEl,
+ "The negative delay path element should be in animation item element"
+ );
+ const pathEl = negativeDelayPathEl.querySelector("path");
+ assertPathSegments(pathEl, true, expectedNegativePath);
+ } else {
+ ok(
+ !negativeDelayPathEl,
+ "The negative delay path element should not be in animation item element"
+ );
+ }
+
+ if (!expectedIterationPathList) {
+ // We don't need to test for iteration path.
+ continue;
+ }
+
+ info(`Checking computed timing path existance for ${targetClass}`);
+ const computedTimingPathEl = animationItemEl.querySelector(
+ ".animation-computed-timing-path"
+ );
+ ok(
+ computedTimingPathEl,
+ "The computed timing path element should be in each animation item element"
+ );
+
+ info(`Checking iteration path list for ${targetClass}`);
+ const iterationPathEls = computedTimingPathEl.querySelectorAll(
+ ".animation-iteration-path"
+ );
+ is(
+ iterationPathEls.length,
+ expectedIterationPathList.length,
+ `Number of iteration path should be ${expectedIterationPathList.length}`
+ );
+
+ for (const [j, iterationPathEl] of iterationPathEls.entries()) {
+ assertPathSegments(iterationPathEl, true, expectedIterationPathList[j]);
+ }
+ }
+});
diff --git a/devtools/client/inspector/animation/test/browser_animation_summary-graph_negative-end-delay-path.js b/devtools/client/inspector/animation/test/browser_animation_summary-graph_negative-end-delay-path.js
new file mode 100644
index 0000000000..69ce5007b5
--- /dev/null
+++ b/devtools/client/inspector/animation/test/browser_animation_summary-graph_negative-end-delay-path.js
@@ -0,0 +1,56 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test for following NegativeEndDelayPath component works.
+// * element existance
+// * path
+
+const TEST_DATA = [
+ {
+ targetClass: "enddelay-positive",
+ },
+ {
+ targetClass: "enddelay-negative",
+ expectedPath: [
+ { x: 500000, y: 0 },
+ { x: 500000, y: 50 },
+ { x: 750000, y: 75 },
+ { x: 1000000, y: 100 },
+ { x: 1000000, y: 0 },
+ ],
+ },
+];
+
+add_task(async function () {
+ await addTab(URL_ROOT + "doc_multi_timings.html");
+ await removeAnimatedElementsExcept(TEST_DATA.map(t => `.${t.targetClass}`));
+ const { panel } = await openAnimationInspector();
+
+ for (const { targetClass, expectedPath } of TEST_DATA) {
+ const animationItemEl = await findAnimationItemByTargetSelector(
+ panel,
+ `.${targetClass}`
+ );
+
+ info(`Checking negative endDelay path existance for ${targetClass}`);
+ const negativeEndDelayPathEl = animationItemEl.querySelector(
+ ".animation-negative-end-delay-path"
+ );
+
+ if (expectedPath) {
+ ok(
+ negativeEndDelayPathEl,
+ "The negative endDelay path element should be in animation item element"
+ );
+ const pathEl = negativeEndDelayPathEl.querySelector("path");
+ assertPathSegments(pathEl, true, expectedPath);
+ } else {
+ ok(
+ !negativeEndDelayPathEl,
+ "The negative endDelay path element should not be in animation item element"
+ );
+ }
+ }
+});
diff --git a/devtools/client/inspector/animation/test/browser_animation_summary-graph_tooltip.js b/devtools/client/inspector/animation/test/browser_animation_summary-graph_tooltip.js
new file mode 100644
index 0000000000..1be3c92f4f
--- /dev/null
+++ b/devtools/client/inspector/animation/test/browser_animation_summary-graph_tooltip.js
@@ -0,0 +1,294 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test for existance and content of tooltip on summary graph element.
+
+const TEST_DATA = [
+ {
+ targetClass: "cssanimation-normal",
+ expectedResult: {
+ nameAndType: "cssanimation — CSS Animation",
+ duration: "1,000s",
+ },
+ },
+ {
+ targetClass: "cssanimation-linear",
+ expectedResult: {
+ nameAndType: "cssanimation — CSS Animation",
+ duration: "1,000s",
+ animationTimingFunction: "linear",
+ },
+ },
+ {
+ targetClass: "delay-positive",
+ expectedResult: {
+ nameAndType: "test-delay-animation — Script Animation",
+ delay: "500s",
+ duration: "1,000s",
+ },
+ },
+ {
+ targetClass: "delay-negative",
+ expectedResult: {
+ nameAndType: "test-negative-delay-animation — Script Animation",
+ delay: "-500s",
+ duration: "1,000s",
+ },
+ },
+ {
+ targetClass: "easing-step",
+ expectedResult: {
+ nameAndType: "Script Animation",
+ duration: "1,000s",
+ easing: "steps(2)",
+ },
+ },
+ {
+ targetClass: "enddelay-positive",
+ expectedResult: {
+ nameAndType: "Script Animation",
+ duration: "1,000s",
+ endDelay: "500s",
+ },
+ },
+ {
+ targetClass: "enddelay-negative",
+ expectedResult: {
+ nameAndType: "Script Animation",
+ duration: "1,000s",
+ endDelay: "-500s",
+ },
+ },
+ {
+ targetClass: "enddelay-with-fill-forwards",
+ expectedResult: {
+ nameAndType: "Script Animation",
+ duration: "1,000s",
+ endDelay: "500s",
+ fill: "forwards",
+ },
+ },
+ {
+ targetClass: "enddelay-with-iterations-infinity",
+ expectedResult: {
+ nameAndType: "Script Animation",
+ duration: "1,000s",
+ endDelay: "500s",
+ iterations: "\u221E",
+ },
+ },
+ {
+ targetClass: "direction-alternate-with-iterations-infinity",
+ expectedResult: {
+ nameAndType: "Script Animation",
+ duration: "1,000s",
+ direction: "alternate",
+ iterations: "\u221E",
+ },
+ },
+ {
+ targetClass: "direction-alternate-reverse-with-iterations-infinity",
+ expectedResult: {
+ nameAndType: "Script Animation",
+ duration: "1,000s",
+ direction: "alternate-reverse",
+ iterations: "\u221E",
+ },
+ },
+ {
+ targetClass: "direction-reverse-with-iterations-infinity",
+ expectedResult: {
+ nameAndType: "Script Animation",
+ duration: "1,000s",
+ direction: "reverse",
+ iterations: "\u221E",
+ },
+ },
+ {
+ targetClass: "fill-backwards",
+ expectedResult: {
+ nameAndType: "Script Animation",
+ duration: "1,000s",
+ fill: "backwards",
+ },
+ },
+ {
+ targetClass: "fill-backwards-with-delay-iterationstart",
+ expectedResult: {
+ nameAndType: "Script Animation",
+ delay: "500s",
+ duration: "1,000s",
+ fill: "backwards",
+ iterationStart: "0.5",
+ },
+ },
+ {
+ targetClass: "fill-both",
+ expectedResult: {
+ nameAndType: "Script Animation",
+ duration: "1,000s",
+ fill: "both",
+ },
+ },
+ {
+ targetClass: "fill-both-width-delay-iterationstart",
+ expectedResult: {
+ nameAndType: "Script Animation",
+ delay: "500s",
+ duration: "1,000s",
+ fill: "both",
+ iterationStart: "0.5",
+ },
+ },
+ {
+ targetClass: "fill-forwards",
+ expectedResult: {
+ nameAndType: "Script Animation",
+ duration: "1,000s",
+ fill: "forwards",
+ },
+ },
+ {
+ targetClass: "iterationstart",
+ expectedResult: {
+ nameAndType: "Script Animation",
+ duration: "1,000s",
+ iterationStart: "0.5",
+ },
+ },
+ {
+ targetClass: "no-compositor",
+ expectedResult: {
+ nameAndType: "Script Animation",
+ duration: "1,000s",
+ },
+ },
+ {
+ targetClass: "keyframes-easing-step",
+ expectedResult: {
+ nameAndType: "Script Animation",
+ duration: "1,000s",
+ },
+ },
+];
+
+add_task(async function () {
+ await addTab(URL_ROOT + "doc_multi_timings.html");
+ await removeAnimatedElementsExcept(TEST_DATA.map(t => `.${t.targetClass}`));
+ const { panel } = await openAnimationInspector();
+
+ for (const { targetClass, expectedResult } of TEST_DATA) {
+ const animationItemEl = await findAnimationItemByTargetSelector(
+ panel,
+ `.${targetClass}`
+ );
+ const summaryGraphEl = animationItemEl.querySelector(
+ ".animation-summary-graph"
+ );
+
+ info(`Checking tooltip for ${targetClass}`);
+ ok(
+ summaryGraphEl.hasAttribute("title"),
+ "Summary graph should have 'title' attribute"
+ );
+
+ const tooltip = summaryGraphEl.getAttribute("title");
+ const {
+ animationTimingFunction,
+ delay,
+ easing,
+ endDelay,
+ direction,
+ duration,
+ fill,
+ iterations,
+ iterationStart,
+ nameAndType,
+ } = expectedResult;
+
+ ok(
+ tooltip.startsWith(nameAndType),
+ "Tooltip should start with name and type"
+ );
+
+ if (animationTimingFunction) {
+ const expected = `Animation timing function: ${animationTimingFunction}`;
+ ok(tooltip.includes(expected), `Tooltip should include '${expected}'`);
+ } else {
+ ok(
+ !tooltip.includes("Animation timing function:"),
+ "Tooltip should not include animation timing function"
+ );
+ }
+
+ if (delay) {
+ const expected = `Delay: ${delay}`;
+ ok(tooltip.includes(expected), `Tooltip should include '${expected}'`);
+ } else {
+ ok(!tooltip.includes("Delay:"), "Tooltip should not include delay");
+ }
+
+ if (direction) {
+ const expected = `Direction: ${direction}`;
+ ok(tooltip.includes(expected), `Tooltip should include '${expected}'`);
+ } else {
+ ok(!tooltip.includes("Direction:"), "Tooltip should not include delay");
+ }
+
+ if (duration) {
+ const expected = `Duration: ${duration}`;
+ ok(tooltip.includes(expected), `Tooltip should include '${expected}'`);
+ } else {
+ ok(!tooltip.includes("Duration:"), "Tooltip should not include delay");
+ }
+
+ if (easing) {
+ const expected = `Overall easing: ${easing}`;
+ ok(tooltip.includes(expected), `Tooltip should include '${expected}'`);
+ } else {
+ ok(
+ !tooltip.includes("Overall easing:"),
+ "Tooltip should not include easing"
+ );
+ }
+
+ if (endDelay) {
+ const expected = `End delay: ${endDelay}`;
+ ok(tooltip.includes(expected), `Tooltip should include '${expected}'`);
+ } else {
+ ok(
+ !tooltip.includes("End delay:"),
+ "Tooltip should not include endDelay"
+ );
+ }
+
+ if (fill) {
+ const expected = `Fill: ${fill}`;
+ ok(tooltip.includes(expected), `Tooltip should include '${expected}'`);
+ } else {
+ ok(!tooltip.includes("Fill:"), "Tooltip should not include fill");
+ }
+
+ if (iterations) {
+ const expected = `Repeats: ${iterations}`;
+ ok(tooltip.includes(expected), `Tooltip should include '${expected}'`);
+ } else {
+ ok(
+ !tooltip.includes("Repeats:"),
+ "Tooltip should not include iterations"
+ );
+ }
+
+ if (iterationStart) {
+ const expected = `Iteration start: ${iterationStart}`;
+ ok(tooltip.includes(expected), `Tooltip should include '${expected}'`);
+ } else {
+ ok(
+ !tooltip.includes("Iteration start:"),
+ "Tooltip should not include iterationStart"
+ );
+ }
+ }
+});
diff --git a/devtools/client/inspector/animation/test/browser_animation_timing_negative-playback-rate_current-time-scrubber.js b/devtools/client/inspector/animation/test/browser_animation_timing_negative-playback-rate_current-time-scrubber.js
new file mode 100644
index 0000000000..e5f05d4f18
--- /dev/null
+++ b/devtools/client/inspector/animation/test/browser_animation_timing_negative-playback-rate_current-time-scrubber.js
@@ -0,0 +1,47 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test whether the scrubber was working in case of negative playback rate.
+
+add_task(async function () {
+ await addTab(URL_ROOT + "doc_negative_playback_rate.html");
+ await removeAnimatedElementsExcept([".normal"]);
+ const { animationInspector, panel } = await openAnimationInspector();
+
+ info("Set initial state");
+ clickOnCurrentTimeScrubberController(animationInspector, panel, 0);
+ await waitUntilAnimationsPlayState(animationInspector, "paused");
+ const initialCurrentTime =
+ animationInspector.state.animations[0].state.currentTime;
+ const initialProgressBarX = getProgressBarX(panel);
+
+ info("Check whether the animation currentTime was decreased");
+ clickOnCurrentTimeScrubberController(animationInspector, panel, 0.5);
+ await waitUntilCurrentTimeChangedAt(
+ animationInspector,
+ animationInspector.state.timeScale.getDuration() * 0.5
+ );
+ Assert.greater(
+ initialCurrentTime,
+ animationInspector.state.animations[0].state.currentTime,
+ "currentTime should be decreased"
+ );
+
+ info("Check whether the progress bar was moved to left");
+ Assert.greater(
+ initialProgressBarX,
+ getProgressBarX(panel),
+ "Progress bar should be moved to left"
+ );
+});
+
+function getProgressBarX(panel) {
+ const areaEl = panel.querySelector(".keyframes-progress-bar-area");
+ const barEl = areaEl.querySelector(".keyframes-progress-bar");
+ const controllerBounds = areaEl.getBoundingClientRect();
+ const barBounds = barEl.getBoundingClientRect();
+ const barX = barBounds.x + barBounds.width / 2 - controllerBounds.x;
+ return barX;
+}
diff --git a/devtools/client/inspector/animation/test/browser_animation_timing_negative-playback-rate_summary-graph.js b/devtools/client/inspector/animation/test/browser_animation_timing_negative-playback-rate_summary-graph.js
new file mode 100644
index 0000000000..ef326f5eb2
--- /dev/null
+++ b/devtools/client/inspector/animation/test/browser_animation_timing_negative-playback-rate_summary-graph.js
@@ -0,0 +1,235 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test for following summary graph with the animation which is negative playback rate.
+// * Tooltips
+// * Graph path
+// * Delay sign
+// * End delay sign
+
+const TEST_DATA = [
+ {
+ targetSelector: ".normal",
+ expectedPathList: [
+ {
+ selector: ".animation-iteration-path",
+ path: [
+ { x: 0, y: 100 },
+ { x: 50000, y: 50 },
+ { x: 100000, y: 0 },
+ ],
+ },
+ ],
+ expectedTooltip: "Playback rate: -1",
+ expectedViewboxWidth: 200000,
+ },
+ {
+ targetSelector: ".normal-playbackrate-2",
+ expectedPathList: [
+ {
+ selector: ".animation-iteration-path",
+ path: [
+ { x: 0, y: 100 },
+ { x: 50000, y: 50 },
+ { x: 100000, y: 0 },
+ ],
+ },
+ ],
+ expectedTooltip: "Playback rate: -2",
+ expectedViewboxWidth: 400000,
+ },
+ {
+ targetSelector: ".positive-delay",
+ expectedSignList: [
+ {
+ selector: ".animation-end-delay-sign",
+ sign: {
+ marginInlineStart: "75%",
+ width: "25%",
+ },
+ },
+ ],
+ expectedPathList: [
+ {
+ selector: ".animation-iteration-path",
+ path: [
+ { x: 0, y: 100 },
+ { x: 50000, y: 50 },
+ { x: 100000, y: 0 },
+ ],
+ },
+ ],
+ expectedTooltip: "Playback rate: -1",
+ },
+ {
+ targetSelector: ".negative-delay",
+ expectedSignList: [
+ {
+ selector: ".animation-end-delay-sign",
+ sign: {
+ marginInlineStart: "50%",
+ width: "25%",
+ },
+ },
+ ],
+ expectedPathList: [
+ {
+ selector: ".animation-iteration-path",
+ path: [
+ { x: 0, y: 100 },
+ { x: 50000, y: 50 },
+ { x: 50000, y: 0 },
+ ],
+ },
+ {
+ selector: ".animation-negative-delay-path path",
+ path: [
+ { x: 50000, y: 50 },
+ { x: 100000, y: 0 },
+ ],
+ },
+ ],
+ expectedTooltip: "Playback rate: -1",
+ },
+ {
+ targetSelector: ".positive-end-delay",
+ expectedSignList: [
+ {
+ selector: ".animation-delay-sign",
+ sign: {
+ isFilled: true,
+ marginInlineStart: "25%",
+ width: "25%",
+ },
+ },
+ ],
+ expectedPathList: [
+ {
+ selector: ".animation-iteration-path",
+ path: [
+ { x: 50000, y: 100 },
+ { x: 100000, y: 50 },
+ { x: 150000, y: 0 },
+ ],
+ },
+ ],
+ expectedTooltip: "Playback rate: -1",
+ },
+ {
+ targetSelector: ".negative-end-delay",
+ expectedSignList: [
+ {
+ selector: ".animation-delay-sign",
+ sign: {
+ isFilled: true,
+ marginInlineStart: "0%",
+ width: "25%",
+ },
+ },
+ ],
+ expectedPathList: [
+ {
+ selector: ".animation-iteration-path",
+ path: [
+ { x: 0, y: 50 },
+ { x: 50000, y: 0 },
+ ],
+ },
+ {
+ selector: ".animation-negative-end-delay-path path",
+ path: [
+ { x: -50000, y: 100 },
+ { x: 0, y: 0 },
+ ],
+ },
+ ],
+ expectedTooltip: "Playback rate: -1",
+ },
+];
+
+add_task(async function () {
+ await addTab(URL_ROOT + "doc_negative_playback_rate.html");
+ const { panel } = await openAnimationInspector();
+
+ for (const testData of TEST_DATA) {
+ const {
+ targetSelector,
+ expectedPathList,
+ expectedSignList,
+ expectedTooltip,
+ expectedViewboxWidth,
+ } = testData;
+
+ const animationItemEl = await findAnimationItemByTargetSelector(
+ panel,
+ targetSelector
+ );
+ const summaryGraphEl = animationItemEl.querySelector(
+ ".animation-summary-graph"
+ );
+
+ info(`Check tooltip for the animation of ${targetSelector}`);
+ assertTooltip(summaryGraphEl, expectedTooltip);
+
+ if (expectedPathList) {
+ for (const { selector, path } of expectedPathList) {
+ info(`Check path for ${selector}`);
+ assertPath(summaryGraphEl, selector, path);
+ }
+ }
+
+ if (expectedSignList) {
+ for (const { selector, sign } of expectedSignList) {
+ info(`Check sign for ${selector}`);
+ assertSign(summaryGraphEl, selector, sign);
+ }
+ }
+
+ if (expectedViewboxWidth) {
+ info("Check width of viewbox of SVG");
+ const svgEl = summaryGraphEl.querySelector(
+ ".animation-summary-graph-path"
+ );
+ is(
+ svgEl.viewBox.baseVal.width,
+ expectedViewboxWidth,
+ `width of viewbox should be ${expectedViewboxWidth}`
+ );
+ }
+ }
+});
+
+function assertPath(summaryGraphEl, pathSelector, expectedPath) {
+ const pathEl = summaryGraphEl.querySelector(pathSelector);
+ assertPathSegments(pathEl, true, expectedPath);
+}
+
+function assertSign(summaryGraphEl, selector, expectedSign) {
+ const signEl = summaryGraphEl.querySelector(selector);
+
+ is(
+ signEl.style.marginInlineStart,
+ expectedSign.marginInlineStart,
+ `marginInlineStart position should be ${expectedSign.marginInlineStart}`
+ );
+ is(
+ signEl.style.width,
+ expectedSign.width,
+ `Width should be ${expectedSign.width}`
+ );
+ is(
+ signEl.classList.contains("fill"),
+ expectedSign.isFilled || false,
+ "signEl should be correct"
+ );
+}
+
+function assertTooltip(summaryGraphEl, expectedTooltip) {
+ const tooltip = summaryGraphEl.getAttribute("title");
+ ok(
+ tooltip.includes(expectedTooltip),
+ `Tooltip should include '${expectedTooltip}'`
+ );
+}
diff --git a/devtools/client/inspector/animation/test/current-time-scrubber_head.js b/devtools/client/inspector/animation/test/current-time-scrubber_head.js
new file mode 100644
index 0000000000..1e94a7562c
--- /dev/null
+++ b/devtools/client/inspector/animation/test/current-time-scrubber_head.js
@@ -0,0 +1,101 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/* import-globals-from head.js */
+
+// Test for following CurrentTimeScrubber and CurrentTimeScrubberController components:
+// * element existence
+// * scrubber position validity
+// * make animations currentTime to change by click on the controller
+// * mouse drag on the scrubber
+
+// eslint-disable-next-line no-unused-vars
+async function testCurrentTimeScrubber(isRTL) {
+ await addTab(URL_ROOT + "doc_simple_animation.html");
+ await removeAnimatedElementsExcept([".long"]);
+ const { animationInspector, panel } = await openAnimationInspector();
+
+ info("Checking scrubber controller existence");
+ const controllerEl = panel.querySelector(".current-time-scrubber-area");
+ ok(controllerEl, "scrubber controller should exist");
+
+ info("Checking scrubber existence");
+ const scrubberEl = controllerEl.querySelector(".current-time-scrubber");
+ ok(scrubberEl, "scrubber should exist");
+
+ info("Checking scrubber changes current time of animation and the position");
+ const duration = animationInspector.state.timeScale.getDuration();
+ clickOnCurrentTimeScrubberController(
+ animationInspector,
+ panel,
+ isRTL ? 1 : 0
+ );
+ await waitUntilAnimationsPlayState(animationInspector, "paused");
+ await waitUntilCurrentTimeChangedAt(animationInspector, 0);
+ assertPosition(
+ scrubberEl,
+ controllerEl,
+ isRTL ? duration : 0,
+ animationInspector
+ );
+
+ clickOnCurrentTimeScrubberController(
+ animationInspector,
+ panel,
+ isRTL ? 0 : 1
+ );
+ await waitUntilCurrentTimeChangedAt(animationInspector, duration);
+ assertPosition(
+ scrubberEl,
+ controllerEl,
+ isRTL ? 0 : duration,
+ animationInspector
+ );
+
+ clickOnCurrentTimeScrubberController(animationInspector, panel, 0.5);
+ await waitUntilCurrentTimeChangedAt(animationInspector, duration * 0.5);
+ assertPosition(scrubberEl, controllerEl, duration * 0.5, animationInspector);
+
+ info("Checking current time scrubber position during running");
+ // Running again
+ clickOnPauseResumeButton(animationInspector, panel);
+ await waitUntilAnimationsPlayState(animationInspector, "running");
+ let previousX = scrubberEl.getBoundingClientRect().x;
+ await wait(1000);
+ let currentX = scrubberEl.getBoundingClientRect().x;
+ isnot(previousX, currentX, "Scrubber should be moved");
+
+ info("Checking draggable on scrubber over animation list");
+ clickOnPauseResumeButton(animationInspector, panel);
+ await waitUntilAnimationsPlayState(animationInspector, "paused");
+ previousX = scrubberEl.getBoundingClientRect().x;
+ await dragOnCurrentTimeScrubber(animationInspector, panel, 5, 30);
+ currentX = scrubberEl.getBoundingClientRect().x;
+ isnot(previousX, currentX, "Scrubber should be draggable");
+
+ info(
+ "Checking a behavior which mouse out from animation inspector area " +
+ "during dragging from controller"
+ );
+ await dragOnCurrentTimeScrubberController(animationInspector, panel, 0.5, 2);
+ ok(
+ !panel
+ .querySelector(".animation-list-container")
+ .classList.contains("active-scrubber"),
+ "Click and DnD should be inactive"
+ );
+}
+
+function assertPosition(scrubberEl, controllerEl, time, animationInspector) {
+ const controllerBounds = controllerEl.getBoundingClientRect();
+ const scrubberBounds = scrubberEl.getBoundingClientRect();
+ const scrubberX =
+ scrubberBounds.x + scrubberBounds.width / 2 - controllerBounds.x;
+ const timeScale = animationInspector.state.timeScale;
+ const expected = Math.round(
+ (time / timeScale.getDuration()) * controllerBounds.width
+ );
+ is(scrubberX, expected, `Position should be ${expected} at ${time}ms`);
+}
diff --git a/devtools/client/inspector/animation/test/doc_custom_playback_rate.html b/devtools/client/inspector/animation/test/doc_custom_playback_rate.html
new file mode 100644
index 0000000000..9adee99884
--- /dev/null
+++ b/devtools/client/inspector/animation/test/doc_custom_playback_rate.html
@@ -0,0 +1,30 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="UTF-8">
+ <style>
+ div {
+ background-color: lime;
+ height: 100px;
+ }
+ </style>
+ </head>
+ <body>
+ <script>
+ "use strict";
+
+ const duration = 100000;
+
+ function createAnimation(cls) {
+ const div = document.createElement("div");
+ div.classList.add(cls);
+ document.body.appendChild(div);
+ const animation = div.animate([{ opacity: 0 }], duration);
+ animation.playbackRate = 1.5;
+ }
+
+ createAnimation("div1");
+ createAnimation("div2");
+ </script>
+ </body>
+</html>
diff --git a/devtools/client/inspector/animation/test/doc_infinity_duration.html b/devtools/client/inspector/animation/test/doc_infinity_duration.html
new file mode 100644
index 0000000000..10d19fc3cf
--- /dev/null
+++ b/devtools/client/inspector/animation/test/doc_infinity_duration.html
@@ -0,0 +1,41 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="UTF-8">
+ <style>
+ div {
+ background-color: lime;
+ height: 100px;
+ }
+ </style>
+ </head>
+ <body>
+ <div class="infinity"></div>
+ <div class="infinity-delay-iteration-start"></div>
+ <div class="limited"></div>
+ <script>
+ "use strict";
+
+ document.querySelector(".infinity").animate(
+ { opacity: [1, 0] },
+ { duration: Infinity }
+ );
+
+ document.querySelector(".infinity-delay-iteration-start").animate(
+ { opacity: [1, 0] },
+ {
+ delay: 100000,
+ duration: Infinity,
+ iterationStart: 0.5,
+ }
+ );
+
+ document.querySelector(".limited").animate(
+ { opacity: [1, 0] },
+ {
+ duration: 100000,
+ }
+ );
+ </script>
+ </body>
+</html>
diff --git a/devtools/client/inspector/animation/test/doc_multi_easings.html b/devtools/client/inspector/animation/test/doc_multi_easings.html
new file mode 100644
index 0000000000..cedcb027fe
--- /dev/null
+++ b/devtools/client/inspector/animation/test/doc_multi_easings.html
@@ -0,0 +1,121 @@
+<!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.classList.add(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 },
+ ],
+ "steps(5, jump-none)"
+ );
+
+ 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" },
+ ]
+ );
+
+ createAnimation(
+ "jump-start",
+ [
+ { opacity: 1, easing: "steps(2, jump-start)" },
+ { opacity: 0 },
+ ],
+ );
+
+ createAnimation(
+ "jump-end",
+ [
+ { opacity: 1, easing: "steps(2, jump-end)" },
+ { opacity: 0 },
+ ],
+ );
+
+ createAnimation(
+ "jump-both",
+ [
+ { opacity: 1, easing: "steps(3, jump-both)" },
+ { opacity: 0 },
+ ],
+ );
+ </script>
+ </body>
+</html>
diff --git a/devtools/client/inspector/animation/test/doc_multi_keyframes.html b/devtools/client/inspector/animation/test/doc_multi_keyframes.html
new file mode 100644
index 0000000000..8977f77dde
--- /dev/null
+++ b/devtools/client/inspector/animation/test/doc_multi_keyframes.html
@@ -0,0 +1,229 @@
+<!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.classList.add(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(
+ "steps-jump-none-keyframe",
+ [
+ {
+ easing: "steps(5, jump-none)",
+ 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,
+ },
+ ]
+ );
+
+ createAnimation(
+ "same-color",
+ [
+ {
+ backgroundColor: "lime",
+ },
+ {
+ backgroundColor: "lime",
+ },
+ ]
+ );
+
+ createAnimation(
+ "currentcolor",
+ [
+ {
+ backgroundColor: "currentColor",
+ },
+ {
+ backgroundColor: "lime",
+ },
+ ]
+ );
+ </script>
+ </body>
+</html>
diff --git a/devtools/client/inspector/animation/test/doc_multi_timings.html b/devtools/client/inspector/animation/test/doc_multi_timings.html
new file mode 100644
index 0000000000..a999431917
--- /dev/null
+++ b/devtools/client/inspector/animation/test/doc_multi_timings.html
@@ -0,0 +1,169 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="UTF-8">
+ <style>
+ div {
+ background-color: lime;
+ height: 100px;
+ width: 100px;
+ }
+
+ .cssanimation-normal {
+ animation: cssanimation 1000s;
+ }
+
+ .cssanimation-linear {
+ animation: cssanimation 1000s linear;
+ }
+
+ @keyframes cssanimation {
+ from {
+ opacity: 0;
+ }
+ to {
+ opacity: 1;
+ }
+ }
+ </style>
+ </head>
+ <body>
+ <div class="cssanimation-normal"></div>
+ <div class="cssanimation-linear"></div>
+ <script>
+ "use strict";
+
+ const duration = 1000000;
+
+ function createAnimation(keyframes, effect, className) {
+ const div = document.createElement("div");
+ div.classList.add(className);
+ document.body.appendChild(div);
+ effect.duration = duration;
+ div.animate(keyframes, effect);
+ }
+
+ createAnimation({ opacity: [0, 1] },
+ { delay: 500000, id: "test-delay-animation" },
+ "delay-positive");
+
+ createAnimation({ opacity: [0, 1] },
+ { delay: -500000, id: "test-negative-delay-animation" },
+ "delay-negative");
+
+ createAnimation({ opacity: [0, 1] },
+ { easing: "steps(2)" },
+ "easing-step");
+
+ createAnimation({ opacity: [0, 1] },
+ { endDelay: 500000 },
+ "enddelay-positive");
+
+ createAnimation({ opacity: [0, 1] },
+ { endDelay: -500000 },
+ "enddelay-negative");
+
+ createAnimation({ opacity: [0, 1] },
+ { endDelay: 500000, fill: "forwards" },
+ "enddelay-with-fill-forwards");
+
+ createAnimation({ opacity: [0, 1] },
+ { endDelay: 500000, iterations: Infinity },
+ "enddelay-with-iterations-infinity");
+
+ createAnimation({ opacity: [0, 1] },
+ { direction: "alternate", iterations: Infinity },
+ "direction-alternate-with-iterations-infinity");
+
+ createAnimation({ opacity: [0, 1] },
+ { direction: "alternate-reverse", iterations: Infinity },
+ "direction-alternate-reverse-with-iterations-infinity");
+
+ createAnimation({ opacity: [0, 1] },
+ { direction: "reverse", iterations: Infinity },
+ "direction-reverse-with-iterations-infinity");
+
+ createAnimation({ opacity: [0, 1] },
+ { fill: "backwards" },
+ "fill-backwards");
+
+ createAnimation({ opacity: [0, 1] },
+ { fill: "backwards", delay: 500000, iterationStart: 0.5 },
+ "fill-backwards-with-delay-iterationstart");
+
+ createAnimation({ opacity: [0, 1] },
+ { fill: "both" },
+ "fill-both");
+
+ createAnimation({ opacity: [0, 1] },
+ { fill: "both", delay: 500000, iterationStart: 0.5 },
+ "fill-both-width-delay-iterationstart");
+
+ createAnimation({ opacity: [0, 1] },
+ { fill: "forwards" },
+ "fill-forwards");
+
+ createAnimation({ opacity: [0, 1] },
+ { iterationStart: 0.5 },
+ "iterationstart");
+
+ createAnimation({ width: ["100px", "150px"] },
+ {},
+ "no-compositor");
+
+ createAnimation([{ opacity: 0, easing: "steps(2)" }, { opacity: 1 }],
+ {},
+ "keyframes-easing-step");
+
+ createAnimation(
+ [
+ {
+ opacity: 0,
+ offset: 0,
+ },
+ {
+ opacity: 1,
+ offset: 0.1,
+ easing: "steps(1)",
+ },
+ {
+ opacity: 0,
+ offset: 0.13,
+ },
+ ],
+ {},
+ "narrow-keyframes");
+
+ createAnimation(
+ [
+ {
+ offset: 0,
+ opacity: 1,
+ },
+ {
+ offset: 0.5,
+ opacity: 1,
+ },
+ {
+ offset: 0.5,
+ easing: "steps(1)",
+ opacity: 0,
+ },
+ {
+ offset: 1,
+ opacity: 1,
+ },
+ ],
+ {},
+ "duplicate-offsets");
+
+ createAnimation({ opacity: [0, 1] },
+ { delay: -250000 },
+ "delay-negative-25");
+
+ createAnimation({ opacity: [0, 1] },
+ { delay: -750000 },
+ "delay-negative-75");
+ </script>
+ </body>
+</html>
diff --git a/devtools/client/inspector/animation/test/doc_mutations_add_remove_immediately.html b/devtools/client/inspector/animation/test/doc_mutations_add_remove_immediately.html
new file mode 100644
index 0000000000..c8b3db749b
--- /dev/null
+++ b/devtools/client/inspector/animation/test/doc_mutations_add_remove_immediately.html
@@ -0,0 +1,22 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="UTF-8">
+ </head>
+ <body>
+ <div></div>
+
+ <script>
+ "use strict";
+
+ // This function is called from test.
+ // eslint-disable-next-line
+ function startMutation() {
+ const target = document.querySelector("div");
+ const animation = target.animate({ opacity: [1, 0] }, 100000);
+ animation.currentTime = 1;
+ animation.cancel();
+ }
+ </script>
+ </body>
+</html>
diff --git a/devtools/client/inspector/animation/test/doc_mutations_fast.html b/devtools/client/inspector/animation/test/doc_mutations_fast.html
new file mode 100644
index 0000000000..3622846953
--- /dev/null
+++ b/devtools/client/inspector/animation/test/doc_mutations_fast.html
@@ -0,0 +1,53 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="UTF-8">
+ <style>
+ div {
+ background-color: lime;
+ height: 20px;
+ opacity: 1;
+ transition: 0.5s opacity;
+ }
+
+ .transition {
+ opacity: 0;
+ }
+ </style>
+ </head>
+ <body>
+ <div></div>
+ <div></div>
+ <div></div>
+ <div></div>
+ <div></div>
+ <div></div>
+ <div></div>
+ <div></div>
+ <div></div>
+ <div></div>
+
+ <script>
+ "use strict";
+
+ // This function is called from test.
+ // eslint-disable-next-line
+ async function startFastMutations() {
+ const targets = document.querySelectorAll("div");
+
+ for (let i = 0; i < 10; i++) {
+ for (const target of targets) {
+ target.classList.toggle("transition");
+ await wait(15);
+ }
+ }
+ }
+
+ async function wait(ms) {
+ return new Promise(resolve => {
+ setTimeout(resolve, ms);
+ });
+ }
+ </script>
+ </body>
+</html>
diff --git a/devtools/client/inspector/animation/test/doc_negative_playback_rate.html b/devtools/client/inspector/animation/test/doc_negative_playback_rate.html
new file mode 100644
index 0000000000..a98700712d
--- /dev/null
+++ b/devtools/client/inspector/animation/test/doc_negative_playback_rate.html
@@ -0,0 +1,38 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="UTF-8">
+ <style>
+ div {
+ background-color: lime;
+ height: 100px;
+ }
+ </style>
+ </head>
+ <body>
+ <script>
+ "use strict";
+
+ const DURATION = 100000;
+ const KEYFRAMES = { backgroundColor: ["lime", "red"] };
+
+ function createAnimation(effect, className, playbackRate = -1) {
+ const div = document.createElement("div");
+ div.classList.add(className);
+ document.body.appendChild(div);
+ effect.duration = DURATION;
+ effect.fill = "forwards";
+ const animation = div.animate(KEYFRAMES, effect);
+ animation.updatePlaybackRate(playbackRate);
+ animation.play();
+ }
+
+ createAnimation({}, "normal");
+ createAnimation({}, "normal-playbackrate-2", -2);
+ createAnimation({ delay: 50000 }, "positive-delay");
+ createAnimation({ delay: -50000 }, "negative-delay");
+ createAnimation({ endDelay: 50000 }, "positive-end-delay");
+ createAnimation({ endDelay: -50000 }, "negative-end-delay");
+ </script>
+ </body>
+</html>
diff --git a/devtools/client/inspector/animation/test/doc_overflowed_delay_end_delay.html b/devtools/client/inspector/animation/test/doc_overflowed_delay_end_delay.html
new file mode 100644
index 0000000000..a4d91ae4ef
--- /dev/null
+++ b/devtools/client/inspector/animation/test/doc_overflowed_delay_end_delay.html
@@ -0,0 +1,75 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="UTF-8">
+ <style>
+ div {
+ width: 100px;
+ height: 100px;
+ outline: 1px solid lime;
+ }
+ </style>
+ </head>
+ <body>
+ <div id="target"></div>
+ <script>
+ "use strict";
+
+ const target = document.getElementById("target");
+ target.animate(
+ {
+ color: ["red", "lime"],
+ },
+ {
+ id: "big-delay",
+ duration: 1000,
+ delay: Number.MAX_VALUE,
+ iterations: Infinity,
+ });
+
+ target.animate(
+ {
+ opacity: [1, 0],
+ },
+ {
+ id: "big-end-delay",
+ duration: 1000,
+ endDelay: Number.MAX_VALUE,
+ iterations: Infinity,
+ });
+
+ target.animate(
+ {
+ marginLeft: ["0px", "100px"],
+ },
+ {
+ id: "negative-big-delay",
+ duration: 1000,
+ delay: -Number.MAX_VALUE,
+ iterations: Infinity,
+ });
+
+ target.animate(
+ {
+ paddingLeft: ["0px", "100px"],
+ },
+ {
+ id: "negative-big-end-delay",
+ duration: 1000,
+ endDelay: -Number.MAX_VALUE,
+ iterations: Infinity,
+ });
+
+ target.animate(
+ {
+ backgroundColor: ["lime", "white"],
+ },
+ {
+ id: "big-iteration-start",
+ duration: 1000,
+ iterations: Infinity,
+ iterationStart: Number.MAX_VALUE,
+ });
+ </script>
+ </body>
+</html>
diff --git a/devtools/client/inspector/animation/test/doc_pseudo.html b/devtools/client/inspector/animation/test/doc_pseudo.html
new file mode 100644
index 0000000000..3cc0c93470
--- /dev/null
+++ b/devtools/client/inspector/animation/test/doc_pseudo.html
@@ -0,0 +1,91 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="UTF-8">
+ <style>
+ body::before {
+ animation: body 10s infinite;
+ background-color: lime;
+ content: "body-before";
+ width: 100px;
+ }
+
+ .div-before::before {
+ animation: div-before 10s infinite;
+ background-color: lime;
+ content: "div-before";
+ width: 100px;
+ }
+
+ .div-after::after {
+ animation: div-after 10s infinite;
+ background-color: lime;
+ content: "div-after";
+ width: 100px;
+ }
+
+ .div-marker {
+ display: list-item;
+ list-style-position: inside;
+ }
+
+ .div-marker::marker {
+ content: "div-marker";
+ }
+
+ @keyframes body {
+ from {
+ opacity: 0;
+ }
+ to {
+ opacity: 1;
+ }
+ }
+
+ @keyframes div-before {
+ from {
+ opacity: 1;
+ }
+ to {
+ opacity: 0;
+ }
+ }
+
+ @keyframes div-after {
+ from {
+ opacity: 1;
+ }
+ 50% {
+ opacity: 0.9;
+ }
+ to {
+ opacity: 0;
+ }
+ }
+ </style>
+ </head>
+ <body>
+ <div class="div-before"></div>
+ <div class="div-after"></div>
+ <div class="div-marker"></div>
+
+ <script>
+ "use strict";
+
+ // The reason why we currently run the animation on `::marker` with Web Animations API
+ // instead of CSS Animations is because it requires `layout.css.marker.restricted`
+ // pref change.
+ document.querySelector(".div-marker").animate(
+ {
+ color: ["black", "lime"],
+ },
+ {
+ id: "div-marker",
+ duration: 10000,
+ iterations: Infinity,
+ pseudoElement: "::marker",
+ }
+ );
+ </script>
+ </body>
+</html>
diff --git a/devtools/client/inspector/animation/test/doc_short_duration.html b/devtools/client/inspector/animation/test/doc_short_duration.html
new file mode 100644
index 0000000000..ed9b2d94dc
--- /dev/null
+++ b/devtools/client/inspector/animation/test/doc_short_duration.html
@@ -0,0 +1,26 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="UTF-8">
+ <style>
+ div {
+ background-color: lime;
+ height: 100px;
+ }
+ </style>
+ </head>
+ <body>
+ <div class="short"></div>
+ <script>
+ "use strict";
+
+ document.querySelector(".short").animate(
+ { opacity: [1, 0] },
+ {
+ duration: 1,
+ iterations: Infinity,
+ }
+ );
+ </script>
+ </body>
+</html>
diff --git a/devtools/client/inspector/animation/test/doc_simple_animation.html b/devtools/client/inspector/animation/test/doc_simple_animation.html
new file mode 100644
index 0000000000..7e145166fe
--- /dev/null
+++ b/devtools/client/inspector/animation/test/doc_simple_animation.html
@@ -0,0 +1,193 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+ <style>
+ @property --ball-color {
+ syntax: "<color>";
+ inherits: true;
+ initial-value: #f06;
+ }
+
+ .ball {
+ width: 80px;
+ height: 80px;
+ /* Add a border here to avoid layout warnings in Linux debug builds: Bug 1329784 */
+ border: 1px solid transparent;
+ border-radius: 50%;
+ background: var(--ball-color);
+ position: absolute;
+ }
+
+ .still {
+ top: 0;
+ left: 10px;
+ }
+
+ .animated {
+ top: 100px;
+ left: 10px;
+
+ animation: simple-animation 2s infinite alternate;
+ }
+
+ .multi {
+ top: 200px;
+ left: 10px;
+
+ animation: simple-animation 2s infinite alternate,
+ other-animation 5s infinite alternate;
+ }
+
+ .delayed {
+ top: 300px;
+ left: 10px;
+ background: rebeccapurple;
+
+ animation: simple-animation 3s 60s 10;
+ }
+
+ .multi-finite {
+ top: 400px;
+ left: 10px;
+ background: yellow;
+
+ animation: simple-animation 3s,
+ other-animation 4s;
+ }
+
+ .short {
+ top: 500px;
+ left: 10px;
+ background: red;
+
+ animation: simple-animation 2s normal;
+ }
+
+ .long {
+ top: 600px;
+ left: 10px;
+ background: blue;
+
+ animation: simple-animation 120s;
+ }
+
+ .negative-delay {
+ top: 700px;
+ left: 10px;
+ background: gray;
+
+ animation: simple-animation 15s -10s;
+ animation-fill-mode: forwards;
+ }
+
+ .no-compositor {
+ top: 0;
+ right: 10px;
+ background: gold;
+
+ animation: no-compositor 10s cubic-bezier(.57,-0.02,1,.31) forwards;
+ }
+
+ .compositor-all {
+ animation: compositor-all 2s infinite;
+ }
+
+ .compositor-notall {
+ animation: compositor-notall 2s infinite;
+ }
+
+ .compositor-warning {
+ animation: compositor-all 2s infinite;
+ }
+
+ .warning-observer {
+ width: 10px;
+ height: 10px;
+ background-image: -moz-element(#warning);
+ }
+
+ .longhand {
+ animation: longhand 10s infinite;
+ }
+
+ @keyframes simple-animation {
+ 100% {
+ transform: translateX(300px);
+ }
+ }
+
+ @keyframes other-animation {
+ 100% {
+ background: blue;
+ }
+ }
+
+ @keyframes no-compositor {
+ 100% {
+ margin-right: 600px;
+ }
+ }
+
+ @keyframes compositor-all {
+ to { opacity: 0.5 }
+ }
+
+ @keyframes compositor-notall {
+ from {
+ --ball-color: tomato;
+ opacity: 0;
+ width: 0px;
+ transform: translate(0px);
+ }
+ to {
+ --ball-color: gold;
+ opacity: 1;
+ width: 100px;
+ transform: translate(100px);
+ }
+ }
+
+ @keyframes longhand {
+ from {
+ background: red;
+ padding: 0 0 0 10px;
+ }
+ to {
+ background: lime;
+ padding: 0 0 0 20px;
+ }
+ }
+ </style>
+</head>
+<body>
+ <!-- Comment node -->
+ <div class="ball still"></div>
+ <div class="ball animated"></div>
+ <div class="ball multi"></div>
+ <div class="ball delayed"></div>
+ <div class="ball multi-finite"></div>
+ <div class="ball short"></div>
+ <div class="ball long"></div>
+ <div class="ball negative-delay"></div>
+ <div class="ball no-compositor"></div>
+ <div class="ball end-delay"></div>
+ <div class="ball compositor-all"></div>
+ <div class="ball compositor-notall"></div>
+ <div class="ball compositor-warning" id="warning"></div>
+ <div class="ball longhand"></div>
+ <div class="warning-observer"></div>
+ <script>
+ /* globals KeyframeEffect, Animation */
+ "use strict";
+
+ const el = document.querySelector(".end-delay");
+ const effect = new KeyframeEffect(el, [
+ { opacity: 0, offset: 0 },
+ { opacity: 1, offset: 1 },
+ ], { duration: 1000000, endDelay: 500000, fill: "none" });
+ const animation = new Animation(effect, document.timeline);
+ animation.play();
+ </script>
+</body>
+</html>
diff --git a/devtools/client/inspector/animation/test/doc_special_colors.html b/devtools/client/inspector/animation/test/doc_special_colors.html
new file mode 100644
index 0000000000..2c71b2c963
--- /dev/null
+++ b/devtools/client/inspector/animation/test/doc_special_colors.html
@@ -0,0 +1,28 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="UTF-8">
+ <style>
+ div {
+ animation: anim 5s infinite;
+ border: 1px solid lime;
+ height: 100px;
+ width: 100px;
+ }
+
+ @keyframes anim {
+ from {
+ caret-color: auto;
+ scrollbar-color: lime red;
+ }
+ to {
+ caret-color: lime;
+ scrollbar-color: auto;
+ }
+ }
+ </style>
+ </head>
+ <body>
+ <div></div>
+ </body>
+</html>
diff --git a/devtools/client/inspector/animation/test/head.js b/devtools/client/inspector/animation/test/head.js
new file mode 100644
index 0000000000..959ad270a5
--- /dev/null
+++ b/devtools/client/inspector/animation/test/head.js
@@ -0,0 +1,1028 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* eslint no-unused-vars: [2, {"vars": "local", "args": "none"}] */
+
+"use strict";
+
+// Import the inspector's head.js first (which itself imports shared-head.js).
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/devtools/client/inspector/test/head.js",
+ this
+);
+
+const TAB_NAME = "animationinspector";
+
+const ANIMATION_L10N = new LocalizationHelper(
+ "devtools/client/locales/animationinspector.properties"
+);
+
+// Auto clean-up when a test ends.
+// Clean-up all prefs that might have been changed during a test run
+// (safer here because if the test fails, then the pref is never reverted)
+registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("devtools.toolsidebar-width.inspector");
+});
+
+/**
+ * Open the toolbox, with the inspector tool visible and the animationinspector
+ * sidebar selected.
+ *
+ * @return {Promise} that resolves when the inspector is ready.
+ */
+const openAnimationInspector = async function () {
+ const { inspector, toolbox } = await openInspectorSidebarTab(TAB_NAME);
+ await inspector.once("inspector-updated");
+ const animationInspector = inspector.getPanel("animationinspector");
+ const panel = inspector.panelWin.document.getElementById(
+ "animation-container"
+ );
+
+ info("Wait for loading first content");
+ const count = getDisplayedGraphCount(animationInspector, panel);
+ await waitUntil(
+ () =>
+ panel.querySelectorAll(".animation-summary-graph-path").length >= count &&
+ panel.querySelectorAll(".animation-target .objectBox").length >= count
+ );
+
+ if (
+ animationInspector.state.selectedAnimation &&
+ animationInspector.state.detailVisibility
+ ) {
+ await waitUntil(() => panel.querySelector(".animated-property-list"));
+ }
+
+ return { animationInspector, toolbox, inspector, panel };
+};
+
+/**
+ * Close the toolbox.
+ *
+ * @return {Promise} that resolves when the toolbox has closed.
+ */
+const closeAnimationInspector = async function () {
+ return gDevTools.closeToolboxForTab(gBrowser.selectedTab);
+};
+
+/**
+ * Some animation features are not enabled by default in release/beta channels
+ * yet including parts of the Web Animations API.
+ */
+const enableAnimationFeatures = function () {
+ return SpecialPowers.pushPrefEnv({
+ set: [["dom.animations-api.timelines.enabled", true]],
+ });
+};
+
+/**
+ * Add a new test tab in the browser and load the given url.
+ *
+ * @param {String} url
+ * The url to be loaded in the new tab
+ * @return a promise that resolves to the tab object when the url is loaded
+ */
+const _addTab = addTab;
+addTab = async function (url) {
+ await enableAnimationFeatures();
+ return _addTab(url);
+};
+
+/**
+ * Remove animated elements from document except given selectors.
+ *
+ * @param {Array} selectors
+ * @return {Promise}
+ */
+const removeAnimatedElementsExcept = function (selectors) {
+ return SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [selectors],
+ selectorsChild => {
+ function isRemovableElement(animation, selectorsInner) {
+ for (const selector of selectorsInner) {
+ if (animation.effect.target.matches(selector)) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ for (const animation of content.document.getAnimations()) {
+ if (isRemovableElement(animation, selectorsChild)) {
+ animation.effect.target.remove();
+ }
+ }
+ }
+ );
+};
+
+/**
+ * Click on an animation in the timeline to select it.
+ *
+ * @param {AnimationInspector} animationInspector.
+ * @param {DOMElement} panel
+ * #animation-container element.
+ * @param {Number} index
+ * The index of the animation to click on.
+ */
+const clickOnAnimation = async function (animationInspector, panel, index) {
+ info("Click on animation " + index + " in the timeline");
+ const animationItemEl = await findAnimationItemByIndex(panel, index);
+ const summaryGraphEl = animationItemEl.querySelector(
+ ".animation-summary-graph"
+ );
+ clickOnSummaryGraph(animationInspector, panel, summaryGraphEl);
+};
+
+/**
+ * Click on an animation by given selector of node which is target element of animation.
+ *
+ * @param {AnimationInspector} animationInspector.
+ * @param {DOMElement} panel
+ * #animation-container element.
+ * @param {String} selector
+ * Selector of node which is target element of animation.
+ */
+const clickOnAnimationByTargetSelector = async function (
+ animationInspector,
+ panel,
+ selector
+) {
+ info(`Click on animation whose selector of target element is '${selector}'`);
+ const animationItemEl = await findAnimationItemByTargetSelector(
+ panel,
+ selector
+ );
+ const summaryGraphEl = animationItemEl.querySelector(
+ ".animation-summary-graph"
+ );
+ clickOnSummaryGraph(animationInspector, panel, summaryGraphEl);
+};
+
+/**
+ * Click on close button for animation detail pane.
+ *
+ * @param {DOMElement} panel
+ * #animation-container element.
+ */
+const clickOnDetailCloseButton = function (panel) {
+ info("Click on close button for animation detail pane");
+ const buttonEl = panel.querySelector(".animation-detail-close-button");
+ const bounds = buttonEl.getBoundingClientRect();
+ const x = bounds.width / 2;
+ const y = bounds.height / 2;
+ EventUtils.synthesizeMouse(buttonEl, x, y, {}, buttonEl.ownerGlobal);
+};
+
+/**
+ * Click on pause/resume button.
+ *
+ * @param {AnimationInspector} animationInspector
+ * @param {DOMElement} panel
+ * #animation-container element.
+ */
+const clickOnPauseResumeButton = function (animationInspector, panel) {
+ info("Click on pause/resume button");
+ const buttonEl = panel.querySelector(".pause-resume-button");
+ const bounds = buttonEl.getBoundingClientRect();
+ const x = bounds.width / 2;
+ const y = bounds.height / 2;
+ EventUtils.synthesizeMouse(buttonEl, x, y, {}, buttonEl.ownerGlobal);
+};
+
+/**
+ * Click on rewind button.
+ *
+ * @param {AnimationInspector} animationInspector
+ * @param {DOMElement} panel
+ * #animation-container element.
+ */
+const clickOnRewindButton = function (animationInspector, panel) {
+ info("Click on rewind button");
+ const buttonEl = panel.querySelector(".rewind-button");
+ const bounds = buttonEl.getBoundingClientRect();
+ const x = bounds.width / 2;
+ const y = bounds.height / 2;
+ EventUtils.synthesizeMouse(buttonEl, x, y, {}, buttonEl.ownerGlobal);
+};
+
+/**
+ * Click on the scrubber controller pane to update the animation current time.
+ *
+ * @param {DOMElement} panel
+ * #animation-container element.
+ * @param {Number} mouseDownPosition
+ * rate on scrubber controller pane.
+ * This method calculates
+ * `mouseDownPosition * offsetWidth + offsetLeft of scrubber controller pane`
+ * as the clientX of MouseEvent.
+ */
+const clickOnCurrentTimeScrubberController = function (
+ animationInspector,
+ panel,
+ mouseDownPosition
+) {
+ const controllerEl = panel.querySelector(".current-time-scrubber-area");
+ const bounds = controllerEl.getBoundingClientRect();
+ const mousedonwX = bounds.width * mouseDownPosition;
+
+ info(`Click ${mousedonwX} on scrubber controller`);
+ EventUtils.synthesizeMouse(
+ controllerEl,
+ mousedonwX,
+ 0,
+ {},
+ controllerEl.ownerGlobal
+ );
+};
+
+/**
+ * Click on the inspect icon for the given AnimationTargetComponent.
+ *
+ * @param {AnimationInspector} animationInspector.
+ * @param {DOMElement} panel
+ * #animation-container element.
+ * @param {Number} index
+ * The index of the AnimationTargetComponent to click on.
+ */
+const clickOnInspectIcon = async function (animationInspector, panel, index) {
+ info(`Click on an inspect icon in animation target component[${index}]`);
+ const animationItemEl = await findAnimationItemByIndex(panel, index);
+ const iconEl = animationItemEl.querySelector(
+ ".animation-target .objectBox .highlight-node"
+ );
+ iconEl.scrollIntoView(false);
+ EventUtils.synthesizeMouseAtCenter(iconEl, {}, iconEl.ownerGlobal);
+};
+
+/**
+ * Change playback rate selector to select given rate.
+ *
+ * @param {AnimationInspector} animationInspector
+ * @param {DOMElement} panel
+ * #animation-container element.
+ * @param {Number} rate
+ */
+const changePlaybackRateSelector = async function (
+ animationInspector,
+ panel,
+ rate
+) {
+ info(`Click on playback rate selector to select ${rate}`);
+ const selectEl = panel.querySelector(".playback-rate-selector");
+ const optionIndex = [...selectEl.options].findIndex(o => +o.value == rate);
+
+ if (optionIndex == -1) {
+ ok(
+ false,
+ `Could not find an option for rate ${rate} in the rate selector. ` +
+ `Values are: ${[...selectEl.options].map(o => o.value)}`
+ );
+ return;
+ }
+
+ selectEl.focus();
+
+ const win = selectEl.ownerGlobal;
+ while (selectEl.selectedIndex != optionIndex) {
+ const key = selectEl.selectedIndex > optionIndex ? "LEFT" : "RIGHT";
+ EventUtils.sendKey(key, win);
+ }
+};
+
+/**
+ * Click on given summary graph element.
+ *
+ * @param {AnimationInspector} animationInspector
+ * @param {DOMElement} panel
+ * #animation-container element.
+ * @param {Element} summaryGraphEl
+ */
+const clickOnSummaryGraph = function (
+ animationInspector,
+ panel,
+ summaryGraphEl
+) {
+ // Disable pointer-events of the scrubber in order to avoid to click accidently.
+ const scrubberEl = panel.querySelector(".current-time-scrubber");
+ scrubberEl.style.pointerEvents = "none";
+ // Scroll to show the timeBlock since the element may be out of displayed area.
+ summaryGraphEl.scrollIntoView(false);
+ EventUtils.synthesizeMouseAtCenter(
+ summaryGraphEl,
+ {},
+ summaryGraphEl.ownerGlobal
+ );
+ // Restore the scrubber style.
+ scrubberEl.style.pointerEvents = "unset";
+};
+
+/**
+ * Click on the target node for the given AnimationTargetComponent index.
+ *
+ * @param {AnimationInspector} animationInspector.
+ * @param {DOMElement} panel
+ * #animation-container element.
+ * @param {Number} index
+ * The index of the AnimationTargetComponent to click on.
+ */
+const clickOnTargetNode = async function (animationInspector, panel, index) {
+ const { inspector } = animationInspector;
+ const { waitForHighlighterTypeShown } = getHighlighterTestHelpers(inspector);
+ info(`Click on a target node in animation target component[${index}]`);
+
+ const animationItemEl = await findAnimationItemByIndex(panel, index);
+ const targetEl = animationItemEl.querySelector(
+ ".animation-target .objectBox"
+ );
+ const onHighlight = waitForHighlighterTypeShown(
+ inspector.highlighters.TYPES.BOXMODEL
+ );
+ EventUtils.synthesizeMouseAtCenter(targetEl, {}, targetEl.ownerGlobal);
+ await onHighlight;
+};
+
+/**
+ * Drag on the scrubber to update the animation current time.
+ *
+ * @param {DOMElement} panel
+ * #animation-container element.
+ * @param {Number} mouseMovePixel
+ * Dispatch mousemove event with mouseMovePosition after mousedown.
+ * @param {Number} mouseYPixel
+ * Y of mouse in pixel.
+ */
+const dragOnCurrentTimeScrubber = async function (
+ animationInspector,
+ panel,
+ mouseMovePixel,
+ mouseYPixel
+) {
+ const controllerEl = panel.querySelector(".current-time-scrubber");
+ info(`Drag scrubber to X ${mouseMovePixel}`);
+ EventUtils.synthesizeMouse(
+ controllerEl,
+ 0,
+ mouseYPixel,
+ { type: "mousedown" },
+ controllerEl.ownerGlobal
+ );
+ await waitUntilAnimationsPlayState(animationInspector, "paused");
+
+ const animation = animationInspector.state.animations[0];
+ let currentTime = animation.state.currentTime;
+ EventUtils.synthesizeMouse(
+ controllerEl,
+ mouseMovePixel,
+ mouseYPixel,
+ { type: "mousemove" },
+ controllerEl.ownerGlobal
+ );
+ await waitUntil(() => animation.state.currentTime !== currentTime);
+
+ currentTime = animation.state.currentTime;
+ EventUtils.synthesizeMouse(
+ controllerEl,
+ mouseMovePixel,
+ mouseYPixel,
+ { type: "mouseup" },
+ controllerEl.ownerGlobal
+ );
+ await waitUntil(() => animation.state.currentTime !== currentTime);
+};
+
+/**
+ * Drag on the scrubber controller pane to update the animation current time.
+ *
+ * @param {DOMElement} panel
+ * #animation-container element.
+ * @param {Number} mouseDownPosition
+ * rate on scrubber controller pane.
+ * This method calculates
+ * `mouseDownPosition * offsetWidth + offsetLeft of scrubber controller pane`
+ * as the clientX of MouseEvent.
+ * @param {Number} mouseMovePosition
+ * Dispatch mousemove event with mouseMovePosition after mousedown.
+ * Calculation for clinetX is same to above.
+ */
+const dragOnCurrentTimeScrubberController = async function (
+ animationInspector,
+ panel,
+ mouseDownPosition,
+ mouseMovePosition
+) {
+ const controllerEl = panel.querySelector(".current-time-scrubber-area");
+ const bounds = controllerEl.getBoundingClientRect();
+ const mousedonwX = bounds.width * mouseDownPosition;
+ const mousemoveX = bounds.width * mouseMovePosition;
+
+ info(`Drag on scrubber controller from ${mousedonwX} to ${mousemoveX}`);
+ EventUtils.synthesizeMouse(
+ controllerEl,
+ mousedonwX,
+ 0,
+ { type: "mousedown" },
+ controllerEl.ownerGlobal
+ );
+ await waitUntilAnimationsPlayState(animationInspector, "paused");
+
+ const animation = animationInspector.state.animations[0];
+ let currentTime = animation.state.currentTime;
+ EventUtils.synthesizeMouse(
+ controllerEl,
+ mousemoveX,
+ 0,
+ { type: "mousemove" },
+ controllerEl.ownerGlobal
+ );
+ await waitUntil(() => animation.state.currentTime !== currentTime);
+
+ currentTime = animation.state.currentTime;
+ EventUtils.synthesizeMouse(
+ controllerEl,
+ mousemoveX,
+ 0,
+ { type: "mouseup" },
+ controllerEl.ownerGlobal
+ );
+ await waitUntil(() => animation.state.currentTime !== currentTime);
+};
+
+/**
+ * Get current animation duration and rate of
+ * clickOrDragOnCurrentTimeScrubberController in given pixels.
+ *
+ * @param {AnimationInspector} animationInspector
+ * @param {DOMElement} panel
+ * #animation-container element.
+ * @param {Number} pixels
+ * @return {Object}
+ * {
+ * duration,
+ * rate,
+ * }
+ */
+const getDurationAndRate = function (animationInspector, panel, pixels) {
+ const controllerEl = panel.querySelector(".current-time-scrubber-area");
+ const bounds = controllerEl.getBoundingClientRect();
+ const duration =
+ (animationInspector.state.timeScale.getDuration() / bounds.width) * pixels;
+ const rate = (1 / bounds.width) * pixels;
+ return { duration, rate };
+};
+
+/**
+ * Mouse over the target node for the given AnimationTargetComponent index.
+ *
+ * @param {AnimationInspector} animationInspector.
+ * @param {DOMElement} panel
+ * #animation-container element.
+ * @param {Number} index
+ * The index of the AnimationTargetComponent to click on.
+ */
+const mouseOverOnTargetNode = function (animationInspector, panel, index) {
+ info(`Mouse over on a target node in animation target component[${index}]`);
+ const el = panel.querySelectorAll(".animation-target .objectBox")[index];
+ el.scrollIntoView(false);
+ EventUtils.synthesizeMouse(el, 10, 5, { type: "mouseover" }, el.ownerGlobal);
+};
+
+/**
+ * Mouse out of the target node for the given AnimationTargetComponent index.
+ *
+ * @param {AnimationInspector} animationInspector.
+ * @param {DOMElement} panel
+ * #animation-container element.
+ * @param {Number} index
+ * The index of the AnimationTargetComponent to click on.
+ */
+const mouseOutOnTargetNode = function (animationInspector, panel, index) {
+ info(`Mouse out on a target node in animation target component[${index}]`);
+ const el = panel.querySelectorAll(".animation-target .objectBox")[index];
+ el.scrollIntoView(false);
+ EventUtils.synthesizeMouse(el, -1, -1, { type: "mouseout" }, el.ownerGlobal);
+};
+
+/**
+ * Select animation inspector in sidebar and toolbar.
+ *
+ * @param {InspectorPanel} inspector
+ */
+const selectAnimationInspector = async function (inspector) {
+ await inspector.toolbox.selectTool("inspector");
+ const onDispatched = waitForDispatch(inspector.store, "UPDATE_ANIMATIONS");
+ inspector.sidebar.select("animationinspector");
+ await onDispatched;
+};
+
+/**
+ * Send keyboard event of space to given panel.
+ *
+ * @param {AnimationInspector} animationInspector
+ * @param {DOMElement} target element.
+ */
+const sendSpaceKeyEvent = function (animationInspector, element) {
+ element.focus();
+ EventUtils.sendKey("SPACE", element.ownerGlobal);
+};
+
+/**
+ * Set a node class attribute to the given selector.
+ *
+ * @param {AnimationInspector} animationInspector
+ * @param {String} selector
+ * @param {String} cls
+ * e.g. ".ball.still"
+ */
+const setClassAttribute = async function (animationInspector, selector, cls) {
+ await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [cls, selector],
+ (attributeValue, selectorChild) => {
+ const node = content.document.querySelector(selectorChild);
+ if (!node) {
+ return;
+ }
+
+ node.setAttribute("class", attributeValue);
+ }
+ );
+};
+
+/**
+ * Set a new style properties to the node for the given selector.
+ *
+ * @param {AnimationInspector} animationInspector
+ * @param {String} selector
+ * @param {Object} properties
+ * e.g. {
+ * animationDuration: "1000ms",
+ * animationTimingFunction: "linear",
+ * }
+ */
+const setEffectTimingAndPlayback = async function (
+ animationInspector,
+ selector,
+ effectTiming,
+ playbackRate
+) {
+ await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [selector, playbackRate, effectTiming],
+ (selectorChild, playbackRateChild, effectTimingChild) => {
+ let selectedAnimation = null;
+
+ for (const animation of content.document.getAnimations()) {
+ if (animation.effect.target.matches(selectorChild)) {
+ selectedAnimation = animation;
+ break;
+ }
+ }
+
+ if (!selectedAnimation) {
+ return;
+ }
+
+ selectedAnimation.playbackRate = playbackRateChild;
+ selectedAnimation.effect.updateTiming(effectTimingChild);
+ }
+ );
+};
+
+/**
+ * Set the sidebar width by given parameter.
+ *
+ * @param {String} width
+ * Change sidebar width by given parameter.
+ * @param {InspectorPanel} inspector
+ * The instance of InspectorPanel currently loaded in the toolbox
+ * @return {Promise} Resolves when the sidebar size changed.
+ */
+const setSidebarWidth = async function (width, inspector) {
+ const onUpdated = inspector.toolbox.once("inspector-sidebar-resized");
+ inspector.splitBox.setState({ width });
+ await onUpdated;
+};
+
+/**
+ * Set a new style property declaration to the node for the given selector.
+ *
+ * @param {AnimationInspector} animationInspector
+ * @param {String} selector
+ * @param {String} propertyName
+ * e.g. "animationDuration"
+ * @param {String} propertyValue
+ * e.g. "5.5s"
+ */
+const setStyle = async function (
+ animationInspector,
+ selector,
+ propertyName,
+ propertyValue
+) {
+ await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [selector, propertyName, propertyValue],
+ (selectorChild, propertyNameChild, propertyValueChild) => {
+ const node = content.document.querySelector(selectorChild);
+ if (!node) {
+ return;
+ }
+
+ node.style[propertyNameChild] = propertyValueChild;
+ }
+ );
+};
+
+/**
+ * Set a new style properties to the node for the given selector.
+ *
+ * @param {AnimationInspector} animationInspector
+ * @param {String} selector
+ * @param {Object} properties
+ * e.g. {
+ * animationDuration: "1000ms",
+ * animationTimingFunction: "linear",
+ * }
+ */
+const setStyles = async function (animationInspector, selector, properties) {
+ await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [properties, selector],
+ (propertiesChild, selectorChild) => {
+ const node = content.document.querySelector(selectorChild);
+ if (!node) {
+ return;
+ }
+
+ for (const propertyName in propertiesChild) {
+ const propertyValue = propertiesChild[propertyName];
+ node.style[propertyName] = propertyValue;
+ }
+ }
+ );
+};
+
+/**
+ * Wait until current time of animations will be changed to given current time.
+ *
+ * @param {AnimationInspector} animationInspector
+ * @param {Number} currentTime
+ */
+const waitUntilCurrentTimeChangedAt = async function (
+ animationInspector,
+ currentTime
+) {
+ info(`Wait until current time will be change to ${currentTime}`);
+ await waitUntil(() =>
+ animationInspector.state.animations.every(a => {
+ return a.state.currentTime === currentTime;
+ })
+ );
+};
+
+/**
+ * Wait until animations' play state will be changed to given state.
+ *
+ * @param {Array} animationInspector
+ * @param {String} state
+ */
+const waitUntilAnimationsPlayState = async function (
+ animationInspector,
+ state
+) {
+ info(`Wait until play state will be change to ${state}`);
+ await waitUntil(() =>
+ animationInspector.state.animations.every(a => a.state.playState === state)
+ );
+};
+
+/**
+ * Return count of graph that animation inspector is displaying.
+ *
+ * @param {AnimationInspector} animationInspector
+ * @param {DOMElement} panel
+ * @return {Number} count
+ */
+const getDisplayedGraphCount = (animationInspector, panel) => {
+ const animationLength = animationInspector.state.animations.length;
+ if (animationLength === 0) {
+ return 0;
+ }
+
+ const inspectionPanelEl = panel.querySelector(".progress-inspection-panel");
+ const itemEl = panel.querySelector(".animation-item");
+ const listEl = panel.querySelector(".animation-list");
+ const itemHeight = itemEl.offsetHeight;
+ // This calculation should be same as AnimationListContainer.updateDisplayableRange.
+ const count = Math.floor(listEl.offsetHeight / itemHeight) + 1;
+ const index = Math.floor(inspectionPanelEl.scrollTop / itemHeight);
+
+ return animationLength > index + count ? count : animationLength - index;
+};
+
+/**
+ * Check whether the animations are pausing.
+ *
+ * @param {AnimationInspector} animationInspector
+ */
+function assertAnimationsPausing(animationInspector) {
+ assertAnimationsPausingOrRunning(animationInspector, true);
+}
+
+/**
+ * Check whether the animations are pausing/running.
+ *
+ * @param {AnimationInspector} animationInspector
+ * @param {boolean} shouldPause
+ */
+function assertAnimationsPausingOrRunning(animationInspector, shouldPause) {
+ const hasRunningAnimation = animationInspector.state.animations.some(
+ ({ state }) => state.playState === "running"
+ );
+
+ if (shouldPause) {
+ is(hasRunningAnimation, false, "All animations should be paused");
+ } else {
+ is(hasRunningAnimation, true, "Animations should be running at least one");
+ }
+}
+
+/**
+ * Check whether the animations are running.
+ *
+ * @param {AnimationInspector} animationInspector
+ */
+function assertAnimationsRunning(animationInspector) {
+ assertAnimationsPausingOrRunning(animationInspector, false);
+}
+
+/**
+ * Check the <stop> element in the given linearGradientEl for the correct offset
+ * and color attributes.
+ *
+ * @param {Element} linearGradientEl
+ <linearGradient> element which has <stop> element.
+ * @param {Number} offset
+ * float which represents the "offset" attribute of <stop>.
+ * @param {String} expectedColor
+ * 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
+ * JSON object format. We can test the vertex and color.
+ * e.g.
+ * [
+ * { x: 0, y: 0 },
+ * { x: 0, y: 1 },
+ * ]
+ */
+function assertPathSegments(pathEl, hasClosePath, expectedValues) {
+ ok(
+ isExpectedPath(pathEl, hasClosePath, expectedValues),
+ "All of path segments are correct"
+ );
+}
+
+function isExpectedPath(pathEl, hasClosePath, expectedValues) {
+ const pathSegList = pathEl.pathSegList;
+ if (!pathSegList) {
+ return false;
+ }
+
+ if (
+ !expectedValues.every(value =>
+ isPassingThrough(pathSegList, value.x, value.y)
+ )
+ ) {
+ return false;
+ }
+
+ if (hasClosePath) {
+ const closePathSeg = pathSegList.getItem(pathSegList.numberOfItems - 1);
+ if (closePathSeg.pathSegType !== closePathSeg.PATHSEG_CLOSEPATH) {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+/**
+ * Check whether the given vertex is passing throug on the path.
+ *
+ * @param {pathSegList} pathSegList - pathSegList of <path> element.
+ * @param {float} x - x of vertex.
+ * @param {float} y - y of vertex.
+ * @return {boolean} true: passing through, false: no on the path.
+ */
+function isPassingThrough(pathSegList, x, y) {
+ let previousPathSeg = pathSegList.getItem(0);
+ for (let i = 0; i < pathSegList.numberOfItems; i++) {
+ const pathSeg = pathSegList.getItem(i);
+ if (pathSeg.x === undefined) {
+ continue;
+ }
+ const currentX = parseFloat(pathSeg.x.toFixed(3));
+ const currentY = parseFloat(pathSeg.y.toFixed(3));
+ if (currentX === x && currentY === y) {
+ return true;
+ }
+ const previousX = parseFloat(previousPathSeg.x.toFixed(3));
+ const previousY = parseFloat(previousPathSeg.y.toFixed(3));
+ if (
+ previousX <= x &&
+ x <= currentX &&
+ Math.min(previousY, currentY) <= y &&
+ y <= Math.max(previousY, currentY)
+ ) {
+ return true;
+ }
+ previousPathSeg = pathSeg;
+ }
+ return false;
+}
+
+/**
+ * Return animation item element by the index.
+ *
+ * @param {DOMElement} panel
+ * #animation-container element.
+ * @param {Number} index
+ * @return {DOMElement}
+ * Animation item element.
+ */
+async function findAnimationItemByIndex(panel, index) {
+ const itemEls = [...panel.querySelectorAll(".animation-item")];
+ const itemEl = itemEls[index];
+ itemEl.scrollIntoView(false);
+
+ await waitUntil(
+ () =>
+ itemEl.querySelector(".animation-target .attrName") &&
+ itemEl.querySelector(".animation-computed-timing-path")
+ );
+
+ return itemEl;
+}
+
+/**
+ * Return animation item element by target node selector.
+ * This function compares betweem animation-target textContent and given selector.
+ * Then returns matched first item.
+ *
+ * @param {DOMElement} panel
+ * #animation-container element.
+ * @param {String} selector
+ * Selector of tested element.
+ * @return {DOMElement}
+ * Animation item element.
+ */
+async function findAnimationItemByTargetSelector(panel, selector) {
+ for (const itemEl of panel.querySelectorAll(".animation-item")) {
+ itemEl.scrollIntoView(false);
+
+ await waitUntil(
+ () =>
+ itemEl.querySelector(".animation-target .attrName") &&
+ itemEl.querySelector(".animation-computed-timing-path")
+ );
+
+ const attrNameEl = itemEl.querySelector(".animation-target .attrName");
+ const regexp = new RegExp(`\\${selector}(\\.|$)`, "gi");
+ if (regexp.exec(attrNameEl.textContent)) {
+ return itemEl;
+ }
+ }
+
+ 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;
+}
+
+/**
+ * Do test for keyframes-graph_computed-value-path-1/2.
+ *
+ * @param {Array} testData
+ */
+async function testKeyframesGraphComputedValuePath(testData) {
+ await addTab(URL_ROOT + "doc_multi_keyframes.html");
+ await removeAnimatedElementsExcept(testData.map(t => `.${t.targetClass}`));
+ const { animationInspector, panel } = await openAnimationInspector();
+
+ for (const { properties, targetClass } of testData) {
+ info(`Checking keyframes graph for ${targetClass}`);
+ const onDetailRendered = animationInspector.once(
+ "animation-keyframes-rendered"
+ );
+ await clickOnAnimationByTargetSelector(
+ animationInspector,
+ panel,
+ `.${targetClass}`
+ );
+ await onDetailRendered;
+
+ for (const property of properties) {
+ const {
+ name,
+ computedValuePathClass,
+ expectedPathSegments,
+ expectedStopColors,
+ } = property;
+
+ const testTarget = `${name} in ${targetClass}`;
+ 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);
+ }
+ }
+ }
+}
+
+/**
+ * Check the adjusted current time and created time from specified two animations.
+ *
+ * @param {AnimationPlayerFront.state} animation1
+ * @param {AnimationPlayerFront.state} animation2
+ */
+function checkAdjustingTheTime(animation1, animation2) {
+ const adjustedCurrentTimeDiff =
+ animation2.currentTime / animation2.playbackRate -
+ animation1.currentTime / animation1.playbackRate;
+ const createdTimeDiff = animation1.createdTime - animation2.createdTime;
+ Assert.less(
+ Math.abs(adjustedCurrentTimeDiff - createdTimeDiff),
+ 0.1,
+ "Adjusted time is correct"
+ );
+}
diff --git a/devtools/client/inspector/animation/test/keyframes-graph_keyframe-marker_head.js b/devtools/client/inspector/animation/test/keyframes-graph_keyframe-marker_head.js
new file mode 100644
index 0000000000..97c7040553
--- /dev/null
+++ b/devtools/client/inspector/animation/test/keyframes-graph_keyframe-marker_head.js
@@ -0,0 +1,237 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/* import-globals-from head.js */
+
+// Test for following keyframe marker.
+// * element existence
+// * title
+// * and marginInlineStart style
+
+const KEYFRAMES_TEST_DATA = [
+ {
+ targetClass: "multi-types",
+ properties: [
+ {
+ name: "background-color",
+ expectedValues: [
+ {
+ title: "rgb(255, 0, 0)",
+ marginInlineStart: "0%",
+ },
+ {
+ title: "rgb(0, 255, 0)",
+ marginInlineStart: "100%",
+ },
+ ],
+ },
+ {
+ name: "background-repeat",
+ expectedValues: [
+ {
+ title: "space round",
+ marginInlineStart: "0%",
+ },
+ {
+ title: "round space",
+ marginInlineStart: "100%",
+ },
+ ],
+ },
+ {
+ name: "font-size",
+ expectedValues: [
+ {
+ title: "10px",
+ marginInlineStart: "0%",
+ },
+ {
+ title: "20px",
+ marginInlineStart: "100%",
+ },
+ ],
+ },
+ {
+ name: "margin-left",
+ expectedValues: [
+ {
+ title: "0px",
+ marginInlineStart: "0%",
+ },
+ {
+ title: "100px",
+ marginInlineStart: "100%",
+ },
+ ],
+ },
+ {
+ name: "opacity",
+ expectedValues: [
+ {
+ title: "0",
+ marginInlineStart: "0%",
+ },
+ {
+ title: "1",
+ marginInlineStart: "100%",
+ },
+ ],
+ },
+ {
+ name: "text-align",
+ expectedValues: [
+ {
+ title: "right",
+ marginInlineStart: "0%",
+ },
+ {
+ title: "center",
+ marginInlineStart: "100%",
+ },
+ ],
+ },
+ {
+ name: "transform",
+ expectedValues: [
+ {
+ title: "translate(0px)",
+ marginInlineStart: "0%",
+ },
+ {
+ title: "translate(100px)",
+ marginInlineStart: "100%",
+ },
+ ],
+ },
+ ],
+ },
+ {
+ targetClass: "narrow-offsets",
+ properties: [
+ {
+ name: "opacity",
+ expectedValues: [
+ {
+ title: "0",
+ marginInlineStart: "0%",
+ },
+ {
+ title: "1",
+ marginInlineStart: "10%",
+ },
+ {
+ title: "0",
+ marginInlineStart: "13%",
+ },
+ {
+ title: "1",
+ marginInlineStart: "100%",
+ },
+ ],
+ },
+ ],
+ },
+ {
+ targetClass: "same-color",
+ properties: [
+ {
+ name: "background-color",
+ expectedValues: [
+ {
+ title: "rgb(0, 255, 0)",
+ marginInlineStart: "0%",
+ },
+ {
+ title: "rgb(0, 255, 0)",
+ marginInlineStart: "100%",
+ },
+ ],
+ },
+ ],
+ },
+ {
+ targetClass: "currentcolor",
+ properties: [
+ {
+ name: "background-color",
+ expectedValues: [
+ {
+ title: "currentcolor",
+ marginInlineStart: "0%",
+ },
+ {
+ title: "rgb(0, 255, 0)",
+ marginInlineStart: "100%",
+ },
+ ],
+ },
+ ],
+ },
+];
+
+/**
+ * Do test for keyframes-graph_keyframe-marker-ltf/rtl.
+ *
+ * @param {Array} testData
+ */
+// eslint-disable-next-line no-unused-vars
+async function testKeyframesGraphKeyframesMarker() {
+ await addTab(URL_ROOT + "doc_multi_keyframes.html");
+ await removeAnimatedElementsExcept(
+ KEYFRAMES_TEST_DATA.map(t => `.${t.targetClass}`)
+ );
+ const { animationInspector, panel } = await openAnimationInspector();
+
+ for (const { properties, targetClass } of KEYFRAMES_TEST_DATA) {
+ info(`Checking keyframe marker for ${targetClass}`);
+ const onDetailRendered = animationInspector.once(
+ "animation-keyframes-rendered"
+ );
+ await clickOnAnimationByTargetSelector(
+ animationInspector,
+ panel,
+ `.${targetClass}`
+ );
+ await onDetailRendered;
+
+ for (const { name, expectedValues } of properties) {
+ const testTarget = `${name} in ${targetClass}`;
+ 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 marginInlineStart style in ${hintTarget}`);
+ is(
+ markerEl.style.marginInlineStart,
+ expectedValue.marginInlineStart,
+ `marginInlineStart in ${hintTarget} should be ` +
+ `${expectedValue.marginInlineStart}`
+ );
+ }
+ }
+ }
+}
diff --git a/devtools/client/inspector/animation/test/summary-graph_computed-timing-path_head.js b/devtools/client/inspector/animation/test/summary-graph_computed-timing-path_head.js
new file mode 100644
index 0000000000..8516e96fa3
--- /dev/null
+++ b/devtools/client/inspector/animation/test/summary-graph_computed-timing-path_head.js
@@ -0,0 +1,103 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/* import-globals-from head.js */
+
+/**
+ * Test for computed timing path on summary graph using given test data.
+ * @param {Array} testData
+ */
+// eslint-disable-next-line no-unused-vars
+async function testComputedTimingPath(testData) {
+ await addTab(URL_ROOT + "doc_multi_timings.html");
+ await removeAnimatedElementsExcept(testData.map(t => `.${t.targetClass}`));
+ const { panel } = await openAnimationInspector();
+
+ for (const {
+ expectedDelayPath,
+ expectedEndDelayPath,
+ expectedForwardsPath,
+ expectedIterationPathList,
+ isInfinity,
+ targetClass,
+ } of testData) {
+ const animationItemEl = await findAnimationItemByTargetSelector(
+ panel,
+ `.${targetClass}`
+ );
+
+ info(`Checking computed timing path existance for ${targetClass}`);
+ const computedTimingPathEl = animationItemEl.querySelector(
+ ".animation-computed-timing-path"
+ );
+ ok(
+ computedTimingPathEl,
+ "The computed timing path element should be in each animation item element"
+ );
+
+ info(`Checking delay path for ${targetClass}`);
+ const delayPathEl = computedTimingPathEl.querySelector(
+ ".animation-delay-path"
+ );
+
+ if (expectedDelayPath) {
+ ok(delayPathEl, "delay path should be existance");
+ assertPathSegments(delayPathEl, true, expectedDelayPath);
+ } else {
+ ok(!delayPathEl, "delay path should not be existance");
+ }
+
+ info(`Checking iteration path list for ${targetClass}`);
+ const iterationPathEls = computedTimingPathEl.querySelectorAll(
+ ".animation-iteration-path"
+ );
+ is(
+ iterationPathEls.length,
+ expectedIterationPathList.length,
+ `Number of iteration path should be ${expectedIterationPathList.length}`
+ );
+
+ for (const [j, iterationPathEl] of iterationPathEls.entries()) {
+ assertPathSegments(iterationPathEl, true, expectedIterationPathList[j]);
+
+ info(`Checking infinity ${targetClass}`);
+ if (isInfinity && j >= 1) {
+ ok(
+ iterationPathEl.classList.contains("infinity"),
+ "iteration path should have 'infinity' class"
+ );
+ } else {
+ ok(
+ !iterationPathEl.classList.contains("infinity"),
+ "iteration path should not have 'infinity' class"
+ );
+ }
+ }
+
+ info(`Checking endDelay path for ${targetClass}`);
+ const endDelayPathEl = computedTimingPathEl.querySelector(
+ ".animation-enddelay-path"
+ );
+
+ if (expectedEndDelayPath) {
+ ok(endDelayPathEl, "endDelay path should be existance");
+ assertPathSegments(endDelayPathEl, true, expectedEndDelayPath);
+ } else {
+ ok(!endDelayPathEl, "endDelay path should not be existance");
+ }
+
+ info(`Checking forwards fill path for ${targetClass}`);
+ const forwardsPathEl = computedTimingPathEl.querySelector(
+ ".animation-fill-forwards-path"
+ );
+
+ if (expectedForwardsPath) {
+ ok(forwardsPathEl, "forwards path should be existance");
+ assertPathSegments(forwardsPathEl, true, expectedForwardsPath);
+ } else {
+ ok(!forwardsPathEl, "forwards path should not be existance");
+ }
+ }
+}
diff --git a/devtools/client/inspector/animation/test/summary-graph_delay-sign_head.js b/devtools/client/inspector/animation/test/summary-graph_delay-sign_head.js
new file mode 100644
index 0000000000..fd601821b6
--- /dev/null
+++ b/devtools/client/inspector/animation/test/summary-graph_delay-sign_head.js
@@ -0,0 +1,106 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/* import-globals-from head.js */
+
+// Test for following DelaySign component works.
+// * element existance
+// * marginInlineStart position
+// * width
+// * additinal class
+
+const TEST_DATA = [
+ {
+ targetClass: "delay-positive",
+ expectedResult: {
+ marginInlineStart: "25%",
+ width: "25%",
+ },
+ },
+ {
+ targetClass: "delay-negative",
+ expectedResult: {
+ additionalClass: "negative",
+ marginInlineStart: "0%",
+ width: "25%",
+ },
+ },
+ {
+ targetClass: "fill-backwards-with-delay-iterationstart",
+ expectedResult: {
+ additionalClass: "fill",
+ marginInlineStart: "25%",
+ width: "25%",
+ },
+ },
+ {
+ targetClass: "fill-both",
+ },
+ {
+ targetClass: "fill-both-width-delay-iterationstart",
+ expectedResult: {
+ additionalClass: "fill",
+ marginInlineStart: "25%",
+ width: "25%",
+ },
+ },
+ {
+ targetClass: "keyframes-easing-step",
+ },
+];
+
+// eslint-disable-next-line no-unused-vars
+async function testSummaryGraphDelaySign() {
+ await addTab(URL_ROOT + "doc_multi_timings.html");
+ await removeAnimatedElementsExcept(TEST_DATA.map(t => `.${t.targetClass}`));
+ const { panel } = await openAnimationInspector();
+
+ for (const { targetClass, expectedResult } of TEST_DATA) {
+ const animationItemEl = await findAnimationItemByTargetSelector(
+ panel,
+ `.${targetClass}`
+ );
+
+ info(`Checking delay sign existance for ${targetClass}`);
+ const delaySignEl = animationItemEl.querySelector(".animation-delay-sign");
+
+ if (expectedResult) {
+ ok(
+ delaySignEl,
+ "The delay sign element should be in animation item element"
+ );
+
+ function assertExpected(key) {
+ const actual = parseFloat(delaySignEl.style[key]);
+ const expected = parseFloat(expectedResult[key]);
+ ok(
+ Math.abs(actual - expected) < 0.01,
+ `${key} should be ${expected} (got ${actual})`
+ );
+ }
+
+ assertExpected(`marginInlineStart`);
+ assertExpected(`width`);
+
+ if (expectedResult.additionalClass) {
+ ok(
+ delaySignEl.classList.contains(expectedResult.additionalClass),
+ `delay sign element should have ${expectedResult.additionalClass} class`
+ );
+ } else {
+ ok(
+ !delaySignEl.classList.contains(expectedResult.additionalClass),
+ "delay sign element should not have " +
+ `${expectedResult.additionalClass} class`
+ );
+ }
+ } else {
+ ok(
+ !delaySignEl,
+ "The delay sign element should not be in animation item element"
+ );
+ }
+ }
+}
diff --git a/devtools/client/inspector/animation/test/summary-graph_end-delay-sign_head.js b/devtools/client/inspector/animation/test/summary-graph_end-delay-sign_head.js
new file mode 100644
index 0000000000..f87a554420
--- /dev/null
+++ b/devtools/client/inspector/animation/test/summary-graph_end-delay-sign_head.js
@@ -0,0 +1,100 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/* import-globals-from head.js */
+
+// Test for following EndDelaySign component works.
+// * element existance
+// * marginInlineStart position
+// * width
+// * additinal class
+
+const TEST_DATA = [
+ {
+ targetClass: "enddelay-positive",
+ expectedResult: {
+ marginInlineStart: "75%",
+ width: "25%",
+ },
+ },
+ {
+ targetClass: "enddelay-negative",
+ expectedResult: {
+ additionalClass: "negative",
+ marginInlineStart: "50%",
+ width: "25%",
+ },
+ },
+ {
+ targetClass: "enddelay-with-fill-forwards",
+ expectedResult: {
+ additionalClass: "fill",
+ marginInlineStart: "75%",
+ width: "25%",
+ },
+ },
+ {
+ targetClass: "enddelay-with-iterations-infinity",
+ },
+ {
+ targetClass: "delay-negative",
+ },
+];
+
+// eslint-disable-next-line no-unused-vars
+async function testSummaryGraphEndDelaySign() {
+ await addTab(URL_ROOT + "doc_multi_timings.html");
+ await removeAnimatedElementsExcept(TEST_DATA.map(t => `.${t.targetClass}`));
+ const { panel } = await openAnimationInspector();
+
+ for (const { targetClass, expectedResult } of TEST_DATA) {
+ const animationItemEl = await findAnimationItemByTargetSelector(
+ panel,
+ `.${targetClass}`
+ );
+
+ info(`Checking endDelay sign existance for ${targetClass}`);
+ const endDelaySignEl = animationItemEl.querySelector(
+ ".animation-end-delay-sign"
+ );
+
+ if (expectedResult) {
+ ok(
+ endDelaySignEl,
+ "The endDelay sign element should be in animation item element"
+ );
+
+ function assertExpected(key) {
+ const actual = parseFloat(endDelaySignEl.style[key]);
+ const expected = parseFloat(expectedResult[key]);
+ ok(
+ Math.abs(actual - expected) < 0.01,
+ `${key} should be ${expected} (got ${actual})`
+ );
+ }
+
+ assertExpected(`marginInlineStart`);
+ assertExpected(`width`);
+
+ if (expectedResult.additionalClass) {
+ ok(
+ endDelaySignEl.classList.contains(expectedResult.additionalClass),
+ `endDelay sign element should have ${expectedResult.additionalClass} class`
+ );
+ } else {
+ ok(
+ !endDelaySignEl.classList.contains(expectedResult.additionalClass),
+ "endDelay sign element should not have " +
+ `${expectedResult.additionalClass} class`
+ );
+ }
+ } else {
+ ok(
+ !endDelaySignEl,
+ "The endDelay sign element should not be in animation item element"
+ );
+ }
+ }
+}