Bug 835880 - Implement the basic DownloadList object. r=enn

--HG--
extra : rebase_source : 15c2ba2132c87fdd633dc069a1e2ebef727c6392
This commit is contained in:
Paolo Amadini 2013-03-05 12:10:03 +01:00
parent c81286236b
commit aac56c7c18
7 changed files with 375 additions and 24 deletions

View File

@ -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);
},
};

View File

@ -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",

View File

@ -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.
*

View File

@ -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

View File

@ -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);
});

View File

@ -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);
});

View File

@ -3,4 +3,5 @@ head = head.js
tail = tail.js
[test_DownloadCore.js]
[test_DownloadList.js]
[test_Downloads.js]