tor-browser

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

shared-head.js (109296B)


      1 /* This Source Code Form is subject to the terms of the Mozilla Public
      2 * License, v. 2.0. If a copy of the MPL was not distributed with this
      3 * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
      4 
      5 // This file is loaded in a `spawn` context sometimes which doesn't have,
      6 // `Assert`, so we can't use its comparison functions.
      7 /* eslint-disable mozilla/no-comparison-or-assignment-inside-ok */
      8 
      9 /**
     10 * Helper methods to drive with the debugger during mochitests. This file can be safely
     11 * required from other panel test files.
     12 */
     13 
     14 "use strict";
     15 
     16 /* eslint-disable no-unused-vars */
     17 
     18 // We can't use "import globals from head.js" because of bug 1395426.
     19 // So workaround by manually importing the few symbols we are using from it.
     20 // (Note that only ./mach eslint devtools/client fails while devtools/client/debugger passes)
     21 /* global EXAMPLE_URL, ContentTask */
     22 
     23 // Assume that shared-head is always imported before this file
     24 /* import-globals-from ../../../shared/test/shared-head.js */
     25 
     26 /**
     27 * Helper method to create a "dbg" context for other tools to use
     28 */
     29 function createDebuggerContext(toolbox) {
     30  const panel = toolbox.getPanel("jsdebugger");
     31  const win = panel.panelWin;
     32 
     33  return {
     34    ...win.dbg,
     35    commands: toolbox.commands,
     36    toolbox,
     37    win,
     38    panel,
     39  };
     40 }
     41 
     42 var { Toolbox } = require("devtools/client/framework/toolbox");
     43 const asyncStorage = require("devtools/shared/async-storage");
     44 
     45 const {
     46  getSelectedLocation,
     47 } = require("devtools/client/debugger/src/utils/selected-location");
     48 const {
     49  createLocation,
     50 } = require("devtools/client/debugger/src/utils/location");
     51 
     52 const {
     53  resetSchemaVersion,
     54 } = require("devtools/client/debugger/src/utils/prefs");
     55 
     56 const {
     57  getUnicodeUrlPath,
     58 } = require("resource://devtools/client/shared/unicode-url.js");
     59 
     60 const {
     61  isGeneratedId,
     62 } = require("devtools/client/shared/source-map-loader/index");
     63 
     64 const DEBUGGER_L10N = new LocalizationHelper(
     65  "devtools/client/locales/debugger.properties"
     66 );
     67 
     68 /**
     69 * Waits for `predicate()` to be true. `state` is the redux app state.
     70 *
     71 * @param {object} dbg
     72 * @param {Function} predicate
     73 * @param {string} msg
     74 * @return {Promise}
     75 */
     76 function waitForState(dbg, predicate, msg = "") {
     77  return new Promise(resolve => {
     78    info(`Waiting for state change: ${msg}`);
     79    let result = predicate(dbg.store.getState());
     80    if (result) {
     81      info(
     82        `--> The state was immediately correct (should rather do an immediate assertion?)`
     83      );
     84      resolve(result);
     85      return;
     86    }
     87 
     88    const unsubscribe = dbg.store.subscribe(
     89      () => {
     90        result = predicate(dbg.store.getState());
     91        if (result) {
     92          info(`Finished waiting for state change: ${msg}`);
     93          unsubscribe();
     94          resolve(result);
     95        }
     96      },
     97      // The `visibilityHandlerStore` wrapper may prevent the test helper from being
     98      // notified about store updates while the debugger is in background.
     99      { ignoreVisibility: true }
    100    );
    101  });
    102 }
    103 
    104 /**
    105 * Waits for sources to be loaded.
    106 *
    107 * @memberof mochitest/waits
    108 * @param {object} dbg
    109 * @param {Array} sources
    110 * @return {Promise}
    111 * @static
    112 */
    113 async function waitForSources(dbg, ...sources) {
    114  if (sources.length === 0) {
    115    return;
    116  }
    117 
    118  info(`Waiting on sources: ${sources.join(", ")}`);
    119  await Promise.all(
    120    sources.map(url => {
    121      if (!sourceExists(dbg, url)) {
    122        return waitForState(
    123          dbg,
    124          () => sourceExists(dbg, url),
    125          `source ${url} exists`
    126        );
    127      }
    128      return Promise.resolve();
    129    })
    130  );
    131 
    132  info(`Finished waiting on sources: ${sources.join(", ")}`);
    133 }
    134 
    135 /**
    136 * Waits for a source to be loaded.
    137 *
    138 * @memberof mochitest/waits
    139 * @param {object} dbg
    140 * @param {string} source
    141 * @return {Promise}
    142 * @static
    143 */
    144 function waitForSource(dbg, url) {
    145  return waitForState(
    146    dbg,
    147    () => findSource(dbg, url, { silent: true }),
    148    "source exists"
    149  );
    150 }
    151 
    152 async function waitForElement(dbg, name, ...args) {
    153  info(`Waiting for debugger element by name: ${name}`);
    154  await waitUntil(() => findElement(dbg, name, ...args));
    155  return findElement(dbg, name, ...args);
    156 }
    157 
    158 /**
    159 * Wait for a count of given elements to be rendered on screen.
    160 *
    161 * @param {DebuggerPanel} dbg
    162 * @param {string} name
    163 * @param {Integer} count: Number of elements to match. Defaults to 1.
    164 * @param {boolean} countStrictlyEqual: When set to true, will wait until the exact number
    165 *                  of elements is displayed on screen. When undefined or false, will wait
    166 *                  until there's at least `${count}` elements on screen (e.g. if count
    167 *                  is 1, it will resolve if there are 2 elements rendered).
    168 */
    169 async function waitForAllElements(
    170  dbg,
    171  name,
    172  count = 1,
    173  countStrictlyEqual = false
    174 ) {
    175  info(`Waiting for N=${count} debugger elements by name: ${name}`);
    176  await waitUntil(() => {
    177    const elsCount = findAllElements(dbg, name).length;
    178    return countStrictlyEqual ? elsCount === count : elsCount >= count;
    179  });
    180  return findAllElements(dbg, name);
    181 }
    182 
    183 async function waitForElementWithSelector(dbg, selector) {
    184  info(`Waiting for debugger element by selector: ${selector}`);
    185  await waitUntil(() => findElementWithSelector(dbg, selector));
    186  return findElementWithSelector(dbg, selector);
    187 }
    188 
    189 function waitForRequestsToSettle(dbg) {
    190  return dbg.commands.client.waitForRequestsToSettle();
    191 }
    192 
    193 function assertClass(el, className, exists = true) {
    194  if (exists) {
    195    ok(el.classList.contains(className), `${className} class exists`);
    196  } else {
    197    ok(!el.classList.contains(className), `${className} class does not exist`);
    198  }
    199 }
    200 
    201 async function waitForSelectedLocation(dbg, line, column) {
    202  // Assert the state in Redux
    203  await waitForState(dbg, () => {
    204    const location = dbg.selectors.getSelectedLocation();
    205    return (
    206      location &&
    207      location.line == line &&
    208      // location's column is 0-based, while all line and columns mentioned in tests
    209      // are 1-based.
    210      (typeof column == "number" ? location.column + 1 == column : true)
    211    );
    212  });
    213 
    214  // Also assert the cursor position in CodeMirror
    215  await waitFor(function () {
    216    const cursor = getCMEditor(dbg).getSelectionCursor();
    217    if (!cursor) {
    218      return false;
    219    }
    220    if (line && cursor.from.line != line) {
    221      return false;
    222    }
    223    // Asserted column is 1-based while CodeMirror's cursor column is 0-based
    224    if (column && cursor.from.ch + 1 != column) {
    225      return false;
    226    }
    227    return true;
    228  });
    229 }
    230 
    231 /**
    232 * Wait for a given source to be selected and ready.
    233 *
    234 * @memberof mochitest/waits
    235 * @param {object} dbg
    236 * @param {null|string|Source} sourceOrUrl Optional. Either a source URL (string) or a source object (typically fetched via `findSource`)
    237 * @return {Promise}
    238 * @static
    239 */
    240 function waitForSelectedSource(dbg, sourceOrUrl) {
    241  const {
    242    getSelectedSourceTextContent,
    243    getBreakableLines,
    244    getSourceActorsForSource,
    245    getSourceActorBreakableLines,
    246    getFirstSourceActorForGeneratedSource,
    247    getSelectedFrame,
    248    getCurrentThread,
    249  } = dbg.selectors;
    250 
    251  return waitForState(
    252    dbg,
    253    () => {
    254      const location = dbg.selectors.getSelectedLocation() || {};
    255      const sourceTextContent = getSelectedSourceTextContent();
    256      if (!sourceTextContent) {
    257        return false;
    258      }
    259 
    260      if (sourceOrUrl) {
    261        // Second argument is either a source URL (string)
    262        // or a Source object.
    263        if (typeof sourceOrUrl == "string") {
    264          const url = location.source.url;
    265          if (
    266            typeof url != "string" ||
    267            (!url.includes(encodeURI(sourceOrUrl)) &&
    268              !url.includes(sourceOrUrl))
    269          ) {
    270            return false;
    271          }
    272        } else if (location.source.id != sourceOrUrl.id) {
    273          return false;
    274        }
    275      }
    276 
    277      // Finaly wait for breakable lines to be set
    278      if (location.source.isHTML) {
    279        // For HTML sources we need to wait for each source actor to be processed.
    280        // getBreakableLines will return the aggregation without being able to know
    281        // if that's complete, with all the source actors.
    282        const sourceActors = getSourceActorsForSource(location.source.id);
    283        const allSourceActorsProcessed = sourceActors.every(
    284          sourceActor => !!getSourceActorBreakableLines(sourceActor.id)
    285        );
    286        return allSourceActorsProcessed;
    287      }
    288 
    289      if (!getBreakableLines(location.source.id)) {
    290        return false;
    291      }
    292 
    293      // Also ensure that CodeMirror updated its content
    294      return getEditorContent(dbg) !== DEBUGGER_L10N.getStr("loadingText");
    295    },
    296    "selected source"
    297  );
    298 }
    299 
    300 /**
    301 * The generated source of WASM source are WASM binary file,
    302 * which have many broken/disabled features in the debugger.
    303 *
    304 * They especially have a very special behavior in CodeMirror
    305 * where line labels aren't line number, but hex addresses.
    306 */
    307 function isWasmBinarySource(source) {
    308  return source.isWasm && !source.isOriginal;
    309 }
    310 
    311 function getVisibleSelectedFrameLine(dbg) {
    312  const frame = dbg.selectors.getVisibleSelectedFrame();
    313  return frame?.location.line;
    314 }
    315 
    316 function getVisibleSelectedFrameColumn(dbg) {
    317  const frame = dbg.selectors.getVisibleSelectedFrame();
    318  return frame?.location.column;
    319 }
    320 
    321 /**
    322 * Assert that a given line is breakable or not.
    323 * Verify that CodeMirror gutter is grayed out via the empty line classname if not breakable.
    324 */
    325 async function assertLineIsBreakable(dbg, file, line, shouldBeBreakable) {
    326  const el = await getNodeAtEditorGutterLine(dbg, line);
    327  const lineText = `${line}| ${el.innerText.substring(0, 50)}${
    328    el.innerText.length > 50 ? "…" : ""
    329  } — in ${file}`;
    330  // When a line is not breakable, the "empty-line" class is added
    331  // and the line is greyed out
    332  if (shouldBeBreakable) {
    333    ok(!el.classList.contains("empty-line"), `${lineText} should be breakable`);
    334  } else {
    335    ok(
    336      el.classList.contains("empty-line"),
    337      `${lineText} should NOT be breakable`
    338    );
    339  }
    340 }
    341 
    342 /**
    343 * Assert that the debugger is highlighting the correct location.
    344 *
    345 * @memberof mochitest/asserts
    346 * @param {object} dbg
    347 * @param {string} source
    348 * @param {number} line
    349 * @static
    350 */
    351 function assertHighlightLocation(dbg, source, line) {
    352  source = findSource(dbg, source);
    353 
    354  // Check the selected source
    355  is(
    356    dbg.selectors.getSelectedSource().url,
    357    source.url,
    358    "source url is correct"
    359  );
    360 
    361  // Check the highlight line
    362  const lineEl = findElement(dbg, "highlightLine");
    363  ok(lineEl, "Line is highlighted");
    364 
    365  is(
    366    findAllElements(dbg, "highlightLine").length,
    367    1,
    368    "Only 1 line is highlighted"
    369  );
    370 
    371  ok(isVisibleInEditor(dbg, lineEl), "Highlighted line is visible");
    372 
    373  const lineInfo = getCMEditor(dbg).lineInfo(line);
    374  ok(lineInfo.wrapClass.includes("highlight-line"), "Line is highlighted");
    375 }
    376 
    377 /**
    378 * Helper function for assertPausedAtSourceAndLine.
    379 *
    380 * Assert that CodeMirror reports to be paused at the given line/column.
    381 */
    382 async function _assertDebugLine(dbg, line, column) {
    383  const source = dbg.selectors.getSelectedSource();
    384  // WASM lines are hex addresses which have to be mapped to decimal line number
    385  if (isWasmBinarySource(source)) {
    386    line = wasmOffsetToLine(dbg, line);
    387  }
    388 
    389  // Check the debug line
    390  // cm6 lines are 1-based, while cm5 are 0-based, to keep compatibility with
    391  // .lineInfo usage in other locations.
    392  const lineInfo = getCMEditor(dbg).lineInfo(line);
    393  const sourceTextContent = dbg.selectors.getSelectedSourceTextContent();
    394  if (source && !sourceTextContent) {
    395    const url = source.url;
    396    ok(
    397      false,
    398      `Looks like the source ${url} is still loading. Try adding waitForLoadedSource in the test.`
    399    );
    400    return;
    401  }
    402 
    403  // Scroll the line into view to make sure the content
    404  // on the line is rendered and in the dom.
    405  await scrollEditorIntoView(dbg, line, 0);
    406 
    407  if (!lineInfo.wrapClass) {
    408    const pauseLine = getVisibleSelectedFrameLine(dbg);
    409    ok(false, `Expected pause line on line ${line}, it is on ${pauseLine}`);
    410    return;
    411  }
    412 
    413  // Consider pausing on error also as being paused
    414  ok(
    415    lineInfo?.wrapClass.includes("paused-line") ||
    416      lineInfo?.wrapClass.includes("new-debug-line-error"),
    417    `Line ${line} is not highlighted as paused`
    418  );
    419 
    420  const pausedLine =
    421    findElement(dbg, "pausedLine") || findElement(dbg, "debugErrorLine");
    422 
    423  is(
    424    findAllElements(dbg, "pausedLine").length +
    425      findAllElements(dbg, "debugErrorLine").length,
    426    1,
    427    "There is only one line"
    428  );
    429 
    430  ok(isVisibleInEditor(dbg, pausedLine), "debug line is visible");
    431 
    432  const editorLineEl = getCMEditor(dbg).getElementAtLine(line);
    433  const pauseLocationMarker = editorLineEl.querySelector(".paused-location");
    434  is(
    435    pauseLocationMarker.cmView.widget.line,
    436    line,
    437    "The paused caret is at the right line"
    438  );
    439  is(
    440    pauseLocationMarker.cmView.widget.column,
    441    column,
    442    "The paused caret is at the right column"
    443  );
    444  info(`Paused on line ${line}`);
    445 }
    446 
    447 /**
    448 * Make sure the debugger is paused at a certain source ID and line.
    449 *
    450 * @param {object} dbg
    451 * @param {string} expectedSourceId
    452 * @param {number} expectedLine
    453 * @param {number} [expectedColumn]
    454 */
    455 async function assertPausedAtSourceAndLine(
    456  dbg,
    457  expectedSourceId,
    458  expectedLine,
    459  expectedColumn
    460 ) {
    461  // Check that the debugger is paused.
    462  assertPaused(dbg);
    463 
    464  // Check that the paused location is correctly rendered.
    465  ok(isSelectedFrameSelected(dbg), "top frame's source is selected");
    466 
    467  // Check the pause location
    468  const pauseLine = getVisibleSelectedFrameLine(dbg);
    469  is(
    470    pauseLine,
    471    expectedLine,
    472    "Redux state for currently selected frame's line is correct"
    473  );
    474 
    475  const selectedSource = dbg.selectors.getSelectedSource();
    476  // WASM binary source is pausing at 0 column, whereas visible selected frame returns 1
    477  const pauseColumn = isWasmBinarySource(selectedSource)
    478    ? 0
    479    : getVisibleSelectedFrameColumn(dbg);
    480  if (expectedColumn) {
    481    // `pauseColumn` is 0-based, coming from internal state,
    482    // while `expectedColumn` is manually passed from test scripts and so is 1-based.
    483    is(
    484      pauseColumn + 1,
    485      expectedColumn,
    486      "Redux state for currently selected frame's column is correct"
    487    );
    488  }
    489  await _assertDebugLine(dbg, pauseLine, pauseColumn);
    490 
    491  ok(isVisibleInEditor(dbg, findElement(dbg, "gutters")), "gutter is visible");
    492 
    493  const frames = dbg.selectors.getCurrentThreadFrames();
    494 
    495  // WASM support is limited when we are on the generated binary source
    496  if (isWasmBinarySource(selectedSource)) {
    497    return;
    498  }
    499 
    500  ok(frames.length >= 1, "Got at least one frame");
    501 
    502  // Lets make sure we can assert both original and generated file locations when needed
    503  const { source, line, column } = isGeneratedId(expectedSourceId)
    504    ? frames[0].generatedLocation
    505    : frames[0].location;
    506  is(source.id, expectedSourceId, "Frame has correct source");
    507  is(
    508    line,
    509    expectedLine,
    510    `Frame paused at line ${line}, but expected line ${expectedLine}`
    511  );
    512 
    513  if (expectedColumn) {
    514    // `column` is 0-based, coming from internal state,
    515    // while `expectedColumn` is manually passed from test scripts and so is 1-based.
    516    is(
    517      column + 1,
    518      expectedColumn,
    519      `Frame paused at column ${
    520        column + 1
    521      }, but expected column ${expectedColumn}`
    522    );
    523  }
    524 }
    525 
    526 async function waitForThreadCount(dbg, count) {
    527  return waitForState(
    528    dbg,
    529    state => dbg.selectors.getThreads(state).length == count
    530  );
    531 }
    532 
    533 async function waitForLoadedScopes(dbg) {
    534  const scopes = await waitForElement(dbg, "scopes");
    535  // Since scopes auto-expand, we can assume they are loaded when there is a tree node
    536  // with the aria-level attribute equal to "2".
    537  info("Wait for loaded scopes - ie when a tree node has aria-level=2");
    538  await waitUntil(() => scopes.querySelector('.tree-node[aria-level="2"]'));
    539 }
    540 
    541 function waitForBreakpointCount(dbg, count) {
    542  return waitForState(dbg, () => dbg.selectors.getBreakpointCount() == count);
    543 }
    544 
    545 function waitForBreakpoint(dbg, url, line) {
    546  return waitForState(dbg, () => findBreakpoint(dbg, url, line));
    547 }
    548 
    549 function waitForBreakpointRemoved(dbg, url, line) {
    550  return waitForState(dbg, () => !findBreakpoint(dbg, url, line));
    551 }
    552 
    553 /**
    554 * Returns boolean for whether the debugger is paused.
    555 *
    556 * @param {object} dbg
    557 */
    558 function isPaused(dbg) {
    559  return dbg.selectors.getIsCurrentThreadPaused();
    560 }
    561 
    562 /**
    563 * Assert that the debugger is not currently paused.
    564 *
    565 * @param {object} dbg
    566 * @param {string} msg
    567 *        Optional assertion message
    568 */
    569 function assertNotPaused(dbg, msg = "client is not paused") {
    570  ok(!isPaused(dbg), msg);
    571 }
    572 
    573 /**
    574 * Assert that the debugger is currently paused.
    575 *
    576 * @param {object} dbg
    577 */
    578 function assertPaused(dbg, msg = "client is paused") {
    579  ok(isPaused(dbg), msg);
    580 }
    581 
    582 /**
    583 * Waits for the debugger to be fully paused.
    584 *
    585 * @param {object} dbg
    586 * @param {string} url
    587 *        Optional URL of the script we should be pausing on.
    588 * @param {object} options
    589 *         {Boolean} shouldWaitForLoadScopes
    590 *        When paused in original files with original variable mapping disabled, scopes are
    591 *        not going to exist, lets not wait for it. defaults to true
    592 */
    593 async function waitForPaused(
    594  dbg,
    595  url,
    596  options = {
    597    shouldWaitForLoadedScopes: true,
    598    shouldWaitForInlinePreviews: true,
    599  }
    600 ) {
    601  info("Waiting for the debugger to pause");
    602  const { getSelectedScope, getCurrentThread, getCurrentThreadFrames } =
    603    dbg.selectors;
    604 
    605  await waitForState(
    606    dbg,
    607    () => isPaused(dbg) && !!getSelectedScope(),
    608    "paused"
    609  );
    610 
    611  await waitForState(dbg, getCurrentThreadFrames, "fetched frames");
    612 
    613  if (options.shouldWaitForLoadedScopes) {
    614    await waitForLoadedScopes(dbg);
    615  }
    616 
    617  await waitForSelectedSource(dbg, url);
    618 
    619  if (options.shouldWaitForInlinePreviews) {
    620    await waitForInlinePreviews(dbg);
    621  }
    622 }
    623 
    624 /**
    625 * Waits for the debugger to resume.
    626 *
    627 * @param {Objeect} dbg
    628 */
    629 function waitForResumed(dbg) {
    630  info("Waiting for the debugger to resume");
    631  return waitForState(dbg, () => !dbg.selectors.getIsCurrentThreadPaused());
    632 }
    633 
    634 function waitForInlinePreviews(dbg) {
    635  return waitForState(dbg, () => dbg.selectors.getInlinePreviews());
    636 }
    637 
    638 function waitForCondition(dbg, condition) {
    639  return waitForState(dbg, () =>
    640    dbg.selectors
    641      .getBreakpointsList()
    642      .find(bp => bp.options.condition == condition)
    643  );
    644 }
    645 
    646 function waitForLog(dbg, logValue) {
    647  return waitForState(dbg, () =>
    648    dbg.selectors
    649      .getBreakpointsList()
    650      .find(bp => bp.options.logValue == logValue)
    651  );
    652 }
    653 
    654 async function waitForPausedThread(dbg, thread) {
    655  return waitForState(dbg, () => dbg.selectors.getIsPaused(thread));
    656 }
    657 
    658 function isSelectedFrameSelected(dbg) {
    659  const frame = dbg.selectors.getVisibleSelectedFrame();
    660 
    661  // Make sure the source text is completely loaded for the
    662  // source we are paused in.
    663  const source = dbg.selectors.getSelectedSource();
    664  const sourceTextContent = dbg.selectors.getSelectedSourceTextContent();
    665 
    666  if (!source || !sourceTextContent) {
    667    return false;
    668  }
    669 
    670  return source.id == frame.location.source.id;
    671 }
    672 
    673 /**
    674 * Checks to see if the frame is selected and the displayed title is correct.
    675 *
    676 * @param {object} dbg
    677 * @param {DOM Node} frameElement
    678 * @param {string} expectedTitle
    679 */
    680 function assertFrameIsSelected(dbg, frameElement, expectedTitle) {
    681  const selectedFrame = dbg.selectors.getSelectedFrame();
    682  ok(frameElement.classList.contains("selected"), "The frame is selected");
    683  is(
    684    frameElement.querySelector(".title").innerText,
    685    expectedTitle,
    686    "The selected frame element has the expected title"
    687  );
    688  // For `<anonymous>` frames, there is likely no displayName
    689  is(
    690    selectedFrame.displayName,
    691    expectedTitle == "<anonymous>" ? undefined : expectedTitle,
    692    "The selected frame has the correct display title"
    693  );
    694 }
    695 
    696 /**
    697 * Checks to see if the frame is  not selected.
    698 *
    699 * @param {object} dbg
    700 * @param {DOM Node} frameElement
    701 * @param {string} expectedTitle
    702 */
    703 function assertFrameIsNotSelected(dbg, frameElement, expectedTitle) {
    704  const selectedFrame = dbg.selectors.getSelectedFrame();
    705  ok(!frameElement.classList.contains("selected"), "The frame is selected");
    706  is(
    707    frameElement.querySelector(".title").innerText,
    708    expectedTitle,
    709    "The selected frame element has the expected title"
    710  );
    711 }
    712 
    713 /**
    714 *  Clear all the debugger related preferences.
    715 */
    716 async function clearDebuggerPreferences(prefs = []) {
    717  resetSchemaVersion();
    718  await asyncStorage.clear();
    719  Services.prefs.clearUserPref("devtools.debugger.alphabetize-outline");
    720  Services.prefs.clearUserPref("devtools.debugger.pause-on-exceptions");
    721  Services.prefs.clearUserPref("devtools.debugger.pause-on-caught-exceptions");
    722  Services.prefs.clearUserPref("devtools.debugger.ignore-caught-exceptions");
    723  Services.prefs.clearUserPref("devtools.debugger.pending-selected-location");
    724  Services.prefs.clearUserPref("devtools.debugger.expressions");
    725  Services.prefs.clearUserPref("devtools.debugger.breakpoints-visible");
    726  Services.prefs.clearUserPref("devtools.debugger.call-stack-visible");
    727  Services.prefs.clearUserPref("devtools.debugger.scopes-visible");
    728  Services.prefs.clearUserPref("devtools.debugger.skip-pausing");
    729 
    730  for (const pref of prefs) {
    731    await pushPref(...pref);
    732  }
    733 }
    734 
    735 /**
    736 * Intilializes the debugger.
    737 *
    738 * @memberof mochitest
    739 * @param {string} url
    740 * @return {Promise} dbg
    741 * @static
    742 */
    743 
    744 async function initDebugger(url, ...sources) {
    745  // We depend on EXAMPLE_URLs origin to do cross origin/process iframes via
    746  // EXAMPLE_REMOTE_URL. If the top level document origin changes,
    747  // we may break this. So be careful if you want to change EXAMPLE_URL.
    748  return initDebuggerWithAbsoluteURL(EXAMPLE_URL + url, ...sources);
    749 }
    750 
    751 async function initDebuggerWithAbsoluteURL(url, ...sources) {
    752  await clearDebuggerPreferences();
    753  const toolbox = await openNewTabAndToolbox(url, "jsdebugger");
    754  const dbg = createDebuggerContext(toolbox);
    755 
    756  await waitForSources(dbg, ...sources);
    757  return dbg;
    758 }
    759 
    760 async function initPane(url, pane, prefs) {
    761  await clearDebuggerPreferences(prefs);
    762  return openNewTabAndToolbox(EXAMPLE_URL + url, pane);
    763 }
    764 
    765 /**
    766 * Returns a source that matches a given filename, or a URL.
    767 * This also accept a source as input argument, in such case it just returns it.
    768 *
    769 * @param {object} dbg
    770 * @param {string} filenameOrUrlOrSource
    771 *        The typical case will be to pass only a filename,
    772 *        but you may also pass a full URL to match sources without filesnames like data: URL
    773 *        or pass the source itself, which is just returned.
    774 * @param {object} options
    775 * @param {boolean} options.silent
    776 *        If true, won't throw if the source is missing.
    777 * @return {object} source
    778 */
    779 function findSource(
    780  dbg,
    781  filenameOrUrlOrSource,
    782  { silent } = { silent: false }
    783 ) {
    784  if (typeof filenameOrUrlOrSource !== "string") {
    785    // Support passing in a source object itself all APIs that use this
    786    // function support both styles
    787    return filenameOrUrlOrSource;
    788  }
    789 
    790  const sources = dbg.selectors.getSourceList();
    791  const source = sources.find(s => {
    792    // Sources don't have a file name attribute, we need to compute it here:
    793    const sourceFileName = s.url
    794      ? getUnicodeUrlPath(s.url.substring(s.url.lastIndexOf("/") + 1))
    795      : "";
    796 
    797    // The input argument may either be only the filename, or the complete URL
    798    // This helps match sources whose URL doesn't contain a filename, like data: URLs
    799    return (
    800      sourceFileName == filenameOrUrlOrSource || s.url == filenameOrUrlOrSource
    801    );
    802  });
    803 
    804  if (!source) {
    805    if (silent) {
    806      return false;
    807    }
    808 
    809    throw new Error(`Unable to find source: ${filenameOrUrlOrSource}`);
    810  }
    811 
    812  return source;
    813 }
    814 
    815 function findSourceContent(dbg, url, opts) {
    816  const source = findSource(dbg, url, opts);
    817 
    818  if (!source) {
    819    return null;
    820  }
    821  const content = dbg.selectors.getSettledSourceTextContent(
    822    createLocation({
    823      source,
    824    })
    825  );
    826 
    827  if (!content) {
    828    return null;
    829  }
    830 
    831  if (content.state !== "fulfilled") {
    832    throw new Error(`Expected loaded source, got${content.value}`);
    833  }
    834 
    835  return content.value;
    836 }
    837 
    838 function sourceExists(dbg, url) {
    839  return !!findSource(dbg, url, { silent: true });
    840 }
    841 
    842 function waitForLoadedSource(dbg, url) {
    843  return waitForState(
    844    dbg,
    845    () => {
    846      const source = findSource(dbg, url, { silent: true });
    847      return (
    848        source &&
    849        dbg.selectors.getSettledSourceTextContent(
    850          createLocation({
    851            source,
    852          })
    853        )
    854      );
    855    },
    856    "loaded source"
    857  );
    858 }
    859 
    860 /**
    861 * Selects the source node for a specific source
    862 * from the source tree.
    863 *
    864 * @param {object} dbg
    865 * @param {string} filename - The filename for the specific source
    866 */
    867 async function selectSourceFromSourceTree(dbg, fileName) {
    868  info(`Selecting '${fileName}' source from source tree`);
    869  // Ensure that the source is visible in the tree before trying to click on it
    870  const elt = await waitForSourceInSourceTree(dbg, fileName);
    871  elt.scrollIntoView();
    872  clickDOMElement(dbg, elt);
    873  await waitForSelectedSource(dbg, fileName);
    874  await waitFor(
    875    () => getEditorContent(dbg) !== `Loading…`,
    876    "Wait for source to completely load"
    877  );
    878 }
    879 
    880 /**
    881 * Similar to selectSourceFromSourceTree, but with a precise location
    882 * in the source tree.
    883 */
    884 async function selectSourceFromSourceTreeWithIndex(
    885  dbg,
    886  fileName,
    887  sourcePosition,
    888  message
    889 ) {
    890  info(message);
    891  await clickElement(dbg, "sourceNode", sourcePosition);
    892  await waitForSelectedSource(dbg, fileName);
    893  await waitFor(
    894    () => getEditorContent(dbg) !== `Loading…`,
    895    "Wait for source to completely load"
    896  );
    897 }
    898 
    899 /**
    900 * Trigger a context menu in the debugger source tree
    901 *
    902 * @param {object} dbg
    903 * @param {Obejct} sourceTreeNode - The node in the source tree which the context menu
    904 *                                  item needs to be triggered on.
    905 * @param {string} contextMenuItem - The id for the context menu item to be selected
    906 */
    907 async function triggerSourceTreeContextMenu(
    908  dbg,
    909  sourceTreeNode,
    910  contextMenuItem
    911 ) {
    912  const onContextMenu = waitForContextMenu(dbg);
    913  rightClickEl(dbg, sourceTreeNode);
    914  const menupopup = await onContextMenu;
    915  const onHidden = new Promise(resolve => {
    916    menupopup.addEventListener("popuphidden", resolve, { once: true });
    917  });
    918  selectDebuggerContextMenuItem(dbg, contextMenuItem);
    919  await onHidden;
    920 }
    921 
    922 /**
    923 * Selects the source.
    924 *
    925 * @memberof mochitest/actions
    926 * @param {object} dbg
    927 * @param {string} url
    928 * @param {number} line
    929 * @param {number} column
    930 * @return {Promise}
    931 * @static
    932 */
    933 async function selectSource(dbg, url, line, column) {
    934  const source = findSource(dbg, url);
    935 
    936  await dbg.actions.selectLocation(createLocation({ source, line, column }), {
    937    keepContext: false,
    938  });
    939  return waitForSelectedSource(dbg, source);
    940 }
    941 
    942 async function closeTab(dbg, url) {
    943  const source = findSource(dbg, url);
    944  await dbg.actions.closeTabForSource(source);
    945 }
    946 
    947 function countTabs(dbg) {
    948  // The sourceTabs elements won't be rendered if there is no source.
    949  const sourceTabs = findElement(dbg, "sourceTabs");
    950  return sourceTabs ? sourceTabs.children.length : 0;
    951 }
    952 
    953 /**
    954 * Steps over.
    955 *
    956 * @memberof mochitest/actions
    957 * @param {object} dbg
    958 * @param {object} pauseOptions
    959 * @return {Promise}
    960 * @static
    961 */
    962 async function stepOver(dbg, pauseOptions) {
    963  const pauseLine = getVisibleSelectedFrameLine(dbg);
    964  info(`Stepping over from ${pauseLine}`);
    965  await dbg.actions.stepOver();
    966  return waitForPaused(dbg, null, pauseOptions);
    967 }
    968 
    969 /**
    970 * Steps in.
    971 *
    972 * @memberof mochitest/actions
    973 * @param {object} dbg
    974 * @return {Promise}
    975 * @static
    976 */
    977 async function stepIn(dbg) {
    978  const pauseLine = getVisibleSelectedFrameLine(dbg);
    979  info(`Stepping in from ${pauseLine}`);
    980  await dbg.actions.stepIn();
    981  return waitForPaused(dbg);
    982 }
    983 
    984 /**
    985 * Steps out.
    986 *
    987 * @memberof mochitest/actions
    988 * @param {object} dbg
    989 * @return {Promise}
    990 * @static
    991 */
    992 async function stepOut(dbg) {
    993  const pauseLine = getVisibleSelectedFrameLine(dbg);
    994  info(`Stepping out from ${pauseLine}`);
    995  await dbg.actions.stepOut();
    996  return waitForPaused(dbg);
    997 }
    998 
    999 /**
   1000 * Resumes.
   1001 *
   1002 * @memberof mochitest/actions
   1003 * @param {object} dbg
   1004 * @return {Promise}
   1005 * @static
   1006 */
   1007 async function resume(dbg) {
   1008  const pauseLine = getVisibleSelectedFrameLine(dbg);
   1009  info(`Resuming from ${pauseLine}`);
   1010  const onResumed = waitForResumed(dbg);
   1011  await dbg.actions.resume();
   1012  return onResumed;
   1013 }
   1014 
   1015 function deleteExpression(dbg, input) {
   1016  info(`Delete expression "${input}"`);
   1017  return dbg.actions.deleteExpression({ input });
   1018 }
   1019 
   1020 /**
   1021 * Reloads the debuggee.
   1022 *
   1023 * @memberof mochitest/actions
   1024 * @param {object} dbg
   1025 * @param {Array} sources
   1026 * @return {Promise}
   1027 * @static
   1028 */
   1029 async function reload(dbg, ...sources) {
   1030  await reloadBrowser();
   1031  return waitForSources(dbg, ...sources);
   1032 }
   1033 
   1034 // Only use this method when the page is paused by the debugger
   1035 // during page load and we navigate away without resuming.
   1036 //
   1037 // In this particular scenario, the page will never be "loaded".
   1038 // i.e. emit DOCUMENT_EVENT's dom-complete
   1039 // And consequently, debugger panel won't emit "reloaded" event.
   1040 async function reloadWhenPausedBeforePageLoaded(dbg, ...sources) {
   1041  // But we can at least listen for the next DOCUMENT_EVENT's dom-loading,
   1042  // which should be fired even if the page is pause the earliest.
   1043  const { resourceCommand } = dbg.commands;
   1044  const { onResource: onTopLevelDomLoading } =
   1045    await resourceCommand.waitForNextResource(
   1046      resourceCommand.TYPES.DOCUMENT_EVENT,
   1047      {
   1048        ignoreExistingResources: true,
   1049        predicate: resource =>
   1050          resource.targetFront.isTopLevel && resource.name === "dom-loading",
   1051      }
   1052    );
   1053 
   1054  gBrowser.reloadTab(gBrowser.selectedTab);
   1055 
   1056  info("Wait for DOCUMENT_EVENT dom-loading after reload");
   1057  await onTopLevelDomLoading;
   1058  return waitForSources(dbg, ...sources);
   1059 }
   1060 
   1061 /**
   1062 * Navigates the debuggee to another url.
   1063 *
   1064 * @memberof mochitest/actions
   1065 * @param {object} dbg
   1066 * @param {string} url
   1067 * @param {Array} sources
   1068 * @return {Promise}
   1069 * @static
   1070 */
   1071 async function navigate(dbg, url, ...sources) {
   1072  return navigateToAbsoluteURL(dbg, EXAMPLE_URL + url, ...sources);
   1073 }
   1074 
   1075 /**
   1076 * Navigates the debuggee to another absolute url.
   1077 *
   1078 * @memberof mochitest/actions
   1079 * @param {object} dbg
   1080 * @param {string} url
   1081 * @param {Array} sources
   1082 * @return {Promise}
   1083 * @static
   1084 */
   1085 async function navigateToAbsoluteURL(dbg, url, ...sources) {
   1086  await navigateTo(url);
   1087  return waitForSources(dbg, ...sources);
   1088 }
   1089 
   1090 function getFirstBreakpointColumn(dbg, source, line) {
   1091  const position = dbg.selectors.getFirstBreakpointPosition(
   1092    createLocation({
   1093      line,
   1094      source,
   1095    })
   1096  );
   1097 
   1098  return getSelectedLocation(position, source).column;
   1099 }
   1100 
   1101 function isMatchingLocation(location1, location2) {
   1102  return (
   1103    location1?.source.id == location2?.source.id &&
   1104    location1?.line == location2?.line &&
   1105    location1?.column == location2?.column
   1106  );
   1107 }
   1108 
   1109 function getBreakpointForLocation(dbg, location) {
   1110  if (!location) {
   1111    return undefined;
   1112  }
   1113 
   1114  const isGeneratedSource = isGeneratedId(location.source.id);
   1115  return dbg.selectors.getBreakpointsList().find(bp => {
   1116    const loc = isGeneratedSource ? bp.generatedLocation : bp.location;
   1117    return isMatchingLocation(loc, location);
   1118  });
   1119 }
   1120 
   1121 /**
   1122 * Adds a breakpoint to a source at line/col.
   1123 *
   1124 * @memberof mochitest/actions
   1125 * @param {object} dbg
   1126 * @param {string} source
   1127 * @param {number} line
   1128 * @param {number} col
   1129 * @return {Promise}
   1130 * @static
   1131 */
   1132 async function addBreakpoint(dbg, source, line, column, options) {
   1133  source = findSource(dbg, source);
   1134  const bpCount = dbg.selectors.getBreakpointCount();
   1135  const onBreakpoint = waitForDispatch(dbg.store, "SET_BREAKPOINT");
   1136  await dbg.actions.addBreakpoint(
   1137    // column is 0-based internally, but tests are using 1-based.
   1138    createLocation({ source, line, column: column - 1 }),
   1139    options
   1140  );
   1141  await onBreakpoint;
   1142  is(
   1143    dbg.selectors.getBreakpointCount(),
   1144    bpCount + 1,
   1145    "a new breakpoint was created"
   1146  );
   1147 }
   1148 
   1149 // use shortcut to open conditional panel.
   1150 function setConditionalBreakpointWithKeyboardShortcut(dbg, condition) {
   1151  pressKey(dbg, "toggleCondPanel");
   1152  return typeInPanel(dbg, condition);
   1153 }
   1154 
   1155 /**
   1156 * Similar to `addBreakpoint`, but uses the UI instead or calling
   1157 * the actions directly. This only support breakpoint on lines,
   1158 * not on a specific column.
   1159 */
   1160 async function addBreakpointViaGutter(dbg, line) {
   1161  info(`Add breakpoint via the editor on line ${line}`);
   1162  await clickGutter(dbg, line);
   1163  return waitForDispatch(dbg.store, "SET_BREAKPOINT");
   1164 }
   1165 
   1166 async function removeBreakpointViaGutter(dbg, line) {
   1167  const onRemoved = waitForDispatch(dbg.store, "REMOVE_BREAKPOINT");
   1168  await clickGutter(dbg, line);
   1169  await onRemoved;
   1170 }
   1171 
   1172 function disableBreakpoint(dbg, source, line, column) {
   1173  if (column === 0) {
   1174    throw new Error("disableBreakpoint expect a 1-based column argument");
   1175  }
   1176  // `internalColumn` is 0-based internally, while `column` manually defined in test scripts is 1-based.
   1177  const internalColumn = column
   1178    ? column - 1
   1179    : getFirstBreakpointColumn(dbg, source, line);
   1180  const location = createLocation({
   1181    source,
   1182    line,
   1183    column: internalColumn,
   1184  });
   1185  const bp = getBreakpointForLocation(dbg, location);
   1186  return dbg.actions.disableBreakpoint(bp);
   1187 }
   1188 
   1189 function findBreakpoint(dbg, url, line) {
   1190  const source = findSource(dbg, url);
   1191  return dbg.selectors.getBreakpointsForSource(source, line)[0];
   1192 }
   1193 
   1194 // helper for finding column breakpoints.
   1195 function findColumnBreakpoint(dbg, url, line, column) {
   1196  const source = findSource(dbg, url);
   1197  const lineBreakpoints = dbg.selectors.getBreakpointsForSource(source, line);
   1198 
   1199  return lineBreakpoints.find(bp => {
   1200    return source.isOriginal
   1201      ? bp.location.column === column
   1202      : bp.generatedLocation.column === column;
   1203  });
   1204 }
   1205 
   1206 async function loadAndAddBreakpoint(dbg, filename, line, column) {
   1207  const {
   1208    selectors: { getBreakpoint, getBreakpointCount, getBreakpointsMap },
   1209  } = dbg;
   1210 
   1211  await waitForSources(dbg, filename);
   1212 
   1213  ok(true, "Original sources exist");
   1214  const source = findSource(dbg, filename);
   1215 
   1216  await selectSource(dbg, source);
   1217 
   1218  // Test that breakpoint is not off by a line.
   1219  await addBreakpoint(dbg, source, line, column);
   1220 
   1221  is(getBreakpointCount(), 1, "One breakpoint exists");
   1222  // column is 0-based internally, but tests are using 1-based.
   1223  if (!getBreakpoint(createLocation({ source, line, column: column - 1 }))) {
   1224    const breakpoints = getBreakpointsMap();
   1225    const id = Object.keys(breakpoints).pop();
   1226    const loc = breakpoints[id].location;
   1227    ok(
   1228      false,
   1229      `Breakpoint has correct line ${line}, column ${column}, but was line ${
   1230        loc.line
   1231      } column ${loc.column + 1}`
   1232    );
   1233  }
   1234 
   1235  return source;
   1236 }
   1237 
   1238 async function invokeWithBreakpoint(
   1239  dbg,
   1240  fnName,
   1241  filename,
   1242  { line, column },
   1243  handler,
   1244  pauseOptions
   1245 ) {
   1246  const source = await loadAndAddBreakpoint(dbg, filename, line, column);
   1247 
   1248  const invokeResult = invokeInTab(fnName);
   1249 
   1250  const invokeFailed = await Promise.race([
   1251    waitForPaused(dbg, null, pauseOptions),
   1252    invokeResult.then(
   1253      () => new Promise(() => {}),
   1254      () => true
   1255    ),
   1256  ]);
   1257 
   1258  if (invokeFailed) {
   1259    await invokeResult;
   1260    return;
   1261  }
   1262 
   1263  await assertPausedAtSourceAndLine(
   1264    dbg,
   1265    findSource(dbg, filename).id,
   1266    line,
   1267    column
   1268  );
   1269 
   1270  await removeBreakpoint(dbg, source.id, line, column);
   1271 
   1272  is(dbg.selectors.getBreakpointCount(), 0, "Breakpoint reverted");
   1273 
   1274  await handler(source);
   1275 
   1276  await resume(dbg);
   1277 
   1278  // eslint-disable-next-line max-len
   1279  // If the invoke errored later somehow, capture here so the error is reported nicely.
   1280  await invokeResult;
   1281 }
   1282 
   1283 async function togglePrettyPrint(dbg) {
   1284  const source = dbg.selectors.getSelectedSource();
   1285  clickElement(dbg, "prettyPrintButton");
   1286  if (source.isPrettyPrinted) {
   1287    await waitForSelectedSource(dbg, source.generatedSource);
   1288  } else {
   1289    const prettyURL = source.url ? source.url : source.id.split("/").at(-1);
   1290    await waitForSelectedSource(dbg, prettyURL + ":formatted");
   1291  }
   1292 }
   1293 
   1294 async function expandAllScopes(dbg) {
   1295  const scopes = await waitForElement(dbg, "scopes");
   1296  const scopeElements = scopes.querySelectorAll(
   1297    '.tree-node[aria-level="1"][data-expandable="true"]:not([aria-expanded="true"])'
   1298  );
   1299  const indices = Array.from(scopeElements, el => {
   1300    return Array.prototype.indexOf.call(el.parentNode.childNodes, el);
   1301  }).reverse();
   1302 
   1303  for (const index of indices) {
   1304    await toggleScopeNode(dbg, index + 1);
   1305  }
   1306 }
   1307 
   1308 async function assertScopes(dbg, items) {
   1309  await expandAllScopes(dbg);
   1310 
   1311  for (const [i, val] of items.entries()) {
   1312    if (Array.isArray(val)) {
   1313      is(getScopeNodeLabel(dbg, i + 1), val[0]);
   1314      is(
   1315        getScopeNodeValue(dbg, i + 1),
   1316        val[1],
   1317        `"${val[0]}" has the expected "${val[1]}" value`
   1318      );
   1319    } else {
   1320      is(getScopeNodeLabel(dbg, i + 1), val);
   1321    }
   1322  }
   1323 
   1324  is(getScopeNodeLabel(dbg, items.length + 1), "Window");
   1325 }
   1326 
   1327 function findSourceTreeThreadByName(dbg, name) {
   1328  return [...findAllElements(dbg, "sourceTreeThreads")].find(el => {
   1329    return el.textContent.includes(name);
   1330  });
   1331 }
   1332 
   1333 function findSourceTreeGroupByName(dbg, name) {
   1334  return [...findAllElements(dbg, "sourceTreeGroups")].find(el => {
   1335    return el.textContent.includes(name);
   1336  });
   1337 }
   1338 
   1339 function findSourceNodeWithText(dbg, text) {
   1340  return [...findAllElements(dbg, "sourceNodes")].find(el => {
   1341    return el.textContent.includes(text);
   1342  });
   1343 }
   1344 
   1345 /**
   1346 * Assert the icon type used in the SourceTree for a given source
   1347 *
   1348 * @param {object} dbg
   1349 * @param {string} sourceName
   1350 *        Name of the source displayed in the source tree
   1351 * @param {string} icon
   1352 *        Expected icon CSS classname
   1353 */
   1354 function assertSourceIcon(dbg, sourceName, icon) {
   1355  const sourceItem = findSourceNodeWithText(dbg, sourceName);
   1356  ok(sourceItem, `Found the source item for ${sourceName}`);
   1357  is(
   1358    sourceItem.querySelector(".source-icon").className,
   1359    `dbg-img dbg-img-${icon} source-icon`,
   1360    `The icon for ${sourceName} is correct`
   1361  );
   1362 }
   1363 
   1364 async function expandSourceTree(dbg) {
   1365  // Click on expand all context menu for all top level "expandable items".
   1366  // If there is no project root, it will be thread items.
   1367  // But when there is a project root, it can be directory or group items.
   1368  // Select only expandable in order to ignore source items.
   1369  for (const rootNode of dbg.win.document.querySelectorAll(
   1370    ".sources-list > .tree > .tree-node[data-expandable=true]"
   1371  )) {
   1372    await expandAllSourceNodes(dbg, rootNode);
   1373  }
   1374 }
   1375 
   1376 async function expandAllSourceNodes(dbg, treeNode) {
   1377  return triggerSourceTreeContextMenu(dbg, treeNode, "#node-menu-expand-all");
   1378 }
   1379 
   1380 /**
   1381 * Removes a breakpoint from a source at line/col.
   1382 *
   1383 * @memberof mochitest/actions
   1384 * @param {object} dbg
   1385 * @param {string} source
   1386 * @param {number} line
   1387 * @param {number} col
   1388 * @return {Promise}
   1389 * @static
   1390 */
   1391 function removeBreakpoint(dbg, sourceId, line, column) {
   1392  const source = dbg.selectors.getSource(sourceId);
   1393  // column is 0-based internally, but tests are using 1-based.
   1394  column = column ? column - 1 : getFirstBreakpointColumn(dbg, source, line);
   1395  const location = createLocation({
   1396    source,
   1397    line,
   1398    column,
   1399  });
   1400  const bp = getBreakpointForLocation(dbg, location);
   1401  return dbg.actions.removeBreakpoint(bp);
   1402 }
   1403 
   1404 /**
   1405 * Toggles the Pause on exceptions feature in the debugger.
   1406 *
   1407 * @memberof mochitest/actions
   1408 * @param {object} dbg
   1409 * @param {boolean} pauseOnExceptions
   1410 * @param {boolean} pauseOnCaughtExceptions
   1411 * @return {Promise}
   1412 * @static
   1413 */
   1414 async function togglePauseOnExceptions(
   1415  dbg,
   1416  pauseOnExceptions,
   1417  pauseOnCaughtExceptions
   1418 ) {
   1419  return dbg.actions.pauseOnExceptions(
   1420    pauseOnExceptions,
   1421    pauseOnCaughtExceptions
   1422  );
   1423 }
   1424 
   1425 // Helpers
   1426 
   1427 /**
   1428 * Invokes a global function in the debuggee tab.
   1429 *
   1430 * @memberof mochitest/helpers
   1431 * @param {string} fnc The name of a global function on the content window to
   1432 *                     call. This is applied to structured clones of the
   1433 *                     remaining arguments to invokeInTab.
   1434 * @param {Any} ...args Remaining args to serialize and pass to fnc.
   1435 * @return {Promise}
   1436 * @static
   1437 */
   1438 function invokeInTab(fnc, ...args) {
   1439  info(`Invoking in tab: ${fnc}(${args.map(uneval).join(",")})`);
   1440  return ContentTask.spawn(gBrowser.selectedBrowser, { fnc, args }, options =>
   1441    content.wrappedJSObject[options.fnc](...options.args)
   1442  );
   1443 }
   1444 
   1445 function clickElementInTab(selector) {
   1446  info(`click element ${selector} in tab`);
   1447 
   1448  return SpecialPowers.spawn(
   1449    gBrowser.selectedBrowser,
   1450    [selector],
   1451    function (_selector) {
   1452      const element = content.document.querySelector(_selector);
   1453      // Run the click in another event loop in order to immediately resolve spawn's promise.
   1454      // Otherwise if we pause on click and navigate, the JSWindowActor used by spawn will
   1455      // be destroyed while its query is still pending. And this would reject the promise.
   1456      content.setTimeout(() => {
   1457        element.click();
   1458      });
   1459    }
   1460  );
   1461 }
   1462 
   1463 const isLinux = Services.appinfo.OS === "Linux";
   1464 const isMac = Services.appinfo.OS === "Darwin";
   1465 const cmdOrCtrl = isMac ? { metaKey: true } : { ctrlKey: true };
   1466 const shiftOrAlt = isMac
   1467  ? { accelKey: true, shiftKey: true }
   1468  : { accelKey: true, altKey: true };
   1469 
   1470 const cmdShift = isMac
   1471  ? { accelKey: true, shiftKey: true, metaKey: true }
   1472  : { accelKey: true, shiftKey: true, ctrlKey: true };
   1473 
   1474 // On Mac, going to beginning/end only works with meta+left/right.  On
   1475 // Windows, it only works with home/end.  On Linux, apparently, either
   1476 // ctrl+left/right or home/end work.
   1477 const endKey = isMac
   1478  ? { code: "VK_RIGHT", modifiers: cmdOrCtrl }
   1479  : { code: "VK_END" };
   1480 const startKey = isMac
   1481  ? { code: "VK_LEFT", modifiers: cmdOrCtrl }
   1482  : { code: "VK_HOME" };
   1483 
   1484 const keyMappings = {
   1485  close: { code: "w", modifiers: cmdOrCtrl },
   1486  commandKeyDown: { code: "VK_META", modifiers: { type: "keydown" } },
   1487  commandKeyUp: { code: "VK_META", modifiers: { type: "keyup" } },
   1488  debugger: { code: "s", modifiers: shiftOrAlt },
   1489  // test conditional panel shortcut
   1490  toggleCondPanel: { code: "b", modifiers: cmdShift },
   1491  toggleLogPanel: { code: "y", modifiers: cmdShift },
   1492  toggleBreakpoint: { code: "b", modifiers: cmdOrCtrl },
   1493  inspector: { code: "c", modifiers: shiftOrAlt },
   1494  quickOpen: { code: "p", modifiers: cmdOrCtrl },
   1495  quickOpenFunc: { code: "o", modifiers: cmdShift },
   1496  quickOpenLine: { code: ":", modifiers: cmdOrCtrl },
   1497  fileSearch: { code: "f", modifiers: cmdOrCtrl },
   1498  projectSearch: { code: "f", modifiers: cmdShift },
   1499  fileSearchNext: { code: "g", modifiers: { metaKey: true } },
   1500  fileSearchPrev: { code: "g", modifiers: cmdShift },
   1501  goToLine: { code: "g", modifiers: { ctrlKey: true } },
   1502  sourceeditorGoToLine: { code: "j", modifiers: cmdOrCtrl },
   1503  Enter: { code: "VK_RETURN" },
   1504  ShiftEnter: { code: "VK_RETURN", modifiers: { shiftKey: true } },
   1505  AltEnter: {
   1506    code: "VK_RETURN",
   1507    modifiers: { altKey: true },
   1508  },
   1509  Space: { code: "VK_SPACE" },
   1510  Up: { code: "VK_UP" },
   1511  Down: { code: "VK_DOWN" },
   1512  Right: { code: "VK_RIGHT" },
   1513  Left: { code: "VK_LEFT" },
   1514  End: endKey,
   1515  Start: startKey,
   1516  Tab: { code: "VK_TAB" },
   1517  ShiftTab: { code: "VK_TAB", modifiers: { shiftKey: true } },
   1518  Escape: { code: "VK_ESCAPE" },
   1519  Delete: { code: "VK_DELETE" },
   1520  pauseKey: { code: "VK_F8" },
   1521  resumeKey: { code: "VK_F8" },
   1522  stepOverKey: { code: "VK_F10" },
   1523  stepInKey: { code: "VK_F11" },
   1524  stepOutKey: {
   1525    code: "VK_F11",
   1526    modifiers: { shiftKey: true },
   1527  },
   1528  Backspace: { code: "VK_BACK_SPACE" },
   1529 };
   1530 
   1531 /**
   1532 * Simulates a key press in the debugger window.
   1533 *
   1534 * @memberof mochitest/helpers
   1535 * @param {object} dbg
   1536 * @param {string} keyName
   1537 * @return {Promise}
   1538 * @static
   1539 */
   1540 function pressKey(dbg, keyName) {
   1541  const keyEvent = keyMappings[keyName];
   1542  const { code, modifiers } = keyEvent;
   1543  info(`The ${keyName} key is pressed`);
   1544  return EventUtils.synthesizeKey(code, modifiers || {}, dbg.win);
   1545 }
   1546 
   1547 function type(dbg, string) {
   1548  string.split("").forEach(char => EventUtils.synthesizeKey(char, {}, dbg.win));
   1549 }
   1550 
   1551 /**
   1552 * Checks to see if the inner element is visible inside the editor.
   1553 *
   1554 * @memberof mochitest/helpers
   1555 * @param {object} dbg
   1556 * @param {HTMLElement} inner element
   1557 * @return {boolean}
   1558 * @static
   1559 */
   1560 
   1561 function isVisibleInEditor(dbg, element) {
   1562  return isVisible(findElement(dbg, "codeMirror"), element);
   1563 }
   1564 
   1565 /**
   1566 * Checks to see if the inner element is visible inside the
   1567 * outer element.
   1568 *
   1569 * Note, the inner element does not need to be entirely visible,
   1570 * it is possible for it to be somewhat clipped by the outer element's
   1571 * bounding element or for it to span the entire length, starting before the
   1572 * outer element and ending after.
   1573 *
   1574 * @memberof mochitest/helpers
   1575 * @param {HTMLElement} outer element
   1576 * @param {HTMLElement} inner element
   1577 * @return {boolean}
   1578 * @static
   1579 */
   1580 function isVisible(outerEl, innerEl) {
   1581  if (!innerEl || !outerEl) {
   1582    return false;
   1583  }
   1584 
   1585  const innerRect = innerEl.getBoundingClientRect();
   1586  const outerRect = outerEl.getBoundingClientRect();
   1587 
   1588  const verticallyVisible =
   1589    innerRect.top >= outerRect.top ||
   1590    innerRect.bottom <= outerRect.bottom ||
   1591    (innerRect.top < outerRect.top && innerRect.bottom > outerRect.bottom);
   1592 
   1593  const horizontallyVisible =
   1594    innerRect.left >= outerRect.left ||
   1595    innerRect.right <= outerRect.right ||
   1596    (innerRect.left < outerRect.left && innerRect.right > outerRect.right);
   1597 
   1598  const visible = verticallyVisible && horizontallyVisible;
   1599  return visible;
   1600 }
   1601 
   1602 // Handles virtualization scenarios
   1603 async function scrollAndGetEditorLineGutterElement(dbg, line) {
   1604  const editor = getCMEditor(dbg);
   1605  await scrollEditorIntoView(dbg, line, 0);
   1606  const selectedSource = dbg.selectors.getSelectedSource();
   1607  // For WASM sources get the hexadecimal line number displayed in the gutter
   1608  if (editor.isWasm && !selectedSource.isOriginal) {
   1609    const wasmLineFormatter = editor.getWasmLineNumberFormatter();
   1610    line = wasmLineFormatter(line);
   1611  }
   1612 
   1613  const els = findAllElementsWithSelector(
   1614    dbg,
   1615    ".cm-gutter.cm-lineNumbers .cm-gutterElement"
   1616  );
   1617  return [...els].find(el => el.innerText == line);
   1618 }
   1619 
   1620 /**
   1621 * Gets node at a specific line in the editor
   1622 *
   1623 * @param {*} dbg
   1624 * @param {number} line
   1625 * @returns {Element} DOM Element
   1626 */
   1627 async function getNodeAtEditorLine(dbg, line) {
   1628  await scrollEditorIntoView(dbg, line, 0);
   1629  return getCMEditor(dbg).getElementAtLine(line);
   1630 }
   1631 
   1632 /**
   1633 * Gets node at a specific line in the gutter
   1634 *
   1635 * @param {*} dbg
   1636 * @param {number} line
   1637 * @returns {Element} DOM Element
   1638 */
   1639 async function getNodeAtEditorGutterLine(dbg, line) {
   1640  return scrollAndGetEditorLineGutterElement(dbg, line);
   1641 }
   1642 
   1643 async function getConditionalPanelAtLine(dbg, line) {
   1644  info(`Get conditional panel at line ${line}`);
   1645  const el = await getNodeAtEditorLine(dbg, line);
   1646  return el.nextSibling.querySelector(".conditional-breakpoint-panel");
   1647 }
   1648 
   1649 async function waitForConditionalPanelFocus(dbg) {
   1650  return waitFor(
   1651    () =>
   1652      dbg.win.document.activeElement.classList.contains("cm-content") &&
   1653      dbg.win.document.activeElement.closest(".conditional-breakpoint-panel")
   1654  );
   1655 }
   1656 
   1657 /**
   1658 * Opens the debugger editor context menu in either codemirror or the
   1659 * the debugger gutter.
   1660 *
   1661 * @param {object} dbg
   1662 * @param {string} elementName
   1663 *                  The element to select
   1664 * @param {number} line
   1665 *                  The line to open the context menu on.
   1666 */
   1667 async function openContextMenuInDebugger(dbg, elementName, line) {
   1668  const waitForOpen = waitForContextMenu(dbg);
   1669  info(`Open ${elementName} context menu on line ${line || ""}`);
   1670  rightClickElement(dbg, elementName, line);
   1671  return waitForOpen;
   1672 }
   1673 
   1674 /**
   1675 * Select a range of lines in the editor and open the contextmenu
   1676 *
   1677 * @param {object} dbg
   1678 * @param {object} lines
   1679 * @param {string} elementName
   1680 * @returns
   1681 */
   1682 async function selectEditorLinesAndOpenContextMenu(
   1683  dbg,
   1684  lines,
   1685  elementName = "line"
   1686 ) {
   1687  const { startLine, endLine } = lines;
   1688  setSelection(dbg, startLine, endLine ?? startLine);
   1689  return openContextMenuInDebugger(dbg, elementName, startLine);
   1690 }
   1691 
   1692 /**
   1693 * Asserts that the styling for ignored lines are applied
   1694 *
   1695 * @param {object} dbg
   1696 * @param {object} options
   1697 *                 lines {null | Number[]} [lines] Line(s) to assert.
   1698 *                   - If null is passed, the assertion is on all the blackboxed lines
   1699 *                   - If an array of one item (start line) is passed, the assertion is on the specified line
   1700 *                   - If an array (start and end lines) is passed, the assertion is on the multiple lines seelected
   1701 *                 hasBlackboxedLinesClass
   1702 *                   If `true` assert that style exist, else assert that style does not exist
   1703 */
   1704 async function assertIgnoredStyleInSourceLines(
   1705  dbg,
   1706  { lines, hasBlackboxedLinesClass }
   1707 ) {
   1708  if (lines) {
   1709    let currentLine = lines[0];
   1710    do {
   1711      const element = await getNodeAtEditorLine(dbg, currentLine);
   1712      const hasStyle = element.classList.contains("blackboxed-line");
   1713      is(
   1714        hasStyle,
   1715        hasBlackboxedLinesClass,
   1716        `Line ${currentLine} ${
   1717          hasBlackboxedLinesClass ? "does not have" : "has"
   1718        } ignored styling`
   1719      );
   1720      currentLine = currentLine + 1;
   1721    } while (currentLine <= lines[1]);
   1722  } else {
   1723    const codeLines = findAllElements(dbg, "codeLines");
   1724    const blackboxedLines = findAllElements(dbg, "blackboxedLines");
   1725    is(
   1726      hasBlackboxedLinesClass ? codeLines.length : 0,
   1727      blackboxedLines.length,
   1728      `${blackboxedLines.length} of ${codeLines.length} lines are blackboxed`
   1729    );
   1730  }
   1731 }
   1732 
   1733 /**
   1734 * Assert the text content on the line matches what is
   1735 * expected.
   1736 *
   1737 * @param {object} dbg
   1738 * @param {number} line
   1739 * @param {string} expectedTextContent
   1740 */
   1741 function assertTextContentOnLine(dbg, line, expectedTextContent) {
   1742  const lineInfo = getCMEditor(dbg).lineInfo(line);
   1743  const textContent = lineInfo.text.trim();
   1744  is(textContent, expectedTextContent, `Expected text content on line ${line}`);
   1745 }
   1746 
   1747 /**
   1748 * Assert that no breakpoint is set on a given line of
   1749 * the currently selected source in the editor.
   1750 *
   1751 * @memberof mochitest/helpers
   1752 * @param {object} dbg
   1753 * @param {number} line Line where to check for a breakpoint in the editor
   1754 * @static
   1755 */
   1756 async function assertNoBreakpoint(dbg, line) {
   1757  const el = await getNodeAtEditorGutterLine(dbg, line);
   1758 
   1759  const exists = el.classList.contains("cm6-gutter-breakpoint");
   1760  ok(!exists, `Breakpoint doesn't exists on line ${line}`);
   1761 }
   1762 
   1763 /**
   1764 * Assert that a regular breakpoint is set in the currently
   1765 * selected source in the editor. (no conditional, nor log breakpoint)
   1766 *
   1767 * @memberof mochitest/helpers
   1768 * @param {object} dbg
   1769 * @param {number} line Line where to check for a breakpoint
   1770 * @static
   1771 */
   1772 async function assertBreakpoint(dbg, line) {
   1773  const el = await getNodeAtEditorGutterLine(dbg, line);
   1774  ok(
   1775    el.firstChild.classList.contains(selectors.gutterBreakpoint),
   1776    `Breakpoint exists on line ${line}`
   1777  );
   1778 
   1779  const hasConditionClass = el.firstChild.classList.contains("has-condition");
   1780  ok(
   1781    !hasConditionClass,
   1782    `Regular breakpoint doesn't have condition on line ${line}`
   1783  );
   1784 
   1785  const hasLogClass = el.firstChild.classList.contains("has-log");
   1786  ok(!hasLogClass, `Regular breakpoint doesn't have log on line ${line}`);
   1787 }
   1788 
   1789 /**
   1790 * Assert that a conditionnal breakpoint is set.
   1791 *
   1792 * @memberof mochitest/helpers
   1793 * @param {object} dbg
   1794 * @param {number} line Line where to check for a breakpoint
   1795 * @static
   1796 */
   1797 async function assertConditionBreakpoint(dbg, line) {
   1798  const el = await getNodeAtEditorGutterLine(dbg, line);
   1799 
   1800  ok(
   1801    el.firstChild.classList.contains(selectors.gutterBreakpoint),
   1802    `Breakpoint exists on line ${line}`
   1803  );
   1804 
   1805  const hasConditionClass = el.firstChild.classList.contains("has-condition");
   1806  ok(hasConditionClass, `Conditional breakpoint on line ${line}`);
   1807 
   1808  const hasLogClass = el.firstChild.classList.contains("has-log");
   1809  ok(
   1810    !hasLogClass,
   1811    `Conditional breakpoint doesn't have log breakpoint on line ${line}`
   1812  );
   1813 }
   1814 
   1815 /**
   1816 * Assert that a log breakpoint is set.
   1817 *
   1818 * @memberof mochitest/helpers
   1819 * @param {object} dbg
   1820 * @param {number} line Line where to check for a breakpoint
   1821 * @static
   1822 */
   1823 async function assertLogBreakpoint(dbg, line) {
   1824  const el = await getNodeAtEditorGutterLine(dbg, line);
   1825  ok(
   1826    el.firstChild.classList.contains(selectors.gutterBreakpoint),
   1827    `Breakpoint exists on line ${line}`
   1828  );
   1829 
   1830  const hasConditionClass = el.firstChild.classList.contains("has-condition");
   1831  ok(
   1832    !hasConditionClass,
   1833    `Log breakpoint doesn't have condition on line ${line}`
   1834  );
   1835 
   1836  const hasLogClass = el.firstChild.classList.contains("has-log");
   1837  ok(hasLogClass, `Log breakpoint on line ${line}`);
   1838 }
   1839 
   1840 function assertBreakpointSnippet(dbg, index, expectedSnippet) {
   1841  const actualSnippet = findElement(dbg, "breakpointLabel", 2).innerText;
   1842  is(actualSnippet, expectedSnippet, `Breakpoint ${index} snippet`);
   1843 }
   1844 
   1845 const selectors = {
   1846  callStackBody: ".call-stack-pane .pane",
   1847  domMutationItem: ".dom-mutation-list li",
   1848  expressionNode: i =>
   1849    `.expressions-list .expression-container:nth-child(${i}) .object-label`,
   1850  expressionValue: i =>
   1851    // eslint-disable-next-line max-len
   1852    `.expressions-list .expression-container:nth-child(${i}) .object-delimiter + *`,
   1853  expressionInput: ".watch-expressions-pane input.input-expression",
   1854  expressionNodes: ".expressions-list .tree-node",
   1855  expressionPlus: ".watch-expressions-pane button.plus",
   1856  expressionRefresh: ".watch-expressions-pane button.refresh",
   1857  expressionsHeader: ".watch-expressions-pane ._header .header-label",
   1858  scopesHeader: ".scopes-pane ._header .header-label",
   1859  breakpointItem: i => `.breakpoints-list div:nth-of-type(${i})`,
   1860  breakpointLabel: i => `${selectors.breakpointItem(i)} .breakpoint-label`,
   1861  breakpointHeadings: ".breakpoints-list .breakpoint-heading",
   1862  breakpointItems: ".breakpoints-list .breakpoint",
   1863  breakpointContextMenu: {
   1864    disableSelf: "#node-menu-disable-self",
   1865    disableAll: "#node-menu-disable-all",
   1866    disableOthers: "#node-menu-disable-others",
   1867    enableSelf: "#node-menu-enable-self",
   1868    enableOthers: "#node-menu-enable-others",
   1869    disableDbgStatement: "#node-menu-disable-dbgStatement",
   1870    enableDbgStatement: "#node-menu-enable-dbgStatement",
   1871    remove: "#node-menu-delete-self",
   1872    removeOthers: "#node-menu-delete-other",
   1873    removeCondition: "#node-menu-remove-condition",
   1874  },
   1875  blackboxedLines: ".cm-content > .blackboxed-line",
   1876  codeLines: ".cm-content > .cm-line",
   1877  editorContextMenu: {
   1878    continueToHere: "#node-menu-continue-to-here",
   1879  },
   1880  columnBreakpoints: ".column-breakpoint",
   1881  scopes: ".scopes-list",
   1882  scopeNodes: ".scopes-list .object-label",
   1883  scopeNode: i => `.scopes-list .tree-node:nth-child(${i}) .object-label`,
   1884  scopeValue: i =>
   1885    `.scopes-list .tree-node:nth-child(${i}) .object-delimiter + *`,
   1886  mapScopesCheckbox: ".map-scopes-header input",
   1887  asyncframe: i =>
   1888    `.frames div[role=listbox] .location-async-cause:nth-child(${i})`,
   1889  frame: i => `.frames div[role=listbox] .frame:nth-child(${i})`,
   1890  frames: ".frames [role='listbox'] .frame",
   1891  gutterBreakpoint: "breakpoint-marker",
   1892  // This is used to trigger events (click etc) on the gutter
   1893  gutterElement: i =>
   1894    `.cm-gutter.cm-lineNumbers .cm-gutterElement:nth-child(${i + 1})`,
   1895  gutters: `.cm-gutters`,
   1896  line: i => `.cm-content > div.cm-line:nth-child(${i})`,
   1897  addConditionItem:
   1898    "#node-menu-add-condition, #node-menu-add-conditional-breakpoint",
   1899  editConditionItem:
   1900    "#node-menu-edit-condition, #node-menu-edit-conditional-breakpoint",
   1901  addLogItem: "#node-menu-add-log-point",
   1902  editLogItem: "#node-menu-edit-log-point",
   1903  disableItem: "#node-menu-disable-breakpoint",
   1904  breakpoint: ".cm-gutter > .cm6-gutter-breakpoint",
   1905  highlightLine: ".cm-content > .highlight-line",
   1906  pausedLine: ".paused-line",
   1907  tracedLine: ".traced-line",
   1908  debugErrorLine: ".new-debug-line-error",
   1909  codeMirror: ".cm-editor",
   1910  resume: ".resume.active",
   1911  pause: ".pause.active",
   1912  sourceTabs: ".source-tabs",
   1913  activeTab: ".source-tab.active",
   1914  stepOver: ".stepOver.active",
   1915  stepOut: ".stepOut.active",
   1916  stepIn: ".stepIn.active",
   1917  prettyPrintButton: ".source-footer .prettyPrint",
   1918  mappedSourceLink: ".source-footer .mapped-source",
   1919  sourceMapFooterButton: ".debugger-source-map-button",
   1920  sourceNode: i => `.sources-list .tree-node:nth-child(${i}) .node`,
   1921  sourceNodes: ".sources-list .tree-node",
   1922  sourceTreeThreads: '.sources-list .tree-node[aria-level="1"]',
   1923  sourceTreeGroups: '.sources-list .tree-node[aria-level="2"]',
   1924  sourceTreeFiles: ".sources-list .tree-node[data-expandable=false]",
   1925  sourceTreeFilesElement: i =>
   1926    `.sources-list .tree-node[data-expandable=false]:nth-child(${i})`,
   1927  threadSourceTree: i => `.threads-list .sources-pane:nth-child(${i})`,
   1928  sourceDirectoryLabel: i => `.sources-list .tree-node:nth-child(${i}) .label`,
   1929  resultItems: ".result-list .result-item",
   1930  resultItemName: (name, i) =>
   1931    `${selectors.resultItems}:nth-child(${i})[title$="${name}"]`,
   1932  fileMatch: ".project-text-search .line-value",
   1933  popup: ".popover",
   1934  previewPopup: ".preview-popup",
   1935  openInspector: "button.open-inspector",
   1936  outlineItem: i =>
   1937    `.outline-list__element:nth-child(${i}) .function-signature`,
   1938  outlineItems: ".outline-list__element",
   1939  conditionalPanel: ".conditional-breakpoint-panel",
   1940  conditionalPanelInput: `.conditional-breakpoint-panel .cm-content`,
   1941  logPanelInput: `.conditional-breakpoint-panel.log-point .cm-content`,
   1942  conditionalBreakpointInSecPane: ".breakpoint.is-conditional",
   1943  logPointPanel: ".conditional-breakpoint-panel.log-point",
   1944  logPointInSecPane: ".breakpoint.is-log",
   1945  tracePanel: ".trace-panel",
   1946  searchField: ".search-field",
   1947  blackbox: ".action.black-box",
   1948  projectSearchSearchInput: ".project-text-search .search-field input",
   1949  projectSearchCollapsed: ".project-text-search .dbg-img-arrow:not(.expanded)",
   1950  projectSearchExpandedResults: ".project-text-search .result",
   1951  projectSearchFileResults: ".project-text-search .file-result",
   1952  projectSearchModifiersCaseSensitive:
   1953    ".project-text-search button.case-sensitive-btn",
   1954  projectSearchModifiersRegexMatch:
   1955    ".project-text-search button.regex-match-btn",
   1956  projectSearchModifiersWholeWordMatch:
   1957    ".project-text-search button.whole-word-btn",
   1958  projectSearchRefreshButton: ".project-text-search button.refresh-btn",
   1959  threadsPaneItems: ".threads-pane .thread",
   1960  threadsPaneItem: i => `.threads-pane .thread:nth-child(${i})`,
   1961  threadsPaneItemPause: i => `${selectors.threadsPaneItem(i)}.paused`,
   1962  CodeMirrorLines: ".cm-content",
   1963  CodeMirrorCode: ".cm-content",
   1964  visibleInlinePreviews: ".inline-preview .inline-preview-outer",
   1965  inlinePreviewsOnLine: i =>
   1966    `.cm-content > div.cm-line:nth-child(${i}) .inline-preview .inline-preview-outer`,
   1967  inlinePreviewOpenInspector: ".inline-preview-value button.open-inspector",
   1968  watchpointsSubmenu: "#node-menu-watchpoints",
   1969  addGetWatchpoint: "#node-menu-add-get-watchpoint",
   1970  logEventsCheckbox: ".events-header input",
   1971  previewPopupInvokeGetterButton: ".preview-popup .invoke-getter",
   1972  previewPopupObjectNumber: ".preview-popup .objectBox-number",
   1973  previewPopupObjectObject: ".preview-popup .objectBox-object",
   1974  previewPopupObjectFunction: ".preview-popup .objectBox-function",
   1975  previewPopupObjectFunctionJumpToDefinition:
   1976    ".preview-popup .objectBox-function .jump-definition",
   1977  sourceTreeRootNode: ".sources-panel .node .dbg-img-window",
   1978  sourceTreeFolderNode: ".sources-panel .node .dbg-img-folder",
   1979  excludePatternsInput: ".project-text-search .exclude-patterns-field input",
   1980  fileSearchInput: ".search-bar input",
   1981  fileSearchSummary: ".search-bar .search-field-summary",
   1982  watchExpressionsHeader: ".watch-expressions-pane ._header .header-label",
   1983  watchExpressionsAddButton: ".watch-expressions-pane ._header .plus",
   1984  editorNotificationFooter: ".editor-notification-footer",
   1985 };
   1986 
   1987 function getSelector(elementName, ...args) {
   1988  let selector = selectors[elementName];
   1989  if (!selector) {
   1990    throw new Error(`The selector ${elementName} is not defined`);
   1991  }
   1992 
   1993  if (typeof selector == "function") {
   1994    selector = selector(...args);
   1995  }
   1996 
   1997  return selector;
   1998 }
   1999 
   2000 function findElement(dbg, elementName, ...args) {
   2001  const selector = getSelector(elementName, ...args);
   2002  return findElementWithSelector(dbg, selector);
   2003 }
   2004 
   2005 function findElementWithSelector(dbg, selector) {
   2006  return dbg.win.document.querySelector(selector);
   2007 }
   2008 
   2009 function findAllElements(dbg, elementName, ...args) {
   2010  const selector = getSelector(elementName, ...args);
   2011  return findAllElementsWithSelector(dbg, selector);
   2012 }
   2013 
   2014 function findAllElementsWithSelector(dbg, selector) {
   2015  return dbg.win.document.querySelectorAll(selector);
   2016 }
   2017 
   2018 function getSourceNodeLabel(dbg, index) {
   2019  return findElement(dbg, "sourceNode", index)
   2020    .textContent.trim()
   2021    .replace(/^[\s\u200b]*/g, "");
   2022 }
   2023 
   2024 /**
   2025 * Simulates a mouse click in the debugger DOM.
   2026 *
   2027 * @memberof mochitest/helpers
   2028 * @param {object} dbg
   2029 * @param {string} elementName
   2030 * @param {Array} args
   2031 * @return {Promise}
   2032 * @static
   2033 */
   2034 async function clickElement(dbg, elementName, ...args) {
   2035  const selector = getSelector(elementName, ...args);
   2036  const el = await waitForElementWithSelector(dbg, selector);
   2037 
   2038  el.scrollIntoView();
   2039 
   2040  return clickElementWithSelector(dbg, selector);
   2041 }
   2042 
   2043 function clickElementWithSelector(dbg, selector) {
   2044  clickDOMElement(dbg, findElementWithSelector(dbg, selector));
   2045 }
   2046 
   2047 function clickDOMElement(dbg, element, options = {}) {
   2048  EventUtils.synthesizeMouseAtCenter(element, options, dbg.win);
   2049 }
   2050 
   2051 function dblClickElement(dbg, elementName, ...args) {
   2052  const selector = getSelector(elementName, ...args);
   2053 
   2054  return EventUtils.synthesizeMouseAtCenter(
   2055    findElementWithSelector(dbg, selector),
   2056    { clickCount: 2 },
   2057    dbg.win
   2058  );
   2059 }
   2060 
   2061 function clickElementWithOptions(dbg, elementName, options, ...args) {
   2062  const selector = getSelector(elementName, ...args);
   2063  const el = findElementWithSelector(dbg, selector);
   2064  el.scrollIntoView();
   2065 
   2066  return EventUtils.synthesizeMouseAtCenter(el, options, dbg.win);
   2067 }
   2068 
   2069 function altClickElement(dbg, elementName, ...args) {
   2070  return clickElementWithOptions(dbg, elementName, { altKey: true }, ...args);
   2071 }
   2072 
   2073 function shiftClickElement(dbg, elementName, ...args) {
   2074  return clickElementWithOptions(dbg, elementName, { shiftKey: true }, ...args);
   2075 }
   2076 
   2077 function rightClickElement(dbg, elementName, ...args) {
   2078  const selector = getSelector(elementName, ...args);
   2079  return rightClickEl(dbg, dbg.win.document.querySelector(selector));
   2080 }
   2081 
   2082 function rightClickEl(dbg, el) {
   2083  el.scrollIntoView();
   2084  EventUtils.synthesizeMouseAtCenter(el, { type: "contextmenu" }, dbg.win);
   2085 }
   2086 
   2087 async function clearElement(dbg, elementName) {
   2088  await clickElement(dbg, elementName);
   2089  await pressKey(dbg, "End");
   2090  const selector = getSelector(elementName);
   2091  const el = findElementWithSelector(dbg, getSelector(elementName));
   2092  let len = el.value.length;
   2093  while (len) {
   2094    pressKey(dbg, "Backspace");
   2095    len--;
   2096  }
   2097 }
   2098 
   2099 async function clickGutter(dbg, line) {
   2100  const el = await scrollAndGetEditorLineGutterElement(dbg, line);
   2101  clickDOMElement(dbg, el);
   2102 }
   2103 
   2104 async function cmdClickGutter(dbg, line) {
   2105  const el = await scrollAndGetEditorLineGutterElement(dbg, line);
   2106  clickDOMElement(dbg, el, cmdOrCtrl);
   2107 }
   2108 
   2109 function findContextMenu(dbg, selector) {
   2110  // the context menu is in the toolbox window
   2111  const doc = dbg.toolbox.topDoc;
   2112 
   2113  // there are several context menus, we want the one with the menu-api
   2114  const popup = doc.querySelector('menupopup[menu-api="true"]');
   2115 
   2116  return popup.querySelector(selector);
   2117 }
   2118 
   2119 async function assertContextMenuItemDisabled(dbg, selector, expectedState) {
   2120  const item = await waitFor(() => findContextMenu(dbg, selector));
   2121  is(item.disabled, expectedState, "The context menu item is disabled");
   2122 }
   2123 
   2124 // Waits for the context menu to exist and to fully open. Once this function
   2125 // completes, selectDebuggerContextMenuItem can be called.
   2126 // waitForContextMenu must be called after menu opening has been triggered, e.g.
   2127 // after synthesizing a right click / contextmenu event.
   2128 async function waitForContextMenu(dbg) {
   2129  // the context menu is in the toolbox window
   2130  const doc = dbg.toolbox.topDoc;
   2131 
   2132  // there are several context menus, we want the one with the menu-api
   2133  const popup = await waitFor(() =>
   2134    doc.querySelector('menupopup[menu-api="true"]')
   2135  );
   2136 
   2137  if (popup.state == "open") {
   2138    return popup;
   2139  }
   2140 
   2141  await new Promise(resolve => {
   2142    popup.addEventListener("popupshown", () => resolve(), { once: true });
   2143  });
   2144 
   2145  return popup;
   2146 }
   2147 
   2148 /**
   2149 * Closes and open context menu popup.
   2150 *
   2151 * @memberof mochitest/helpers
   2152 * @param {object} dbg
   2153 * @param {string} popup - The currently opened popup returned by
   2154 *                         `waitForContextMenu`.
   2155 * @return {Promise}
   2156 */
   2157 
   2158 async function closeContextMenu(dbg, popup) {
   2159  const onHidden = new Promise(resolve => {
   2160    popup.addEventListener("popuphidden", resolve, { once: true });
   2161  });
   2162  popup.hidePopup();
   2163  return onHidden;
   2164 }
   2165 
   2166 function selectDebuggerContextMenuItem(dbg, selector) {
   2167  const item = findContextMenu(dbg, selector);
   2168  item.closest("menupopup").activateItem(item);
   2169 }
   2170 
   2171 async function openContextMenuSubmenu(dbg, selector) {
   2172  const item = findContextMenu(dbg, selector);
   2173  const popup = item.menupopup;
   2174  const popupshown = new Promise(resolve => {
   2175    popup.addEventListener("popupshown", () => resolve(), { once: true });
   2176  });
   2177  item.openMenu(true);
   2178  await popupshown;
   2179  return popup;
   2180 }
   2181 
   2182 async function assertContextMenuLabel(dbg, selector, expectedLabel) {
   2183  const item = await waitFor(() => findContextMenu(dbg, selector));
   2184  is(
   2185    item.label,
   2186    expectedLabel,
   2187    "The label of the context menu item shown to the user"
   2188  );
   2189 }
   2190 
   2191 async function typeInPanel(dbg, text, inLogPanel = false) {
   2192  const panelName = inLogPanel ? "logPanelInput" : "conditionalPanelInput";
   2193  await waitForElement(dbg, panelName);
   2194 
   2195  // Wait a bit for panel's codemirror document to complete any updates
   2196  // so the  input does not lose focus after the it has been opened
   2197  await waitForInPanelDocumentLoadComplete(dbg, panelName);
   2198 
   2199  // Position cursor reliably at the end of the text.
   2200  pressKey(dbg, "End");
   2201 
   2202  type(dbg, text);
   2203  // Wait for any possible scroll actions in the conditional panel editor to complete
   2204  await wait(1000);
   2205  pressKey(dbg, "Enter");
   2206 }
   2207 
   2208 async function toggleMapScopes(dbg) {
   2209  info("Turn on original variable mapping");
   2210  const scopesLoaded = waitForLoadedScopes(dbg);
   2211  const onDispatch = waitForDispatch(dbg.store, "TOGGLE_MAP_SCOPES");
   2212  clickElement(dbg, "mapScopesCheckbox");
   2213  return Promise.all([onDispatch, scopesLoaded]);
   2214 }
   2215 
   2216 async function waitForPausedInOriginalFileAndToggleMapScopes(
   2217  dbg,
   2218  expectedSelectedSource = null
   2219 ) {
   2220  // Original variable mapping is not switched on, so do not wait for any loaded scopes
   2221  await waitForPaused(dbg, expectedSelectedSource, {
   2222    shouldWaitForLoadedScopes: false,
   2223  });
   2224  await toggleMapScopes(dbg);
   2225 }
   2226 
   2227 function toggleExpressions(dbg) {
   2228  return findElement(dbg, "expressionsHeader").click();
   2229 }
   2230 
   2231 function toggleScopes(dbg) {
   2232  return findElement(dbg, "scopesHeader").click();
   2233 }
   2234 
   2235 function toggleExpressionNode(dbg, index) {
   2236  return toggleObjectInspectorNode(findElement(dbg, "expressionNode", index));
   2237 }
   2238 
   2239 function toggleScopeNode(dbg, index) {
   2240  return toggleObjectInspectorNode(findElement(dbg, "scopeNode", index));
   2241 }
   2242 
   2243 function rightClickScopeNode(dbg, index) {
   2244  rightClickObjectInspectorNode(dbg, findElement(dbg, "scopeNode", index));
   2245 }
   2246 
   2247 function getScopeNodeLabel(dbg, index) {
   2248  return findElement(dbg, "scopeNode", index).innerText;
   2249 }
   2250 
   2251 function getScopeNodeValue(dbg, index) {
   2252  return findElement(dbg, "scopeValue", index).innerText;
   2253 }
   2254 
   2255 function toggleObjectInspectorNode(node) {
   2256  const objectInspector = node.closest(".object-inspector");
   2257  const properties = objectInspector.querySelectorAll(".node").length;
   2258 
   2259  info(`Toggle node ${node.innerText}`);
   2260  node.click();
   2261 
   2262  info(`Waiting for object inspector properties update`);
   2263  return waitUntil(
   2264    () => objectInspector.querySelectorAll(".node").length !== properties
   2265  );
   2266 }
   2267 
   2268 function rightClickObjectInspectorNode(dbg, node) {
   2269  const objectInspector = node.closest(".object-inspector");
   2270  const properties = objectInspector.querySelectorAll(".node").length;
   2271 
   2272  info(`Right clicking node ${node.innerText}`);
   2273  rightClickEl(dbg, node);
   2274 
   2275  info(`Waiting for object inspector properties update`);
   2276  return waitUntil(
   2277    () => objectInspector.querySelectorAll(".node").length !== properties
   2278  );
   2279 }
   2280 
   2281 /*******************************************
   2282 * Utilities for handling codemirror
   2283 ******************************************/
   2284 
   2285 // Gets the current source editor for CM6 tests
   2286 function getCMEditor(dbg) {
   2287  return dbg.win.codeMirrorSourceEditorTestInstance;
   2288 }
   2289 
   2290 function wasmOffsetToLine(dbg, offset) {
   2291  return getCMEditor(dbg).wasmOffsetToLine(offset) + 1;
   2292 }
   2293 
   2294 // Gets the number of lines in the editor
   2295 function getLineCount(dbg) {
   2296  return getCMEditor(dbg).getLineCount();
   2297 }
   2298 
   2299 /**
   2300 * Wait for CodeMirror to start searching
   2301 */
   2302 function waitForSearchState(dbg) {
   2303  return waitFor(() => getCMEditor(dbg).isSearchStateReady());
   2304 }
   2305 
   2306 /**
   2307 * Wait for the document of the main debugger editor codemirror instance
   2308 * to completely load (for CM6 only)
   2309 */
   2310 function waitForDocumentLoadComplete(dbg) {
   2311  return waitFor(() => getCMEditor(dbg).codeMirror.isDocumentLoadComplete);
   2312 }
   2313 
   2314 /**
   2315 * Wait for the document of the conditional/log point panel's codemirror instance
   2316 * to completely load (for CM6 only)
   2317 */
   2318 function waitForInPanelDocumentLoadComplete(dbg, panelName) {
   2319  return waitFor(
   2320    () => getCodeMirrorInstance(dbg, panelName).isDocumentLoadComplete
   2321  );
   2322 }
   2323 
   2324 /**
   2325 * Gets the content for the editor as a string. it uses the
   2326 * newline character to separate lines.
   2327 */
   2328 function getEditorContent(dbg) {
   2329  return getCMEditor(dbg).getEditorContent();
   2330 }
   2331 
   2332 /**
   2333 * Retrieve the codemirror instance for the provided debugger instance.
   2334 * Optionally provide a panel name such as "logPanelInput" or
   2335 * "conditionalPanelInput" to retrieve the codemirror instances specific to
   2336 * those panels.
   2337 *
   2338 * @param {object} dbg
   2339 * @param {string} panelName
   2340 * @returns {CodeMirror}
   2341 *     The codemirror instance corresponding to the provided debugger and panel name.
   2342 */
   2343 function getCodeMirrorInstance(dbg, panelName = null) {
   2344  if (panelName !== null) {
   2345    const panel = findElement(dbg, panelName);
   2346    return dbg.win.codeMirrorSourceEditorTestInstance.CodeMirror.findFromDOM(
   2347      panel
   2348    );
   2349  }
   2350  return dbg.win.codeMirrorSourceEditorTestInstance.codeMirror;
   2351 }
   2352 
   2353 async function waitForCursorPosition(dbg, expectedLine) {
   2354  return waitFor(() => {
   2355    const cursorPosition = findElementWithSelector(dbg, ".cursor-position");
   2356    if (!cursorPosition) {
   2357      return false;
   2358    }
   2359    const { innerText } = cursorPosition;
   2360    // Cursor position text has the following shape: (L, C)
   2361    // where L is the line number, and C the column number
   2362    const line = innerText.substring(1, innerText.indexOf(","));
   2363    return parseInt(line, 10) == expectedLine;
   2364  });
   2365 }
   2366 
   2367 /**
   2368 * Set the cursor  at a specific location in the editor
   2369 *
   2370 * @param {*} dbg
   2371 * @param {number} line
   2372 * @param {number} column
   2373 * @returns {Promise}
   2374 */
   2375 async function setEditorCursorAt(dbg, line, column) {
   2376  const cursorSet = waitForCursorPosition(dbg, line);
   2377  await getCMEditor(dbg).setCursorAt(line, column);
   2378  return cursorSet;
   2379 }
   2380 
   2381 /**
   2382 * Scrolls a specific line and column into view in the editor
   2383 *
   2384 * @param {*} dbg
   2385 * @param {number} line
   2386 * @param {number} column
   2387 * @param {string | null} yAlign
   2388 * @returns
   2389 */
   2390 async function scrollEditorIntoView(dbg, line, column, yAlign) {
   2391  const onScrolled = waitForScrolling(dbg);
   2392  getCMEditor(dbg).scrollTo(line + 1, column, yAlign);
   2393  // Ensure the line is visible with margin because the bar at the bottom of
   2394  // the editor overlaps into what the editor thinks is its own space, blocking
   2395  // the click event below.
   2396  return onScrolled;
   2397 }
   2398 
   2399 /**
   2400 * Wrapper around source editor api to check if a scrolled position is visible
   2401 *
   2402 * @param {*} dbg
   2403 * @param {number} line 1-based
   2404 * @param {number} column
   2405 * @returns
   2406 */
   2407 function isScrolledPositionVisible(dbg, line, column = 0) {
   2408  // CodeMirror 6 uses 1-based lines.
   2409  return getCMEditor(dbg).isPositionVisible(line, column);
   2410 }
   2411 
   2412 function setSelection(dbg, startLine, endLine) {
   2413  getCMEditor(dbg).setSelectionAt(
   2414    { line: startLine, column: 0 },
   2415    { line: endLine, column: 0 }
   2416  );
   2417 }
   2418 
   2419 function getSearchQuery(dbg) {
   2420  return getCMEditor(dbg).getSearchQuery();
   2421 }
   2422 
   2423 function getSearchSelection(dbg) {
   2424  return getCMEditor(dbg).getSearchSelection();
   2425 }
   2426 
   2427 // Gets the mode used for the file
   2428 function getEditorFileMode(dbg) {
   2429  return getCMEditor(dbg).getEditorFileMode();
   2430 }
   2431 
   2432 function getCoordsFromPosition(dbg, line, ch) {
   2433  return getCMEditor(dbg).getCoords(line, ch);
   2434 }
   2435 
   2436 async function getTokenFromPosition(dbg, { line, column = 0 }) {
   2437  info(`Get token at ${line}:${column}`);
   2438  await scrollEditorIntoView(dbg, line, column);
   2439  return getCMEditor(dbg).getElementAtPos(line, column);
   2440 }
   2441 /**
   2442 * Waits for the currently triggered scroll to complete
   2443 *
   2444 * @param {*} dbg
   2445 * @param {object} options
   2446 * @param {boolean} options.useTimeoutFallback - defaults to true. When set to false
   2447 *                                               a scroll must happen for the wait for scrolling to complete
   2448 * @returns
   2449 */
   2450 async function waitForScrolling(dbg, { useTimeoutFallback = true } = {}) {
   2451  return new Promise(resolve => {
   2452    const editor = getCMEditor(dbg);
   2453    editor.once("cm-editor-scrolled", resolve);
   2454    if (useTimeoutFallback) {
   2455      setTimeout(resolve, 500);
   2456    }
   2457  });
   2458 }
   2459 
   2460 async function clickAtPos(dbg, pos) {
   2461  const tokenEl = await getTokenFromPosition(dbg, pos);
   2462 
   2463  if (!tokenEl) {
   2464    return;
   2465  }
   2466 
   2467  const { top, left } = tokenEl.getBoundingClientRect();
   2468  info(
   2469    `Clicking on token ${tokenEl.innerText} in line ${tokenEl.parentNode.innerText}`
   2470  );
   2471  // TODO: Unify the usage for CM6 and CM5 Bug 1919694
   2472  EventUtils.synthesizeMouseAtCenter(tokenEl, {}, dbg.win);
   2473 }
   2474 
   2475 async function rightClickAtPos(dbg, pos) {
   2476  const el = await getTokenFromPosition(dbg, pos);
   2477  if (!el) {
   2478    return;
   2479  }
   2480  // In CM6 when clicking in the editor an extra click is needed
   2481  // TODO: Investiage and remove Bug 1919693
   2482  EventUtils.synthesizeMouseAtCenter(el, {}, dbg.win);
   2483  rightClickEl(dbg, el);
   2484 }
   2485 
   2486 async function hoverAtPos(dbg, pos) {
   2487  const tokenEl = await getTokenFromPosition(dbg, pos);
   2488 
   2489  if (!tokenEl) {
   2490    return;
   2491  }
   2492 
   2493  hoverToken(tokenEl);
   2494 }
   2495 
   2496 function hoverToken(tokenEl) {
   2497  info(`Hovering on token <${tokenEl.innerText}>`);
   2498 
   2499  // We can't use synthesizeMouse(AtCenter) as it's using the element bounding client rect.
   2500  // But here, we might have a token that wraps on multiple line and the center of the
   2501  // bounding client rect won't actually hover the token.
   2502  // +───────────────────────+
   2503  // │      myLongVariableNa│
   2504  // │me         +          │
   2505  // +───────────────────────+
   2506 
   2507  // Instead, we need to get the first quad.
   2508  const { p1, p2, p3 } = tokenEl.getBoxQuads()[0];
   2509  const x = p1.x + (p2.x - p1.x) / 2;
   2510  const y = p1.y + (p3.y - p1.y) / 2;
   2511 
   2512  // This first event helps utils/editor/tokens.js to receive the right mouseover event
   2513  EventUtils.synthesizeMouseAtPoint(
   2514    x,
   2515    y,
   2516    {
   2517      type: "mouseover",
   2518    },
   2519    tokenEl.ownerGlobal
   2520  );
   2521 
   2522  // This second event helps Popover to have :hover pseudoclass set on the token element
   2523  EventUtils.synthesizeMouseAtPoint(
   2524    x,
   2525    y,
   2526    {
   2527      type: "mousemove",
   2528    },
   2529    tokenEl.ownerGlobal
   2530  );
   2531 }
   2532 
   2533 /**
   2534 * Helper to close a variable preview popup.
   2535 *
   2536 * @param {object} dbg
   2537 * @param {DOM Element} tokenEl
   2538 *        The DOM element on which we hovered to display the popup.
   2539 * @param {string} previewType
   2540 *        Based on the actual JS value being hovered we may have two different kinds
   2541 *        of popups: popup (for js objects) or previewPopup (for primitives)
   2542 */
   2543 async function closePreviewForToken(
   2544  dbg,
   2545  tokenEl,
   2546  previewType = "previewPopup"
   2547 ) {
   2548  ok(
   2549    findElement(dbg, previewType),
   2550    "A preview was opened before trying to close it"
   2551  );
   2552 
   2553  // Force "mousing out" from all elements.
   2554  //
   2555  // This helps utils/editor/tokens.js to receive the right mouseleave event.
   2556  // This is super important as it will then allow re-emitting a tokenenter event if you try to re-preview the same token!
   2557  // We can't use synthesizeMouse(AtCenter) as it's using the element bounding client rect.
   2558  // But here, we might have a token that wraps on multiple line and the center of the
   2559  // bounding client rect won't actually hover the token.
   2560  // +───────────────────────+
   2561  // │      myLongVariableNa│
   2562  // │me         +          │
   2563  // +───────────────────────+
   2564 
   2565  // Instead, we need to get the first quad.
   2566  const { p1, p2, p3 } = tokenEl.getBoxQuads()[0];
   2567  const x = p1.x + (p2.x - p1.x) / 2;
   2568  const y = p1.y + (p3.y - p1.y) / 2;
   2569  EventUtils.synthesizeMouseAtPoint(
   2570    tokenEl,
   2571    x,
   2572    y,
   2573    {
   2574      type: "mouseout",
   2575    },
   2576    tokenEl.ownerGlobal
   2577  );
   2578 
   2579  // This second event helps Popover to have :hover pseudoclass removed on the token element
   2580  //
   2581  // For some unexplained reason, the precise element onto which we emit mousemove is actually important.
   2582  // Emitting it on documentElement, or random other element within CodeMirror would cause
   2583  // a "mousemove" event to be emitted on preview-popup element instead and wouldn't cause :hover
   2584  // pseudoclass to be dropped.
   2585  const element = tokenEl.ownerDocument.querySelector(
   2586    ".debugger-settings-menu-button"
   2587  );
   2588  EventUtils.synthesizeMouseAtCenter(
   2589    element,
   2590    {
   2591      type: "mousemove",
   2592    },
   2593    element.ownerGlobal
   2594  );
   2595 
   2596  info(`Waiting for preview to be closed (preview type=${previewType})`);
   2597  await waitUntil(() => findElement(dbg, previewType) == null);
   2598  info("Preview closed");
   2599 }
   2600 
   2601 /**
   2602 * Hover at a position until we see a preview element (popup, tooltip) appear.
   2603 * ⚠️ Note that this is using CodeMirror method to retrieve the token element
   2604 * and that could be subject to CodeMirror bugs / outdated internal state
   2605 *
   2606 * @param {Debugger} dbg
   2607 * @param {Integer} line: The line we want to hover over
   2608 * @param {Integer} column: The column we want to hover over
   2609 * @param {string} elementName: "Selector" string that will be passed to waitForElement,
   2610 *                              describing the element that should be displayed on hover.
   2611 * @returns Promise<{element, tokenEl}>
   2612 *          element is the DOM element matching the passed elementName
   2613 *          tokenEl is the DOM element for the token we hovered
   2614 */
   2615 async function tryHovering(dbg, line, column, elementName) {
   2616  ok(
   2617    !findElement(dbg, elementName),
   2618    "The expected preview element on hover should not exist beforehand"
   2619  );
   2620  // Wait for all the updates to the document to complete to make all
   2621  // token elements have been rendered
   2622  await waitForDocumentLoadComplete(dbg);
   2623  const tokenEl = await getTokenFromPosition(dbg, { line, column });
   2624  return tryHoverToken(dbg, tokenEl, elementName);
   2625 }
   2626 
   2627 /**
   2628 * Retrieve the token element matching `expression` at line `line` and hover it.
   2629 * This is retrieving the token from the DOM, contrary to `tryHovering`, which calls
   2630 * CodeMirror internal method for this (and which might suffer from bugs / outdated internal state)
   2631 *
   2632 * @param {Debugger} dbg
   2633 * @param {string} expression: The text of the token we want to hover
   2634 * @param {Integer} line: The line the token should be at
   2635 * @param {Integer} column: The column the token should be at
   2636 * @param {string} elementName: "Selector" string that will be passed to waitForElement,
   2637 *                              describing the element that should be displayed on hover.
   2638 * @returns Promise<{element, tokenEl}>
   2639 *          element is the DOM element matching the passed elementName
   2640 *          tokenEl is the DOM element for the token we hovered
   2641 */
   2642 async function tryHoverTokenAtLine(dbg, expression, line, column, elementName) {
   2643  info("Scroll codeMirror to make the token visible");
   2644  await scrollEditorIntoView(dbg, line, 0);
   2645  // Wait for all the updates to the document to complete to make all
   2646  // token elements have been rendered
   2647  await waitForDocumentLoadComplete(dbg);
   2648  // Lookup for the token matching the passed expression
   2649  const tokenEl = await getTokenElAtLine(dbg, expression, line, column);
   2650  if (!tokenEl) {
   2651    throw new Error(
   2652      `Couldn't find token <${expression}> on ${line}:${column}\n`
   2653    );
   2654  }
   2655 
   2656  ok(true, `Found token <${expression}> on ${line}:${column}`);
   2657 
   2658  return tryHoverToken(dbg, tokenEl, elementName);
   2659 }
   2660 
   2661 async function tryHoverToken(dbg, tokenEl, elementName) {
   2662  hoverToken(tokenEl);
   2663 
   2664  // Wait for the preview element to be created
   2665  const element = await waitForElement(dbg, elementName);
   2666  return { element, tokenEl };
   2667 }
   2668 
   2669 /**
   2670 * Retrieve the token element matching `expression` at line `line`, from the DOM.
   2671 *
   2672 * @param {Debugger} dbg
   2673 * @param {string} expression: The text of the token we want to hover
   2674 * @param {Integer} line: The line the token should be at
   2675 * @param {Integer} column: The column the token should be at
   2676 * @returns {Element} the token element, or null if not found
   2677 */
   2678 async function getTokenElAtLine(dbg, expression, line, column = 0) {
   2679  info(`Search for <${expression}> token on ${line}:${column}`);
   2680  // Get the related editor line
   2681  const tokenParent = getCMEditor(dbg).getElementAtLine(line);
   2682 
   2683  // Lookup for the token matching the passed expression
   2684  const tokenElements = [...tokenParent.childNodes];
   2685  let currentColumn = 1;
   2686  return tokenElements.find(el => {
   2687    const childText = el.textContent;
   2688    currentColumn += childText.length;
   2689 
   2690    // Only consider elements that are after the passed column
   2691    if (currentColumn < column) {
   2692      return false;
   2693    }
   2694    return childText == expression;
   2695  });
   2696 }
   2697 
   2698 /**
   2699 * Wait for a few ms and assert that a tooltip preview was not displayed.
   2700 *
   2701 * @param {*} dbg
   2702 */
   2703 async function assertNoTooltip(dbg) {
   2704  await wait(200);
   2705  const el = findElement(dbg, "previewPopup");
   2706  is(el, null, "Tooltip should not exist");
   2707 }
   2708 
   2709 /**
   2710 * Hovers and asserts tooltip previews with simple text expressions (i.e numbers and strings)
   2711 *
   2712 * @param {*} dbg
   2713 * @param {number} line
   2714 * @param {number} column
   2715 * @param {object} options
   2716 * @param {string}  options.result - Expected text shown in the preview
   2717 * @param {string}  options.expression - The expression hovered over
   2718 * @param {boolean} options.doNotClose - Set to true to not close the tooltip
   2719 */
   2720 async function assertPreviewTextValue(
   2721  dbg,
   2722  line,
   2723  column,
   2724  { result, expression, doNotClose = false }
   2725 ) {
   2726  // CodeMirror refreshes after inline previews are displayed, so wait until they're rendered.
   2727  await waitForInlinePreviews(dbg);
   2728 
   2729  const { element: previewEl, tokenEl } = await tryHoverTokenAtLine(
   2730    dbg,
   2731    expression,
   2732    line,
   2733    column,
   2734    "previewPopup"
   2735  );
   2736 
   2737  ok(
   2738    previewEl.innerText.includes(result),
   2739    "Popup preview text shown to user. Got: " +
   2740      previewEl.innerText +
   2741      " Expected: " +
   2742      result
   2743  );
   2744 
   2745  if (!doNotClose) {
   2746    await closePreviewForToken(dbg, tokenEl);
   2747  }
   2748 }
   2749 
   2750 /**
   2751 * Asserts multiple previews
   2752 *
   2753 * @param {*} dbg
   2754 * @param {Array} previews
   2755 */
   2756 async function assertPreviews(dbg, previews) {
   2757  // Move the cursor to the top left corner to have a clean state
   2758  EventUtils.synthesizeMouse(
   2759    findElement(dbg, "codeMirror"),
   2760    0,
   2761    0,
   2762    {
   2763      type: "mousemove",
   2764    },
   2765    dbg.win
   2766  );
   2767 
   2768  // CodeMirror refreshes after inline previews are displayed, so wait until they're rendered.
   2769  await waitForInlinePreviews(dbg);
   2770 
   2771  for (const { line, column, expression, result, header, fields } of previews) {
   2772    info(" # Assert preview on " + line + ":" + column);
   2773 
   2774    if (result) {
   2775      await assertPreviewTextValue(dbg, line, column, {
   2776        expression,
   2777        result,
   2778      });
   2779    }
   2780 
   2781    if (fields) {
   2782      const { element: popupEl, tokenEl } = expression
   2783        ? await tryHoverTokenAtLine(dbg, expression, line, column, "popup")
   2784        : await tryHovering(dbg, line, column, "popup");
   2785 
   2786      info("Wait for child nodes to load");
   2787      await waitUntil(
   2788        () => popupEl.querySelectorAll(".preview-popup .node").length > 1
   2789      );
   2790      ok(true, "child nodes loaded");
   2791 
   2792      const oiNodes = Array.from(
   2793        popupEl.querySelectorAll(".preview-popup .node")
   2794      );
   2795 
   2796      if (header) {
   2797        is(
   2798          oiNodes[0].querySelector(".objectBox").textContent,
   2799          header,
   2800          "popup has expected value"
   2801        );
   2802      }
   2803 
   2804      for (const [field, value] of fields) {
   2805        const node = oiNodes.find(
   2806          oiNode => oiNode.querySelector(".object-label")?.textContent === field
   2807        );
   2808        if (!node) {
   2809          ok(false, `The "${field}" property is not displayed in the popup`);
   2810        } else {
   2811          is(
   2812            node.querySelector(".object-label").textContent,
   2813            field,
   2814            `The "${field}" property is displayed in the popup`
   2815          );
   2816          if (value !== undefined) {
   2817            is(
   2818              node.querySelector(".objectBox").textContent,
   2819              value,
   2820              `The "${field}" property has the expected value`
   2821            );
   2822          }
   2823        }
   2824      }
   2825 
   2826      await closePreviewForToken(dbg, tokenEl, "popup");
   2827    }
   2828  }
   2829 }
   2830 
   2831 /**
   2832 * Asserts the inline expression preview value
   2833 *
   2834 * @param {*} dbg
   2835 * @param {number} line
   2836 * @param {number} column
   2837 * @param {object} options
   2838 * @param {string}  options.result - Expected text shown in the preview
   2839 * @param {Array}  options.fields - The expected stacktrace information
   2840 */
   2841 async function assertInlineExceptionPreview(
   2842  dbg,
   2843  line,
   2844  column,
   2845  { result, fields }
   2846 ) {
   2847  info(" # Assert preview on " + line + ":" + column);
   2848  const { element: popupEl, tokenEl } = await tryHovering(
   2849    dbg,
   2850    line,
   2851    column,
   2852    "previewPopup"
   2853  );
   2854 
   2855  info("Wait for top level node to expand and child nodes to load");
   2856  await waitForElementWithSelector(
   2857    dbg,
   2858    ".exception-popup .exception-message .dbg-img-arrow.expanded"
   2859  );
   2860 
   2861  is(
   2862    popupEl.querySelector(".preview-popup .exception-message .objectBox")
   2863      .textContent,
   2864    result,
   2865    "The correct result is not displayed in the popup"
   2866  );
   2867 
   2868  await waitFor(() =>
   2869    popupEl.querySelectorAll(".preview-popup .exception-stacktrace .frame")
   2870  );
   2871  const stackFrameNodes = Array.from(
   2872    popupEl.querySelectorAll(".preview-popup .exception-stacktrace .frame")
   2873  );
   2874 
   2875  for (const [field, value] of fields) {
   2876    const node = stackFrameNodes.find(
   2877      frameNode => frameNode.querySelector(".title")?.textContent === field
   2878    );
   2879    if (!node) {
   2880      ok(false, `The "${field}" property is not displayed in the popup`);
   2881    } else {
   2882      is(
   2883        node.querySelector(".location").textContent,
   2884        value,
   2885        `The "${field}" property has the expected value`
   2886      );
   2887    }
   2888  }
   2889 
   2890  await closePreviewForToken(dbg, tokenEl, "previewPopup");
   2891 }
   2892 
   2893 /**
   2894 * Wait until a preview popup containing the given result is shown
   2895 *
   2896 * @param {*} dbg
   2897 * @param {string} result
   2898 */
   2899 async function waitForPreviewWithResult(dbg, result) {
   2900  info(`Wait for preview popup with result ${result}`);
   2901  await waitUntil(async () => {
   2902    const previewEl = await waitForElement(dbg, "previewPopup");
   2903    return previewEl.innerText.includes(result);
   2904  });
   2905 }
   2906 
   2907 /**
   2908 * Expand or collapse a node in the preview popup
   2909 *
   2910 * @param {*} dbg
   2911 * @param {number} index
   2912 */
   2913 async function toggleExpanded(dbg, index) {
   2914  let initialNodesLength;
   2915  await waitFor(() => {
   2916    const nodes = findElement(dbg, "previewPopup")?.querySelectorAll(".node");
   2917    if (nodes?.length > index) {
   2918      initialNodesLength = nodes.length;
   2919      nodes[index].querySelector(".theme-twisty").click();
   2920      return true;
   2921    }
   2922    return false;
   2923  });
   2924  await waitFor(
   2925    () =>
   2926      findElement(dbg, "previewPopup").querySelectorAll(".node").length !==
   2927      initialNodesLength
   2928  );
   2929 }
   2930 
   2931 async function waitForBreakableLine(dbg, source, lineNumber) {
   2932  await waitForState(
   2933    dbg,
   2934    () => {
   2935      const currentSource = findSource(dbg, source);
   2936 
   2937      const breakableLines =
   2938        currentSource && dbg.selectors.getBreakableLines(currentSource.id);
   2939 
   2940      return breakableLines && breakableLines.includes(lineNumber);
   2941    },
   2942    `waiting for breakable line ${lineNumber}`
   2943  );
   2944 }
   2945 
   2946 async function waitForSourceTreeThreadsCount(dbg, i) {
   2947  info(`Waiting for ${i} threads in the source tree`);
   2948  await waitUntil(() => {
   2949    return findAllElements(dbg, "sourceTreeThreads").length === i;
   2950  });
   2951 }
   2952 
   2953 function getDisplayedSourceTree(dbg) {
   2954  return [...findAllElements(dbg, "sourceNodes")];
   2955 }
   2956 
   2957 function getDisplayedSourceElements(dbg) {
   2958  return [...findAllElements(dbg, "sourceTreeFiles")];
   2959 }
   2960 
   2961 function getDisplayedSources(dbg) {
   2962  return getDisplayedSourceElements(dbg).map(e => {
   2963    // Replace some non visible space characters that prevents Array.includes from working correctly
   2964    return e.textContent.trim().replace(/^[\s\u200b]*/g, "");
   2965  });
   2966 }
   2967 
   2968 /**
   2969 * Wait for a single source to be visible in the Source Tree.
   2970 */
   2971 async function waitForSourceInSourceTree(dbg, fileName) {
   2972  return waitFor(
   2973    async () => {
   2974      await expandSourceTree(dbg);
   2975 
   2976      return getDisplayedSourceElements(dbg).find(e => {
   2977        // Replace some non visible space characters that prevents Array.includes from working correctly
   2978        return e.textContent.trim().replace(/^[\s\u200b]*/g, "") == fileName;
   2979      });
   2980    },
   2981    null,
   2982    100,
   2983    50
   2984  );
   2985 }
   2986 
   2987 /**
   2988 * Wait for a precise list of sources to be shown in the Source Tree.
   2989 * No more, no less than the list.
   2990 */
   2991 async function waitForSourcesInSourceTree(
   2992  dbg,
   2993  sources,
   2994  { noExpand = false } = {}
   2995 ) {
   2996  info(`waiting for ${sources.length} files in the source tree`);
   2997  try {
   2998    // Use custom timeout and retry count for waitFor as the test method is slow to resolve
   2999    // and default value makes the timeout unecessarily long
   3000    await waitFor(
   3001      async () => {
   3002        if (!noExpand) {
   3003          await expandSourceTree(dbg);
   3004        }
   3005        const displayedSources = getDisplayedSources(dbg);
   3006        return (
   3007          displayedSources.length == sources.length &&
   3008          sources.every(source => displayedSources.includes(source))
   3009        );
   3010      },
   3011      null,
   3012      100,
   3013      50
   3014    );
   3015  } catch (e) {
   3016    // Craft a custom error message to help understand what's wrong with the Source Tree content
   3017    const displayedSources = getDisplayedSources(dbg);
   3018    let msg = "Invalid Source Tree Content.\n";
   3019    const missingElements = [];
   3020    for (const source of sources) {
   3021      const idx = displayedSources.indexOf(source);
   3022      if (idx != -1) {
   3023        displayedSources.splice(idx, 1);
   3024      } else {
   3025        missingElements.push(source);
   3026      }
   3027    }
   3028    if (missingElements.length) {
   3029      msg += "Missing elements: " + missingElements.join(", ") + "\n";
   3030    }
   3031    if (displayedSources.length) {
   3032      msg += "Unexpected elements: " + displayedSources.join(", ");
   3033    }
   3034    throw new Error(msg);
   3035  }
   3036 }
   3037 
   3038 async function waitForNodeToGainFocus(dbg, index) {
   3039  info(`Waiting for source node #${index} to be focused`);
   3040  await waitUntil(() => {
   3041    const element = findElement(dbg, "sourceNode", index);
   3042 
   3043    if (element) {
   3044      return element.classList.contains("focused");
   3045    }
   3046 
   3047    return false;
   3048  });
   3049 }
   3050 
   3051 async function assertNodeIsFocused(dbg, index) {
   3052  await waitForNodeToGainFocus(dbg, index);
   3053  const node = findElement(dbg, "sourceNode", index);
   3054  ok(node.classList.contains("focused"), `node ${index} is focused`);
   3055 }
   3056 
   3057 /**
   3058 * Asserts that the debugger is paused and the debugger tab is
   3059 * highlighted.
   3060 *
   3061 * @param {*} toolbox
   3062 * @returns
   3063 */
   3064 async function assertDebuggerIsHighlightedAndPaused(toolbox) {
   3065  info("Wait for the debugger to be automatically selected on pause");
   3066  await waitUntil(() => toolbox.currentToolId == "jsdebugger");
   3067  ok(true, "Debugger selected");
   3068 
   3069  // Wait for the debugger to finish loading.
   3070  await toolbox.getPanelWhenReady("jsdebugger");
   3071 
   3072  // And to be fully paused
   3073  const dbg = createDebuggerContext(toolbox);
   3074  await waitForPaused(dbg);
   3075 
   3076  ok(toolbox.isHighlighted("jsdebugger"), "Debugger is highlighted");
   3077 
   3078  return dbg;
   3079 }
   3080 
   3081 async function addExpression(dbg, input) {
   3082  info("Adding an expression");
   3083 
   3084  const plusIcon = findElement(dbg, "expressionPlus");
   3085  if (plusIcon) {
   3086    plusIcon.click();
   3087  }
   3088  findElement(dbg, "expressionInput").focus();
   3089  type(dbg, input);
   3090  const evaluated = waitForDispatch(dbg.store, "EVALUATE_EXPRESSION");
   3091  const clearAutocomplete = waitForDispatch(dbg.store, "CLEAR_AUTOCOMPLETE");
   3092  pressKey(dbg, "Enter");
   3093  await evaluated;
   3094  await clearAutocomplete;
   3095 }
   3096 
   3097 async function editExpression(dbg, input) {
   3098  info("Updating the expression");
   3099  dblClickElement(dbg, "expressionNode", 1);
   3100  // Position cursor reliably at the end of the text.
   3101  pressKey(dbg, "End");
   3102  type(dbg, input);
   3103  const evaluated = waitForDispatch(dbg.store, "EVALUATE_EXPRESSIONS");
   3104  pressKey(dbg, "Enter");
   3105  await evaluated;
   3106 }
   3107 
   3108 /**
   3109 * Get the text representation of a watch expression label given its position in the panel
   3110 *
   3111 * @param {object} dbg
   3112 * @param {number} index: Position in the panel of the expression we want the label of
   3113 * @returns {string}
   3114 */
   3115 function getWatchExpressionLabel(dbg, index) {
   3116  return findElement(dbg, "expressionNode", index).innerText;
   3117 }
   3118 
   3119 /**
   3120 * Get the text representation of a watch expression value given its position in the panel
   3121 *
   3122 * @param {object} dbg
   3123 * @param {number} index: Position in the panel of the expression we want the value of
   3124 * @returns {string}
   3125 */
   3126 function getWatchExpressionValue(dbg, index) {
   3127  return findElement(dbg, "expressionValue", index).innerText;
   3128 }
   3129 
   3130 // Return a promise with a reference to jsterm, opening the split
   3131 // console if necessary.  This cleans up the split console pref so
   3132 // it won't pollute other tests.
   3133 async function getDebuggerSplitConsole(dbg) {
   3134  let { toolbox, win } = dbg;
   3135 
   3136  if (!win) {
   3137    win = toolbox.win;
   3138  }
   3139 
   3140  if (!toolbox.splitConsole) {
   3141    pressKey(dbg, "Escape");
   3142  }
   3143 
   3144  await toolbox.openSplitConsole();
   3145  return toolbox.getPanel("webconsole");
   3146 }
   3147 
   3148 // Return a promise that resolves with the result of a thread evaluating a
   3149 // string in the topmost frame.
   3150 async function evaluateInTopFrame(dbg, text) {
   3151  const threadFront = dbg.toolbox.target.threadFront;
   3152  const { frames } = await threadFront.getFrames(0, 1);
   3153  ok(frames.length == 1, "Got one frame");
   3154  const response = await dbg.commands.scriptCommand.execute(text, {
   3155    frameActor: frames[0].actorID,
   3156  });
   3157  return response.result.type == "undefined" ? undefined : response.result;
   3158 }
   3159 
   3160 // Return a promise that resolves when a thread evaluates a string in the
   3161 // topmost frame, ensuring the result matches the expected value.
   3162 async function checkEvaluateInTopFrame(dbg, text, expected) {
   3163  const rval = await evaluateInTopFrame(dbg, text);
   3164  ok(rval == expected, `Eval returned ${expected}`);
   3165 }
   3166 
   3167 async function findConsoleMessage({ toolbox }, query) {
   3168  const [message] = await findConsoleMessages(toolbox, query);
   3169  const value = message.querySelector(".message-body").innerText;
   3170  // There are console messages which might not have a link e.g Error messages
   3171  const link = message.querySelector(".frame-link-source")?.innerText;
   3172  return { value, link };
   3173 }
   3174 
   3175 async function hasConsoleMessage({ toolbox }, msg) {
   3176  return waitFor(async () => {
   3177    const messages = await findConsoleMessages(toolbox, msg);
   3178    return !!messages.length;
   3179  });
   3180 }
   3181 
   3182 function evaluateExpressionInConsole(
   3183  hud,
   3184  expression,
   3185  expectedClassName = "result"
   3186 ) {
   3187  const seenMessages = new Set(
   3188    JSON.parse(
   3189      hud.ui.outputNode
   3190        .querySelector("[data-visible-messages]")
   3191        .getAttribute("data-visible-messages")
   3192    )
   3193  );
   3194  const onResult = new Promise(res => {
   3195    const onNewMessage = messages => {
   3196      for (const message of messages) {
   3197        if (
   3198          message.node.classList.contains(expectedClassName) &&
   3199          !seenMessages.has(message.node.getAttribute("data-message-id"))
   3200        ) {
   3201          hud.ui.off("new-messages", onNewMessage);
   3202          res(message.node);
   3203        }
   3204      }
   3205    };
   3206    hud.ui.on("new-messages", onNewMessage);
   3207  });
   3208  hud.ui.wrapper.dispatchEvaluateExpression(expression);
   3209  return onResult;
   3210 }
   3211 
   3212 function waitForInspectorPanelChange(dbg) {
   3213  return dbg.toolbox.getPanelWhenReady("inspector");
   3214 }
   3215 
   3216 function getEagerEvaluationElement(hud) {
   3217  return hud.ui.outputNode.querySelector(".eager-evaluation-result");
   3218 }
   3219 
   3220 async function waitForEagerEvaluationResult(hud, text) {
   3221  info(`Waiting for eager evaluation result: ${text}`);
   3222  await waitUntil(() => {
   3223    const elem = getEagerEvaluationElement(hud);
   3224    if (elem) {
   3225      if (text instanceof RegExp) {
   3226        return text.test(elem.innerText);
   3227      }
   3228      return elem.innerText == text;
   3229    }
   3230    return false;
   3231  });
   3232  ok(true, `Got eager evaluation result ${text}`);
   3233 }
   3234 
   3235 function setInputValue(hud, value) {
   3236  const onValueSet = hud.jsterm.once("set-input-value");
   3237  hud.jsterm._setValue(value);
   3238  return onValueSet;
   3239 }
   3240 
   3241 function assertMenuItemChecked(menuItem, isChecked) {
   3242  is(
   3243    !!menuItem.getAttribute("aria-checked"),
   3244    isChecked,
   3245    `Item has expected state: ${isChecked ? "checked" : "unchecked"}`
   3246  );
   3247 }
   3248 
   3249 async function toggleDebuggerSettingsMenuItem(dbg, { className, isChecked }) {
   3250  const menuButton = findElementWithSelector(
   3251    dbg,
   3252    ".command-bar .debugger-settings-menu-button"
   3253  );
   3254  const { parent } = dbg.panel.panelWin;
   3255  const { document } = parent;
   3256 
   3257  menuButton.click();
   3258  // Waits for the debugger settings panel to appear.
   3259  await waitFor(() => {
   3260    const menuListEl = document.querySelector("#debugger-settings-menu-list");
   3261    // Lets check the offsetParent property to make sure the menu list is actually visible
   3262    // by its parents display property being no longer "none".
   3263    return menuListEl && menuListEl.offsetParent !== null;
   3264  });
   3265 
   3266  const menuItem = document.querySelector(className);
   3267 
   3268  assertMenuItemChecked(menuItem, isChecked);
   3269 
   3270  menuItem.click();
   3271 
   3272  // Waits for the debugger settings panel to disappear.
   3273  await waitFor(() => menuButton.getAttribute("aria-expanded") === "false");
   3274 }
   3275 
   3276 async function toggleSourcesTreeSettingsMenuItem(
   3277  dbg,
   3278  { className, isChecked }
   3279 ) {
   3280  const menuButton = findElementWithSelector(
   3281    dbg,
   3282    ".sources-list .debugger-settings-menu-button"
   3283  );
   3284  const { parent } = dbg.panel.panelWin;
   3285  const { document } = parent;
   3286 
   3287  menuButton.click();
   3288  // Waits for the debugger settings panel to appear.
   3289  await waitFor(() => {
   3290    const menuListEl = document.querySelector(
   3291      "#sources-tree-settings-menu-list"
   3292    );
   3293    // Lets check the offsetParent property to make sure the menu list is actually visible
   3294    // by its parents display property being no longer "none".
   3295    return menuListEl && menuListEl.offsetParent !== null;
   3296  });
   3297 
   3298  const menuItem = document.querySelector(className);
   3299 
   3300  assertMenuItemChecked(menuItem, isChecked);
   3301 
   3302  menuItem.click();
   3303 
   3304  // Waits for the debugger settings panel to disappear.
   3305  await waitFor(() => menuButton.getAttribute("aria-expanded") === "false");
   3306 }
   3307 
   3308 /**
   3309 * Click on the source map button in the editor's footer
   3310 * and wait for its context menu to be rendered before clicking
   3311 * on one menuitem of it.
   3312 *
   3313 * @param {object} dbg
   3314 * @param {string} className
   3315 *        The class name of the menuitem to click in the context menu.
   3316 */
   3317 async function clickOnSourceMapMenuItem(dbg, className) {
   3318  const menuButton = findElement(dbg, "sourceMapFooterButton");
   3319  const { parent } = dbg.panel.panelWin;
   3320  const { document } = parent;
   3321 
   3322  menuButton.click();
   3323  // Waits for the debugger settings panel to appear.
   3324  await waitFor(() => {
   3325    const menuListEl = document.querySelector("#debugger-source-map-list");
   3326    // Lets check the offsetParent property to make sure the menu list is actually visible
   3327    // by its parents display property being no longer "none".
   3328    return menuListEl && menuListEl.offsetParent !== null;
   3329  });
   3330 
   3331  const menuItem = document.querySelector(className);
   3332  menuItem.click();
   3333 }
   3334 
   3335 async function setLogPoint(dbg, index, value, showStacktrace = false) {
   3336  // Wait a bit for CM6 to complete any updates so the log panel
   3337  // does not lose focus after the it has been opened
   3338  await waitForDocumentLoadComplete(dbg);
   3339  rightClickElement(dbg, "gutterElement", index);
   3340  await waitForContextMenu(dbg);
   3341 
   3342  selectDebuggerContextMenuItem(
   3343    dbg,
   3344    `${selectors.addLogItem},${selectors.editLogItem}`
   3345  );
   3346  await waitForConditionalPanelFocus(dbg);
   3347 
   3348  const { document } = dbg.win;
   3349 
   3350  if (showStacktrace) {
   3351    const checkbox = document.querySelector("#showStacktrace");
   3352    checkbox.click();
   3353    ok(checkbox.checked, "Stacktrace checkbox is checked");
   3354  }
   3355 
   3356  if (value) {
   3357    const onBreakpointSet = waitForDispatch(dbg.store, "SET_BREAKPOINT");
   3358    await typeInPanel(dbg, value, true);
   3359    info("Wait for breakpoint set");
   3360    await onBreakpointSet;
   3361    ok(true, "breakpoint set");
   3362  }
   3363 }
   3364 
   3365 /**
   3366 * Opens the project search panel
   3367 *
   3368 * @param {object} dbg
   3369 * @return {boolean} The project search is open
   3370 */
   3371 function openProjectSearch(dbg) {
   3372  info("Opening the project search panel");
   3373  synthesizeKeyShortcut("CmdOrCtrl+Shift+F");
   3374  return waitForState(dbg, () => dbg.selectors.getActiveSearch() === "project");
   3375 }
   3376 
   3377 /**
   3378 * Starts a project search based on the specified search term
   3379 *
   3380 * @param {object} dbg
   3381 * @param {string} searchTerm - The test to search for
   3382 * @param {number} expectedResults - The expected no of results to wait for.
   3383 *                                   This is the number of file results and not the numer of matches in all files.
   3384 *                                   When falsy value is passed, expects no match.
   3385 * @return {Array} List of search results element nodes
   3386 */
   3387 async function doProjectSearch(dbg, searchTerm, expectedResults) {
   3388  await clearElement(dbg, "projectSearchSearchInput");
   3389  type(dbg, searchTerm);
   3390  pressKey(dbg, "Enter");
   3391  return waitForSearchResults(dbg, expectedResults);
   3392 }
   3393 
   3394 /**
   3395 * Waits for the search results node to render
   3396 *
   3397 * @param {object} dbg
   3398 * @param {number} expectedResults - The expected no of results to wait for
   3399 *                                   This is the number of file results and not the numer of matches in all files.
   3400 * @return (Array) List of search result element nodes
   3401 */
   3402 async function waitForSearchResults(dbg, expectedResults) {
   3403  if (expectedResults) {
   3404    info(`Waiting for ${expectedResults} project search results`);
   3405    await waitUntil(
   3406      () =>
   3407        findAllElements(dbg, "projectSearchFileResults").length ==
   3408        expectedResults
   3409    );
   3410  } else {
   3411    // If no results are expected, wait for the "no results" message to be displayed.
   3412    info("Wait for project search to complete with no results");
   3413    await waitUntil(() => {
   3414      const projectSearchResult = findElementWithSelector(
   3415        dbg,
   3416        ".no-result-msg"
   3417      );
   3418      return projectSearchResult
   3419        ? projectSearchResult.textContent ==
   3420            DEBUGGER_L10N.getStr("projectTextSearch.noResults")
   3421        : false;
   3422    });
   3423  }
   3424  return findAllElements(dbg, "projectSearchFileResults");
   3425 }
   3426 
   3427 /**
   3428 * Get the no of expanded search results
   3429 *
   3430 * @param {object} dbg
   3431 * @return {number} No of expanded results
   3432 */
   3433 function getExpandedResultsCount(dbg) {
   3434  return findAllElements(dbg, "projectSearchExpandedResults").length;
   3435 }
   3436 
   3437 // This module is also loaded for Browser Toolbox tests, within the browser toolbox process
   3438 // which doesn't contain mochitests resource://testing-common URL.
   3439 // This isn't important to allow rejections in the context of the browser toolbox tests.
   3440 const protocolHandler = Services.io
   3441  .getProtocolHandler("resource")
   3442  .QueryInterface(Ci.nsIResProtocolHandler);
   3443 if (protocolHandler.hasSubstitution("testing-common")) {
   3444  const { PromiseTestUtils } = ChromeUtils.importESModule(
   3445    "resource://testing-common/PromiseTestUtils.sys.mjs"
   3446  );
   3447  PromiseTestUtils.allowMatchingRejectionsGlobally(/Connection closed/);
   3448  this.PromiseTestUtils = PromiseTestUtils;
   3449 
   3450  // Debugger operations that are canceled because they were rendered obsolete by
   3451  // a navigation or pause/resume end up as uncaught rejections. These never
   3452  // indicate errors and are allowed in all debugger tests.
   3453  // All the following are related to context middleware throwing on obsolete async actions:
   3454  PromiseTestUtils.allowMatchingRejectionsGlobally(/DebuggerContextError/);
   3455 }
   3456 
   3457 /**
   3458 * Selects the specific black box context menu item
   3459 *
   3460 * @param {object} dbg
   3461 * @param {string} itemName
   3462 *                  The name of the context menu item.
   3463 */
   3464 async function selectBlackBoxContextMenuItem(dbg, itemName) {
   3465  let wait = null;
   3466  if (itemName == "blackbox-line" || itemName == "blackbox-lines") {
   3467    wait = Promise.any([
   3468      waitForDispatch(dbg.store, "BLACKBOX_SOURCE_RANGES"),
   3469      waitForDispatch(dbg.store, "UNBLACKBOX_SOURCE_RANGES"),
   3470    ]);
   3471  } else if (itemName == "blackbox") {
   3472    wait = Promise.any([
   3473      waitForDispatch(dbg.store, "BLACKBOX_WHOLE_SOURCES"),
   3474      waitForDispatch(dbg.store, "UNBLACKBOX_WHOLE_SOURCES"),
   3475    ]);
   3476  }
   3477 
   3478  info(`Select the ${itemName} context menu item`);
   3479  selectDebuggerContextMenuItem(dbg, `#node-menu-${itemName}`);
   3480  return wait;
   3481 }
   3482 
   3483 function openOutlinePanel(dbg, waitForOutlineList = true) {
   3484  info("Select the outline panel");
   3485  const outlineTab = findElementWithSelector(dbg, ".outline-tab a");
   3486  EventUtils.synthesizeMouseAtCenter(outlineTab, {}, outlineTab.ownerGlobal);
   3487 
   3488  if (!waitForOutlineList) {
   3489    return Promise.resolve();
   3490  }
   3491 
   3492  return waitForElementWithSelector(dbg, ".outline-list");
   3493 }
   3494 
   3495 // Test empty panel when source has not function or class symbols
   3496 // Test that anonymous functions do not show in the outline panel
   3497 function assertOutlineItems(dbg, expectedItems) {
   3498  const outlineItems = Array.from(
   3499    findAllElementsWithSelector(
   3500      dbg,
   3501      ".outline-list h2, .outline-list .outline-list__element"
   3502    )
   3503  );
   3504  SimpleTest.isDeeply(
   3505    outlineItems.map(i => i.innerText.trim()),
   3506    expectedItems,
   3507    "The expected items are displayed in the outline panel"
   3508  );
   3509 }
   3510 
   3511 async function checkAdditionalThreadCount(dbg, count) {
   3512  await waitForState(
   3513    dbg,
   3514    () => {
   3515      return dbg.selectors.getThreads().length == count;
   3516    },
   3517    "Have the expected number of additional threads"
   3518  );
   3519  ok(true, `Have ${count} threads`);
   3520 }
   3521 
   3522 /**
   3523 * Retrieve the text displayed as warning under the editor.
   3524 */
   3525 function findFooterNotificationMessage(dbg) {
   3526  return findElement(dbg, "editorNotificationFooter")?.innerText;
   3527 }
   3528 
   3529 /**
   3530 * Toggle a JavaScript Tracer settings via the toolbox toolbar button's context menu.
   3531 *
   3532 * @param {object} dbg
   3533 * @param {string} selector
   3534 *        Selector for the menu item of the settings defined in devtools/client/framework/definitions.js.
   3535 */
   3536 async function toggleJsTracerMenuItem(dbg, selector) {
   3537  const button = dbg.toolbox.doc.getElementById("command-button-jstracer");
   3538  EventUtils.synthesizeMouseAtCenter(
   3539    button,
   3540    { type: "contextmenu" },
   3541    dbg.toolbox.win
   3542  );
   3543  const popup = await waitForContextMenu(dbg);
   3544  const onHidden = BrowserTestUtils.waitForEvent(popup, "popuphidden");
   3545  selectDebuggerContextMenuItem(dbg, selector);
   3546  await onHidden;
   3547 }
   3548 
   3549 /**
   3550 * Asserts that the number of displayed inline previews, the contents of the inline previews and the lines
   3551 * that they are displayed on, are accurate
   3552 *
   3553 * @param {object} dbg
   3554 * @param {Array} expectedInlinePreviews
   3555 * @param {string} fnName
   3556 */
   3557 async function assertInlinePreviews(dbg, expectedInlinePreviews, fnName) {
   3558  // Accumulate all the previews over the various lines
   3559  let expectedNumberOfInlinePreviews = 0;
   3560  for (const { previews } of expectedInlinePreviews) {
   3561    expectedNumberOfInlinePreviews += previews.length;
   3562  }
   3563 
   3564  const inlinePreviews = await waitForAllElements(
   3565    dbg,
   3566    "visibleInlinePreviews",
   3567    expectedNumberOfInlinePreviews,
   3568    true
   3569  );
   3570 
   3571  ok(true, `Displayed ${inlinePreviews.length} inline previews`);
   3572 
   3573  for (const expectedInlinePreview of expectedInlinePreviews) {
   3574    const { previews, line } = expectedInlinePreview;
   3575 
   3576    const inlinePreviewElsOnLine = findAllElements(
   3577      dbg,
   3578      "inlinePreviewsOnLine",
   3579      line
   3580    );
   3581    previews.forEach(({ identifier, value }, index) => {
   3582      const inlinePreviewEl = inlinePreviewElsOnLine[index];
   3583 
   3584      const actualIdentifier = inlinePreviewEl.querySelector(
   3585        ".inline-preview-label"
   3586      ).innerText;
   3587      is(
   3588        inlinePreviewEl.querySelector(".inline-preview-label").innerText,
   3589        identifier,
   3590        `${identifier} in "${fnName}" has correct inline preview label "${actualIdentifier}" on line "${line}"`
   3591      );
   3592 
   3593      const actualValue = inlinePreviewEl.querySelector(
   3594        ".inline-preview-value"
   3595      ).innerText;
   3596      is(
   3597        inlinePreviewEl.querySelector(".inline-preview-value").innerText,
   3598        value,
   3599        `${identifier} in "${fnName}" has correct inline preview value "${actualValue}" on line "${line}"`
   3600      );
   3601    });
   3602  }
   3603 }