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