tor-browser

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

head.js (56858B)


      1 /* Any copyright is dedicated to the Public Domain.
      2 * http://creativecommons.org/publicdomain/zero/1.0/ */
      3 
      4 "use strict";
      5 
      6 /* globals Task, openToolboxForTab, gBrowser */
      7 
      8 // shared-head.js handles imports, constants, and utility functions
      9 // Load the shared-head file first.
     10 Services.scriptloader.loadSubScript(
     11  "chrome://mochitests/content/browser/devtools/client/shared/test/shared-head.js",
     12  this
     13 );
     14 
     15 // Import helpers for the new debugger
     16 Services.scriptloader.loadSubScript(
     17  "chrome://mochitests/content/browser/devtools/client/debugger/test/mochitest/shared-head.js",
     18  this
     19 );
     20 
     21 Services.scriptloader.loadSubScript(
     22  "chrome://mochitests/content/browser/devtools/client/webconsole/test/browser/shared-head.js",
     23  this
     24 );
     25 
     26 var {
     27  BrowserConsoleManager,
     28 } = require("resource://devtools/client/webconsole/browser-console-manager.js");
     29 
     30 var WCUL10n = require("resource://devtools/client/webconsole/utils/l10n.js");
     31 const DOCS_GA_PARAMS = `?${new URLSearchParams({
     32  utm_source: "devtools",
     33  utm_medium: "firefox-console-errors",
     34  utm_campaign: "default",
     35 })}`;
     36 const GA_PARAMS = `?${new URLSearchParams({
     37  utm_source: "devtools",
     38  utm_medium: "devtools-webconsole",
     39  utm_campaign: "default",
     40 })}`;
     41 
     42 const wcActions = require("resource://devtools/client/webconsole/actions/index.js");
     43 
     44 registerCleanupFunction(async function () {
     45  // Reset all cookies, tests loading sjs_slow-response-test-server.sjs will
     46  // set a foo cookie which might have side effects on other tests.
     47  Services.cookies.removeAll();
     48 });
     49 
     50 /**
     51 * Add a new tab and open the toolbox in it, and select the webconsole.
     52 *
     53 * @param string url
     54 *        The URL for the tab to be opened.
     55 * @param Boolean clearJstermHistory
     56 *        true (default) if the jsterm history should be cleared.
     57 * @param String hostId (optional)
     58 *        The type of toolbox host to be used.
     59 * @return Promise
     60 *         Resolves when the tab has been added, loaded and the toolbox has been opened.
     61 *         Resolves to the hud.
     62 */
     63 async function openNewTabAndConsole(url, clearJstermHistory = true, hostId) {
     64  const toolbox = await openNewTabAndToolbox(url, "webconsole", hostId);
     65  const hud = toolbox.getCurrentPanel().hud;
     66 
     67  if (clearJstermHistory) {
     68    // Clearing history that might have been set in previous tests.
     69    await hud.ui.wrapper.dispatchClearHistory();
     70  }
     71 
     72  return hud;
     73 }
     74 
     75 /**
     76 * Add a new tab with iframes, open the toolbox in it, and select the webconsole.
     77 *
     78 * @param string url
     79 *        The URL for the tab to be opened.
     80 * @param Arra<string> iframes
     81 *        An array of URLs that will be added to the top document.
     82 * @return Promise
     83 *         Resolves when the tab has been added, loaded, iframes loaded, and the toolbox
     84 *         has been opened. Resolves to the hud.
     85 */
     86 async function openNewTabWithIframesAndConsole(tabUrl, iframes) {
     87  // We need to add the tab and the iframes before opening the console in case we want
     88  // to handle remote frames (we don't support creating frames target when the toolbox
     89  // is already open).
     90  await addTab(tabUrl);
     91  await ContentTask.spawn(
     92    gBrowser.selectedBrowser,
     93    iframes,
     94    async function (urls) {
     95      const iframesLoadPromises = urls.map((url, i) => {
     96        const iframe = content.document.createElement("iframe");
     97        iframe.classList.add(`iframe-${i + 1}`);
     98        const onLoad = ContentTaskUtils.waitForEvent(iframe, "load");
     99        iframe.src = url;
    100        content.document.body.append(iframe);
    101        return onLoad;
    102      });
    103 
    104      await Promise.all(iframesLoadPromises);
    105    }
    106  );
    107 
    108  return openConsole();
    109 }
    110 
    111 /**
    112 * Open a new window with a tab,open the toolbox, and select the webconsole.
    113 *
    114 * @param string url
    115 *        The URL for the tab to be opened.
    116 * @return Promise<{win, hud, tab}>
    117 *         Resolves when the tab has been added, loaded and the toolbox has been opened.
    118 *         Resolves to the toolbox.
    119 */
    120 async function openNewWindowAndConsole(url) {
    121  const win = await BrowserTestUtils.openNewBrowserWindow();
    122  const tab = await addTab(url, { window: win });
    123  win.gBrowser.selectedTab = tab;
    124  const hud = await openConsole(tab);
    125  return { win, hud, tab };
    126 }
    127 
    128 /**
    129 * Subscribe to the store and log out stringinfied versions of messages.
    130 * This is a helper function for debugging, to make is easier to see what
    131 * happened during the test in the log.
    132 *
    133 * @param object hud
    134 */
    135 function logAllStoreChanges(hud) {
    136  const store = hud.ui.wrapper.getStore();
    137  // Adding logging each time the store is modified in order to check
    138  // the store state in case of failure.
    139  store.subscribe(() => {
    140    const messages = [
    141      ...store.getState().messages.mutableMessagesById.values(),
    142    ];
    143    const debugMessages = messages.map(
    144      ({ id, type, parameters, messageText }) => {
    145        return { id, type, parameters, messageText };
    146      }
    147    );
    148    info(
    149      "messages : " +
    150        JSON.stringify(debugMessages, function (key, value) {
    151          if (value && value.getGrip) {
    152            return value.getGrip();
    153          }
    154          return value;
    155        })
    156    );
    157  });
    158 }
    159 
    160 /**
    161 * Wait for messages with given message type in the web console output,
    162 * resolving once they are received.
    163 *
    164 * @param object options
    165 *        - hud: the webconsole
    166 *        - messages: Array[Object]. An array of messages to match.
    167 *          Current supported options:
    168 *            - text: {String} Partial text match in .message-body
    169 *            - typeSelector: {String} A part of selector for the message, to
    170 *                                     specify the message type.
    171 * @return promise
    172 *         A promise that is resolved to an array of the message nodes
    173 */
    174 function waitForMessagesByType({ hud, messages }) {
    175  return new Promise(resolve => {
    176    const matchedMessages = [];
    177    hud.ui.on("new-messages", function messagesReceived(newMessages) {
    178      for (const message of messages) {
    179        if (message.matched) {
    180          continue;
    181        }
    182 
    183        const typeSelector = message.typeSelector;
    184        if (!typeSelector) {
    185          throw new Error("typeSelector property is required");
    186        }
    187        if (!typeSelector.startsWith(".")) {
    188          throw new Error(
    189            "typeSelector property start with a dot e.g. `.result`"
    190          );
    191        }
    192        const selector = ".message" + typeSelector;
    193 
    194        for (const newMessage of newMessages) {
    195          const messageBody = newMessage.node.querySelector(`.message-body`);
    196          if (
    197            messageBody &&
    198            newMessage.node.matches(selector) &&
    199            messageBody.textContent.includes(message.text)
    200          ) {
    201            matchedMessages.push(newMessage);
    202            message.matched = true;
    203            const messagesLeft = messages.length - matchedMessages.length;
    204            info(
    205              `Matched a message with text: "${message.text}", ` +
    206                (messagesLeft > 0
    207                  ? `still waiting for ${messagesLeft} messages.`
    208                  : `all messages received.`)
    209            );
    210            break;
    211          }
    212        }
    213 
    214        if (matchedMessages.length === messages.length) {
    215          hud.ui.off("new-messages", messagesReceived);
    216          resolve(matchedMessages);
    217          return;
    218        }
    219      }
    220    });
    221  });
    222 }
    223 
    224 /**
    225 * Wait for a message with the provided text and showing the provided repeat count.
    226 *
    227 * @param {object} hud : the webconsole
    228 * @param {string} text : text included in .message-body
    229 * @param {string} typeSelector : A part of selector for the message, to
    230 *                                specify the message type.
    231 * @param {number} repeat : expected repeat count in .message-repeats
    232 */
    233 function waitForRepeatedMessageByType(hud, text, typeSelector, repeat) {
    234  return waitFor(() => {
    235    // Wait for a message matching the provided text.
    236    const node = findMessageByType(hud, text, typeSelector);
    237    if (!node) {
    238      return false;
    239    }
    240 
    241    // Check if there is a repeat node with the expected count.
    242    const repeatNode = node.querySelector(".message-repeats");
    243    if (repeatNode && parseInt(repeatNode.textContent, 10) === repeat) {
    244      return node;
    245    }
    246 
    247    return false;
    248  });
    249 }
    250 
    251 /**
    252 * Wait for a single message with given message type in the web console output,
    253 * resolving with the first message that matches the query once it is received.
    254 *
    255 * @param {object} hud : the webconsole
    256 * @param {string} text : text included in .message-body
    257 * @param {string} typeSelector : A part of selector for the message, to
    258 *                                specify the message type.
    259 * @return promise
    260 *         A promise that is resolved to the message node
    261 */
    262 async function waitForMessageByType(hud, text, typeSelector) {
    263  const messages = await waitForMessagesByType({
    264    hud,
    265    messages: [{ text, typeSelector }],
    266  });
    267  return messages[0];
    268 }
    269 
    270 /**
    271 * Wait for the Source editor to be available.
    272 *
    273 * @param {object} panel
    274 * @returns
    275 */
    276 async function waitForSourceEditor(panel) {
    277  return waitUntil(() => {
    278    return !!panel.querySelector(".cm-editor");
    279  });
    280 }
    281 
    282 /**
    283 * Execute an input expression.
    284 *
    285 * @param {object} hud : The webconsole.
    286 * @param {string} input : The input expression to execute.
    287 */
    288 function execute(hud, input) {
    289  return hud.ui.wrapper.dispatchEvaluateExpression(input);
    290 }
    291 
    292 /**
    293 * Execute an input expression and wait for a message with the expected text
    294 * with given message type to be displayed in the output.
    295 *
    296 * @param {object} hud : The webconsole.
    297 * @param {string} input : The input expression to execute.
    298 * @param {string} matchingText : A string that should match the message body content.
    299 * @param {string} typeSelector : A part of selector for the message, to
    300 *                                specify the message type.
    301 */
    302 function executeAndWaitForMessageByType(
    303  hud,
    304  input,
    305  matchingText,
    306  typeSelector
    307 ) {
    308  const onMessage = waitForMessageByType(hud, matchingText, typeSelector);
    309  execute(hud, input);
    310  return onMessage;
    311 }
    312 
    313 /**
    314 * Type-specific wrappers for executeAndWaitForMessageByType
    315 *
    316 * @param {object} hud : The webconsole.
    317 * @param {string} input : The input expression to execute.
    318 * @param {string} matchingText : A string that should match the message body
    319 *                                content.
    320 */
    321 function executeAndWaitForResultMessage(hud, input, matchingText) {
    322  return executeAndWaitForMessageByType(hud, input, matchingText, ".result");
    323 }
    324 
    325 function executeAndWaitForErrorMessage(hud, input, matchingText) {
    326  return executeAndWaitForMessageByType(hud, input, matchingText, ".error");
    327 }
    328 
    329 /**
    330 * Set the input value, simulates the right keyboard event to evaluate it,
    331 * depending on if the console is in editor mode or not, and wait for a message
    332 * with the expected text with given message type to be displayed in the output.
    333 *
    334 * @param {object} hud : The webconsole.
    335 * @param {string} input : The input expression to execute.
    336 * @param {string} matchingText : A string that should match the message body
    337 *                                content.
    338 * @param {string} typeSelector : A part of selector for the message, to
    339 *                                specify the message type.
    340 */
    341 function keyboardExecuteAndWaitForMessageByType(
    342  hud,
    343  input,
    344  matchingText,
    345  typeSelector
    346 ) {
    347  hud.jsterm.focus();
    348  setInputValue(hud, input);
    349  const onMessage = waitForMessageByType(hud, matchingText, typeSelector);
    350  if (isEditorModeEnabled(hud)) {
    351    EventUtils.synthesizeKey("KEY_Enter", {
    352      [Services.appinfo.OS === "Darwin" ? "metaKey" : "ctrlKey"]: true,
    353    });
    354  } else {
    355    EventUtils.synthesizeKey("VK_RETURN");
    356  }
    357  return onMessage;
    358 }
    359 
    360 /**
    361 * Type-specific wrappers for keyboardExecuteAndWaitForMessageByType
    362 *
    363 * @param {object} hud : The webconsole.
    364 * @param {string} input : The input expression to execute.
    365 * @param {string} matchingText : A string that should match the message body
    366 *                                content.
    367 */
    368 function keyboardExecuteAndWaitForResultMessage(hud, input, matchingText) {
    369  return keyboardExecuteAndWaitForMessageByType(
    370    hud,
    371    input,
    372    matchingText,
    373    ".result"
    374  );
    375 }
    376 
    377 /**
    378 * Wait for a message to be logged and ensure it is logged only once.
    379 *
    380 * @param object hud
    381 *        The web console.
    382 * @param string text
    383 *        A substring that can be found in the message.
    384 * @param string typeSelector
    385 *        A part of selector for the message, to specify the message type.
    386 * @return {Node} the node corresponding the found message
    387 */
    388 async function checkUniqueMessageExists(hud, msg, typeSelector) {
    389  info(`Checking "${msg}" was logged`);
    390  let messages;
    391  try {
    392    messages = await waitFor(async () => {
    393      const msgs = await findMessagesVirtualizedByType({
    394        hud,
    395        text: msg,
    396        typeSelector,
    397      });
    398      return msgs.length ? msgs : null;
    399    });
    400  } catch (e) {
    401    ok(false, `Message "${msg}" wasn't logged\n`);
    402    return null;
    403  }
    404 
    405  is(messages.length, 1, `"${msg}" was logged once`);
    406  const [messageEl] = messages;
    407  const repeatNode = messageEl.querySelector(".message-repeats");
    408  is(repeatNode, null, `"${msg}" wasn't repeated`);
    409  return messageEl;
    410 }
    411 
    412 /**
    413 * Simulate a context menu event on the provided element, and wait for the console context
    414 * menu to open. Returns a promise that resolves the menu popup element.
    415 *
    416 * @param object hud
    417 *        The web console.
    418 * @param element element
    419 *        The dom element on which the context menu event should be synthesized.
    420 * @return promise
    421 */
    422 async function openContextMenu(hud, element) {
    423  const onConsoleMenuOpened = hud.ui.wrapper.once("menu-open");
    424  synthesizeContextMenuEvent(element);
    425  await onConsoleMenuOpened;
    426  return _getContextMenu(hud);
    427 }
    428 
    429 /**
    430 * Hide the webconsole context menu popup. Returns a promise that will resolve when the
    431 * context menu popup is hidden or immediately if the popup can't be found.
    432 *
    433 * @param object hud
    434 *        The web console.
    435 * @return promise
    436 */
    437 function hideContextMenu(hud) {
    438  const popup = _getContextMenu(hud);
    439  if (!popup || popup.state == "hidden") {
    440    return Promise.resolve();
    441  }
    442 
    443  const onPopupHidden = once(popup, "popuphidden");
    444  popup.hidePopup();
    445  return onPopupHidden;
    446 }
    447 
    448 function _getContextMenu(hud) {
    449  const toolbox = hud.toolbox;
    450  const doc = toolbox ? toolbox.topWindow.document : hud.chromeWindow.document;
    451  return doc.getElementById("webconsole-menu");
    452 }
    453 
    454 /**
    455 * Toggle Enable network monitoring setting
    456 *
    457 *  @param object hud
    458 *         The web console.
    459 *  @param boolean shouldBeSwitchedOn
    460 *         The expected state the setting should be in after the toggle.
    461 */
    462 async function toggleNetworkMonitoringConsoleSetting(hud, shouldBeSwitchedOn) {
    463  const selector =
    464    ".webconsole-console-settings-menu-item-enableNetworkMonitoring";
    465  const settingChanged = waitFor(() => {
    466    const el = getConsoleSettingElement(hud, selector);
    467    return shouldBeSwitchedOn
    468      ? el.getAttribute("aria-checked") === "true"
    469      : el.getAttribute("aria-checked") !== "true";
    470  });
    471  await toggleConsoleSetting(hud, selector);
    472  await settingChanged;
    473 }
    474 
    475 async function toggleConsoleSetting(hud, selector) {
    476  const toolbox = hud.toolbox;
    477  const doc = toolbox ? toolbox.doc : hud.chromeWindow.document;
    478 
    479  const menuItem = doc.querySelector(selector);
    480  menuItem.click();
    481 }
    482 
    483 function getConsoleSettingElement(hud, selector) {
    484  const toolbox = hud.toolbox;
    485  const doc = toolbox ? toolbox.doc : hud.chromeWindow.document;
    486 
    487  return doc.querySelector(selector);
    488 }
    489 
    490 function checkConsoleSettingState(hud, selector, enabled) {
    491  const el = getConsoleSettingElement(hud, selector);
    492  const checked = el.getAttribute("aria-checked") === "true";
    493 
    494  if (enabled) {
    495    ok(checked, "setting is enabled");
    496  } else {
    497    ok(!checked, "setting is disabled");
    498  }
    499 }
    500 
    501 /**
    502 * Returns a promise that resolves when the node passed as an argument mutate
    503 * according to the passed configuration.
    504 *
    505 * @param {Node} node - The node to observe mutations on.
    506 * @param {object} observeConfig - A configuration object for MutationObserver.observe.
    507 * @returns {Promise}
    508 */
    509 function waitForNodeMutation(node, observeConfig = {}) {
    510  return new Promise(resolve => {
    511    const observer = new MutationObserver(mutations => {
    512      resolve(mutations);
    513      observer.disconnect();
    514    });
    515    observer.observe(node, observeConfig);
    516  });
    517 }
    518 
    519 /**
    520 * Search for a given message. When found, simulate a click on the
    521 * message's location, checking to make sure that the debugger opens
    522 * the corresponding URL. If the message was generated by a logpoint,
    523 * check if the corresponding logpoint editing panel is opened.
    524 *
    525 * @param {object} hud
    526 *        The webconsole
    527 * @param {object} options
    528 *        - text: {String} The text to search for. This should be contained in
    529 *                         the message. The searching is done with
    530 *                         @see findMessageByType.
    531 *        - typeSelector: {string} A part of selector for the message, to
    532 *                                 specify the message type.
    533 *        - url : {String|null} URL expected to be opened.
    534 *        - line : {Number|null} Line expected to be opened.
    535 *        - column : {Number|null} Column expected to be opened.
    536 *        - logPointExpr: {String} The logpoint expression
    537 */
    538 async function testOpenInDebugger(
    539  hud,
    540  { text, typeSelector, url, column, line, logPointExpr = undefined }
    541 ) {
    542  info(`Finding message for open-in-debugger test; text is "${text}"`);
    543  const messageNode = await waitFor(() =>
    544    findMessageByType(hud, text, typeSelector)
    545  );
    546  const locationNode = messageNode.querySelector(".message-location");
    547  ok(locationNode, "The message does have a location link");
    548 
    549  await clickAndAssertFrameLinkNode(
    550    hud.toolbox,
    551    locationNode,
    552    { url, column, line },
    553    logPointExpr
    554  );
    555 }
    556 
    557 /**
    558 * Returns true if the give node is currently focused.
    559 */
    560 function hasFocus(node) {
    561  return (
    562    node.ownerDocument.activeElement == node && node.ownerDocument.hasFocus()
    563  );
    564 }
    565 
    566 /**
    567 * Get the value of the console input .
    568 *
    569 * @param {WebConsole} hud: The webconsole
    570 * @returns {string}: The value of the console input.
    571 */
    572 function getInputValue(hud) {
    573  return hud.jsterm._getValue();
    574 }
    575 
    576 /**
    577 * Set the value of the console input .
    578 *
    579 * @param {WebConsole} hud: The webconsole
    580 * @param {string} value : The value to set the console input to.
    581 */
    582 function setInputValue(hud, value) {
    583  const onValueSet = hud.jsterm.once("set-input-value");
    584  hud.jsterm._setValue(value);
    585  return onValueSet;
    586 }
    587 
    588 /**
    589 * Set the value of the console input and its caret position, and wait for the
    590 * autocompletion to be updated.
    591 *
    592 * @param {WebConsole} hud: The webconsole
    593 * @param {string} value : The value to set the jsterm to.
    594 * @param {Integer} caretPosition : The index where to place the cursor. A negative
    595 *                  number will place the caret at (value.length - offset) position.
    596 *                  Default to value.length (caret set at the end).
    597 * @returns {Promise} resolves when the jsterm is completed.
    598 */
    599 async function setInputValueForAutocompletion(
    600  hud,
    601  value,
    602  caretPosition = value.length
    603 ) {
    604  const { jsterm } = hud;
    605 
    606  const initialPromises = [];
    607  if (jsterm.autocompletePopup.isOpen) {
    608    initialPromises.push(jsterm.autocompletePopup.once("popup-closed"));
    609  }
    610  setInputValue(hud, "");
    611  await Promise.all(initialPromises);
    612 
    613  // Wait for next tick. Tooltip tests sometimes fail to successively hide and
    614  // show tooltips on Win32 debug.
    615  await waitForTick();
    616 
    617  jsterm.focus();
    618 
    619  const updated = jsterm.once("autocomplete-updated");
    620  EventUtils.sendString(value, hud.iframeWindow);
    621  await updated;
    622 
    623  // Wait for next tick. Tooltip tests sometimes fail to successively hide and
    624  // show tooltips on Win32 debug.
    625  await waitForTick();
    626 
    627  if (caretPosition < 0) {
    628    caretPosition = value.length + caretPosition;
    629  }
    630 
    631  if (Number.isInteger(caretPosition)) {
    632    jsterm.editor.setCursor(jsterm.editor.getPosition(caretPosition));
    633  }
    634 }
    635 
    636 /**
    637 * Set the value of the console input and wait for the confirm dialog to be displayed.
    638 *
    639 * @param {Toolbox} toolbox
    640 * @param {WebConsole} hud
    641 * @param {string} value : The value to set the jsterm to.
    642 *                  Default to value.length (caret set at the end).
    643 * @returns {Promise<HTMLElement>} resolves with dialog element when it is opened.
    644 */
    645 async function setInputValueForGetterConfirmDialog(toolbox, hud, value) {
    646  await setInputValueForAutocompletion(hud, value);
    647  await waitFor(() => isConfirmDialogOpened(toolbox));
    648  ok(true, "The confirm dialog is displayed");
    649  return getConfirmDialog(toolbox);
    650 }
    651 
    652 /**
    653 * Checks if the console input has the expected completion value.
    654 *
    655 * @param {WebConsole} hud
    656 * @param {string} expectedValue
    657 * @param {string} assertionInfo: Description of the assertion passed to `is`.
    658 */
    659 function checkInputCompletionValue(hud, expectedValue, assertionInfo) {
    660  const completionValue = getInputCompletionValue(hud);
    661  if (completionValue === null) {
    662    ok(false, "Couldn't retrieve the completion value");
    663  }
    664 
    665  info(`Expects "${expectedValue}", is "${completionValue}"`);
    666  is(completionValue, expectedValue, assertionInfo);
    667 }
    668 
    669 /**
    670 * Checks if the cursor on console input is at expected position.
    671 *
    672 * @param {WebConsole} hud
    673 * @param {Integer} expectedCursorIndex
    674 * @param {string} assertionInfo: Description of the assertion passed to `is`.
    675 */
    676 function checkInputCursorPosition(hud, expectedCursorIndex, assertionInfo) {
    677  const { jsterm } = hud;
    678  is(jsterm.editor.getCursor().ch, expectedCursorIndex, assertionInfo);
    679 }
    680 
    681 /**
    682 * Checks the console input value and the cursor position given an expected string
    683 * containing a "|" to indicate the expected cursor position.
    684 *
    685 * @param {WebConsole} hud
    686 * @param {string} expectedStringWithCursor:
    687 *                  String with a "|" to indicate the expected cursor position.
    688 *                  For example, this is how you assert an empty value with the focus "|",
    689 *                  and this indicates the value should be "test" and the cursor at the
    690 *                  end of the input: "test|".
    691 * @param {string} assertionInfo: Description of the assertion passed to `is`.
    692 */
    693 function checkInputValueAndCursorPosition(
    694  hud,
    695  expectedStringWithCursor,
    696  assertionInfo
    697 ) {
    698  info(`Checking jsterm state: \n${expectedStringWithCursor}`);
    699  if (!expectedStringWithCursor.includes("|")) {
    700    ok(
    701      false,
    702      `expectedStringWithCursor must contain a "|" char to indicate cursor position`
    703    );
    704  }
    705 
    706  const inputValue = expectedStringWithCursor.replace("|", "");
    707  const { jsterm } = hud;
    708 
    709  is(getInputValue(hud), inputValue, "console input has expected value");
    710  const lines = expectedStringWithCursor.split("\n");
    711  const lineWithCursor = lines.findIndex(line => line.includes("|"));
    712  const { ch, line } = jsterm.editor.getCursor();
    713  is(line, lineWithCursor, assertionInfo + " - correct line");
    714  is(ch, lines[lineWithCursor].indexOf("|"), assertionInfo + " - correct ch");
    715 }
    716 
    717 /**
    718 * Returns the console input completion value.
    719 *
    720 * @param {WebConsole} hud
    721 * @returns {string}
    722 */
    723 function getInputCompletionValue(hud) {
    724  const { jsterm } = hud;
    725  return jsterm.editor.getAutoCompletionText();
    726 }
    727 
    728 function closeAutocompletePopup(hud) {
    729  const { jsterm } = hud;
    730 
    731  if (!jsterm.autocompletePopup.isOpen) {
    732    return Promise.resolve();
    733  }
    734 
    735  const onPopupClosed = jsterm.autocompletePopup.once("popup-closed");
    736  const onAutocompleteUpdated = jsterm.once("autocomplete-updated");
    737  EventUtils.synthesizeKey("KEY_Escape");
    738  return Promise.all([onPopupClosed, onAutocompleteUpdated]);
    739 }
    740 
    741 /**
    742 * Returns a boolean indicating if the console input is focused.
    743 *
    744 * @param {WebConsole} hud
    745 * @returns {boolean}
    746 */
    747 function isInputFocused(hud) {
    748  const { jsterm } = hud;
    749  const document = hud.ui.outputNode.ownerDocument;
    750  const documentIsFocused = document.hasFocus();
    751  return documentIsFocused && jsterm.editor.hasFocus();
    752 }
    753 
    754 /**
    755 * Open the JavaScript debugger.
    756 *
    757 * @param object options
    758 *        Options for opening the debugger:
    759 *        - tab: the tab you want to open the debugger for.
    760 * @return object
    761 *         A promise that is resolved once the debugger opens, or rejected if
    762 *         the open fails. The resolution callback is given one argument, an
    763 *         object that holds the following properties:
    764 *         - target: the Target object for the Tab.
    765 *         - toolbox: the Toolbox instance.
    766 *         - panel: the jsdebugger panel instance.
    767 */
    768 async function openDebugger(options = {}) {
    769  if (!options.tab) {
    770    options.tab = gBrowser.selectedTab;
    771  }
    772 
    773  let toolbox = gDevTools.getToolboxForTab(options.tab);
    774  const dbgPanelAlreadyOpen = toolbox && toolbox.getPanel("jsdebugger");
    775  if (dbgPanelAlreadyOpen) {
    776    await toolbox.selectTool("jsdebugger");
    777 
    778    return {
    779      target: toolbox.target,
    780      toolbox,
    781      panel: toolbox.getCurrentPanel(),
    782    };
    783  }
    784 
    785  toolbox = await gDevTools.showToolboxForTab(options.tab, {
    786    toolId: "jsdebugger",
    787  });
    788  const panel = toolbox.getCurrentPanel();
    789 
    790  await toolbox.threadFront.getSources();
    791 
    792  return { target: toolbox.target, toolbox, panel };
    793 }
    794 
    795 async function openInspector(options = {}) {
    796  if (!options.tab) {
    797    options.tab = gBrowser.selectedTab;
    798  }
    799 
    800  const toolbox = await gDevTools.showToolboxForTab(options.tab, {
    801    toolId: "inspector",
    802  });
    803 
    804  return toolbox.getCurrentPanel();
    805 }
    806 
    807 /**
    808 * Open the netmonitor for the given tab, or the current one if none given.
    809 *
    810 * @param Element tab
    811 *        Optional tab element for which you want open the netmonitor.
    812 *        Defaults to current selected tab.
    813 * @return Promise
    814 *         A promise that is resolved with the netmonitor panel once the netmonitor is open.
    815 */
    816 async function openNetMonitor(tab) {
    817  tab = tab || gBrowser.selectedTab;
    818  let toolbox = gDevTools.getToolboxForTab(tab);
    819  if (!toolbox) {
    820    toolbox = await gDevTools.showToolboxForTab(tab);
    821  }
    822  await toolbox.selectTool("netmonitor");
    823  return toolbox.getCurrentPanel();
    824 }
    825 
    826 /**
    827 * Open the Web Console for the given tab, or the current one if none given.
    828 *
    829 * @param Element tab
    830 *        Optional tab element for which you want open the Web Console.
    831 *        Defaults to current selected tab.
    832 * @return Promise
    833 *         A promise that is resolved with the console hud once the web console is open.
    834 */
    835 async function openConsole(tab) {
    836  tab = tab || gBrowser.selectedTab;
    837  const toolbox = await gDevTools.showToolboxForTab(tab, {
    838    toolId: "webconsole",
    839  });
    840  return toolbox.getCurrentPanel().hud;
    841 }
    842 
    843 /**
    844 * Close the Web Console for the given tab.
    845 *
    846 * @param Element [tab]
    847 *        Optional tab element for which you want close the Web Console.
    848 *        Defaults to current selected tab.
    849 * @return object
    850 *         A promise that is resolved once the web console is closed.
    851 */
    852 async function closeConsole(tab = gBrowser.selectedTab) {
    853  const toolbox = gDevTools.getToolboxForTab(tab);
    854  if (toolbox) {
    855    await toolbox.destroy();
    856  }
    857 }
    858 
    859 /**
    860 * Open a network request logged in the webconsole in the netmonitor panel.
    861 *
    862 * @param {object} toolbox
    863 * @param {object} hud
    864 * @param {string} url
    865 *        URL of the request as logged in the netmonitor.
    866 * @param {string} urlInConsole
    867 *        (optional) Use if the logged URL in webconsole is different from the real URL.
    868 */
    869 async function openMessageInNetmonitor(toolbox, hud, url, urlInConsole) {
    870  // By default urlInConsole should be the same as the complete url.
    871  urlInConsole = urlInConsole || url;
    872 
    873  const message = await waitFor(() =>
    874    findMessageByType(hud, urlInConsole, ".network")
    875  );
    876 
    877  const onNetmonitorSelected = toolbox.once(
    878    "netmonitor-selected",
    879    (event, panel) => {
    880      return panel;
    881    }
    882  );
    883 
    884  const menuPopup = await openContextMenu(hud, message);
    885  const openInNetMenuItem = menuPopup.querySelector(
    886    "#console-menu-open-in-network-panel"
    887  );
    888  ok(openInNetMenuItem, "open in network panel item is enabled");
    889  menuPopup.activateItem(openInNetMenuItem);
    890 
    891  const { panelWin } = await onNetmonitorSelected;
    892  ok(
    893    true,
    894    "The netmonitor panel is selected when clicking on the network message"
    895  );
    896 
    897  const { store, windowRequire } = panelWin;
    898  const nmActions = windowRequire(
    899    "devtools/client/netmonitor/src/actions/index"
    900  );
    901  const { getSelectedRequest } = windowRequire(
    902    "devtools/client/netmonitor/src/selectors/index"
    903  );
    904 
    905  store.dispatch(nmActions.batchEnable(false));
    906 
    907  await waitFor(() => {
    908    const selected = getSelectedRequest(store.getState());
    909    return selected && selected.url === url;
    910  }, `network entry for the URL "${url}" wasn't found`);
    911 
    912  ok(true, "The attached url is correct.");
    913 
    914  info(
    915    "Wait for the netmonitor headers panel to appear as it spawns RDP requests"
    916  );
    917  await waitFor(() =>
    918    panelWin.document.querySelector("#headers-panel .headers-overview")
    919  );
    920 }
    921 
    922 function selectNode(hud, node) {
    923  const outputContainer = hud.ui.outputNode.querySelector(".webconsole-output");
    924 
    925  // We must first blur the input or else we can't select anything.
    926  outputContainer.ownerDocument.activeElement.blur();
    927 
    928  const selection = outputContainer.ownerDocument.getSelection();
    929  const range = document.createRange();
    930  range.selectNodeContents(node);
    931  selection.removeAllRanges();
    932  selection.addRange(range);
    933 
    934  return selection;
    935 }
    936 
    937 async function waitForBrowserConsole() {
    938  return new Promise(resolve => {
    939    Services.obs.addObserver(function observer(subject) {
    940      Services.obs.removeObserver(observer, "web-console-created");
    941      subject.QueryInterface(Ci.nsISupportsString);
    942 
    943      const hud = BrowserConsoleManager.getBrowserConsole();
    944      ok(hud, "browser console is open");
    945      is(subject.data, hud.hudId, "notification hudId is correct");
    946 
    947      executeSoon(() => resolve(hud));
    948    }, "web-console-created");
    949  });
    950 }
    951 
    952 /**
    953 * Get the state of a console filter.
    954 *
    955 * @param {object} hud
    956 */
    957 async function getFilterState(hud) {
    958  const { outputNode } = hud.ui;
    959  const filterBar = outputNode.querySelector(".webconsole-filterbar-secondary");
    960  const buttons = filterBar.querySelectorAll("button");
    961  const result = {};
    962 
    963  for (const button of buttons) {
    964    result[button.dataset.category] =
    965      button.getAttribute("aria-pressed") === "true";
    966  }
    967 
    968  return result;
    969 }
    970 
    971 /**
    972 * Return the filter input element.
    973 *
    974 * @param {object} hud
    975 * @return {HTMLInputElement}
    976 */
    977 function getFilterInput(hud) {
    978  return hud.ui.outputNode.querySelector(".devtools-searchbox input");
    979 }
    980 
    981 /**
    982 * Set the state of a console filter.
    983 *
    984 * @param {object} hud
    985 * @param {object} settings
    986 *        Category settings in the following format:
    987 *          {
    988 *            error: true,
    989 *            warn: true,
    990 *            log: true,
    991 *            info: true,
    992 *            debug: true,
    993 *            css: false,
    994 *            netxhr: false,
    995 *            net: false,
    996 *            text: ""
    997 *          }
    998 */
    999 async function setFilterState(hud, settings) {
   1000  const { outputNode } = hud.ui;
   1001  const filterBar = outputNode.querySelector(".webconsole-filterbar-secondary");
   1002 
   1003  for (const category in settings) {
   1004    const value = settings[category];
   1005    const button = filterBar.querySelector(`[data-category="${category}"]`);
   1006 
   1007    if (category === "text") {
   1008      const filterInput = getFilterInput(hud);
   1009      filterInput.focus();
   1010      filterInput.select();
   1011      const win = outputNode.ownerDocument.defaultView;
   1012      if (!value) {
   1013        EventUtils.synthesizeKey("KEY_Delete", {}, win);
   1014      } else {
   1015        EventUtils.sendString(value, win);
   1016      }
   1017      await waitFor(() => filterInput.value === value);
   1018      continue;
   1019    }
   1020 
   1021    if (!button) {
   1022      ok(
   1023        false,
   1024        `setFilterState() called with a category of ${category}, ` +
   1025          `which doesn't exist.`
   1026      );
   1027    }
   1028 
   1029    info(
   1030      `Setting the ${category} category to ${value ? "checked" : "disabled"}`
   1031    );
   1032 
   1033    const isPressed = button.getAttribute("aria-pressed");
   1034 
   1035    if ((!value && isPressed === "true") || (value && isPressed !== "true")) {
   1036      button.click();
   1037 
   1038      await waitFor(() => {
   1039        const pressed = button.getAttribute("aria-pressed");
   1040        if (!value) {
   1041          return pressed === "false" || pressed === null;
   1042        }
   1043        return pressed === "true";
   1044      });
   1045    }
   1046  }
   1047 }
   1048 
   1049 /**
   1050 * Reset the filters at the end of a test that has changed them. This is
   1051 * important when using the `--verify` test option as when it is used you need
   1052 * to manually reset the filters.
   1053 *
   1054 * The css, netxhr and net filters are disabled by default.
   1055 *
   1056 * @param {object} hud
   1057 */
   1058 async function resetFilters(hud) {
   1059  info("Resetting filters to their default state");
   1060 
   1061  const store = hud.ui.wrapper.getStore();
   1062  store.dispatch(wcActions.filtersClear());
   1063 }
   1064 
   1065 /**
   1066 * Open the reverse search input by simulating the appropriate keyboard shortcut.
   1067 *
   1068 * @param {object} hud
   1069 * @returns {DOMNode} The reverse search dom node.
   1070 */
   1071 async function openReverseSearch(hud) {
   1072  info("Open the reverse search UI with a keyboard shortcut");
   1073  const onReverseSearchUiOpen = waitFor(() => getReverseSearchElement(hud));
   1074  const isMacOS = AppConstants.platform === "macosx";
   1075  if (isMacOS) {
   1076    EventUtils.synthesizeKey("r", { ctrlKey: true });
   1077  } else {
   1078    EventUtils.synthesizeKey("VK_F9");
   1079  }
   1080 
   1081  const element = await onReverseSearchUiOpen;
   1082  return element;
   1083 }
   1084 
   1085 function getReverseSearchElement(hud) {
   1086  const { outputNode } = hud.ui;
   1087  return outputNode.querySelector(".reverse-search");
   1088 }
   1089 
   1090 function getReverseSearchInfoElement(hud) {
   1091  const reverseSearchElement = getReverseSearchElement(hud);
   1092  if (!reverseSearchElement) {
   1093    return null;
   1094  }
   1095 
   1096  return reverseSearchElement.querySelector(".reverse-search-info");
   1097 }
   1098 
   1099 /**
   1100 * Returns a boolean indicating if the reverse search input is focused.
   1101 *
   1102 * @param {WebConsole} hud
   1103 * @returns {boolean}
   1104 */
   1105 function isReverseSearchInputFocused(hud) {
   1106  const { outputNode } = hud.ui;
   1107  const document = outputNode.ownerDocument;
   1108  const documentIsFocused = document.hasFocus();
   1109  const reverseSearchInput = outputNode.querySelector(".reverse-search-input");
   1110 
   1111  return document.activeElement == reverseSearchInput && documentIsFocused;
   1112 }
   1113 
   1114 function getEagerEvaluationElement(hud) {
   1115  return hud.ui.outputNode.querySelector(".eager-evaluation-result");
   1116 }
   1117 
   1118 async function waitForEagerEvaluationResult(hud, text) {
   1119  await waitUntil(() => {
   1120    const elem = getEagerEvaluationElement(hud);
   1121    if (elem) {
   1122      if (text instanceof RegExp) {
   1123        return text.test(elem.innerText);
   1124      }
   1125      return elem.innerText == text;
   1126    }
   1127    return false;
   1128  });
   1129  ok(true, `Got eager evaluation result ${text}`);
   1130 }
   1131 
   1132 // This just makes sure the eager evaluation result disappears. This will pass
   1133 // even for inputs which eventually have a result because nothing will be shown
   1134 // while the evaluation happens. Waiting here does make sure that a previous
   1135 // input was processed and sent down to the server for evaluating.
   1136 async function waitForNoEagerEvaluationResult(hud) {
   1137  await waitUntil(() => {
   1138    const elem = getEagerEvaluationElement(hud);
   1139    return elem && elem.innerText == "";
   1140  });
   1141  ok(true, `Eager evaluation result disappeared`);
   1142 }
   1143 
   1144 /**
   1145 * Selects a node in the inspector.
   1146 *
   1147 * @param {object} toolbox
   1148 * @param {string} selector: The selector for the node we want to select.
   1149 */
   1150 async function selectNodeWithPicker(toolbox, selector) {
   1151  const inspector = toolbox.getPanel("inspector");
   1152 
   1153  const onPickerStarted = toolbox.nodePicker.once("picker-started");
   1154  toolbox.nodePicker.start();
   1155  await onPickerStarted;
   1156 
   1157  info(
   1158    `Picker mode started, now clicking on "${selector}" to select that node`
   1159  );
   1160  const onPickerStopped = toolbox.nodePicker.once("picker-stopped");
   1161  const onInspectorUpdated = inspector.once("inspector-updated");
   1162 
   1163  await safeSynthesizeMouseEventAtCenterInContentPage(selector);
   1164 
   1165  await onPickerStopped;
   1166  await onInspectorUpdated;
   1167 }
   1168 
   1169 /**
   1170 * Clicks on the arrow of a single object inspector node if it exists.
   1171 *
   1172 * @param {HTMLElement} node: Object inspector node (.tree-node)
   1173 */
   1174 async function expandObjectInspectorNode(node) {
   1175  if (!node.classList.contains("tree-node")) {
   1176    ok(false, "Node should be a .tree-node");
   1177    return;
   1178  }
   1179  const arrow = getObjectInspectorNodeArrow(node);
   1180  if (!arrow) {
   1181    ok(false, "Node can't be expanded");
   1182    return;
   1183  }
   1184  if (arrow.classList.contains("open")) {
   1185    ok(false, "Node already expanded");
   1186    return;
   1187  }
   1188  const isLongString = node.querySelector(".node > .objectBox-string");
   1189  let onMutation;
   1190  let textContentBeforeExpand;
   1191  if (!isLongString) {
   1192    const objectInspector = node.closest(".object-inspector");
   1193    onMutation = waitForNodeMutation(objectInspector, {
   1194      childList: true,
   1195    });
   1196  } else {
   1197    textContentBeforeExpand = node.textContent;
   1198  }
   1199  arrow.click();
   1200 
   1201  // Long strings are not going to be expanded into children element.
   1202  // Instead the tree node will update itself to show the long string.
   1203  // So that we can't wait for the childList mutation.
   1204  if (isLongString) {
   1205    // Reps will expand on click...
   1206    await waitFor(() => arrow.classList.contains("open"));
   1207    // ...but it will fetch the long string content asynchronously after having expanded the TreeNode.
   1208    // So also wait for the string to be updated and be longer.
   1209    await waitFor(
   1210      () => node.textContent.length > textContentBeforeExpand.length
   1211    );
   1212  } else {
   1213    await onMutation;
   1214    // Waiting for the object inspector mutation isn't enough,
   1215    // also wait for the children element, with higher aria-level to be added to the DOM.
   1216    await waitFor(() => !!getObjectInspectorChildrenNodes(node).length);
   1217  }
   1218 
   1219  ok(
   1220    arrow.classList.contains("open"),
   1221    "The arrow of the root node of the tree is expanded after clicking on it"
   1222  );
   1223 }
   1224 
   1225 /**
   1226 * Retrieve the arrow of a single object inspector node.
   1227 *
   1228 * @param {HTMLElement} node: Object inspector node (.tree-node)
   1229 * @return {HTMLElement|null} the arrow element
   1230 */
   1231 function getObjectInspectorNodeArrow(node) {
   1232  return node.querySelector(".theme-twisty");
   1233 }
   1234 
   1235 /**
   1236 * Check if a single object inspector node is expandable.
   1237 *
   1238 * @param {HTMLElement} node: Object inspector node (.tree-node)
   1239 * @return {boolean} true if the node can be expanded
   1240 */
   1241 function isObjectInspectorNodeExpandable(node) {
   1242  return !!getObjectInspectorNodeArrow(node);
   1243 }
   1244 
   1245 /**
   1246 * Retrieve the nodes for a given object inspector element.
   1247 *
   1248 * @param {HTMLElement} oi: Object inspector element
   1249 * @return {NodeList} the object inspector nodes
   1250 */
   1251 function getObjectInspectorNodes(oi) {
   1252  return oi.querySelectorAll(".tree-node");
   1253 }
   1254 
   1255 /**
   1256 * Retrieve the "children" nodes for a given object inspector node.
   1257 *
   1258 * @param {HTMLElement} node: Object inspector node (.tree-node)
   1259 * @return {Array<HTMLElement>} the direct children (i.e. the ones that are one level
   1260 *                              deeper than the passed node)
   1261 */
   1262 function getObjectInspectorChildrenNodes(node) {
   1263  const getLevel = n => parseInt(n.getAttribute("aria-level") || "0", 10);
   1264  const level = getLevel(node);
   1265  const childLevel = level + 1;
   1266  const children = [];
   1267 
   1268  let currentNode = node;
   1269  while (
   1270    currentNode.nextSibling &&
   1271    getLevel(currentNode.nextSibling) === childLevel
   1272  ) {
   1273    currentNode = currentNode.nextSibling;
   1274    children.push(currentNode);
   1275  }
   1276 
   1277  return children;
   1278 }
   1279 
   1280 /**
   1281 * Retrieve the invoke getter button for a given object inspector node.
   1282 *
   1283 * @param {HTMLElement} node: Object inspector node (.tree-node)
   1284 * @return {HTMLElement|null} the invoke button element
   1285 */
   1286 function getObjectInspectorInvokeGetterButton(node) {
   1287  return node.querySelector(".invoke-getter");
   1288 }
   1289 
   1290 /**
   1291 * Retrieve the first node that match the passed node label, for a given object inspector
   1292 * element.
   1293 *
   1294 * @param {HTMLElement} oi: Object inspector element
   1295 * @param {string} nodeLabel: label of the searched node
   1296 * @return {HTMLElement|null} the Object inspector node with the matching label
   1297 */
   1298 function findObjectInspectorNode(oi, nodeLabel) {
   1299  return [...oi.querySelectorAll(".tree-node")].find(node => {
   1300    const label = node.querySelector(".object-label");
   1301    if (!label) {
   1302      return false;
   1303    }
   1304    return label.textContent === nodeLabel;
   1305  });
   1306 }
   1307 
   1308 /**
   1309 * Return an array of the label of the autocomplete popup items.
   1310 *
   1311 * @param {AutocompletPopup} popup
   1312 * @returns {Array<string>}
   1313 */
   1314 function getAutocompletePopupLabels(popup) {
   1315  return popup.getItems().map(item => item.label);
   1316 }
   1317 
   1318 /**
   1319 * Check if the retrieved list of autocomplete labels of the specific popup
   1320 * includes all of the expected labels.
   1321 *
   1322 * @param {AutocompletPopup} popup
   1323 * @param {Array<string>} expected the array of expected labels
   1324 */
   1325 function hasExactPopupLabels(popup, expected) {
   1326  return hasPopupLabels(popup, expected, true);
   1327 }
   1328 
   1329 /**
   1330 * Check if the expected label is included in the list of autocomplete labels
   1331 * of the specific popup.
   1332 *
   1333 * @param {AutocompletPopup} popup
   1334 * @param {string} label the label to check
   1335 */
   1336 function hasPopupLabel(popup, label) {
   1337  return hasPopupLabels(popup, [label]);
   1338 }
   1339 
   1340 /**
   1341 * Validate the expected labels against the autocomplete labels.
   1342 *
   1343 * @param {AutocompletPopup} popup
   1344 * @param {Array<string>} expectedLabels
   1345 * @param {boolean} checkAll
   1346 */
   1347 function hasPopupLabels(popup, expectedLabels, checkAll = false) {
   1348  const autocompleteLabels = getAutocompletePopupLabels(popup);
   1349  if (checkAll) {
   1350    return (
   1351      autocompleteLabels.length === expectedLabels.length &&
   1352      autocompleteLabels.every((autoLabel, idx) => {
   1353        return expectedLabels.indexOf(autoLabel) === idx;
   1354      })
   1355    );
   1356  }
   1357  return expectedLabels.every(expectedLabel => {
   1358    return autocompleteLabels.includes(expectedLabel);
   1359  });
   1360 }
   1361 
   1362 /**
   1363 * Return the "Confirm Dialog" element.
   1364 *
   1365 * @param toolbox
   1366 * @returns {HTMLElement|null}
   1367 */
   1368 function getConfirmDialog(toolbox) {
   1369  const { doc } = toolbox;
   1370  return doc.querySelector(".invoke-confirm");
   1371 }
   1372 
   1373 /**
   1374 * Returns true if the Confirm Dialog is opened.
   1375 *
   1376 * @param toolbox
   1377 * @returns {boolean}
   1378 */
   1379 function isConfirmDialogOpened(toolbox) {
   1380  const tooltip = getConfirmDialog(toolbox);
   1381  if (!tooltip) {
   1382    return false;
   1383  }
   1384 
   1385  return tooltip.classList.contains("tooltip-visible");
   1386 }
   1387 
   1388 async function selectFrame(dbg, frame) {
   1389  const onScopes = waitForDispatch(dbg.store, "ADD_SCOPES");
   1390  await dbg.actions.selectFrame(frame);
   1391  await onScopes;
   1392 }
   1393 
   1394 async function pauseDebugger(dbg, options) {
   1395  info("Waiting for debugger to pause");
   1396  const onPaused = waitForPaused(dbg, null, options);
   1397  SpecialPowers.spawn(gBrowser.selectedBrowser, [], function () {
   1398    content.wrappedJSObject.firstCall();
   1399  }).catch(() => {});
   1400  await onPaused;
   1401 }
   1402 
   1403 /**
   1404 * Check that the passed HTMLElement vertically overflows.
   1405 *
   1406 * @param {HTMLElement} container
   1407 * @returns {boolean}
   1408 */
   1409 function hasVerticalOverflow(container) {
   1410  return container.scrollHeight > container.clientHeight;
   1411 }
   1412 
   1413 /**
   1414 * Check that the passed HTMLElement is scrolled to the bottom.
   1415 *
   1416 * @param {HTMLElement} container
   1417 * @returns {boolean}
   1418 */
   1419 function isScrolledToBottom(container) {
   1420  if (!container.lastChild) {
   1421    return true;
   1422  }
   1423  const lastNodeHeight = container.lastChild.clientHeight;
   1424  return (
   1425    container.scrollTop + container.clientHeight >=
   1426    container.scrollHeight - lastNodeHeight / 2
   1427  );
   1428 }
   1429 
   1430 /**
   1431 *
   1432 * @param {WebConsole} hud
   1433 * @param {Array<string>} expectedMessages: An array of string representing the messages
   1434 *                        from the output. This can only be a part of the string of the
   1435 *                        message.
   1436 *                        Start the string with "▶︎⚠ " or "▼⚠ " to indicate that the
   1437 *                        message is a warningGroup (with respectively an open or
   1438 *                        collapsed arrow).
   1439 *                        Start the string with "|︎ " to indicate that the message is
   1440 *                        inside a group and should be indented.
   1441 */
   1442 async function checkConsoleOutputForWarningGroup(hud, expectedMessages) {
   1443  const messages = await findAllMessagesVirtualized(hud);
   1444  is(
   1445    messages.length,
   1446    expectedMessages.length,
   1447    "Got the expected number of messages"
   1448  );
   1449 
   1450  const isInWarningGroup = index => {
   1451    const message = expectedMessages[index];
   1452    if (!message.startsWith("|")) {
   1453      return false;
   1454    }
   1455    const groups = expectedMessages
   1456      .slice(0, index)
   1457      .reverse()
   1458      .filter(m => !m.startsWith("|"));
   1459    if (groups.length === 0) {
   1460      ok(false, "Unexpected structure: an indented message isn't in a group");
   1461    }
   1462 
   1463    return groups[0].startsWith("▼︎⚠");
   1464  };
   1465 
   1466  for (let [i, expectedMessage] of expectedMessages.entries()) {
   1467    // Refresh the reference to the message, as it may have been scrolled out of existence.
   1468    const message = await findMessageVirtualizedById({
   1469      hud,
   1470      messageId: messages[i].getAttribute("data-message-id"),
   1471    });
   1472    info(`Checking "${expectedMessage}"`);
   1473 
   1474    // Collapsed Warning group
   1475    if (expectedMessage.startsWith("▶︎⚠")) {
   1476      is(
   1477        message.querySelector(".arrow").getAttribute("aria-expanded"),
   1478        "false",
   1479        "There's a collapsed arrow"
   1480      );
   1481      is(
   1482        message.getAttribute("data-indent"),
   1483        "0",
   1484        "The warningGroup has the expected indent"
   1485      );
   1486      expectedMessage = expectedMessage.replace("▶︎⚠ ", "");
   1487    }
   1488 
   1489    // Expanded Warning group
   1490    if (expectedMessage.startsWith("▼︎⚠")) {
   1491      is(
   1492        message.querySelector(".arrow").getAttribute("aria-expanded"),
   1493        "true",
   1494        "There's an expanded arrow"
   1495      );
   1496      is(
   1497        message.getAttribute("data-indent"),
   1498        "0",
   1499        "The warningGroup has the expected indent"
   1500      );
   1501      expectedMessage = expectedMessage.replace("▼︎⚠ ", "");
   1502    }
   1503 
   1504    // Collapsed console.group
   1505    if (expectedMessage.startsWith("▶︎")) {
   1506      is(
   1507        message.querySelector(".arrow").getAttribute("aria-expanded"),
   1508        "false",
   1509        "There's a collapsed arrow"
   1510      );
   1511      expectedMessage = expectedMessage.replace("▶︎ ", "");
   1512    }
   1513 
   1514    // Expanded console.group
   1515    if (expectedMessage.startsWith("â–¼")) {
   1516      is(
   1517        message.querySelector(".arrow").getAttribute("aria-expanded"),
   1518        "true",
   1519        "There's an expanded arrow"
   1520      );
   1521      expectedMessage = expectedMessage.replace("â–¼ ", "");
   1522    }
   1523 
   1524    // In-group message
   1525    if (expectedMessage.startsWith("|")) {
   1526      if (isInWarningGroup(i)) {
   1527        ok(
   1528          message.querySelector(".warning-indent"),
   1529          "The message has the expected indent"
   1530        );
   1531      }
   1532 
   1533      expectedMessage = expectedMessage.replace("| ", "");
   1534    } else {
   1535      is(
   1536        message.getAttribute("data-indent"),
   1537        "0",
   1538        "The message has the expected indent"
   1539      );
   1540    }
   1541 
   1542    ok(
   1543      message.textContent.trim().includes(expectedMessage.trim()),
   1544      `Message includes ` +
   1545        `the expected "${expectedMessage}" content - "${message.textContent.trim()}"`
   1546    );
   1547  }
   1548 }
   1549 
   1550 /**
   1551 * Check that there is a message with the specified text that has the specified
   1552 * stack information.  Self-hosted frames are ignored.
   1553 *
   1554 * @param {WebConsole} hud
   1555 * @param {string} text
   1556 *        message substring to look for
   1557 * @param {Array<number>} expectedFrameLines
   1558 *        line numbers of the frames expected in the stack
   1559 */
   1560 async function checkMessageStack(hud, text, expectedFrameLines) {
   1561  info(`Checking message stack for "${text}"`);
   1562  const msgNode = await waitFor(
   1563    () => findErrorMessage(hud, text),
   1564    `Couln't find message including "${text}"`
   1565  );
   1566  ok(!msgNode.classList.contains("open"), `Error logged not expanded`);
   1567 
   1568  const button = await waitFor(
   1569    () => msgNode.querySelector(".collapse-button"),
   1570    `Couldn't find the expand button on "${text}" message`
   1571  );
   1572  button.click();
   1573 
   1574  const framesNode = await waitFor(
   1575    () => msgNode.querySelector(".message-body-wrapper > .stacktrace .frames"),
   1576    `Couldn't find stacktrace frames on "${text}" message`
   1577  );
   1578  const frameNodes = Array.from(framesNode.querySelectorAll(".frame")).filter(
   1579    el => {
   1580      const fileName = el.querySelector(".filename").textContent;
   1581      return (
   1582        fileName !== "self-hosted" &&
   1583        !fileName.startsWith("chrome:") &&
   1584        !fileName.startsWith("resource:")
   1585      );
   1586    }
   1587  );
   1588 
   1589  for (let i = 0; i < frameNodes.length; i++) {
   1590    const frameNode = frameNodes[i];
   1591    is(
   1592      frameNode.querySelector(".line").textContent,
   1593      expectedFrameLines[i].toString(),
   1594      `Found line ${expectedFrameLines[i]} for frame #${i}`
   1595    );
   1596  }
   1597 
   1598  is(
   1599    frameNodes.length,
   1600    expectedFrameLines.length,
   1601    `Found ${frameNodes.length} frames`
   1602  );
   1603 }
   1604 
   1605 /**
   1606 * Reload the content page.
   1607 *
   1608 * @returns {Promise} A promise that will return when the page is fully loaded (i.e., the
   1609 *                    `load` event was fired).
   1610 */
   1611 function reloadPage() {
   1612  const onLoad = BrowserTestUtils.waitForContentEvent(
   1613    gBrowser.selectedBrowser,
   1614    "load",
   1615    true
   1616  );
   1617  SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => {
   1618    content.location.reload();
   1619  });
   1620  return onLoad;
   1621 }
   1622 
   1623 /**
   1624 * Check if the editor mode is enabled (i.e. .webconsole-app has the expected class).
   1625 *
   1626 * @param {WebConsole} hud
   1627 * @returns {boolean}
   1628 */
   1629 function isEditorModeEnabled(hud) {
   1630  const { outputNode } = hud.ui;
   1631  const appNode = outputNode.querySelector(".webconsole-app");
   1632  return appNode.classList.contains("jsterm-editor");
   1633 }
   1634 
   1635 /**
   1636 * Toggle the layout between in-line and editor.
   1637 *
   1638 * @param {WebConsole} hud
   1639 * @returns {Promise} A promise that resolves once the layout change was rendered.
   1640 */
   1641 function toggleLayout(hud) {
   1642  const isMacOS = Services.appinfo.OS === "Darwin";
   1643  const enabled = isEditorModeEnabled(hud);
   1644 
   1645  EventUtils.synthesizeKey("b", {
   1646    [isMacOS ? "metaKey" : "ctrlKey"]: true,
   1647  });
   1648  return waitFor(() => isEditorModeEnabled(hud) === !enabled);
   1649 }
   1650 
   1651 /**
   1652 * Wait until all lazily fetch requests in netmonitor get finished.
   1653 * Otherwise test will be shutdown too early and cause failure.
   1654 */
   1655 async function waitForLazyRequests(toolbox) {
   1656  const ui = toolbox.getCurrentPanel().hud.ui;
   1657  return waitUntil(() => {
   1658    return (
   1659      !ui.networkDataProvider.lazyRequestData.size &&
   1660      // Make sure that batched request updates are all complete
   1661      // as they trigger late lazy data requests.
   1662      !ui.wrapper.queuedRequestUpdates.length
   1663    );
   1664  });
   1665 }
   1666 
   1667 /**
   1668 * Clear the console output and wait for eventual object actors to be released.
   1669 *
   1670 * @param {WebConsole} hud
   1671 * @param {object} An options object with the following properties:
   1672 *                 - {Boolean} keepStorage: true to prevent clearing the messages storage.
   1673 */
   1674 async function clearOutput(hud, { keepStorage = false } = {}) {
   1675  const { ui } = hud;
   1676  const promises = [ui.once("messages-cleared")];
   1677 
   1678  // If there's an object inspector, we need to wait for the actors to be released.
   1679  if (ui.outputNode.querySelector(".object-inspector")) {
   1680    promises.push(ui.once("fronts-released"));
   1681  }
   1682 
   1683  ui.clearOutput(!keepStorage);
   1684  await Promise.all(promises);
   1685 }
   1686 
   1687 /**
   1688 * Retrieve all the items of the context selector menu.
   1689 *
   1690 * @param {WebConsole} hud
   1691 * @return Array<Element>
   1692 */
   1693 function getContextSelectorItems(hud) {
   1694  const toolbox = hud.toolbox;
   1695  const doc = toolbox ? toolbox.doc : hud.chromeWindow.document;
   1696  const list = doc.getElementById(
   1697    "webconsole-console-evaluation-context-selector-menu-list"
   1698  );
   1699  return Array.from(list.querySelectorAll("li.menuitem button, hr"));
   1700 }
   1701 
   1702 /**
   1703 * Check that the evaluation context selector menu has the expected item, in the expected
   1704 * state.
   1705 *
   1706 * @param {WebConsole} hud
   1707 * @param {Array<object>} expected: An array of object (see checkContextSelectorMenuItemAt
   1708 *                        for expected properties)
   1709 */
   1710 function checkContextSelectorMenu(hud, expected) {
   1711  const items = getContextSelectorItems(hud);
   1712 
   1713  is(
   1714    items.length,
   1715    expected.length,
   1716    "The context selector menu has the expected number of items"
   1717  );
   1718 
   1719  expected.forEach((expectedItem, i) => {
   1720    checkContextSelectorMenuItemAt(hud, i, expectedItem);
   1721  });
   1722 }
   1723 
   1724 /**
   1725 * Check that the evaluation context selector menu has the expected item at the specified index.
   1726 *
   1727 * @param {WebConsole} hud
   1728 * @param {number} index
   1729 * @param {object} expected
   1730 * @param {string} expected.label: The label of the target
   1731 * @param {string} expected.tooltip: The tooltip of the target element in the menu
   1732 * @param {boolean} expected.checked: if the target should be selected or not
   1733 * @param {boolean} expected.separator: if the element is a simple separator
   1734 * @param {boolean} expected.indented: if the element is indented
   1735 */
   1736 function checkContextSelectorMenuItemAt(hud, index, expected) {
   1737  const el = getContextSelectorItems(hud).at(index);
   1738 
   1739  if (expected.separator === true) {
   1740    is(el.getAttribute("role"), "menuseparator", "The element is a separator");
   1741    return;
   1742  }
   1743 
   1744  const elChecked = el.getAttribute("aria-checked") === "true";
   1745  const elTooltip = el.getAttribute("title");
   1746  const elLabel = el.querySelector(".label").innerText;
   1747  const indented = el.classList.contains("indented");
   1748 
   1749  is(elLabel, expected.label, `The item has the expected label`);
   1750  is(elTooltip, expected.tooltip, `Item "${elLabel}" has the expected tooltip`);
   1751  is(
   1752    elChecked,
   1753    expected.checked,
   1754    `Item "${elLabel}" is ${expected.checked ? "checked" : "unchecked"}`
   1755  );
   1756  is(
   1757    indented,
   1758    expected.indented ?? false,
   1759    `Item "${elLabel}" is ${!indented ? " not" : ""} indented`
   1760  );
   1761 }
   1762 
   1763 /**
   1764 * Select a target in the context selector.
   1765 *
   1766 * @param {WebConsole} hud
   1767 * @param {string} targetLabel: The label of the target to select.
   1768 */
   1769 function selectTargetInContextSelector(hud, targetLabel) {
   1770  const items = getContextSelectorItems(hud);
   1771  const itemToSelect = items.find(
   1772    item => item.querySelector(".label")?.innerText === targetLabel
   1773  );
   1774  if (!itemToSelect) {
   1775    ok(false, `Couldn't find target with "${targetLabel}" label`);
   1776    return;
   1777  }
   1778 
   1779  itemToSelect.click();
   1780 }
   1781 
   1782 /**
   1783 * A helper that returns the size of the image that was just put into the clipboard by the
   1784 * :screenshot command.
   1785 *
   1786 * @return The {width, height} dimension object.
   1787 */
   1788 async function getImageSizeFromClipboard() {
   1789  const clipid = Ci.nsIClipboard;
   1790  const clip = Cc["@mozilla.org/widget/clipboard;1"].getService(clipid);
   1791  const trans = Cc["@mozilla.org/widget/transferable;1"].createInstance(
   1792    Ci.nsITransferable
   1793  );
   1794  const flavor = "image/png";
   1795  trans.init(null);
   1796  trans.addDataFlavor(flavor);
   1797 
   1798  clip.getData(
   1799    trans,
   1800    clipid.kGlobalClipboard,
   1801    SpecialPowers.wrap(window).browsingContext.currentWindowContext
   1802  );
   1803  const data = {};
   1804  trans.getTransferData(flavor, data);
   1805 
   1806  ok(data.value, "screenshot exists");
   1807 
   1808  let image = data.value;
   1809 
   1810  // Due to the differences in how images could be stored in the clipboard the
   1811  // checks below are needed. The clipboard could already provide the image as
   1812  // byte streams or as image container. If it's not possible obtain a
   1813  // byte stream, the function throws.
   1814 
   1815  if (image instanceof Ci.imgIContainer) {
   1816    image = Cc["@mozilla.org/image/tools;1"]
   1817      .getService(Ci.imgITools)
   1818      .encodeImage(image, flavor);
   1819  }
   1820 
   1821  if (!(image instanceof Ci.nsIInputStream)) {
   1822    throw new Error("Unable to read image data");
   1823  }
   1824 
   1825  const binaryStream = Cc["@mozilla.org/binaryinputstream;1"].createInstance(
   1826    Ci.nsIBinaryInputStream
   1827  );
   1828  binaryStream.setInputStream(image);
   1829  const available = binaryStream.available();
   1830  const buffer = new ArrayBuffer(available);
   1831  is(
   1832    binaryStream.readArrayBuffer(available, buffer),
   1833    available,
   1834    "Read expected amount of data"
   1835  );
   1836 
   1837  // We are going to load the image in the content page to measure its size.
   1838  // We don't want to insert the image directly in the browser's document
   1839  // (which is value of the global `document` here). Doing so might push the
   1840  // toolbox upwards, shrink the content page and fail the fullpage screenshot
   1841  // test.
   1842  return SpecialPowers.spawn(
   1843    gBrowser.selectedBrowser,
   1844    [buffer],
   1845    async function (_buffer) {
   1846      const img = content.document.createElement("img");
   1847      const loaded = new Promise(r => {
   1848        img.addEventListener("load", r, { once: true });
   1849      });
   1850 
   1851      // Build a URL from the buffer passed to the ContentTask
   1852      const url = content.URL.createObjectURL(
   1853        new Blob([_buffer], { type: "image/png" })
   1854      );
   1855 
   1856      // Load the image
   1857      img.src = url;
   1858      content.document.documentElement.appendChild(img);
   1859 
   1860      info("Waiting for the clipboard image to load in the content page");
   1861      await loaded;
   1862 
   1863      // Remove the image and revoke the URL.
   1864      img.remove();
   1865      content.URL.revokeObjectURL(url);
   1866 
   1867      return {
   1868        width: img.width,
   1869        height: img.height,
   1870      };
   1871    }
   1872  );
   1873 }