tor-browser

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

BrowserTestUtils.sys.mjs (98766B)


      1 /* This Source Code Form is subject to the terms of the Mozilla Public
      2 * License, v. 2.0. If a copy of the MPL was not distributed with this
      3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      4 
      5 /*
      6 * This module implements a number of utilities useful for browser tests.
      7 *
      8 * All asynchronous helper methods should return promises, rather than being
      9 * callback based.
     10 */
     11 
     12 // This file uses ContentTask & frame scripts, where these are available.
     13 /* global ContentTaskUtils */
     14 
     15 import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
     16 import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
     17 import { TestUtils } from "resource://testing-common/TestUtils.sys.mjs";
     18 
     19 const lazy = {};
     20 
     21 ChromeUtils.defineESModuleGetters(lazy, {
     22  BrowserWindowTracker: "resource:///modules/BrowserWindowTracker.sys.mjs",
     23  ContentTask: "resource://testing-common/ContentTask.sys.mjs",
     24 });
     25 
     26 XPCOMUtils.defineLazyServiceGetters(lazy, {
     27  ProtocolProxyService: [
     28    "@mozilla.org/network/protocol-proxy-service;1",
     29    Ci.nsIProtocolProxyService,
     30  ],
     31 });
     32 
     33 let gListenerId = 0;
     34 
     35 const DISABLE_CONTENT_PROCESS_REUSE_PREF = "dom.ipc.disableContentProcessReuse";
     36 
     37 const kAboutPageRegistrationContentScript =
     38  "chrome://mochikit/content/tests/BrowserTestUtils/content-about-page-utils.js";
     39 
     40 /**
     41 * Create and register the BrowserTestUtils and ContentEventListener window
     42 * actors.
     43 */
     44 function registerActors() {
     45  ChromeUtils.registerWindowActor("BrowserTestUtils", {
     46    parent: {
     47      esModuleURI: "resource://testing-common/BrowserTestUtilsParent.sys.mjs",
     48    },
     49    child: {
     50      esModuleURI: "resource://testing-common/BrowserTestUtilsChild.sys.mjs",
     51      events: {
     52        DOMContentLoaded: { capture: true },
     53        load: { capture: true },
     54      },
     55    },
     56    allFrames: true,
     57    includeChrome: true,
     58  });
     59 
     60  ChromeUtils.registerWindowActor("ContentEventListener", {
     61    parent: {
     62      esModuleURI:
     63        "resource://testing-common/ContentEventListenerParent.sys.mjs",
     64    },
     65    child: {
     66      esModuleURI:
     67        "resource://testing-common/ContentEventListenerChild.sys.mjs",
     68      events: {
     69        // We need to see the creation of all new windows, in case they have
     70        // a browsing context we are interested in.
     71        DOMWindowCreated: { capture: true },
     72      },
     73    },
     74    allFrames: true,
     75  });
     76 }
     77 
     78 registerActors();
     79 
     80 /**
     81 * BrowserTestUtils provides useful test utilities for working with the browser
     82 * in browser mochitests. Most common operations (opening, closing and switching
     83 * between tabs and windows, loading URLs, waiting for events in the parent or
     84 * content process, clicking things in the content process, registering about
     85 * pages, etc.) have dedicated helpers on this object.
     86 *
     87 * @class
     88 */
     89 export var BrowserTestUtils = {
     90  // We define the function separately, rather than using an arrow function
     91  // inline due to https://github.com/jsdoc/jsdoc/issues/2143.
     92  /**
     93   * @template T
     94   * @typedef {Function} withNewTabTaskFn
     95   * @param {MozBrowser} browser
     96   * @returns {Promise<T> | T}
     97   */
     98 
     99  /**
    100   * Loads a page in a new tab, executes a Task and closes the tab.
    101   *
    102   * @template T
    103   * @param {object | string} options
    104   *        If this is a string it is the url to open and will be opened in the
    105   *        currently active browser window.
    106   * @param {tabbrowser} [options.gBrowser]
    107   *        A reference to the ``tabbrowser`` element where the new tab should
    108   *        be opened,
    109   * @param {string} options.url
    110   *        The URL of the page to load.
    111   * @param {withNewTabTaskFn<T>} taskFn
    112   *        Async function representing that will be executed while
    113   *        the tab is loaded. The first argument passed to the function is a
    114   *        reference to the browser object for the new tab.
    115   *
    116   * @return {Promise<T>} Resolves to the value that is returned from taskFn.
    117   * @rejects Any exception from taskFn is propagated.
    118   */
    119  async withNewTab(options, taskFn) {
    120    if (typeof options == "string") {
    121      options = {
    122        gBrowser: Services.wm.getMostRecentWindow("navigator:browser").gBrowser,
    123        url: options,
    124      };
    125    }
    126    let tab = await BrowserTestUtils.openNewForegroundTab(options);
    127    let originalWindow = tab.ownerGlobal;
    128    let result;
    129    try {
    130      result = await taskFn(tab.linkedBrowser);
    131    } finally {
    132      let finalWindow = tab.ownerGlobal;
    133      if (originalWindow == finalWindow && !tab.closing && tab.linkedBrowser) {
    134        // taskFn may resolve within a tick after opening a new tab.
    135        // We shouldn't remove the newly opened tab in the same tick.
    136        // Wait for the next tick here.
    137        await TestUtils.waitForTick();
    138        BrowserTestUtils.removeTab(tab);
    139      } else {
    140        Services.console.logStringMessage(
    141          "BrowserTestUtils.withNewTab: Tab was already closed before " +
    142            "removeTab would have been called"
    143        );
    144      }
    145    }
    146 
    147    return Promise.resolve(result);
    148  },
    149 
    150  /**
    151   * Opens a new tab in the foreground.
    152   *
    153   * This function takes an options object (which is preferred) or actual
    154   * parameters. The names of the options must correspond to the names below.
    155   * gBrowser is required and all other options are optional.
    156   *
    157   * @param {tabbrowser} gBrowser
    158   *        The tabbrowser to open the tab new in.
    159   * @param {string|function} opening (or url)
    160   *        May be either a string URL to load in the tab, or a function that
    161   *        will be called to open a foreground tab. Defaults to "about:blank".
    162   * @param {boolean} waitForLoad
    163   *        True to wait for the page in the new tab to load. Defaults to true.
    164   * @param {boolean} waitForStateStop
    165   *        True to wait for the web progress listener to send STATE_STOP for the
    166   *        document in the tab. Defaults to false.
    167   * @param {boolean} forceNewProcess
    168   *        True to force the new tab to load in a new process. Defaults to
    169   *        false.
    170   *
    171   * @return {Promise}
    172   *         Resolves when the tab is ready and loaded as necessary.
    173   */
    174  openNewForegroundTab(tabbrowser, ...args) {
    175    let startTime = ChromeUtils.now();
    176    let options;
    177    if (
    178      tabbrowser.ownerGlobal &&
    179      tabbrowser === tabbrowser.ownerGlobal.gBrowser
    180    ) {
    181      // tabbrowser is a tabbrowser, read the rest of the arguments from args.
    182      let [
    183        opening = "about:blank",
    184        waitForLoad = true,
    185        waitForStateStop = false,
    186        forceNewProcess = false,
    187      ] = args;
    188 
    189      options = { opening, waitForLoad, waitForStateStop, forceNewProcess };
    190    } else {
    191      if ("url" in tabbrowser && !("opening" in tabbrowser)) {
    192        tabbrowser.opening = tabbrowser.url;
    193      }
    194 
    195      let {
    196        opening = "about:blank",
    197        waitForLoad = true,
    198        waitForStateStop = false,
    199        forceNewProcess = false,
    200      } = tabbrowser;
    201 
    202      tabbrowser = tabbrowser.gBrowser;
    203      options = { opening, waitForLoad, waitForStateStop, forceNewProcess };
    204    }
    205 
    206    let {
    207      opening: opening,
    208      waitForLoad: aWaitForLoad,
    209      waitForStateStop: aWaitForStateStop,
    210    } = options;
    211 
    212    let promises, tab;
    213    try {
    214      // If we're asked to force a new process, set the pref to disable process
    215      // re-use while we insert this new tab.
    216      if (options.forceNewProcess) {
    217        Services.ppmm.releaseCachedProcesses();
    218        Services.prefs.setBoolPref(DISABLE_CONTENT_PROCESS_REUSE_PREF, true);
    219      }
    220 
    221      promises = [
    222        BrowserTestUtils.switchTab(tabbrowser, function () {
    223          if (typeof opening == "function") {
    224            opening();
    225            tab = tabbrowser.selectedTab;
    226          } else {
    227            tabbrowser.selectedTab = tab = BrowserTestUtils.addTab(
    228              tabbrowser,
    229              opening
    230            );
    231          }
    232        }),
    233      ];
    234 
    235      if (aWaitForLoad) {
    236        // accept any load, including about:blank
    237        promises.push(
    238          BrowserTestUtils.browserLoaded(tab.linkedBrowser, {
    239            wantLoad: () => true,
    240          })
    241        );
    242      }
    243      if (aWaitForStateStop) {
    244        promises.push(BrowserTestUtils.browserStopped(tab.linkedBrowser));
    245      }
    246    } finally {
    247      // Clear the pref once we're done, if needed.
    248      if (options.forceNewProcess) {
    249        Services.prefs.clearUserPref(DISABLE_CONTENT_PROCESS_REUSE_PREF);
    250      }
    251    }
    252    return Promise.all(promises).then(() => {
    253      let { innerWindowId } = tabbrowser.ownerGlobal.windowGlobalChild;
    254      ChromeUtils.addProfilerMarker(
    255        "BrowserTestUtils",
    256        { startTime, category: "Test", innerWindowId },
    257        "openNewForegroundTab"
    258      );
    259      return tab;
    260    });
    261  },
    262 
    263  showOnlyTheseTabs(tabbrowser, tabs) {
    264    for (let tab of tabs) {
    265      tabbrowser.showTab(tab);
    266    }
    267    for (let tab of tabbrowser.tabs) {
    268      if (!tabs.includes(tab)) {
    269        tabbrowser.hideTab(tab);
    270      }
    271    }
    272  },
    273 
    274  /**
    275   * Checks if a DOM element is hidden.
    276   *
    277   * @param {Element} element
    278   *        The element which is to be checked.
    279   *
    280   * @return {boolean}
    281   */
    282  isHidden(element) {
    283    if (
    284      element.nodeType == Node.DOCUMENT_FRAGMENT_NODE &&
    285      element.containingShadowRoot == element
    286    ) {
    287      return BrowserTestUtils.isHidden(element.getRootNode().host);
    288    }
    289 
    290    let win = element.ownerGlobal;
    291    let style = win.getComputedStyle(element);
    292    if (style.display == "none") {
    293      return true;
    294    }
    295    if (style.visibility != "visible") {
    296      return true;
    297    }
    298    if (win.XULPopupElement.isInstance(element)) {
    299      return ["hiding", "closed"].includes(element.state);
    300    }
    301 
    302    // Hiding a parent element will hide all its children
    303    if (element.parentNode != element.ownerDocument) {
    304      return BrowserTestUtils.isHidden(element.parentNode);
    305    }
    306 
    307    return false;
    308  },
    309 
    310  /**
    311   * Checks if a DOM element is visible.
    312   *
    313   * @param {Element} element
    314   *        The element which is to be checked.
    315   *
    316   * @return {boolean}
    317   */
    318  isVisible(element) {
    319    if (
    320      element.nodeType == Node.DOCUMENT_FRAGMENT_NODE &&
    321      element.containingShadowRoot == element
    322    ) {
    323      return BrowserTestUtils.isVisible(element.getRootNode().host);
    324    }
    325 
    326    let win = element.ownerGlobal;
    327    let style = win.getComputedStyle(element);
    328    if (style.display == "none") {
    329      return false;
    330    }
    331    if (style.visibility != "visible") {
    332      return false;
    333    }
    334    if (win.XULPopupElement.isInstance(element) && element.state != "open") {
    335      return false;
    336    }
    337 
    338    // Hiding a parent element will hide all its children
    339    if (element.parentNode != element.ownerDocument) {
    340      return BrowserTestUtils.isVisible(element.parentNode);
    341    }
    342 
    343    return true;
    344  },
    345 
    346  /**
    347   * If the argument is a browsingContext, return it. If the
    348   * argument is a browser/frame, returns the browsing context for it.
    349   */
    350  getBrowsingContextFrom(browser) {
    351    if (Element.isInstance(browser)) {
    352      return browser.browsingContext;
    353    }
    354 
    355    return browser;
    356  },
    357 
    358  /**
    359   * Switches to a tab and resolves when it is ready.
    360   *
    361   * @param {tabbrowser} tabbrowser
    362   *        The tabbrowser.
    363   * @param {tab} tab
    364   *        Either a tab element to switch to or a function to perform the switch.
    365   *
    366   * @return {Promise}
    367   *         Resolves when the tab has been switched to.
    368   */
    369  switchTab(tabbrowser, tab) {
    370    let startTime = ChromeUtils.now();
    371    let { innerWindowId } = tabbrowser.ownerGlobal.windowGlobalChild;
    372 
    373    // Some tests depend on the delay and TabSwitched only fires if the browser is visible.
    374    // Bug 1977993 tracks always dispatching TabSwitched.
    375    let switchEvent =
    376      Services.prefs.getBoolPref("test.wait300msAfterTabSwitch", false) ||
    377      tabbrowser.ownerDocument.hidden
    378        ? "TabSwitchDone"
    379        : "TabSwitched";
    380 
    381    let promise = new Promise(resolve => {
    382      tabbrowser.addEventListener(
    383        switchEvent,
    384        function () {
    385          TestUtils.executeSoon(() => {
    386            ChromeUtils.addProfilerMarker(
    387              "BrowserTestUtils",
    388              { category: "Test", startTime, innerWindowId },
    389              "switchTab"
    390            );
    391            resolve(tabbrowser.selectedTab);
    392          });
    393        },
    394        { once: true }
    395      );
    396    });
    397 
    398    if (typeof tab == "function") {
    399      tab();
    400    } else {
    401      tabbrowser.selectedTab = tab;
    402    }
    403    return promise;
    404  },
    405 
    406  /**
    407   * Waits for an ongoing page load in a browser window to complete. By default
    408   * about:blank loads are ignored.
    409   *
    410   * This can be used in conjunction with any synchronous method for starting a
    411   * load, like the "addTab" method on "tabbrowser", and must be called before
    412   * yielding control to the event loop.
    413   *
    414   * Note that calling this after multiple successive load operations can be racy,
    415   * so ``wantLoad`` should be specified in these cases. The same holds if we're
    416   * interested in about:blank to load.
    417   *
    418   * This function works by listening for custom load events on ``browser``. These
    419   * are sent by a BrowserTestUtils window actor in response to "load" and
    420   * "DOMContentLoaded" content events.
    421   *
    422   * @param {xul:browser} browser
    423   *        A xul:browser.
    424   * @param {object} options
    425   * @param {boolean} [options.includeSubFrames = false]
    426   *        A boolean indicating if loads from subframes should be included.
    427   * @param {string|function} [options.wantLoad]
    428   *        If a function, takes a URL and returns true if that's the load we're
    429   *        interested in. If a string, gives the URL of the load we're interested
    430   *        in. If not present, the first non-about:blank load resolves the promise.
    431   * @param {boolean} [options.maybeErrorPage = false]
    432   *        If true, this uses DOMContentLoaded event instead of load event.
    433   *        Also wantLoad will be called with visible URL, instead of
    434   *        'about:neterror?...' for error page.
    435   *
    436   * @return {Promise}
    437   *   Resovles when a load event is triggered for the browser.
    438   */
    439  browserLoaded(browser, ...args) {
    440    const options =
    441      args.length && typeof args[0] === "object"
    442        ? args[0]
    443        : {
    444            includeSubFrames: args[0] ?? false,
    445            wantLoad: args[1] ?? null,
    446            maybeErrorPage: args[2] ?? false,
    447          };
    448    const {
    449      includeSubFrames = false,
    450      wantLoad = null,
    451      maybeErrorPage = false,
    452    } = options;
    453    let startTime = ChromeUtils.now();
    454    let { innerWindowId } = browser.ownerGlobal.windowGlobalChild;
    455 
    456    // Passing a url as second argument is a common mistake we should prevent.
    457    if (includeSubFrames && typeof includeSubFrames != "boolean") {
    458      throw new Error(
    459        "The second argument to browserLoaded should be a boolean."
    460      );
    461    }
    462 
    463    // Consumers may pass gBrowser instead of a browser, so adjust for that.
    464    if ("selectedBrowser" in browser) {
    465      browser = browser.selectedBrowser;
    466    }
    467 
    468    // If browser belongs to tabbrowser-tab, ensure it has been
    469    // inserted into the document.
    470    let tabbrowser = browser.ownerGlobal.gBrowser;
    471    if (tabbrowser && tabbrowser.getTabForBrowser) {
    472      let tab = tabbrowser.getTabForBrowser(browser);
    473      if (tab) {
    474        tabbrowser._insertBrowser(tab);
    475      }
    476    }
    477 
    478    function isWanted(url) {
    479      if (!wantLoad) {
    480        return !url.startsWith("about:blank");
    481      } else if (typeof wantLoad == "function") {
    482        return wantLoad(url);
    483      }
    484 
    485      // HTTPS-First (Bug 1704453) TODO: In case we are waiting
    486      // for an http:// URL to be loaded and https-first is enabled,
    487      // then we also return true in case the backend upgraded
    488      // the load to https://.
    489      if (
    490        BrowserTestUtils._httpsFirstEnabled &&
    491        typeof wantLoad == "string" &&
    492        wantLoad.startsWith("http://")
    493      ) {
    494        let wantLoadHttps = wantLoad.replace("http://", "https://");
    495        if (wantLoadHttps == url) {
    496          return true;
    497        }
    498      }
    499 
    500      // It's a string.
    501      return wantLoad == url;
    502    }
    503 
    504    // Error pages are loaded slightly differently, so listen for the
    505    // DOMContentLoaded event for those instead.
    506    let loadEvent = maybeErrorPage ? "DOMContentLoaded" : "load";
    507    let eventName = `BrowserTestUtils:ContentEvent:${loadEvent}`;
    508 
    509    return new Promise((resolve, reject) => {
    510      function listener(event) {
    511        switch (event.type) {
    512          case eventName: {
    513            let { browsingContext, internalURL, visibleURL } = event.detail;
    514 
    515            // Sometimes we arrive here without an internalURL. If that's the
    516            // case, just keep waiting until we get one.
    517            if (!internalURL) {
    518              return;
    519            }
    520 
    521            // Ignore subframes if we only care about the top-level load.
    522            let subframe = browsingContext !== browsingContext.top;
    523            if (subframe && !includeSubFrames) {
    524              return;
    525            }
    526 
    527            // See testing/mochitest/BrowserTestUtils/content/BrowserTestUtilsChild.sys.mjs
    528            // for the difference between visibleURL and internalURL.
    529            if (!isWanted(maybeErrorPage ? visibleURL : internalURL)) {
    530              return;
    531            }
    532 
    533            ChromeUtils.addProfilerMarker(
    534              "BrowserTestUtils",
    535              { startTime, category: "Test", innerWindowId },
    536              "browserLoaded: " + internalURL
    537            );
    538            resolve(internalURL);
    539            break;
    540          }
    541 
    542          case "unload":
    543            reject(
    544              new Error(
    545                "The window unloaded while we were waiting for the browser to load - this should never happen."
    546              )
    547            );
    548            break;
    549 
    550          default:
    551            return;
    552        }
    553 
    554        browser.removeEventListener(eventName, listener, true);
    555        browser.ownerGlobal.removeEventListener("unload", listener);
    556      }
    557 
    558      browser.addEventListener(eventName, listener, true);
    559      browser.ownerGlobal.addEventListener("unload", listener);
    560    });
    561  },
    562 
    563  /**
    564   * Waits for the selected browser to load in a new window. This
    565   * is most useful when you've got a window that might not have
    566   * loaded its DOM yet, and where you can't easily use browserLoaded
    567   * on gBrowser.selectedBrowser since gBrowser doesn't yet exist.
    568   *
    569   * @param {xul:window} window
    570   *        A newly opened window for which we're waiting for the
    571   *        first browser load.
    572   * @param {boolean} aboutBlank [optional]
    573   *        If false, about:blank loads are ignored and we continue
    574   *        to wait.
    575   * @param {function|null} checkFn [optional]
    576   *        If checkFn(browser) returns false, the load is ignored
    577   *        and we continue to wait.
    578   *
    579   * @return {Promise<Event>}
    580   *   Resolves to the fired load event.
    581   */
    582  firstBrowserLoaded(win, aboutBlank = true, checkFn = null) {
    583    return this.waitForEvent(
    584      win,
    585      "BrowserTestUtils:ContentEvent:load",
    586      true,
    587      event => {
    588        if (checkFn) {
    589          return checkFn(event.target);
    590        }
    591        return (
    592          win.gBrowser.selectedBrowser.currentURI.spec !== "about:blank" ||
    593          aboutBlank
    594        );
    595      }
    596    );
    597  },
    598 
    599  _webProgressListeners: new Set(),
    600 
    601  _contentEventListenerSharedState: new Map(),
    602 
    603  _contentEventListeners: new Map(),
    604 
    605  /**
    606   * Waits for the web progress listener associated with this tab to fire a
    607   * state change that matches checkFn for the toplevel document.
    608   *
    609   * @param {xul:browser} browser
    610   *        A xul:browser.
    611   * @param {string} expectedURI (optional)
    612   *        A specific URL to check the channel load against
    613   * @param {Function} checkFn
    614   *        If checkFn(aStateFlags, aStatus) returns false, the state change
    615   *        is ignored and we continue to wait.
    616   *
    617   * @return {Promise<void>}
    618   *   Resolves when the desired state change reaches the tab's progress listener.
    619   */
    620  waitForBrowserStateChange(browser, expectedURI, checkFn) {
    621    return new Promise(resolve => {
    622      let wpl = {
    623        onStateChange(aWebProgress, aRequest, aStateFlags, aStatus) {
    624          dump(
    625            "Saw state " +
    626              aStateFlags.toString(16) +
    627              " and status " +
    628              aStatus.toString(16) +
    629              "\n"
    630          );
    631          if (checkFn(aStateFlags, aStatus) && aWebProgress.isTopLevel) {
    632            let chan = aRequest.QueryInterface(Ci.nsIChannel);
    633            dump(
    634              "Browser got expected state change " +
    635                chan.originalURI.spec +
    636                "\n"
    637            );
    638            if (!expectedURI || chan.originalURI.spec == expectedURI) {
    639              browser.removeProgressListener(wpl);
    640              BrowserTestUtils._webProgressListeners.delete(wpl);
    641              resolve();
    642            }
    643          }
    644        },
    645        onSecurityChange() {},
    646        onStatusChange() {},
    647        onLocationChange() {},
    648        onContentBlockingEvent() {},
    649        QueryInterface: ChromeUtils.generateQI([
    650          "nsIWebProgressListener",
    651          "nsIWebProgressListener2",
    652          "nsISupportsWeakReference",
    653        ]),
    654      };
    655      browser.addProgressListener(wpl);
    656      this._webProgressListeners.add(wpl);
    657      dump(
    658        "Waiting for browser state change" +
    659          (expectedURI ? " of " + expectedURI : "") +
    660          "\n"
    661      );
    662    });
    663  },
    664 
    665  /**
    666   * Waits for the web progress listener associated with this tab to fire a
    667   * STATE_STOP for the toplevel document.
    668   *
    669   * @param {xul:browser} browser
    670   *        A xul:browser.
    671   * @param {string} expectedURI (optional)
    672   *        A specific URL to check the channel load against
    673   * @param {boolean} checkAborts (optional, defaults to false)
    674   *        Whether NS_BINDING_ABORTED stops 'count' as 'real' stops
    675   *        (e.g. caused by the stop button or equivalent APIs)
    676   *
    677   * @return {Promise<void>}
    678   *   Resolves when STATE_STOP reaches the tab's progress listener.
    679   */
    680  browserStopped(browser, expectedURI, checkAborts = false) {
    681    let testFn = function (aStateFlags, aStatus) {
    682      return (
    683        aStateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK &&
    684        aStateFlags & Ci.nsIWebProgressListener.STATE_STOP &&
    685        (checkAborts || aStatus != Cr.NS_BINDING_ABORTED)
    686      );
    687    };
    688    dump(
    689      "Waiting for browser load" +
    690        (expectedURI ? " of " + expectedURI : "") +
    691        "\n"
    692    );
    693    return BrowserTestUtils.waitForBrowserStateChange(
    694      browser,
    695      expectedURI,
    696      testFn
    697    );
    698  },
    699 
    700  /**
    701   * Waits for the web progress listener associated with this tab to fire a
    702   * STATE_START for the toplevel document.
    703   *
    704   * @param {xul:browser} browser
    705   *        A xul:browser.
    706   * @param {string} expectedURI (optional)
    707   *        A specific URL to check the channel load against
    708   *
    709   * @return {Promise<void>}
    710   *   Resolves when STATE_START reaches the tab's progress listener
    711   */
    712  browserStarted(browser, expectedURI) {
    713    let testFn = function (aStateFlags) {
    714      return (
    715        aStateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK &&
    716        aStateFlags & Ci.nsIWebProgressListener.STATE_START
    717      );
    718    };
    719    dump(
    720      "Waiting for browser to start load" +
    721        (expectedURI ? " of " + expectedURI : "") +
    722        "\n"
    723    );
    724    return BrowserTestUtils.waitForBrowserStateChange(
    725      browser,
    726      expectedURI,
    727      testFn
    728    );
    729  },
    730 
    731  /**
    732   * Waits for a tab to open and load a given URL.
    733   *
    734   * By default, the method doesn't wait for the tab contents to load.
    735   *
    736   * @param {tabbrowser} tabbrowser
    737   *        The tabbrowser to look for the next new tab in.
    738   * @param {string|function} [wantLoad = null]
    739   *        If a function, takes a URL and returns true if that's the load we're
    740   *        interested in. If a string, gives the URL of the load we're interested
    741   *        in. If not present, the first non-about:blank load is used.
    742   * @param {boolean} [waitForLoad = false]
    743   *        True to wait for the page in the new tab to load. Defaults to false.
    744   * @param {boolean} [waitForAnyTab = false]
    745   *        True to wait for the url to be loaded in any new tab, not just the next
    746   *        one opened.
    747   * @param {boolean} [maybeErrorPage = false]
    748   *        See ``browserLoaded`` function.
    749   *
    750   * @return {Promise}
    751   *   Resolves with the {xul:tab} when a tab is opened and its location changes
    752   *   to the given URL and optionally that browser has loaded.
    753   *
    754   * NB: this method will not work if you open a new tab with e.g. BrowserCommands.openTab
    755   * and the tab does not load a URL, because no onLocationChange will fire.
    756   */
    757  waitForNewTab(
    758    tabbrowser,
    759    wantLoad = null,
    760    waitForLoad = false,
    761    waitForAnyTab = false,
    762    maybeErrorPage = false
    763  ) {
    764    let urlMatches;
    765    if (wantLoad && typeof wantLoad == "function") {
    766      urlMatches = wantLoad;
    767    } else if (wantLoad) {
    768      urlMatches = urlToMatch => urlToMatch == wantLoad;
    769    } else {
    770      urlMatches = urlToMatch => urlToMatch != "about:blank";
    771    }
    772    return new Promise(resolve => {
    773      tabbrowser.tabContainer.addEventListener(
    774        "TabOpen",
    775        function tabOpenListener(openEvent) {
    776          if (!waitForAnyTab) {
    777            tabbrowser.tabContainer.removeEventListener(
    778              "TabOpen",
    779              tabOpenListener
    780            );
    781          }
    782          let newTab = openEvent.target;
    783          if (wantLoad == "about:blank") {
    784            TestUtils.executeSoon(() => resolve(newTab));
    785            return;
    786          }
    787          let newBrowser = newTab.linkedBrowser;
    788          let result;
    789          if (waitForLoad) {
    790            // If waiting for load, resolve with promise for that, which when load
    791            // completes resolves to the new tab.
    792            result = BrowserTestUtils.browserLoaded(newBrowser, {
    793              includeSubFrames: false,
    794              wantLoad: urlMatches,
    795              maybeErrorPage,
    796            }).then(() => newTab);
    797          } else {
    798            // If not waiting for load, just resolve with the new tab.
    799            result = newTab;
    800          }
    801 
    802          let progressListener = {
    803            onLocationChange(aBrowser) {
    804              // Only interested in location changes on our browser.
    805              if (aBrowser != newBrowser) {
    806                return;
    807              }
    808 
    809              // Check that new location is the URL we want.
    810              if (!urlMatches(aBrowser.currentURI.spec)) {
    811                return;
    812              }
    813              if (waitForAnyTab) {
    814                tabbrowser.tabContainer.removeEventListener(
    815                  "TabOpen",
    816                  tabOpenListener
    817                );
    818              }
    819              tabbrowser.removeTabsProgressListener(progressListener);
    820              TestUtils.executeSoon(() => resolve(result));
    821            },
    822          };
    823          tabbrowser.addTabsProgressListener(progressListener);
    824        }
    825      );
    826    });
    827  },
    828 
    829  /**
    830   * Waits for onLocationChange.
    831   *
    832   * @param {tabbrowser} tabbrowser
    833   *        The tabbrowser to wait for the location change on.
    834   * @param {string} [url]
    835   *        The string URL to look for. The URL must match the URL in the
    836   *        location bar exactly.
    837   * @return {Promise<{webProgress: nsIWebProgress, request: nsIRequest, flags: number}>}
    838   */
    839  waitForLocationChange(tabbrowser, url) {
    840    return new Promise(resolve => {
    841      let progressListener = {
    842        onLocationChange(browser, webProgress, request, newURI, flags) {
    843          if (
    844            (url && newURI.spec != url) ||
    845            (!url && newURI.spec == "about:blank")
    846          ) {
    847            return;
    848          }
    849 
    850          tabbrowser.removeTabsProgressListener(progressListener);
    851          resolve({ webProgress, request, flags });
    852        },
    853      };
    854      tabbrowser.addTabsProgressListener(progressListener);
    855    });
    856  },
    857 
    858  /**
    859   * Waits for the next browser window to open and be fully loaded.
    860   *
    861   * @param {object} aParams
    862   * @param {string} [aParams.url]
    863   *        The url to await being loaded. If unset this may or may not wait for
    864   *        any page to be loaded, according to the waitForAnyURLLoaded param.
    865   * @param {bool} [aParams.waitForAnyURLLoaded] When `url` is unset, this
    866   *        controls whether to wait for any initial URL to be loaded.
    867   *        Defaults to false, that means the initial browser may or may not
    868   *        have finished loading its first page when this resolves.
    869   *        When `url` is set, this is ignored, thus the load is always awaited for.
    870   * @param {bool} [aParams.anyWindow]
    871   * @param {bool} [aParams.maybeErrorPage]
    872   *        See ``browserLoaded`` function.
    873   * @return {Promise}
    874   *         A Promise which resolves the next time that a DOM window
    875   *         opens and the delayed startup observer notification fires.
    876   */
    877  waitForNewWindow(aParams = {}) {
    878    let {
    879      url = null,
    880      anyWindow = false,
    881      maybeErrorPage = false,
    882      waitForAnyURLLoaded = false,
    883    } = aParams;
    884 
    885    if (anyWindow && !url) {
    886      throw new Error("url should be specified if anyWindow is true");
    887    }
    888 
    889    return new Promise((resolve, reject) => {
    890      let observe = async (win, topic) => {
    891        if (topic != "domwindowopened") {
    892          return;
    893        }
    894 
    895        try {
    896          if (!anyWindow) {
    897            Services.ww.unregisterNotification(observe);
    898          }
    899 
    900          // Add these event listeners now since they may fire before the
    901          // DOMContentLoaded event down below.
    902          let promises = [
    903            this.waitForEvent(win, "focus", true),
    904            this.waitForEvent(win, "activate"),
    905          ];
    906 
    907          if (url || waitForAnyURLLoaded) {
    908            await this.waitForEvent(win, "DOMContentLoaded");
    909 
    910            if (win.document.documentURI != AppConstants.BROWSER_CHROME_URL) {
    911              return;
    912            }
    913          }
    914 
    915          if (!(win.gBrowserInit && win.gBrowserInit.delayedStartupFinished)) {
    916            promises.push(
    917              TestUtils.topicObserved(
    918                "browser-delayed-startup-finished",
    919                subject => subject == win
    920              )
    921            );
    922          }
    923 
    924          if (url || waitForAnyURLLoaded) {
    925            let loadPromise = this.browserLoaded(win.gBrowser.selectedBrowser, {
    926              includeSubFrames: false,
    927              wantLoad: waitForAnyURLLoaded ? null : url,
    928              maybeErrorPage,
    929            });
    930            promises.push(loadPromise);
    931          }
    932 
    933          await Promise.all(promises);
    934 
    935          if (anyWindow) {
    936            Services.ww.unregisterNotification(observe);
    937          }
    938          resolve(win);
    939        } catch (err) {
    940          // We failed to wait for the load in this URI. This is only an error
    941          // if `anyWindow` is not set, as if it is we can just wait for another
    942          // window.
    943          if (!anyWindow) {
    944            reject(err);
    945          }
    946        }
    947      };
    948      Services.ww.registerNotification(observe);
    949    });
    950  },
    951 
    952  /**
    953   * Starts the load of a new URI in the given browser, triggered by the system
    954   * principal.
    955   * Note this won't want for the load to be complete. For that you may either
    956   * use BrowserTestUtils.browserLoaded(), BrowserTestUtils.waitForErrorPage(),
    957   * or make your own handler.
    958   *
    959   * @param {xul:browser} browser
    960   *        A xul:browser.
    961   * @param {string} uri
    962   *        The URI to load.
    963   * @param {number} loadFlags [optional]
    964   *        Load flags to pass to nsIWebNavigation.loadURI.
    965   */
    966  startLoadingURIString(browser, uri, loadFlags) {
    967    browser.fixupAndLoadURIString(uri, {
    968      triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
    969      loadFlags,
    970    });
    971  },
    972 
    973  /**
    974   * Loads a given URI in the specified tab and waits for the load to complete.
    975   *
    976   * @param {object} options
    977   * @param {xul:browser} options.browser
    978   *   The browser to load the URI into.
    979   * @param {string} options.uriString
    980   *   The string URI to load.
    981   * @param {string} [options.finalURI]
    982   *   The expected final URI to wait for, e.g. if the load is automatically
    983   *   redirected.
    984   */
    985  loadURIString({ browser, uriString, finalURI = uriString }) {
    986    this.startLoadingURIString(browser, uriString);
    987    return this.browserLoaded(browser, { wantLoad: finalURI });
    988  },
    989 
    990  /**
    991   * Maybe create a preloaded browser and ensure it's finished loading.
    992   *
    993   * @param gBrowser (<xul:tabbrowser>)
    994   *        The tabbrowser in which to preload a browser.
    995   */
    996  async maybeCreatePreloadedBrowser(gBrowser) {
    997    let win = gBrowser.ownerGlobal;
    998    win.NewTabPagePreloading.maybeCreatePreloadedBrowser(win);
    999 
   1000    // We cannot use the regular BrowserTestUtils helper for waiting here, since that
   1001    // would try to insert the preloaded browser, which would only break things.
   1002    await lazy.ContentTask.spawn(gBrowser.preloadedBrowser, [], async () => {
   1003      await ContentTaskUtils.waitForCondition(() => {
   1004        return (
   1005          this.content.document &&
   1006          this.content.document.readyState == "complete"
   1007        );
   1008      });
   1009    });
   1010  },
   1011 
   1012  /**
   1013   * @param win (optional)
   1014   *        The window we should wait to have "domwindowopened" sent through
   1015   *        the observer service for. If this is not supplied, we'll just
   1016   *        resolve when the first "domwindowopened" notification is seen.
   1017   * @param {function} checkFn [optional]
   1018   *        Called with the nsIDOMWindow object as argument, should return true
   1019   *        if the event is the expected one, or false if it should be ignored
   1020   *        and observing should continue. If not specified, the first window
   1021   *        resolves the returned promise.
   1022   * @return {Promise}
   1023   *         A Promise which resolves when a "domwindowopened" notification
   1024   *         has been fired by the window watcher.
   1025   */
   1026  domWindowOpened(win, checkFn) {
   1027    return new Promise(resolve => {
   1028      async function observer(subject, topic) {
   1029        if (topic == "domwindowopened" && (!win || subject === win)) {
   1030          let observedWindow = subject;
   1031          if (checkFn && !(await checkFn(observedWindow))) {
   1032            return;
   1033          }
   1034          Services.ww.unregisterNotification(observer);
   1035          resolve(observedWindow);
   1036        }
   1037      }
   1038      Services.ww.registerNotification(observer);
   1039    });
   1040  },
   1041 
   1042  /**
   1043   * @param win (optional)
   1044   *        The window we should wait to have "domwindowopened" sent through
   1045   *        the observer service for. If this is not supplied, we'll just
   1046   *        resolve when the first "domwindowopened" notification is seen.
   1047   *        The promise will be resolved once the new window's document has been
   1048   *        loaded.
   1049   *
   1050   * @param {function} checkFn (optional)
   1051   *        Called with the nsIDOMWindow object as argument, should return true
   1052   *        if the event is the expected one, or false if it should be ignored
   1053   *        and observing should continue. If not specified, the first window
   1054   *        resolves the returned promise.
   1055   *
   1056   * @return {Promise}
   1057   *         A Promise which resolves when a "domwindowopened" notification
   1058   *         has been fired by the window watcher.
   1059   */
   1060  domWindowOpenedAndLoaded(win, checkFn) {
   1061    return this.domWindowOpened(win, async observedWin => {
   1062      await this.waitForEvent(observedWin, "load");
   1063      if (checkFn && !(await checkFn(observedWin))) {
   1064        return false;
   1065      }
   1066      return true;
   1067    });
   1068  },
   1069 
   1070  /**
   1071   * @param win (optional)
   1072   *        The window we should wait to have "domwindowclosed" sent through
   1073   *        the observer service for. If this is not supplied, we'll just
   1074   *        resolve when the first "domwindowclosed" notification is seen.
   1075   * @return {Promise}
   1076   *         A Promise which resolves when a "domwindowclosed" notification
   1077   *         has been fired by the window watcher.
   1078   */
   1079  domWindowClosed(win) {
   1080    return new Promise(resolve => {
   1081      function observer(subject, topic) {
   1082        if (topic == "domwindowclosed" && (!win || subject === win)) {
   1083          Services.ww.unregisterNotification(observer);
   1084          resolve(subject);
   1085        }
   1086      }
   1087      Services.ww.registerNotification(observer);
   1088    });
   1089  },
   1090 
   1091  /**
   1092   * Open a new browser window from an existing one.
   1093   * This relies on OpenBrowserWindow in browser.js, and waits for the window
   1094   * to be completely loaded before resolving.
   1095   *
   1096   * @param {object} [options]
   1097   *        Options to pass to OpenBrowserWindow. Additionally, supports:
   1098   * @param {bool} [options.waitForTabURL]
   1099   *        Forces the initial browserLoaded check to wait for the tab to
   1100   *        load the given URL (instead of about:blank)
   1101   *
   1102   * @return {Promise}
   1103   *         Resolves with the new window once it is loaded.
   1104   */
   1105  async openNewBrowserWindow(options = {}) {
   1106    let startTime = ChromeUtils.now();
   1107 
   1108    let openerWindow = lazy.BrowserWindowTracker.getTopWindow({
   1109      private: false,
   1110    });
   1111    let win = lazy.BrowserWindowTracker.openWindow({
   1112      openerWindow,
   1113      ...options,
   1114    });
   1115 
   1116    let promises = [
   1117      this.waitForEvent(win, "focus", true),
   1118      this.waitForEvent(win, "activate"),
   1119    ];
   1120 
   1121    // Wait for browser-delayed-startup-finished notification, it indicates
   1122    // that the window has loaded completely and is ready to be used for
   1123    // testing.
   1124    promises.push(
   1125      TestUtils.topicObserved(
   1126        "browser-delayed-startup-finished",
   1127        subject => subject == win
   1128      ).then(() => win)
   1129    );
   1130 
   1131    promises.push(
   1132      this.firstBrowserLoaded(win, !options.waitForTabURL, browser => {
   1133        return (
   1134          !options.waitForTabURL ||
   1135          options.waitForTabURL == browser.currentURI.spec
   1136        );
   1137      })
   1138    );
   1139 
   1140    await Promise.all(promises);
   1141    ChromeUtils.addProfilerMarker(
   1142      "BrowserTestUtils",
   1143      { startTime, category: "Test" },
   1144      "openNewBrowserWindow"
   1145    );
   1146 
   1147    return win;
   1148  },
   1149 
   1150  /**
   1151   * Closes a window.
   1152   *
   1153   * @param {Window} win
   1154   *        A window to close.
   1155   *
   1156   * @return {Promise}
   1157   *         Resolves when the provided window has been closed. For browser
   1158   *         windows, the Promise will also wait until all final SessionStore
   1159   *         messages have been sent up from all browser tabs.
   1160   */
   1161  closeWindow(win) {
   1162    let closedPromise = BrowserTestUtils.windowClosed(win);
   1163    win.close();
   1164    return closedPromise;
   1165  },
   1166 
   1167  /**
   1168   * Returns a Promise that resolves when a window has finished closing.
   1169   *
   1170   * @param {Window} win
   1171   *        The closing window.
   1172   *
   1173   * @return {Promise}
   1174   *        Resolves when the provided window has been fully closed. For
   1175   *        browser windows, the Promise will also wait until all final
   1176   *        SessionStore messages have been sent up from all browser tabs.
   1177   */
   1178  windowClosed(win) {
   1179    let domWinClosedPromise = BrowserTestUtils.domWindowClosed(win);
   1180    let promises = [domWinClosedPromise];
   1181    let winType = win.document.documentElement.getAttribute("windowtype");
   1182    let flushTopic = "sessionstore-browser-shutdown-flush";
   1183 
   1184    if (winType == "navigator:browser") {
   1185      let finalMsgsPromise = new Promise(resolve => {
   1186        let browserSet = new Set(win.gBrowser.browsers);
   1187        // Ensure all browsers have been inserted or we won't get
   1188        // messages back from them.
   1189        browserSet.forEach(browser => {
   1190          win.gBrowser._insertBrowser(win.gBrowser.getTabForBrowser(browser));
   1191        });
   1192 
   1193        let observer = subject => {
   1194          if (browserSet.has(subject)) {
   1195            browserSet.delete(subject);
   1196          }
   1197          if (!browserSet.size) {
   1198            Services.obs.removeObserver(observer, flushTopic);
   1199            // Give the TabStateFlusher a chance to react to this final
   1200            // update and for the TabStateFlusher.flushWindow promise
   1201            // to resolve before we resolve.
   1202            TestUtils.executeSoon(resolve);
   1203          }
   1204        };
   1205 
   1206        Services.obs.addObserver(observer, flushTopic);
   1207      });
   1208 
   1209      promises.push(finalMsgsPromise);
   1210    }
   1211 
   1212    return Promise.all(promises);
   1213  },
   1214 
   1215  /**
   1216   * Returns a Promise that resolves once the SessionStore information for the
   1217   * given tab is updated and all listeners are called.
   1218   *
   1219   * @param {xul:tab} tab
   1220   *        The tab that will be removed.
   1221   * @returns {Promise<void>}
   1222   *   Resolves when the SessionStore information is updated.
   1223   */
   1224  waitForSessionStoreUpdate(tab) {
   1225    let browser = tab.linkedBrowser;
   1226    return TestUtils.topicObserved(
   1227      "sessionstore-browser-shutdown-flush",
   1228      s => s === browser
   1229    );
   1230  },
   1231 
   1232  /**
   1233   * @returns {Promise<void>}
   1234   *   Resolves when the locale has been changed.
   1235   */
   1236  enableRtlLocale() {
   1237    let localeChanged = TestUtils.topicObserved("intl:app-locales-changed");
   1238    Services.prefs.setStringPref("intl.l10n.pseudo", "bidi");
   1239    return localeChanged;
   1240  },
   1241 
   1242  /**
   1243   * @returns {Promise<void>}
   1244   *   Resolves when the locale has been changed.
   1245   */
   1246  disableRtlLocale() {
   1247    let localeChanged = TestUtils.topicObserved("intl:app-locales-changed");
   1248    Services.prefs.setStringPref("intl.l10n.pseudo", "");
   1249    return localeChanged;
   1250  },
   1251 
   1252  /**
   1253   * Waits for an event to be fired on a specified element.
   1254   *
   1255   * @example
   1256   *
   1257   *    let promiseEvent = BrowserTestUtils.waitForEvent(element, "eventName");
   1258   *    // Do some processing here that will cause the event to be fired
   1259   *    // ...
   1260   *    // Now wait until the Promise is fulfilled
   1261   *    let receivedEvent = await promiseEvent;
   1262   *
   1263   * @example
   1264   *    // The promise resolution/rejection handler for the returned promise is
   1265   *    // guaranteed not to be called until the next event tick after the event
   1266   *    // listener gets called, so that all other event listeners for the element
   1267   *    // are executed before the handler is executed.
   1268   *
   1269   *    let promiseEvent = BrowserTestUtils.waitForEvent(element, "eventName");
   1270   *    // Same event tick here.
   1271   *    await promiseEvent;
   1272   *    // Next event tick here.
   1273   *
   1274   * @example
   1275   *    // If some code, such like adding yet another event listener, needs to be
   1276   *    // executed in the same event tick, use raw addEventListener instead and
   1277   *    // place the code inside the event listener.
   1278   *
   1279   *    element.addEventListener("load", () => {
   1280   *      // Add yet another event listener in the same event tick as the load
   1281   *      // event listener.
   1282   *      p = BrowserTestUtils.waitForEvent(element, "ready");
   1283   *    }, { once: true });
   1284   *
   1285   * @param {Element} subject
   1286   *        The element that should receive the event.
   1287   * @param {string} eventName
   1288   *        Name of the event to listen to.
   1289   * @param {bool} [capture]
   1290   *        True to use a capturing listener.
   1291   * @param {function} [checkFn]
   1292   *        Called with the Event object as argument, should return true if the
   1293   *        event is the expected one, or false if it should be ignored and
   1294   *        listening should continue. If not specified, the first event with
   1295   *        the specified name resolves the returned promise.
   1296   * @param {bool} [wantsUntrusted=false]
   1297   *        True to receive synthetic events dispatched by web content.
   1298   *
   1299   * Note: Because this function is intended for testing, any error in checkFn
   1300   *       will cause the returned promise to be rejected instead of waiting for
   1301   *       the next event, since this is probably a bug in the test.
   1302   *
   1303   * @returns {Promise<Event>}
   1304   */
   1305  waitForEvent(subject, eventName, capture, checkFn, wantsUntrusted) {
   1306    let startTime = ChromeUtils.now();
   1307    let innerWindowId = subject.ownerGlobal?.windowGlobalChild.innerWindowId;
   1308 
   1309    return new Promise((resolve, reject) => {
   1310      let removed = false;
   1311      function listener(event) {
   1312        function cleanup() {
   1313          removed = true;
   1314          // Avoid keeping references to objects after the promise resolves.
   1315          subject = null;
   1316          checkFn = null;
   1317        }
   1318        try {
   1319          if (checkFn && !checkFn(event)) {
   1320            return;
   1321          }
   1322          subject.removeEventListener(eventName, listener, capture);
   1323          cleanup();
   1324          TestUtils.executeSoon(() => {
   1325            ChromeUtils.addProfilerMarker(
   1326              "BrowserTestUtils",
   1327              { startTime, category: "Test", innerWindowId },
   1328              "waitForEvent: " + eventName
   1329            );
   1330            resolve(event);
   1331          });
   1332        } catch (ex) {
   1333          try {
   1334            subject.removeEventListener(eventName, listener, capture);
   1335          } catch (ex2) {
   1336            // Maybe the provided object does not support removeEventListener.
   1337          }
   1338          cleanup();
   1339          TestUtils.executeSoon(() => reject(ex));
   1340        }
   1341      }
   1342 
   1343      subject.addEventListener(eventName, listener, capture, wantsUntrusted);
   1344 
   1345      TestUtils.promiseTestFinished?.then(() => {
   1346        if (removed) {
   1347          return;
   1348        }
   1349 
   1350        subject.removeEventListener(eventName, listener, capture);
   1351        let text = eventName + " listener";
   1352        if (subject.id) {
   1353          text += ` on #${subject.id}`;
   1354        }
   1355        text += " not removed before the end of test";
   1356        reject(text);
   1357        ChromeUtils.addProfilerMarker(
   1358          "BrowserTestUtils",
   1359          { startTime, category: "Test", innerWindowId },
   1360          "waitForEvent: " + text
   1361        );
   1362      });
   1363    });
   1364  },
   1365 
   1366  /**
   1367   * Like waitForEvent, but adds the event listener to the message manager
   1368   * global for browser.
   1369   *
   1370   * @param {string} eventName
   1371   *        Name of the event to listen to.
   1372   * @param {bool} capture [optional]
   1373   *        Whether to use a capturing listener.
   1374   * @param {function} checkFn [optional]
   1375   *        Called with the Event object as argument, should return true if the
   1376   *        event is the expected one, or false if it should be ignored and
   1377   *        listening should continue. If not specified, the first event with
   1378   *        the specified name resolves the returned promise.
   1379   * @param {bool} wantUntrusted [optional]
   1380   *        Whether to accept untrusted events
   1381   *
   1382   * Note: As of bug 1588193, this function no longer rejects the returned
   1383   *       promise in the case of a checkFn error. Instead, since checkFn is now
   1384   *       called through eval in the content process, the error is thrown in
   1385   *       the listener created by ContentEventListenerChild. Work to improve
   1386   *       error handling (eg. to reject the promise as before and to preserve
   1387   *       the filename/stack) is being tracked in bug 1593811.
   1388   *
   1389   * @returns {Promise<string>}
   1390   *   Resolves with the event name.
   1391   */
   1392  waitForContentEvent(
   1393    browser,
   1394    eventName,
   1395    capture = false,
   1396    checkFn,
   1397    wantUntrusted = false
   1398  ) {
   1399    return new Promise(resolve => {
   1400      let removeEventListener = this.addContentEventListener(
   1401        browser,
   1402        eventName,
   1403        () => {
   1404          removeEventListener();
   1405          resolve(eventName);
   1406        },
   1407        { capture, wantUntrusted },
   1408        checkFn
   1409      );
   1410    });
   1411  },
   1412 
   1413  /**
   1414   * Like waitForEvent, but acts on a popup. It ensures the popup is not already
   1415   * in the expected state.
   1416   *
   1417   * @param {Element} popup
   1418   *        The popup element that should receive the event.
   1419   * @param {string} eventSuffix
   1420   *        The event suffix expected to be received, one of "shown" or "hidden".
   1421   * @returns {Promise}
   1422   */
   1423  waitForPopupEvent(popup, eventSuffix) {
   1424    let endState = { shown: "open", hidden: "closed" }[eventSuffix];
   1425 
   1426    if (popup.state == endState) {
   1427      return Promise.resolve();
   1428    }
   1429    return this.waitForEvent(popup, "popup" + eventSuffix);
   1430  },
   1431 
   1432  /**
   1433   * Waits for the select popup to be shown. This is needed because the select
   1434   * dropdown is created lazily.
   1435   *
   1436   * @param {Window} win
   1437   *        A window to expect the popup in.
   1438   *
   1439   * @return {Promise}
   1440   *        Resolves when the popup has been fully opened. The resolution value
   1441   *        is the select popup.
   1442   */
   1443  async waitForSelectPopupShown(win) {
   1444    let getMenulist = () =>
   1445      win.document.getElementById("ContentSelectDropdown");
   1446    let menulist = getMenulist();
   1447    if (!menulist) {
   1448      await this.waitForMutationCondition(
   1449        win.document,
   1450        { childList: true, subtree: true },
   1451        getMenulist
   1452      );
   1453      menulist = getMenulist();
   1454      if (menulist.menupopup.state == "open") {
   1455        return menulist.menupopup;
   1456      }
   1457    }
   1458    await this.waitForEvent(menulist.menupopup, "popupshown");
   1459    return menulist.menupopup;
   1460  },
   1461 
   1462  /**
   1463   * Waits for the datetime picker popup to be shown.
   1464   *
   1465   * @param {Window} win
   1466   *        A window to expect the popup in.
   1467   *
   1468   * @return {Promise}
   1469   *        Resolves when the popup has been fully opened. The resolution value
   1470   *        is the select popup.
   1471   */
   1472  async waitForDateTimePickerPanelShown(win) {
   1473    let getPanel = () => win.document.getElementById("DateTimePickerPanel");
   1474    let panel = getPanel();
   1475    let ensureReady = async () => {
   1476      let frame = panel.querySelector("#DateTimePickerPanelPopupFrame");
   1477      let isValidUrl = () => {
   1478        return (
   1479          frame.browsingContext?.currentURI?.spec ==
   1480          "chrome://global/content/datetimepicker.xhtml"
   1481        );
   1482      };
   1483 
   1484      // Ensure it's loaded.
   1485      if (!isValidUrl() || frame.contentDocument.readyState != "complete") {
   1486        await new Promise(resolve => {
   1487          frame.addEventListener(
   1488            "load",
   1489            function listener() {
   1490              if (isValidUrl()) {
   1491                frame.removeEventListener("load", listener, { capture: true });
   1492                resolve();
   1493              }
   1494            },
   1495            { capture: true }
   1496          );
   1497        });
   1498      }
   1499 
   1500      // Ensure it's ready.
   1501      if (!frame.contentWindow.PICKER_READY) {
   1502        await new Promise(resolve => {
   1503          frame.contentDocument.addEventListener("PickerReady", resolve, {
   1504            once: true,
   1505          });
   1506        });
   1507      }
   1508      // And that l10n mutations are flushed.
   1509      // FIXME(bug 1828721): We should ideally localize everything before
   1510      // showing the panel.
   1511      if (frame.contentDocument.hasPendingL10nMutations) {
   1512        await new Promise(resolve => {
   1513          frame.contentDocument.addEventListener(
   1514            "L10nMutationsFinished",
   1515            resolve,
   1516            {
   1517              once: true,
   1518            }
   1519          );
   1520        });
   1521      }
   1522    };
   1523 
   1524    if (!panel) {
   1525      await this.waitForMutationCondition(
   1526        win.document,
   1527        { childList: true, subtree: true },
   1528        getPanel
   1529      );
   1530      panel = getPanel();
   1531      if (panel.state == "open") {
   1532        await ensureReady();
   1533        return panel;
   1534      }
   1535    }
   1536    await this.waitForEvent(panel, "popupshown");
   1537    await ensureReady();
   1538    return panel;
   1539  },
   1540 
   1541  /**
   1542   * Adds a content event listener on the given browser
   1543   * element. Similar to waitForContentEvent, but the listener will
   1544   * fire until it is removed. A callable object is returned that,
   1545   * when called, removes the event listener. Note that this function
   1546   * works even if the browser's frameloader is swapped.
   1547   *
   1548   * @param {xul:browser} browser
   1549   *        The browser element to listen for events in.
   1550   * @param {string} eventName
   1551   *        Name of the event to listen to.
   1552   * @param {function} listener
   1553   *        Function to call in parent process when event fires.
   1554   *        Not passed any arguments.
   1555   * @param {object} listenerOptions [optional]
   1556   *        Options to pass to the event listener.
   1557   * @param {function} checkFn [optional]
   1558   *        Called with the Event object as argument, should return true if the
   1559   *        event is the expected one, or false if it should be ignored and
   1560   *        listening should continue. If not specified, the first event with
   1561   *        the specified name resolves the returned promise. This is called
   1562   *        within the content process and can have no closure environment.
   1563   *
   1564   * @returns function
   1565   *        If called, the return value will remove the event listener.
   1566   */
   1567  addContentEventListener(
   1568    browser,
   1569    eventName,
   1570    listener,
   1571    listenerOptions = {},
   1572    checkFn
   1573  ) {
   1574    let id = gListenerId++;
   1575    let contentEventListeners = this._contentEventListeners;
   1576    contentEventListeners.set(id, {
   1577      listener,
   1578      browserId: browser.browserId,
   1579    });
   1580 
   1581    let eventListenerState = this._contentEventListenerSharedState;
   1582    eventListenerState.set(id, {
   1583      eventName,
   1584      listenerOptions,
   1585      checkFnSource: checkFn ? checkFn.toSource() : "",
   1586    });
   1587 
   1588    Services.ppmm.sharedData.set(
   1589      "BrowserTestUtils:ContentEventListener",
   1590      eventListenerState
   1591    );
   1592    Services.ppmm.sharedData.flush();
   1593 
   1594    let unregisterFunction = function () {
   1595      if (!eventListenerState.has(id)) {
   1596        return;
   1597      }
   1598      eventListenerState.delete(id);
   1599      contentEventListeners.delete(id);
   1600      Services.ppmm.sharedData.set(
   1601        "BrowserTestUtils:ContentEventListener",
   1602        eventListenerState
   1603      );
   1604      Services.ppmm.sharedData.flush();
   1605    };
   1606    return unregisterFunction;
   1607  },
   1608 
   1609  /**
   1610   * This is an internal method to be invoked by
   1611   * BrowserTestUtilsParent.sys.mjs when a content event we were listening for
   1612   * happens.
   1613   *
   1614   * @private
   1615   */
   1616  _receivedContentEventListener(listenerId, browserId) {
   1617    let listenerData = this._contentEventListeners.get(listenerId);
   1618    if (!listenerData) {
   1619      return;
   1620    }
   1621    if (listenerData.browserId != browserId) {
   1622      return;
   1623    }
   1624    listenerData.listener();
   1625  },
   1626 
   1627  /**
   1628   * This is an internal method that cleans up any state from content event
   1629   * listeners.
   1630   *
   1631   * @private
   1632   */
   1633  _cleanupContentEventListeners() {
   1634    this._contentEventListeners.clear();
   1635 
   1636    if (this._contentEventListenerSharedState.size != 0) {
   1637      this._contentEventListenerSharedState.clear();
   1638      Services.ppmm.sharedData.set(
   1639        "BrowserTestUtils:ContentEventListener",
   1640        this._contentEventListenerSharedState
   1641      );
   1642      Services.ppmm.sharedData.flush();
   1643    }
   1644 
   1645    if (this._contentEventListenerActorRegistered) {
   1646      this._contentEventListenerActorRegistered = false;
   1647      ChromeUtils.unregisterWindowActor("ContentEventListener");
   1648    }
   1649  },
   1650 
   1651  observe(subject, topic) {
   1652    switch (topic) {
   1653      case "test-complete":
   1654        this._cleanupContentEventListeners();
   1655        break;
   1656    }
   1657  },
   1658 
   1659  /**
   1660   * Wait until DOM mutations cause the condition expressed in checkFn
   1661   * to pass.
   1662   *
   1663   * Intended as an easy-to-use alternative to waitForCondition.
   1664   *
   1665   * @param {Element} target    The target in which to observe mutations.
   1666   * @param {object}  options   The options to pass to MutationObserver.observe();
   1667   * @param {function} checkFn  Function that returns true when it wants the promise to be
   1668   * resolved.
   1669   */
   1670  waitForMutationCondition(target, options, checkFn) {
   1671    if (checkFn()) {
   1672      return Promise.resolve();
   1673    }
   1674    return new Promise(resolve => {
   1675      let obs = new target.ownerGlobal.MutationObserver(function () {
   1676        if (checkFn()) {
   1677          obs.disconnect();
   1678          resolve();
   1679        }
   1680      });
   1681      obs.observe(target, options);
   1682    });
   1683  },
   1684 
   1685  /**
   1686   * Like browserLoaded, but waits for an error page to appear.
   1687   *
   1688   * @param {xul:browser} browser
   1689   *        A xul:browser.
   1690   *
   1691   * @return {Promise<string>}
   1692   *   Resolves when an error page has been loaded in the browser, with the name
   1693   *   of the event.
   1694   */
   1695  waitForErrorPage(browser) {
   1696    return this.waitForContentEvent(
   1697      browser,
   1698      "AboutNetErrorLoad",
   1699      false,
   1700      null,
   1701      true
   1702    );
   1703  },
   1704 
   1705  /**
   1706   * Waits for the next top-level document load in the current browser.  The URI
   1707   * of the document is compared against expectedURL.  The load is then stopped
   1708   * before it actually starts.
   1709   *
   1710   * @param {string} expectedURL
   1711   *        The URL of the document that is expected to load.
   1712   * @param {object} browser
   1713   *        The browser to wait for.
   1714   * @param {function} checkFn (optional)
   1715   *        Function to run on the channel before stopping it.
   1716   * @returns {Promise}
   1717   */
   1718  waitForDocLoadAndStopIt(expectedURL, browser, checkFn) {
   1719    let isHttp = url => /^https?:/.test(url);
   1720 
   1721    return new Promise(resolve => {
   1722      // Redirect non-http URIs to http://mochi.test:8888/, so we can still
   1723      // use http-on-before-connect to listen for loads. Since we're
   1724      // aborting the load as early as possible, it doesn't matter whether the
   1725      // server handles it sensibly or not. However, this also means that this
   1726      // helper shouldn't be used to load local URIs (about pages, chrome://
   1727      // URIs, etc).
   1728      let proxyFilter;
   1729      if (!isHttp(expectedURL)) {
   1730        proxyFilter = {
   1731          proxyInfo: lazy.ProtocolProxyService.newProxyInfo(
   1732            "http",
   1733            "mochi.test",
   1734            8888,
   1735            "",
   1736            "",
   1737            0,
   1738            4096,
   1739            null
   1740          ),
   1741 
   1742          applyFilter(channel, defaultProxyInfo, callback) {
   1743            callback.onProxyFilterResult(
   1744              isHttp(channel.URI.spec) ? defaultProxyInfo : this.proxyInfo
   1745            );
   1746          },
   1747        };
   1748 
   1749        lazy.ProtocolProxyService.registerChannelFilter(proxyFilter, 0);
   1750      }
   1751 
   1752      function observer(chan) {
   1753        chan.QueryInterface(Ci.nsIHttpChannel);
   1754        if (!chan.originalURI || chan.originalURI.spec !== expectedURL) {
   1755          return;
   1756        }
   1757        if (checkFn && !checkFn(chan)) {
   1758          return;
   1759        }
   1760 
   1761        // TODO: We should check that the channel's BrowsingContext matches
   1762        // the browser's. See bug 1587114.
   1763 
   1764        try {
   1765          chan.cancel(Cr.NS_BINDING_ABORTED);
   1766        } finally {
   1767          if (proxyFilter) {
   1768            lazy.ProtocolProxyService.unregisterChannelFilter(proxyFilter);
   1769          }
   1770          Services.obs.removeObserver(observer, "http-on-before-connect");
   1771          resolve();
   1772        }
   1773      }
   1774 
   1775      Services.obs.addObserver(observer, "http-on-before-connect");
   1776    });
   1777  },
   1778 
   1779  /**
   1780   *  Versions of EventUtils.sys.mjs synthesizeMouse functions that synthesize a
   1781   *  mouse event in a child process and return promises that resolve when the
   1782   *  event has fired and completed. Instead of a window, a browser or
   1783   *  browsing context is required to be passed to this function.
   1784   *
   1785   * @param target
   1786   *        One of the following:
   1787   *        - a selector string that identifies the element to target. The syntax is as
   1788   *          for querySelector.
   1789   *        - a function to be run in the content process that returns the element to
   1790   *        target
   1791   *        - null, in which case the offset is from the content document's edge.
   1792   * @param {integer} offsetX
   1793   *        x offset from target's left bounding edge
   1794   * @param {integer} offsetY
   1795   *        y offset from target's top bounding edge
   1796   * @param {object} event object
   1797   *        Additional arguments, similar to the EventUtils.sys.mjs version
   1798   * @param {BrowserContext|MozFrameLoaderOwner} browsingContext
   1799   *        Browsing context or browser element, must not be null
   1800   * @param {boolean} handlingUserInput
   1801   *        Whether the synthesize should be perfomed while simulating
   1802   *        user interaction (making windowUtils.isHandlingUserInput be true).
   1803   *
   1804   * @returns {Promise<boolean>}
   1805   *   Resolves to true if the mouse event was cancelled.
   1806   */
   1807  synthesizeMouse(
   1808    target,
   1809    offsetX,
   1810    offsetY,
   1811    event,
   1812    browsingContext,
   1813    handlingUserInput
   1814  ) {
   1815    let targetFn = null;
   1816    if (typeof target == "function") {
   1817      targetFn = target.toString();
   1818      target = null;
   1819    } else if (typeof target != "string" && !Array.isArray(target)) {
   1820      target = null;
   1821    }
   1822 
   1823    browsingContext = this.getBrowsingContextFrom(browsingContext);
   1824    return this.sendQuery(browsingContext, "Test:SynthesizeMouse", {
   1825      target,
   1826      targetFn,
   1827      x: offsetX,
   1828      y: offsetY,
   1829      event,
   1830      handlingUserInput,
   1831    });
   1832  },
   1833 
   1834  /**
   1835   *  Versions of EventUtils.sys.mjs synthesizeTouch functions that synthesize a
   1836   *  touch event in a child process and return promises that resolve when the
   1837   *  event has fired and completed. Instead of a window, a browser or
   1838   *  browsing context is required to be passed to this function.
   1839   *
   1840   * @param target
   1841   *        One of the following:
   1842   *        - a selector string that identifies the element to target. The syntax is as
   1843   *          for querySelector.
   1844   *        - a function to be run in the content process that returns the element to
   1845   *        target
   1846   *        - null, in which case the offset is from the content document's edge.
   1847   * @param {integer} offsetX
   1848   *        x offset from target's left bounding edge
   1849   * @param {integer} offsetY
   1850   *        y offset from target's top bounding edge
   1851   * @param {object} event object
   1852   *        Additional arguments, similar to the EventUtils.sys.mjs version
   1853   * @param {BrowserContext|MozFrameLoaderOwner} browsingContext
   1854   *        Browsing context or browser element, must not be null
   1855   *
   1856   * @returns {Promise<boolean>}
   1857   *   Resolves to true if the touch event was cancelled.
   1858   */
   1859  synthesizeTouch(target, offsetX, offsetY, event, browsingContext) {
   1860    let targetFn = null;
   1861    if (typeof target == "function") {
   1862      targetFn = target.toString();
   1863      target = null;
   1864    } else if (typeof target != "string" && !Array.isArray(target)) {
   1865      target = null;
   1866    }
   1867 
   1868    browsingContext = this.getBrowsingContextFrom(browsingContext);
   1869    return this.sendQuery(browsingContext, "Test:SynthesizeTouch", {
   1870      target,
   1871      targetFn,
   1872      x: offsetX,
   1873      y: offsetY,
   1874      event,
   1875    });
   1876  },
   1877 
   1878  /**
   1879   * Wait for a message to be fired from a particular message manager
   1880   *
   1881   * @param {nsIMessageManager} messageManager
   1882   *                            The message manager that should be used.
   1883   * @param {string}            message
   1884   *                            The message we're waiting for.
   1885   * @param {Function}          checkFn (optional)
   1886   *                            Optional function to invoke to check the message.
   1887   */
   1888  waitForMessage(messageManager, message, checkFn) {
   1889    return new Promise(resolve => {
   1890      messageManager.addMessageListener(message, function onMessage(msg) {
   1891        if (!checkFn || checkFn(msg)) {
   1892          messageManager.removeMessageListener(message, onMessage);
   1893          resolve(msg.data);
   1894        }
   1895      });
   1896    });
   1897  },
   1898 
   1899  /**
   1900   *  Version of synthesizeMouse that uses the center of the target as the mouse
   1901   *  location. Arguments and the return value are the same.
   1902   */
   1903  synthesizeMouseAtCenter(target, event, browsingContext) {
   1904    // Use a flag to indicate to center rather than having a separate message.
   1905    event.centered = true;
   1906    return BrowserTestUtils.synthesizeMouse(
   1907      target,
   1908      0,
   1909      0,
   1910      event,
   1911      browsingContext
   1912    );
   1913  },
   1914 
   1915  /**
   1916   *  Version of synthesizeMouse that uses a client point within the child
   1917   *  window instead of a target as the offset. Otherwise, the arguments and
   1918   *  return value are the same as synthesizeMouse.
   1919   */
   1920  synthesizeMouseAtPoint(offsetX, offsetY, event, browsingContext) {
   1921    return BrowserTestUtils.synthesizeMouse(
   1922      null,
   1923      offsetX,
   1924      offsetY,
   1925      event,
   1926      browsingContext
   1927    );
   1928  },
   1929 
   1930  /**
   1931   * Removes the given tab from its parent tabbrowser.
   1932   * This method doesn't SessionStore etc.
   1933   *
   1934   * @param (tab) tab
   1935   *        The tab to remove.
   1936   * @param (Object) options
   1937   *        Extra options to pass to tabbrowser's removeTab method.
   1938   */
   1939  removeTab(tab, options = {}) {
   1940    tab.ownerGlobal.gBrowser.removeTab(tab, options);
   1941  },
   1942 
   1943  /**
   1944   * Returns a Promise that resolves once the tab starts closing.
   1945   *
   1946   * @param {tab} tab
   1947   *        The tab that will be removed.
   1948   * @returns {Promise<Event>}
   1949   *   Resolves with the event when the tab starts closing.
   1950   */
   1951  waitForTabClosing(tab) {
   1952    return this.waitForEvent(tab, "TabClose");
   1953  },
   1954 
   1955  /**
   1956   *
   1957   * @param {tab} tab
   1958   *        The tab that will be reloaded.
   1959   * @param {object} [options]
   1960   *        Options for the reload.
   1961   * @param {boolean} options.includeSubFrames = false [optional]
   1962   *        A boolean indicating if loads from subframes should be included
   1963   *        when waiting for the frame to reload.
   1964   * @param {boolean} options.bypassCache = false [optional]
   1965   *        A boolean indicating if loads should bypass the cache.
   1966   *        If bypassCache is true, this skips some steps that normally happen
   1967   *        when a user reloads a tab.
   1968   * @returns {Promise}
   1969   *   Resolves when the tab finishes reloading.
   1970   */
   1971  reloadTab(tab, options = {}) {
   1972    const finished = BrowserTestUtils.browserLoaded(tab.linkedBrowser, {
   1973      includeSubFrames: !!options.includeSubFrames,
   1974    });
   1975    if (options.bypassCache) {
   1976      tab.linkedBrowser.reloadWithFlags(
   1977        Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_CACHE
   1978      );
   1979    } else {
   1980      tab.ownerGlobal.gBrowser.reloadTab(tab);
   1981    }
   1982    return finished;
   1983  },
   1984 
   1985  /**
   1986   * Create enough tabs to cause a tab overflow in the given window.
   1987   *
   1988   * @param {Function|null} registerCleanupFunction
   1989   *    The test framework doesn't keep its cleanup stuff anywhere accessible,
   1990   *    so the first argument is a reference to your cleanup registration
   1991   *    function, allowing us to clean up after you if necessary. This can be
   1992   *    null if you are using a temporary window for the test.
   1993   * @param {Window} win
   1994   *    The window where the tabs need to be overflowed.
   1995   * @param {object} params [optional]
   1996   *        Parameters object for BrowserTestUtils.overflowTabs.
   1997   *        overflowAtStart: bool
   1998   *          Determines whether the new tabs are added at the beginning of the
   1999   *          URL bar or at the end of it.
   2000   *        overflowTabFactor: 3 | 1.1
   2001   *          Factor that helps in determining the tab count for overflow.
   2002   */
   2003  async overflowTabs(registerCleanupFunction, win, params = {}) {
   2004    if (!params.hasOwnProperty("overflowAtStart")) {
   2005      params.overflowAtStart = true;
   2006    }
   2007    if (!params.hasOwnProperty("overflowTabFactor")) {
   2008      params.overflowTabFactor = 1.1;
   2009    }
   2010    let { gBrowser } = win;
   2011    let overflowDirection = gBrowser.tabContainer.verticalMode
   2012      ? "height"
   2013      : "width";
   2014    let tabIndex = params.overflowAtStart ? 0 : undefined;
   2015    let arrowScrollbox = gBrowser.tabContainer.arrowScrollbox;
   2016    if (arrowScrollbox.hasAttribute("overflowing")) {
   2017      return;
   2018    }
   2019    let promises = [];
   2020    promises.push(
   2021      BrowserTestUtils.waitForEvent(
   2022        arrowScrollbox,
   2023        "overflow",
   2024        false,
   2025        e => e.target == arrowScrollbox
   2026      )
   2027    );
   2028    const originalSmoothScroll = arrowScrollbox.smoothScroll;
   2029    arrowScrollbox.smoothScroll = false;
   2030    if (registerCleanupFunction) {
   2031      registerCleanupFunction(() => {
   2032        arrowScrollbox.smoothScroll = originalSmoothScroll;
   2033      });
   2034    }
   2035 
   2036    let size = ele => ele.getBoundingClientRect()[overflowDirection];
   2037    let tabMinSize = gBrowser.tabContainer.verticalMode
   2038      ? size(gBrowser.selectedTab)
   2039      : parseInt(win.getComputedStyle(gBrowser.selectedTab).minWidth);
   2040    let tabCountForOverflow = Math.ceil(
   2041      (size(arrowScrollbox) / tabMinSize) * params.overflowTabFactor
   2042    );
   2043    while (gBrowser.tabs.length < tabCountForOverflow) {
   2044      promises.push(
   2045        BrowserTestUtils.addTab(gBrowser, "about:blank", {
   2046          skipAnimation: true,
   2047          tabIndex,
   2048        })
   2049      );
   2050    }
   2051    await Promise.all(promises);
   2052  },
   2053 
   2054  /**
   2055   * Crashes a remote frame tab and cleans up the generated minidumps.
   2056   * Resolves with the data from the .extra file (the crash annotations).
   2057   *
   2058   * @param (Browser) browser
   2059   *        A remote <xul:browser> element. Must not be null.
   2060   * @param (bool) shouldShowTabCrashPage
   2061   *        True if it is expected that the tab crashed page will be shown
   2062   *        for this browser. If so, the Promise will only resolve once the
   2063   *        tab crash page has loaded.
   2064   * @param (bool) shouldClearMinidumps
   2065   *        True if the minidumps left behind by the crash should be removed.
   2066   * @param (BrowsingContext) browsingContext
   2067   *        The context where the frame leaves. Default to
   2068   *        top level context if not supplied.
   2069   * @param (object?) options
   2070   *        An object with any of the following fields:
   2071   *          crashType: "CRASH_INVALID_POINTER_DEREF" | "CRASH_OOM" | "CRASH_SYSCALL"
   2072   *            The type of crash. If unspecified, default to "CRASH_INVALID_POINTER_DEREF"
   2073   *          asyncCrash: bool
   2074   *            If specified and `true`, cause the crash asynchronously.
   2075   *
   2076   * @returns (Promise)
   2077   *   An Object with key-value pairs representing the data from the crash
   2078   *   report's extra file (if applicable).
   2079   */
   2080  async crashFrame(
   2081    browser,
   2082    shouldShowTabCrashPage = true,
   2083    shouldClearMinidumps = true,
   2084    browsingContext,
   2085    options = {}
   2086  ) {
   2087    let extra = {};
   2088 
   2089    if (!browser.isRemoteBrowser) {
   2090      throw new Error("<xul:browser> needs to be remote in order to crash");
   2091    }
   2092 
   2093    /**
   2094     * Returns the directory where crash dumps are stored.
   2095     *
   2096     * @return nsIFile
   2097     */
   2098    function getMinidumpDirectory() {
   2099      let dir = Services.dirsvc.get("ProfD", Ci.nsIFile);
   2100      dir.append("minidumps");
   2101      return dir;
   2102    }
   2103 
   2104    /**
   2105     * Removes a file from a directory. This is a no-op if the file does not
   2106     * exist.
   2107     *
   2108     * @param directory
   2109     *        The nsIFile representing the directory to remove from.
   2110     * @param filename
   2111     *        A string for the file to remove from the directory.
   2112     */
   2113    function removeFile(directory, filename) {
   2114      let file = directory.clone();
   2115      file.append(filename);
   2116      if (file.exists()) {
   2117        file.remove(false);
   2118      }
   2119    }
   2120 
   2121    let expectedPromises = [];
   2122 
   2123    let crashCleanupPromise = new Promise((resolve, reject) => {
   2124      let observer = (subject, topic) => {
   2125        if (topic != "ipc:content-shutdown") {
   2126          reject("Received incorrect observer topic: " + topic);
   2127          return;
   2128        }
   2129        if (!(subject instanceof Ci.nsIPropertyBag2)) {
   2130          reject("Subject did not implement nsIPropertyBag2");
   2131          return;
   2132        }
   2133        // we might see this called as the process terminates due to previous tests.
   2134        // We are only looking for "abnormal" exits...
   2135        if (!subject.hasKey("abnormal")) {
   2136          dump(
   2137            "\nThis is a normal termination and isn't the one we are looking for...\n"
   2138          );
   2139          return;
   2140        }
   2141 
   2142        Services.obs.removeObserver(observer, "ipc:content-shutdown");
   2143 
   2144        let dumpID;
   2145        if (AppConstants.MOZ_CRASHREPORTER) {
   2146          dumpID = subject.getPropertyAsAString("dumpID");
   2147          if (!dumpID) {
   2148            reject(
   2149              "dumpID was not present despite crash reporting being enabled"
   2150            );
   2151            return;
   2152          }
   2153        }
   2154 
   2155        let removalPromise = Promise.resolve();
   2156 
   2157        if (dumpID) {
   2158          removalPromise = Services.crashmanager
   2159            .ensureCrashIsPresent(dumpID)
   2160            .then(async () => {
   2161              let minidumpDirectory = getMinidumpDirectory();
   2162              let extrafile = minidumpDirectory.clone();
   2163              extrafile.append(dumpID + ".extra");
   2164              if (extrafile.exists()) {
   2165                if (AppConstants.MOZ_CRASHREPORTER) {
   2166                  extra = await IOUtils.readJSON(extrafile.path);
   2167                } else {
   2168                  dump(
   2169                    "\nCrashReporter not enabled - will not return any extra data\n"
   2170                  );
   2171                }
   2172              } else {
   2173                dump(`\nNo .extra file for dumpID: ${dumpID}\n`);
   2174              }
   2175 
   2176              if (shouldClearMinidumps) {
   2177                removeFile(minidumpDirectory, dumpID + ".dmp");
   2178                removeFile(minidumpDirectory, dumpID + ".extra");
   2179              }
   2180            });
   2181        }
   2182 
   2183        removalPromise.then(() => {
   2184          dump("\nCrash cleaned up\n");
   2185          // There might be other ipc:content-shutdown handlers that need to
   2186          // run before we want to continue, so we'll resolve on the next tick
   2187          // of the event loop.
   2188          TestUtils.executeSoon(() => resolve());
   2189        });
   2190      };
   2191 
   2192      Services.obs.addObserver(observer, "ipc:content-shutdown");
   2193    });
   2194 
   2195    expectedPromises.push(crashCleanupPromise);
   2196 
   2197    if (shouldShowTabCrashPage) {
   2198      expectedPromises.push(
   2199        new Promise(resolve => {
   2200          browser.addEventListener(
   2201            "AboutTabCrashedReady",
   2202            function onCrash() {
   2203              browser.removeEventListener("AboutTabCrashedReady", onCrash);
   2204              dump("\nabout:tabcrashed loaded and ready\n");
   2205              resolve();
   2206            },
   2207            false,
   2208            true
   2209          );
   2210        })
   2211      );
   2212    }
   2213 
   2214    // Trigger crash by sending a message to BrowserTestUtils actor.
   2215    this.sendAsyncMessage(
   2216      browsingContext || browser.browsingContext,
   2217      "BrowserTestUtils:CrashFrame",
   2218      {
   2219        crashType: options.crashType || "",
   2220        asyncCrash: options.asyncCrash || false,
   2221      }
   2222    );
   2223 
   2224    await Promise.all(expectedPromises);
   2225 
   2226    if (shouldShowTabCrashPage) {
   2227      let gBrowser = browser.ownerGlobal.gBrowser;
   2228      let tab = gBrowser.getTabForBrowser(browser);
   2229      if (tab.getAttribute("crashed") != "true") {
   2230        throw new Error("Tab should be marked as crashed");
   2231      }
   2232    }
   2233 
   2234    return extra;
   2235  },
   2236 
   2237  /**
   2238   * Attempts to simulate a launch fail by crashing a browser, but
   2239   * stripping the browser of its childID so that the TabCrashHandler
   2240   * thinks it was a launch fail.
   2241   *
   2242   * @param browser (<xul:browser>)
   2243   *   The browser to simulate a content process launch failure on.
   2244   * @return {Promise<void>}
   2245   *   Resolves when the TabCrashHandler should be done handling the
   2246   *   simulated crash.
   2247   */
   2248  simulateProcessLaunchFail(browser, dueToBuildIDMismatch = false) {
   2249    const NORMAL_CRASH_TOPIC = "ipc:content-shutdown";
   2250 
   2251    Object.defineProperty(browser.frameLoader, "childID", {
   2252      get: () => 0,
   2253    });
   2254 
   2255    let sawNormalCrash = false;
   2256    let observer = () => {
   2257      sawNormalCrash = true;
   2258    };
   2259 
   2260    Services.obs.addObserver(observer, NORMAL_CRASH_TOPIC);
   2261 
   2262    Services.obs.notifyObservers(
   2263      browser.frameLoader,
   2264      "oop-frameloader-crashed"
   2265    );
   2266 
   2267    let eventType = dueToBuildIDMismatch
   2268      ? "oop-browser-buildid-mismatch"
   2269      : "oop-browser-crashed";
   2270 
   2271    let event = new browser.ownerGlobal.CustomEvent(eventType, {
   2272      bubbles: true,
   2273    });
   2274    event.isTopFrame = true;
   2275    browser.dispatchEvent(event);
   2276 
   2277    Services.obs.removeObserver(observer, NORMAL_CRASH_TOPIC);
   2278 
   2279    if (sawNormalCrash) {
   2280      throw new Error(`Unexpectedly saw ${NORMAL_CRASH_TOPIC}`);
   2281    }
   2282 
   2283    return new Promise(resolve => TestUtils.executeSoon(resolve));
   2284  },
   2285 
   2286  /**
   2287   * Returns a promise that is resolved when element gains attribute (or,
   2288   * optionally, when it is set to value).
   2289   *
   2290   * @param {string} attr
   2291   *        The attribute to wait for
   2292   * @param {Element} element
   2293   *        The element which should gain the attribute
   2294   * @param {string} value (optional)
   2295   *        Optional, the value the attribute should have.
   2296   *
   2297   * @returns {Promise}
   2298   */
   2299  waitForAttribute(attr, element, value) {
   2300    let MutationObserver = element.ownerGlobal.MutationObserver;
   2301    return new Promise(resolve => {
   2302      let mut = new MutationObserver(() => {
   2303        if (
   2304          (!value && element.hasAttribute(attr)) ||
   2305          (value && element.getAttribute(attr) === value)
   2306        ) {
   2307          resolve();
   2308          mut.disconnect();
   2309        }
   2310      });
   2311 
   2312      mut.observe(element, { attributeFilter: [attr] });
   2313    });
   2314  },
   2315 
   2316  /**
   2317   * Returns a promise that is resolved when element loses an attribute.
   2318   *
   2319   * @param {string} attr
   2320   *        The attribute to wait for
   2321   * @param {Element} element
   2322   *        The element which should lose the attribute
   2323   *
   2324   * @returns {Promise}
   2325   */
   2326  waitForAttributeRemoval(attr, element) {
   2327    if (!element.hasAttribute(attr)) {
   2328      return Promise.resolve();
   2329    }
   2330 
   2331    let MutationObserver = element.ownerGlobal.MutationObserver;
   2332    return new Promise(resolve => {
   2333      dump("Waiting for removal\n");
   2334      let mut = new MutationObserver(() => {
   2335        if (!element.hasAttribute(attr)) {
   2336          resolve();
   2337          mut.disconnect();
   2338        }
   2339      });
   2340 
   2341      mut.observe(element, { attributeFilter: [attr] });
   2342    });
   2343  },
   2344 
   2345  /**
   2346   * Version of EventUtils' `sendChar` function; it will synthesize a keypress
   2347   * event in a child process and returns a Promise that will resolve when the
   2348   * event was fired. Instead of a Window, a Browser or Browsing Context
   2349   * is required to be passed to this function.
   2350   *
   2351   * @param {string} char
   2352   *        A character for the keypress event that is sent to the browser.
   2353   * @param {BrowserContext|MozFrameLoaderOwner} browsingContext
   2354   *        Browsing context or browser element, must not be null
   2355   *
   2356   * @returns {Promise<boolean>}
   2357   *   Resolves to true if the keypress event was synthesized.
   2358   */
   2359  sendChar(char, browsingContext) {
   2360    browsingContext = this.getBrowsingContextFrom(browsingContext);
   2361    return this.sendQuery(browsingContext, "Test:SendChar", { char });
   2362  },
   2363 
   2364  /**
   2365   * Version of EventUtils' `synthesizeKey` function; it will synthesize a key
   2366   * event in a child process and returns a Promise that will resolve when the
   2367   * event was fired. Instead of a Window, a Browser or Browsing Context
   2368   * is required to be passed to this function.
   2369   *
   2370   * @param {string} key
   2371   *        See the documentation available for EventUtils#synthesizeKey.
   2372   * @param {object} event
   2373   *        See the documentation available for EventUtils#synthesizeKey.
   2374   * @param {BrowserContext|MozFrameLoaderOwner} browsingContext
   2375   *        Browsing context or browser element, must not be null
   2376   *
   2377   * @returns {Promise}
   2378   */
   2379  synthesizeKey(key, event, browsingContext) {
   2380    browsingContext = this.getBrowsingContextFrom(browsingContext);
   2381    return this.sendQuery(browsingContext, "Test:SynthesizeKey", {
   2382      key,
   2383      event,
   2384    });
   2385  },
   2386 
   2387  /**
   2388   * Version of EventUtils' `synthesizeComposition` function; it will synthesize
   2389   * a composition event in a child process and returns a Promise that will
   2390   * resolve when the event was fired. Instead of a Window, a Browser or
   2391   * Browsing Context is required to be passed to this function.
   2392   *
   2393   * @param {object} event
   2394   *        See the documentation available for EventUtils#synthesizeComposition.
   2395   * @param {BrowserContext|MozFrameLoaderOwner} browsingContext
   2396   *        Browsing context or browser element, must not be null
   2397   *
   2398   * @returns {Promise<boolean>}
   2399   *   Resolves to false if the composition event could not be synthesized.
   2400   */
   2401  synthesizeComposition(event, browsingContext) {
   2402    browsingContext = this.getBrowsingContextFrom(browsingContext);
   2403    return this.sendQuery(browsingContext, "Test:SynthesizeComposition", {
   2404      event,
   2405    });
   2406  },
   2407 
   2408  /**
   2409   * Version of EventUtils' `synthesizeCompositionChange` function; it will
   2410   * synthesize a compositionchange event in a child process and returns a
   2411   * Promise that will resolve when the event was fired. Instead of a Window, a
   2412   * Browser or Browsing Context object is required to be passed to this function.
   2413   *
   2414   * @param {object} event
   2415   *        See the documentation available for EventUtils#synthesizeCompositionChange.
   2416   * @param {BrowserContext|MozFrameLoaderOwner} browsingContext
   2417   *        Browsing context or browser element, must not be null
   2418   *
   2419   * @returns {Promise}
   2420   */
   2421  synthesizeCompositionChange(event, browsingContext) {
   2422    browsingContext = this.getBrowsingContextFrom(browsingContext);
   2423    return this.sendQuery(browsingContext, "Test:SynthesizeCompositionChange", {
   2424      event,
   2425    });
   2426  },
   2427 
   2428  // TODO: Fix consumers and remove me.
   2429  waitForCondition: TestUtils.waitForCondition,
   2430 
   2431  /**
   2432   * Waits for a <xul:notification> with a particular value to appear
   2433   * for the <xul:notificationbox> of the passed in browser.
   2434   *
   2435   * @param {xul:tabbrowser} tabbrowser
   2436   *        The gBrowser that hosts the browser that should show
   2437   *        the notification. For most tests, this will probably be
   2438   *        gBrowser.
   2439   * @param {xul:browser} browser
   2440   *        The browser that should be showing the notification.
   2441   * @param {string} notificationValue
   2442   *        The "value" of the notification, which is often used as
   2443   *        a unique identifier. Example: "plugin-crashed".
   2444   *
   2445   * @return {Promise}
   2446   *        Resolves to the <xul:notification> that is being shown.
   2447   */
   2448  waitForNotificationBar(tabbrowser, browser, notificationValue) {
   2449    let notificationBox = tabbrowser.getNotificationBox(browser);
   2450    return this.waitForNotificationInNotificationBox(
   2451      notificationBox,
   2452      notificationValue
   2453    );
   2454  },
   2455 
   2456  /**
   2457   * Waits for a <xul:notification> with a particular value to appear
   2458   * in the global <xul:notificationbox> of the given browser window.
   2459   *
   2460   * @param {Window} win
   2461   *        The browser window in whose global notificationbox the
   2462   *        notification is expected to appear.
   2463   * @param {string} notificationValue
   2464   *        The "value" of the notification, which is often used as
   2465   *        a unique identifier. Example: "captive-portal-detected".
   2466   *
   2467   * @return {Promise}
   2468   *        Resolves to the <xul:notification> that is being shown.
   2469   */
   2470  waitForGlobalNotificationBar(win, notificationValue) {
   2471    return this.waitForNotificationInNotificationBox(
   2472      win.gNotificationBox,
   2473      notificationValue
   2474    );
   2475  },
   2476 
   2477  waitForNotificationInNotificationBox(notificationBox, notificationValue) {
   2478    return new Promise(resolve => {
   2479      let check = event => {
   2480        return event.target.getAttribute("value") == notificationValue;
   2481      };
   2482 
   2483      BrowserTestUtils.waitForEvent(
   2484        notificationBox.stack,
   2485        "AlertActive",
   2486        false,
   2487        check
   2488      ).then(event => {
   2489        // The originalTarget of the AlertActive on a notificationbox
   2490        // will be the notification itself.
   2491        resolve(event.originalTarget);
   2492      });
   2493    });
   2494  },
   2495 
   2496  /**
   2497   * Waits for CSS transitions to complete for an element. Tracks any
   2498   * transitions that start after this function is called and resolves once all
   2499   * started transitions complete.
   2500   *
   2501   * @param {Element} element
   2502   *        The element that will transition.
   2503   * @param {number} timeout
   2504   *        The maximum time to wait in milliseconds. Defaults to 5 seconds.
   2505   * @return {Promise}
   2506   *        Resolves when transitions complete or rejects if the timeout is hit.
   2507   */
   2508  waitForTransition(element, timeout = 5000) {
   2509    return new Promise((resolve, reject) => {
   2510      let cleanup = () => {
   2511        element.removeEventListener("transitionrun", listener);
   2512        element.removeEventListener("transitionend", listener);
   2513      };
   2514 
   2515      let timer = element.ownerGlobal.setTimeout(() => {
   2516        cleanup();
   2517        reject();
   2518      }, timeout);
   2519 
   2520      let transitionCount = 0;
   2521 
   2522      let listener = event => {
   2523        if (event.type == "transitionrun") {
   2524          transitionCount++;
   2525        } else {
   2526          transitionCount--;
   2527          if (transitionCount == 0) {
   2528            cleanup();
   2529            element.ownerGlobal.clearTimeout(timer);
   2530            resolve();
   2531          }
   2532        }
   2533      };
   2534 
   2535      element.addEventListener("transitionrun", listener);
   2536      element.addEventListener("transitionend", listener);
   2537      element.addEventListener("transitioncancel", listener);
   2538    });
   2539  },
   2540 
   2541  _knownAboutPages: new Set(),
   2542  _loadedAboutContentScript: false,
   2543 
   2544  /**
   2545   * Registers an about: page with particular flags in both the parent
   2546   * and any content processes. Returns a promise that resolves when
   2547   * registration is complete.
   2548   *
   2549   * @param {Function} registerCleanupFunction
   2550   *        The test framework doesn't keep its cleanup stuff anywhere accessible,
   2551   *        so the first argument is a reference to your cleanup registration
   2552   *        function, allowing us to clean up after you if necessary.
   2553   * @param {string} aboutModule
   2554   *        The name of the about page.
   2555   * @param {string} pageURI
   2556   *        The URI the about: page should point to.
   2557   * @param {number} flags
   2558   *        The nsIAboutModule flags to use for registration.
   2559   *
   2560   * @returns {Promise}
   2561   *        Promise that resolves when registration has finished.
   2562   */
   2563  registerAboutPage(registerCleanupFunction, aboutModule, pageURI, flags) {
   2564    // Return a promise that resolves when registration finished.
   2565    const kRegistrationMsgId =
   2566      "browser-test-utils:about-registration:registered";
   2567    let rv = this.waitForMessage(Services.ppmm, kRegistrationMsgId, msg => {
   2568      return msg.data == aboutModule;
   2569    });
   2570    // Load a script that registers our page, then send it a message to execute the registration.
   2571    if (!this._loadedAboutContentScript) {
   2572      Services.ppmm.loadProcessScript(
   2573        kAboutPageRegistrationContentScript,
   2574        true
   2575      );
   2576      this._loadedAboutContentScript = true;
   2577      registerCleanupFunction(this._removeAboutPageRegistrations.bind(this));
   2578    }
   2579    Services.ppmm.broadcastAsyncMessage(
   2580      "browser-test-utils:about-registration:register",
   2581      { aboutModule, pageURI, flags }
   2582    );
   2583    return rv.then(() => {
   2584      this._knownAboutPages.add(aboutModule);
   2585    });
   2586  },
   2587 
   2588  unregisterAboutPage(aboutModule) {
   2589    if (!this._knownAboutPages.has(aboutModule)) {
   2590      return Promise.reject(
   2591        new Error("We don't think this about page exists!")
   2592      );
   2593    }
   2594    const kUnregistrationMsgId =
   2595      "browser-test-utils:about-registration:unregistered";
   2596    let rv = this.waitForMessage(Services.ppmm, kUnregistrationMsgId, msg => {
   2597      return msg.data == aboutModule;
   2598    });
   2599    Services.ppmm.broadcastAsyncMessage(
   2600      "browser-test-utils:about-registration:unregister",
   2601      aboutModule
   2602    );
   2603    return rv.then(() => this._knownAboutPages.delete(aboutModule));
   2604  },
   2605 
   2606  async _removeAboutPageRegistrations() {
   2607    for (let aboutModule of this._knownAboutPages) {
   2608      await this.unregisterAboutPage(aboutModule);
   2609    }
   2610    Services.ppmm.removeDelayedProcessScript(
   2611      kAboutPageRegistrationContentScript
   2612    );
   2613  },
   2614 
   2615  /**
   2616   * Waits for the dialog to open, and clicks the specified button.
   2617   *
   2618   * @param {string} buttonNameOrElementID
   2619   *        The name of the button ("accept", "cancel", etc) or element ID to
   2620   *        click.
   2621   * @param {string} uri
   2622   *        The URI of the dialog to wait for.  Defaults to the common dialog.
   2623   * @return {Promise}
   2624   *         A Promise which resolves when a "domwindowopened" notification
   2625   *         for a dialog has been fired by the window watcher and the
   2626   *         specified button is clicked.
   2627   */
   2628  async promiseAlertDialogOpen(
   2629    buttonNameOrElementID,
   2630    uri = "chrome://global/content/commonDialog.xhtml",
   2631    options = { callback: null, isSubDialog: false }
   2632  ) {
   2633    let win;
   2634    if (uri == "chrome://global/content/commonDialog.xhtml") {
   2635      [win] = await TestUtils.topicObserved("common-dialog-loaded");
   2636    } else if (options.isSubDialog) {
   2637      for (let attempts = 0; attempts < 3; attempts++) {
   2638        [win] = await TestUtils.topicObserved("subdialog-loaded");
   2639        if (uri === undefined || uri === null || uri === "") {
   2640          break;
   2641        }
   2642        if (win.document.documentURI === uri) {
   2643          break;
   2644        }
   2645      }
   2646    } else {
   2647      // The test listens for the "load" event which guarantees that the alert
   2648      // class has already been added (it is added when "DOMContentLoaded" is
   2649      // fired).
   2650      win = await this.domWindowOpenedAndLoaded(null, win => {
   2651        return win.document.documentURI === uri;
   2652      });
   2653    }
   2654 
   2655    if (options.callback) {
   2656      await options.callback(win);
   2657      return win;
   2658    }
   2659 
   2660    if (buttonNameOrElementID) {
   2661      let dialog = win.document.querySelector("dialog");
   2662      let element =
   2663        dialog.getButton(buttonNameOrElementID) ||
   2664        win.document.getElementById(buttonNameOrElementID);
   2665      element.click();
   2666    }
   2667 
   2668    return win;
   2669  },
   2670 
   2671  /**
   2672   * Wait for the containing dialog with the id `window-modal-dialog` to become
   2673   * empty and close.
   2674   *
   2675   * @param  {HTMLDialogElement} dialog
   2676   *           The dialog to wait on.
   2677   * @return {Promise}
   2678   *           Resolves once the the dialog has closed
   2679   */
   2680  async waitForDialogClose(dialog) {
   2681    return this.waitForEvent(dialog, "close").then(() => {
   2682      return this.waitForMutationCondition(
   2683        dialog,
   2684        { childList: true, attributes: true },
   2685        () => !dialog.hasChildNodes() && !dialog.open
   2686      );
   2687    });
   2688  },
   2689 
   2690  /**
   2691   * Waits for the dialog to open, and clicks the specified button, and waits
   2692   * for the dialog to close.
   2693   *
   2694   * @param {string} buttonNameOrElementID
   2695   *        The name of the button ("accept", "cancel", etc) or element ID to
   2696   *        click.
   2697   * @param {string} uri
   2698   *        The URI of the dialog to wait for.  Defaults to the common dialog.
   2699   *
   2700   * @return {Promise}
   2701   *         A Promise which resolves when a "domwindowopened" notification
   2702   *         for a dialog has been fired by the window watcher and the
   2703   *         specified button is clicked, and the dialog has been fully closed.
   2704   */
   2705  async promiseAlertDialog(
   2706    buttonNameOrElementID,
   2707    uri = "chrome://global/content/commonDialog.xhtml",
   2708    options = { callback: null, isSubDialog: false }
   2709  ) {
   2710    let win = await this.promiseAlertDialogOpen(
   2711      buttonNameOrElementID,
   2712      uri,
   2713      options
   2714    );
   2715    if (!win.docShell.browsingContext.embedderElement) {
   2716      return this.windowClosed(win);
   2717    }
   2718    const dialog = win.top.document.getElementById("window-modal-dialog");
   2719    return this.waitForDialogClose(dialog);
   2720  },
   2721 
   2722  /**
   2723   * Opens a tab with a given uri and params object. If the params object is not set
   2724   * or the params parameter does not include a triggeringPrincipal then this function
   2725   * provides a params object using the systemPrincipal as the default triggeringPrincipal.
   2726   *
   2727   * @param {xul:tabbrowser} tabbrowser
   2728   *        The gBrowser object to open the tab with.
   2729   * @param {string} uri
   2730   *        The URI to open in the new tab.
   2731   * @param {object} params [optional]
   2732   *        Parameters object for gBrowser.addTab.
   2733   * @param {function} beforeLoadFunc [optional]
   2734   *        A function to run after that xul:browser has been created but before the URL is
   2735   *        loaded. Can spawn a content task in the tab, for example.
   2736   */
   2737  addTab(tabbrowser, uri, params = {}, beforeLoadFunc = null) {
   2738    if (!params.triggeringPrincipal) {
   2739      params.triggeringPrincipal =
   2740        Services.scriptSecurityManager.getSystemPrincipal();
   2741    }
   2742    if (beforeLoadFunc) {
   2743      let window = tabbrowser.ownerGlobal;
   2744      window.addEventListener(
   2745        "TabOpen",
   2746        function (e) {
   2747          beforeLoadFunc(e.target);
   2748        },
   2749        { once: true }
   2750      );
   2751    }
   2752    return tabbrowser.addTab(uri, params);
   2753  },
   2754 
   2755  /**
   2756   * There are two ways to listen for observers in a content process:
   2757   *   1. Call contentTopicObserved which will watch for an observer notification
   2758   *      in a content process to occur, and will return a promise which resolves
   2759   *      when that notification occurs.
   2760   *   2. Enclose calls to contentTopicObserved inside a pair of calls to
   2761   *      startObservingTopics and stopObservingTopics. Usually this pair will be
   2762   *      placed at the start and end of a test or set of tests. Any observer
   2763   *      notification that happens between the start and stop that doesn't match
   2764   *      any explicitly expected by using contentTopicObserved will cause
   2765   *      stopObservingTopics to reject with an error.
   2766   *      For example:
   2767   *
   2768   *        await BrowserTestUtils.startObservingTopics(bc, ["a", "b", "c"]);
   2769   *        await BrowserTestUtils contentTopicObserved(bc, "a", 2);
   2770   *        await BrowserTestUtils.stopObservingTopics(bc, ["a", "b", "c"]);
   2771   *
   2772   *      This will expect two "a" notifications to occur, but will fail if more
   2773   *      than two occur, or if any "b" or "c" notifications occur.
   2774   *
   2775   * Note that this function doesn't handle adding a listener for the same topic
   2776   * more than once. To do that, use the aCount argument.
   2777   *
   2778   * @param aBrowsingContext
   2779   *        The browsing context associated with the content process to listen to.
   2780   * @param {string} aTopic
   2781   *        Observer topic to listen to. May be null to listen to any topic.
   2782   * @param {number} aCount
   2783   *        Number of such matching topics to listen to, defaults to 1. A match
   2784   *        occurs when the topic and filter function match.
   2785   * @param {function} aFilterFn
   2786   *        Function to be evaluated in the content process which should
   2787   *        return true if the notification matches. This function is passed
   2788   *        the same arguments as nsIObserver.observe(). May be null to
   2789   *        always match.
   2790   * @returns {Promise} resolves when the notification occurs.
   2791   */
   2792  contentTopicObserved(aBrowsingContext, aTopic, aCount = 1, aFilterFn = null) {
   2793    return this.sendQuery(aBrowsingContext, "BrowserTestUtils:ObserveTopic", {
   2794      topic: aTopic,
   2795      count: aCount,
   2796      filterFunctionSource: aFilterFn ? aFilterFn.toSource() : null,
   2797    });
   2798  },
   2799 
   2800  /**
   2801   * Starts observing a list of topics in a content process. Use contentTopicObserved
   2802   * to allow an observer notification. Any other observer notification that occurs that
   2803   * matches one of the specified topics will cause the promise to reject.
   2804   *
   2805   * Calling this function more than once adds additional topics to be observed without
   2806   * replacing the existing ones.
   2807   *
   2808   * @param {BrowsingContext} aBrowsingContext
   2809   *        The browsing context associated with the content process to listen to.
   2810   * @param {string[]} aTopics array of observer topics
   2811   * @returns {Promise} resolves when the listeners have been added.
   2812   */
   2813  startObservingTopics(aBrowsingContext, aTopics) {
   2814    return this.sendQuery(
   2815      aBrowsingContext,
   2816      "BrowserTestUtils:StartObservingTopics",
   2817      {
   2818        topics: aTopics,
   2819      }
   2820    );
   2821  },
   2822 
   2823  /**
   2824   * Stop listening to a set of observer topics.
   2825   *
   2826   * @param {BrowsingContext} aBrowsingContext
   2827   *        The browsing context associated with the content process to listen to.
   2828   * @param {string[]} aTopics array of observer topics. If empty, then all
   2829   *                           current topics being listened to are removed.
   2830   * @returns {Promise} promise that fails if an unexpected observer occurs.
   2831   */
   2832  stopObservingTopics(aBrowsingContext, aTopics) {
   2833    return this.sendQuery(
   2834      aBrowsingContext,
   2835      "BrowserTestUtils:StopObservingTopics",
   2836      {
   2837        topics: aTopics,
   2838      }
   2839    );
   2840  },
   2841 
   2842  /**
   2843   * Sends a message to a specific BrowserTestUtils window actor.
   2844   *
   2845   * @param {BrowsingContext} aBrowsingContext
   2846   *        The browsing context where the actor lives.
   2847   * @param {string} aMessageName
   2848   *        Name of the message to be sent to the actor.
   2849   * @param {object} aMessageData
   2850   *        Extra information to pass to the actor.
   2851   */
   2852  async sendAsyncMessage(aBrowsingContext, aMessageName, aMessageData) {
   2853    if (!aBrowsingContext.currentWindowGlobal) {
   2854      await this.waitForCondition(() => aBrowsingContext.currentWindowGlobal);
   2855    }
   2856 
   2857    let actor =
   2858      aBrowsingContext.currentWindowGlobal.getActor("BrowserTestUtils");
   2859    actor.sendAsyncMessage(aMessageName, aMessageData);
   2860  },
   2861 
   2862  /**
   2863   * Sends a query to a specific BrowserTestUtils window actor.
   2864   *
   2865   * @param {BrowsingContext} aBrowsingContext
   2866   *        The browsing context where the actor lives.
   2867   * @param {string} aMessageName
   2868   *        Name of the message to be sent to the actor.
   2869   * @param {object} aMessageData
   2870   *        Extra information to pass to the actor.
   2871   */
   2872  async sendQuery(aBrowsingContext, aMessageName, aMessageData) {
   2873    let startTime = ChromeUtils.now();
   2874    if (!aBrowsingContext.currentWindowGlobal) {
   2875      await this.waitForCondition(() => aBrowsingContext.currentWindowGlobal);
   2876    }
   2877 
   2878    let actor =
   2879      aBrowsingContext.currentWindowGlobal.getActor("BrowserTestUtils");
   2880    return actor.sendQuery(aMessageName, aMessageData).then(val => {
   2881      ChromeUtils.addProfilerMarker(
   2882        "BrowserTestUtils",
   2883        { startTime, category: "Test" },
   2884        aMessageName
   2885      );
   2886      return val;
   2887    });
   2888  },
   2889 
   2890  /**
   2891   * A helper function for this test that returns a Promise that resolves
   2892   * once the migration wizard appears.
   2893   *
   2894   * @param {DOMWindow} window
   2895   *   The top-level window that the about:preferences tab is likely to open
   2896   *   in if the new migration wizard is enabled.
   2897   * @returns {Promise<Element>}
   2898   *   Resolves to the opened about:preferences tab with the migration wizard
   2899   *   running and loaded in it.
   2900   */
   2901  async waitForMigrationWizard(window) {
   2902    let wizardReady = this.waitForEvent(window, "MigrationWizard:Ready");
   2903    let wizardTab = await this.waitForNewTab(window.gBrowser, url => {
   2904      return url.startsWith("about:preferences");
   2905    });
   2906    await wizardReady;
   2907 
   2908    return wizardTab;
   2909  },
   2910 
   2911  /**
   2912   * When calling this function, the window will be hidden from various APIs,
   2913   * so that they won't be able to find it.
   2914   *
   2915   * This makes it possible to hide the main window to test some behaviors when
   2916   * it doesn't exist, e.g. when only private or non-browser windows exist.
   2917   *
   2918   * @param {ChromeWindow} window The window to be concealed.
   2919   * @param {object} options
   2920   * @param {AbortSignal} options.signal
   2921   *        Unconceals the window when the signal aborts.
   2922   */
   2923  concealWindow(window, { signal }) {
   2924    let oldWinType = window.document.documentElement.getAttribute("windowtype");
   2925    // Check if we've already done this to allow calling multiple times:
   2926    if (oldWinType != "navigator:testrunner") {
   2927      // Make the main test window not count as a browser window any longer
   2928      window.document.documentElement.setAttribute(
   2929        "windowtype",
   2930        "navigator:testrunner"
   2931      );
   2932      lazy.BrowserWindowTracker.untrackForTestsOnly(window);
   2933 
   2934      signal.addEventListener("abort", () => {
   2935        lazy.BrowserWindowTracker.track(window);
   2936        window.document.documentElement.setAttribute("windowtype", oldWinType);
   2937      });
   2938    }
   2939  },
   2940 };
   2941 
   2942 XPCOMUtils.defineLazyPreferenceGetter(
   2943  BrowserTestUtils,
   2944  "_httpsFirstEnabled",
   2945  "dom.security.https_first",
   2946  false
   2947 );
   2948 
   2949 Services.obs.addObserver(BrowserTestUtils, "test-complete");