--- a/toolkit/components/jsdownloads/src/DownloadCore.jsm+++ b/toolkit/components/jsdownloads/src/DownloadCore.jsm@@ -2096,16 +2096,19 @@ this.DownloadCopySaver.prototype = { } // If the operation succeeded, store the object to allow cancellation. this._backgroundFileSaver = backgroundFileSaver; } catch (ex) { // In case an error occurs while setting up the chain of objects for // the download, ensure that we release the resources of the saver. backgroundFileSaver.finish(Cr.NS_ERROR_FAILURE);+ // Since we're not going to handle deferSaveComplete.promise below,+ // we need to make sure that the rejection is handled.+ deferSaveComplete.promise.catch(() => {}); throw ex; } // We will wait on this promise in case no error occurred while setting // up the chain of objects for the download. yield deferSaveComplete.promise; yield this._checkReputationAndMove();

--- a/toolkit/components/jsdownloads/test/unit/test_DownloadList.js+++ b/toolkit/components/jsdownloads/test/unit/test_DownloadList.js@@ -343,17 +343,17 @@ add_task(function* test_history_expirati deferred.resolve(); } }, }; yield list.addView(downloadView); // Work with one finished download and one canceled download. yield downloadOne.start();- downloadTwo.start();+ downloadTwo.start().catch(() => {}); yield downloadTwo.cancel(); // We must replace the visits added while executing the downloads with visits // that are older than 7 days, otherwise they will not be expired. yield PlacesTestUtils.clearHistory(); yield promiseExpirableDownloadVisit(); yield promiseExpirableDownloadVisit(httpUrl("interruptible.txt"));@@ -466,34 +466,34 @@ add_task(function* test_DownloadSummary( // Add a public download that has succeeded. let succeededPublicDownload = yield promiseNewDownload(); yield succeededPublicDownload.start(); yield publicList.add(succeededPublicDownload); // Add a public download that has been canceled midway. let canceledPublicDownload = yield promiseNewDownload(httpUrl("interruptible.txt"));- canceledPublicDownload.start();+ canceledPublicDownload.start().catch(() => {}); yield promiseDownloadMidway(canceledPublicDownload); yield canceledPublicDownload.cancel(); yield publicList.add(canceledPublicDownload); // Add a public download that is in progress. let inProgressPublicDownload = yield promiseNewDownload(httpUrl("interruptible.txt"));- inProgressPublicDownload.start();+ inProgressPublicDownload.start().catch(() => {}); yield promiseDownloadMidway(inProgressPublicDownload); yield publicList.add(inProgressPublicDownload); // Add a private download that is in progress. let inProgressPrivateDownload = yield Downloads.createDownload({ source: { url: httpUrl("interruptible.txt"), isPrivate: true }, target: getTempFile(TEST_TARGET_FILE_NAME).path, });- inProgressPrivateDownload.start();+ inProgressPrivateDownload.start().catch(() => {}); yield promiseDownloadMidway(inProgressPrivateDownload); yield privateList.add(inProgressPrivateDownload); // Verify that the summary includes the total number of bytes and the // currently transferred bytes only for the downloads that are not stopped. // For simplicity, we assume that after a download is added to the list, its // current state is immediately propagated to the summary object, which is // true in the current implementation, though it is not guaranteed as all the

new file mode 100644--- /dev/null+++ b/toolkit/modules/tests/PromiseTestUtils.jsm@@ -0,0 +1,241 @@+/* Any copyright is dedicated to the Public Domain.+ * http://creativecommons.org/publicdomain/zero/1.0/ */++/*+ * Detects and reports unhandled rejections during test runs. Test harnesses+ * will fail tests in this case, unless the test whitelists itself.+ */++"use strict";++this.EXPORTED_SYMBOLS = [+ "PromiseTestUtils",+];++const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;++Cu.import("resource://gre/modules/Services.jsm", this);++// Keep "JSMPromise" separate so "Promise" still refers to DOM Promises.+let JSMPromise = Cu.import("resource://gre/modules/Promise.jsm", {}).Promise;++// For now, we need test harnesses to provide a reference to Assert.jsm.+let Assert = null;++this.PromiseTestUtils = {+ /**+ * Array of objects containing the details of the Promise rejections that are+ * currently left uncaught. This includes DOM Promise and Promise.jsm. When+ * rejections in DOM Promises are consumed, they are removed from this list.+ *+ * The objects contain at least the following properties:+ * {+ * message: The error message associated with the rejection, if any.+ * date: Date object indicating when the rejection was observed.+ * id: For DOM Promise only, the Promise ID from PromiseDebugging. This is+ * only used for tracking and should not be checked by the callers.+ * stack: nsIStackFrame, SavedFrame, or string indicating the stack at the+ * time the rejection was triggered. May also be null if the+ * rejection was triggered while a script was on the stack.+ * }+ */+ _rejections: [],++ /**+ * When an uncaught rejection is detected, it is ignored if one of the+ * functions in this array returns true when called with the rejection details+ * as its only argument. When a function matches an expected rejection, it is+ * then removed from the array.+ */+ _rejectionIgnoreFns: [],++ /**+ * Called only by the test infrastructure, registers the rejection observers.+ *+ * This should be called only once, and a matching "uninit" call must be made+ * or the tests will crash on shutdown.+ */+ init() {+ if (this._initialized) {+ Cu.reportError("This object was already initialized.");+ return;+ }++ PromiseDebugging.addUncaughtRejectionObserver(this);++ // Promise.jsm rejections are only reported to this observer when requested,+ // so we don't have to store a key to remove them when consumed.+ JSMPromise.Debugging.addUncaughtErrorObserver(+ rejection => this._rejections.push(rejection));++ this._initialized = true;+ },+ _initialized: false,++ /**+ * Called only by the test infrastructure, unregisters the observers.+ */+ uninit() {+ if (!this._initialized) {+ return;+ }++ PromiseDebugging.removeUncaughtRejectionObserver(this);+ JSMPromise.Debugging.clearUncaughtErrorObservers();++ this._initialized = false;+ },++ /**+ * Called only by the test infrastructure, spins the event loop until the+ * messages for pending DOM Promise rejections have been processed.+ */+ ensureDOMPromiseRejectionsProcessed() {+ let observed = false;+ let observer = {+ onLeftUncaught: promise => {+ if (PromiseDebugging.getState(promise).reason ===+ this._ensureDOMPromiseRejectionsProcessedReason) {+ observed = true;+ }+ },+ onConsumed() {},+ };++ PromiseDebugging.addUncaughtRejectionObserver(observer);+ Promise.reject(this._ensureDOMPromiseRejectionsProcessedReason);+ while (!observed) {+ Services.tm.mainThread.processNextEvent(true);+ }+ PromiseDebugging.removeUncaughtRejectionObserver(observer);+ },+ _ensureDOMPromiseRejectionsProcessedReason: {},++ /**+ * Called only by the tests for PromiseDebugging.addUncaughtRejectionObserver+ * and for JSMPromise.Debugging, disables the observers in this module.+ */+ disableUncaughtRejectionObserverForSelfTest() {+ this.uninit();+ },++ /**+ * Called by tests that have been whitelisted, disables the observers in this+ * module. For new tests where uncaught rejections are expected, you should+ * use the more granular expectUncaughtRejection function instead.+ */+ thisTestLeaksUncaughtRejectionsAndShouldBeFixed() {+ this.uninit();+ },++ /**+ * Sets or updates the Assert object instance to be used for error reporting.+ */+ set Assert(assert) {+ Assert = assert;+ },++ // UncaughtRejectionObserver+ onLeftUncaught(promise) {+ let message = "(Unable to convert rejection reason to string.)";+ try {+ let reason = PromiseDebugging.getState(promise).reason;+ if (reason === this._ensureDOMPromiseRejectionsProcessedReason) {+ // Ignore the special promise for ensureDOMPromiseRejectionsProcessed.+ return;+ }+ message = reason.message || ("" + reason);+ } catch (ex) {}++ // It's important that we don't store any reference to the provided Promise+ // object or its value after this function returns in order to avoid leaks.+ this._rejections.push({+ id: PromiseDebugging.getPromiseID(promise),+ message,+ date: new Date(),+ stack: PromiseDebugging.getRejectionStack(promise),+ });+ },++ // UncaughtRejectionObserver+ onConsumed(promise) {+ // We don't expect that many unhandled rejections will appear at the same+ // time, so the algorithm doesn't need to be optimized for that case.+ let id = PromiseDebugging.getPromiseID(promise);+ let index = this._rejections.findIndex(rejection => rejection.id == id);+ // If we get a consumption notification for a rejection that was left+ // uncaught before this module was initialized, we can safely ignore it.+ if (index != -1) {+ this._rejections.splice(index, 1);+ }+ },++ /**+ * Informs the test suite that the test code will generate a Promise rejection+ * that will still be unhandled when the test file terminates.+ *+ * This method must be called once for each instance of Promise that is+ * expected to be uncaught, even if the rejection reason is the same for each+ * instance.+ *+ * If the expected rejection does not occur, the test will fail.+ *+ * @param regExpOrCheckFn+ * This can either be a regular expression that should match the error+ * message of the rejection, or a check function that is invoked with+ * the rejection details object as its first argument.+ */+ expectUncaughtRejection(regExpOrCheckFn) {+ let checkFn = !("test" in regExpOrCheckFn) ? regExpOrCheckFn :+ rejection => regExpOrCheckFn.test(rejection.message);+ this._rejectionIgnoreFns.push(checkFn);+ },++ /**+ * Fails the test if there are any uncaught rejections at this time that have+ * not been whitelisted using expectUncaughtRejection.+ *+ * Depending on the configuration of the test suite, this function might only+ * report the details of the first uncaught rejection that was generated.+ *+ * This is called by the test suite at the end of each test function.+ */+ assertNoUncaughtRejections() {+ // Ask Promise.jsm to report all uncaught rejections to the observer now.+ JSMPromise.Debugging.flushUncaughtErrors();++ // If there is any uncaught rejection left at this point, the test fails.+ while (this._rejections.length > 0) {+ let rejection = this._rejections.shift();++ // If one of the ignore functions matches, ignore the rejection, then+ // remove the function so that each function only matches one rejection.+ let index = this._rejectionIgnoreFns.findIndex(f => f(rejection));+ if (index != -1) {+ this._rejectionIgnoreFns.splice(index, 1);+ continue;+ }++ // Report the error. This operation can throw an exception, depending on+ // the configuration of the test suite that handles the assertion.+ Assert.ok(false,+ `A promise chain failed to handle a rejection:` ++ ` ${rejection.message} - rejection date: ${rejection.date}`++ ` - stack: ${rejection.stack}`);+ }+ },++ /**+ * Fails the test if any rejection indicated by expectUncaughtRejection has+ * not yet been reported at this time.+ *+ * This is called by the test suite at the end of each test file.+ */+ assertNoMoreExpectedRejections() {+ // Only log this condition is there is a failure.+ if (this._rejectionIgnoreFns.length > 0) {+ Assert.equal(this._rejectionIgnoreFns.length, 0,+ "Unable to find a rejection expected by expectUncaughtRejection.");+ }+ },+};