tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

shared-head.js (38390B)


      1 /* This Source Code Form is subject to the terms of the Mozilla Public
      2 * License, v. 2.0. If a copy of the MPL was not distributed with this
      3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      4 
      5 "use strict";
      6 
      7 /* eslint no-unused-vars: [2, {"vars": "local"}] */
      8 /* globals getHighlighterTestFront, openToolboxForTab, gBrowser */
      9 /* import-globals-from ../../shared/test/shared-head.js */
     10 
     11 var {
     12  getInplaceEditorForSpan: inplaceEditor,
     13 } = require("resource://devtools/client/shared/inplace-editor.js");
     14 
     15 // This file contains functions related to the inspector that are also of interest to
     16 // other test directores as well.
     17 
     18 /**
     19 * Open the toolbox, with the inspector tool visible.
     20 *
     21 * @param {string} hostType Optional hostType, as defined in Toolbox.HostType
     22 * @return {Promise} A promise that resolves when the inspector is ready.The promise
     23 *         resolves with an object containing the following properties:
     24 *           - toolbox
     25 *           - inspector
     26 *           - highlighterTestFront
     27 */
     28 var openInspector = async function (hostType) {
     29  info("Opening the inspector");
     30 
     31  const toolbox = await openToolboxForTab(
     32    gBrowser.selectedTab,
     33    "inspector",
     34    hostType
     35  );
     36  const inspector = toolbox.getPanel("inspector");
     37 
     38  const highlighterTestFront = await getHighlighterTestFront(toolbox);
     39 
     40  return { toolbox, inspector, highlighterTestFront };
     41 };
     42 
     43 /**
     44 * Open the toolbox, with the inspector tool visible, and the one of the sidebar
     45 * tabs selected.
     46 *
     47 * @param {string} id
     48 *        The ID of the sidebar tab to be opened
     49 * @return {Promise<object>} A promise that resolves when the inspector is ready and the tab is
     50 *         visible and ready. The promise resolves with an object containing the
     51 *         following properties:
     52 *           - toolbox
     53 *           - inspector
     54 *           - highlighterTestFront
     55 */
     56 var openInspectorSidebarTab = async function (id) {
     57  const { toolbox, inspector, highlighterTestFront } = await openInspector();
     58 
     59  info("Selecting the " + id + " sidebar");
     60 
     61  const onSidebarSelect = inspector.sidebar.once("select");
     62  if (id === "layoutview") {
     63    // The layout view should wait until the box-model and grid-panel are ready.
     64    const onBoxModelViewReady = inspector.once("boxmodel-view-updated");
     65    const onGridPanelReady = inspector.once("grid-panel-updated");
     66    inspector.sidebar.select(id);
     67    await onBoxModelViewReady;
     68    await onGridPanelReady;
     69  } else {
     70    inspector.sidebar.select(id);
     71  }
     72  await onSidebarSelect;
     73 
     74  return {
     75    toolbox,
     76    inspector,
     77    highlighterTestFront,
     78  };
     79 };
     80 
     81 /**
     82 * Open the toolbox, with the inspector tool visible, and the rule-view
     83 * sidebar tab selected.
     84 *
     85 * @param {object} options
     86 * @param {boolean} options.overrideDebounce: Whether to replace the rule view debounce
     87 *        method with manual debounce (requires explicit calls to trigger the debounced calls).
     88 *        Defaults to true.
     89 * @return a promise that resolves when the inspector is ready and the rule view
     90 * is visible and ready
     91 */
     92 async function openRuleView({ overrideDebounce = true } = {}) {
     93  const { inspector, toolbox, highlighterTestFront } = await openInspector();
     94 
     95  const ruleViewPanel = inspector.getPanel("ruleview");
     96  await ruleViewPanel.readyPromise;
     97  const view = ruleViewPanel.view;
     98 
     99  if (overrideDebounce) {
    100    // Replace the view to use a custom debounce function that can be triggered manually
    101    // through an additional ".flush()" property.
    102    view.debounce = manualDebounce();
    103  }
    104 
    105  return {
    106    toolbox,
    107    inspector,
    108    highlighterTestFront,
    109    view,
    110  };
    111 }
    112 
    113 /**
    114 * Open the toolbox, with the inspector tool visible, and the computed-view
    115 * sidebar tab selected.
    116 *
    117 * @return a promise that resolves when the inspector is ready and the computed
    118 * view is visible and ready
    119 */
    120 function openComputedView() {
    121  return openInspectorSidebarTab("computedview").then(data => {
    122    const view = data.inspector.getPanel("computedview").computedView;
    123 
    124    return {
    125      toolbox: data.toolbox,
    126      inspector: data.inspector,
    127      highlighterTestFront: data.highlighterTestFront,
    128      view,
    129    };
    130  });
    131 }
    132 
    133 /**
    134 * Open the toolbox, with the inspector tool visible, and the changes view
    135 * sidebar tab selected.
    136 *
    137 * @return a promise that resolves when the inspector is ready and the changes
    138 * view is visible and ready
    139 */
    140 function openChangesView() {
    141  return openInspectorSidebarTab("changesview").then(data => {
    142    return {
    143      toolbox: data.toolbox,
    144      inspector: data.inspector,
    145      highlighterTestFront: data.highlighterTestFront,
    146      view: data.inspector.getPanel("changesview"),
    147    };
    148  });
    149 }
    150 
    151 /**
    152 * Open the toolbox, with the inspector tool visible, and the layout view
    153 * sidebar tab selected to display the box model view with properties.
    154 *
    155 * @return {Promise} a promise that resolves when the inspector is ready and the layout
    156 *         view is visible and ready.
    157 */
    158 function openLayoutView() {
    159  return openInspectorSidebarTab("layoutview").then(data => {
    160    return {
    161      toolbox: data.toolbox,
    162      inspector: data.inspector,
    163      boxmodel: data.inspector.getPanel("boxmodel"),
    164      gridInspector: data.inspector.getPanel("layoutview").gridInspector,
    165      flexboxInspector: data.inspector.getPanel("layoutview").flexboxInspector,
    166      layoutView: data.inspector.getPanel("layoutview"),
    167      highlighterTestFront: data.highlighterTestFront,
    168    };
    169  });
    170 }
    171 
    172 /**
    173 * Select the rule view sidebar tab on an already opened inspector panel.
    174 *
    175 * @param {InspectorPanel} inspector
    176 *        The opened inspector panel
    177 * @return {CssRuleView} the rule view
    178 */
    179 function selectRuleView(inspector) {
    180  return inspector.getPanel("ruleview").view;
    181 }
    182 
    183 /**
    184 * Select the computed view sidebar tab on an already opened inspector panel.
    185 *
    186 * @param {InspectorPanel} inspector
    187 *        The opened inspector panel
    188 * @return {CssComputedView} the computed view
    189 */
    190 function selectComputedView(inspector) {
    191  inspector.sidebar.select("computedview");
    192  return inspector.getPanel("computedview").computedView;
    193 }
    194 
    195 /**
    196 * Select the changes view sidebar tab on an already opened inspector panel.
    197 *
    198 * @param {InspectorPanel} inspector
    199 *        The opened inspector panel
    200 * @return {ChangesView} the changes view
    201 */
    202 function selectChangesView(inspector) {
    203  inspector.sidebar.select("changesview");
    204  return inspector.getPanel("changesview");
    205 }
    206 
    207 /**
    208 * Select the layout view sidebar tab on an already opened inspector panel.
    209 *
    210 * @param  {InspectorPanel} inspector
    211 * @return {BoxModel} the box model
    212 */
    213 function selectLayoutView(inspector) {
    214  inspector.sidebar.select("layoutview");
    215  return inspector.getPanel("boxmodel");
    216 }
    217 
    218 /**
    219 * Get the NodeFront for a node that matches a given css selector, via the
    220 * protocol.
    221 *
    222 * @param {string | NodeFront} selector
    223 * @param {InspectorPanel} inspector The instance of InspectorPanel currently
    224 * loaded in the toolbox
    225 * @return {Promise} Resolves to the NodeFront instance
    226 */
    227 function getNodeFront(selector, { walker }) {
    228  if (selector._form) {
    229    return selector;
    230  }
    231  return walker.querySelector(walker.rootNode, selector);
    232 }
    233 
    234 /**
    235 * Set the inspector's current selection to the first match of the given css
    236 * selector
    237 *
    238 * @param {string | NodeFront} selector
    239 * @param {InspectorPanel} inspector
    240 *        The instance of InspectorPanel currently loaded in the toolbox.
    241 * @param {string} reason
    242 *        Defaults to "test" which instructs the inspector not to highlight the
    243 *        node upon selection.
    244 * @param {boolean} isSlotted
    245 *        Is the selection representing the slotted version the node.
    246 * @return {Promise} Resolves when the inspector is updated with the new node
    247 */
    248 var selectNode = async function (
    249  selector,
    250  inspector,
    251  reason = "test",
    252  isSlotted
    253 ) {
    254  info("Selecting the node for '" + selector + "'");
    255  const nodeFront = await getNodeFront(selector, inspector);
    256  const updated = inspector.once("inspector-updated");
    257 
    258  const {
    259    ELEMENT_NODE,
    260  } = require("resource://devtools/shared/dom-node-constants.js");
    261  const onSelectionCssSelectorsUpdated =
    262    nodeFront?.nodeType == ELEMENT_NODE
    263      ? inspector.once("selection-css-selectors-updated")
    264      : null;
    265 
    266  inspector.selection.setNodeFront(nodeFront, { reason, isSlotted });
    267  await updated;
    268  await onSelectionCssSelectorsUpdated;
    269 };
    270 
    271 /**
    272 * Using the markupview's _waitForChildren function, wait for all queued
    273 * children updates to be handled.
    274 *
    275 * @param {InspectorPanel} inspector The instance of InspectorPanel currently
    276 * loaded in the toolbox
    277 * @return a promise that resolves when all queued children updates have been
    278 * handled
    279 */
    280 function waitForChildrenUpdated({ markup }) {
    281  info("Waiting for queued children updates to be handled");
    282  return new Promise(resolve => {
    283    markup._waitForChildren().then(() => {
    284      executeSoon(resolve);
    285    });
    286  });
    287 }
    288 
    289 // The expand all operation of the markup-view calls itself recursively and
    290 // there's not one event we can wait for to know when it's done, so use this
    291 // helper function to wait until all recursive children updates are done.
    292 async function waitForMultipleChildrenUpdates(inspector) {
    293  // As long as child updates are queued up while we wait for an update already
    294  // wait again
    295  if (
    296    inspector.markup._queuedChildUpdates &&
    297    inspector.markup._queuedChildUpdates.size
    298  ) {
    299    await waitForChildrenUpdated(inspector);
    300    return waitForMultipleChildrenUpdates(inspector);
    301  }
    302  return null;
    303 }
    304 
    305 /**
    306 * Expand the provided markup container programmatically and  wait for all
    307 * children to update.
    308 */
    309 async function expandContainer(inspector, container) {
    310  await inspector.markup.expandNode(container.node);
    311  await waitForMultipleChildrenUpdates(inspector);
    312 }
    313 
    314 /**
    315 * Get the NodeFront for a node that matches a given css selector inside a
    316 * given iframe.
    317 *
    318 * @param {Array} selectors
    319 *        Arrays of CSS selectors from the root document to the node.
    320 *        The last CSS selector of the array is for the node in its frame doc.
    321 *        The before-last CSS selector is for the frame in its parent frame, etc...
    322 *        Ex: ["frame.first-frame", ..., "frame.last-frame", ".target-node"]
    323 * @param {InspectorPanel} inspector
    324 *        See `selectNode`
    325 * @return {NodeFront} Resolves the corresponding node front.
    326 */
    327 async function getNodeFrontInFrames(selectors, inspector) {
    328  let walker = inspector.walker;
    329  let rootNode = walker.rootNode;
    330 
    331  // clone the array since `selectors` could be used from callsite after.
    332  selectors = [...selectors];
    333  // Extract the last selector from the provided array of selectors.
    334  const nodeSelector = selectors.pop();
    335 
    336  // Remaining selectors should all be frame selectors. Renaming for clarity.
    337  const frameSelectors = selectors;
    338 
    339  info("Loop through all frame selectors");
    340  for (const frameSelector of frameSelectors) {
    341    const url = walker.targetFront.url;
    342    info(`Find the frame element for selector ${frameSelector} in ${url}`);
    343 
    344    const frameNodeFront = await walker.querySelector(rootNode, frameSelector);
    345 
    346    // If needed, connect to the corresponding frame target.
    347    // Otherwise, reuse the current targetFront.
    348    let frameTarget = frameNodeFront.targetFront;
    349    if (frameNodeFront.useChildTargetToFetchChildren) {
    350      info("Connect to frame and retrieve the targetFront");
    351      frameTarget = await frameNodeFront.connectToFrame();
    352    }
    353 
    354    walker = (await frameTarget.getFront("inspector")).walker;
    355 
    356    if (frameNodeFront.useChildTargetToFetchChildren) {
    357      // For frames or browser elements, use the walker's rootNode.
    358      rootNode = walker.rootNode;
    359    } else {
    360      // For same-process frames, select the document front as the root node.
    361      // It is a different node from the walker's rootNode.
    362      info("Retrieve the children of the frame to find the document node");
    363      const { nodes } = await walker.children(frameNodeFront);
    364      rootNode = nodes.find(n => n.nodeType === Node.DOCUMENT_NODE);
    365    }
    366  }
    367 
    368  return walker.querySelector(rootNode, nodeSelector);
    369 }
    370 
    371 /**
    372 * Helper to select a node in the markup-view, in a nested tree of
    373 * frames/browser elements. The iframes can either be remote or same-process.
    374 *
    375 * Note: "frame" will refer to either "frame" or "browser" in the documentation
    376 * and method.
    377 *
    378 * @param {Array} selectors
    379 *        Arrays of CSS selectors from the root document to the node.
    380 *        The last CSS selector of the array is for the node in its frame doc.
    381 *        The before-last CSS selector is for the frame in its parent frame, etc...
    382 *        Ex: ["frame.first-frame", ..., "frame.last-frame", ".target-node"]
    383 * @param {InspectorPanel} inspector
    384 *        See `selectNode`
    385 * @param {string} reason
    386 *        See `selectNode`
    387 * @param {boolean} isSlotted
    388 *        See `selectNode`
    389 * @return {NodeFront} The selected node front.
    390 */
    391 async function selectNodeInFrames(
    392  selectors,
    393  inspector,
    394  reason = "test",
    395  isSlotted
    396 ) {
    397  const nodeFront = await getNodeFrontInFrames(selectors, inspector);
    398  await selectNode(nodeFront, inspector, reason, isSlotted);
    399  return nodeFront;
    400 }
    401 
    402 /**
    403 * Create a throttling function that can be manually "flushed". This is to replace the
    404 * use of the `debounce` function from `devtools/client/inspector/shared/utils.js`, which
    405 * has a setTimeout that can cause intermittents.
    406 *
    407 * @return {Function} This function has the same function signature as debounce, but
    408 *                    the property `.flush()` has been added for flushing out any
    409 *                    debounced calls.
    410 */
    411 function manualDebounce() {
    412  let calls = [];
    413 
    414  function debounce(func, wait, scope) {
    415    return function () {
    416      const existingCall = calls.find(call => call.func === func);
    417      if (existingCall) {
    418        existingCall.args = arguments;
    419      } else {
    420        calls.push({ func, wait, scope, args: arguments });
    421      }
    422    };
    423  }
    424 
    425  debounce.flush = function () {
    426    calls.forEach(({ func, scope, args }) => func.apply(scope, args));
    427    calls = [];
    428  };
    429 
    430  return debounce;
    431 }
    432 
    433 /**
    434 * Get the requested rule style property from the current browser.
    435 *
    436 * @param {number} styleSheetIndex
    437 * @param {number} ruleIndex
    438 * @param {string} name
    439 * @return {string} The value, if found, null otherwise
    440 */
    441 
    442 async function getRulePropertyValue(styleSheetIndex, ruleIndex, name) {
    443  return SpecialPowers.spawn(
    444    gBrowser.selectedBrowser,
    445    [styleSheetIndex, ruleIndex, name],
    446    (styleSheetIndexChild, ruleIndexChild, nameChild) => {
    447      let value = null;
    448 
    449      info(
    450        "Getting the value for property name " +
    451          nameChild +
    452          " in sheet " +
    453          styleSheetIndexChild +
    454          " and rule " +
    455          ruleIndexChild
    456      );
    457 
    458      const sheet = content.document.styleSheets[styleSheetIndexChild];
    459      if (sheet) {
    460        const rule = sheet.cssRules[ruleIndexChild];
    461        if (rule) {
    462          value = rule.style.getPropertyValue(nameChild);
    463        }
    464      }
    465 
    466      return value;
    467    }
    468  );
    469 }
    470 
    471 /**
    472 * Get the requested computed style property from the current browser.
    473 *
    474 * @param {string} selector
    475 *        The selector used to obtain the element.
    476 * @param {string} pseudo
    477 *        pseudo id to query, or null.
    478 * @param {string} propName
    479 *        name of the property.
    480 */
    481 async function getComputedStyleProperty(selector, pseudo, propName) {
    482  return SpecialPowers.spawn(
    483    gBrowser.selectedBrowser,
    484    [selector, pseudo, propName],
    485    (selectorChild, pseudoChild, propNameChild) => {
    486      const element = content.document.querySelector(selectorChild);
    487      return content
    488        .getComputedStyle(element, pseudoChild)
    489        .getPropertyValue(propNameChild);
    490    }
    491  );
    492 }
    493 
    494 /**
    495 * Wait until the requested computed style property has the
    496 * expected value in the the current browser.
    497 *
    498 * @param {string} selector
    499 *        The selector used to obtain the element.
    500 * @param {string} pseudo
    501 *        pseudo id to query, or null.
    502 * @param {string} propName
    503 *        name of the property.
    504 * @param {string} expected
    505 *        expected value of property
    506 */
    507 async function waitForComputedStyleProperty(
    508  selector,
    509  pseudo,
    510  propName,
    511  expected
    512 ) {
    513  return SpecialPowers.spawn(
    514    gBrowser.selectedBrowser,
    515    [selector, pseudo, propName, expected],
    516    (selectorChild, pseudoChild, propNameChild, expectedChild) => {
    517      const element = content.document.querySelector(selectorChild);
    518      return ContentTaskUtils.waitForCondition(() => {
    519        const value = content
    520          .getComputedStyle(element, pseudoChild)
    521          .getPropertyValue(propNameChild);
    522        return value === expectedChild;
    523      });
    524    }
    525  );
    526 }
    527 
    528 /**
    529 * Given an inplace editable element, click to switch it to edit mode, wait for
    530 * focus
    531 *
    532 * @return a promise that resolves to the inplace-editor element when ready
    533 */
    534 var focusEditableField = async function (
    535  ruleView,
    536  editable,
    537  xOffset = 1,
    538  yOffset = 1,
    539  options = {}
    540 ) {
    541  editable.scrollIntoView();
    542  const onFocus = once(editable.parentNode, "focus", true);
    543  info("Clicking on editable field to turn to edit mode");
    544  if (options.type === undefined) {
    545    // "mousedown" and "mouseup" flushes any pending layout.  Therefore,
    546    // if the caller wants to click an element, e.g., closebrace to add new
    547    // property, we need to guarantee that the element is clicked here even
    548    // if it's moved by flushing the layout because whether the UI is useful
    549    // or not when there is pending reflow is not scope of the tests.
    550    options.type = "mousedown";
    551    EventUtils.synthesizeMouse(
    552      editable,
    553      xOffset,
    554      yOffset,
    555      options,
    556      editable.ownerGlobal
    557    );
    558    options.type = "mouseup";
    559    EventUtils.synthesizeMouse(
    560      editable,
    561      xOffset,
    562      yOffset,
    563      options,
    564      editable.ownerGlobal
    565    );
    566  } else {
    567    EventUtils.synthesizeMouse(
    568      editable,
    569      xOffset,
    570      yOffset,
    571      options,
    572      editable.ownerGlobal
    573    );
    574  }
    575  await onFocus;
    576 
    577  info("Editable field gained focus, returning the input field now");
    578  return inplaceEditor(editable.ownerDocument.activeElement);
    579 };
    580 
    581 /**
    582 * Get the DOMNode for a css rule in the rule-view that corresponds to the given
    583 * selector.
    584 *
    585 * @param {CssRuleView} view
    586 *        The instance of the rule-view panel
    587 * @param {string} selectorText
    588 *        The selector in the rule-view for which the rule
    589 *        object is wanted
    590 * @param {number} index
    591 *        If there are more than 1 rule with the same selector, you may pass a
    592 *        index to determine which of the rules you want.
    593 * @return {DOMNode}
    594 */
    595 function getRuleViewRule(view, selectorText, index = 0) {
    596  let rule;
    597  let pos = 0;
    598  for (const r of view.styleDocument.querySelectorAll(".ruleview-rule")) {
    599    const selector = r.querySelector(
    600      ".ruleview-selectors-container, .ruleview-selector.matched"
    601    );
    602    if (selector && selector.textContent === selectorText) {
    603      if (index == pos) {
    604        rule = r;
    605        break;
    606      }
    607      pos++;
    608    }
    609  }
    610 
    611  return rule;
    612 }
    613 
    614 /**
    615 * Get references to the name and value span nodes corresponding to a given
    616 * selector and property name in the rule-view.
    617 *
    618 * @param {CssRuleView} view
    619 *        The instance of the rule-view panel
    620 * @param {string} selectorText
    621 *        The selector in the rule-view to look for the property in
    622 * @param {string} propertyName
    623 *        The name of the property
    624 * @param {object=} options
    625 * @param {boolean=} options.wait
    626 *        When true, returns a promise which waits until a valid rule view
    627 *        property can be retrieved for the provided selectorText & propertyName.
    628 *        Defaults to false.
    629 * @return {object} An object like {nameSpan: DOMNode, valueSpan: DOMNode}
    630 */
    631 function getRuleViewProperty(view, selectorText, propertyName, options = {}) {
    632  if (options.wait) {
    633    return waitFor(() =>
    634      _syncGetRuleViewProperty(view, selectorText, propertyName)
    635    );
    636  }
    637  return _syncGetRuleViewProperty(view, selectorText, propertyName);
    638 }
    639 
    640 function _syncGetRuleViewProperty(view, selectorText, propertyName) {
    641  const rule = getRuleViewRule(view, selectorText);
    642  if (!rule) {
    643    return null;
    644  }
    645 
    646  // Look for the propertyName in that rule element
    647  for (const p of rule.querySelectorAll(".ruleview-property")) {
    648    const nameSpan = p.querySelector(".ruleview-propertyname");
    649    const valueSpan = p.querySelector(".ruleview-propertyvalue");
    650 
    651    if (nameSpan.textContent === propertyName) {
    652      return { nameSpan, valueSpan };
    653    }
    654  }
    655  return null;
    656 }
    657 
    658 /**
    659 * Get the text value of the property corresponding to a given selector and name
    660 * in the rule-view
    661 *
    662 * @param {CssRuleView} view
    663 *        The instance of the rule-view panel
    664 * @param {string} selectorText
    665 *        The selector in the rule-view to look for the property in
    666 * @param {string} propertyName
    667 *        The name of the property
    668 * @return {string} The property value
    669 */
    670 function getRuleViewPropertyValue(view, selectorText, propertyName) {
    671  return getRuleViewProperty(view, selectorText, propertyName).valueSpan
    672    .textContent;
    673 }
    674 
    675 /**
    676 * Get a reference to the selector DOM element corresponding to a given selector
    677 * in the rule-view
    678 *
    679 * @param {CssRuleView} view
    680 *        The instance of the rule-view panel
    681 * @param {string} selectorText
    682 *        The selector in the rule-view to look for
    683 * @return {DOMNode} The selector DOM element
    684 */
    685 function getRuleViewSelector(view, selectorText) {
    686  const rule = getRuleViewRule(view, selectorText);
    687  return rule.querySelector(
    688    ".ruleview-selectors-container, .ruleview-selector.matched"
    689  );
    690 }
    691 
    692 /**
    693 * Get a rule-link from the rule-view given the rule index
    694 *
    695 * @param {CssRuleView} view
    696 *        The instance of the rule-view panel
    697 * @param {number} index
    698 *        The index of the link to get
    699 * @return {DOMNode|null} The link if any at this rule index, or null if it doesn't exist
    700 */
    701 function getRuleViewLinkByIndex(view, index) {
    702  const ruleEl = view.styleDocument.querySelectorAll(".ruleview-rule")[index];
    703  return ruleEl?.querySelector(".ruleview-rule-source") || null;
    704 }
    705 
    706 /**
    707 * Get rule-link text from the rule-view given its index
    708 *
    709 * @param {CssRuleView} view
    710 *        The instance of the rule-view panel
    711 * @param {number} index
    712 *        The index of the link to get
    713 * @return {string} The string at this index
    714 */
    715 function getRuleViewLinkTextByIndex(view, index) {
    716  const link = getRuleViewLinkByIndex(view, index);
    717  return link.querySelector(".ruleview-rule-source-label").textContent;
    718 }
    719 
    720 /**
    721 * Click on a rule-view's close brace to focus a new property name editor
    722 *
    723 * @param {RuleEditor} ruleEditor
    724 *        An instance of RuleEditor that will receive the new property
    725 * @return a promise that resolves to the newly created editor when ready and
    726 * focused
    727 */
    728 var focusNewRuleViewProperty = async function (ruleEditor) {
    729  info("Clicking on a close ruleEditor brace to start editing a new property");
    730 
    731  // Use bottom alignment to avoid scrolling out of the parent element area.
    732  ruleEditor.closeBrace.scrollIntoView(false);
    733  const editor = await focusEditableField(
    734    ruleEditor.ruleView,
    735    ruleEditor.closeBrace
    736  );
    737 
    738  is(
    739    inplaceEditor(ruleEditor.newPropSpan),
    740    editor,
    741    "Focused editor is the new property editor."
    742  );
    743 
    744  return editor;
    745 };
    746 
    747 /**
    748 * Create a new property name in the rule-view, focusing a new property editor
    749 * by clicking on the close brace, and then entering the given text.
    750 * Keep in mind that the rule-view knows how to handle strings with multiple
    751 * properties, so the input text may be like: "p1:v1;p2:v2;p3:v3".
    752 *
    753 * @param {RuleEditor} ruleEditor
    754 *        The instance of RuleEditor that will receive the new property(ies)
    755 * @param {string} inputValue
    756 *        The text to be entered in the new property name field
    757 * @return a promise that resolves when the new property name has been entered
    758 * and once the value field is focused
    759 */
    760 var createNewRuleViewProperty = async function (ruleEditor, inputValue) {
    761  info("Creating a new property editor");
    762  const editor = await focusNewRuleViewProperty(ruleEditor);
    763 
    764  info("Entering the value " + inputValue);
    765  editor.input.value = inputValue;
    766 
    767  info("Submitting the new value and waiting for value field focus");
    768  const onFocus = once(ruleEditor.element, "focus", true);
    769  EventUtils.synthesizeKey(
    770    "VK_RETURN",
    771    {},
    772    ruleEditor.element.ownerDocument.defaultView
    773  );
    774  await onFocus;
    775 };
    776 
    777 /**
    778 * Set the search value for the rule-view filter styles search box.
    779 *
    780 * @param {CssRuleView} view
    781 *        The instance of the rule-view panel
    782 * @param {string} searchValue
    783 *        The filter search value
    784 * @return a promise that resolves when the rule-view is filtered for the
    785 * search term
    786 */
    787 var setSearchFilter = async function (view, searchValue) {
    788  info('Setting filter text to "' + searchValue + '"');
    789 
    790  const searchField = view.searchField;
    791  searchField.focus();
    792 
    793  const onRuleviewFiltered = view.inspector.once("ruleview-filtered");
    794  for (const key of searchValue.split("")) {
    795    EventUtils.synthesizeKey(key, {}, view.styleWindow);
    796  }
    797  await onRuleviewFiltered;
    798 };
    799 
    800 /**
    801 * Flatten all context menu items into a single array to make searching through
    802 * it easier.
    803 */
    804 function buildContextMenuItems(menu) {
    805  const allItems = [].concat.apply(
    806    [],
    807    menu.items.map(function addItem(item) {
    808      if (item.submenu) {
    809        return addItem(item.submenu.items);
    810      }
    811      return item;
    812    })
    813  );
    814 
    815  return allItems;
    816 }
    817 
    818 /**
    819 * Open the style editor context menu and return all of it's items in a flat array
    820 *
    821 * @param {CssRuleView} view
    822 *        The instance of the rule-view panel
    823 * @return An array of MenuItems
    824 */
    825 function openStyleContextMenuAndGetAllItems(view, target) {
    826  const menu = view.contextMenu._openMenu({ target });
    827  return buildContextMenuItems(menu);
    828 }
    829 
    830 /**
    831 * Open the inspector menu and return all of it's items in a flat array
    832 *
    833 * @param {InspectorPanel} inspector
    834 * @param {object} options to pass into openMenu
    835 * @return An array of MenuItems
    836 */
    837 function openContextMenuAndGetAllItems(inspector, options) {
    838  const menu = inspector.markup.contextMenu._openMenu(options);
    839  return buildContextMenuItems(menu);
    840 }
    841 
    842 /**
    843 * Wait until the elements the given selectors indicate come to have the visited state.
    844 *
    845 * @param {Tab} tab
    846 *        The tab where the elements on.
    847 * @param {Array} selectors
    848 *        The selectors for the elements.
    849 */
    850 async function waitUntilVisitedState(tab, selectors) {
    851  await asyncWaitUntil(async () => {
    852    const hasVisitedState = await ContentTask.spawn(
    853      tab.linkedBrowser,
    854      selectors,
    855      args => {
    856        // ElementState::VISITED
    857        const ELEMENT_STATE_VISITED = 1 << 18;
    858 
    859        for (const selector of args) {
    860          const target =
    861            content.wrappedJSObject.document.querySelector(selector);
    862          if (
    863            !(
    864              target &&
    865              InspectorUtils.getContentState(target) & ELEMENT_STATE_VISITED
    866            )
    867          ) {
    868            return false;
    869          }
    870        }
    871        return true;
    872      }
    873    );
    874    return hasVisitedState;
    875  });
    876 }
    877 
    878 /**
    879 * Return wether or not the passed selector matches an element in the content page.
    880 *
    881 * @param {string} selector
    882 * @returns Promise<Boolean>
    883 */
    884 function hasMatchingElementInContentPage(selector) {
    885  return SpecialPowers.spawn(
    886    gBrowser.selectedBrowser,
    887    [selector],
    888    function (innerSelector) {
    889      return content.document.querySelector(innerSelector) !== null;
    890    }
    891  );
    892 }
    893 
    894 /**
    895 * Return the number of elements matching the passed selector.
    896 *
    897 * @param {string} selector
    898 * @returns Promise<Number> the number of matching elements
    899 */
    900 function getNumberOfMatchingElementsInContentPage(selector) {
    901  return SpecialPowers.spawn(
    902    gBrowser.selectedBrowser,
    903    [selector],
    904    function (innerSelector) {
    905      return content.document.querySelectorAll(innerSelector).length;
    906    }
    907  );
    908 }
    909 
    910 /**
    911 * Get the property of an element in the content page
    912 *
    913 * @param {string} selector: The selector to get the element we want the property of
    914 * @param {string} propertyName: The name of the property we want the value of
    915 * @returns {Promise} A promise that returns with the value of the property for the element
    916 */
    917 function getContentPageElementProperty(selector, propertyName) {
    918  return SpecialPowers.spawn(
    919    gBrowser.selectedBrowser,
    920    [selector, propertyName],
    921    function (innerSelector, innerPropertyName) {
    922      return content.document.querySelector(innerSelector)[innerPropertyName];
    923    }
    924  );
    925 }
    926 
    927 /**
    928 * Set the property of an element in the content page
    929 *
    930 * @param {string} selector: The selector to get the element we want to set the property on
    931 * @param {string} propertyName: The name of the property we want to set
    932 * @param {string} propertyValue: The value that is going to be assigned to the property
    933 * @returns {Promise}
    934 */
    935 function setContentPageElementProperty(selector, propertyName, propertyValue) {
    936  return SpecialPowers.spawn(
    937    gBrowser.selectedBrowser,
    938    [selector, propertyName, propertyValue],
    939    function (innerSelector, innerPropertyName, innerPropertyValue) {
    940      content.document.querySelector(innerSelector)[innerPropertyName] =
    941        innerPropertyValue;
    942    }
    943  );
    944 }
    945 
    946 /**
    947 * Get all the attributes for a DOM Node living in the content page.
    948 *
    949 * @param {string} selector The node selector
    950 * @returns {Array<object>} An array of {name, value} objects.
    951 */
    952 async function getContentPageElementAttributes(selector) {
    953  return SpecialPowers.spawn(
    954    gBrowser.selectedBrowser,
    955    [selector],
    956    _selector => {
    957      const node = content.document.querySelector(_selector);
    958      return Array.from(node.attributes).map(({ name, value }) => ({
    959        name,
    960        value,
    961      }));
    962    }
    963  );
    964 }
    965 
    966 /**
    967 * Get an attribute on a DOM Node living in the content page.
    968 *
    969 * @param {string} selector The node selector
    970 * @param {string} attribute The attribute name
    971 * @return {string} value The attribute value
    972 */
    973 async function getContentPageElementAttribute(selector, attribute) {
    974  return SpecialPowers.spawn(
    975    gBrowser.selectedBrowser,
    976    [selector, attribute],
    977    (_selector, _attribute) => {
    978      return content.document.querySelector(_selector).getAttribute(_attribute);
    979    }
    980  );
    981 }
    982 
    983 /**
    984 * Set an attribute on a DOM Node living in the content page.
    985 *
    986 * @param {string} selector The node selector
    987 * @param {string} attribute The attribute name
    988 * @param {string} value The attribute value
    989 */
    990 async function setContentPageElementAttribute(selector, attribute, value) {
    991  return SpecialPowers.spawn(
    992    gBrowser.selectedBrowser,
    993    [selector, attribute, value],
    994    (_selector, _attribute, _value) => {
    995      content.document
    996        .querySelector(_selector)
    997        .setAttribute(_attribute, _value);
    998    }
    999  );
   1000 }
   1001 
   1002 /**
   1003 * Remove an attribute from a DOM Node living in the content page.
   1004 *
   1005 * @param {string} selector The node selector
   1006 * @param {string} attribute The attribute name
   1007 */
   1008 async function removeContentPageElementAttribute(selector, attribute) {
   1009  return SpecialPowers.spawn(
   1010    gBrowser.selectedBrowser,
   1011    [selector, attribute],
   1012    (_selector, _attribute) => {
   1013      content.document.querySelector(_selector).removeAttribute(_attribute);
   1014    }
   1015  );
   1016 }
   1017 
   1018 /**
   1019 * Get the rule editor from the rule-view given its index
   1020 *
   1021 * @param {CssRuleView} ruleView
   1022 *        The instance of the rule-view panel
   1023 * @param {number} childrenIndex
   1024 *        The children index of the element to get
   1025 * @param {number} nodeIndex
   1026 *        The child node index of the element to get
   1027 * @return {DOMNode} The rule editor if any at this index
   1028 */
   1029 function getRuleViewRuleEditor(ruleView, childrenIndex, nodeIndex) {
   1030  const child = ruleView.element.children[childrenIndex];
   1031  if (!child) {
   1032    return null;
   1033  }
   1034 
   1035  return nodeIndex !== undefined
   1036    ? child.childNodes[nodeIndex]?._ruleEditor
   1037    : child._ruleEditor;
   1038 }
   1039 
   1040 /**
   1041 * Get the TextProperty instance corresponding to a CSS declaration
   1042 * from a CSS rule in the Rules view.
   1043 *
   1044 * @param  {RuleView} ruleView
   1045 *         Instance of RuleView.
   1046 * @param  {number} ruleIndex
   1047 *         The index of the CSS rule where to find the declaration.
   1048 * @param  {object} declaration
   1049 *         An object representing the target declaration e.g. { color: red }.
   1050 *         The first TextProperty instance which matches will be returned.
   1051 * @return {TextProperty}
   1052 */
   1053 function getTextProperty(ruleView, ruleIndex, declaration) {
   1054  const ruleEditor = getRuleViewRuleEditor(ruleView, ruleIndex);
   1055  const [[name, value]] = Object.entries(declaration);
   1056  const textProp = ruleEditor.rule.textProps.find(prop => {
   1057    return prop.name === name && prop.value === value;
   1058  });
   1059 
   1060  if (!textProp) {
   1061    throw Error(
   1062      `Declaration ${name}:${value} not found on rule at index ${ruleIndex}`
   1063    );
   1064  }
   1065 
   1066  return textProp;
   1067 }
   1068 
   1069 /**
   1070 * Simulate changing the value of a property in a rule in the rule-view.
   1071 *
   1072 * @param {CssRuleView} ruleView
   1073 *        The instance of the rule-view panel
   1074 * @param {TextProperty} textProp
   1075 *        The instance of the TextProperty to be changed
   1076 * @param {string} value
   1077 *        The new value to be used. If null is passed, then the value will be
   1078 *        deleted
   1079 * @param {object} options
   1080 * @param {boolean} options.blurNewProperty
   1081 *        After the value has been changed, a new property would have been
   1082 *        focused. This parameter is true by default, and that causes the new
   1083 *        property to be blurred. Set to false if you don't want this.
   1084 * @param {number} options.flushCount
   1085 *        The ruleview uses a manual flush for tests only, and some properties are
   1086 *        only updated after several flush. Allow tests to trigger several flushes
   1087 *        if necessary. Defaults to 1.
   1088 */
   1089 async function setProperty(
   1090  ruleView,
   1091  textProp,
   1092  value,
   1093  { blurNewProperty = true, flushCount = 1 } = {}
   1094 ) {
   1095  info("Set property to: " + value);
   1096  const editor = await focusEditableField(ruleView, textProp.editor.valueSpan);
   1097 
   1098  // Because of the manual flush approach used for tests, we might have an
   1099  // unknown number of debounced "preview" requests . Each preview should
   1100  // synchronously emit "start-preview-property-value".
   1101  // Listen to both this event and "ruleview-changed" which is emitted at the
   1102  // end of a preview and make sure each preview completes successfully.
   1103  let previewStartedCounter = 0;
   1104  const onStartPreview = () => previewStartedCounter++;
   1105  ruleView.on("start-preview-property-value", onStartPreview);
   1106 
   1107  let previewCounter = 0;
   1108  const onPreviewApplied = () => previewCounter++;
   1109  ruleView.on("ruleview-changed", onPreviewApplied);
   1110 
   1111  if (value === null) {
   1112    const onPopupOpened = once(ruleView.popup, "popup-opened");
   1113    EventUtils.synthesizeKey("VK_DELETE", {}, ruleView.styleWindow);
   1114    await onPopupOpened;
   1115  } else {
   1116    await wait(500);
   1117    // Since some time have passed since we made the input visible and focused it,
   1118    // we might have some previous async work that causes the input to be blurred
   1119    // (see intermittent Bug 1845152).
   1120    // Make sure the input is focused before triggering the keyboard event.
   1121    editor.input.focus();
   1122    EventUtils.sendString(value, ruleView.styleWindow);
   1123  }
   1124 
   1125  info(`Flush debounced ruleview methods (remaining: ${flushCount})`);
   1126  ruleView.debounce.flush();
   1127  await waitFor(() => previewCounter >= previewStartedCounter);
   1128 
   1129  flushCount--;
   1130 
   1131  while (flushCount > 0) {
   1132    // Wait for some time before triggering a new flush to let new debounced
   1133    // functions queue in-between.
   1134    await wait(100);
   1135 
   1136    info(`Flush debounced ruleview methods (remaining: ${flushCount})`);
   1137    ruleView.debounce.flush();
   1138    await waitFor(() => previewCounter >= previewStartedCounter);
   1139 
   1140    flushCount--;
   1141  }
   1142 
   1143  ruleView.off("start-preview-property-value", onStartPreview);
   1144  ruleView.off("ruleview-changed", onPreviewApplied);
   1145 
   1146  const onValueDone = ruleView.once("ruleview-changed");
   1147  // In case the popup was opened, wait until it closes
   1148  let onPopupClosed;
   1149  if (ruleView.popup?.isOpen) {
   1150    // it might happen that the popup is still in the process of being opened,
   1151    // so wait until it's properly opened
   1152    await ruleView.popup._pendingShowPromise;
   1153    onPopupClosed = once(ruleView.popup, "popup-closed");
   1154  }
   1155 
   1156  // Since some time have passed since we made the input visible and focused it,
   1157  // we might have some previous async work that causes the input to be blurred
   1158  // (see intermittent Bug 1845152).
   1159  // Make sure the input is focused before triggering the keyboard event.
   1160  editor.input.focus();
   1161  EventUtils.synthesizeKey(
   1162    blurNewProperty ? "VK_RETURN" : "VK_TAB",
   1163    {},
   1164    ruleView.styleWindow
   1165  );
   1166 
   1167  info("Waiting for another ruleview-changed after setting property");
   1168  await onValueDone;
   1169 
   1170  const focusNextOnEnter = Services.prefs.getBoolPref(
   1171    "devtools.inspector.rule-view.focusNextOnEnter"
   1172  );
   1173  if (blurNewProperty && !focusNextOnEnter) {
   1174    info("Force blur on the active element");
   1175    ruleView.styleDocument.activeElement.blur();
   1176  }
   1177  await onPopupClosed;
   1178 }
   1179 
   1180 /**
   1181 * Return the markup view search input
   1182 *
   1183 * @param {Inspector} inspector
   1184 * @returns {Element}
   1185 */
   1186 function getMarkupViewSearchInput(inspector) {
   1187  return inspector.panelWin.document.getElementById("inspector-searchbox");
   1188 }
   1189 
   1190 /**
   1191 * Using the inspector panel's selector search box, search for a given selector.
   1192 * The selector input string will be entered in the input field and the <ENTER>
   1193 * keypress will be simulated.
   1194 */
   1195 async function searchInMarkupView(inspector, search) {
   1196  info(`Entering "${search}" into the markup view search field`);
   1197  const inspectorSearchboxEl = getMarkupViewSearchInput(inspector);
   1198  inspectorSearchboxEl.focus();
   1199  inspectorSearchboxEl.value = search;
   1200 
   1201  const onNewNodeFront = inspector.selection.once("new-node-front");
   1202  const onSearchResult = inspector.search.once("search-result");
   1203  const onSearchResultHighlightingUpdated = inspector.markup.once(
   1204    "search-results-highlighting-updated"
   1205  );
   1206  EventUtils.sendKey("return", inspector.panelWin);
   1207 
   1208  info("Wait for search-result");
   1209  await onSearchResult;
   1210 
   1211  info("Wait for new node being selected");
   1212  await onNewNodeFront;
   1213 
   1214  info("Wait for the search results highlighted to be updated");
   1215  await onSearchResultHighlightingUpdated;
   1216 }