Bug 1242505 - Part 3 - Detect unhandled rejections of native Promises in browser-chrome tests. r=Mossop
☠☠ backed out by 9dbe428fc209 ☠ ☠
authorPaolo Amadini <paolo.mozmail@amadzone.org>
Thu, 25 May 2017 15:00:35 +0100
changeset 409156 8d53be05afc59519c5ce8cfae96d284a972fda71
parent 409155 22e7144f857ce6a49764375f1e5ddf3e1ead7a96
child 409157 c9c01c06908add6e1631121bed3d0cfaa8feeefa
push id7391
push usermtabara@mozilla.com
push dateMon, 12 Jun 2017 13:08:53 +0000
treeherdermozilla-beta@2191d7f87e2e [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersMossop
bugs1242505
milestone55.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1242505 - Part 3 - Detect unhandled rejections of native Promises in browser-chrome tests. r=Mossop The browser-chrome test suite now detects and reports unhandled rejections of native Promises, in addition to those created by Promise.jsm. The whitelisting mechanism is updated to use primarily the PromiseTestUtils.expectUncaughtRejection function. Tests will fail if a rejection that is not whitelisted occurs, or if a whitelisted rejection does not occur anymore. MozReview-Commit-ID: 1beGB5GG8Ty
browser/base/content/test/siteIdentity/browser_no_mcb_for_loopback.js
browser/base/content/test/webrtc/browser_devices_get_user_media_screen.js
browser/components/extensions/test/browser/browser_ext_tabs_executeScript_bad.js
browser/components/extensions/test/browser/head.js
browser/components/sessionstore/test/browser_354894_perwindowpb.js
devtools/client/debugger/test/mochitest/browser_dbg_promises-rejection-stack.js
devtools/client/debugger/test/mochitest/browser_dbg_terminate-on-tab-close.js
devtools/client/debugger/test/mochitest/browser_dbg_worker-window.js
devtools/client/framework/test/shared-head.js
devtools/client/inspector/markup/test/browser_markup_links_06.js
devtools/client/netmonitor/test/browser_net_simple-request-data.js
devtools/client/projecteditor/test/head.js
devtools/client/webaudioeditor/test/head.js
dom/base/test/browser_timeout_throttling_with_audio_playback.js
dom/tests/browser/helper_largeAllocation.js
testing/mochitest/browser-test.js
toolkit/content/tests/browser/browser_block_webAudio.js
toolkit/content/tests/browser/browser_mute_webAudio.js
--- a/browser/base/content/test/siteIdentity/browser_no_mcb_for_loopback.js
+++ b/browser/base/content/test/siteIdentity/browser_no_mcb_for_loopback.js
@@ -3,16 +3,24 @@
 
 // The test loads a HTTPS web page with active content from HTTP loopback URLs
 // and makes sure that the mixed content flags on the docshell are not set.
 //
 // Note that the URLs referenced within the test page intentionally use the
 // unassigned port 8 because we don't want to actually load anything, we just
 // want to check that the URLs are not blocked.
 
+// The following rejections should not be left uncaught. This test has been
+// whitelisted until the issue is fixed.
+if (!gMultiProcessBrowser) {
+  Cu.import("resource://testing-common/PromiseTestUtils.jsm", this);
+  PromiseTestUtils.expectUncaughtRejection(/NetworkError/);
+  PromiseTestUtils.expectUncaughtRejection(/NetworkError/);
+}
+
 const TEST_URL = getRootDirectory(gTestPath).replace("chrome://mochitests/content", "https://example.com") + "test_no_mcb_for_loopback.html";
 
 const LOOPBACK_PNG_URL = getRootDirectory(gTestPath).replace("chrome://mochitests/content", "http://127.0.0.1:8888") + "moz.png";
 
 const PREF_BLOCK_DISPLAY = "security.mixed_content.block_display_content";
 const PREF_BLOCK_ACTIVE = "security.mixed_content.block_active_content";
 
 registerCleanupFunction(function() {
--- a/browser/base/content/test/webrtc/browser_devices_get_user_media_screen.js
+++ b/browser/base/content/test/webrtc/browser_devices_get_user_media_screen.js
@@ -1,12 +1,21 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
+// The rejection "The fetching process for the media resource was aborted by the
+// user agent at the user's request." is left unhandled in some cases. This bug
+// should be fixed, but for the moment this file is whitelisted.
+//
+// NOTE: Whitelisting a class of rejections should be limited. Normally you
+//       should use "expectUncaughtRejection" to flag individual failures.
+Cu.import("resource://testing-common/PromiseTestUtils.jsm", this);
+PromiseTestUtils.whitelistRejectionsGlobally(/aborted by the user agent/);
+
 const permissionError = "error: NotAllowedError: The request is not allowed " +
     "by the user agent or the platform in the current context.";
 
 const notFoundError =
     "error: NotFoundError: The object can not be found here.";
 
 var gTests = [
 
--- a/browser/components/extensions/test/browser/browser_ext_tabs_executeScript_bad.js
+++ b/browser/components/extensions/test/browser/browser_ext_tabs_executeScript_bad.js
@@ -1,10 +1,19 @@
 "use strict";
 
+// Various "Missing host permission" rejections are left uncaught. This may be
+// caused by issues in the test, in the testing framework, or in production
+// code. This bug should be fixed, but for the moment this file is whitelisted.
+//
+// NOTE: Whitelisting a class of rejections should be limited. Normally you
+//       should use "expectUncaughtRejection" to flag individual failures.
+Cu.import("resource://testing-common/PromiseTestUtils.jsm", this);
+PromiseTestUtils.whitelistRejectionsGlobally(/Missing host permission/);
+
 // This is a pretty terrible hack, but it's the best we can do until we
 // support |executeScript| callbacks and |lastError|.
 async function testHasNoPermission(params) {
   let contentSetup = params.contentSetup || (() => Promise.resolve());
 
   async function background(contentSetup) {
     browser.runtime.onMessage.addListener((msg, sender) => {
       browser.test.assertEq(msg, "second script ran", "second script ran");
--- a/browser/components/extensions/test/browser/head.js
+++ b/browser/components/extensions/test/browser/head.js
@@ -16,16 +16,26 @@
  *          imageBuffer imageBufferFromDataURI
  *          getListStyleImage getPanelForNode
  *          awaitExtensionPanel awaitPopupResize
  *          promiseContentDimensions alterContent
  *          promisePrefChangeObserved openContextMenuInFrame
  *          promiseAnimationFrame
  */
 
+// There are shutdown issues for which multiple rejections are left uncaught.
+// This bug should be fixed, but for the moment this directory is whitelisted.
+//
+// NOTE: Entire directory whitelisting should be kept to a minimum. Normally you
+//       should use "expectUncaughtRejection" to flag individual failures.
+const {PromiseTestUtils} = Cu.import("resource://testing-common/PromiseTestUtils.jsm", {});
+PromiseTestUtils.whitelistRejectionsGlobally(/Message manager disconnected/);
+PromiseTestUtils.whitelistRejectionsGlobally(/No matching message handler/);
+PromiseTestUtils.whitelistRejectionsGlobally(/Receiving end does not exist/);
+
 const {AppConstants} = Cu.import("resource://gre/modules/AppConstants.jsm", {});
 const {CustomizableUI} = Cu.import("resource:///modules/CustomizableUI.jsm", {});
 
 // We run tests under two different configurations, from browser.ini and
 // browser-remote.ini. When running from browser-remote.ini, the tests are
 // copied to the sub-directory "test-oop-extensions", which we detect here, and
 // use to select our configuration.
 if (gTestPath.includes("test-oop-extensions")) {
--- a/browser/components/sessionstore/test/browser_354894_perwindowpb.js
+++ b/browser/components/sessionstore/test/browser_354894_perwindowpb.js
@@ -19,16 +19,25 @@
  * not enabled on that platform (platform shim; the application is kept running
  * although there are no windows left)
  * @note There is a difference when closing a browser window with
  * BrowserTryToCloseWindow() as opposed to close(). The former will make
  * nsSessionStore restore a window next time it gets a chance and will post
  * notifications. The latter won't.
  */
 
+// The rejection "RecentWindow.getMostRecentBrowserWindow(...) is null" is left
+// unhandled in some cases. This bug should be fixed, but for the moment this
+// file is whitelisted.
+//
+// NOTE: Whitelisting a class of rejections should be limited. Normally you
+//       should use "expectUncaughtRejection" to flag individual failures.
+Cu.import("resource://testing-common/PromiseTestUtils.jsm", this);
+PromiseTestUtils.whitelistRejectionsGlobally(/getMostRecentBrowserWindow/);
+
 // Some urls that might be opened in tabs and/or popups
 // Do not use about:blank:
 // That one is reserved for special purposes in the tests
 const TEST_URLS = ["about:mozilla", "about:buildconfig"];
 
 // Number of -request notifications to except
 // remember to adjust when adding new tests
 const NOTIFICATIONS_EXPECTED = 6;
--- a/devtools/client/debugger/test/mochitest/browser_dbg_promises-rejection-stack.js
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_promises-rejection-stack.js
@@ -8,16 +8,23 @@
  */
 
 "use strict";
 
 const TAB_URL = EXAMPLE_URL + "doc_promise-get-rejection-stack.html";
 const { PromisesFront } = require("devtools/shared/fronts/promises");
 var events = require("sdk/event/core");
 
+// The code in the document above leaves an uncaught rejection. This is only
+// reported to the testing framework if the code is loaded in the main process.
+if (!gMultiProcessBrowser) {
+  Cu.import("resource://testing-common/PromiseTestUtils.jsm", this);
+  PromiseTestUtils.expectUncaughtRejection(/hello/);
+}
+
 const TEST_DATA = [
   {
     functionDisplayName: "returnPromise/<",
     line: 19,
     column: 47
   },
   {
     functionDisplayName: "returnPromise",
--- a/devtools/client/debugger/test/mochitest/browser_dbg_terminate-on-tab-close.js
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_terminate-on-tab-close.js
@@ -6,16 +6,23 @@
 // Whitelisting this test.
 // As part of bug 1077403, the leaking uncaught rejections should be fixed.
 thisTestLeaksUncaughtRejectionsAndShouldBeFixed("[object Object]");
 
 /**
  * Tests that debuggee scripts are terminated on tab closure.
  */
 
+// The following rejection should not be left uncaught. This test has been
+// whitelisted until the issue is fixed.
+if (!gMultiProcessBrowser) {
+  Cu.import("resource://testing-common/PromiseTestUtils.jsm", this);
+  PromiseTestUtils.expectUncaughtRejection(/error\.message is undefined/);
+}
+
 const TAB_URL = EXAMPLE_URL + "doc_terminate-on-tab-close.html";
 
 function test() {
   let options = {
     source: TAB_URL,
     line: 1
   };
   initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
--- a/devtools/client/debugger/test/mochitest/browser_dbg_worker-window.js
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_worker-window.js
@@ -2,16 +2,21 @@
 // directly, and that the toolbox has expected properties.
 
 "use strict";
 
 // Whitelisting this test.
 // As part of bug 1077403, the leaking uncaught rejections should be fixed.
 thisTestLeaksUncaughtRejectionsAndShouldBeFixed("[object Object]");
 
+// The following "connectionClosed" rejection should not be left uncaught. This
+// test has been whitelisted until the issue is fixed.
+Cu.import("resource://testing-common/PromiseTestUtils.jsm", this);
+PromiseTestUtils.expectUncaughtRejection(/[object Object]/);
+
 var TAB_URL = EXAMPLE_URL + "doc_WorkerActor.attachThread-tab.html";
 var WORKER_URL = "code_WorkerActor.attachThread-worker.js";
 
 add_task(function* () {
   yield pushPrefs(["devtools.scratchpad.enabled", true]);
 
   DebuggerServer.init();
   DebuggerServer.addBrowserActors();
--- a/devtools/client/framework/test/shared-head.js
+++ b/devtools/client/framework/test/shared-head.js
@@ -14,16 +14,32 @@ const {classes: Cc, interfaces: Ci, util
   = Components;
 
 function scopedCuImport(path) {
   const scope = {};
   Cu.import(path, scope);
   return scope;
 }
 
+// There are shutdown issues for which multiple rejections are left uncaught.
+// This bug should be fixed, but for the moment devtools are whitelisted.
+//
+// NOTE: Entire directory whitelisting should be kept to a minimum. Normally you
+//       should use "expectUncaughtRejection" to flag individual failures.
+const {PromiseTestUtils} = scopedCuImport("resource://testing-common/PromiseTestUtils.jsm");
+PromiseTestUtils.whitelistRejectionsGlobally(/Component not initialized/);
+PromiseTestUtils.whitelistRejectionsGlobally(/Connection closed/);
+PromiseTestUtils.whitelistRejectionsGlobally(/destroy/);
+PromiseTestUtils.whitelistRejectionsGlobally(/is no longer, usable/);
+PromiseTestUtils.whitelistRejectionsGlobally(/this\._urls is null/);
+PromiseTestUtils.whitelistRejectionsGlobally(/this\.tabTarget is null/);
+PromiseTestUtils.whitelistRejectionsGlobally(/this\.toolbox is null/);
+PromiseTestUtils.whitelistRejectionsGlobally(/this\.webConsoleClient is null/);
+PromiseTestUtils.whitelistRejectionsGlobally(/this\.worker is null/);
+
 const {console} = scopedCuImport("resource://gre/modules/Console.jsm");
 const {ScratchpadManager} = scopedCuImport("resource://devtools/client/scratchpad/scratchpad-manager.jsm");
 const {loader, require} = scopedCuImport("resource://devtools/shared/Loader.jsm");
 
 const {gDevTools} = require("devtools/client/framework/devtools");
 const {TargetFactory} = require("devtools/client/framework/target");
 const DevToolsUtils = require("devtools/shared/DevToolsUtils");
 const flags = require("devtools/shared/flags");
--- a/devtools/client/inspector/markup/test/browser_markup_links_06.js
+++ b/devtools/client/inspector/markup/test/browser_markup_links_06.js
@@ -2,16 +2,21 @@
 /* Any copyright is dedicated to the Public Domain.
  http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
 // Tests that the contextual menu items shown when clicking on linked attributes
 // for <script> and <link> tags actually open the right tools.
 
+// The following rejection should not be left uncaught. This test has been
+// whitelisted until the issue is fixed.
+Cu.import("resource://testing-common/PromiseTestUtils.jsm", this);
+PromiseTestUtils.expectUncaughtRejection(/NS_ERROR_NOT_INITIALIZED/);
+
 const TEST_URL = URL_ROOT + "doc_markup_links.html";
 
 add_task(function* () {
   let {toolbox, inspector} = yield openInspectorForURL(TEST_URL);
 
   info("Select a node with a cssresource attribute");
   yield selectNode("link", inspector);
 
--- a/devtools/client/netmonitor/test/browser_net_simple-request-data.js
+++ b/devtools/client/netmonitor/test/browser_net_simple-request-data.js
@@ -2,16 +2,22 @@
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
 /**
  * Tests if requests render correct information in the menu UI.
  */
 
+// The following rejections should not be left uncaught. This test has been
+// whitelisted until the issue is fixed.
+Cu.import("resource://testing-common/PromiseTestUtils.jsm", this);
+PromiseTestUtils.expectUncaughtRejection(/cookies is undefined/);
+PromiseTestUtils.expectUncaughtRejection(/cookies is undefined/);
+
 function test() {
   // Disable tcp fast open, because it is setting a response header indicator
   // (bug 1352274). TCP Fast Open is not present on all platforms therefore the
   // number of response headers will vary depending on the platform.
   Services.prefs.setBoolPref("network.tcp.tcp_fastopen_enable", false);
 
   let { L10N } = require("devtools/client/netmonitor/src/utils/l10n");
 
--- a/devtools/client/projecteditor/test/head.js
+++ b/devtools/client/projecteditor/test/head.js
@@ -1,13 +1,30 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 var Cu = Components.utils;
+
+// There are shutdown issues for which multiple rejections are left uncaught.
+// This bug should be fixed, but for the moment devtools are whitelisted.
+//
+// NOTE: Entire directory whitelisting should be kept to a minimum. Normally you
+//       should use "expectUncaughtRejection" to flag individual failures.
+const {PromiseTestUtils} = Cu.import("resource://testing-common/PromiseTestUtils.jsm", {});
+PromiseTestUtils.whitelistRejectionsGlobally(/Component not initialized/);
+PromiseTestUtils.whitelistRejectionsGlobally(/Connection closed/);
+PromiseTestUtils.whitelistRejectionsGlobally(/destroy/);
+PromiseTestUtils.whitelistRejectionsGlobally(/is no longer, usable/);
+PromiseTestUtils.whitelistRejectionsGlobally(/this\._urls is null/);
+PromiseTestUtils.whitelistRejectionsGlobally(/this\.tabTarget is null/);
+PromiseTestUtils.whitelistRejectionsGlobally(/this\.toolbox is null/);
+PromiseTestUtils.whitelistRejectionsGlobally(/this\.webConsoleClient is null/);
+PromiseTestUtils.whitelistRejectionsGlobally(/this\.worker is null/);
+
 const {require} = Cu.import("resource://devtools/shared/Loader.jsm", {});
 const {TargetFactory} = require("devtools/client/framework/target");
 const {console} = Cu.import("resource://gre/modules/Console.jsm", {});
 const promise = require("promise");
 const {FileUtils} = Cu.import("resource://gre/modules/FileUtils.jsm", {});
 const {NetUtil} = Cu.import("resource://gre/modules/NetUtil.jsm", {});
 const ProjectEditor = require("devtools/client/projecteditor/lib/projecteditor");
 const DevToolsUtils = require("devtools/shared/DevToolsUtils");
--- a/devtools/client/webaudioeditor/test/head.js
+++ b/devtools/client/webaudioeditor/test/head.js
@@ -1,14 +1,30 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 "use strict";
 
 var { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
 
+// There are shutdown issues for which multiple rejections are left uncaught.
+// This bug should be fixed, but for the moment devtools are whitelisted.
+//
+// NOTE: Entire directory whitelisting should be kept to a minimum. Normally you
+//       should use "expectUncaughtRejection" to flag individual failures.
+const { PromiseTestUtils } = Cu.import("resource://testing-common/PromiseTestUtils.jsm", {});
+PromiseTestUtils.whitelistRejectionsGlobally(/Component not initialized/);
+PromiseTestUtils.whitelistRejectionsGlobally(/Connection closed/);
+PromiseTestUtils.whitelistRejectionsGlobally(/destroy/);
+PromiseTestUtils.whitelistRejectionsGlobally(/is no longer, usable/);
+PromiseTestUtils.whitelistRejectionsGlobally(/this\._urls is null/);
+PromiseTestUtils.whitelistRejectionsGlobally(/this\.tabTarget is null/);
+PromiseTestUtils.whitelistRejectionsGlobally(/this\.toolbox is null/);
+PromiseTestUtils.whitelistRejectionsGlobally(/this\.webConsoleClient is null/);
+PromiseTestUtils.whitelistRejectionsGlobally(/this\.worker is null/);
+
 var { require } = Cu.import("resource://devtools/shared/Loader.jsm", {});
 var { Task } = require("devtools/shared/task");
 var Services = require("Services");
 var { gDevTools } = require("devtools/client/framework/devtools");
 var { TargetFactory } = require("devtools/client/framework/target");
 var { DebuggerServer } = require("devtools/server/main");
 var { generateUUID } = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator);
 
--- a/dom/base/test/browser_timeout_throttling_with_audio_playback.js
+++ b/dom/base/test/browser_timeout_throttling_with_audio_playback.js
@@ -1,8 +1,15 @@
+// The tab closing code leaves an uncaught rejection. This test has been
+// whitelisted until the issue is fixed.
+if (!gMultiProcessBrowser) {
+  Cu.import("resource://testing-common/PromiseTestUtils.jsm", this);
+  PromiseTestUtils.expectUncaughtRejection(/is no longer, usable/);
+}
+
 const kBaseURI = "http://mochi.test:8888/browser/dom/base/test/empty.html";
 const kPluginJS = "chrome://mochitests/content/browser/dom/base/test/plugin.js";
 var testURLs = [
   "http://mochi.test:8888/browser/dom/base/test/file_audioLoop.html",
   "http://mochi.test:8888/browser/dom/base/test/file_audioLoopInIframe.html",
   "http://mochi.test:8888/browser/dom/base/test/file_pluginAudio.html",
   "http://mochi.test:8888/browser/dom/base/test/file_webaudioLoop.html",
 ];
--- a/dom/tests/browser/helper_largeAllocation.js
+++ b/dom/tests/browser/helper_largeAllocation.js
@@ -288,16 +288,17 @@ function* largeAllocSuccessTests() {
     // process which was created for the normal content to be loaded into as the
     // browsing context was booted out of the fresh process. If we discover that
     // this was not a fresh process, we'll need to wait for another process.
     // Start listening now.
     epc = expectProcessCreated();
     if (!(yield getInLAProc(aBrowser))) {
       yield epc;
     } else {
+      epc.catch(() => {});
       epc.kill();
     }
 
     let pid3 = yield getPID(aBrowser);
 
     isnot(pid1, pid3, "PIDs 1 and 3 should not match");
     isnot(pid2, pid3, "PIDs 2 and 3 should not match");
     is(true, yield getInLAProc(aBrowser));
--- a/testing/mochitest/browser-test.js
+++ b/testing/mochitest/browser-test.js
@@ -149,52 +149,27 @@ function Tester(aTests, structuredLogger
 
   this.MemoryStats = simpleTestScope.MemoryStats;
   this.Task = Task;
   this.ContentTask = Components.utils.import("resource://testing-common/ContentTask.jsm", null).ContentTask;
   this.BrowserTestUtils = Components.utils.import("resource://testing-common/BrowserTestUtils.jsm", null).BrowserTestUtils;
   this.TestUtils = Components.utils.import("resource://testing-common/TestUtils.jsm", null).TestUtils;
   this.Task.Debugging.maintainStack = true;
   this.Promise = Components.utils.import("resource://gre/modules/Promise.jsm", null).Promise;
+  this.PromiseTestUtils = Components.utils.import("resource://testing-common/PromiseTestUtils.jsm", null).PromiseTestUtils;
   this.Assert = Components.utils.import("resource://testing-common/Assert.jsm", null).Assert;
 
+  this.PromiseTestUtils.init();
+
   this.SimpleTestOriginal = {};
   SIMPLETEST_OVERRIDES.forEach(m => {
     this.SimpleTestOriginal[m] = this.SimpleTest[m];
   });
 
   this._coverageCollector = null;
-
-  this._toleratedUncaughtRejections = null;
-  this._uncaughtErrorObserver = ({message, date, fileName, stack, lineNumber}) => {
-    let ex = message;
-    if (fileName || lineNumber) {
-      ex = {
-        fileName: fileName,
-        lineNumber: lineNumber,
-        message: message,
-        toString: function() {
-          return message;
-        }
-      };
-    }
-
-    // We may have a whitelist of rejections we wish to tolerate.
-    let pass = this._toleratedUncaughtRejections &&
-      this._toleratedUncaughtRejections.indexOf(message) != -1;
-    let name = "A promise chain failed to handle a rejection: ";
-    if (pass) {
-      name = "WARNING: (PLEASE FIX THIS AS PART OF BUG 1077403) " + name;
-    }
-
-    this.currentTest.addResult(new testResult({
-      pass, name, ex, todo: pass, stack,
-      allowFailure: this.currentTest.allowFailure,
-    }));
-  };
 }
 Tester.prototype = {
   EventUtils: {},
   SimpleTest: {},
   Task: null,
   ContentTask: null,
   ExtensionTestUtils: null,
   Assert: null,
@@ -239,19 +214,16 @@ Tester.prototype = {
     this._globalProperties = Object.keys(window);
     this._globalPropertyWhitelist = [
       "navigator", "constructor", "top",
       "Application",
       "__SS_tabsToRestore", "__SSi",
       "webConsoleCommandController",
     ];
 
-    this.Promise.Debugging.clearUncaughtErrorObservers();
-    this.Promise.Debugging.addUncaughtErrorObserver(this._uncaughtErrorObserver);
-
     if (this.tests.length)
       this.waitForGraphicsTestWindowToBeGone(this.nextTest.bind(this));
     else
       this.finish();
   },
 
   waitForGraphicsTestWindowToBeGone(aCallback) {
     let windowsEnum = Services.wm.getEnumerator(null);
@@ -339,34 +311,33 @@ Tester.prototype = {
                                     "finished window state check in " + time + "ms");
     }
 
     // Make sure the window is raised before each test.
     this.SimpleTest.waitForFocus(aCallback);
   },
 
   finish: function Tester_finish(aSkipSummary) {
-    this.Promise.Debugging.flushUncaughtErrors();
-
     var passCount = this.tests.reduce((a, f) => a + f.passCount, 0);
     var failCount = this.tests.reduce((a, f) => a + f.failCount, 0);
     var todoCount = this.tests.reduce((a, f) => a + f.todoCount, 0);
 
     // Include failures from window state checking prior to running the first test
     failCount += this.failuresFromInitialWindowState;
 
     if (this.repeat > 0) {
       --this.repeat;
       this.currentTestIndex = -1;
       this.nextTest();
     } else {
       TabDestroyObserver.destroy();
       Services.console.unregisterListener(this);
-      this.Promise.Debugging.clearUncaughtErrorObservers();
-      this._treatUncaughtRejectionsAsFailures = false;
+
+      // It's important to terminate the module to avoid crashes on shutdown.
+      this.PromiseTestUtils.uninit();
 
       // In the main process, we print the ShutdownLeaksCollector message here.
       let pid = Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULRuntime).processID;
       dump("Completed ShutdownLeaks collections in process " + pid + "\n");
 
       this.structuredLogger.info("TEST-START | Shutdown");
 
       if (this.tests.length) {
@@ -417,17 +388,16 @@ Tester.prototype = {
     } catch (ex) {
       // Swallow exception so we don't lead to another error being reported,
       // throwing us into an infinite loop
     }
   },
 
   nextTest: Task.async(function*() {
     if (this.currentTest) {
-      this.Promise.Debugging.flushUncaughtErrors();
       if (this._coverageCollector) {
         this._coverageCollector.recordTestCoverage(this.currentTest.path);
       }
 
       // Run cleanup functions for the current test before moving on to the
       // next one.
       let testScope = this.currentTest.scope;
       while (testScope.__cleanupFunctions.length > 0) {
@@ -449,18 +419,16 @@ Tester.prototype = {
           this.currentTest.todoCount === 0) {
         this.currentTest.addResult(new testResult({
           name: "This test contains no passes, no fails and no todos. Maybe" +
                 " it threw a silent exception? Make sure you use" +
                 " waitForExplicitFinish() if you need it.",
         }));
       }
 
-      this.Promise.Debugging.flushUncaughtErrors();
-
       let winUtils = window.QueryInterface(Ci.nsIInterfaceRequestor)
                            .getInterface(Ci.nsIDOMWindowUtils);
       if (winUtils.isTestControllingRefreshes) {
         this.currentTest.addResult(new testResult({
           name: "test left refresh driver under test control",
         }));
         winUtils.restoreNormalRefresh();
       }
@@ -468,16 +436,20 @@ Tester.prototype = {
       if (this.SimpleTest.isExpectingUncaughtException()) {
         this.currentTest.addResult(new testResult({
           name: "expectUncaughtException was called but no uncaught" +
                 " exception was detected!",
           allowFailure: this.currentTest.allowFailure,
         }));
       }
 
+      this.PromiseTestUtils.ensureDOMPromiseRejectionsProcessed();
+      this.PromiseTestUtils.assertNoUncaughtRejections();
+      this.PromiseTestUtils.assertNoMoreExpectedRejections();
+
       Object.keys(window).forEach(function (prop) {
         if (parseInt(prop) == prop) {
           // This is a string which when parsed as an integer and then
           // stringified gives the original string.  As in, this is in fact a
           // string representation of an integer, so an index into
           // window.frames.  Skip those.
           return;
         }
@@ -743,16 +715,17 @@ Tester.prototype = {
       } : {
         name: message,
         pass: true,
         stack,
         allowFailure: currentTest.allowFailure,
       }));
     });
 
+    this.PromiseTestUtils.Assert = this.currentTest.scope.Assert;
     this.ContentTask.setTestScope(currentScope);
 
     // Allow Assert.jsm methods to be tacked to the current scope.
     this.currentTest.scope.export_assertions = function() {
       for (let func in this.Assert) {
         this[func] = this.Assert[func].bind(this.Assert);
       }
     };
@@ -784,41 +757,41 @@ Tester.prototype = {
        }));
       }
     }
 
     // Import the test script.
     try {
       this._scriptLoader.loadSubScript(this.currentTest.path,
                                        this.currentTest.scope);
-      this.Promise.Debugging.flushUncaughtErrors();
       // Run the test
       this.lastStartTime = Date.now();
       if (this.currentTest.scope.__tasks) {
         // This test consists of tasks, added via the `add_task()` API.
         if ("test" in this.currentTest.scope) {
           throw "Cannot run both a add_task test and a normal test at the same time.";
         }
         let Promise = this.Promise;
+        let PromiseTestUtils = this.PromiseTestUtils;
         this.Task.spawn(function*() {
           let task;
           while ((task = this.__tasks.shift())) {
             this.SimpleTest.info("Entering test " + task.name);
             try {
               yield task();
             } catch (ex) {
               currentTest.addResult(new testResult({
                 name: "Uncaught exception",
                 pass: this.SimpleTest.isExpectingUncaughtException(),
                 ex,
                 stack: (typeof ex == "object" && "stack" in ex) ? ex.stack : null,
                 allowFailure: currentTest.allowFailure,
               }));
             }
-            Promise.Debugging.flushUncaughtErrors();
+            PromiseTestUtils.assertNoUncaughtRejections();
             this.SimpleTest.info("Leaving test " + task.name);
           }
           this.finish();
         }.bind(currentScope));
       } else if (typeof this.currentTest.scope.test == "function") {
         this.currentTest.scope.test();
       } else {
         throw "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.";
@@ -1057,20 +1030,16 @@ function testScope(aTester, aTest, expec
     self.SimpleTest.expectUncaughtException(aExpecting);
   };
 
   this.ignoreAllUncaughtExceptions = function test_ignoreAllUncaughtExceptions(aIgnoring) {
     self.SimpleTest.ignoreAllUncaughtExceptions(aIgnoring);
   };
 
   this.thisTestLeaksUncaughtRejectionsAndShouldBeFixed = function(...rejections) {
-    if (!aTester._toleratedUncaughtRejections) {
-      aTester._toleratedUncaughtRejections = [];
-    }
-    aTester._toleratedUncaughtRejections.push(...rejections);
   };
 
   this.expectAssertions = function test_expectAssertions(aMin, aMax) {
     let min = aMin;
     let max = aMax;
     if (typeof(max) == "undefined") {
       max = min;
     }
--- a/toolkit/content/tests/browser/browser_block_webAudio.js
+++ b/toolkit/content/tests/browser/browser_block_webAudio.js
@@ -1,10 +1,17 @@
 const PAGE = "https://example.com/browser/toolkit/content/tests/browser/file_webAudio.html";
 
+// The tab closing code leaves an uncaught rejection. This test has been
+// whitelisted until the issue is fixed.
+if (!gMultiProcessBrowser) {
+  Cu.import("resource://testing-common/PromiseTestUtils.jsm", this);
+  PromiseTestUtils.expectUncaughtRejection(/is no longer, usable/);
+}
+
 add_task(async function setup_test_preference() {
   await SpecialPowers.pushPrefEnv({"set": [
     ["media.useAudioChannelService.testing", true],
     ["media.block-autoplay-until-in-foreground", true]
   ]});
 });
 
 add_task(async function block_web_audio() {
--- a/toolkit/content/tests/browser/browser_mute_webAudio.js
+++ b/toolkit/content/tests/browser/browser_mute_webAudio.js
@@ -1,8 +1,15 @@
+// The tab closing code leaves an uncaught rejection. This test has been
+// whitelisted until the issue is fixed.
+if (!gMultiProcessBrowser) {
+  Cu.import("resource://testing-common/PromiseTestUtils.jsm", this);
+  PromiseTestUtils.expectUncaughtRejection(/is no longer, usable/);
+}
+
 const PAGE = "https://example.com/browser/toolkit/content/tests/browser/file_webAudio.html";
 
 async function click_icon(tab) {
   let icon = document.getAnonymousElementByAttribute(tab, "anonid", "soundplaying-icon");
 
   await hover_icon(icon, document.getElementById("tabbrowser-tab-tooltip"));
   EventUtils.synthesizeMouseAtCenter(icon, {button: 0});
   leave_icon(icon);