Bug 1063217 - Support a PDF DownloadSaver. r=paolo
authorWes Johnston <wjohnston@mozilla.com>
Tue, 28 Oct 2014 16:05:32 -0700
changeset 219145 a8f9ed3d7554f4eb51cf6f0f6295f00dc22bc6a1
parent 219144 477d76b9d58d93cdbb183e76eee52c936ee4d53c
child 219146 00b588dac8bc373f019a400baa894808721d7fc2
push id27956
push userkwierso@gmail.com
push dateFri, 12 Dec 2014 00:47:19 +0000
treeherdermozilla-central@32a2c5bd2f68 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerspaolo
bugs1063217
milestone37.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1063217 - Support a PDF DownloadSaver. r=paolo
toolkit/components/jsdownloads/moz.build
toolkit/components/jsdownloads/src/DownloadCore.jsm
toolkit/components/jsdownloads/src/DownloadStore.jsm
toolkit/components/jsdownloads/test/browser/browser.ini
toolkit/components/jsdownloads/test/browser/browser_DownloadPDFSaver.js
toolkit/components/jsdownloads/test/browser/head.js
toolkit/components/jsdownloads/test/browser/testFile.html
toolkit/components/jsdownloads/test/unit/test_DownloadStore.js
toolkit/components/jsdownloads/test/unit/test_Downloads.js
--- a/toolkit/components/jsdownloads/moz.build
+++ b/toolkit/components/jsdownloads/moz.build
@@ -2,8 +2,9 @@
 # vim: set filetype=python:
 # 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/.
 
 DIRS += ['public', 'src']
 
 XPCSHELL_TESTS_MANIFESTS += ['test/data/xpcshell.ini', 'test/unit/xpcshell.ini']
+BROWSER_CHROME_MANIFESTS += ['test/browser/browser.ini']
--- a/toolkit/components/jsdownloads/src/DownloadCore.jsm
+++ b/toolkit/components/jsdownloads/src/DownloadCore.jsm
@@ -25,28 +25,34 @@
  * DownloadSaver
  * Template for an object that actually transfers the data for the download.
  *
  * DownloadCopySaver
  * Saver object that simply copies the entire source file to the target.
  *
  * DownloadLegacySaver
  * Saver object that integrates with the legacy nsITransfer interface.
+ *
+ * DownloadPDFSaver
+ * This DownloadSaver type creates a PDF file from the current document in a
+ * given window, specified using the windowRef property of the DownloadSource
+ * object associated with the download.
  */
 
 "use strict";
 
 this.EXPORTED_SYMBOLS = [
   "Download",
   "DownloadSource",
   "DownloadTarget",
   "DownloadError",
   "DownloadSaver",
   "DownloadCopySaver",
   "DownloadLegacySaver",
+  "DownloadPDFSaver",
 ];
 
 ////////////////////////////////////////////////////////////////////////////////
 //// Globals
 
 const Cc = Components.classes;
 const Ci = Components.interfaces;
 const Cu = Components.utils;
@@ -63,26 +69,31 @@ XPCOMUtils.defineLazyModuleGetter(this, 
 XPCOMUtils.defineLazyModuleGetter(this, "OS",
                                   "resource://gre/modules/osfile.jsm")
 XPCOMUtils.defineLazyModuleGetter(this, "Promise",
                                   "resource://gre/modules/Promise.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Services",
                                   "resource://gre/modules/Services.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Task",
                                   "resource://gre/modules/Task.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
+                                  "resource://gre/modules/PrivateBrowsingUtils.jsm");
 
 XPCOMUtils.defineLazyServiceGetter(this, "gDownloadHistory",
            "@mozilla.org/browser/download-history;1",
            Ci.nsIDownloadHistory);
 XPCOMUtils.defineLazyServiceGetter(this, "gExternalAppLauncher",
            "@mozilla.org/uriloader/external-helper-app-service;1",
            Ci.nsPIExternalAppLauncher);
 XPCOMUtils.defineLazyServiceGetter(this, "gExternalHelperAppService",
            "@mozilla.org/uriloader/external-helper-app-service;1",
            Ci.nsIExternalHelperAppService);
+XPCOMUtils.defineLazyServiceGetter(this, "gPrintSettingsService",
+           "@mozilla.org/gfx/printsettings-service;1",
+           Ci.nsIPrintSettingsService);
 
 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.
  */
@@ -539,17 +550,17 @@ this.Download.prototype = {
    * @return {Promise}
    * @resolves When the instruction to launch the file has been
    *           successfully given to the operating system. Note that
    *           the OS might still take a while until the file is actually
    *           launched.
    * @rejects  JavaScript exception if there was an error trying to launch
    *           the file.
    */
-  launch: function() {
+  launch: function () {
     if (!this.succeeded) {
       return Promise.reject(
         new Error("launch can only be called if the download succeeded")
       );
     }
 
     return DownloadIntegration.launchDownload(this);
   },
@@ -906,21 +917,26 @@ this.Download.prototype = {
    */
   toSerializable: function ()
   {
     let serializable = {
       source: this.source.toSerializable(),
       target: this.target.toSerializable(),
     };
 
+    let saver = this.saver.toSerializable();
+    if (!saver) {
+      // If we are unable to serialize the saver, we won't persist the download.
+      return null;
+    }
+
     // 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;
     }
 
     if (this.error) {
       serializable.errorObj = this.error.toSerializable();
     }
 
@@ -1121,16 +1137,20 @@ this.DownloadSource.prototype = {
  */
 this.DownloadSource.fromSerializable = function (aSerializable) {
   let source = new DownloadSource();
   if (isString(aSerializable)) {
     // Convert String objects to primitive strings at this point.
     source.url = aSerializable.toString();
   } else if (aSerializable instanceof Ci.nsIURI) {
     source.url = aSerializable.spec;
+  } else if (aSerializable instanceof Ci.nsIDOMWindow) {
+    source.url = aSerializable.location.href;
+    source.isPrivate = PrivateBrowsingUtils.isContentWindowPrivate(aSerializable);
+    source.windowRef = Cu.getWeakReference(aSerializable);
   } else {
     // Convert String objects to primitive strings at this point.
     source.url = aSerializable.url.toString();
     if ("isPrivate" in aSerializable) {
       source.isPrivate = aSerializable.isPrivate;
     }
     if ("referrer" in aSerializable) {
       source.referrer = aSerializable.referrer;
@@ -1521,16 +1541,19 @@ this.DownloadSaver.fromSerializable = fu
   let saver;
   switch (serializable.type) {
     case "copy":
       saver = DownloadCopySaver.fromSerializable(serializable);
       break;
     case "legacy":
       saver = DownloadLegacySaver.fromSerializable(serializable);
       break;
+    case "pdf":
+      saver = DownloadPDFSaver.fromSerializable(serializable);
+      break;
     default:
       throw new Error("Unrecoginzed download saver type.");
   }
   return saver;
 };
 
 ////////////////////////////////////////////////////////////////////////////////
 //// DownloadCopySaver
@@ -1944,17 +1967,17 @@ this.DownloadCopySaver.fromSerializable 
 ////////////////////////////////////////////////////////////////////////////////
 //// DownloadLegacySaver
 
 /**
  * Saver object that integrates with the legacy nsITransfer interface.
  *
  * For more background on the process, see the DownloadLegacyTransfer object.
  */
-this.DownloadLegacySaver = function()
+this.DownloadLegacySaver = function ()
 {
   this.deferExecuted = Promise.defer();
   this.deferCanceled = Promise.defer();
 }
 
 this.DownloadLegacySaver.prototype = {
   __proto__: DownloadSaver.prototype,
 
@@ -2297,8 +2320,164 @@ this.DownloadLegacySaver.prototype = {
 /**
  * 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.
  */
 this.DownloadLegacySaver.fromSerializable = function () {
   return new DownloadLegacySaver();
 };
+
+////////////////////////////////////////////////////////////////////////////////
+//// DownloadPDFSaver
+
+/**
+ * This DownloadSaver type creates a PDF file from the current document in a
+ * given window, specified using the windowRef property of the DownloadSource
+ * object associated with the download.
+ *
+ * In order to prevent the download from saving a different document than the one
+ * originally loaded in the window, any attempt to restart the download will fail.
+ *
+ * Since this DownloadSaver type requires a live document as a source, it cannot
+ * be persisted across sessions, unless the download already succeeded.
+ */
+this.DownloadPDFSaver = function () {
+}
+
+this.DownloadPDFSaver.prototype = {
+  __proto__: DownloadSaver.prototype,
+
+  /**
+   * An nsIWebBrowserPrint instance for printing this page.
+   * This is null when saving has not started or has completed,
+   * or while the operation is being canceled.
+   */
+  _webBrowserPrint: null,
+
+  /**
+   * Implements "DownloadSaver.execute".
+   */
+  execute: function (aSetProgressBytesFn, aSetPropertiesFn)
+  {
+    return Task.spawn(function task_DCS_execute() {
+      if (!this.download.source.windowRef) {
+        throw new DownloadError({
+          message: "PDF saver must be passed an open window, and cannot be restarted.",
+          becauseSourceFailed: true,
+        });
+      }
+
+      let win = this.download.source.windowRef.get();
+
+      // Set windowRef to null to avoid re-trying.
+      this.download.source.windowRef = null;
+
+      if (!win) {
+        throw new DownloadError({
+          message: "PDF saver can't save a window that has been closed.",
+          becauseSourceFailed: true,
+        });
+      }
+
+      this.addToHistory();
+
+      let targetPath = this.download.target.path;
+
+      // An empty target file must exist for the PDF printer to work correctly.
+      let file = yield OS.File.open(targetPath, { truncate: true });
+      yield file.close();
+
+      let printSettings = gPrintSettingsService.newPrintSettings;
+
+      printSettings.printToFile = true;
+      printSettings.outputFormat = Ci.nsIPrintSettings.kOutputFormatPDF;
+      printSettings.toFileName = targetPath;
+
+      printSettings.printSilent = true;
+      printSettings.showPrintProgress = false;
+
+      printSettings.printBGImages = true;
+      printSettings.printBGColors = true;
+      printSettings.printFrameType = Ci.nsIPrintSettings.kFramesAsIs;
+      printSettings.headerStrCenter = "";
+      printSettings.headerStrLeft = "";
+      printSettings.headerStrRight = "";
+      printSettings.footerStrCenter = "";
+      printSettings.footerStrLeft = "";
+      printSettings.footerStrRight = "";
+
+      this._webBrowserPrint = win.QueryInterface(Ci.nsIInterfaceRequestor)
+                                 .getInterface(Ci.nsIWebBrowserPrint);
+
+      try {
+        yield new Promise((resolve, reject) => {
+          this._webBrowserPrint.print(printSettings, {
+            onStateChange: function (webProgress, request, stateFlags, status) {
+              if (stateFlags & Ci.nsIWebProgressListener.STATE_STOP) {
+                if (!Components.isSuccessCode(status)) {
+                  reject(new DownloadError({ result: status,
+                                             inferCause: true }));
+                } else {
+                  resolve();
+                }
+              }
+            },
+            onProgressChange: function (webProgress, request, curSelfProgress,
+                                        maxSelfProgress, curTotalProgress,
+                                        maxTotalProgress) {
+              aSetProgressBytesFn(curTotalProgress, maxTotalProgress, false);
+            },
+            onLocationChange: function () {},
+            onStatusChange: function () {},
+            onSecurityChange: function () {},
+          });
+        });
+      } finally {
+        // Remove the print object to avoid leaks
+        this._webBrowserPrint = null;
+      }
+
+      let fileInfo = yield OS.File.stat(targetPath);
+      aSetProgressBytesFn(fileInfo.size, fileInfo.size, false);
+    }.bind(this));
+  },
+
+  /**
+   * Implements "DownloadSaver.cancel".
+   */
+  cancel: function DCS_cancel()
+  {
+    if (this._webBrowserPrint) {
+      this._webBrowserPrint.cancel();
+      this._webBrowserPrint = null;
+    }
+  },
+
+  /**
+   * Implements "DownloadSaver.toSerializable".
+   */
+  toSerializable: function ()
+  {
+    if (this.download.succeeded) {
+      return DownloadCopySaver.prototype.toSerializable.call(this);
+    }
+
+    // This object needs a window to recreate itself. If it didn't succeded
+    // it will not be possible to restart. Returning null here will
+    // prevent us from serializing it at all.
+    return null;
+  },
+};
+
+/**
+ * Creates a new DownloadPDFSaver object, with its initial state derived from
+ * its serializable representation.
+ *
+ * @param aSerializable
+ *        Serializable representation of a DownloadPDFSaver object.
+ *
+ * @return The newly created DownloadPDFSaver object.
+ */
+this.DownloadPDFSaver.fromSerializable = function (aSerializable) {
+  return new DownloadPDFSaver();
+};
+
--- a/toolkit/components/jsdownloads/src/DownloadStore.jsm
+++ b/toolkit/components/jsdownloads/src/DownloadStore.jsm
@@ -159,17 +159,23 @@ this.DownloadStore.prototype = {
       // Take a static snapshot of the current state of all the downloads.
       let storeData = { list: [] };
       let atLeastOneDownload = false;
       for (let download of downloads) {
         try {
           if (!this.onsaveitem(download)) {
             continue;
           }
-          storeData.list.push(download.toSerializable());
+
+          let serializable = download.toSerializable();
+          if (!serializable) {
+            // This item cannot be persisted across sessions.
+            continue;
+          }
+          storeData.list.push(serializable);
           atLeastOneDownload = true;
         } catch (ex) {
           // If an item cannot be converted to a serializable form, don't
           // prevent others from being saved.
           Cu.reportError(ex);
         }
       }
 
new file mode 100644
--- /dev/null
+++ b/toolkit/components/jsdownloads/test/browser/browser.ini
@@ -0,0 +1,7 @@
+[DEFAULT]
+support-files =
+  head.js
+  testFile.html
+
+[browser_DownloadPDFSaver.js]
+skip-if = e10s || os != "win"
new file mode 100644
--- /dev/null
+++ b/toolkit/components/jsdownloads/test/browser/browser_DownloadPDFSaver.js
@@ -0,0 +1,101 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 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 PDF download saver, and tests using a window as a
+ * source for the copy download saver.
+ */
+
+"use strict";
+
+/**
+ * Helper function to make sure a window reference exists on the download source.
+ */
+function* test_download_windowRef(aTab, aDownload) {
+  ok(aDownload.source.windowRef, "Download source had a window reference");
+  ok(aDownload.source.windowRef instanceof Ci.xpcIJSWeakReference, "Download window reference is a weak ref");
+  is(aDownload.source.windowRef.get(), aTab.linkedBrowser.contentWindow, "Download window exists during test");
+}
+
+/**
+ * Helper function to check the state of a completed download.
+ */
+function* test_download_state_complete(aTab, aDownload, aPrivate, aCanceled) {
+  ok(aDownload.source, "Download has a source");
+  is(aDownload.source.url, aTab.linkedBrowser.contentWindow.location, "Download source has correct url");
+  is(aDownload.source.isPrivate, aPrivate, "Download source has correct private state");
+  ok(aDownload.stopped, "Download is stopped");
+  is(aCanceled, aDownload.canceled, "Download has correct canceled state");
+  is(!aCanceled, aDownload.succeeded, "Download has correct succeeded state");
+  is(aDownload.error, null, "Download error is not defined");
+}
+
+function* test_createDownload_common(aPrivate, aType) {
+  let tab = gBrowser.addTab(getRootDirectory(gTestPath) + "testFile.html");
+  yield promiseBrowserLoaded(tab.linkedBrowser);
+
+  if (aPrivate) {
+    tab.linkedBrowser.docShell.QueryInterface(Ci.nsILoadContext)
+                              .usePrivateBrowsing = true;
+  }
+
+  let download = yield Downloads.createDownload({
+    source: tab.linkedBrowser.contentWindow,
+    target: { path: getTempFile(TEST_TARGET_FILE_NAME_PDF).path },
+    saver: { type: aType },
+  });
+
+  yield test_download_windowRef(tab, download);
+  yield download.start();
+
+  yield test_download_state_complete(tab, download, aPrivate, false);
+  if (aType == "pdf") {
+    let signature = yield OS.File.read(download.target.path,
+                                       { bytes: 4, encoding: "us-ascii" });
+    is(signature, "%PDF", "File exists and signature matches");
+  } else {
+    ok((yield OS.File.exists(download.target.path)), "File exists");
+  }
+
+  gBrowser.removeTab(tab);
+}
+
+add_task(function* test_createDownload_pdf_private() {
+  yield test_createDownload_common(true, "pdf");
+});
+add_task(function* test_createDownload_pdf_not_private() {
+  yield test_createDownload_common(false, "pdf");
+});
+
+// Even for the copy saver, using a window should produce valid results
+add_task(function* test_createDownload_copy_private() {
+  yield test_createDownload_common(true, "copy");
+});
+add_task(function* test_createDownload_copy_not_private() {
+  yield test_createDownload_common(false, "copy");
+});
+
+add_task(function* test_cancel_pdf_download() {
+  let tab = gBrowser.addTab(getRootDirectory(gTestPath) + "testFile.html");
+  yield promiseBrowserLoaded(tab.linkedBrowser);
+
+  let download = yield Downloads.createDownload({
+    source: tab.linkedBrowser.contentWindow,
+    target: { path: getTempFile(TEST_TARGET_FILE_NAME_PDF).path },
+    saver: "pdf",
+  });
+
+  yield test_download_windowRef(tab, download);
+  download.start();
+
+  // Immediately cancel the download to test that it is erased correctly.
+  yield download.cancel();
+  yield test_download_state_complete(tab, download, false, true);
+
+  let exists = yield OS.File.exists(download.target.path)
+  ok(!exists, "Target file does not exist");
+
+  gBrowser.removeTab(tab);
+});
new file mode 100644
--- /dev/null
+++ b/toolkit/components/jsdownloads/test/browser/head.js
@@ -0,0 +1,89 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 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/ */
+
+/**
+ * Provides infrastructure for automated download components tests.
+ */
+
+"use strict";
+
+////////////////////////////////////////////////////////////////////////////////
+//// Globals
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cu = Components.utils;
+const Cr = Components.results;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "DownloadPaths",
+                                  "resource://gre/modules/DownloadPaths.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Downloads",
+                                  "resource://gre/modules/Downloads.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
+                                  "resource://gre/modules/FileUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Services",
+                                  "resource://gre/modules/Services.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "HttpServer",
+                                  "resource://testing-common/httpd.js");
+XPCOMUtils.defineLazyModuleGetter(this, "OS",
+                                  "resource://gre/modules/osfile.jsm");
+
+const TEST_TARGET_FILE_NAME_PDF = "test-download.pdf";
+
+////////////////////////////////////////////////////////////////////////////////
+//// Support functions
+
+// While the previous test file should have deleted all the temporary files it
+// used, on Windows these might still be pending deletion on the physical file
+// system.  Thus, start from a new base number every time, to make a collision
+// with a file that is still pending deletion highly unlikely.
+let gFileCounter = Math.floor(Math.random() * 1000000);
+
+/**
+ * Returns a reference to a temporary file, that is guaranteed not to exist, and
+ * to have never been created before.
+ *
+ * @param aLeafName
+ *        Suggested leaf name for the file to be created.
+ *
+ * @return nsIFile pointing to a non-existent file in a temporary directory.
+ *
+ * @note It is not enough to delete the file if it exists, or to delete the file
+ *       after calling nsIFile.createUnique, because on Windows the delete
+ *       operation in the file system may still be pending, preventing a new
+ *       file with the same name to be created.
+ */
+function getTempFile(aLeafName)
+{
+  // Prepend a serial number to the extension in the suggested leaf name.
+  let [base, ext] = DownloadPaths.splitBaseNameAndExtension(aLeafName);
+  let leafName = base + "-" + gFileCounter + ext;
+  gFileCounter++;
+
+  // Get a file reference under the temporary directory for this test file.
+  let file = FileUtils.getFile("TmpD", [leafName]);
+  ok(!file.exists(), "Temp file does not exist");
+
+  registerCleanupFunction(function () {
+    if (file.exists()) {
+      file.remove(false);
+    }
+  });
+
+  return file;
+}
+
+function promiseBrowserLoaded(browser) {
+  return new Promise(resolve => {
+    browser.addEventListener("load", function onLoad(event) {
+      if (event.target == browser.contentDocument) {
+        browser.removeEventListener("load", onLoad, true);
+        resolve();
+      }
+    }, true);
+  });
+}
new file mode 100644
--- /dev/null
+++ b/toolkit/components/jsdownloads/test/browser/testFile.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <title>Test Save as PDF</title>
+  </head>
+  <body>
+    <p>Save me as a PDF!</p>
+  </body>
+</html>
--- a/toolkit/components/jsdownloads/test/unit/test_DownloadStore.js
+++ b/toolkit/components/jsdownloads/test/unit/test_DownloadStore.js
@@ -52,23 +52,35 @@ add_task(function test_save_reload()
 
   listForSave.add(yield promiseNewDownload(httpUrl("source.txt")));
   listForSave.add(yield Downloads.createDownload({
     source: { url: httpUrl("empty.txt"),
               referrer: TEST_REFERRER_URL },
     target: getTempFile(TEST_TARGET_FILE_NAME),
   }));
 
+  // This PDF download should not be serialized because it never succeeds.
+  let pdfDownload = yield Downloads.createDownload({
+    source: { url: httpUrl("empty.txt"),
+              referrer: TEST_REFERRER_URL },
+    target: getTempFile(TEST_TARGET_FILE_NAME),
+    saver: "pdf",
+  });
+  listForSave.add(pdfDownload);
+
   let legacyDownload = yield promiseStartLegacyDownload();
   yield legacyDownload.cancel();
   listForSave.add(legacyDownload);
 
   yield storeForSave.save();
   yield storeForLoad.load();
 
+  // Remove the PDF download because it should not appear in this list.
+  listForSave.remove(pdfDownload);
+
   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.
--- a/toolkit/components/jsdownloads/test/unit/test_Downloads.js
+++ b/toolkit/components/jsdownloads/test/unit/test_Downloads.js
@@ -56,16 +56,41 @@ add_task(function test_createDownload_pu
     source: { url: "about:blank" },
     target: { path: tempPath },
     saver: { type: "copy" }
   });
   do_check_false(download.source.isPrivate);
 });
 
 /**
+ * Tests createDownload for a pdf saver throws if only given a url.
+ */
+add_task(function test_createDownload_pdf()
+{
+  let download = yield Downloads.createDownload({
+    source: { url: "about:blank" },
+    target: { path: getTempFile(TEST_TARGET_FILE_NAME).path },
+    saver: { type: "pdf" },
+  });
+
+  try {
+    yield download.start();
+    do_throw("The download should have failed.");
+  } catch (ex if ex instanceof Downloads.Error && ex.becauseSourceFailed) { }
+
+  do_check_false(download.succeeded);
+  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);
+  do_check_false(yield OS.File.exists(download.target.path));
+});
+
+/**
  * Tests "fetch" with nsIURI and nsIFile as arguments.
  */
 add_task(function test_fetch_uri_file_arguments()
 {
   let targetFile = getTempFile(TEST_TARGET_FILE_NAME);
   yield Downloads.fetch(NetUtil.newURI(httpUrl("source.txt")), targetFile);
   yield promiseVerifyContents(targetFile.path, TEST_DATA_SHORT);
 });