diff options
Diffstat (limited to 'devtools/client/animationinspector/test')
67 files changed, 3997 insertions, 0 deletions
diff --git a/devtools/client/animationinspector/test/.eslintrc.js b/devtools/client/animationinspector/test/.eslintrc.js new file mode 100644 index 000000000..8d15a76d9 --- /dev/null +++ b/devtools/client/animationinspector/test/.eslintrc.js @@ -0,0 +1,6 @@ +"use strict"; + +module.exports = { + // Extend from the shared list of defined globals for mochitests. + "extends": "../../../.eslintrc.mochitests.js" +}; diff --git a/devtools/client/animationinspector/test/browser.ini b/devtools/client/animationinspector/test/browser.ini new file mode 100644 index 000000000..08bce344d --- /dev/null +++ b/devtools/client/animationinspector/test/browser.ini @@ -0,0 +1,71 @@ +[DEFAULT] +tags = devtools +subsuite = devtools +support-files = + doc_body_animation.html + doc_end_delay.html + doc_frame_script.js + doc_keyframes.html + doc_modify_playbackRate.html + doc_negative_animation.html + doc_pseudo_elements.html + doc_script_animation.html + doc_simple_animation.html + doc_multiple_animation_types.html + doc_timing_combination_animation.html + head.js + !/devtools/client/commandline/test/helpers.js + !/devtools/client/framework/test/shared-head.js + !/devtools/client/inspector/test/head.js + !/devtools/client/inspector/test/shared-head.js + !/devtools/client/shared/test/test-actor-registry.js + !/devtools/client/shared/test/test-actor.js + +[browser_animation_animated_properties_displayed.js] +[browser_animation_click_selects_animation.js] +[browser_animation_controller_exposes_document_currentTime.js] +skip-if = os == "linux" && !debug # Bug 1234567 +[browser_animation_empty_on_invalid_nodes.js] +[browser_animation_keyframe_click_to_set_time.js] +[browser_animation_keyframe_markers.js] +[browser_animation_mutations_with_same_names.js] +[browser_animation_panel_exists.js] +[browser_animation_participate_in_inspector_update.js] +[browser_animation_playerFronts_are_refreshed.js] +[browser_animation_playerWidgets_appear_on_panel_init.js] +[browser_animation_playerWidgets_target_nodes.js] +[browser_animation_pseudo_elements.js] +[browser_animation_refresh_on_added_animation.js] +[browser_animation_refresh_on_removed_animation.js] +skip-if = os == "linux" && !debug # Bug 1227792 +[browser_animation_refresh_when_active.js] +[browser_animation_running_on_compositor.js] +[browser_animation_same_nb_of_playerWidgets_and_playerFronts.js] +[browser_animation_shows_player_on_valid_node.js] +[browser_animation_spacebar_toggles_animations.js] +[browser_animation_spacebar_toggles_node_animations.js] +[browser_animation_target_highlight_select.js] +[browser_animation_target_highlighter_lock.js] +[browser_animation_timeline_currentTime.js] +[browser_animation_timeline_header.js] +[browser_animation_timeline_iterationStart.js] +[browser_animation_timeline_pause_button_01.js] +[browser_animation_timeline_pause_button_02.js] +[browser_animation_timeline_pause_button_03.js] +[browser_animation_timeline_rate_selector.js] +[browser_animation_timeline_rewind_button.js] +[browser_animation_timeline_scrubber_exists.js] +[browser_animation_timeline_scrubber_movable.js] +[browser_animation_timeline_scrubber_moves.js] +[browser_animation_timeline_setCurrentTime.js] +[browser_animation_timeline_shows_delay.js] +[browser_animation_timeline_shows_endDelay.js] +[browser_animation_timeline_shows_iterations.js] +[browser_animation_timeline_shows_name_label.js] +[browser_animation_timeline_shows_time_info.js] +[browser_animation_timeline_takes_rate_into_account.js] +[browser_animation_timeline_ui.js] +[browser_animation_toggle_button_resets_on_navigate.js] +[browser_animation_toggle_button_toggles_animations.js] +[browser_animation_toolbar_exists.js] +[browser_animation_ui_updates_when_animation_data_changes.js] diff --git a/devtools/client/animationinspector/test/browser_animation_animated_properties_displayed.js b/devtools/client/animationinspector/test/browser_animation_animated_properties_displayed.js new file mode 100644 index 000000000..214a33bd4 --- /dev/null +++ b/devtools/client/animationinspector/test/browser_animation_animated_properties_displayed.js @@ -0,0 +1,91 @@ +/* vim: set ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const LAYOUT_ERRORS_L10N = + new LocalizationHelper("toolkit/locales/layout_errors.properties"); + +// Test that when an animation is selected, its list of animated properties is +// displayed below it. + +const EXPECTED_PROPERTIES = [ + "background-attachment", + "background-clip", + "background-color", + "background-image", + "background-origin", + "background-position-x", + "background-position-y", + "background-repeat", + "background-size", + "border-bottom-left-radius", + "border-bottom-right-radius", + "border-top-left-radius", + "border-top-right-radius", + "filter", + "height", + "transform", + "width" +].sort(); + +add_task(function* () { + yield addTab(URL_ROOT + "doc_keyframes.html"); + let {panel} = yield openAnimationInspector(); + let timeline = panel.animationsTimelineComponent; + let propertiesList = timeline.rootWrapperEl + .querySelector(".animated-properties"); + + ok(!isNodeVisible(propertiesList), + "The list of properties panel is hidden by default"); + + info("Click to select the animation"); + yield clickOnAnimation(panel, 0); + + ok(isNodeVisible(propertiesList), + "The list of properties panel is shown"); + ok(propertiesList.querySelectorAll(".property").length, + "The list of properties panel actually contains properties"); + ok(hasExpectedProperties(propertiesList), + "The list of properties panel contains the right properties"); + + ok(hasExpectedWarnings(propertiesList), + "The list of properties panel contains the right warnings"); + + info("Click to unselect the animation"); + yield clickOnAnimation(panel, 0, true); + + ok(!isNodeVisible(propertiesList), + "The list of properties panel is hidden again"); +}); + +function hasExpectedProperties(containerEl) { + let names = [...containerEl.querySelectorAll(".property .name")] + .map(n => n.textContent) + .sort(); + + if (names.length !== EXPECTED_PROPERTIES.length) { + return false; + } + + for (let i = 0; i < names.length; i++) { + if (names[i] !== EXPECTED_PROPERTIES[i]) { + return false; + } + } + + return true; +} + +function hasExpectedWarnings(containerEl) { + let warnings = [...containerEl.querySelectorAll(".warning")]; + for (let warning of warnings) { + let warningID = + "CompositorAnimationWarningTransformWithGeometricProperties"; + if (warning.getAttribute("title") == LAYOUT_ERRORS_L10N.getStr(warningID)) { + return true; + } + } + return false; +} diff --git a/devtools/client/animationinspector/test/browser_animation_click_selects_animation.js b/devtools/client/animationinspector/test/browser_animation_click_selects_animation.js new file mode 100644 index 000000000..d6d393d5a --- /dev/null +++ b/devtools/client/animationinspector/test/browser_animation_click_selects_animation.js @@ -0,0 +1,44 @@ +/* vim: set ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Check that animations displayed in the timeline can be selected by clicking +// them, and that this emits the right events and adds the right classes. + +add_task(function* () { + yield addTab(URL_ROOT + "doc_simple_animation.html"); + let {panel} = yield openAnimationInspector(); + let timeline = panel.animationsTimelineComponent; + + let selected = timeline.rootWrapperEl.querySelectorAll(".animation.selected"); + ok(!selected.length, "There are no animations selected by default"); + + info("Click on the first animation, expect the right event and right class"); + let animation0 = yield clickOnAnimation(panel, 0); + is(animation0, timeline.animations[0], + "The selected event was emitted with the right animation"); + ok(isTimeBlockSelected(timeline, 0), + "The time block has the right selected class"); + + info("Click on the second animation, expect it to be selected too"); + let animation1 = yield clickOnAnimation(panel, 1); + is(animation1, timeline.animations[1], + "The selected event was emitted with the right animation"); + ok(isTimeBlockSelected(timeline, 1), + "The second time block has the right selected class"); + + info("Click again on the first animation and check if it unselects"); + yield clickOnAnimation(panel, 0, true); + ok(!isTimeBlockSelected(timeline, 0), + "The first time block has been unselected"); +}); + +function isTimeBlockSelected(timeline, index) { + let animation = timeline.rootWrapperEl.querySelectorAll(".animation")[index]; + let animatedProperties = timeline.rootWrapperEl.querySelectorAll( + ".animated-properties")[index]; + return animation.classList.contains("selected") && + animatedProperties.classList.contains("selected"); +} diff --git a/devtools/client/animationinspector/test/browser_animation_controller_exposes_document_currentTime.js b/devtools/client/animationinspector/test/browser_animation_controller_exposes_document_currentTime.js new file mode 100644 index 000000000..ae970a426 --- /dev/null +++ b/devtools/client/animationinspector/test/browser_animation_controller_exposes_document_currentTime.js @@ -0,0 +1,43 @@ +/* vim: set ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that the controller provides the document.timeline currentTime (at least +// the last known version since new animations were added). + +add_task(function* () { + yield addTab(URL_ROOT + "doc_simple_animation.html"); + let {panel, controller} = yield openAnimationInspector(); + + ok(controller.documentCurrentTime, "The documentCurrentTime getter exists"); + checkDocumentTimeIsCorrect(controller); + let time1 = controller.documentCurrentTime; + + yield startNewAnimation(controller, panel); + checkDocumentTimeIsCorrect(controller); + let time2 = controller.documentCurrentTime; + ok(time2 > time1, "The new documentCurrentTime is higher than the old one"); +}); + +function checkDocumentTimeIsCorrect(controller) { + let time = 0; + for (let {state} of controller.animationPlayers) { + time = Math.max(time, state.documentCurrentTime); + } + is(controller.documentCurrentTime, time, + "The documentCurrentTime is correct"); +} + +function* startNewAnimation(controller, panel) { + info("Add a new animation to the page and check the time again"); + let onPlayerAdded = controller.once(controller.PLAYERS_UPDATED_EVENT); + yield executeInContent("devtools:test:setAttribute", { + selector: ".still", + attributeName: "class", + attributeValue: "ball still short" + }); + yield onPlayerAdded; + yield waitForAllAnimationTargets(panel); +} diff --git a/devtools/client/animationinspector/test/browser_animation_empty_on_invalid_nodes.js b/devtools/client/animationinspector/test/browser_animation_empty_on_invalid_nodes.js new file mode 100644 index 000000000..9fda89a9a --- /dev/null +++ b/devtools/client/animationinspector/test/browser_animation_empty_on_invalid_nodes.js @@ -0,0 +1,42 @@ +/* vim: set ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +requestLongerTimeout(2); + +// Test that the panel shows no animation data for invalid or not animated nodes + +add_task(function* () { + yield addTab(URL_ROOT + "doc_simple_animation.html"); + let {inspector, panel, window} = yield openAnimationInspector(); + let {document} = window; + + info("Select node .still and check that the panel is empty"); + let stillNode = yield getNodeFront(".still", inspector); + let onUpdated = panel.once(panel.UI_UPDATED_EVENT); + yield selectNodeAndWaitForAnimations(stillNode, inspector); + yield onUpdated; + + is(panel.animationsTimelineComponent.animations.length, 0, + "No animation players stored in the timeline component for a still node"); + is(panel.animationsTimelineComponent.animationsEl.childNodes.length, 0, + "No animation displayed in the timeline component for a still node"); + is(document.querySelector("#error-type").textContent, + ANIMATION_L10N.getStr("panel.invalidElementSelected"), + "The correct error message is displayed"); + + info("Select the comment text node and check that the panel is empty"); + let commentNode = yield inspector.walker.previousSibling(stillNode); + onUpdated = panel.once(panel.UI_UPDATED_EVENT); + yield selectNodeAndWaitForAnimations(commentNode, inspector); + yield onUpdated; + + is(panel.animationsTimelineComponent.animations.length, 0, + "No animation players stored in the timeline component for a text node"); + is(panel.animationsTimelineComponent.animationsEl.childNodes.length, 0, + "No animation displayed in the timeline component for a text node"); + is(document.querySelector("#error-type").textContent, + ANIMATION_L10N.getStr("panel.invalidElementSelected"), + "The correct error message is displayed"); +}); diff --git a/devtools/client/animationinspector/test/browser_animation_keyframe_click_to_set_time.js b/devtools/client/animationinspector/test/browser_animation_keyframe_click_to_set_time.js new file mode 100644 index 000000000..ba700b7a5 --- /dev/null +++ b/devtools/client/animationinspector/test/browser_animation_keyframe_click_to_set_time.js @@ -0,0 +1,52 @@ +/* vim: set ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that animated properties' keyframes can be clicked, and that doing so +// sets the current time in the timeline. + +add_task(function* () { + yield addTab(URL_ROOT + "doc_keyframes.html"); + let {panel} = yield openAnimationInspector(); + let timeline = panel.animationsTimelineComponent; + let {scrubberEl} = timeline; + + // XXX: The scrollbar is placed in the timeline in such a way that it causes + // the animations to be slightly offset with the header when it appears. + // So for now, let's hide the scrollbar. Bug 1229340 should fix this. + timeline.animationsEl.style.overflow = "hidden"; + + info("Expand the animation"); + yield clickOnAnimation(panel, 0); + + info("Click on the first keyframe of the first animated property"); + yield clickKeyframe(panel, 0, "background-color", 0); + + info("Make sure the scrubber stopped moving and is at the right position"); + yield assertScrubberMoving(panel, false); + checkScrubberPos(scrubberEl, 0); + + info("Click on a keyframe in the middle"); + yield clickKeyframe(panel, 0, "transform", 2); + + info("Make sure the scrubber is at the right position"); + checkScrubberPos(scrubberEl, 50); +}); + +function* clickKeyframe(panel, animIndex, property, index) { + let keyframeComponent = getKeyframeComponent(panel, animIndex, property); + let keyframeEl = getKeyframeEl(panel, animIndex, property, index); + + let onSelect = keyframeComponent.once("frame-selected"); + EventUtils.sendMouseEvent({type: "click"}, keyframeEl, + keyframeEl.ownerDocument.defaultView); + yield onSelect; +} + +function checkScrubberPos(scrubberEl, pos) { + let newPos = Math.round(parseFloat(scrubberEl.style.left)); + let expectedPos = Math.round(pos); + is(newPos, expectedPos, `The scrubber is at ${pos}%`); +} diff --git a/devtools/client/animationinspector/test/browser_animation_keyframe_markers.js b/devtools/client/animationinspector/test/browser_animation_keyframe_markers.js new file mode 100644 index 000000000..789c0efb6 --- /dev/null +++ b/devtools/client/animationinspector/test/browser_animation_keyframe_markers.js @@ -0,0 +1,74 @@ +/* vim: set ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that when an animation is selected and its list of properties is shown, +// there are keyframes markers next to each property being animated. + +const EXPECTED_PROPERTIES = [ + "backgroundColor", + "backgroundPosition", + "backgroundSize", + "borderBottomLeftRadius", + "borderBottomRightRadius", + "borderTopLeftRadius", + "borderTopRightRadius", + "filter", + "height", + "transform", + "width" +]; + +add_task(function* () { + yield addTab(URL_ROOT + "doc_keyframes.html"); + let {panel} = yield openAnimationInspector(); + let timeline = panel.animationsTimelineComponent; + + info("Expand the animation"); + yield clickOnAnimation(panel, 0); + + ok(timeline.rootWrapperEl.querySelectorAll(".frames .keyframes").length, + "There are container elements for displaying keyframes"); + + let data = yield getExpectedKeyframesData(timeline.animations[0]); + for (let propertyName in data) { + info("Check the keyframe markers for " + propertyName); + let widthMarkerSelector = ".frame[data-property=" + propertyName + "]"; + let markers = timeline.rootWrapperEl.querySelectorAll(widthMarkerSelector); + + is(markers.length, data[propertyName].length, + "The right number of keyframes was found for " + propertyName); + + let offsets = [...markers].map(m => parseFloat(m.dataset.offset)); + let values = [...markers].map(m => m.dataset.value); + for (let i = 0; i < markers.length; i++) { + is(markers[i].dataset.offset, offsets[i], + "Marker " + i + " for " + propertyName + " has the right offset"); + is(markers[i].dataset.value, values[i], + "Marker " + i + " for " + propertyName + " has the right value"); + } + } +}); + +function* getExpectedKeyframesData(animation) { + // We're testing the UI state here, so it's fine to get the list of expected + // properties from the animation actor. + let properties = yield animation.getProperties(); + let data = {}; + + for (let expectedProperty of EXPECTED_PROPERTIES) { + data[expectedProperty] = []; + for (let {name, values} of properties) { + if (name !== expectedProperty) { + continue; + } + for (let {offset, value} of values) { + data[expectedProperty].push({offset, value}); + } + } + } + + return data; +} diff --git a/devtools/client/animationinspector/test/browser_animation_mutations_with_same_names.js b/devtools/client/animationinspector/test/browser_animation_mutations_with_same_names.js new file mode 100644 index 000000000..1ae19c277 --- /dev/null +++ b/devtools/client/animationinspector/test/browser_animation_mutations_with_same_names.js @@ -0,0 +1,31 @@ +/* vim: set ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Check that when animations are added later (through animation mutations) and +// if these animations have the same names, then all of them are still being +// displayed (which should be true as long as these animations apply to +// different nodes). + +add_task(function* () { + yield addTab(URL_ROOT + "doc_negative_animation.html"); + let {controller, panel} = yield openAnimationInspector(); + + info("Wait until all animations have been added " + + "(they're added with setTimeout)"); + while (controller.animationPlayers.length < 3) { + yield controller.once(controller.PLAYERS_UPDATED_EVENT); + } + yield waitForAllAnimationTargets(panel); + + is(panel.animationsTimelineComponent.animations.length, 3, + "The timeline shows 3 animations too"); + + // Reduce the known nodeFronts to a set to make them unique. + let nodeFronts = new Set(panel.animationsTimelineComponent + .targetNodes.map(n => n.previewer.nodeFront)); + is(nodeFronts.size, 3, + "The animations are applied to 3 different node fronts"); +}); diff --git a/devtools/client/animationinspector/test/browser_animation_panel_exists.js b/devtools/client/animationinspector/test/browser_animation_panel_exists.js new file mode 100644 index 000000000..1f12605a5 --- /dev/null +++ b/devtools/client/animationinspector/test/browser_animation_panel_exists.js @@ -0,0 +1,23 @@ +/* vim: set ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that the animation panel sidebar exists + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8,welcome to the animation panel"); + let {panel, controller} = yield openAnimationInspector(); + + ok(controller, + "The animation controller exists"); + ok(controller.animationsFront, + "The animation controller has been initialized"); + ok(panel, + "The animation panel exists"); + ok(panel.playersEl, + "The animation panel has been initialized"); + ok(panel.animationsTimelineComponent, + "The animation panel has been initialized"); +}); diff --git a/devtools/client/animationinspector/test/browser_animation_participate_in_inspector_update.js b/devtools/client/animationinspector/test/browser_animation_participate_in_inspector_update.js new file mode 100644 index 000000000..fec529568 --- /dev/null +++ b/devtools/client/animationinspector/test/browser_animation_participate_in_inspector_update.js @@ -0,0 +1,46 @@ +/* vim: set ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +requestLongerTimeout(2); + +// Test that the update of the animation panel participate in the +// inspector-updated event. This means that the test verifies that the +// inspector-updated event is emitted *after* the animation panel is ready. + +add_task(function* () { + yield addTab(URL_ROOT + "doc_simple_animation.html"); + let {inspector, panel, controller} = yield openAnimationInspector(); + + info("Listen for the players-updated, ui-updated and " + + "inspector-updated events"); + let receivedEvents = []; + controller.once(controller.PLAYERS_UPDATED_EVENT, () => { + receivedEvents.push(controller.PLAYERS_UPDATED_EVENT); + }); + panel.once(panel.UI_UPDATED_EVENT, () => { + receivedEvents.push(panel.UI_UPDATED_EVENT); + }); + inspector.once("inspector-updated", () => { + receivedEvents.push("inspector-updated"); + }); + + info("Selecting an animated node"); + let node = yield getNodeFront(".animated", inspector); + yield selectNodeAndWaitForAnimations(node, inspector); + + info("Check that all events were received"); + // Only assert that the inspector-updated event is last, the order of the + // first 2 events is irrelevant. + + is(receivedEvents.length, 3, "3 events were received"); + is(receivedEvents[2], "inspector-updated", + "The third event received was the inspector-updated event"); + + ok(receivedEvents.indexOf(controller.PLAYERS_UPDATED_EVENT) !== -1, + "The players-updated event was received"); + ok(receivedEvents.indexOf(panel.UI_UPDATED_EVENT) !== -1, + "The ui-updated event was received"); +}); diff --git a/devtools/client/animationinspector/test/browser_animation_playerFronts_are_refreshed.js b/devtools/client/animationinspector/test/browser_animation_playerFronts_are_refreshed.js new file mode 100644 index 000000000..7144adf6c --- /dev/null +++ b/devtools/client/animationinspector/test/browser_animation_playerFronts_are_refreshed.js @@ -0,0 +1,36 @@ +/* vim: set ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +requestLongerTimeout(2); + +// Check that the AnimationPlayerFront objects lifecycle is managed by the +// AnimationController. + +add_task(function* () { + yield addTab(URL_ROOT + "doc_simple_animation.html"); + let {controller, inspector} = yield openAnimationInspector(); + + info("Selecting an animated node"); + // selectNode waits for the inspector-updated event before resolving, which + // means the controller.PLAYERS_UPDATED_EVENT event has been emitted before + // and players are ready. + yield selectNodeAndWaitForAnimations(".animated", inspector); + + is(controller.animationPlayers.length, 1, + "One AnimationPlayerFront has been created"); + + info("Selecting a node with mutliple animations"); + yield selectNodeAndWaitForAnimations(".multi", inspector); + + is(controller.animationPlayers.length, 2, + "2 AnimationPlayerFronts have been created"); + + info("Selecting a node with no animations"); + yield selectNodeAndWaitForAnimations(".still", inspector); + + is(controller.animationPlayers.length, 0, + "There are no more AnimationPlayerFront objects"); +}); diff --git a/devtools/client/animationinspector/test/browser_animation_playerWidgets_appear_on_panel_init.js b/devtools/client/animationinspector/test/browser_animation_playerWidgets_appear_on_panel_init.js new file mode 100644 index 000000000..271b26df3 --- /dev/null +++ b/devtools/client/animationinspector/test/browser_animation_playerWidgets_appear_on_panel_init.js @@ -0,0 +1,41 @@ +/* vim: set ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that player widgets are displayed right when the animation panel is +// initialized, if the selected node (<body> by default) is animated. + +const { ANIMATION_TYPES } = require("devtools/server/actors/animation"); + +add_task(function* () { + yield addTab(URL_ROOT + "doc_multiple_animation_types.html"); + + let {panel} = yield openAnimationInspector(); + is(panel.animationsTimelineComponent.animations.length, 3, + "Three animations are handled by the timeline after init"); + assertAnimationsDisplayed(panel, 3, + "Three animations are displayed after init"); + is( + panel.animationsTimelineComponent + .animationsEl + .querySelectorAll(`.animation.${ANIMATION_TYPES.SCRIPT_ANIMATION}`) + .length, + 1, + "One script-generated animation is displayed"); + is( + panel.animationsTimelineComponent + .animationsEl + .querySelectorAll(`.animation.${ANIMATION_TYPES.CSS_ANIMATION}`) + .length, + 1, + "One CSS animation is displayed"); + is( + panel.animationsTimelineComponent + .animationsEl + .querySelectorAll(`.animation.${ANIMATION_TYPES.CSS_TRANSITION}`) + .length, + 1, + "One CSS transition is displayed"); +}); diff --git a/devtools/client/animationinspector/test/browser_animation_playerWidgets_target_nodes.js b/devtools/client/animationinspector/test/browser_animation_playerWidgets_target_nodes.js new file mode 100644 index 000000000..1fbaa7ae3 --- /dev/null +++ b/devtools/client/animationinspector/test/browser_animation_playerWidgets_target_nodes.js @@ -0,0 +1,33 @@ +/* vim: set ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +requestLongerTimeout(2); + +// Test that player widgets display information about target nodes + +add_task(function* () { + yield addTab(URL_ROOT + "doc_simple_animation.html"); + let {inspector, panel} = yield openAnimationInspector(); + + info("Select the simple animated node"); + yield selectNodeAndWaitForAnimations(".animated", inspector); + + let targetNodeComponent = panel.animationsTimelineComponent.targetNodes[0]; + let {previewer} = targetNodeComponent; + + // Make sure to wait for the target-retrieved event if the nodeFront hasn't + // yet been retrieved by the TargetNodeComponent. + if (!previewer.nodeFront) { + yield targetNodeComponent.once("target-retrieved"); + } + + is(previewer.el.textContent, "div#.ball.animated", + "The target element's content is correct"); + + let highlighterEl = previewer.el.querySelector(".node-highlighter"); + ok(highlighterEl, + "The icon to highlight the target element in the page exists"); +}); diff --git a/devtools/client/animationinspector/test/browser_animation_pseudo_elements.js b/devtools/client/animationinspector/test/browser_animation_pseudo_elements.js new file mode 100644 index 000000000..38b2f10af --- /dev/null +++ b/devtools/client/animationinspector/test/browser_animation_pseudo_elements.js @@ -0,0 +1,49 @@ +/* vim: set ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that animated pseudo-elements do show in the timeline. + +add_task(function* () { + yield addTab(URL_ROOT + "doc_pseudo_elements.html"); + let {inspector, panel} = yield openAnimationInspector(); + let timeline = panel.animationsTimelineComponent; + + info("With <body> selected by default check the content of the timeline"); + is(timeline.timeBlocks.length, 3, "There are 3 animations in the timeline"); + + let getTargetNodeText = index => { + let el = timeline.targetNodes[index].previewer.previewEl; + return [...el.childNodes] + .map(n => n.style.display === "none" ? "" : n.textContent) + .join(""); + }; + + is(getTargetNodeText(0), "body", "The first animated node is <body>"); + is(getTargetNodeText(1), "::before", "The second animated node is ::before"); + is(getTargetNodeText(2), "::after", "The third animated node is ::after"); + + info("Getting the before and after nodeFronts"); + let bodyContainer = yield getContainerForSelector("body", inspector); + let getBodyChildNodeFront = index => { + return bodyContainer.elt.children[1].childNodes[index].container.node; + }; + let beforeNode = getBodyChildNodeFront(0); + let afterNode = getBodyChildNodeFront(1); + + info("Select the ::before pseudo-element in the inspector"); + yield selectNode(beforeNode, inspector); + is(timeline.timeBlocks.length, 1, "There is 1 animation in the timeline"); + is(timeline.targetNodes[0].previewer.nodeFront, + inspector.selection.nodeFront, + "The right node front is displayed in the timeline"); + + info("Select the ::after pseudo-element in the inspector"); + yield selectNode(afterNode, inspector); + is(timeline.timeBlocks.length, 1, "There is 1 animation in the timeline"); + is(timeline.targetNodes[0].previewer.nodeFront, + inspector.selection.nodeFront, + "The right node front is displayed in the timeline"); +}); diff --git a/devtools/client/animationinspector/test/browser_animation_refresh_on_added_animation.js b/devtools/client/animationinspector/test/browser_animation_refresh_on_added_animation.js new file mode 100644 index 000000000..0bc652476 --- /dev/null +++ b/devtools/client/animationinspector/test/browser_animation_refresh_on_added_animation.js @@ -0,0 +1,47 @@ +/* vim: set ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +requestLongerTimeout(2); + +// Test that the panel content refreshes when new animations are added. + +add_task(function* () { + yield addTab(URL_ROOT + "doc_simple_animation.html"); + let {inspector, panel} = yield openAnimationInspector(); + + info("Select a non animated node"); + yield selectNodeAndWaitForAnimations(".still", inspector); + + assertAnimationsDisplayed(panel, 0); + + info("Start an animation on the node"); + yield changeElementAndWait({ + selector: ".still", + attributeName: "class", + attributeValue: "ball animated" + }, panel, inspector); + + assertAnimationsDisplayed(panel, 1); + + info("Remove the animation class on the node"); + yield changeElementAndWait({ + selector: ".ball.animated", + attributeName: "class", + attributeValue: "ball still" + }, panel, inspector); + + assertAnimationsDisplayed(panel, 0); +}); + +function* changeElementAndWait(options, panel, inspector) { + let onPanelUpdated = panel.once(panel.UI_UPDATED_EVENT); + let onInspectorUpdated = inspector.once("inspector-updated"); + + yield executeInContent("devtools:test:setAttribute", options); + + yield promise.all([ + onInspectorUpdated, onPanelUpdated, waitForAllAnimationTargets(panel)]); +} diff --git a/devtools/client/animationinspector/test/browser_animation_refresh_on_removed_animation.js b/devtools/client/animationinspector/test/browser_animation_refresh_on_removed_animation.js new file mode 100644 index 000000000..011d4a086 --- /dev/null +++ b/devtools/client/animationinspector/test/browser_animation_refresh_on_removed_animation.js @@ -0,0 +1,50 @@ +/* vim: set ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +requestLongerTimeout(2); + +// Test that the panel content refreshes when animations are removed. + +add_task(function* () { + yield addTab(URL_ROOT + "doc_simple_animation.html"); + + let {inspector, panel} = yield openAnimationInspector(); + yield testRefreshOnRemove(inspector, panel); +}); + +function* testRefreshOnRemove(inspector, panel) { + info("Select a animated node"); + yield selectNodeAndWaitForAnimations(".animated", inspector); + + assertAnimationsDisplayed(panel, 1); + + info("Listen to the next UI update event"); + let onPanelUpdated = panel.once(panel.UI_UPDATED_EVENT); + + info("Remove the animation on the node by removing the class"); + yield executeInContent("devtools:test:setAttribute", { + selector: ".animated", + attributeName: "class", + attributeValue: "ball still test-node" + }); + + yield onPanelUpdated; + ok(true, "The panel update event was fired"); + + assertAnimationsDisplayed(panel, 0); + + info("Add an finite animation on the node again, and wait for it to appear"); + onPanelUpdated = panel.once(panel.UI_UPDATED_EVENT); + yield executeInContent("devtools:test:setAttribute", { + selector: ".test-node", + attributeName: "class", + attributeValue: "ball short test-node" + }); + yield onPanelUpdated; + yield waitForAllAnimationTargets(panel); + + assertAnimationsDisplayed(panel, 1); +} diff --git a/devtools/client/animationinspector/test/browser_animation_refresh_when_active.js b/devtools/client/animationinspector/test/browser_animation_refresh_when_active.js new file mode 100644 index 000000000..6fb244b1e --- /dev/null +++ b/devtools/client/animationinspector/test/browser_animation_refresh_when_active.js @@ -0,0 +1,53 @@ +/* vim: set ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +requestLongerTimeout(2); + +// Test that the panel only refreshes when it is visible in the sidebar. + +add_task(function* () { + yield addTab(URL_ROOT + "doc_simple_animation.html"); + + let {inspector, panel} = yield openAnimationInspector(); + yield testRefresh(inspector, panel); +}); + +function* testRefresh(inspector, panel) { + info("Select a non animated node"); + yield selectNodeAndWaitForAnimations(".still", inspector); + + info("Switch to the rule-view panel"); + inspector.sidebar.select("ruleview"); + + info("Select the animated node now"); + yield selectNodeAndWaitForAnimations(".animated", inspector); + + assertAnimationsDisplayed(panel, 0, + "The panel doesn't show the animation data while inactive"); + + info("Switch to the animation panel"); + inspector.sidebar.select("animationinspector"); + yield panel.once(panel.UI_UPDATED_EVENT); + + assertAnimationsDisplayed(panel, 1, + "The panel shows the animation data after selecting it"); + + info("Switch again to the rule-view"); + inspector.sidebar.select("ruleview"); + + info("Select the non animated node again"); + yield selectNodeAndWaitForAnimations(".still", inspector); + + assertAnimationsDisplayed(panel, 1, + "The panel still shows the previous animation data since it is inactive"); + + info("Switch to the animation panel again"); + inspector.sidebar.select("animationinspector"); + yield panel.once(panel.UI_UPDATED_EVENT); + + assertAnimationsDisplayed(panel, 0, + "The panel is now empty after refreshing"); +} diff --git a/devtools/client/animationinspector/test/browser_animation_running_on_compositor.js b/devtools/client/animationinspector/test/browser_animation_running_on_compositor.js new file mode 100644 index 000000000..b23479b6c --- /dev/null +++ b/devtools/client/animationinspector/test/browser_animation_running_on_compositor.js @@ -0,0 +1,57 @@ +/* vim: set ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +requestLongerTimeout(2); + +// Test that when animations displayed in the timeline are running on the +// compositor, they get a special icon and information in the tooltip. + +add_task(function* () { + yield addTab(URL_ROOT + "doc_simple_animation.html"); + let {inspector, panel} = yield openAnimationInspector(); + let timeline = panel.animationsTimelineComponent; + + info("Select a test node we know has an animation running on the compositor"); + yield selectNodeAndWaitForAnimations(".animated", inspector); + + let animationEl = timeline.animationsEl.querySelector(".animation"); + ok(animationEl.classList.contains("fast-track"), + "The animation element has the fast-track css class"); + ok(hasTooltip(animationEl, + ANIMATION_L10N.getStr("player.allPropertiesOnCompositorTooltip")), + "The animation element has the right tooltip content"); + + info("Select a node we know doesn't have an animation on the compositor"); + yield selectNodeAndWaitForAnimations(".no-compositor", inspector); + + animationEl = timeline.animationsEl.querySelector(".animation"); + ok(!animationEl.classList.contains("fast-track"), + "The animation element does not have the fast-track css class"); + ok(!hasTooltip(animationEl, + ANIMATION_L10N.getStr("player.allPropertiesOnCompositorTooltip")), + "The animation element does not have oncompositor tooltip content"); + ok(!hasTooltip(animationEl, + ANIMATION_L10N.getStr("player.somePropertiesOnCompositorTooltip")), + "The animation element does not have oncompositor tooltip content"); + + info("Select a node we know has animation on the compositor and not on the" + + " compositor"); + yield selectNodeAndWaitForAnimations(".compositor-notall", inspector); + + animationEl = timeline.animationsEl.querySelector(".animation"); + ok(animationEl.classList.contains("fast-track"), + "The animation element has the fast-track css class"); + ok(hasTooltip(animationEl, + ANIMATION_L10N.getStr("player.somePropertiesOnCompositorTooltip")), + "The animation element has the right tooltip content"); +}); + +function hasTooltip(animationEl, expected) { + let el = animationEl.querySelector(".name"); + let tooltip = el.getAttribute("title"); + + return tooltip.indexOf(expected) !== -1; +} diff --git a/devtools/client/animationinspector/test/browser_animation_same_nb_of_playerWidgets_and_playerFronts.js b/devtools/client/animationinspector/test/browser_animation_same_nb_of_playerWidgets_and_playerFronts.js new file mode 100644 index 000000000..a3aa8974c --- /dev/null +++ b/devtools/client/animationinspector/test/browser_animation_same_nb_of_playerWidgets_and_playerFronts.js @@ -0,0 +1,23 @@ +/* vim: set ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +requestLongerTimeout(2); + +// Check that when playerFronts are updated, the same number of playerWidgets +// are created in the panel. + +add_task(function* () { + yield addTab(URL_ROOT + "doc_simple_animation.html"); + let {inspector, panel, controller} = yield openAnimationInspector(); + let timeline = panel.animationsTimelineComponent; + + info("Selecting the test animated node again"); + yield selectNodeAndWaitForAnimations(".multi", inspector); + + is(controller.animationPlayers.length, + timeline.animationsEl.querySelectorAll(".animation").length, + "As many timeline elements were created as there are playerFronts"); +}); diff --git a/devtools/client/animationinspector/test/browser_animation_shows_player_on_valid_node.js b/devtools/client/animationinspector/test/browser_animation_shows_player_on_valid_node.js new file mode 100644 index 000000000..57e6a68fb --- /dev/null +++ b/devtools/client/animationinspector/test/browser_animation_shows_player_on_valid_node.js @@ -0,0 +1,21 @@ +/* vim: set ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +requestLongerTimeout(2); + +// Test that the panel shows an animation player when an animated node is +// selected. + +add_task(function* () { + yield addTab(URL_ROOT + "doc_simple_animation.html"); + let {inspector, panel} = yield openAnimationInspector(); + + info("Select node .animated and check that the panel is not empty"); + let node = yield getNodeFront(".animated", inspector); + yield selectNodeAndWaitForAnimations(node, inspector); + + assertAnimationsDisplayed(panel, 1); +}); diff --git a/devtools/client/animationinspector/test/browser_animation_spacebar_toggles_animations.js b/devtools/client/animationinspector/test/browser_animation_spacebar_toggles_animations.js new file mode 100644 index 000000000..799ecc28d --- /dev/null +++ b/devtools/client/animationinspector/test/browser_animation_spacebar_toggles_animations.js @@ -0,0 +1,49 @@ +/* vim: set ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(function* setup() { + yield SpecialPowers.pushPrefEnv({ + set: [["dom.ipc.processCount", 1]] + }); +}); + +// Test that the spacebar key press toggles the toggleAll button state +// when a node with no animation is selected. +// This test doesn't need to test if animations actually pause/resume +// because there's an other test that does this : +// browser_animation_toggle_button_toggles_animation.js + +add_task(function* () { + yield addTab(URL_ROOT + "doc_simple_animation.html"); + let {panel, inspector, window, controller} = yield openAnimationInspector(); + let {toggleAllButtonEl} = panel; + + // select a node without animations + yield selectNodeAndWaitForAnimations(".still", inspector); + + // ensure the focus is on the animation panel + window.focus(); + + info("Simulate spacebar stroke and check toggleAll button" + + " is in paused state"); + + // sending the key will lead to a ALL_ANIMATIONS_TOGGLED_EVENT + let onToggled = once(controller, controller.ALL_ANIMATIONS_TOGGLED_EVENT); + EventUtils.sendKey("SPACE", window); + yield onToggled; + ok(toggleAllButtonEl.classList.contains("paused"), + "The toggle all button is in its paused state"); + + info("Simulate spacebar stroke and check toggleAll button" + + " is in playing state"); + + // sending the key will lead to a ALL_ANIMATIONS_TOGGLED_EVENT + onToggled = once(controller, controller.ALL_ANIMATIONS_TOGGLED_EVENT); + EventUtils.sendKey("SPACE", window); + yield onToggled; + ok(!toggleAllButtonEl.classList.contains("paused"), + "The toggle all button is in its playing state again"); +}); diff --git a/devtools/client/animationinspector/test/browser_animation_spacebar_toggles_node_animations.js b/devtools/client/animationinspector/test/browser_animation_spacebar_toggles_node_animations.js new file mode 100644 index 000000000..634d4bc49 --- /dev/null +++ b/devtools/client/animationinspector/test/browser_animation_spacebar_toggles_node_animations.js @@ -0,0 +1,45 @@ +/* vim: set ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that the spacebar key press toggles the play/resume button state. +// This test doesn't need to test if animations actually pause/resume +// because there's an other test that does this. +// There are animations in the test page and since, by default, the <body> node +// is selected, animations will be displayed in the timeline, so the timeline +// play/resume button will be displayed + +requestLongerTimeout(2); + +add_task(function* () { + requestLongerTimeout(2); + + yield addTab(URL_ROOT + "doc_simple_animation.html"); + let {panel, window} = yield openAnimationInspector(); + let {playTimelineButtonEl} = panel; + + // ensure the focus is on the animation panel + window.focus(); + + info("Simulate spacebar stroke and check playResume button" + + " is in paused state"); + + // sending the key will lead to a UI_UPDATE_EVENT + let onUpdated = panel.once(panel.UI_UPDATED_EVENT); + EventUtils.sendKey("SPACE", window); + yield onUpdated; + ok(playTimelineButtonEl.classList.contains("paused"), + "The play/resume button is in its paused state"); + + info("Simulate spacebar stroke and check playResume button" + + " is in playing state"); + + // sending the key will lead to a UI_UPDATE_EVENT + onUpdated = panel.once(panel.UI_UPDATED_EVENT); + EventUtils.sendKey("SPACE", window); + yield onUpdated; + ok(!playTimelineButtonEl.classList.contains("paused"), + "The play/resume button is in its play state again"); +}); diff --git a/devtools/client/animationinspector/test/browser_animation_target_highlight_select.js b/devtools/client/animationinspector/test/browser_animation_target_highlight_select.js new file mode 100644 index 000000000..de14e6aca --- /dev/null +++ b/devtools/client/animationinspector/test/browser_animation_target_highlight_select.js @@ -0,0 +1,73 @@ +/* vim: set ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +requestLongerTimeout(2); + +// Test that the DOM element targets displayed in animation player widgets can +// be used to highlight elements in the DOM and select them in the inspector. + +add_task(function* () { + yield addTab(URL_ROOT + "doc_simple_animation.html"); + + let {toolbox, inspector, panel} = yield openAnimationInspector(); + + info("Select the simple animated node"); + let onPanelUpdated = panel.once(panel.UI_UPDATED_EVENT); + yield selectNodeAndWaitForAnimations(".animated", inspector); + yield onPanelUpdated; + + let targets = yield waitForAllAnimationTargets(panel); + // Arbitrary select the first one + let targetNodeComponent = targets[0]; + + info("Retrieve the part of the widget that highlights the node on hover"); + let highlightingEl = targetNodeComponent.previewer.previewEl; + + info("Listen to node-highlight event and mouse over the widget"); + let onHighlight = toolbox.once("node-highlight"); + EventUtils.synthesizeMouse(highlightingEl, 10, 5, {type: "mouseover"}, + highlightingEl.ownerDocument.defaultView); + let nodeFront = yield onHighlight; + + // Do not forget to mouseout, otherwise we get random mouseover event + // when selecting another node, which triggers some requests in animation + // inspector. + EventUtils.synthesizeMouse(highlightingEl, 10, 5, {type: "mouseout"}, + highlightingEl.ownerDocument.defaultView); + + ok(true, "The node-highlight event was fired"); + is(targetNodeComponent.previewer.nodeFront, nodeFront, + "The highlighted node is the one stored on the animation widget"); + 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, "ball animated", + "The highlighted node has the correct class"); + + info("Select the body node in order to have the list of all animations"); + onPanelUpdated = panel.once(panel.UI_UPDATED_EVENT); + yield selectNodeAndWaitForAnimations("body", inspector); + yield onPanelUpdated; + + targets = yield waitForAllAnimationTargets(panel); + targetNodeComponent = targets[0]; + + info("Click on the first animated node component and wait for the " + + "selection to change"); + let onSelection = inspector.selection.once("new-node-front"); + onPanelUpdated = panel.once(panel.UI_UPDATED_EVENT); + let nodeEl = targetNodeComponent.previewer.previewEl; + EventUtils.sendMouseEvent({type: "click"}, nodeEl, + nodeEl.ownerDocument.defaultView); + yield onSelection; + + is(inspector.selection.nodeFront, targetNodeComponent.previewer.nodeFront, + "The selected node is the one stored on the animation widget"); + + yield onPanelUpdated; + yield waitForAllAnimationTargets(panel); +}); diff --git a/devtools/client/animationinspector/test/browser_animation_target_highlighter_lock.js b/devtools/client/animationinspector/test/browser_animation_target_highlighter_lock.js new file mode 100644 index 000000000..b5e952679 --- /dev/null +++ b/devtools/client/animationinspector/test/browser_animation_target_highlighter_lock.js @@ -0,0 +1,54 @@ +/* vim: set ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +requestLongerTimeout(2); + +// Test that the DOM element targets displayed in animation player widgets can +// be used to highlight elements in the DOM and select them in the inspector. + +add_task(function* () { + yield addTab(URL_ROOT + "doc_simple_animation.html"); + let {panel} = yield openAnimationInspector(); + + let targets = panel.animationsTimelineComponent.targetNodes; + + info("Click on the highlighter icon for the first animated node"); + let domNodePreview1 = targets[0].previewer; + yield lockHighlighterOn(domNodePreview1); + ok(domNodePreview1.highlightNodeEl.classList.contains("selected"), + "The highlighter icon is selected"); + + info("Click on the highlighter icon for the second animated node"); + let domNodePreview2 = targets[1].previewer; + yield lockHighlighterOn(domNodePreview2); + ok(domNodePreview2.highlightNodeEl.classList.contains("selected"), + "The highlighter icon is selected"); + ok(!domNodePreview1.highlightNodeEl.classList.contains("selected"), + "The highlighter icon for the first node is unselected"); + + info("Click again to unhighlight"); + yield unlockHighlighterOn(domNodePreview2); + ok(!domNodePreview2.highlightNodeEl.classList.contains("selected"), + "The highlighter icon for the second node is unselected"); +}); + +function* lockHighlighterOn(domNodePreview) { + let onLocked = domNodePreview.once("target-highlighter-locked"); + clickOnHighlighterIcon(domNodePreview); + yield onLocked; +} + +function* unlockHighlighterOn(domNodePreview) { + let onUnlocked = domNodePreview.once("target-highlighter-unlocked"); + clickOnHighlighterIcon(domNodePreview); + yield onUnlocked; +} + +function clickOnHighlighterIcon(domNodePreview) { + let lockEl = domNodePreview.highlightNodeEl; + EventUtils.sendMouseEvent({type: "click"}, lockEl, + lockEl.ownerDocument.defaultView); +} diff --git a/devtools/client/animationinspector/test/browser_animation_timeline_currentTime.js b/devtools/client/animationinspector/test/browser_animation_timeline_currentTime.js new file mode 100644 index 000000000..d5caaff28 --- /dev/null +++ b/devtools/client/animationinspector/test/browser_animation_timeline_currentTime.js @@ -0,0 +1,48 @@ +/* vim: set ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +requestLongerTimeout(2); + +// Check that the timeline toolbar displays the current time, and that it +// changes when animations are playing, gets back to 0 when animations are +// rewound, and stops when animations are paused. + +add_task(function* () { + yield addTab(URL_ROOT + "doc_simple_animation.html"); + + let {panel} = yield openAnimationInspector(); + let label = panel.timelineCurrentTimeEl; + ok(label, "The current time label exists"); + + // On page load animations are playing so the time shoud change, although we + // don't want to test the exact value of the time displayed, just that it + // actually changes. + info("Make sure the time displayed actually changes"); + yield isCurrentTimeLabelChanging(panel, true); + + info("Pause the animations and check that the time stops changing"); + yield clickTimelinePlayPauseButton(panel); + yield isCurrentTimeLabelChanging(panel, false); + + info("Rewind the animations and check that the time stops changing"); + yield clickTimelineRewindButton(panel); + yield isCurrentTimeLabelChanging(panel, false); + is(label.textContent, "00:00.000"); +}); + +function* isCurrentTimeLabelChanging(panel, isChanging) { + let label = panel.timelineCurrentTimeEl; + + let time1 = label.textContent; + yield new Promise(r => setTimeout(r, 200)); + let time2 = label.textContent; + + if (isChanging) { + ok(time1 !== time2, "The text displayed in the label changes with time"); + } else { + is(time1, time2, "The text displayed in the label doesn't change"); + } +} diff --git a/devtools/client/animationinspector/test/browser_animation_timeline_header.js b/devtools/client/animationinspector/test/browser_animation_timeline_header.js new file mode 100644 index 000000000..3a0a0412a --- /dev/null +++ b/devtools/client/animationinspector/test/browser_animation_timeline_header.js @@ -0,0 +1,59 @@ +/* vim: set ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +requestLongerTimeout(2); + +// Check that the timeline shows correct time graduations in the header. + +const {findOptimalTimeInterval, TimeScale} = require("devtools/client/animationinspector/utils"); + +// Should be kept in sync with TIME_GRADUATION_MIN_SPACING in +// animation-timeline.js +const TIME_GRADUATION_MIN_SPACING = 40; + +add_task(function* () { + yield addTab(URL_ROOT + "doc_simple_animation.html"); + + // System scrollbar is enabled by default on our testing envionment and it + // would shrink width of inspector and affect number of time-ticks causing + // unexpected results. So, we set it wider to avoid this kind of edge case. + yield pushPref("devtools.toolsidebar-width.inspector", 350); + + let {panel} = yield openAnimationInspector(); + + let timeline = panel.animationsTimelineComponent; + let headerEl = timeline.timeHeaderEl; + + info("Find out how many time graduations should there be"); + let width = headerEl.offsetWidth; + + let animationDuration = TimeScale.maxEndTime - TimeScale.minStartTime; + let minTimeInterval = TIME_GRADUATION_MIN_SPACING * animationDuration / width; + + // Note that findOptimalTimeInterval is tested separately in xpcshell test + // test_findOptimalTimeInterval.js, so we assume that it works here. + let interval = findOptimalTimeInterval(minTimeInterval); + let nb = Math.ceil(animationDuration / interval); + + is(headerEl.querySelectorAll(".header-item").length, nb, + "The expected number of time ticks were found"); + + info("Make sure graduations are evenly distributed and show the right times"); + [...headerEl.querySelectorAll(".time-tick")].forEach((tick, i) => { + let left = parseFloat(tick.style.left); + let expectedPos = i * interval * 100 / animationDuration; + is(Math.round(left), Math.round(expectedPos), + `Graduation ${i} 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. + let formattedTime = TimeScale.formatTime( + TimeScale.distanceToRelativeTime(expectedPos, width)); + is(tick.textContent, formattedTime, + `Graduation ${i} has the right text content`); + }); +}); diff --git a/devtools/client/animationinspector/test/browser_animation_timeline_iterationStart.js b/devtools/client/animationinspector/test/browser_animation_timeline_iterationStart.js new file mode 100644 index 000000000..c05f15d27 --- /dev/null +++ b/devtools/client/animationinspector/test/browser_animation_timeline_iterationStart.js @@ -0,0 +1,71 @@ +/* vim: set ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Check that the iteration start is displayed correctly in time blocks. + +add_task(function* () { + yield addTab(URL_ROOT + "doc_script_animation.html"); + let {panel} = yield openAnimationInspector(); + let timelineComponent = panel.animationsTimelineComponent; + let timeBlockComponents = timelineComponent.timeBlocks; + let detailsComponents = timelineComponent.details; + + for (let i = 0; i < timeBlockComponents.length; i++) { + info(`Expand time block ${i} so its keyframes are visible`); + yield clickOnAnimation(panel, i); + + info(`Check the state of time block ${i}`); + let {containerEl, animation: {state}} = timeBlockComponents[i]; + + checkAnimationTooltip(containerEl, state); + checkProgressAtStartingTime(containerEl, state); + + // Get the first set of keyframes (there's only one animated property + // anyway), and the first frame element from there, we're only interested in + // its offset. + let keyframeComponent = detailsComponents[i].keyframeComponents[0]; + let frameEl = keyframeComponent.keyframesEl.querySelector(".frame"); + checkKeyframeOffset(containerEl, frameEl, state); + } +}); + +function checkAnimationTooltip(el, {iterationStart, duration}) { + info("Check an animation's iterationStart data in its tooltip"); + let title = el.querySelector(".name").getAttribute("title"); + + let iterationStartTime = iterationStart * duration / 1000; + let iterationStartTimeString = iterationStartTime.toLocaleString(undefined, { + maximumFractionDigits: 2, + minimumFractionDigits: 2 + }).replace(".", "\\."); + let iterationStartString = iterationStart.toString().replace(".", "\\."); + + let regex = new RegExp("Iteration start: " + iterationStartString + + " \\(" + iterationStartTimeString + "s\\)"); + ok(title.match(regex), "The tooltip shows the expected iteration start"); +} + +function checkProgressAtStartingTime(el, { iterationStart }) { + info("Check the progress of starting time"); + const pathEl = el.querySelector(".iteration-path"); + const pathSegList = pathEl.pathSegList; + const pathSeg = pathSegList.getItem(1); + const progress = pathSeg.y; + is(progress, iterationStart % 1, + `The progress at starting point should be ${ iterationStart % 1 }`); +} + +function checkKeyframeOffset(timeBlockEl, frameEl, {iterationStart}) { + info("Check that the first keyframe is offset correctly"); + + let start = getIterationStartFromLeft(frameEl); + is(start, iterationStart % 1, "The frame offset for iteration start"); +} + +function getIterationStartFromLeft(el) { + let left = 100 - parseFloat(/(\d+)%/.exec(el.style.left)[1]); + return left / 100; +} diff --git a/devtools/client/animationinspector/test/browser_animation_timeline_pause_button_01.js b/devtools/client/animationinspector/test/browser_animation_timeline_pause_button_01.js new file mode 100644 index 000000000..a3a2b4c61 --- /dev/null +++ b/devtools/client/animationinspector/test/browser_animation_timeline_pause_button_01.js @@ -0,0 +1,34 @@ +/* vim: set ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +requestLongerTimeout(2); + +// Check that the timeline toolbar contains a pause button and that this pause button can +// be clicked. Check that when it is, the button changes state and the scrubber stops and +// resumes. + +add_task(function* () { + yield addTab(URL_ROOT + "doc_simple_animation.html"); + + let {panel} = yield openAnimationInspector(); + let btn = panel.playTimelineButtonEl; + + ok(btn, "The play/pause button exists"); + ok(!btn.classList.contains("paused"), "The play/pause button is in its playing state"); + + info("Click on the button to pause all timeline animations"); + yield clickTimelinePlayPauseButton(panel); + + ok(btn.classList.contains("paused"), "The play/pause button is in its paused state"); + yield assertScrubberMoving(panel, false); + + info("Click again on the button to play all timeline animations"); + yield clickTimelinePlayPauseButton(panel); + + ok(!btn.classList.contains("paused"), + "The play/pause button is in its playing state again"); + yield assertScrubberMoving(panel, true); +}); diff --git a/devtools/client/animationinspector/test/browser_animation_timeline_pause_button_02.js b/devtools/client/animationinspector/test/browser_animation_timeline_pause_button_02.js new file mode 100644 index 000000000..1c440dd88 --- /dev/null +++ b/devtools/client/animationinspector/test/browser_animation_timeline_pause_button_02.js @@ -0,0 +1,48 @@ +/* vim: set ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +requestLongerTimeout(2); + +// Checks that the play/pause button goes to the right state when the scrubber has reached +// the end of the timeline but there are infinite animations playing. + +add_task(function* () { + yield addTab(URL_ROOT + "doc_simple_animation.html"); + + let {panel, inspector} = yield openAnimationInspector(); + let timeline = panel.animationsTimelineComponent; + let btn = panel.playTimelineButtonEl; + + info("Select an infinite animation and wait for the scrubber to reach the end"); + yield selectNodeAndWaitForAnimations(".multi", inspector); + yield waitForOutOfBoundScrubber(timeline); + + ok(!btn.classList.contains("paused"), + "The button is in its playing state still, animations are infinite."); + yield assertScrubberMoving(panel, true); + + info("Click on the button after the scrubber has moved out of bounds"); + yield clickTimelinePlayPauseButton(panel); + + ok(btn.classList.contains("paused"), + "The button can be paused after the scrubber has moved out of bounds"); + yield assertScrubberMoving(panel, false); +}); + +function waitForOutOfBoundScrubber({win, scrubberEl}) { + return new Promise(resolve => { + function check() { + let pos = scrubberEl.getBoxQuads()[0].bounds.right; + let width = win.document.documentElement.offsetWidth; + if (pos >= width) { + setTimeout(resolve, 50); + } else { + setTimeout(check, 50); + } + } + check(); + }); +} diff --git a/devtools/client/animationinspector/test/browser_animation_timeline_pause_button_03.js b/devtools/client/animationinspector/test/browser_animation_timeline_pause_button_03.js new file mode 100644 index 000000000..5c6e324ed --- /dev/null +++ b/devtools/client/animationinspector/test/browser_animation_timeline_pause_button_03.js @@ -0,0 +1,60 @@ +/* vim: set ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +requestLongerTimeout(2); + +// Also checks that the button goes to the right state when the scrubber has +// reached the end of the timeline: continues to be in playing mode for infinite +// animations, goes to paused mode otherwise. +// And test that clicking the button once the scrubber has reached the end of +// the timeline does the right thing. + +add_task(function* () { + yield addTab(URL_ROOT + "doc_simple_animation.html"); + + let {panel, controller, inspector} = yield openAnimationInspector(); + let btn = panel.playTimelineButtonEl; + + // For a finite animation, once the scrubber reaches the end of the timeline, the pause + // button should go back to paused mode. + info("Select a finite animation and wait for the animation to complete"); + yield selectNodeAndWaitForAnimations(".negative-delay", inspector); + + let onButtonPaused = waitForButtonPaused(btn); + let onTimelineUpdated = controller.once(controller.PLAYERS_UPDATED_EVENT); + // The page is reloaded to avoid missing the animation. + yield reloadTab(inspector); + yield onTimelineUpdated; + yield onButtonPaused; + + ok(btn.classList.contains("paused"), + "The button is in paused state once finite animations are done"); + yield assertScrubberMoving(panel, false); + + info("Click again on the button to play the animation from the start again"); + yield clickTimelinePlayPauseButton(panel); + + ok(!btn.classList.contains("paused"), + "Clicking the button once finite animations are done should restart them"); + yield assertScrubberMoving(panel, true); +}); + +function waitForButtonPaused(btn) { + return new Promise(resolve => { + let observer = new btn.ownerDocument.defaultView.MutationObserver(mutations => { + for (let mutation of mutations) { + if (mutation.type === "attributes" && + mutation.attributeName === "class" && + !mutation.oldValue.includes("paused") && + btn.classList.contains("paused")) { + observer.disconnect(); + resolve(); + } + } + }); + observer.observe(btn, { attributes: true, attributeOldValue: true }); + }); +} diff --git a/devtools/client/animationinspector/test/browser_animation_timeline_rate_selector.js b/devtools/client/animationinspector/test/browser_animation_timeline_rate_selector.js new file mode 100644 index 000000000..37ac20de0 --- /dev/null +++ b/devtools/client/animationinspector/test/browser_animation_timeline_rate_selector.js @@ -0,0 +1,56 @@ +/* vim: set ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +requestLongerTimeout(2); + +// Check that the timeline toolbar contains a playback rate selector UI and that +// it can be used to change the playback rate of animations in the timeline. +// Also check that it displays the rate of the current animations in case they +// all have the same rate, or that it displays the empty value in case they +// have mixed rates. + +add_task(function* () { + yield addTab(URL_ROOT + "doc_simple_animation.html"); + + let {panel, controller, inspector, toolbox} = yield openAnimationInspector(); + + // In this test, we disable the highlighter on purpose because of the way + // events are simulated to select an option in the playbackRate <select>. + // Indeed, this may cause mousemove events to be triggered on the nodes that + // are underneath the <select>, and these are AnimationTargetNode instances. + // Simulating mouse events on them will cause the highlighter to emit requests + // and this might cause the test to fail if they happen after it has ended. + disableHighlighter(toolbox); + + let select = panel.rateSelectorEl.firstChild; + + ok(select, "The rate selector exists"); + + info("Change all of the current animations' rates to 0.5"); + yield changeTimelinePlaybackRate(panel, .5); + checkAllAnimationsRatesChanged(controller, select, .5); + + info("Select just one animated node and change its rate only"); + yield selectNodeAndWaitForAnimations(".animated", inspector); + + yield changeTimelinePlaybackRate(panel, 2); + checkAllAnimationsRatesChanged(controller, select, 2); + + info("Select the <body> again, it should now have mixed-rates animations"); + yield selectNodeAndWaitForAnimations("body", inspector); + + is(select.value, "", "The selected rate is empty"); + + info("Change the rate for these mixed-rate animations"); + yield changeTimelinePlaybackRate(panel, 1); + checkAllAnimationsRatesChanged(controller, select, 1); +}); + +function checkAllAnimationsRatesChanged({animationPlayers}, select, rate) { + ok(animationPlayers.every(({state}) => state.playbackRate === rate), + "All animations' rates have been set to " + rate); + is(select.value, rate, "The right value is displayed in the select"); +} diff --git a/devtools/client/animationinspector/test/browser_animation_timeline_rewind_button.js b/devtools/client/animationinspector/test/browser_animation_timeline_rewind_button.js new file mode 100644 index 000000000..c4dcbd161 --- /dev/null +++ b/devtools/client/animationinspector/test/browser_animation_timeline_rewind_button.js @@ -0,0 +1,51 @@ +/* vim: set ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +requestLongerTimeout(2); + +// Check that the timeline toolbar contains a rewind button and that it can be +// clicked. Check that when it is, the current animations displayed in the +// timeline get their playstates changed to paused, and their currentTimes +// reset to 0, and that the scrubber stops moving and is positioned to the +// start. + +add_task(function* () { + yield addTab(URL_ROOT + "doc_simple_animation.html"); + + let {panel, controller} = yield openAnimationInspector(); + let players = controller.animationPlayers; + let btn = panel.rewindTimelineButtonEl; + + ok(btn, "The rewind button exists"); + + info("Click on the button to rewind all timeline animations"); + yield clickTimelineRewindButton(panel); + + info("Check that the scrubber has stopped moving"); + yield assertScrubberMoving(panel, false); + + ok(players.every(({state}) => state.currentTime === 0), + "All animations' currentTimes have been set to 0"); + ok(players.every(({state}) => state.playState === "paused"), + "All animations have been paused"); + + info("Play the animations again"); + yield clickTimelinePlayPauseButton(panel); + + info("And pause them after a short while"); + yield new Promise(r => setTimeout(r, 200)); + + info("Check that rewinding when animations are paused works too"); + yield clickTimelineRewindButton(panel); + + info("Check that the scrubber has stopped moving"); + yield assertScrubberMoving(panel, false); + + ok(players.every(({state}) => state.currentTime === 0), + "All animations' currentTimes have been set to 0"); + ok(players.every(({state}) => state.playState === "paused"), + "All animations have been paused"); +}); diff --git a/devtools/client/animationinspector/test/browser_animation_timeline_scrubber_exists.js b/devtools/client/animationinspector/test/browser_animation_timeline_scrubber_exists.js new file mode 100644 index 000000000..9fa22e007 --- /dev/null +++ b/devtools/client/animationinspector/test/browser_animation_timeline_scrubber_exists.js @@ -0,0 +1,20 @@ +/* vim: set ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +requestLongerTimeout(2); + +// Check that the timeline does have a scrubber element. + +add_task(function* () { + yield addTab(URL_ROOT + "doc_simple_animation.html"); + let {panel} = yield openAnimationInspector(); + + let timeline = panel.animationsTimelineComponent; + let scrubberEl = timeline.scrubberEl; + + ok(scrubberEl, "The scrubber element exists"); + ok(scrubberEl.classList.contains("scrubber"), "It has the right classname"); +}); diff --git a/devtools/client/animationinspector/test/browser_animation_timeline_scrubber_movable.js b/devtools/client/animationinspector/test/browser_animation_timeline_scrubber_movable.js new file mode 100644 index 000000000..a690dd78e --- /dev/null +++ b/devtools/client/animationinspector/test/browser_animation_timeline_scrubber_movable.js @@ -0,0 +1,70 @@ +/* vim: set ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +requestLongerTimeout(2); + +// Check that the scrubber in the timeline can be moved by clicking & dragging +// in the header area. +// Also check that doing so changes the timeline's play/pause button to paused +// state. +// Finally, also check that the scrubber can be moved using the scrubber handle. + +add_task(function* () { + yield addTab(URL_ROOT + "doc_simple_animation.html"); + + let {panel} = yield openAnimationInspector(); + let timeline = panel.animationsTimelineComponent; + let {win, timeHeaderEl, scrubberEl, scrubberHandleEl} = timeline; + let playTimelineButtonEl = panel.playTimelineButtonEl; + + ok(!playTimelineButtonEl.classList.contains("paused"), + "The timeline play button is in its playing state by default"); + + info("Mousedown in the header to move the scrubber"); + yield synthesizeInHeaderAndWaitForChange(timeline, 50, 1, "mousedown"); + checkScrubberIsAt(scrubberEl, timeHeaderEl, 50); + + ok(playTimelineButtonEl.classList.contains("paused"), + "The timeline play button is in its paused state after mousedown"); + + info("Continue moving the mouse and verify that the scrubber tracks it"); + yield synthesizeInHeaderAndWaitForChange(timeline, 100, 1, "mousemove"); + checkScrubberIsAt(scrubberEl, timeHeaderEl, 100); + + ok(playTimelineButtonEl.classList.contains("paused"), + "The timeline play button is in its paused state after mousemove"); + + info("Release the mouse and move again and verify that the scrubber stays"); + EventUtils.synthesizeMouse(timeHeaderEl, 100, 1, {type: "mouseup"}, win); + EventUtils.synthesizeMouse(timeHeaderEl, 200, 1, {type: "mousemove"}, win); + checkScrubberIsAt(scrubberEl, timeHeaderEl, 100); + + info("Try to drag the scrubber handle and check that the scrubber moves"); + let onDataChanged = timeline.once("timeline-data-changed"); + EventUtils.synthesizeMouse(scrubberHandleEl, 1, 20, {type: "mousedown"}, win); + EventUtils.synthesizeMouse(timeHeaderEl, 0, 0, {type: "mousemove"}, win); + EventUtils.synthesizeMouse(timeHeaderEl, 0, 0, {type: "mouseup"}, win); + yield onDataChanged; + + checkScrubberIsAt(scrubberEl, timeHeaderEl, 0); +}); + +function* synthesizeInHeaderAndWaitForChange(timeline, x, y, type) { + let onDataChanged = timeline.once("timeline-data-changed"); + EventUtils.synthesizeMouse(timeline.timeHeaderEl, x, y, {type}, timeline.win); + yield onDataChanged; +} + +function getPositionPercentage(pos, headerEl) { + return pos * 100 / headerEl.offsetWidth; +} + +function checkScrubberIsAt(scrubberEl, timeHeaderEl, pos) { + let newPos = Math.round(parseFloat(scrubberEl.style.left)); + let expectedPos = Math.round(getPositionPercentage(pos, timeHeaderEl)); + is(newPos, expectedPos, + `The scrubber is at position ${pos} (${expectedPos}%)`); +} diff --git a/devtools/client/animationinspector/test/browser_animation_timeline_scrubber_moves.js b/devtools/client/animationinspector/test/browser_animation_timeline_scrubber_moves.js new file mode 100644 index 000000000..494c581a4 --- /dev/null +++ b/devtools/client/animationinspector/test/browser_animation_timeline_scrubber_moves.js @@ -0,0 +1,28 @@ +/* vim: set ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +requestLongerTimeout(2); + +// Check that the scrubber in the timeline moves when animations are playing. +// The animations in the test page last for a very long time, so the test just +// measures the position of the scrubber once, then waits for some time to pass +// and measures its position again. + +add_task(function* () { + yield addTab(URL_ROOT + "doc_simple_animation.html"); + let {panel} = yield openAnimationInspector(); + + let timeline = panel.animationsTimelineComponent; + let scrubberEl = timeline.scrubberEl; + let startPos = scrubberEl.getBoundingClientRect().left; + + info("Wait for some time to check that the scrubber moves"); + yield new Promise(r => setTimeout(r, 2000)); + + let endPos = scrubberEl.getBoundingClientRect().left; + + ok(endPos > startPos, "The scrubber has moved"); +}); diff --git a/devtools/client/animationinspector/test/browser_animation_timeline_setCurrentTime.js b/devtools/client/animationinspector/test/browser_animation_timeline_setCurrentTime.js new file mode 100644 index 000000000..efc32c001 --- /dev/null +++ b/devtools/client/animationinspector/test/browser_animation_timeline_setCurrentTime.js @@ -0,0 +1,88 @@ +/* vim: set ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +requestLongerTimeout(2); + +// Animation.currentTime ignores neagtive delay and positive/negative endDelay +// during fill-mode, even if they are set. +// For example, when the animation timing is +// { duration: 1000, iterations: 1, endDelay: -500, easing: linear }, +// the animation progress is 0.5 at 700ms because the progress stops as 0.5 at +// 500ms in original animation. However, if you set as +// animation.currentTime = 700 manually, the progress will be 0.7. +// So we modify setCurrentTime method since +// AnimationInspector should re-produce same as original animation. +// In these tests, +// we confirm the behavior of setCurrentTime by delay and endDelay. + +add_task(function* () { + yield addTab(URL_ROOT + "doc_timing_combination_animation.html"); + const { panel, controller } = yield openAnimationInspector(); + + yield clickTimelinePlayPauseButton(panel); + + const timelineComponent = panel.animationsTimelineComponent; + const timeBlockComponents = timelineComponent.timeBlocks; + + // Test -5000ms. + let time = -5000; + yield controller.setCurrentTimeAll(time, true); + for (let i = 0; i < timeBlockComponents.length; i++) { + yield timeBlockComponents[i].animation.refreshState(); + const state = yield timeBlockComponents[i].animation.state; + info(`Check the state at ${ time }ms with ` + + `delay:${ state.delay } and endDelay:${ state.endDelay }`); + is(state.currentTime, 0, + `The currentTime should be 0 at setCurrentTime(${ time })`); + } + + // Test 10000ms. + time = 10000; + yield controller.setCurrentTimeAll(time, true); + for (let i = 0; i < timeBlockComponents.length; i++) { + yield timeBlockComponents[i].animation.refreshState(); + const state = yield timeBlockComponents[i].animation.state; + info(`Check the state at ${ time }ms with ` + + `delay:${ state.delay } and endDelay:${ state.endDelay }`); + const expected = state.delay < 0 ? 0 : time; + is(state.currentTime, expected, + `The currentTime should be ${ expected } at setCurrentTime(${ time }).` + + ` delay: ${ state.delay } and endDelay: ${ state.endDelay }`); + } + + // Test 60000ms. + time = 60000; + yield controller.setCurrentTimeAll(time, true); + for (let i = 0; i < timeBlockComponents.length; i++) { + yield timeBlockComponents[i].animation.refreshState(); + const state = yield timeBlockComponents[i].animation.state; + info(`Check the state at ${ time }ms with ` + + `delay:${ state.delay } and endDelay:${ state.endDelay }`); + const expected = state.delay < 0 ? time + state.delay : time; + is(state.currentTime, expected, + `The currentTime should be ${ expected } at setCurrentTime(${ time }).` + + ` delay: ${ state.delay } and endDelay: ${ state.endDelay }`); + } + + // Test 150000ms. + time = 150000; + yield controller.setCurrentTimeAll(time, true); + for (let i = 0; i < timeBlockComponents.length; i++) { + yield timeBlockComponents[i].animation.refreshState(); + const state = yield timeBlockComponents[i].animation.state; + info(`Check the state at ${ time }ms with ` + + `delay:${ state.delay } and endDelay:${ state.endDelay }`); + const currentTime = state.delay < 0 ? time + state.delay : time; + const endTime = + state.delay + state.iterationCount * state.duration + state.endDelay; + const expected = + state.endDelay < 0 && state.fill === "both" && currentTime > endTime + ? endTime : currentTime; + is(state.currentTime, expected, + `The currentTime should be ${ expected } at setCurrentTime(${ time }).` + + ` delay: ${ state.delay } and endDelay: ${ state.endDelay }`); + } +}); diff --git a/devtools/client/animationinspector/test/browser_animation_timeline_shows_delay.js b/devtools/client/animationinspector/test/browser_animation_timeline_shows_delay.js new file mode 100644 index 000000000..8c9b0653d --- /dev/null +++ b/devtools/client/animationinspector/test/browser_animation_timeline_shows_delay.js @@ -0,0 +1,96 @@ +/* vim: set ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +requestLongerTimeout(2); + +// Check that animation delay is visualized in the timeline when the animation +// is delayed. +// Also check that negative delays do not overflow the UI, and are shown like +// positive delays. + +add_task(function* () { + yield addTab(URL_ROOT + "doc_simple_animation.html"); + let {inspector, panel} = yield openAnimationInspector(); + + info("Selecting a delayed animated node"); + yield selectNodeAndWaitForAnimations(".delayed", inspector); + let timelineEl = panel.animationsTimelineComponent.rootWrapperEl; + checkDelayAndName(timelineEl, true); + let animationEl = timelineEl.querySelector(".animation"); + let state = panel.animationsTimelineComponent.timeBlocks[0].animation.state; + checkPath(animationEl, state); + + info("Selecting a no-delay animated node"); + yield selectNodeAndWaitForAnimations(".animated", inspector); + checkDelayAndName(timelineEl, false); + animationEl = timelineEl.querySelector(".animation"); + state = panel.animationsTimelineComponent.timeBlocks[0].animation.state; + checkPath(animationEl, state); + + info("Selecting a negative-delay animated node"); + yield selectNodeAndWaitForAnimations(".negative-delay", inspector); + checkDelayAndName(timelineEl, true); + animationEl = timelineEl.querySelector(".animation"); + state = panel.animationsTimelineComponent.timeBlocks[0].animation.state; + checkPath(animationEl, state); +}); + +function checkDelayAndName(timelineEl, hasDelay) { + let delay = timelineEl.querySelector(".delay"); + + is(!!delay, hasDelay, "The timeline " + + (hasDelay ? "contains" : "does not contain") + + " a delay element, as expected"); + + if (hasDelay) { + let targetNode = timelineEl.querySelector(".target"); + + // Check that the delay element does not cause the timeline to overflow. + let delayLeft = Math.round(delay.getBoundingClientRect().x); + let sidebarWidth = Math.round(targetNode.getBoundingClientRect().width); + ok(delayLeft >= sidebarWidth, + "The delay element isn't displayed over the sidebar"); + } +} + +function checkPath(animationEl, state) { + // Check existance of delay path. + const delayPathEl = animationEl.querySelector(".delay-path"); + if (!state.iterationCount && state.delay < 0) { + // Infinity + ok(!delayPathEl, "The delay path for Infinity should not exist"); + return; + } + if (state.delay === 0) { + ok(!delayPathEl, "The delay path for zero delay should not exist"); + return; + } + ok(delayPathEl, "The delay path should exist"); + + // Check delay path coordinates. + const pathSegList = delayPathEl.pathSegList; + const startingPathSeg = pathSegList.getItem(0); + const endingPathSeg = pathSegList.getItem(pathSegList.numberOfItems - 2); + if (state.delay < 0) { + ok(delayPathEl.classList.contains("negative"), + "The delay path should have 'negative' class"); + const startingX = state.delay; + const endingX = 0; + is(startingPathSeg.x, startingX, + `The x of starting point should be ${ startingX }`); + is(endingPathSeg.x, endingX, + `The x of ending point should be ${ endingX }`); + } else { + ok(!delayPathEl.classList.contains("negative"), + "The delay path should not have 'negative' class"); + const startingX = 0; + const endingX = state.delay; + is(startingPathSeg.x, startingX, + `The x of starting point should be ${ startingX }`); + is(endingPathSeg.x, endingX, + `The x of ending point should be ${ endingX }`); + } +} diff --git a/devtools/client/animationinspector/test/browser_animation_timeline_shows_endDelay.js b/devtools/client/animationinspector/test/browser_animation_timeline_shows_endDelay.js new file mode 100644 index 000000000..0aa5c16c0 --- /dev/null +++ b/devtools/client/animationinspector/test/browser_animation_timeline_shows_endDelay.js @@ -0,0 +1,78 @@ +/* vim: set ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +requestLongerTimeout(2); + +// Check that animation endDelay is visualized in the timeline when the +// animation is delayed. +// Also check that negative endDelays do not overflow the UI, and are shown +// like positive endDelays. + +add_task(function* () { + yield addTab(URL_ROOT + "doc_end_delay.html"); + let {inspector, panel} = yield openAnimationInspector(); + + let selectors = ["#target1", "#target2", "#target3", "#target4"]; + for (let i = 0; i < selectors.length; i++) { + let selector = selectors[i]; + yield selectNode(selector, inspector); + let timelineEl = panel.animationsTimelineComponent.rootWrapperEl; + let animationEl = timelineEl.querySelector(".animation"); + checkEndDelayAndName(animationEl); + const state = + panel.animationsTimelineComponent.timeBlocks[0].animation.state; + checkPath(animationEl, state); + } +}); + +function checkEndDelayAndName(animationEl) { + let endDelay = animationEl.querySelector(".end-delay"); + let name = animationEl.querySelector(".name"); + let targetNode = animationEl.querySelector(".target"); + + // Check that the endDelay element does not cause the timeline to overflow. + let endDelayLeft = Math.round(endDelay.getBoundingClientRect().x); + let sidebarWidth = Math.round(targetNode.getBoundingClientRect().width); + ok(endDelayLeft >= sidebarWidth, + "The endDelay element isn't displayed over the sidebar"); + + // Check that the endDelay is not displayed on top of the name. + let endDelayRight = Math.round(endDelay.getBoundingClientRect().right); + let nameLeft = Math.round(name.getBoundingClientRect().left); + ok(endDelayRight >= nameLeft, + "The endDelay element does not span over the name element"); +} + +function checkPath(animationEl, state) { + // Check existance of enddelay path. + const endDelayPathEl = animationEl.querySelector(".enddelay-path"); + ok(endDelayPathEl, "The endDelay path should exist"); + + // Check enddelay path coordinates. + const pathSegList = endDelayPathEl.pathSegList; + const startingPathSeg = pathSegList.getItem(0); + const endingPathSeg = pathSegList.getItem(pathSegList.numberOfItems - 2); + if (state.endDelay < 0) { + ok(endDelayPathEl.classList.contains("negative"), + "The endDelay path should have 'negative' class"); + const endingX = state.delay + state.iterationCount * state.duration; + const startingX = endingX + state.endDelay; + is(startingPathSeg.x, startingX, + `The x of starting point should be ${ startingX }`); + is(endingPathSeg.x, endingX, + `The x of ending point should be ${ endingX }`); + } else { + ok(!endDelayPathEl.classList.contains("negative"), + "The endDelay path should not have 'negative' class"); + const startingX = + state.delay + state.iterationCount * state.duration; + const endingX = startingX + state.endDelay; + is(startingPathSeg.x, startingX, + `The x of starting point should be ${ startingX }`); + is(endingPathSeg.x, endingX, + `The x of ending point should be ${ endingX }`); + } +} diff --git a/devtools/client/animationinspector/test/browser_animation_timeline_shows_iterations.js b/devtools/client/animationinspector/test/browser_animation_timeline_shows_iterations.js new file mode 100644 index 000000000..08e5a2620 --- /dev/null +++ b/devtools/client/animationinspector/test/browser_animation_timeline_shows_iterations.js @@ -0,0 +1,47 @@ +/* vim: set ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +requestLongerTimeout(2); + +// Check that the timeline is displays as many iteration elements as there are +// iterations in an animation. + +add_task(function* () { + yield addTab(URL_ROOT + "doc_simple_animation.html"); + let {inspector, panel} = yield openAnimationInspector(); + + info("Selecting the test node"); + yield selectNodeAndWaitForAnimations(".delayed", inspector); + + info("Getting the animation element from the panel"); + const timelineComponent = panel.animationsTimelineComponent; + const timelineEl = timelineComponent.rootWrapperEl; + let animation = timelineEl.querySelector(".time-block"); + // Get iteration count from summary graph path. + let iterationCount = getIterationCount(animation); + + is(iterationCount, 10, + "The animation timeline contains the right number of iterations"); + ok(!animation.querySelector(".infinity"), + "The summary graph does not have any elements " + + " that have infinity class"); + + info("Selecting another test node with an infinite animation"); + yield selectNodeAndWaitForAnimations(".animated", inspector); + + info("Getting the animation element from the panel again"); + animation = timelineEl.querySelector(".time-block"); + iterationCount = getIterationCount(animation); + + is(iterationCount, 1, + "The animation timeline contains one iteration"); + ok(animation.querySelector(".infinity"), + "The summary graph has an element that has infinity class"); +}); + +function getIterationCount(timeblockEl) { + return timeblockEl.querySelectorAll(".iteration-path").length; +} diff --git a/devtools/client/animationinspector/test/browser_animation_timeline_shows_name_label.js b/devtools/client/animationinspector/test/browser_animation_timeline_shows_name_label.js new file mode 100644 index 000000000..e5778c943 --- /dev/null +++ b/devtools/client/animationinspector/test/browser_animation_timeline_shows_name_label.js @@ -0,0 +1,46 @@ +/* vim: set ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +requestLongerTimeout(2); + +// Check the text content and width of name label. + +add_task(function* () { + yield addTab(URL_ROOT + "doc_simple_animation.html"); + let {inspector, panel} = yield openAnimationInspector(); + + info("Selecting 'simple-animation' animation which is running on compositor"); + yield selectNodeAndWaitForAnimations(".animated", inspector); + checkNameLabel(panel.animationsTimelineComponent.rootWrapperEl, "simple-animation"); + + info("Selecting 'no-compositor' animation which is not running on compositor"); + yield selectNodeAndWaitForAnimations(".no-compositor", inspector); + checkNameLabel(panel.animationsTimelineComponent.rootWrapperEl, "no-compositor"); +}); + +function checkNameLabel(rootWrapperEl, expectedLabelContent) { + const timeblockEl = rootWrapperEl.querySelector(".time-block"); + const labelEl = rootWrapperEl.querySelector(".name div"); + is(labelEl.textContent, expectedLabelContent, + `Text content of labelEl sould be ${ expectedLabelContent }`); + + // Expand timeblockEl to avoid max-width of the label. + timeblockEl.style.width = "10000px"; + const originalLabelWidth = labelEl.clientWidth; + ok(originalLabelWidth < timeblockEl.clientWidth / 2, + "Label width should be less than 50%"); + + // Set timeblockEl width to double of original label width. + timeblockEl.style.width = `${ originalLabelWidth * 2 }px`; + is(labelEl.clientWidth + labelEl.offsetLeft, originalLabelWidth, + `Label width + offsetLeft should be ${ originalLabelWidth }px`); + + // Shrink timeblockEl to enable max-width. + timeblockEl.style.width = `${ originalLabelWidth }px`; + is(labelEl.clientWidth + labelEl.offsetLeft, + Math.round(timeblockEl.clientWidth / 2), + "Label width + offsetLeft should be half of timeblockEl"); +} diff --git a/devtools/client/animationinspector/test/browser_animation_timeline_shows_time_info.js b/devtools/client/animationinspector/test/browser_animation_timeline_shows_time_info.js new file mode 100644 index 000000000..f330e880e --- /dev/null +++ b/devtools/client/animationinspector/test/browser_animation_timeline_shows_time_info.js @@ -0,0 +1,50 @@ +/* vim: set ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +requestLongerTimeout(2); + +// Check that the timeline displays animations' duration, delay iteration +// counts and iteration start in tooltips. + +add_task(function* () { + yield addTab(URL_ROOT + "doc_simple_animation.html"); + let {panel, controller} = yield openAnimationInspector(); + + info("Getting the animation element from the panel"); + let timelineEl = panel.animationsTimelineComponent.rootWrapperEl; + let timeBlockNameEls = timelineEl.querySelectorAll(".time-block .name"); + + // Verify that each time-block's name element has a tooltip that looks sort of + // ok. We don't need to test the actual content. + [...timeBlockNameEls].forEach((el, i) => { + ok(el.hasAttribute("title"), "The tooltip is defined for animation " + i); + + let title = el.getAttribute("title"); + if (controller.animationPlayers[i].state.delay) { + ok(title.match(/Delay: [\d.-]+s/), "The tooltip shows the delay"); + } + ok(title.match(/Duration: [\d.]+s/), "The tooltip shows the duration"); + if (controller.animationPlayers[i].state.endDelay) { + ok(title.match(/End delay: [\d.-]+s/), "The tooltip shows the endDelay"); + } + if (controller.animationPlayers[i].state.iterationCount !== 1) { + ok(title.match(/Repeats: /), "The tooltip shows the iterations"); + } else { + ok(!title.match(/Repeats: /), "The tooltip doesn't show the iterations"); + } + if (controller.animationPlayers[i].state.easing) { + ok(title.match(/Easing: /), "The tooltip shows the easing"); + } + if (controller.animationPlayers[i].state.fill) { + ok(title.match(/Fill: /), "The tooltip shows the fill"); + } + if (controller.animationPlayers[i].state.direction) { + ok(title.match(/Direction: /), "The tooltip shows the direction"); + } + ok(!title.match(/Iteration start:/), + "The tooltip doesn't show the iteration start"); + }); +}); diff --git a/devtools/client/animationinspector/test/browser_animation_timeline_takes_rate_into_account.js b/devtools/client/animationinspector/test/browser_animation_timeline_takes_rate_into_account.js new file mode 100644 index 000000000..42309203a --- /dev/null +++ b/devtools/client/animationinspector/test/browser_animation_timeline_takes_rate_into_account.js @@ -0,0 +1,81 @@ +/* vim: set ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Check that if an animation has had its playbackRate changed via the DOM, then +// the timeline UI shows the right delay and duration. +// Indeed, the header in the timeline UI always shows the unaltered time, +// because there might be multiple animations displayed at the same time, some +// of which may have a different rate than others. Those that have had their +// rate changed have a delay = delay/rate and a duration = duration/rate. + +add_task(function* () { + yield addTab(URL_ROOT + "doc_modify_playbackRate.html"); + + let {panel} = yield openAnimationInspector(); + + let timelineEl = panel.animationsTimelineComponent.rootWrapperEl; + + let timeBlocks = timelineEl.querySelectorAll(".time-block"); + is(timeBlocks.length, 2, "2 animations are displayed"); + + info("The first animation has its rate set to 1, let's measure it"); + + let el = timeBlocks[0]; + let duration = getDuration(el.querySelector("path")); + let delay = parseInt(el.querySelector(".delay").style.width, 10); + + info("The second animation has its rate set to 2, so should be shorter"); + + let el2 = timeBlocks[1]; + let duration2 = getDuration(el2.querySelector("path")); + let delay2 = parseInt(el2.querySelector(".delay").style.width, 10); + + // The width are calculated by the animation-inspector dynamically depending + // on the size of the panel, and therefore depends on the test machine/OS. + // Let's not try to be too precise here and compare numbers. + let durationDelta = (2 * duration2) - duration; + ok(durationDelta <= 1, "The duration width is correct"); + let delayDelta = (2 * delay2) - delay; + ok(delayDelta <= 1, "The delay width is correct"); +}); + +function getDuration(pathEl) { + const pathSegList = pathEl.pathSegList; + // Find the index of starting iterations. + let startingIterationIndex = 0; + const firstPathSeg = pathSegList.getItem(1); + for (let i = 2, n = pathSegList.numberOfItems - 2; i < n; i++) { + // Changing point of the progress acceleration is the time. + const pathSeg = pathSegList.getItem(i); + if (firstPathSeg.y != pathSeg.y) { + startingIterationIndex = i; + break; + } + } + // Find the index of ending iterations. + let endingIterationIndex = 0; + let previousPathSegment = pathSegList.getItem(startingIterationIndex); + for (let i = startingIterationIndex + 1, n = pathSegList.numberOfItems - 2; + i < n; i++) { + // Find forwards fill-mode. + const pathSeg = pathSegList.getItem(i); + if (previousPathSegment.y == pathSeg.y) { + endingIterationIndex = i; + break; + } + previousPathSegment = pathSeg; + } + if (endingIterationIndex) { + // Not forwards fill-mode + endingIterationIndex = pathSegList.numberOfItems - 2; + } + // Return the distance of starting and ending + const startingIterationPathSegment = + pathSegList.getItem(startingIterationIndex); + const endingIterationPathSegment = + pathSegList.getItem(startingIterationIndex); + return endingIterationPathSegment.x - startingIterationPathSegment.x; +} diff --git a/devtools/client/animationinspector/test/browser_animation_timeline_ui.js b/devtools/client/animationinspector/test/browser_animation_timeline_ui.js new file mode 100644 index 000000000..43c148482 --- /dev/null +++ b/devtools/client/animationinspector/test/browser_animation_timeline_ui.js @@ -0,0 +1,43 @@ +/* vim: set ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +requestLongerTimeout(2); + +// Check that the timeline contains the right elements. + +add_task(function* () { + yield addTab(URL_ROOT + "doc_simple_animation.html"); + let {panel} = yield openAnimationInspector(); + + let timeline = panel.animationsTimelineComponent; + let el = timeline.rootWrapperEl; + + ok(el.querySelector(".time-header"), + "The header element is in the DOM of the timeline"); + ok(el.querySelectorAll(".time-header .header-item").length, + "The header has some time graduations"); + + ok(el.querySelector(".animations"), + "The animations container is in the DOM of the timeline"); + is(el.querySelectorAll(".animations .animation").length, + timeline.animations.length, + "The number of animations displayed matches the number of animations"); + + for (let i = 0; i < timeline.animations.length; i++) { + let animation = timeline.animations[i]; + let animationEl = el.querySelectorAll(".animations .animation")[i]; + + ok(animationEl.querySelector(".target"), + "The animated node target element is in the DOM"); + ok(animationEl.querySelector(".time-block"), + "The timeline element is in the DOM"); + is(animationEl.querySelector(".name").textContent, + animation.state.name, + "The name on the timeline is correct"); + ok(animationEl.querySelector("svg path"), + "The timeline has svg and path element as summary graph"); + } +}); diff --git a/devtools/client/animationinspector/test/browser_animation_toggle_button_resets_on_navigate.js b/devtools/client/animationinspector/test/browser_animation_toggle_button_resets_on_navigate.js new file mode 100644 index 000000000..d9a92b905 --- /dev/null +++ b/devtools/client/animationinspector/test/browser_animation_toggle_button_resets_on_navigate.js @@ -0,0 +1,31 @@ +/* vim: set ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +requestLongerTimeout(2); + +// Test that a page navigation resets the state of the global toggle button. + +add_task(function* () { + yield addTab(URL_ROOT + "doc_simple_animation.html"); + let {inspector, panel} = yield openAnimationInspector(); + + info("Select the non-animated test node"); + yield selectNodeAndWaitForAnimations(".still", inspector); + + ok(!panel.toggleAllButtonEl.classList.contains("paused"), + "The toggle button is in its running state by default"); + + info("Toggle all animations, so that they pause"); + yield panel.toggleAll(); + ok(panel.toggleAllButtonEl.classList.contains("paused"), + "The toggle button now is in its paused state"); + + info("Reloading the page"); + yield reloadTab(inspector); + + ok(!panel.toggleAllButtonEl.classList.contains("paused"), + "The toggle button is back in its running state"); +}); diff --git a/devtools/client/animationinspector/test/browser_animation_toggle_button_toggles_animations.js b/devtools/client/animationinspector/test/browser_animation_toggle_button_toggles_animations.js new file mode 100644 index 000000000..4d55e0433 --- /dev/null +++ b/devtools/client/animationinspector/test/browser_animation_toggle_button_toggles_animations.js @@ -0,0 +1,32 @@ +/* vim: set ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +requestLongerTimeout(2); + +// Test that the main toggle button actually toggles animations. +// This test doesn't need to be extra careful about checking that *all* +// animations have been paused (including inside iframes) because there's an +// actor test in /devtools/server/tests/browser/ that does this. + +add_task(function* () { + yield addTab(URL_ROOT + "doc_simple_animation.html"); + let {panel} = yield openAnimationInspector(); + + info("Click the toggle button"); + yield panel.toggleAll(); + yield checkState("paused"); + + info("Click again the toggle button"); + yield panel.toggleAll(); + yield checkState("running"); +}); + +function* checkState(state) { + for (let selector of [".animated", ".multi", ".long"]) { + let playState = yield getAnimationPlayerState(selector); + is(playState, state, "The animation on node " + selector + " is " + state); + } +} diff --git a/devtools/client/animationinspector/test/browser_animation_toolbar_exists.js b/devtools/client/animationinspector/test/browser_animation_toolbar_exists.js new file mode 100644 index 000000000..aa8b69e02 --- /dev/null +++ b/devtools/client/animationinspector/test/browser_animation_toolbar_exists.js @@ -0,0 +1,36 @@ +/* vim: set ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +requestLongerTimeout(2); + +// Test that the animation panel has a top toolbar that contains the play/pause +// button and that is displayed at all times. +// Also test that this toolbar gets replaced by the timeline toolbar when there +// are animations to be displayed. + +add_task(function* () { + yield addTab(URL_ROOT + "doc_simple_animation.html"); + let {inspector, window} = yield openAnimationInspector(); + let doc = window.document; + let toolbar = doc.querySelector("#global-toolbar"); + + ok(toolbar, "The panel contains the toolbar element with the new UI"); + ok(!isNodeVisible(toolbar), + "The toolbar is hidden while there are animations"); + + let timelineToolbar = doc.querySelector("#timeline-toolbar"); + ok(timelineToolbar, "The panel contains a timeline toolbar element"); + ok(isNodeVisible(timelineToolbar), + "The timeline toolbar is visible when there are animations"); + + info("Select a node that has no animations"); + yield selectNodeAndWaitForAnimations(".still", inspector); + + ok(isNodeVisible(toolbar), + "The toolbar is shown when there are no animations"); + ok(!isNodeVisible(timelineToolbar), + "The timeline toolbar is hidden when there are no animations"); +}); diff --git a/devtools/client/animationinspector/test/browser_animation_ui_updates_when_animation_data_changes.js b/devtools/client/animationinspector/test/browser_animation_ui_updates_when_animation_data_changes.js new file mode 100644 index 000000000..aa71fd9af --- /dev/null +++ b/devtools/client/animationinspector/test/browser_animation_ui_updates_when_animation_data_changes.js @@ -0,0 +1,53 @@ +/* vim: set ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +requestLongerTimeout(2); + +// Verify that if the animation's duration, iterations or delay change in +// content, then the widget reflects the changes. + +add_task(function* () { + yield addTab(URL_ROOT + "doc_simple_animation.html"); + let {panel, controller, inspector} = yield openAnimationInspector(); + + info("Select the test node"); + yield selectNodeAndWaitForAnimations(".animated", inspector); + + let animation = controller.animationPlayers[0]; + yield setStyle(animation, panel, "animationDuration", "5.5s"); + yield setStyle(animation, panel, "animationIterationCount", "300"); + yield setStyle(animation, panel, "animationDelay", "45s"); + + let animationsEl = panel.animationsTimelineComponent.animationsEl; + let timeBlockEl = animationsEl.querySelector(".time-block"); + + // 45s delay + (300 * 5.5)s duration + let expectedTotalDuration = 1695 * 1000; + + // XXX: the nb and size of each iteration cannot be tested easily (displayed + // using a linear-gradient background and capped at 2px wide). They should + // be tested in bug 1173761. + let delayWidth = parseFloat(timeBlockEl.querySelector(".delay").style.width); + is(Math.round(delayWidth * expectedTotalDuration / 100), 45 * 1000, + "The timeline has the right delay"); +}); + +function* setStyle(animation, panel, name, value) { + info("Change the animation style via the content DOM. Setting " + + name + " to " + value); + + let onAnimationChanged = once(animation, "changed"); + yield executeInContent("devtools:test:setStyle", { + selector: ".animated", + propertyName: name, + propertyValue: value + }); + yield onAnimationChanged; + + // Also wait for the target node previews to be loaded if the panel got + // refreshed as a result of this animation mutation. + yield waitForAllAnimationTargets(panel); +} diff --git a/devtools/client/animationinspector/test/doc_body_animation.html b/devtools/client/animationinspector/test/doc_body_animation.html new file mode 100644 index 000000000..3813ea09c --- /dev/null +++ b/devtools/client/animationinspector/test/doc_body_animation.html @@ -0,0 +1,23 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <style> + body { + background-color: white; + color: black; + animation: change-background-color 3s infinite alternate; + } + + @keyframes change-background-color { + to { + background-color: black; + color: white; + } + } + </style> +</head> +<body> + <h1>Animated body element</h1> +</body> +</html> diff --git a/devtools/client/animationinspector/test/doc_end_delay.html b/devtools/client/animationinspector/test/doc_end_delay.html new file mode 100644 index 000000000..02018bc8a --- /dev/null +++ b/devtools/client/animationinspector/test/doc_end_delay.html @@ -0,0 +1,69 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <style> + .target { + width: 50px; + height: 50px; + background: blue; + } + </style> +</head> +<body> + <div id="target1" class="target"></div> + <div id="target2" class="target"></div> + <div id="target3" class="target"></div> + <div id="target4" class="target"></div> + <script> + /* globals KeyframeEffect, Animation */ + "use strict"; + + let animations = [{ + id: "target1", + frames: [{ opacity: 0, offset: 0 }, { opacity: 1, offset: 1 }], + timing: { + id: "endDelay_animation1", + duration: 1000000, + endDelay: 500000, + fill: "none" + } + }, { + id: "target2", + frames: [{ opacity: 0, offset: 0 }, { opacity: 1, offset: 1 }], + timing: { + id: "endDelay_animation2", + duration: 1000000, + endDelay: -500000, + fill: "none" + } + }, { + id: "target3", + frames: [{ opacity: 0, offset: 0 }, { opacity: 1, offset: 1 }], + timing: { + id: "endDelay_animation3", + duration: 1000000, + endDelay: -1500000, + fill: "forwards" + } + }, { + id: "target4", + frames: [{ opacity: 0, offset: 0 }, { opacity: 1, offset: 1 }], + timing: { + id: "endDelay_animation4", + duration: 100000, + delay: 100000, + endDelay: -1500000, + fill: "forwards" + } + }]; + + for (let {id, frames, timing} of animations) { + let effect = new KeyframeEffect(document.getElementById(id), + frames, timing); + let animation = new Animation(effect, document.timeline); + animation.play(); + } + </script> +</body> +</html> diff --git a/devtools/client/animationinspector/test/doc_frame_script.js b/devtools/client/animationinspector/test/doc_frame_script.js new file mode 100644 index 000000000..6846c9b29 --- /dev/null +++ b/devtools/client/animationinspector/test/doc_frame_script.js @@ -0,0 +1,122 @@ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +/* globals addMessageListener, sendAsyncMessage */ + +"use strict"; + +// A helper frame-script for brower/devtools/animationinspector tests. + +/** + * Toggle (play or pause) one of the animation players of a given node. + * @param {Object} data + * - {String} selector The CSS selector to get the node (can be a "super" + * selector). + * - {Number} animationIndex The index of the node's animationPlayers to play + * or pause + * - {Boolean} pause True to pause the animation, false to play. + */ +addMessageListener("Test:ToggleAnimationPlayer", function (msg) { + let {selector, animationIndex, pause} = msg.data; + let node = superQuerySelector(selector); + if (!node) { + return; + } + + let animation = node.getAnimations()[animationIndex]; + if (pause) { + animation.pause(); + } else { + animation.play(); + } + + sendAsyncMessage("Test:ToggleAnimationPlayer"); +}); + +/** + * Change the currentTime of one of the animation players of a given node. + * @param {Object} data + * - {String} selector The CSS selector to get the node (can be a "super" + * selector). + * - {Number} animationIndex The index of the node's animationPlayers to change. + * - {Number} currentTime The current time to set. + */ +addMessageListener("Test:SetAnimationPlayerCurrentTime", function (msg) { + let {selector, animationIndex, currentTime} = msg.data; + let node = superQuerySelector(selector); + if (!node) { + return; + } + + let animation = node.getAnimations()[animationIndex]; + animation.currentTime = currentTime; + + sendAsyncMessage("Test:SetAnimationPlayerCurrentTime"); +}); + +/** + * Change the playbackRate of one of the animation players of a given node. + * @param {Object} data + * - {String} selector The CSS selector to get the node (can be a "super" + * selector). + * - {Number} animationIndex The index of the node's animationPlayers to change. + * - {Number} playbackRate The rate to set. + */ +addMessageListener("Test:SetAnimationPlayerPlaybackRate", function (msg) { + let {selector, animationIndex, playbackRate} = msg.data; + let node = superQuerySelector(selector); + if (!node) { + return; + } + + let player = node.getAnimations()[animationIndex]; + player.playbackRate = playbackRate; + + sendAsyncMessage("Test:SetAnimationPlayerPlaybackRate"); +}); + +/** + * Get the current playState of an animation player on a given node. + * @param {Object} data + * - {String} selector The CSS selector to get the node (can be a "super" + * selector). + * - {Number} animationIndex The index of the node's animationPlayers to check + */ +addMessageListener("Test:GetAnimationPlayerState", function (msg) { + let {selector, animationIndex} = msg.data; + let node = superQuerySelector(selector); + if (!node) { + return; + } + + let animation = node.getAnimations()[animationIndex]; + animation.ready.then(() => { + sendAsyncMessage("Test:GetAnimationPlayerState", animation.playState); + }); +}); + +/** + * Like document.querySelector but can go into iframes too. + * ".container iframe || .sub-container div" will first try to find the node + * matched by ".container iframe" in the root document, then try to get the + * content document inside it, and then try to match ".sub-container div" inside + * this document. + * Any selector coming before the || separator *MUST* match a frame node. + * @param {String} superSelector. + * @return {DOMNode} The node, or null if not found. + */ +function superQuerySelector(superSelector, root = content.document) { + let frameIndex = superSelector.indexOf("||"); + if (frameIndex === -1) { + return root.querySelector(superSelector); + } + + let rootSelector = superSelector.substring(0, frameIndex).trim(); + let childSelector = superSelector.substring(frameIndex + 2).trim(); + root = root.querySelector(rootSelector); + if (!root || !root.contentWindow) { + return null; + } + + return superQuerySelector(childSelector, root.contentWindow.document); +} diff --git a/devtools/client/animationinspector/test/doc_keyframes.html b/devtools/client/animationinspector/test/doc_keyframes.html new file mode 100644 index 000000000..7671e09e3 --- /dev/null +++ b/devtools/client/animationinspector/test/doc_keyframes.html @@ -0,0 +1,55 @@ +<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+ <title>Yay! Keyframes!</title>
+ <style>
+ div {
+ animation: wow 100s forwards;
+ }
+ @keyframes wow {
+ 0% {
+ width: 100px;
+ height: 100px;
+ border-radius: 0px;
+ background: #f06;
+ }
+ 10% {
+ border-radius: 2px;
+ }
+ 20% {
+ transform: rotate(13deg);
+ }
+ 30% {
+ background: gold;
+ }
+ 40% {
+ filter: blur(40px);
+ }
+ 50% {
+ transform: rotate(720deg) translateX(300px) skew(-13deg);
+ }
+ 60% {
+ width: 200px;
+ height: 200px;
+ }
+ 70% {
+ border-radius: 10px;
+ }
+ 80% {
+ background: #333;
+ }
+ 90% {
+ border-radius: 50%;
+ }
+ 100% {
+ width: 500px;
+ height: 500px;
+ }
+ }
+ </style>
+</head>
+<body>
+ <div></div>
+</body>
+</html>
diff --git a/devtools/client/animationinspector/test/doc_modify_playbackRate.html b/devtools/client/animationinspector/test/doc_modify_playbackRate.html new file mode 100644 index 000000000..7b83f1c38 --- /dev/null +++ b/devtools/client/animationinspector/test/doc_modify_playbackRate.html @@ -0,0 +1,32 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <style> + div { + width: 50px; + height: 50px; + background: blue; + animation: move 20s 20s linear; + animation-fill-mode: forwards; + } + + @keyframes move { + to { + margin-left: 200px; + } + } + </style> +</head> +<body> + <div></div> + <div class="rate"></div> + <script> + "use strict"; + + var el = document.querySelector(".rate"); + var ani = el.getAnimations()[0]; + ani.playbackRate = 2; + </script> +</body> +</html> diff --git a/devtools/client/animationinspector/test/doc_multiple_animation_types.html b/devtools/client/animationinspector/test/doc_multiple_animation_types.html new file mode 100644 index 000000000..318f14d0a --- /dev/null +++ b/devtools/client/animationinspector/test/doc_multiple_animation_types.html @@ -0,0 +1,61 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <style> + .ball { + width: 80px; + height: 80px; + border-radius: 50%; + } + + .script-animation { + background: #f06; + } + + .css-transition { + background: #006; + transition: background-color 20s; + } + + .css-animation { + background: #a06; + animation: flash 10s forwards; + } + + @keyframes flash { + 0% { + opacity: 1; + } + 50% { + opacity: 0; + } + 100% { + opacity: 1; + } + } + </style> +</head> +<body> + <div class="ball script-animation"></div> + <div class="ball css-animation"></div> + <div class="ball css-transition"></div> + + <script> + /* globals KeyframeEffect, Animation */ + "use strict"; + + setTimeout(function () { + document.querySelector(".css-transition").style.backgroundColor = "yellow"; + }, 0); + + let effect = new KeyframeEffect( + document.querySelector(".script-animation"), [ + {opacity: 1, offset: 0}, + {opacity: .1, offset: 1} + ], { duration: 10000, fill: "forwards" }); + let animation = new Animation(effect, document.timeline); + animation.play(); + </script> +</body> +</html> diff --git a/devtools/client/animationinspector/test/doc_negative_animation.html b/devtools/client/animationinspector/test/doc_negative_animation.html new file mode 100644 index 000000000..ea412025b --- /dev/null +++ b/devtools/client/animationinspector/test/doc_negative_animation.html @@ -0,0 +1,66 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <style> + html, body { + margin: 0; + height: 100%; + overflow: hidden; + } + + div { + position: absolute; + top: 0; + left: -500px; + height: 20px; + width: 500px; + color: red; + background: linear-gradient(to left, currentColor, currentColor 2px, transparent); + } + + .zero { + color: blue; + top: 20px; + } + + .positive { + color: green; + top: 40px; + } + + .negative.move { animation: 5s -1s move linear forwards; } + .zero.move { animation: 5s 0s move linear forwards; } + .positive.move { animation: 5s 1s move linear forwards; } + + @keyframes move { + to { + transform: translateX(500px); + } + } + </style> +</head> +<body> + <div class="negative"></div> + <div class="zero"></div> + <div class="positive"></div> + <script> + "use strict"; + + var negative = document.querySelector(".negative"); + var zero = document.querySelector(".zero"); + var positive = document.querySelector(".positive"); + + // The non-delayed animation starts now. + zero.classList.add("move"); + // The negative-delayed animation starts in 1 second. + setTimeout(function () { + negative.classList.add("move"); + }, 1000); + // The positive-delayed animation starts in 200 ms. + setTimeout(function () { + positive.classList.add("move"); + }, 200); + </script> +</body> +</html> diff --git a/devtools/client/animationinspector/test/doc_pseudo_elements.html b/devtools/client/animationinspector/test/doc_pseudo_elements.html new file mode 100644 index 000000000..587608b19 --- /dev/null +++ b/devtools/client/animationinspector/test/doc_pseudo_elements.html @@ -0,0 +1,61 @@ +<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="UTF-8">
+ <title>Animated pseudo elements</title>
+ <style>
+ html, body {
+ margin: 0;
+ height: 100%;
+ width: 100%;
+ overflow: hidden;
+ display: flex;
+ justify-content: center;
+ align-items: flex-end;
+ }
+
+ body {
+ animation: color 2s linear infinite;
+ background: #333;
+ }
+
+ @keyframes color {
+ to {
+ filter: hue-rotate(360deg);
+ }
+ }
+
+ body::before,
+ body::after {
+ content: "";
+ flex-grow: 1;
+ height: 100%;
+ animation: grow 1s linear infinite alternate;
+ }
+
+ body::before {
+ background: hsl(120, 80%, 80%);
+ }
+ body::after {
+ background: hsl(240, 80%, 80%);
+ animation-delay: -.5s;
+ }
+
+ @keyframes grow {
+ 0% {height: 100%; animation-timing-function: ease-in-out;}
+ 10% {height: 80%; animation-timing-function: ease-in-out;}
+ 20% {height: 60%; animation-timing-function: ease-in-out;}
+ 30% {height: 70%; animation-timing-function: ease-in-out;}
+ 40% {height: 50%; animation-timing-function: ease-in-out;}
+ 50% {height: 30%; animation-timing-function: ease-in-out;}
+ 60% {height: 80%; animation-timing-function: ease-in-out;}
+ 70% {height: 90%; animation-timing-function: ease-in-out;}
+ 80% {height: 70%; animation-timing-function: ease-in-out;}
+ 90% {height: 60%; animation-timing-function: ease-in-out;}
+ 100% {height: 100%; animation-timing-function: ease-in-out;}
+ }
+ </style>
+ </head>
+ <body>
+ </body>
+</html>
\ No newline at end of file diff --git a/devtools/client/animationinspector/test/doc_script_animation.html b/devtools/client/animationinspector/test/doc_script_animation.html new file mode 100644 index 000000000..b7839622e --- /dev/null +++ b/devtools/client/animationinspector/test/doc_script_animation.html @@ -0,0 +1,71 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <style> + #target1 { + width: 50px; + height: 50px; + background: red; + } + + #target2 { + width: 50px; + height: 50px; + background: green; + } + + #target3 { + width: 50px; + height: 50px; + background: blue; + } + </style> +</head> +<body> + <div id="target1"></div> + <div id="target2"></div> + <div id="target3"></div> + + <script> + /* globals KeyframeEffect, Animation */ + "use strict"; + + let animations = [{ + id: "target1", + frames: [{ opacity: 0, offset: 0 }, { opacity: 1, offset: 1 }], + timing: { + duration: 100, + iterations: 2, + iterationStart: 0.25, + fill: "both" + } + }, { + id: "target2", + frames: [{ opacity: 0, offset: 0 }, { opacity: 1, offset: 1 }], + timing: { + duration: 100, + iterations: 1, + iterationStart: 0.25, + fill: "both" + } + }, { + id: "target3", + frames: [{ opacity: 0, offset: 0 }, { opacity: 1, offset: 1 }], + timing: { + duration: 100, + iterations: 1.5, + iterationStart: 2.5, + fill: "both" + } + }]; + + for (let {id, frames, timing} of animations) { + let effect = new KeyframeEffect(document.getElementById(id), + frames, timing); + let animation = new Animation(effect, document.timeline); + animation.play(); + } + </script> +</body> +</html> diff --git a/devtools/client/animationinspector/test/doc_simple_animation.html b/devtools/client/animationinspector/test/doc_simple_animation.html new file mode 100644 index 000000000..fc65a5744 --- /dev/null +++ b/devtools/client/animationinspector/test/doc_simple_animation.html @@ -0,0 +1,147 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <style> + .ball { + width: 80px; + height: 80px; + 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; + } + + .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-notall { + animation: compositor-notall 2s infinite; + } + + @keyframes simple-animation { + 100% { + transform: translateX(300px); + } + } + + @keyframes other-animation { + 100% { + background: blue; + } + } + + @keyframes no-compositor { + 100% { + margin-right: 600px; + } + } + + @keyframes compositor-notall { + from { + opacity: 0; + width: 0px; + transform: translate(0px); + } + to { + opacity: 1; + width: 100px; + transform: translate(100px); + } + } + </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" id="endDelayed"></div> + <div class="ball compositor-notall"></div> + <script> + /* globals KeyframeEffect, Animation */ + "use strict"; + + var el = document.getElementById("endDelayed"); + let effect = new KeyframeEffect(el, [ + { opacity: 0, offset: 0 }, + { opacity: 1, offset: 1 } + ], { duration: 1000000, endDelay: 500000, fill: "none" }); + let animation = new Animation(effect, document.timeline); + animation.play(); + </script> +</body> +</html> diff --git a/devtools/client/animationinspector/test/doc_timing_combination_animation.html b/devtools/client/animationinspector/test/doc_timing_combination_animation.html new file mode 100644 index 000000000..8b39af015 --- /dev/null +++ b/devtools/client/animationinspector/test/doc_timing_combination_animation.html @@ -0,0 +1,35 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="UTF-8"> + <style> + div { + display: inline-block; + width: 100px; + height: 100px; + background-color: lime; + } + </style> + </head> + <body> + <script> + "use strict"; + + const delayList = [0, 50000, -50000]; + const endDelayList = [0, 50000, -50000]; + + delayList.forEach(delay => { + endDelayList.forEach(endDelay => { + const el = document.createElement("div"); + document.body.appendChild(el); + el.animate({ opacity: [0, 1] }, + { duration: 200000, + iterations: 1, + fill: "both", + delay: delay, + endDelay: endDelay }); + }); + }); + </script> + </body> +</html> diff --git a/devtools/client/animationinspector/test/head.js b/devtools/client/animationinspector/test/head.js new file mode 100644 index 000000000..554a36430 --- /dev/null +++ b/devtools/client/animationinspector/test/head.js @@ -0,0 +1,426 @@ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* 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-globals-from ../../inspector/test/head.js */ +// 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 FRAME_SCRIPT_URL = CHROME_URL_ROOT + "doc_frame_script.js"; +const COMMON_FRAME_SCRIPT_URL = "chrome://devtools/content/shared/frame-script-utils.js"; +const TAB_NAME = "animationinspector"; +const ANIMATION_L10N = + new LocalizationHelper("devtools/client/locales/animationinspector.properties"); + +// Auto clean-up when a test ends +registerCleanupFunction(function* () { + yield closeAnimationInspector(); + + while (gBrowser.tabs.length > 1) { + gBrowser.removeCurrentTab(); + } +}); + +// 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.debugger.log"); +}); + +// WebAnimations API is not enabled by default in all release channels yet, see +// Bug 1264101. +function enableWebAnimationsAPI() { + return new Promise(resolve => { + SpecialPowers.pushPrefEnv({"set": [ + ["dom.animations-api.core.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 + */ +var _addTab = addTab; +addTab = function (url) { + return enableWebAnimationsAPI().then(() => _addTab(url)).then(tab => { + let browser = tab.linkedBrowser; + info("Loading the helper frame script " + FRAME_SCRIPT_URL); + browser.messageManager.loadFrameScript(FRAME_SCRIPT_URL, false); + info("Loading the helper frame script " + COMMON_FRAME_SCRIPT_URL); + browser.messageManager.loadFrameScript(COMMON_FRAME_SCRIPT_URL, false); + return tab; + }); +}; + +/** + * Reload the current tab location. + * @param {InspectorPanel} inspector The instance of InspectorPanel currently + * loaded in the toolbox + */ +function* reloadTab(inspector) { + let onNewRoot = inspector.once("new-root"); + yield executeInContent("devtools:test:reload", {}, {}, false); + yield onNewRoot; + yield inspector.once("inspector-updated"); +} + +/* + * Set the inspector's current selection to a node or to the first match of the + * given css selector and wait for the animations to be displayed + * @param {String|NodeFront} + * data The node to select + * @param {InspectorPanel} inspector + * The instance of InspectorPanel currently + * loaded in the toolbox + * @param {String} reason + * Defaults to "test" which instructs the inspector not + * to highlight the node upon selection + * @return {Promise} Resolves when the inspector is updated with the new node + and animations of its subtree are properly displayed. + */ +var selectNodeAndWaitForAnimations = Task.async( + function* (data, inspector, reason = "test") { + yield selectNode(data, inspector, reason); + + // We want to make sure the rest of the test waits for the animations to + // be properly displayed (wait for all target DOM nodes to be previewed). + let {AnimationsPanel} = inspector.sidebar.getWindowForTab(TAB_NAME); + yield waitForAllAnimationTargets(AnimationsPanel); + } +); + +/** + * Check if there are the expected number of animations being displayed in the + * panel right now. + * @param {AnimationsPanel} panel + * @param {Number} nbAnimations The expected number of animations. + * @param {String} msg An optional string to be used as the assertion message. + */ +function assertAnimationsDisplayed(panel, nbAnimations, msg = "") { + msg = msg || `There are ${nbAnimations} animations in the panel`; + is(panel.animationsTimelineComponent + .animationsEl + .querySelectorAll(".animation").length, nbAnimations, msg); +} + +/** + * Takes an Inspector panel that was just created, and waits + * for a "inspector-updated" event as well as the animation inspector + * sidebar to be ready. Returns a promise once these are completed. + * + * @param {InspectorPanel} inspector + * @return {Promise} + */ +var waitForAnimationInspectorReady = Task.async(function* (inspector) { + let win = inspector.sidebar.getWindowForTab(TAB_NAME); + let updated = inspector.once("inspector-updated"); + + // In e10s, if we wait for underlying toolbox actors to + // load (by setting DevToolsUtils.testing to true), we miss the + // "animationinspector-ready" event on the sidebar, so check to see if the + // iframe is already loaded. + let tabReady = win.document.readyState === "complete" ? + promise.resolve() : + inspector.sidebar.once("animationinspector-ready"); + + return promise.all([updated, tabReady]); +}); + +/** + * Open the toolbox, with the inspector tool visible and the animationinspector + * sidebar selected. + * @return a promise that resolves when the inspector is ready. + */ +var openAnimationInspector = Task.async(function* () { + let {inspector, toolbox} = yield openInspectorSidebarTab(TAB_NAME); + + info("Waiting for the inspector and sidebar to be ready"); + yield waitForAnimationInspectorReady(inspector); + + let win = inspector.sidebar.getWindowForTab(TAB_NAME); + let {AnimationsController, AnimationsPanel} = win; + + info("Waiting for the animation controller and panel to be ready"); + if (AnimationsPanel.initialized) { + yield AnimationsPanel.initialized; + } else { + yield AnimationsPanel.once(AnimationsPanel.PANEL_INITIALIZED); + } + + // Make sure we wait for all animations to be loaded (especially their target + // nodes to be lazily displayed). This is safe to do even if there are no + // animations displayed. + yield waitForAllAnimationTargets(AnimationsPanel); + + return { + toolbox: toolbox, + inspector: inspector, + controller: AnimationsController, + panel: AnimationsPanel, + window: win + }; +}); + +/** + * Close the toolbox. + * @return a promise that resolves when the toolbox has closed. + */ +var closeAnimationInspector = Task.async(function* () { + let target = TargetFactory.forTab(gBrowser.selectedTab); + yield gDevTools.closeToolbox(target); +}); + +/** + * Wait for a content -> chrome message on the message manager (the window + * messagemanager is used). + * @param {String} name The message name + * @return {Promise} A promise that resolves to the response data when the + * message has been received + */ +function waitForContentMessage(name) { + info("Expecting message " + name + " from content"); + + let mm = gBrowser.selectedBrowser.messageManager; + + return new Promise(resolve => { + mm.addMessageListener(name, function onMessage(msg) { + mm.removeMessageListener(name, onMessage); + resolve(msg.data); + }); + }); +} + +/** + * Send an async message to the frame script (chrome -> content) and wait for a + * response message with the same name (content -> chrome). + * @param {String} name The message name. Should be one of the messages defined + * in doc_frame_script.js + * @param {Object} data Optional data to send along + * @param {Object} objects Optional CPOW objects to send along + * @param {Boolean} expectResponse If set to false, don't wait for a response + * with the same name from the content script. Defaults to true. + * @return {Promise} Resolves to the response data if a response is expected, + * immediately resolves otherwise + */ +function executeInContent(name, data = {}, objects = {}, + expectResponse = true) { + info("Sending message " + name + " to content"); + let mm = gBrowser.selectedBrowser.messageManager; + + mm.sendAsyncMessage(name, data, objects); + if (expectResponse) { + return waitForContentMessage(name); + } + + return promise.resolve(); +} + +/** + * Get the current playState of an animation player on a given node. + */ +var getAnimationPlayerState = Task.async(function* (selector, + animationIndex = 0) { + let playState = yield executeInContent("Test:GetAnimationPlayerState", + {selector, animationIndex}); + return playState; +}); + +/** + * Is the given node visible in the page (rendered in the frame tree). + * @param {DOMNode} + * @return {Boolean} + */ +function isNodeVisible(node) { + return !!node.getClientRects().length; +} + +/** + * Wait for all AnimationTargetNode instances to be fully loaded + * (fetched their related actor and rendered), and return them. + * @param {AnimationsPanel} panel + * @return {Array} all AnimationTargetNode instances + */ +var waitForAllAnimationTargets = Task.async(function* (panel) { + let targets = panel.animationsTimelineComponent.targetNodes; + yield promise.all(targets.map(t => { + if (!t.previewer.nodeFront) { + return t.once("target-retrieved"); + } + return false; + })); + return targets; +}); + +/** + * Check the scrubber element in the timeline is moving. + * @param {AnimationPanel} panel + * @param {Boolean} isMoving + */ +function* assertScrubberMoving(panel, isMoving) { + let timeline = panel.animationsTimelineComponent; + + if (isMoving) { + // If we expect the scrubber to move, just wait for a couple of + // timeline-data-changed events and compare times. + let {time: time1} = yield timeline.once("timeline-data-changed"); + let {time: time2} = yield timeline.once("timeline-data-changed"); + ok(time2 > time1, "The scrubber is moving"); + } else { + // If instead we expect the scrubber to remain at its position, just wait + // for some time and make sure timeline-data-changed isn't emitted. + let hasMoved = false; + timeline.once("timeline-data-changed", () => { + hasMoved = true; + }); + yield new Promise(r => setTimeout(r, 500)); + ok(!hasMoved, "The scrubber is not moving"); + } +} + +/** + * Click the play/pause button in the timeline toolbar and wait for animations + * to update. + * @param {AnimationsPanel} panel + */ +function* clickTimelinePlayPauseButton(panel) { + let onUiUpdated = panel.once(panel.UI_UPDATED_EVENT); + + let btn = panel.playTimelineButtonEl; + let win = btn.ownerDocument.defaultView; + EventUtils.sendMouseEvent({type: "click"}, btn, win); + + yield onUiUpdated; + yield waitForAllAnimationTargets(panel); +} + +/** + * Click the rewind button in the timeline toolbar and wait for animations to + * update. + * @param {AnimationsPanel} panel + */ +function* clickTimelineRewindButton(panel) { + let onUiUpdated = panel.once(panel.UI_UPDATED_EVENT); + + let btn = panel.rewindTimelineButtonEl; + let win = btn.ownerDocument.defaultView; + EventUtils.sendMouseEvent({type: "click"}, btn, win); + + yield onUiUpdated; + yield waitForAllAnimationTargets(panel); +} + +/** + * Select a rate inside the playback rate selector in the timeline toolbar and + * wait for animations to update. + * @param {AnimationsPanel} panel + * @param {Number} rate The new rate value to be selected + */ +function* changeTimelinePlaybackRate(panel, rate) { + let onUiUpdated = panel.once(panel.UI_UPDATED_EVENT); + + let select = panel.rateSelectorEl.firstChild; + let win = select.ownerDocument.defaultView; + + // Get the right option. + let option = [...select.options].filter(o => o.value === rate + "")[0]; + if (!option) { + ok(false, + "Could not find an option for rate " + rate + " in the rate selector. " + + "Values are: " + [...select.options].map(o => o.value)); + return; + } + + // Simulate the right events to select the option in the drop-down. + EventUtils.synthesizeMouseAtCenter(select, {type: "mousedown"}, win); + EventUtils.synthesizeMouseAtCenter(option, {type: "mouseup"}, win); + + yield onUiUpdated; + yield waitForAllAnimationTargets(panel); + + // Simulate a mousemove outside of the rate selector area to avoid subsequent + // tests from failing because of unwanted mouseover events. + EventUtils.synthesizeMouseAtCenter( + win.document.querySelector("#timeline-toolbar"), {type: "mousemove"}, win); +} + +/** + * Prevent the toolbox common highlighter from making backend requests. + * @param {Toolbox} toolbox + */ +function disableHighlighter(toolbox) { + toolbox._highlighter = { + showBoxModel: () => new Promise(r => r()), + hideBoxModel: () => new Promise(r => r()), + pick: () => new Promise(r => r()), + cancelPick: () => new Promise(r => r()), + destroy: () => {}, + traits: {} + }; +} + +/** + * Click on an animation in the timeline to select/unselect it. + * @param {AnimationsPanel} panel The panel instance. + * @param {Number} index The index of the animation to click on. + * @param {Boolean} shouldClose Set to true if clicking should close the + * animation. + * @return {Promise} resolves to the animation whose state has changed. + */ +function* clickOnAnimation(panel, index, shouldClose) { + let timeline = panel.animationsTimelineComponent; + + // Expect a selection event. + let onSelectionChanged = timeline.once(shouldClose + ? "animation-unselected" + : "animation-selected"); + + // If we're opening the animation, also wait for the keyframes-retrieved + // event. + let onReady = shouldClose + ? Promise.resolve() + : timeline.details[index].once("keyframes-retrieved"); + + info("Click on animation " + index + " in the timeline"); + let timeBlock = timeline.rootWrapperEl.querySelectorAll(".time-block")[index]; + EventUtils.sendMouseEvent({type: "click"}, timeBlock, + timeBlock.ownerDocument.defaultView); + + yield onReady; + return yield onSelectionChanged; +} + +/** + * Get an instance of the Keyframes component from the timeline. + * @param {AnimationsPanel} panel The panel instance. + * @param {Number} animationIndex The index of the animation in the timeline. + * @param {String} propertyName The name of the animated property. + * @return {Keyframes} The Keyframes component instance. + */ +function getKeyframeComponent(panel, animationIndex, propertyName) { + let timeline = panel.animationsTimelineComponent; + let detailsComponent = timeline.details[animationIndex]; + return detailsComponent.keyframeComponents + .find(c => c.propertyName === propertyName); +} + +/** + * Get a keyframe element from the timeline. + * @param {AnimationsPanel} panel The panel instance. + * @param {Number} animationIndex The index of the animation in the timeline. + * @param {String} propertyName The name of the animated property. + * @param {Index} keyframeIndex The index of the keyframe. + * @return {DOMNode} The keyframe element. + */ +function getKeyframeEl(panel, animationIndex, propertyName, keyframeIndex) { + let keyframeComponent = getKeyframeComponent(panel, animationIndex, + propertyName); + return keyframeComponent.keyframesEl + .querySelectorAll(".frame")[keyframeIndex]; +} diff --git a/devtools/client/animationinspector/test/unit/.eslintrc.js b/devtools/client/animationinspector/test/unit/.eslintrc.js new file mode 100644 index 000000000..59adf410a --- /dev/null +++ b/devtools/client/animationinspector/test/unit/.eslintrc.js @@ -0,0 +1,6 @@ +"use strict"; + +module.exports = { + // Extend from the common devtools xpcshell eslintrc config. + "extends": "../../../../.eslintrc.xpcshell.js" +}; diff --git a/devtools/client/animationinspector/test/unit/test_findOptimalTimeInterval.js b/devtools/client/animationinspector/test/unit/test_findOptimalTimeInterval.js new file mode 100644 index 000000000..64451bfdf --- /dev/null +++ b/devtools/client/animationinspector/test/unit/test_findOptimalTimeInterval.js @@ -0,0 +1,81 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +/* eslint no-eval:0 */ + +"use strict"; + +var Cu = Components.utils; +const {require} = Cu.import("resource://devtools/shared/Loader.jsm", {}); +const {findOptimalTimeInterval} = require("devtools/client/animationinspector/utils"); + +// This test array contains objects that are used to test the +// findOptimalTimeInterval function. Each object should have the following +// properties: +// - desc: an optional string that will be printed out +// - minTimeInterval: a number that represents the minimum time in ms +// that should be displayed in one interval +// - expectedInterval: a number that you expect the findOptimalTimeInterval +// function to return as a result. +// Optionally you can pass a string where `interval` is the calculated +// interval, this string will be eval'd and tested to be truthy. +const TEST_DATA = [{ + desc: "With no minTimeInterval, expect the interval to be 0", + minTimeInterval: null, + expectedInterval: 0 +}, { + desc: "With a minTimeInterval of 0 ms, expect the interval to be 0", + minTimeInterval: 0, + expectedInterval: 0 +}, { + desc: "With a minInterval of 1ms, expect the interval to be the 1ms too", + minTimeInterval: 1, + expectedInterval: 1 +}, { + desc: "With a very small minTimeInterval, expect the interval to be 1ms", + minTimeInterval: 1e-31, + expectedInterval: 1 +}, { + desc: "With a minInterval of 2.5ms, expect the interval to be 2.5ms too", + minTimeInterval: 2.5, + expectedInterval: 2.5 +}, { + desc: "With a minInterval of 5ms, expect the interval to be 5ms too", + minTimeInterval: 5, + expectedInterval: 5 +}, { + desc: "With a minInterval of 7ms, expect the interval to be the next " + + "multiple of 5", + minTimeInterval: 7, + expectedInterval: 10 +}, { + minTimeInterval: 20, + expectedInterval: 25 +}, { + minTimeInterval: 33, + expectedInterval: 50 +}, { + minTimeInterval: 987, + expectedInterval: 1000 +}, { + minTimeInterval: 1234, + expectedInterval: 2500 +}, { + minTimeInterval: 9800, + expectedInterval: 10000 +}]; + +function run_test() { + for (let {minTimeInterval, desc, expectedInterval} of TEST_DATA) { + do_print(`Testing minTimeInterval: ${minTimeInterval}. + Expecting ${expectedInterval}.`); + + let interval = findOptimalTimeInterval(minTimeInterval); + if (typeof expectedInterval == "string") { + ok(eval(expectedInterval), desc); + } else { + equal(interval, expectedInterval, desc); + } + } +} diff --git a/devtools/client/animationinspector/test/unit/test_formatStopwatchTime.js b/devtools/client/animationinspector/test/unit/test_formatStopwatchTime.js new file mode 100644 index 000000000..12584a2a4 --- /dev/null +++ b/devtools/client/animationinspector/test/unit/test_formatStopwatchTime.js @@ -0,0 +1,62 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +var Cu = Components.utils; +const {require} = Cu.import("resource://devtools/shared/Loader.jsm", {}); +const {formatStopwatchTime} = require("devtools/client/animationinspector/utils"); + +const TEST_DATA = [{ + desc: "Formatting 0", + time: 0, + expected: "00:00.000" +}, { + desc: "Formatting null", + time: null, + expected: "00:00.000" +}, { + desc: "Formatting undefined", + time: undefined, + expected: "00:00.000" +}, { + desc: "Formatting a small number of ms", + time: 13, + expected: "00:00.013" +}, { + desc: "Formatting a slightly larger number of ms", + time: 500, + expected: "00:00.500" +}, { + desc: "Formatting 1 second", + time: 1000, + expected: "00:01.000" +}, { + desc: "Formatting a number of seconds", + time: 1532, + expected: "00:01.532" +}, { + desc: "Formatting a big number of seconds", + time: 58450, + expected: "00:58.450" +}, { + desc: "Formatting 1 minute", + time: 60000, + expected: "01:00.000" +}, { + desc: "Formatting a number of minutes", + time: 263567, + expected: "04:23.567" +}, { + desc: "Formatting a large number of minutes", + time: 1000 * 60 * 60 * 3, + expected: "180:00.000" +}]; + +function run_test() { + for (let {desc, time, expected} of TEST_DATA) { + equal(formatStopwatchTime(time), expected, desc); + } +} diff --git a/devtools/client/animationinspector/test/unit/test_getCssPropertyName.js b/devtools/client/animationinspector/test/unit/test_getCssPropertyName.js new file mode 100644 index 000000000..21470d5fb --- /dev/null +++ b/devtools/client/animationinspector/test/unit/test_getCssPropertyName.js @@ -0,0 +1,27 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +var Cu = Components.utils; +const {require} = Cu.import("resource://devtools/shared/Loader.jsm", {}); +const {getCssPropertyName} = require("devtools/client/animationinspector/components/animation-details"); + +const TEST_DATA = [{ + jsName: "alllowercase", + cssName: "alllowercase" +}, { + jsName: "borderWidth", + cssName: "border-width" +}, { + jsName: "borderTopRightRadius", + cssName: "border-top-right-radius" +}]; + +function run_test() { + for (let {jsName, cssName} of TEST_DATA) { + equal(getCssPropertyName(jsName), cssName); + } +} diff --git a/devtools/client/animationinspector/test/unit/test_timeScale.js b/devtools/client/animationinspector/test/unit/test_timeScale.js new file mode 100644 index 000000000..9ee4b8a59 --- /dev/null +++ b/devtools/client/animationinspector/test/unit/test_timeScale.js @@ -0,0 +1,207 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +var Cu = Components.utils; +const {require} = Cu.import("resource://devtools/shared/Loader.jsm", {}); +const {TimeScale} = require("devtools/client/animationinspector/utils"); +const TEST_ANIMATIONS = [{ + desc: "Testing a few standard animations", + animations: [{ + previousStartTime: 500, + delay: 0, + duration: 1000, + iterationCount: 1, + playbackRate: 1 + }, { + previousStartTime: 400, + delay: 100, + duration: 10, + iterationCount: 100, + playbackRate: 1 + }, { + previousStartTime: 50, + delay: 1000, + duration: 100, + iterationCount: 20, + playbackRate: 1 + }], + expectedMinStart: 50, + expectedMaxEnd: 3050 +}, { + desc: "Testing a single negative-delay animation", + animations: [{ + previousStartTime: 100, + delay: -100, + duration: 100, + iterationCount: 1, + playbackRate: 1 + }], + expectedMinStart: 0, + expectedMaxEnd: 100 +}, { + desc: "Testing a single negative-delay animation with a different rate", + animations: [{ + previousStartTime: 3500, + delay: -1000, + duration: 10000, + iterationCount: 2, + playbackRate: 2 + }], + expectedMinStart: 3000, + expectedMaxEnd: 13000 +}]; + +const TEST_STARTTIME_TO_DISTANCE = [{ + time: 50, + expectedDistance: 0 +}, { + time: 50, + expectedDistance: 0 +}, { + time: 3050, + expectedDistance: 100 +}, { + time: 1550, + expectedDistance: 50 +}]; + +const TEST_DURATION_TO_DISTANCE = [{ + time: 3000, + expectedDistance: 100 +}, { + time: 0, + expectedDistance: 0 +}]; + +const TEST_DISTANCE_TO_TIME = [{ + distance: 100, + expectedTime: 3050 +}, { + distance: 0, + expectedTime: 50 +}, { + distance: 25, + expectedTime: 800 +}]; + +const TEST_DISTANCE_TO_RELATIVE_TIME = [{ + distance: 100, + expectedTime: 3000 +}, { + distance: 0, + expectedTime: 0 +}, { + distance: 25, + expectedTime: 750 +}]; + +const TEST_FORMAT_TIME_MS = [{ + time: 0, + expectedFormattedTime: "0ms" +}, { + time: 3540.341, + expectedFormattedTime: "3540ms" +}, { + time: 1.99, + expectedFormattedTime: "2ms" +}, { + time: 4000, + expectedFormattedTime: "4000ms" +}]; + +const TEST_FORMAT_TIME_S = [{ + time: 0, + expectedFormattedTime: "0.0s" +}, { + time: 3540.341, + expectedFormattedTime: "3.5s" +}, { + time: 1.99, + expectedFormattedTime: "0.0s" +}, { + time: 4000, + expectedFormattedTime: "4.0s" +}, { + time: 102540, + expectedFormattedTime: "102.5s" +}, { + time: 102940, + expectedFormattedTime: "102.9s" +}]; + +function run_test() { + do_print("Check the default min/max range values"); + equal(TimeScale.minStartTime, Infinity); + equal(TimeScale.maxEndTime, 0); + + for (let {desc, animations, expectedMinStart, expectedMaxEnd} of + TEST_ANIMATIONS) { + do_print("Test adding a few animations: " + desc); + for (let state of animations) { + TimeScale.addAnimation(state); + } + + do_print("Checking the time scale range"); + equal(TimeScale.minStartTime, expectedMinStart); + equal(TimeScale.maxEndTime, expectedMaxEnd); + + do_print("Test reseting the animations"); + TimeScale.reset(); + equal(TimeScale.minStartTime, Infinity); + equal(TimeScale.maxEndTime, 0); + } + + do_print("Add a set of animations again"); + for (let state of TEST_ANIMATIONS[0].animations) { + TimeScale.addAnimation(state); + } + + do_print("Test converting start times to distances"); + for (let {time, expectedDistance} of TEST_STARTTIME_TO_DISTANCE) { + let distance = TimeScale.startTimeToDistance(time); + equal(distance, expectedDistance); + } + + do_print("Test converting durations to distances"); + for (let {time, expectedDistance} of TEST_DURATION_TO_DISTANCE) { + let distance = TimeScale.durationToDistance(time); + equal(distance, expectedDistance); + } + + do_print("Test converting distances to times"); + for (let {distance, expectedTime} of TEST_DISTANCE_TO_TIME) { + let time = TimeScale.distanceToTime(distance); + equal(time, expectedTime); + } + + do_print("Test converting distances to relative times"); + for (let {distance, expectedTime} of TEST_DISTANCE_TO_RELATIVE_TIME) { + let time = TimeScale.distanceToRelativeTime(distance); + equal(time, expectedTime); + } + + do_print("Test formatting times (millis)"); + for (let {time, expectedFormattedTime} of TEST_FORMAT_TIME_MS) { + let formattedTime = TimeScale.formatTime(time); + equal(formattedTime, expectedFormattedTime); + } + + // Add 1 more animation to increase the range and test more time formatting + // cases. + TimeScale.addAnimation({ + startTime: 3000, + duration: 5000, + delay: 0, + iterationCount: 1 + }); + + do_print("Test formatting times (seconds)"); + for (let {time, expectedFormattedTime} of TEST_FORMAT_TIME_S) { + let formattedTime = TimeScale.formatTime(time); + equal(formattedTime, expectedFormattedTime); + } +} diff --git a/devtools/client/animationinspector/test/unit/test_timeScale_dimensions.js b/devtools/client/animationinspector/test/unit/test_timeScale_dimensions.js new file mode 100644 index 000000000..f6d80e60b --- /dev/null +++ b/devtools/client/animationinspector/test/unit/test_timeScale_dimensions.js @@ -0,0 +1,54 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + https://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const Cu = Components.utils; +const {require} = Cu.import("resource://devtools/shared/Loader.jsm", {}); +const {TimeScale} = require("devtools/client/animationinspector/utils"); + +const TEST_ENDDELAY_X = [{ + desc: "Testing positive-endDelay animations", + animations: [{ + previousStartTime: 0, + duration: 500, + playbackRate: 1, + iterationCount: 3, + delay: 500, + endDelay: 500 + }], + expectedEndDelayX: 80 +}, { + desc: "Testing negative-endDelay animations", + animations: [{ + previousStartTime: 0, + duration: 500, + playbackRate: 1, + iterationCount: 9, + delay: 500, + endDelay: -500 + }], + expectedEndDelayX: 90 +}]; + +function run_test() { + do_print("Test calculating endDelayX"); + + // Be independent of possible prior tests + TimeScale.reset(); + + for (let {desc, animations, expectedEndDelayX} of TEST_ENDDELAY_X) { + do_print(`Adding animations: ${desc}`); + + for (let state of animations) { + TimeScale.addAnimation(state); + + let {endDelayX} = TimeScale.getAnimationDimensions({state}); + equal(endDelayX, expectedEndDelayX); + + TimeScale.reset(); + } + } +} diff --git a/devtools/client/animationinspector/test/unit/xpcshell.ini b/devtools/client/animationinspector/test/unit/xpcshell.ini new file mode 100644 index 000000000..c88e01cf9 --- /dev/null +++ b/devtools/client/animationinspector/test/unit/xpcshell.ini @@ -0,0 +1,12 @@ +[DEFAULT] +tags = devtools +head = +tail = +firefox-appdir = browser +skip-if = toolkit == 'android' + +[test_findOptimalTimeInterval.js] +[test_formatStopwatchTime.js] +[test_getCssPropertyName.js] +[test_timeScale.js] +[test_timeScale_dimensions.js] |