1713 lines
54 KiB
JavaScript
1713 lines
54 KiB
JavaScript
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
/* eslint no-unused-vars: [2, {"vars": "local"}] */
|
|
|
|
"use strict";
|
|
|
|
// Load the shared-head file first.
|
|
Services.scriptloader.loadSubScript(
|
|
"chrome://mochitests/content/browser/devtools/client/shared/test/shared-head.js",
|
|
this
|
|
);
|
|
|
|
// Services.prefs.setBoolPref("devtools.debugger.log", true);
|
|
// SimpleTest.registerCleanupFunction(() => {
|
|
// Services.prefs.clearUserPref("devtools.debugger.log");
|
|
// });
|
|
|
|
// Import helpers for the inspector that are also shared with others
|
|
Services.scriptloader.loadSubScript(
|
|
"chrome://mochitests/content/browser/devtools/client/inspector/test/shared-head.js",
|
|
this
|
|
);
|
|
|
|
const INSPECTOR_L10N = new LocalizationHelper(
|
|
"devtools/client/locales/inspector.properties"
|
|
);
|
|
|
|
registerCleanupFunction(() => {
|
|
Services.prefs.clearUserPref("devtools.inspector.activeSidebar");
|
|
Services.prefs.clearUserPref("devtools.inspector.selectedSidebar");
|
|
});
|
|
|
|
registerCleanupFunction(function () {
|
|
// Move the mouse outside inspector. If the test happened fake a mouse event
|
|
// somewhere over inspector the pointer is considered to be there when the
|
|
// next test begins. This might cause unexpected events to be emitted when
|
|
// another test moves the mouse.
|
|
// Move the mouse at the top-right corner of the browser, to prevent
|
|
// the mouse from triggering the tab tooltip to be shown while the tab is
|
|
// being closed because the test is exiting (See Bug 1378524 for rationale).
|
|
EventUtils.synthesizeMouseAtPoint(
|
|
window.innerWidth,
|
|
1,
|
|
{ type: "mousemove" },
|
|
window
|
|
);
|
|
});
|
|
|
|
/**
|
|
* Start the element picker and focus the content window.
|
|
* @param {Toolbox} toolbox
|
|
* @param {Boolean} skipFocus - Allow tests to bypass the focus event.
|
|
*/
|
|
var startPicker = async function (toolbox, skipFocus) {
|
|
info("Start the element picker");
|
|
toolbox.win.focus();
|
|
await toolbox.nodePicker.start();
|
|
if (!skipFocus) {
|
|
// By default make sure the content window is focused since the picker may not focus
|
|
// the content window by default.
|
|
await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async function () {
|
|
content.focus();
|
|
});
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Stop the element picker using the Escape keyboard shortcut
|
|
* @param {Toolbox} toolbox
|
|
*/
|
|
var stopPickerWithEscapeKey = async function (toolbox) {
|
|
const onPickerStopped = toolbox.nodePicker.once("picker-node-canceled");
|
|
EventUtils.synthesizeKey("VK_ESCAPE", {}, toolbox.win);
|
|
await onPickerStopped;
|
|
};
|
|
|
|
/**
|
|
* Start the eye dropper tool.
|
|
* @param {Toolbox} toolbox
|
|
*/
|
|
var startEyeDropper = async function (toolbox) {
|
|
info("Start the eye dropper tool");
|
|
toolbox.win.focus();
|
|
await toolbox.getPanel("inspector").showEyeDropper();
|
|
};
|
|
|
|
/**
|
|
* Pick an element from the content page using the element picker.
|
|
*
|
|
* @param {Inspector} inspector
|
|
* Inspector instance
|
|
* @param {String} selector
|
|
* CSS selector to identify the click target
|
|
* @param {Number} x
|
|
* X-offset from the top-left corner of the element matching the provided selector
|
|
* @param {Number} y
|
|
* Y-offset from the top-left corner of the element matching the provided selector
|
|
* @return {Promise} promise that resolves when the selection is updated with the picked
|
|
* node.
|
|
*/
|
|
function pickElement(inspector, selector, x, y) {
|
|
info("Waiting for element " + selector + " to be picked");
|
|
// Use an empty options argument in order trigger the default synthesizeMouse behavior
|
|
// which will trigger mousedown, then mouseup.
|
|
const onNewNodeFront = inspector.selection.once("new-node-front");
|
|
BrowserTestUtils.synthesizeMouse(
|
|
selector,
|
|
x,
|
|
y,
|
|
{},
|
|
gBrowser.selectedTab.linkedBrowser
|
|
);
|
|
return onNewNodeFront;
|
|
}
|
|
|
|
/**
|
|
* Hover an element from the content page using the element picker.
|
|
*
|
|
* @param {Inspector} inspector
|
|
* Inspector instance
|
|
* @param {String|Array} selector
|
|
* CSS selector to identify the hover target.
|
|
* Example: ".target"
|
|
* If the element is at the bottom of a nested iframe stack, the selector should
|
|
* be an array with each item identifying the iframe within its host document.
|
|
* The last item of the array should be the element selector within the deepest
|
|
* nested iframe.
|
|
Example: ["iframe#top", "iframe#nested", ".target"]
|
|
* @param {Number} x
|
|
* X-offset from the top-left corner of the element matching the provided selector
|
|
* @param {Number} y
|
|
* Y-offset from the top-left corner of the element matching the provided selector
|
|
* @param {Object} eventOptions
|
|
* Options that will be passed to synthesizeMouse
|
|
* @return {Promise} promise that resolves when both the "picker-node-hovered" and
|
|
* "highlighter-shown" events are emitted.
|
|
*/
|
|
async function hoverElement(inspector, selector, x, y, eventOptions = {}) {
|
|
const { waitForHighlighterTypeShown } = getHighlighterTestHelpers(inspector);
|
|
info(`Waiting for element "${selector}" to be hovered`);
|
|
const onHovered = inspector.toolbox.nodePicker.once("picker-node-hovered");
|
|
const onHighlighterShown = waitForHighlighterTypeShown(
|
|
inspector.highlighters.TYPES.BOXMODEL
|
|
);
|
|
|
|
// Default to the top-level target browsing context
|
|
let browsingContext = gBrowser.selectedTab.linkedBrowser;
|
|
|
|
if (Array.isArray(selector)) {
|
|
// Get the browsing context for the deepest nested frame; exclude the last array item.
|
|
// Cloning the array so it can be safely mutated.
|
|
browsingContext = await getBrowsingContextForNestedFrame(
|
|
selector.slice(0, selector.length - 1)
|
|
);
|
|
// Assume the last item in the selector array is the actual element selector.
|
|
// DO NOT mutate the selector array with .pop(), it might still be used by a test.
|
|
selector = selector[selector.length - 1];
|
|
}
|
|
|
|
if (isNaN(x) || isNaN(y)) {
|
|
BrowserTestUtils.synthesizeMouseAtCenter(
|
|
selector,
|
|
{ ...eventOptions, type: "mousemove" },
|
|
browsingContext
|
|
);
|
|
} else {
|
|
BrowserTestUtils.synthesizeMouse(
|
|
selector,
|
|
x,
|
|
y,
|
|
{ ...eventOptions, type: "mousemove" },
|
|
browsingContext
|
|
);
|
|
}
|
|
|
|
info("Wait for picker-node-hovered");
|
|
await onHovered;
|
|
|
|
info("Wait for highlighter shown");
|
|
await onHighlighterShown;
|
|
|
|
return Promise.all([onHighlighterShown, onHovered]);
|
|
}
|
|
|
|
/**
|
|
* Get the browsing context for the deepest nested iframe
|
|
* as identified by an array of selectors.
|
|
*
|
|
* @param {Array} selectorArray
|
|
* Each item in the array is a selector that identifies the iframe
|
|
* within its host document.
|
|
* Example: ["iframe#top", "iframe#nested"]
|
|
* @return {BrowsingContext}
|
|
* BrowsingContext for the deepest nested iframe.
|
|
*/
|
|
async function getBrowsingContextForNestedFrame(selectorArray = []) {
|
|
// Default to the top-level target browsing context
|
|
let browsingContext = gBrowser.selectedTab.linkedBrowser;
|
|
|
|
// Return the top-level target browsing context if the selector is not an array.
|
|
if (!Array.isArray(selectorArray)) {
|
|
return browsingContext;
|
|
}
|
|
|
|
// Recursively get the browsing context for each nested iframe.
|
|
while (selectorArray.length) {
|
|
browsingContext = await SpecialPowers.spawn(
|
|
browsingContext,
|
|
[selectorArray.shift()],
|
|
function (selector) {
|
|
const iframe = content.document.querySelector(selector);
|
|
return iframe.browsingContext;
|
|
}
|
|
);
|
|
}
|
|
|
|
return browsingContext;
|
|
}
|
|
|
|
/**
|
|
* Highlight a node and set the inspector's current selection to the node or
|
|
* the first match of the given css selector.
|
|
* @param {String|NodeFront} selector
|
|
* @param {InspectorPanel} inspector
|
|
* The instance of InspectorPanel currently loaded in the toolbox
|
|
* @return a promise that resolves when the inspector is updated with the new
|
|
* node
|
|
*/
|
|
async function selectAndHighlightNode(selector, inspector) {
|
|
const { waitForHighlighterTypeShown } = getHighlighterTestHelpers(inspector);
|
|
info("Highlighting and selecting the node " + selector);
|
|
const onHighlighterShown = waitForHighlighterTypeShown(
|
|
inspector.highlighters.TYPES.BOXMODEL
|
|
);
|
|
|
|
await selectNode(selector, inspector, "test-highlight");
|
|
await onHighlighterShown;
|
|
}
|
|
|
|
/**
|
|
* Select node for a given selector, make it focusable and set focus in its
|
|
* container element.
|
|
* @param {String|NodeFront} selector
|
|
* @param {InspectorPanel} inspector The current inspector-panel instance.
|
|
* @return {MarkupContainer}
|
|
*/
|
|
async function focusNode(selector, inspector) {
|
|
getContainerForNodeFront(inspector.walker.rootNode, inspector).elt.focus();
|
|
const nodeFront = await getNodeFront(selector, inspector);
|
|
const container = getContainerForNodeFront(nodeFront, inspector);
|
|
await selectNode(nodeFront, inspector);
|
|
EventUtils.sendKey("return", inspector.panelWin);
|
|
return container;
|
|
}
|
|
|
|
/**
|
|
* Set the inspector's current selection to null so that no node is selected
|
|
*
|
|
* @param {InspectorPanel} inspector
|
|
* The instance of InspectorPanel currently loaded in the toolbox
|
|
* @return a promise that resolves when the inspector is updated
|
|
*/
|
|
function clearCurrentNodeSelection(inspector) {
|
|
info("Clearing the current selection");
|
|
const updated = inspector.once("inspector-updated");
|
|
inspector.selection.setNodeFront(null);
|
|
return updated;
|
|
}
|
|
|
|
/**
|
|
* Right click on a node in the test page and click on the inspect menu item.
|
|
* @param {String} selector The selector for the node to click on in the page.
|
|
* @return {Promise} Resolves to the inspector when it has opened and is updated
|
|
*/
|
|
var clickOnInspectMenuItem = async function (selector) {
|
|
info("Showing the contextual menu on node " + selector);
|
|
const contentAreaContextMenu = document.querySelector(
|
|
"#contentAreaContextMenu"
|
|
);
|
|
const contextOpened = once(contentAreaContextMenu, "popupshown");
|
|
|
|
await safeSynthesizeMouseEventAtCenterInContentPage(selector, {
|
|
type: "contextmenu",
|
|
button: 2,
|
|
});
|
|
|
|
await contextOpened;
|
|
|
|
info("Triggering the inspect action");
|
|
await gContextMenu.inspectNode();
|
|
|
|
info("Hiding the menu");
|
|
const contextClosed = once(contentAreaContextMenu, "popuphidden");
|
|
contentAreaContextMenu.hidePopup();
|
|
await contextClosed;
|
|
|
|
return getActiveInspector();
|
|
};
|
|
|
|
/**
|
|
* Get the NodeFront for the document node inside a given iframe.
|
|
*
|
|
* @param {String|NodeFront} frameSelector
|
|
* A selector that matches the iframe the document node is in
|
|
* @param {InspectorPanel} inspector
|
|
* The instance of InspectorPanel currently loaded in the toolbox
|
|
* @return {Promise} Resolves the node front when the inspector is updated with the new
|
|
* node.
|
|
*/
|
|
var getFrameDocument = async function (frameSelector, inspector) {
|
|
const iframe = await getNodeFront(frameSelector, inspector);
|
|
const { nodes } = await inspector.walker.children(iframe);
|
|
|
|
// Find the document node in the children of the iframe element.
|
|
return nodes.filter(node => node.displayName === "#document")[0];
|
|
};
|
|
|
|
/**
|
|
* Get the NodeFront for the shadowRoot of a shadow host.
|
|
*
|
|
* @param {String|NodeFront} hostSelector
|
|
* Selector or front of the element to which the shadow root is attached.
|
|
* @param {InspectorPanel} inspector
|
|
* The instance of InspectorPanel currently loaded in the toolbox
|
|
* @return {Promise} Resolves the node front when the inspector is updated with the new
|
|
* node.
|
|
*/
|
|
var getShadowRoot = async function (hostSelector, inspector) {
|
|
const hostFront = await getNodeFront(hostSelector, inspector);
|
|
const { nodes } = await inspector.walker.children(hostFront);
|
|
|
|
// Find the shadow root in the children of the host element.
|
|
return nodes.filter(node => node.isShadowRoot)[0];
|
|
};
|
|
|
|
/**
|
|
* Get the NodeFront for a node that matches a given css selector inside a shadow root.
|
|
*
|
|
* @param {String} selector
|
|
* CSS selector of the node inside the shadow root.
|
|
* @param {String|NodeFront} hostSelector
|
|
* Selector or front of the element to which the shadow root is attached.
|
|
* @param {InspectorPanel} inspector
|
|
* The instance of InspectorPanel currently loaded in the toolbox
|
|
* @return {Promise} Resolves the node front when the inspector is updated with the new
|
|
* node.
|
|
*/
|
|
var getNodeFrontInShadowDom = async function (
|
|
selector,
|
|
hostSelector,
|
|
inspector
|
|
) {
|
|
const shadowRoot = await getShadowRoot(hostSelector, inspector);
|
|
if (!shadowRoot) {
|
|
throw new Error(
|
|
"Could not find a shadow root under selector: " + hostSelector
|
|
);
|
|
}
|
|
|
|
return inspector.walker.querySelector(shadowRoot, selector);
|
|
};
|
|
|
|
var focusSearchBoxUsingShortcut = async function (panelWin, callback) {
|
|
info("Focusing search box");
|
|
const searchBox = panelWin.document.getElementById("inspector-searchbox");
|
|
const focused = once(searchBox, "focus");
|
|
|
|
panelWin.focus();
|
|
|
|
synthesizeKeyShortcut(INSPECTOR_L10N.getStr("inspector.searchHTML.key"));
|
|
|
|
await focused;
|
|
|
|
if (callback) {
|
|
callback();
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Get the MarkupContainer object instance that corresponds to the given
|
|
* NodeFront
|
|
* @param {NodeFront} nodeFront
|
|
* @param {InspectorPanel} inspector The instance of InspectorPanel currently
|
|
* loaded in the toolbox
|
|
* @return {MarkupContainer}
|
|
*/
|
|
function getContainerForNodeFront(nodeFront, { markup }) {
|
|
return markup.getContainer(nodeFront);
|
|
}
|
|
|
|
/**
|
|
* Get the MarkupContainer object instance that corresponds to the given
|
|
* selector
|
|
* @param {String|NodeFront} selector
|
|
* @param {InspectorPanel} inspector The instance of InspectorPanel currently
|
|
* loaded in the toolbox
|
|
* @param {Boolean} Set to true in the event that the node shouldn't be found.
|
|
* @return {MarkupContainer}
|
|
*/
|
|
var getContainerForSelector = async function (
|
|
selector,
|
|
inspector,
|
|
expectFailure = false
|
|
) {
|
|
info("Getting the markup-container for node " + selector);
|
|
const nodeFront = await getNodeFront(selector, inspector);
|
|
const container = getContainerForNodeFront(nodeFront, inspector);
|
|
|
|
if (expectFailure) {
|
|
ok(!container, "Shouldn't find markup-container for selector: " + selector);
|
|
} else {
|
|
ok(container, "Found markup-container for selector: " + selector);
|
|
}
|
|
|
|
return container;
|
|
};
|
|
|
|
/**
|
|
* Simulate a mouse-over on the markup-container (a line in the markup-view)
|
|
* that corresponds to the selector passed.
|
|
* @param {String|NodeFront} selector
|
|
* @param {InspectorPanel} inspector The instance of InspectorPanel currently
|
|
* loaded in the toolbox
|
|
* @return {Promise} Resolves when the container is hovered and the higlighter
|
|
* is shown on the corresponding node
|
|
*/
|
|
var hoverContainer = async function (selector, inspector) {
|
|
const { waitForHighlighterTypeShown } = getHighlighterTestHelpers(inspector);
|
|
info("Hovering over the markup-container for node " + selector);
|
|
|
|
const nodeFront = await getNodeFront(selector, inspector);
|
|
const container = getContainerForNodeFront(nodeFront, inspector);
|
|
|
|
const onHighlighterShown = waitForHighlighterTypeShown(
|
|
inspector.highlighters.TYPES.BOXMODEL
|
|
);
|
|
EventUtils.synthesizeMouseAtCenter(
|
|
container.tagLine,
|
|
{ type: "mousemove" },
|
|
inspector.markup.doc.defaultView
|
|
);
|
|
await onHighlighterShown;
|
|
};
|
|
|
|
/**
|
|
* Simulate a click on the markup-container (a line in the markup-view)
|
|
* that corresponds to the selector passed.
|
|
* @param {String|NodeFront} selector
|
|
* @param {InspectorPanel} inspector The instance of InspectorPanel currently
|
|
* loaded in the toolbox
|
|
* @return {Promise} Resolves when the node has been selected.
|
|
*/
|
|
var clickContainer = async function (selector, inspector) {
|
|
info("Clicking on the markup-container for node " + selector);
|
|
|
|
const nodeFront = await getNodeFront(selector, inspector);
|
|
const container = getContainerForNodeFront(nodeFront, inspector);
|
|
|
|
const updated = inspector.once("inspector-updated");
|
|
EventUtils.synthesizeMouseAtCenter(
|
|
container.tagLine,
|
|
{ type: "mousedown" },
|
|
inspector.markup.doc.defaultView
|
|
);
|
|
EventUtils.synthesizeMouseAtCenter(
|
|
container.tagLine,
|
|
{ type: "mouseup" },
|
|
inspector.markup.doc.defaultView
|
|
);
|
|
return updated;
|
|
};
|
|
|
|
/**
|
|
* Simulate the mouse leaving the markup-view area
|
|
* @param {InspectorPanel} inspector The instance of InspectorPanel currently
|
|
* loaded in the toolbox
|
|
* @return a promise when done
|
|
*/
|
|
function mouseLeaveMarkupView(inspector) {
|
|
info("Leaving the markup-view area");
|
|
|
|
// Find another element to mouseover over in order to leave the markup-view
|
|
const btn = inspector.toolbox.doc.querySelector("#toolbox-controls");
|
|
|
|
EventUtils.synthesizeMouseAtCenter(
|
|
btn,
|
|
{ type: "mousemove" },
|
|
inspector.toolbox.win
|
|
);
|
|
|
|
return new Promise(resolve => {
|
|
executeSoon(resolve);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Dispatch the copy event on the given element
|
|
*/
|
|
function fireCopyEvent(element) {
|
|
const evt = element.ownerDocument.createEvent("Event");
|
|
evt.initEvent("copy", true, true);
|
|
element.dispatchEvent(evt);
|
|
}
|
|
|
|
/**
|
|
* Undo the last markup-view action and wait for the corresponding mutation to
|
|
* occur
|
|
* @param {InspectorPanel} inspector The instance of InspectorPanel currently
|
|
* loaded in the toolbox
|
|
* @return a promise that resolves when the markup-mutation has been treated or
|
|
* rejects if no undo action is possible
|
|
*/
|
|
function undoChange(inspector) {
|
|
const canUndo = inspector.markup.undo.canUndo();
|
|
ok(canUndo, "The last change in the markup-view can be undone");
|
|
if (!canUndo) {
|
|
return Promise.reject();
|
|
}
|
|
|
|
const mutated = inspector.once("markupmutation");
|
|
inspector.markup.undo.undo();
|
|
return mutated;
|
|
}
|
|
|
|
/**
|
|
* Redo the last markup-view action and wait for the corresponding mutation to
|
|
* occur
|
|
* @param {InspectorPanel} inspector The instance of InspectorPanel currently
|
|
* loaded in the toolbox
|
|
* @return a promise that resolves when the markup-mutation has been treated or
|
|
* rejects if no redo action is possible
|
|
*/
|
|
function redoChange(inspector) {
|
|
const canRedo = inspector.markup.undo.canRedo();
|
|
ok(canRedo, "The last change in the markup-view can be redone");
|
|
if (!canRedo) {
|
|
return Promise.reject();
|
|
}
|
|
|
|
const mutated = inspector.once("markupmutation");
|
|
inspector.markup.undo.redo();
|
|
return mutated;
|
|
}
|
|
|
|
/**
|
|
* A helper that fetches a front for a node that matches the given selector or
|
|
* doctype node if the selector is falsy.
|
|
*/
|
|
async function getNodeFrontForSelector(selector, inspector) {
|
|
if (selector) {
|
|
info("Retrieving front for selector " + selector);
|
|
return getNodeFront(selector, inspector);
|
|
}
|
|
|
|
info("Retrieving front for doctype node");
|
|
const { nodes } = await inspector.walker.children(inspector.walker.rootNode);
|
|
return nodes[0];
|
|
}
|
|
|
|
/**
|
|
* A simple polling helper that executes a given function until it returns true.
|
|
* @param {Function} check A generator function that is expected to return true at some
|
|
* stage.
|
|
* @param {String} desc A text description to be displayed when the polling starts.
|
|
* @param {Number} attemptes Optional number of times we poll. Defaults to 10.
|
|
* @param {Number} timeBetweenAttempts Optional time to wait between each attempt.
|
|
* Defaults to 200ms.
|
|
*/
|
|
async function poll(check, desc, attempts = 10, timeBetweenAttempts = 200) {
|
|
info(desc);
|
|
|
|
for (let i = 0; i < attempts; i++) {
|
|
if (await check()) {
|
|
return;
|
|
}
|
|
await new Promise(resolve => setTimeout(resolve, timeBetweenAttempts));
|
|
}
|
|
|
|
throw new Error(`Timeout while: ${desc}`);
|
|
}
|
|
|
|
/**
|
|
* Encapsulate some common operations for highlighter's tests, to have
|
|
* the tests cleaner, without exposing directly `inspector`, `highlighter`, and
|
|
* `highlighterTestFront` if not needed.
|
|
*
|
|
* @param {String}
|
|
* The highlighter's type
|
|
* @return
|
|
* A generator function that takes an object with `inspector` and `highlighterTestFront`
|
|
* properties. (see `openInspector`)
|
|
*/
|
|
const getHighlighterHelperFor = type =>
|
|
async function ({ inspector, highlighterTestFront }) {
|
|
const front = inspector.inspectorFront;
|
|
const highlighter = await front.getHighlighterByType(type);
|
|
|
|
let prefix = "";
|
|
|
|
// Internals for mouse events
|
|
let prevX, prevY;
|
|
|
|
// Highlighted node
|
|
let highlightedNode = null;
|
|
|
|
return {
|
|
set prefix(value) {
|
|
prefix = value;
|
|
},
|
|
|
|
get highlightedNode() {
|
|
if (!highlightedNode) {
|
|
return null;
|
|
}
|
|
|
|
return {
|
|
async getComputedStyle(options = {}) {
|
|
const pageStyle = highlightedNode.inspectorFront.pageStyle;
|
|
return pageStyle.getComputed(highlightedNode, options);
|
|
},
|
|
};
|
|
},
|
|
|
|
get actorID() {
|
|
if (!highlighter) {
|
|
return null;
|
|
}
|
|
|
|
return highlighter.actorID;
|
|
},
|
|
|
|
async show(selector = ":root", options, frameSelector = null) {
|
|
if (frameSelector) {
|
|
highlightedNode = await getNodeFrontInFrames(
|
|
[frameSelector, selector],
|
|
inspector
|
|
);
|
|
} else {
|
|
highlightedNode = await getNodeFront(selector, inspector);
|
|
}
|
|
return highlighter.show(highlightedNode, options);
|
|
},
|
|
|
|
async hide() {
|
|
await highlighter.hide();
|
|
},
|
|
|
|
async isElementHidden(id) {
|
|
return (
|
|
(await highlighterTestFront.getHighlighterNodeAttribute(
|
|
prefix + id,
|
|
"hidden",
|
|
highlighter
|
|
)) === "true"
|
|
);
|
|
},
|
|
|
|
async getElementTextContent(id) {
|
|
return highlighterTestFront.getHighlighterNodeTextContent(
|
|
prefix + id,
|
|
highlighter
|
|
);
|
|
},
|
|
|
|
async getElementAttribute(id, name) {
|
|
return highlighterTestFront.getHighlighterNodeAttribute(
|
|
prefix + id,
|
|
name,
|
|
highlighter
|
|
);
|
|
},
|
|
|
|
async waitForElementAttributeSet(id, name) {
|
|
await poll(async function () {
|
|
const value = await highlighterTestFront.getHighlighterNodeAttribute(
|
|
prefix + id,
|
|
name,
|
|
highlighter
|
|
);
|
|
return !!value;
|
|
}, `Waiting for element ${id} to have attribute ${name} set`);
|
|
},
|
|
|
|
async waitForElementAttributeRemoved(id, name) {
|
|
await poll(async function () {
|
|
const value = await highlighterTestFront.getHighlighterNodeAttribute(
|
|
prefix + id,
|
|
name,
|
|
highlighter
|
|
);
|
|
return !value;
|
|
}, `Waiting for element ${id} to have attribute ${name} removed`);
|
|
},
|
|
|
|
async synthesizeMouse({
|
|
selector = ":root",
|
|
center,
|
|
x,
|
|
y,
|
|
options,
|
|
} = {}) {
|
|
if (center === true) {
|
|
await safeSynthesizeMouseEventAtCenterInContentPage(
|
|
selector,
|
|
options
|
|
);
|
|
} else {
|
|
await safeSynthesizeMouseEventInContentPage(selector, x, y, options);
|
|
}
|
|
},
|
|
|
|
// This object will synthesize any "mouse" prefixed event to the
|
|
// `highlighterTestFront`, using the name of method called as suffix for the
|
|
// event's name.
|
|
// If no x, y coords are given, the previous ones are used.
|
|
//
|
|
// For example:
|
|
// mouse.down(10, 20); // synthesize "mousedown" at 10,20
|
|
// mouse.move(20, 30); // synthesize "mousemove" at 20,30
|
|
// mouse.up(); // synthesize "mouseup" at 20,30
|
|
mouse: new Proxy(
|
|
{},
|
|
{
|
|
get: (target, name) =>
|
|
async function (x = prevX, y = prevY, selector = ":root") {
|
|
prevX = x;
|
|
prevY = y;
|
|
await safeSynthesizeMouseEventInContentPage(selector, x, y, {
|
|
type: "mouse" + name,
|
|
});
|
|
},
|
|
}
|
|
),
|
|
|
|
async finalize() {
|
|
highlightedNode = null;
|
|
await highlighter.finalize();
|
|
},
|
|
};
|
|
};
|
|
|
|
/**
|
|
* Inspector-scoped wrapper for highlighter helpers to be used in tests.
|
|
*
|
|
* @param {Inspector} inspector
|
|
* Inspector client object instance.
|
|
* @return {Object} Object with helper methods
|
|
*/
|
|
function getHighlighterTestHelpers(inspector) {
|
|
/**
|
|
* Return a promise which resolves when a highlighter triggers the given event.
|
|
*
|
|
* @param {String} type
|
|
* Highlighter type.
|
|
* @param {String} eventName
|
|
* Name of the event to listen to.
|
|
* @return {Promise}
|
|
* Promise which resolves when the highlighter event occurs.
|
|
* Resolves with the data payload attached to the event.
|
|
*/
|
|
function _waitForHighlighterTypeEvent(type, eventName) {
|
|
return new Promise(resolve => {
|
|
function _handler(data) {
|
|
if (type === data.type) {
|
|
inspector.highlighters.off(eventName, _handler);
|
|
resolve(data);
|
|
}
|
|
}
|
|
|
|
inspector.highlighters.on(eventName, _handler);
|
|
});
|
|
}
|
|
|
|
return {
|
|
getActiveHighlighter(type) {
|
|
return inspector.highlighters.getActiveHighlighter(type);
|
|
},
|
|
getNodeForActiveHighlighter(type) {
|
|
return inspector.highlighters.getNodeForActiveHighlighter(type);
|
|
},
|
|
waitForHighlighterTypeShown(type) {
|
|
return _waitForHighlighterTypeEvent(type, "highlighter-shown");
|
|
},
|
|
waitForHighlighterTypeHidden(type) {
|
|
return _waitForHighlighterTypeEvent(type, "highlighter-hidden");
|
|
},
|
|
waitForHighlighterTypeRestored(type) {
|
|
return _waitForHighlighterTypeEvent(type, "highlighter-restored");
|
|
},
|
|
waitForHighlighterTypeDiscarded(type) {
|
|
return _waitForHighlighterTypeEvent(type, "highlighter-discarded");
|
|
},
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Wait for the toolbox to emit the styleeditor-selected event and when done
|
|
* wait for the stylesheet identified by href to be loaded in the stylesheet
|
|
* editor
|
|
*
|
|
* @param {Toolbox} toolbox
|
|
* @param {String} href
|
|
* Optional, if not provided, wait for the first editor to be ready
|
|
* @return a promise that resolves to the editor when the stylesheet editor is
|
|
* ready
|
|
*/
|
|
function waitForStyleEditor(toolbox, href) {
|
|
info("Waiting for the toolbox to switch to the styleeditor");
|
|
|
|
return new Promise(resolve => {
|
|
toolbox.once("styleeditor-selected").then(() => {
|
|
const panel = toolbox.getCurrentPanel();
|
|
ok(panel && panel.UI, "Styleeditor panel switched to front");
|
|
|
|
// A helper that resolves the promise once it receives an editor that
|
|
// matches the expected href. Returns false if the editor was not correct.
|
|
const gotEditor = editor => {
|
|
if (!editor) {
|
|
info("Editor went away after selected?");
|
|
return false;
|
|
}
|
|
|
|
const currentHref = editor.styleSheet.href;
|
|
if (!href || (href && currentHref.endsWith(href))) {
|
|
info("Stylesheet editor selected");
|
|
panel.UI.off("editor-selected", gotEditor);
|
|
|
|
editor.getSourceEditor().then(sourceEditor => {
|
|
info("Stylesheet editor fully loaded");
|
|
resolve(sourceEditor);
|
|
});
|
|
|
|
return true;
|
|
}
|
|
|
|
info("The editor was incorrect. Waiting for editor-selected event.");
|
|
return false;
|
|
};
|
|
|
|
// The expected editor may already be selected. Check the if the currently
|
|
// selected editor is the expected one and if not wait for an
|
|
// editor-selected event.
|
|
if (!gotEditor(panel.UI.selectedEditor)) {
|
|
// The expected editor is not selected (yet). Wait for it.
|
|
panel.UI.on("editor-selected", gotEditor);
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Checks if document's active element is within the given element.
|
|
* @param {HTMLDocument} doc document with active element in question
|
|
* @param {DOMNode} container element tested on focus containment
|
|
* @return {Boolean}
|
|
*/
|
|
function containsFocus(doc, container) {
|
|
let elm = doc.activeElement;
|
|
while (elm) {
|
|
if (elm === container) {
|
|
return true;
|
|
}
|
|
elm = elm.parentNode;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Listen for a new tab to open and return a promise that resolves when one
|
|
* does and completes the load event.
|
|
*
|
|
* @return a promise that resolves to the tab object
|
|
*/
|
|
var waitForTab = async function () {
|
|
info("Waiting for a tab to open");
|
|
await once(gBrowser.tabContainer, "TabOpen");
|
|
const tab = gBrowser.selectedTab;
|
|
await BrowserTestUtils.browserLoaded(tab.linkedBrowser);
|
|
info("The tab load completed");
|
|
return tab;
|
|
};
|
|
|
|
/**
|
|
* Simulate the key input for the given input in the window.
|
|
*
|
|
* @param {String} input
|
|
* The string value to input
|
|
* @param {Window} win
|
|
* The window containing the panel
|
|
*/
|
|
function synthesizeKeys(input, win) {
|
|
for (const key of input.split("")) {
|
|
EventUtils.synthesizeKey(key, {}, win);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Make sure window is properly focused before sending a key event.
|
|
*
|
|
* @param {Window} win
|
|
* The window containing the panel
|
|
* @param {String} key
|
|
* The string value to input
|
|
*/
|
|
function focusAndSendKey(win, key) {
|
|
win.document.documentElement.focus();
|
|
EventUtils.sendKey(key, win);
|
|
}
|
|
|
|
/**
|
|
* Given a Tooltip instance, fake a mouse event on the `target` DOM Element
|
|
* and assert that the `tooltip` is correctly displayed.
|
|
*
|
|
* @param {Tooltip} tooltip
|
|
* The tooltip instance
|
|
* @param {DOMElement} target
|
|
* The DOM Element on which a tooltip should appear
|
|
*
|
|
* @return a promise that resolves with the tooltip object
|
|
*/
|
|
async function assertTooltipShownOnHover(tooltip, target) {
|
|
const mouseEvent = new target.ownerDocument.defaultView.MouseEvent(
|
|
"mousemove",
|
|
{
|
|
bubbles: true,
|
|
}
|
|
);
|
|
target.dispatchEvent(mouseEvent);
|
|
|
|
if (!tooltip.isVisible()) {
|
|
info("Waiting for tooltip to be shown");
|
|
await tooltip.once("shown");
|
|
}
|
|
|
|
ok(tooltip.isVisible(), `The tooltip is visible`);
|
|
|
|
return tooltip;
|
|
}
|
|
|
|
/**
|
|
* Given an inspector `view` object, fake a mouse event on the `target` DOM
|
|
* Element and assert that the preview tooltip is correctly displayed.
|
|
*
|
|
* @param {CssRuleView|ComputedView|...} view
|
|
* The instance of an inspector panel
|
|
* @param {DOMElement} target
|
|
* The DOM Element on which a tooltip should appear
|
|
*
|
|
* @return a promise that resolves with the tooltip object
|
|
*/
|
|
async function assertShowPreviewTooltip(view, target) {
|
|
const name = "previewTooltip";
|
|
|
|
// Get the tooltip. If it does not exist one will be created.
|
|
const tooltip = view.tooltips.getTooltip(name);
|
|
ok(tooltip, `Tooltip '${name}' has been instantiated`);
|
|
|
|
const shown = tooltip.once("shown");
|
|
const mouseEvent = new target.ownerDocument.defaultView.MouseEvent(
|
|
"mousemove",
|
|
{
|
|
bubbles: true,
|
|
}
|
|
);
|
|
target.dispatchEvent(mouseEvent);
|
|
|
|
info("Waiting for tooltip to be shown");
|
|
await shown;
|
|
|
|
ok(tooltip.isVisible(), `The tooltip '${name}' is visible`);
|
|
|
|
return tooltip;
|
|
}
|
|
|
|
/**
|
|
* Given a `tooltip` instance, fake a mouse event on `target` DOM element
|
|
* and check that the tooltip correctly disappear.
|
|
*
|
|
* @param {Tooltip} tooltip
|
|
* The tooltip instance
|
|
* @param {DOMElement} target
|
|
* The DOM Element on which a tooltip should appear
|
|
*/
|
|
async function assertTooltipHiddenOnMouseOut(tooltip, target) {
|
|
// The tooltip actually relies on mousemove events to check if it should be hidden.
|
|
const mouseEvent = new target.ownerDocument.defaultView.MouseEvent(
|
|
"mousemove",
|
|
{
|
|
bubbles: true,
|
|
relatedTarget: target,
|
|
}
|
|
);
|
|
target.parentNode.dispatchEvent(mouseEvent);
|
|
|
|
await tooltip.once("hidden");
|
|
|
|
ok(!tooltip.isVisible(), "The tooltip is hidden on mouseout");
|
|
}
|
|
|
|
/**
|
|
* Check the content of a `var()` tooltip on a given rule and property name.
|
|
*
|
|
* @param {CssRuleView} view
|
|
* @param {String} ruleSelector
|
|
* @param {String} propertyName
|
|
* @param {Object} tooltipExpected
|
|
* @param {String} tooltipExpected.header: The HTML for the top section
|
|
* (might be the only section when the variable is not a registered property and
|
|
* there is no starting-style, nor computed value).
|
|
* @param {Array<String>} tooltipExpected.headerClasses: Classes applied on the header element
|
|
* (no need to include `variable-value` which is always added).
|
|
* @param {String} tooltipExpected.computed: The HTML for the computed value section.
|
|
* @param {Array<String>} tooltipExpected.computedClasses: Classes applied on the computed value element.
|
|
* @param {Integer} tooltipExpected.index: The index in the property value for the variable
|
|
* element we want to check. Defaults to 0 so we can quickly check values when only
|
|
* one variable is used.
|
|
* @param {Boolean} tooltipExpected.isMatched: Is the element matched or unmatched, defaults
|
|
* to true.
|
|
* @param {String} tooltipExpected.startingStyle: The HTML for the starting-style section.
|
|
* Pass undefined if the tooltip isn't supposed to have a `@starting-style` section.
|
|
* @param {Array<String>} tooltipExpected.startingStyleClasses: Classes applied on the
|
|
* starting-style value element.
|
|
* @param {Object} tooltipExpected.registeredProperty: Object whose properties should match
|
|
* the displayed registered property fields, e.g:
|
|
* {syntax:`"<color>"`, inherits:"true", "initial-value": "10px"}
|
|
* The properties values are the HTML of the dd elements.
|
|
* Pass undefined if the tooltip isn't supposed to have a @property section.
|
|
*/
|
|
async function assertVariableTooltipForProperty(
|
|
view,
|
|
ruleSelector,
|
|
propertyName,
|
|
{
|
|
computed,
|
|
computedClasses = ["theme-fg-color1"],
|
|
header,
|
|
headerClasses = ["theme-fg-color1"],
|
|
index = 0,
|
|
isMatched = true,
|
|
registeredProperty,
|
|
startingStyle,
|
|
startingStyleClasses = ["theme-fg-color1"],
|
|
}
|
|
) {
|
|
// retrieve tooltip target
|
|
const variableEl = await waitFor(
|
|
() =>
|
|
getRuleViewProperty(
|
|
view,
|
|
ruleSelector,
|
|
propertyName
|
|
).valueSpan.querySelectorAll(".inspector-variable,.inspector-unmatched")[
|
|
index
|
|
]
|
|
);
|
|
|
|
if (isMatched) {
|
|
ok(
|
|
!variableEl.classList.contains("inspector-unmatched"),
|
|
`CSS variable #${index} for ${propertyName} in ${ruleSelector} is matched`
|
|
);
|
|
} else {
|
|
ok(
|
|
variableEl.classList.contains("inspector-unmatched"),
|
|
`CSS variable #${index} for ${propertyName} in ${ruleSelector} is unmatched`
|
|
);
|
|
}
|
|
|
|
const previewTooltip = await assertShowPreviewTooltip(view, variableEl);
|
|
const valueEl = previewTooltip.panel.querySelector(".variable-value");
|
|
const computedValueEl = previewTooltip.panel.querySelector(".computed div");
|
|
const startingStyleEl = previewTooltip.panel.querySelector(
|
|
".starting-style div"
|
|
);
|
|
const registeredPropertyEl = previewTooltip.panel.querySelector(
|
|
".registered-property dl"
|
|
);
|
|
is(
|
|
valueEl?.innerHTML,
|
|
header,
|
|
`CSS variable #${index} preview tooltip has expected header text for ${propertyName} in ${ruleSelector}`
|
|
);
|
|
Assert.deepEqual(
|
|
[...valueEl.classList],
|
|
["variable-value", ...headerClasses],
|
|
`CSS variable #${index} preview tooltip has expected classes for ${propertyName} in ${ruleSelector}`
|
|
);
|
|
|
|
if (typeof computed !== "string") {
|
|
is(
|
|
computedValueEl,
|
|
null,
|
|
`CSS variable #${index} preview tooltip doesn't have computed value section for ${propertyName} in ${ruleSelector}`
|
|
);
|
|
} else {
|
|
is(
|
|
computedValueEl?.innerHTML,
|
|
computed,
|
|
`CSS variable #${index} preview tooltip has expected computed value section for ${propertyName} in ${ruleSelector}`
|
|
);
|
|
Assert.deepEqual(
|
|
[...computedValueEl.classList],
|
|
computedClasses,
|
|
`CSS variable #${index} preview tooltip has expected classes on computed value for ${propertyName} in ${ruleSelector}`
|
|
);
|
|
}
|
|
|
|
if (!registeredProperty) {
|
|
is(
|
|
registeredPropertyEl,
|
|
null,
|
|
`CSS variable #${index} preview tooltip doesn't have registered property section for ${propertyName} in ${ruleSelector}`
|
|
);
|
|
} else {
|
|
const dts = registeredPropertyEl.querySelectorAll("dt");
|
|
const registeredPropertyEntries = Object.entries(registeredProperty);
|
|
is(
|
|
dts.length,
|
|
registeredPropertyEntries.length,
|
|
`CSS variable #${index} preview tooltip has the expected number of element in the registered property section for ${propertyName} in ${ruleSelector}`
|
|
);
|
|
for (let i = 0; i < registeredPropertyEntries.length; i++) {
|
|
const [label, value] = registeredPropertyEntries[i];
|
|
const dt = dts[i];
|
|
const dd = dt.nextElementSibling;
|
|
is(
|
|
dt.innerText,
|
|
`${label}:`,
|
|
`CSS variable #${index} preview tooltip has expected ${label} registered property element for ${propertyName} in ${ruleSelector}`
|
|
);
|
|
is(
|
|
dd.innerHTML,
|
|
value,
|
|
`CSS variable #${index} preview tooltip has expected HTML for ${label} registered property element for ${propertyName} in ${ruleSelector}`
|
|
);
|
|
}
|
|
}
|
|
|
|
if (!startingStyle) {
|
|
is(
|
|
startingStyleEl,
|
|
null,
|
|
`CSS variable #${index} preview tooltip doesn't have a starting-style section for ${propertyName} in ${ruleSelector}`
|
|
);
|
|
} else {
|
|
is(
|
|
startingStyleEl?.innerHTML,
|
|
startingStyle,
|
|
`CSS variable #${index} preview tooltip has expected starting-style section for ${propertyName} in ${ruleSelector}`
|
|
);
|
|
Assert.deepEqual(
|
|
[...startingStyleEl.classList],
|
|
startingStyleClasses,
|
|
`CSS variable #${index} preview tooltip has expected classes on starting-style value for ${propertyName} in ${ruleSelector}`
|
|
);
|
|
}
|
|
|
|
await assertTooltipHiddenOnMouseOut(previewTooltip, variableEl);
|
|
}
|
|
|
|
/**
|
|
* Get the text displayed for a given DOM Element's textContent within the
|
|
* markup view.
|
|
*
|
|
* @param {String} selector
|
|
* @param {InspectorPanel} inspector
|
|
* @return {String} The text displayed in the markup view
|
|
*/
|
|
async function getDisplayedNodeTextContent(selector, inspector) {
|
|
// We have to ensure that the textContent is displayed, for that the DOM
|
|
// Element has to be selected in the markup view and to be expanded.
|
|
await selectNode(selector, inspector);
|
|
|
|
const container = await getContainerForSelector(selector, inspector);
|
|
await inspector.markup.expandNode(container.node);
|
|
await waitForMultipleChildrenUpdates(inspector);
|
|
if (container) {
|
|
const textContainer = container.elt.querySelector("pre");
|
|
return textContainer.textContent;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Toggle the shapes highlighter by simulating a click on the toggle
|
|
* in the rules view with the given selector and property
|
|
*
|
|
* @param {CssRuleView} view
|
|
* The instance of the rule-view panel
|
|
* @param {String} selector
|
|
* The selector in the rule-view to look for the property in
|
|
* @param {String} property
|
|
* The name of the property
|
|
* @param {Boolean} show
|
|
* If true, the shapes highlighter is being shown. If false, it is being hidden
|
|
* @param {Options} options
|
|
* Config option for the shapes highlighter. Contains:
|
|
* - {Boolean} transformMode: whether to show the highlighter in transforms mode
|
|
*/
|
|
async function toggleShapesHighlighter(
|
|
view,
|
|
selector,
|
|
property,
|
|
show,
|
|
options = {}
|
|
) {
|
|
info(
|
|
`Toggle shapes highlighter ${
|
|
show ? "on" : "off"
|
|
} for ${property} on ${selector}`
|
|
);
|
|
const highlighters = view.highlighters;
|
|
const container = getRuleViewProperty(view, selector, property).valueSpan;
|
|
const shapesToggle = container.querySelector(".inspector-shapeswatch");
|
|
|
|
const metaKey = options.transformMode;
|
|
const ctrlKey = options.transformMode;
|
|
|
|
if (show) {
|
|
const onHighlighterShown = highlighters.once("shapes-highlighter-shown");
|
|
EventUtils.sendMouseEvent(
|
|
{ type: "click", metaKey, ctrlKey },
|
|
shapesToggle,
|
|
view.styleWindow
|
|
);
|
|
await onHighlighterShown;
|
|
} else {
|
|
const onHighlighterHidden = highlighters.once("shapes-highlighter-hidden");
|
|
EventUtils.sendMouseEvent(
|
|
{ type: "click", metaKey, ctrlKey },
|
|
shapesToggle,
|
|
view.styleWindow
|
|
);
|
|
await onHighlighterHidden;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Toggle the provided markup container by clicking on the expand arrow and waiting for
|
|
* children to update. Similar to expandContainer helper, but this method
|
|
* uses a click rather than programatically calling expandNode().
|
|
*
|
|
* @param {InspectorPanel} inspector
|
|
* The current inspector instance.
|
|
* @param {MarkupContainer} container
|
|
* The markup container to click on.
|
|
* @param {Object} modifiers
|
|
* options.altKey {Boolean} Use the altKey modifier, to recursively apply
|
|
* the action to all the children of the container.
|
|
*/
|
|
async function toggleContainerByClick(
|
|
inspector,
|
|
container,
|
|
{ altKey = false } = {}
|
|
) {
|
|
EventUtils.synthesizeMouseAtCenter(
|
|
container.expander,
|
|
{
|
|
altKey,
|
|
},
|
|
inspector.markup.doc.defaultView
|
|
);
|
|
|
|
// Wait for any pending children updates
|
|
await waitForMultipleChildrenUpdates(inspector);
|
|
}
|
|
|
|
/**
|
|
* Simulate a color change in a given color picker tooltip.
|
|
*
|
|
* @param {Spectrum} colorPicker
|
|
* The color picker widget.
|
|
* @param {Array} newRgba
|
|
* Array of the new rgba values to be set in the color widget.
|
|
*/
|
|
async function simulateColorPickerChange(colorPicker, newRgba) {
|
|
info("Getting the spectrum colorpicker object");
|
|
const spectrum = await colorPicker.spectrum;
|
|
info("Setting the new color");
|
|
spectrum.rgb = newRgba;
|
|
info("Applying the change");
|
|
spectrum.updateUI();
|
|
spectrum.onChange();
|
|
}
|
|
|
|
/**
|
|
* Assert method to compare the current content of the markupview to a text based tree.
|
|
*
|
|
* @param {String} tree
|
|
* Multiline string representing the markup view tree, for instance:
|
|
* `root
|
|
* child1
|
|
* subchild1
|
|
* subchild2
|
|
* child2
|
|
* subchild3!slotted`
|
|
* child3!ignore-children
|
|
* Each sub level should be indented by 2 spaces.
|
|
* Each line contains text expected to match with the text of the corresponding
|
|
* node in the markup view. Some suffixes are supported:
|
|
* - !slotted -> indicates that the line corresponds to the slotted version
|
|
* - !ignore-children -> the node might have children but do not assert them
|
|
* @param {String} selector
|
|
* A CSS selector that will uniquely match the "root" element from the tree
|
|
* @param {Inspector} inspector
|
|
* The inspector instance.
|
|
*/
|
|
async function assertMarkupViewAsTree(tree, selector, inspector) {
|
|
const { markup } = inspector;
|
|
|
|
info(`Find and expand the shadow DOM host matching selector ${selector}.`);
|
|
const rootFront = await getNodeFront(selector, inspector);
|
|
const rootContainer = markup.getContainer(rootFront);
|
|
|
|
const parsedTree = _parseMarkupViewTree(tree);
|
|
const treeRoot = parsedTree.children[0];
|
|
await _checkMarkupViewNode(treeRoot, rootContainer, inspector);
|
|
}
|
|
|
|
async function _checkMarkupViewNode(treeNode, container, inspector) {
|
|
const { node, children, path } = treeNode;
|
|
info("Checking [" + path + "]");
|
|
info("Checking node: " + node);
|
|
|
|
const ignoreChildren = node.includes("!ignore-children");
|
|
const slotted = node.includes("!slotted");
|
|
|
|
// Remove optional suffixes.
|
|
const nodeText = node.replace("!slotted", "").replace("!ignore-children", "");
|
|
|
|
assertContainerHasText(container, nodeText);
|
|
|
|
if (slotted) {
|
|
assertContainerSlotted(container);
|
|
}
|
|
|
|
if (ignoreChildren) {
|
|
return;
|
|
}
|
|
|
|
if (!children.length) {
|
|
ok(!container.canExpand, "Container for [" + path + "] has no children");
|
|
return;
|
|
}
|
|
|
|
// Expand the container if not already done.
|
|
if (!container.expanded) {
|
|
await expandContainer(inspector, container);
|
|
}
|
|
|
|
const containers = container.getChildContainers();
|
|
is(
|
|
containers.length,
|
|
children.length,
|
|
"Node [" + path + "] has the expected number of children"
|
|
);
|
|
for (let i = 0; i < children.length; i++) {
|
|
await _checkMarkupViewNode(children[i], containers[i], inspector);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Helper designed to parse a tree represented as:
|
|
* root
|
|
* child1
|
|
* subchild1
|
|
* subchild2
|
|
* child2
|
|
* subchild3!slotted
|
|
*
|
|
* Lines represent a simplified view of the markup, where the trimmed line is supposed to
|
|
* be included in the text content of the actual markupview container.
|
|
* This method returns an object that can be passed to _checkMarkupViewNode() to verify
|
|
* the current markup view displays the expected structure.
|
|
*/
|
|
function _parseMarkupViewTree(inputString) {
|
|
const tree = {
|
|
level: 0,
|
|
children: [],
|
|
};
|
|
let lines = inputString.split("\n");
|
|
lines = lines.filter(l => l.trim());
|
|
|
|
let currentNode = tree;
|
|
for (const line of lines) {
|
|
const nodeString = line.trim();
|
|
const level = line.split(" ").length;
|
|
|
|
let parent;
|
|
if (level > currentNode.level) {
|
|
parent = currentNode;
|
|
} else {
|
|
parent = currentNode.parent;
|
|
for (let i = 0; i < currentNode.level - level; i++) {
|
|
parent = parent.parent;
|
|
}
|
|
}
|
|
|
|
const node = {
|
|
node: nodeString,
|
|
children: [],
|
|
parent,
|
|
level,
|
|
path: parent.path + " " + nodeString,
|
|
};
|
|
|
|
parent.children.push(node);
|
|
currentNode = node;
|
|
}
|
|
|
|
return tree;
|
|
}
|
|
|
|
/**
|
|
* Assert whether the provided container is slotted.
|
|
*/
|
|
function assertContainerSlotted(container) {
|
|
ok(container.isSlotted(), "Container is a slotted container");
|
|
ok(
|
|
container.elt.querySelector(".reveal-link"),
|
|
"Slotted container has a reveal link element"
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Check if the provided text can be matched anywhere in the text content for the provided
|
|
* container.
|
|
*/
|
|
function assertContainerHasText(container, expectedText) {
|
|
const textContent = container.elt.textContent;
|
|
ok(
|
|
textContent.includes(expectedText),
|
|
"Container has expected text: " + expectedText
|
|
);
|
|
}
|
|
|
|
function waitForMutation(inspector, type) {
|
|
return waitForNMutations(inspector, type, 1);
|
|
}
|
|
|
|
function waitForNMutations(inspector, type, count) {
|
|
info(`Expecting ${count} markupmutation of type ${type}`);
|
|
let receivedMutations = 0;
|
|
return new Promise(resolve => {
|
|
inspector.on("markupmutation", function onMutation(mutations) {
|
|
const validMutations = mutations.filter(m => m.type === type).length;
|
|
receivedMutations = receivedMutations + validMutations;
|
|
if (receivedMutations == count) {
|
|
inspector.off("markupmutation", onMutation);
|
|
resolve();
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Move the mouse on the content page at the x,y position and check the color displayed
|
|
* in the eyedropper label.
|
|
*
|
|
* @param {HighlighterTestFront} highlighterTestFront
|
|
* @param {Number} x
|
|
* @param {Number} y
|
|
* @param {String} expectedColor: Hexa string of the expected color
|
|
* @param {String} assertionDescription
|
|
*/
|
|
async function checkEyeDropperColorAt(
|
|
highlighterTestFront,
|
|
x,
|
|
y,
|
|
expectedColor,
|
|
assertionDescription
|
|
) {
|
|
info(`Move mouse to ${x},${y}`);
|
|
await safeSynthesizeMouseEventInContentPage(":root", x, y, {
|
|
type: "mousemove",
|
|
});
|
|
|
|
const colorValue = await highlighterTestFront.getEyeDropperColorValue();
|
|
is(colorValue, expectedColor, assertionDescription);
|
|
}
|
|
|
|
/**
|
|
* Delete the provided node front using the context menu in the markup view.
|
|
* Will resolve after the inspector UI was fully updated.
|
|
*
|
|
* @param {NodeFront} node
|
|
* The node front to delete.
|
|
* @param {Inspector} inspector
|
|
* The current inspector panel instance.
|
|
*/
|
|
async function deleteNodeWithContextMenu(node, inspector) {
|
|
const container = inspector.markup.getContainer(node);
|
|
|
|
const allMenuItems = openContextMenuAndGetAllItems(inspector, {
|
|
target: container.tagLine,
|
|
});
|
|
const menuItem = allMenuItems.find(item => item.id === "node-menu-delete");
|
|
const onInspectorUpdated = inspector.once("inspector-updated");
|
|
|
|
info("Clicking 'Delete Node' in the context menu.");
|
|
is(menuItem.disabled, false, "delete menu item is enabled");
|
|
menuItem.click();
|
|
|
|
// close the open context menu
|
|
EventUtils.synthesizeKey("KEY_Escape");
|
|
|
|
info("Waiting for inspector to update.");
|
|
await onInspectorUpdated;
|
|
|
|
// Since the mutations are sent asynchronously from the server, the
|
|
// inspector-updated event triggered by the deletion might happen before
|
|
// the mutation is received and the element is removed from the
|
|
// breadcrumbs. See bug 1284125.
|
|
if (inspector.breadcrumbs.indexOf(node) > -1) {
|
|
info("Crumbs haven't seen deletion. Waiting for breadcrumbs-updated.");
|
|
await inspector.once("breadcrumbs-updated");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Forces the content page to reflow and waits for the next repaint.
|
|
*/
|
|
function reflowContentPage() {
|
|
return SpecialPowers.spawn(gBrowser.selectedBrowser, [], async function () {
|
|
return new Promise(resolve => {
|
|
content.document.documentElement.offsetWidth;
|
|
content.requestAnimationFrame(resolve);
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Get all box-model regions' adjusted boxquads for the given element
|
|
* @param {String|Array} selector The node selector to target a given element
|
|
* @return {Promise<Object>} A promise that resolves with an object with each property of
|
|
* a box-model region, each of them being an object with the p1/p2/p3/p4 properties.
|
|
*/
|
|
async function getAllAdjustedQuadsForContentPageElement(
|
|
selector,
|
|
useTopWindowAsBoundary = true
|
|
) {
|
|
const selectors = Array.isArray(selector) ? selector : [selector];
|
|
|
|
const browsingContext =
|
|
selectors.length == 1
|
|
? gBrowser.selectedBrowser.browsingContext
|
|
: await getBrowsingContextInFrames(
|
|
gBrowser.selectedBrowser.browsingContext,
|
|
selectors.slice(0, -1)
|
|
);
|
|
|
|
const inBrowsingContextSelector = selectors.at(-1);
|
|
return SpecialPowers.spawn(
|
|
browsingContext,
|
|
[inBrowsingContextSelector, useTopWindowAsBoundary],
|
|
(_selector, _useTopWindowAsBoundary) => {
|
|
const { require } = ChromeUtils.importESModule(
|
|
"resource://devtools/shared/loader/Loader.sys.mjs"
|
|
);
|
|
const {
|
|
getAdjustedQuads,
|
|
} = require("resource://devtools/shared/layout/utils.js");
|
|
|
|
const node = content.document.querySelector(_selector);
|
|
|
|
const boundaryWindow = _useTopWindowAsBoundary ? content.top : content;
|
|
const regions = {};
|
|
for (const boxType of ["content", "padding", "border", "margin"]) {
|
|
regions[boxType] = getAdjustedQuads(boundaryWindow, node, boxType);
|
|
}
|
|
|
|
return regions;
|
|
}
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Assert that the box-model highlighter's current position corresponds to the
|
|
* given node boxquads.
|
|
*
|
|
* @param {HighlighterTestFront} highlighterTestFront
|
|
* @param {String} selector The node selector to get the boxQuads from
|
|
*/
|
|
async function isNodeCorrectlyHighlighted(highlighterTestFront, selector) {
|
|
const boxModel = await highlighterTestFront.getBoxModelStatus();
|
|
|
|
const useTopWindowAsBoundary = !!highlighterTestFront.parentFront.isTopLevel;
|
|
const regions = await getAllAdjustedQuadsForContentPageElement(
|
|
selector,
|
|
useTopWindowAsBoundary
|
|
);
|
|
|
|
for (const boxType of ["content", "padding", "border", "margin"]) {
|
|
const [quad] = regions[boxType];
|
|
for (const point in boxModel[boxType].points) {
|
|
is(
|
|
boxModel[boxType].points[point].x,
|
|
quad[point].x,
|
|
`${selector} ${boxType} point ${point} x coordinate is correct`
|
|
);
|
|
is(
|
|
boxModel[boxType].points[point].y,
|
|
quad[point].y,
|
|
`${selector} ${boxType} point ${point} y coordinate is correct`
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get the position and size of the measuring tool.
|
|
*
|
|
* @param {Object} Object returned by getHighlighterHelperFor()
|
|
* @return {Promise<Object>} A promise that resolves with an object containing
|
|
* the x, y, width, and height properties of the measuring tool which has
|
|
* been drawn on-screen
|
|
*/
|
|
async function getAreaRect({ getElementAttribute }) {
|
|
// The 'box-path' element holds the width and height of the
|
|
// measuring area as well as the position relative to its
|
|
// parent <g> element.
|
|
const d = await getElementAttribute("box-path", "d");
|
|
// The tool element itself is a <g> element grouping all paths.
|
|
// Though <g> elements do not have coordinates by themselves,
|
|
// therefore it is positioned using the 'transform' CSS property.
|
|
// So, in order to get the position of the measuring area, the
|
|
// coordinates need to be read from the translate() function.
|
|
const transform = await getElementAttribute("tool", "transform");
|
|
const reDir = /(\d+) (\d+)/g;
|
|
const reTransform = /(\d+),(\d+)/;
|
|
const coords = {
|
|
x: 0,
|
|
y: 0,
|
|
width: 0,
|
|
height: 0,
|
|
};
|
|
let match;
|
|
while ((match = reDir.exec(d))) {
|
|
let [, x, y] = match;
|
|
x = Number(x);
|
|
y = Number(y);
|
|
if (x < coords.x) {
|
|
coords.x = x;
|
|
}
|
|
if (y < coords.y) {
|
|
coords.y = y;
|
|
}
|
|
if (x > coords.width) {
|
|
coords.width = x;
|
|
}
|
|
if (y > coords.height) {
|
|
coords.height = y;
|
|
}
|
|
}
|
|
|
|
match = reTransform.exec(transform);
|
|
coords.x += Number(match[1]);
|
|
coords.y += Number(match[2]);
|
|
|
|
return coords;
|
|
}
|
|
|
|
/**
|
|
* Follow a sequence of keys to be pressed in the markup view search input and check
|
|
* that the input value and the suggestions are the expected ones.
|
|
*
|
|
* @param {Inspector} inspector
|
|
* @param {Array} expected: This is the array describing the sequence.
|
|
* Each item hasthe following shape:
|
|
* - key {String}: The keyboard key that is pressed
|
|
* - value {String}: The expected input value after the key was pressed
|
|
* - suggestions {Array<String>}: An array of the labels in the autocomplete popup.
|
|
* Pass an empty array if the popup should be hidden.
|
|
*/
|
|
async function checkMarkupSearchSuggestions(inspector, expected) {
|
|
const searchBox = inspector.searchBox;
|
|
const popup = inspector.searchSuggestions.searchPopup;
|
|
|
|
await focusSearchBoxUsingShortcut(inspector.panelWin);
|
|
|
|
for (const { key, suggestions, value } of expected) {
|
|
info("Pressing " + key + " to get " + JSON.stringify(suggestions));
|
|
|
|
const command = once(searchBox, "input");
|
|
const onSearchProcessingDone =
|
|
inspector.searchSuggestions.once("processing-done");
|
|
EventUtils.synthesizeKey(key, {}, inspector.panelWin);
|
|
await command;
|
|
|
|
is(searchBox.value, value, "search input has expected value");
|
|
|
|
info("Waiting for search query to complete");
|
|
await onSearchProcessingDone;
|
|
|
|
info(
|
|
"Query completed. Performing checks for input '" +
|
|
searchBox.value +
|
|
"' - key pressed: " +
|
|
key
|
|
);
|
|
|
|
if (suggestions.length === 0) {
|
|
ok(!popup.isOpen, `There is no suggestion for "${searchBox.value}"`);
|
|
} else {
|
|
Assert.deepEqual(
|
|
popup.getItems().map(item => item.label),
|
|
suggestions,
|
|
`Suggestions are correct for "${searchBox.value}"`
|
|
);
|
|
}
|
|
}
|
|
}
|