tor-browser

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

shared-head.js (87355B)


      1 /* Any copyright is dedicated to the Public Domain.
      2 * http://creativecommons.org/publicdomain/zero/1.0/ */
      3 /* eslint no-unused-vars: [2, {"vars": "local", caughtErrors: "none"}] */
      4 
      5 /* import-globals-from ../../inspector/test/shared-head.js */
      6 
      7 "use strict";
      8 
      9 // This shared-head.js file is used by most mochitests
     10 // and we start using it in xpcshell tests as well.
     11 // It contains various common helper functions.
     12 
     13 // Recording already set preferences.
     14 const devtoolsPreferences = Services.prefs.getBranch("devtools");
     15 const alreadySetPreferences = new Set();
     16 for (const pref of devtoolsPreferences.getChildList("")) {
     17  if (devtoolsPreferences.prefHasUserValue(pref)) {
     18    alreadySetPreferences.add(pref);
     19  }
     20 }
     21 
     22 {
     23  const { PromiseTestUtils } = ChromeUtils.importESModule(
     24    "resource://testing-common/PromiseTestUtils.sys.mjs"
     25  );
     26  PromiseTestUtils.allowMatchingRejectionsGlobally(
     27    /REDUX_MIDDLEWARE_IGNORED_REDUX_ACTION/
     28  );
     29 }
     30 
     31 async function resetPreferencesModifiedDuringTest() {
     32  if (!isXpcshell) {
     33    await SpecialPowers.flushPrefEnv();
     34  }
     35 
     36  // Reset devtools preferences modified by the test.
     37  for (const pref of devtoolsPreferences.getChildList("")) {
     38    if (
     39      devtoolsPreferences.prefHasUserValue(pref) &&
     40      !alreadySetPreferences.has(pref)
     41    ) {
     42      devtoolsPreferences.clearUserPref(pref);
     43    }
     44  }
     45 
     46  // Cleanup some generic Firefox preferences set indirectly by tests.
     47  for (const pref of [
     48    "browser.firefox-view.view-count",
     49    "extensions.ui.lastCategory",
     50    "sidebar.old-sidebar.has-used",
     51  ]) {
     52    Services.prefs.clearUserPref(pref);
     53  }
     54 }
     55 
     56 const isMochitest = "gTestPath" in this;
     57 const isXpcshell = !isMochitest;
     58 if (isXpcshell) {
     59  // gTestPath isn't exposed to xpcshell tests
     60  // _TEST_FILE is an array for a unique string
     61  /* global _TEST_FILE */
     62  this.gTestPath = _TEST_FILE[0];
     63 }
     64 
     65 const { Constructor: CC } = Components;
     66 
     67 // Print allocation count if DEBUG_DEVTOOLS_ALLOCATIONS is set to "normal",
     68 // and allocation sites if DEBUG_DEVTOOLS_ALLOCATIONS is set to "verbose".
     69 const DEBUG_ALLOCATIONS = Services.env.get("DEBUG_DEVTOOLS_ALLOCATIONS");
     70 if (DEBUG_ALLOCATIONS) {
     71  // Load the allocation tracker from the distinct privileged loader in order
     72  // to be able to debug all privileged code (ESMs, XPCOM,...) running in the shared privileged global.
     73  const {
     74    useDistinctSystemPrincipalLoader,
     75    releaseDistinctSystemPrincipalLoader,
     76  } = ChromeUtils.importESModule(
     77    "resource://devtools/shared/loader/DistinctSystemPrincipalLoader.sys.mjs",
     78    { global: "shared" }
     79  );
     80  const requester = {};
     81  const loader = useDistinctSystemPrincipalLoader(requester);
     82  registerCleanupFunction(() =>
     83    releaseDistinctSystemPrincipalLoader(requester)
     84  );
     85 
     86  const { allocationTracker } = loader.require(
     87    "resource://devtools/shared/test-helpers/allocation-tracker.js"
     88  );
     89  const tracker = allocationTracker({ watchAllGlobals: true });
     90  registerCleanupFunction(() => {
     91    if (DEBUG_ALLOCATIONS == "normal") {
     92      tracker.logCount();
     93    } else if (DEBUG_ALLOCATIONS == "verbose") {
     94      tracker.logAllocationSites();
     95    }
     96    tracker.stop();
     97  });
     98 }
     99 
    100 // When DEBUG_STEP environment variable is set,
    101 // automatically start a tracer which will log all line being executed
    102 // in the running test (and nothing else) and also pause its execution
    103 // for the given amount of milliseconds.
    104 //
    105 // Be careful that these pause have significant side effect.
    106 // This will pause the test script event loop and allow running the other
    107 // tasks queued in the parent process's main thread event loop queue.
    108 //
    109 // Passing any non-number value, like `DEBUG_STEP=true` will still
    110 // log the executed lines without any pause, and without this side effect.
    111 //
    112 // For now, the tracer can only work once per thread.
    113 // So when using this feature you will not be able to use the JS tracer
    114 // in any other way on parent process's main thread.
    115 const DEBUG_STEP = Services.env.get("DEBUG_STEP");
    116 if (DEBUG_STEP) {
    117  // Load the stepper code from the distinct privileged loader in order
    118  // to be able to debug all privileged code (ESMs, XPCOM,...) running in the shared privileged global.
    119  const {
    120    useDistinctSystemPrincipalLoader,
    121    releaseDistinctSystemPrincipalLoader,
    122  } = ChromeUtils.importESModule(
    123    "resource://devtools/shared/loader/DistinctSystemPrincipalLoader.sys.mjs",
    124    { global: "shared" }
    125  );
    126  const requester = {};
    127  const loader = useDistinctSystemPrincipalLoader(requester);
    128 
    129  const stepper = loader.require(
    130    "resource://devtools/shared/test-helpers/test-stepper.js"
    131  );
    132  stepper.start(globalThis, gTestPath, DEBUG_STEP);
    133  registerCleanupFunction(() => {
    134    stepper.stop();
    135    releaseDistinctSystemPrincipalLoader(requester);
    136  });
    137 }
    138 
    139 const DEBUG_TRACE_LINE = Services.env.get("DEBUG_TRACE_LINE");
    140 if (DEBUG_TRACE_LINE) {
    141  // Load the tracing code from the distinct privileged loader in order
    142  // to be able to debug all privileged code (ESMs, XPCOM,...) running in the shared privileged global.
    143  const {
    144    useDistinctSystemPrincipalLoader,
    145    releaseDistinctSystemPrincipalLoader,
    146  } = ChromeUtils.importESModule(
    147    "resource://devtools/shared/loader/DistinctSystemPrincipalLoader.sys.mjs"
    148  );
    149  const requester = {};
    150  const loader = useDistinctSystemPrincipalLoader(requester);
    151 
    152  const lineTracer = loader.require(
    153    "resource://devtools/shared/test-helpers/test-line-tracer.js"
    154  );
    155  lineTracer.start(globalThis, gTestPath, DEBUG_TRACE_LINE);
    156  registerCleanupFunction(() => {
    157    lineTracer.stop();
    158    releaseDistinctSystemPrincipalLoader(requester);
    159  });
    160 }
    161 
    162 const { loader, require } = ChromeUtils.importESModule(
    163  "resource://devtools/shared/loader/Loader.sys.mjs"
    164 );
    165 const { sinon } = ChromeUtils.importESModule(
    166  "resource://testing-common/Sinon.sys.mjs"
    167 );
    168 
    169 // When loaded from xpcshell test, this file is loaded via xpcshell.toml's head property
    170 // and so it loaded first before anything else and isn't having access to Services global.
    171 // Whereas many head.js files from mochitest import this file via loadSubScript
    172 // and already expose Services as a global.
    173 
    174 const {
    175  gDevTools,
    176 } = require("resource://devtools/client/framework/devtools.js");
    177 const {
    178  CommandsFactory,
    179 } = require("resource://devtools/shared/commands/commands-factory.js");
    180 const DevToolsUtils = require("resource://devtools/shared/DevToolsUtils.js");
    181 
    182 const KeyShortcuts = require("resource://devtools/client/shared/key-shortcuts.js");
    183 
    184 const { LocalizationHelper } = require("resource://devtools/shared/l10n.js");
    185 
    186 loader.lazyRequireGetter(
    187  this,
    188  "ResponsiveUIManager",
    189  "resource://devtools/client/responsive/manager.js"
    190 );
    191 loader.lazyRequireGetter(
    192  this,
    193  "localTypes",
    194  "resource://devtools/client/responsive/types.js"
    195 );
    196 loader.lazyRequireGetter(
    197  this,
    198  "ResponsiveMessageHelper",
    199  "resource://devtools/client/responsive/utils/message.js"
    200 );
    201 
    202 loader.lazyRequireGetter(
    203  this,
    204  "FluentReact",
    205  "resource://devtools/client/shared/vendor/fluent-react.js"
    206 );
    207 
    208 const TEST_DIR = gTestPath.substr(0, gTestPath.lastIndexOf("/"));
    209 const CHROME_URL_ROOT = TEST_DIR + "/";
    210 const URL_ROOT = CHROME_URL_ROOT.replace(
    211  "chrome://mochitests/content/",
    212  "http://example.com/"
    213 );
    214 const URL_ROOT_SSL = CHROME_URL_ROOT.replace(
    215  "chrome://mochitests/content/",
    216  "https://example.com/"
    217 );
    218 
    219 // Add aliases which make it more explicit that URL_ROOT uses a com TLD.
    220 const URL_ROOT_COM = URL_ROOT;
    221 const URL_ROOT_COM_SSL = URL_ROOT_SSL;
    222 
    223 // Also expose http://example.org, http://example.net, https://example.org to
    224 // test Fission scenarios easily.
    225 // Note: example.net is not available for https.
    226 const URL_ROOT_ORG = CHROME_URL_ROOT.replace(
    227  "chrome://mochitests/content/",
    228  "http://example.org/"
    229 );
    230 const URL_ROOT_ORG_SSL = CHROME_URL_ROOT.replace(
    231  "chrome://mochitests/content/",
    232  "https://example.org/"
    233 );
    234 const URL_ROOT_NET = CHROME_URL_ROOT.replace(
    235  "chrome://mochitests/content/",
    236  "http://example.net/"
    237 );
    238 const URL_ROOT_NET_SSL = CHROME_URL_ROOT.replace(
    239  "chrome://mochitests/content/",
    240  "https://example.net/"
    241 );
    242 // mochi.test:8888 is the actual primary location where files are served.
    243 const URL_ROOT_MOCHI_8888 = CHROME_URL_ROOT.replace(
    244  "chrome://mochitests/content/",
    245  "http://mochi.test:8888/"
    246 );
    247 
    248 try {
    249  if (isMochitest) {
    250    Services.scriptloader.loadSubScript(
    251      "chrome://mochitests/content/browser/devtools/client/shared/test/telemetry-test-helpers.js",
    252      this
    253    );
    254  }
    255 } catch (e) {
    256  ok(
    257    false,
    258    "MISSING DEPENDENCY ON telemetry-test-helpers.js\n" +
    259      "Please add the following line in browser.toml:\n" +
    260      "  !/devtools/client/shared/test/telemetry-test-helpers.js\n"
    261  );
    262  throw e;
    263 }
    264 
    265 // Force devtools to be initialized so menu items and keyboard shortcuts get installed
    266 require("resource://devtools/client/framework/devtools-browser.js");
    267 
    268 // All tests are asynchronous
    269 if (isMochitest) {
    270  waitForExplicitFinish();
    271 }
    272 
    273 var EXPECTED_DTU_ASSERT_FAILURE_COUNT = 0;
    274 
    275 registerCleanupFunction(function () {
    276  if (
    277    DevToolsUtils.assertionFailureCount !== EXPECTED_DTU_ASSERT_FAILURE_COUNT
    278  ) {
    279    ok(
    280      false,
    281      "Should have had the expected number of DevToolsUtils.assert() failures." +
    282        " Expected " +
    283        EXPECTED_DTU_ASSERT_FAILURE_COUNT +
    284        ", got " +
    285        DevToolsUtils.assertionFailureCount
    286    );
    287  }
    288 });
    289 
    290 // Uncomment this pref to dump all devtools emitted events to the console.
    291 // Services.prefs.setBoolPref("devtools.dump.emit", true);
    292 
    293 /**
    294 * Watch console messages for failed propType definitions in React components.
    295 */
    296 function onConsoleMessage(subject) {
    297  const message = subject.wrappedJSObject.arguments[0];
    298 
    299  if (message && /Failed propType/.test(message.toString())) {
    300    ok(false, message);
    301  }
    302 }
    303 
    304 const ConsoleAPIStorage = Cc["@mozilla.org/consoleAPI-storage;1"].getService(
    305  Ci.nsIConsoleAPIStorage
    306 );
    307 
    308 ConsoleAPIStorage.addLogEventListener(
    309  onConsoleMessage,
    310  Cc["@mozilla.org/systemprincipal;1"].createInstance(Ci.nsIPrincipal)
    311 );
    312 registerCleanupFunction(() => {
    313  ConsoleAPIStorage.removeLogEventListener(onConsoleMessage);
    314 });
    315 
    316 // Disable this preference to reduce exceptions related to pending `listWorkers`
    317 // requests occuring after a process is created/destroyed. See Bug 1620983.
    318 add_setup(async () => {
    319  if (!isXpcshell) {
    320    await pushPref("dom.ipc.processPrelaunch.enabled", false);
    321  }
    322 });
    323 
    324 // On some Linux platforms, prefers-reduced-motion is enabled, which would
    325 // trigger the notification to be displayed in the toolbox. Dismiss the message
    326 // by default.
    327 Services.prefs.setBoolPref(
    328  "devtools.inspector.simple-highlighters.message-dismissed",
    329  true
    330 );
    331 
    332 // Enable dumping scope variables when a test failure occurs.
    333 Services.prefs.setBoolPref("devtools.testing.testScopes", true);
    334 
    335 var {
    336  BrowserConsoleManager,
    337 } = require("resource://devtools/client/webconsole/browser-console-manager.js");
    338 
    339 registerCleanupFunction(async function cleanup() {
    340  // Closing the browser console if there's one
    341  const browserConsole = BrowserConsoleManager.getBrowserConsole();
    342  if (browserConsole) {
    343    await safeCloseBrowserConsole({ clearOutput: true });
    344  }
    345 
    346  // Close any tab opened by the test.
    347  // There should be only one tab opened by default when firefox starts the test.
    348  while (isMochitest && gBrowser.tabs.length > 1) {
    349    await closeTabAndToolbox(gBrowser.selectedTab);
    350  }
    351 
    352  // Note that this will run before cleanup functions registered by tests or other head.js files.
    353  // So all connections must be cleaned up by the test when the test ends,
    354  // before the harness starts invoking the cleanup functions
    355  await waitForTick();
    356 
    357  // All connections must be cleaned up by the test when the test ends.
    358  const {
    359    DevToolsServer,
    360  } = require("resource://devtools/server/devtools-server.js");
    361  ok(
    362    !DevToolsServer.hasConnection(),
    363    "The main process DevToolsServer has no pending connection when the test ends"
    364  );
    365  // If there is still open connection, close all of them so that following tests
    366  // could pass.
    367  if (DevToolsServer.hasConnection()) {
    368    for (const conn of Object.values(DevToolsServer._connections)) {
    369      conn.close();
    370    }
    371  }
    372 
    373  // Reset all preferences AFTER the toolbox is closed.
    374  // NOTE: Doing it before toolbox destruction could trigger observers.
    375  await resetPreferencesModifiedDuringTest();
    376 });
    377 
    378 async function safeCloseBrowserConsole({ clearOutput = false } = {}) {
    379  const hud = BrowserConsoleManager.getBrowserConsole();
    380  if (!hud) {
    381    return;
    382  }
    383 
    384  if (clearOutput) {
    385    info("Clear the browser console output");
    386    const { ui } = hud;
    387    const promises = [ui.once("messages-cleared")];
    388    // If there's an object inspector, we need to wait for the actors to be released.
    389    if (ui.outputNode.querySelector(".object-inspector")) {
    390      promises.push(ui.once("fronts-released"));
    391    }
    392    await ui.clearOutput(true);
    393    await Promise.all(promises);
    394    info("Browser console cleared");
    395  }
    396 
    397  info("Wait for all Browser Console targets to be attached");
    398  // It might happen that waitForAllTargetsToBeAttached does not resolve, so we set a
    399  // timeout of 1s before closing
    400  await Promise.race([
    401    waitForAllTargetsToBeAttached(hud.commands.targetCommand),
    402    wait(1000),
    403  ]);
    404 
    405  info("Close the Browser Console");
    406  await BrowserConsoleManager.closeBrowserConsole();
    407  info("Browser Console closed");
    408 }
    409 
    410 /**
    411 * Observer code to register the test actor in every DevTools server which
    412 * starts registering its own actors.
    413 *
    414 * We require immediately the highlighter test actor file, because it will force to load and
    415 * register the front and the spec for HighlighterTestActor. Normally specs and fronts are
    416 * in separate files registered in specs/index.js. But here to simplify the
    417 * setup everything is in the same file and we force to load it here.
    418 *
    419 * DevToolsServer will emit "devtools-server-initialized" after finishing its
    420 * initialization. We watch this observable to add our custom actor.
    421 *
    422 * As a single test may create several DevTools servers, we keep the observer
    423 * alive until the test ends.
    424 *
    425 * To avoid leaks, the observer needs to be removed at the end of each test.
    426 * The test cleanup will send the async message "remove-devtools-highlightertestactor-observer",
    427 * we listen to this message to cleanup the observer.
    428 */
    429 function highlighterTestActorBootstrap() {
    430  /* eslint-env mozilla/process-script */
    431  const HIGHLIGHTER_TEST_ACTOR_URL =
    432    "chrome://mochitests/content/browser/devtools/client/shared/test/highlighter-test-actor.js";
    433 
    434  const { require: _require } = ChromeUtils.importESModule(
    435    "resource://devtools/shared/loader/Loader.sys.mjs"
    436  );
    437  _require(HIGHLIGHTER_TEST_ACTOR_URL);
    438 
    439  const actorRegistryObserver = subject => {
    440    const actorRegistry = subject.wrappedJSObject;
    441    actorRegistry.registerModule(HIGHLIGHTER_TEST_ACTOR_URL, {
    442      prefix: "highlighterTest",
    443      constructor: "HighlighterTestActor",
    444      type: { target: true },
    445    });
    446  };
    447  Services.obs.addObserver(
    448    actorRegistryObserver,
    449    "devtools-server-initialized"
    450  );
    451 
    452  const unloadListener = () => {
    453    Services.cpmm.removeMessageListener(
    454      "remove-devtools-testactor-observer",
    455      unloadListener
    456    );
    457    Services.obs.removeObserver(
    458      actorRegistryObserver,
    459      "devtools-server-initialized"
    460    );
    461  };
    462  Services.cpmm.addMessageListener(
    463    "remove-devtools-testactor-observer",
    464    unloadListener
    465  );
    466 }
    467 
    468 if (isMochitest) {
    469  const highlighterTestActorBootstrapScript =
    470    "data:,(" + highlighterTestActorBootstrap + ")()";
    471  Services.ppmm.loadProcessScript(
    472    highlighterTestActorBootstrapScript,
    473    // Load this script in all processes (created or to be created)
    474    true
    475  );
    476 
    477  registerCleanupFunction(() => {
    478    Services.ppmm.broadcastAsyncMessage("remove-devtools-testactor-observer");
    479    Services.ppmm.removeDelayedProcessScript(
    480      highlighterTestActorBootstrapScript
    481    );
    482  });
    483 }
    484 
    485 /**
    486 * Spawn an instance of the highlighter test actor for the given toolbox
    487 *
    488 * @param {Toolbox} toolbox
    489 * @param {object} options
    490 * @param {Function} options.target: Optional target to get the highlighterTestFront for.
    491 *        If not provided, the top level target will be used.
    492 * @returns {HighlighterTestFront}
    493 */
    494 async function getHighlighterTestFront(toolbox, { target } = {}) {
    495  // Loading the Inspector panel in order to overwrite the TestActor getter for the
    496  // highlighter instance with a method that points to the currently visible
    497  // Box Model Highlighter managed by the Inspector panel.
    498  const inspector = await toolbox.loadTool("inspector");
    499 
    500  const highlighterTestFront = await (target || toolbox.target).getFront(
    501    "highlighterTest"
    502  );
    503  // Override the highligher getter with a method to return the active box model
    504  // highlighter. Adaptation for multi-process scenarios where there can be multiple
    505  // highlighters, one per process.
    506  highlighterTestFront.highlighter = () => {
    507    return inspector.highlighters.getActiveHighlighter(
    508      inspector.highlighters.TYPES.BOXMODEL
    509    );
    510  };
    511  return highlighterTestFront;
    512 }
    513 
    514 /**
    515 * Spawn an instance of the highlighter test actor for the given tab, when we need the
    516 * highlighter test front before opening or without a toolbox.
    517 *
    518 * @param {Tab} tab
    519 * @returns {HighlighterTestFront}
    520 */
    521 async function getHighlighterTestFrontWithoutToolbox(tab) {
    522  const commands = await CommandsFactory.forTab(tab);
    523  // Initialize the TargetCommands which require some async stuff to be done
    524  // before being fully ready. This will define the `targetCommand.targetFront` attribute.
    525  await commands.targetCommand.startListening();
    526 
    527  const targetFront = commands.targetCommand.targetFront;
    528  return targetFront.getFront("highlighterTest");
    529 }
    530 
    531 /**
    532 * Returns a Promise that resolves when all the targets are fully attached.
    533 *
    534 * @param {TargetCommand} targetCommand
    535 */
    536 function waitForAllTargetsToBeAttached(targetCommand) {
    537  return Promise.allSettled(
    538    targetCommand
    539      .getAllTargets(targetCommand.ALL_TYPES)
    540      .map(target => target.initialized)
    541  );
    542 }
    543 
    544 /**
    545 * Add a new test tab in the browser and load the given url.
    546 *
    547 * @param {string} url The url to be loaded in the new tab
    548 * @param {object} options Object with various optional fields:
    549 *   - {Boolean} background If true, open the tab in background
    550 *   - {ChromeWindow} window Firefox top level window we should use to open the tab
    551 *   - {Number} userContextId The userContextId of the tab.
    552 *   - {String} preferredRemoteType
    553 *   - {Boolean} waitForLoad Wait for the page in the new tab to load. (Defaults to true.)
    554 * @return a promise that resolves to the tab object when the url is loaded
    555 */
    556 async function addTab(url, options = {}) {
    557  info("Adding a new tab with URL: " + url);
    558 
    559  const {
    560    background = false,
    561    userContextId,
    562    preferredRemoteType,
    563    waitForLoad = true,
    564  } = options;
    565  const { gBrowser } = options.window ? options.window : window;
    566 
    567  const tab = BrowserTestUtils.addTab(gBrowser, url, {
    568    userContextId,
    569    preferredRemoteType,
    570  });
    571 
    572  if (!background) {
    573    gBrowser.selectedTab = tab;
    574  }
    575 
    576  if (waitForLoad) {
    577    // accept any URL as url arg might not be serialized or redirects might happen
    578    await BrowserTestUtils.browserLoaded(tab.linkedBrowser, {
    579      wantLoad: () => true,
    580    });
    581    // Waiting for presShell helps with test timeouts in webrender platforms.
    582    await waitForPresShell(tab.linkedBrowser);
    583    info("Tab added and finished loading");
    584  } else {
    585    info("Tab added");
    586  }
    587 
    588  return tab;
    589 }
    590 
    591 /**
    592 * Remove the given tab.
    593 *
    594 * @param {object} tab The tab to be removed.
    595 * @return Promise<undefined> resolved when the tab is successfully removed.
    596 */
    597 async function removeTab(tab) {
    598  info("Removing tab.");
    599 
    600  const { gBrowser } = tab.ownerDocument.defaultView;
    601  const onClose = once(gBrowser.tabContainer, "TabClose");
    602  gBrowser.removeTab(tab);
    603  await onClose;
    604 
    605  info("Tab removed and finished closing");
    606 }
    607 
    608 /**
    609 * Alias for navigateTo which will reuse the current URI of the provided browser
    610 * to trigger a navigation.
    611 */
    612 async function reloadBrowser({
    613  browser = gBrowser.selectedBrowser,
    614  isErrorPage = false,
    615  waitForLoad = true,
    616 } = {}) {
    617  return navigateTo(browser.currentURI.spec, {
    618    browser,
    619    isErrorPage,
    620    waitForLoad,
    621  });
    622 }
    623 
    624 /**
    625 * Navigate the currently selected tab to a new URL and wait for it to load.
    626 * Also wait for the toolbox to attach to the new target, if we navigated
    627 * to a new process.
    628 *
    629 * @param {string} url The url to be loaded in the current tab.
    630 * @param {JSON} options Optional dictionary object with the following keys:
    631 *        - {XULBrowser} browser
    632 *          The browser element which should navigate. Defaults to the selected
    633 *          browser.
    634 *        - {Boolean} isErrorPage
    635 *          You may pass `true` if the URL is an error page. Otherwise
    636 *          BrowserTestUtils.browserLoaded will wait for 'load' event, which
    637 *          never fires for error pages.
    638 *        - {Boolean} waitForLoad
    639 *          You may pass `false` if the page load is expected to be blocked by
    640 *          a script or a breakpoint.
    641 *
    642 * @return a promise that resolves when the page has fully loaded.
    643 */
    644 async function navigateTo(
    645  uri,
    646  {
    647    browser = gBrowser.selectedBrowser,
    648    isErrorPage = false,
    649    waitForLoad = true,
    650  } = {}
    651 ) {
    652  const waitForDevToolsReload = await watchForDevToolsReload(browser, {
    653    isErrorPage,
    654    waitForLoad,
    655  });
    656 
    657  uri = uri.replaceAll("\n", "");
    658  info(`Navigating to "${uri}"`);
    659 
    660  const onBrowserLoaded = BrowserTestUtils.browserLoaded(
    661    browser,
    662    // includeSubFrames
    663    false,
    664    // resolve on this specific page to load (if null, it would be any page load)
    665    loadedUrl => {
    666      // loadedUrl is encoded, while uri might not be.
    667      return loadedUrl === uri || decodeURI(loadedUrl) === uri;
    668    },
    669    isErrorPage
    670  );
    671 
    672  // if we're navigating to the same page we're already on, use reloadTab instead as the
    673  // behavior slightly differs from loadURI (e.g. scroll position isn't keps with the latter).
    674  if (uri === browser.currentURI.spec) {
    675    gBrowser.reloadTab(gBrowser.getTabForBrowser(browser));
    676  } else {
    677    BrowserTestUtils.startLoadingURIString(browser, uri);
    678  }
    679 
    680  if (waitForLoad) {
    681    info(`Waiting for page to be loaded…`);
    682    await onBrowserLoaded;
    683    info(`→ page loaded`);
    684  }
    685 
    686  await waitForDevToolsReload();
    687 }
    688 
    689 /**
    690 * This method should be used to watch for completion of any browser navigation
    691 * performed with a DevTools UI.
    692 *
    693 * It should watch for:
    694 * - Toolbox reload
    695 * - Toolbox commands reload
    696 * - RDM reload
    697 * - RDM commands reload
    698 *
    699 * And it should work both for target switching or old-style navigations.
    700 *
    701 * This method, similarly to all the other watch* navigation methods in this file,
    702 * is async but returns another method which should be called after the navigation
    703 * is done. Browser navigation might be monitored differently depending on the
    704 * situation, so it's up to the caller to handle it as needed.
    705 *
    706 * Typically, this would be used as follows:
    707 * ```
    708 *   async function someNavigationHelper(browser) {
    709 *     const waitForDevToolsFn = await watchForDevToolsReload(browser);
    710 *
    711 *     // This step should wait for the load to be completed from the browser's
    712 *     // point of view, so that waitForDevToolsFn can compare pIds, browsing
    713 *     // contexts etc... and check if we should expect a target switch
    714 *     await performBrowserNavigation(browser);
    715 *
    716 *     await waitForDevToolsFn();
    717 *   }
    718 * ```
    719 */
    720 async function watchForDevToolsReload(
    721  browser,
    722  { isErrorPage = false, waitForLoad = true } = {}
    723 ) {
    724  const waitForToolboxReload = await _watchForToolboxReload(browser, {
    725    isErrorPage,
    726    waitForLoad,
    727  });
    728  const waitForResponsiveReload = await _watchForResponsiveReload(browser, {
    729    isErrorPage,
    730    waitForLoad,
    731  });
    732 
    733  return async function () {
    734    info("Wait for the toolbox to reload");
    735    await waitForToolboxReload();
    736 
    737    info("Wait for Responsive UI to reload");
    738    await waitForResponsiveReload();
    739  };
    740 }
    741 
    742 /**
    743 * Start watching for the toolbox reload to be completed:
    744 * - watch for the toolbox's commands to be fully reloaded
    745 * - watch for the toolbox's current panel to be reloaded
    746 */
    747 async function _watchForToolboxReload(
    748  browser,
    749  { isErrorPage, waitForLoad } = {}
    750 ) {
    751  const tab = gBrowser.getTabForBrowser(browser);
    752 
    753  const toolbox = gDevTools.getToolboxForTab(tab);
    754 
    755  if (!toolbox) {
    756    // No toolbox to wait for
    757    return function () {};
    758  }
    759 
    760  const waitForCurrentPanelReload = watchForCurrentPanelReload(toolbox);
    761  const waitForToolboxCommandsReload = await watchForCommandsReload(
    762    toolbox.commands,
    763    { isErrorPage, waitForLoad }
    764  );
    765  const checkTargetSwitching = await watchForTargetSwitching(
    766    toolbox.commands,
    767    browser
    768  );
    769 
    770  return async function () {
    771    const isTargetSwitching = checkTargetSwitching();
    772 
    773    info(`Waiting for toolbox commands to be reloaded…`);
    774    await waitForToolboxCommandsReload(isTargetSwitching);
    775 
    776    // TODO: We should wait for all loaded panels to reload here, because some
    777    // of them might still perform background updates.
    778    if (waitForCurrentPanelReload) {
    779      info(`Waiting for ${toolbox.currentToolId} to be reloaded…`);
    780      await waitForCurrentPanelReload();
    781      info(`→ panel reloaded`);
    782    }
    783  };
    784 }
    785 
    786 /**
    787 * Start watching for Responsive UI (RDM) reload to be completed:
    788 * - watch for the Responsive UI's commands to be fully reloaded
    789 * - watch for the Responsive UI's target switch to be done
    790 */
    791 async function _watchForResponsiveReload(
    792  browser,
    793  { isErrorPage, waitForLoad } = {}
    794 ) {
    795  const tab = gBrowser.getTabForBrowser(browser);
    796  const ui = ResponsiveUIManager.getResponsiveUIForTab(tab);
    797 
    798  if (!ui) {
    799    // No responsive UI to wait for
    800    return function () {};
    801  }
    802 
    803  const onResponsiveTargetSwitch = ui.once("responsive-ui-target-switch-done");
    804  const waitForResponsiveCommandsReload = await watchForCommandsReload(
    805    ui.commands,
    806    { isErrorPage, waitForLoad }
    807  );
    808  const checkTargetSwitching = await watchForTargetSwitching(
    809    ui.commands,
    810    browser
    811  );
    812 
    813  return async function () {
    814    const isTargetSwitching = checkTargetSwitching();
    815 
    816    info(`Waiting for responsive ui commands to be reloaded…`);
    817    await waitForResponsiveCommandsReload(isTargetSwitching);
    818 
    819    if (isTargetSwitching) {
    820      await onResponsiveTargetSwitch;
    821    }
    822  };
    823 }
    824 
    825 /**
    826 * Watch for the current panel selected in the provided toolbox to be reloaded.
    827 * Some panels implement custom events that should be expected for every reload.
    828 *
    829 * Note about returning a method instead of a promise:
    830 * In general this pattern is useful so that we can check if a target switch
    831 * occurred or not, and decide which events to listen for. So far no panel is
    832 * behaving differently whether there was a target switch or not. But to remain
    833 * consistent with other watch* methods we still return a function here.
    834 *
    835 * @param {Toolbox}
    836 *        The Toolbox instance which is going to experience a reload
    837 * @return {function} An async method to be called and awaited after the reload
    838 *         started. Will return `null` for panels which don't implement any
    839 *         specific reload event.
    840 */
    841 function watchForCurrentPanelReload(toolbox) {
    842  return _watchForPanelReload(toolbox, toolbox.currentToolId);
    843 }
    844 
    845 /**
    846 * Watch for all the panels loaded in the provided toolbox to be reloaded.
    847 * Some panels implement custom events that should be expected for every reload.
    848 *
    849 * Note about returning a method instead of a promise:
    850 * See comment for watchForCurrentPanelReload
    851 *
    852 * @param {Toolbox}
    853 *        The Toolbox instance which is going to experience a reload
    854 * @return {function} An async method to be called and awaited after the reload
    855 *         started.
    856 */
    857 function watchForLoadedPanelsReload(toolbox) {
    858  const waitForPanels = [];
    859  for (const [id] of toolbox.getToolPanels()) {
    860    // Store a watcher method for each panel already loaded.
    861    waitForPanels.push(_watchForPanelReload(toolbox, id));
    862  }
    863 
    864  return function () {
    865    return Promise.all(
    866      waitForPanels.map(async watchPanel => {
    867        // Wait for all panels to be reloaded.
    868        if (watchPanel) {
    869          await watchPanel();
    870        }
    871      })
    872    );
    873  };
    874 }
    875 
    876 function _watchForPanelReload(toolbox, toolId) {
    877  const panel = toolbox.getPanel(toolId);
    878 
    879  if (toolId == "inspector") {
    880    const markuploaded = panel.once("markuploaded");
    881    const onNewRoot = panel.once("new-root");
    882    const onUpdated = panel.once("inspector-updated");
    883    const onReloaded = panel.once("reloaded");
    884 
    885    return async function () {
    886      info("Waiting for markup view to load after navigation.");
    887      await markuploaded;
    888 
    889      info("Waiting for new root.");
    890      await onNewRoot;
    891 
    892      info("Waiting for inspector to update after new-root event.");
    893      await onUpdated;
    894 
    895      info("Waiting for inspector updates after page reload");
    896      await onReloaded;
    897 
    898      info("Received 'reloaded' event for inspector");
    899    };
    900  } else if (
    901    ["netmonitor", "accessibility", "webconsole", "jsdebugger"].includes(toolId)
    902  ) {
    903    const onReloaded = panel.once("reloaded");
    904    return async function () {
    905      info(`Waiting for ${toolId} updates after page reload`);
    906      await onReloaded;
    907 
    908      info(`Received 'reloaded' event for ${toolId}`);
    909    };
    910  }
    911  return null;
    912 }
    913 
    914 /**
    915 * Watch for a Commands instance to be reloaded after a navigation.
    916 *
    917 * As for other navigation watch* methods, this should be called before the
    918 * navigation starts, and the function it returns should be called after the
    919 * navigation is done from a Browser point of view.
    920 *
    921 * !!! The wait function expects a `isTargetSwitching` argument to be provided,
    922 * which needs to be monitored using watchForTargetSwitching !!!
    923 */
    924 async function watchForCommandsReload(
    925  commands,
    926  { isErrorPage = false, waitForLoad = true } = {}
    927 ) {
    928  // If we're switching origins, we need to wait for the 'switched-target'
    929  // event to make sure everything is ready.
    930  // Navigating from/to pages loaded in the parent process, like about:robots,
    931  // also spawn new targets.
    932  // (If target switching is disabled, the toolbox will reboot)
    933  const onTargetSwitched = commands.targetCommand.once("switched-target");
    934 
    935  // Wait until we received a page load resource:
    936  // - dom-complete if we can wait for a full page load
    937  // - dom-loading otherwise
    938  // This allows to wait for page load for consumers calling directly
    939  // waitForDevTools instead of navigateTo/reloadBrowser.
    940  // This is also useful as an alternative to target switching, when no target
    941  // switch is supposed to happen.
    942  const waitForCompleteLoad = waitForLoad && !isErrorPage;
    943  const documentEventName = waitForCompleteLoad
    944    ? "dom-complete"
    945    : "dom-loading";
    946 
    947  const { onResource: onTopLevelDomEvent } =
    948    await commands.resourceCommand.waitForNextResource(
    949      commands.resourceCommand.TYPES.DOCUMENT_EVENT,
    950      {
    951        ignoreExistingResources: true,
    952        predicate: resource =>
    953          resource.targetFront.isTopLevel &&
    954          resource.name === documentEventName,
    955      }
    956    );
    957 
    958  return async function (isTargetSwitching) {
    959    if (typeof isTargetSwitching === "undefined") {
    960      throw new Error("isTargetSwitching was not provided to the wait method");
    961    }
    962 
    963    if (isTargetSwitching) {
    964      info(`Waiting for target switch…`);
    965      await onTargetSwitched;
    966      info(`→ switched-target emitted`);
    967    }
    968 
    969    info(`Waiting for '${documentEventName}' resource…`);
    970    await onTopLevelDomEvent;
    971    info(`→ '${documentEventName}' resource emitted`);
    972 
    973    return isTargetSwitching;
    974  };
    975 }
    976 
    977 /**
    978 * Watch if an upcoming navigation will trigger a target switching, for the
    979 * provided Commands instance and the provided Browser.
    980 *
    981 * As for other navigation watch* methods, this should be called before the
    982 * navigation starts, and the function it returns should be called after the
    983 * navigation is done from a Browser point of view.
    984 */
    985 async function watchForTargetSwitching(commands, browser) {
    986  browser = browser || gBrowser.selectedBrowser;
    987  const currentPID = browser.browsingContext.currentWindowGlobal.osPid;
    988  const currentBrowsingContextID = browser.browsingContext.id;
    989 
    990  // If the current top-level target follows the window global lifecycle, a
    991  // target switch will occur regardless of process changes.
    992  const targetFollowsWindowLifecycle =
    993    commands.targetCommand.targetFront.targetForm.followWindowGlobalLifeCycle;
    994 
    995  return function () {
    996    // Compare the PIDs (and not the toolbox's targets) as PIDs are updated also immediately,
    997    // while target may be updated slightly later.
    998    const switchedProcess =
    999      currentPID !== browser.browsingContext.currentWindowGlobal.osPid;
   1000    const switchedBrowsingContext =
   1001      currentBrowsingContextID !== browser.browsingContext.id;
   1002 
   1003    return (
   1004      targetFollowsWindowLifecycle || switchedProcess || switchedBrowsingContext
   1005    );
   1006  };
   1007 }
   1008 
   1009 /**
   1010 * Create a Target for the provided tab and attach to it before resolving.
   1011 * This should only be used for tests which don't involve the frontend or a
   1012 * toolbox. Typically, retrieving the target and attaching to it should be
   1013 * handled at framework level when a Toolbox is used.
   1014 *
   1015 * @param {XULTab} tab
   1016 *        The tab for which a target should be created.
   1017 * @return {WindowGlobalTargetFront} The attached target front.
   1018 */
   1019 async function createAndAttachTargetForTab(tab) {
   1020  info("Creating and attaching to a local tab target");
   1021 
   1022  const commands = await CommandsFactory.forTab(tab);
   1023 
   1024  // Initialize the TargetCommands which require some async stuff to be done
   1025  // before being fully ready. This will define the `targetCommand.targetFront` attribute.
   1026  await commands.targetCommand.startListening();
   1027 
   1028  const target = commands.targetCommand.targetFront;
   1029  return target;
   1030 }
   1031 
   1032 /**
   1033 * Open the inspector in a tab with given URL.
   1034 *
   1035 * @param {string} url  The URL to open.
   1036 * @param {string} hostType Optional hostType, as defined in Toolbox.HostType
   1037 * @return A promise that is resolved once the tab and inspector have loaded
   1038 *         with an object: { tab, toolbox, inspector, highlighterTestFront }.
   1039 */
   1040 async function openInspectorForURL(url, hostType) {
   1041  const tab = await addTab(url);
   1042  const { inspector, toolbox, highlighterTestFront } =
   1043    await openInspector(hostType);
   1044  return { tab, inspector, toolbox, highlighterTestFront };
   1045 }
   1046 
   1047 function getActiveInspector() {
   1048  const toolbox = gDevTools.getToolboxForTab(gBrowser.selectedTab);
   1049  return toolbox.getPanel("inspector");
   1050 }
   1051 
   1052 /**
   1053 * Simulate a key event from an electron key shortcut string:
   1054 * https://github.com/electron/electron/blob/master/docs/api/accelerator.md
   1055 *
   1056 * @param {string} key
   1057 * @param {DOMWindow} target
   1058 *        Optional window where to fire the key event
   1059 */
   1060 function synthesizeKeyShortcut(key, target) {
   1061  const shortcut = KeyShortcuts.parseElectronKey(key);
   1062  const keyEvent = {
   1063    altKey: shortcut.alt,
   1064    ctrlKey: shortcut.ctrl,
   1065    metaKey: shortcut.meta,
   1066    shiftKey: shortcut.shift,
   1067  };
   1068  if (shortcut.keyCode) {
   1069    keyEvent.keyCode = shortcut.keyCode;
   1070  }
   1071 
   1072  info("Synthesizing key shortcut: " + key);
   1073  EventUtils.synthesizeKey(shortcut.key || "", keyEvent, target);
   1074 }
   1075 
   1076 var waitForTime = DevToolsUtils.waitForTime;
   1077 
   1078 /**
   1079 * Wait for a tick.
   1080 *
   1081 * @return {Promise}
   1082 */
   1083 function waitForTick() {
   1084  return new Promise(resolve => DevToolsUtils.executeSoon(resolve));
   1085 }
   1086 
   1087 /**
   1088 * This shouldn't be used in the tests, but is useful when writing new tests or
   1089 * debugging existing tests in order to introduce delays in the test steps
   1090 *
   1091 * @param {number} ms
   1092 *        The time to wait
   1093 * @return A promise that resolves when the time is passed
   1094 */
   1095 function wait(ms) {
   1096  return new Promise(resolve => {
   1097    setTimeout(resolve, ms);
   1098    info("Waiting " + ms / 1000 + " seconds.");
   1099  });
   1100 }
   1101 
   1102 /**
   1103 * Wait for a predicate to return a result.
   1104 *
   1105 * @param function condition
   1106 *        Invoked once in a while until it returns a truthy value. This should be an
   1107 *        idempotent function, since we have to run it a second time after it returns
   1108 *        true in order to return the value.
   1109 * @param string message [optional]
   1110 *        A message to output if the condition fails.
   1111 * @param number interval [optional]
   1112 *        How often the predicate is invoked, in milliseconds.
   1113 *        Can be set globally for a test via `waitFor.overrideIntervalForTestFile = someNumber;`.
   1114 * @param number maxTries [optional]
   1115 *        How many times the predicate is invoked before timing out.
   1116 *        Can be set globally for a test via `waitFor.overrideMaxTriesForTestFile = someNumber;`.
   1117 * @param boolean expectTimeout [optional]
   1118 *        Whether the helper is expected to reach the timeout or not. Consider
   1119 *        using waitForTimeout instead of passing this to true.
   1120 * @return object
   1121 *         A promise that is resolved with the result of the condition.
   1122 */
   1123 async function waitFor(
   1124  condition,
   1125  message = "",
   1126  interval = 10,
   1127  maxTries = 500,
   1128  expectTimeout = false
   1129 ) {
   1130  // Update interval & maxTries if overrides are defined on the waitFor object.
   1131  interval =
   1132    typeof waitFor.overrideIntervalForTestFile !== "undefined"
   1133      ? waitFor.overrideIntervalForTestFile
   1134      : interval;
   1135  maxTries =
   1136    typeof waitFor.overrideMaxTriesForTestFile !== "undefined"
   1137      ? waitFor.overrideMaxTriesForTestFile
   1138      : maxTries;
   1139 
   1140  try {
   1141    const value = await BrowserTestUtils.waitForCondition(
   1142      condition,
   1143      message,
   1144      interval,
   1145      maxTries
   1146    );
   1147    if (expectTimeout) {
   1148      // If we expected a timeout, fail the test here.
   1149      const errorMessage = `Expected timeout in waitFor(): ${message} \nUnexpected condition: ${condition} \n`;
   1150      ok(false, errorMessage);
   1151    }
   1152    return value;
   1153  } catch (e) {
   1154    if (expectTimeout) {
   1155      // If we expected a timeout, simply return null, the consumer should not
   1156      // need any return value.
   1157      return null;
   1158    }
   1159 
   1160    // If we didn't expect a timeout, fail the test here.
   1161    const errorMessage = `Failed waitFor(): ${message} \nFailed condition: ${condition} \nException Message: ${e}`;
   1162    // Use both assert ok(false) and throw.
   1163    // Assert captures the correct frame to log variables with dump-scope.js.
   1164    // Error will make sure the test stops.
   1165    ok(false, errorMessage);
   1166    throw new Error(errorMessage);
   1167  }
   1168 }
   1169 
   1170 /**
   1171 * Similar to @see waitFor defined above but expect the predicate to never
   1172 * be satisfied and therefore to timeout.
   1173 *
   1174 * Arguments are identical to `waitFor` but the default values for interval and
   1175 * maxTries are more conservative since this method expects a timeout.
   1176 *
   1177 */
   1178 async function waitForTimeout(
   1179  condition,
   1180  message = "",
   1181  interval = 100,
   1182  maxTries = 10
   1183 ) {
   1184  return waitFor(condition, message, interval, maxTries, true);
   1185 }
   1186 
   1187 /**
   1188 * Wait for eventName on target to be delivered a number of times.
   1189 *
   1190 * @param {object} target
   1191 *        An observable object that either supports on/off or
   1192 *        addEventListener/removeEventListener
   1193 * @param {string} eventName
   1194 * @param {number} numTimes
   1195 *        Number of deliveries to wait for.
   1196 * @param {boolean} useCapture
   1197 *        Optional, for addEventListener/removeEventListener
   1198 * @return A promise that resolves when the event has been handled
   1199 */
   1200 function waitForNEvents(target, eventName, numTimes, useCapture = false) {
   1201  info("Waiting for event: '" + eventName + "' on " + target + ".");
   1202 
   1203  let count = 0;
   1204 
   1205  return new Promise(resolve => {
   1206    for (const [add, remove] of [
   1207      ["on", "off"],
   1208      ["addEventListener", "removeEventListener"],
   1209      ["addListener", "removeListener"],
   1210      ["addMessageListener", "removeMessageListener"],
   1211    ]) {
   1212      if (add in target && remove in target) {
   1213        target[add](
   1214          eventName,
   1215          function onEvent(...args) {
   1216            if (typeof info === "function") {
   1217              info("Got event: '" + eventName + "' on " + target + ".");
   1218            }
   1219 
   1220            if (++count == numTimes) {
   1221              target[remove](eventName, onEvent, useCapture);
   1222              resolve(...args);
   1223            }
   1224          },
   1225          useCapture
   1226        );
   1227        break;
   1228      }
   1229    }
   1230  });
   1231 }
   1232 
   1233 /**
   1234 * Wait for DOM change on target.
   1235 *
   1236 * @param {object} target
   1237 *        The Node on which to observe DOM mutations.
   1238 * @param {string} selector
   1239 *        Given a selector to watch whether the expected element is changed
   1240 *        on target.
   1241 * @param {number} expectedLength
   1242 *        Optional, default set to 1
   1243 *        There may be more than one element match an array match the selector,
   1244 *        give an expected length to wait for more elements.
   1245 * @return A promise that resolves when the event has been handled
   1246 */
   1247 function waitForDOM(target, selector, expectedLength = 1) {
   1248  return new Promise(resolve => {
   1249    const observer = new MutationObserver(mutations => {
   1250      mutations.forEach(mutation => {
   1251        const elements = mutation.target.querySelectorAll(selector);
   1252 
   1253        if (elements.length === expectedLength) {
   1254          observer.disconnect();
   1255          resolve(elements);
   1256        }
   1257      });
   1258    });
   1259 
   1260    observer.observe(target, {
   1261      attributes: true,
   1262      childList: true,
   1263      subtree: true,
   1264    });
   1265  });
   1266 }
   1267 
   1268 /**
   1269 * Wait for eventName on target.
   1270 *
   1271 * @param {object} target
   1272 *        An observable object that either supports on/off or
   1273 *        addEventListener/removeEventListener
   1274 * @param {string} eventName
   1275 * @param {boolean} useCapture
   1276 *        Optional, for addEventListener/removeEventListener
   1277 * @return A promise that resolves when the event has been handled
   1278 */
   1279 function once(target, eventName, useCapture = false) {
   1280  return waitForNEvents(target, eventName, 1, useCapture);
   1281 }
   1282 
   1283 /**
   1284 * Some tests may need to import one or more of the test helper scripts.
   1285 * A test helper script is simply a js file that contains common test code that
   1286 * is either not common-enough to be in head.js, or that is located in a
   1287 * separate directory.
   1288 * The script will be loaded synchronously and in the test's scope.
   1289 *
   1290 * @param {string} filePath The file path, relative to the current directory.
   1291 *                 Examples:
   1292 *                 - "helper_attributes_test_runner.js"
   1293 */
   1294 function loadHelperScript(filePath) {
   1295  const testDir = gTestPath.substr(0, gTestPath.lastIndexOf("/"));
   1296  Services.scriptloader.loadSubScript(testDir + "/" + filePath, this);
   1297 }
   1298 
   1299 /**
   1300 * Open the toolbox in a given tab.
   1301 *
   1302 * @param {XULNode} tab The tab the toolbox should be opened in.
   1303 * @param {string} toolId Optional. The ID of the tool to be selected.
   1304 * @param {string} hostType Optional. The type of toolbox host to be used.
   1305 * @return {Promise} Resolves with the toolbox, when it has been opened.
   1306 */
   1307 async function openToolboxForTab(tab, toolId, hostType) {
   1308  info("Opening the toolbox");
   1309 
   1310  // Check if the toolbox is already loaded.
   1311  let toolbox = gDevTools.getToolboxForTab(tab);
   1312  if (toolbox) {
   1313    if (!toolId || (toolId && toolbox.getPanel(toolId))) {
   1314      info("Toolbox is already opened");
   1315      return toolbox;
   1316    }
   1317  }
   1318 
   1319  // If not, load it now.
   1320  toolbox = await gDevTools.showToolboxForTab(tab, { toolId, hostType });
   1321 
   1322  // Make sure that the toolbox frame is focused.
   1323  await new Promise(resolve => waitForFocus(resolve, toolbox.win));
   1324 
   1325  info("Toolbox opened and focused");
   1326 
   1327  return toolbox;
   1328 }
   1329 
   1330 /**
   1331 * Add a new tab and open the toolbox in it.
   1332 *
   1333 * @param {string} url The URL for the tab to be opened.
   1334 * @param {string} toolId Optional. The ID of the tool to be selected.
   1335 * @param {string} hostType Optional. The type of toolbox host to be used.
   1336 * @return {Promise} Resolves when the tab has been added, loaded and the
   1337 * toolbox has been opened. Resolves to the toolbox.
   1338 */
   1339 async function openNewTabAndToolbox(url, toolId, hostType) {
   1340  const tab = await addTab(url);
   1341  return openToolboxForTab(tab, toolId, hostType);
   1342 }
   1343 
   1344 /**
   1345 * Close a tab and if necessary, the toolbox that belongs to it
   1346 *
   1347 * @param {Tab} tab The tab to close.
   1348 * @return {Promise} Resolves when the toolbox and tab have been destroyed and
   1349 * closed.
   1350 */
   1351 async function closeTabAndToolbox(tab = gBrowser.selectedTab) {
   1352  if (gDevTools.hasToolboxForTab(tab)) {
   1353    await gDevTools.closeToolboxForTab(tab);
   1354  }
   1355 
   1356  await removeTab(tab);
   1357 
   1358  await new Promise(resolve => setTimeout(resolve, 0));
   1359 }
   1360 
   1361 /**
   1362 * Close a toolbox and the current tab.
   1363 *
   1364 * @param {Toolbox} toolbox The toolbox to close.
   1365 * @return {Promise} Resolves when the toolbox and tab have been destroyed and
   1366 * closed.
   1367 */
   1368 async function closeToolboxAndTab(toolbox) {
   1369  await toolbox.destroy();
   1370  await removeTab(gBrowser.selectedTab);
   1371 }
   1372 
   1373 /**
   1374 * Waits until a predicate returns true.
   1375 *
   1376 * @param function predicate
   1377 *        Invoked once in a while until it returns true.
   1378 * @param number interval [optional]
   1379 *        How often the predicate is invoked, in milliseconds.
   1380 */
   1381 function waitUntil(predicate, interval = 10) {
   1382  if (predicate()) {
   1383    return Promise.resolve(true);
   1384  }
   1385  return new Promise(resolve => {
   1386    setTimeout(function () {
   1387      waitUntil(predicate, interval).then(() => resolve(true));
   1388    }, interval);
   1389  });
   1390 }
   1391 
   1392 /**
   1393 * Variant of waitUntil that accepts a predicate returning a promise.
   1394 */
   1395 async function asyncWaitUntil(predicate, interval = 10) {
   1396  let success = await predicate();
   1397  while (!success) {
   1398    // Wait for X milliseconds.
   1399    await new Promise(resolve => setTimeout(resolve, interval));
   1400    // Test the predicate again.
   1401    success = await predicate();
   1402  }
   1403 }
   1404 
   1405 /**
   1406 * Wait for a context menu popup to open.
   1407 *
   1408 * @param Element popup
   1409 *        The XUL popup you expect to open.
   1410 * @param Element button
   1411 *        The button/element that receives the contextmenu event. This is
   1412 *        expected to open the popup.
   1413 * @param function onShown
   1414 *        Function to invoke on popupshown event.
   1415 * @param function onHidden
   1416 *        Function to invoke on popuphidden event.
   1417 * @return object
   1418 *         A Promise object that is resolved after the popuphidden event
   1419 *         callback is invoked.
   1420 */
   1421 function waitForContextMenu(popup, button, onShown, onHidden) {
   1422  return new Promise(resolve => {
   1423    function onPopupShown() {
   1424      info("onPopupShown");
   1425      popup.removeEventListener("popupshown", onPopupShown);
   1426 
   1427      onShown && onShown();
   1428 
   1429      // Use executeSoon() to get out of the popupshown event.
   1430      popup.addEventListener("popuphidden", onPopupHidden);
   1431      DevToolsUtils.executeSoon(() => popup.hidePopup());
   1432    }
   1433    function onPopupHidden() {
   1434      info("onPopupHidden");
   1435      popup.removeEventListener("popuphidden", onPopupHidden);
   1436 
   1437      onHidden && onHidden();
   1438 
   1439      resolve(popup);
   1440    }
   1441 
   1442    popup.addEventListener("popupshown", onPopupShown);
   1443 
   1444    info("wait for the context menu to open");
   1445    synthesizeContextMenuEvent(button);
   1446  });
   1447 }
   1448 
   1449 function synthesizeContextMenuEvent(el) {
   1450  el.scrollIntoView();
   1451  const eventDetails = { type: "contextmenu", button: 2 };
   1452  EventUtils.synthesizeMouse(
   1453    el,
   1454    5,
   1455    2,
   1456    eventDetails,
   1457    el.ownerDocument.defaultView
   1458  );
   1459 }
   1460 
   1461 /**
   1462 * Promise wrapper around SimpleTest.waitForClipboard
   1463 */
   1464 function waitForClipboardPromise(setup, expected) {
   1465  return new Promise((resolve, reject) => {
   1466    SimpleTest.waitForClipboard(expected, setup, resolve, reject);
   1467  });
   1468 }
   1469 
   1470 /**
   1471 * Simple helper to push a temporary preference. Wrapper on SpecialPowers
   1472 * pushPrefEnv that returns a promise resolving when the preferences have been
   1473 * updated.
   1474 *
   1475 * @param {string} preferenceName
   1476 *        The name of the preference to updated
   1477 * @param {} value
   1478 *        The preference value, type can vary
   1479 * @return {Promise} resolves when the preferences have been updated
   1480 */
   1481 function pushPref(preferenceName, value) {
   1482  const options = { set: [[preferenceName, value]] };
   1483  return SpecialPowers.pushPrefEnv(options);
   1484 }
   1485 
   1486 /**
   1487 * Close the toolbox for the selected tab if needed.
   1488 */
   1489 async function closeToolboxIfOpen() {
   1490  // `closeToolboxForTab` will be a noop if the selected tab does not have any
   1491  // toolbox.
   1492  await gDevTools.closeToolboxForTab(gBrowser.selectedTab);
   1493 }
   1494 
   1495 /**
   1496 * Clean the logical clipboard content. This method only clears the OS clipboard on
   1497 * Windows (see Bug 666254).
   1498 */
   1499 function emptyClipboard() {
   1500  const clipboard = Services.clipboard;
   1501  clipboard.emptyClipboard(clipboard.kGlobalClipboard);
   1502 }
   1503 
   1504 /**
   1505 * Check if the current operating system is Windows.
   1506 */
   1507 function isWindows() {
   1508  return Services.appinfo.OS === "WINNT";
   1509 }
   1510 
   1511 /**
   1512 * Create an HTTP server that can be used to simulate custom requests within
   1513 * a test.  It is automatically cleaned up when the test ends, so no need to
   1514 * call `destroy`.
   1515 *
   1516 * See https://developer.mozilla.org/en-US/docs/Httpd.js/HTTP_server_for_unit_tests
   1517 * for more information about how to register handlers.
   1518 *
   1519 * The server can be accessed like:
   1520 *
   1521 * ```js
   1522 * const server = createTestHTTPServer();
   1523 * let url = "http://localhost: " + server.identity.primaryPort + "/path";
   1524 * ```
   1525 *
   1526 * @returns {HttpServer}
   1527 */
   1528 function createTestHTTPServer() {
   1529  const { HttpServer } = ChromeUtils.importESModule(
   1530    "resource://testing-common/httpd.sys.mjs"
   1531  );
   1532  const server = new HttpServer();
   1533 
   1534  registerCleanupFunction(async function cleanup() {
   1535    await new Promise(resolve => server.stop(resolve));
   1536  });
   1537 
   1538  server.start(-1);
   1539  return server;
   1540 }
   1541 
   1542 /**
   1543 * Register an actor in the content process of the current tab.
   1544 *
   1545 * Calling ActorRegistry.registerModule only registers the actor in the current process.
   1546 * As all test scripts are ran in the parent process, it is only registered here.
   1547 * This function helps register them in the content process used for the current tab.
   1548 *
   1549 * @param {string} url
   1550 *        Actor module URL or absolute require path
   1551 * @param {json} options
   1552 *        Arguments to be passed to DevToolsServer.registerModule
   1553 */
   1554 async function registerActorInContentProcess(url, options) {
   1555  function convertChromeToFile(uri) {
   1556    return Cc["@mozilla.org/chrome/chrome-registry;1"]
   1557      .getService(Ci.nsIChromeRegistry)
   1558      .convertChromeURL(Services.io.newURI(uri)).spec;
   1559  }
   1560  // chrome://mochitests URI is registered only in the parent process, so convert these
   1561  // URLs to file:// one in order to work in the content processes
   1562  url = url.startsWith("chrome://mochitests") ? convertChromeToFile(url) : url;
   1563  return SpecialPowers.spawn(
   1564    gBrowser.selectedBrowser,
   1565    [{ url, options }],
   1566    args => {
   1567      // eslint-disable-next-line no-shadow
   1568      const { require } = ChromeUtils.importESModule(
   1569        "resource://devtools/shared/loader/Loader.sys.mjs"
   1570      );
   1571      const {
   1572        ActorRegistry,
   1573      } = require("resource://devtools/server/actors/utils/actor-registry.js");
   1574      ActorRegistry.registerModule(args.url, args.options);
   1575    }
   1576  );
   1577 }
   1578 
   1579 /**
   1580 * Move the provided Window to the provided left, top coordinates and wait for
   1581 * the window position to be updated.
   1582 */
   1583 async function moveWindowTo(win, left, top) {
   1584  // Check that the expected coordinates are within the window available area.
   1585  left = Math.max(win.screen.availLeft, left);
   1586  left = Math.min(win.screen.width, left);
   1587  top = Math.max(win.screen.availTop, top);
   1588  top = Math.min(win.screen.height, top);
   1589 
   1590  info(`Moving window to {${left}, ${top}}`);
   1591  win.moveTo(left, top);
   1592 
   1593  // Bug 1600809: window move/resize can be async on Linux sometimes.
   1594  // Wait so that the anchor's position is correctly measured.
   1595  return waitUntil(() => {
   1596    info(
   1597      `Wait for window screenLeft and screenTop to be updated: (${win.screenLeft}, ${win.screenTop})`
   1598    );
   1599    return win.screenLeft === left && win.screenTop === top;
   1600  });
   1601 }
   1602 
   1603 function getCurrentTestFilePath() {
   1604  return gTestPath.replace("chrome://mochitests/content/browser/", "");
   1605 }
   1606 
   1607 /**
   1608 * Unregister all registered service workers.
   1609 *
   1610 * @param {DevToolsClient} client
   1611 */
   1612 async function unregisterAllServiceWorkers(client) {
   1613  info("Wait until all workers have a valid registrationFront");
   1614  let workers;
   1615  await asyncWaitUntil(async function () {
   1616    workers = await client.mainRoot.listAllWorkers();
   1617    const allWorkersRegistered = workers.service.every(
   1618      worker => !!worker.registrationFront
   1619    );
   1620    return allWorkersRegistered;
   1621  });
   1622 
   1623  info("Unregister all service workers");
   1624  const promises = [];
   1625  for (const worker of workers.service) {
   1626    promises.push(worker.registrationFront.unregister());
   1627  }
   1628  await Promise.all(promises);
   1629 }
   1630 
   1631 /**********************
   1632 * Screenshot helpers *
   1633 **********************/
   1634 
   1635 /**
   1636 * Returns an object containing the r,g and b colors of the provided image at
   1637 * the passed position
   1638 *
   1639 * @param {Image} image
   1640 * @param {Int} x
   1641 * @param {Int} y
   1642 * @returns Object with the following properties:
   1643 *           - {Int} r: The red component of the pixel
   1644 *           - {Int} g: The green component of the pixel
   1645 *           - {Int} b: The blue component of the pixel
   1646 */
   1647 function colorAt(image, x, y) {
   1648  // Create a test canvas element.
   1649  const HTML_NS = "http://www.w3.org/1999/xhtml";
   1650  const canvas = document.createElementNS(HTML_NS, "canvas");
   1651  canvas.width = image.width;
   1652  canvas.height = image.height;
   1653 
   1654  // Draw the image in the canvas
   1655  const context = canvas.getContext("2d");
   1656  context.drawImage(image, 0, 0, image.width, image.height);
   1657 
   1658  // Return the color found at the provided x,y coordinates as a "r, g, b" string.
   1659  const [r, g, b] = context.getImageData(x, y, 1, 1).data;
   1660  return { r, g, b };
   1661 }
   1662 
   1663 let allDownloads = [];
   1664 /**
   1665 * Returns a Promise that resolves when a new file (e.g. screenshot, JSON, …) is available
   1666 * in the download folder.
   1667 *
   1668 * @param {object} [options]
   1669 * @param {boolean} options.isWindowPrivate: Set to true if the window from which the file
   1670 *                  is downloaded is a private window. This will ensure that we check that the
   1671 *                  file appears in the private window, not the non-private one (See Bug 1783373)
   1672 */
   1673 async function waitUntilDownload({ isWindowPrivate = false } = {}) {
   1674  const { Downloads } = ChromeUtils.importESModule(
   1675    "resource://gre/modules/Downloads.sys.mjs"
   1676  );
   1677  const list = await Downloads.getList(Downloads.ALL);
   1678 
   1679  return new Promise(function (resolve) {
   1680    const view = {
   1681      onDownloadAdded: async download => {
   1682        await download.whenSucceeded();
   1683        if (allDownloads.includes(download)) {
   1684          return;
   1685        }
   1686 
   1687        is(
   1688          !!download.source.isPrivate,
   1689          isWindowPrivate,
   1690          `The download occured in the expected${
   1691            isWindowPrivate ? " private" : ""
   1692          } window`
   1693        );
   1694 
   1695        allDownloads.push(download);
   1696        resolve(download.target.path);
   1697        list.removeView(view);
   1698      },
   1699    };
   1700 
   1701    list.addView(view);
   1702  });
   1703 }
   1704 
   1705 /**
   1706 * Clear all the download references.
   1707 */
   1708 async function resetDownloads() {
   1709  info("Reset downloads");
   1710  const { Downloads } = ChromeUtils.importESModule(
   1711    "resource://gre/modules/Downloads.sys.mjs"
   1712  );
   1713  const downloadList = await Downloads.getList(Downloads.ALL);
   1714  const downloads = await downloadList.getAll();
   1715  for (const download of downloads) {
   1716    downloadList.remove(download);
   1717    await download.finalize(true);
   1718  }
   1719  allDownloads = [];
   1720 }
   1721 
   1722 /**
   1723 * Return a screenshot of the currently selected node in the inspector (using the internal
   1724 * Inspector#screenshotNode method).
   1725 *
   1726 * @param {Inspector} inspector
   1727 * @returns {Image}
   1728 */
   1729 async function takeNodeScreenshot(inspector) {
   1730  // Cleanup all downloads at the end of the test.
   1731  registerCleanupFunction(resetDownloads);
   1732 
   1733  info(
   1734    "Call screenshotNode() and wait until the screenshot is found in the Downloads"
   1735  );
   1736  const whenScreenshotSucceeded = waitUntilDownload();
   1737  inspector.screenshotNode();
   1738  const filePath = await whenScreenshotSucceeded;
   1739 
   1740  info("Create an image using the downloaded fileas source");
   1741  const image = new Image();
   1742  const onImageLoad = once(image, "load");
   1743  image.src = PathUtils.toFileURI(filePath);
   1744  await onImageLoad;
   1745 
   1746  info("Remove the downloaded screenshot file");
   1747  await IOUtils.remove(filePath);
   1748 
   1749  // See intermittent Bug 1508435. Even after removing the file, tests still manage to
   1750  // reuse files from the previous test if they have the same name. Since our file name
   1751  // is based on a timestamp that has "second" precision, wait for one second to make sure
   1752  // screenshots will have different names.
   1753  info(
   1754    "Wait for one second to make sure future screenshots will use a different name"
   1755  );
   1756  await new Promise(r => setTimeout(r, 1000));
   1757 
   1758  return image;
   1759 }
   1760 
   1761 /**
   1762 * Check that the provided image has the expected width, height, and color.
   1763 * NOTE: This test assumes that the image is only made of a single color and will only
   1764 * check one pixel.
   1765 */
   1766 async function assertSingleColorScreenshotImage(
   1767  image,
   1768  width,
   1769  height,
   1770  { r, g, b }
   1771 ) {
   1772  info(`Assert ${image.src} content`);
   1773  const ratio = await SpecialPowers.spawn(
   1774    gBrowser.selectedBrowser,
   1775    [],
   1776    () => content.wrappedJSObject.devicePixelRatio
   1777  );
   1778 
   1779  is(
   1780    image.width,
   1781    ratio * width,
   1782    `node screenshot has the expected width (dpr = ${ratio})`
   1783  );
   1784  is(
   1785    image.height,
   1786    height * ratio,
   1787    `node screenshot has the expected height (dpr = ${ratio})`
   1788  );
   1789 
   1790  const color = colorAt(image, 0, 0);
   1791  is(color.r, r, "node screenshot has the expected red component");
   1792  is(color.g, g, "node screenshot has the expected green component");
   1793  is(color.b, b, "node screenshot has the expected blue component");
   1794 }
   1795 
   1796 /**
   1797 * Check that the provided image has the expected color at a given position
   1798 */
   1799 function checkImageColorAt({ image, x = 0, y, expectedColor, label }) {
   1800  const color = colorAt(image, x, y);
   1801  is(`rgb(${Object.values(color).join(", ")})`, expectedColor, label);
   1802 }
   1803 
   1804 /**
   1805 * Wait until the store has reached a state that matches the predicate.
   1806 *
   1807 * @param Store store
   1808 *        The Redux store being used.
   1809 * @param function predicate
   1810 *        A function that returns true when the store has reached the expected
   1811 *        state.
   1812 * @return Promise
   1813 *         Resolved once the store reaches the expected state.
   1814 */
   1815 function waitUntilState(store, predicate) {
   1816  return new Promise(resolve => {
   1817    const unsubscribe = store.subscribe(check);
   1818 
   1819    info(`Waiting for state predicate "${predicate}"`);
   1820    function check() {
   1821      if (predicate(store.getState())) {
   1822        info(`Found state predicate "${predicate}"`);
   1823        unsubscribe();
   1824        resolve();
   1825      }
   1826    }
   1827 
   1828    // Fire the check immediately in case the action has already occurred
   1829    check();
   1830  });
   1831 }
   1832 
   1833 /**
   1834 * Wait for a specific action type to be dispatched.
   1835 *
   1836 * If the action is async and defines a `status` property, this helper will wait
   1837 * for the status to reach either "error" or "done".
   1838 *
   1839 * @param {object} store
   1840 *        Redux store where the action should be dispatched.
   1841 * @param {string} actionType
   1842 *        The actionType to wait for.
   1843 * @param {number} repeat
   1844 *        Optional, number of time the action is expected to be dispatched.
   1845 *        Defaults to 1
   1846 * @return {Promise}
   1847 */
   1848 function waitForDispatch(store, actionType, repeat = 1) {
   1849  let count = 0;
   1850  return new Promise(resolve => {
   1851    store.dispatch({
   1852      type: "@@service/waitUntil",
   1853      predicate: action => {
   1854        const isDone =
   1855          !action.status ||
   1856          action.status === "done" ||
   1857          action.status === "error";
   1858 
   1859        if (action.type === actionType && isDone && ++count == repeat) {
   1860          return true;
   1861        }
   1862 
   1863        return false;
   1864      },
   1865      run: (dispatch, getState, action) => {
   1866        resolve(action);
   1867      },
   1868    });
   1869  });
   1870 }
   1871 
   1872 /**
   1873 * Retrieve a browsing context in nested frames.
   1874 *
   1875 * @param {BrowsingContext|XULBrowser} browsingContext
   1876 *        The topmost browsing context under which we should search for the
   1877 *        browsing context.
   1878 * @param {Array<string>} selectors
   1879 *        Array of CSS selectors that form a path to a specific nested frame.
   1880 * @return {BrowsingContext} The nested browsing context.
   1881 */
   1882 async function getBrowsingContextInFrames(browsingContext, selectors) {
   1883  let context = browsingContext;
   1884 
   1885  if (!Array.isArray(selectors)) {
   1886    throw new Error(
   1887      "getBrowsingContextInFrames called with an invalid selectors argument"
   1888    );
   1889  }
   1890 
   1891  if (selectors.length === 0) {
   1892    throw new Error(
   1893      "getBrowsingContextInFrames called with an empty selectors array"
   1894    );
   1895  }
   1896 
   1897  const clonedSelectors = [...selectors];
   1898  while (clonedSelectors.length) {
   1899    const selector = clonedSelectors.shift();
   1900    context = await SpecialPowers.spawn(context, [selector], _selector => {
   1901      return content.document.querySelector(_selector).browsingContext;
   1902    });
   1903  }
   1904 
   1905  return context;
   1906 }
   1907 
   1908 /**
   1909 * Synthesize a mouse event on an element, after ensuring that it is visible
   1910 * in the viewport.
   1911 *
   1912 * @param {string | Array} selector: The node selector to get the node target for the event.
   1913 *        To target an element in a specific iframe, pass an array of CSS selectors
   1914 *        (e.g. ["iframe", ".el-in-iframe"])
   1915 * @param {number} x
   1916 * @param {number} y
   1917 * @param {object} options: Options that will be passed to BrowserTestUtils.synthesizeMouse
   1918 */
   1919 async function safeSynthesizeMouseEventInContentPage(
   1920  selector,
   1921  x,
   1922  y,
   1923  options = {}
   1924 ) {
   1925  let context = gBrowser.selectedBrowser.browsingContext;
   1926 
   1927  // If an array of selector is passed, we need to retrieve the context in which the node
   1928  // lives in.
   1929  if (Array.isArray(selector)) {
   1930    if (selector.length === 1) {
   1931      selector = selector[0];
   1932    } else {
   1933      context = await getBrowsingContextInFrames(
   1934        context,
   1935        // only pass the iframe path
   1936        selector.slice(0, -1)
   1937      );
   1938      // retrieve the last item of the selector, which should be the one for the node we want.
   1939      selector = selector.at(-1);
   1940    }
   1941  }
   1942 
   1943  await scrollContentPageNodeIntoView(context, selector);
   1944  BrowserTestUtils.synthesizeMouse(selector, x, y, options, context);
   1945 }
   1946 
   1947 /**
   1948 * Synthesize a mouse event at the center of an element, after ensuring that it is visible
   1949 * in the viewport.
   1950 *
   1951 * @param {string | Array} selector: The node selector to get the node target for the event.
   1952 *        To target an element in a specific iframe, pass an array of CSS selectors
   1953 *        (e.g. ["iframe", ".el-in-iframe"])
   1954 * @param {object} options: Options that will be passed to BrowserTestUtils.synthesizeMouse
   1955 */
   1956 async function safeSynthesizeMouseEventAtCenterInContentPage(
   1957  selector,
   1958  options = {}
   1959 ) {
   1960  let context = gBrowser.selectedBrowser.browsingContext;
   1961 
   1962  // If an array of selector is passed, we need to retrieve the context in which the node
   1963  // lives in.
   1964  if (Array.isArray(selector)) {
   1965    if (selector.length === 1) {
   1966      selector = selector[0];
   1967    } else {
   1968      context = await getBrowsingContextInFrames(
   1969        context,
   1970        // only pass the iframe path
   1971        selector.slice(0, -1)
   1972      );
   1973      // retrieve the last item of the selector, which should be the one for the node we want.
   1974      selector = selector.at(-1);
   1975    }
   1976  }
   1977 
   1978  await scrollContentPageNodeIntoView(context, selector);
   1979  BrowserTestUtils.synthesizeMouseAtCenter(selector, options, context);
   1980 }
   1981 
   1982 /**
   1983 * Scroll into view an element in the content page matching the passed selector
   1984 *
   1985 * @param {BrowsingContext} browsingContext: The browsing context the element lives in.
   1986 * @param {string} selector: The node selector to get the node to scroll into view
   1987 * @returns {Promise}
   1988 */
   1989 function scrollContentPageNodeIntoView(browsingContext, selector) {
   1990  return SpecialPowers.spawn(
   1991    browsingContext,
   1992    [selector],
   1993    function (innerSelector) {
   1994      const node =
   1995        content.wrappedJSObject.document.querySelector(innerSelector);
   1996      node.scrollIntoView();
   1997    }
   1998  );
   1999 }
   2000 
   2001 /**
   2002 * Change the zoom level of the selected page.
   2003 *
   2004 * @param {number} zoomLevel
   2005 */
   2006 function setContentPageZoomLevel(zoomLevel) {
   2007  gBrowser.selectedBrowser.fullZoom = zoomLevel;
   2008 }
   2009 
   2010 /**
   2011 * Wait for the next DOCUMENT_EVENT dom-complete resource on a top-level target
   2012 *
   2013 * @param {object} commands
   2014 * @return {Promise<object>}
   2015 *         Return a promise which resolves once we fully settle the resource listener.
   2016 *         You should await for its resolution before doing the action which may fire
   2017 *         your resource.
   2018 *         This promise will resolve with an object containing a `onDomCompleteResource` property,
   2019 *         which is also a promise, that will resolve once a "top-level" DOCUMENT_EVENT dom-complete
   2020 *         is received.
   2021 */
   2022 async function waitForNextTopLevelDomCompleteResource(commands) {
   2023  const { onResource: onDomCompleteResource } =
   2024    await commands.resourceCommand.waitForNextResource(
   2025      commands.resourceCommand.TYPES.DOCUMENT_EVENT,
   2026      {
   2027        ignoreExistingResources: true,
   2028        predicate: resource =>
   2029          resource.name === "dom-complete" && resource.targetFront.isTopLevel,
   2030      }
   2031    );
   2032  return { onDomCompleteResource };
   2033 }
   2034 
   2035 /**
   2036 * Wait for the provided context to have a valid presShell. This can be useful
   2037 * for tests which try to create popup panels or interact with the document very
   2038 * early.
   2039 *
   2040 * @param {BrowsingContext} context
   2041 */
   2042 function waitForPresShell(context) {
   2043  return SpecialPowers.spawn(context, [], async () => {
   2044    const winUtils = SpecialPowers.getDOMWindowUtils(content);
   2045    await ContentTaskUtils.waitForCondition(() => {
   2046      try {
   2047        return !!winUtils.getPresShellId();
   2048      } catch (e) {
   2049        return false;
   2050      }
   2051    }, "Waiting for a valid presShell");
   2052  });
   2053 }
   2054 
   2055 /**
   2056 * In tests using Fluent localization, it is preferable to match DOM elements using
   2057 * a message ID rather than the raw string as:
   2058 *
   2059 *  1. It allows testing infrastructure to be multilingual if needed.
   2060 *  2. It isolates the tests from localization changes.
   2061 *
   2062 * @param {Array<string>} resourceIds A list of .ftl files to load.
   2063 * @returns {(id: string, args?: Record<string, FluentVariable>) => string}
   2064 */
   2065 async function getFluentStringHelper(resourceIds) {
   2066  const locales = Services.locale.appLocalesAsBCP47;
   2067  const generator = L10nRegistry.getInstance().generateBundles(
   2068    locales,
   2069    resourceIds
   2070  );
   2071 
   2072  const bundles = [];
   2073  for await (const bundle of generator) {
   2074    bundles.push(bundle);
   2075  }
   2076 
   2077  const reactLocalization = new FluentReact.ReactLocalization(bundles);
   2078 
   2079  /**
   2080   * Get the string from a message id. It throws when the message is not found.
   2081   *
   2082   * @param {string} id
   2083   * @param {string} attributeName: attribute name if you need to access a specific attribute
   2084   *                 defined in the fluent string, e.g. setting "title" for this param
   2085   *                 will retrieve the `title` string in
   2086   *                    compatibility-issue-browsers-list =
   2087   *                      .title = This is the title
   2088   * @param {Record<string, FluentVariable>} [args] optional
   2089   * @returns {string}
   2090   */
   2091  return (id, attributeName, args) => {
   2092    let string;
   2093 
   2094    if (!attributeName) {
   2095      string = reactLocalization.getString(id, args);
   2096    } else {
   2097      for (const bundle of reactLocalization.bundles) {
   2098        const msg = bundle.getMessage(id);
   2099        if (msg?.attributes[attributeName]) {
   2100          string = bundle.formatPattern(
   2101            msg.attributes[attributeName],
   2102            args,
   2103            []
   2104          );
   2105          break;
   2106        }
   2107      }
   2108    }
   2109 
   2110    if (!string) {
   2111      throw new Error(
   2112        `Could not find a string for "${id}"${
   2113          attributeName ? ` and attribute "${attributeName}")` : ""
   2114        }. Was the correct resource bundle loaded?`
   2115      );
   2116    }
   2117    return string;
   2118  };
   2119 }
   2120 
   2121 /**
   2122 * Open responsive design mode for the given tab.
   2123 */
   2124 async function openRDM(tab, { waitForDeviceList = true } = {}) {
   2125  info("Opening responsive design mode");
   2126  const manager = ResponsiveUIManager;
   2127  const ui = await manager.openIfNeeded(tab.ownerGlobal, tab, {
   2128    trigger: "test",
   2129  });
   2130  info("Responsive design mode opened");
   2131 
   2132  await ResponsiveMessageHelper.wait(ui.toolWindow, "post-init");
   2133  info("Responsive design initialized");
   2134 
   2135  await waitForRDMLoaded(ui, { waitForDeviceList });
   2136 
   2137  return { ui, manager };
   2138 }
   2139 
   2140 async function waitForRDMLoaded(ui, { waitForDeviceList = true } = {}) {
   2141  // Always wait for the viewport to be added.
   2142  const { store } = ui.toolWindow;
   2143  await waitUntilState(store, state => state.viewports.length == 1);
   2144 
   2145  if (waitForDeviceList) {
   2146    // Wait until the device list has been loaded.
   2147    await waitUntilState(
   2148      store,
   2149      state => state.devices.listState == localTypes.loadableState.LOADED
   2150    );
   2151  }
   2152 }
   2153 
   2154 /**
   2155 * Close responsive design mode for the given tab.
   2156 */
   2157 async function closeRDM(tab, options) {
   2158  info("Closing responsive design mode");
   2159  const manager = ResponsiveUIManager;
   2160  await manager.closeIfNeeded(tab.ownerGlobal, tab, options);
   2161  info("Responsive design mode closed");
   2162 }
   2163 
   2164 function getInputStream(data) {
   2165  const BufferStream = Components.Constructor(
   2166    "@mozilla.org/io/arraybuffer-input-stream;1",
   2167    "nsIArrayBufferInputStream",
   2168    "setData"
   2169  );
   2170  const buffer = new TextEncoder().encode(data).buffer;
   2171  return new BufferStream(buffer, 0, buffer.byteLength);
   2172 }
   2173 
   2174 /**
   2175 * Wait for a specific target to have been fully processed by targetCommand.
   2176 *
   2177 * @param {Commands} commands
   2178 *        The commands instance
   2179 * @param {Function} isExpectedTargetFn
   2180 *        Predicate which will be called with a target front argument. Should
   2181 *        return true if the target front is the expected one, false otherwise.
   2182 * @return {Promise}
   2183 *         Promise which resolves when a target matching `isExpectedTargetFn`
   2184 *         has been processed by targetCommand.
   2185 */
   2186 function waitForTargetProcessed(commands, isExpectedTargetFn) {
   2187  return new Promise(resolve => {
   2188    const onProcessed = targetFront => {
   2189      try {
   2190        if (isExpectedTargetFn(targetFront)) {
   2191          commands.targetCommand.off("processed-available-target", onProcessed);
   2192          resolve();
   2193        }
   2194      } catch {
   2195        // Ignore errors from isExpectedTargetFn.
   2196      }
   2197    };
   2198 
   2199    commands.targetCommand.on("processed-available-target", onProcessed);
   2200  });
   2201 }
   2202 
   2203 /**
   2204 * Instantiate a HTTP Server that serves files from a given test folder.
   2205 * The test folder should be made of multiple sub folder named: v1, v2, v3,...
   2206 * We will serve the content from one of these sub folder
   2207 * and switch to the next one, each time `httpServer.switchToNextVersion()`
   2208 * is called.
   2209 *
   2210 * @return Object Test server with two functions:
   2211 *   - urlFor(path)
   2212 *     Returns the absolute url for a given file.
   2213 *   - switchToNextVersion()
   2214 *     Start serving files from the next available sub folder.
   2215 *   - backToFirstVersion()
   2216 *     When running more than one test, helps restart from the first folder.
   2217 */
   2218 function createVersionizedHttpTestServer(testFolderName) {
   2219  const httpServer = createTestHTTPServer();
   2220 
   2221  let currentVersion = 1;
   2222 
   2223  httpServer.registerPrefixHandler("/", async (request, response) => {
   2224    response.processAsync();
   2225    response.setStatusLine(request.httpVersion, 200, "OK");
   2226    if (request.path.endsWith(".js")) {
   2227      response.setHeader("Content-Type", "application/javascript");
   2228    } else if (request.path.endsWith(".js.map")) {
   2229      response.setHeader("Content-Type", "application/json");
   2230    }
   2231    if (request.path == "/" || request.path.endsWith(".html")) {
   2232      response.setHeader("Content-Type", "text/html");
   2233    }
   2234    // If a query string is passed, lookup with a matching file, if available
   2235    // The '?' is replaced by '.'
   2236    let fetchResponse;
   2237 
   2238    if (request.queryString) {
   2239      const url = `${URL_ROOT_SSL}${testFolderName}/v${currentVersion}${request.path}.${request.queryString}`;
   2240      try {
   2241        fetchResponse = await fetch(url);
   2242        // Log this only if the request succeed
   2243        info(`[test-http-server] serving: ${url}`);
   2244      } catch (e) {
   2245        // Ignore any error and proceed without the query string
   2246        fetchResponse = null;
   2247      }
   2248    }
   2249 
   2250    if (!fetchResponse) {
   2251      const url = `${URL_ROOT_SSL}${testFolderName}/v${currentVersion}${request.path}`;
   2252      info(`[test-http-server] serving: ${url}`);
   2253      fetchResponse = await fetch(url);
   2254    }
   2255 
   2256    // Ensure forwarding the response headers generated by the other http server
   2257    // (this can be especially useful when query .sjs files)
   2258    for (const [name, value] of fetchResponse.headers.entries()) {
   2259      response.setHeader(name, value);
   2260    }
   2261 
   2262    // Override cache settings so that versionized requests are never cached
   2263    // and we get brand new content for any request.
   2264    response.setHeader("Cache-Control", "no-store");
   2265 
   2266    const text = await fetchResponse.text();
   2267    response.write(text);
   2268    response.finish();
   2269  });
   2270 
   2271  return {
   2272    switchToNextVersion() {
   2273      currentVersion++;
   2274    },
   2275    backToFirstVersion() {
   2276      currentVersion = 1;
   2277    },
   2278    urlFor(path) {
   2279      const port = httpServer.identity.primaryPort;
   2280      return `http://localhost:${port}/${path}`;
   2281    },
   2282  };
   2283 }
   2284 
   2285 /**
   2286 * Fake clicking a link and return the URL we would have navigated to.
   2287 * This function should be used to check external links since we can't access
   2288 * network in tests.
   2289 * This can also be used to test that a click will not be fired.
   2290 *
   2291 * @param ElementNode element
   2292 *        The <a> element we want to simulate click on.
   2293 * @returns Promise
   2294 *          A Promise that is resolved when the link click simulation occured or
   2295 *          when the click is not dispatched.
   2296 *          The promise resolves with an object that holds the following properties
   2297 *          - link: url of the link or null(if event not fired)
   2298 *          - where: "tab" if tab is active or "tabshifted" if tab is inactive
   2299 *            or null(if event not fired)
   2300 */
   2301 function simulateLinkClick(element) {
   2302  const browserWindow = Services.wm.getMostRecentWindow(
   2303    gDevTools.chromeWindowType
   2304  );
   2305 
   2306  const onOpenLink = new Promise(resolve => {
   2307    const openLinkIn = (link, where) => resolve({ link, where });
   2308    sinon.replace(browserWindow, "openTrustedLinkIn", openLinkIn);
   2309    sinon.replace(browserWindow, "openWebLinkIn", openLinkIn);
   2310  });
   2311 
   2312  element.click();
   2313 
   2314  // Declare a timeout Promise that we can use to make sure spied methods were not called.
   2315  const onTimeout = new Promise(function (resolve) {
   2316    setTimeout(() => {
   2317      resolve({ link: null, where: null });
   2318    }, 1000);
   2319  });
   2320 
   2321  const raceResult = Promise.race([onOpenLink, onTimeout]);
   2322  sinon.restore();
   2323  return raceResult;
   2324 }
   2325 
   2326 /**
   2327 * Since the MDN data is updated frequently, it might happen that the properties used in
   2328 * this test are not in the dataset anymore/now have URLs.
   2329 * This function will return properties in the dataset that don't have MDN url so you
   2330 * can easily find a replacement.
   2331 */
   2332 function logCssCompatDataPropertiesWithoutMDNUrl() {
   2333  const cssPropertiesCompatData = require("resource://devtools/shared/compatibility/dataset/css-properties.json");
   2334 
   2335  function walk(node) {
   2336    for (const propertyName in node) {
   2337      const property = node[propertyName];
   2338      if (property.__compat) {
   2339        if (!property.__compat.mdn_url) {
   2340          dump(
   2341            `"${propertyName}" - MDN URL: ${
   2342              property.__compat.mdn_url || "❌"
   2343            } - Spec URL: ${property.__compat.spec_url || "❌"}\n`
   2344          );
   2345        }
   2346      } else if (typeof property == "object") {
   2347        walk(property);
   2348      }
   2349    }
   2350  }
   2351  walk(cssPropertiesCompatData);
   2352 }
   2353 
   2354 /**
   2355 * Craft a CssProperties instance without involving RDP for tests
   2356 * manually spawning OutputParser, CssCompleter, Editor...
   2357 *
   2358 * Otherwise this should instead be fetched from CssPropertiesFront.
   2359 *
   2360 * @return {CssProperties}
   2361 */
   2362 function getClientCssProperties() {
   2363  const {
   2364    generateCssProperties,
   2365  } = require("resource://devtools/server/actors/css-properties.js");
   2366  const {
   2367    CssProperties,
   2368    normalizeCssData,
   2369  } = require("resource://devtools/client/fronts/css-properties.js");
   2370  return new CssProperties(
   2371    normalizeCssData({ properties: generateCssProperties(document) })
   2372  );
   2373 }
   2374 
   2375 /**
   2376 * Helper method to stop a Service Worker promptly.
   2377 *
   2378 * @param {string} workerUrl
   2379 *        Absolute Worker URL to stop.
   2380 */
   2381 async function stopServiceWorker(workerUrl) {
   2382  info(`Stop Service Worker: ${workerUrl}\n`);
   2383 
   2384  // Help the SW to be immediately destroyed after unregistering it.
   2385  Services.prefs.setIntPref("dom.serviceWorkers.idle_timeout", 0);
   2386 
   2387  const swm = Cc["@mozilla.org/serviceworkers/manager;1"].getService(
   2388    Ci.nsIServiceWorkerManager
   2389  );
   2390  // Unfortunately we can't use swm.getRegistrationByPrincipal, as it requires a "scope", which doesn't seem to be the worker URL.
   2391  // So let's use getAllRegistrations to find the nsIServiceWorkerInfo in order to:
   2392  // - retrieve its active worker,
   2393  // - call attach+detachDebugger,
   2394  // - reset the idle timeout.
   2395  // This way, the unregister instruction is immediate, thanks to the 0 dom.serviceWorkers.idle_timeout we set at the beginning of the function
   2396  const registrations = swm.getAllRegistrations();
   2397  let matchedInfo;
   2398  for (let i = 0; i < registrations.length; i++) {
   2399    const info = registrations.queryElementAt(
   2400      i,
   2401      Ci.nsIServiceWorkerRegistrationInfo
   2402    );
   2403    // Lookup for an exact URL match.
   2404    if (info.scriptSpec === workerUrl) {
   2405      matchedInfo = info;
   2406      break;
   2407    }
   2408  }
   2409  ok(!!matchedInfo, "Found the service worker info");
   2410 
   2411  info("Wait for the worker to be active");
   2412  await waitFor(() => matchedInfo.activeWorker, "Wait for the SW to be active");
   2413 
   2414  // We need to attach+detach the debugger in order to reset the idle timeout.
   2415  // Otherwise the worker would still be waiting for a previously registered timeout
   2416  // which would be the 0ms one we set by tweaking the preference.
   2417  function resetWorkerTimeout(worker) {
   2418    worker.attachDebugger();
   2419    worker.detachDebugger();
   2420  }
   2421  resetWorkerTimeout(matchedInfo.activeWorker);
   2422  // Also reset all the other possible worker instances
   2423  if (matchedInfo.evaluatingWorker) {
   2424    resetWorkerTimeout(matchedInfo.evaluatingWorker);
   2425  }
   2426  if (matchedInfo.installingWorker) {
   2427    resetWorkerTimeout(matchedInfo.installingWorker);
   2428  }
   2429  if (matchedInfo.waitingWorker) {
   2430    resetWorkerTimeout(matchedInfo.waitingWorker);
   2431  }
   2432  // Reset this preference in order to ensure other SW are not immediately destroyed.
   2433  Services.prefs.clearUserPref("dom.serviceWorkers.idle_timeout");
   2434 
   2435  // Spin the event loop to ensure the worker had time to really be shut down.
   2436  await wait(0);
   2437 
   2438  return matchedInfo;
   2439 }
   2440 
   2441 /**
   2442 * Helper method to stop and unregister a Service Worker promptly.
   2443 *
   2444 * @param {string} workerUrl
   2445 *        Absolute Worker URL to unregister.
   2446 */
   2447 async function unregisterServiceWorker(workerUrl) {
   2448  const swInfo = await stopServiceWorker(workerUrl);
   2449 
   2450  info(`Unregister Service Worker: ${workerUrl}\n`);
   2451  // Now call unregister on that worker so that it can be destroyed immediately
   2452  const swm = Cc["@mozilla.org/serviceworkers/manager;1"].getService(
   2453    Ci.nsIServiceWorkerManager
   2454  );
   2455  const unregisterSuccess = await new Promise(resolve => {
   2456    swm.unregister(
   2457      swInfo.principal,
   2458      {
   2459        unregisterSucceeded(success) {
   2460          resolve(success);
   2461        },
   2462      },
   2463      swInfo.scope
   2464    );
   2465  });
   2466  ok(unregisterSuccess, "Service worker successfully unregistered");
   2467 }
   2468 
   2469 /**
   2470 * Toggle the JavavaScript tracer via its toolbox toolbar button.
   2471 */
   2472 async function toggleJsTracer(toolbox) {
   2473  const { tracerCommand } = toolbox.commands;
   2474  const { isTracingEnabled } = tracerCommand;
   2475  const { logMethod, traceOnNextInteraction, traceOnNextLoad } =
   2476    toolbox.commands.tracerCommand.getTracingOptions();
   2477 
   2478  // When the tracer is waiting for user interaction or page load, it won't be made active
   2479  // right away. The test should manually wait for its activation.
   2480  const shouldWaitForToggle = !traceOnNextInteraction && !traceOnNextLoad;
   2481  let onTracingToggled;
   2482  if (shouldWaitForToggle) {
   2483    onTracingToggled = new Promise(resolve => {
   2484      tracerCommand.on("toggle", async function listener() {
   2485        // Ignore the event, if we are still in the same state as before the click
   2486        if (tracerCommand.isTracingActive == isTracingEnabled) {
   2487          return;
   2488        }
   2489        tracerCommand.off("toggle", listener);
   2490        resolve();
   2491      });
   2492    });
   2493  }
   2494 
   2495  const toolbarButton = toolbox.doc.getElementById("command-button-jstracer");
   2496  toolbarButton.click();
   2497 
   2498  if (shouldWaitForToggle) {
   2499    info("Waiting for the tracer to be active");
   2500    await onTracingToggled;
   2501  }
   2502 
   2503  const {
   2504    TRACER_LOG_METHODS,
   2505  } = require("resource://devtools/shared/specs/tracer.js");
   2506  if (logMethod != TRACER_LOG_METHODS.CONSOLE) {
   2507    return;
   2508  }
   2509 
   2510  // We were tracing and just requested to stop it.
   2511  // Wait for the stop message to appear in the console before clearing its content.
   2512  // This simplifies writting tests toggling the tracer ON multiple times and checking
   2513  // for the display of traces in the console.
   2514  if (isTracingEnabled) {
   2515    const { hud } = await toolbox.getPanel("webconsole");
   2516    info("Wait for tracing to be disabled");
   2517    await waitFor(() =>
   2518      [...hud.ui.outputNode.querySelectorAll(".message")].some(msg =>
   2519        msg.textContent.includes("Stopped tracing")
   2520      )
   2521    );
   2522 
   2523    hud.ui.clearOutput();
   2524    await waitFor(
   2525      () => hud.ui.outputNode.querySelectorAll(".message").length === 0
   2526    );
   2527  } else {
   2528    // We are enabling the tracing to the console, and the console may not be opened just yet.
   2529    const { hud } = await toolbox.getPanelWhenReady("webconsole");
   2530    if (!traceOnNextInteraction && !traceOnNextLoad) {
   2531      await waitFor(() =>
   2532        [...hud.ui.outputNode.querySelectorAll(".message")].some(msg =>
   2533          msg.textContent.includes("Started tracing to Web Console")
   2534        )
   2535      );
   2536    }
   2537  }
   2538 }
   2539 
   2540 /**
   2541 * Retrieve the context menu element corresponding to the provided id, for the
   2542 * provided netmonitor instance.
   2543 *
   2544 * @param {object} monitor
   2545 *        The network monitor object
   2546 * @param {string} id
   2547 *        The id of the context menu item
   2548 */
   2549 function getNetmonitorContextMenuItem(monitor, id) {
   2550  const Menu = require("resource://devtools/client/framework/menu.js");
   2551  return Menu.getMenuElementById(id, monitor.panelWin.document);
   2552 }
   2553 
   2554 /**
   2555 * Selects and clicks the context menu item of the netmonitor, it should
   2556 * also wait for the popup to close.
   2557 *
   2558 * @param {object} monitor
   2559 *        The network monitor object
   2560 * @param {string} id
   2561 *        The id of the context menu item
   2562 */
   2563 async function selectNetmonitorContextMenuItem(monitor, id) {
   2564  const contextMenuItem = getNetmonitorContextMenuItem(monitor, id);
   2565 
   2566  const popup = contextMenuItem.parentNode;
   2567  await _maybeOpenAncestorMenu(contextMenuItem);
   2568  const hidden = BrowserTestUtils.waitForEvent(popup, "popuphidden");
   2569  popup.activateItem(contextMenuItem);
   2570  await hidden;
   2571 }
   2572 
   2573 async function _maybeOpenAncestorMenu(menuItem) {
   2574  const parentPopup = menuItem.parentNode;
   2575  if (parentPopup.state == "open") {
   2576    return;
   2577  }
   2578  const shown = BrowserTestUtils.waitForEvent(parentPopup, "popupshown");
   2579  if (parentPopup.state == "showing") {
   2580    await shown;
   2581    return;
   2582  }
   2583  const parentMenu = parentPopup.parentNode;
   2584  await _maybeOpenAncestorMenu(parentMenu);
   2585  parentMenu.openMenu(true);
   2586  await shown;
   2587 }
   2588 
   2589 /**
   2590 * Returns the list of console messages DOM Element display in the Web Console
   2591 * which contains a given string.
   2592 *
   2593 * @param {Toolbox} toolbox
   2594 * @param {string} query
   2595 * @return {Array<DOMElement>}
   2596 */
   2597 async function findConsoleMessages(toolbox, query) {
   2598  const webConsole = await toolbox.getPanel("webconsole");
   2599  const win = webConsole._frameWindow;
   2600  return Array.prototype.filter.call(
   2601    win.document.querySelectorAll(".message"),
   2602    e => e.innerText.includes(query)
   2603  );
   2604 }
   2605 
   2606 /**
   2607 * Wait for a console message to appear with a given text and a given link to
   2608 * a specific location in a JS source.
   2609 * Returns the DOM Element in the Web Console for the link to the JS Source.
   2610 *
   2611 * @param {Toolbox} toolbox
   2612 * @param {string} messageText
   2613 * @param {string} linkText
   2614 * @return {DOMElement}
   2615 */
   2616 async function waitForConsoleMessageLink(toolbox, messageText, linkText) {
   2617  await toolbox.selectTool("webconsole");
   2618 
   2619  return waitFor(async () => {
   2620    // Wait until the message updates.
   2621    const [message] = await findConsoleMessages(toolbox, messageText);
   2622    if (!message) {
   2623      return false;
   2624    }
   2625    const linkEl = message.querySelector(".frame-link-source");
   2626    if (!linkEl || linkEl.textContent !== linkText) {
   2627      return false;
   2628    }
   2629 
   2630    return linkEl;
   2631  });
   2632 }
   2633 
   2634 /**
   2635 * Click on a Frame component link and ensure it opens the debugger on the expected location
   2636 *
   2637 * @param {Toolbox} toolbox
   2638 * @param {DOMElement} frameLinkNode
   2639 * @param {Object] options
   2640 * @param {string | null} options.url
   2641 * @param {number | null} options.line
   2642 * @param {number | null} options.column
   2643 * @param {string | undefined} logPointExpr
   2644 */
   2645 async function clickAndAssertFrameLinkNode(
   2646  toolbox,
   2647  frameLinkNode,
   2648  { url, line, column },
   2649  logPointExpr
   2650 ) {
   2651  info("checking click on node location");
   2652 
   2653  // If the debugger hasn't fully loaded yet and breakpoints are still being
   2654  // added when we click on the logpoint link, the logpoint panel might not
   2655  // render. Work around this for now, see bug 1592854.
   2656  if (logPointExpr) {
   2657    await waitForTime(1000);
   2658  }
   2659 
   2660  const onSourceOpened = toolbox.once("source-opened-in-debugger");
   2661 
   2662  EventUtils.sendMouseEvent(
   2663    { type: "click" },
   2664    // The frame DOM Element may be coming from a Debugger Frame component, or a shared compoentn Frame component
   2665    // and the link would be at a distinct selector.
   2666    frameLinkNode.querySelector(".frame-link-filename") ||
   2667      frameLinkNode.querySelector(".location")
   2668  );
   2669 
   2670  // Wait for the source to finish loading, if it is pending.
   2671  await onSourceOpened;
   2672 
   2673  // Wait for the debugger to have fully processed the opened source
   2674  const dbg = toolbox.getPanel("jsdebugger");
   2675 
   2676  const selectedLocation = await waitFor(() =>
   2677    dbg._selectors.getSelectedLocation(dbg._getState())
   2678  );
   2679 
   2680  if (typeof url == "string") {
   2681    const frameUrl = frameLinkNode.getAttribute("data-url");
   2682    is(frameUrl, url, "Frame link url is correct");
   2683 
   2684    is(selectedLocation.source.url, url, "debugger opened url is correct");
   2685  }
   2686  if (typeof line == "number") {
   2687    const frameLine = frameLinkNode.getAttribute("data-line");
   2688    is(parseInt(frameLine, 10), line, "Frame link line is correct");
   2689 
   2690    is(selectedLocation.line, line, "debugger opened line is correct");
   2691  }
   2692  if (typeof column == "number") {
   2693    // Note that debugger's Frame component doesn't show the column
   2694    const frameColumn = frameLinkNode.getAttribute("data-column");
   2695    is(parseInt(frameColumn, 10), column, "Frame link column is correct");
   2696 
   2697    // Redux location object uses 0-based column, while we display a 1-based one.
   2698    is(
   2699      selectedLocation.column + 1,
   2700      column,
   2701      "debugger opened column is correct"
   2702    );
   2703  }
   2704 
   2705  if (logPointExpr !== undefined && logPointExpr !== "") {
   2706    const inputEl = dbg.panelWin.document.activeElement;
   2707 
   2708    const isPanelFocused =
   2709      inputEl.classList.contains("cm-content") &&
   2710      inputEl.closest(".conditional-breakpoint-panel.log-point");
   2711 
   2712    ok(isPanelFocused, "The textarea of logpoint panel is focused");
   2713 
   2714    const inputValue = inputEl.parentElement.parentElement.innerText.trim();
   2715    is(
   2716      inputValue,
   2717      logPointExpr,
   2718      "The input in the open logpoint panel matches the logpoint expression"
   2719    );
   2720  }
   2721 }