diff options
Diffstat (limited to 'devtools/client/inspector/test/head.js')
-rw-r--r-- | devtools/client/inspector/test/head.js | 1459 |
1 files changed, 1459 insertions, 0 deletions
diff --git a/devtools/client/inspector/test/head.js b/devtools/client/inspector/test/head.js new file mode 100644 index 0000000000..ee90687a40 --- /dev/null +++ b/devtools/client/inspector/test/head.js @@ -0,0 +1,1459 @@ +/* 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 { LocalizationHelper } = require("resource://devtools/shared/l10n.js"); +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 => { + 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 rule editor from the rule-view given its index + * + * @param {CssRuleView} view + * The instance of the rule-view panel + * @param {Number} childrenIndex + * The children index of the element to get + * @param {Number} nodeIndex + * The child node index of the element to get + * @return {DOMNode} The rule editor if any at this index + */ +function getRuleViewRuleEditor(view, childrenIndex, nodeIndex) { + return nodeIndex !== undefined + ? view.element.children[childrenIndex].childNodes[nodeIndex]._ruleEditor + : view.element.children[childrenIndex]._ruleEditor; +} + +/** + * 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<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` + ); + } + } +} |