Bug 851461 - Make the JavaScript API for downloads available in parallel to nsIDownloadManager. r=mak
authorPaolo Amadini <paolo.mozmail@amadzone.org>
Mon, 22 Apr 2013 04:23:25 +0200
changeset 140396 833dc6a7c4754a4ee5753e6af0609c0f546e49cc
parent 140395 0589a420723035ee490c1b3e4f72ccdc5bf92cf3
child 140397 890ed498bae6df9c60e43dc830294596ce0b9795
push id2579
push userakeybl@mozilla.com
push dateMon, 24 Jun 2013 18:52:47 +0000
treeherdermozilla-beta@b69b7de8a05a [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmak
bugs851461
milestone23.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 851461 - Make the JavaScript API for downloads available in parallel to nsIDownloadManager. r=mak
browser/components/downloads/src/BrowserDownloads.manifest
browser/components/downloads/src/DownloadsStartup.js
browser/installer/package-manifest.in
configure.in
toolkit/components/build/nsToolkitCompsModule.cpp
toolkit/components/jsdownloads/src/Downloads.manifest
toolkit/components/jsdownloads/test/unit/head.js
toolkit/components/jsdownloads/test/unit/test_DownloadCore.js
toolkit/components/jsdownloads/test/unit/test_DownloadLegacy.js
toolkit/components/moz.build
--- a/browser/components/downloads/src/BrowserDownloads.manifest
+++ b/browser/components/downloads/src/BrowserDownloads.manifest
@@ -1,4 +1,4 @@
 component {49507fe5-2cee-4824-b6a3-e999150ce9b8} DownloadsStartup.js
 contract @mozilla.org/browser/downloadsstartup;1 {49507fe5-2cee-4824-b6a3-e999150ce9b8}
-category app-startup DownloadsStartup service,@mozilla.org/browser/downloadsstartup;1
+category profile-after-change DownloadsStartup @mozilla.org/browser/downloadsstartup;1
 component {4d99321e-d156-455b-81f7-e7aa2308134f} DownloadsUI.js
--- a/browser/components/downloads/src/DownloadsStartup.js
+++ b/browser/components/downloads/src/DownloadsStartup.js
@@ -48,16 +48,26 @@ const kObservedTopics = [
  */
 const kDownloadsUICid = Components.ID("{4d99321e-d156-455b-81f7-e7aa2308134f}");
 
 /**
  * Contract ID of the service implementing nsIDownloadManagerUI.
  */
 const kDownloadsUIContractId = "@mozilla.org/download-manager-ui;1";
 
+/**
+ * CID of the JavaScript implementation of nsITransfer.
+ */
+const kTransferCid = Components.ID("{1b4c85df-cbdd-4bb6-b04e-613caece083c}");
+
+/**
+ * Contract ID of the service implementing nsITransfer.
+ */
+const kTransferContractId = "@mozilla.org/transfer;1";
+
 ////////////////////////////////////////////////////////////////////////////////
 //// DownloadsStartup
 
 function DownloadsStartup() { }
 
 DownloadsStartup.prototype = {
   classID: Components.ID("{49507fe5-2cee-4824-b6a3-e999150ce9b8}"),
 
@@ -70,27 +80,45 @@ DownloadsStartup.prototype = {
                                          Ci.nsISupportsWeakReference]),
 
   //////////////////////////////////////////////////////////////////////////////
   //// nsIObserver
 
   observe: function DS_observe(aSubject, aTopic, aData)
   {
     switch (aTopic) {
-      case "app-startup":
+      case "profile-after-change":
         kObservedTopics.forEach(
           function (topic) Services.obs.addObserver(this, topic, true),
           this);
 
         // Override Toolkit's nsIDownloadManagerUI implementation with our own.
         // This must be done at application startup and not in the manifest to
         // ensure that our implementation overrides the original one.
         Components.manager.QueryInterface(Ci.nsIComponentRegistrar)
                           .registerFactory(kDownloadsUICid, "",
                                            kDownloadsUIContractId, null);
+
+        // If the integration preference is enabled, override Toolkit's
+        // nsITransfer implementation with the one from the JavaScript API for
+        // downloads.  This should be used only by developers while testing new
+        // code that uses the JavaScript API, and will eventually be removed
+        // when nsIDownloadManager will not be available anymore (bug 851471).
+        let useJSTransfer = false;
+        try {
+          useJSTransfer =
+            Services.prefs.getBoolPref("browser.download.useJSTransfer");
+        } catch (ex) {
+          // This is a hidden preference that does not exist by default.
+        }
+        if (useJSTransfer) {
+          Components.manager.QueryInterface(Ci.nsIComponentRegistrar)
+                            .registerFactory(kTransferCid, "",
+                                             kTransferContractId, null);
+        }
         break;
 
       case "sessionstore-windows-restored":
       case "sessionstore-browser-state-restored":
         // Unless there is no saved session, there is a chance that we are
         // starting up after a restart or a crash.  We should check the disk
         // database to see if there are completed downloads to recover and show
         // in the panel, in addition to in-progress downloads.
--- a/browser/installer/package-manifest.in
+++ b/browser/installer/package-manifest.in
@@ -350,20 +350,18 @@
 @BINPATH@/browser/components/nsBrowserContentHandler.js
 @BINPATH@/browser/components/nsBrowserGlue.js
 @BINPATH@/browser/components/nsSetDefaultBrowser.manifest
 @BINPATH@/browser/components/nsSetDefaultBrowser.js
 @BINPATH@/browser/components/BrowserDownloads.manifest
 @BINPATH@/browser/components/DownloadsStartup.js
 @BINPATH@/browser/components/DownloadsUI.js
 @BINPATH@/browser/components/BrowserPlaces.manifest
-#ifdef MOZ_JSDOWNLOADS
 @BINPATH@/components/Downloads.manifest
 @BINPATH@/components/DownloadLegacy.js
-#endif
 @BINPATH@/components/BrowserPageThumbs.manifest
 @BINPATH@/components/SiteSpecificUserAgent.js
 @BINPATH@/components/SiteSpecificUserAgent.manifest
 @BINPATH@/components/toolkitsearch.manifest
 @BINPATH@/components/nsSearchService.js
 @BINPATH@/components/nsSearchSuggestions.js
 @BINPATH@/components/passwordmgr.manifest
 @BINPATH@/components/nsLoginInfo.js
--- a/configure.in
+++ b/configure.in
@@ -8625,22 +8625,16 @@ AC_SUBST(USE_DEPENDENT_LIBS)
 
 AC_SUBST(MOZ_BUILD_ROOT)
 AC_SUBST(MOZ_OS2_TOOLS)
 
 AC_SUBST(MOZ_POST_DSO_LIB_COMMAND)
 AC_SUBST(MOZ_POST_PROGRAM_COMMAND)
 AC_SUBST(MOZ_LINKER_EXTRACT)
 
-AC_SUBST(MOZ_JSDOWNLOADS)
-
-if test -n "$MOZ_JSDOWNLOADS" ; then
-    AC_DEFINE(MOZ_JSDOWNLOADS)
-fi
-
 dnl ========================================================
 dnl = Mac bundle name prefix
 dnl ========================================================
 MOZ_ARG_WITH_STRING(macbundlename-prefix,
 [  --with-macbundlename-prefix=prefix
                           Prefix for MOZ_MACBUNDLE_NAME],
 [ MOZ_MACBUNDLE_NAME_PREFIX="$withval"])
 
--- a/toolkit/components/build/nsToolkitCompsModule.cpp
+++ b/toolkit/components/build/nsToolkitCompsModule.cpp
@@ -106,19 +106,17 @@ NS_DEFINE_NAMED_CID(NS_UPDATEPROCESSOR_C
 static const mozilla::Module::CIDEntry kToolkitCIDs[] = {
   { &kNS_TOOLKIT_APPSTARTUP_CID, false, NULL, nsAppStartupConstructor },
   { &kNS_USERINFO_CID, false, NULL, nsUserInfoConstructor },
   { &kNS_ALERTSSERVICE_CID, false, NULL, nsAlertsServiceConstructor },
 #if defined(XP_WIN) && !defined(MOZ_DISABLE_PARENTAL_CONTROLS)
   { &kNS_PARENTALCONTROLSSERVICE_CID, false, NULL, nsParentalControlsServiceWinConstructor },
 #endif
   { &kNS_DOWNLOADMANAGER_CID, false, NULL, nsDownloadManagerConstructor },
-#ifndef MOZ_JSDOWNLOADS
   { &kNS_DOWNLOAD_CID, false, NULL, nsDownloadProxyConstructor },
-#endif
   { &kNS_FIND_SERVICE_CID, false, NULL, nsFindServiceConstructor },
   { &kNS_TYPEAHEADFIND_CID, false, NULL, nsTypeAheadFindConstructor },
 #ifdef MOZ_URL_CLASSIFIER
   { &kNS_URLCLASSIFIERPREFIXSET_CID, false, NULL, nsUrlClassifierPrefixSetConstructor },
   { &kNS_URLCLASSIFIERDBSERVICE_CID, false, NULL, nsUrlClassifierDBServiceConstructor },
   { &kNS_URLCLASSIFIERSTREAMUPDATER_CID, false, NULL, nsUrlClassifierStreamUpdaterConstructor },
   { &kNS_URLCLASSIFIERUTILS_CID, false, NULL, nsUrlClassifierUtilsConstructor },
 #endif
@@ -133,19 +131,17 @@ static const mozilla::Module::CIDEntry k
 static const mozilla::Module::ContractIDEntry kToolkitContracts[] = {
   { NS_APPSTARTUP_CONTRACTID, &kNS_TOOLKIT_APPSTARTUP_CID },
   { NS_USERINFO_CONTRACTID, &kNS_USERINFO_CID },
   { NS_ALERTSERVICE_CONTRACTID, &kNS_ALERTSSERVICE_CID },
 #if defined(XP_WIN) && !defined(MOZ_DISABLE_PARENTAL_CONTROLS)
   { NS_PARENTALCONTROLSSERVICE_CONTRACTID, &kNS_PARENTALCONTROLSSERVICE_CID },
 #endif
   { NS_DOWNLOADMANAGER_CONTRACTID, &kNS_DOWNLOADMANAGER_CID },
-#ifndef MOZ_JSDOWNLOADS
   { NS_TRANSFER_CONTRACTID, &kNS_DOWNLOAD_CID },
-#endif
   { NS_FIND_SERVICE_CONTRACTID, &kNS_FIND_SERVICE_CID },
   { NS_TYPEAHEADFIND_CONTRACTID, &kNS_TYPEAHEADFIND_CID },
 #ifdef MOZ_URL_CLASSIFIER
   { NS_URLCLASSIFIERPREFIXSET_CONTRACTID, &kNS_URLCLASSIFIERPREFIXSET_CID },
   { NS_URLCLASSIFIERDBSERVICE_CONTRACTID, &kNS_URLCLASSIFIERDBSERVICE_CID },
   { NS_URICLASSIFIERSERVICE_CONTRACTID, &kNS_URLCLASSIFIERDBSERVICE_CID },
   { NS_URLCLASSIFIERSTREAMUPDATER_CONTRACTID, &kNS_URLCLASSIFIERSTREAMUPDATER_CID },
   { NS_URLCLASSIFIERUTILS_CONTRACTID, &kNS_URLCLASSIFIERUTILS_CID },
--- a/toolkit/components/jsdownloads/src/Downloads.manifest
+++ b/toolkit/components/jsdownloads/src/Downloads.manifest
@@ -1,2 +1,8 @@
 component {1b4c85df-cbdd-4bb6-b04e-613caece083c} DownloadLegacy.js
-contract @mozilla.org/transfer;1 {1b4c85df-cbdd-4bb6-b04e-613caece083c}
+
+# The following contract definition is commented out because the same contract
+# is also implemented in "toolkit/components/downloads".  To use the component
+# in this folder experimentally, the contract must be registered manually.  When
+# the other folder is not included in builds anymore (bug 851471), we'll be able
+# to define the contract implementation in this manifest.
+# contract @mozilla.org/transfer;1 {1b4c85df-cbdd-4bb6-b04e-613caece083c}
--- a/toolkit/components/jsdownloads/test/unit/head.js
+++ b/toolkit/components/jsdownloads/test/unit/head.js
@@ -14,16 +14,18 @@
 
 const Cc = Components.classes;
 const Ci = Components.interfaces;
 const Cu = Components.utils;
 const Cr = Components.results;
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
+XPCOMUtils.defineLazyModuleGetter(this, "DownloadPaths",
+                                  "resource://gre/modules/DownloadPaths.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Downloads",
                                   "resource://gre/modules/Downloads.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
                                   "resource://gre/modules/FileUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "HttpServer",
                                   "resource://testing-common/httpd.js");
 XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
                                   "resource://gre/modules/NetUtil.jsm");
@@ -66,36 +68,52 @@ const TEST_DATA_SHORT = "This test strin
 function run_test()
 {
   run_next_test();
 }
 
 ////////////////////////////////////////////////////////////////////////////////
 //// Support functions
 
+// While the previous test file should have deleted all the temporary files it
+// used, on Windows these might still be pending deletion on the physical file
+// system.  Thus, start from a new base number every time, to make a collision
+// with a file that is still pending deletion highly unlikely.
+let gFileCounter = Math.floor(Math.random() * 1000000);
+
 /**
- * Returns a reference to a temporary file.  The file is deleted if it already
- * exists.  If the file is then created by the test suite, it will be removed
- * when tests in this file finish.
+ * Returns a reference to a temporary file, that is guaranteed not to exist, and
+ * to have never been created before.
+ *
+ * @param aLeafName
+ *        Suggested leaf name for the file to be created.
+ *
+ * @return nsIFile pointing to a non-existent file in a temporary directory.
+ *
+ * @note It is not enough to delete the file if it exists, or to delete the file
+ *       after calling nsIFile.createUnique, because on Windows the delete
+ *       operation in the file system may still be pending, preventing a new
+ *       file with the same name to be created.
  */
 function getTempFile(aLeafName)
 {
-  let file = FileUtils.getFile("TmpD", [aLeafName]);
-  function GTF_removeFile()
-  {
+  // Prepend a serial number to the extension in the suggested leaf name.
+  let [base, ext] = DownloadPaths.splitBaseNameAndExtension(aLeafName);
+  let leafName = base + "-" + gFileCounter + ext;
+  gFileCounter++;
+
+  // Get a file reference under the temporary directory for this test file.
+  let file = FileUtils.getFile("TmpD", [leafName]);
+  do_check_false(file.exists());
+
+  do_register_cleanup(function () {
     if (file.exists()) {
       file.remove(false);
     }
-  }
-
-  // Remove the file in case a previous test created it.
-  GTF_removeFile();
-
-  // Remove the file at the end of the test suite.
-  do_register_cleanup(GTF_removeFile);
+  });
 
   return file;
 }
 
 /**
  * Waits for pending events to be processed.
  *
  * @return {Promise}
@@ -105,18 +123,17 @@ function getTempFile(aLeafName)
 function promiseExecuteSoon()
 {
   let deferred = Promise.defer();
   do_execute_soon(deferred.resolve);
   return deferred.promise;
 }
 
 /**
- * Creates a new Download object, using TEST_TARGET_FILE_NAME as the target.
- * The target is deleted by getTempFile when this function is called.
+ * Creates a new Download object, setting a temporary file as the target.
  *
  * @param aSourceURI
  *        The nsIURI for the download source, or null to use TEST_SOURCE_URI.
  *
  * @return {Promise}
  * @resolves The newly created Download object.
  * @rejects JavaScript exception.
  */
--- a/toolkit/components/jsdownloads/test/unit/test_DownloadCore.js
+++ b/toolkit/components/jsdownloads/test/unit/test_DownloadCore.js
@@ -297,17 +297,19 @@ add_task(function test_download_cancel_i
   } finally {
     deferResponse.resolve();
   }
 
   // Even if we canceled the download immediately, the HTTP request might have
   // been made, and the internal HTTP handler might be waiting to process it.
   // Thus, we process any pending events now, to avoid that the request is
   // processed during the tests that follow, interfering with them.
-  yield promiseExecuteSoon();
+  for (let i = 0; i < 5; i++) {
+    yield promiseExecuteSoon();
+  }
 });
 
 /**
  * Cancels and restarts a download sequentially.
  */
 add_task(function test_download_cancel_midway_restart()
 {
   let download = yield promiseSimpleDownload(TEST_INTERRUPTIBLE_URI);
@@ -384,17 +386,19 @@ add_task(function test_download_cancel_i
   do_check_eq(download.progress, 0);
   do_check_eq(download.totalBytes, 0);
   do_check_eq(download.currentBytes, 0);
 
   // Even if we canceled the download immediately, the HTTP request might have
   // been made, and the internal HTTP handler might be waiting to process it.
   // Thus, we process any pending events now, to avoid that the request is
   // processed during the tests that follow, interfering with them.
-  yield promiseExecuteSoon();
+  for (let i = 0; i < 5; i++) {
+    yield promiseExecuteSoon();
+  }
 
   // Ensure the next request is now allowed to complete, regardless of whether
   // the canceled request was received by the server or not.
   deferResponse.resolve();
 
   try {
     yield promiseAttempt;
     do_throw("The download should have been canceled.");
@@ -601,29 +605,36 @@ add_task(function test_download_error_so
 add_task(function test_download_error_target()
 {
   let download = yield promiseSimpleDownload();
 
   do_check_true(download.error === null);
 
   // Create a file without write access permissions before downloading.
   download.target.file.create(Ci.nsIFile.NORMAL_FILE_TYPE, 0);
+  try {
+    try {
+      yield download.start();
+      do_throw("The download should have failed.");
+    } catch (ex if ex instanceof Downloads.Error && ex.becauseTargetFailed) {
+      // A specific error object is thrown when writing to the target fails.
+    }
 
-  try {
-    yield download.start();
-    do_throw("The download should have failed.");
-  } catch (ex if ex instanceof Downloads.Error && ex.becauseTargetFailed) {
-    // A specific error object is thrown when writing to the target fails.
+    do_check_true(download.stopped);
+    do_check_false(download.canceled);
+    do_check_true(download.error !== null);
+    do_check_true(download.error.becauseTargetFailed);
+    do_check_false(download.error.becauseSourceFailed);
+  } finally {
+    // Restore the default permissions to allow deleting the file on Windows.
+    if (download.target.file.exists()) {
+      download.target.file.permissions = FileUtils.PERMS_FILE;
+      download.target.file.remove(false);
+    }
   }
-
-  do_check_true(download.stopped);
-  do_check_false(download.canceled);
-  do_check_true(download.error !== null);
-  do_check_true(download.error.becauseTargetFailed);
-  do_check_false(download.error.becauseSourceFailed);
 });
 
 /**
  * Restarts a failed download.
  */
 add_task(function test_download_error_restart()
 {
   let download = yield promiseSimpleDownload();
@@ -633,20 +644,28 @@ add_task(function test_download_error_re
   // Create a file without write access permissions before downloading.
   download.target.file.create(Ci.nsIFile.NORMAL_FILE_TYPE, 0);
 
   try {
     yield download.start();
     do_throw("The download should have failed.");
   } catch (ex if ex instanceof Downloads.Error && ex.becauseTargetFailed) {
     // A specific error object is thrown when writing to the target fails.
-  }
+  } finally {
+    // Restore the default permissions to allow deleting the file on Windows.
+    if (download.target.file.exists()) {
+      download.target.file.permissions = FileUtils.PERMS_FILE;
 
-  if (download.target.file.exists()) {
-    download.target.file.remove(false);
+      // Also for Windows, rename the file before deleting.  This makes the
+      // current file name available immediately for a new file, while deleting
+      // in place prevents creation of a file with the same name for some time.
+      let fileToRemove = download.target.file.clone();
+      fileToRemove.moveTo(null, fileToRemove.leafName + ".delete.tmp");
+      fileToRemove.remove(false);
+    }
   }
 
   // Restart the download and wait for completion.
   yield download.start();
 
   do_check_true(download.stopped);
   do_check_true(download.succeeded);
   do_check_false(download.canceled);
--- a/toolkit/components/jsdownloads/test/unit/test_DownloadLegacy.js
+++ b/toolkit/components/jsdownloads/test/unit/test_DownloadLegacy.js
@@ -28,17 +28,24 @@
  * @rejects Never.  The current test fails in case of exceptions.
  */
 function promiseStartLegacyDownload(aSourceURI, aOutPersist) {
   let sourceURI = aSourceURI || TEST_SOURCE_URI;
   let targetFile = getTempFile(TEST_TARGET_FILE_NAME);
 
   let persist = Cc["@mozilla.org/embedding/browser/nsWebBrowserPersist;1"]
                   .createInstance(Ci.nsIWebBrowserPersist);
-  let transfer = Cc["@mozilla.org/transfer;1"].createInstance(Ci.nsITransfer);
+
+  // We must create the nsITransfer implementation using its class ID because
+  // the "@mozilla.org/transfer;1" contract is currently implemented in
+  // "toolkit/components/downloads".  When the other folder is not included in
+  // builds anymore (bug 851471), we'll be able to use the contract ID.
+  let transfer =
+      Components.classesByID["{1b4c85df-cbdd-4bb6-b04e-613caece083c}"]
+                .createInstance(Ci.nsITransfer);
 
   if (aOutPersist) {
     aOutPersist.value = persist;
   }
 
   let deferred = Promise.defer();
 
   Downloads.getPublicDownloadList().then(function (aList) {
@@ -73,39 +80,43 @@ function promiseStartLegacyDownload(aSou
 ////////////////////////////////////////////////////////////////////////////////
 //// Tests
 
 /**
  * Executes a download controlled by the legacy nsITransfer interface.
  */
 add_task(function test_basic()
 {
-  let targetFile = getTempFile(TEST_TARGET_FILE_NAME);
+  let tempDirectory = FileUtils.getDir("TmpD", []);
 
   let download = yield promiseStartLegacyDownload();
 
   // Checks the generated DownloadSource and DownloadTarget properties.
   do_check_true(download.source.uri.equals(TEST_SOURCE_URI));
-  do_check_true(download.target.file.equals(targetFile));
+  do_check_true(download.target.file.parent.equals(tempDirectory));
 
-  // The download is already started, just wait for completion.
-  yield download.whenSucceeded();
+  // The download is already started, wait for completion and report any errors.
+  if (!download.stopped) {
+    yield download.start();
+  }
 
-  yield promiseVerifyContents(targetFile, TEST_DATA_SHORT);
+  yield promiseVerifyContents(download.target.file, TEST_DATA_SHORT);
 });
 
 /**
  * Checks final state and progress for a successful download.
  */
 add_task(function test_final_state()
 {
   let download = yield promiseStartLegacyDownload();
 
-  // The download is already started, just wait for completion.
-  yield download.whenSucceeded();
+  // The download is already started, wait for completion and report any errors.
+  if (!download.stopped) {
+    yield download.start();
+  }
 
   do_check_true(download.stopped);
   do_check_true(download.succeeded);
   do_check_false(download.canceled);
   do_check_true(download.error === null);
   do_check_eq(download.progress, 100);
 });
 
@@ -129,34 +140,39 @@ add_task(function test_intermediate_prog
     }
   };
 
   // Register for the notification, but also call the function directly in case
   // the download already reached the expected progress.
   download.onchange = onchange;
   onchange();
 
-  // The download is already started, just wait for completion.
-  yield download.whenSucceeded();
+  // The download is already started, wait for completion and report any errors.
+  if (!download.stopped) {
+    yield download.start();
+  }
 
   do_check_true(download.stopped);
   do_check_eq(download.progress, 100);
 
   yield promiseVerifyContents(download.target.file,
                               TEST_DATA_SHORT + TEST_DATA_SHORT);
 });
 
 /**
  * Downloads a file with a "Content-Length" of 0 and checks the progress.
  */
 add_task(function test_empty_progress()
 {
   let download = yield promiseStartLegacyDownload(TEST_EMPTY_URI);
 
-  yield download.whenSucceeded();
+  // The download is already started, wait for completion and report any errors.
+  if (!download.stopped) {
+    yield download.start();
+  }
 
   do_check_true(download.stopped);
   do_check_true(download.hasProgress);
   do_check_eq(download.progress, 100);
   do_check_eq(download.currentBytes, 0);
   do_check_eq(download.totalBytes, 0);
 
   do_check_eq(download.target.file.fileSize, 0);
@@ -179,19 +195,22 @@ add_task(function test_empty_noprogress(
   yield promiseExecuteSoon();
 
   // Check that this download has no progress report.
   do_check_false(download.stopped);
   do_check_false(download.hasProgress);
   do_check_eq(download.currentBytes, 0);
   do_check_eq(download.totalBytes, 0);
 
-  // Now allow the response to finish, and wait for the download to complete.
+  // Now allow the response to finish, and wait for the download to complete,
+  // while reporting any errors that may occur.
   deferResponse.resolve();
-  yield download.whenSucceeded();
+  if (!download.stopped) {
+    yield download.start();
+  }
 
   // Verify the state of the completed download.
   do_check_true(download.stopped);
   do_check_false(download.hasProgress);
   do_check_eq(download.progress, 100);
   do_check_eq(download.currentBytes, 0);
   do_check_eq(download.totalBytes, 0);
 
--- a/toolkit/components/moz.build
+++ b/toolkit/components/moz.build
@@ -16,16 +16,17 @@ PARALLEL_DIRS += [
     'console',
     'contentprefs',
     'cookie',
     'downloads',
     'exthelper',
     'filepicker',
     'find',
     'intl',
+    'jsdownloads',
     'mediasniffer',
     'microformats',
     'osfile',
     'parentalcontrols',
     'passwordmgr',
     'perf',
     'places',
     'prompts',
@@ -46,19 +47,16 @@ if CONFIG['BUILD_CTYPES']:
     PARALLEL_DIRS += ['ctypes']
 
 if CONFIG['MOZ_FEEDS']:
     PARALLEL_DIRS += ['feeds']
 
 if CONFIG['MOZ_HELP_VIEWER']:
     PARALLEL_DIRS += ['help']
 
-if CONFIG['MOZ_JSDOWNLOADS']:
-    PARALLEL_DIRS += ['jsdownloads']
-
 if CONFIG['NS_PRINTING']:
     PARALLEL_DIRS += ['printing']
 
 if CONFIG['MOZ_XUL']:
     PARALLEL_DIRS += ['autocomplete', 'satchel']
 
 if CONFIG['MOZ_TOOLKIT_SEARCH']:
     PARALLEL_DIRS += ['search']