Bug 1815682 - Improve the output of uncaught exceptions. r=gbrown
[gecko.git] / testing / mochitest / browser-test.js
blob335ed853e125609f89230100d4544159274b0960
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} ${task.name}`,
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} ${task.name}`,
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     // Ensure we are not idle at the beginning of the test. If we don't do this,
1201     // the browser may behave differently if the previous tests ran long.
1202     // eg. the session store behavior changes 3 minutes after the last user event.
1203     Cc["@mozilla.org/widget/useridleservice;1"]
1204       .getService(Ci.nsIUserIdleServiceInternal)
1205       .resetIdleTimeOut(0);
1207     // Import head.js script if it exists.
1208     var currentTestDirPath = this.currentTest.path.substr(
1209       0,
1210       this.currentTest.path.lastIndexOf("/")
1211     );
1212     var headPath = currentTestDirPath + "/head.js";
1213     try {
1214       this._scriptLoader.loadSubScript(headPath, scope);
1215     } catch (ex) {
1216       // Bug 755558 - Ignore loadSubScript errors due to a missing head.js.
1217       const isImportError = /^Error opening input stream/.test(ex.toString());
1219       // Bug 1503169 - head.js may call loadSubScript, and generate similar errors.
1220       // Only swallow errors that are strictly related to loading head.js.
1221       const containsHeadPath = ex.toString().includes(headPath);
1223       if (!isImportError || !containsHeadPath) {
1224         this.currentTest.addResult(
1225           new testResult({
1226             name: "head.js import threw an exception",
1227             ex,
1228           })
1229         );
1230       }
1231     }
1233     // Import the test script.
1234     try {
1235       this.lastStartTimestamp = performance.now();
1236       this.TestUtils.promiseTestFinished = new Promise(resolve => {
1237         this.resolveFinishTestPromise = resolve;
1238       });
1239       this._scriptLoader.loadSubScript(this.currentTest.path, scope);
1240       // Run the test
1241       this.lastStartTime = Date.now();
1242       if (this.currentTest.scope.__tasks) {
1243         // This test consists of tasks, added via the `add_task()` API.
1244         if ("test" in this.currentTest.scope) {
1245           throw new Error(
1246             "Cannot run both a add_task test and a normal test at the same time."
1247           );
1248         }
1249         // Spin off the async work without waiting for it to complete.
1250         // It'll call finish() when it's done.
1251         this._runTaskBasedTest(this.currentTest);
1252       } else if (typeof scope.test == "function") {
1253         scope.test();
1254       } else {
1255         throw new Error(
1256           "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."
1257         );
1258       }
1259     } catch (ex) {
1260       if (!this.SimpleTest.isIgnoringAllUncaughtExceptions()) {
1261         this.currentTest.addResult(
1262           new testResult({
1263             name: "Exception thrown",
1264             pass: this.SimpleTest.isExpectingUncaughtException(),
1265             ex,
1266             allowFailure: this.currentTest.allowFailure,
1267           })
1268         );
1269         this.SimpleTest.expectUncaughtException(false);
1270       } else {
1271         this.currentTest.addResult(new testMessage("Exception thrown: " + ex));
1272       }
1273       this.currentTest.scope.finish();
1274     }
1276     // If the test ran synchronously, move to the next test, otherwise the test
1277     // will trigger the next test when it is done.
1278     if (this.currentTest.scope.__done) {
1279       this.nextTest();
1280     } else {
1281       var self = this;
1282       var timeoutExpires = Date.now() + gTimeoutSeconds * 1000;
1283       var waitUntilAtLeast = timeoutExpires - 1000;
1284       this.currentTest.scope.__waitTimer = this.SimpleTest._originalSetTimeout.apply(
1285         window,
1286         [
1287           function timeoutFn() {
1288             // We sometimes get woken up long before the gTimeoutSeconds
1289             // have elapsed (when running in chaos mode for example). This
1290             // code ensures that we don't wrongly time out in that case.
1291             if (Date.now() < waitUntilAtLeast) {
1292               self.currentTest.scope.__waitTimer = setTimeout(
1293                 timeoutFn,
1294                 timeoutExpires - Date.now()
1295               );
1296               return;
1297             }
1299             if (--self.currentTest.scope.__timeoutFactor > 0) {
1300               // We were asked to wait a bit longer.
1301               self.currentTest.scope.info(
1302                 "Longer timeout required, waiting longer...  Remaining timeouts: " +
1303                   self.currentTest.scope.__timeoutFactor
1304               );
1305               self.currentTest.scope.__waitTimer = setTimeout(
1306                 timeoutFn,
1307                 gTimeoutSeconds * 1000
1308               );
1309               return;
1310             }
1312             // If the test is taking longer than expected, but it's not hanging,
1313             // mark the fact, but let the test continue.  At the end of the test,
1314             // if it didn't timeout, we will notify the problem through an error.
1315             // To figure whether it's an actual hang, compare the time of the last
1316             // result or message to half of the timeout time.
1317             // Though, to protect against infinite loops, limit the number of times
1318             // we allow the test to proceed.
1319             const MAX_UNEXPECTED_TIMEOUTS = 10;
1320             if (
1321               Date.now() - self.currentTest.lastOutputTime <
1322                 (gTimeoutSeconds / 2) * 1000 &&
1323               ++self.currentTest.unexpectedTimeouts <= MAX_UNEXPECTED_TIMEOUTS
1324             ) {
1325               self.currentTest.scope.__waitTimer = setTimeout(
1326                 timeoutFn,
1327                 gTimeoutSeconds * 1000
1328               );
1329               return;
1330             }
1332             let knownFailure = false;
1333             if (gConfig.timeoutAsPass) {
1334               knownFailure = true;
1335             }
1336             self.currentTest.addResult(
1337               new testResult({
1338                 name: "Test timed out",
1339                 allowFailure: knownFailure,
1340               })
1341             );
1342             self.currentTest.timedOut = true;
1343             self.currentTest.scope.__waitTimer = null;
1344             self.nextTest();
1345           },
1346           gTimeoutSeconds * 1000,
1347         ]
1348       );
1349     }
1350   },
1352   QueryInterface: ChromeUtils.generateQI(["nsIConsoleListener"]),
1356  * Represents the result of one test assertion. This is described with a string
1357  * in traditional logging, and has a "status" and "expected" property used in
1358  * structured logging. Normally, results are mapped as follows:
1360  *   pass:    todo:    Added to:    Described as:           Status:  Expected:
1361  *     true     false    passCount    TEST-PASS               PASS     PASS
1362  *     true     true     todoCount    TEST-KNOWN-FAIL         FAIL     FAIL
1363  *     false    false    failCount    TEST-UNEXPECTED-FAIL    FAIL     PASS
1364  *     false    true     failCount    TEST-UNEXPECTED-PASS    PASS     FAIL
1366  * The "allowFailure" argument indicates that this is one of the assertions that
1367  * should be allowed to fail, for example because "fail-if" is true for the
1368  * current test file in the manifest. In this case, results are mapped this way:
1370  *   pass:    todo:    Added to:    Described as:           Status:  Expected:
1371  *     true     false    passCount    TEST-PASS               PASS     PASS
1372  *     true     true     todoCount    TEST-KNOWN-FAIL         FAIL     FAIL
1373  *     false    false    todoCount    TEST-KNOWN-FAIL         FAIL     FAIL
1374  *     false    true     todoCount    TEST-KNOWN-FAIL         FAIL     FAIL
1375  */
1376 function testResult({ name, pass, todo, ex, stack, allowFailure }) {
1377   this.info = false;
1378   this.name = name;
1379   this.msg = "";
1381   if (allowFailure && !pass) {
1382     this.allowedFailure = true;
1383     this.pass = true;
1384     this.todo = false;
1385   } else if (allowFailure && pass) {
1386     this.pass = true;
1387     this.todo = false;
1388   } else {
1389     this.pass = !!pass;
1390     this.todo = todo;
1391   }
1393   this.expected = this.todo ? "FAIL" : "PASS";
1395   if (this.pass) {
1396     this.status = this.expected;
1397     return;
1398   }
1400   this.status = this.todo ? "PASS" : "FAIL";
1402   if (ex) {
1403     if (typeof ex == "object" && "fileName" in ex) {
1404       // we have an exception - print filename and linenumber information
1405       this.msg += "at " + ex.fileName + ":" + ex.lineNumber + " - ";
1406     }
1408     if (ex instanceof Error) {
1409       this.msg += String(ex);
1410     } else {
1411       this.msg += JSON.stringify(ex);
1412     }
1413   }
1415   if (stack) {
1416     this.msg += "\nStack trace:\n";
1417     let normalized;
1418     if (stack instanceof Ci.nsIStackFrame) {
1419       let frames = [];
1420       for (
1421         let frame = stack;
1422         frame;
1423         frame = frame.asyncCaller || frame.caller
1424       ) {
1425         let msg = `${frame.filename}:${frame.name}:${frame.lineNumber}`;
1426         frames.push(frame.asyncCause ? `${frame.asyncCause}*${msg}` : msg);
1427       }
1428       normalized = frames.join("\n");
1429     } else {
1430       normalized = "" + stack;
1431     }
1432     this.msg += normalized;
1433   }
1435   if (gConfig.debugOnFailure) {
1436     // You've hit this line because you requested to break into the
1437     // debugger upon a testcase failure on your test run.
1438     // eslint-disable-next-line no-debugger
1439     debugger;
1440   }
1443 function testMessage(msg) {
1444   this.msg = msg || "";
1445   this.info = true;
1448 // Need to be careful adding properties to this object, since its properties
1449 // cannot conflict with global variables used in tests.
1450 function testScope(aTester, aTest, expected) {
1451   this.__tester = aTester;
1453   aTest.allowFailure = expected == "fail";
1455   var self = this;
1456   this.ok = function test_ok(condition, name) {
1457     if (arguments.length > 2) {
1458       const ex = "Too many arguments passed to ok(condition, name)`.";
1459       self.record(false, name, ex);
1460     } else {
1461       self.record(condition, name);
1462     }
1463   };
1464   this.record = function test_record(condition, name, ex, stack, expected) {
1465     if (expected == "fail") {
1466       aTest.addResult(
1467         new testResult({
1468           name,
1469           pass: !condition,
1470           todo: true,
1471           ex,
1472           stack: stack || Components.stack.caller,
1473           allowFailure: aTest.allowFailure,
1474         })
1475       );
1476     } else {
1477       aTest.addResult(
1478         new testResult({
1479           name,
1480           pass: condition,
1481           ex,
1482           stack: stack || Components.stack.caller,
1483           allowFailure: aTest.allowFailure,
1484         })
1485       );
1486     }
1487   };
1488   this.is = function test_is(a, b, name) {
1489     self.record(
1490       Object.is(a, b),
1491       name,
1492       `Got ${self.repr(a)}, expected ${self.repr(b)}`,
1493       false,
1494       Components.stack.caller
1495     );
1496   };
1497   this.isfuzzy = function test_isfuzzy(a, b, epsilon, name) {
1498     self.record(
1499       a >= b - epsilon && a <= b + epsilon,
1500       name,
1501       `Got ${self.repr(a)}, expected ${self.repr(b)} epsilon: +/- ${self.repr(
1502         epsilon
1503       )}`,
1504       false,
1505       Components.stack.caller
1506     );
1507   };
1508   this.isnot = function test_isnot(a, b, name) {
1509     self.record(
1510       !Object.is(a, b),
1511       name,
1512       `Didn't expect ${self.repr(a)}, but got it`,
1513       false,
1514       Components.stack.caller
1515     );
1516   };
1517   this.todo = function test_todo(condition, name, ex, stack) {
1518     aTest.addResult(
1519       new testResult({
1520         name,
1521         pass: !condition,
1522         todo: true,
1523         ex,
1524         stack: stack || Components.stack.caller,
1525         allowFailure: aTest.allowFailure,
1526       })
1527     );
1528   };
1529   this.todo_is = function test_todo_is(a, b, name) {
1530     self.todo(
1531       Object.is(a, b),
1532       name,
1533       `Got ${self.repr(a)}, expected ${self.repr(b)}`,
1534       Components.stack.caller
1535     );
1536   };
1537   this.todo_isnot = function test_todo_isnot(a, b, name) {
1538     self.todo(
1539       !Object.is(a, b),
1540       name,
1541       `Didn't expect ${self.repr(a)}, but got it`,
1542       Components.stack.caller
1543     );
1544   };
1545   this.info = function test_info(name) {
1546     aTest.addResult(new testMessage(name));
1547   };
1548   this.repr = function repr(o) {
1549     if (typeof o == "undefined") {
1550       return "undefined";
1551     } else if (o === null) {
1552       return "null";
1553     }
1554     try {
1555       if (typeof o.__repr__ == "function") {
1556         return o.__repr__();
1557       } else if (typeof o.repr == "function" && o.repr != repr) {
1558         return o.repr();
1559       }
1560     } catch (e) {}
1561     try {
1562       if (
1563         typeof o.NAME == "string" &&
1564         (o.toString == Function.prototype.toString ||
1565           o.toString == Object.prototype.toString)
1566       ) {
1567         return o.NAME;
1568       }
1569     } catch (e) {}
1570     var ostring;
1571     try {
1572       if (Object.is(o, +0)) {
1573         ostring = "+0";
1574       } else if (Object.is(o, -0)) {
1575         ostring = "-0";
1576       } else if (typeof o === "string") {
1577         ostring = JSON.stringify(o);
1578       } else if (Array.isArray(o)) {
1579         ostring = "[" + o.map(val => repr(val)).join(", ") + "]";
1580       } else {
1581         ostring = String(o);
1582       }
1583     } catch (e) {
1584       return `[${Object.prototype.toString.call(o)}]`;
1585     }
1586     if (typeof o == "function") {
1587       ostring = ostring.replace(/\) \{[^]*/, ") { ... }");
1588     }
1589     return ostring;
1590   };
1592   this.executeSoon = function test_executeSoon(func) {
1593     Services.tm.dispatchToMainThread({
1594       run() {
1595         func();
1596       },
1597     });
1598   };
1600   this.waitForExplicitFinish = function test_waitForExplicitFinish() {
1601     self.__done = false;
1602   };
1604   this.waitForFocus = function test_waitForFocus(
1605     callback,
1606     targetWindow,
1607     expectBlankPage
1608   ) {
1609     self.SimpleTest.waitForFocus(callback, targetWindow, expectBlankPage);
1610   };
1612   this.waitForClipboard = function test_waitForClipboard(
1613     expected,
1614     setup,
1615     success,
1616     failure,
1617     flavor
1618   ) {
1619     self.SimpleTest.waitForClipboard(expected, setup, success, failure, flavor);
1620   };
1622   this.registerCleanupFunction = function test_registerCleanupFunction(
1623     aFunction
1624   ) {
1625     self.__cleanupFunctions.push(aFunction);
1626   };
1628   this.requestLongerTimeout = function test_requestLongerTimeout(aFactor) {
1629     self.__timeoutFactor = aFactor;
1630   };
1632   this.expectUncaughtException = function test_expectUncaughtException(
1633     aExpecting
1634   ) {
1635     self.SimpleTest.expectUncaughtException(aExpecting);
1636   };
1638   this.ignoreAllUncaughtExceptions = function test_ignoreAllUncaughtExceptions(
1639     aIgnoring
1640   ) {
1641     self.SimpleTest.ignoreAllUncaughtExceptions(aIgnoring);
1642   };
1644   this.expectAssertions = function test_expectAssertions(aMin, aMax) {
1645     let min = aMin;
1646     let max = aMax;
1647     if (typeof max == "undefined") {
1648       max = min;
1649     }
1650     if (
1651       typeof min != "number" ||
1652       typeof max != "number" ||
1653       min < 0 ||
1654       max < min
1655     ) {
1656       throw new Error("bad parameter to expectAssertions");
1657     }
1658     self.__expectedMinAsserts = min;
1659     self.__expectedMaxAsserts = max;
1660   };
1662   this.setExpectedFailuresForSelfTest = function test_setExpectedFailuresForSelfTest(
1663     expectedAllowedFailureCount
1664   ) {
1665     aTest.allowFailure = true;
1666     aTest.expectedAllowedFailureCount = expectedAllowedFailureCount;
1667   };
1669   this.finish = function test_finish() {
1670     self.__done = true;
1671     if (self.__waitTimer) {
1672       self.executeSoon(function() {
1673         if (self.__done && self.__waitTimer) {
1674           clearTimeout(self.__waitTimer);
1675           self.__waitTimer = null;
1676           self.__tester.nextTest();
1677         }
1678       });
1679     }
1680   };
1682   this.requestCompleteLog = function test_requestCompleteLog() {
1683     self.__tester.structuredLogger.deactivateBuffering();
1684     self.registerCleanupFunction(function() {
1685       self.__tester.structuredLogger.activateBuffering();
1686     });
1687   };
1689   return this;
1692 function decorateTaskFn(fn) {
1693   fn = fn.bind(this);
1694   fn.skip = (val = true) => (fn.__skipMe = val);
1695   fn.only = () => (this.__runOnlyThisTask = fn);
1696   return fn;
1699 testScope.prototype = {
1700   __done: true,
1701   __tasks: null,
1702   __setups: [],
1703   __runOnlyThisTask: null,
1704   __waitTimer: null,
1705   __cleanupFunctions: [],
1706   __timeoutFactor: 1,
1707   __expectedMinAsserts: 0,
1708   __expectedMaxAsserts: 0,
1710   EventUtils: {},
1711   AccessibilityUtils: {},
1712   SimpleTest: {},
1713   ContentTask: null,
1714   BrowserTestUtils: null,
1715   TestUtils: null,
1716   ExtensionTestUtils: null,
1717   Assert: null,
1719   /**
1720    * Add a function which returns a promise (usually an async function)
1721    * as a test task.
1722    *
1723    * The task ends when the promise returned by the function resolves or
1724    * rejects. If the test function throws, or the promise it returns
1725    * rejects, the test is reported as a failure. Execution continues
1726    * with the next test function.
1727    *
1728    * Example usage:
1729    *
1730    * add_task(async function test() {
1731    *   let result = await Promise.resolve(true);
1732    *
1733    *   ok(result);
1734    *
1735    *   let secondary = await someFunctionThatReturnsAPromise(result);
1736    *   is(secondary, "expected value");
1737    * });
1738    *
1739    * add_task(async function test_early_return() {
1740    *   let result = await somethingThatReturnsAPromise();
1741    *
1742    *   if (!result) {
1743    *     // Test is ended immediately, with success.
1744    *     return;
1745    *   }
1746    *
1747    *   is(result, "foo");
1748    * });
1749    */
1750   add_task(aFunction) {
1751     if (!this.__tasks) {
1752       this.waitForExplicitFinish();
1753       this.__tasks = [];
1754     }
1755     let bound = decorateTaskFn.call(this, aFunction);
1756     this.__tasks.push(bound);
1757     return bound;
1758   },
1760   add_setup(aFunction) {
1761     if (!this.__setups.length) {
1762       this.waitForExplicitFinish();
1763     }
1764     let bound = aFunction.bind(this);
1765     this.__setups.push(bound);
1766     return bound;
1767   },
1769   destroy: function test_destroy() {
1770     for (let prop in this) {
1771       delete this[prop];
1772     }
1773   },