/* 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 * @return {Promise} promise that resolves when both the "picker-node-hovered" and * "highlighter-shown" events are emitted. */ async function hoverElement(inspector, selector, x, y) { 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, { type: "mousemove" }, browsingContext ); } else { BrowserTestUtils.synthesizeMouse( selector, x, y, { 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 sould 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"); } /** * 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(".ruleview-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} 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} 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 element. const d = await getElementAttribute("box-path", "d"); // The tool element itself is a element grouping all paths. // Though 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; }