Files
2020-09-24 08:10:23 +00:00

426 lines
15 KiB
JavaScript

/* 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];
}