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