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;
11 var { AppConstants } = ChromeUtils.importESModule(
12 "resource://gre/modules/AppConstants.sys.mjs"
15 ChromeUtils.defineModuleGetter(
18 "resource://gre/modules/AddonManager.jsm"
21 const SIMPLETEST_OVERRIDES = [
34 setTimeout(testInit, 0);
36 var TabDestroyObserver = {
37 outstanding: new Set(),
38 promiseResolver: null,
41 Services.obs.addObserver(this, "message-manager-close");
42 Services.obs.addObserver(this, "message-manager-disconnect");
46 Services.obs.removeObserver(this, "message-manager-close");
47 Services.obs.removeObserver(this, "message-manager-disconnect");
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();
62 if (!this.outstanding.size) {
63 return Promise.resolve();
66 return new Promise(resolve => {
67 this.promiseResolver = resolve;
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")) {
81 prefs.setBoolPref("testing.browserTestHarness.running", true);
83 if (prefs.prefHasUserValue("testing.browserTestHarness.timeout")) {
84 gTimeoutSeconds = prefs.getIntPref("testing.browserTestHarness.timeout");
87 var sstring = Cc["@mozilla.org/supports-string;1"].createInstance(
90 sstring.data = location.search;
92 Services.ww.openWindow(
94 "chrome://mochikit/content/browser-harness.xhtml",
96 "chrome,centerscreen,dialog=no,resizable,titlebar,toolbar=no,width=800,height=600",
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(),
113 webNav.loadURI(url, loadURIOptions);
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);
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);
133 Services.mm.loadFrameScript(
134 "chrome://mochikit/content/shutdown-leaks-collector.js",
138 // In non-e10s, only run the ShutdownLeaksCollector in the parent process.
139 ChromeUtils.importESModule(
140 "chrome://mochikit/content/ShutdownLeaksCollector.sys.mjs"
145 function isGenerator(value) {
146 return value && typeof value === "object" && typeof value.next === "function";
149 function Tester(aTests, structuredLogger, aCallback) {
150 this.structuredLogger = structuredLogger;
152 this.callback = aCallback;
154 this._scriptLoader = Services.scriptloader;
155 this.EventUtils = {};
156 this._scriptLoader.loadSubScript(
157 "chrome://mochikit/content/tests/SimpleTest/EventUtils.js",
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.
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
173 void window.windowGlobalChild.getActor("SpecialPowers");
175 var simpleTestScope = {};
176 this._scriptLoader.loadSubScript(
177 "chrome://mochikit/content/tests/SimpleTest/SimpleTest.js",
180 this._scriptLoader.loadSubScript(
181 "chrome://mochikit/content/tests/SimpleTest/MemoryStats.js",
184 this._scriptLoader.loadSubScript(
185 "chrome://mochikit/content/chrome-harness.js",
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);
198 extensionUtilsScope.SimpleTest = this.SimpleTest;
199 this._scriptLoader.loadSubScript(
200 "chrome://mochikit/content/tests/SimpleTest/ExtensionTestUtils.js",
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"
211 this.BrowserTestUtils = ChromeUtils.importESModule(
212 "resource://testing-common/BrowserTestUtils.sys.mjs"
214 this.TestUtils = ChromeUtils.importESModule(
215 "resource://testing-common/TestUtils.sys.mjs"
217 this.PromiseTestUtils = ChromeUtils.importESModule(
218 "resource://testing-common/PromiseTestUtils.sys.mjs"
220 this.Assert = ChromeUtils.importESModule(
221 "resource://testing-common/Assert.sys.mjs"
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];
234 this._coverageCollector = null;
236 const { XPCOMUtils } = ChromeUtils.importESModule(
237 "resource://gre/modules/XPCOMUtils.sys.mjs"
240 // Avoid failing tests when XPCOMUtils.defineLazyScriptGetter is used.
241 XPCOMUtils.overrideScriptLoaderForTests({
242 loadSubScript: (url, obj) => {
243 let before = Object.keys(window);
245 return this._scriptLoader.loadSubScript(url, obj);
247 for (let property of Object.keys(window)) {
249 !before.includes(property) &&
250 !this._globalProperties.includes(property)
252 this._globalProperties.push(property);
253 this.SimpleTest.info(
254 `Global property added while loading ${url}: ${property}`
260 loadSubScriptWithOptions: this._scriptLoader.loadSubScriptWithOptions.bind(
265 // ensure the mouse is reset before each test run
266 if (Services.env.exists("MOZ_AUTOMATION")) {
267 this.EventUtils.synthesizeNativeMouseEvent({
276 AccessibilityUtils: {},
279 ExtensionTestUtils: null,
284 runUntilFailure: false,
286 currentTestIndex: -1,
288 lastStartTimestamp: null,
289 lastAssertionCount: 0,
290 failuresFromInitialWindowState: 0,
293 return this.tests[this.currentTestIndex];
296 return this.currentTestIndex == this.tests.length - 1 && this.repeat <= 0;
299 start: function Tester_start() {
300 TabDestroyObserver.init();
302 // if testOnLoad was not called, then gConfig is not defined
304 gConfig = readConfig();
307 if (gConfig.runUntilFailure) {
308 this.runUntilFailure = true;
311 if (gConfig.a11y_checks != undefined) {
312 this.a11y_checks = gConfig.a11y_checks;
315 if (gConfig.repeat) {
316 this.repeat = gConfig.repeat;
319 if (gConfig.jscovDirPrefix) {
320 let coveragePath = gConfig.jscovDirPrefix;
321 let { CoverageCollector } = ChromeUtils.importESModule(
322 "resource://testing-common/CoverageUtils.sys.mjs"
324 this._coverageCollector = new CoverageCollector(coveragePath);
327 this.structuredLogger.info("*** Start BrowserChrome Test Results ***");
328 Services.console.registerListener(this);
329 this._globalProperties = Object.keys(window);
330 this._globalPropertyWhitelist = [
335 "__SS_tabsToRestore",
337 "webConsoleCommandController",
343 this.PerTestCoverageUtils.beforeTestSync();
345 if (this.tests.length) {
346 this.waitForWindowsReady().then(() => {
354 async waitForWindowsReady() {
355 await this.setupDefaultTheme();
356 await new Promise(resolve =>
357 this.waitForGraphicsTestWindowToBeGone(resolve)
359 await this.promiseMainWindowReady();
362 async promiseMainWindowReady() {
363 if (window.gBrowserInit) {
364 await window.gBrowserInit.idleTasksFinishedPromise;
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();
375 waitForGraphicsTestWindowToBeGone(aCallback) {
376 for (let win of Services.wm.getEnumerator(null)) {
380 win.document.documentURI ==
381 "chrome://gfxsanity/content/sanityparent.html"
383 this.BrowserTestUtils.domWindowClosed(win).then(aCallback);
387 // graphics test window is already gone, just call callback immediately
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"
397 ? "Found an unexpected {elt} at the end of test run"
398 : "Found an unexpected {elt}";
404 AppConstants.MOZ_APP_NAME != "thunderbird" &&
405 gBrowser.tabs.length > 1
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;
418 if (lastURIcount >= 3) {
419 this.currentTest.addResult(
422 "terminating browser early - unable to close tabs; skipping remaining tests in folder",
423 allowFailure: this.currentTest.allowFailure,
429 this.currentTest.addResult(
432 baseMsg.replace("{elt}", "tab") +
434 lastTab.linkedBrowser.currentURI.spec,
435 allowFailure: this.currentTest.allowFailure,
439 gBrowser.removeTab(lastTab);
443 // Replace the last tab with a fresh one
444 if (window.gBrowser && AppConstants.MOZ_APP_NAME != "thunderbird") {
445 gBrowser.addTab("about:blank", {
447 triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
449 gBrowser.removeTab(gBrowser.selectedTab, { skipPermitUnload: true });
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");
460 win.document.documentElement.getAttribute("id") !=
461 "browserTestHarness" &&
462 type != "devtools:webconsole"
465 case "navigator:browser":
466 type = "browser window";
469 type = "mail window";
473 "unknown window with document URI: " +
474 win.document.documentURI +
479 let msg = baseMsg.replace("{elt}", type);
480 if (this.currentTest) {
481 this.currentTest.addResult(
484 allowFailure: this.currentTest.allowFailure,
488 this.failuresFromInitialWindowState++;
489 this.structuredLogger.error("browser-test.js | " + msg);
496 // Make sure the window is raised before each test.
497 this.SimpleTest.waitForFocus(aCallback);
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);
528 this.structuredLogger.error(
529 "browser-test.js | No tests to run. Did you pass invalid test_paths?"
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;
541 haltTests: function Tester_haltTests() {
542 // Do not run any further tests
543 this.currentTestIndex = this.tests.length - 1;
547 observe: function Tester_observe(aSubject, aTopic, aData) {
549 this.onConsoleMessage(aSubject);
553 onConsoleMessage: function Tester_onConsoleMessage(aConsoleMessage) {
554 // Ignore empty messages.
555 if (!aConsoleMessage.message) {
560 var msg = "Console message: " + aConsoleMessage.message;
561 if (this.currentTest) {
562 this.currentTest.addResult(new testMessage(msg));
564 this.structuredLogger.info(
565 "TEST-INFO | (browser-test.js) | " + msg.replace(/\n$/, "") + "\n"
569 // Swallow exception so we don't lead to another error being reported,
570 // throwing us into an infinite loop
574 async ensureVsyncDisabled() {
575 // The WebExtension process keeps vsync enabled forever in headless mode.
577 if (Services.env.get("MOZ_HEADLESS")) {
582 await this.TestUtils.waitForCondition(
583 () => !ChromeUtils.vsyncEnabled(),
584 "waiting for vsync to be disabled"
587 this.Assert.ok(false, e);
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."
598 if (this.currentTest) {
599 if (this._coverageCollector) {
600 this._coverageCollector.recordTestCoverage(this.currentTest.path);
603 this.PerTestCoverageUtils.afterTestSync();
605 // Run cleanup functions for the current test before moving on to the
607 let testScope = this.currentTest.scope;
608 while (testScope.__cleanupFunctions.length) {
609 let func = testScope.__cleanupFunctions.shift();
611 let result = await func.apply(testScope);
612 if (isGenerator(result)) {
613 this.SimpleTest.ok(false, "Cleanup function returned a generator");
616 this.currentTest.addResult(
618 name: "Cleanup function threw an exception",
620 allowFailure: this.currentTest.allowFailure,
626 // Spare tests cleanup work.
627 // Reset gReduceMotionOverride in case the test set it.
628 if (typeof gReduceMotionOverride == "boolean") {
629 gReduceMotionOverride = null;
632 Services.obs.notifyObservers(null, "test-complete");
635 this.currentTest.passCount === 0 &&
636 this.currentTest.failCount === 0 &&
637 this.currentTest.todoCount === 0
639 this.currentTest.addResult(
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.",
649 let winUtils = window.windowUtils;
650 if (winUtils.isTestControllingRefreshes) {
651 this.currentTest.addResult(
653 name: "test left refresh driver under test control",
656 winUtils.restoreNormalRefresh();
659 if (this.SimpleTest.isExpectingUncaughtException()) {
660 this.currentTest.addResult(
663 "expectUncaughtException was called but no uncaught" +
664 " exception was detected!",
665 allowFailure: this.currentTest.allowFailure,
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.
687 if (!this._globalProperties.includes(prop)) {
688 this._globalProperties.push(prop);
689 if (!this._globalPropertyWhitelist.includes(prop)) {
690 this.currentTest.addResult(
692 name: "test left unexpected property on window: " + prop,
693 allowFailure: this.currentTest.allowFailure,
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");
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; ";
715 msg += "deleted " + entry.path;
717 msg += "could not delete " + entry.path;
719 this.structuredLogger.info(msg);
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(
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.",
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(
755 " is greater than expected range " +
760 pass: true, // TEMPORARILY TEST-KNOWN-FAIL
762 allowFailure: this.currentTest.allowFailure,
765 } else if (numAsserts < min) {
766 // TEST-UNEXPECTED-PASS
767 this.currentTest.addResult(
772 " is less than expected range " +
778 allowFailure: this.currentTest.allowFailure,
781 } else if (numAsserts > 0) {
783 this.currentTest.addResult(
788 " is within expected range " +
795 allowFailure: this.currentTest.allowFailure,
801 if (this.currentTest.allowFailure) {
802 if (this.currentTest.expectedAllowedFailureCount) {
803 this.currentTest.addResult(
807 this.currentTest.expectedAllowedFailureCount +
808 " failures in this file, got " +
809 this.currentTest.allowedFailureCount +
812 this.currentTest.expectedAllowedFailureCount ==
813 this.currentTest.allowedFailureCount,
816 } else if (this.currentTest.allowedFailureCount == 0) {
817 this.currentTest.addResult(
820 "We expect at least one assertion to fail because this" +
821 " test file is marked as fail-if in the manifest.",
823 knownFailure: this.currentTest.allowFailure,
829 // Dump memory stats for main thread.
831 Services.appinfo.processType == Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT
833 this.MemoryStats.dump(
834 this.currentTestIndex,
835 this.currentTest.path,
836 gConfig.dumpOutputDirectory,
837 gConfig.dumpAboutMemoryAfterTest,
838 gConfig.dumpDMDAfterTest
842 // Note the test run time
843 let name = this.currentTest.path;
844 name = name.slice(name.lastIndexOf("/") + 1);
845 ChromeUtils.addProfilerMarker(
847 { category: "Test", startTime: this.lastStartTimestamp },
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.
856 Services.env.exists("MOZ_UPLOAD_DIR") &&
857 !Services.env.exists("MOZ_PROFILER_SHUTDOWN") &&
858 Services.profiler.IsActive()
860 let filename = `profile_${name}.json`;
861 let path = Services.env.get("MOZ_UPLOAD_DIR");
862 let profilePath = PathUtils.join(path, filename);
864 let profileData = await Services.profiler.getProfileDataAsGzippedArrayBuffer();
865 await IOUtils.write(profilePath, new Uint8Array(profileData));
866 this.currentTest.addResult(
869 "Found unexpected failures during the test; profile uploaded in " +
874 // If the profile is large, we may encounter out of memory errors.
875 this.currentTest.addResult(
878 "Found unexpected failures during the test; failed to upload profile: " +
886 let time = Date.now() - this.lastStartTime;
888 this.structuredLogger.testEnd(
889 this.currentTest.path,
892 "finished in " + time + "ms"
894 this.currentTest.setDuration(time);
896 if (this.runUntilFailure && this.currentTest.failCount > 0) {
900 // Restore original SimpleTest methods to avoid leaks.
901 SIMPLETEST_OVERRIDES.forEach(m => {
902 this.SimpleTest[m] = this.SimpleTestOriginal[m];
905 this.ContentTask.setTestScope(null);
907 this.currentTest.scope = null;
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(() => {
915 if (this._coverageCollector) {
916 this._coverageCollector.finalize();
918 !AppConstants.RELEASE_OR_BETA &&
919 !AppConstants.DEBUG &&
920 !AppConstants.MOZ_CODE_COVERAGE &&
921 !AppConstants.ASAN &&
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") {
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
938 let sidebar = document.getElementById("sidebar");
940 sidebar.setAttribute("src", "data:text/html;charset=utf-8,");
941 sidebar.docShell.createAboutBlankContentViewer(null, null);
942 sidebar.setAttribute("src", "about:blank");
946 // Destroy BackgroundPageThumbs resources.
947 let { BackgroundPageThumbs } = ChromeUtils.import(
948 "resource://gre/modules/BackgroundPageThumbs.jsm"
950 BackgroundPageThumbs._destroy();
952 if (window.gBrowser) {
953 NewTabPagePreloading.removePreloadedBrowser(window);
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.
967 for (let i = 0; i < numCycles; i++) {
975 let { AsyncShutdown } = ChromeUtils.importESModule(
976 "resource://gre/modules/AsyncShutdown.sys.mjs"
979 let barrier = new AsyncShutdown.Barrier(
980 "ShutdownLeaks: Wait for cleanup to be finished before checking for leaks"
982 Services.obs.notifyObservers(
983 { wrappedJSObject: barrier },
984 "shutdown-leaks-before-check"
987 barrier.client.addBlocker(
988 "ShutdownLeaks: Wait for tabs to finish closing",
989 TabDestroyObserver.wait()
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(
1001 Services.ppmm.broadcastAsyncMessage("browser-test:collect-request");
1003 shutdownCleanup(() => {
1005 shutdownCleanup(() => {
1015 if (this.repeat > 0) {
1017 if (this.currentTestIndex < 0) {
1018 this.currentTestIndex = 0;
1022 this.currentTestIndex++;
1023 if (gConfig.repeat) {
1024 this.repeat = gConfig.repeat;
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();
1037 let result = await task();
1038 if (isGenerator(result)) {
1039 currentScope.SimpleTest.ok(false, "Task returned a generator");
1042 if (currentTest.timedOut) {
1043 currentTest.addResult(
1045 name: `Uncaught exception received from previously timed out ${desc} ${task.name}`,
1048 stack: typeof ex == "object" && "stack" in ex ? ex.stack : null,
1049 allowFailure: currentTest.allowFailure,
1052 // We timed out, so we've already cleaned up for this test, just get outta here.
1055 currentTest.addResult(
1057 name: `Uncaught exception in ${desc} ${task.name}`,
1058 pass: currentScope.SimpleTest.isExpectingUncaughtException(),
1060 stack: typeof ex == "object" && "stack" in ex ? ex.stack : null,
1061 allowFailure: currentTest.allowFailure,
1065 PromiseTestUtils.assertNoUncaughtRejections();
1066 ChromeUtils.addProfilerMarker(
1067 isSetup ? "setup-task" : "task",
1068 { category: "Test", startTime: startTimestamp },
1069 task.name.replace(/^bound /, "") || undefined
1071 currentScope.SimpleTest.info(`Leaving ${desc} ${task.name}`);
1074 async _runTaskBasedTest(currentTest) {
1075 let currentScope = currentTest.scope;
1077 // First run all the setups:
1079 while ((setupFn = currentScope.__setups.shift())) {
1080 await this.handleTask(
1083 this.PromiseTestUtils,
1084 true /* is setup task */
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();
1100 while ((task = currentScope.__tasks.shift())) {
1103 (currentScope.__runOnlyThisTask &&
1104 task != currentScope.__runOnlyThisTask)
1109 await this.handleTask(task, currentTest, this.PromiseTestUtils);
1111 currentScope.finish();
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(
1125 this.currentTest.expected
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]],
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" },
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(
1164 allowFailure: currentTest.allowFailure,
1170 allowFailure: currentTest.allowFailure,
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);
1185 // Override SimpleTest methods with ours.
1186 SIMPLETEST_OVERRIDES.forEach(function(m) {
1187 this.SimpleTest[m] = this[m];
1190 // load the tools to work with chrome .jar and remote
1192 this._scriptLoader.loadSubScript(
1193 "chrome://mochikit/content/chrome-harness.js",
1197 /* no chrome-harness tools */
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(
1210 this.currentTest.path.lastIndexOf("/")
1212 var headPath = currentTestDirPath + "/head.js";
1214 this._scriptLoader.loadSubScript(headPath, scope);
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(
1226 name: "head.js import threw an exception",
1233 // Import the test script.
1235 this.lastStartTimestamp = performance.now();
1236 this.TestUtils.promiseTestFinished = new Promise(resolve => {
1237 this.resolveFinishTestPromise = resolve;
1239 this._scriptLoader.loadSubScript(this.currentTest.path, scope);
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) {
1246 "Cannot run both a add_task test and a normal test at the same time."
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") {
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."
1260 if (!this.SimpleTest.isIgnoringAllUncaughtExceptions()) {
1261 this.currentTest.addResult(
1263 name: "Exception thrown",
1264 pass: this.SimpleTest.isExpectingUncaughtException(),
1266 allowFailure: this.currentTest.allowFailure,
1269 this.SimpleTest.expectUncaughtException(false);
1271 this.currentTest.addResult(new testMessage("Exception thrown: " + ex));
1273 this.currentTest.scope.finish();
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) {
1282 var timeoutExpires = Date.now() + gTimeoutSeconds * 1000;
1283 var waitUntilAtLeast = timeoutExpires - 1000;
1284 this.currentTest.scope.__waitTimer = this.SimpleTest._originalSetTimeout.apply(
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(
1294 timeoutExpires - Date.now()
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
1305 self.currentTest.scope.__waitTimer = setTimeout(
1307 gTimeoutSeconds * 1000
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;
1321 Date.now() - self.currentTest.lastOutputTime <
1322 (gTimeoutSeconds / 2) * 1000 &&
1323 ++self.currentTest.unexpectedTimeouts <= MAX_UNEXPECTED_TIMEOUTS
1325 self.currentTest.scope.__waitTimer = setTimeout(
1327 gTimeoutSeconds * 1000
1332 let knownFailure = false;
1333 if (gConfig.timeoutAsPass) {
1334 knownFailure = true;
1336 self.currentTest.addResult(
1338 name: "Test timed out",
1339 allowFailure: knownFailure,
1342 self.currentTest.timedOut = true;
1343 self.currentTest.scope.__waitTimer = null;
1346 gTimeoutSeconds * 1000,
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
1376 function testResult({ name, pass, todo, ex, stack, allowFailure }) {
1381 if (allowFailure && !pass) {
1382 this.allowedFailure = true;
1385 } else if (allowFailure && pass) {
1393 this.expected = this.todo ? "FAIL" : "PASS";
1396 this.status = this.expected;
1400 this.status = this.todo ? "PASS" : "FAIL";
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 + " - ";
1408 if (ex instanceof Error) {
1409 this.msg += String(ex);
1411 this.msg += JSON.stringify(ex);
1416 this.msg += "\nStack trace:\n";
1418 if (stack instanceof Ci.nsIStackFrame) {
1423 frame = frame.asyncCaller || frame.caller
1425 let msg = `${frame.filename}:${frame.name}:${frame.lineNumber}`;
1426 frames.push(frame.asyncCause ? `${frame.asyncCause}*${msg}` : msg);
1428 normalized = frames.join("\n");
1430 normalized = "" + stack;
1432 this.msg += normalized;
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
1443 function testMessage(msg) {
1444 this.msg = msg || "";
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";
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);
1461 self.record(condition, name);
1464 this.record = function test_record(condition, name, ex, stack, expected) {
1465 if (expected == "fail") {
1472 stack: stack || Components.stack.caller,
1473 allowFailure: aTest.allowFailure,
1482 stack: stack || Components.stack.caller,
1483 allowFailure: aTest.allowFailure,
1488 this.is = function test_is(a, b, name) {
1492 `Got ${self.repr(a)}, expected ${self.repr(b)}`,
1494 Components.stack.caller
1497 this.isfuzzy = function test_isfuzzy(a, b, epsilon, name) {
1499 a >= b - epsilon && a <= b + epsilon,
1501 `Got ${self.repr(a)}, expected ${self.repr(b)} epsilon: +/- ${self.repr(
1505 Components.stack.caller
1508 this.isnot = function test_isnot(a, b, name) {
1512 `Didn't expect ${self.repr(a)}, but got it`,
1514 Components.stack.caller
1517 this.todo = function test_todo(condition, name, ex, stack) {
1524 stack: stack || Components.stack.caller,
1525 allowFailure: aTest.allowFailure,
1529 this.todo_is = function test_todo_is(a, b, name) {
1533 `Got ${self.repr(a)}, expected ${self.repr(b)}`,
1534 Components.stack.caller
1537 this.todo_isnot = function test_todo_isnot(a, b, name) {
1541 `Didn't expect ${self.repr(a)}, but got it`,
1542 Components.stack.caller
1545 this.info = function test_info(name) {
1546 aTest.addResult(new testMessage(name));
1548 this.repr = function repr(o) {
1549 if (typeof o == "undefined") {
1551 } else if (o === null) {
1555 if (typeof o.__repr__ == "function") {
1556 return o.__repr__();
1557 } else if (typeof o.repr == "function" && o.repr != repr) {
1563 typeof o.NAME == "string" &&
1564 (o.toString == Function.prototype.toString ||
1565 o.toString == Object.prototype.toString)
1572 if (Object.is(o, +0)) {
1574 } else if (Object.is(o, -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(", ") + "]";
1581 ostring = String(o);
1584 return `[${Object.prototype.toString.call(o)}]`;
1586 if (typeof o == "function") {
1587 ostring = ostring.replace(/\) \{[^]*/, ") { ... }");
1592 this.executeSoon = function test_executeSoon(func) {
1593 Services.tm.dispatchToMainThread({
1600 this.waitForExplicitFinish = function test_waitForExplicitFinish() {
1601 self.__done = false;
1604 this.waitForFocus = function test_waitForFocus(
1609 self.SimpleTest.waitForFocus(callback, targetWindow, expectBlankPage);
1612 this.waitForClipboard = function test_waitForClipboard(
1619 self.SimpleTest.waitForClipboard(expected, setup, success, failure, flavor);
1622 this.registerCleanupFunction = function test_registerCleanupFunction(
1625 self.__cleanupFunctions.push(aFunction);
1628 this.requestLongerTimeout = function test_requestLongerTimeout(aFactor) {
1629 self.__timeoutFactor = aFactor;
1632 this.expectUncaughtException = function test_expectUncaughtException(
1635 self.SimpleTest.expectUncaughtException(aExpecting);
1638 this.ignoreAllUncaughtExceptions = function test_ignoreAllUncaughtExceptions(
1641 self.SimpleTest.ignoreAllUncaughtExceptions(aIgnoring);
1644 this.expectAssertions = function test_expectAssertions(aMin, aMax) {
1647 if (typeof max == "undefined") {
1651 typeof min != "number" ||
1652 typeof max != "number" ||
1656 throw new Error("bad parameter to expectAssertions");
1658 self.__expectedMinAsserts = min;
1659 self.__expectedMaxAsserts = max;
1662 this.setExpectedFailuresForSelfTest = function test_setExpectedFailuresForSelfTest(
1663 expectedAllowedFailureCount
1665 aTest.allowFailure = true;
1666 aTest.expectedAllowedFailureCount = expectedAllowedFailureCount;
1669 this.finish = function test_finish() {
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();
1682 this.requestCompleteLog = function test_requestCompleteLog() {
1683 self.__tester.structuredLogger.deactivateBuffering();
1684 self.registerCleanupFunction(function() {
1685 self.__tester.structuredLogger.activateBuffering();
1692 function decorateTaskFn(fn) {
1694 fn.skip = (val = true) => (fn.__skipMe = val);
1695 fn.only = () => (this.__runOnlyThisTask = fn);
1699 testScope.prototype = {
1703 __runOnlyThisTask: null,
1705 __cleanupFunctions: [],
1707 __expectedMinAsserts: 0,
1708 __expectedMaxAsserts: 0,
1711 AccessibilityUtils: {},
1714 BrowserTestUtils: null,
1716 ExtensionTestUtils: null,
1720 * Add a function which returns a promise (usually an async function)
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.
1730 * add_task(async function test() {
1731 * let result = await Promise.resolve(true);
1735 * let secondary = await someFunctionThatReturnsAPromise(result);
1736 * is(secondary, "expected value");
1739 * add_task(async function test_early_return() {
1740 * let result = await somethingThatReturnsAPromise();
1743 * // Test is ended immediately, with success.
1747 * is(result, "foo");
1750 add_task(aFunction) {
1751 if (!this.__tasks) {
1752 this.waitForExplicitFinish();
1755 let bound = decorateTaskFn.call(this, aFunction);
1756 this.__tasks.push(bound);
1760 add_setup(aFunction) {
1761 if (!this.__setups.length) {
1762 this.waitForExplicitFinish();
1764 let bound = aFunction.bind(this);
1765 this.__setups.push(bound);
1769 destroy: function test_destroy() {
1770 for (let prop in this) {