From aac56c7c189b7ea5b5f87dc0b98fde095023ba63 Mon Sep 17 00:00:00 2001 From: Paolo Amadini Date: Tue, 5 Mar 2013 12:10:03 +0100 Subject: [PATCH] Bug 835880 - Implement the basic DownloadList object. r=enn --HG-- extra : rebase_source : 15c2ba2132c87fdd633dc069a1e2ebef727c6392 --- .../jsdownloads/src/DownloadList.jsm | 146 ++++++++++++++- .../components/jsdownloads/src/Downloads.jsm | 24 +++ .../components/jsdownloads/test/unit/head.js | 19 ++ .../test/unit/test_DownloadCore.js | 22 --- .../test/unit/test_DownloadList.js | 172 ++++++++++++++++++ .../jsdownloads/test/unit/test_Downloads.js | 15 +- .../jsdownloads/test/unit/xpcshell.ini | 1 + 7 files changed, 375 insertions(+), 24 deletions(-) create mode 100644 toolkit/components/jsdownloads/test/unit/test_DownloadList.js diff --git a/toolkit/components/jsdownloads/src/DownloadList.jsm b/toolkit/components/jsdownloads/src/DownloadList.jsm index b8757a90d63d..968ba679f6ec 100644 --- a/toolkit/components/jsdownloads/src/DownloadList.jsm +++ b/toolkit/components/jsdownloads/src/DownloadList.jsm @@ -25,6 +25,9 @@ const Cr = Components.results; Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "Promise", + "resource://gre/modules/commonjs/sdk/core/promise.js"); + //////////////////////////////////////////////////////////////////////////////// //// DownloadList @@ -32,7 +35,148 @@ Cu.import("resource://gre/modules/XPCOMUtils.jsm"); * Represents a collection of Download objects that can be viewed and managed by * the user interface, and persisted across sessions. */ -function DownloadList() { } +function DownloadList() { + this._downloads = []; + this._views = new Set(); +} DownloadList.prototype = { + /** + * Array of Download objects currently in the list. + */ + _downloads: null, + + /** + * Retrieves a snapshot of the downloads that are currently in the list. The + * returned array does not change when downloads are added or removed, though + * the Download objects it contains are still updated in real time. + * + * @return {Promise} + * @resolves An array of Download objects. + * @rejects JavaScript exception. + */ + getAll: function DL_getAll() { + return Promise.resolve(Array.slice(this._downloads, 0)); + }, + + /** + * Adds a new download to the end of the items list. + * + * @note When a download is added to the list, its "onchange" event is + * registered by the list, thus it cannot be used to monitor the + * download. To receive change notifications for downloads that are + * added to the list, use the addView method to register for + * onDownloadChanged notifications. + * + * @param aDownload + * The Download object to add. + */ + add: function DL_add(aDownload) { + this._downloads.push(aDownload); + aDownload.onchange = this._change.bind(this, aDownload); + + for (let view of this._views) { + try { + if (view.onDownloadAdded) { + view.onDownloadAdded(aDownload); + } + } catch (ex) { + Cu.reportError(ex); + } + } + }, + + /** + * Removes a download from the list. If the download was already removed, + * this method has no effect. + * + * @param aDownload + * The Download object to remove. + */ + remove: function DL_remove(aDownload) { + let index = this._downloads.indexOf(aDownload); + if (index != -1) { + this._downloads.splice(index, 1); + aDownload.onchange = null; + + for (let view of this._views) { + try { + if (view.onDownloadRemoved) { + view.onDownloadRemoved(aDownload); + } + } catch (ex) { + Cu.reportError(ex); + } + } + } + }, + + /** + * This function is called when "onchange" events of downloads occur. + * + * @param aDownload + * The Download object that changed. + */ + _change: function DL_change(aDownload) { + for (let view of this._views) { + try { + if (view.onDownloadChanged) { + view.onDownloadChanged(aDownload); + } + } catch (ex) { + Cu.reportError(ex); + } + } + }, + + /** + * Set of currently registered views. + */ + _views: null, + + /** + * Adds a view that will be notified of changes to downloads. The newly added + * view will receive onDownloadAdded notifications for all the downloads that + * are already in the list. + * + * @param aView + * The view object to add. The following methods may be defined: + * { + * onDownloadAdded: function (aDownload) { + * // Called after aDownload is added to the end of the list. + * }, + * onDownloadChanged: function (aDownload) { + * // Called after the properties of aDownload change. + * }, + * onDownloadRemoved: function (aDownload) { + * // Called after aDownload is removed from the list. + * }, + * } + */ + addView: function DL_addView(aView) + { + this._views.add(aView); + + if (aView.onDownloadAdded) { + for (let download of this._downloads) { + try { + aView.onDownloadAdded(download); + } catch (ex) { + Cu.reportError(ex); + } + } + } + }, + + /** + * Removes a view that was previously added using addView. The removed view + * will not receive any more notifications after this method returns. + * + * @param aView + * The view object to remove. + */ + removeView: function DL_removeView(aView) + { + this._views.delete(aView); + }, }; diff --git a/toolkit/components/jsdownloads/src/Downloads.jsm b/toolkit/components/jsdownloads/src/Downloads.jsm index d405cb537fc9..f58e3dc0444d 100644 --- a/toolkit/components/jsdownloads/src/Downloads.jsm +++ b/toolkit/components/jsdownloads/src/Downloads.jsm @@ -33,6 +33,8 @@ XPCOMUtils.defineLazyModuleGetter(this, "DownloadStore", "resource://gre/modules/DownloadStore.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "DownloadUIHelper", "resource://gre/modules/DownloadUIHelper.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", @@ -125,6 +127,28 @@ this.Downloads = { }); }, + /** + * Retrieves the DownloadList object for downloads that were not started from + * a private browsing window. + * + * Calling this function may cause the download list to be reloaded from the + * previous session, if it wasn't loaded already. + * + * This method always retrieves a reference to the same download list. + * + * @return {Promise} + * @resolves The DownloadList object for public downloads. + * @rejects JavaScript exception. + */ + getPublicDownloadList: function D_getPublicDownloadList() + { + if (!this._publicDownloadList) { + this._publicDownloadList = new DownloadList(); + } + return Promise.resolve(this._publicDownloadList); + }, + _publicDownloadList: null, + /** * Constructor for a DownloadError object. When you catch an exception during * a download, you can use this to verify if "ex instanceof Downloads.Error", diff --git a/toolkit/components/jsdownloads/test/unit/head.js b/toolkit/components/jsdownloads/test/unit/head.js index fab9cd583edc..cdb2750195a5 100644 --- a/toolkit/components/jsdownloads/test/unit/head.js +++ b/toolkit/components/jsdownloads/test/unit/head.js @@ -90,6 +90,25 @@ function getTempFile(aLeafName) return file; } +/** + * Creates a new Download object, using TEST_TARGET_FILE_NAME as the target. + * The target is deleted by getTempFile when this function is called. + * + * @param aSourceURI + * The nsIURI for the download source, or null to use TEST_SOURCE_URI. + * + * @return {Promise} + * @resolves The newly created Download object. + * @rejects JavaScript exception. + */ +function promiseSimpleDownload(aSourceURI) { + return Downloads.createDownload({ + source: { uri: aSourceURI || TEST_SOURCE_URI }, + target: { file: getTempFile(TEST_TARGET_FILE_NAME) }, + saver: { type: "copy" }, + }); +} + /** * Ensures that the given file contents are equal to the given string. * diff --git a/toolkit/components/jsdownloads/test/unit/test_DownloadCore.js b/toolkit/components/jsdownloads/test/unit/test_DownloadCore.js index d248b77bbc29..6400ccf95610 100644 --- a/toolkit/components/jsdownloads/test/unit/test_DownloadCore.js +++ b/toolkit/components/jsdownloads/test/unit/test_DownloadCore.js @@ -9,28 +9,6 @@ "use strict"; -//////////////////////////////////////////////////////////////////////////////// -//// Globals - -/** - * Creates a new Download object, using TEST_TARGET_FILE_NAME as the target. - * The target is deleted by getTempFile when this function is called. - * - * @param aSourceURI - * The nsIURI for the download source, or null to use TEST_SOURCE_URI. - * - * @return {Promise} - * @resolves The newly created Download object. - * @rejects JavaScript exception. - */ -function promiseSimpleDownload(aSourceURI) { - return Downloads.createDownload({ - source: { uri: aSourceURI || TEST_SOURCE_URI }, - target: { file: getTempFile(TEST_TARGET_FILE_NAME) }, - saver: { type: "copy" }, - }); -} - //////////////////////////////////////////////////////////////////////////////// //// Tests diff --git a/toolkit/components/jsdownloads/test/unit/test_DownloadList.js b/toolkit/components/jsdownloads/test/unit/test_DownloadList.js new file mode 100644 index 000000000000..d3219c75ce6a --- /dev/null +++ b/toolkit/components/jsdownloads/test/unit/test_DownloadList.js @@ -0,0 +1,172 @@ +/* -*- 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 DownloadList object. + */ + +"use strict"; + +//////////////////////////////////////////////////////////////////////////////// +//// Globals + +/** + * Returns a new DownloadList object. + * + * @return {Promise} + * @resolves The newly created DownloadList object. + * @rejects JavaScript exception. + */ +function promiseNewDownloadList() { + // Force the creation of a new public download list. + Downloads._publicDownloadList = null; + return Downloads.getPublicDownloadList(); +} + +//////////////////////////////////////////////////////////////////////////////// +//// Tests + +/** + * Checks the testing mechanism used to build different download lists. + */ +add_task(function test_construction() +{ + let downloadListOne = yield promiseNewDownloadList(); + let downloadListTwo = yield promiseNewDownloadList(); + + do_check_neq(downloadListOne, downloadListTwo); +}); + +/** + * 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(); + list.add(downloadOne); + + let itemsOne = yield list.getAll(); + do_check_eq(itemsOne.length, 1); + do_check_eq(itemsOne[0], downloadOne); + + let downloadTwo = yield promiseSimpleDownload(); + 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. + do_check_eq(itemsOne.length, 1); +}); + +/** + * 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()); + + 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()); + + 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(); + 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. + if (addNotifications == 0) { + do_check_eq(aDownload, downloadOne); + } else if (addNotifications == 1) { + do_check_eq(aDownload, downloadTwo); + } + addNotifications++; + }, + }; + list.addView(viewOne); + do_check_eq(addNotifications, 2); + + // Check that we receive add notifications for new elements. + list.add(yield promiseSimpleDownload()); + 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++; + }, + }; + list.addView(viewTwo); + list.remove(downloadOne); + do_check_eq(removeNotifications, 1); + + // 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()); + 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(); + 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); + receivedOnDownloadChanged = true; + }, + }); + yield downloadOne.start(); + do_check_true(receivedOnDownloadChanged); + + // We should not receive change notifications after a download is removed. + receivedOnDownloadChanged = false; + list.remove(downloadTwo); + yield downloadTwo.start(); + do_check_false(receivedOnDownloadChanged); +}); diff --git a/toolkit/components/jsdownloads/test/unit/test_Downloads.js b/toolkit/components/jsdownloads/test/unit/test_Downloads.js index d926310d39ea..c7223b5a1d1f 100644 --- a/toolkit/components/jsdownloads/test/unit/test_Downloads.js +++ b/toolkit/components/jsdownloads/test/unit/test_Downloads.js @@ -14,7 +14,7 @@ /** * Tests that the createDownload function exists and can be called. More - * detailed tests are implemented separately for the DownloadsCore module. + * detailed tests are implemented separately for the DownloadCore module. */ add_task(function test_createDownload() { @@ -46,3 +46,16 @@ add_task(function test_simpleDownload_object_arguments() { file: targetFile }); yield promiseVerifyContents(targetFile, 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() +{ + let downloadListOne = yield Downloads.getPublicDownloadList(); + let downloadListTwo = yield Downloads.getPublicDownloadList(); + + do_check_eq(downloadListOne, downloadListTwo); +}); diff --git a/toolkit/components/jsdownloads/test/unit/xpcshell.ini b/toolkit/components/jsdownloads/test/unit/xpcshell.ini index cab557ef55e3..ba0358c8b9a1 100644 --- a/toolkit/components/jsdownloads/test/unit/xpcshell.ini +++ b/toolkit/components/jsdownloads/test/unit/xpcshell.ini @@ -3,4 +3,5 @@ head = head.js tail = tail.js [test_DownloadCore.js] +[test_DownloadList.js] [test_Downloads.js]