Merge m-c to birch.
authorRyan VanderMeulen <ryanvm@gmail.com>
Fri, 26 Jul 2013 12:10:00 -0400
changeset 140130 343f7c10ed89efdaafca4367e595f3b083088ae3
parent 140129 5d6c8cda86088ff0aabc7e9d4012f83dc0d414f0 (current diff)
parent 140104 8da2f00eb92ea5dc7f8b0e2f312153f6c03fd607 (diff)
child 140131 9e8286127eb3960f1aa8fda6da1d1bce0bdc0292
push id1945
push userryanvm@gmail.com
push dateSat, 27 Jul 2013 02:27:26 +0000
treeherderfx-team@4874fa438b1c [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
milestone25.0a1
Merge m-c to birch.
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -723,28 +723,16 @@ const gFormSubmitObserver = {
     }
 
     this.panel.openPopup(element, position, offset, 0);
   }
 };
 
 var gBrowserInit = {
   onLoad: function() {
-    // window.arguments[0]: URI to load (string), or an nsISupportsArray of
-    //                      nsISupportsStrings to load, or a xul:tab of
-    //                      a tabbrowser, which will be replaced by this
-    //                      window (for this case, all other arguments are
-    //                      ignored).
-    //                 [1]: character set (string)
-    //                 [2]: referrer (nsIURI)
-    //                 [3]: postData (nsIInputStream)
-    //                 [4]: allowThirdPartyFixup (bool)
-    if ("arguments" in window && window.arguments[0])
-      var uriToLoad = window.arguments[0];
-
     gMultiProcessBrowser = gPrefService.getBoolPref("browser.tabs.remote");
 
     var mustLoadSidebar = false;
 
     Cc["@mozilla.org/eventlistenerservice;1"]
       .getService(Ci.nsIEventListenerService)
       .addSystemEventListener(gBrowser, "click", contentAreaClick, true);
 
@@ -775,16 +763,17 @@ var gBrowserInit = {
           .QueryInterface(Ci.nsIDocShellTreeItem).treeOwner
           .QueryInterface(Ci.nsIInterfaceRequestor)
           .getInterface(Ci.nsIXULWindow)
           .XULBrowserWindow = window.XULBrowserWindow;
     window.QueryInterface(Ci.nsIDOMChromeWindow).browserDOMWindow =
       new nsBrowserAccess();
 
     // set default character set if provided
+    // window.arguments[1]: character set (string)
     if ("arguments" in window && window.arguments.length > 1 && window.arguments[1]) {
       if (window.arguments[1].startsWith("charset=")) {
         var arrayArgComponents = window.arguments[1].split("=");
         if (arrayArgComponents) {
           //we should "inherit" the charset menu setting in a new window
           getMarkupDocumentViewer().defaultCharacterSet = arrayArgComponents[1];
         }
       }
@@ -941,28 +930,28 @@ var gBrowserInit = {
     // Misc. inits.
     CombinedStopReload.init();
     TabsOnTop.init();
     gPrivateBrowsingUI.init();
     TabsInTitlebar.init();
     retrieveToolbarIconsizesFromTheme();
 
     // Wait until chrome is painted before executing code not critical to making the window visible
-    this._boundDelayedStartup = this._delayedStartup.bind(this, uriToLoad, mustLoadSidebar);
+    this._boundDelayedStartup = this._delayedStartup.bind(this, mustLoadSidebar);
     window.addEventListener("MozAfterPaint", this._boundDelayedStartup);
 
     this._loadHandled = true;
   },
 
   _cancelDelayedStartup: function () {
     window.removeEventListener("MozAfterPaint", this._boundDelayedStartup);
     this._boundDelayedStartup = null;
   },
 
-  _delayedStartup: function(uriToLoad, mustLoadSidebar) {
+  _delayedStartup: function(mustLoadSidebar) {
     let tmp = {};
     Cu.import("resource://gre/modules/TelemetryTimestamps.jsm", tmp);
     let TelemetryTimestamps = tmp.TelemetryTimestamps;
     TelemetryTimestamps.add("delayedStartupStarted");
 
     this._cancelDelayedStartup();
 
     // We need to set the MozApplicationManifest event listeners up
@@ -971,16 +960,17 @@ var gBrowserInit = {
     // will be fired.
     gBrowser.addEventListener("MozApplicationManifest",
                               OfflineApps, false);
     // listen for offline apps on social
     let socialBrowser = document.getElementById("social-sidebar-browser");
     socialBrowser.addEventListener("MozApplicationManifest",
                               OfflineApps, false);
 
+    let uriToLoad = this._getUriToLoad();
     var isLoadingBlank = isBlankPageURL(uriToLoad);
 
     // This pageshow listener needs to be registered before we may call
     // swapBrowsersAndCloseOther() to receive pageshow events fired by that.
     gBrowser.addEventListener("pageshow", function(event) {
       // Filter out events that are not about the document load we are interested in
       if (content && event.target == content.document)
         setTimeout(pageShowEventHandlers, 0, event.persisted);
@@ -1007,16 +997,19 @@ var gBrowserInit = {
 
         // Stop the about:blank load
         gBrowser.stop();
         // make sure it has a docshell
         gBrowser.docShell;
 
         gBrowser.swapBrowsersAndCloseOther(gBrowser.selectedTab, uriToLoad);
       }
+      // window.arguments[2]: referrer (nsIURI)
+      //                 [3]: postData (nsIInputStream)
+      //                 [4]: allowThirdPartyFixup (bool)
       else if (window.arguments.length >= 3) {
         loadURI(uriToLoad, window.arguments[2], window.arguments[3] || null,
                 window.arguments[4] || false);
         window.focus();
       }
       // Note: loadOneOrMoreURIs *must not* be called if window.arguments.length >= 3.
       // Such callers expect that window.arguments[0] is handled as a single URI.
       else
@@ -1288,16 +1281,41 @@ var gBrowserInit = {
 
       setTimeout(function () { BrowserChromeTest.markAsReady(); }, 0);
     });
 
     Services.obs.notifyObservers(window, "browser-delayed-startup-finished", "");
     TelemetryTimestamps.add("delayedStartupFinished");
   },
 
+  // Returns the URI(s) to load at startup.
+  _getUriToLoad: function () {
+    // window.arguments[0]: URI to load (string), or an nsISupportsArray of
+    //                      nsISupportsStrings to load, or a xul:tab of
+    //                      a tabbrowser, which will be replaced by this
+    //                      window (for this case, all other arguments are
+    //                      ignored).
+    if (!window.arguments || !window.arguments[0])
+      return null;
+
+    let uri = window.arguments[0];
+    let sessionStartup = Cc["@mozilla.org/browser/sessionstartup;1"]
+                           .getService(Ci.nsISessionStartup);
+    let defaultArgs = Cc["@mozilla.org/browser/clh;1"]
+                        .getService(Ci.nsIBrowserHandler)
+                        .defaultArgs;
+
+    // If the given URI matches defaultArgs (the default homepage) we want
+    // to block its load if we're going to restore a session anyway.
+    if (uri == defaultArgs && sessionStartup.willOverrideHomepage)
+      return null;
+
+    return uri;
+  },
+
   onUnload: function() {
     // In certain scenarios it's possible for unload to be fired before onload,
     // (e.g. if the window is being closed after browser.js loads but before the
     // load completes). In that case, there's nothing to do here.
     if (!this._loadHandled)
       return;
 
     gDevToolsBrowser.forgetBrowserWindow(window);
--- a/browser/components/sessionstore/nsISessionStartup.idl
+++ b/browser/components/sessionstore/nsISessionStartup.idl
@@ -5,34 +5,48 @@
 #include "nsISupports.idl"
 
 /**
  * nsISessionStore keeps track of the current browsing state - i.e.
  * tab history, cookies, scroll state, form data, POSTDATA and window features
  * - and allows to restore everything into one window.
  */
 
-[scriptable, uuid(35235b39-7098-4b3b-8e28-cd004a88b06f)]
+[scriptable, uuid(51f4b9f0-f3d2-11e2-bb62-2c24dd830245)]
 interface nsISessionStartup: nsISupports
 {
   /**
    * Return a promise that is resolved once initialization
    * is complete.
    */
   readonly attribute jsval onceInitialized;
 
   // Get session state
   readonly attribute jsval state;
 
   /**
-   * Determine if session should be restored
+   * Determines whether there is a pending session restore and makes sure that
+   * we're initialized before returning. If we're not yet this will read the
+   * session file synchronously.
    */
   boolean doRestore();
 
   /**
+   * Returns whether we will restore a session that ends up replacing the
+   * homepage. The browser uses this to not start loading the homepage if
+   * we're going to stop its load anyway shortly after.
+   *
+   * This is meant to be an optimization for the average case that loading the
+   * session file finishes before we may want to start loading the default
+   * homepage. Should this be called before the session file has been read it
+   * will just return false.
+   */
+  readonly attribute bool willOverrideHomepage;
+
+  /**
    * What type of session we're restoring.
    * NO_SESSION       There is no data available from the previous session
    * RECOVER_SESSION  The last session crashed. It will either be restored or
    *                  about:sessionrestore will be shown.
    * RESUME_SESSION   The previous session should be restored at startup
    * DEFER_SESSION    The previous session is fine, but it shouldn't be restored
    *                  without explicit action (with the exception of pinned tabs)
    */
--- a/browser/components/sessionstore/src/nsSessionStartup.js
+++ b/browser/components/sessionstore/src/nsSessionStartup.js
@@ -157,25 +157,16 @@ SessionStartup.prototype = {
         this._sessionType = Ci.nsISessionStartup.RECOVER_SESSION;
       else if (!lastSessionCrashed && doResumeSession)
         this._sessionType = Ci.nsISessionStartup.RESUME_SESSION;
       else if (this._initialState)
         this._sessionType = Ci.nsISessionStartup.DEFER_SESSION;
       else
         this._initialState = null; // reset the state
 
-      // wait for the first browser window to open
-      // Don't reset the initial window's default args (i.e. the home page(s))
-      // if all stored tabs are pinned.
-      if (this.doRestore() &&
-          (!this._initialState.windows ||
-           !this._initialState.windows.every(function (win)
-             win.tabs.every(function (tab) tab.pinned))))
-        Services.obs.addObserver(this, "domwindowopened", true);
-
       Services.obs.addObserver(this, "sessionstore-windows-restored", true);
 
       if (this._sessionType != Ci.nsISessionStartup.NO_SESSION)
         Services.obs.addObserver(this, "browser:purge-session-history", true);
 
     } finally {
       // We're ready. Notify everyone else.
       Services.obs.notifyObservers(null, "sessionstore-state-finalized", "");
@@ -199,99 +190,86 @@ SessionStartup.prototype = {
       break;
     case "quit-application":
       // no reason for initializing at this point (cf. bug 409115)
       Services.obs.removeObserver(this, "final-ui-startup");
       Services.obs.removeObserver(this, "quit-application");
       if (this._sessionType != Ci.nsISessionStartup.NO_SESSION)
         Services.obs.removeObserver(this, "browser:purge-session-history");
       break;
-    case "domwindowopened":
-      var window = aSubject;
-      var self = this;
-      window.addEventListener("load", function() {
-        self._onWindowOpened(window);
-        window.removeEventListener("load", arguments.callee, false);
-      }, false);
-      break;
     case "sessionstore-windows-restored":
       Services.obs.removeObserver(this, "sessionstore-windows-restored");
       // free _initialState after nsSessionStore is done with it
       this._initialState = null;
       break;
     case "browser:purge-session-history":
       Services.obs.removeObserver(this, "browser:purge-session-history");
       // reset all state on sanitization
       this._sessionType = Ci.nsISessionStartup.NO_SESSION;
       break;
     }
   },
 
-  /**
-   * Removes the default arguments from the first browser window
-   * (and removes the "domwindowopened" observer afterwards).
-   */
-  _onWindowOpened: function sss_onWindowOpened(aWindow) {
-    var wType = aWindow.document.documentElement.getAttribute("windowtype");
-    if (wType != "navigator:browser")
-      return;
-
-    /**
-     * Note: this relies on the fact that nsBrowserContentHandler will return
-     * a different value the first time its getter is called after an update,
-     * due to its needHomePageOverride() logic. We don't want to remove the
-     * default arguments in the update case, since they include the "What's
-     * New" page.
-     *
-     * Since we're garanteed to be at least the second caller of defaultArgs
-     * (nsBrowserContentHandler calls it to determine which arguments to pass
-     * at startup), we know that if the window's arguments don't match the
-     * current defaultArguments, we're either in the update case, or we're
-     * launching a non-default browser window, so we shouldn't remove the
-     * window's arguments.
-     */
-    var defaultArgs = Cc["@mozilla.org/browser/clh;1"].
-                      getService(Ci.nsIBrowserHandler).defaultArgs;
-    if (aWindow.arguments && aWindow.arguments[0] &&
-        aWindow.arguments[0] == defaultArgs)
-      aWindow.arguments[0] = null;
-
-    try {
-      Services.obs.removeObserver(this, "domwindowopened");
-    } catch (e) {
-      // This might throw if we're removing the observer multiple times,
-      // but this is safe to ignore.
-    }
-  },
-
 /* ........ Public API ................*/
 
   get onceInitialized() {
     return gOnceInitializedDeferred.promise;
   },
 
   /**
    * Get the session state as a jsval
    */
   get state() {
     this._ensureInitialized();
     return this._initialState;
   },
 
   /**
-   * Determine whether there is a pending session restore.
+   * Determines whether there is a pending session restore and makes sure that
+   * we're initialized before returning. If we're not yet this will read the
+   * session file synchronously.
    * @returns bool
    */
   doRestore: function sss_doRestore() {
     this._ensureInitialized();
+    return this._willRestore();
+  },
+
+  /**
+   * Determines whether there is a pending session restore.
+   * @returns bool
+   */
+  _willRestore: function () {
     return this._sessionType == Ci.nsISessionStartup.RECOVER_SESSION ||
            this._sessionType == Ci.nsISessionStartup.RESUME_SESSION;
   },
 
   /**
+   * Returns whether we will restore a session that ends up replacing the
+   * homepage. The browser uses this to not start loading the homepage if
+   * we're going to stop its load anyway shortly after.
+   *
+   * This is meant to be an optimization for the average case that loading the
+   * session file finishes before we may want to start loading the default
+   * homepage. Should this be called before the session file has been read it
+   * will just return false.
+   *
+   * @returns bool
+   */
+  get willOverrideHomepage() {
+    if (this._initialState && this._willRestore()) {
+      let windows = this._initialState.windows || null;
+      // If there are valid windows with not only pinned tabs, signal that we
+      // will override the default homepage by restoring a session.
+      return windows && windows.some(w => w.tabs.some(t => !t.pinned));
+    }
+    return false;
+  },
+
+  /**
    * Get the type of pending session store, if any.
    */
   get sessionType() {
     this._ensureInitialized();
     return this._sessionType;
   },
 
   // Ensure that initialization is complete.
--- a/netwerk/protocol/http/UserAgentOverrides.jsm
+++ b/netwerk/protocol/http/UserAgentOverrides.jsm
@@ -13,17 +13,17 @@ Components.utils.import("resource://gre/
 
 const PREF_OVERRIDES_ENABLED = "general.useragent.site_specific_overrides";
 const DEFAULT_UA = Cc["@mozilla.org/network/protocol;1?name=http"]
                      .getService(Ci.nsIHttpProtocolHandler)
                      .userAgent;
 const MAX_OVERRIDE_FOR_HOST_CACHE_SIZE = 250;
 
 var gPrefBranch;
-var gOverrides;
+var gOverrides = new Map;
 var gOverrideForHostCache = new Map;
 var gInitialized = false;
 var gOverrideFunctions = [
   function (aHttpChannel) UserAgentOverrides.getOverrideForURI(aHttpChannel.URI)
 ];
 
 this.UserAgentOverrides = {
   init: function uao_init() {
@@ -58,20 +58,20 @@ this.UserAgentOverrides = {
     let host = aURI.asciiHost;
 
     let override = gOverrideForHostCache.get(host);
     if (override !== undefined)
       return override;
 
     override = null;
 
-    for (let domain in gOverrides) {
+    for (let [domain, userAgent] of gOverrides) {
       if (host == domain ||
           host.endsWith("." + domain)) {
-        override = gOverrides[domain];
+        override = userAgent;
         break;
       }
     }
 
     if (gOverrideForHostCache.size >= MAX_OVERRIDE_FOR_HOST_CACHE_SIZE) {
       gOverrideForHostCache.clear();
     }
     gOverrideForHostCache.set(host, override);
@@ -88,33 +88,40 @@ this.UserAgentOverrides = {
 
     Services.prefs.removeObserver(PREF_OVERRIDES_ENABLED, buildOverrides);
 
     Services.obs.removeObserver(HTTP_on_modify_request, "http-on-modify-request");
   }
 };
 
 function buildOverrides() {
-  gOverrides = {};
+  gOverrides.clear();
   gOverrideForHostCache.clear();
 
   if (!Services.prefs.getBoolPref(PREF_OVERRIDES_ENABLED))
     return;
 
+  let builtUAs = new Map;
   let domains = gPrefBranch.getChildList("");
 
   for (let domain of domains) {
     let override = gPrefBranch.getCharPref(domain);
+    let userAgent = builtUAs.get(override);
 
-    let [search, replace] = override.split("#", 2);
-    if (search && replace) {
-      gOverrides[domain] = DEFAULT_UA.replace(new RegExp(search, "g"), replace);
-    } else {
-      gOverrides[domain] = override;
+    if (userAgent === undefined) {
+      let [search, replace] = override.split("#", 2);
+      if (search && replace) {
+        userAgent = DEFAULT_UA.replace(new RegExp(search, "g"), replace);
+      } else {
+        userAgent = override;
+      }
+      builtUAs.set(override, userAgent);
     }
+
+    gOverrides.set(domain, userAgent);
   }
 }
 
 function HTTP_on_modify_request(aSubject, aTopic, aData) {
   let channel = aSubject.QueryInterface(Ci.nsIHttpChannel);
 
   for (let callback of gOverrideFunctions) {
     let modifiedUA = callback(channel, DEFAULT_UA);
--- a/toolkit/components/jsdownloads/src/DownloadCore.jsm
+++ b/toolkit/components/jsdownloads/src/DownloadCore.jsm
@@ -51,29 +51,40 @@ 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, "DownloadIntegration",
                                   "resource://gre/modules/DownloadIntegration.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
+                                  "resource://gre/modules/FileUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
                                   "resource://gre/modules/NetUtil.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "OS",
                                   "resource://gre/modules/osfile.jsm")
 XPCOMUtils.defineLazyModuleGetter(this, "Promise",
                                   "resource://gre/modules/commonjs/sdk/core/promise.js");
 XPCOMUtils.defineLazyModuleGetter(this, "Task",
                                   "resource://gre/modules/Task.jsm");
 
 const BackgroundFileSaverStreamListener = Components.Constructor(
       "@mozilla.org/network/background-file-saver;1?mode=streamlistener",
       "nsIBackgroundFileSaver");
 
+/**
+ * Returns true if the given value is a primitive string or a String object.
+ */
+function isString(aValue) {
+  // We cannot use the "instanceof" operator reliably across module boundaries.
+  return (typeof aValue == "string") ||
+         (typeof aValue == "object" && "charAt" in aValue);
+}
+
 ////////////////////////////////////////////////////////////////////////////////
 //// Download
 
 /**
  * Represents a single download, with associated state and actions.  This object
  * is transient, though it can be included in a DownloadList so that it can be
  * managed by the user interface and persisted across sessions.
  */
@@ -417,89 +428,224 @@ Download.prototype = {
       this.hasProgress = true;
       this.totalBytes = aTotalBytes;
       if (aTotalBytes > 0) {
         this.progress = Math.floor(aCurrentBytes / aTotalBytes * 100);
       }
     }
     this._notifyChange();
   },
+
+  /**
+   * Returns a static representation of the current object state.
+   *
+   * @return A JavaScript object that can be serialized to JSON.
+   */
+  toSerializable: function ()
+  {
+    let serializable = {
+      source: this.source.toSerializable(),
+      target: this.target.toSerializable(),
+    };
+
+    // Simplify the representation for the most common saver type.  If the saver
+    // is an object instead of a simple string, we can't simplify it because we
+    // need to persist all its properties, not only "type".  This may happen for
+    // savers of type "copy" as well as other types.
+    let saver = this.saver.toSerializable();
+    if (saver !== "copy") {
+      serializable.saver = saver;
+    }
+
+    return serializable;
+  },
+};
+
+/**
+ * Creates a new Download object from a serializable representation.  This
+ * function is used by the createDownload method of Downloads.jsm when a new
+ * Download object is requested, thus some properties may refer to live objects
+ * in place of their serializable representations.
+ *
+ * @param aSerializable
+ *        An object with the following fields:
+ *        {
+ *          source: DownloadSource object, or its serializable representation.
+ *                  See DownloadSource.fromSerializable for details.
+ *          target: DownloadTarget object, or its serializable representation.
+ *                  See DownloadTarget.fromSerializable for details.
+ *          saver: Serializable representation of a DownloadSaver object.  See
+ *                 DownloadSaver.fromSerializable for details.  If omitted,
+ *                 defaults to "copy".
+ *        }
+ *
+ * @return The newly created Download object.
+ */
+Download.fromSerializable = function (aSerializable) {
+  let download = new Download();
+  if (aSerializable.source instanceof DownloadSource) {
+    download.source = aSerializable.source;
+  } else {
+    download.source = DownloadSource.fromSerializable(aSerializable.source);
+  }
+  if (aSerializable.target instanceof DownloadTarget) {
+    download.target = aSerializable.target;
+  } else {
+    download.target = DownloadTarget.fromSerializable(aSerializable.target);
+  }
+  if ("saver" in aSerializable) {
+    download.saver = DownloadSaver.fromSerializable(aSerializable.saver);
+  } else {
+    download.saver = DownloadSaver.fromSerializable("copy");
+  }
+  download.saver.download = download;
+  return download;
 };
 
 ////////////////////////////////////////////////////////////////////////////////
 //// DownloadSource
 
 /**
  * Represents the source of a download, for example a document or an URI.
  */
 function DownloadSource() { }
 
 DownloadSource.prototype = {
   /**
-   * The nsIURI for the download source.
+   * String containing the URI for the download source.
    */
-  uri: null,
+  url: null,
 
   /**
    * Indicates whether the download originated from a private window.  This
-   * determines the context of the network request that is made to retrieve the 
+   * determines the context of the network request that is made to retrieve the
    * resource.
    */
   isPrivate: false,
 
   /**
-   * The nsIURI for the referrer of the download source, or null if no referrer
-   * should be sent or the download source is not HTTP.
+   * String containing the referrer URI of the download source, or null if no
+   * referrer should be sent or the download source is not HTTP.
    */
   referrer: null,
 
   /**
    * Returns a static representation of the current object state.
    *
    * @return A JavaScript object that can be serialized to JSON.
    */
-  serialize: function DS_serialize()
+  toSerializable: function ()
   {
-    let serialized = { uri: this.uri.spec };
+    // Simplify the representation if we don't have other details.
+    if (!this.isPrivate && !this.referrer) {
+      return this.url;
+    }
+
+    let serializable = { url: this.url };
     if (this.isPrivate) {
-      serialized.isPrivate = true;
+      serializable.isPrivate = true;
     }
     if (this.referrer) {
-      serialized.referrer = this.referrer.spec;
+      serializable.referrer = this.referrer;
     }
-    return serialized;
+    return serializable;
   },
 };
 
+/**
+ * Creates a new DownloadSource object from its serializable representation.
+ *
+ * @param aSerializable
+ *        Serializable representation of a DownloadSource object.  This may be a
+ *        string containing the URI for the download source, an nsIURI, or an
+ *        object with the following properties:
+ *        {
+ *          url: String containing the URI for the download source.
+ *          isPrivate: Indicates whether the download originated from a private
+ *                     window.  If omitted, the download is public.
+ *          referrer: String containing the referrer URI of the download source.
+ *                    Can be omitted or null if no referrer should be sent or
+ *                    the download source is not HTTP.
+ *        }
+ *
+ * @return The newly created DownloadSource object.
+ */
+DownloadSource.fromSerializable = function (aSerializable) {
+  let source = new DownloadSource();
+  if (isString(aSerializable)) {
+    source.url = aSerializable;
+  } else if (aSerializable instanceof Ci.nsIURI) {
+    source.url = aSerializable.spec;
+  } else {
+    source.url = aSerializable.url;
+    if ("isPrivate" in aSerializable) {
+      source.isPrivate = aSerializable.isPrivate;
+    }
+    if ("referrer" in aSerializable) {
+      source.referrer = aSerializable.referrer;
+    }
+  }
+  return source;
+};
+
 ////////////////////////////////////////////////////////////////////////////////
 //// DownloadTarget
 
 /**
  * Represents the target of a download, for example a file in the global
  * downloads directory, or a file in the system temporary directory.
  */
 function DownloadTarget() { }
 
 DownloadTarget.prototype = {
   /**
-   * The nsIFile for the download target.
+   * String containing the path of the target file.
    */
-  file: null,
+  path: null,
 
   /**
    * Returns a static representation of the current object state.
    *
    * @return A JavaScript object that can be serialized to JSON.
    */
-  serialize: function DT_serialize()
+  toSerializable: function ()
   {
-    return { file: this.file.path };
+    // Simplify the representation since we don't have other details for now.
+    return this.path;
   },
 };
 
+/**
+ * Creates a new DownloadTarget object from its serializable representation.
+ *
+ * @param aSerializable
+ *        Serializable representation of a DownloadTarget object.  This may be a
+ *        string containing the path of the target file, an nsIFile, or an
+ *        object with the following properties:
+ *        {
+ *          path: String containing the path of the target file.
+ *        }
+ *
+ * @return The newly created DownloadTarget object.
+ */
+DownloadTarget.fromSerializable = function (aSerializable) {
+  let target = new DownloadTarget();
+  if (isString(aSerializable)) {
+    target.path = aSerializable;
+  } else if (aSerializable instanceof Ci.nsIFile) {
+    // Read the "path" property of nsIFile after checking the object type.
+    target.path = aSerializable.path;
+  } else {
+    // Read the "path" property of the serializable DownloadTarget
+    // representation.
+    target.path = aSerializable.path;
+  }
+  return target;
+};
+
 ////////////////////////////////////////////////////////////////////////////////
 //// DownloadError
 
 /**
  * Provides detailed information about a download failure.
  *
  * @param aResult
  *        The result code associated with the error.
@@ -605,22 +751,49 @@ DownloadSaver.prototype = {
     throw new Error("Not implemented.");
   },
 
   /**
    * Returns a static representation of the current object state.
    *
    * @return A JavaScript object that can be serialized to JSON.
    */
-  serialize: function DS_serialize()
+  toSerializable: function ()
   {
     throw new Error("Not implemented.");
   },
 };
 
+/**
+ * Creates a new DownloadSaver object from its serializable representation.
+ *
+ * @param aSerializable
+ *        Serializable representation of a DownloadSaver object.  If no initial
+ *        state information for the saver object is needed, can be a string
+ *        representing the class of the download operation, for example "copy".
+ *
+ * @return The newly created DownloadSaver object.
+ */
+DownloadSaver.fromSerializable = function (aSerializable) {
+  let serializable = isString(aSerializable) ? { type: aSerializable }
+                                             : aSerializable;
+  let saver;
+  switch (serializable.type) {
+    case "copy":
+      saver = DownloadCopySaver.fromSerializable(serializable);
+      break;
+    case "legacy":
+      saver = DownloadLegacySaver.fromSerializable(serializable);
+      break;
+    default:
+      throw new Error("Unrecoginzed download saver type.");
+  }
+  return saver;
+};
+
 ////////////////////////////////////////////////////////////////////////////////
 //// DownloadCopySaver
 
 /**
  * Saver object that simply copies the entire source file to the target.
  */
 function DownloadCopySaver() { }
 
@@ -660,25 +833,26 @@ DownloadCopySaver.prototype = {
             // Infer the origin of the error from the failure code, because
             // BackgroundFileSaver does not provide more specific data.
             deferred.reject(new DownloadError(aStatus, null, true));
           }
         },
       };
 
       // Set the target file, that will be deleted if the download fails.
-      backgroundFileSaver.setTarget(download.target.file, false);
+      backgroundFileSaver.setTarget(new FileUtils.File(download.target.path),
+                                    false);
 
       // Create a channel from the source, and listen to progress notifications.
-      let channel = NetUtil.newChannel(download.source.uri);
+      let channel = NetUtil.newChannel(NetUtil.newURI(download.source.url));
       if (channel instanceof Ci.nsIPrivateBrowsingChannel) {
         channel.setPrivate(download.source.isPrivate);
       }
-      if (channel instanceof Ci.nsIHttpChannel) {
-        channel.referrer = download.source.referrer;
+      if (channel instanceof Ci.nsIHttpChannel && download.source.referrer) {
+        channel.referrer = NetUtil.newURI(download.source.referrer);
       }
 
       channel.notificationCallbacks = {
         QueryInterface: XPCOMUtils.generateQI([Ci.nsIInterfaceRequestor]),
         getInterface: XPCOMUtils.generateQI([Ci.nsIProgressEventSink]),
         onProgress: function DCSE_onProgress(aRequest, aContext, aProgress,
                                              aProgressMax)
         {
@@ -742,24 +916,39 @@ DownloadCopySaver.prototype = {
   {
     if (this._backgroundFileSaver) {
       this._backgroundFileSaver.finish(Cr.NS_ERROR_FAILURE);
       this._backgroundFileSaver = null;
     }
   },
 
   /**
-   * Implements "DownloadSaver.serialize".
+   * Implements "DownloadSaver.toSerializable".
    */
-  serialize: function DCS_serialize()
+  toSerializable: function ()
   {
-    return { type: "copy" };
+    // Simplify the representation since we don't have other details for now.
+    return "copy";
   },
 };
 
+/**
+ * Creates a new DownloadCopySaver object, with its initial state derived from
+ * its serializable representation.
+ *
+ * @param aSerializable
+ *        Serializable representation of a DownloadCopySaver object.
+ *
+ * @return The newly created DownloadCopySaver object.
+ */
+DownloadCopySaver.fromSerializable = function (aSerializable) {
+  // We don't have other state details for now.
+  return new DownloadCopySaver();
+};
+
 ////////////////////////////////////////////////////////////////////////////////
 //// DownloadLegacySaver
 
 /**
  * Saver object that integrates with the legacy nsITransfer interface.
  *
  * For more background on the process, see the DownloadLegacyTransfer object.
  */
@@ -867,17 +1056,17 @@ DownloadLegacySaver.prototype = {
           aSetProgressBytesFn(0, this.request.contentLength);
         }
 
         // The download implementation may not have created the target file if
         // no data was received from the source.  In this case, ensure that an
         // empty file is created as expected.
         try {
           // This atomic operation is more efficient than an existence check.
-          let file = yield OS.File.open(this.download.target.file.path,
+          let file = yield OS.File.open(this.download.target.path,
                                         { create: true });
           yield file.close();
         } catch (ex if ex instanceof OS.File.Error && ex.becauseExists) { }
       } finally {
         // We don't need the reference to the request anymore.
         this.request = null;
       }
     }.bind(this));
@@ -893,8 +1082,17 @@ DownloadLegacySaver.prototype = {
 
     // We don't necessarily receive status notifications after we call "cancel",
     // but cancellation through nsICancelable should be synchronous, thus force
     // the rejection of the execution promise immediately.
     this.deferExecuted.reject(new DownloadError(Cr.NS_ERROR_FAILURE,
                                                 "Download canceled."));
   },
 };
+
+/**
+ * Returns a new DownloadLegacySaver object.  This saver type has a
+ * deserializable form only when creating a new object in memory, because it
+ * cannot be serialized to disk.
+ */
+DownloadLegacySaver.fromSerializable = function () {
+  return new DownloadLegacySaver();
+};
--- a/toolkit/components/jsdownloads/src/DownloadIntegration.jsm
+++ b/toolkit/components/jsdownloads/src/DownloadIntegration.jsm
@@ -24,16 +24,18 @@ const Cu = Components.utils;
 const Cr = Components.results;
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "DownloadStore",
                                   "resource://gre/modules/DownloadStore.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
                                   "resource://gre/modules/FileUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
+                                  "resource://gre/modules/NetUtil.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "OS",
                                   "resource://gre/modules/osfile.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Promise",
                                   "resource://gre/modules/commonjs/sdk/core/promise.js");
 XPCOMUtils.defineLazyModuleGetter(this, "Services",
                                   "resource://gre/modules/Services.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Task",
                                   "resource://gre/modules/Task.jsm");
@@ -249,17 +251,18 @@ this.DownloadIntegration = {
     let isEnabled = gParentalControlsService &&
                     gParentalControlsService.parentalControlsEnabled;
     let shouldBlock = isEnabled &&
                       gParentalControlsService.blockFileDownloadsEnabled;
 
     // Log the event if required by parental controls settings.
     if (isEnabled && gParentalControlsService.loggingEnabled) {
       gParentalControlsService.log(gParentalControlsService.ePCLog_FileDownload,
-                                   shouldBlock, aDownload.source.uri, null);
+                                   shouldBlock,
+                                   NetUtil.newURI(aDownload.source.url), null);
     }
 
     return Promise.resolve(shouldBlock);
   },
 
   /**
    * Determines whether it's a Windows Metro app.
    */
--- a/toolkit/components/jsdownloads/src/DownloadLegacy.js
+++ b/toolkit/components/jsdownloads/src/DownloadLegacy.js
@@ -156,19 +156,19 @@ DownloadLegacyTransfer.prototype = {
 
   init: function DLT_init(aSource, aTarget, aDisplayName, aMIMEInfo, aStartTime,
                           aTempFile, aCancelable, aIsPrivate)
   {
     // Create a new Download object associated to a DownloadLegacySaver, and
     // wait for it to be available.  This operation may cause the entire
     // download system to initialize before the object is created.
     Downloads.createDownload({
-      source: { uri: aSource, isPrivate: aIsPrivate },
-      target: { file: aTarget.QueryInterface(Ci.nsIFileURL).file },
-      saver: { type: "legacy" },
+      source: { url: aSource.spec, isPrivate: aIsPrivate },
+      target: aTarget.QueryInterface(Ci.nsIFileURL).file,
+      saver: "legacy",
     }).then(function DLT_I_onDownload(aDownload) {
       // Now that the saver is available, hook up the cancellation handler.
       aDownload.saver.deferCanceled.promise.then(() => {
         // Only cancel if the object executing the download is still running.
         if (!this._componentFailed) {
           aCancelable.cancel(Cr.NS_ERROR_ABORT);
         }
       }).then(null, Cu.reportError);
--- a/toolkit/components/jsdownloads/src/DownloadList.jsm
+++ b/toolkit/components/jsdownloads/src/DownloadList.jsm
@@ -20,16 +20,18 @@ this.EXPORTED_SYMBOLS = [
 
 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, "NetUtil",
+                                  "resource://gre/modules/NetUtil.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
                                   "resource://gre/modules/PlacesUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Promise",
                                   "resource://gre/modules/commonjs/sdk/core/promise.js");
 XPCOMUtils.defineLazyModuleGetter(this, "Task",
                                   "resource://gre/modules/Task.jsm");
 
 ////////////////////////////////////////////////////////////////////////////////
@@ -229,17 +231,18 @@ DownloadList.prototype = {
   //// nsISupports
 
   QueryInterface: XPCOMUtils.generateQI([Ci.nsINavHistoryObserver]),
 
   ////////////////////////////////////////////////////////////////////////////
   //// nsINavHistoryObserver
 
   onDeleteURI: function DL_onDeleteURI(aURI, aGUID) {
-    this._removeWhere(download => aURI.equals(download.source.uri));
+    this._removeWhere(download => aURI.equals(NetUtil.newURI(
+                                                      download.source.url)));
   },
 
   onClearHistory: function DL_onClearHistory() {
     this._removeWhere(() => true);
   },
 
   onTitleChanged: function () {},
   onBeginUpdateBatch: function () {},
--- a/toolkit/components/jsdownloads/src/DownloadStore.jsm
+++ b/toolkit/components/jsdownloads/src/DownloadStore.jsm
@@ -2,16 +2,35 @@
 /* vim: set ts=2 et sw=2 tw=80 filetype=javascript: */
 /* 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/. */
 
 /**
  * Handles serialization of Download objects and persistence into a file, so
  * that the state of downloads can be restored across sessions.
+ *
+ * The file is stored in JSON format, without indentation.  With indentation
+ * applied, the file would look like this:
+ *
+ * {
+ *   "list": [
+ *     {
+ *       "source": "http://www.example.com/download.txt",
+ *       "target": "/home/user/Downloads/download.txt"
+ *     },
+ *     {
+ *       "source": {
+ *         "url": "http://www.example.com/download.txt",
+ *         "referrer": "http://www.example.com/referrer.html"
+ *       },
+ *       "target": "/home/user/Downloads/download-2.txt"
+ *     }
+ *   ]
+ * }
  */
 
 "use strict";
 
 this.EXPORTED_SYMBOLS = [
   "DownloadStore",
 ];
 
@@ -22,26 +41,21 @@ 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, "Downloads",
                                   "resource://gre/modules/Downloads.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
-                                  "resource://gre/modules/NetUtil.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "OS",
                                   "resource://gre/modules/osfile.jsm")
 XPCOMUtils.defineLazyModuleGetter(this, "Task",
                                   "resource://gre/modules/Task.jsm");
 
-const LocalFile = Components.Constructor("@mozilla.org/file/local;1",
-                                         "nsIFile", "initWithPath");
-
 XPCOMUtils.defineLazyGetter(this, "gTextDecoder", function () {
   return new TextDecoder();
 });
 
 XPCOMUtils.defineLazyGetter(this, "gTextEncoder", function () {
   return new TextEncoder();
 });
 
@@ -90,29 +104,19 @@ DownloadStore.prototype = {
       } catch (ex if ex instanceof OS.File.Error && ex.becauseNoSuchFile) {
         // If the file does not exist, there are no downloads to load.
         return;
       }
 
       let storeData = JSON.parse(gTextDecoder.decode(bytes));
 
       // Create live downloads based on the static snapshot.
-      for (let downloadData of storeData) {
+      for (let downloadData of storeData.list) {
         try {
-          let source = { uri: NetUtil.newURI(downloadData.source.uri) };
-          if ("referrer" in downloadData.source) {
-            source.referrer = NetUtil.newURI(downloadData.source.referrer);
-          }
-          let download = yield Downloads.createDownload({
-            source: source,
-            target: { file: new LocalFile(downloadData.target.file) },
-            saver: downloadData.saver,
-          });
-
-          this.list.add(download);
+          this.list.add(yield Downloads.createDownload(downloadData));
         } catch (ex) {
           // If an item is unrecognized, don't prevent others from being loaded.
           Cu.reportError(ex);
         }
       }
     }.bind(this));
   },
 
@@ -126,29 +130,25 @@ DownloadStore.prototype = {
    * @rejects JavaScript exception.
    */
   save: function DS_save()
   {
     return Task.spawn(function task_DS_save() {
       let downloads = yield this.list.getAll();
 
       // Take a static snapshot of the current state of all the downloads.
-      let storeData = [];
+      let storeData = { list: [] };
       let atLeastOneDownload = false;
       for (let download of downloads) {
         try {
-          storeData.push({
-            source: download.source.serialize(),
-            target: download.target.serialize(),
-            saver: download.saver.serialize(),
-          });
+          storeData.list.push(download.toSerializable());
           atLeastOneDownload = true;
         } catch (ex) {
-          // If an item cannot be serialized, don't prevent others from being
-          // saved.
+          // If an item cannot be converted to a serializable form, don't
+          // prevent others from being saved.
           Cu.reportError(ex);
         }
       }
 
       if (atLeastOneDownload) {
         // Create or overwrite the file if there are downloads to save.
         let bytes = gTextEncoder.encode(JSON.stringify(storeData));
         yield OS.File.writeAtomic(this.path, bytes,
--- a/toolkit/components/jsdownloads/src/Downloads.jsm
+++ b/toolkit/components/jsdownloads/src/Downloads.jsm
@@ -26,137 +26,105 @@ Cu.import("resource://gre/modules/XPCOMU
 Cu.import("resource://gre/modules/DownloadCore.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "DownloadIntegration",
                                   "resource://gre/modules/DownloadIntegration.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "DownloadList",
                                   "resource://gre/modules/DownloadList.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "DownloadUIHelper",
                                   "resource://gre/modules/DownloadUIHelper.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
-                                  "resource://gre/modules/FileUtils.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
-                                  "resource://gre/modules/NetUtil.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Promise",
                                   "resource://gre/modules/commonjs/sdk/core/promise.js");
-XPCOMUtils.defineLazyModuleGetter(this, "Services",
-                                  "resource://gre/modules/Services.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Task",
                                   "resource://gre/modules/Task.jsm");
 
 ////////////////////////////////////////////////////////////////////////////////
 //// Downloads
 
 /**
  * This object is exposed directly to the consumers of this JavaScript module,
  * and provides the only entry point to get references to back-end objects.
  */
 this.Downloads = {
   /**
    * Creates a new Download object.
    *
    * @param aProperties
    *        Provides the initial properties for the newly created download.
+   *        This matches the serializable representation of a Download object.
+   *        Some of the most common properties in this object include:
    *        {
-   *          source: {
-   *            uri: The nsIURI for the download source.
+   *          source: String containing the URI for the download source.
+   *                  Alternatively, may be an nsIURI, a DownloadSource object,
+   *                  or an object with the following properties:
+   *          {
+   *            url: String containing the URI for the download source.
    *            isPrivate: Indicates whether the download originated from a
-   *                       private window.
+   *                       private window.  If omitted, the download is public.
+   *            referrer: String containing the referrer URI of the download
+   *                      source.  Can be omitted or null if no referrer should
+   *                      be sent or the download source is not HTTP.
    *          },
-   *          target: {
-   *            file: The nsIFile for the download target.
+   *          target: String containing the path of the target file.
+   *                  Alternatively, may be an nsIFile, a DownloadTarget object,
+   *                  or an object with the following properties:
+   *          {
+   *            path: String containing the path of the target file.
    *          },
-   *          saver: {
-   *            type: String representing the class of download operation
-   *                  handled by this saver object, for example "copy".
-   *          },
+   *          saver: String representing the class of the download operation.
+   *                 If omitted, defaults to "copy".  Alternatively, may be the
+   *                 serializable representation of a DownloadSaver object.
    *        }
    *
    * @return {Promise}
    * @resolves The newly created Download object.
    * @rejects JavaScript exception.
    */
   createDownload: function D_createDownload(aProperties)
   {
-    return Task.spawn(function task_D_createDownload() {
-      let download = new Download();
-
-      download.source = new DownloadSource();
-      download.source.uri = aProperties.source.uri;
-      if ("isPrivate" in aProperties.source) {
-        download.source.isPrivate = aProperties.source.isPrivate;
-      }
-      if ("referrer" in aProperties.source) {
-        download.source.referrer = aProperties.source.referrer;
-      }
-      download.target = new DownloadTarget();
-      download.target.file = aProperties.target.file;
-
-      // Support for different aProperties.saver values isn't implemented yet.
-      download.saver = aProperties.saver.type == "legacy"
-                       ? new DownloadLegacySaver()
-                       : new DownloadCopySaver();
-      download.saver.download = download;
-
-      // This explicitly makes this function a generator for Task.jsm, so that
-      // exceptions in the above calls can be reported asynchronously.
-      yield;
-      throw new Task.Result(download);
-    });
+    try {
+      return Promise.resolve(Download.fromSerializable(aProperties));
+    } catch (ex) {
+      return Promise.reject(ex);
+    }
   },
 
   /**
    * Downloads data from a remote network location to a local file.
    *
    * This download method does not provide user interface or the ability to
    * cancel the download programmatically.  For that, you should obtain a
    * reference to a Download object using the createDownload function.
    *
    * @param aSource
-   *        The nsIURI or string containing the URI spec for the download
-   *        source, or alternative DownloadSource.
+   *        String containing the URI for the download source.  Alternatively,
+   *        may be an nsIURI or a DownloadSource object.
    * @param aTarget
-   *        The nsIFile or string containing the file path, or alternative
-   *        DownloadTarget.
+   *        String containing the path of the target file.  Alternatively, may
+   *        be an nsIFile or a DownloadTarget object.
    * @param aOptions
-   *        The object contains different additional options or null.
-   *        {  isPrivate: Indicates whether the download originated from a
-   *                      private window.
+   *        An optional object used to control the behavior of this function.
+   *        You may pass an object with a subset of the following fields:
+   *        {
+   *          isPrivate: Indicates whether the download originated from a
+   *                     private window.
    *        }
    *
    * @return {Promise}
    * @resolves When the download has finished successfully.
    * @rejects JavaScript exception if the download failed.
    */
   simpleDownload: function D_simpleDownload(aSource, aTarget, aOptions) {
-    // Wrap the arguments into simple objects resembling DownloadSource and
-    // DownloadTarget, if they are not objects of that type already.
-    if (aSource instanceof Ci.nsIURI) {
-      aSource = { uri: aSource };
-    } else if (typeof aSource == "string" ||
-               (typeof aSource == "object" && "charAt" in aSource)) {
-      aSource = { uri: NetUtil.newURI(aSource) };
-    }
-
-    if (aSource && aOptions && ("isPrivate" in aOptions)) {
-      aSource.isPrivate = aOptions.isPrivate;
-    }
-    if (aTarget instanceof Ci.nsIFile) {
-      aTarget = { file: aTarget };
-    } else if (typeof aTarget == "string" ||
-               (typeof aTarget == "object" && "charAt" in aTarget)) {
-      aTarget = { file: new FileUtils.File(aTarget) };
-    }
-
-    // Create and start the actual download.
     return this.createDownload({
       source: aSource,
       target: aTarget,
-      saver: { type: "copy" },
     }).then(function D_SD_onSuccess(aDownload) {
+      if (aOptions && ("isPrivate" in aOptions)) {
+        aDownload.source.isPrivate = aOptions.isPrivate;
+      }
       return aDownload.start();
     });
   },
 
   /**
    * Retrieves the DownloadList object for downloads that were not started from
    * a private browsing window.
    *
copy from toolkit/components/jsdownloads/test/unit/test_DownloadCore.js
copy to toolkit/components/jsdownloads/test/unit/common_test_Download.js
--- a/toolkit/components/jsdownloads/test/unit/test_DownloadCore.js
+++ b/toolkit/components/jsdownloads/test/unit/common_test_Download.js
@@ -1,331 +1,459 @@
 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
 /* vim: set ts=2 et sw=2 tw=80: */
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 /**
- * Tests the objects defined in the "DownloadCore" module.
+ * This script is loaded by "test_DownloadCore.js" and "test_DownloadLegacy.js"
+ * with different values of the gUseLegacySaver variable, to apply tests to both
+ * the "copy" and "legacy" saver implementations.
  */
 
 "use strict";
 
 ////////////////////////////////////////////////////////////////////////////////
+//// Globals
+
+/**
+ * Creates and starts a new download, using either DownloadCopySaver or
+ * DownloadLegacySaver based on the current test run.
+ *
+ * @return {Promise}
+ * @resolves The newly created Download object.  The download may be in progress
+ *           or already finished.  The promiseDownloadStopped function can be
+ *           used to wait for completion.
+ * @rejects JavaScript exception.
+ */
+function promiseStartDownload(aSourceUrl) {
+  if (gUseLegacySaver) {
+    return promiseStartLegacyDownload(aSourceUrl);
+  }
+
+  return promiseNewDownload(aSourceUrl).then(download => {
+    download.start();
+    return download;
+  });
+}
+
+/**
+ * Waits for a download to finish, in case it has not finished already.
+ *
+ * @param aDownload
+ *        The Download object to wait upon.
+ *
+ * @return {Promise}
+ * @resolves When the download has finished successfully.
+ * @rejects JavaScript exception if the download failed.
+ */
+function promiseDownloadStopped(aDownload) {
+  if (!aDownload.stopped) {
+    // The download is in progress, wait for the current attempt to finish and
+    // report any errors that may occur.
+    return aDownload.start();
+  }
+
+  if (aDownload.succeeded) {
+    return Promise.resolve();
+  }
+
+  // The download failed or was canceled.
+  return Promise.reject(aDownload.error || new Error("Download canceled."));
+}
+
+////////////////////////////////////////////////////////////////////////////////
 //// Tests
 
 /**
- * Executes a download, started by constructing the simplest Download object.
+ * Executes a download and checks its basic properties after construction.
+ * The download is started by constructing the simplest Download object with
+ * the "copy" saver, or using the legacy nsITransfer interface.
  */
-add_task(function test_download_construction()
+add_task(function test_basic()
 {
   let targetFile = getTempFile(TEST_TARGET_FILE_NAME);
 
-  let download = yield Downloads.createDownload({
-    source: { uri: TEST_SOURCE_URI },
-    target: { file: targetFile },
-    saver: { type: "copy" },
-  });
+  let download;
+  if (!gUseLegacySaver) {
+    // When testing DownloadCopySaver, we have control over the download, thus
+    // we can check its basic properties before it starts.
+    download = yield Downloads.createDownload({
+      source: { url: httpUrl("source.txt") },
+      target: { path: targetFile.path },
+      saver: { type: "copy" },
+    });
+
+    do_check_eq(download.source.url, httpUrl("source.txt"));
+    do_check_eq(download.target.path, targetFile.path);
 
-  // Checks the generated DownloadSource and DownloadTarget properties.
-  do_check_true(download.source.uri.equals(TEST_SOURCE_URI));
-  do_check_eq(download.target.file, targetFile);
+    yield download.start();
+  } else {
+    // When testing DownloadLegacySaver, the download is already started when it
+    // is created, thus we must check its basic properties while in progress.
+    download = yield promiseStartLegacyDownload(null,
+                                                { targetFile: targetFile });
+
+    do_check_eq(download.source.url, httpUrl("source.txt"));
+    do_check_eq(download.target.path, targetFile.path);
+
+    yield promiseDownloadStopped(download);
+  }
+
+  // Check additional properties on the finished download.
   do_check_true(download.source.referrer === null);
 
-  // Starts the download and waits for completion.
-  yield download.start();
-
-  yield promiseVerifyContents(targetFile, TEST_DATA_SHORT);
+  yield promiseVerifyContents(download.target.path, TEST_DATA_SHORT);
 });
 
 /**
  * Checks the referrer for downloads.
  */
-add_task(function test_download_referrer()
+add_task(function test_referrer()
 {
-  let source_path = "/test_download_referrer.txt";
-  let source_uri = NetUtil.newURI(HTTP_BASE + source_path);
-  let target_uri = getTempFile(TEST_TARGET_FILE_NAME);
+  let sourcePath = "/test_referrer.txt";
+  let sourceUrl = httpUrl("test_referrer.txt");
+  let targetPath = getTempFile(TEST_TARGET_FILE_NAME).path;
 
   function cleanup() {
-    gHttpServer.registerPathHandler(source_path, null);
+    gHttpServer.registerPathHandler(sourcePath, null);
   }
 
   do_register_cleanup(cleanup);
 
-  gHttpServer.registerPathHandler(source_path, function (aRequest, aResponse) {
+  gHttpServer.registerPathHandler(sourcePath, function (aRequest, aResponse) {
     aResponse.setHeader("Content-Type", "text/plain", false);
 
     do_check_true(aRequest.hasHeader("Referer"));
-    do_check_eq(aRequest.getHeader("Referer"), TEST_REFERRER_URI.spec);
+    do_check_eq(aRequest.getHeader("Referer"), TEST_REFERRER_URL);
   });
   let download = yield Downloads.createDownload({
-    source: { uri: source_uri, referrer: TEST_REFERRER_URI },
-    target: { file: target_uri },
-    saver: { type: "copy" },
+    source: { url: sourceUrl, referrer: TEST_REFERRER_URL },
+    target: targetPath,
   });
-  do_check_true(download.source.referrer.equals(TEST_REFERRER_URI));
+  do_check_eq(download.source.referrer, TEST_REFERRER_URL);
   yield download.start();
 
   download = yield Downloads.createDownload({
-    source: { uri: source_uri, referrer: TEST_REFERRER_URI, isPrivate: true },
-    target: { file: target_uri },
-    saver: { type: "copy" },
+    source: { url: sourceUrl, referrer: TEST_REFERRER_URL,
+              isPrivate: true },
+    target: targetPath,
   });
-  do_check_true(download.source.referrer.equals(TEST_REFERRER_URI));
+  do_check_eq(download.source.referrer, TEST_REFERRER_URL);
   yield download.start();
 
   // Test the download still works for non-HTTP channel with referrer.
-  source_uri = NetUtil.newURI("data:text/html,<html><body></body></html>");
+  sourceUrl = "data:text/html,<html><body></body></html>";
   download = yield Downloads.createDownload({
-    source: { uri: source_uri, referrer: TEST_REFERRER_URI },
-    target: { file: target_uri },
-    saver: { type: "copy" },
+    source: { url: sourceUrl, referrer: TEST_REFERRER_URL },
+    target: targetPath,
   });
-  do_check_true(download.source.referrer.equals(TEST_REFERRER_URI));
+  do_check_eq(download.source.referrer, TEST_REFERRER_URL);
   yield download.start();
 
   cleanup();
 });
 
 /**
  * Checks initial and final state and progress for a successful download.
  */
-add_task(function test_download_initial_final_state()
+add_task(function test_initial_final_state()
 {
-  let download = yield promiseSimpleDownload();
+  let download;
+  if (!gUseLegacySaver) {
+    // When testing DownloadCopySaver, we have control over the download, thus
+    // we can check its state before it starts.
+    download = yield promiseNewDownload();
 
-  do_check_true(download.stopped);
-  do_check_false(download.succeeded);
-  do_check_false(download.canceled);
-  do_check_true(download.error === null);
-  do_check_eq(download.progress, 0);
-  do_check_true(download.startTime === null);
+    do_check_true(download.stopped);
+    do_check_false(download.succeeded);
+    do_check_false(download.canceled);
+    do_check_true(download.error === null);
+    do_check_eq(download.progress, 0);
+    do_check_true(download.startTime === null);
 
-  // Starts the download and waits for completion.
-  yield download.start();
+    yield download.start();
+  } else {
+    // When testing DownloadLegacySaver, the download is already started when it
+    // is created, thus we cannot check its initial state.
+    download = yield promiseStartLegacyDownload();
+    yield promiseDownloadStopped(download);
+  }
 
   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);
   do_check_true(isValidDate(download.startTime));
 });
 
 /**
  * Checks the notification of the final download state.
  */
-add_task(function test_download_final_state_notified()
+add_task(function test_final_state_notified()
 {
-  let download = yield promiseSimpleDownload();
+  let deferResponse = deferNextResponse();
+
+  let download = yield promiseStartDownload(httpUrl("interruptible.txt"));
 
   let onchangeNotified = false;
   let lastNotifiedStopped;
   let lastNotifiedProgress;
   download.onchange = function () {
     onchangeNotified = true;
     lastNotifiedStopped = download.stopped;
     lastNotifiedProgress = download.progress;
   };
 
-  // Starts the download and waits for completion.
-  yield download.start();
+  // Allow the download to complete.
+  let promiseAttempt = download.start();
+  deferResponse.resolve();
+  yield promiseAttempt;
 
   // The view should have been notified before the download completes.
   do_check_true(onchangeNotified);
   do_check_true(lastNotifiedStopped);
   do_check_eq(lastNotifiedProgress, 100);
 });
 
 /**
  * Checks intermediate progress for a successful download.
  */
-add_task(function test_download_intermediate_progress()
+add_task(function test_intermediate_progress()
 {
   let deferResponse = deferNextResponse();
 
-  let download = yield promiseSimpleDownload(TEST_INTERRUPTIBLE_URI);
+  let download = yield promiseStartDownload(httpUrl("interruptible.txt"));
 
-  download.onchange = function () {
+  let onchange = function () {
     if (download.progress == 50) {
       do_check_true(download.hasProgress);
       do_check_eq(download.currentBytes, TEST_DATA_SHORT.length);
       do_check_eq(download.totalBytes, TEST_DATA_SHORT.length * 2);
 
       // Continue after the first chunk of data is fully received.
       deferResponse.resolve();
     }
   };
 
-  // Starts the download and waits for completion.
-  yield download.start();
+  // Register for the notification, but also call the function directly in case
+  // the download already reached the expected progress.
+  download.onchange = onchange;
+  onchange();
+
+  yield promiseDownloadStopped(download);
 
   do_check_true(download.stopped);
   do_check_eq(download.progress, 100);
 
-  yield promiseVerifyContents(download.target.file,
+  yield promiseVerifyContents(download.target.path,
                               TEST_DATA_SHORT + TEST_DATA_SHORT);
 });
 
 /**
  * Downloads a file with a "Content-Length" of 0 and checks the progress.
  */
-add_task(function test_download_empty_progress()
+add_task(function test_empty_progress()
 {
-  let download = yield promiseSimpleDownload(TEST_EMPTY_URI);
-
-  yield download.start();
+  let download = yield promiseStartDownload(httpUrl("empty.txt"));
+  yield promiseDownloadStopped(download);
 
   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);
+  do_check_eq((yield OS.File.stat(download.target.path)).size, 0);
 });
 
 /**
  * Downloads an empty file with no "Content-Length" and checks the progress.
  */
-add_task(function test_download_empty_noprogress()
+add_task(function test_empty_noprogress()
 {
   let deferResponse = deferNextResponse();
   let promiseEmptyRequestReceived = promiseNextRequestReceived();
 
-  let download = yield promiseSimpleDownload(TEST_EMPTY_NOPROGRESS_URI);
+  let download;
+  if (!gUseLegacySaver) {
+    // When testing DownloadCopySaver, we have control over the download, thus
+    // we can hook its onchange callback that will be notified when the
+    // download starts.
+    download = yield promiseNewDownload(httpUrl("empty-noprogress.txt"));
 
-  download.onchange = function () {
-    if (!download.stopped) {
-      do_check_false(download.hasProgress);
-      do_check_eq(download.currentBytes, 0);
-      do_check_eq(download.totalBytes, 0);
-    }
-  };
+    download.onchange = function () {
+      if (!download.stopped) {
+        do_check_false(download.hasProgress);
+        do_check_eq(download.currentBytes, 0);
+        do_check_eq(download.totalBytes, 0);
+      }
+    };
 
-  // Start the download, while waiting for the request to be received.
-  let promiseAttempt = download.start();
+    download.start();
+  } else {
+    // When testing DownloadLegacySaver, the download is already started when it
+    // is created, and it may have already made all needed property change
+    // notifications, thus there is no point in checking the onchange callback.
+    download = yield promiseStartLegacyDownload(
+                                       httpUrl("empty-noprogress.txt"));
+  }
 
   // Wait for the request to be received by the HTTP server, but don't allow the
   // request to finish yet.  Before checking the download state, wait for the
   // events to be processed by the client.
   yield promiseEmptyRequestReceived;
   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.
   deferResponse.resolve();
-  yield promiseAttempt;
+  yield promiseDownloadStopped(download);
 
   // 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);
 
-  do_check_eq(download.target.file.fileSize, 0);
+  do_check_eq((yield OS.File.stat(download.target.path)).size, 0);
 });
 
 /**
  * Calls the "start" method two times before the download is finished.
  */
-add_task(function test_download_start_twice()
+add_task(function test_start_twice()
 {
-  let download = yield promiseSimpleDownload(TEST_INTERRUPTIBLE_URI);
-
   // Ensure that the download cannot complete before start is called twice.
   let deferResponse = deferNextResponse();
 
+  let download;
+  if (!gUseLegacySaver) {
+    // When testing DownloadCopySaver, we have control over the download, thus
+    // we can start the download later during the test.
+    download = yield promiseNewDownload(httpUrl("interruptible.txt"));
+  } else {
+    // When testing DownloadLegacySaver, the download is already started when it
+    // is created.  Effectively, we are starting the download three times.
+    download = yield promiseStartLegacyDownload(httpUrl("interruptible.txt"));
+  }
+
   // Call the start method two times.
   let promiseAttempt1 = download.start();
   let promiseAttempt2 = download.start();
 
   // Allow the download to finish.
   deferResponse.resolve();
 
   // Both promises should now be resolved.
   yield promiseAttempt1;
   yield promiseAttempt2;
 
   do_check_true(download.stopped);
   do_check_true(download.succeeded);
   do_check_false(download.canceled);
   do_check_true(download.error === null);
 
-  yield promiseVerifyContents(download.target.file,
+  yield promiseVerifyContents(download.target.path,
                               TEST_DATA_SHORT + TEST_DATA_SHORT);
 });
 
 /**
  * Cancels a download and verifies that its state is reported correctly.
  */
-add_task(function test_download_cancel_midway()
+add_task(function test_cancel_midway()
 {
-  let download = yield promiseSimpleDownload(TEST_INTERRUPTIBLE_URI);
+  let deferResponse = deferNextResponse();
 
-  let deferResponse = deferNextResponse();
+  // In this test case, we execute different checks that are only possible with
+  // DownloadCopySaver or DownloadLegacySaver respectively.
+  let download;
+  let options = {};
+  if (!gUseLegacySaver) {
+    download = yield promiseNewDownload(httpUrl("interruptible.txt"));
+  } else {
+    download = yield promiseStartLegacyDownload(httpUrl("interruptible.txt"),
+                                                options);
+  }
+
   try {
     // Cancel the download after receiving the first part of the response.
     let deferCancel = Promise.defer();
-    download.onchange = function () {
+    let onchange = function () {
       if (!download.stopped && !download.canceled && download.progress == 50) {
         deferCancel.resolve(download.cancel());
 
         // The state change happens immediately after calling "cancel", but
         // temporary files or part files may still exist at this point.
         do_check_true(download.canceled);
       }
     };
 
-    let promiseAttempt = download.start();
+    // Register for the notification, but also call the function directly in
+    // case the download already reached the expected progress.  This may happen
+    // when using DownloadLegacySaver.
+    download.onchange = onchange;
+    onchange();
+
+    let promiseAttempt;
+    if (!gUseLegacySaver) {
+      promiseAttempt = download.start();
+    }
 
     // Wait on the promise returned by the "cancel" method to ensure that the
     // cancellation process finished and temporary files were removed.
     yield deferCancel.promise;
 
+    if (gUseLegacySaver) {
+      // The nsIWebBrowserPersist instance should have been canceled now.
+      do_check_eq(options.outPersist.result, Cr.NS_ERROR_ABORT);
+    }
+
     do_check_true(download.stopped);
     do_check_true(download.canceled);
     do_check_true(download.error === null);
 
-    do_check_false(download.target.file.exists());
+    do_check_false(yield OS.File.exists(download.target.path));
 
     // Progress properties are not reset by canceling.
     do_check_eq(download.progress, 50);
     do_check_eq(download.totalBytes, TEST_DATA_SHORT.length * 2);
     do_check_eq(download.currentBytes, TEST_DATA_SHORT.length);
 
-    // The promise returned by "start" should have been rejected meanwhile.
-    try {
-      yield promiseAttempt;
-      do_throw("The download should have been canceled.");
-    } catch (ex if ex instanceof Downloads.Error) {
-      do_check_false(ex.becauseSourceFailed);
-      do_check_false(ex.becauseTargetFailed);
+    if (!gUseLegacySaver) {
+      // The promise returned by "start" should have been rejected meanwhile.
+      try {
+        yield promiseAttempt;
+        do_throw("The download should have been canceled.");
+      } catch (ex if ex instanceof Downloads.Error) {
+        do_check_false(ex.becauseSourceFailed);
+        do_check_false(ex.becauseTargetFailed);
+      }
     }
   } finally {
     deferResponse.resolve();
   }
 });
 
 /**
  * Cancels a download right after starting it.
  */
-add_task(function test_download_cancel_immediately()
+add_task(function test_cancel_immediately()
 {
   // Ensure that the download cannot complete before cancel is called.
   let deferResponse = deferNextResponse();
   try {
-    let download = yield promiseSimpleDownload(TEST_INTERRUPTIBLE_URI);
+    let download = yield promiseStartDownload(httpUrl("interruptible.txt"));
 
     let promiseAttempt = download.start();
     do_check_false(download.stopped);
 
     let promiseCancel = download.cancel();
     do_check_true(download.canceled);
 
     // At this point, we don't know whether the download has already stopped or
@@ -338,17 +466,17 @@ add_task(function test_download_cancel_i
       do_check_false(ex.becauseSourceFailed);
       do_check_false(ex.becauseTargetFailed);
     }
 
     do_check_true(download.stopped);
     do_check_true(download.canceled);
     do_check_true(download.error === null);
 
-    do_check_false(download.target.file.exists());
+    do_check_false(yield OS.File.exists(download.target.path));
 
     // Check that the promise returned by the "cancel" method has been resolved.
     yield promiseCancel;
   } finally {
     deferResponse.resolve();
   }
 
   // Even if we canceled the download immediately, the HTTP request might have
@@ -358,19 +486,24 @@ add_task(function test_download_cancel_i
   for (let i = 0; i < 5; i++) {
     yield promiseExecuteSoon();
   }
 });
 
 /**
  * Cancels and restarts a download sequentially.
  */
-add_task(function test_download_cancel_midway_restart()
+add_task(function test_cancel_midway_restart()
 {
-  let download = yield promiseSimpleDownload(TEST_INTERRUPTIBLE_URI);
+  // TODO: Enable all the restart tests for DownloadLegacySaver.
+  if (gUseLegacySaver) {
+    return;
+  }
+
+  let download = yield promiseNewDownload(httpUrl("interruptible.txt"));
 
   // The first time, cancel the download midway.
   let deferResponse = deferNextResponse();
   try {
     let deferCancel = Promise.defer();
     download.onchange = function () {
       if (!download.stopped && !download.canceled && download.progress == 50) {
         deferCancel.resolve(download.cancel());
@@ -402,26 +535,31 @@ add_task(function test_download_cancel_m
 
   yield promiseAttempt;
 
   do_check_true(download.stopped);
   do_check_true(download.succeeded);
   do_check_false(download.canceled);
   do_check_true(download.error === null);
 
-  yield promiseVerifyContents(download.target.file,
+  yield promiseVerifyContents(download.target.path,
                               TEST_DATA_SHORT + TEST_DATA_SHORT);
 });
 
 /**
  * Cancels a download right after starting it, then restarts it immediately.
  */
-add_task(function test_download_cancel_immediately_restart_immediately()
+add_task(function test_cancel_immediately_restart_immediately()
 {
-  let download = yield promiseSimpleDownload(TEST_INTERRUPTIBLE_URI);
+  // TODO: Enable all the restart tests for DownloadLegacySaver.
+  if (gUseLegacySaver) {
+    return;
+  }
+
+  let download = yield promiseNewDownload(httpUrl("interruptible.txt"));
 
   // Ensure that the download cannot complete before cancel is called.
   let deferResponse = deferNextResponse();
 
   let promiseAttempt = download.start();
   do_check_false(download.stopped);
 
   download.cancel();
@@ -462,26 +600,31 @@ add_task(function test_download_cancel_i
 
   yield promiseRestarted;
 
   do_check_true(download.stopped);
   do_check_true(download.succeeded);
   do_check_false(download.canceled);
   do_check_true(download.error === null);
 
-  yield promiseVerifyContents(download.target.file,
+  yield promiseVerifyContents(download.target.path,
                               TEST_DATA_SHORT + TEST_DATA_SHORT);
 });
 
 /**
  * Cancels a download midway, then restarts it immediately.
  */
-add_task(function test_download_cancel_midway_restart_immediately()
+add_task(function test_cancel_midway_restart_immediately()
 {
-  let download = yield promiseSimpleDownload(TEST_INTERRUPTIBLE_URI);
+  // TODO: Enable all the restart tests for DownloadLegacySaver.
+  if (gUseLegacySaver) {
+    return;
+  }
+
+  let download = yield promiseNewDownload(httpUrl("interruptible.txt"));
 
   // The first time, cancel the download midway.
   let deferResponse = deferNextResponse();
 
   let deferMidway = Promise.defer();
   download.onchange = function () {
     if (!download.stopped && !download.canceled && download.progress == 50) {
       do_check_eq(download.progress, 50);
@@ -520,51 +663,49 @@ add_task(function test_download_cancel_m
 
   yield promiseRestarted;
 
   do_check_true(download.stopped);
   do_check_true(download.succeeded);
   do_check_false(download.canceled);
   do_check_true(download.error === null);
 
-  yield promiseVerifyContents(download.target.file,
+  yield promiseVerifyContents(download.target.path,
                               TEST_DATA_SHORT + TEST_DATA_SHORT);
 });
 
 /**
  * Calls the "cancel" method on a successful download.
  */
-add_task(function test_download_cancel_successful()
+add_task(function test_cancel_successful()
 {
-  let download = yield promiseSimpleDownload();
-
-  // Starts the download and waits for completion.
-  yield download.start();
+  let download = yield promiseStartDownload();
+  yield promiseDownloadStopped(download);
 
   // The cancel method should succeed with no effect.
   yield download.cancel();
 
   do_check_true(download.stopped);
   do_check_true(download.succeeded);
   do_check_false(download.canceled);
   do_check_true(download.error === null);
 
-  yield promiseVerifyContents(download.target.file, TEST_DATA_SHORT);
+  yield promiseVerifyContents(download.target.path, TEST_DATA_SHORT);
 });
 
 /**
  * Calls the "cancel" method two times in a row.
  */
-add_task(function test_download_cancel_twice()
+add_task(function test_cancel_twice()
 {
-  let download = yield promiseSimpleDownload(TEST_INTERRUPTIBLE_URI);
-
   // Ensure that the download cannot complete before cancel is called.
   let deferResponse = deferNextResponse();
   try {
+    let download = yield promiseStartDownload(httpUrl("interruptible.txt"));
+
     let promiseAttempt = download.start();
     do_check_false(download.stopped);
 
     let promiseCancel1 = download.cancel();
     do_check_true(download.canceled);
     let promiseCancel2 = download.cancel();
 
     try {
@@ -579,28 +720,33 @@ add_task(function test_download_cancel_t
     yield promiseCancel1;
     yield promiseCancel2;
 
     do_check_true(download.stopped);
     do_check_false(download.succeeded);
     do_check_true(download.canceled);
     do_check_true(download.error === null);
 
-    do_check_false(download.target.file.exists());
+    do_check_false(yield OS.File.exists(download.target.path));
   } finally {
     deferResponse.resolve();
   }
 });
 
 /**
  * Checks that whenSucceeded returns a promise that is resolved after a restart.
  */
-add_task(function test_download_whenSucceeded()
+add_task(function test_whenSucceeded_after_restart()
 {
-  let download = yield promiseSimpleDownload(TEST_INTERRUPTIBLE_URI);
+  // TODO: Enable all the restart tests for DownloadLegacySaver.
+  if (gUseLegacySaver) {
+    return;
+  }
+
+  let download = yield promiseNewDownload(httpUrl("interruptible.txt"));
 
   // Ensure that the download cannot complete before cancel is called.
   let deferResponse = deferNextResponse();
 
   // Get a reference before the first download attempt.
   let promiseSucceeded = download.whenSucceeded();
 
   // Cancel the first download attempt.
@@ -615,144 +761,179 @@ add_task(function test_download_whenSucc
   // Wait for the download to finish by waiting on the whenSucceeded promise.
   yield promiseSucceeded;
 
   do_check_true(download.stopped);
   do_check_true(download.succeeded);
   do_check_false(download.canceled);
   do_check_true(download.error === null);
 
-  yield promiseVerifyContents(download.target.file,
+  yield promiseVerifyContents(download.target.path,
                               TEST_DATA_SHORT + TEST_DATA_SHORT);
 });
 
 /**
  * Ensures download error details are reported on network failures.
  */
-add_task(function test_download_error_source()
+add_task(function test_error_source()
 {
   let serverSocket = startFakeServer();
   try {
-    let download = yield promiseSimpleDownload(TEST_FAKE_SOURCE_URI);
+    let sourceUrl = "http://localhost:" + serverSocket.port + "/source.txt";
+
+    let download;
+    try {
+      if (!gUseLegacySaver) {
+        // When testing DownloadCopySaver, we want to check that the promise
+        // returned by the "start" method is rejected.
+        download = yield promiseNewDownload(sourceUrl);
 
-    do_check_true(download.error === null);
+        do_check_true(download.error === null);
 
-    try {
-      yield download.start();
+        yield download.start();
+      } else {
+        // When testing DownloadLegacySaver, we cannot be sure whether we are
+        // testing the promise returned by the "start" method or we are testing
+        // the "error" property checked by promiseDownloadStopped.  This happens
+        // because we don't have control over when the download is started.
+        download = yield promiseStartLegacyDownload(sourceUrl);
+        yield promiseDownloadStopped(download);
+      }
       do_throw("The download should have failed.");
     } catch (ex if ex instanceof Downloads.Error && ex.becauseSourceFailed) {
       // A specific error object is thrown when reading from the source fails.
     }
 
+    // Check the properties now that the download stopped.
     do_check_true(download.stopped);
     do_check_false(download.canceled);
     do_check_true(download.error !== null);
     do_check_true(download.error.becauseSourceFailed);
     do_check_false(download.error.becauseTargetFailed);
   } finally {
     serverSocket.close();
   }
 });
 
 /**
  * Ensures download error details are reported on local writing failures.
  */
-add_task(function test_download_error_target()
+add_task(function test_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);
+  let targetFile = getTempFile(TEST_TARGET_FILE_NAME);
+  targetFile.create(Ci.nsIFile.NORMAL_FILE_TYPE, 0);
   try {
+    let download;
     try {
-      yield download.start();
+      if (!gUseLegacySaver) {
+        // When testing DownloadCopySaver, we want to check that the promise
+        // returned by the "start" method is rejected.
+        download = yield Downloads.createDownload({
+          source: httpUrl("source.txt"),
+          target: targetFile,
+        });
+        yield download.start();
+      } else {
+        // When testing DownloadLegacySaver, we cannot be sure whether we are
+        // testing the promise returned by the "start" method or we are testing
+        // the "error" property checked by promiseDownloadStopped.  This happens
+        // because we don't have control over when the download is started.
+        download = yield promiseStartLegacyDownload(null,
+                                                    { targetFile: targetFile });
+        yield promiseDownloadStopped(download);
+      }
       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.
     }
 
+    // Check the properties now that the download stopped.
     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);
+    if (targetFile.exists()) {
+      targetFile.permissions = FileUtils.PERMS_FILE;
+      targetFile.remove(false);
     }
   }
 });
 
 /**
  * Restarts a failed download.
  */
-add_task(function test_download_error_restart()
+add_task(function test_error_restart()
 {
-  let download = yield promiseSimpleDownload();
+  // TODO: Enable all the restart tests for DownloadLegacySaver.
+  if (gUseLegacySaver) {
+    return;
+  }
+
+  let download = yield promiseNewDownload();
 
   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);
+  let targetFile = new FileUtils.File(download.target.path);
+  targetFile.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 (targetFile.exists()) {
+      targetFile.permissions = FileUtils.PERMS_FILE;
 
       // 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);
+      targetFile.moveTo(null, targetFile.leafName + ".delete.tmp");
+      targetFile.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);
   do_check_true(download.error === null);
   do_check_eq(download.progress, 100);
 
-  yield promiseVerifyContents(download.target.file, TEST_DATA_SHORT);
+  yield promiseVerifyContents(download.target.path, TEST_DATA_SHORT);
 });
 
 /**
  * Executes download in both public and private modes.
  */
-add_task(function test_download_public_and_private()
+add_task(function test_public_and_private()
 {
-  let source_path = "/test_download_public_and_private.txt";
-  let source_uri = NetUtil.newURI(HTTP_BASE + source_path);
+  let sourcePath = "/test_public_and_private.txt";
+  let sourceUrl = httpUrl("test_public_and_private.txt");
   let testCount = 0;
 
   // Apply pref to allow all cookies.
   Services.prefs.setIntPref("network.cookie.cookieBehavior", 0);
 
   function cleanup() {
     Services.prefs.clearUserPref("network.cookie.cookieBehavior");
     Services.cookies.removeAll();
-    gHttpServer.registerPathHandler(source_path, null);
+    gHttpServer.registerPathHandler(sourcePath, null);
   }
   do_register_cleanup(cleanup);
 
-  gHttpServer.registerPathHandler(source_path, function (aRequest, aResponse) {
+  gHttpServer.registerPathHandler(sourcePath, function (aRequest, aResponse) {
     aResponse.setHeader("Content-Type", "text/plain", false);
 
     if (testCount == 0) {
       // No cookies should exist for first public download.
       do_check_false(aRequest.hasHeader("Cookie"));
       aResponse.setHeader("Set-Cookie", "foobar=1", false);
       testCount++;
     } else if (testCount == 1) {
@@ -762,34 +943,45 @@ add_task(function test_download_public_a
       testCount++;
     } else if (testCount == 2)  {
       // No cookies should exist for first private download.
       do_check_false(aRequest.hasHeader("Cookie"));
     }
   });
 
   let targetFile = getTempFile(TEST_TARGET_FILE_NAME);
-  yield Downloads.simpleDownload(source_uri, targetFile);
-  yield Downloads.simpleDownload(source_uri, targetFile);
-  let download = yield Downloads.createDownload({
-    source: { uri: source_uri, isPrivate: true },
-    target: { file: targetFile },
-    saver: { type: "copy" },
-  });
-  yield download.start();
+  yield Downloads.simpleDownload(sourceUrl, targetFile);
+  yield Downloads.simpleDownload(sourceUrl, targetFile);
+
+  if (!gUseLegacySaver) {
+    let download = yield Downloads.createDownload({
+      source: { url: sourceUrl, isPrivate: true },
+      target: targetFile,
+    });
+    yield download.start();
+  } else {
+    let download = yield promiseStartLegacyDownload(sourceUrl,
+                                                    { isPrivate: true });
+    yield promiseDownloadStopped(download);
+  }
 
   cleanup();
 });
 
 /**
  * Checks the startTime gets updated even after a restart.
  */
-add_task(function test_download_cancel_immediately_restart_and_check_startTime()
+add_task(function test_cancel_immediately_restart_and_check_startTime()
 {
-  let download = yield promiseSimpleDownload();
+  // TODO: Enable all the restart tests for DownloadLegacySaver.
+  if (gUseLegacySaver) {
+    return;
+  }
+
+  let download = yield promiseNewDownload();
 
   download.start();
   let startTime = download.startTime;
   do_check_true(isValidDate(download.startTime));
 
   yield download.cancel();
   do_check_eq(download.startTime.getTime(), startTime.getTime());
 
@@ -798,57 +990,58 @@ add_task(function test_download_cancel_i
 
   yield download.start();
   do_check_true(download.startTime.getTime() > startTime.getTime());
 });
 
 /**
  * Executes download with content-encoding.
  */
-add_task(function test_download_with_content_encoding()
+add_task(function test_with_content_encoding()
 {
-  let source_path = "/test_download_with_content_encoding.txt";
-  let source_uri = NetUtil.newURI(HTTP_BASE + source_path);
+  let sourcePath = "/test_with_content_encoding.txt";
+  let sourceUrl = httpUrl("test_with_content_encoding.txt");
 
   function cleanup() {
-    gHttpServer.registerPathHandler(source_path, null);
+    gHttpServer.registerPathHandler(sourcePath, null);
   }
   do_register_cleanup(cleanup);
 
-  gHttpServer.registerPathHandler(source_path, function (aRequest, aResponse) {
+  gHttpServer.registerPathHandler(sourcePath, function (aRequest, aResponse) {
     aResponse.setHeader("Content-Type", "text/plain", false);
     aResponse.setHeader("Content-Encoding", "gzip", false);
     aResponse.setHeader("Content-Length",
                         "" + TEST_DATA_SHORT_GZIP_ENCODED.length, false);
 
     let bos =  new BinaryOutputStream(aResponse.bodyOutputStream);
     bos.writeByteArray(TEST_DATA_SHORT_GZIP_ENCODED,
                        TEST_DATA_SHORT_GZIP_ENCODED.length);
   });
 
-  let download = yield Downloads.createDownload({
-    source: { uri: source_uri },
-    target: { file: getTempFile(TEST_TARGET_FILE_NAME) },
-    saver: { type: "copy" },
-  });
-  yield download.start();
+  let download = yield promiseStartDownload(sourceUrl);
+  yield promiseDownloadStopped(download);
 
   do_check_eq(download.progress, 100);
   do_check_eq(download.totalBytes, TEST_DATA_SHORT_GZIP_ENCODED.length);
 
   // Ensure the content matches the decoded test data.
-  yield promiseVerifyContents(download.target.file, TEST_DATA_SHORT);
+  yield promiseVerifyContents(download.target.path, TEST_DATA_SHORT);
 });
 
 /**
  * Cancels and restarts a download sequentially with content-encoding.
  */
-add_task(function test_download_cancel_midway_restart_with_content_encoding()
+add_task(function test_cancel_midway_restart_with_content_encoding()
 {
-  let download = yield promiseSimpleDownload(TEST_INTERRUPTIBLE_GZIP_URI);
+  // TODO: Enable all the restart tests for DownloadLegacySaver.
+  if (gUseLegacySaver) {
+    return;
+  }
+
+  let download = yield promiseNewDownload(httpUrl("interruptible_gzip.txt"));
 
   // The first time, cancel the download midway.
   let deferResponse = deferNextResponse();
   try {
     let deferCancel = Promise.defer();
     download.onchange = function () {
       if (!download.stopped && !download.canceled &&
           download.currentBytes == TEST_DATA_SHORT_GZIP_ENCODED_FIRST.length) {
@@ -865,33 +1058,48 @@ add_task(function test_download_cancel_m
 
   // The second time, we'll provide the entire interruptible response.
   download.onchange = null;
   yield download.start()
 
   do_check_eq(download.progress, 100);
   do_check_eq(download.totalBytes, TEST_DATA_SHORT_GZIP_ENCODED.length);
 
-  yield promiseVerifyContents(download.target.file, TEST_DATA_SHORT);
+  yield promiseVerifyContents(download.target.path, TEST_DATA_SHORT);
 });
 
 /**
  * Download with parental controls enabled.
  */
-add_task(function test_download_blocked_parental_controls()
+add_task(function test_blocked_parental_controls()
 {
   function cleanup() {
     DownloadIntegration.shouldBlockInTest = false;
   }
   do_register_cleanup(cleanup);
   DownloadIntegration.shouldBlockInTest = true;
 
-  let download = yield promiseSimpleDownload();
-
+  let download;
   try {
-    yield download.start();
+    if (!gUseLegacySaver) {
+      // When testing DownloadCopySaver, we want to check that the promise
+      // returned by the "start" method is rejected.
+      download = yield promiseNewDownload();
+      yield download.start();
+    } else {
+      // When testing DownloadLegacySaver, we cannot be sure whether we are
+      // testing the promise returned by the "start" method or we are testing
+      // the "error" property checked by promiseDownloadStopped.  This happens
+      // because we don't have control over when the download is started.
+      download = yield promiseStartLegacyDownload();
+      yield promiseDownloadStopped(download);
+    }
     do_throw("The download should have blocked.");
   } catch (ex if ex instanceof Downloads.Error && ex.becauseBlocked) {
     do_check_true(ex.becauseBlockedByParentalControls);
   }
+
+  // Now that the download stopped, the target file should not exist.
+  do_check_false(yield OS.File.exists(download.target.path));
+
   cleanup();
 });
 
--- a/toolkit/components/jsdownloads/test/unit/head.js
+++ b/toolkit/components/jsdownloads/test/unit/head.js
@@ -46,61 +46,21 @@ const ServerSocket = Components.Construc
                                 "@mozilla.org/network/server-socket;1",
                                 "nsIServerSocket",
                                 "init");
 const BinaryOutputStream = Components.Constructor(
                                       "@mozilla.org/binaryoutputstream;1",
                                       "nsIBinaryOutputStream",
                                       "setOutputStream")
 
-Object.defineProperty(this, "HTTP_BASE", {get: function() {
-  return "http://localhost:" + gHttpServer.identity.primaryPort;
-}});
-
-Object.defineProperty(this, "FAKE_BASE", {get: function() {
-  return "http://localhost:" + gFakeServerPort;
-}});
-
-Object.defineProperty(this, "TEST_REFERRER_URI", {get: function() {
-  return NetUtil.newURI(HTTP_BASE + "/referrer.html");
-}});
-
-Object.defineProperty(this, "TEST_SOURCE_URI", {get: function() {
-  return NetUtil.newURI(HTTP_BASE + "/source.txt");
-}});
-
-Object.defineProperty(this, "TEST_EMPTY_URI", {get: function() {
-  return NetUtil.newURI(HTTP_BASE + "/empty.txt");
-}});
-
-Object.defineProperty(this, "TEST_FAKE_SOURCE_URI", {get: function() {
-  return NetUtil.newURI(FAKE_BASE + "/source.txt");
-}});
-
-const TEST_EMPTY_NOPROGRESS_PATH = "/empty-noprogress.txt";
-
-Object.defineProperty(this, "TEST_EMPTY_NOPROGRESS_URI", {get: function() {
-  return NetUtil.newURI(HTTP_BASE + TEST_EMPTY_NOPROGRESS_PATH);
-}});
-
-const TEST_INTERRUPTIBLE_PATH = "/interruptible.txt";
-
-Object.defineProperty(this, "TEST_INTERRUPTIBLE_URI", {get: function() {
-  return NetUtil.newURI(HTTP_BASE + TEST_INTERRUPTIBLE_PATH);
-}});
-
-const TEST_INTERRUPTIBLE_GZIP_PATH = "/interruptible_gzip.txt";
-
-Object.defineProperty(this, "TEST_INTERRUPTIBLE_GZIP_URI", {get: function() {
-  return NetUtil.newURI(HTTP_BASE + TEST_INTERRUPTIBLE_GZIP_PATH);
-}});
-
 const TEST_TARGET_FILE_NAME = "test-download.txt";
 const TEST_STORE_FILE_NAME = "test-downloads.json";
 
+const TEST_REFERRER_URL = "http://www.example.com/referrer.html";
+
 const TEST_DATA_SHORT = "This test string is downloaded.";
 // Generate using gzipCompressString in TelemetryPing.js.
 const TEST_DATA_SHORT_GZIP_ENCODED_FIRST = [
  31,139,8,0,0,0,0,0,0,3,11,201,200,44,86,40,73,45,46,81,40,46,41,202,204
 ];
 const TEST_DATA_SHORT_GZIP_ENCODED_SECOND = [
   75,87,0,114,83,242,203,243,114,242,19,83,82,83,244,0,151,222,109,43,31,0,0,0
 ];
@@ -114,16 +74,30 @@ function run_test()
 {
   do_get_profile();
   run_next_test();
 }
 
 ////////////////////////////////////////////////////////////////////////////////
 //// Support functions
 
+/**
+ * HttpServer object initialized before tests start.
+ */
+let gHttpServer;
+
+/**
+ * Given a file name, returns a string containing an URI that points to the file
+ * on the currently running instance of the test HTTP server.
+ */
+function httpUrl(aFileName) {
+  return "http://localhost:" + gHttpServer.identity.primaryPort + "/" +
+         aFileName;
+}
+
 // 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, that is guaranteed not to exist, and
@@ -185,32 +159,112 @@ function promiseTimeout(aTime)
   let deferred = Promise.defer();
   do_timeout(aTime, deferred.resolve);
   return deferred.promise;
 }
 
 /**
  * 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.
+ * @param aSourceUrl
+ *        String containing the URI for the download source, or null to use
+ *        httpUrl("source.txt").
  *
  * @return {Promise}
  * @resolves The newly created Download object.
  * @rejects JavaScript exception.
  */
-function promiseSimpleDownload(aSourceURI) {
+function promiseNewDownload(aSourceUrl) {
   return Downloads.createDownload({
-    source: { uri: aSourceURI || TEST_SOURCE_URI },
-    target: { file: getTempFile(TEST_TARGET_FILE_NAME) },
-    saver: { type: "copy" },
+    source: aSourceUrl || httpUrl("source.txt"),
+    target: getTempFile(TEST_TARGET_FILE_NAME),
   });
 }
 
 /**
+ * Starts a new download using the nsIWebBrowserPersist interface, and controls
+ * it using the legacy nsITransfer interface.
+ *
+ * @param aSourceUrl
+ *        String containing the URI for the download source, or null to use
+ *        httpUrl("source.txt").
+ * @param aOptions
+ *        An optional object used to control the behavior of this function.
+ *        You may pass an object with a subset of the following fields:
+ *        {
+ *          isPrivate: Boolean indicating whether the download originated from a
+ *                     private window.
+ *          targetFile: nsIFile for the target, or null to use a temporary file.
+ *          outPersist: Receives a reference to the created nsIWebBrowserPersist
+ *                      instance.
+ *        }
+ *
+ * @return {Promise}
+ * @resolves The Download object created as a consequence of controlling the
+ *           download through the legacy nsITransfer interface.
+ * @rejects Never.  The current test fails in case of exceptions.
+ */
+function promiseStartLegacyDownload(aSourceUrl, aOptions) {
+  let sourceURI = NetUtil.newURI(aSourceUrl || httpUrl("source.txt"));
+  let targetFile = (aOptions && aOptions.targetFile)
+                   || getTempFile(TEST_TARGET_FILE_NAME);
+
+  let persist = Cc["@mozilla.org/embedding/browser/nsWebBrowserPersist;1"]
+                  .createInstance(Ci.nsIWebBrowserPersist);
+  if (aOptions) {
+    aOptions.outPersist = persist;
+  }
+
+  // Apply decoding if required by the "Content-Encoding" header.
+  persist.persistFlags &= ~Ci.nsIWebBrowserPersist.PERSIST_FLAGS_NO_CONVERSION;
+
+  // 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);
+
+  let deferred = Promise.defer();
+
+  let isPrivate = aOptions && aOptions.isPrivate;
+  let promise = isPrivate ? Downloads.getPrivateDownloadList()
+                          : Downloads.getPublicDownloadList();
+  promise.then(function (aList) {
+    // Temporarily register a view that will get notified when the download we
+    // are controlling becomes visible in the list of downloads.
+    aList.addView({
+      onDownloadAdded: function (aDownload) {
+        aList.removeView(this);
+
+        // Remove the download to keep the list empty for the next test.  This
+        // also allows the caller to register the "onchange" event directly.
+        aList.remove(aDownload);
+
+        // When the download object is ready, make it available to the caller.
+        deferred.resolve(aDownload);
+      },
+    });
+
+    // Initialize the components so they reference each other.  This will cause
+    // the Download object to be created and added to the public downloads.
+    transfer.init(sourceURI, NetUtil.newURI(targetFile), null, null, null, null,
+                  persist, isPrivate);
+    persist.progressListener = transfer;
+
+    // Start the actual download process.
+    persist.savePrivacyAwareURI(sourceURI, null, null, null, null, targetFile,
+                                isPrivate);
+  }.bind(this)).then(null, do_report_unexpected_exception);
+
+  return deferred.promise;
+}
+
+/**
  * Returns a new public DownloadList object.
  *
  * @return {Promise}
  * @resolves The newly created DownloadList object.
  * @rejects JavaScript exception.
  */
 function promiseNewDownloadList() {
   // Force the creation of a new public download list.
@@ -229,29 +283,31 @@ function promiseNewPrivateDownloadList()
   // Force the creation of a new public download list.
   Downloads._privateDownloadList = null;
   return Downloads.getPrivateDownloadList();
 }
 
 /**
  * Ensures that the given file contents are equal to the given string.
  *
- * @param aFile
- *        nsIFile whose contents should be verified.
+ * @param aPath
+ *        String containing the path of the file whose contents should be
+ *        verified.
  * @param aExpectedContents
  *        String containing the octets that are expected in the file.
  *
  * @return {Promise}
  * @resolves When the operation completes.
  * @rejects Never.
  */
-function promiseVerifyContents(aFile, aExpectedContents)
+function promiseVerifyContents(aPath, aExpectedContents)
 {
   let deferred = Promise.defer();
-  NetUtil.asyncFetch(aFile, function(aInputStream, aStatus) {
+  let file = new FileUtils.File(aPath);
+  NetUtil.asyncFetch(file, function(aInputStream, aStatus) {
     do_check_true(Components.isSuccessCode(aStatus));
     let contents = NetUtil.readInputStreamToString(aInputStream,
                                                    aInputStream.available());
     if (contents.length <= TEST_DATA_SHORT.length * 2) {
       do_check_eq(contents, aExpectedContents);
     } else {
       // Do not print the entire content string to the test log.
       do_check_eq(contents.length, aExpectedContents.length);
@@ -260,27 +316,28 @@ function promiseVerifyContents(aFile, aE
     deferred.resolve();
   });
   return deferred.promise;
 }
 
 /**
  * Adds entry for download.
  *
- * @param aSourceURI
- *        The nsIURI for the download source, or null to use TEST_SOURCE_URI.
+ * @param aSourceUrl
+ *        String containing the URI for the download source, or null to use
+ *        httpUrl("source.txt").
  *
  * @return {Promise}
  * @rejects JavaScript exception.
  */
-function promiseAddDownloadToHistory(aSourceURI) {
+function promiseAddDownloadToHistory(aSourceUrl) {
   let deferred = Promise.defer();
   PlacesUtils.asyncHistory.updatePlaces(
     {
-      uri: aSourceURI || TEST_SOURCE_URI,
+      uri: NetUtil.newURI(aSourceUrl || httpUrl("source.txt")),
       visits: [{
         transitionType: Ci.nsINavHistoryService.TRANSITION_DOWNLOAD,
         visitDate:  Date.now()
       }]
     },
     {
       handleError: function handleError(aResultCode, aPlaceInfo) {
         let ex = new Components.Exception("Unexpected error in adding visits.",
@@ -299,17 +356,16 @@ function promiseAddDownloadToHistory(aSo
  * Starts a socket listener that closes each incoming connection.
  *
  * @returns nsIServerSocket that listens for connections.  Call its "close"
  *          method to stop listening and free the server port.
  */
 function startFakeServer()
 {
   let serverSocket = new ServerSocket(-1, true, -1);
-  gFakeServerPort = serverSocket.port;
   serverSocket.asyncListen({
     onSocketAccepted: function (aServ, aTransport) {
       aTransport.close(Cr.NS_BINDING_ABORTED);
     },
     onStopListening: function () { },
   });
   return serverSocket;
 }
@@ -321,20 +377,20 @@ function startFakeServer()
  * Normally, the internal HTTP server returns all the available data as soon as
  * a request is received.  In order for some requests to be served one part at a
  * time, special interruptible handlers are registered on the HTTP server.
  *
  * Before making a request to one of the addresses served by the interruptible
  * handlers, you may call "deferNextResponse" to get a reference to an object
  * that allows you to control the next request.
  *
- * For example, the handler accessible at the TEST_INTERRUPTIBLE_URI address
- * returns the TEST_DATA_SHORT text, then waits until the "resolve" method is
- * called on the object returned by the function.  At this point, the handler
- * sends the TEST_DATA_SHORT text again to complete the response.
+ * For example, the handler accessible at the httpUri("interruptible.txt")
+ * address returns the TEST_DATA_SHORT text, then waits until the "resolve"
+ * method is called on the object returned by the function.  At this point, the
+ * handler sends the TEST_DATA_SHORT text again to complete the response.
  *
  * You can also call the "reject" method on the returned object to interrupt the
  * response midway.  Because of how the network layer is implemented, this does
  * not cause the socket to return an error.
  *
  * @returns Deferred object used to control the response.
  */
 function deferNextResponse()
@@ -424,43 +480,40 @@ function registerInterruptibleHandler(aP
  */
 function isValidDate(aDate) {
   return aDate && aDate.getTime && !isNaN(aDate.getTime());
 }
 
 ////////////////////////////////////////////////////////////////////////////////
 //// Initialization functions common to all tests
 
-let gHttpServer;
-let gFakeServerPort;
-
 add_task(function test_common_initialize()
 {
   // Start the HTTP server.
   gHttpServer = new HttpServer();
   gHttpServer.registerDirectory("/", do_get_file("../data"));
   gHttpServer.start(-1);
 
-  registerInterruptibleHandler(TEST_INTERRUPTIBLE_PATH,
+  registerInterruptibleHandler("/interruptible.txt",
     function firstPart(aRequest, aResponse) {
       aResponse.setHeader("Content-Type", "text/plain", false);
       aResponse.setHeader("Content-Length", "" + (TEST_DATA_SHORT.length * 2),
                           false);
       aResponse.write(TEST_DATA_SHORT);
     }, function secondPart(aRequest, aResponse) {
       aResponse.write(TEST_DATA_SHORT);
     });
 
-  registerInterruptibleHandler(TEST_EMPTY_NOPROGRESS_PATH,
+  registerInterruptibleHandler("/empty-noprogress.txt",
     function firstPart(aRequest, aResponse) {
       aResponse.setHeader("Content-Type", "text/plain", false);
     }, function secondPart(aRequest, aResponse) { });
 
 
-  registerInterruptibleHandler(TEST_INTERRUPTIBLE_GZIP_PATH,
+  registerInterruptibleHandler("/interruptible_gzip.txt",
     function firstPart(aRequest, aResponse) {
       aResponse.setHeader("Content-Type", "text/plain", false);
       aResponse.setHeader("Content-Encoding", "gzip", false);
       aResponse.setHeader("Content-Length", "" + TEST_DATA_SHORT_GZIP_ENCODED.length);
 
       let bos =  new BinaryOutputStream(aResponse.bodyOutputStream);
       bos.writeByteArray(TEST_DATA_SHORT_GZIP_ENCODED_FIRST,
                          TEST_DATA_SHORT_GZIP_ENCODED_FIRST.length);
--- a/toolkit/components/jsdownloads/test/unit/test_DownloadCore.js
+++ b/toolkit/components/jsdownloads/test/unit/test_DownloadCore.js
@@ -1,897 +1,18 @@
 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
 /* vim: set ts=2 et sw=2 tw=80: */
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 /**
- * Tests the objects defined in the "DownloadCore" module.
+ * Tests the main download interfaces using DownloadCopySaver.
  */
 
 "use strict";
 
 ////////////////////////////////////////////////////////////////////////////////
-//// Tests
-
-/**
- * Executes a download, started by constructing the simplest Download object.
- */
-add_task(function test_download_construction()
-{
-  let targetFile = getTempFile(TEST_TARGET_FILE_NAME);
-
-  let download = yield Downloads.createDownload({
-    source: { uri: TEST_SOURCE_URI },
-    target: { file: targetFile },
-    saver: { type: "copy" },
-  });
-
-  // Checks the generated DownloadSource and DownloadTarget properties.
-  do_check_true(download.source.uri.equals(TEST_SOURCE_URI));
-  do_check_eq(download.target.file, targetFile);
-  do_check_true(download.source.referrer === null);
-
-  // Starts the download and waits for completion.
-  yield download.start();
-
-  yield promiseVerifyContents(targetFile, TEST_DATA_SHORT);
-});
-
-/**
- * Checks the referrer for downloads.
- */
-add_task(function test_download_referrer()
-{
-  let source_path = "/test_download_referrer.txt";
-  let source_uri = NetUtil.newURI(HTTP_BASE + source_path);
-  let target_uri = getTempFile(TEST_TARGET_FILE_NAME);
-
-  function cleanup() {
-    gHttpServer.registerPathHandler(source_path, null);
-  }
-
-  do_register_cleanup(cleanup);
-
-  gHttpServer.registerPathHandler(source_path, function (aRequest, aResponse) {
-    aResponse.setHeader("Content-Type", "text/plain", false);
-
-    do_check_true(aRequest.hasHeader("Referer"));
-    do_check_eq(aRequest.getHeader("Referer"), TEST_REFERRER_URI.spec);
-  });
-  let download = yield Downloads.createDownload({
-    source: { uri: source_uri, referrer: TEST_REFERRER_URI },
-    target: { file: target_uri },
-    saver: { type: "copy" },
-  });
-  do_check_true(download.source.referrer.equals(TEST_REFERRER_URI));
-  yield download.start();
-
-  download = yield Downloads.createDownload({
-    source: { uri: source_uri, referrer: TEST_REFERRER_URI, isPrivate: true },
-    target: { file: target_uri },
-    saver: { type: "copy" },
-  });
-  do_check_true(download.source.referrer.equals(TEST_REFERRER_URI));
-  yield download.start();
-
-  // Test the download still works for non-HTTP channel with referrer.
-  source_uri = NetUtil.newURI("data:text/html,<html><body></body></html>");
-  download = yield Downloads.createDownload({
-    source: { uri: source_uri, referrer: TEST_REFERRER_URI },
-    target: { file: target_uri },
-    saver: { type: "copy" },
-  });
-  do_check_true(download.source.referrer.equals(TEST_REFERRER_URI));
-  yield download.start();
-
-  cleanup();
-});
-
-/**
- * Checks initial and final state and progress for a successful download.
- */
-add_task(function test_download_initial_final_state()
-{
-  let download = yield promiseSimpleDownload();
-
-  do_check_true(download.stopped);
-  do_check_false(download.succeeded);
-  do_check_false(download.canceled);
-  do_check_true(download.error === null);
-  do_check_eq(download.progress, 0);
-  do_check_true(download.startTime === null);
-
-  // Starts the download and waits for completion.
-  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);
-  do_check_true(isValidDate(download.startTime));
-});
-
-/**
- * Checks the notification of the final download state.
- */
-add_task(function test_download_final_state_notified()
-{
-  let download = yield promiseSimpleDownload();
-
-  let onchangeNotified = false;
-  let lastNotifiedStopped;
-  let lastNotifiedProgress;
-  download.onchange = function () {
-    onchangeNotified = true;
-    lastNotifiedStopped = download.stopped;
-    lastNotifiedProgress = download.progress;
-  };
-
-  // Starts the download and waits for completion.
-  yield download.start();
-
-  // The view should have been notified before the download completes.
-  do_check_true(onchangeNotified);
-  do_check_true(lastNotifiedStopped);
-  do_check_eq(lastNotifiedProgress, 100);
-});
-
-/**
- * Checks intermediate progress for a successful download.
- */
-add_task(function test_download_intermediate_progress()
-{
-  let deferResponse = deferNextResponse();
-
-  let download = yield promiseSimpleDownload(TEST_INTERRUPTIBLE_URI);
-
-  download.onchange = function () {
-    if (download.progress == 50) {
-      do_check_true(download.hasProgress);
-      do_check_eq(download.currentBytes, TEST_DATA_SHORT.length);
-      do_check_eq(download.totalBytes, TEST_DATA_SHORT.length * 2);
-
-      // Continue after the first chunk of data is fully received.
-      deferResponse.resolve();
-    }
-  };
-
-  // Starts the download and waits for completion.
-  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_download_empty_progress()
-{
-  let download = yield promiseSimpleDownload(TEST_EMPTY_URI);
-
-  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);
-});
-
-/**
- * Downloads an empty file with no "Content-Length" and checks the progress.
- */
-add_task(function test_download_empty_noprogress()
-{
-  let deferResponse = deferNextResponse();
-  let promiseEmptyRequestReceived = promiseNextRequestReceived();
-
-  let download = yield promiseSimpleDownload(TEST_EMPTY_NOPROGRESS_URI);
-
-  download.onchange = function () {
-    if (!download.stopped) {
-      do_check_false(download.hasProgress);
-      do_check_eq(download.currentBytes, 0);
-      do_check_eq(download.totalBytes, 0);
-    }
-  };
-
-  // Start the download, while waiting for the request to be received.
-  let promiseAttempt = download.start();
-
-  // Wait for the request to be received by the HTTP server, but don't allow the
-  // request to finish yet.  Before checking the download state, wait for the
-  // events to be processed by the client.
-  yield promiseEmptyRequestReceived;
-  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.
-  deferResponse.resolve();
-  yield promiseAttempt;
-
-  // 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);
-
-  do_check_eq(download.target.file.fileSize, 0);
-});
-
-/**
- * Calls the "start" method two times before the download is finished.
- */
-add_task(function test_download_start_twice()
-{
-  let download = yield promiseSimpleDownload(TEST_INTERRUPTIBLE_URI);
-
-  // Ensure that the download cannot complete before start is called twice.
-  let deferResponse = deferNextResponse();
-
-  // Call the start method two times.
-  let promiseAttempt1 = download.start();
-  let promiseAttempt2 = download.start();
-
-  // Allow the download to finish.
-  deferResponse.resolve();
-
-  // Both promises should now be resolved.
-  yield promiseAttempt1;
-  yield promiseAttempt2;
-
-  do_check_true(download.stopped);
-  do_check_true(download.succeeded);
-  do_check_false(download.canceled);
-  do_check_true(download.error === null);
-
-  yield promiseVerifyContents(download.target.file,
-                              TEST_DATA_SHORT + TEST_DATA_SHORT);
-});
-
-/**
- * Cancels a download and verifies that its state is reported correctly.
- */
-add_task(function test_download_cancel_midway()
-{
-  let download = yield promiseSimpleDownload(TEST_INTERRUPTIBLE_URI);
-
-  let deferResponse = deferNextResponse();
-  try {
-    // Cancel the download after receiving the first part of the response.
-    let deferCancel = Promise.defer();
-    download.onchange = function () {
-      if (!download.stopped && !download.canceled && download.progress == 50) {
-        deferCancel.resolve(download.cancel());
-
-        // The state change happens immediately after calling "cancel", but
-        // temporary files or part files may still exist at this point.
-        do_check_true(download.canceled);
-      }
-    };
-
-    let promiseAttempt = download.start();
-
-    // Wait on the promise returned by the "cancel" method to ensure that the
-    // cancellation process finished and temporary files were removed.
-    yield deferCancel.promise;
-
-    do_check_true(download.stopped);
-    do_check_true(download.canceled);
-    do_check_true(download.error === null);
-
-    do_check_false(download.target.file.exists());
-
-    // Progress properties are not reset by canceling.
-    do_check_eq(download.progress, 50);
-    do_check_eq(download.totalBytes, TEST_DATA_SHORT.length * 2);
-    do_check_eq(download.currentBytes, TEST_DATA_SHORT.length);
-
-    // The promise returned by "start" should have been rejected meanwhile.
-    try {
-      yield promiseAttempt;
-      do_throw("The download should have been canceled.");
-    } catch (ex if ex instanceof Downloads.Error) {
-      do_check_false(ex.becauseSourceFailed);
-      do_check_false(ex.becauseTargetFailed);
-    }
-  } finally {
-    deferResponse.resolve();
-  }
-});
-
-/**
- * Cancels a download right after starting it.
- */
-add_task(function test_download_cancel_immediately()
-{
-  // Ensure that the download cannot complete before cancel is called.
-  let deferResponse = deferNextResponse();
-  try {
-    let download = yield promiseSimpleDownload(TEST_INTERRUPTIBLE_URI);
-
-    let promiseAttempt = download.start();
-    do_check_false(download.stopped);
-
-    let promiseCancel = download.cancel();
-    do_check_true(download.canceled);
-
-    // At this point, we don't know whether the download has already stopped or
-    // is still waiting for cancellation.  We can wait on the promise returned
-    // by the "start" method to know for sure.
-    try {
-      yield promiseAttempt;
-      do_throw("The download should have been canceled.");
-    } catch (ex if ex instanceof Downloads.Error) {
-      do_check_false(ex.becauseSourceFailed);
-      do_check_false(ex.becauseTargetFailed);
-    }
-
-    do_check_true(download.stopped);
-    do_check_true(download.canceled);
-    do_check_true(download.error === null);
-
-    do_check_false(download.target.file.exists());
-
-    // Check that the promise returned by the "cancel" method has been resolved.
-    yield promiseCancel;
-  } 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.
-  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);
-
-  // The first time, cancel the download midway.
-  let deferResponse = deferNextResponse();
-  try {
-    let deferCancel = Promise.defer();
-    download.onchange = function () {
-      if (!download.stopped && !download.canceled && download.progress == 50) {
-        deferCancel.resolve(download.cancel());
-      }
-    };
-    download.start();
-    yield deferCancel.promise;
-  } finally {
-    deferResponse.resolve();
-  }
-
-  do_check_true(download.stopped);
-
-  // The second time, we'll provide the entire interruptible response.
-  download.onchange = null;
-  let promiseAttempt = download.start();
-
-  // Download state should have already been reset.
-  do_check_false(download.stopped);
-  do_check_false(download.canceled);
-  do_check_true(download.error === null);
-
-  // For the following test, we rely on the network layer reporting its progress
-  // asynchronously.  Otherwise, there is nothing stopping the restarted
-  // download from reaching the same progress as the first request already.
-  do_check_eq(download.progress, 0);
-  do_check_eq(download.totalBytes, 0);
-  do_check_eq(download.currentBytes, 0);
-
-  yield promiseAttempt;
-
-  do_check_true(download.stopped);
-  do_check_true(download.succeeded);
-  do_check_false(download.canceled);
-  do_check_true(download.error === null);
-
-  yield promiseVerifyContents(download.target.file,
-                              TEST_DATA_SHORT + TEST_DATA_SHORT);
-});
-
-/**
- * Cancels a download right after starting it, then restarts it immediately.
- */
-add_task(function test_download_cancel_immediately_restart_immediately()
-{
-  let download = yield promiseSimpleDownload(TEST_INTERRUPTIBLE_URI);
-
-  // Ensure that the download cannot complete before cancel is called.
-  let deferResponse = deferNextResponse();
-
-  let promiseAttempt = download.start();
-  do_check_false(download.stopped);
-
-  download.cancel();
-  do_check_true(download.canceled);
-
-  let promiseRestarted = download.start();
-  do_check_false(download.stopped);
-  do_check_false(download.canceled);
-  do_check_true(download.error === null);
-
-  // For the following test, we rely on the network layer reporting its progress
-  // asynchronously.  Otherwise, there is nothing stopping the restarted
-  // download from reaching the same progress as the first request already.
-  do_check_eq(download.hasProgress, false);
-  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.
-  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();
+//// Execution of common tests
 
-  try {
-    yield promiseAttempt;
-    do_throw("The download should have been canceled.");
-  } catch (ex if ex instanceof Downloads.Error) {
-    do_check_false(ex.becauseSourceFailed);
-    do_check_false(ex.becauseTargetFailed);
-  }
-
-  yield promiseRestarted;
-
-  do_check_true(download.stopped);
-  do_check_true(download.succeeded);
-  do_check_false(download.canceled);
-  do_check_true(download.error === null);
-
-  yield promiseVerifyContents(download.target.file,
-                              TEST_DATA_SHORT + TEST_DATA_SHORT);
-});
-
-/**
- * Cancels a download midway, then restarts it immediately.
- */
-add_task(function test_download_cancel_midway_restart_immediately()
-{
-  let download = yield promiseSimpleDownload(TEST_INTERRUPTIBLE_URI);
-
-  // The first time, cancel the download midway.
-  let deferResponse = deferNextResponse();
-
-  let deferMidway = Promise.defer();
-  download.onchange = function () {
-    if (!download.stopped && !download.canceled && download.progress == 50) {
-      do_check_eq(download.progress, 50);
-      deferMidway.resolve();
-    }
-  };
-  let promiseAttempt = download.start();
-  yield deferMidway.promise;
-
-  download.cancel();
-  do_check_true(download.canceled);
-
-  let promiseRestarted = download.start();
-  do_check_false(download.stopped);
-  do_check_false(download.canceled);
-  do_check_true(download.error === null);
-
-  // For the following test, we rely on the network layer reporting its progress
-  // asynchronously.  Otherwise, there is nothing stopping the restarted
-  // download from reaching the same progress as the first request already.
-  do_check_eq(download.hasProgress, false);
-  do_check_eq(download.progress, 0);
-  do_check_eq(download.totalBytes, 0);
-  do_check_eq(download.currentBytes, 0);
-
-  deferResponse.resolve();
-
-  // The second request is allowed to complete.
-  try {
-    yield promiseAttempt;
-    do_throw("The download should have been canceled.");
-  } catch (ex if ex instanceof Downloads.Error) {
-    do_check_false(ex.becauseSourceFailed);
-    do_check_false(ex.becauseTargetFailed);
-  }
-
-  yield promiseRestarted;
-
-  do_check_true(download.stopped);
-  do_check_true(download.succeeded);
-  do_check_false(download.canceled);
-  do_check_true(download.error === null);
-
-  yield promiseVerifyContents(download.target.file,
-                              TEST_DATA_SHORT + TEST_DATA_SHORT);
-});
-
-/**
- * Calls the "cancel" method on a successful download.
- */
-add_task(function test_download_cancel_successful()
-{
-  let download = yield promiseSimpleDownload();
-
-  // Starts the download and waits for completion.
-  yield download.start();
-
-  // The cancel method should succeed with no effect.
-  yield download.cancel();
-
-  do_check_true(download.stopped);
-  do_check_true(download.succeeded);
-  do_check_false(download.canceled);
-  do_check_true(download.error === null);
-
-  yield promiseVerifyContents(download.target.file, TEST_DATA_SHORT);
-});
-
-/**
- * Calls the "cancel" method two times in a row.
- */
-add_task(function test_download_cancel_twice()
-{
-  let download = yield promiseSimpleDownload(TEST_INTERRUPTIBLE_URI);
-
-  // Ensure that the download cannot complete before cancel is called.
-  let deferResponse = deferNextResponse();
-  try {
-    let promiseAttempt = download.start();
-    do_check_false(download.stopped);
-
-    let promiseCancel1 = download.cancel();
-    do_check_true(download.canceled);
-    let promiseCancel2 = download.cancel();
-
-    try {
-      yield promiseAttempt;
-      do_throw("The download should have been canceled.");
-    } catch (ex if ex instanceof Downloads.Error) {
-      do_check_false(ex.becauseSourceFailed);
-      do_check_false(ex.becauseTargetFailed);
-    }
-
-    // Both promises should now be resolved.
-    yield promiseCancel1;
-    yield promiseCancel2;
-
-    do_check_true(download.stopped);
-    do_check_false(download.succeeded);
-    do_check_true(download.canceled);
-    do_check_true(download.error === null);
-
-    do_check_false(download.target.file.exists());
-  } finally {
-    deferResponse.resolve();
-  }
-});
-
-/**
- * Checks that whenSucceeded returns a promise that is resolved after a restart.
- */
-add_task(function test_download_whenSucceeded()
-{
-  let download = yield promiseSimpleDownload(TEST_INTERRUPTIBLE_URI);
-
-  // Ensure that the download cannot complete before cancel is called.
-  let deferResponse = deferNextResponse();
-
-  // Get a reference before the first download attempt.
-  let promiseSucceeded = download.whenSucceeded();
-
-  // Cancel the first download attempt.
-  download.start();
-  yield download.cancel();
-
-  deferResponse.resolve();
-
-  // The second request is allowed to complete.
-  download.start();
-
-  // Wait for the download to finish by waiting on the whenSucceeded promise.
-  yield promiseSucceeded;
-
-  do_check_true(download.stopped);
-  do_check_true(download.succeeded);
-  do_check_false(download.canceled);
-  do_check_true(download.error === null);
-
-  yield promiseVerifyContents(download.target.file,
-                              TEST_DATA_SHORT + TEST_DATA_SHORT);
-});
-
-/**
- * Ensures download error details are reported on network failures.
- */
-add_task(function test_download_error_source()
-{
-  let serverSocket = startFakeServer();
-  try {
-    let download = yield promiseSimpleDownload(TEST_FAKE_SOURCE_URI);
-
-    do_check_true(download.error === null);
-
-    try {
-      yield download.start();
-      do_throw("The download should have failed.");
-    } catch (ex if ex instanceof Downloads.Error && ex.becauseSourceFailed) {
-      // A specific error object is thrown when reading from the source fails.
-    }
-
-    do_check_true(download.stopped);
-    do_check_false(download.canceled);
-    do_check_true(download.error !== null);
-    do_check_true(download.error.becauseSourceFailed);
-    do_check_false(download.error.becauseTargetFailed);
-  } finally {
-    serverSocket.close();
-  }
-});
-
-/**
- * Ensures download error details are reported on local writing failures.
- */
-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.
-    }
+let gUseLegacySaver = 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);
-  } 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);
-    }
-  }
-});
-
-/**
- * Restarts a failed download.
- */
-add_task(function test_download_error_restart()
-{
-  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 {
-    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;
-
-      // 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);
-  do_check_true(download.error === null);
-  do_check_eq(download.progress, 100);
-
-  yield promiseVerifyContents(download.target.file, TEST_DATA_SHORT);
-});
-
-/**
- * Executes download in both public and private modes.
- */
-add_task(function test_download_public_and_private()
-{
-  let source_path = "/test_download_public_and_private.txt";
-  let source_uri = NetUtil.newURI(HTTP_BASE + source_path);
-  let testCount = 0;
-
-  // Apply pref to allow all cookies.
-  Services.prefs.setIntPref("network.cookie.cookieBehavior", 0);
-
-  function cleanup() {
-    Services.prefs.clearUserPref("network.cookie.cookieBehavior");
-    Services.cookies.removeAll();
-    gHttpServer.registerPathHandler(source_path, null);
-  }
-  do_register_cleanup(cleanup);
-
-  gHttpServer.registerPathHandler(source_path, function (aRequest, aResponse) {
-    aResponse.setHeader("Content-Type", "text/plain", false);
-
-    if (testCount == 0) {
-      // No cookies should exist for first public download.
-      do_check_false(aRequest.hasHeader("Cookie"));
-      aResponse.setHeader("Set-Cookie", "foobar=1", false);
-      testCount++;
-    } else if (testCount == 1) {
-      // The cookie should exists for second public download.
-      do_check_true(aRequest.hasHeader("Cookie"));
-      do_check_eq(aRequest.getHeader("Cookie"), "foobar=1");
-      testCount++;
-    } else if (testCount == 2)  {
-      // No cookies should exist for first private download.
-      do_check_false(aRequest.hasHeader("Cookie"));
-    }
-  });
-
-  let targetFile = getTempFile(TEST_TARGET_FILE_NAME);
-  yield Downloads.simpleDownload(source_uri, targetFile);
-  yield Downloads.simpleDownload(source_uri, targetFile);
-  let download = yield Downloads.createDownload({
-    source: { uri: source_uri, isPrivate: true },
-    target: { file: targetFile },
-    saver: { type: "copy" },
-  });
-  yield download.start();
-
-  cleanup();
-});
-
-/**
- * Checks the startTime gets updated even after a restart.
- */
-add_task(function test_download_cancel_immediately_restart_and_check_startTime()
-{
-  let download = yield promiseSimpleDownload();
-
-  download.start();
-  let startTime = download.startTime;
-  do_check_true(isValidDate(download.startTime));
-
-  yield download.cancel();
-  do_check_eq(download.startTime.getTime(), startTime.getTime());
-
-  // Wait for a timeout.
-  yield promiseTimeout(10);
-
-  yield download.start();
-  do_check_true(download.startTime.getTime() > startTime.getTime());
-});
-
-/**
- * Executes download with content-encoding.
- */
-add_task(function test_download_with_content_encoding()
-{
-  let source_path = "/test_download_with_content_encoding.txt";
-  let source_uri = NetUtil.newURI(HTTP_BASE + source_path);
-
-  function cleanup() {
-    gHttpServer.registerPathHandler(source_path, null);
-  }
-  do_register_cleanup(cleanup);
-
-  gHttpServer.registerPathHandler(source_path, function (aRequest, aResponse) {
-    aResponse.setHeader("Content-Type", "text/plain", false);
-    aResponse.setHeader("Content-Encoding", "gzip", false);
-    aResponse.setHeader("Content-Length",
-                        "" + TEST_DATA_SHORT_GZIP_ENCODED.length, false);
-
-    let bos =  new BinaryOutputStream(aResponse.bodyOutputStream);
-    bos.writeByteArray(TEST_DATA_SHORT_GZIP_ENCODED,
-                       TEST_DATA_SHORT_GZIP_ENCODED.length);
-  });
-
-  let download = yield Downloads.createDownload({
-    source: { uri: source_uri },
-    target: { file: getTempFile(TEST_TARGET_FILE_NAME) },
-    saver: { type: "copy" },
-  });
-  yield download.start();
-
-  do_check_eq(download.progress, 100);
-  do_check_eq(download.totalBytes, TEST_DATA_SHORT_GZIP_ENCODED.length);
-
-  // Ensure the content matches the decoded test data.
-  yield promiseVerifyContents(download.target.file, TEST_DATA_SHORT);
-});
-
-/**
- * Cancels and restarts a download sequentially with content-encoding.
- */
-add_task(function test_download_cancel_midway_restart_with_content_encoding()
-{
-  let download = yield promiseSimpleDownload(TEST_INTERRUPTIBLE_GZIP_URI);
-
-  // The first time, cancel the download midway.
-  let deferResponse = deferNextResponse();
-  try {
-    let deferCancel = Promise.defer();
-    download.onchange = function () {
-      if (!download.stopped && !download.canceled &&
-          download.currentBytes == TEST_DATA_SHORT_GZIP_ENCODED_FIRST.length) {
-        deferCancel.resolve(download.cancel());
-      }
-    };
-    download.start();
-    yield deferCancel.promise;
-  } finally {
-    deferResponse.resolve();
-  }
-
-  do_check_true(download.stopped);
-
-  // The second time, we'll provide the entire interruptible response.
-  download.onchange = null;
-  yield download.start()
-
-  do_check_eq(download.progress, 100);
-  do_check_eq(download.totalBytes, TEST_DATA_SHORT_GZIP_ENCODED.length);
-
-  yield promiseVerifyContents(download.target.file, TEST_DATA_SHORT);
-});
-
-/**
- * Download with parental controls enabled.
- */
-add_task(function test_download_blocked_parental_controls()
-{
-  function cleanup() {
-    DownloadIntegration.shouldBlockInTest = false;
-  }
-  do_register_cleanup(cleanup);
-  DownloadIntegration.shouldBlockInTest = true;
-
-  let download = yield promiseSimpleDownload();
-
-  try {
-    yield download.start();
-    do_throw("The download should have blocked.");
-  } catch (ex if ex instanceof Downloads.Error && ex.becauseBlocked) {
-    do_check_true(ex.becauseBlockedByParentalControls);
-  }
-  cleanup();
-});
-
+let scriptFile = do_get_file("common_test_Download.js");
+Services.scriptloader.loadSubScript(NetUtil.newURI(scriptFile).spec);
--- a/toolkit/components/jsdownloads/test/unit/test_DownloadLegacy.js
+++ b/toolkit/components/jsdownloads/test/unit/test_DownloadLegacy.js
@@ -5,375 +5,14 @@
 
 /**
  * Tests the integration with legacy interfaces for downloads.
  */
 
 "use strict";
 
 ////////////////////////////////////////////////////////////////////////////////
-//// Globals
-
-/**
- * Starts a new download using the nsIWebBrowserPersist interface, and controls
- * it using the legacy nsITransfer interface.
- *
- * @param aSourceURI
- *        The nsIURI for the download source, or null to use TEST_SOURCE_URI.
- * @param isPrivate
- *        Optional boolean indicates whether the download originated from a
- *        private window.
- * @param aOutPersist
- *        Optional object that receives a reference to the created
- *        nsIWebBrowserPersist instance in the "value" property.
- *
- * @return {Promise}
- * @resolves The Download object created as a consequence of controlling the
- *           download through the legacy nsITransfer interface.
- * @rejects Never.  The current test fails in case of exceptions.
- */
-function promiseStartLegacyDownload(aSourceURI, aIsPrivate, 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);
-
-  // 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();
-  let promise = aIsPrivate ? Downloads.getPrivateDownloadList() :
-                Downloads.getPublicDownloadList();
-  promise.then(function (aList) {
-    // Temporarily register a view that will get notified when the download we
-    // are controlling becomes visible in the list of public downloads.
-    aList.addView({
-      onDownloadAdded: function (aDownload) {
-        aList.removeView(this);
-
-        // Remove the download to keep the list empty for the next test.  This
-        // also allows the caller to register the "onchange" event directly.
-        aList.remove(aDownload);
-
-        // When the download object is ready, make it available to the caller.
-        deferred.resolve(aDownload);
-      },
-    });
-
-    // Initialize the components so they reference each other.  This will cause
-    // the Download object to be created and added to the public downloads.
-    transfer.init(sourceURI, NetUtil.newURI(targetFile), null, null, null, null,
-                  persist, aIsPrivate);
-    persist.progressListener = transfer;
-
-    // Start the actual download process.
-    persist.savePrivacyAwareURI(sourceURI, null, null, null, null, targetFile, aIsPrivate);
-  }.bind(this)).then(null, do_report_unexpected_exception);
-
-  return deferred.promise;
-}
-
-////////////////////////////////////////////////////////////////////////////////
-//// Tests
-
-/**
- * Executes a download controlled by the legacy nsITransfer interface.
- */
-add_task(function test_basic()
-{
-  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.parent.equals(tempDirectory));
-
-  // The download is already started, wait for completion and report any errors.
-  if (!download.stopped) {
-    yield download.start();
-  }
-
-  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, 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);
-});
-
-/**
- * Checks intermediate progress for a successful download.
- */
-add_task(function test_intermediate_progress()
-{
-  let deferResponse = deferNextResponse();
-
-  let download = yield promiseStartLegacyDownload(TEST_INTERRUPTIBLE_URI);
-
-  let onchange = function () {
-    if (download.progress == 50) {
-      do_check_true(download.hasProgress);
-      do_check_eq(download.currentBytes, TEST_DATA_SHORT.length);
-      do_check_eq(download.totalBytes, TEST_DATA_SHORT.length * 2);
-
-      // Continue after the first chunk of data is fully received.
-      deferResponse.resolve();
-    }
-  };
-
-  // 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, 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);
-
-  // 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);
-});
-
-/**
- * Downloads an empty file with no "Content-Length" and checks the progress.
- */
-add_task(function test_empty_noprogress()
-{
-  let deferResponse = deferNextResponse();
-  let promiseEmptyRequestReceived = promiseNextRequestReceived();
-
-  let download = yield promiseStartLegacyDownload(TEST_EMPTY_NOPROGRESS_URI);
+//// Execution of common tests
 
-  // Wait for the request to be received by the HTTP server, but don't allow the
-  // request to finish yet.  Before checking the download state, wait for the
-  // events to be processed by the client.
-  yield promiseEmptyRequestReceived;
-  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,
-  // while reporting any errors that may occur.
-  deferResponse.resolve();
-  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);
-
-  do_check_eq(download.target.file.fileSize, 0);
-});
-
-/**
- * Cancels a download and verifies that its state is reported correctly.
- */
-add_task(function test_cancel_midway()
-{
-  let deferResponse = deferNextResponse();
-  let outPersist = {};
-  let download = yield promiseStartLegacyDownload(TEST_INTERRUPTIBLE_URI, false,
-                                                  outPersist);
-
-  try {
-    // Cancel the download after receiving the first part of the response.
-    let deferCancel = Promise.defer();
-    let onchange = function () {
-      if (!download.stopped && !download.canceled && download.progress == 50) {
-        deferCancel.resolve(download.cancel());
-
-        // The state change happens immediately after calling "cancel", but
-        // temporary files or part files may still exist at this point.
-        do_check_true(download.canceled);
-      }
-    };
-
-    // Register for the notification, but also call the function directly in
-    // case the download already reached the expected progress.
-    download.onchange = onchange;
-    onchange();
-
-    // Wait on the promise returned by the "cancel" method to ensure that the
-    // cancellation process finished and temporary files were removed.
-    yield deferCancel.promise;
-
-    // The nsIWebBrowserPersist instance should have been canceled now.
-    do_check_eq(outPersist.value.result, Cr.NS_ERROR_ABORT);
-
-    do_check_true(download.stopped);
-    do_check_true(download.canceled);
-    do_check_true(download.error === null);
-
-    do_check_false(download.target.file.exists());
-
-    // Progress properties are not reset by canceling.
-    do_check_eq(download.progress, 50);
-    do_check_eq(download.totalBytes, TEST_DATA_SHORT.length * 2);
-    do_check_eq(download.currentBytes, TEST_DATA_SHORT.length);
-  } finally {
-    deferResponse.resolve();
-  }
-});
-
-/**
- * Ensures download error details are reported for legacy downloads.
- */
-add_task(function test_error()
-{
-  let serverSocket = startFakeServer();
-  try {
-    let download = yield promiseStartLegacyDownload(TEST_FAKE_SOURCE_URI);
+let gUseLegacySaver = true;
 
-    // We must check the download properties instead of calling the "start"
-    // method because the download has been started and may already be stopped.
-    let deferStopped = Promise.defer();
-    let onchange = function () {
-      if (download.stopped) {
-        deferStopped.resolve();
-      }
-    };
-    download.onchange = onchange;
-    onchange();
-    yield deferStopped.promise;
-
-    // Check the properties now that the download stopped.
-    do_check_false(download.canceled);
-    do_check_true(download.error !== null);
-    do_check_true(download.error.becauseSourceFailed);
-    do_check_false(download.error.becauseTargetFailed);
-  } finally {
-    serverSocket.close();
-  }
-});
-
-/**
- * Executes download in both public and private modes.
- */
-add_task(function test_download_public_and_private()
-{
-  let source_path = "/test_download_public_and_private.txt";
-  let source_uri = NetUtil.newURI(HTTP_BASE + source_path);
-  let testCount = 0;
-
-  // Apply pref to allow all cookies.
-  Services.prefs.setIntPref("network.cookie.cookieBehavior", 0);
-
-  function cleanup() {
-    Services.prefs.clearUserPref("network.cookie.cookieBehavior");
-    Services.cookies.removeAll();
-    gHttpServer.registerPathHandler(source_path, null);
-  }
-
-  do_register_cleanup(cleanup);
-
-  gHttpServer.registerPathHandler(source_path, function (aRequest, aResponse) {
-    aResponse.setHeader("Content-Type", "text/plain", false);
-
-    if (testCount == 0) {
-      // No cookies should exist for first public download.
-      do_check_false(aRequest.hasHeader("Cookie"));
-      aResponse.setHeader("Set-Cookie", "foobar=1", false);
-      testCount++;
-    } else if (testCount == 1) {
-      // The cookie should exists for second public download.
-      do_check_true(aRequest.hasHeader("Cookie"));
-      do_check_eq(aRequest.getHeader("Cookie"), "foobar=1");
-      testCount++;
-    } else if (testCount == 2)  {
-      // No cookies should exist for first private download.
-      do_check_false(aRequest.hasHeader("Cookie"));
-    }
-  });
-
-  let targetFile = getTempFile(TEST_TARGET_FILE_NAME);
-  yield Downloads.simpleDownload(source_uri, targetFile);
-  yield Downloads.simpleDownload(source_uri, targetFile);
-  let download = yield promiseStartLegacyDownload(source_uri, true);
-  // The download is already started, wait for completion and report any errors.
-  if (!download.stopped) {
-    yield download.start();
-  }
-
-  cleanup();
-});
-
-/**
- * Download with parental controls enabled.
- */
-add_task(function test_download_blocked_parental_controls()
-{
-  function cleanup() {
-    DownloadIntegration.shouldBlockInTest = false;
-  }
-  do_register_cleanup(cleanup);
-  DownloadIntegration.shouldBlockInTest = true;
-
-  let download = yield promiseStartLegacyDownload();
-
-  try {
-    yield download.start();
-    do_throw("The download should have blocked.");
-  } catch (ex if ex instanceof Downloads.Error && ex.becauseBlocked) {
-    do_check_true(ex.becauseBlockedByParentalControls);
-  }
-
-  do_check_false(download.target.file.exists());
-
-  cleanup();
-});
+let scriptFile = do_get_file("common_test_Download.js");
+Services.scriptloader.loadSubScript(NetUtil.newURI(scriptFile).spec);
--- a/toolkit/components/jsdownloads/test/unit/test_DownloadList.js
+++ b/toolkit/components/jsdownloads/test/unit/test_DownloadList.js
@@ -29,24 +29,24 @@ add_task(function test_construction()
 
 /**
  * Checks the methods to add and retrieve items from the list.
  */
 add_task(function test_add_getAll()
 {
   let list = yield promiseNewDownloadList();
 
-  let downloadOne = yield promiseSimpleDownload();
+  let downloadOne = yield promiseNewDownload();
   list.add(downloadOne);
 
   let itemsOne = yield list.getAll();
   do_check_eq(itemsOne.length, 1);
   do_check_eq(itemsOne[0], downloadOne);
 
-  let downloadTwo = yield promiseSimpleDownload();
+  let downloadTwo = yield promiseNewDownload();
   list.add(downloadTwo);
 
   let itemsTwo = yield list.getAll();
   do_check_eq(itemsTwo.length, 2);
   do_check_eq(itemsTwo[0], downloadOne);
   do_check_eq(itemsTwo[1], downloadTwo);
 
   // The first snapshot should not have been modified.
@@ -55,39 +55,39 @@ add_task(function test_add_getAll()
 
 /**
  * Checks the method to remove items from the list.
  */
 add_task(function test_remove()
 {
   let list = yield promiseNewDownloadList();
 
-  list.add(yield promiseSimpleDownload());
-  list.add(yield promiseSimpleDownload());
+  list.add(yield promiseNewDownload());
+  list.add(yield promiseNewDownload());
 
   let items = yield list.getAll();
   list.remove(items[0]);
 
   // Removing an item that was never added should not raise an error.
-  list.remove(yield promiseSimpleDownload());
+  list.remove(yield promiseNewDownload());
 
   items = yield list.getAll();
   do_check_eq(items.length, 1);
 });
 
 /**
  * Checks that views receive the download add and remove notifications, and that
  * adding and removing views works as expected.
  */
 add_task(function test_notifications_add_remove()
 {
   let list = yield promiseNewDownloadList();
 
-  let downloadOne = yield promiseSimpleDownload();
-  let downloadTwo = yield promiseSimpleDownload();
+  let downloadOne = yield promiseNewDownload();
+  let downloadTwo = yield promiseNewDownload();
   list.add(downloadOne);
   list.add(downloadTwo);
 
   // Check that we receive add notifications for existing elements.
   let addNotifications = 0;
   let viewOne = {
     onDownloadAdded: function (aDownload) {
       // The first download to be notified should be the first that was added.
@@ -98,17 +98,17 @@ add_task(function test_notifications_add
       }
       addNotifications++;
     },
   };
   list.addView(viewOne);
   do_check_eq(addNotifications, 2);
 
   // Check that we receive add notifications for new elements.
-  list.add(yield promiseSimpleDownload());
+  list.add(yield promiseNewDownload());
   do_check_eq(addNotifications, 3);
 
   // Check that we receive remove notifications.
   let removeNotifications = 0;
   let viewTwo = {
     onDownloadRemoved: function (aDownload) {
       do_check_eq(aDownload, downloadOne);
       removeNotifications++;
@@ -120,29 +120,29 @@ add_task(function test_notifications_add
 
   // We should not receive remove notifications after the view is removed.
   list.removeView(viewTwo);
   list.remove(downloadTwo);
   do_check_eq(removeNotifications, 1);
 
   // We should not receive add notifications after the view is removed.
   list.removeView(viewOne);
-  list.add(yield promiseSimpleDownload());
+  list.add(yield promiseNewDownload());
   do_check_eq(addNotifications, 3);
 });
 
 /**
  * Checks that views receive the download change notifications.
  */
 add_task(function test_notifications_change()
 {
   let list = yield promiseNewDownloadList();
 
-  let downloadOne = yield promiseSimpleDownload();
-  let downloadTwo = yield promiseSimpleDownload();
+  let downloadOne = yield promiseNewDownload();
+  let downloadTwo = yield promiseNewDownload();
   list.add(downloadOne);
   list.add(downloadTwo);
 
   // Check that we receive change notifications.
   let receivedOnDownloadChanged = false;
   list.addView({
     onDownloadChanged: function (aDownload) {
       do_check_eq(aDownload, downloadOne);
@@ -169,21 +169,21 @@ add_task(function test_history_expiratio
   }
   do_register_cleanup(cleanup);
 
   // Set max pages to 0 to make the download expire.
   Services.prefs.setIntPref("places.history.expiration.max_pages", 0);
 
   // Add expirable visit for downloads.
   yield promiseAddDownloadToHistory();
-  yield promiseAddDownloadToHistory(TEST_INTERRUPTIBLE_URI);
+  yield promiseAddDownloadToHistory(httpUrl("interruptible.txt"));
 
   let list = yield promiseNewDownloadList();
-  let downloadOne = yield promiseSimpleDownload();
-  let downloadTwo = yield promiseSimpleDownload(TEST_INTERRUPTIBLE_URI);
+  let downloadOne = yield promiseNewDownload();
+  let downloadTwo = yield promiseNewDownload(httpUrl("interruptible.txt"));
   list.add(downloadOne);
   list.add(downloadTwo);
 
   let deferred = Promise.defer();
   let removeNotifications = 0;
   let downloadView = {
     onDownloadRemoved: function (aDownload) {
       if (++removeNotifications == 2) {
@@ -216,18 +216,18 @@ add_task(function test_history_expiratio
  */
 add_task(function test_history_clear()
 {
   // Add expirable visit for downloads.
   yield promiseAddDownloadToHistory();
   yield promiseAddDownloadToHistory();
 
   let list = yield promiseNewDownloadList();
-  let downloadOne = yield promiseSimpleDownload();
-  let downloadTwo = yield promiseSimpleDownload();
+  let downloadOne = yield promiseNewDownload();
+  let downloadTwo = yield promiseNewDownload();
   list.add(downloadOne);
   list.add(downloadTwo);
 
   let deferred = Promise.defer();
   let removeNotifications = 0;
   let downloadView = {
     onDownloadRemoved: function (aDownload) {
       if (++removeNotifications == 2) {
--- a/toolkit/components/jsdownloads/test/unit/test_DownloadStore.js
+++ b/toolkit/components/jsdownloads/test/unit/test_DownloadStore.js
@@ -44,48 +44,43 @@ function promiseNewListAndStore(aStorePa
  * Saves downloads to a file, then reloads them.
  */
 add_task(function test_save_reload()
 {
   let [listForSave, storeForSave] = yield promiseNewListAndStore();
   let [listForLoad, storeForLoad] = yield promiseNewListAndStore(
                                                  storeForSave.path);
 
-  listForSave.add(yield promiseSimpleDownload(TEST_SOURCE_URI));
+  listForSave.add(yield promiseNewDownload(httpUrl("source.txt")));
   listForSave.add(yield Downloads.createDownload({
-    source: { uri: TEST_EMPTY_URI,
-              referrer: TEST_REFERRER_URI },
-    target: { file: getTempFile(TEST_TARGET_FILE_NAME) },
-    saver: { type: "copy" },
+    source: { url: httpUrl("empty.txt"),
+              referrer: TEST_REFERRER_URL },
+    target: getTempFile(TEST_TARGET_FILE_NAME),
   }));
 
   yield storeForSave.save();
   yield storeForLoad.load();
 
   let itemsForSave = yield listForSave.getAll();
   let itemsForLoad = yield listForLoad.getAll();
 
   do_check_eq(itemsForSave.length, itemsForLoad.length);
 
   // Downloads should be reloaded in the same order.
   for (let i = 0; i < itemsForSave.length; i++) {
     // The reloaded downloads are different objects.
     do_check_neq(itemsForSave[i], itemsForLoad[i]);
 
     // The reloaded downloads have the same properties.
-    do_check_true(itemsForSave[i].source.uri.equals(
-                  itemsForLoad[i].source.uri));
-    if (itemsForSave[i].source.referrer) {
-      do_check_true(itemsForSave[i].source.referrer.equals(
-                    itemsForLoad[i].source.referrer));
-    } else {
-      do_check_true(itemsForLoad[i].source.referrer === null);
-    }
-    do_check_true(itemsForSave[i].target.file.equals(
-                  itemsForLoad[i].target.file));
+    do_check_eq(itemsForSave[i].source.url,
+                itemsForLoad[i].source.url);
+    do_check_eq(itemsForSave[i].source.referrer,
+                itemsForLoad[i].source.referrer);
+    do_check_eq(itemsForSave[i].target.path,
+                itemsForLoad[i].target.path);
     do_check_eq(itemsForSave[i].saver.type,
                 itemsForLoad[i].saver.type);
   }
 });
 
 /**
  * Checks that saving an empty list deletes any existing file.
  */
@@ -124,89 +119,87 @@ add_task(function test_load_empty()
  * test is to verify that the JSON format used in previous versions can be
  * loaded, assuming the file is reloaded on the same platform.
  */
 add_task(function test_load_string_predefined()
 {
   let [list, store] = yield promiseNewListAndStore();
 
   // The platform-dependent file name should be generated dynamically.
-  let targetFile = getTempFile(TEST_TARGET_FILE_NAME);
-  let filePathLiteral = JSON.stringify(targetFile.path);
-  let sourceUriLiteral = JSON.stringify(TEST_SOURCE_URI.spec);
-  let emptyUriLiteral = JSON.stringify(TEST_EMPTY_URI.spec);
-  let referrerUriLiteral = JSON.stringify(TEST_REFERRER_URI.spec);
+  let targetPath = getTempFile(TEST_TARGET_FILE_NAME).path;
+  let filePathLiteral = JSON.stringify(targetPath);
+  let sourceUriLiteral = JSON.stringify(httpUrl("source.txt"));
+  let emptyUriLiteral = JSON.stringify(httpUrl("empty.txt"));
+  let referrerUriLiteral = JSON.stringify(TEST_REFERRER_URL);
 
-  let string = "[{\"source\":{\"uri\":" + sourceUriLiteral + "}," +
-                "\"target\":{\"file\":" + filePathLiteral + "}," +
-                "\"saver\":{\"type\":\"copy\"}}," +
-                "{\"source\":{\"uri\":" + emptyUriLiteral + "," +
+  let string = "{\"list\":[{\"source\":" + sourceUriLiteral + "," +
+                "\"target\":" + filePathLiteral + "}," +
+                "{\"source\":{\"url\":" + emptyUriLiteral + "," +
                 "\"referrer\":" + referrerUriLiteral + "}," +
-                "\"target\":{\"file\":" + filePathLiteral + "}," +
-                "\"saver\":{\"type\":\"copy\"}}]";
+                "\"target\":" + filePathLiteral + "}]}";
 
   yield OS.File.writeAtomic(store.path,
                             new TextEncoder().encode(string),
                             { tmpPath: store.path + ".tmp" });
 
   yield store.load();
 
   let items = yield list.getAll();
 
   do_check_eq(items.length, 2);
 
-  do_check_true(items[0].source.uri.equals(TEST_SOURCE_URI));
-  do_check_true(items[0].target.file.equals(targetFile));
+  do_check_eq(items[0].source.url, httpUrl("source.txt"));
+  do_check_eq(items[0].target.path, targetPath);
 
-  do_check_true(items[1].source.uri.equals(TEST_EMPTY_URI));
-  do_check_true(items[1].source.referrer.equals(TEST_REFERRER_URI));
-  do_check_true(items[1].target.file.equals(targetFile));
+  do_check_eq(items[1].source.url, httpUrl("empty.txt"));
+  do_check_eq(items[1].source.referrer, TEST_REFERRER_URL);
+  do_check_eq(items[1].target.path, targetPath);
 });
 
 /**
  * Loads downloads from a well-formed JSON string containing unrecognized data.
  */
 add_task(function test_load_string_unrecognized()
 {
   let [list, store] = yield promiseNewListAndStore();
 
   // The platform-dependent file name should be generated dynamically.
-  let targetFile = getTempFile(TEST_TARGET_FILE_NAME);
-  let filePathLiteral = JSON.stringify(targetFile.path);
-  let sourceUriLiteral = JSON.stringify(TEST_SOURCE_URI.spec);
+  let targetPath = getTempFile(TEST_TARGET_FILE_NAME).path;
+  let filePathLiteral = JSON.stringify(targetPath);
+  let sourceUriLiteral = JSON.stringify(httpUrl("source.txt"));
 
-  let string = "[{\"source\":null," +
+  let string = "{\"list\":[{\"source\":null," +
                 "\"target\":null}," +
-                "{\"source\":{\"uri\":" + sourceUriLiteral + "}," +
-                "\"target\":{\"file\":" + filePathLiteral + "}," +
-                "\"saver\":{\"type\":\"copy\"}}]";
+                "{\"source\":{\"url\":" + sourceUriLiteral + "}," +
+                "\"target\":{\"path\":" + filePathLiteral + "}," +
+                "\"saver\":{\"type\":\"copy\"}}]}";
 
   yield OS.File.writeAtomic(store.path,
                             new TextEncoder().encode(string),
                             { tmpPath: store.path + ".tmp" });
 
   yield store.load();
 
   let items = yield list.getAll();
 
   do_check_eq(items.length, 1);
 
-  do_check_true(items[0].source.uri.equals(TEST_SOURCE_URI));
-  do_check_true(items[0].target.file.equals(targetFile));
+  do_check_eq(items[0].source.url, httpUrl("source.txt"));
+  do_check_eq(items[0].target.path, targetPath);
 });
 
 /**
  * Loads downloads from a malformed JSON string.
  */
 add_task(function test_load_string_malformed()
 {
   let [list, store] = yield promiseNewListAndStore();
 
-  let string = "[{\"source\":null,\"target\":null}," +
-                "{\"source\":{\"uri\":\"about:blank\"}}";
+  let string = "{\"list\":[{\"source\":null,\"target\":null}," +
+                "{\"source\":{\"url\":\"about:blank\"}}}";
 
   yield OS.File.writeAtomic(store.path, new TextEncoder().encode(string),
                             { tmpPath: store.path + ".tmp" });
 
   try {
     yield store.load();
     do_throw("Exception expected when JSON data is malformed.");
   } catch (ex if ex.name == "SyntaxError") {
--- a/toolkit/components/jsdownloads/test/unit/test_Downloads.js
+++ b/toolkit/components/jsdownloads/test/unit/test_Downloads.js
@@ -15,93 +15,92 @@
 /**
  * Tests that the createDownload function exists and can be called.  More
  * detailed tests are implemented separately for the DownloadCore module.
  */
 add_task(function test_createDownload()
 {
   // Creates a simple Download object without starting the download.
   yield Downloads.createDownload({
-    source: { uri: NetUtil.newURI("about:blank") },
-    target: { file: getTempFile(TEST_TARGET_FILE_NAME) },
+    source: { url: "about:blank" },
+    target: { path: getTempFile(TEST_TARGET_FILE_NAME).path },
     saver: { type: "copy" },
   });
 });
 
 /**
-* Tests createDownload for private download.
+ * Tests createDownload for private download.
  */
 add_task(function test_createDownload_private()
 {
   let download = yield Downloads.createDownload({
-    source: { uri: NetUtil.newURI("about:blank"),
-              isPrivate: true },
-    target: { file: getTempFile(TEST_TARGET_FILE_NAME) },
+    source: { url: "about:blank", isPrivate: true },
+    target: { path: getTempFile(TEST_TARGET_FILE_NAME).path },
     saver: { type: "copy" }
   });
   do_check_true(download.source.isPrivate);
 });
 
 /**
  * Tests createDownload for normal (public) download.
  */
 add_task(function test_createDownload_public()
 {
-  let uri = NetUtil.newURI("about:blank");
-  let tempFile = getTempFile(TEST_TARGET_FILE_NAME);
+  let tempPath = getTempFile(TEST_TARGET_FILE_NAME).path;
   let download = yield Downloads.createDownload({
-    source: { uri: uri, isPrivate: false },
-    target: { file: tempFile },
+    source: { url: "about:blank", isPrivate: false },
+    target: { path: tempPath },
     saver: { type: "copy" }
   });
   do_check_false(download.source.isPrivate);
 
   download = yield Downloads.createDownload({
-    source: { uri: uri },
-    target: { file: tempFile },
+    source: { url: "about:blank" },
+    target: { path: tempPath },
     saver: { type: "copy" }
   });
-  do_check_true(!download.source.isPrivate);
+  do_check_false(download.source.isPrivate);
 });
 
 /**
  * Tests simpleDownload with nsIURI and nsIFile as arguments.
  */
 add_task(function test_simpleDownload_uri_file_arguments()
 {
   let targetFile = getTempFile(TEST_TARGET_FILE_NAME);
-  yield Downloads.simpleDownload(TEST_SOURCE_URI, targetFile);
-  yield promiseVerifyContents(targetFile, TEST_DATA_SHORT);
+  yield Downloads.simpleDownload(NetUtil.newURI(httpUrl("source.txt")),
+                                 targetFile);
+  yield promiseVerifyContents(targetFile.path, TEST_DATA_SHORT);
 });
 
 /**
  * Tests simpleDownload with DownloadSource and DownloadTarget as arguments.
  */
 add_task(function test_simpleDownload_object_arguments()
 {
-  let targetFile = getTempFile(TEST_TARGET_FILE_NAME);
-  yield Downloads.simpleDownload({ uri: TEST_SOURCE_URI },
-                                 { file: targetFile });
-  yield promiseVerifyContents(targetFile, TEST_DATA_SHORT);
+  let targetPath = getTempFile(TEST_TARGET_FILE_NAME).path;
+  yield Downloads.simpleDownload({ url: httpUrl("source.txt") },
+                                 { path: targetPath });
+  yield promiseVerifyContents(targetPath, TEST_DATA_SHORT);
 });
 
 /**
  * Tests simpleDownload with string arguments.
  */
 add_task(function test_simpleDownload_string_arguments()
 {
-  let targetFile = getTempFile(TEST_TARGET_FILE_NAME);
-  yield Downloads.simpleDownload(TEST_SOURCE_URI.spec,
-                                 targetFile.path);
-  yield promiseVerifyContents(targetFile, TEST_DATA_SHORT);
+  let targetPath = getTempFile(TEST_TARGET_FILE_NAME).path;
+  yield Downloads.simpleDownload(httpUrl("source.txt"),
+                                 targetPath);
+  yield promiseVerifyContents(targetPath, TEST_DATA_SHORT);
 
-  targetFile = getTempFile(TEST_TARGET_FILE_NAME);
-  yield Downloads.simpleDownload(new String(TEST_SOURCE_URI.spec),
-                                 new String(targetFile.path));
-  yield promiseVerifyContents(targetFile, TEST_DATA_SHORT);
+  targetPath = getTempFile(TEST_TARGET_FILE_NAME).path;
+  yield Downloads.simpleDownload(new String(httpUrl("source.txt")),
+                                 new String(targetPath));
+  yield promiseVerifyContents(targetPath, TEST_DATA_SHORT);
 });
 
 /**
  * Tests that the getPublicDownloadList function returns the same list when
  * called multiple times.  More detailed tests are implemented separately for
  * the DownloadList module.
  */
 add_task(function test_getPublicDownloadList()