tor-browser

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

head.js (54672B)


      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 /* eslint no-unused-vars: [2, {"vars": "local"}] */
      5 
      6 "use strict";
      7 
      8 // Load the shared-head file first.
      9 Services.scriptloader.loadSubScript(
     10  "chrome://mochitests/content/browser/devtools/client/shared/test/shared-head.js",
     11  this
     12 );
     13 
     14 // Services.prefs.setBoolPref("devtools.debugger.log", true);
     15 
     16 // Import helpers for the inspector that are also shared with others
     17 Services.scriptloader.loadSubScript(
     18  "chrome://mochitests/content/browser/devtools/client/inspector/test/shared-head.js",
     19  this
     20 );
     21 
     22 const INSPECTOR_L10N = new LocalizationHelper(
     23  "devtools/client/locales/inspector.properties"
     24 );
     25 
     26 registerCleanupFunction(function () {
     27  // Move the mouse outside inspector. If the test happened fake a mouse event
     28  // somewhere over inspector the pointer is considered to be there when the
     29  // next test begins. This might cause unexpected events to be emitted when
     30  // another test moves the mouse.
     31  // Move the mouse at the top-right corner of the browser, to prevent
     32  // the mouse from triggering the tab tooltip to be shown while the tab is
     33  // being closed because the test is exiting (See Bug 1378524 for rationale).
     34  EventUtils.synthesizeMouseAtPoint(
     35    window.innerWidth,
     36    1,
     37    { type: "mousemove" },
     38    window
     39  );
     40 });
     41 
     42 /**
     43 * Start the element picker and focus the content window.
     44 *
     45 * @param {Toolbox} toolbox
     46 * @param {boolean} skipFocus - Allow tests to bypass the focus event.
     47 */
     48 var startPicker = async function (toolbox, skipFocus) {
     49  info("Start the element picker");
     50  toolbox.win.focus();
     51  await toolbox.nodePicker.start();
     52  if (!skipFocus) {
     53    // By default make sure the content window is focused since the picker may not focus
     54    // the content window by default.
     55    await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async function () {
     56      content.focus();
     57    });
     58  }
     59 };
     60 
     61 /**
     62 * Stop the element picker using the Escape keyboard shortcut
     63 *
     64 * @param {Toolbox} toolbox
     65 */
     66 var stopPickerWithEscapeKey = async function (toolbox) {
     67  const onPickerStopped = toolbox.nodePicker.once("picker-node-canceled");
     68  EventUtils.synthesizeKey("VK_ESCAPE", {}, toolbox.win);
     69  await onPickerStopped;
     70 };
     71 
     72 /**
     73 * Start the eye dropper tool.
     74 *
     75 * @param {Toolbox} toolbox
     76 */
     77 var startEyeDropper = async function (toolbox) {
     78  info("Start the eye dropper tool");
     79  toolbox.win.focus();
     80  await toolbox.getPanel("inspector").showEyeDropper();
     81 };
     82 
     83 /**
     84 * Pick an element from the content page using the element picker.
     85 *
     86 * @param {Inspector} inspector
     87 *        Inspector instance
     88 * @param {string} selector
     89 *        CSS selector to identify the click target
     90 * @param {number} x
     91 *        X-offset from the top-left corner of the element matching the provided selector
     92 * @param {number} y
     93 *        Y-offset from the top-left corner of the element matching the provided selector
     94 * @return {Promise} promise that resolves when the selection is updated with the picked
     95 *         node.
     96 */
     97 function pickElement(inspector, selector, x, y) {
     98  info("Waiting for element " + selector + " to be picked");
     99  // Use an empty options argument in order trigger the default synthesizeMouse behavior
    100  // which will trigger mousedown, then mouseup.
    101  const onNewNodeFront = inspector.selection.once("new-node-front");
    102  BrowserTestUtils.synthesizeMouse(
    103    selector,
    104    x,
    105    y,
    106    {},
    107    gBrowser.selectedTab.linkedBrowser
    108  );
    109  return onNewNodeFront;
    110 }
    111 
    112 /**
    113 * Hover an element from the content page using the element picker.
    114 *
    115 * @param {Inspector} inspector
    116 *        Inspector instance
    117 * @param {string | Array} selector
    118 *        CSS selector to identify the hover target.
    119 *        Example: ".target"
    120 *        If the element is at the bottom of a nested iframe stack, the selector should
    121 *        be an array with each item identifying the iframe within its host document.
    122 *        The last item of the array should be the element selector within the deepest
    123 *        nested iframe.
    124          Example: ["iframe#top", "iframe#nested", ".target"]
    125 * @param {number} x
    126 *        X-offset from the top-left corner of the element matching the provided selector
    127 * @param {number} y
    128 *        Y-offset from the top-left corner of the element matching the provided selector
    129 * @param {object} eventOptions
    130 *        Options that will be passed to synthesizeMouse
    131 * @return {Promise} promise that resolves when both the "picker-node-hovered" and
    132 *                   "highlighter-shown" events are emitted.
    133 */
    134 async function hoverElement(inspector, selector, x, y, eventOptions = {}) {
    135  const { waitForHighlighterTypeShown } = getHighlighterTestHelpers(inspector);
    136  info(`Waiting for element "${selector}" to be hovered`);
    137  const onHovered = inspector.toolbox.nodePicker.once("picker-node-hovered");
    138  const onHighlighterShown = waitForHighlighterTypeShown(
    139    inspector.highlighters.TYPES.BOXMODEL
    140  );
    141 
    142  // Default to the top-level target browsing context
    143  let browsingContext = gBrowser.selectedTab.linkedBrowser;
    144 
    145  if (Array.isArray(selector)) {
    146    // Get the browsing context for the deepest nested frame; exclude the last array item.
    147    // Cloning the array so it can be safely mutated.
    148    browsingContext = await getBrowsingContextForNestedFrame(
    149      selector.slice(0, selector.length - 1)
    150    );
    151    // Assume the last item in the selector array is the actual element selector.
    152    // DO NOT mutate the selector array with .pop(), it might still be used by a test.
    153    selector = selector[selector.length - 1];
    154  }
    155 
    156  if (isNaN(x) || isNaN(y)) {
    157    BrowserTestUtils.synthesizeMouseAtCenter(
    158      selector,
    159      { ...eventOptions, type: "mousemove" },
    160      browsingContext
    161    );
    162  } else {
    163    BrowserTestUtils.synthesizeMouse(
    164      selector,
    165      x,
    166      y,
    167      { ...eventOptions, type: "mousemove" },
    168      browsingContext
    169    );
    170  }
    171 
    172  info("Wait for picker-node-hovered");
    173  await onHovered;
    174 
    175  info("Wait for highlighter shown");
    176  await onHighlighterShown;
    177 
    178  return Promise.all([onHighlighterShown, onHovered]);
    179 }
    180 
    181 /**
    182 * Get the browsing context for the deepest nested iframe
    183 * as identified by an array of selectors.
    184 *
    185 * @param  {Array} selectorArray
    186 *         Each item in the array is a selector that identifies the iframe
    187 *         within its host document.
    188 *         Example: ["iframe#top", "iframe#nested"]
    189 * @return {BrowsingContext}
    190 *         BrowsingContext for the deepest nested iframe.
    191 */
    192 async function getBrowsingContextForNestedFrame(selectorArray = []) {
    193  // Default to the top-level target browsing context
    194  let browsingContext = gBrowser.selectedTab.linkedBrowser;
    195 
    196  // Return the top-level target browsing context if the selector is not an array.
    197  if (!Array.isArray(selectorArray)) {
    198    return browsingContext;
    199  }
    200 
    201  // Recursively get the browsing context for each nested iframe.
    202  while (selectorArray.length) {
    203    browsingContext = await SpecialPowers.spawn(
    204      browsingContext,
    205      [selectorArray.shift()],
    206      function (selector) {
    207        const iframe = content.document.querySelector(selector);
    208        return iframe.browsingContext;
    209      }
    210    );
    211  }
    212 
    213  return browsingContext;
    214 }
    215 
    216 /**
    217 * Highlight a node and set the inspector's current selection to the node or
    218 * the first match of the given css selector.
    219 *
    220 * @param {string | NodeFront} selector
    221 * @param {InspectorPanel} inspector
    222 *        The instance of InspectorPanel currently loaded in the toolbox
    223 * @return a promise that resolves when the inspector is updated with the new
    224 * node
    225 */
    226 async function selectAndHighlightNode(selector, inspector) {
    227  const { waitForHighlighterTypeShown } = getHighlighterTestHelpers(inspector);
    228  info("Highlighting and selecting the node " + selector);
    229  const onHighlighterShown = waitForHighlighterTypeShown(
    230    inspector.highlighters.TYPES.BOXMODEL
    231  );
    232 
    233  await selectNode(selector, inspector, "test-highlight");
    234  await onHighlighterShown;
    235 }
    236 
    237 /**
    238 * Select node for a given selector, make it focusable and set focus in its
    239 * container element.
    240 *
    241 * @param {string | NodeFront} selector
    242 * @param {InspectorPanel} inspector The current inspector-panel instance.
    243 * @return {MarkupContainer}
    244 */
    245 async function focusNode(selector, inspector) {
    246  getContainerForNodeFront(inspector.walker.rootNode, inspector).elt.focus();
    247  const nodeFront = await getNodeFront(selector, inspector);
    248  const container = getContainerForNodeFront(nodeFront, inspector);
    249  await selectNode(nodeFront, inspector);
    250  EventUtils.sendKey("return", inspector.panelWin);
    251  return container;
    252 }
    253 
    254 /**
    255 * Set the inspector's current selection to null so that no node is selected
    256 *
    257 * @param {InspectorPanel} inspector
    258 *        The instance of InspectorPanel currently loaded in the toolbox
    259 * @return a promise that resolves when the inspector is updated
    260 */
    261 function clearCurrentNodeSelection(inspector) {
    262  info("Clearing the current selection");
    263  const updated = inspector.once("inspector-updated");
    264  inspector.selection.setNodeFront(null);
    265  return updated;
    266 }
    267 
    268 /**
    269 * Right click on a node in the test page and click on the inspect menu item.
    270 *
    271 * @param {string} selector The selector for the node to click on in the page.
    272 * @return {Promise} Resolves to the inspector when it has opened and is updated
    273 */
    274 var clickOnInspectMenuItem = async function (selector) {
    275  info("Showing the contextual menu on node " + selector);
    276  const contentAreaContextMenu = document.querySelector(
    277    "#contentAreaContextMenu"
    278  );
    279  const contextOpened = once(contentAreaContextMenu, "popupshown");
    280 
    281  await safeSynthesizeMouseEventAtCenterInContentPage(selector, {
    282    type: "contextmenu",
    283    button: 2,
    284  });
    285 
    286  await contextOpened;
    287 
    288  info("Triggering the inspect action");
    289  await gContextMenu.inspectNode();
    290 
    291  info("Hiding the menu");
    292  const contextClosed = once(contentAreaContextMenu, "popuphidden");
    293  contentAreaContextMenu.hidePopup();
    294  await contextClosed;
    295 
    296  return getActiveInspector();
    297 };
    298 
    299 /**
    300 * Get the NodeFront for the document node inside a given iframe.
    301 *
    302 * @param {string | NodeFront} frameSelector
    303 *        A selector that matches the iframe the document node is in
    304 * @param {InspectorPanel} inspector
    305 *        The instance of InspectorPanel currently loaded in the toolbox
    306 * @return {Promise} Resolves the node front when the inspector is updated with the new
    307 *         node.
    308 */
    309 var getFrameDocument = async function (frameSelector, inspector) {
    310  const iframe = await getNodeFront(frameSelector, inspector);
    311  const { nodes } = await inspector.walker.children(iframe);
    312 
    313  // Find the document node in the children of the iframe element.
    314  return nodes.filter(node => node.displayName === "#document")[0];
    315 };
    316 
    317 /**
    318 * Get the NodeFront for the shadowRoot of a shadow host.
    319 *
    320 * @param {string | NodeFront} hostSelector
    321 *        Selector or front of the element to which the shadow root is attached.
    322 * @param {InspectorPanel} inspector
    323 *        The instance of InspectorPanel currently loaded in the toolbox
    324 * @return {Promise} Resolves the node front when the inspector is updated with the new
    325 *         node.
    326 */
    327 var getShadowRoot = async function (hostSelector, inspector) {
    328  const hostFront = await getNodeFront(hostSelector, inspector);
    329  const { nodes } = await inspector.walker.children(hostFront);
    330 
    331  // Find the shadow root in the children of the host element.
    332  return nodes.filter(node => node.isShadowRoot)[0];
    333 };
    334 
    335 /**
    336 * Get the NodeFront for a node that matches a given css selector inside a shadow root.
    337 *
    338 * @param {string} selector
    339 *        CSS selector of the node inside the shadow root.
    340 * @param {string | NodeFront} hostSelector
    341 *        Selector or front of the element to which the shadow root is attached.
    342 * @param {InspectorPanel} inspector
    343 *        The instance of InspectorPanel currently loaded in the toolbox
    344 * @return {Promise} Resolves the node front when the inspector is updated with the new
    345 *         node.
    346 */
    347 var getNodeFrontInShadowDom = async function (
    348  selector,
    349  hostSelector,
    350  inspector
    351 ) {
    352  const shadowRoot = await getShadowRoot(hostSelector, inspector);
    353  if (!shadowRoot) {
    354    throw new Error(
    355      "Could not find a shadow root under selector: " + hostSelector
    356    );
    357  }
    358 
    359  return inspector.walker.querySelector(shadowRoot, selector);
    360 };
    361 
    362 var focusSearchBoxUsingShortcut = async function (panelWin, callback) {
    363  info("Focusing search box");
    364  const searchBox = panelWin.document.getElementById("inspector-searchbox");
    365  const focused = once(searchBox, "focus");
    366 
    367  panelWin.focus();
    368 
    369  synthesizeKeyShortcut(INSPECTOR_L10N.getStr("inspector.searchHTML.key"));
    370 
    371  await focused;
    372 
    373  if (callback) {
    374    callback();
    375  }
    376 };
    377 
    378 /**
    379 * Get the MarkupContainer object instance that corresponds to the given
    380 * NodeFront
    381 *
    382 * @param {NodeFront} nodeFront
    383 * @param {InspectorPanel} inspector The instance of InspectorPanel currently
    384 * loaded in the toolbox
    385 * @return {MarkupContainer}
    386 */
    387 function getContainerForNodeFront(nodeFront, { markup }) {
    388  return markup.getContainer(nodeFront);
    389 }
    390 
    391 /**
    392 * Get the MarkupContainer object instance that corresponds to the given
    393 * selector
    394 *
    395 * @param {string | NodeFront} selector
    396 * @param {InspectorPanel} inspector The instance of InspectorPanel currently
    397 * loaded in the toolbox
    398 * @param {boolean} Set to true in the event that the node shouldn't be found.
    399 * @return {MarkupContainer}
    400 */
    401 var getContainerForSelector = async function (
    402  selector,
    403  inspector,
    404  expectFailure = false
    405 ) {
    406  info("Getting the markup-container for node " + selector);
    407  const nodeFront = await getNodeFront(selector, inspector);
    408  const container = getContainerForNodeFront(nodeFront, inspector);
    409 
    410  if (expectFailure) {
    411    ok(!container, "Shouldn't find markup-container for selector: " + selector);
    412  } else {
    413    ok(container, "Found markup-container for selector: " + selector);
    414  }
    415 
    416  return container;
    417 };
    418 
    419 /**
    420 * Simulate a mouse-over on the markup-container (a line in the markup-view)
    421 * that corresponds to the selector passed.
    422 *
    423 * @param {string | NodeFront} selector
    424 * @param {InspectorPanel} inspector The instance of InspectorPanel currently
    425 * loaded in the toolbox
    426 * @return {Promise} Resolves when the container is hovered and the higlighter
    427 * is shown on the corresponding node
    428 */
    429 var hoverContainer = async function (selector, inspector) {
    430  const { waitForHighlighterTypeShown } = getHighlighterTestHelpers(inspector);
    431  info("Hovering over the markup-container for node " + selector);
    432 
    433  const nodeFront = await getNodeFront(selector, inspector);
    434  const container = getContainerForNodeFront(nodeFront, inspector);
    435 
    436  const onHighlighterShown = waitForHighlighterTypeShown(
    437    inspector.highlighters.TYPES.BOXMODEL
    438  );
    439  EventUtils.synthesizeMouseAtCenter(
    440    container.tagLine,
    441    { type: "mousemove" },
    442    inspector.markup.doc.defaultView
    443  );
    444  await onHighlighterShown;
    445 };
    446 
    447 /**
    448 * Simulate a click on the markup-container (a line in the markup-view)
    449 * that corresponds to the selector passed.
    450 *
    451 * @param {string | NodeFront} selector
    452 * @param {InspectorPanel} inspector The instance of InspectorPanel currently
    453 * loaded in the toolbox
    454 * @return {Promise} Resolves when the node has been selected.
    455 */
    456 var clickContainer = async function (selector, inspector) {
    457  info("Clicking on the markup-container for node " + selector);
    458 
    459  const nodeFront = await getNodeFront(selector, inspector);
    460  const container = getContainerForNodeFront(nodeFront, inspector);
    461 
    462  const updated = inspector.once("inspector-updated");
    463  EventUtils.synthesizeMouseAtCenter(
    464    container.tagLine,
    465    { type: "mousedown" },
    466    inspector.markup.doc.defaultView
    467  );
    468  EventUtils.synthesizeMouseAtCenter(
    469    container.tagLine,
    470    { type: "mouseup" },
    471    inspector.markup.doc.defaultView
    472  );
    473  return updated;
    474 };
    475 
    476 /**
    477 * Simulate the mouse leaving the markup-view area
    478 *
    479 * @param {InspectorPanel} inspector The instance of InspectorPanel currently
    480 * loaded in the toolbox
    481 * @return a promise when done
    482 */
    483 function mouseLeaveMarkupView(inspector) {
    484  info("Leaving the markup-view area");
    485 
    486  // Find another element to mouseover over in order to leave the markup-view
    487  const btn = inspector.toolbox.doc.querySelector("#toolbox-controls");
    488 
    489  EventUtils.synthesizeMouseAtCenter(
    490    btn,
    491    { type: "mousemove" },
    492    inspector.toolbox.win
    493  );
    494 
    495  return new Promise(resolve => {
    496    executeSoon(resolve);
    497  });
    498 }
    499 
    500 /**
    501 * Dispatch the copy event on the given element
    502 */
    503 function fireCopyEvent(element) {
    504  const evt = element.ownerDocument.createEvent("Event");
    505  evt.initEvent("copy", true, true);
    506  element.dispatchEvent(evt);
    507 }
    508 
    509 /**
    510 * Undo the last markup-view action and wait for the corresponding mutation to
    511 * occur
    512 *
    513 * @param {InspectorPanel} inspector The instance of InspectorPanel currently
    514 * loaded in the toolbox
    515 * @return a promise that resolves when the markup-mutation has been treated or
    516 * rejects if no undo action is possible
    517 */
    518 function undoChange(inspector) {
    519  const canUndo = inspector.markup.undo.canUndo();
    520  ok(canUndo, "The last change in the markup-view can be undone");
    521  if (!canUndo) {
    522    return Promise.reject();
    523  }
    524 
    525  const mutated = inspector.once("markupmutation");
    526  inspector.markup.undo.undo();
    527  return mutated;
    528 }
    529 
    530 /**
    531 * Redo the last markup-view action and wait for the corresponding mutation to
    532 * occur
    533 *
    534 * @param {InspectorPanel} inspector The instance of InspectorPanel currently
    535 * loaded in the toolbox
    536 * @return a promise that resolves when the markup-mutation has been treated or
    537 * rejects if no redo action is possible
    538 */
    539 function redoChange(inspector) {
    540  const canRedo = inspector.markup.undo.canRedo();
    541  ok(canRedo, "The last change in the markup-view can be redone");
    542  if (!canRedo) {
    543    return Promise.reject();
    544  }
    545 
    546  const mutated = inspector.once("markupmutation");
    547  inspector.markup.undo.redo();
    548  return mutated;
    549 }
    550 
    551 /**
    552 * A helper that fetches a front for a node that matches the given selector or
    553 * doctype node if the selector is falsy.
    554 */
    555 async function getNodeFrontForSelector(selector, inspector) {
    556  if (selector) {
    557    info("Retrieving front for selector " + selector);
    558    return getNodeFront(selector, inspector);
    559  }
    560 
    561  info("Retrieving front for doctype node");
    562  const { nodes } = await inspector.walker.children(inspector.walker.rootNode);
    563  return nodes[0];
    564 }
    565 
    566 /**
    567 * A simple polling helper that executes a given function until it returns true.
    568 *
    569 * @param {Function} check A generator function that is expected to return true at some
    570 * stage.
    571 * @param {string} desc A text description to be displayed when the polling starts.
    572 * @param {number} attemptes Optional number of times we poll. Defaults to 10.
    573 * @param {number} timeBetweenAttempts Optional time to wait between each attempt.
    574 * Defaults to 200ms.
    575 */
    576 async function poll(check, desc, attempts = 10, timeBetweenAttempts = 200) {
    577  info(desc);
    578 
    579  for (let i = 0; i < attempts; i++) {
    580    if (await check()) {
    581      return;
    582    }
    583    await new Promise(resolve => setTimeout(resolve, timeBetweenAttempts));
    584  }
    585 
    586  throw new Error(`Timeout while: ${desc}`);
    587 }
    588 
    589 /**
    590 * Encapsulate some common operations for highlighter's tests, to have
    591 * the tests cleaner, without exposing directly `inspector`, `highlighter`, and
    592 * `highlighterTestFront` if not needed.
    593 *
    594 * @param  {string}
    595 *    The highlighter's type
    596 * @return
    597 *    A generator function that takes an object with `inspector` and `highlighterTestFront`
    598 *    properties. (see `openInspector`)
    599 */
    600 const getHighlighterHelperFor = type =>
    601  async function ({ inspector, highlighterTestFront }) {
    602    const front = inspector.inspectorFront;
    603    const highlighter = await front.getHighlighterByType(type);
    604 
    605    let prefix = "";
    606 
    607    // Internals for mouse events
    608    let prevX, prevY;
    609 
    610    // Highlighted node
    611    let highlightedNode = null;
    612 
    613    return {
    614      set prefix(value) {
    615        prefix = value;
    616      },
    617 
    618      get highlightedNode() {
    619        if (!highlightedNode) {
    620          return null;
    621        }
    622 
    623        return {
    624          async getComputedStyle(options = {}) {
    625            const pageStyle = highlightedNode.inspectorFront.pageStyle;
    626            return pageStyle.getComputed(highlightedNode, options);
    627          },
    628        };
    629      },
    630 
    631      get actorID() {
    632        if (!highlighter) {
    633          return null;
    634        }
    635 
    636        return highlighter.actorID;
    637      },
    638 
    639      async show(selector = ":root", options, frameSelector = null) {
    640        if (frameSelector) {
    641          highlightedNode = await getNodeFrontInFrames(
    642            [frameSelector, selector],
    643            inspector
    644          );
    645        } else {
    646          highlightedNode = await getNodeFront(selector, inspector);
    647        }
    648        return highlighter.show(highlightedNode, options);
    649      },
    650 
    651      async hide() {
    652        await highlighter.hide();
    653      },
    654 
    655      async isElementHidden(id) {
    656        return (
    657          (await highlighterTestFront.getHighlighterNodeAttribute(
    658            prefix + id,
    659            "hidden",
    660            highlighter
    661          )) === "true"
    662        );
    663      },
    664 
    665      async getElementTextContent(id) {
    666        return highlighterTestFront.getHighlighterNodeTextContent(
    667          prefix + id,
    668          highlighter
    669        );
    670      },
    671 
    672      async getElementAttribute(id, name) {
    673        return highlighterTestFront.getHighlighterNodeAttribute(
    674          prefix + id,
    675          name,
    676          highlighter
    677        );
    678      },
    679 
    680      async waitForElementAttributeSet(id, name) {
    681        await poll(async function () {
    682          const value = await highlighterTestFront.getHighlighterNodeAttribute(
    683            prefix + id,
    684            name,
    685            highlighter
    686          );
    687          return !!value;
    688        }, `Waiting for element ${id} to have attribute ${name} set`);
    689      },
    690 
    691      async waitForElementAttributeRemoved(id, name) {
    692        await poll(async function () {
    693          const value = await highlighterTestFront.getHighlighterNodeAttribute(
    694            prefix + id,
    695            name,
    696            highlighter
    697          );
    698          return !value;
    699        }, `Waiting for element ${id} to have attribute ${name} removed`);
    700      },
    701 
    702      async synthesizeMouse({
    703        selector = ":root",
    704        center,
    705        x,
    706        y,
    707        options,
    708      } = {}) {
    709        if (center === true) {
    710          await safeSynthesizeMouseEventAtCenterInContentPage(
    711            selector,
    712            options
    713          );
    714        } else {
    715          await safeSynthesizeMouseEventInContentPage(selector, x, y, options);
    716        }
    717      },
    718 
    719      // This object will synthesize any "mouse" prefixed event to the
    720      // `highlighterTestFront`, using the name of method called as suffix for the
    721      // event's name.
    722      // If no x, y coords are given, the previous ones are used.
    723      //
    724      // For example:
    725      //   mouse.down(10, 20); // synthesize "mousedown" at 10,20
    726      //   mouse.move(20, 30); // synthesize "mousemove" at 20,30
    727      //   mouse.up();         // synthesize "mouseup" at 20,30
    728      mouse: new Proxy(
    729        {},
    730        {
    731          get: (target, name) =>
    732            async function (x = prevX, y = prevY, selector = ":root") {
    733              prevX = x;
    734              prevY = y;
    735              await safeSynthesizeMouseEventInContentPage(selector, x, y, {
    736                type: "mouse" + name,
    737              });
    738            },
    739        }
    740      ),
    741 
    742      async finalize() {
    743        highlightedNode = null;
    744        await highlighter.finalize();
    745      },
    746    };
    747  };
    748 
    749 /**
    750 * Inspector-scoped wrapper for highlighter helpers to be used in tests.
    751 *
    752 * @param  {Inspector} inspector
    753 *         Inspector client object instance.
    754 * @return {object} Object with helper methods
    755 */
    756 function getHighlighterTestHelpers(inspector) {
    757  /**
    758   * Return a promise which resolves when a highlighter triggers the given event.
    759   *
    760   * @param  {string} type
    761   *         Highlighter type.
    762   * @param  {string} eventName
    763   *         Name of the event to listen to.
    764   * @return {Promise}
    765   *         Promise which resolves when the highlighter event occurs.
    766   *         Resolves with the data payload attached to the event.
    767   */
    768  function _waitForHighlighterTypeEvent(type, eventName) {
    769    return new Promise(resolve => {
    770      function _handler(data) {
    771        if (type === data.type) {
    772          inspector.highlighters.off(eventName, _handler);
    773          resolve(data);
    774        }
    775      }
    776 
    777      inspector.highlighters.on(eventName, _handler);
    778    });
    779  }
    780 
    781  return {
    782    getActiveHighlighter(type) {
    783      return inspector.highlighters.getActiveHighlighter(type);
    784    },
    785    getNodeForActiveHighlighter(type) {
    786      return inspector.highlighters.getNodeForActiveHighlighter(type);
    787    },
    788    waitForHighlighterTypeShown(type) {
    789      return _waitForHighlighterTypeEvent(type, "highlighter-shown");
    790    },
    791    waitForHighlighterTypeHidden(type) {
    792      return _waitForHighlighterTypeEvent(type, "highlighter-hidden");
    793    },
    794    waitForHighlighterTypeRestored(type) {
    795      return _waitForHighlighterTypeEvent(type, "highlighter-restored");
    796    },
    797    waitForHighlighterTypeDiscarded(type) {
    798      return _waitForHighlighterTypeEvent(type, "highlighter-discarded");
    799    },
    800  };
    801 }
    802 
    803 /**
    804 * Wait for the toolbox to emit the styleeditor-selected event and when done
    805 * wait for the stylesheet identified by href to be loaded in the stylesheet
    806 * editor
    807 *
    808 * @param {Toolbox} toolbox
    809 * @param {string} href
    810 *        Optional, if not provided, wait for the first editor to be ready
    811 * @return a promise that resolves to the editor when the stylesheet editor is
    812 * ready
    813 */
    814 function waitForStyleEditor(toolbox, href) {
    815  info("Waiting for the toolbox to switch to the styleeditor");
    816 
    817  return new Promise(resolve => {
    818    toolbox.once("styleeditor-selected").then(() => {
    819      const panel = toolbox.getCurrentPanel();
    820      ok(panel && panel.UI, "Styleeditor panel switched to front");
    821 
    822      // A helper that resolves the promise once it receives an editor that
    823      // matches the expected href. Returns false if the editor was not correct.
    824      const gotEditor = editor => {
    825        if (!editor) {
    826          info("Editor went away after selected?");
    827          return false;
    828        }
    829 
    830        const currentHref = editor.styleSheet.href;
    831        if (!href || (href && currentHref.endsWith(href))) {
    832          info("Stylesheet editor selected");
    833          panel.UI.off("editor-selected", gotEditor);
    834 
    835          editor.getSourceEditor().then(sourceEditor => {
    836            info("Stylesheet editor fully loaded");
    837            resolve(sourceEditor);
    838          });
    839 
    840          return true;
    841        }
    842 
    843        info("The editor was incorrect. Waiting for editor-selected event.");
    844        return false;
    845      };
    846 
    847      // The expected editor may already be selected. Check the if the currently
    848      // selected editor is the expected one and if not wait for an
    849      // editor-selected event.
    850      if (!gotEditor(panel.UI.selectedEditor)) {
    851        // The expected editor is not selected (yet). Wait for it.
    852        panel.UI.on("editor-selected", gotEditor);
    853      }
    854    });
    855  });
    856 }
    857 
    858 /**
    859 * Checks if document's active element is within the given element.
    860 *
    861 * @param  {HTMLDocument}  doc document with active element in question
    862 * @param  {DOMNode}       container element tested on focus containment
    863 * @return {boolean}
    864 */
    865 function containsFocus(doc, container) {
    866  let elm = doc.activeElement;
    867  while (elm) {
    868    if (elm === container) {
    869      return true;
    870    }
    871    elm = elm.parentNode;
    872  }
    873  return false;
    874 }
    875 
    876 /**
    877 * Listen for a new tab to open and return a promise that resolves when one
    878 * does and completes the load event.
    879 *
    880 * @return a promise that resolves to the tab object
    881 */
    882 var waitForTab = async function () {
    883  info("Waiting for a tab to open");
    884  await once(gBrowser.tabContainer, "TabOpen");
    885  const tab = gBrowser.selectedTab;
    886  await BrowserTestUtils.browserLoaded(tab.linkedBrowser);
    887  info("The tab load completed");
    888  return tab;
    889 };
    890 
    891 /**
    892 * Simulate the key input for the given input in the window.
    893 *
    894 * @param {string} input
    895 *        The string value to input
    896 * @param {Window} win
    897 *        The window containing the panel
    898 */
    899 function synthesizeKeys(input, win) {
    900  for (const key of input.split("")) {
    901    EventUtils.synthesizeKey(key, {}, win);
    902  }
    903 }
    904 
    905 /**
    906 * Make sure window is properly focused before sending a key event.
    907 *
    908 * @param {Window} win
    909 *        The window containing the panel
    910 * @param {string} key
    911 *        The string value to input
    912 */
    913 function focusAndSendKey(win, key) {
    914  win.document.documentElement.focus();
    915  EventUtils.sendKey(key, win);
    916 }
    917 
    918 /**
    919 * Given a Tooltip instance, fake a mouse event on the `target` DOM Element
    920 * and assert that the `tooltip` is correctly displayed.
    921 *
    922 * @param {Tooltip} tooltip
    923 *        The tooltip instance
    924 * @param {DOMElement} target
    925 *        The DOM Element on which a tooltip should appear
    926 *
    927 * @return a promise that resolves with the tooltip object
    928 */
    929 async function assertTooltipShownOnHover(tooltip, target) {
    930  const mouseEvent = new target.ownerDocument.defaultView.MouseEvent(
    931    "mousemove",
    932    {
    933      bubbles: true,
    934    }
    935  );
    936  target.dispatchEvent(mouseEvent);
    937 
    938  if (!tooltip.isVisible()) {
    939    info("Waiting for tooltip to be shown");
    940    await tooltip.once("shown");
    941  }
    942 
    943  ok(tooltip.isVisible(), `The tooltip is visible`);
    944 
    945  return tooltip;
    946 }
    947 
    948 /**
    949 * Given an inspector `view` object, fake a mouse event on the `target` DOM
    950 * Element and assert that the preview tooltip  is correctly displayed.
    951 *
    952 * @param {CssRuleView|ComputedView|...} view
    953 *        The instance of an inspector panel
    954 * @param {DOMElement} target
    955 *        The DOM Element on which a tooltip should appear
    956 *
    957 * @return a promise that resolves with the tooltip object
    958 */
    959 async function assertShowPreviewTooltip(view, target) {
    960  const name = "previewTooltip";
    961 
    962  // Get the tooltip. If it does not exist one will be created.
    963  const tooltip = view.tooltips.getTooltip(name);
    964  ok(tooltip, `Tooltip '${name}' has been instantiated`);
    965 
    966  const shown = tooltip.once("shown");
    967  const mouseEvent = new target.ownerDocument.defaultView.MouseEvent(
    968    "mousemove",
    969    {
    970      bubbles: true,
    971    }
    972  );
    973  target.dispatchEvent(mouseEvent);
    974 
    975  info("Waiting for tooltip to be shown");
    976  await shown;
    977 
    978  ok(tooltip.isVisible(), `The tooltip '${name}' is visible`);
    979 
    980  return tooltip;
    981 }
    982 
    983 /**
    984 * Given a `tooltip` instance, fake a mouse event on `target` DOM element
    985 * and check that the tooltip correctly disappear.
    986 *
    987 * @param {Tooltip} tooltip
    988 *        The tooltip instance
    989 * @param {DOMElement} target
    990 *        The DOM Element on which a tooltip should appear
    991 */
    992 async function assertTooltipHiddenOnMouseOut(tooltip, target) {
    993  // The tooltip actually relies on mousemove events to check if it should be hidden.
    994  const mouseEvent = new target.ownerDocument.defaultView.MouseEvent(
    995    "mousemove",
    996    {
    997      bubbles: true,
    998      relatedTarget: target,
    999    }
   1000  );
   1001  target.parentNode.dispatchEvent(mouseEvent);
   1002 
   1003  await tooltip.once("hidden");
   1004 
   1005  ok(!tooltip.isVisible(), "The tooltip is hidden on mouseout");
   1006 }
   1007 
   1008 /**
   1009 * Check the content of a `var()` tooltip on a given rule and property name.
   1010 *
   1011 * @param {CssRuleView} view
   1012 * @param {string} ruleSelector
   1013 * @param {string} propertyName
   1014 * @param {object} tooltipExpected
   1015 * @param {string} tooltipExpected.header: The HTML for the top section
   1016 *        (might be the only section when the variable is not a registered property and
   1017 *        there is no starting-style, nor computed value).
   1018 * @param {Array<string>} tooltipExpected.headerClasses: Classes applied on the header element
   1019 *        (no need to include `variable-value` which is always added).
   1020 * @param {string} tooltipExpected.computed: The HTML for the computed value section.
   1021 * @param {Array<string>} tooltipExpected.computedClasses: Classes applied on the computed value element.
   1022 * @param {Integer} tooltipExpected.index: The index in the property value for the variable
   1023 *        element we want to check. Defaults to 0 so we can quickly check values when only
   1024 *        one variable is used.
   1025 * @param {boolean} tooltipExpected.isMatched: Is the element matched or unmatched, defaults
   1026 *        to true.
   1027 * @param {string} tooltipExpected.startingStyle: The HTML for the starting-style section.
   1028 *        Pass undefined if the tooltip isn't supposed to have a `@starting-style` section.
   1029 * @param {Array<string>} tooltipExpected.startingStyleClasses: Classes applied on the
   1030 *        starting-style value element.
   1031 * @param {object} tooltipExpected.registeredProperty: Object whose properties should match
   1032 *        the displayed registered property fields, e.g:
   1033 *        {syntax:`"&lt;color&gt;"`, inherits:"true", "initial-value": "10px"}
   1034 *        The properties values are the HTML of the dd elements.
   1035 *        Pass undefined if the tooltip isn't supposed to have a @property section.
   1036 */
   1037 async function assertVariableTooltipForProperty(
   1038  view,
   1039  ruleSelector,
   1040  propertyName,
   1041  {
   1042    computed,
   1043    computedClasses = ["theme-fg-color1"],
   1044    header,
   1045    headerClasses = ["theme-fg-color1"],
   1046    index = 0,
   1047    isMatched = true,
   1048    registeredProperty,
   1049    startingStyle,
   1050    startingStyleClasses = ["theme-fg-color1"],
   1051  }
   1052 ) {
   1053  // retrieve tooltip target
   1054  const variableEl = await waitFor(
   1055    () =>
   1056      getRuleViewProperty(
   1057        view,
   1058        ruleSelector,
   1059        propertyName
   1060      ).valueSpan.querySelectorAll(".inspector-variable,.inspector-unmatched")[
   1061        index
   1062      ]
   1063  );
   1064 
   1065  if (isMatched) {
   1066    ok(
   1067      !variableEl.classList.contains("inspector-unmatched"),
   1068      `CSS variable #${index} for ${propertyName} in ${ruleSelector} is matched`
   1069    );
   1070  } else {
   1071    ok(
   1072      variableEl.classList.contains("inspector-unmatched"),
   1073      `CSS variable #${index} for ${propertyName} in ${ruleSelector} is unmatched`
   1074    );
   1075  }
   1076 
   1077  const previewTooltip = await assertShowPreviewTooltip(view, variableEl);
   1078  const valueEl = previewTooltip.panel.querySelector(".variable-value");
   1079  const computedValueEl = previewTooltip.panel.querySelector(".computed div");
   1080  const startingStyleEl = previewTooltip.panel.querySelector(
   1081    ".starting-style div"
   1082  );
   1083  const registeredPropertyEl = previewTooltip.panel.querySelector(
   1084    ".registered-property dl"
   1085  );
   1086  is(
   1087    valueEl?.innerHTML,
   1088    header,
   1089    `CSS variable #${index} preview tooltip has expected header text for ${propertyName} in ${ruleSelector}`
   1090  );
   1091  Assert.deepEqual(
   1092    [...valueEl.classList],
   1093    ["variable-value", ...headerClasses],
   1094    `CSS variable #${index} preview tooltip has expected classes for ${propertyName} in ${ruleSelector}`
   1095  );
   1096 
   1097  if (typeof computed !== "string") {
   1098    is(
   1099      computedValueEl,
   1100      null,
   1101      `CSS variable #${index} preview tooltip doesn't have computed value section for ${propertyName} in ${ruleSelector}`
   1102    );
   1103  } else {
   1104    is(
   1105      computedValueEl?.innerHTML,
   1106      computed,
   1107      `CSS variable #${index} preview tooltip has expected computed value section for ${propertyName} in ${ruleSelector}`
   1108    );
   1109    Assert.deepEqual(
   1110      [...computedValueEl.classList],
   1111      computedClasses,
   1112      `CSS variable #${index} preview tooltip has expected classes on computed value for ${propertyName} in ${ruleSelector}`
   1113    );
   1114  }
   1115 
   1116  if (!registeredProperty) {
   1117    is(
   1118      registeredPropertyEl,
   1119      null,
   1120      `CSS variable #${index} preview tooltip doesn't have registered property section for ${propertyName} in ${ruleSelector}`
   1121    );
   1122  } else {
   1123    const dts = registeredPropertyEl.querySelectorAll("dt");
   1124    const registeredPropertyEntries = Object.entries(registeredProperty);
   1125    is(
   1126      dts.length,
   1127      registeredPropertyEntries.length,
   1128      `CSS variable #${index} preview tooltip has the expected number of element in the registered property section for ${propertyName} in ${ruleSelector}`
   1129    );
   1130    for (let i = 0; i < registeredPropertyEntries.length; i++) {
   1131      const [label, value] = registeredPropertyEntries[i];
   1132      const dt = dts[i];
   1133      const dd = dt.nextElementSibling;
   1134      is(
   1135        dt.innerText,
   1136        `${label}:`,
   1137        `CSS variable #${index} preview tooltip has expected ${label} registered property element for ${propertyName} in ${ruleSelector}`
   1138      );
   1139      is(
   1140        dd.innerHTML,
   1141        value,
   1142        `CSS variable #${index} preview tooltip has expected HTML for ${label} registered property element for ${propertyName} in ${ruleSelector}`
   1143      );
   1144    }
   1145  }
   1146 
   1147  if (!startingStyle) {
   1148    is(
   1149      startingStyleEl,
   1150      null,
   1151      `CSS variable #${index} preview tooltip doesn't have a starting-style section for ${propertyName} in ${ruleSelector}`
   1152    );
   1153  } else {
   1154    is(
   1155      startingStyleEl?.innerHTML,
   1156      startingStyle,
   1157      `CSS variable #${index} preview tooltip has expected starting-style section for ${propertyName} in ${ruleSelector}`
   1158    );
   1159    Assert.deepEqual(
   1160      [...startingStyleEl.classList],
   1161      startingStyleClasses,
   1162      `CSS variable #${index} preview tooltip has expected classes on starting-style value for ${propertyName} in ${ruleSelector}`
   1163    );
   1164  }
   1165 
   1166  await assertTooltipHiddenOnMouseOut(previewTooltip, variableEl);
   1167 }
   1168 
   1169 /**
   1170 * Get the text displayed for a given DOM Element's textContent within the
   1171 * markup view.
   1172 *
   1173 * @param {string} selector
   1174 * @param {InspectorPanel} inspector
   1175 * @return {string} The text displayed in the markup view
   1176 */
   1177 async function getDisplayedNodeTextContent(selector, inspector) {
   1178  // We have to ensure that the textContent is displayed, for that the DOM
   1179  // Element has to be selected in the markup view and to be expanded.
   1180  await selectNode(selector, inspector);
   1181 
   1182  const container = await getContainerForSelector(selector, inspector);
   1183  await inspector.markup.expandNode(container.node);
   1184  await waitForMultipleChildrenUpdates(inspector);
   1185  if (container) {
   1186    const textContainer = container.elt.querySelector("pre");
   1187    return textContainer?.textContent;
   1188  }
   1189  return null;
   1190 }
   1191 
   1192 /**
   1193 * Toggle the shapes highlighter by simulating a click on the toggle
   1194 * in the rules view with the given selector and property
   1195 *
   1196 * @param {CssRuleView} view
   1197 *        The instance of the rule-view panel
   1198 * @param {string} selector
   1199 *        The selector in the rule-view to look for the property in
   1200 * @param {string} property
   1201 *        The name of the property
   1202 * @param {boolean} show
   1203 *        If true, the shapes highlighter is being shown. If false, it is being hidden
   1204 * @param {Options} options
   1205 *        Config option for the shapes highlighter. Contains:
   1206 *        - {Boolean} transformMode: whether to show the highlighter in transforms mode
   1207 */
   1208 async function toggleShapesHighlighter(
   1209  view,
   1210  selector,
   1211  property,
   1212  show,
   1213  options = {}
   1214 ) {
   1215  info(
   1216    `Toggle shapes highlighter ${
   1217      show ? "on" : "off"
   1218    } for ${property} on ${selector}`
   1219  );
   1220  const highlighters = view.highlighters;
   1221  const container = getRuleViewProperty(view, selector, property).valueSpan;
   1222  const shapesToggle = container.querySelector(".inspector-shapeswatch");
   1223 
   1224  const metaKey = options.transformMode;
   1225  const ctrlKey = options.transformMode;
   1226 
   1227  if (show) {
   1228    const onHighlighterShown = highlighters.once("shapes-highlighter-shown");
   1229    EventUtils.sendMouseEvent(
   1230      { type: "click", metaKey, ctrlKey },
   1231      shapesToggle,
   1232      view.styleWindow
   1233    );
   1234    await onHighlighterShown;
   1235  } else {
   1236    const onHighlighterHidden = highlighters.once("shapes-highlighter-hidden");
   1237    EventUtils.sendMouseEvent(
   1238      { type: "click", metaKey, ctrlKey },
   1239      shapesToggle,
   1240      view.styleWindow
   1241    );
   1242    await onHighlighterHidden;
   1243  }
   1244 }
   1245 
   1246 /**
   1247 * Toggle the provided markup container by clicking on the expand arrow and waiting for
   1248 * children to update. Similar to expandContainer helper, but this method
   1249 * uses a click rather than programatically calling expandNode().
   1250 *
   1251 * @param {InspectorPanel} inspector
   1252 *        The current inspector instance.
   1253 * @param {MarkupContainer} container
   1254 *        The markup container to click on.
   1255 * @param {object} modifiers
   1256 *        options.altKey {Boolean} Use the altKey modifier, to recursively apply
   1257 *        the action to all the children of the container.
   1258 */
   1259 async function toggleContainerByClick(
   1260  inspector,
   1261  container,
   1262  { altKey = false } = {}
   1263 ) {
   1264  EventUtils.synthesizeMouseAtCenter(
   1265    container.expander,
   1266    {
   1267      altKey,
   1268    },
   1269    inspector.markup.doc.defaultView
   1270  );
   1271 
   1272  // Wait for any pending children updates
   1273  await waitForMultipleChildrenUpdates(inspector);
   1274 }
   1275 
   1276 /**
   1277 * Simulate a color change in a given color picker tooltip.
   1278 *
   1279 * @param  {Spectrum} colorPicker
   1280 *         The color picker widget.
   1281 * @param  {Array} newRgba
   1282 *         Array of the new rgba values to be set in the color widget.
   1283 */
   1284 async function simulateColorPickerChange(colorPicker, newRgba) {
   1285  info("Getting the spectrum colorpicker object");
   1286  const spectrum = await colorPicker.spectrum;
   1287  info("Setting the new color");
   1288  spectrum.rgb = newRgba;
   1289  info("Applying the change");
   1290  spectrum.updateUI();
   1291  spectrum.onChange();
   1292 }
   1293 
   1294 /**
   1295 * Assert method to compare the current content of the markupview to a text based tree.
   1296 *
   1297 * @param {string} tree
   1298 *        Multiline string representing the markup view tree, for instance:
   1299 *        `root
   1300 *           child1
   1301 *             subchild1
   1302 *             subchild2
   1303 *           child2
   1304 *             subchild3!slotted`
   1305 *           child3!ignore-children
   1306 *        Each sub level should be indented by 2 spaces.
   1307 *        Each line contains text expected to match with the text of the corresponding
   1308 *        node in the markup view. Some suffixes are supported:
   1309 *        - !slotted -> indicates that the line corresponds to the slotted version
   1310 *        - !ignore-children -> the node might have children but do not assert them
   1311 * @param {string} selector
   1312 *        A CSS selector that will uniquely match the "root" element from the tree
   1313 * @param {Inspector} inspector
   1314 *        The inspector instance.
   1315 */
   1316 async function assertMarkupViewAsTree(tree, selector, inspector) {
   1317  const { markup } = inspector;
   1318 
   1319  info(`Find and expand the shadow DOM host matching selector ${selector}.`);
   1320  const rootFront = await getNodeFront(selector, inspector);
   1321  const rootContainer = markup.getContainer(rootFront);
   1322 
   1323  const parsedTree = _parseMarkupViewTree(tree);
   1324  const treeRoot = parsedTree.children[0];
   1325  await _checkMarkupViewNode(treeRoot, rootContainer, inspector);
   1326 }
   1327 
   1328 async function _checkMarkupViewNode(treeNode, container, inspector) {
   1329  const { node, children, path } = treeNode;
   1330  info(`Checking [${path}]`);
   1331 
   1332  const ignoreChildren = node.includes("!ignore-children");
   1333  const slotted = node.includes("!slotted");
   1334 
   1335  // Remove optional suffixes.
   1336  const nodeText = node.replace("!slotted", "").replace("!ignore-children", "");
   1337 
   1338  assertContainerHasText(container, nodeText);
   1339 
   1340  if (slotted) {
   1341    assertContainerSlotted(container);
   1342  }
   1343 
   1344  if (ignoreChildren) {
   1345    return;
   1346  }
   1347 
   1348  if (!children.length) {
   1349    ok(!container.canExpand, "Container for [" + path + "] has no children");
   1350    return;
   1351  }
   1352 
   1353  // Expand the container if not already done.
   1354  if (!container.expanded) {
   1355    await expandContainer(inspector, container);
   1356  }
   1357 
   1358  const containers = container.getChildContainers();
   1359  is(
   1360    containers.length,
   1361    children.length,
   1362    "Node [" + path + "] has the expected number of children"
   1363  );
   1364  for (let i = 0; i < children.length; i++) {
   1365    await _checkMarkupViewNode(children[i], containers[i], inspector);
   1366  }
   1367 }
   1368 
   1369 /**
   1370 * Helper designed to parse a tree represented as:
   1371 * root
   1372 *   child1
   1373 *     subchild1
   1374 *     subchild2
   1375 *   child2
   1376 *     subchild3!slotted
   1377 *
   1378 * Lines represent a simplified view of the markup, where the trimmed line is supposed to
   1379 * be included in the text content of the actual markupview container.
   1380 * This method returns an object that can be passed to _checkMarkupViewNode() to verify
   1381 * the current markup view displays the expected structure.
   1382 */
   1383 function _parseMarkupViewTree(inputString) {
   1384  const tree = {
   1385    level: 0,
   1386    children: [],
   1387  };
   1388  let lines = inputString.split("\n");
   1389  lines = lines.filter(l => l.trim());
   1390 
   1391  let currentNode = tree;
   1392  for (const line of lines) {
   1393    const nodeString = line.trim();
   1394    const level = line.split("  ").length;
   1395 
   1396    let parent;
   1397    if (level > currentNode.level) {
   1398      parent = currentNode;
   1399    } else {
   1400      parent = currentNode.parent;
   1401      for (let i = 0; i < currentNode.level - level; i++) {
   1402        parent = parent.parent;
   1403      }
   1404    }
   1405 
   1406    const node = {
   1407      node: nodeString,
   1408      children: [],
   1409      parent,
   1410      level,
   1411      path: (parent.path ? parent.path + " > " : "") + nodeString,
   1412    };
   1413 
   1414    parent.children.push(node);
   1415    currentNode = node;
   1416  }
   1417 
   1418  return tree;
   1419 }
   1420 
   1421 /**
   1422 * Assert whether the provided container is slotted.
   1423 */
   1424 function assertContainerSlotted(container) {
   1425  ok(container.isSlotted(), "Container is a slotted container");
   1426  ok(
   1427    container.elt.querySelector(".reveal-link"),
   1428    "Slotted container has a reveal link element"
   1429  );
   1430 }
   1431 
   1432 /**
   1433 * Check if the provided text can be matched anywhere in the text content for the provided
   1434 * container.
   1435 */
   1436 function assertContainerHasText(container, expectedText) {
   1437  const textContent = container.elt.textContent;
   1438  ok(
   1439    textContent.includes(expectedText),
   1440    `Container has expected text "${expectedText}"${!textContent.includes(expectedText) ? ` - got "${textContent}"` : ""}`
   1441  );
   1442 }
   1443 
   1444 function waitForMutation(inspector, type) {
   1445  return waitForNMutations(inspector, type, 1);
   1446 }
   1447 
   1448 function waitForNMutations(inspector, type, count) {
   1449  info(`Expecting ${count} markupmutation of type ${type}`);
   1450  let receivedMutations = 0;
   1451  return new Promise(resolve => {
   1452    inspector.on("markupmutation", function onMutation(mutations) {
   1453      const validMutations = mutations.filter(m => m.type === type).length;
   1454      receivedMutations = receivedMutations + validMutations;
   1455      if (receivedMutations == count) {
   1456        inspector.off("markupmutation", onMutation);
   1457        resolve();
   1458      }
   1459    });
   1460  });
   1461 }
   1462 
   1463 /**
   1464 * Move the mouse on the content page at the x,y position and check the color displayed
   1465 * in the eyedropper label.
   1466 *
   1467 * @param {HighlighterTestFront} highlighterTestFront
   1468 * @param {number} x
   1469 * @param {number} y
   1470 * @param {string} expectedColor: Hexa string of the expected color
   1471 * @param {string} assertionDescription
   1472 */
   1473 async function checkEyeDropperColorAt(
   1474  highlighterTestFront,
   1475  x,
   1476  y,
   1477  expectedColor,
   1478  assertionDescription
   1479 ) {
   1480  info(`Move mouse to ${x},${y}`);
   1481  await safeSynthesizeMouseEventInContentPage(":root", x, y, {
   1482    type: "mousemove",
   1483  });
   1484 
   1485  const colorValue = await highlighterTestFront.getEyeDropperColorValue();
   1486  is(colorValue, expectedColor, assertionDescription);
   1487 }
   1488 
   1489 /**
   1490 * Delete the provided node front using the context menu in the markup view.
   1491 * Will resolve after the inspector UI was fully updated.
   1492 *
   1493 * @param {NodeFront} node
   1494 *        The node front to delete.
   1495 * @param {Inspector} inspector
   1496 *        The current inspector panel instance.
   1497 */
   1498 async function deleteNodeWithContextMenu(node, inspector) {
   1499  const container = inspector.markup.getContainer(node);
   1500 
   1501  const allMenuItems = openContextMenuAndGetAllItems(inspector, {
   1502    target: container.tagLine,
   1503  });
   1504  const menuItem = allMenuItems.find(item => item.id === "node-menu-delete");
   1505  const onInspectorUpdated = inspector.once("inspector-updated");
   1506 
   1507  info("Clicking 'Delete Node' in the context menu.");
   1508  is(menuItem.disabled, false, "delete menu item is enabled");
   1509  menuItem.click();
   1510 
   1511  // close the open context menu
   1512  EventUtils.synthesizeKey("KEY_Escape");
   1513 
   1514  info("Waiting for inspector to update.");
   1515  await onInspectorUpdated;
   1516 
   1517  // Since the mutations are sent asynchronously from the server, the
   1518  // inspector-updated event triggered by the deletion might happen before
   1519  // the mutation is received and the element is removed from the
   1520  // breadcrumbs. See bug 1284125.
   1521  if (inspector.breadcrumbs.indexOf(node) > -1) {
   1522    info("Crumbs haven't seen deletion. Waiting for breadcrumbs-updated.");
   1523    await inspector.once("breadcrumbs-updated");
   1524  }
   1525 }
   1526 
   1527 /**
   1528 * Forces the content page to reflow and waits for the next repaint.
   1529 */
   1530 function reflowContentPage() {
   1531  return SpecialPowers.spawn(gBrowser.selectedBrowser, [], async function () {
   1532    return new Promise(resolve => {
   1533      content.document.documentElement.offsetWidth;
   1534      content.requestAnimationFrame(resolve);
   1535    });
   1536  });
   1537 }
   1538 
   1539 /**
   1540 * Get all box-model regions' adjusted boxquads for the given element
   1541 *
   1542 * @param {string | Array} selector The node selector to target a given element
   1543 * @return {Promise<object>} A promise that resolves with an object with each property of
   1544 *         a box-model region, each of them being an object with the p1/p2/p3/p4 properties.
   1545 */
   1546 async function getAllAdjustedQuadsForContentPageElement(
   1547  selector,
   1548  useTopWindowAsBoundary = true
   1549 ) {
   1550  const selectors = Array.isArray(selector) ? selector : [selector];
   1551 
   1552  const browsingContext =
   1553    selectors.length == 1
   1554      ? gBrowser.selectedBrowser.browsingContext
   1555      : await getBrowsingContextInFrames(
   1556          gBrowser.selectedBrowser.browsingContext,
   1557          selectors.slice(0, -1)
   1558        );
   1559 
   1560  const inBrowsingContextSelector = selectors.at(-1);
   1561  return SpecialPowers.spawn(
   1562    browsingContext,
   1563    [inBrowsingContextSelector, useTopWindowAsBoundary],
   1564    (_selector, _useTopWindowAsBoundary) => {
   1565      const { require } = ChromeUtils.importESModule(
   1566        "resource://devtools/shared/loader/Loader.sys.mjs"
   1567      );
   1568      const {
   1569        getAdjustedQuads,
   1570      } = require("resource://devtools/shared/layout/utils.js");
   1571 
   1572      const node = content.document.querySelector(_selector);
   1573 
   1574      const boundaryWindow = _useTopWindowAsBoundary ? content.top : content;
   1575      const regions = {};
   1576      for (const boxType of ["content", "padding", "border", "margin"]) {
   1577        regions[boxType] = getAdjustedQuads(boundaryWindow, node, boxType);
   1578      }
   1579 
   1580      return regions;
   1581    }
   1582  );
   1583 }
   1584 
   1585 /**
   1586 * Assert that the box-model highlighter's current position corresponds to the
   1587 * given node boxquads.
   1588 *
   1589 * @param {HighlighterTestFront} highlighterTestFront
   1590 * @param {string} selector The node selector to get the boxQuads from
   1591 */
   1592 async function isNodeCorrectlyHighlighted(highlighterTestFront, selector) {
   1593  const boxModel = await highlighterTestFront.getBoxModelStatus();
   1594 
   1595  const useTopWindowAsBoundary = !!highlighterTestFront.parentFront.isTopLevel;
   1596  const regions = await getAllAdjustedQuadsForContentPageElement(
   1597    selector,
   1598    useTopWindowAsBoundary
   1599  );
   1600 
   1601  for (const boxType of ["content", "padding", "border", "margin"]) {
   1602    const [quad] = regions[boxType];
   1603    for (const point in boxModel[boxType].points) {
   1604      is(
   1605        boxModel[boxType].points[point].x,
   1606        quad[point].x,
   1607        `${selector} ${boxType} point ${point} x coordinate is correct`
   1608      );
   1609      is(
   1610        boxModel[boxType].points[point].y,
   1611        quad[point].y,
   1612        `${selector} ${boxType} point ${point} y coordinate is correct`
   1613      );
   1614    }
   1615  }
   1616 }
   1617 
   1618 /**
   1619 * Get the position and size of the measuring tool.
   1620 *
   1621 * @param {object} Object returned by getHighlighterHelperFor()
   1622 * @return {Promise<object>} A promise that resolves with an object containing
   1623 *    the x, y, width, and height properties of the measuring tool which has
   1624 *    been drawn on-screen
   1625 */
   1626 async function getAreaRect({ getElementAttribute }) {
   1627  // The 'box-path' element holds the width and height of the
   1628  // measuring area as well as the position relative to its
   1629  // parent <g> element.
   1630  const d = await getElementAttribute("box-path", "d");
   1631  // The tool element itself is a <g> element grouping all paths.
   1632  // Though <g> elements do not have coordinates by themselves,
   1633  // therefore it is positioned using the 'transform' CSS property.
   1634  // So, in order to get the position of the measuring area, the
   1635  // coordinates need to be read from the translate() function.
   1636  const transform = await getElementAttribute("tool", "transform");
   1637  const reDir = /(\d+) (\d+)/g;
   1638  const reTransform = /(\d+),(\d+)/;
   1639  const coords = {
   1640    x: 0,
   1641    y: 0,
   1642    width: 0,
   1643    height: 0,
   1644  };
   1645  let match;
   1646  while ((match = reDir.exec(d))) {
   1647    let [, x, y] = match;
   1648    x = Number(x);
   1649    y = Number(y);
   1650    if (x < coords.x) {
   1651      coords.x = x;
   1652    }
   1653    if (y < coords.y) {
   1654      coords.y = y;
   1655    }
   1656    if (x > coords.width) {
   1657      coords.width = x;
   1658    }
   1659    if (y > coords.height) {
   1660      coords.height = y;
   1661    }
   1662  }
   1663 
   1664  match = reTransform.exec(transform);
   1665  coords.x += Number(match[1]);
   1666  coords.y += Number(match[2]);
   1667 
   1668  return coords;
   1669 }
   1670 
   1671 /**
   1672 * Follow a sequence of keys to be pressed in the markup view search input and check
   1673 * that the input value and the suggestions are the expected ones.
   1674 *
   1675 * @param {Inspector} inspector
   1676 * @param {Array} expected: This is the array describing the sequence.
   1677 *        Each item hasthe following shape:
   1678 *        - key {String}: The keyboard key that is pressed
   1679 *        - value {String}: The expected input value after the key was pressed
   1680 *        - suggestions {Array<String>}: An array of the labels in the autocomplete popup.
   1681 *                                       Pass an empty array if the popup should be hidden.
   1682 */
   1683 async function checkMarkupSearchSuggestions(inspector, expected) {
   1684  const searchBox = inspector.searchBox;
   1685  const popup = inspector.searchSuggestions.searchPopup;
   1686 
   1687  await focusSearchBoxUsingShortcut(inspector.panelWin);
   1688 
   1689  for (const { key, suggestions, value } of expected) {
   1690    info("Pressing " + key + " to get " + JSON.stringify(suggestions));
   1691 
   1692    const command = once(searchBox, "input");
   1693    const onSearchProcessingDone =
   1694      inspector.searchSuggestions.once("processing-done");
   1695    EventUtils.synthesizeKey(key, {}, inspector.panelWin);
   1696    await command;
   1697 
   1698    is(searchBox.value, value, "search input has expected value");
   1699 
   1700    info("Waiting for search query to complete");
   1701    await onSearchProcessingDone;
   1702 
   1703    info(
   1704      "Query completed. Performing checks for input '" +
   1705        searchBox.value +
   1706        "' - key pressed: " +
   1707        key
   1708    );
   1709 
   1710    if (suggestions.length === 0) {
   1711      ok(!popup.isOpen, `There is no suggestion for "${searchBox.value}"`);
   1712    } else {
   1713      Assert.deepEqual(
   1714        popup.getItems().map(item => item.label),
   1715        suggestions,
   1716        `Suggestions are correct for "${searchBox.value}"`
   1717      );
   1718    }
   1719  }
   1720 }