diff options
Diffstat (limited to 'devtools/client/inspector/animation/test')
89 files changed, 8346 insertions, 0 deletions
diff --git a/devtools/client/inspector/animation/test/browser.ini b/devtools/client/inspector/animation/test/browser.ini new file mode 100644 index 0000000000..d2eea4f782 --- /dev/null +++ b/devtools/client/inspector/animation/test/browser.ini @@ -0,0 +1,118 @@ +[DEFAULT] +prefs = + dom.animations.mainthread-synchronization-with-geometric-animations=true + dom.svg.pathSeg.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] +[browser_animation_animated-property-list_unchanged-items.js] +[browser_animation_animated-property-name.js] +[browser_animation_animation-detail_close-button.js] +[browser_animation_animation-detail_title.js] +[browser_animation_animation-detail_visibility.js] +[browser_animation_animation-list.js] +[browser_animation_animation-list_one-animation-select.js] +[browser_animation_animation-list_select.js] +[browser_animation_animation-target.js] +skip-if = win10_2004 # Bug 1723573 +[browser_animation_animation-target_highlight.js] +skip-if = + (apple_catalina && !debug) # Disabled in Bug 1713158. Intemittent bug: Bug 1665011 + (os == "linux" && !debug && !asan && !swgl && !ccov) # Bug 1665011 + win10_2004 # Bug 1723573 + win11_2009 # Bug 1798331 +[browser_animation_animation-target_select.js] +[browser_animation_animation-timeline-tick.js] +[browser_animation_css-transition-with-playstate-idle.js] +[browser_animation_current-time-label.js] +[browser_animation_current-time-scrubber.js] +[browser_animation_current-time-scrubber-rtl.js] +skip-if = + os == "linux" && debug # Bug 1721716 +[browser_animation_current-time-scrubber_each-different-creation-time-animations.js] +[browser_animation_current-time-scrubber-with-negative-delay.js] +[browser_animation_empty_on_invalid_nodes.js] +[browser_animation_fission_switch-target.js] +[browser_animation_indication-bar.js] +[browser_animation_infinity-duration_current-time-scrubber.js] +[browser_animation_infinity-duration_summary-graph.js] +[browser_animation_infinity-duration_tick-label.js] +[browser_animation_keyframes-graph_computed-value-path-01.js] +[browser_animation_keyframes-graph_computed-value-path-02.js] +[browser_animation_keyframes-graph_computed-value-path-03.js] +[browser_animation_keyframes-graph_computed-value-path_easing-hint.js] +skip-if = (verify && !debug) +[browser_animation_keyframes-graph_keyframe-marker.js] +[browser_animation_keyframes-graph_keyframe-marker-rtl.js] +[browser_animation_keyframes-graph_special-colors.js] +[browser_animation_keyframes-progress-bar.js] +skip-if = (os == "win" && ccov) # Bug 1490981 +[browser_animation_keyframes-progress-bar_after-resuming.js] +[browser_animation_logic_adjust-time.js] +[browser_animation_logic_adjust-time-with-playback-rate.js] +[browser_animation_logic_auto-stop.js] +[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 + (os == "win" && 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] +[browser_animation_pause-resume-button.js] +[browser_animation_pause-resume-button_end-time.js] +skip-if = + os == 'linux' && bits == 64 && debug # Bug 1767699 +[browser_animation_pause-resume-button_respectively.js] +[browser_animation_pause-resume-button_spacebar.js] +[browser_animation_playback-rate-selector.js] +[browser_animation_pseudo-element.js] +[browser_animation_rewind-button.js] +[browser_animation_short-duration.js] +[browser_animation_summary-graph_animation-name.js] +[browser_animation_summary-graph_compositor.js] +[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.js] +[browser_animation_summary-graph_delay-sign-rtl.js] +[browser_animation_summary-graph_end-delay-sign.js] +[browser_animation_summary-graph_end-delay-sign-rtl.js] +[browser_animation_summary-graph_effect-timing-path.js] +[browser_animation_summary-graph_layout-by-seek.js] +[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_summary-graph.js] +[browser_animation_timing_negative-playback-rate_current-time-scrubber.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..2516f47c79 --- /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: 3, + }, +]; + +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..e4b896b5ea --- /dev/null +++ b/devtools/client/inspector/animation/test/browser_animation_animated-property-name.js @@ -0,0 +1,113 @@ +/* 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. + +const TEST_DATA = [ + { + property: "opacity", + isOnCompositor: true, + }, + { + property: "transform", + isWarning: true, + }, + { + property: "width", + }, +]; + +add_task(async function () { + await addTab(URL_ROOT + "doc_simple_animation.html"); + await removeAnimatedElementsExcept([".compositor-notall"]); + const { panel } = await openAnimationInspector(); + + info("Checking animated property name component"); + const animatedPropertyNameEls = panel.querySelectorAll( + ".animated-property-name" + ); + is( + animatedPropertyNameEls.length, + TEST_DATA.length, + `Number of animated property name elements should be ${TEST_DATA.length}` + ); + + for (const [ + index, + animatedPropertyNameEl, + ] of animatedPropertyNameEls.entries()) { + const { property, isOnCompositor, isWarning } = TEST_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'" + ); + } + } +}); 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..c1bab42cc2 --- /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); + ok( + 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..85373b3295 --- /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; + ok( + 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..023428fad4 --- /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.loadURIString(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..8fae912d1f --- /dev/null +++ b/devtools/client/inspector/animation/test/browser_animation_infinity-duration_current-time-scrubber.js @@ -0,0 +1,39 @@ +/* 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; + ok( + 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..1ef8606afd --- /dev/null +++ b/devtools/client/inspector/animation/test/browser_animation_pause-resume-button_end-time.js @@ -0,0 +1,77 @@ +/* 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) => { + ok( + 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); + ok( + 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..f9906329ea --- /dev/null +++ b/devtools/client/inspector/animation/test/browser_animation_timing_negative-playback-rate_current-time-scrubber.js @@ -0,0 +1,46 @@ +/* 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 + ); + ok( + initialCurrentTime > + animationInspector.state.animations[0].state.currentTime, + "currentTime should be decreased" + ); + + info("Check whether the progress bar was moved to left"); + ok( + 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..5150c241dd --- /dev/null +++ b/devtools/client/inspector/animation/test/doc_simple_animation.html @@ -0,0 +1,174 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <style> + .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: #f06; + + 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; + } + + .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 { + opacity: 0; + width: 0px; + transform: translate(0px); + } + to { + 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 longhand"></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..33cb52125f --- /dev/null +++ b/devtools/client/inspector/animation/test/head.js @@ -0,0 +1,1038 @@ +/* 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 new Promise(resolve => { + SpecialPowers.pushPrefEnv( + { + set: [ + ["dom.animations-api.core.enabled", true], + ["dom.animations-api.getAnimations.enabled", true], + ["dom.animations-api.implicit-keyframes.enabled", true], + ["dom.animations-api.timelines.enabled", true], + ["layout.css.step-position-jump.enabled", true], + ], + }, + resolve + ); + }); +}; + +/** + * 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 curren time of animations will be changed to give currrent 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 => 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; + ok( + 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..38332ecfc0 --- /dev/null +++ b/devtools/client/inspector/animation/test/summary-graph_delay-sign_head.js @@ -0,0 +1,105 @@ +/* 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" + ); + + is( + delaySignEl.style.marginInlineStart, + expectedResult.marginInlineStart, + `marginInlineStart position should be ${expectedResult.marginInlineStart}` + ); + is( + delaySignEl.style.width, + expectedResult.width, + `Width should be ${expectedResult.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..945cced3a4 --- /dev/null +++ b/devtools/client/inspector/animation/test/summary-graph_end-delay-sign_head.js @@ -0,0 +1,99 @@ +/* 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" + ); + + is( + endDelaySignEl.style.marginInlineStart, + expectedResult.marginInlineStart, + `marginInlineStart position should be ${expectedResult.marginInlineStart}` + ); + is( + endDelaySignEl.style.width, + expectedResult.width, + `Width should be ${expectedResult.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" + ); + } + } +} |