summaryrefslogtreecommitdiffstats
path: root/devtools/client/animationinspector/test
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/client/animationinspector/test')
-rw-r--r--devtools/client/animationinspector/test/.eslintrc.js6
-rw-r--r--devtools/client/animationinspector/test/browser.ini71
-rw-r--r--devtools/client/animationinspector/test/browser_animation_animated_properties_displayed.js91
-rw-r--r--devtools/client/animationinspector/test/browser_animation_click_selects_animation.js44
-rw-r--r--devtools/client/animationinspector/test/browser_animation_controller_exposes_document_currentTime.js43
-rw-r--r--devtools/client/animationinspector/test/browser_animation_empty_on_invalid_nodes.js42
-rw-r--r--devtools/client/animationinspector/test/browser_animation_keyframe_click_to_set_time.js52
-rw-r--r--devtools/client/animationinspector/test/browser_animation_keyframe_markers.js74
-rw-r--r--devtools/client/animationinspector/test/browser_animation_mutations_with_same_names.js31
-rw-r--r--devtools/client/animationinspector/test/browser_animation_panel_exists.js23
-rw-r--r--devtools/client/animationinspector/test/browser_animation_participate_in_inspector_update.js46
-rw-r--r--devtools/client/animationinspector/test/browser_animation_playerFronts_are_refreshed.js36
-rw-r--r--devtools/client/animationinspector/test/browser_animation_playerWidgets_appear_on_panel_init.js41
-rw-r--r--devtools/client/animationinspector/test/browser_animation_playerWidgets_target_nodes.js33
-rw-r--r--devtools/client/animationinspector/test/browser_animation_pseudo_elements.js49
-rw-r--r--devtools/client/animationinspector/test/browser_animation_refresh_on_added_animation.js47
-rw-r--r--devtools/client/animationinspector/test/browser_animation_refresh_on_removed_animation.js50
-rw-r--r--devtools/client/animationinspector/test/browser_animation_refresh_when_active.js53
-rw-r--r--devtools/client/animationinspector/test/browser_animation_running_on_compositor.js57
-rw-r--r--devtools/client/animationinspector/test/browser_animation_same_nb_of_playerWidgets_and_playerFronts.js23
-rw-r--r--devtools/client/animationinspector/test/browser_animation_shows_player_on_valid_node.js21
-rw-r--r--devtools/client/animationinspector/test/browser_animation_spacebar_toggles_animations.js49
-rw-r--r--devtools/client/animationinspector/test/browser_animation_spacebar_toggles_node_animations.js45
-rw-r--r--devtools/client/animationinspector/test/browser_animation_target_highlight_select.js73
-rw-r--r--devtools/client/animationinspector/test/browser_animation_target_highlighter_lock.js54
-rw-r--r--devtools/client/animationinspector/test/browser_animation_timeline_currentTime.js48
-rw-r--r--devtools/client/animationinspector/test/browser_animation_timeline_header.js59
-rw-r--r--devtools/client/animationinspector/test/browser_animation_timeline_iterationStart.js71
-rw-r--r--devtools/client/animationinspector/test/browser_animation_timeline_pause_button_01.js34
-rw-r--r--devtools/client/animationinspector/test/browser_animation_timeline_pause_button_02.js48
-rw-r--r--devtools/client/animationinspector/test/browser_animation_timeline_pause_button_03.js60
-rw-r--r--devtools/client/animationinspector/test/browser_animation_timeline_rate_selector.js56
-rw-r--r--devtools/client/animationinspector/test/browser_animation_timeline_rewind_button.js51
-rw-r--r--devtools/client/animationinspector/test/browser_animation_timeline_scrubber_exists.js20
-rw-r--r--devtools/client/animationinspector/test/browser_animation_timeline_scrubber_movable.js70
-rw-r--r--devtools/client/animationinspector/test/browser_animation_timeline_scrubber_moves.js28
-rw-r--r--devtools/client/animationinspector/test/browser_animation_timeline_setCurrentTime.js88
-rw-r--r--devtools/client/animationinspector/test/browser_animation_timeline_shows_delay.js96
-rw-r--r--devtools/client/animationinspector/test/browser_animation_timeline_shows_endDelay.js78
-rw-r--r--devtools/client/animationinspector/test/browser_animation_timeline_shows_iterations.js47
-rw-r--r--devtools/client/animationinspector/test/browser_animation_timeline_shows_name_label.js46
-rw-r--r--devtools/client/animationinspector/test/browser_animation_timeline_shows_time_info.js50
-rw-r--r--devtools/client/animationinspector/test/browser_animation_timeline_takes_rate_into_account.js81
-rw-r--r--devtools/client/animationinspector/test/browser_animation_timeline_ui.js43
-rw-r--r--devtools/client/animationinspector/test/browser_animation_toggle_button_resets_on_navigate.js31
-rw-r--r--devtools/client/animationinspector/test/browser_animation_toggle_button_toggles_animations.js32
-rw-r--r--devtools/client/animationinspector/test/browser_animation_toolbar_exists.js36
-rw-r--r--devtools/client/animationinspector/test/browser_animation_ui_updates_when_animation_data_changes.js53
-rw-r--r--devtools/client/animationinspector/test/doc_body_animation.html23
-rw-r--r--devtools/client/animationinspector/test/doc_end_delay.html69
-rw-r--r--devtools/client/animationinspector/test/doc_frame_script.js122
-rw-r--r--devtools/client/animationinspector/test/doc_keyframes.html55
-rw-r--r--devtools/client/animationinspector/test/doc_modify_playbackRate.html32
-rw-r--r--devtools/client/animationinspector/test/doc_multiple_animation_types.html61
-rw-r--r--devtools/client/animationinspector/test/doc_negative_animation.html66
-rw-r--r--devtools/client/animationinspector/test/doc_pseudo_elements.html61
-rw-r--r--devtools/client/animationinspector/test/doc_script_animation.html71
-rw-r--r--devtools/client/animationinspector/test/doc_simple_animation.html147
-rw-r--r--devtools/client/animationinspector/test/doc_timing_combination_animation.html35
-rw-r--r--devtools/client/animationinspector/test/head.js426
-rw-r--r--devtools/client/animationinspector/test/unit/.eslintrc.js6
-rw-r--r--devtools/client/animationinspector/test/unit/test_findOptimalTimeInterval.js81
-rw-r--r--devtools/client/animationinspector/test/unit/test_formatStopwatchTime.js62
-rw-r--r--devtools/client/animationinspector/test/unit/test_getCssPropertyName.js27
-rw-r--r--devtools/client/animationinspector/test/unit/test_timeScale.js207
-rw-r--r--devtools/client/animationinspector/test/unit/test_timeScale_dimensions.js54
-rw-r--r--devtools/client/animationinspector/test/unit/xpcshell.ini12
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]