Merge m-c to birch.
authorRyan VanderMeulen <ryanvm@gmail.com>
Fri, 26 Jul 2013 12:10:00 -0400
changeset 152408 343f7c10ed89efdaafca4367e595f3b083088ae3
parent 152407 5d6c8cda86088ff0aabc7e9d4012f83dc0d414f0 (current diff)
parent 152389 8da2f00eb92ea5dc7f8b0e2f312153f6c03fd607 (diff)
child 152409 9e8286127eb3960f1aa8fda6da1d1bce0bdc0292
push id2859
push userakeybl@mozilla.com
push dateMon, 16 Sep 2013 19:14:59 +0000
treeherdermozilla-beta@87d3c51cd2bf [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
milestone25.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
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()