/* 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/. */ "use strict"; /* eslint no-unused-vars: [2, {"vars": "local"}] */ /* globals getHighlighterTestFront, openToolboxForTab, gBrowser */ /* import-globals-from ../../shared/test/shared-head.js */ var { getInplaceEditorForSpan: inplaceEditor, } = require("resource://devtools/client/shared/inplace-editor.js"); // This file contains functions related to the inspector that are also of interest to // other test directores as well. /** * Open the toolbox, with the inspector tool visible. * @param {String} hostType Optional hostType, as defined in Toolbox.HostType * @return {Promise} A promise that resolves when the inspector is ready.The promise * resolves with an object containing the following properties: * - toolbox * - inspector * - highlighterTestFront */ var openInspector = async function (hostType) { info("Opening the inspector"); const toolbox = await openToolboxForTab( gBrowser.selectedTab, "inspector", hostType ); const inspector = toolbox.getPanel("inspector"); const highlighterTestFront = await getHighlighterTestFront(toolbox); return { toolbox, inspector, highlighterTestFront }; }; /** * Open the toolbox, with the inspector tool visible, and the one of the sidebar * tabs selected. * * @param {String} id * The ID of the sidebar tab to be opened * @return {Promise} A promise that resolves when the inspector is ready and the tab is * visible and ready. The promise resolves with an object containing the * following properties: * - toolbox * - inspector * - highlighterTestFront */ var openInspectorSidebarTab = async function (id) { const { toolbox, inspector, highlighterTestFront } = await openInspector(); info("Selecting the " + id + " sidebar"); const onSidebarSelect = inspector.sidebar.once("select"); if (id === "layoutview") { // The layout view should wait until the box-model and grid-panel are ready. const onBoxModelViewReady = inspector.once("boxmodel-view-updated"); const onGridPanelReady = inspector.once("grid-panel-updated"); inspector.sidebar.select(id); await onBoxModelViewReady; await onGridPanelReady; } else { inspector.sidebar.select(id); } await onSidebarSelect; return { toolbox, inspector, highlighterTestFront, }; }; /** * Open the toolbox, with the inspector tool visible, and the rule-view * sidebar tab selected. * * @return a promise that resolves when the inspector is ready and the rule view * is visible and ready */ async function openRuleView() { const { inspector, toolbox, highlighterTestFront } = await openInspector(); const ruleViewPanel = inspector.getPanel("ruleview"); await ruleViewPanel.readyPromise; const view = ruleViewPanel.view; // Replace the view to use a custom debounce function that can be triggered manually // through an additional ".flush()" property. view.debounce = manualDebounce(); return { toolbox, inspector, highlighterTestFront, view, }; } /** * Open the toolbox, with the inspector tool visible, and the computed-view * sidebar tab selected. * * @return a promise that resolves when the inspector is ready and the computed * view is visible and ready */ function openComputedView() { return openInspectorSidebarTab("computedview").then(data => { const view = data.inspector.getPanel("computedview").computedView; return { toolbox: data.toolbox, inspector: data.inspector, highlighterTestFront: data.highlighterTestFront, view, }; }); } /** * Open the toolbox, with the inspector tool visible, and the changes view * sidebar tab selected. * * @return a promise that resolves when the inspector is ready and the changes * view is visible and ready */ function openChangesView() { return openInspectorSidebarTab("changesview").then(data => { return { toolbox: data.toolbox, inspector: data.inspector, highlighterTestFront: data.highlighterTestFront, view: data.inspector.getPanel("changesview"), }; }); } /** * Open the toolbox, with the inspector tool visible, and the layout view * sidebar tab selected to display the box model view with properties. * * @return {Promise} a promise that resolves when the inspector is ready and the layout * view is visible and ready. */ function openLayoutView() { return openInspectorSidebarTab("layoutview").then(data => { return { toolbox: data.toolbox, inspector: data.inspector, boxmodel: data.inspector.getPanel("boxmodel"), gridInspector: data.inspector.getPanel("layoutview").gridInspector, flexboxInspector: data.inspector.getPanel("layoutview").flexboxInspector, layoutView: data.inspector.getPanel("layoutview"), highlighterTestFront: data.highlighterTestFront, }; }); } /** * Select the rule view sidebar tab on an already opened inspector panel. * * @param {InspectorPanel} inspector * The opened inspector panel * @return {CssRuleView} the rule view */ function selectRuleView(inspector) { return inspector.getPanel("ruleview").view; } /** * Select the computed view sidebar tab on an already opened inspector panel. * * @param {InspectorPanel} inspector * The opened inspector panel * @return {CssComputedView} the computed view */ function selectComputedView(inspector) { inspector.sidebar.select("computedview"); return inspector.getPanel("computedview").computedView; } /** * Select the changes view sidebar tab on an already opened inspector panel. * * @param {InspectorPanel} inspector * The opened inspector panel * @return {ChangesView} the changes view */ function selectChangesView(inspector) { inspector.sidebar.select("changesview"); return inspector.getPanel("changesview"); } /** * Select the layout view sidebar tab on an already opened inspector panel. * * @param {InspectorPanel} inspector * @return {BoxModel} the box model */ function selectLayoutView(inspector) { inspector.sidebar.select("layoutview"); return inspector.getPanel("boxmodel"); } /** * Get the NodeFront for a node that matches a given css selector, via the * protocol. * @param {String|NodeFront} selector * @param {InspectorPanel} inspector The instance of InspectorPanel currently * loaded in the toolbox * @return {Promise} Resolves to the NodeFront instance */ function getNodeFront(selector, { walker }) { if (selector._form) { return selector; } return walker.querySelector(walker.rootNode, selector); } /** * Set the inspector's current selection to the first match of the given css * selector * * @param {String|NodeFront} selector * @param {InspectorPanel} inspector * The instance of InspectorPanel currently loaded in the toolbox. * @param {String} reason * Defaults to "test" which instructs the inspector not to highlight the * node upon selection. * @param {Boolean} isSlotted * Is the selection representing the slotted version the node. * @return {Promise} Resolves when the inspector is updated with the new node */ var selectNode = async function ( selector, inspector, reason = "test", isSlotted ) { info("Selecting the node for '" + selector + "'"); const nodeFront = await getNodeFront(selector, inspector); const updated = inspector.once("inspector-updated"); const { ELEMENT_NODE, } = require("resource://devtools/shared/dom-node-constants.js"); const onSelectionCssSelectorsUpdated = nodeFront?.nodeType == ELEMENT_NODE ? inspector.once("selection-css-selectors-updated") : null; inspector.selection.setNodeFront(nodeFront, { reason, isSlotted }); await updated; await onSelectionCssSelectorsUpdated; }; /** * Using the markupview's _waitForChildren function, wait for all queued * children updates to be handled. * @param {InspectorPanel} inspector The instance of InspectorPanel currently * loaded in the toolbox * @return a promise that resolves when all queued children updates have been * handled */ function waitForChildrenUpdated({ markup }) { info("Waiting for queued children updates to be handled"); return new Promise(resolve => { markup._waitForChildren().then(() => { executeSoon(resolve); }); }); } // The expand all operation of the markup-view calls itself recursively and // there's not one event we can wait for to know when it's done, so use this // helper function to wait until all recursive children updates are done. async function waitForMultipleChildrenUpdates(inspector) { // As long as child updates are queued up while we wait for an update already // wait again if ( inspector.markup._queuedChildUpdates && inspector.markup._queuedChildUpdates.size ) { await waitForChildrenUpdated(inspector); return waitForMultipleChildrenUpdates(inspector); } return null; } /** * Expand the provided markup container programmatically and wait for all * children to update. */ async function expandContainer(inspector, container) { await inspector.markup.expandNode(container.node); await waitForMultipleChildrenUpdates(inspector); } /** * Get the NodeFront for a node that matches a given css selector inside a * given iframe. * * @param {Array} selectors * Arrays of CSS selectors from the root document to the node. * The last CSS selector of the array is for the node in its frame doc. * The before-last CSS selector is for the frame in its parent frame, etc... * Ex: ["frame.first-frame", ..., "frame.last-frame", ".target-node"] * @param {InspectorPanel} inspector * See `selectNode` * @return {NodeFront} Resolves the corresponding node front. */ async function getNodeFrontInFrames(selectors, inspector) { let walker = inspector.walker; let rootNode = walker.rootNode; // clone the array since `selectors` could be used from callsite after. selectors = [...selectors]; // Extract the last selector from the provided array of selectors. const nodeSelector = selectors.pop(); // Remaining selectors should all be frame selectors. Renaming for clarity. const frameSelectors = selectors; info("Loop through all frame selectors"); for (const frameSelector of frameSelectors) { const url = walker.targetFront.url; info(`Find the frame element for selector ${frameSelector} in ${url}`); const frameNodeFront = await walker.querySelector(rootNode, frameSelector); // If needed, connect to the corresponding frame target. // Otherwise, reuse the current targetFront. let frameTarget = frameNodeFront.targetFront; if (frameNodeFront.useChildTargetToFetchChildren) { info("Connect to frame and retrieve the targetFront"); frameTarget = await frameNodeFront.connectToFrame(); } walker = (await frameTarget.getFront("inspector")).walker; if (frameNodeFront.useChildTargetToFetchChildren) { // For frames or browser elements, use the walker's rootNode. rootNode = walker.rootNode; } else { // For same-process frames, select the document front as the root node. // It is a different node from the walker's rootNode. info("Retrieve the children of the frame to find the document node"); const { nodes } = await walker.children(frameNodeFront); rootNode = nodes.find(n => n.nodeType === Node.DOCUMENT_NODE); } } return walker.querySelector(rootNode, nodeSelector); } /** * Helper to select a node in the markup-view, in a nested tree of * frames/browser elements. The iframes can either be remote or same-process. * * Note: "frame" will refer to either "frame" or "browser" in the documentation * and method. * * @param {Array} selectors * Arrays of CSS selectors from the root document to the node. * The last CSS selector of the array is for the node in its frame doc. * The before-last CSS selector is for the frame in its parent frame, etc... * Ex: ["frame.first-frame", ..., "frame.last-frame", ".target-node"] * @param {InspectorPanel} inspector * See `selectNode` * @param {String} reason * See `selectNode` * @param {Boolean} isSlotted * See `selectNode` * @return {NodeFront} The selected node front. */ async function selectNodeInFrames( selectors, inspector, reason = "test", isSlotted ) { const nodeFront = await getNodeFrontInFrames(selectors, inspector); await selectNode(nodeFront, inspector, reason, isSlotted); return nodeFront; } /** * Create a throttling function that can be manually "flushed". This is to replace the * use of the `debounce` function from `devtools/client/inspector/shared/utils.js`, which * has a setTimeout that can cause intermittents. * @return {Function} This function has the same function signature as debounce, but * the property `.flush()` has been added for flushing out any * debounced calls. */ function manualDebounce() { let calls = []; function debounce(func, wait, scope) { return function () { const existingCall = calls.find(call => call.func === func); if (existingCall) { existingCall.args = arguments; } else { calls.push({ func, wait, scope, args: arguments }); } }; } debounce.flush = function () { calls.forEach(({ func, scope, args }) => func.apply(scope, args)); calls = []; }; return debounce; } /** * Get the requested rule style property from the current browser. * * @param {Number} styleSheetIndex * @param {Number} ruleIndex * @param {String} name * @return {String} The value, if found, null otherwise */ async function getRulePropertyValue(styleSheetIndex, ruleIndex, name) { return SpecialPowers.spawn( gBrowser.selectedBrowser, [styleSheetIndex, ruleIndex, name], (styleSheetIndexChild, ruleIndexChild, nameChild) => { let value = null; info( "Getting the value for property name " + nameChild + " in sheet " + styleSheetIndexChild + " and rule " + ruleIndexChild ); const sheet = content.document.styleSheets[styleSheetIndexChild]; if (sheet) { const rule = sheet.cssRules[ruleIndexChild]; if (rule) { value = rule.style.getPropertyValue(nameChild); } } return value; } ); } /** * Get the requested computed style property from the current browser. * * @param {String} selector * The selector used to obtain the element. * @param {String} pseudo * pseudo id to query, or null. * @param {String} propName * name of the property. */ async function getComputedStyleProperty(selector, pseudo, propName) { return SpecialPowers.spawn( gBrowser.selectedBrowser, [selector, pseudo, propName], (selectorChild, pseudoChild, propNameChild) => { const element = content.document.querySelector(selectorChild); return content.document.defaultView .getComputedStyle(element, pseudoChild) .getPropertyValue(propNameChild); } ); } /** * Wait until the requested computed style property has the * expected value in the the current browser. * * @param {String} selector * The selector used to obtain the element. * @param {String} pseudo * pseudo id to query, or null. * @param {String} propName * name of the property. * @param {String} expected * expected value of property */ async function waitForComputedStyleProperty( selector, pseudo, propName, expected ) { return SpecialPowers.spawn( gBrowser.selectedBrowser, [selector, pseudo, propName, expected], (selectorChild, pseudoChild, propNameChild, expectedChild) => { const element = content.document.querySelector(selectorChild); return ContentTaskUtils.waitForCondition(() => { const value = content.document.defaultView .getComputedStyle(element, pseudoChild) .getPropertyValue(propNameChild); return value === expectedChild; }); } ); } /** * Given an inplace editable element, click to switch it to edit mode, wait for * focus * * @return a promise that resolves to the inplace-editor element when ready */ var focusEditableField = async function ( ruleView, editable, xOffset = 1, yOffset = 1, options = {} ) { const onFocus = once(editable.parentNode, "focus", true); info("Clicking on editable field to turn to edit mode"); if (options.type === undefined) { // "mousedown" and "mouseup" flushes any pending layout. Therefore, // if the caller wants to click an element, e.g., closebrace to add new // property, we need to guarantee that the element is clicked here even // if it's moved by flushing the layout because whether the UI is useful // or not when there is pending reflow is not scope of the tests. options.type = "mousedown"; EventUtils.synthesizeMouse( editable, xOffset, yOffset, options, editable.ownerGlobal ); options.type = "mouseup"; EventUtils.synthesizeMouse( editable, xOffset, yOffset, options, editable.ownerGlobal ); } else { EventUtils.synthesizeMouse( editable, xOffset, yOffset, options, editable.ownerGlobal ); } await onFocus; info("Editable field gained focus, returning the input field now"); const onEdit = inplaceEditor(editable.ownerDocument.activeElement); return onEdit; }; /** * Get the DOMNode for a css rule in the rule-view that corresponds to the given * selector. * * @param {CssRuleView} view * The instance of the rule-view panel * @param {String} selectorText * The selector in the rule-view for which the rule * object is wanted * @param {Number} index * If there are more than 1 rule with the same selector, you may pass a * index to determine which of the rules you want. * @return {DOMNode} */ function getRuleViewRule(view, selectorText, index = 0) { let rule; let pos = 0; for (const r of view.styleDocument.querySelectorAll(".ruleview-rule")) { const selector = r.querySelector( ".ruleview-selectorcontainer, " + ".ruleview-selector-matched" ); if (selector && selector.textContent === selectorText) { if (index == pos) { rule = r; break; } pos++; } } return rule; } /** * Get references to the name and value span nodes corresponding to a given * selector and property name in the rule-view. * * @param {CssRuleView} view * The instance of the rule-view panel * @param {String} selectorText * The selector in the rule-view to look for the property in * @param {String} propertyName * The name of the property * @param {Object=} options * @param {Boolean=} options.wait * When true, returns a promise which waits until a valid rule view * property can be retrieved for the provided selectorText & propertyName. * Defaults to false. * @return {Object} An object like {nameSpan: DOMNode, valueSpan: DOMNode} */ function getRuleViewProperty(view, selectorText, propertyName, options = {}) { if (options.wait) { return waitFor(() => _syncGetRuleViewProperty(view, selectorText, propertyName) ); } return _syncGetRuleViewProperty(view, selectorText, propertyName); } function _syncGetRuleViewProperty(view, selectorText, propertyName) { const rule = getRuleViewRule(view, selectorText); if (!rule) { return null; } // Look for the propertyName in that rule element for (const p of rule.querySelectorAll(".ruleview-property")) { const nameSpan = p.querySelector(".ruleview-propertyname"); const valueSpan = p.querySelector(".ruleview-propertyvalue"); if (nameSpan.textContent === propertyName) { return { nameSpan, valueSpan }; } } return null; } /** * Get the text value of the property corresponding to a given selector and name * in the rule-view * * @param {CssRuleView} view * The instance of the rule-view panel * @param {String} selectorText * The selector in the rule-view to look for the property in * @param {String} propertyName * The name of the property * @return {String} The property value */ function getRuleViewPropertyValue(view, selectorText, propertyName) { return getRuleViewProperty(view, selectorText, propertyName).valueSpan .textContent; } /** * Get a reference to the selector DOM element corresponding to a given selector * in the rule-view * * @param {CssRuleView} view * The instance of the rule-view panel * @param {String} selectorText * The selector in the rule-view to look for * @return {DOMNode} The selector DOM element */ function getRuleViewSelector(view, selectorText) { const rule = getRuleViewRule(view, selectorText); return rule.querySelector(".ruleview-selector, .ruleview-selector-matched"); } /** * Get a rule-link from the rule-view given its index * * @param {CssRuleView} view * The instance of the rule-view panel * @param {Number} index * The index of the link to get * @return {DOMNode} The link if any at this index */ function getRuleViewLinkByIndex(view, index) { const links = view.styleDocument.querySelectorAll(".ruleview-rule-source"); return links[index]; } /** * Get rule-link text from the rule-view given its index * * @param {CssRuleView} view * The instance of the rule-view panel * @param {Number} index * The index of the link to get * @return {String} The string at this index */ function getRuleViewLinkTextByIndex(view, index) { const link = getRuleViewLinkByIndex(view, index); return link.querySelector(".ruleview-rule-source-label").textContent; } /** * Click on a rule-view's close brace to focus a new property name editor * * @param {RuleEditor} ruleEditor * An instance of RuleEditor that will receive the new property * @return a promise that resolves to the newly created editor when ready and * focused */ var focusNewRuleViewProperty = async function (ruleEditor) { info("Clicking on a close ruleEditor brace to start editing a new property"); // Use bottom alignment to avoid scrolling out of the parent element area. ruleEditor.closeBrace.scrollIntoView(false); const editor = await focusEditableField( ruleEditor.ruleView, ruleEditor.closeBrace ); is( inplaceEditor(ruleEditor.newPropSpan), editor, "Focused editor is the new property editor." ); return editor; }; /** * Create a new property name in the rule-view, focusing a new property editor * by clicking on the close brace, and then entering the given text. * Keep in mind that the rule-view knows how to handle strings with multiple * properties, so the input text may be like: "p1:v1;p2:v2;p3:v3". * * @param {RuleEditor} ruleEditor * The instance of RuleEditor that will receive the new property(ies) * @param {String} inputValue * The text to be entered in the new property name field * @return a promise that resolves when the new property name has been entered * and once the value field is focused */ var createNewRuleViewProperty = async function (ruleEditor, inputValue) { info("Creating a new property editor"); const editor = await focusNewRuleViewProperty(ruleEditor); info("Entering the value " + inputValue); editor.input.value = inputValue; info("Submitting the new value and waiting for value field focus"); const onFocus = once(ruleEditor.element, "focus", true); EventUtils.synthesizeKey( "VK_RETURN", {}, ruleEditor.element.ownerDocument.defaultView ); await onFocus; }; /** * Set the search value for the rule-view filter styles search box. * * @param {CssRuleView} view * The instance of the rule-view panel * @param {String} searchValue * The filter search value * @return a promise that resolves when the rule-view is filtered for the * search term */ var setSearchFilter = async function (view, searchValue) { info('Setting filter text to "' + searchValue + '"'); const searchField = view.searchField; searchField.focus(); for (const key of searchValue.split("")) { EventUtils.synthesizeKey(key, {}, view.styleWindow); } await view.inspector.once("ruleview-filtered"); }; /** * Flatten all context menu items into a single array to make searching through * it easier. */ function buildContextMenuItems(menu) { const allItems = [].concat.apply( [], menu.items.map(function addItem(item) { if (item.submenu) { return addItem(item.submenu.items); } return item; }) ); return allItems; } /** * Open the style editor context menu and return all of it's items in a flat array * @param {CssRuleView} view * The instance of the rule-view panel * @return An array of MenuItems */ function openStyleContextMenuAndGetAllItems(view, target) { const menu = view.contextMenu._openMenu({ target }); return buildContextMenuItems(menu); } /** * Open the inspector menu and return all of it's items in a flat array * @param {InspectorPanel} inspector * @param {Object} options to pass into openMenu * @return An array of MenuItems */ function openContextMenuAndGetAllItems(inspector, options) { const menu = inspector.markup.contextMenu._openMenu(options); return buildContextMenuItems(menu); } /** * Wait until the elements the given selectors indicate come to have the visited state. * * @param {Tab} tab * The tab where the elements on. * @param {Array} selectors * The selectors for the elements. */ async function waitUntilVisitedState(tab, selectors) { await asyncWaitUntil(async () => { const hasVisitedState = await ContentTask.spawn( tab.linkedBrowser, selectors, args => { const ELEMENT_STATE_VISITED = 1 << 19; for (const selector of args) { const target = content.wrappedJSObject.document.querySelector(selector); if ( !( target && InspectorUtils.getContentState(target) & ELEMENT_STATE_VISITED ) ) { return false; } } return true; } ); return hasVisitedState; }); } /** * Return wether or not the passed selector matches an element in the content page. * * @param {string} selector * @returns Promise */ function hasMatchingElementInContentPage(selector) { return SpecialPowers.spawn( gBrowser.selectedBrowser, [selector], function (innerSelector) { return content.document.querySelector(innerSelector) !== null; } ); } /** * Return the number of elements matching the passed selector. * * @param {string} selector * @returns Promise the number of matching elements */ function getNumberOfMatchingElementsInContentPage(selector) { return SpecialPowers.spawn( gBrowser.selectedBrowser, [selector], function (innerSelector) { return content.document.querySelectorAll(innerSelector).length; } ); } /** * Get the property of an element in the content page * * @param {string} selector: The selector to get the element we want the property of * @param {string} propertyName: The name of the property we want the value of * @returns {Promise} A promise that returns with the value of the property for the element */ function getContentPageElementProperty(selector, propertyName) { return SpecialPowers.spawn( gBrowser.selectedBrowser, [selector, propertyName], function (innerSelector, innerPropertyName) { return content.document.querySelector(innerSelector)[innerPropertyName]; } ); } /** * Set the property of an element in the content page * * @param {string} selector: The selector to get the element we want to set the property on * @param {string} propertyName: The name of the property we want to set * @param {string} propertyValue: The value that is going to be assigned to the property * @returns {Promise} */ function setContentPageElementProperty(selector, propertyName, propertyValue) { return SpecialPowers.spawn( gBrowser.selectedBrowser, [selector, propertyName, propertyValue], function (innerSelector, innerPropertyName, innerPropertyValue) { content.document.querySelector(innerSelector)[innerPropertyName] = innerPropertyValue; } ); } /** * Get all the attributes for a DOM Node living in the content page. * * @param {String} selector The node selector * @returns {Array} An array of {name, value} objects. */ async function getContentPageElementAttributes(selector) { return SpecialPowers.spawn( gBrowser.selectedBrowser, [selector], _selector => { const node = content.document.querySelector(_selector); return Array.from(node.attributes).map(({ name, value }) => ({ name, value, })); } ); } /** * Get an attribute on a DOM Node living in the content page. * * @param {String} selector The node selector * @param {String} attribute The attribute name * @return {String} value The attribute value */ async function getContentPageElementAttribute(selector, attribute) { return SpecialPowers.spawn( gBrowser.selectedBrowser, [selector, attribute], (_selector, _attribute) => { return content.document.querySelector(_selector).getAttribute(_attribute); } ); } /** * Set an attribute on a DOM Node living in the content page. * * @param {String} selector The node selector * @param {String} attribute The attribute name * @param {String} value The attribute value */ async function setContentPageElementAttribute(selector, attribute, value) { return SpecialPowers.spawn( gBrowser.selectedBrowser, [selector, attribute, value], (_selector, _attribute, _value) => { content.document .querySelector(_selector) .setAttribute(_attribute, _value); } ); } /** * Remove an attribute from a DOM Node living in the content page. * * @param {String} selector The node selector * @param {String} attribute The attribute name */ async function removeContentPageElementAttribute(selector, attribute) { return SpecialPowers.spawn( gBrowser.selectedBrowser, [selector, attribute], (_selector, _attribute) => { content.document.querySelector(_selector).removeAttribute(_attribute); } ); }