Bug 1806808 [wpt PR 37624] - Revert "WPT: Allow `window.onload` to contain multiple...
[gecko.git] / testing / mochitest / browser-test.js
blob7bc708504333bb32fe1870b86ff976e9554214e2
1 /* -*- js-indent-level: 2; tab-width: 2; indent-tabs-mode: nil -*- */
3 /* eslint-env mozilla/browser-window */
4 /* import-globals-from chrome-harness.js */
5 /* import-globals-from mochitest-e10s-utils.js */
7 // Test timeout (seconds)
8 var gTimeoutSeconds = 45;
9 var gConfig;
11 var { AppConstants } = ChromeUtils.importESModule(
12   "resource://gre/modules/AppConstants.sys.mjs"
15 ChromeUtils.defineModuleGetter(
16   this,
17   "AddonManager",
18   "resource://gre/modules/AddonManager.jsm"
21 const SIMPLETEST_OVERRIDES = [
22   "ok",
23   "record",
24   "is",
25   "isnot",
26   "todo",
27   "todo_is",
28   "todo_isnot",
29   "info",
30   "expectAssertions",
31   "requestCompleteLog",
34 setTimeout(testInit, 0);
36 var TabDestroyObserver = {
37   outstanding: new Set(),
38   promiseResolver: null,
40   init() {
41     Services.obs.addObserver(this, "message-manager-close");
42     Services.obs.addObserver(this, "message-manager-disconnect");
43   },
45   destroy() {
46     Services.obs.removeObserver(this, "message-manager-close");
47     Services.obs.removeObserver(this, "message-manager-disconnect");
48   },
50   observe(subject, topic, data) {
51     if (topic == "message-manager-close") {
52       this.outstanding.add(subject);
53     } else if (topic == "message-manager-disconnect") {
54       this.outstanding.delete(subject);
55       if (!this.outstanding.size && this.promiseResolver) {
56         this.promiseResolver();
57       }
58     }
59   },
61   wait() {
62     if (!this.outstanding.size) {
63       return Promise.resolve();
64     }
66     return new Promise(resolve => {
67       this.promiseResolver = resolve;
68     });
69   },
72 function testInit() {
73   gConfig = readConfig();
74   if (gConfig.testRoot == "browser") {
75     // Make sure to launch the test harness for the first opened window only
76     var prefs = Services.prefs;
77     if (prefs.prefHasUserValue("testing.browserTestHarness.running")) {
78       return;
79     }
81     prefs.setBoolPref("testing.browserTestHarness.running", true);
83     if (prefs.prefHasUserValue("testing.browserTestHarness.timeout")) {
84       gTimeoutSeconds = prefs.getIntPref("testing.browserTestHarness.timeout");
85     }
87     var sstring = Cc["@mozilla.org/supports-string;1"].createInstance(
88       Ci.nsISupportsString
89     );
90     sstring.data = location.search;
92     Services.ww.openWindow(
93       window,
94       "chrome://mochikit/content/browser-harness.xhtml",
95       "browserTest",
96       "chrome,centerscreen,dialog=no,resizable,titlebar,toolbar=no,width=800,height=600",
97       sstring
98     );
99   } else {
100     // This code allows us to redirect without requiring specialpowers for chrome and a11y tests.
101     let messageHandler = function(m) {
102       // eslint-disable-next-line no-undef
103       messageManager.removeMessageListener("chromeEvent", messageHandler);
104       var url = m.json.data;
106       // Window is the [ChromeWindow] for messageManager, so we need content.window
107       // Currently chrome tests are run in a content window instead of a ChromeWindow
108       // eslint-disable-next-line no-undef
109       var webNav = content.window.docShell.QueryInterface(Ci.nsIWebNavigation);
110       let loadURIOptions = {
111         triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
112       };
113       webNav.loadURI(url, loadURIOptions);
114     };
116     var listener =
117       '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);';
118     // eslint-disable-next-line no-undef
119     messageManager.addMessageListener("chromeEvent", messageHandler);
120     // eslint-disable-next-line no-undef
121     messageManager.loadFrameScript(listener, true);
122   }
123   if (gConfig.e10s) {
124     e10s_init();
126     let processCount = prefs.getIntPref("dom.ipc.processCount", 1);
127     if (processCount > 1) {
128       // Currently starting a content process is slow, to aviod timeouts, let's
129       // keep alive content processes.
130       prefs.setIntPref("dom.ipc.keepProcessesAlive.web", processCount);
131     }
133     Services.mm.loadFrameScript(
134       "chrome://mochikit/content/shutdown-leaks-collector.js",
135       true
136     );
137   } else {
138     // In non-e10s, only run the ShutdownLeaksCollector in the parent process.
139     ChromeUtils.importESModule(
140       "chrome://mochikit/content/ShutdownLeaksCollector.sys.mjs"
141     );
142   }
145 function isGenerator(value) {
146   return value && typeof value === "object" && typeof value.next === "function";
149 function Tester(aTests, structuredLogger, aCallback) {
150   this.structuredLogger = structuredLogger;
151   this.tests = aTests;
152   this.callback = aCallback;
154   this._scriptLoader = Services.scriptloader;
155   this.EventUtils = {};
156   this._scriptLoader.loadSubScript(
157     "chrome://mochikit/content/tests/SimpleTest/EventUtils.js",
158     this.EventUtils
159   );
161   this._scriptLoader.loadSubScript(
162     "chrome://mochikit/content/tests/SimpleTest/AccessibilityUtils.js",
163     // AccessibilityUtils are integrated with EventUtils to perform additional
164     // accessibility checks for certain user interactions (clicks, etc). Load
165     // them into the EventUtils scope here.
166     this.EventUtils
167   );
168   this.AccessibilityUtils = this.EventUtils.AccessibilityUtils;
170   // Make sure our SpecialPowers actor is instantiated, in case it was
171   // registered after our DOMWindowCreated event was fired (which it
172   // most likely was).
173   void window.windowGlobalChild.getActor("SpecialPowers");
175   var simpleTestScope = {};
176   this._scriptLoader.loadSubScript(
177     "chrome://mochikit/content/tests/SimpleTest/SimpleTest.js",
178     simpleTestScope
179   );
180   this._scriptLoader.loadSubScript(
181     "chrome://mochikit/content/tests/SimpleTest/MemoryStats.js",
182     simpleTestScope
183   );
184   this._scriptLoader.loadSubScript(
185     "chrome://mochikit/content/chrome-harness.js",
186     simpleTestScope
187   );
188   this.SimpleTest = simpleTestScope.SimpleTest;
190   window.SpecialPowers.SimpleTest = this.SimpleTest;
191   window.SpecialPowers.setAsDefaultAssertHandler();
193   var extensionUtilsScope = {
194     registerCleanupFunction: fn => {
195       this.currentTest.scope.registerCleanupFunction(fn);
196     },
197   };
198   extensionUtilsScope.SimpleTest = this.SimpleTest;
199   this._scriptLoader.loadSubScript(
200     "chrome://mochikit/content/tests/SimpleTest/ExtensionTestUtils.js",
201     extensionUtilsScope
202   );
203   this.ExtensionTestUtils = extensionUtilsScope.ExtensionTestUtils;
205   this.SimpleTest.harnessParameters = gConfig;
207   this.MemoryStats = simpleTestScope.MemoryStats;
208   this.ContentTask = ChromeUtils.importESModule(
209     "resource://testing-common/ContentTask.sys.mjs"
210   ).ContentTask;
211   this.BrowserTestUtils = ChromeUtils.importESModule(
212     "resource://testing-common/BrowserTestUtils.sys.mjs"
213   ).BrowserTestUtils;
214   this.TestUtils = ChromeUtils.importESModule(
215     "resource://testing-common/TestUtils.sys.mjs"
216   ).TestUtils;
217   this.PromiseTestUtils = ChromeUtils.importESModule(
218     "resource://testing-common/PromiseTestUtils.sys.mjs"
219   ).PromiseTestUtils;
220   this.Assert = ChromeUtils.importESModule(
221     "resource://testing-common/Assert.sys.mjs"
222   ).Assert;
223   this.PerTestCoverageUtils = ChromeUtils.import(
224     "resource://testing-common/PerTestCoverageUtils.jsm"
225   ).PerTestCoverageUtils;
227   this.PromiseTestUtils.init();
229   this.SimpleTestOriginal = {};
230   SIMPLETEST_OVERRIDES.forEach(m => {
231     this.SimpleTestOriginal[m] = this.SimpleTest[m];
232   });
234   this._coverageCollector = null;
236   const { XPCOMUtils } = ChromeUtils.importESModule(
237     "resource://gre/modules/XPCOMUtils.sys.mjs"
238   );
240   // Avoid failing tests when XPCOMUtils.defineLazyScriptGetter is used.
241   XPCOMUtils.overrideScriptLoaderForTests({
242     loadSubScript: (url, obj) => {
243       let before = Object.keys(window);
244       try {
245         return this._scriptLoader.loadSubScript(url, obj);
246       } finally {
247         for (let property of Object.keys(window)) {
248           if (
249             !before.includes(property) &&
250             !this._globalProperties.includes(property)
251           ) {
252             this._globalProperties.push(property);
253             this.SimpleTest.info(
254               `Global property added while loading ${url}: ${property}`
255             );
256           }
257         }
258       }
259     },
260     loadSubScriptWithOptions: this._scriptLoader.loadSubScriptWithOptions.bind(
261       this._scriptLoader
262     ),
263   });
265   // ensure the mouse is reset before each test run
266   if (Services.env.exists("MOZ_AUTOMATION")) {
267     this.EventUtils.synthesizeNativeMouseEvent({
268       type: "mousemove",
269       screenX: 1000,
270       screenY: 10,
271     });
272   }
274 Tester.prototype = {
275   EventUtils: {},
276   AccessibilityUtils: {},
277   SimpleTest: {},
278   ContentTask: null,
279   ExtensionTestUtils: null,
280   Assert: null,
282   repeat: 0,
283   a11y_checks: false,
284   runUntilFailure: false,
285   checker: null,
286   currentTestIndex: -1,
287   lastStartTime: null,
288   lastStartTimestamp: null,
289   lastAssertionCount: 0,
290   failuresFromInitialWindowState: 0,
292   get currentTest() {
293     return this.tests[this.currentTestIndex];
294   },
295   get done() {
296     return this.currentTestIndex == this.tests.length - 1 && this.repeat <= 0;
297   },
299   start: function Tester_start() {
300     TabDestroyObserver.init();
302     // if testOnLoad was not called, then gConfig is not defined
303     if (!gConfig) {
304       gConfig = readConfig();
305     }
307     if (gConfig.runUntilFailure) {
308       this.runUntilFailure = true;
309     }
311     if (gConfig.a11y_checks != undefined) {
312       this.a11y_checks = gConfig.a11y_checks;
313     }
315     if (gConfig.repeat) {
316       this.repeat = gConfig.repeat;
317     }
319     if (gConfig.jscovDirPrefix) {
320       let coveragePath = gConfig.jscovDirPrefix;
321       let { CoverageCollector } = ChromeUtils.importESModule(
322         "resource://testing-common/CoverageUtils.sys.mjs"
323       );
324       this._coverageCollector = new CoverageCollector(coveragePath);
325     }
327     this.structuredLogger.info("*** Start BrowserChrome Test Results ***");
328     Services.console.registerListener(this);
329     this._globalProperties = Object.keys(window);
330     this._globalPropertyWhitelist = [
331       "navigator",
332       "constructor",
333       "top",
334       "Application",
335       "__SS_tabsToRestore",
336       "__SSi",
337       "webConsoleCommandController",
338       // Thunderbird
339       "MailMigrator",
340       "SearchIntegration",
341     ];
343     this.PerTestCoverageUtils.beforeTestSync();
345     if (this.tests.length) {
346       this.waitForWindowsReady().then(() => {
347         this.nextTest();
348       });
349     } else {
350       this.finish();
351     }
352   },
354   async waitForWindowsReady() {
355     await this.setupDefaultTheme();
356     await new Promise(resolve =>
357       this.waitForGraphicsTestWindowToBeGone(resolve)
358     );
359     await this.promiseMainWindowReady();
360   },
362   async promiseMainWindowReady() {
363     if (window.gBrowserInit) {
364       await window.gBrowserInit.idleTasksFinishedPromise;
365     }
366   },
368   async setupDefaultTheme() {
369     // Developer Edition enables the wrong theme by default. Make sure
370     // the ordinary default theme is enabled.
371     let theme = await AddonManager.getAddonByID("default-theme@mozilla.org");
372     await theme.enable();
373   },
375   waitForGraphicsTestWindowToBeGone(aCallback) {
376     for (let win of Services.wm.getEnumerator(null)) {
377       if (
378         win != window &&
379         !win.closed &&
380         win.document.documentURI ==
381           "chrome://gfxsanity/content/sanityparent.html"
382       ) {
383         this.BrowserTestUtils.domWindowClosed(win).then(aCallback);
384         return;
385       }
386     }
387     // graphics test window is already gone, just call callback immediately
388     aCallback();
389   },
391   waitForWindowsState: function Tester_waitForWindowsState(aCallback) {
392     let timedOut = this.currentTest && this.currentTest.timedOut;
393     // eslint-disable-next-line no-nested-ternary
394     let baseMsg = timedOut
395       ? "Found a {elt} after previous test timed out"
396       : this.currentTest
397       ? "Found an unexpected {elt} at the end of test run"
398       : "Found an unexpected {elt}";
400     // Remove stale tabs
401     if (
402       this.currentTest &&
403       window.gBrowser &&
404       AppConstants.MOZ_APP_NAME != "thunderbird" &&
405       gBrowser.tabs.length > 1
406     ) {
407       let lastURI = "";
408       let lastURIcount = 0;
409       while (gBrowser.tabs.length > 1) {
410         let lastTab = gBrowser.tabs[gBrowser.tabs.length - 1];
411         if (!lastTab.closing) {
412           // Report the stale tab as an error only when they're not closing.
413           // Tests can finish without waiting for the closing tabs.
414           if (lastURI != lastTab.linkedBrowser.currentURI.spec) {
415             lastURI = lastTab.linkedBrowser.currentURI.spec;
416           } else {
417             lastURIcount++;
418             if (lastURIcount >= 3) {
419               this.currentTest.addResult(
420                 new testResult({
421                   name:
422                     "terminating browser early - unable to close tabs; skipping remaining tests in folder",
423                   allowFailure: this.currentTest.allowFailure,
424                 })
425               );
426               this.finish();
427             }
428           }
429           this.currentTest.addResult(
430             new testResult({
431               name:
432                 baseMsg.replace("{elt}", "tab") +
433                 ": " +
434                 lastTab.linkedBrowser.currentURI.spec,
435               allowFailure: this.currentTest.allowFailure,
436             })
437           );
438         }
439         gBrowser.removeTab(lastTab);
440       }
441     }
443     // Replace the last tab with a fresh one
444     if (window.gBrowser && AppConstants.MOZ_APP_NAME != "thunderbird") {
445       gBrowser.addTab("about:blank", {
446         skipAnimation: true,
447         triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
448       });
449       gBrowser.removeTab(gBrowser.selectedTab, { skipPermitUnload: true });
450       gBrowser.stop();
451     }
453     // Remove stale windows
454     this.structuredLogger.info("checking window state");
455     for (let win of Services.wm.getEnumerator(null)) {
456       let type = win.document.documentElement.getAttribute("windowtype");
457       if (
458         win != window &&
459         !win.closed &&
460         win.document.documentElement.getAttribute("id") !=
461           "browserTestHarness" &&
462         type != "devtools:webconsole"
463       ) {
464         switch (type) {
465           case "navigator:browser":
466             type = "browser window";
467             break;
468           case "mail:3pane":
469             type = "mail window";
470             break;
471           case null:
472             type =
473               "unknown window with document URI: " +
474               win.document.documentURI +
475               " and title: " +
476               win.document.title;
477             break;
478         }
479         let msg = baseMsg.replace("{elt}", type);
480         if (this.currentTest) {
481           this.currentTest.addResult(
482             new testResult({
483               name: msg,
484               allowFailure: this.currentTest.allowFailure,
485             })
486           );
487         } else {
488           this.failuresFromInitialWindowState++;
489           this.structuredLogger.error("browser-test.js | " + msg);
490         }
492         win.close();
493       }
494     }
496     // Make sure the window is raised before each test.
497     this.SimpleTest.waitForFocus(aCallback);
498   },
500   finish: function Tester_finish(aSkipSummary) {
501     var passCount = this.tests.reduce((a, f) => a + f.passCount, 0);
502     var failCount = this.tests.reduce((a, f) => a + f.failCount, 0);
503     var todoCount = this.tests.reduce((a, f) => a + f.todoCount, 0);
505     // Include failures from window state checking prior to running the first test
506     failCount += this.failuresFromInitialWindowState;
508     TabDestroyObserver.destroy();
509     Services.console.unregisterListener(this);
511     // It's important to terminate the module to avoid crashes on shutdown.
512     this.PromiseTestUtils.uninit();
514     // In the main process, we print the ShutdownLeaksCollector message here.
515     let pid = Services.appinfo.processID;
516     dump("Completed ShutdownLeaks collections in process " + pid + "\n");
518     this.structuredLogger.info("TEST-START | Shutdown");
520     if (this.tests.length) {
521       let e10sMode = window.gMultiProcessBrowser ? "e10s" : "non-e10s";
522       this.structuredLogger.info("Browser Chrome Test Summary");
523       this.structuredLogger.info("Passed:  " + passCount);
524       this.structuredLogger.info("Failed:  " + failCount);
525       this.structuredLogger.info("Todo:    " + todoCount);
526       this.structuredLogger.info("Mode:    " + e10sMode);
527     } else {
528       this.structuredLogger.error(
529         "browser-test.js | No tests to run. Did you pass invalid test_paths?"
530       );
531     }
532     this.structuredLogger.info("*** End BrowserChrome Test Results ***");
534     // Tests complete, notify the callback and return
535     this.callback(this.tests);
536     this.accService = null;
537     this.callback = null;
538     this.tests = null;
539   },
541   haltTests: function Tester_haltTests() {
542     // Do not run any further tests
543     this.currentTestIndex = this.tests.length - 1;
544     this.repeat = 0;
545   },
547   observe: function Tester_observe(aSubject, aTopic, aData) {
548     if (!aTopic) {
549       this.onConsoleMessage(aSubject);
550     }
551   },
553   onConsoleMessage: function Tester_onConsoleMessage(aConsoleMessage) {
554     // Ignore empty messages.
555     if (!aConsoleMessage.message) {
556       return;
557     }
559     try {
560       var msg = "Console message: " + aConsoleMessage.message;
561       if (this.currentTest) {
562         this.currentTest.addResult(new testMessage(msg));
563       } else {
564         this.structuredLogger.info(
565           "TEST-INFO | (browser-test.js) | " + msg.replace(/\n$/, "") + "\n"
566         );
567       }
568     } catch (ex) {
569       // Swallow exception so we don't lead to another error being reported,
570       // throwing us into an infinite loop
571     }
572   },
574   async ensureVsyncDisabled() {
575     // The WebExtension process keeps vsync enabled forever in headless mode.
576     // See bug 1782541.
577     if (Services.env.get("MOZ_HEADLESS")) {
578       return;
579     }
581     try {
582       await this.TestUtils.waitForCondition(
583         () => !ChromeUtils.vsyncEnabled(),
584         "waiting for vsync to be disabled"
585       );
586     } catch (e) {
587       this.Assert.ok(false, e);
588       this.Assert.ok(
589         false,
590         "vsync remained enabled at the end of the test. " +
591           "Is there an animation still running? " +
592           "Consider talking to the performance team for tips to solve this."
593       );
594     }
595   },
597   async nextTest() {
598     if (this.currentTest) {
599       if (this._coverageCollector) {
600         this._coverageCollector.recordTestCoverage(this.currentTest.path);
601       }
603       this.PerTestCoverageUtils.afterTestSync();
605       // Run cleanup functions for the current test before moving on to the
606       // next one.
607       let testScope = this.currentTest.scope;
608       while (testScope.__cleanupFunctions.length) {
609         let func = testScope.__cleanupFunctions.shift();
610         try {
611           let result = await func.apply(testScope);
612           if (isGenerator(result)) {
613             this.SimpleTest.ok(false, "Cleanup function returned a generator");
614           }
615         } catch (ex) {
616           this.currentTest.addResult(
617             new testResult({
618               name: "Cleanup function threw an exception",
619               ex,
620               allowFailure: this.currentTest.allowFailure,
621             })
622           );
623         }
624       }
626       // Spare tests cleanup work.
627       // Reset gReduceMotionOverride in case the test set it.
628       if (typeof gReduceMotionOverride == "boolean") {
629         gReduceMotionOverride = null;
630       }
632       Services.obs.notifyObservers(null, "test-complete");
634       if (
635         this.currentTest.passCount === 0 &&
636         this.currentTest.failCount === 0 &&
637         this.currentTest.todoCount === 0
638       ) {
639         this.currentTest.addResult(
640           new testResult({
641             name:
642               "This test contains no passes, no fails and no todos. Maybe" +
643               " it threw a silent exception? Make sure you use" +
644               " waitForExplicitFinish() if you need it.",
645           })
646         );
647       }
649       let winUtils = window.windowUtils;
650       if (winUtils.isTestControllingRefreshes) {
651         this.currentTest.addResult(
652           new testResult({
653             name: "test left refresh driver under test control",
654           })
655         );
656         winUtils.restoreNormalRefresh();
657       }
659       if (this.SimpleTest.isExpectingUncaughtException()) {
660         this.currentTest.addResult(
661           new testResult({
662             name:
663               "expectUncaughtException was called but no uncaught" +
664               " exception was detected!",
665             allowFailure: this.currentTest.allowFailure,
666           })
667         );
668       }
670       this.resolveFinishTestPromise();
671       this.resolveFinishTestPromise = null;
672       this.TestUtils.promiseTestFinished = null;
674       this.PromiseTestUtils.ensureDOMPromiseRejectionsProcessed();
675       this.PromiseTestUtils.assertNoUncaughtRejections();
676       this.PromiseTestUtils.assertNoMoreExpectedRejections();
677       await this.ensureVsyncDisabled();
679       Object.keys(window).forEach(function(prop) {
680         if (parseInt(prop) == prop) {
681           // This is a string which when parsed as an integer and then
682           // stringified gives the original string.  As in, this is in fact a
683           // string representation of an integer, so an index into
684           // window.frames.  Skip those.
685           return;
686         }
687         if (!this._globalProperties.includes(prop)) {
688           this._globalProperties.push(prop);
689           if (!this._globalPropertyWhitelist.includes(prop)) {
690             this.currentTest.addResult(
691               new testResult({
692                 name: "test left unexpected property on window: " + prop,
693                 allowFailure: this.currentTest.allowFailure,
694               })
695             );
696           }
697         }
698       }, this);
700       // eslint-disable-next-line no-undef
701       await new Promise(resolve => SpecialPowers.flushPrefEnv(resolve));
703       if (gConfig.cleanupCrashes) {
704         let gdir = Services.dirsvc.get("UAppData", Ci.nsIFile);
705         gdir.append("Crash Reports");
706         gdir.append("pending");
707         if (gdir.exists()) {
708           let entries = gdir.directoryEntries;
709           while (entries.hasMoreElements()) {
710             let entry = entries.nextFile;
711             if (entry.isFile()) {
712               let msg = "this test left a pending crash report; ";
713               try {
714                 entry.remove(false);
715                 msg += "deleted " + entry.path;
716               } catch (e) {
717                 msg += "could not delete " + entry.path;
718               }
719               this.structuredLogger.info(msg);
720             }
721           }
722         }
723       }
725       // Notify a long running test problem if it didn't end up in a timeout.
726       if (this.currentTest.unexpectedTimeouts && !this.currentTest.timedOut) {
727         this.currentTest.addResult(
728           new testResult({
729             name:
730               "This test exceeded the timeout threshold. It should be" +
731               " rewritten or split up. If that's not possible, use" +
732               " requestLongerTimeout(N), but only as a last resort.",
733           })
734         );
735       }
737       // If we're in a debug build, check assertion counts.  This code
738       // is similar to the code in TestRunner.testUnloaded in
739       // TestRunner.js used for all other types of mochitests.
740       let debugsvc = Cc["@mozilla.org/xpcom/debug;1"].getService(Ci.nsIDebug2);
741       if (debugsvc.isDebugBuild) {
742         let newAssertionCount = debugsvc.assertionCount;
743         let numAsserts = newAssertionCount - this.lastAssertionCount;
744         this.lastAssertionCount = newAssertionCount;
746         let max = testScope.__expectedMaxAsserts;
747         let min = testScope.__expectedMinAsserts;
748         if (numAsserts > max) {
749           // TEST-UNEXPECTED-FAIL
750           this.currentTest.addResult(
751             new testResult({
752               name:
753                 "Assertion count " +
754                 numAsserts +
755                 " is greater than expected range " +
756                 min +
757                 "-" +
758                 max +
759                 " assertions.",
760               pass: true, // TEMPORARILY TEST-KNOWN-FAIL
761               todo: true,
762               allowFailure: this.currentTest.allowFailure,
763             })
764           );
765         } else if (numAsserts < min) {
766           // TEST-UNEXPECTED-PASS
767           this.currentTest.addResult(
768             new testResult({
769               name:
770                 "Assertion count " +
771                 numAsserts +
772                 " is less than expected range " +
773                 min +
774                 "-" +
775                 max +
776                 " assertions.",
777               todo: true,
778               allowFailure: this.currentTest.allowFailure,
779             })
780           );
781         } else if (numAsserts > 0) {
782           // TEST-KNOWN-FAIL
783           this.currentTest.addResult(
784             new testResult({
785               name:
786                 "Assertion count " +
787                 numAsserts +
788                 " is within expected range " +
789                 min +
790                 "-" +
791                 max +
792                 " assertions.",
793               pass: true,
794               todo: true,
795               allowFailure: this.currentTest.allowFailure,
796             })
797           );
798         }
799       }
801       if (this.currentTest.allowFailure) {
802         if (this.currentTest.expectedAllowedFailureCount) {
803           this.currentTest.addResult(
804             new testResult({
805               name:
806                 "Expected " +
807                 this.currentTest.expectedAllowedFailureCount +
808                 " failures in this file, got " +
809                 this.currentTest.allowedFailureCount +
810                 ".",
811               pass:
812                 this.currentTest.expectedAllowedFailureCount ==
813                 this.currentTest.allowedFailureCount,
814             })
815           );
816         } else if (this.currentTest.allowedFailureCount == 0) {
817           this.currentTest.addResult(
818             new testResult({
819               name:
820                 "We expect at least one assertion to fail because this" +
821                 " test file is marked as fail-if in the manifest.",
822               todo: true,
823               knownFailure: this.currentTest.allowFailure,
824             })
825           );
826         }
827       }
829       // Dump memory stats for main thread.
830       if (
831         Services.appinfo.processType == Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT
832       ) {
833         this.MemoryStats.dump(
834           this.currentTestIndex,
835           this.currentTest.path,
836           gConfig.dumpOutputDirectory,
837           gConfig.dumpAboutMemoryAfterTest,
838           gConfig.dumpDMDAfterTest
839         );
840       }
842       // Note the test run time
843       let name = this.currentTest.path;
844       name = name.slice(name.lastIndexOf("/") + 1);
845       ChromeUtils.addProfilerMarker(
846         "browser-test",
847         { category: "Test", startTime: this.lastStartTimestamp },
848         name
849       );
851       // See if we should upload a profile of a failing test.
852       if (this.currentTest.failCount) {
853         // If MOZ_PROFILER_SHUTDOWN is set, the profiler got started from --profiler
854         // and a profile will be shown even if there's no test failure.
855         if (
856           Services.env.exists("MOZ_UPLOAD_DIR") &&
857           !Services.env.exists("MOZ_PROFILER_SHUTDOWN") &&
858           Services.profiler.IsActive()
859         ) {
860           let filename = `profile_${name}.json`;
861           let path = Services.env.get("MOZ_UPLOAD_DIR");
862           let profilePath = PathUtils.join(path, filename);
863           try {
864             let profileData = await Services.profiler.getProfileDataAsGzippedArrayBuffer();
865             await IOUtils.write(profilePath, new Uint8Array(profileData));
866             this.currentTest.addResult(
867               new testResult({
868                 name:
869                   "Found unexpected failures during the test; profile uploaded in " +
870                   filename,
871               })
872             );
873           } catch (e) {
874             // If the profile is large, we may encounter out of memory errors.
875             this.currentTest.addResult(
876               new testResult({
877                 name:
878                   "Found unexpected failures during the test; failed to upload profile: " +
879                   e,
880               })
881             );
882           }
883         }
884       }
886       let time = Date.now() - this.lastStartTime;
888       this.structuredLogger.testEnd(
889         this.currentTest.path,
890         "OK",
891         undefined,
892         "finished in " + time + "ms"
893       );
894       this.currentTest.setDuration(time);
896       if (this.runUntilFailure && this.currentTest.failCount > 0) {
897         this.haltTests();
898       }
900       // Restore original SimpleTest methods to avoid leaks.
901       SIMPLETEST_OVERRIDES.forEach(m => {
902         this.SimpleTest[m] = this.SimpleTestOriginal[m];
903       });
905       this.ContentTask.setTestScope(null);
906       testScope.destroy();
907       this.currentTest.scope = null;
908     }
910     // Check the window state for the current test before moving to the next one.
911     // This also causes us to check before starting any tests, since nextTest()
912     // is invoked to start the tests.
913     this.waitForWindowsState(() => {
914       if (this.done) {
915         if (this._coverageCollector) {
916           this._coverageCollector.finalize();
917         } else if (
918           !AppConstants.RELEASE_OR_BETA &&
919           !AppConstants.DEBUG &&
920           !AppConstants.MOZ_CODE_COVERAGE &&
921           !AppConstants.ASAN &&
922           !AppConstants.TSAN
923         ) {
924           this.finish();
925           return;
926         }
928         // Uninitialize a few things explicitly so that they can clean up
929         // frames and browser intentionally kept alive until shutdown to
930         // eliminate false positives.
931         if (gConfig.testRoot == "browser") {
932           // Skip if SeaMonkey
933           if (AppConstants.MOZ_APP_NAME != "seamonkey") {
934             // Replace the document currently loaded in the browser's sidebar.
935             // This will prevent false positives for tests that were the last
936             // to touch the sidebar. They will thus not be blamed for leaking
937             // a document.
938             let sidebar = document.getElementById("sidebar");
939             if (sidebar) {
940               sidebar.setAttribute("src", "data:text/html;charset=utf-8,");
941               sidebar.docShell.createAboutBlankContentViewer(null, null);
942               sidebar.setAttribute("src", "about:blank");
943             }
944           }
946           // Destroy BackgroundPageThumbs resources.
947           let { BackgroundPageThumbs } = ChromeUtils.import(
948             "resource://gre/modules/BackgroundPageThumbs.jsm"
949           );
950           BackgroundPageThumbs._destroy();
952           if (window.gBrowser) {
953             NewTabPagePreloading.removePreloadedBrowser(window);
954           }
955         }
957         // Schedule GC and CC runs before finishing in order to detect
958         // DOM windows leaked by our tests or the tested code. Note that we
959         // use a shrinking GC so that the JS engine will discard JIT code and
960         // JIT caches more aggressively.
962         let shutdownCleanup = aCallback => {
963           Cu.schedulePreciseShrinkingGC(() => {
964             // Run the GC and CC a few times to make sure that as much
965             // as possible is freed.
966             let numCycles = 3;
967             for (let i = 0; i < numCycles; i++) {
968               Cu.forceGC();
969               Cu.forceCC();
970             }
971             aCallback();
972           });
973         };
975         let { AsyncShutdown } = ChromeUtils.importESModule(
976           "resource://gre/modules/AsyncShutdown.sys.mjs"
977         );
979         let barrier = new AsyncShutdown.Barrier(
980           "ShutdownLeaks: Wait for cleanup to be finished before checking for leaks"
981         );
982         Services.obs.notifyObservers(
983           { wrappedJSObject: barrier },
984           "shutdown-leaks-before-check"
985         );
987         barrier.client.addBlocker(
988           "ShutdownLeaks: Wait for tabs to finish closing",
989           TabDestroyObserver.wait()
990         );
992         barrier.wait().then(() => {
993           // Simulate memory pressure so that we're forced to free more resources
994           // and thus get rid of more false leaks like already terminated workers.
995           Services.obs.notifyObservers(
996             null,
997             "memory-pressure",
998             "heap-minimize"
999           );
1001           Services.ppmm.broadcastAsyncMessage("browser-test:collect-request");
1003           shutdownCleanup(() => {
1004             setTimeout(() => {
1005               shutdownCleanup(() => {
1006                 this.finish();
1007               });
1008             }, 1000);
1009           });
1010         });
1012         return;
1013       }
1015       if (this.repeat > 0) {
1016         --this.repeat;
1017         if (this.currentTestIndex < 0) {
1018           this.currentTestIndex = 0;
1019         }
1020         this.execTest();
1021       } else {
1022         this.currentTestIndex++;
1023         if (gConfig.repeat) {
1024           this.repeat = gConfig.repeat;
1025         }
1026         this.execTest();
1027       }
1028     });
1029   },
1031   async handleTask(task, currentTest, PromiseTestUtils, isSetup = false) {
1032     let currentScope = currentTest.scope;
1033     let desc = isSetup ? "setup" : "test";
1034     currentScope.SimpleTest.info(`Entering ${desc} ${task.name}`);
1035     let startTimestamp = performance.now();
1036     try {
1037       let result = await task();
1038       if (isGenerator(result)) {
1039         currentScope.SimpleTest.ok(false, "Task returned a generator");
1040       }
1041     } catch (ex) {
1042       if (currentTest.timedOut) {
1043         currentTest.addResult(
1044           new testResult({
1045             name: `Uncaught exception received from previously timed out ${desc}`,
1046             pass: false,
1047             ex,
1048             stack: typeof ex == "object" && "stack" in ex ? ex.stack : null,
1049             allowFailure: currentTest.allowFailure,
1050           })
1051         );
1052         // We timed out, so we've already cleaned up for this test, just get outta here.
1053         return;
1054       }
1055       currentTest.addResult(
1056         new testResult({
1057           name: `Uncaught exception in ${desc}`,
1058           pass: currentScope.SimpleTest.isExpectingUncaughtException(),
1059           ex,
1060           stack: typeof ex == "object" && "stack" in ex ? ex.stack : null,
1061           allowFailure: currentTest.allowFailure,
1062         })
1063       );
1064     }
1065     PromiseTestUtils.assertNoUncaughtRejections();
1066     ChromeUtils.addProfilerMarker(
1067       isSetup ? "setup-task" : "task",
1068       { category: "Test", startTime: startTimestamp },
1069       task.name.replace(/^bound /, "") || undefined
1070     );
1071     currentScope.SimpleTest.info(`Leaving ${desc} ${task.name}`);
1072   },
1074   async _runTaskBasedTest(currentTest) {
1075     let currentScope = currentTest.scope;
1077     // First run all the setups:
1078     let setupFn;
1079     while ((setupFn = currentScope.__setups.shift())) {
1080       await this.handleTask(
1081         setupFn,
1082         currentTest,
1083         this.PromiseTestUtils,
1084         true /* is setup task */
1085       );
1086     }
1088     // Allow for a task to be skipped; we need only use the structured logger
1089     // for this, whilst deactivating log buffering to ensure that messages
1090     // are always printed to stdout.
1091     let skipTask = task => {
1092       let logger = this.structuredLogger;
1093       logger.deactivateBuffering();
1094       logger.testStatus(this.currentTest.path, task.name, "SKIP");
1095       logger.warning("Skipping test " + task.name);
1096       logger.activateBuffering();
1097     };
1099     let task;
1100     while ((task = currentScope.__tasks.shift())) {
1101       if (
1102         task.__skipMe ||
1103         (currentScope.__runOnlyThisTask &&
1104           task != currentScope.__runOnlyThisTask)
1105       ) {
1106         skipTask(task);
1107         continue;
1108       }
1109       await this.handleTask(task, currentTest, this.PromiseTestUtils);
1110     }
1111     currentScope.finish();
1112   },
1114   execTest: function Tester_execTest() {
1115     this.structuredLogger.testStart(this.currentTest.path);
1117     this.SimpleTest.reset();
1118     // Reset accessibility environment.
1119     this.AccessibilityUtils.reset(this.a11y_checks);
1121     // Load the tests into a testscope
1122     let currentScope = (this.currentTest.scope = new testScope(
1123       this,
1124       this.currentTest,
1125       this.currentTest.expected
1126     ));
1127     let currentTest = this.currentTest;
1129     // HTTPS-First (Bug 1704453) TODO: in case a test is annoated
1130     // with https_first_disabled then we explicitly flip the pref
1131     // dom.security.https_first to false for the duration of the test.
1132     if (currentTest.https_first_disabled) {
1133       window.SpecialPowers.pushPrefEnv({
1134         set: [["dom.security.https_first", false]],
1135       });
1136     }
1138     if (currentTest.allow_xul_xbl) {
1139       window.SpecialPowers.pushPermissions([
1140         { type: "allowXULXBL", allow: true, context: "http://mochi.test:8888" },
1141         { type: "allowXULXBL", allow: true, context: "http://example.org" },
1142       ]);
1143     }
1145     // Import utils in the test scope.
1146     let { scope } = this.currentTest;
1147     scope.EventUtils = this.EventUtils;
1148     scope.AccessibilityUtils = this.AccessibilityUtils;
1149     scope.SimpleTest = this.SimpleTest;
1150     scope.gTestPath = this.currentTest.path;
1151     scope.ContentTask = this.ContentTask;
1152     scope.BrowserTestUtils = this.BrowserTestUtils;
1153     scope.TestUtils = this.TestUtils;
1154     scope.ExtensionTestUtils = this.ExtensionTestUtils;
1155     // Pass a custom report function for mochitest style reporting.
1156     scope.Assert = new this.Assert(function(err, message, stack) {
1157       currentTest.addResult(
1158         new testResult(
1159           err
1160             ? {
1161                 name: err.message,
1162                 ex: err.stack,
1163                 stack: err.stack,
1164                 allowFailure: currentTest.allowFailure,
1165               }
1166             : {
1167                 name: message,
1168                 pass: true,
1169                 stack,
1170                 allowFailure: currentTest.allowFailure,
1171               }
1172         )
1173       );
1174     }, true);
1176     this.ContentTask.setTestScope(currentScope);
1178     // Allow Assert.sys.mjs methods to be tacked to the current scope.
1179     scope.export_assertions = function() {
1180       for (let func in this.Assert) {
1181         this[func] = this.Assert[func].bind(this.Assert);
1182       }
1183     };
1185     // Override SimpleTest methods with ours.
1186     SIMPLETEST_OVERRIDES.forEach(function(m) {
1187       this.SimpleTest[m] = this[m];
1188     }, scope);
1190     // load the tools to work with chrome .jar and remote
1191     try {
1192       this._scriptLoader.loadSubScript(
1193         "chrome://mochikit/content/chrome-harness.js",
1194         scope
1195       );
1196     } catch (ex) {
1197       /* no chrome-harness tools */
1198     }
1200     // Import head.js script if it exists.
1201     var currentTestDirPath = this.currentTest.path.substr(
1202       0,
1203       this.currentTest.path.lastIndexOf("/")
1204     );
1205     var headPath = currentTestDirPath + "/head.js";
1206     try {
1207       this._scriptLoader.loadSubScript(headPath, scope);
1208     } catch (ex) {
1209       // Bug 755558 - Ignore loadSubScript errors due to a missing head.js.
1210       const isImportError = /^Error opening input stream/.test(ex.toString());
1212       // Bug 1503169 - head.js may call loadSubScript, and generate similar errors.
1213       // Only swallow errors that are strictly related to loading head.js.
1214       const containsHeadPath = ex.toString().includes(headPath);
1216       if (!isImportError || !containsHeadPath) {
1217         this.currentTest.addResult(
1218           new testResult({
1219             name: "head.js import threw an exception",
1220             ex,
1221           })
1222         );
1223       }
1224     }
1226     // Import the test script.
1227     try {
1228       this.lastStartTimestamp = performance.now();
1229       this.TestUtils.promiseTestFinished = new Promise(resolve => {
1230         this.resolveFinishTestPromise = resolve;
1231       });
1232       this._scriptLoader.loadSubScript(this.currentTest.path, scope);
1233       // Run the test
1234       this.lastStartTime = Date.now();
1235       if (this.currentTest.scope.__tasks) {
1236         // This test consists of tasks, added via the `add_task()` API.
1237         if ("test" in this.currentTest.scope) {
1238           throw new Error(
1239             "Cannot run both a add_task test and a normal test at the same time."
1240           );
1241         }
1242         // Spin off the async work without waiting for it to complete.
1243         // It'll call finish() when it's done.
1244         this._runTaskBasedTest(this.currentTest);
1245       } else if (typeof scope.test == "function") {
1246         scope.test();
1247       } else {
1248         throw new Error(
1249           "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."
1250         );
1251       }
1252     } catch (ex) {
1253       if (!this.SimpleTest.isIgnoringAllUncaughtExceptions()) {
1254         this.currentTest.addResult(
1255           new testResult({
1256             name: "Exception thrown",
1257             pass: this.SimpleTest.isExpectingUncaughtException(),
1258             ex,
1259             allowFailure: this.currentTest.allowFailure,
1260           })
1261         );
1262         this.SimpleTest.expectUncaughtException(false);
1263       } else {
1264         this.currentTest.addResult(new testMessage("Exception thrown: " + ex));
1265       }
1266       this.currentTest.scope.finish();
1267     }
1269     // If the test ran synchronously, move to the next test, otherwise the test
1270     // will trigger the next test when it is done.
1271     if (this.currentTest.scope.__done) {
1272       this.nextTest();
1273     } else {
1274       var self = this;
1275       var timeoutExpires = Date.now() + gTimeoutSeconds * 1000;
1276       var waitUntilAtLeast = timeoutExpires - 1000;
1277       this.currentTest.scope.__waitTimer = this.SimpleTest._originalSetTimeout.apply(
1278         window,
1279         [
1280           function timeoutFn() {
1281             // We sometimes get woken up long before the gTimeoutSeconds
1282             // have elapsed (when running in chaos mode for example). This
1283             // code ensures that we don't wrongly time out in that case.
1284             if (Date.now() < waitUntilAtLeast) {
1285               self.currentTest.scope.__waitTimer = setTimeout(
1286                 timeoutFn,
1287                 timeoutExpires - Date.now()
1288               );
1289               return;
1290             }
1292             if (--self.currentTest.scope.__timeoutFactor > 0) {
1293               // We were asked to wait a bit longer.
1294               self.currentTest.scope.info(
1295                 "Longer timeout required, waiting longer...  Remaining timeouts: " +
1296                   self.currentTest.scope.__timeoutFactor
1297               );
1298               self.currentTest.scope.__waitTimer = setTimeout(
1299                 timeoutFn,
1300                 gTimeoutSeconds * 1000
1301               );
1302               return;
1303             }
1305             // If the test is taking longer than expected, but it's not hanging,
1306             // mark the fact, but let the test continue.  At the end of the test,
1307             // if it didn't timeout, we will notify the problem through an error.
1308             // To figure whether it's an actual hang, compare the time of the last
1309             // result or message to half of the timeout time.
1310             // Though, to protect against infinite loops, limit the number of times
1311             // we allow the test to proceed.
1312             const MAX_UNEXPECTED_TIMEOUTS = 10;
1313             if (
1314               Date.now() - self.currentTest.lastOutputTime <
1315                 (gTimeoutSeconds / 2) * 1000 &&
1316               ++self.currentTest.unexpectedTimeouts <= MAX_UNEXPECTED_TIMEOUTS
1317             ) {
1318               self.currentTest.scope.__waitTimer = setTimeout(
1319                 timeoutFn,
1320                 gTimeoutSeconds * 1000
1321               );
1322               return;
1323             }
1325             let knownFailure = false;
1326             if (gConfig.timeoutAsPass) {
1327               knownFailure = true;
1328             }
1329             self.currentTest.addResult(
1330               new testResult({
1331                 name: "Test timed out",
1332                 allowFailure: knownFailure,
1333               })
1334             );
1335             self.currentTest.timedOut = true;
1336             self.currentTest.scope.__waitTimer = null;
1337             self.nextTest();
1338           },
1339           gTimeoutSeconds * 1000,
1340         ]
1341       );
1342     }
1343   },
1345   QueryInterface: ChromeUtils.generateQI(["nsIConsoleListener"]),
1349  * Represents the result of one test assertion. This is described with a string
1350  * in traditional logging, and has a "status" and "expected" property used in
1351  * structured logging. Normally, results are mapped as follows:
1353  *   pass:    todo:    Added to:    Described as:           Status:  Expected:
1354  *     true     false    passCount    TEST-PASS               PASS     PASS
1355  *     true     true     todoCount    TEST-KNOWN-FAIL         FAIL     FAIL
1356  *     false    false    failCount    TEST-UNEXPECTED-FAIL    FAIL     PASS
1357  *     false    true     failCount    TEST-UNEXPECTED-PASS    PASS     FAIL
1359  * The "allowFailure" argument indicates that this is one of the assertions that
1360  * should be allowed to fail, for example because "fail-if" is true for the
1361  * current test file in the manifest. In this case, results are mapped this way:
1363  *   pass:    todo:    Added to:    Described as:           Status:  Expected:
1364  *     true     false    passCount    TEST-PASS               PASS     PASS
1365  *     true     true     todoCount    TEST-KNOWN-FAIL         FAIL     FAIL
1366  *     false    false    todoCount    TEST-KNOWN-FAIL         FAIL     FAIL
1367  *     false    true     todoCount    TEST-KNOWN-FAIL         FAIL     FAIL
1368  */
1369 function testResult({ name, pass, todo, ex, stack, allowFailure }) {
1370   this.info = false;
1371   this.name = name;
1372   this.msg = "";
1374   if (allowFailure && !pass) {
1375     this.allowedFailure = true;
1376     this.pass = true;
1377     this.todo = false;
1378   } else if (allowFailure && pass) {
1379     this.pass = true;
1380     this.todo = false;
1381   } else {
1382     this.pass = !!pass;
1383     this.todo = todo;
1384   }
1386   this.expected = this.todo ? "FAIL" : "PASS";
1388   if (this.pass) {
1389     this.status = this.expected;
1390     return;
1391   }
1393   this.status = this.todo ? "PASS" : "FAIL";
1395   if (ex) {
1396     if (typeof ex == "object" && "fileName" in ex) {
1397       // we have an exception - print filename and linenumber information
1398       this.msg += "at " + ex.fileName + ":" + ex.lineNumber + " - ";
1399     }
1400     this.msg += String(ex);
1401   }
1403   if (stack) {
1404     this.msg += "\nStack trace:\n";
1405     let normalized;
1406     if (stack instanceof Ci.nsIStackFrame) {
1407       let frames = [];
1408       for (
1409         let frame = stack;
1410         frame;
1411         frame = frame.asyncCaller || frame.caller
1412       ) {
1413         let msg = `${frame.filename}:${frame.name}:${frame.lineNumber}`;
1414         frames.push(frame.asyncCause ? `${frame.asyncCause}*${msg}` : msg);
1415       }
1416       normalized = frames.join("\n");
1417     } else {
1418       normalized = "" + stack;
1419     }
1420     this.msg += normalized;
1421   }
1423   if (gConfig.debugOnFailure) {
1424     // You've hit this line because you requested to break into the
1425     // debugger upon a testcase failure on your test run.
1426     // eslint-disable-next-line no-debugger
1427     debugger;
1428   }
1431 function testMessage(msg) {
1432   this.msg = msg || "";
1433   this.info = true;
1436 // Need to be careful adding properties to this object, since its properties
1437 // cannot conflict with global variables used in tests.
1438 function testScope(aTester, aTest, expected) {
1439   this.__tester = aTester;
1441   aTest.allowFailure = expected == "fail";
1443   var self = this;
1444   this.ok = function test_ok(condition, name) {
1445     if (arguments.length > 2) {
1446       const ex = "Too many arguments passed to ok(condition, name)`.";
1447       self.record(false, name, ex);
1448     } else {
1449       self.record(condition, name);
1450     }
1451   };
1452   this.record = function test_record(condition, name, ex, stack, expected) {
1453     if (expected == "fail") {
1454       aTest.addResult(
1455         new testResult({
1456           name,
1457           pass: !condition,
1458           todo: true,
1459           ex,
1460           stack: stack || Components.stack.caller,
1461           allowFailure: aTest.allowFailure,
1462         })
1463       );
1464     } else {
1465       aTest.addResult(
1466         new testResult({
1467           name,
1468           pass: condition,
1469           ex,
1470           stack: stack || Components.stack.caller,
1471           allowFailure: aTest.allowFailure,
1472         })
1473       );
1474     }
1475   };
1476   this.is = function test_is(a, b, name) {
1477     self.record(
1478       Object.is(a, b),
1479       name,
1480       `Got ${self.repr(a)}, expected ${self.repr(b)}`,
1481       false,
1482       Components.stack.caller
1483     );
1484   };
1485   this.isfuzzy = function test_isfuzzy(a, b, epsilon, name) {
1486     self.record(
1487       a >= b - epsilon && a <= b + epsilon,
1488       name,
1489       `Got ${self.repr(a)}, expected ${self.repr(b)} epsilon: +/- ${self.repr(
1490         epsilon
1491       )}`,
1492       false,
1493       Components.stack.caller
1494     );
1495   };
1496   this.isnot = function test_isnot(a, b, name) {
1497     self.record(
1498       !Object.is(a, b),
1499       name,
1500       `Didn't expect ${self.repr(a)}, but got it`,
1501       false,
1502       Components.stack.caller
1503     );
1504   };
1505   this.todo = function test_todo(condition, name, ex, stack) {
1506     aTest.addResult(
1507       new testResult({
1508         name,
1509         pass: !condition,
1510         todo: true,
1511         ex,
1512         stack: stack || Components.stack.caller,
1513         allowFailure: aTest.allowFailure,
1514       })
1515     );
1516   };
1517   this.todo_is = function test_todo_is(a, b, name) {
1518     self.todo(
1519       Object.is(a, b),
1520       name,
1521       `Got ${self.repr(a)}, expected ${self.repr(b)}`,
1522       Components.stack.caller
1523     );
1524   };
1525   this.todo_isnot = function test_todo_isnot(a, b, name) {
1526     self.todo(
1527       !Object.is(a, b),
1528       name,
1529       `Didn't expect ${self.repr(a)}, but got it`,
1530       Components.stack.caller
1531     );
1532   };
1533   this.info = function test_info(name) {
1534     aTest.addResult(new testMessage(name));
1535   };
1536   this.repr = function repr(o) {
1537     if (typeof o == "undefined") {
1538       return "undefined";
1539     } else if (o === null) {
1540       return "null";
1541     }
1542     try {
1543       if (typeof o.__repr__ == "function") {
1544         return o.__repr__();
1545       } else if (typeof o.repr == "function" && o.repr != repr) {
1546         return o.repr();
1547       }
1548     } catch (e) {}
1549     try {
1550       if (
1551         typeof o.NAME == "string" &&
1552         (o.toString == Function.prototype.toString ||
1553           o.toString == Object.prototype.toString)
1554       ) {
1555         return o.NAME;
1556       }
1557     } catch (e) {}
1558     var ostring;
1559     try {
1560       if (Object.is(o, +0)) {
1561         ostring = "+0";
1562       } else if (Object.is(o, -0)) {
1563         ostring = "-0";
1564       } else if (typeof o === "string") {
1565         ostring = JSON.stringify(o);
1566       } else if (Array.isArray(o)) {
1567         ostring = "[" + o.map(val => repr(val)).join(", ") + "]";
1568       } else {
1569         ostring = String(o);
1570       }
1571     } catch (e) {
1572       return `[${Object.prototype.toString.call(o)}]`;
1573     }
1574     if (typeof o == "function") {
1575       ostring = ostring.replace(/\) \{[^]*/, ") { ... }");
1576     }
1577     return ostring;
1578   };
1580   this.executeSoon = function test_executeSoon(func) {
1581     Services.tm.dispatchToMainThread({
1582       run() {
1583         func();
1584       },
1585     });
1586   };
1588   this.waitForExplicitFinish = function test_waitForExplicitFinish() {
1589     self.__done = false;
1590   };
1592   this.waitForFocus = function test_waitForFocus(
1593     callback,
1594     targetWindow,
1595     expectBlankPage
1596   ) {
1597     self.SimpleTest.waitForFocus(callback, targetWindow, expectBlankPage);
1598   };
1600   this.waitForClipboard = function test_waitForClipboard(
1601     expected,
1602     setup,
1603     success,
1604     failure,
1605     flavor
1606   ) {
1607     self.SimpleTest.waitForClipboard(expected, setup, success, failure, flavor);
1608   };
1610   this.registerCleanupFunction = function test_registerCleanupFunction(
1611     aFunction
1612   ) {
1613     self.__cleanupFunctions.push(aFunction);
1614   };
1616   this.requestLongerTimeout = function test_requestLongerTimeout(aFactor) {
1617     self.__timeoutFactor = aFactor;
1618   };
1620   this.copyToProfile = function test_copyToProfile(filename) {
1621     self.SimpleTest.copyToProfile(filename);
1622   };
1624   this.expectUncaughtException = function test_expectUncaughtException(
1625     aExpecting
1626   ) {
1627     self.SimpleTest.expectUncaughtException(aExpecting);
1628   };
1630   this.ignoreAllUncaughtExceptions = function test_ignoreAllUncaughtExceptions(
1631     aIgnoring
1632   ) {
1633     self.SimpleTest.ignoreAllUncaughtExceptions(aIgnoring);
1634   };
1636   this.expectAssertions = function test_expectAssertions(aMin, aMax) {
1637     let min = aMin;
1638     let max = aMax;
1639     if (typeof max == "undefined") {
1640       max = min;
1641     }
1642     if (
1643       typeof min != "number" ||
1644       typeof max != "number" ||
1645       min < 0 ||
1646       max < min
1647     ) {
1648       throw new Error("bad parameter to expectAssertions");
1649     }
1650     self.__expectedMinAsserts = min;
1651     self.__expectedMaxAsserts = max;
1652   };
1654   this.setExpectedFailuresForSelfTest = function test_setExpectedFailuresForSelfTest(
1655     expectedAllowedFailureCount
1656   ) {
1657     aTest.allowFailure = true;
1658     aTest.expectedAllowedFailureCount = expectedAllowedFailureCount;
1659   };
1661   this.finish = function test_finish() {
1662     self.__done = true;
1663     if (self.__waitTimer) {
1664       self.executeSoon(function() {
1665         if (self.__done && self.__waitTimer) {
1666           clearTimeout(self.__waitTimer);
1667           self.__waitTimer = null;
1668           self.__tester.nextTest();
1669         }
1670       });
1671     }
1672   };
1674   this.requestCompleteLog = function test_requestCompleteLog() {
1675     self.__tester.structuredLogger.deactivateBuffering();
1676     self.registerCleanupFunction(function() {
1677       self.__tester.structuredLogger.activateBuffering();
1678     });
1679   };
1681   return this;
1684 function decorateTaskFn(fn) {
1685   fn = fn.bind(this);
1686   fn.skip = (val = true) => (fn.__skipMe = val);
1687   fn.only = () => (this.__runOnlyThisTask = fn);
1688   return fn;
1691 testScope.prototype = {
1692   __done: true,
1693   __tasks: null,
1694   __setups: [],
1695   __runOnlyThisTask: null,
1696   __waitTimer: null,
1697   __cleanupFunctions: [],
1698   __timeoutFactor: 1,
1699   __expectedMinAsserts: 0,
1700   __expectedMaxAsserts: 0,
1702   EventUtils: {},
1703   AccessibilityUtils: {},
1704   SimpleTest: {},
1705   ContentTask: null,
1706   BrowserTestUtils: null,
1707   TestUtils: null,
1708   ExtensionTestUtils: null,
1709   Assert: null,
1711   /**
1712    * Add a function which returns a promise (usually an async function)
1713    * as a test task.
1714    *
1715    * The task ends when the promise returned by the function resolves or
1716    * rejects. If the test function throws, or the promise it returns
1717    * rejects, the test is reported as a failure. Execution continues
1718    * with the next test function.
1719    *
1720    * Example usage:
1721    *
1722    * add_task(async function test() {
1723    *   let result = await Promise.resolve(true);
1724    *
1725    *   ok(result);
1726    *
1727    *   let secondary = await someFunctionThatReturnsAPromise(result);
1728    *   is(secondary, "expected value");
1729    * });
1730    *
1731    * add_task(async function test_early_return() {
1732    *   let result = await somethingThatReturnsAPromise();
1733    *
1734    *   if (!result) {
1735    *     // Test is ended immediately, with success.
1736    *     return;
1737    *   }
1738    *
1739    *   is(result, "foo");
1740    * });
1741    */
1742   add_task(aFunction) {
1743     if (!this.__tasks) {
1744       this.waitForExplicitFinish();
1745       this.__tasks = [];
1746     }
1747     let bound = decorateTaskFn.call(this, aFunction);
1748     this.__tasks.push(bound);
1749     return bound;
1750   },
1752   add_setup(aFunction) {
1753     if (!this.__setups.length) {
1754       this.waitForExplicitFinish();
1755     }
1756     let bound = aFunction.bind(this);
1757     this.__setups.push(bound);
1758     return bound;
1759   },
1761   destroy: function test_destroy() {
1762     for (let prop in this) {
1763       delete this[prop];
1764     }
1765   },