1009 lines
30 KiB
JavaScript
1009 lines
30 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 the inspector's head.js first (which itself imports shared-head.js).
|
|
Services.scriptloader.loadSubScript(
|
|
"chrome://mochitests/content/browser/devtools/client/inspector/test/head.js",
|
|
this
|
|
);
|
|
|
|
const TAB_NAME = "animationinspector";
|
|
|
|
const ANIMATION_L10N = new LocalizationHelper(
|
|
"devtools/client/locales/animationinspector.properties"
|
|
);
|
|
|
|
// Auto clean-up when a test ends.
|
|
// Clean-up all prefs that might have been changed during a test run
|
|
// (safer here because if the test fails, then the pref is never reverted)
|
|
registerCleanupFunction(() => {
|
|
Services.prefs.clearUserPref("devtools.toolsidebar-width.inspector");
|
|
});
|
|
|
|
/**
|
|
* Open the toolbox, with the inspector tool visible and the animationinspector
|
|
* sidebar selected.
|
|
*
|
|
* @return {Promise} that resolves when the inspector is ready.
|
|
*/
|
|
const openAnimationInspector = async function () {
|
|
const { inspector, toolbox } = await openInspectorSidebarTab(TAB_NAME);
|
|
await inspector.once("inspector-updated");
|
|
const animationInspector = inspector.getPanel("animationinspector");
|
|
const panel = inspector.panelWin.document.getElementById(
|
|
"animation-container"
|
|
);
|
|
|
|
info("Wait for loading first content");
|
|
const count = getDisplayedGraphCount(animationInspector, panel);
|
|
await waitUntil(
|
|
() =>
|
|
panel.querySelectorAll(".animation-summary-graph-path").length >= count &&
|
|
panel.querySelectorAll(".animation-target .objectBox").length >= count
|
|
);
|
|
|
|
if (
|
|
animationInspector.state.selectedAnimation &&
|
|
animationInspector.state.detailVisibility
|
|
) {
|
|
await waitUntil(() => panel.querySelector(".animated-property-list"));
|
|
}
|
|
|
|
return { animationInspector, toolbox, inspector, panel };
|
|
};
|
|
|
|
/**
|
|
* Close the toolbox.
|
|
*
|
|
* @return {Promise} that resolves when the toolbox has closed.
|
|
*/
|
|
const closeAnimationInspector = async function () {
|
|
return gDevTools.closeToolboxForTab(gBrowser.selectedTab);
|
|
};
|
|
|
|
/**
|
|
* Remove animated elements from document except given selectors.
|
|
*
|
|
* @param {Array} selectors
|
|
* @return {Promise}
|
|
*/
|
|
const removeAnimatedElementsExcept = function (selectors) {
|
|
return SpecialPowers.spawn(
|
|
gBrowser.selectedBrowser,
|
|
[selectors],
|
|
selectorsChild => {
|
|
function isRemovableElement(animation, selectorsInner) {
|
|
for (const selector of selectorsInner) {
|
|
if (animation.effect.target.matches(selector)) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
for (const animation of content.document.getAnimations()) {
|
|
if (isRemovableElement(animation, selectorsChild)) {
|
|
animation.effect.target.remove();
|
|
}
|
|
}
|
|
}
|
|
);
|
|
};
|
|
|
|
/**
|
|
* Click on an animation in the timeline to select it.
|
|
*
|
|
* @param {AnimationInspector} animationInspector.
|
|
* @param {DOMElement} panel
|
|
* #animation-container element.
|
|
* @param {Number} index
|
|
* The index of the animation to click on.
|
|
*/
|
|
const clickOnAnimation = async function (animationInspector, panel, index) {
|
|
info("Click on animation " + index + " in the timeline");
|
|
const animationItemEl = await findAnimationItemByIndex(panel, index);
|
|
const summaryGraphEl = animationItemEl.querySelector(
|
|
".animation-summary-graph"
|
|
);
|
|
clickOnSummaryGraph(animationInspector, panel, summaryGraphEl);
|
|
};
|
|
|
|
/**
|
|
* Click on an animation by given selector of node which is target element of animation.
|
|
*
|
|
* @param {AnimationInspector} animationInspector.
|
|
* @param {DOMElement} panel
|
|
* #animation-container element.
|
|
* @param {String} selector
|
|
* Selector of node which is target element of animation.
|
|
*/
|
|
const clickOnAnimationByTargetSelector = async function (
|
|
animationInspector,
|
|
panel,
|
|
selector
|
|
) {
|
|
info(`Click on animation whose selector of target element is '${selector}'`);
|
|
const animationItemEl = await findAnimationItemByTargetSelector(
|
|
panel,
|
|
selector
|
|
);
|
|
const summaryGraphEl = animationItemEl.querySelector(
|
|
".animation-summary-graph"
|
|
);
|
|
clickOnSummaryGraph(animationInspector, panel, summaryGraphEl);
|
|
};
|
|
|
|
/**
|
|
* Click on close button for animation detail pane.
|
|
*
|
|
* @param {DOMElement} panel
|
|
* #animation-container element.
|
|
*/
|
|
const clickOnDetailCloseButton = function (panel) {
|
|
info("Click on close button for animation detail pane");
|
|
const buttonEl = panel.querySelector(".animation-detail-close-button");
|
|
const bounds = buttonEl.getBoundingClientRect();
|
|
const x = bounds.width / 2;
|
|
const y = bounds.height / 2;
|
|
EventUtils.synthesizeMouse(buttonEl, x, y, {}, buttonEl.ownerGlobal);
|
|
};
|
|
|
|
/**
|
|
* Click on pause/resume button.
|
|
*
|
|
* @param {AnimationInspector} animationInspector
|
|
* @param {DOMElement} panel
|
|
* #animation-container element.
|
|
*/
|
|
const clickOnPauseResumeButton = function (animationInspector, panel) {
|
|
info("Click on pause/resume button");
|
|
const buttonEl = panel.querySelector(".pause-resume-button");
|
|
const bounds = buttonEl.getBoundingClientRect();
|
|
const x = bounds.width / 2;
|
|
const y = bounds.height / 2;
|
|
EventUtils.synthesizeMouse(buttonEl, x, y, {}, buttonEl.ownerGlobal);
|
|
};
|
|
|
|
/**
|
|
* Click on rewind button.
|
|
*
|
|
* @param {AnimationInspector} animationInspector
|
|
* @param {DOMElement} panel
|
|
* #animation-container element.
|
|
*/
|
|
const clickOnRewindButton = function (animationInspector, panel) {
|
|
info("Click on rewind button");
|
|
const buttonEl = panel.querySelector(".rewind-button");
|
|
const bounds = buttonEl.getBoundingClientRect();
|
|
const x = bounds.width / 2;
|
|
const y = bounds.height / 2;
|
|
EventUtils.synthesizeMouse(buttonEl, x, y, {}, buttonEl.ownerGlobal);
|
|
};
|
|
|
|
/**
|
|
* Click on the scrubber controller pane to update the animation current time.
|
|
*
|
|
* @param {DOMElement} panel
|
|
* #animation-container element.
|
|
* @param {Number} mouseDownPosition
|
|
* rate on scrubber controller pane.
|
|
* This method calculates
|
|
* `mouseDownPosition * offsetWidth + offsetLeft of scrubber controller pane`
|
|
* as the clientX of MouseEvent.
|
|
*/
|
|
const clickOnCurrentTimeScrubberController = function (
|
|
animationInspector,
|
|
panel,
|
|
mouseDownPosition
|
|
) {
|
|
const controllerEl = panel.querySelector(".current-time-scrubber-area");
|
|
const bounds = controllerEl.getBoundingClientRect();
|
|
const mousedonwX = bounds.width * mouseDownPosition;
|
|
|
|
info(`Click ${mousedonwX} on scrubber controller`);
|
|
EventUtils.synthesizeMouse(
|
|
controllerEl,
|
|
mousedonwX,
|
|
0,
|
|
{},
|
|
controllerEl.ownerGlobal
|
|
);
|
|
};
|
|
|
|
/**
|
|
* Click on the inspect icon for the given AnimationTargetComponent.
|
|
*
|
|
* @param {AnimationInspector} animationInspector.
|
|
* @param {DOMElement} panel
|
|
* #animation-container element.
|
|
* @param {Number} index
|
|
* The index of the AnimationTargetComponent to click on.
|
|
*/
|
|
const clickOnInspectIcon = async function (animationInspector, panel, index) {
|
|
info(`Click on an inspect icon in animation target component[${index}]`);
|
|
const animationItemEl = await findAnimationItemByIndex(panel, index);
|
|
const iconEl = animationItemEl.querySelector(
|
|
".animation-target .objectBox .highlight-node"
|
|
);
|
|
iconEl.scrollIntoView(false);
|
|
// Use click instead of EventUtils.synthesizeMouseAtCenter because the latter
|
|
// seems to trigger additional events (e.g. mouseover) that might interfere with tests
|
|
iconEl.click();
|
|
};
|
|
|
|
/**
|
|
* Change playback rate selector to select given rate.
|
|
*
|
|
* @param {AnimationInspector} animationInspector
|
|
* @param {DOMElement} panel
|
|
* #animation-container element.
|
|
* @param {Number} rate
|
|
*/
|
|
const changePlaybackRateSelector = async function (
|
|
animationInspector,
|
|
panel,
|
|
rate
|
|
) {
|
|
info(`Click on playback rate selector to select ${rate}`);
|
|
const selectEl = panel.querySelector(".playback-rate-selector");
|
|
const optionIndex = [...selectEl.options].findIndex(o => +o.value == rate);
|
|
|
|
if (optionIndex == -1) {
|
|
ok(
|
|
false,
|
|
`Could not find an option for rate ${rate} in the rate selector. ` +
|
|
`Values are: ${[...selectEl.options].map(o => o.value)}`
|
|
);
|
|
return;
|
|
}
|
|
|
|
selectEl.focus();
|
|
|
|
const win = selectEl.ownerGlobal;
|
|
while (selectEl.selectedIndex != optionIndex) {
|
|
const key = selectEl.selectedIndex > optionIndex ? "LEFT" : "RIGHT";
|
|
EventUtils.sendKey(key, win);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Click on given summary graph element.
|
|
*
|
|
* @param {AnimationInspector} animationInspector
|
|
* @param {DOMElement} panel
|
|
* #animation-container element.
|
|
* @param {Element} summaryGraphEl
|
|
*/
|
|
const clickOnSummaryGraph = function (
|
|
animationInspector,
|
|
panel,
|
|
summaryGraphEl
|
|
) {
|
|
// Disable pointer-events of the scrubber in order to avoid to click accidently.
|
|
const scrubberEl = panel.querySelector(".current-time-scrubber");
|
|
scrubberEl.style.pointerEvents = "none";
|
|
// Scroll to show the timeBlock since the element may be out of displayed area.
|
|
summaryGraphEl.scrollIntoView(false);
|
|
EventUtils.synthesizeMouseAtCenter(
|
|
summaryGraphEl,
|
|
{},
|
|
summaryGraphEl.ownerGlobal
|
|
);
|
|
// Restore the scrubber style.
|
|
scrubberEl.style.pointerEvents = "unset";
|
|
};
|
|
|
|
/**
|
|
* Click on the target node for the given AnimationTargetComponent index.
|
|
*
|
|
* @param {AnimationInspector} animationInspector.
|
|
* @param {DOMElement} panel
|
|
* #animation-container element.
|
|
* @param {Number} index
|
|
* The index of the AnimationTargetComponent to click on.
|
|
*/
|
|
const clickOnTargetNode = async function (animationInspector, panel, index) {
|
|
const { inspector } = animationInspector;
|
|
const { waitForHighlighterTypeShown } = getHighlighterTestHelpers(inspector);
|
|
info(`Click on a target node in animation target component[${index}]`);
|
|
|
|
const animationItemEl = await findAnimationItemByIndex(panel, index);
|
|
const targetEl = animationItemEl.querySelector(
|
|
".animation-target .objectBox"
|
|
);
|
|
const onHighlight = waitForHighlighterTypeShown(
|
|
inspector.highlighters.TYPES.BOXMODEL
|
|
);
|
|
EventUtils.synthesizeMouseAtCenter(targetEl, {}, targetEl.ownerGlobal);
|
|
await onHighlight;
|
|
};
|
|
|
|
/**
|
|
* Drag on the scrubber to update the animation current time.
|
|
*
|
|
* @param {DOMElement} panel
|
|
* #animation-container element.
|
|
* @param {Number} mouseMovePixel
|
|
* Dispatch mousemove event with mouseMovePosition after mousedown.
|
|
* @param {Number} mouseYPixel
|
|
* Y of mouse in pixel.
|
|
*/
|
|
const dragOnCurrentTimeScrubber = async function (
|
|
animationInspector,
|
|
panel,
|
|
mouseMovePixel,
|
|
mouseYPixel
|
|
) {
|
|
const controllerEl = panel.querySelector(".current-time-scrubber");
|
|
info(`Drag scrubber to X ${mouseMovePixel}`);
|
|
EventUtils.synthesizeMouse(
|
|
controllerEl,
|
|
0,
|
|
mouseYPixel,
|
|
{ type: "mousedown" },
|
|
controllerEl.ownerGlobal
|
|
);
|
|
await waitUntilAnimationsPlayState(animationInspector, "paused");
|
|
|
|
const animation = animationInspector.state.animations[0];
|
|
let currentTime = animation.state.currentTime;
|
|
EventUtils.synthesizeMouse(
|
|
controllerEl,
|
|
mouseMovePixel,
|
|
mouseYPixel,
|
|
{ type: "mousemove" },
|
|
controllerEl.ownerGlobal
|
|
);
|
|
await waitUntil(() => animation.state.currentTime !== currentTime);
|
|
|
|
currentTime = animation.state.currentTime;
|
|
EventUtils.synthesizeMouse(
|
|
controllerEl,
|
|
mouseMovePixel,
|
|
mouseYPixel,
|
|
{ type: "mouseup" },
|
|
controllerEl.ownerGlobal
|
|
);
|
|
await waitUntil(() => animation.state.currentTime !== currentTime);
|
|
};
|
|
|
|
/**
|
|
* Drag on the scrubber controller pane to update the animation current time.
|
|
*
|
|
* @param {DOMElement} panel
|
|
* #animation-container element.
|
|
* @param {Number} mouseDownPosition
|
|
* rate on scrubber controller pane.
|
|
* This method calculates
|
|
* `mouseDownPosition * offsetWidth + offsetLeft of scrubber controller pane`
|
|
* as the clientX of MouseEvent.
|
|
* @param {Number} mouseMovePosition
|
|
* Dispatch mousemove event with mouseMovePosition after mousedown.
|
|
* Calculation for clinetX is same to above.
|
|
*/
|
|
const dragOnCurrentTimeScrubberController = async function (
|
|
animationInspector,
|
|
panel,
|
|
mouseDownPosition,
|
|
mouseMovePosition
|
|
) {
|
|
const controllerEl = panel.querySelector(".current-time-scrubber-area");
|
|
const bounds = controllerEl.getBoundingClientRect();
|
|
const mousedonwX = bounds.width * mouseDownPosition;
|
|
const mousemoveX = bounds.width * mouseMovePosition;
|
|
|
|
info(`Drag on scrubber controller from ${mousedonwX} to ${mousemoveX}`);
|
|
EventUtils.synthesizeMouse(
|
|
controllerEl,
|
|
mousedonwX,
|
|
0,
|
|
{ type: "mousedown" },
|
|
controllerEl.ownerGlobal
|
|
);
|
|
await waitUntilAnimationsPlayState(animationInspector, "paused");
|
|
|
|
const animation = animationInspector.state.animations[0];
|
|
let currentTime = animation.state.currentTime;
|
|
EventUtils.synthesizeMouse(
|
|
controllerEl,
|
|
mousemoveX,
|
|
0,
|
|
{ type: "mousemove" },
|
|
controllerEl.ownerGlobal
|
|
);
|
|
await waitUntil(() => animation.state.currentTime !== currentTime);
|
|
|
|
currentTime = animation.state.currentTime;
|
|
EventUtils.synthesizeMouse(
|
|
controllerEl,
|
|
mousemoveX,
|
|
0,
|
|
{ type: "mouseup" },
|
|
controllerEl.ownerGlobal
|
|
);
|
|
await waitUntil(() => animation.state.currentTime !== currentTime);
|
|
};
|
|
|
|
/**
|
|
* Get current animation duration and rate of
|
|
* clickOrDragOnCurrentTimeScrubberController in given pixels.
|
|
*
|
|
* @param {AnimationInspector} animationInspector
|
|
* @param {DOMElement} panel
|
|
* #animation-container element.
|
|
* @param {Number} pixels
|
|
* @return {Object}
|
|
* {
|
|
* duration,
|
|
* rate,
|
|
* }
|
|
*/
|
|
const getDurationAndRate = function (animationInspector, panel, pixels) {
|
|
const controllerEl = panel.querySelector(".current-time-scrubber-area");
|
|
const bounds = controllerEl.getBoundingClientRect();
|
|
const duration =
|
|
(animationInspector.state.timeScale.getDuration() / bounds.width) * pixels;
|
|
const rate = (1 / bounds.width) * pixels;
|
|
return { duration, rate };
|
|
};
|
|
|
|
/**
|
|
* Mouse over the target node for the given AnimationTargetComponent index.
|
|
*
|
|
* @param {AnimationInspector} animationInspector.
|
|
* @param {DOMElement} panel
|
|
* #animation-container element.
|
|
* @param {Number} index
|
|
* The index of the AnimationTargetComponent to click on.
|
|
*/
|
|
const mouseOverOnTargetNode = function (animationInspector, panel, index) {
|
|
info(`Mouse over on a target node in animation target component[${index}]`);
|
|
const el = panel.querySelectorAll(".animation-target .objectBox")[index];
|
|
el.scrollIntoView(false);
|
|
EventUtils.synthesizeMouse(el, 10, 5, { type: "mouseover" }, el.ownerGlobal);
|
|
};
|
|
|
|
/**
|
|
* Mouse out of the target node for the given AnimationTargetComponent index.
|
|
*
|
|
* @param {AnimationInspector} animationInspector.
|
|
* @param {DOMElement} panel
|
|
* #animation-container element.
|
|
* @param {Number} index
|
|
* The index of the AnimationTargetComponent to click on.
|
|
*/
|
|
const mouseOutOnTargetNode = function (animationInspector, panel, index) {
|
|
info(`Mouse out on a target node in animation target component[${index}]`);
|
|
const el = panel.querySelectorAll(".animation-target .objectBox")[index];
|
|
el.scrollIntoView(false);
|
|
EventUtils.synthesizeMouse(el, -1, -1, { type: "mouseout" }, el.ownerGlobal);
|
|
};
|
|
|
|
/**
|
|
* Select animation inspector in sidebar and toolbar.
|
|
*
|
|
* @param {InspectorPanel} inspector
|
|
*/
|
|
const selectAnimationInspector = async function (inspector) {
|
|
await inspector.toolbox.selectTool("inspector");
|
|
const onDispatched = waitForDispatch(inspector.store, "UPDATE_ANIMATIONS");
|
|
inspector.sidebar.select("animationinspector");
|
|
await onDispatched;
|
|
};
|
|
|
|
/**
|
|
* Send keyboard event of space to given panel.
|
|
*
|
|
* @param {AnimationInspector} animationInspector
|
|
* @param {DOMElement} target element.
|
|
*/
|
|
const sendSpaceKeyEvent = function (animationInspector, element) {
|
|
element.focus();
|
|
EventUtils.sendKey("SPACE", element.ownerGlobal);
|
|
};
|
|
|
|
/**
|
|
* Set a node class attribute to the given selector.
|
|
*
|
|
* @param {AnimationInspector} animationInspector
|
|
* @param {String} selector
|
|
* @param {String} cls
|
|
* e.g. ".ball.still"
|
|
*/
|
|
const setClassAttribute = async function (animationInspector, selector, cls) {
|
|
await SpecialPowers.spawn(
|
|
gBrowser.selectedBrowser,
|
|
[cls, selector],
|
|
(attributeValue, selectorChild) => {
|
|
const node = content.document.querySelector(selectorChild);
|
|
if (!node) {
|
|
return;
|
|
}
|
|
|
|
node.setAttribute("class", attributeValue);
|
|
}
|
|
);
|
|
};
|
|
|
|
/**
|
|
* Set a new style properties to the node for the given selector.
|
|
*
|
|
* @param {AnimationInspector} animationInspector
|
|
* @param {String} selector
|
|
* @param {Object} properties
|
|
* e.g. {
|
|
* animationDuration: "1000ms",
|
|
* animationTimingFunction: "linear",
|
|
* }
|
|
*/
|
|
const setEffectTimingAndPlayback = async function (
|
|
animationInspector,
|
|
selector,
|
|
effectTiming,
|
|
playbackRate
|
|
) {
|
|
await SpecialPowers.spawn(
|
|
gBrowser.selectedBrowser,
|
|
[selector, playbackRate, effectTiming],
|
|
(selectorChild, playbackRateChild, effectTimingChild) => {
|
|
let selectedAnimation = null;
|
|
|
|
for (const animation of content.document.getAnimations()) {
|
|
if (animation.effect.target.matches(selectorChild)) {
|
|
selectedAnimation = animation;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!selectedAnimation) {
|
|
return;
|
|
}
|
|
|
|
selectedAnimation.playbackRate = playbackRateChild;
|
|
selectedAnimation.effect.updateTiming(effectTimingChild);
|
|
}
|
|
);
|
|
};
|
|
|
|
/**
|
|
* Set the sidebar width by given parameter.
|
|
*
|
|
* @param {String} width
|
|
* Change sidebar width by given parameter.
|
|
* @param {InspectorPanel} inspector
|
|
* The instance of InspectorPanel currently loaded in the toolbox
|
|
* @return {Promise} Resolves when the sidebar size changed.
|
|
*/
|
|
const setSidebarWidth = async function (width, inspector) {
|
|
const onUpdated = inspector.toolbox.once("inspector-sidebar-resized");
|
|
inspector.splitBox.setState({ width });
|
|
await onUpdated;
|
|
};
|
|
|
|
/**
|
|
* Set a new style property declaration to the node for the given selector.
|
|
*
|
|
* @param {AnimationInspector} animationInspector
|
|
* @param {String} selector
|
|
* @param {String} propertyName
|
|
* e.g. "animationDuration"
|
|
* @param {String} propertyValue
|
|
* e.g. "5.5s"
|
|
*/
|
|
const setStyle = async function (
|
|
animationInspector,
|
|
selector,
|
|
propertyName,
|
|
propertyValue
|
|
) {
|
|
await SpecialPowers.spawn(
|
|
gBrowser.selectedBrowser,
|
|
[selector, propertyName, propertyValue],
|
|
(selectorChild, propertyNameChild, propertyValueChild) => {
|
|
const node = content.document.querySelector(selectorChild);
|
|
if (!node) {
|
|
return;
|
|
}
|
|
|
|
node.style[propertyNameChild] = propertyValueChild;
|
|
}
|
|
);
|
|
};
|
|
|
|
/**
|
|
* Set a new style properties to the node for the given selector.
|
|
*
|
|
* @param {AnimationInspector} animationInspector
|
|
* @param {String} selector
|
|
* @param {Object} properties
|
|
* e.g. {
|
|
* animationDuration: "1000ms",
|
|
* animationTimingFunction: "linear",
|
|
* }
|
|
*/
|
|
const setStyles = async function (animationInspector, selector, properties) {
|
|
await SpecialPowers.spawn(
|
|
gBrowser.selectedBrowser,
|
|
[properties, selector],
|
|
(propertiesChild, selectorChild) => {
|
|
const node = content.document.querySelector(selectorChild);
|
|
if (!node) {
|
|
return;
|
|
}
|
|
|
|
for (const propertyName in propertiesChild) {
|
|
const propertyValue = propertiesChild[propertyName];
|
|
node.style[propertyName] = propertyValue;
|
|
}
|
|
}
|
|
);
|
|
};
|
|
|
|
/**
|
|
* Wait until current time of animations will be changed to given current time.
|
|
*
|
|
* @param {AnimationInspector} animationInspector
|
|
* @param {Number} currentTime
|
|
*/
|
|
const waitUntilCurrentTimeChangedAt = async function (
|
|
animationInspector,
|
|
currentTime
|
|
) {
|
|
info(`Wait until current time will be change to ${currentTime}`);
|
|
await waitUntil(() =>
|
|
animationInspector.state.animations.every(a => {
|
|
return a.state.currentTime === currentTime;
|
|
})
|
|
);
|
|
};
|
|
|
|
/**
|
|
* Wait until animations' play state will be changed to given state.
|
|
*
|
|
* @param {Array} animationInspector
|
|
* @param {String} state
|
|
*/
|
|
const waitUntilAnimationsPlayState = async function (
|
|
animationInspector,
|
|
state
|
|
) {
|
|
info(`Wait until play state will be change to ${state}`);
|
|
await waitUntil(() =>
|
|
animationInspector.state.animations.every(a => a.state.playState === state)
|
|
);
|
|
};
|
|
|
|
/**
|
|
* Return count of graph that animation inspector is displaying.
|
|
*
|
|
* @param {AnimationInspector} animationInspector
|
|
* @param {DOMElement} panel
|
|
* @return {Number} count
|
|
*/
|
|
const getDisplayedGraphCount = (animationInspector, panel) => {
|
|
const animationLength = animationInspector.state.animations.length;
|
|
if (animationLength === 0) {
|
|
return 0;
|
|
}
|
|
|
|
const inspectionPanelEl = panel.querySelector(".progress-inspection-panel");
|
|
const itemEl = panel.querySelector(".animation-item");
|
|
const listEl = panel.querySelector(".animation-list");
|
|
const itemHeight = itemEl.offsetHeight;
|
|
// This calculation should be same as AnimationListContainer.updateDisplayableRange.
|
|
const count = Math.floor(listEl.offsetHeight / itemHeight) + 1;
|
|
const index = Math.floor(inspectionPanelEl.scrollTop / itemHeight);
|
|
|
|
return animationLength > index + count ? count : animationLength - index;
|
|
};
|
|
|
|
/**
|
|
* Check whether the animations are pausing.
|
|
*
|
|
* @param {AnimationInspector} animationInspector
|
|
*/
|
|
function assertAnimationsPausing(animationInspector) {
|
|
assertAnimationsPausingOrRunning(animationInspector, true);
|
|
}
|
|
|
|
/**
|
|
* Check whether the animations are pausing/running.
|
|
*
|
|
* @param {AnimationInspector} animationInspector
|
|
* @param {boolean} shouldPause
|
|
*/
|
|
function assertAnimationsPausingOrRunning(animationInspector, shouldPause) {
|
|
const hasRunningAnimation = animationInspector.state.animations.some(
|
|
({ state }) => state.playState === "running"
|
|
);
|
|
|
|
if (shouldPause) {
|
|
is(hasRunningAnimation, false, "All animations should be paused");
|
|
} else {
|
|
is(hasRunningAnimation, true, "Animations should be running at least one");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check whether the animations are running.
|
|
*
|
|
* @param {AnimationInspector} animationInspector
|
|
*/
|
|
function assertAnimationsRunning(animationInspector) {
|
|
assertAnimationsPausingOrRunning(animationInspector, false);
|
|
}
|
|
|
|
/**
|
|
* Check the <stop> element in the given linearGradientEl for the correct offset
|
|
* and color attributes.
|
|
*
|
|
* @param {Element} linearGradientEl
|
|
<linearGradient> element which has <stop> element.
|
|
* @param {Number} offset
|
|
* float which represents the "offset" attribute of <stop>.
|
|
* @param {String} expectedColor
|
|
* e.g. rgb(0, 0, 255)
|
|
*/
|
|
function assertLinearGradient(linearGradientEl, offset, expectedColor) {
|
|
const stopEl = findStopElement(linearGradientEl, offset);
|
|
ok(stopEl, `stop element at offset ${offset} should exist`);
|
|
is(
|
|
stopEl.getAttribute("stop-color"),
|
|
expectedColor,
|
|
`stop-color of stop element at offset ${offset} should be ${expectedColor}`
|
|
);
|
|
}
|
|
|
|
/**
|
|
* SummaryGraph is constructed by <path> element.
|
|
* This function checks the vertex of path segments.
|
|
*
|
|
* @param {Element} pathEl
|
|
* <path> element.
|
|
* @param {boolean} hasClosePath
|
|
* Set true if the path shoud be closing.
|
|
* @param {Object} expectedValues
|
|
* JSON object format. We can test the vertex and color.
|
|
* e.g.
|
|
* [
|
|
* { x: 0, y: 0 },
|
|
* { x: 0, y: 1 },
|
|
* ]
|
|
*/
|
|
function assertPathSegments(pathEl, hasClosePath, expectedValues) {
|
|
const pathData = pathEl.getPathData({ normalize: true });
|
|
ok(
|
|
expectedValues.every(value => isPassingThrough(pathData, value.x, value.y)),
|
|
"unexpected path segment vertices"
|
|
);
|
|
|
|
if (hasClosePath) {
|
|
ok(pathData.length, "Close path expected but path is empty");
|
|
const closePathSeg = pathData.at(-1);
|
|
Assert.strictEqual(
|
|
closePathSeg.type.toLowerCase(),
|
|
"z",
|
|
"Close path not found"
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check whether the given vertex is passing through on the path.
|
|
*
|
|
* @param {pathData} pathData - pathData of <path> element.
|
|
* @param {float} x - x of vertex.
|
|
* @param {float} y - y of vertex.
|
|
* @return {boolean} true: passing through, false: no on the path.
|
|
*/
|
|
function isPassingThrough(pathData, x, y) {
|
|
let previousX, previousY;
|
|
for (let i = 0; i < pathData.length; i++) {
|
|
const pathSeg = pathData[i];
|
|
if (!pathSeg.values.length) {
|
|
continue;
|
|
}
|
|
let currentX, currentY;
|
|
switch (pathSeg.type) {
|
|
case "M":
|
|
case "L":
|
|
currentX = pathSeg.values[0];
|
|
currentY = pathSeg.values[1];
|
|
break;
|
|
case "C":
|
|
currentX = pathSeg.values[4];
|
|
currentY = pathSeg.values[5];
|
|
break;
|
|
}
|
|
currentX = parseFloat(currentX.toFixed(3));
|
|
currentY = parseFloat(currentY.toFixed(3));
|
|
if (currentX === x && currentY === y) {
|
|
return true;
|
|
}
|
|
if (previousX === undefined && previousY === undefined) {
|
|
previousX = currentX;
|
|
previousY = currentY;
|
|
}
|
|
if (
|
|
previousX <= x &&
|
|
x <= currentX &&
|
|
Math.min(previousY, currentY) <= y &&
|
|
y <= Math.max(previousY, currentY)
|
|
) {
|
|
return true;
|
|
}
|
|
previousX = currentX;
|
|
previousY = currentY;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Return animation item element by the index.
|
|
*
|
|
* @param {DOMElement} panel
|
|
* #animation-container element.
|
|
* @param {Number} index
|
|
* @return {DOMElement}
|
|
* Animation item element.
|
|
*/
|
|
async function findAnimationItemByIndex(panel, index) {
|
|
const itemEls = [...panel.querySelectorAll(".animation-item")];
|
|
const itemEl = itemEls[index];
|
|
itemEl.scrollIntoView(false);
|
|
|
|
await waitUntil(
|
|
() =>
|
|
itemEl.querySelector(".animation-target .attrName") &&
|
|
itemEl.querySelector(".animation-computed-timing-path")
|
|
);
|
|
|
|
return itemEl;
|
|
}
|
|
|
|
/**
|
|
* Return animation item element by target node selector.
|
|
* This function compares betweem animation-target textContent and given selector.
|
|
* Then returns matched first item.
|
|
*
|
|
* @param {DOMElement} panel
|
|
* #animation-container element.
|
|
* @param {String} selector
|
|
* Selector of tested element.
|
|
* @return {DOMElement}
|
|
* Animation item element.
|
|
*/
|
|
async function findAnimationItemByTargetSelector(panel, selector) {
|
|
for (const itemEl of panel.querySelectorAll(".animation-item")) {
|
|
itemEl.scrollIntoView(false);
|
|
|
|
await waitUntil(
|
|
() =>
|
|
itemEl.querySelector(".animation-target .attrName") &&
|
|
itemEl.querySelector(".animation-computed-timing-path")
|
|
);
|
|
|
|
const attrNameEl = itemEl.querySelector(".animation-target .attrName");
|
|
const regexp = new RegExp(`\\${selector}(\\.|$)`, "gi");
|
|
if (regexp.exec(attrNameEl.textContent)) {
|
|
return itemEl;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Find the <stop> element which has the given offset in the given linearGradientEl.
|
|
*
|
|
* @param {Element} linearGradientEl
|
|
* <linearGradient> element which has <stop> element.
|
|
* @param {Number} offset
|
|
* Float which represents the "offset" attribute of <stop>.
|
|
* @return {Element}
|
|
* If can't find suitable element, returns null.
|
|
*/
|
|
function findStopElement(linearGradientEl, offset) {
|
|
for (const stopEl of linearGradientEl.querySelectorAll("stop")) {
|
|
if (offset <= parseFloat(stopEl.getAttribute("offset"))) {
|
|
return stopEl;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Do test for keyframes-graph_computed-value-path-1/2.
|
|
*
|
|
* @param {Array} testData
|
|
*/
|
|
async function testKeyframesGraphComputedValuePath(testData) {
|
|
await addTab(URL_ROOT + "doc_multi_keyframes.html");
|
|
await removeAnimatedElementsExcept(testData.map(t => `.${t.targetClass}`));
|
|
const { animationInspector, panel } = await openAnimationInspector();
|
|
|
|
for (const { properties, targetClass } of testData) {
|
|
info(`Checking keyframes graph for ${targetClass}`);
|
|
const onDetailRendered = animationInspector.once(
|
|
"animation-keyframes-rendered"
|
|
);
|
|
await clickOnAnimationByTargetSelector(
|
|
animationInspector,
|
|
panel,
|
|
`.${targetClass}`
|
|
);
|
|
await onDetailRendered;
|
|
|
|
for (const property of properties) {
|
|
const {
|
|
name,
|
|
computedValuePathClass,
|
|
expectedPathSegments,
|
|
expectedStopColors,
|
|
} = property;
|
|
|
|
const testTarget = `${name} in ${targetClass}`;
|
|
info(`Checking keyframes graph for ${testTarget}`);
|
|
info(`Checking keyframes graph path existence for ${testTarget}`);
|
|
const keyframesGraphPathEl = panel.querySelector(`.${name}`);
|
|
ok(
|
|
keyframesGraphPathEl,
|
|
`The keyframes graph path element of ${testTarget} should be existence`
|
|
);
|
|
|
|
info(`Checking computed value path existence for ${testTarget}`);
|
|
const computedValuePathEl = keyframesGraphPathEl.querySelector(
|
|
`.${computedValuePathClass}`
|
|
);
|
|
ok(
|
|
computedValuePathEl,
|
|
`The computed value path element of ${testTarget} should be existence`
|
|
);
|
|
|
|
info(`Checking path segments for ${testTarget}`);
|
|
const pathEl = computedValuePathEl.querySelector("path");
|
|
ok(pathEl, `The <path> element of ${testTarget} should be existence`);
|
|
assertPathSegments(pathEl, true, expectedPathSegments);
|
|
|
|
if (!expectedStopColors) {
|
|
continue;
|
|
}
|
|
|
|
info(`Checking linearGradient for ${testTarget}`);
|
|
const linearGradientEl =
|
|
computedValuePathEl.querySelector("linearGradient");
|
|
ok(
|
|
linearGradientEl,
|
|
`The <linearGradientEl> element of ${testTarget} should be existence`
|
|
);
|
|
|
|
for (const expectedStopColor of expectedStopColors) {
|
|
const { offset, color } = expectedStopColor;
|
|
assertLinearGradient(linearGradientEl, offset, color);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check the adjusted current time and created time from specified two animations.
|
|
*
|
|
* @param {AnimationPlayerFront.state} animation1
|
|
* @param {AnimationPlayerFront.state} animation2
|
|
*/
|
|
function checkAdjustingTheTime(animation1, animation2) {
|
|
const adjustedCurrentTimeDiff =
|
|
animation2.currentTime / animation2.playbackRate -
|
|
animation1.currentTime / animation1.playbackRate;
|
|
const createdTimeDiff = animation1.createdTime - animation2.createdTime;
|
|
Assert.less(
|
|
Math.abs(adjustedCurrentTimeDiff - createdTimeDiff),
|
|
0.1,
|
|
"Adjusted time is correct"
|
|
);
|
|
}
|