tor-browser

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

browser-test.js (67183B)


      1 /* -*- js-indent-level: 2; tab-width: 2; indent-tabs-mode: nil -*- */
      2 
      3 /* import-globals-from chrome-harness.js */
      4 /* import-globals-from mochitest-e10s-utils.js */
      5 
      6 // Test timeout (seconds)
      7 var gTimeoutSeconds = Services.prefs.getIntPref(
      8  "testing.browserTestHarness.timeout"
      9 );
     10 var gConfig;
     11 
     12 var { AppConstants } = ChromeUtils.importESModule(
     13  "resource://gre/modules/AppConstants.sys.mjs"
     14 );
     15 
     16 ChromeUtils.defineESModuleGetters(this, {
     17  AddonManager: "resource://gre/modules/AddonManager.sys.mjs",
     18 });
     19 
     20 // saveScopeVariablesAsJSON is an experimental feature which will attempt to
     21 // save the values of all variables when a test fails (ie an assert fails).
     22 // It relies on the DevTools Debugger API to inspect variables and for the
     23 // moment will only be enabled if either:
     24 // - the preference devtools.testing.testScopes is set to true
     25 // - the environment variable MOZ_DEVTOOLS_TEST_SCOPES is set to 1
     26 //
     27 // For instance for a try push, you can enable it with
     28 // `./mach try fuzzy --env MOZ_DEVTOOLS_TEST_SCOPES=1`
     29 ChromeUtils.defineLazyGetter(this, "saveScopeVariablesAsJSON", () => {
     30  const isDevToolsTestScopesEnabled =
     31    Services.prefs.getBoolPref("devtools.testing.testScopes", false) ||
     32    Services.env.get("MOZ_DEVTOOLS_TEST_SCOPES") === "1";
     33  return () => {
     34    if (isDevToolsTestScopesEnabled) {
     35      ChromeUtils.importESModule(
     36        "resource://devtools/shared/test-helpers/dump-scope.sys.mjs",
     37        { global: "devtools" }
     38      ).dumpScope();
     39    }
     40  };
     41 });
     42 
     43 const SIMPLETEST_OVERRIDES = [
     44  "ok",
     45  "record",
     46  "is",
     47  "isnot",
     48  "todo",
     49  "todo_is",
     50  "todo_isnot",
     51  "info",
     52  "expectAssertions",
     53  "requestCompleteLog",
     54 ];
     55 
     56 setTimeout(testInit, 0);
     57 
     58 var TabDestroyObserver = {
     59  outstanding: new Set(),
     60  promiseResolver: null,
     61 
     62  init() {
     63    Services.obs.addObserver(this, "message-manager-close");
     64    Services.obs.addObserver(this, "message-manager-disconnect");
     65  },
     66 
     67  destroy() {
     68    Services.obs.removeObserver(this, "message-manager-close");
     69    Services.obs.removeObserver(this, "message-manager-disconnect");
     70  },
     71 
     72  observe(subject, topic) {
     73    if (topic == "message-manager-close") {
     74      this.outstanding.add(subject);
     75    } else if (topic == "message-manager-disconnect") {
     76      this.outstanding.delete(subject);
     77      if (!this.outstanding.size && this.promiseResolver) {
     78        this.promiseResolver();
     79      }
     80    }
     81  },
     82 
     83  wait() {
     84    if (!this.outstanding.size) {
     85      return Promise.resolve();
     86    }
     87 
     88    return new Promise(resolve => {
     89      this.promiseResolver = resolve;
     90    });
     91  },
     92 };
     93 
     94 function testInit() {
     95  gConfig = readConfig();
     96 
     97  if (gConfig.testRoot == "browser") {
     98    // Make sure to launch the test harness for the first opened window only
     99    if (Services.prefs.prefHasUserValue("testing.browserTestHarness.running")) {
    100      return;
    101    }
    102    Services.prefs.setBoolPref("testing.browserTestHarness.running", true);
    103 
    104    var sstring = Cc["@mozilla.org/supports-string;1"].createInstance(
    105      Ci.nsISupportsString
    106    );
    107    sstring.data = location.search;
    108 
    109    Services.ww.openWindow(
    110      window,
    111      "chrome://mochikit/content/browser-harness.xhtml",
    112      "browserTest",
    113      "chrome,centerscreen,dialog=no,resizable,titlebar,toolbar=no,width=800,height=600",
    114      sstring
    115    );
    116  } else {
    117    // This code allows us to redirect without requiring specialpowers for chrome and a11y tests.
    118    let messageHandler = function (m) {
    119      // eslint-disable-next-line no-undef
    120      messageManager.removeMessageListener("chromeEvent", messageHandler);
    121      var url = m.json.data;
    122 
    123      // Window is the [ChromeWindow] for messageManager, so we need content.window
    124      // Currently chrome tests are run in a content window instead of a ChromeWindow
    125      // eslint-disable-next-line no-undef
    126      var webNav = content.window.docShell.QueryInterface(Ci.nsIWebNavigation);
    127      let loadURIOptions = {
    128        triggeringPrincipal:
    129          Services.scriptSecurityManager.getSystemPrincipal(),
    130      };
    131      webNav.fixupAndLoadURIString(url, loadURIOptions);
    132    };
    133 
    134    var listener =
    135      'data:,function doLoad(e) { var data=e.detail&&e.detail.data;removeEventListener("contentEvent", function (e) { doLoad(e); }, false, true);sendAsyncMessage("chromeEvent", {"data":data}); };addEventListener("contentEvent", function (e) { doLoad(e); }, false, true);';
    136    // eslint-disable-next-line no-undef
    137    messageManager.addMessageListener("chromeEvent", messageHandler);
    138    // eslint-disable-next-line no-undef
    139    messageManager.loadFrameScript(listener, true);
    140  }
    141  if (gConfig.e10s) {
    142    e10s_init();
    143 
    144    let processCount = Services.prefs.getIntPref("dom.ipc.processCount", 1);
    145    if (processCount > 1) {
    146      // Currently starting a content process is slow, to avoid timeouts, let's
    147      // keep alive content processes.
    148      Services.prefs.setIntPref("dom.ipc.keepProcessesAlive.web", processCount);
    149    }
    150 
    151    Services.mm.loadFrameScript(
    152      "chrome://mochikit/content/shutdown-leaks-collector.js",
    153      true
    154    );
    155  } else {
    156    // In non-e10s, only run the ShutdownLeaksCollector in the parent process.
    157    ChromeUtils.importESModule(
    158      "chrome://mochikit/content/ShutdownLeaksCollector.sys.mjs"
    159    );
    160  }
    161 }
    162 
    163 function isGenerator(value) {
    164  return value && typeof value === "object" && typeof value.next === "function";
    165 }
    166 
    167 function Tester(aTests, structuredLogger, aCallback) {
    168  this.structuredLogger = structuredLogger;
    169  this.tests = aTests;
    170  this.callback = aCallback;
    171 
    172  this._scriptLoader = Services.scriptloader;
    173  this.EventUtils = {};
    174  this._scriptLoader.loadSubScript(
    175    "chrome://mochikit/content/tests/SimpleTest/EventUtils.js",
    176    this.EventUtils
    177  );
    178 
    179  // Make sure our SpecialPowers actor is instantiated, in case it was
    180  // registered after our DOMWindowCreated event was fired (which it
    181  // most likely was).
    182  void window.windowGlobalChild.getActor("SpecialPowers");
    183 
    184  var simpleTestScope = {};
    185  this._scriptLoader.loadSubScript(
    186    "chrome://mochikit/content/tests/SimpleTest/SimpleTest.js",
    187    simpleTestScope
    188  );
    189  this._scriptLoader.loadSubScript(
    190    "chrome://mochikit/content/tests/SimpleTest/MemoryStats.js",
    191    simpleTestScope
    192  );
    193  this._scriptLoader.loadSubScript(
    194    "chrome://mochikit/content/chrome-harness.js",
    195    simpleTestScope
    196  );
    197  this.SimpleTest = simpleTestScope.SimpleTest;
    198 
    199  window.SpecialPowers.SimpleTest = this.SimpleTest;
    200  window.SpecialPowers.setAsDefaultAssertHandler();
    201 
    202  this._scriptLoader.loadSubScript(
    203    "chrome://mochikit/content/tests/SimpleTest/AccessibilityUtils.js",
    204    // AccessibilityUtils are integrated with EventUtils to perform additional
    205    // accessibility checks for certain user interactions (clicks, etc). Load
    206    // them into the EventUtils scope here.
    207    this.EventUtils
    208  );
    209  this.AccessibilityUtils = this.EventUtils.AccessibilityUtils;
    210 
    211  this.AccessibilityUtils.init(this.SimpleTest);
    212 
    213  var extensionUtilsScope = {
    214    registerCleanupFunction: fn => {
    215      this.currentTest.scope.registerCleanupFunction(fn);
    216    },
    217  };
    218  extensionUtilsScope.SimpleTest = this.SimpleTest;
    219  this._scriptLoader.loadSubScript(
    220    "chrome://mochikit/content/tests/SimpleTest/ExtensionTestUtils.js",
    221    extensionUtilsScope
    222  );
    223  this.ExtensionTestUtils = extensionUtilsScope.ExtensionTestUtils;
    224 
    225  this.SimpleTest.harnessParameters = gConfig;
    226 
    227  this.MemoryStats = simpleTestScope.MemoryStats;
    228  this.ContentTask = ChromeUtils.importESModule(
    229    "resource://testing-common/ContentTask.sys.mjs"
    230  ).ContentTask;
    231  this.BrowserTestUtils = ChromeUtils.importESModule(
    232    "resource://testing-common/BrowserTestUtils.sys.mjs"
    233  ).BrowserTestUtils;
    234  this.TestUtils = ChromeUtils.importESModule(
    235    "resource://testing-common/TestUtils.sys.mjs"
    236  ).TestUtils;
    237  this.PromiseTestUtils = ChromeUtils.importESModule(
    238    "resource://testing-common/PromiseTestUtils.sys.mjs"
    239  ).PromiseTestUtils;
    240  this.Assert = ChromeUtils.importESModule(
    241    "resource://testing-common/Assert.sys.mjs"
    242  ).Assert;
    243  this.PerTestCoverageUtils = ChromeUtils.importESModule(
    244    "resource://testing-common/PerTestCoverageUtils.sys.mjs"
    245  ).PerTestCoverageUtils;
    246 
    247  this.PromiseTestUtils.init();
    248 
    249  this.SimpleTestOriginal = {};
    250  SIMPLETEST_OVERRIDES.forEach(m => {
    251    this.SimpleTestOriginal[m] = this.SimpleTest[m];
    252  });
    253 
    254  this._coverageCollector = null;
    255 
    256  const { XPCOMUtils } = ChromeUtils.importESModule(
    257    "resource://gre/modules/XPCOMUtils.sys.mjs"
    258  );
    259 
    260  // Avoid failing tests when XPCOMUtils.defineLazyScriptGetter is used.
    261  XPCOMUtils.overrideScriptLoaderForTests({
    262    loadSubScript: (url, obj) => {
    263      let before = Object.keys(window);
    264      try {
    265        return this._scriptLoader.loadSubScript(url, obj);
    266      } finally {
    267        for (let property of Object.keys(window)) {
    268          if (
    269            !before.includes(property) &&
    270            !this._globalProperties.includes(property)
    271          ) {
    272            this._globalProperties.push(property);
    273            this.SimpleTest.info(
    274              `Global property added while loading ${url}: ${property}`
    275            );
    276          }
    277        }
    278      }
    279    },
    280    loadSubScriptWithOptions: this._scriptLoader.loadSubScriptWithOptions.bind(
    281      this._scriptLoader
    282    ),
    283  });
    284 
    285  // ensure the mouse is reset before each test run
    286  if (Services.env.exists("MOZ_AUTOMATION")) {
    287    this.EventUtils.synthesizeNativeMouseEvent({
    288      type: "mousemove",
    289      screenX: 1000,
    290      screenY: 10,
    291    });
    292  }
    293 }
    294 Tester.prototype = {
    295  EventUtils: {},
    296  AccessibilityUtils: {},
    297  SimpleTest: {},
    298  ContentTask: null,
    299  ExtensionTestUtils: null,
    300  Assert: null,
    301 
    302  repeat: 0,
    303  a11y_checks: false,
    304  runUntilFailure: false,
    305  checker: null,
    306  currentTestIndex: -1,
    307  lastStartTime: null,
    308  lastStartTimestamp: null,
    309  lastAssertionCount: 0,
    310  failuresFromInitialWindowState: 0,
    311 
    312  get currentTest() {
    313    return this.tests[this.currentTestIndex];
    314  },
    315  get done() {
    316    return this.currentTestIndex == this.tests.length - 1 && this.repeat <= 0;
    317  },
    318 
    319  start: function Tester_start() {
    320    TabDestroyObserver.init();
    321 
    322    // if testOnLoad was not called, then gConfig is not defined
    323    if (!gConfig) {
    324      gConfig = readConfig();
    325    }
    326 
    327    if (gConfig.runUntilFailure) {
    328      this.runUntilFailure = true;
    329    }
    330 
    331    if (gConfig.a11y_checks != undefined) {
    332      this.a11y_checks = gConfig.a11y_checks;
    333    }
    334 
    335    if (gConfig.repeat) {
    336      this.repeat = gConfig.repeat;
    337    }
    338 
    339    if (gConfig.jscovDirPrefix) {
    340      let coveragePath = gConfig.jscovDirPrefix;
    341      let { CoverageCollector } = ChromeUtils.importESModule(
    342        "resource://testing-common/CoverageUtils.sys.mjs"
    343      );
    344      this._coverageCollector = new CoverageCollector(coveragePath);
    345    }
    346 
    347    if (gConfig.debugger || gConfig.debuggerInteractive || gConfig.jsdebugger) {
    348      gTimeoutSeconds = 24 * 60 * 60 * 1000; // 24 hours
    349    }
    350 
    351    this.structuredLogger.info("*** Start BrowserChrome Test Results ***");
    352    Services.console.registerListener(this);
    353    this._globalProperties = Object.keys(window);
    354    this._globalPropertyWhitelist = [
    355      "navigator",
    356      "constructor",
    357      "top",
    358      "Application",
    359      "__SS_tabsToRestore",
    360      "__SSi",
    361      "webConsoleCommandController",
    362      // Thunderbird
    363      "MailMigrator",
    364      "SearchIntegration",
    365      // lit
    366      "reactiveElementVersions",
    367      "litHtmlVersions",
    368      "litElementVersions",
    369    ];
    370 
    371    this._repeatingTimers = this._getRepeatingTimers();
    372 
    373    this.PerTestCoverageUtils.beforeTestSync();
    374 
    375    if (this.tests.length) {
    376      this.waitForWindowsReady().then(() => {
    377        this.nextTest();
    378      });
    379    } else {
    380      this.finish();
    381    }
    382  },
    383 
    384  _getRepeatingTimers() {
    385    const kNonRepeatingTimerTypes = [
    386      Ci.nsITimer.TYPE_ONE_SHOT,
    387      Ci.nsITimer.TYPE_ONE_SHOT_LOW_PRIORITY,
    388    ];
    389    return Cc["@mozilla.org/timer-manager;1"]
    390      .getService(Ci.nsITimerManager)
    391      .getTimers()
    392      .filter(t => !kNonRepeatingTimerTypes.includes(t.type));
    393  },
    394 
    395  async waitForWindowsReady() {
    396    await this.setupDefaultTheme();
    397    await new Promise(resolve =>
    398      this.waitForGraphicsTestWindowToBeGone(resolve)
    399    );
    400    await this.promiseMainWindowReady();
    401  },
    402 
    403  async promiseMainWindowReady() {
    404    if (window.gBrowserInit) {
    405      await window.gBrowserInit.idleTasksFinished.promise;
    406    }
    407  },
    408 
    409  async setupDefaultTheme() {
    410    // Developer Edition enables the wrong theme by default. Make sure
    411    // the ordinary default theme is enabled.
    412    let theme = await AddonManager.getAddonByID("default-theme@mozilla.org");
    413    await theme.enable();
    414  },
    415 
    416  waitForGraphicsTestWindowToBeGone(aCallback) {
    417    for (let win of Services.wm.getEnumerator(null)) {
    418      if (
    419        win != window &&
    420        !win.closed &&
    421        win.document.documentURI ==
    422          "chrome://gfxsanity/content/sanityparent.html"
    423      ) {
    424        this.BrowserTestUtils.domWindowClosed(win).then(aCallback);
    425        return;
    426      }
    427    }
    428    // graphics test window is already gone, just call callback immediately
    429    aCallback();
    430  },
    431 
    432  checkWindowsState: function Tester_checkWindowsState() {
    433    let timedOut = this.currentTest && this.currentTest.timedOut;
    434    // eslint-disable-next-line no-nested-ternary
    435    let baseMsg = timedOut
    436      ? "Found a {elt} after previous test timed out"
    437      : this.currentTest
    438        ? "Found an unexpected {elt} at the end of test run"
    439        : "Found an unexpected {elt}";
    440 
    441    // Clean up the Firefox window.
    442    // But not the Thunderbird window, it doesn't have these things!
    443    if (AppConstants.MOZ_APP_NAME != "thunderbird") {
    444      // Remove stale tabs
    445      if (this.currentTest && window.gBrowser && gBrowser.tabs.length > 1) {
    446        let lastURI = "";
    447        let lastURIcount = 0;
    448        while (gBrowser.tabs.length > 1) {
    449          let lastTab = gBrowser.tabs[gBrowser.tabs.length - 1];
    450          if (!lastTab.closing) {
    451            // Report the stale tab as an error only when they're not closing.
    452            // Tests can finish without waiting for the closing tabs.
    453            if (lastURI != lastTab.linkedBrowser.currentURI.spec) {
    454              lastURI = lastTab.linkedBrowser.currentURI.spec;
    455            } else {
    456              lastURIcount++;
    457              if (lastURIcount >= 3) {
    458                this.currentTest.addResult(
    459                  new testResult({
    460                    name: "terminating browser early - unable to close tabs; skipping remaining tests in folder",
    461                    allowFailure: this.currentTest.allowFailure,
    462                  })
    463                );
    464                this.finish();
    465              }
    466            }
    467            this.currentTest.addResult(
    468              new testResult({
    469                name:
    470                  baseMsg.replace("{elt}", "tab") +
    471                  ": " +
    472                  lastTab.linkedBrowser.currentURI.spec,
    473                allowFailure: this.currentTest.allowFailure,
    474              })
    475            );
    476          }
    477          gBrowser.removeTab(lastTab);
    478        }
    479      }
    480 
    481      // Tests shouldn't leave sidebars open
    482      if (this.currentTest) {
    483        this.currentTest.addResult(
    484          new testMessage("checking for open sidebars")
    485        );
    486      } else {
    487        this.structuredLogger.info("checking for open sidebars");
    488      }
    489      const sidebarContainer = document.getElementById("sidebar-box");
    490      if (!sidebarContainer.hidden) {
    491        window.SidebarController.hide({ dismissPanel: true });
    492        this.currentTest.addResult(
    493          new testResult({
    494            name: baseMsg.replace("{elt}", "open sidebar"),
    495            allowFailure: this.currentTest.allowFailure,
    496          })
    497        );
    498      }
    499    }
    500 
    501    // Remove stale windows
    502    if (this.currentTest) {
    503      this.currentTest.addResult(new testMessage("checking window state"));
    504    } else {
    505      this.structuredLogger.info("checking window state");
    506    }
    507    for (let win of Services.wm.getEnumerator(null)) {
    508      let type = win.document.documentElement.getAttribute("windowtype");
    509      if (
    510        win != window &&
    511        !win.closed &&
    512        win.document.documentElement.getAttribute("id") !=
    513          "browserTestHarness" &&
    514        type != "devtools:webconsole"
    515      ) {
    516        switch (type) {
    517          case "navigator:browser":
    518            type = "browser window";
    519            break;
    520          case "mail:3pane":
    521            type = "mail window";
    522            break;
    523          case null:
    524            type =
    525              "unknown window with document URI: " +
    526              win.document.documentURI +
    527              " and title: " +
    528              win.document.title;
    529            break;
    530        }
    531        let msg = baseMsg.replace("{elt}", type);
    532        if (this.currentTest) {
    533          this.currentTest.addResult(
    534            new testResult({
    535              name: msg,
    536              allowFailure: this.currentTest.allowFailure,
    537            })
    538          );
    539        } else {
    540          this.failuresFromInitialWindowState++;
    541          this.structuredLogger.error("browser-test.js | " + msg);
    542        }
    543 
    544        win.close();
    545      }
    546    }
    547  },
    548 
    549  finish: function Tester_finish() {
    550    var passCount = this.tests.reduce((a, f) => a + f.passCount, 0);
    551    var failCount = this.tests.reduce((a, f) => a + f.failCount, 0);
    552    var todoCount = this.tests.reduce((a, f) => a + f.todoCount, 0);
    553 
    554    // Include failures from window state checking prior to running the first test
    555    failCount += this.failuresFromInitialWindowState;
    556 
    557    TabDestroyObserver.destroy();
    558    Services.console.unregisterListener(this);
    559 
    560    this.AccessibilityUtils.uninit();
    561 
    562    // It's important to terminate the module to avoid crashes on shutdown.
    563    this.PromiseTestUtils.uninit();
    564 
    565    // In the main process, we print the ShutdownLeaksCollector message here.
    566    let pid = Services.appinfo.processID;
    567    dump("Completed ShutdownLeaks collections in process " + pid + "\n");
    568 
    569    this.structuredLogger.info("TEST-START | Shutdown");
    570 
    571    if (this.tests.length) {
    572      let e10sMode = window.gMultiProcessBrowser ? "e10s" : "non-e10s";
    573      this.structuredLogger.info("Browser Chrome Test Summary");
    574      this.structuredLogger.info("Passed:  " + passCount);
    575      this.structuredLogger.info("Failed:  " + failCount);
    576      this.structuredLogger.info("Todo:    " + todoCount);
    577      this.structuredLogger.info("Mode:    " + e10sMode);
    578    } else {
    579      this.structuredLogger.error(
    580        "browser-test.js | No tests to run. Did you pass invalid test_paths?"
    581      );
    582    }
    583    this.structuredLogger.info("*** End BrowserChrome Test Results ***");
    584 
    585    // Tests complete, notify the callback and return
    586    this.callback(this.tests);
    587    this.callback = null;
    588    this.tests = null;
    589  },
    590 
    591  haltTests: function Tester_haltTests() {
    592    // Do not run any further tests
    593    this.currentTestIndex = this.tests.length - 1;
    594    this.repeat = 0;
    595  },
    596 
    597  observe: function Tester_observe(aSubject, aTopic) {
    598    if (!aTopic) {
    599      this.onConsoleMessage(aSubject);
    600    }
    601  },
    602 
    603  onConsoleMessage: function Tester_onConsoleMessage(aConsoleMessage) {
    604    // Ignore empty messages.
    605    if (!aConsoleMessage.message) {
    606      return;
    607    }
    608 
    609    try {
    610      var msg = "Console message: " + aConsoleMessage.message;
    611      if (this.currentTest) {
    612        this.currentTest.addResult(new testMessage(msg));
    613      } else {
    614        this.structuredLogger.info(
    615          "TEST-INFO | (browser-test.js) | " + msg.replace(/\n$/, "") + "\n"
    616        );
    617      }
    618    } catch (ex) {
    619      // Swallow exception so we don't lead to another error being reported,
    620      // throwing us into an infinite loop
    621    }
    622  },
    623 
    624  async ensureVsyncDisabled() {
    625    // The WebExtension process keeps vsync enabled forever in headless mode.
    626    // See bug 1782541.
    627    if (Services.env.get("MOZ_HEADLESS")) {
    628      return;
    629    }
    630 
    631    try {
    632      await this.TestUtils.waitForCondition(
    633        () => !ChromeUtils.vsyncEnabled(),
    634        "waiting for vsync to be disabled"
    635      );
    636    } catch (e) {
    637      this.Assert.ok(false, e);
    638      this.Assert.ok(
    639        false,
    640        "vsync remained enabled at the end of the test. " +
    641          "Is there an animation still running? " +
    642          "Consider talking to the performance team for tips to solve this."
    643      );
    644    }
    645  },
    646 
    647  getNewRepeatingTimers() {
    648    let repeatingTimers = this._getRepeatingTimers();
    649    let results = [];
    650    for (let timer of repeatingTimers) {
    651      let { name, delay } = timer;
    652      // For now ignore long repeating timers (typically from nsExpirationTracker).
    653      if (delay >= 10000) {
    654        continue;
    655      }
    656 
    657      // Also ignore the nsAvailableMemoryWatcher timer that is started when the
    658      // user-interaction-active notification is fired, and stopped when the
    659      // user-interaction-inactive notification occurs.
    660      // On Linux it's a 5s timer, on other platforms it's 10s, which is already
    661      // ignored by the previous case.
    662      if (
    663        AppConstants.platform == "linux" &&
    664        name == "nsAvailableMemoryWatcher"
    665      ) {
    666        continue;
    667      }
    668 
    669      // Ignore ScrollFrameActivityTracker, it's a 4s timer which could begin
    670      // shortly after the end of a test and cause failure. See bug 1878627.
    671      if (name == "ScrollFrameActivityTracker") {
    672        continue;
    673      }
    674 
    675      // Same as ScrollFrameActivityTracker and the other expiration trackers
    676      // ignored above.
    677      if (name == "PopupExpirationTracker") {
    678        continue;
    679      }
    680 
    681      // Ignore nsHttpConnectionMgr timers which show up on browser mochitests
    682      // running with http3. See Bug 1829841.
    683      if (name == "nsHttpConnectionMgr") {
    684        continue;
    685      }
    686 
    687      if (
    688        !this._repeatingTimers.find(t => t.delay == delay && t.name == name)
    689      ) {
    690        results.push(timer);
    691      }
    692    }
    693    if (results.length) {
    694      ChromeUtils.addProfilerMarker(
    695        "NewRepeatingTimers",
    696        { category: "Test" },
    697        results.map(t => `${t.name}: ${t.delay}ms`).join(", ")
    698      );
    699    }
    700    return results;
    701  },
    702 
    703  async ensureNoNewRepeatingTimers() {
    704    let newTimers;
    705    try {
    706      await this.TestUtils.waitForCondition(
    707        async function () {
    708          // The array returned by nsITimerManager.getTimers doesn't include
    709          // timers that are queued in the event loop of their target thread.
    710          // By waiting for a tick, we ensure the timers that might fire about
    711          // at the same time as our waitForCondition timer will be included.
    712          await this.TestUtils.waitForTick();
    713 
    714          newTimers = this.getNewRepeatingTimers();
    715          return !newTimers.length;
    716        }.bind(this),
    717        "waiting for new repeating timers to be cancelled"
    718      );
    719    } catch (e) {
    720      this.Assert.ok(false, e);
    721      for (let { name, delay } of newTimers) {
    722        this.Assert.ok(
    723          false,
    724          `test left unexpected repeating timer ${name} (duration: ${delay}ms)`
    725        );
    726      }
    727      // Once the new repeating timers have been reported, add them to
    728      // this._repeatingTimers to avoid reporting them again for the next
    729      // tests of the manifest.
    730      this._repeatingTimers.push(...newTimers);
    731    }
    732  },
    733 
    734  async checkPreferencesAfterTest() {
    735    if (!this._ignorePrefs) {
    736      const ignorePrefsFile = `chrome://mochikit/content/${gConfig.ignorePrefsFile}`;
    737      try {
    738        const res = await fetch(ignorePrefsFile);
    739        this._ignorePrefs = await res.json();
    740      } catch (e) {
    741        this.Assert.ok(
    742          false,
    743          `Failed to load ignorePrefsFile (${ignorePrefsFile}): ${e}`
    744        );
    745      }
    746    }
    747    // This comparison relies on a snapshot of the prefs having been saved by
    748    // a call to getBaselinePrefs in SpecialPowersParent's init().
    749    const failures = await window.SpecialPowers.comparePrefsToBaseline(
    750      this._ignorePrefs
    751    );
    752 
    753    let testPath = this.currentTest.path;
    754    if (testPath.startsWith("chrome://mochitests/content/browser/")) {
    755      testPath = testPath.replace("chrome://mochitests/content/browser/", "");
    756    }
    757    let changedPrefs = [];
    758    for (let p of failures) {
    759      this.structuredLogger.error(
    760        // We only report unexpected failures when --compare-preferences is set.
    761        `TEST-${gConfig.comparePrefs ? "UN" : ""}EXPECTED-FAIL | ${testPath} | changed preference: ${p}`
    762      );
    763      changedPrefs.push(p);
    764    }
    765 
    766    if (changedPrefs.length && Services.env.exists("MOZ_UPLOAD_DIR")) {
    767      let modifiedPrefsPath = PathUtils.join(
    768        Services.env.get("MOZ_UPLOAD_DIR"),
    769        "modifiedPrefs.json"
    770      );
    771 
    772      if (!this._modifiedPrefs) {
    773        try {
    774          this._modifiedPrefs = JSON.parse(
    775            await IOUtils.readUTF8(modifiedPrefsPath)
    776          );
    777        } catch (e) {
    778          this._modifiedPrefs = {};
    779        }
    780      }
    781 
    782      this._modifiedPrefs[testPath] = changedPrefs;
    783      await IOUtils.writeJSON(modifiedPrefsPath, this._modifiedPrefs);
    784    }
    785  },
    786 
    787  /**
    788   * In the event that a test module left any traces in Session Restore state,
    789   * clean those up so that each test module starts execution with the same
    790   * fresh Session Restore state.
    791   */
    792  resetSessionState() {
    793    // Forget all closed windows.
    794    while (window.SessionStore.getClosedWindowCount() > 0) {
    795      window.SessionStore.forgetClosedWindow(0);
    796    }
    797 
    798    // Forget all closed tabs for the test window.
    799    const closedTabCount =
    800      window.SessionStore.getClosedTabCountForWindow(window);
    801    for (let i = 0; i < closedTabCount; i++) {
    802      try {
    803        window.SessionStore.forgetClosedTab(window, 0);
    804      } catch (err) {
    805        // This will fail if there are tab groups in here
    806      }
    807    }
    808 
    809    // Forget saved tab groups.
    810    const savedTabGroups = window.SessionStore.getSavedTabGroups();
    811    savedTabGroups.forEach(tabGroup =>
    812      window.SessionStore.forgetSavedTabGroup(tabGroup.id)
    813    );
    814 
    815    // Forget closed tab groups in the test window.
    816    const closedTabGroups = window.SessionStore.getClosedTabGroups(window);
    817    closedTabGroups.forEach(tabGroup =>
    818      window.SessionStore.forgetClosedTabGroup(window, tabGroup.id)
    819    );
    820  },
    821 
    822  async notifyProfilerOfTestEnd() {
    823    // Note the test run time
    824    let name = this.currentTest.path;
    825    name = name.slice(name.lastIndexOf("/") + 1);
    826    ChromeUtils.addProfilerMarker(
    827      "browser-test",
    828      { category: "Test", startTime: this.lastStartTimestamp },
    829      name
    830    );
    831 
    832    // See if we should upload a profile of a failing test.
    833    if (this.currentTest.failCount) {
    834      // If MOZ_PROFILER_SHUTDOWN is set, the profiler got started from --profiler
    835      // and a profile will be shown even if there's no test failure.
    836      if (
    837        Services.env.exists("MOZ_UPLOAD_DIR") &&
    838        !Services.env.exists("MOZ_PROFILER_SHUTDOWN") &&
    839        Services.profiler.IsActive()
    840      ) {
    841        let filename = `profile_${name}.json`;
    842        let path = Services.env.get("MOZ_UPLOAD_DIR");
    843        let profilePath = PathUtils.join(path, filename);
    844        try {
    845          const { profile } =
    846            await Services.profiler.getProfileDataAsGzippedArrayBuffer();
    847          await IOUtils.write(profilePath, new Uint8Array(profile));
    848          this.currentTest.addResult(
    849            new testResult({
    850              name:
    851                "Found unexpected failures during the test; profile uploaded in " +
    852                filename,
    853            })
    854          );
    855        } catch (e) {
    856          // If the profile is large, we may encounter out of memory errors.
    857          this.currentTest.addResult(
    858            new testResult({
    859              name:
    860                "Found unexpected failures during the test; failed to upload profile: " +
    861                e,
    862            })
    863          );
    864        }
    865      }
    866    }
    867  },
    868 
    869  async nextTest() {
    870    // On first call (no currentTest yet), check for initial window state issues
    871    if (!this.currentTest) {
    872      this.checkWindowsState();
    873    } else {
    874      if (this._coverageCollector) {
    875        this._coverageCollector.recordTestCoverage(this.currentTest.path);
    876      }
    877 
    878      this.PerTestCoverageUtils.afterTestSync();
    879 
    880      // Run cleanup functions for the current test before moving on to the
    881      // next one.
    882      let testScope = this.currentTest.scope;
    883      while (testScope.__cleanupFunctions.length) {
    884        let func = testScope.__cleanupFunctions.shift();
    885        try {
    886          let result = await func.apply(testScope);
    887          if (isGenerator(result)) {
    888            this.SimpleTest.ok(false, "Cleanup function returned a generator");
    889          }
    890        } catch (ex) {
    891          this.currentTest.addResult(
    892            new testResult({
    893              name: "Cleanup function threw an exception",
    894              ex,
    895              allowFailure: this.currentTest.allowFailure,
    896            })
    897          );
    898        }
    899      }
    900 
    901      // Spare tests cleanup work.
    902      // Reset gReduceMotionOverride in case the test set it.
    903      if (typeof gReduceMotionOverride == "boolean") {
    904        gReduceMotionOverride = null;
    905      }
    906 
    907      Services.obs.notifyObservers(null, "test-complete");
    908 
    909      // Ensure to reset the clipboard in case the test has modified it,
    910      // so it won't affect the next tests.
    911      window.SpecialPowers.clipboardCopyString("");
    912 
    913      if (
    914        this.currentTest.passCount === 0 &&
    915        this.currentTest.failCount === 0 &&
    916        this.currentTest.todoCount === 0
    917      ) {
    918        this.currentTest.addResult(
    919          new testResult({
    920            name:
    921              "This test contains no passes, no fails and no todos. Maybe" +
    922              " it threw a silent exception? Make sure you use" +
    923              " waitForExplicitFinish() if you need it.",
    924          })
    925        );
    926      }
    927 
    928      let winUtils = window.windowUtils;
    929      if (winUtils.isTestControllingRefreshes) {
    930        this.currentTest.addResult(
    931          new testResult({
    932            name: "test left refresh driver under test control",
    933          })
    934        );
    935        winUtils.restoreNormalRefresh();
    936      }
    937 
    938      if (this.SimpleTest.isExpectingUncaughtException()) {
    939        this.currentTest.addResult(
    940          new testResult({
    941            name:
    942              "expectUncaughtException was called but no uncaught" +
    943              " exception was detected!",
    944            allowFailure: this.currentTest.allowFailure,
    945          })
    946        );
    947      }
    948 
    949      this.resolveFinishTestPromise();
    950      this.resolveFinishTestPromise = null;
    951      this.TestUtils.promiseTestFinished = null;
    952 
    953      this.PromiseTestUtils.ensureDOMPromiseRejectionsProcessed();
    954      this.PromiseTestUtils.assertNoUncaughtRejections();
    955      this.PromiseTestUtils.assertNoMoreExpectedRejections();
    956      await this.ensureVsyncDisabled();
    957 
    958      Object.keys(window).forEach(function (prop) {
    959        if (parseInt(prop) == prop) {
    960          // This is a string which when parsed as an integer and then
    961          // stringified gives the original string.  As in, this is in fact a
    962          // string representation of an integer, so an index into
    963          // window.frames.  Skip those.
    964          return;
    965        }
    966        if (!this._globalProperties.includes(prop)) {
    967          this._globalProperties.push(prop);
    968          if (!this._globalPropertyWhitelist.includes(prop)) {
    969            this.currentTest.addResult(
    970              new testResult({
    971                name: "test left unexpected property on window: " + prop,
    972                allowFailure: this.currentTest.allowFailure,
    973              })
    974            );
    975          }
    976        }
    977      }, this);
    978 
    979      await this.ensureNoNewRepeatingTimers();
    980 
    981      await new Promise(resolve => window.SpecialPowers.flushPrefEnv(resolve));
    982 
    983      await this.checkPreferencesAfterTest();
    984 
    985      window.SpecialPowers.cleanupAllClipboard();
    986 
    987      if (AppConstants.MOZ_APP_NAME != "thunderbird") {
    988        this.resetSessionState();
    989      }
    990 
    991      if (gConfig.cleanupCrashes) {
    992        let gdir = Services.dirsvc.get("UAppData", Ci.nsIFile);
    993        gdir.append("Crash Reports");
    994        gdir.append("pending");
    995        if (gdir.exists()) {
    996          let entries = gdir.directoryEntries;
    997          while (entries.hasMoreElements()) {
    998            let entry = entries.nextFile;
    999            if (entry.isFile()) {
   1000              let msg = "this test left a pending crash report; ";
   1001              try {
   1002                entry.remove(false);
   1003                msg += "deleted " + entry.path;
   1004              } catch (e) {
   1005                msg += "could not delete " + entry.path;
   1006              }
   1007              this.structuredLogger.info(msg);
   1008            }
   1009          }
   1010        }
   1011      }
   1012 
   1013      // Notify a long running test problem if it didn't end up in a timeout.
   1014      if (this.currentTest.unexpectedTimeouts && !this.currentTest.timedOut) {
   1015        this.currentTest.addResult(
   1016          new testResult({
   1017            name:
   1018              "This test exceeded the timeout threshold. It should be" +
   1019              " rewritten or split up. If that's not possible, use" +
   1020              " requestLongerTimeout(N), but only as a last resort.",
   1021          })
   1022        );
   1023      }
   1024 
   1025      // If we're in a debug build, check assertion counts.  This code
   1026      // is similar to the code in TestRunner.testUnloaded in
   1027      // TestRunner.js used for all other types of mochitests.
   1028      let debugsvc = Cc["@mozilla.org/xpcom/debug;1"].getService(Ci.nsIDebug2);
   1029      if (debugsvc.isDebugBuild) {
   1030        let newAssertionCount = debugsvc.assertionCount;
   1031        let numAsserts = newAssertionCount - this.lastAssertionCount;
   1032        this.lastAssertionCount = newAssertionCount;
   1033 
   1034        let max = testScope.__expectedMaxAsserts;
   1035        let min = testScope.__expectedMinAsserts;
   1036        if (numAsserts > max) {
   1037          // TEST-UNEXPECTED-FAIL
   1038          this.currentTest.addResult(
   1039            new testResult({
   1040              name:
   1041                "Assertion count " +
   1042                numAsserts +
   1043                " is greater than expected range " +
   1044                min +
   1045                "-" +
   1046                max +
   1047                " assertions.",
   1048              pass: true, // TEMPORARILY TEST-KNOWN-FAIL
   1049              todo: true,
   1050              allowFailure: this.currentTest.allowFailure,
   1051            })
   1052          );
   1053        } else if (numAsserts < min) {
   1054          // TEST-UNEXPECTED-PASS
   1055          this.currentTest.addResult(
   1056            new testResult({
   1057              name:
   1058                "Assertion count " +
   1059                numAsserts +
   1060                " is less than expected range " +
   1061                min +
   1062                "-" +
   1063                max +
   1064                " assertions.",
   1065              todo: true,
   1066              allowFailure: this.currentTest.allowFailure,
   1067            })
   1068          );
   1069        } else if (numAsserts > 0) {
   1070          // TEST-KNOWN-FAIL
   1071          this.currentTest.addResult(
   1072            new testResult({
   1073              name:
   1074                "Assertion count " +
   1075                numAsserts +
   1076                " is within expected range " +
   1077                min +
   1078                "-" +
   1079                max +
   1080                " assertions.",
   1081              pass: true,
   1082              todo: true,
   1083              allowFailure: this.currentTest.allowFailure,
   1084            })
   1085          );
   1086        }
   1087      }
   1088 
   1089      if (
   1090        this.currentTest.allowFailure &&
   1091        this.currentTest.allowedFailureCount == 0
   1092      ) {
   1093        this.currentTest.addResult(
   1094          new testResult({
   1095            name:
   1096              "We expect at least one assertion to fail because this" +
   1097              " test file is marked as fail-if in the manifest.",
   1098            todo: true,
   1099            knownFailure: this.currentTest.allowFailure,
   1100          })
   1101        );
   1102      }
   1103 
   1104      // Dump memory stats for main thread.
   1105      if (
   1106        Services.appinfo.processType == Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT
   1107      ) {
   1108        this.MemoryStats.dump(
   1109          this.currentTestIndex,
   1110          this.currentTest.path,
   1111          gConfig.dumpOutputDirectory,
   1112          gConfig.dumpAboutMemoryAfterTest,
   1113          gConfig.dumpDMDAfterTest
   1114        );
   1115      }
   1116 
   1117      this.PromiseTestUtils.assertNoUncaughtRejections();
   1118 
   1119      await this.notifyProfilerOfTestEnd();
   1120 
   1121      // Check the window state before logging testEnd so that any cleanup
   1122      // assertions are included in the test result, not logged after test_end.
   1123      this.checkWindowsState();
   1124 
   1125      let time = Date.now() - this.lastStartTime;
   1126 
   1127      this.structuredLogger.testEnd(
   1128        this.currentTest.path,
   1129        "OK",
   1130        undefined,
   1131        "finished in " + time + "ms"
   1132      );
   1133      this.currentTest.setDuration(time);
   1134 
   1135      if (this.runUntilFailure && this.currentTest.failCount > 0) {
   1136        this.haltTests();
   1137      }
   1138 
   1139      // Restore original SimpleTest methods to avoid leaks.
   1140      SIMPLETEST_OVERRIDES.forEach(m => {
   1141        this.SimpleTest[m] = this.SimpleTestOriginal[m];
   1142      });
   1143 
   1144      this.ContentTask.setTestScope(null);
   1145      testScope.destroy();
   1146      this.currentTest.scope = null;
   1147    }
   1148 
   1149    // Replace the current tab with about:blank. For the first test, this ensures
   1150    // we start with a clean slate. For subsequent tests, this must happen AFTER
   1151    // test_end is logged, otherwise the new windows created by addTab will be
   1152    // tracked by ShutdownLeaks as belonging to the test and cause false leak reports.
   1153    if (window.gBrowser) {
   1154      gBrowser.addTab("about:blank", {
   1155        skipAnimation: true,
   1156        triggeringPrincipal:
   1157          Services.scriptSecurityManager.getSystemPrincipal(),
   1158      });
   1159      gBrowser.removeTab(gBrowser.selectedTab, { skipPermitUnload: true });
   1160      gBrowser.stop();
   1161    }
   1162 
   1163    // Make sure the window is raised before starting the next test.
   1164    this.SimpleTest.waitForFocus(() => {
   1165      if (this.done) {
   1166        if (this._coverageCollector) {
   1167          this._coverageCollector.finalize();
   1168        } else if (
   1169          !AppConstants.RELEASE_OR_BETA &&
   1170          !AppConstants.DEBUG &&
   1171          !AppConstants.MOZ_CODE_COVERAGE &&
   1172          !AppConstants.ASAN &&
   1173          !AppConstants.TSAN
   1174        ) {
   1175          this.finish();
   1176          return;
   1177        }
   1178 
   1179        // Uninitialize a few things explicitly so that they can clean up
   1180        // frames and browser intentionally kept alive until shutdown to
   1181        // eliminate false positives.
   1182        if (gConfig.testRoot == "browser") {
   1183          // Skip if SeaMonkey
   1184          if (AppConstants.MOZ_APP_NAME != "seamonkey") {
   1185            // Replace the document currently loaded in the browser's sidebar.
   1186            // This will prevent false positives for tests that were the last
   1187            // to touch the sidebar. They will thus not be blamed for leaking
   1188            // a document.
   1189            let sidebar = document.getElementById("sidebar");
   1190            if (sidebar) {
   1191              sidebar.setAttribute("src", "data:text/html;charset=utf-8,");
   1192              sidebar.docShell.createAboutBlankDocumentViewer(null, null);
   1193              sidebar.setAttribute("src", "about:blank");
   1194            }
   1195          }
   1196 
   1197          // Destroy BackgroundPageThumbs resources.
   1198          let { BackgroundPageThumbs } = ChromeUtils.importESModule(
   1199            "resource://gre/modules/BackgroundPageThumbs.sys.mjs"
   1200          );
   1201          BackgroundPageThumbs._destroy();
   1202 
   1203          if (window.gBrowser) {
   1204            NewTabPagePreloading.removePreloadedBrowser(window);
   1205          }
   1206        }
   1207 
   1208        // Schedule GC and CC runs before finishing in order to detect
   1209        // DOM windows leaked by our tests or the tested code. Note that we
   1210        // use a shrinking GC so that the JS engine will discard JIT code and
   1211        // JIT caches more aggressively.
   1212 
   1213        let shutdownCleanup = aCallback => {
   1214          Cu.schedulePreciseShrinkingGC(() => {
   1215            // Run the GC and CC a few times to make sure that as much
   1216            // as possible is freed.
   1217            let numCycles = 3;
   1218            for (let i = 0; i < numCycles; i++) {
   1219              Cu.forceGC();
   1220              Cu.forceCC();
   1221            }
   1222            aCallback();
   1223          });
   1224        };
   1225 
   1226        let { AsyncShutdown } = ChromeUtils.importESModule(
   1227          "resource://gre/modules/AsyncShutdown.sys.mjs"
   1228        );
   1229 
   1230        let barrier = new AsyncShutdown.Barrier(
   1231          "ShutdownLeaks: Wait for cleanup to be finished before checking for leaks"
   1232        );
   1233        Services.obs.notifyObservers(
   1234          { wrappedJSObject: barrier },
   1235          "shutdown-leaks-before-check"
   1236        );
   1237 
   1238        barrier.client.addBlocker(
   1239          "ShutdownLeaks: Wait for tabs to finish closing",
   1240          TabDestroyObserver.wait()
   1241        );
   1242 
   1243        barrier.wait().then(() => {
   1244          // Simulate memory pressure so that we're forced to free more resources
   1245          // and thus get rid of more false leaks like already terminated workers.
   1246          Services.obs.notifyObservers(
   1247            null,
   1248            "memory-pressure",
   1249            "heap-minimize"
   1250          );
   1251 
   1252          Services.ppmm.broadcastAsyncMessage("browser-test:collect-request");
   1253 
   1254          shutdownCleanup(() => {
   1255            setTimeout(() => {
   1256              shutdownCleanup(() => {
   1257                this.finish();
   1258              });
   1259            }, 1000);
   1260          });
   1261        });
   1262 
   1263        return;
   1264      }
   1265 
   1266      if (this.repeat > 0) {
   1267        --this.repeat;
   1268        if (this.currentTestIndex < 0) {
   1269          this.currentTestIndex = 0;
   1270        }
   1271        this.execTest();
   1272      } else {
   1273        this.currentTestIndex++;
   1274        if (gConfig.repeat) {
   1275          this.repeat = gConfig.repeat;
   1276        }
   1277        this.execTest();
   1278      }
   1279    });
   1280  },
   1281 
   1282  async handleTask(task, currentTest, PromiseTestUtils, isSetup = false) {
   1283    let currentScope = currentTest.scope;
   1284    let desc = isSetup ? "setup" : "test";
   1285    currentScope.SimpleTest.info(`Entering ${desc} ${task.name}`);
   1286    let startTimestamp = ChromeUtils.now();
   1287    currentScope.SimpleTest._currentTaskName = task.name;
   1288 
   1289    let controller = new AbortController();
   1290    currentScope.__signal = controller.signal;
   1291    if (isSetup) {
   1292      currentScope.registerCleanupFunction(() => {
   1293        controller.abort();
   1294      });
   1295    }
   1296    try {
   1297      let result = await task();
   1298      if (isGenerator(result)) {
   1299        currentScope.SimpleTest.ok(false, "Task returned a generator");
   1300      }
   1301    } catch (ex) {
   1302      if (currentTest.timedOut) {
   1303        currentTest.addResult(
   1304          new testResult({
   1305            name: `Uncaught exception received from previously timed out ${desc} ${task.name}`,
   1306            pass: false,
   1307            ex,
   1308            stack: typeof ex == "object" && "stack" in ex ? ex.stack : null,
   1309            allowFailure: currentTest.allowFailure,
   1310          })
   1311        );
   1312        // We timed out, so we've already cleaned up for this test, just get outta here.
   1313        return;
   1314      }
   1315      currentTest.addResult(
   1316        new testResult({
   1317          name: `Uncaught exception in ${desc}`,
   1318          pass: currentScope.SimpleTest.isExpectingUncaughtException(),
   1319          ex,
   1320          stack: typeof ex == "object" && "stack" in ex ? ex.stack : null,
   1321          allowFailure: currentTest.allowFailure,
   1322        })
   1323      );
   1324    } finally {
   1325      if (!isSetup) {
   1326        controller.abort();
   1327      }
   1328    }
   1329    PromiseTestUtils.assertNoUncaughtRejections();
   1330    ChromeUtils.addProfilerMarker(
   1331      isSetup ? "setup-task" : "task",
   1332      { category: "Test", startTime: startTimestamp },
   1333      task.name || undefined
   1334    );
   1335    currentScope.SimpleTest.info(`Leaving ${desc} ${task.name}`);
   1336    currentScope.SimpleTest._currentTaskName = null;
   1337  },
   1338 
   1339  async _runTaskBasedTest(currentTest) {
   1340    let currentScope = currentTest.scope;
   1341 
   1342    // First run all the setups:
   1343    let setupFn;
   1344    while ((setupFn = currentScope.__setups.shift())) {
   1345      await this.handleTask(
   1346        setupFn,
   1347        currentTest,
   1348        this.PromiseTestUtils,
   1349        true /* is setup task */
   1350      );
   1351    }
   1352 
   1353    // Allow for a task to be skipped; we need only use the structured logger
   1354    // for this, whilst deactivating log buffering to ensure that messages
   1355    // are always printed to stdout.
   1356    let skipTask = (task, reason) => {
   1357      let logger = this.structuredLogger;
   1358      logger.deactivateBuffering();
   1359      logger.testStatus(this.currentTest.path, task.name, "SKIP");
   1360      let message = "Skipping test " + task.name;
   1361      if (reason) {
   1362        message += ` because the following conditions were met: (${reason})`;
   1363      }
   1364      logger.warning(message);
   1365      logger.activateBuffering();
   1366    };
   1367 
   1368    let task;
   1369    while ((task = currentScope.__tasks.shift())) {
   1370      let reason;
   1371      let shouldSkip = false;
   1372      if (
   1373        task.__skipMe ||
   1374        (currentScope.__runOnlyThisTask &&
   1375          task != currentScope.__runOnlyThisTask)
   1376      ) {
   1377        shouldSkip = true;
   1378      } else if (typeof task.__skip_if === "function" && task.__skip_if()) {
   1379        shouldSkip = true;
   1380        reason = task.__skip_if.toSource().replace(/\(\)\s*=>\s*/, "");
   1381      }
   1382 
   1383      if (shouldSkip) {
   1384        skipTask(task, reason);
   1385        continue;
   1386      }
   1387      await this.handleTask(task, currentTest, this.PromiseTestUtils);
   1388    }
   1389    currentScope.finish();
   1390  },
   1391 
   1392  execTest: function Tester_execTest() {
   1393    this.structuredLogger.testStart(this.currentTest.path);
   1394 
   1395    this.SimpleTest.reset();
   1396    // Reset accessibility environment.
   1397    this.AccessibilityUtils.reset(this.a11y_checks, this.currentTest.path);
   1398 
   1399    // Load the tests into a testscope
   1400    let currentScope = (this.currentTest.scope = new testScope(
   1401      this,
   1402      this.currentTest,
   1403      this.currentTest.expected
   1404    ));
   1405    let currentTest = this.currentTest;
   1406 
   1407    // HTTPS-First (Bug 1704453) TODO: in case a test is annoated
   1408    // with https_first_disabled then we explicitly flip the pref
   1409    // dom.security.https_first to false for the duration of the test.
   1410    if (currentTest.https_first_disabled) {
   1411      window.SpecialPowers.pushPrefEnv({
   1412        set: [["dom.security.https_first", false]],
   1413      });
   1414    }
   1415 
   1416    if (currentTest.allow_xul_xbl) {
   1417      window.SpecialPowers.pushPermissions([
   1418        { type: "allowXULXBL", allow: true, context: "http://mochi.test:8888" },
   1419        { type: "allowXULXBL", allow: true, context: "http://example.org" },
   1420      ]);
   1421    }
   1422 
   1423    // Import utils in the test scope.
   1424    let { scope } = this.currentTest;
   1425    scope.EventUtils = this.EventUtils;
   1426    scope.AccessibilityUtils = this.AccessibilityUtils;
   1427    scope.SimpleTest = this.SimpleTest;
   1428    scope.gTestPath = this.currentTest.path;
   1429    scope.ContentTask = this.ContentTask;
   1430    scope.BrowserTestUtils = this.BrowserTestUtils;
   1431    scope.TestUtils = this.TestUtils;
   1432    scope.ExtensionTestUtils = this.ExtensionTestUtils;
   1433    // Pass a custom report function for mochitest style reporting.
   1434    scope.Assert = new this.Assert(function (err, message, stack) {
   1435      currentTest.addResult(
   1436        new testResult(
   1437          err
   1438            ? {
   1439                name: err.message,
   1440                stack: err.stack,
   1441                allowFailure: currentTest.allowFailure,
   1442              }
   1443            : {
   1444                name: message,
   1445                pass: true,
   1446                stack,
   1447                allowFailure: currentTest.allowFailure,
   1448              }
   1449        )
   1450      );
   1451    }, true);
   1452 
   1453    this.ContentTask.setTestScope(currentScope);
   1454 
   1455    // Import Mochia methods in the test scope
   1456    Services.scriptloader.loadSubScript(
   1457      "resource://testing-common/Mochia.js",
   1458      scope
   1459    );
   1460 
   1461    // Allow Assert.sys.mjs methods to be tacked to the current scope.
   1462    scope.export_assertions = function () {
   1463      for (let func in this.Assert) {
   1464        this[func] = this.Assert[func].bind(this.Assert);
   1465      }
   1466    };
   1467 
   1468    // Override SimpleTest methods with ours.
   1469    SIMPLETEST_OVERRIDES.forEach(function (m) {
   1470      this.SimpleTest[m] = this[m];
   1471    }, scope);
   1472 
   1473    // load the tools to work with chrome .jar and remote
   1474    try {
   1475      this._scriptLoader.loadSubScript(
   1476        "chrome://mochikit/content/chrome-harness.js",
   1477        scope
   1478      );
   1479    } catch (ex) {
   1480      /* no chrome-harness tools */
   1481    }
   1482 
   1483    // Ensure we are not idle at the beginning of the test. If we don't do this,
   1484    // the browser may behave differently if the previous tests ran long.
   1485    // eg. the session store behavior changes 3 minutes after the last user event.
   1486    Cc["@mozilla.org/widget/useridleservice;1"]
   1487      .getService(Ci.nsIUserIdleServiceInternal)
   1488      .resetIdleTimeOut(0);
   1489 
   1490    // Import head.js script if it exists.
   1491    var currentTestDirPath = this.currentTest.path.substr(
   1492      0,
   1493      this.currentTest.path.lastIndexOf("/")
   1494    );
   1495    var headPath = currentTestDirPath + "/head.js";
   1496    try {
   1497      this._scriptLoader.loadSubScript(headPath, scope);
   1498    } catch (ex) {
   1499      // Bug 755558 - Ignore loadSubScript errors due to a missing head.js.
   1500      const isImportError = /^Error opening input stream/.test(ex.toString());
   1501 
   1502      // Bug 1503169 - head.js may call loadSubScript, and generate similar errors.
   1503      // Only swallow errors that are strictly related to loading head.js.
   1504      const containsHeadPath = ex.toString().includes(headPath);
   1505 
   1506      if (!isImportError || !containsHeadPath) {
   1507        this.currentTest.addResult(
   1508          new testResult({
   1509            name: "head.js import threw an exception",
   1510            ex,
   1511          })
   1512        );
   1513      }
   1514    }
   1515 
   1516    // Import the test script.
   1517    try {
   1518      this.lastStartTimestamp = ChromeUtils.now();
   1519      this.TestUtils.promiseTestFinished = new Promise(resolve => {
   1520        this.resolveFinishTestPromise = resolve;
   1521      });
   1522      this._scriptLoader.loadSubScript(this.currentTest.path, scope);
   1523      // Run the test
   1524      this.lastStartTime = Date.now();
   1525      if (this.currentTest.scope.__tasks) {
   1526        // This test consists of tasks, added via the `add_task()` API.
   1527        if ("test" in this.currentTest.scope) {
   1528          throw new Error(
   1529            "Cannot run both a add_task test and a normal test at the same time."
   1530          );
   1531        }
   1532        // Spin off the async work without waiting for it to complete.
   1533        // It'll call finish() when it's done.
   1534        this._runTaskBasedTest(this.currentTest);
   1535      } else if (typeof scope.test == "function") {
   1536        scope.test();
   1537      } else {
   1538        throw new Error(
   1539          "This test didn't call add_task, nor did it define a generatorTest() function, nor did it define a test() function, so we don't know how to run it."
   1540        );
   1541      }
   1542    } catch (ex) {
   1543      if (!this.SimpleTest.isIgnoringAllUncaughtExceptions()) {
   1544        this.currentTest.addResult(
   1545          new testResult({
   1546            name: "Exception thrown",
   1547            pass: this.SimpleTest.isExpectingUncaughtException(),
   1548            ex,
   1549            allowFailure: this.currentTest.allowFailure,
   1550          })
   1551        );
   1552        this.SimpleTest.expectUncaughtException(false);
   1553      } else {
   1554        this.currentTest.addResult(new testMessage("Exception thrown: " + ex));
   1555      }
   1556      this.currentTest.scope.finish();
   1557    }
   1558 
   1559    // If the test ran synchronously, move to the next test, otherwise the test
   1560    // will trigger the next test when it is done.
   1561    if (this.currentTest.scope.__done) {
   1562      this.nextTest();
   1563    } else {
   1564      var self = this;
   1565      var timeoutExpires = Date.now() + gTimeoutSeconds * 1000;
   1566      var waitUntilAtLeast = timeoutExpires - 1000;
   1567      this.currentTest.scope.__waitTimer =
   1568        this.SimpleTest._originalSetTimeout.apply(window, [
   1569          async function timeoutFn() {
   1570            // We sometimes get woken up long before the gTimeoutSeconds
   1571            // have elapsed (when running in chaos mode for example). This
   1572            // code ensures that we don't wrongly time out in that case.
   1573            if (Date.now() < waitUntilAtLeast) {
   1574              self.currentTest.scope.__waitTimer = setTimeout(
   1575                timeoutFn,
   1576                timeoutExpires - Date.now()
   1577              );
   1578              return;
   1579            }
   1580 
   1581            if (--self.currentTest.scope.__timeoutFactor > 0) {
   1582              // We were asked to wait a bit longer.
   1583              self.currentTest.scope.info(
   1584                "Longer timeout required, waiting longer...  Remaining timeouts: " +
   1585                  self.currentTest.scope.__timeoutFactor
   1586              );
   1587              self.currentTest.scope.__waitTimer = setTimeout(
   1588                timeoutFn,
   1589                gTimeoutSeconds * 1000
   1590              );
   1591              return;
   1592            }
   1593 
   1594            // If the test is taking longer than expected, but it's not hanging,
   1595            // mark the fact, but let the test continue.  At the end of the test,
   1596            // if it didn't timeout, we will notify the problem through an error.
   1597            // To figure whether it's an actual hang, compare the time of the last
   1598            // result or message to half of the timeout time.
   1599            // Though, to protect against infinite loops, limit the number of times
   1600            // we allow the test to proceed.
   1601            const MAX_UNEXPECTED_TIMEOUTS = 10;
   1602            if (
   1603              Date.now() - self.currentTest.lastOutputTime <
   1604                (gTimeoutSeconds / 2) * 1000 &&
   1605              ++self.currentTest.unexpectedTimeouts <= MAX_UNEXPECTED_TIMEOUTS
   1606            ) {
   1607              self.currentTest.scope.__waitTimer = setTimeout(
   1608                timeoutFn,
   1609                gTimeoutSeconds * 1000
   1610              );
   1611              return;
   1612            }
   1613 
   1614            let knownFailure = false;
   1615            if (gConfig.timeoutAsPass) {
   1616              knownFailure = true;
   1617            }
   1618            self.currentTest.addResult(
   1619              new testResult({
   1620                name: "Test timed out",
   1621                allowFailure: knownFailure,
   1622              })
   1623            );
   1624            self.currentTest.timedOut = true;
   1625            self.currentTest.scope.__waitTimer = null;
   1626            if (gConfig.timeoutAsPass) {
   1627              self.nextTest();
   1628            } else {
   1629              await self.notifyProfilerOfTestEnd();
   1630              self.finish();
   1631            }
   1632          },
   1633          gTimeoutSeconds * 1000,
   1634        ]);
   1635    }
   1636  },
   1637 
   1638  QueryInterface: ChromeUtils.generateQI(["nsIConsoleListener"]),
   1639 };
   1640 
   1641 // Note: duplicated in SimpleTest.js . See also bug 1820150.
   1642 function isErrorOrException(err) {
   1643  // It'd be nice if we had either `Error.isError(err)` or `Error.isInstance(err)`
   1644  // but we don't, so do it ourselves:
   1645  if (!err) {
   1646    return false;
   1647  }
   1648  if (err instanceof Ci.nsIException) {
   1649    return true;
   1650  }
   1651  try {
   1652    let glob = Cu.getGlobalForObject(err);
   1653    return err instanceof glob.Error;
   1654  } catch {
   1655    // getGlobalForObject can be upset if it doesn't get passed an object.
   1656    // Just do a standard instanceof check using this global and cross fingers:
   1657  }
   1658  return err instanceof Error;
   1659 }
   1660 
   1661 /**
   1662 * Represents the result of one test assertion. This is described with a string
   1663 * in traditional logging, and has a "status" and "expected" property used in
   1664 * structured logging. Normally, results are mapped as follows:
   1665 *
   1666 *   pass:    todo:    Added to:    Described as:           Status:  Expected:
   1667 *     true     false    passCount    TEST-PASS               PASS     PASS
   1668 *     true     true     todoCount    TEST-KNOWN-FAIL         FAIL     FAIL
   1669 *     false    false    failCount    TEST-UNEXPECTED-FAIL    FAIL     PASS
   1670 *     false    true     failCount    TEST-UNEXPECTED-PASS    PASS     FAIL
   1671 *
   1672 * The "allowFailure" argument indicates that this is one of the assertions that
   1673 * should be allowed to fail, for example because "fail-if" is true for the
   1674 * current test file in the manifest. In this case, results are mapped this way:
   1675 *
   1676 *   pass:    todo:    Added to:    Described as:           Status:  Expected:
   1677 *     true     false    passCount    TEST-PASS               PASS     PASS
   1678 *     true     true     todoCount    TEST-KNOWN-FAIL         FAIL     FAIL
   1679 *     false    false    todoCount    TEST-KNOWN-FAIL         FAIL     FAIL
   1680 *     false    true     todoCount    TEST-KNOWN-FAIL         FAIL     FAIL
   1681 */
   1682 function testResult({ name, pass, todo, ex, stack, allowFailure }) {
   1683  this.info = false;
   1684  this.name = name;
   1685  this.msg = "";
   1686 
   1687  if (allowFailure && !pass) {
   1688    this.allowedFailure = true;
   1689    this.pass = true;
   1690    this.todo = false;
   1691  } else if (allowFailure && pass) {
   1692    this.pass = true;
   1693    this.todo = false;
   1694  } else {
   1695    this.pass = !!pass;
   1696    this.todo = todo;
   1697  }
   1698 
   1699  this.expected = this.todo ? "FAIL" : "PASS";
   1700 
   1701  if (this.pass) {
   1702    this.status = this.expected;
   1703    return;
   1704  }
   1705 
   1706  this.status = this.todo ? "PASS" : "FAIL";
   1707 
   1708  if (ex) {
   1709    if (typeof ex == "object" && "fileName" in ex) {
   1710      // Only add "at fileName:lineNumber" if stack doesn't start with same location
   1711      let stackMatchesExLocation = false;
   1712 
   1713      if (stack instanceof Ci.nsIStackFrame) {
   1714        stackMatchesExLocation =
   1715          stack.filename == ex.fileName && stack.lineNumber == ex.lineNumber;
   1716      } else if (typeof stack === "string") {
   1717        // For string stacks, format is: functionName@fileName:lineNumber:columnNumber
   1718        // Check if first line contains fileName:lineNumber
   1719        let firstLine = stack.split("\n")[0];
   1720        let expectedLocation = ex.fileName + ":" + ex.lineNumber;
   1721        stackMatchesExLocation = firstLine.includes(expectedLocation);
   1722      }
   1723 
   1724      if (!stackMatchesExLocation) {
   1725        this.msg += "at " + ex.fileName + ":" + ex.lineNumber + " - ";
   1726      }
   1727    }
   1728 
   1729    if (
   1730      typeof ex == "string" ||
   1731      (typeof ex == "object" && isErrorOrException(ex))
   1732    ) {
   1733      this.msg += String(ex);
   1734    } else {
   1735      try {
   1736        this.msg += JSON.stringify(ex);
   1737      } catch {
   1738        this.msg += String(ex);
   1739      }
   1740    }
   1741  }
   1742 
   1743  // Store stack separately instead of appending to msg
   1744  if (stack) {
   1745    let normalized;
   1746    if (stack instanceof Ci.nsIStackFrame) {
   1747      let frames = [];
   1748      for (
   1749        let frame = stack;
   1750        frame;
   1751        frame = frame.asyncCaller || frame.caller
   1752      ) {
   1753        let msg = `${frame.filename}:${frame.name}:${frame.lineNumber}`;
   1754        frames.push(frame.asyncCause ? `${frame.asyncCause}*${msg}` : msg);
   1755      }
   1756      normalized = frames.join("\n");
   1757    } else {
   1758      normalized = "" + stack;
   1759    }
   1760    this.stack = normalized;
   1761  }
   1762 
   1763  if (gConfig.debugOnFailure) {
   1764    // You've hit this line because you requested to break into the
   1765    // debugger upon a testcase failure on your test run.
   1766    // eslint-disable-next-line no-debugger
   1767    debugger;
   1768  }
   1769 
   1770  // Optionally, test variables can be saved to a file, which will be uploaded
   1771  // as an artifact if the test is running on try.
   1772  saveScopeVariablesAsJSON();
   1773 }
   1774 
   1775 function testMessage(msg) {
   1776  this.msg = msg || "";
   1777  this.info = true;
   1778 }
   1779 
   1780 // Need to be careful adding properties to this object, since its properties
   1781 // cannot conflict with global variables used in tests.
   1782 function testScope(aTester, aTest, expected) {
   1783  this.__tester = aTester;
   1784 
   1785  aTest.allowFailure = expected == "fail";
   1786 
   1787  var self = this;
   1788  this.ok = function test_ok(condition, name) {
   1789    if (arguments.length > 2) {
   1790      const ex = "Too many arguments passed to ok(condition, name)`.";
   1791      self.record(false, name, ex);
   1792    } else {
   1793      self.record(condition, name);
   1794    }
   1795  };
   1796  this.record = function test_record(condition, name, ex, stack, expected) {
   1797    if (expected == "fail") {
   1798      aTest.addResult(
   1799        new testResult({
   1800          name,
   1801          pass: !condition,
   1802          todo: true,
   1803          ex,
   1804          stack: stack || Components.stack.caller,
   1805          allowFailure: aTest.allowFailure,
   1806        })
   1807      );
   1808    } else {
   1809      aTest.addResult(
   1810        new testResult({
   1811          name,
   1812          pass: condition,
   1813          ex,
   1814          stack: stack || Components.stack.caller,
   1815          allowFailure: aTest.allowFailure,
   1816        })
   1817      );
   1818    }
   1819  };
   1820  this.is = function test_is(a, b, name) {
   1821    self.record(
   1822      Object.is(a, b),
   1823      name,
   1824      `Got ${self.repr(a)}, expected ${self.repr(b)}`,
   1825      false,
   1826      Components.stack.caller
   1827    );
   1828  };
   1829  this.isfuzzy = function test_isfuzzy(a, b, epsilon, name) {
   1830    self.record(
   1831      a >= b - epsilon && a <= b + epsilon,
   1832      name,
   1833      `Got ${self.repr(a)}, expected ${self.repr(b)} epsilon: +/- ${self.repr(
   1834        epsilon
   1835      )}`,
   1836      false,
   1837      Components.stack.caller
   1838    );
   1839  };
   1840  this.isnot = function test_isnot(a, b, name) {
   1841    self.record(
   1842      !Object.is(a, b),
   1843      name,
   1844      `Didn't expect ${self.repr(a)}, but got it`,
   1845      false,
   1846      Components.stack.caller
   1847    );
   1848  };
   1849  this.todo = function test_todo(condition, name, ex, stack) {
   1850    aTest.addResult(
   1851      new testResult({
   1852        name,
   1853        pass: !condition,
   1854        todo: true,
   1855        ex,
   1856        stack: stack || Components.stack.caller,
   1857        allowFailure: aTest.allowFailure,
   1858      })
   1859    );
   1860  };
   1861  this.todo_is = function test_todo_is(a, b, name) {
   1862    self.todo(
   1863      Object.is(a, b),
   1864      name,
   1865      `Got ${self.repr(a)}, expected ${self.repr(b)}`,
   1866      Components.stack.caller
   1867    );
   1868  };
   1869  this.todo_isnot = function test_todo_isnot(a, b, name) {
   1870    self.todo(
   1871      !Object.is(a, b),
   1872      name,
   1873      `Didn't expect ${self.repr(a)}, but got it`,
   1874      Components.stack.caller
   1875    );
   1876  };
   1877  this.info = function test_info(name) {
   1878    aTest.addResult(new testMessage(name));
   1879  };
   1880  this.repr = function repr(o) {
   1881    if (typeof o == "undefined") {
   1882      return "undefined";
   1883    } else if (o === null) {
   1884      return "null";
   1885    }
   1886    try {
   1887      if (typeof o.__repr__ == "function") {
   1888        return o.__repr__();
   1889      } else if (typeof o.repr == "function" && o.repr != repr) {
   1890        return o.repr();
   1891      }
   1892    } catch (e) {}
   1893    try {
   1894      if (
   1895        typeof o.NAME == "string" &&
   1896        (o.toString == Function.prototype.toString ||
   1897          o.toString == Object.prototype.toString)
   1898      ) {
   1899        return o.NAME;
   1900      }
   1901    } catch (e) {}
   1902    var ostring;
   1903    try {
   1904      if (Object.is(o, +0)) {
   1905        ostring = "+0";
   1906      } else if (Object.is(o, -0)) {
   1907        ostring = "-0";
   1908      } else if (typeof o === "string") {
   1909        ostring = JSON.stringify(o);
   1910      } else if (Array.isArray(o)) {
   1911        ostring = "[" + o.map(val => repr(val)).join(", ") + "]";
   1912      } else {
   1913        ostring = String(o);
   1914      }
   1915    } catch (e) {
   1916      return `[${Object.prototype.toString.call(o)}]`;
   1917    }
   1918    if (typeof o == "function") {
   1919      ostring = ostring.replace(/\) \{[^]*/, ") { ... }");
   1920    }
   1921    return ostring;
   1922  };
   1923 
   1924  this.executeSoon = function test_executeSoon(func) {
   1925    Services.tm.dispatchToMainThread({
   1926      run() {
   1927        func();
   1928      },
   1929    });
   1930  };
   1931 
   1932  this.waitForExplicitFinish = function test_waitForExplicitFinish() {
   1933    self.__done = false;
   1934  };
   1935 
   1936  this.waitForFocus = function test_waitForFocus(
   1937    callback,
   1938    targetWindow,
   1939    expectBlankPage
   1940  ) {
   1941    self.SimpleTest.waitForFocus(callback, targetWindow, expectBlankPage);
   1942  };
   1943 
   1944  this.waitForClipboard = function test_waitForClipboard(
   1945    expected,
   1946    setup,
   1947    success,
   1948    failure,
   1949    flavor
   1950  ) {
   1951    self.SimpleTest.waitForClipboard(expected, setup, success, failure, flavor);
   1952  };
   1953 
   1954  this.registerCleanupFunction = function test_registerCleanupFunction(
   1955    aFunction
   1956  ) {
   1957    self.__cleanupFunctions.push(aFunction);
   1958  };
   1959 
   1960  this.requestLongerTimeout = function test_requestLongerTimeout(aFactor) {
   1961    self.__timeoutFactor = aFactor;
   1962  };
   1963 
   1964  this.expectUncaughtException = function test_expectUncaughtException(
   1965    aExpecting
   1966  ) {
   1967    self.SimpleTest.expectUncaughtException(aExpecting);
   1968  };
   1969 
   1970  this.ignoreAllUncaughtExceptions = function test_ignoreAllUncaughtExceptions(
   1971    aIgnoring
   1972  ) {
   1973    self.SimpleTest.ignoreAllUncaughtExceptions(aIgnoring);
   1974  };
   1975 
   1976  this.expectAssertions = function test_expectAssertions(aMin, aMax) {
   1977    let min = aMin;
   1978    let max = aMax;
   1979    if (typeof max == "undefined") {
   1980      max = min;
   1981    }
   1982    if (
   1983      typeof min != "number" ||
   1984      typeof max != "number" ||
   1985      min < 0 ||
   1986      max < min
   1987    ) {
   1988      throw new Error("bad parameter to expectAssertions");
   1989    }
   1990    self.__expectedMinAsserts = min;
   1991    self.__expectedMaxAsserts = max;
   1992  };
   1993 
   1994  this.finish = function test_finish() {
   1995    self.__done = true;
   1996    if (self.__waitTimer) {
   1997      self.executeSoon(function () {
   1998        if (self.__done && self.__waitTimer) {
   1999          clearTimeout(self.__waitTimer);
   2000          self.__waitTimer = null;
   2001          self.__tester.nextTest();
   2002        }
   2003      });
   2004    }
   2005  };
   2006 
   2007  this.requestCompleteLog = function test_requestCompleteLog() {
   2008    self.__tester.structuredLogger.deactivateBuffering();
   2009    self.registerCleanupFunction(function () {
   2010      self.__tester.structuredLogger.activateBuffering();
   2011    });
   2012  };
   2013 
   2014  return this;
   2015 }
   2016 
   2017 function decorateTaskFn(fn) {
   2018  let originalName = fn.name;
   2019  fn = fn.bind(this);
   2020  // Restore original name to avoid "bound " prefix in task name
   2021  Object.defineProperty(fn, "name", { value: originalName });
   2022  fn.skip = (val = true) => (fn.__skipMe = val);
   2023  fn.only = () => (this.__runOnlyThisTask = fn);
   2024  return fn;
   2025 }
   2026 
   2027 testScope.prototype = {
   2028  __done: true,
   2029  __tasks: null,
   2030  __setups: [],
   2031  __runOnlyThisTask: null,
   2032  __waitTimer: null,
   2033  __cleanupFunctions: [],
   2034  __timeoutFactor: 1,
   2035  __expectedMinAsserts: 0,
   2036  __expectedMaxAsserts: 0,
   2037  /** @type {AbortSignal} */
   2038  __signal: null,
   2039 
   2040  EventUtils: {},
   2041  AccessibilityUtils: {},
   2042  SimpleTest: {},
   2043  ContentTask: null,
   2044  BrowserTestUtils: null,
   2045  TestUtils: null,
   2046  ExtensionTestUtils: null,
   2047  Assert: null,
   2048 
   2049  /**
   2050   * Add a function which returns a promise (usually an async function)
   2051   * as a test task.
   2052   *
   2053   * The task ends when the promise returned by the function resolves or
   2054   * rejects. If the test function throws, or the promise it returns
   2055   * rejects, the test is reported as a failure. Execution continues
   2056   * with the next test function.
   2057   *
   2058   * Example usage:
   2059   *
   2060   * add_task(async function test() {
   2061   *   let result = await Promise.resolve(true);
   2062   *
   2063   *   ok(result);
   2064   *
   2065   *   let secondary = await someFunctionThatReturnsAPromise(result);
   2066   *   is(secondary, "expected value");
   2067   * });
   2068   *
   2069   * add_task(async function test_early_return() {
   2070   *   let result = await somethingThatReturnsAPromise();
   2071   *
   2072   *   if (!result) {
   2073   *     // Test is ended immediately, with success.
   2074   *     return;
   2075   *   }
   2076   *
   2077   *   is(result, "foo");
   2078   * });
   2079   *
   2080   * add_task({
   2081   *   skip_if: () => !AppConstants.DEBUG,
   2082   * },
   2083   * async function test_debug_only() {
   2084   *   ok(true, "Test ran in a debug build");
   2085   * });
   2086   */
   2087  add_task(properties, func = properties) {
   2088    if (!this.__tasks) {
   2089      this.waitForExplicitFinish();
   2090      this.__tasks = [];
   2091    }
   2092 
   2093    let bound = decorateTaskFn.call(this, func);
   2094 
   2095    if (
   2096      typeof properties === "object" &&
   2097      typeof properties.skip_if === "function"
   2098    ) {
   2099      bound.__skip_if = properties.skip_if;
   2100    }
   2101 
   2102    this.__tasks.push(bound);
   2103    return bound;
   2104  },
   2105 
   2106  add_setup(aFunction) {
   2107    if (!this.__setups.length) {
   2108      this.waitForExplicitFinish();
   2109    }
   2110    let bound = aFunction.bind(this);
   2111    this.__setups.push(bound);
   2112    return bound;
   2113  },
   2114 
   2115  destroy: function test_destroy() {
   2116    for (let prop in this) {
   2117      delete this[prop];
   2118    }
   2119  },
   2120 
   2121  get testSignal() {
   2122    return this.__signal;
   2123  },
   2124 };