зеркало из https://github.com/mozilla/gecko-dev.git
Bug 825318 - Implement adoptDownload for mozDownloadManager, r=aus, r=sicking
Implement mozDownloadManager.adoptDownload as a certified-only API. This also fixes and re-enables many of the existing dom/downloads tests failures by virtue of cleanup and not running them on non-gonk toolkits where exceptions will be thrown and things will fail. This should resolve bug 979446 about re-enabling the tests.
This commit is contained in:
Родитель
e7a8e00fc5
Коммит
f4ea4e85aa
|
@ -21,6 +21,12 @@ XPCOMUtils.defineLazyServiceGetter(this, "volumeService",
|
|||
"@mozilla.org/telephony/volume-service;1",
|
||||
"nsIVolumeService");
|
||||
|
||||
/**
|
||||
* The content process implementations of navigator.mozDownloadManager and its
|
||||
* DOMDownload download objects. Uses DownloadsIPC.jsm to communicate with
|
||||
* DownloadsAPI.jsm in the parent process.
|
||||
*/
|
||||
|
||||
function debug(aStr) {
|
||||
#ifdef MOZ_DEBUG
|
||||
dump("-*- DownloadsAPI.js : " + aStr + "\n");
|
||||
|
@ -40,6 +46,15 @@ DOMDownloadManagerImpl.prototype = {
|
|||
this.initDOMRequestHelper(aWindow,
|
||||
["Downloads:Added",
|
||||
"Downloads:Removed"]);
|
||||
|
||||
// Get the manifest URL if this is an installed app
|
||||
let appsService = Cc["@mozilla.org/AppsService;1"]
|
||||
.getService(Ci.nsIAppsService);
|
||||
let principal = aWindow.document.nodePrincipal;
|
||||
// This returns the empty string if we're not an installed app. Coerce to
|
||||
// null.
|
||||
this._manifestURL = appsService.getManifestURLByLocalId(principal.appId) ||
|
||||
null;
|
||||
},
|
||||
|
||||
uninit: function() {
|
||||
|
@ -79,23 +94,8 @@ DOMDownloadManagerImpl.prototype = {
|
|||
|
||||
clearAllDone: function() {
|
||||
debug("clearAllDone()");
|
||||
return this.createPromise(function (aResolve, aReject) {
|
||||
DownloadsIPC.clearAllDone().then(
|
||||
function(aDownloads) {
|
||||
// Turn the list of download objects into DOM objects and
|
||||
// send them.
|
||||
let array = new this._window.Array();
|
||||
for (let id in aDownloads) {
|
||||
let dom = createDOMDownloadObject(this._window, aDownloads[id]);
|
||||
array.push(this._prepareForContent(dom));
|
||||
}
|
||||
aResolve(array);
|
||||
}.bind(this),
|
||||
function() {
|
||||
aReject("ClearAllDoneError");
|
||||
}
|
||||
);
|
||||
}.bind(this));
|
||||
// This is a void function; we just kick it off. No promises, etc.
|
||||
DownloadsIPC.clearAllDone();
|
||||
},
|
||||
|
||||
remove: function(aDownload) {
|
||||
|
@ -121,6 +121,67 @@ DOMDownloadManagerImpl.prototype = {
|
|||
}.bind(this));
|
||||
},
|
||||
|
||||
adoptDownload: function(aAdoptDownloadDict) {
|
||||
// Our AdoptDownloadDict only includes simple types, which WebIDL enforces.
|
||||
// We have no object/any types so we do not need to worry about invoking
|
||||
// JSON.stringify (and it inheriting our security privileges).
|
||||
debug("adoptDownload");
|
||||
return this.createPromise(function (aResolve, aReject) {
|
||||
if (!aAdoptDownloadDict) {
|
||||
debug("Download dictionary is required!");
|
||||
aReject("InvalidDownload");
|
||||
return;
|
||||
}
|
||||
if (!aAdoptDownloadDict.storageName || !aAdoptDownloadDict.storagePath ||
|
||||
!aAdoptDownloadDict.contentType) {
|
||||
debug("Missing one of: storageName, storagePath, contentType");
|
||||
aReject("InvalidDownload");
|
||||
return;
|
||||
}
|
||||
|
||||
// Convert storageName/storagePath to a local filesystem path.
|
||||
let volume;
|
||||
// getVolumeByName throws if you give it something it doesn't like
|
||||
// because XPConnect converts the NS_ERROR_NOT_AVAILABLE to an
|
||||
// exception. So catch it.
|
||||
try {
|
||||
volume = volumeService.getVolumeByName(aAdoptDownloadDict.storageName);
|
||||
} catch (ex) {}
|
||||
if (!volume) {
|
||||
debug("Invalid storage name: " + aAdoptDownloadDict.storageName);
|
||||
aReject("InvalidDownload");
|
||||
return;
|
||||
}
|
||||
let computedPath = volume.mountPoint + '/' +
|
||||
aAdoptDownloadDict.storagePath;
|
||||
// We validate that there is actually a file at the given path in the
|
||||
// parent process in DownloadsAPI.js because that's where the file
|
||||
// access would actually occur either way.
|
||||
|
||||
// Create a DownloadsAPI.jsm 'jsonDownload' style representation.
|
||||
let jsonDownload = {
|
||||
url: aAdoptDownloadDict.url,
|
||||
path: computedPath,
|
||||
contentType: aAdoptDownloadDict.contentType,
|
||||
startTime: aAdoptDownloadDict.startTime.valueOf() || Date.now(),
|
||||
sourceAppManifestURL: this._manifestURL
|
||||
};
|
||||
|
||||
DownloadsIPC.adoptDownload(jsonDownload).then(
|
||||
function(aResult) {
|
||||
let domDownload = createDOMDownloadObject(this._window, aResult);
|
||||
aResolve(this._prepareForContent(domDownload));
|
||||
}.bind(this),
|
||||
function(aResult) {
|
||||
// This will be one of: AdoptError (generic catch-all),
|
||||
// AdoptNoSuchFile, AdoptFileIsDirectory
|
||||
aReject(aResult.error);
|
||||
}
|
||||
);
|
||||
}.bind(this));
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Turns a chrome download object into a content accessible one.
|
||||
* When we have __DOM_IMPL__ available we just use that, otherwise
|
||||
|
@ -295,6 +356,11 @@ DOMDownloadImpl.prototype = {
|
|||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Initialize a DOMDownload instance for the given window using the
|
||||
* 'jsonDownload' serialized format of the download encoded by
|
||||
* DownloadsAPI.jsm.
|
||||
*/
|
||||
_init: function(aWindow, aDownload) {
|
||||
this._window = aWindow;
|
||||
this.id = aDownload.id;
|
||||
|
@ -314,12 +380,13 @@ DOMDownloadImpl.prototype = {
|
|||
}
|
||||
|
||||
let props = ["totalBytes", "currentBytes", "url", "path", "storageName",
|
||||
"storagePath", "state", "contentType", "startTime"];
|
||||
"storagePath", "state", "contentType", "startTime",
|
||||
"sourceAppManifestURL"];
|
||||
let changed = false;
|
||||
let changedProps = {};
|
||||
|
||||
props.forEach((prop) => {
|
||||
if (aDownload[prop] && (aDownload[prop] != this[prop])) {
|
||||
if (prop in aDownload && (aDownload[prop] != this[prop])) {
|
||||
this[prop] = aDownload[prop];
|
||||
changedProps[prop] = changed = true;
|
||||
}
|
||||
|
|
|
@ -13,11 +13,19 @@ this.EXPORTED_SYMBOLS = [];
|
|||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
Cu.import("resource://gre/modules/Downloads.jsm");
|
||||
Cu.import("resource://gre/modules/Task.jsm");
|
||||
Cu.import("resource://gre/modules/osfile.jsm");
|
||||
|
||||
XPCOMUtils.defineLazyServiceGetter(this, "ppmm",
|
||||
"@mozilla.org/parentprocessmessagemanager;1",
|
||||
"nsIMessageBroadcaster");
|
||||
|
||||
/**
|
||||
* Parent process logic that services download API requests from the
|
||||
* DownloadAPI.js instances in content processeses. The actual work of managing
|
||||
* downloads is done by Toolkit's Downloads.jsm. This module is loaded by B2G's
|
||||
* shell.js
|
||||
*/
|
||||
|
||||
function debug(aStr) {
|
||||
#ifdef MOZ_DEBUG
|
||||
dump("-*- DownloadsAPI.jsm : " + aStr + "\n");
|
||||
|
@ -49,7 +57,8 @@ let DownloadsAPI = {
|
|||
"Downloads:ClearAllDone",
|
||||
"Downloads:Remove",
|
||||
"Downloads:Pause",
|
||||
"Downloads:Resume"].forEach((msgName) => {
|
||||
"Downloads:Resume",
|
||||
"Downloads:Adopt"].forEach((msgName) => {
|
||||
ppmm.addMessageListener(msgName, this);
|
||||
});
|
||||
|
||||
|
@ -92,7 +101,9 @@ let DownloadsAPI = {
|
|||
url: aDownload.source.url,
|
||||
path: aDownload.target.path,
|
||||
contentType: aDownload.contentType,
|
||||
startTime: aDownload.startTime.getTime()
|
||||
startTime: aDownload.startTime.getTime(),
|
||||
sourceAppManifestURL: aDownload._unknownProperties &&
|
||||
aDownload._unknownProperties.sourceAppManifestURL
|
||||
};
|
||||
|
||||
if (aDownload.error) {
|
||||
|
@ -165,6 +176,9 @@ let DownloadsAPI = {
|
|||
case "Downloads:Resume":
|
||||
this.resume(aMessage.data, aMessage.target);
|
||||
break;
|
||||
case "Downloads:Adopt":
|
||||
this.adoptDownload(aMessage.data, aMessage.target);
|
||||
break;
|
||||
default:
|
||||
debug("Invalid message: " + aMessage.name);
|
||||
}
|
||||
|
@ -186,17 +200,9 @@ let DownloadsAPI = {
|
|||
|
||||
clearAllDone: function(aData, aMm) {
|
||||
debug("clearAllDone called!");
|
||||
let self = this;
|
||||
Task.spawn(function () {
|
||||
let list = yield Downloads.getList(Downloads.ALL);
|
||||
yield list.removeFinished();
|
||||
list = yield Downloads.getList(Downloads.ALL);
|
||||
let downloads = yield list.getAll();
|
||||
let res = [];
|
||||
downloads.forEach((aDownload) => {
|
||||
res.push(self.jsonDownload(aDownload));
|
||||
});
|
||||
aMm.sendAsyncMessage("Downloads:ClearAllDone:Return", res);
|
||||
list.removeFinished();
|
||||
}).then(null, Components.utils.reportError);
|
||||
},
|
||||
|
||||
|
@ -262,6 +268,97 @@ let DownloadsAPI = {
|
|||
aData, "ResumeError");
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* Receive a download to adopt in the same representation we produce from
|
||||
* our "jsonDownload" normalizer and add it to the list of downloads.
|
||||
*/
|
||||
adoptDownload: function(aData, aMm) {
|
||||
let adoptJsonRep = aData.jsonDownload;
|
||||
debug("adoptDownload " + uneval(adoptJsonRep));
|
||||
|
||||
Task.spawn(function* () {
|
||||
// Verify that the file exists on disk. This will result in a rejection
|
||||
// if the file does not exist. We will also use this information for the
|
||||
// file size to avoid weird inconsistencies. We ignore the filesystem
|
||||
// timestamp in favor of whatever the caller is telling us.
|
||||
let fileInfo = yield OS.File.stat(adoptJsonRep.path);
|
||||
|
||||
// We also require that the file is not a directory.
|
||||
if (fileInfo.isDir) {
|
||||
throw new Error("AdoptFileIsDirectory");
|
||||
}
|
||||
|
||||
// We need to create a Download instance to add to the list. Create a
|
||||
// serialized representation and then from there the instance.
|
||||
let serializedRep = {
|
||||
// explicit initializations in toSerializable
|
||||
source: {
|
||||
url: adoptJsonRep.url
|
||||
// This is where isPrivate would go if adoption supported private
|
||||
// browsing.
|
||||
},
|
||||
target: {
|
||||
path: adoptJsonRep.path,
|
||||
},
|
||||
startTime: adoptJsonRep.startTime,
|
||||
// kPlainSerializableDownloadProperties propagations
|
||||
succeeded: true, // (all adopted downloads are required to be completed)
|
||||
totalBytes: fileInfo.size,
|
||||
contentType: adoptJsonRep.contentType,
|
||||
// unknown properties added/used by the DownloadsAPI
|
||||
currentBytes: fileInfo.size,
|
||||
sourceAppManifestURL: adoptJsonRep.sourceAppManifestURL
|
||||
};
|
||||
|
||||
let download = yield Downloads.createDownload(serializedRep);
|
||||
|
||||
// The ALL list is a DownloadCombinedList instance that combines the
|
||||
// PUBLIC (persisted to disk) and PRIVATE (ephemeral) download lists..
|
||||
// When we call add on it, it dispatches to the appropriate list based on
|
||||
// the 'isPrivate' field of the source. (Which we don't initialize and
|
||||
// defaults to false.)
|
||||
let allDownloadList = yield Downloads.getList(Downloads.ALL);
|
||||
|
||||
// This add will automatically notify all views of the added download,
|
||||
// including DownloadsAPI instances and the DownloadAutoSaveView that's
|
||||
// subscribed to the PUBLIC list and will save the download.
|
||||
yield allDownloadList.add(download);
|
||||
|
||||
debug("download adopted");
|
||||
// The notification above occurred synchronously, and so we will have
|
||||
// already dispatched an added notification for our download to the child
|
||||
// process in question. As such, we only need to relay the download id
|
||||
// since the download will already have been cached.
|
||||
return download;
|
||||
}.bind(this)).then(
|
||||
(download) => {
|
||||
sendPromiseMessage(aMm, "Downloads:Adopt:Return",
|
||||
{
|
||||
id: this.downloadId(download),
|
||||
promiseId: aData.promiseId
|
||||
});
|
||||
},
|
||||
(ex) => {
|
||||
let reportAs = "AdoptError";
|
||||
// Provide better error codes for expected errors.
|
||||
if (ex instanceof OS.File.Error && ex.becauseNoSuchFile) {
|
||||
reportAs = "AdoptNoSuchFile";
|
||||
} else if (ex.message === "AdoptFileIsDirectory") {
|
||||
reportAs = ex.message;
|
||||
} else {
|
||||
// Anything else is unexpected and should be reported to help track
|
||||
// down what's going wrong.
|
||||
debug("unexpected download error: " + ex);
|
||||
Cu.reportError(ex);
|
||||
}
|
||||
sendPromiseMessage(aMm, "Downloads:Adopt:Return",
|
||||
{
|
||||
promiseId: aData.promiseId
|
||||
},
|
||||
reportAs);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -36,10 +36,10 @@ const ipcMessages = ["Downloads:Added",
|
|||
"Downloads:Removed",
|
||||
"Downloads:Changed",
|
||||
"Downloads:GetList:Return",
|
||||
"Downloads:ClearAllDone:Return",
|
||||
"Downloads:Remove:Return",
|
||||
"Downloads:Pause:Return",
|
||||
"Downloads:Resume:Return"];
|
||||
"Downloads:Resume:Return",
|
||||
"Downloads:Adopt:Return"];
|
||||
|
||||
this.DownloadsIPC = {
|
||||
downloads: {},
|
||||
|
@ -54,7 +54,6 @@ this.DownloadsIPC = {
|
|||
// We need to get the list of current downloads.
|
||||
this.ready = false;
|
||||
this.getListPromises = [];
|
||||
this.clearAllPromises = [];
|
||||
this.downloadPromises = {};
|
||||
cpmm.sendAsyncMessage("Downloads:GetList", {});
|
||||
this._promiseId = 0;
|
||||
|
@ -93,12 +92,6 @@ this.DownloadsIPC = {
|
|||
}
|
||||
this.ready = true;
|
||||
break;
|
||||
case "Downloads:ClearAllDone:Return":
|
||||
this._updateDownloadsArray(download);
|
||||
this.clearAllPromises.forEach(aPromise =>
|
||||
aPromise.resolve(this.downloads));
|
||||
this.clearAllPromises.length = 0;
|
||||
break;
|
||||
case "Downloads:Added":
|
||||
this.downloads[download.id] = download;
|
||||
this.notifyChanges(download.id);
|
||||
|
@ -139,6 +132,7 @@ this.DownloadsIPC = {
|
|||
case "Downloads:Remove:Return":
|
||||
case "Downloads:Pause:Return":
|
||||
case "Downloads:Resume:Return":
|
||||
case "Downloads:Adopt:Return":
|
||||
if (this.downloadPromises[download.promiseId]) {
|
||||
if (!download.error) {
|
||||
this.downloadPromises[download.promiseId].resolve(download);
|
||||
|
@ -167,14 +161,11 @@ this.DownloadsIPC = {
|
|||
},
|
||||
|
||||
/**
|
||||
* Returns a promise that is resolved with the list of current downloads.
|
||||
*/
|
||||
* Void function to trigger removal of completed downloads.
|
||||
*/
|
||||
clearAllDone: function() {
|
||||
debug("clearAllDone");
|
||||
let deferred = Promise.defer();
|
||||
this.clearAllPromises.push(deferred);
|
||||
cpmm.sendAsyncMessage("Downloads:ClearAllDone", {});
|
||||
return deferred.promise;
|
||||
},
|
||||
|
||||
promiseId: function() {
|
||||
|
@ -211,6 +202,16 @@ this.DownloadsIPC = {
|
|||
return deferred.promise;
|
||||
},
|
||||
|
||||
adoptDownload: function(aJsonDownload) {
|
||||
debug("adoptDownload");
|
||||
let deferred = Promise.defer();
|
||||
let pId = this.promiseId();
|
||||
this.downloadPromises[pId] = deferred;
|
||||
cpmm.sendAsyncMessage("Downloads:Adopt",
|
||||
{ jsonDownload: aJsonDownload, promiseId: pId });
|
||||
return deferred.promise;
|
||||
},
|
||||
|
||||
observe: function(aSubject, aTopic, aData) {
|
||||
if (aTopic == "xpcom-shutdown") {
|
||||
ipcMessages.forEach((aMessage) => {
|
||||
|
|
|
@ -0,0 +1,67 @@
|
|||
/**
|
||||
* A helper to clear out the existing downloads known to the mozDownloadManager
|
||||
* / downloads.js.
|
||||
*
|
||||
* It exists because previously mozDownloadManager.clearAllDone() thought that
|
||||
* when it returned that all the completed downloads would be cleared out. It
|
||||
* was wrong and this led to various intermittent test failurse. In discussion
|
||||
* on https://bugzil.la/979446#c13 and onwards, it was decided that
|
||||
* clearAllDone() was in the wrong and that the jsdownloads API it depends on
|
||||
* was not going to change to make it be in the right.
|
||||
*
|
||||
* The existing uses of clearAllDone() in tests seemed to be about:
|
||||
* - Exploding if there was somehow still a download in progress
|
||||
* - Clearing out the download list at the start of a test so that calls to
|
||||
* getDownloads() wouldn't have to worry about existing downloads, etc.
|
||||
*
|
||||
* From discussion, the right way to handle clearing is to wait for the expected
|
||||
* removal events to occur for the existing downloads. So that's what we do.
|
||||
* We still generate a test failure if there are any in-progress downloads.
|
||||
*
|
||||
* @param {Boolean} [getDownloads=false]
|
||||
* If true, invoke getDownloads after clearing the download list and return
|
||||
* its value.
|
||||
*/
|
||||
function clearAllDoneHelper(getDownloads) {
|
||||
var clearedPromise = new Promise(function(resolve, reject) {
|
||||
function gotDownloads(downloads) {
|
||||
// If there are no downloads, we're already done.
|
||||
if (downloads.length === 0) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
// Track the set of expected downloads that will be finalized.
|
||||
var expectedIds = new Set();
|
||||
function changeHandler(evt) {
|
||||
var download = evt.download;
|
||||
if (download.state === "finalized") {
|
||||
expectedIds.delete(download.id);
|
||||
if (expectedIds.size === 0) {
|
||||
resolve();
|
||||
}
|
||||
}
|
||||
}
|
||||
downloads.forEach(function(download) {
|
||||
if (download.state === "downloading") {
|
||||
ok(false, "A download is still active: " + download.path);
|
||||
reject("Active download");
|
||||
}
|
||||
download.onstatechange = changeHandler;
|
||||
expectedIds.add(download.id);
|
||||
});
|
||||
navigator.mozDownloadManager.clearAllDone();
|
||||
}
|
||||
function gotBadNews(err) {
|
||||
ok(false, "Problem clearing all downloads: " + err);
|
||||
reject(err);
|
||||
}
|
||||
navigator.mozDownloadManager.getDownloads().then(gotDownloads, gotBadNews);
|
||||
});
|
||||
if (!getDownloads) {
|
||||
return clearedPromise;
|
||||
}
|
||||
return clearedPromise.then(function() {
|
||||
return navigator.mozDownloadManager.getDownloads();
|
||||
});
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
function is(a, b, msg) {
|
||||
alert((a === b ? 'OK' : 'KO') + ' ' + a + ' should equal ' + b + ': ' + msg);
|
||||
}
|
||||
|
||||
function ok(a, msg) {
|
||||
alert((a ? 'OK' : 'KO')+ ' ' + msg);
|
||||
}
|
||||
|
||||
function info(msg) {
|
||||
alert('INFO ' + msg);
|
||||
}
|
||||
|
||||
function cbError() {
|
||||
alert('KO error');
|
||||
}
|
||||
|
||||
function finish() {
|
||||
alert('DONE');
|
||||
}
|
|
@ -0,0 +1,55 @@
|
|||
var gBasePath = "tests/dom/downloads/tests/";
|
||||
var gTemplate = "file_app.template.webapp";
|
||||
|
||||
function handleRequest(request, response) {
|
||||
var query = getQuery(request);
|
||||
|
||||
var testToken = query.testToken || '';
|
||||
var appType = query.appType || 'web';
|
||||
|
||||
var template = gBasePath + gTemplate;
|
||||
response.setHeader("Content-Type", "application/x-web-app-manifest+json", false);
|
||||
var body = readTemplate(template)
|
||||
.replace(/TESTTOKEN/g, testToken)
|
||||
.replace(/APPTYPE/g, appType);
|
||||
response.write();
|
||||
}
|
||||
|
||||
// Copy-pasted incantations. There ought to be a better way to synchronously read
|
||||
// a file into a string, but I guess we're trying to discourage that.
|
||||
function readTemplate(path) {
|
||||
var file = Components.classes["@mozilla.org/file/directory_service;1"].
|
||||
getService(Components.interfaces.nsIProperties).
|
||||
get("CurWorkD", Components.interfaces.nsILocalFile);
|
||||
var fis = Components.classes['@mozilla.org/network/file-input-stream;1'].
|
||||
createInstance(Components.interfaces.nsIFileInputStream);
|
||||
var cis = Components.classes["@mozilla.org/intl/converter-input-stream;1"].
|
||||
createInstance(Components.interfaces.nsIConverterInputStream);
|
||||
var split = path.split("/");
|
||||
for(var i = 0; i < split.length; ++i) {
|
||||
file.append(split[i]);
|
||||
}
|
||||
fis.init(file, -1, -1, false);
|
||||
cis.init(fis, "UTF-8", 0, 0);
|
||||
|
||||
var data = "";
|
||||
let (str = {}) {
|
||||
let read = 0;
|
||||
do {
|
||||
read = cis.readString(0xffffffff, str); // read as much as we can and put it in str.value
|
||||
data += str.value;
|
||||
} while (read != 0);
|
||||
}
|
||||
cis.close();
|
||||
return data;
|
||||
}
|
||||
|
||||
function getQuery(request) {
|
||||
var query = {};
|
||||
request.queryString.split('&').forEach(function (val) {
|
||||
var [name, value] = val.split('=');
|
||||
query[name] = unescape(value);
|
||||
});
|
||||
return query;
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"name": "Really Rapid Release (hosted)",
|
||||
"description": "Updated even faster than <a href='http://mozilla.org'>Firefox</a>, just to annoy slashdotters.",
|
||||
"type": "APPTYPE",
|
||||
"launch_path": "/tests/dom/downloads/tests/TESTTOKEN",
|
||||
"icons": { "128": "default_icon" }
|
||||
}
|
|
@ -1,12 +1,24 @@
|
|||
[DEFAULT]
|
||||
skip-if = buildapp == 'mulet' || buildapp == 'b2g' # bug 979446, frequent failures
|
||||
# The actual requirement for mozDownloadManager is MOZ_GONK because of
|
||||
# the nsIVolumeService dependency. Until https://bugzil.la/1130264 is
|
||||
# addressed, there is no way for mulet to run these tests.
|
||||
run-if = toolkit == 'gonk'
|
||||
support-files =
|
||||
serve_file.sjs
|
||||
clear_all_done_helper.js
|
||||
file_app.template.webapp
|
||||
file_app.sjs
|
||||
common_app.js
|
||||
shim_app_as_test.js
|
||||
shim_app_as_test_chrome.js
|
||||
testapp_downloads_adopt_download.html
|
||||
testapp_downloads_adopt_download.js
|
||||
testapp_downloads_adopt_download.manifest
|
||||
|
||||
[test_downloads_navigator_object.html]
|
||||
[test_downloads_basic.html]
|
||||
[test_downloads_large.html]
|
||||
[test_downloads_adopt_download.html]
|
||||
[test_downloads_bad_file.html]
|
||||
[test_downloads_pause_remove.html]
|
||||
[test_downloads_pause_resume.html]
|
||||
skip-if = toolkit=='gonk' # b2g(bug 947167) b2g-debug(bug 947167)
|
||||
|
|
|
@ -0,0 +1,210 @@
|
|||
/**
|
||||
* Support logic to run a test file as an installed app. This file is derived
|
||||
* from dom/requestsync/tests/test_basic_app.html but uses
|
||||
* DOMApplicationRegistry in a chrome script (shim_app_as_test_chrome.js) to
|
||||
* directly install the apps instead of mozApps.install because mozApps.install
|
||||
* can't install privileged/certified apps. (This is the same mechanism used by
|
||||
* the Firefox OS Gaia email app's backend test runner.)
|
||||
*
|
||||
* You really only want to do this if your test cares about the app's origin
|
||||
* or you REALLY want to double-check AvailableIn and other WebIDL-provided
|
||||
* security mechanisms.
|
||||
*
|
||||
* If you trust WebIDL, your life may be made significantly easier by just
|
||||
* setting the pref "dom.ignore_webidl_scope_checks" to true, which makes
|
||||
* BindingUtils.cpp's IsInPrivilegedApp and IsInCertifiedApp return true no
|
||||
* matter what *on the main thread*. You are potentially out of luck on
|
||||
* workers since at the time of writing this since the values stored on
|
||||
* WorkerPrivateParent are based on the app status and ignore the pref.
|
||||
*
|
||||
* TO USE THIS:
|
||||
*
|
||||
* Make sure you have the usual header boilerplate:
|
||||
* <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
|
||||
* <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
|
||||
*
|
||||
* You also want to add this file!
|
||||
* <script type="application/javascript" src="shim_app_as_test.js"></script>
|
||||
*
|
||||
* In your script body, issue a call like so:
|
||||
* runAppTest({
|
||||
* appFile: 'testapp_downloads_adopt_download.html',
|
||||
* appManifest: 'testapp_downloads_adopt_download.manifest',
|
||||
* appType: 'certified',
|
||||
* extraPrefs: {
|
||||
* set: [["dom.mozDownloads.enabled", true]]
|
||||
* }
|
||||
* });
|
||||
*
|
||||
* You shouldn't be adding other stuff to that file. Instead, you want
|
||||
* everything in your testapp_*.html file. And you probably just want to copy
|
||||
* and paste from an existing one of those...
|
||||
*/
|
||||
|
||||
var gManifestURL;
|
||||
var gApp;
|
||||
var gOptions;
|
||||
|
||||
// Load the chrome script.
|
||||
var gChromeHelper = SpecialPowers.loadChromeScript(
|
||||
SimpleTest.getTestFileURL('shim_app_as_test_chrome.js'));
|
||||
|
||||
function installApp() {
|
||||
info("installing app");
|
||||
var useOrigin = document.location.origin;
|
||||
gChromeHelper.sendAsyncMessage(
|
||||
'install',
|
||||
{
|
||||
origin: useOrigin,
|
||||
manifestURL: SimpleTest.getTestFileURL(gOptions.appManifest),
|
||||
});
|
||||
}
|
||||
|
||||
function installedApp(appInfo) {
|
||||
gApp = appInfo;
|
||||
ok(!!appInfo, 'installed app');
|
||||
runTests();
|
||||
}
|
||||
gChromeHelper.addMessageListener('installed', installedApp);
|
||||
|
||||
function uninstallApp() {
|
||||
info('uninstalling app');
|
||||
gChromeHelper.sendAsyncMessage('uninstall', gApp);
|
||||
}
|
||||
|
||||
function uninstalledApp(success) {
|
||||
ok(success, 'uninstalled app');
|
||||
runTests();
|
||||
}
|
||||
gChromeHelper.addMessageListener('uninstalled', uninstalledApp);
|
||||
|
||||
function testApp() {
|
||||
var cleanupFrame;
|
||||
var handleTestMessage = function(message) {
|
||||
if (/^OK/.exec(message)) {
|
||||
ok(true, "Message from app: " + message);
|
||||
} else if (/^KO/.exec(message)) {
|
||||
ok(false, "Message from app: " + message);
|
||||
} else if (/^INFO/.exec(message)) {
|
||||
info("Message from app: " + message.substring(5));
|
||||
} else if (/^DONE$/.exec(message)) {
|
||||
ok(true, "Messaging from app complete");
|
||||
cleanupFrame();
|
||||
runTests();
|
||||
}
|
||||
};
|
||||
|
||||
// Bug 1097479 means that embed-webapps does not work if you are already
|
||||
// OOP, as we are for b2g. So we need to have the chrome script run our
|
||||
// app in a sibling iframe to the one we're living in. When that bug is
|
||||
// fixed or we are run in a non-b2g context, we can set this value to false
|
||||
// or otherwise conditionalize based on behaviour.
|
||||
var needSiblingIframeHack = true;
|
||||
|
||||
if (needSiblingIframeHack) {
|
||||
gChromeHelper.sendAsyncMessage('run', gApp);
|
||||
|
||||
gChromeHelper.addMessageListener('appMessage', handleTestMessage);
|
||||
gChromeHelper.addMessageListener('appError', function(data) {
|
||||
ok(false, "Error in app frame: " + data.message);
|
||||
});
|
||||
|
||||
cleanupFrame = function() {
|
||||
gChromeHelper.sendAsyncMessage('close', {});
|
||||
};
|
||||
} else {
|
||||
var ifr = document.createElement('iframe');
|
||||
ifr.setAttribute('mozbrowser', 'true');
|
||||
ifr.setAttribute('mozapp', gApp.manifestURL);
|
||||
|
||||
cleanupFrame = function() {
|
||||
ifr.removeEventListener('mozbrowsershowmodalprompt', listener);
|
||||
domParent.removeChild(ifr);
|
||||
};
|
||||
|
||||
// Set us up to listen for messages from the app.
|
||||
var listener = function(e) {
|
||||
var message = e.detail.message; // e.detail.message;
|
||||
handleTestMessage(message);
|
||||
};
|
||||
|
||||
// This event is triggered when the app calls "alert".
|
||||
ifr.addEventListener('mozbrowsershowmodalprompt', listener, false);
|
||||
ifr.addEventListener('mozbrowsererror', function(evt) {
|
||||
ok(false, "Error in app frame: " + evt.detail);
|
||||
});
|
||||
|
||||
ifr.setAttribute('src', gApp.manifest.launch_path);
|
||||
var domParent = document.getElementById('content');
|
||||
if (!domParent) {
|
||||
document.createElement('div');
|
||||
document.body.insertBefore(domParent, document.body.firstChild);
|
||||
}
|
||||
domParent.appendChild(ifr);
|
||||
}
|
||||
}
|
||||
|
||||
var tests = [
|
||||
// Permissions
|
||||
function() {
|
||||
info("pushing permissions");
|
||||
SpecialPowers.pushPermissions(
|
||||
[{ "type": "browser", "allow": 1, "context": document },
|
||||
{ "type": "embed-apps", "allow": 1, "context": document },
|
||||
{ "type": "webapps-manage", "allow": 1, "context": document }
|
||||
],
|
||||
runTests);
|
||||
},
|
||||
|
||||
// Preferences
|
||||
function() {
|
||||
info("pushing preferences: " + gOptions.extraPrefs.set);
|
||||
SpecialPowers.pushPrefEnv({
|
||||
"set": gOptions.extraPrefs.set
|
||||
}, runTests);
|
||||
},
|
||||
|
||||
function() {
|
||||
info("enabling use of mozbrowser");
|
||||
//SpecialPowers.setAllAppsLaunchable(true);
|
||||
SpecialPowers.setBoolPref("dom.mozBrowserFramesEnabled", true);
|
||||
runTests();
|
||||
},
|
||||
|
||||
// No confirmation needed when an app is installed
|
||||
function() {
|
||||
SpecialPowers.autoConfirmAppInstall(function() {
|
||||
SpecialPowers.autoConfirmAppUninstall(runTests);
|
||||
});
|
||||
},
|
||||
|
||||
// Installing the app
|
||||
installApp,
|
||||
|
||||
// Run tests in app
|
||||
testApp,
|
||||
|
||||
// Uninstall the app
|
||||
uninstallApp,
|
||||
];
|
||||
|
||||
function runTests() {
|
||||
if (!tests.length) {
|
||||
ok(true, 'DONE!');
|
||||
SimpleTest.finish();
|
||||
return;
|
||||
}
|
||||
|
||||
var test = tests.shift();
|
||||
test();
|
||||
}
|
||||
|
||||
SimpleTest.waitForExplicitFinish();
|
||||
|
||||
function runAppTest(options) {
|
||||
gOptions = options;
|
||||
var href = document.location.href;
|
||||
gManifestURL = href.substring(0, href.lastIndexOf('/') + 1) +
|
||||
options.appManifest;
|
||||
runTests();
|
||||
}
|
|
@ -0,0 +1,178 @@
|
|||
/**
|
||||
* This is the chrome helper for shim_app_as_test.js. Its load is triggered by
|
||||
* shim_app_as_test.js by a call to SpecialPowers.loadChromeScript and runs
|
||||
* in the parent process in a sandbox created with the system principal. (Which
|
||||
* seems like it can never get collected because it's reachable via the
|
||||
* apparently singleton SpecialPowersObserverAPI instance and there's no logic
|
||||
* to support reaping. Wuh-oh.)
|
||||
*
|
||||
* It exists to help install fake privileged/certified applications. It needs
|
||||
* to exist because:
|
||||
* - We need to poke at DOMApplicationRegistry directly.
|
||||
* - By using SpecialPowers.loadChromeScript we are able to ensure this file
|
||||
* is run in the parent process. This is important because
|
||||
* DOMApplicationRegistry only lives in the parent process!
|
||||
* - By running entirely in a chrome privileged compartment, we avoid crazy
|
||||
* wrapper problems that we would otherwise face with our shenanigans of
|
||||
* directly meddling with DOMApplicationRegistry. (And hopefully save
|
||||
* anyone changing DOMApplicationRegistry from frustration/hating us if
|
||||
* things were just barely working.)
|
||||
* - Bug 1097479 means that embed-webapps doesn't work when the content process
|
||||
* that is telling us to do things is itself OOP. So it falls upon us to
|
||||
* handle the running of the app by creating a sibling mozbrowser/mozapp
|
||||
* iframe to the one running the mochitests.
|
||||
*
|
||||
* Note that in this file we try to do *only* those things that can't otherwise
|
||||
* be cleanly done using SpecialPowers.
|
||||
*
|
||||
* Want to better understand our execution context? Check out
|
||||
* SpecialPowersObserverAPI.js and search on SPLoadChromeScript.
|
||||
*/
|
||||
|
||||
const Cc = Components.classes;
|
||||
const Ci = Components.interfaces;
|
||||
const Cu = Components.utils;
|
||||
const CC = Components.Constructor;
|
||||
|
||||
Cu.import('resource://gre/modules/Webapps.jsm'); // for DOMApplicationRegistry
|
||||
Cu.import('resource://gre/modules/AppsUtils.jsm'); // for AppUtils
|
||||
Cu.import('resource://gre/modules/Services.jsm'); // for AppUtils
|
||||
|
||||
// Yes, you would think there was something like this already exposed easily
|
||||
// in a JSM somewhere. No.
|
||||
function fetchManifest(manifestURL) {
|
||||
return new Promise(function(resolve, reject) {
|
||||
let xhr = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"]
|
||||
.createInstance(Ci.nsIXMLHttpRequest);
|
||||
xhr.open("GET", manifestURL, true);
|
||||
xhr.responseType = "json";
|
||||
|
||||
xhr.addEventListener("load", function() {
|
||||
if (xhr.status == 200) {
|
||||
resolve(xhr.response);
|
||||
} else {
|
||||
reject();
|
||||
}
|
||||
});
|
||||
|
||||
xhr.addEventListener("error", function() {
|
||||
reject();
|
||||
});
|
||||
|
||||
xhr.send(null);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Install an app using confirmInstall using pre-chewed data. This avoids the
|
||||
* check in the normal installApp flow that gets all judgemental about the
|
||||
* installation of privileged and certified apps.
|
||||
*/
|
||||
function installApp(req) {
|
||||
fetchManifest(req.manifestURL).then(function(manifestObj) {
|
||||
var data = {
|
||||
// cloneAppObj normalizes the representation for us
|
||||
app: AppsUtils.cloneAppObject({
|
||||
installOrigin: req.origin,
|
||||
origin: req.origin,
|
||||
manifestURL: req.manifestURL,
|
||||
appStatus: AppsUtils.getAppManifestStatus(manifestObj),
|
||||
receipts: [],
|
||||
categories: []
|
||||
}),
|
||||
|
||||
from: req.origin, // unused?
|
||||
oid: 0, // unused?
|
||||
requestID: 0, // unused-ish
|
||||
appId: 0, // unused
|
||||
isBrowser: false,
|
||||
isPackage: false, // used
|
||||
// magic to auto-ack... don't think we care about this...
|
||||
forceSuccessAck: false
|
||||
// stuff that probably doesn't matter: 'mm', 'apkInstall',
|
||||
};
|
||||
// cloneAppObject does not propagate the manifest
|
||||
data.app.manifest = manifestObj;
|
||||
|
||||
return DOMApplicationRegistry.confirmInstall(data).then(
|
||||
function() {
|
||||
var appId =
|
||||
DOMApplicationRegistry.getAppLocalIdByManifestURL(req.manifestURL);
|
||||
// act like this is a privileged app having all of its permissions
|
||||
// authorized at first run.
|
||||
DOMApplicationRegistry.updatePermissionsForApp(
|
||||
appId,
|
||||
/* preinstalled */ true,
|
||||
/* system update? */ true);
|
||||
|
||||
sendAsyncMessage(
|
||||
'installed',
|
||||
{
|
||||
appId: appId,
|
||||
manifestURL: req.manifestURL,
|
||||
manifest: manifestObj
|
||||
});
|
||||
},
|
||||
function(err) {
|
||||
sendAsyncMessage('installed', false);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function uninstallApp(appInfo) {
|
||||
DOMApplicationRegistry.uninstall(appInfo.manifestURL).then(
|
||||
function() {
|
||||
sendAsyncMessage('uninstalled', true);
|
||||
},
|
||||
function() {
|
||||
sendAsyncMessage('uninstalled', false);
|
||||
});
|
||||
}
|
||||
|
||||
var activeIframe = null;
|
||||
|
||||
/**
|
||||
* Run our app in a sibling mozbrowser/mozapp iframe to the mochitest iframe.
|
||||
* This is needed because we can't nest mozbrowser/mozapp iframes inside our
|
||||
* already-OOP iframe until bug 1097479 is resolved.
|
||||
*/
|
||||
function runApp(appInfo) {
|
||||
let shellDomWindow = Services.wm.getMostRecentWindow('navigator:browser');
|
||||
let sysAppFrame = shellDomWindow.document.body.querySelector('#systemapp');
|
||||
let sysAppDoc = sysAppFrame.contentDocument;
|
||||
|
||||
let siblingFrame = sysAppDoc.body.querySelector('#test-container');
|
||||
|
||||
let ifr = activeIframe = sysAppDoc.createElement('iframe');
|
||||
ifr.setAttribute('mozbrowser', 'true');
|
||||
ifr.setAttribute('remote', 'true');
|
||||
ifr.setAttribute('mozapp', appInfo.manifestURL);
|
||||
|
||||
ifr.addEventListener('mozbrowsershowmodalprompt', function(evt) {
|
||||
var message = evt.detail.message;
|
||||
// only send the message as long as we haven't been told to clean up.
|
||||
if (activeIframe) {
|
||||
sendAsyncMessage('appMessage', message);
|
||||
}
|
||||
}, false);
|
||||
ifr.addEventListener('mozbrowsererror', function(evt) {
|
||||
if (activeIframe) {
|
||||
sendAsyncMessage('appError', { message: '' + evt.detail });
|
||||
}
|
||||
});
|
||||
|
||||
ifr.setAttribute('src', appInfo.manifest.launch_path);
|
||||
siblingFrame.parentElement.appendChild(ifr);
|
||||
}
|
||||
|
||||
function closeApp() {
|
||||
if (activeIframe) {
|
||||
activeIframe.parentElement.removeChild(activeIframe);
|
||||
activeIframe = null;
|
||||
}
|
||||
}
|
||||
|
||||
addMessageListener('install', installApp);
|
||||
addMessageListener('uninstall', uninstallApp);
|
||||
addMessageListener('run', runApp);
|
||||
addMessageListener('close', closeApp);
|
|
@ -0,0 +1,34 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<!--
|
||||
https://bugzilla.mozilla.org/show_bug.cgi?id=825318
|
||||
-->
|
||||
<head>
|
||||
<title>Test for Bug 825318 mozDownloadManager.adoptDownload</title>
|
||||
<script type="application/javascript;version=1.7" src="/tests/SimpleTest/SimpleTest.js"></script>
|
||||
<script type="application/javascript;version=1.7" src="shim_app_as_test.js"></script>
|
||||
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=825318">Mozilla Bug 825318</a>
|
||||
<p id="display"></p>
|
||||
<div id="content">
|
||||
</div>
|
||||
<pre id="test">
|
||||
<script class="testbody" type="text/javascript;version=1.7">
|
||||
|
||||
runAppTest({
|
||||
appFile: 'testapp_downloads_adopt_download.html',
|
||||
appManifest: 'testapp_downloads_adopt_download.manifest',
|
||||
appType: 'certified',
|
||||
extraPrefs: {
|
||||
set: [["dom.mozDownloads.enabled", true]]
|
||||
}
|
||||
});
|
||||
|
||||
</script>
|
||||
</pre>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -72,6 +72,10 @@ function downloadChange(evt) {
|
|||
is(download.currentBytes, 1024, "Download current size is 1024 bytes");
|
||||
SimpleTest.finish();
|
||||
} else if (download.state === "downloading") {
|
||||
// Note that this case may or may not trigger, depending on whether the
|
||||
// download is initially reported with 0 bytes (we should happen) or with
|
||||
// 1024 bytes (we should not happen). If we do happen, an additional 8
|
||||
// TEST-PASS events should be logged.
|
||||
ok(download.currentBytes > lastKnownCurrentBytes,
|
||||
"Download current size is larger than last download change event");
|
||||
lastKnownCurrentBytes = download.currentBytes;
|
||||
|
@ -84,7 +88,8 @@ function downloadStart(evt) {
|
|||
var download = evt.download;
|
||||
checkConsistentDownloadAttributes(download);
|
||||
|
||||
is(download.currentBytes, 0, "Download current size is zero");
|
||||
// We used to check that the currentBytes was 0. This was incorrect. It
|
||||
// is very common to first hear about the download already at 1024 bytes.
|
||||
is(download.state, "downloading", "Download state is downloading");
|
||||
|
||||
download.onstatechange = downloadChange;
|
||||
|
|
|
@ -7,6 +7,7 @@ https://bugzilla.mozilla.org/show_bug.cgi?id=938023
|
|||
<title>Test for Bug 938023 Downloads API</title>
|
||||
<script type="text/javascript" src="/MochiKit/MochiKit.js"></script>
|
||||
<script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
|
||||
<script type="text/javascript" src="clear_all_done_helper.js"></script>
|
||||
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
|
||||
</head>
|
||||
<body>
|
||||
|
@ -46,7 +47,7 @@ function error() {
|
|||
|
||||
function getDownloads(downloads) {
|
||||
ok(downloads.length == 1, "One downloads after getDownloads");
|
||||
navigator.mozDownloadManager.clearAllDone().then(clearAllDone, error);
|
||||
clearAllDoneHelper(true).then(clearAllDone, error);
|
||||
}
|
||||
|
||||
function clearAllDone(downloads) {
|
||||
|
@ -76,7 +77,7 @@ var steps = [
|
|||
SpecialPowers.pushPermissions([
|
||||
{type: "downloads", allow: true, context: document}
|
||||
], function() {
|
||||
navigator.mozDownloadManager.clearAllDone().then(next, error);
|
||||
clearAllDoneHelper(true).then(next, error);
|
||||
});
|
||||
},
|
||||
|
||||
|
|
|
@ -7,6 +7,7 @@ https://bugzilla.mozilla.org/show_bug.cgi?id=938023
|
|||
<title>Test for Bug 938023 Downloads API</title>
|
||||
<script type="text/javascript" src="/MochiKit/MochiKit.js"></script>
|
||||
<script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
|
||||
<script type="text/javascript" src="clear_all_done_helper.js"></script>
|
||||
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
|
||||
</head>
|
||||
<body>
|
||||
|
@ -83,7 +84,7 @@ var steps = [
|
|||
SpecialPowers.pushPermissions([
|
||||
{type: "downloads", allow: true, context: document}
|
||||
], function() {
|
||||
navigator.mozDownloadManager.clearAllDone().then(next, error);
|
||||
clearAllDoneHelper(true).then(next, error);
|
||||
});
|
||||
},
|
||||
|
||||
|
|
|
@ -7,6 +7,7 @@ https://bugzilla.mozilla.org/show_bug.cgi?id=938023
|
|||
<title>Test for Bug 938023 Downloads API</title>
|
||||
<script type="text/javascript" src="/MochiKit/MochiKit.js"></script>
|
||||
<script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
|
||||
<script type="text/javascript" src="clear_all_done_helper.js"></script>
|
||||
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
|
||||
</head>
|
||||
<body>
|
||||
|
@ -54,8 +55,7 @@ function checkDownloadList(downloads) {
|
|||
|
||||
function checkResumeSucceeded(download) {
|
||||
ok(download.state == "succeeded", "Download resumed successfully.");
|
||||
navigator.mozDownloadManager.clearAllDone()
|
||||
.then(checkDownloadList, error);
|
||||
clearAllDoneHelper(true).then(checkDownloadList, error);
|
||||
}
|
||||
|
||||
function downloadChange(evt) {
|
||||
|
@ -85,7 +85,7 @@ var steps = [
|
|||
SpecialPowers.pushPermissions([
|
||||
{type: "downloads", allow: true, context: document}
|
||||
], function() {
|
||||
navigator.mozDownloadManager.clearAllDone().then(next, error);
|
||||
clearAllDoneHelper(true).then(next, error);
|
||||
});
|
||||
},
|
||||
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<script type="application/javascript" src="common_app.js"></script>
|
||||
<meta charset="utf-8">
|
||||
</head>
|
||||
<body>
|
||||
<div id="blah">initial text</div>
|
||||
<pre id="test">
|
||||
<!-- because of certified CSP, this code must NOT be inline -->
|
||||
<script class="testbody" type="text/javascript;version=1.7" src="testapp_downloads_adopt_download.js"></script>
|
||||
</pre>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,218 @@
|
|||
/**
|
||||
* Test the adoptDownload API. Specifically, we expect that when we call
|
||||
* adoptDownload with a valid payload that:
|
||||
* - The method will be resolved with a valid, fully populated DOMDownload
|
||||
* instance, including an id.
|
||||
* - An ondownloadstart notification will be generated and the DOMDownload
|
||||
* instance it receives will be logically equivalent.
|
||||
*
|
||||
* We also explicitly verify that invalid adoptDownload payloads result in a
|
||||
* rejection and that no download is added.
|
||||
*
|
||||
* This test explicitly does not test that the download is correctly persisted
|
||||
* to the database. This is done because Downloads.jsm does not provide a means
|
||||
* of safely restarting itself, so Firefox would need to be restarted. Because
|
||||
* the adoptDownload code is using the Downloads API in a straightforward
|
||||
* manner, it's not considered likely this would regress, and certainly not
|
||||
* considered worth the automated testing overhead of a restart.
|
||||
*/
|
||||
|
||||
function checkInvalidResult(dict, expectedErr, explanation) {
|
||||
navigator.mozDownloadManager.ondownloadstart = function() {
|
||||
ok(false, "No download should have been added!");
|
||||
};
|
||||
navigator.mozDownloadManager.adoptDownload(dict).then(
|
||||
function() {
|
||||
ok(false, "Invalid adoptDownload did not reject!");
|
||||
runTests();
|
||||
},
|
||||
function(rejectedWith) {
|
||||
is(rejectedWith, expectedErr, explanation + " rejection value");
|
||||
runTests();
|
||||
});
|
||||
}
|
||||
|
||||
// Pick a date that Date.now() could not possibly return by picking a date in
|
||||
// the past. (We want to make sure the date we provide works.)
|
||||
var arbitraryDate = new Date(Date.now() - 60000);
|
||||
|
||||
var blobContents = new Uint8Array(256);
|
||||
var memBlob = new Blob([blobContents], { type: 'application/octet-stream' });
|
||||
var blobStorageName;
|
||||
var blobStoragePath = 'blobby.blob';
|
||||
|
||||
function checkAdoptedDownload(download, validPayload) {
|
||||
is(download.totalBytes, memBlob.size, 'size');
|
||||
is(download.url, validPayload.url, 'url');
|
||||
// The filesystem path is not practical to check since we can't hard-code it
|
||||
// and the only way to check is to effectively duplicate the logic in
|
||||
// DownloadsAPI.js. The good news, however, is that the value is
|
||||
// round-tripped from storageName/storagePath to path and back again, and we
|
||||
// also verify the file exists on disk, so we can be reasonably confident this
|
||||
// is correct. We output it to aid in debugging if things should break,
|
||||
// of course.
|
||||
info('path (not checked): ' + download.path);
|
||||
is(download.storageName, validPayload.storageName, 'storageName');
|
||||
is(download.storagePath, validPayload.storagePath, 'storagePath');
|
||||
is(download.state, 'succeeded', 'state');
|
||||
is(download.contentType, validPayload.contentType, 'contentType');
|
||||
is(download.startTime.valueOf(), arbitraryDate.valueOf(), 'startTime');
|
||||
is(download.sourceAppManifestURL,
|
||||
'http://mochi.test:8888/' +
|
||||
'tests/dom/downloads/tests/testapp_downloads_adopt_download.manifest',
|
||||
'app manifest');
|
||||
};
|
||||
|
||||
var tests = [
|
||||
function saveBlobToDeviceStorage() {
|
||||
// Only sdcard can handle arbitrary MIME types and is guaranteed to be a
|
||||
// thing.
|
||||
var storage = navigator.getDeviceStorage('sdcard');
|
||||
// We used the non-array helper, so the name we get may be different than
|
||||
// what we asked for.
|
||||
blobStorageName = storage.storageName;
|
||||
ok(!!storage, 'have storage');
|
||||
var req = storage.addNamed(memBlob, blobStoragePath);
|
||||
req.onerror = function() {
|
||||
ok(false, 'problem saving blob to storage: ' + req.error.name);
|
||||
};
|
||||
req.onsuccess = function(evt) {
|
||||
ok(true, 'saved blob: ' + evt.target.result);
|
||||
runTests();
|
||||
};
|
||||
},
|
||||
function addValid() {
|
||||
var validPayload = {
|
||||
// All currently expected consumers are unable to provide a valid URL, and
|
||||
// as a result need to provide an empty string.
|
||||
url: "",
|
||||
storageName: blobStorageName,
|
||||
storagePath: blobStoragePath,
|
||||
contentType: memBlob.type,
|
||||
startTime: arbitraryDate
|
||||
};
|
||||
// Wrap the notification in a check so we can force our logic to be
|
||||
// consistently ordered in the test even if it's not in reality.
|
||||
var notifiedPromise = new Promise(function(resolve, reject) {
|
||||
navigator.mozDownloadManager.ondownloadstart = function(evt) {
|
||||
resolve(evt.download);
|
||||
};
|
||||
});
|
||||
|
||||
// Start the download
|
||||
navigator.mozDownloadManager.adoptDownload(validPayload).then(
|
||||
function(apiDownload) {
|
||||
checkAdoptedDownload(apiDownload, validPayload);
|
||||
ok(!!apiDownload.id, "Need a download id!");
|
||||
notifiedPromise.then(function(notifiedDownload) {
|
||||
checkAdoptedDownload(notifiedDownload, validPayload);
|
||||
is(apiDownload.id, notifiedDownload.id,
|
||||
"Notification should be for the download we adopted");
|
||||
runTests();
|
||||
});
|
||||
},
|
||||
function() {
|
||||
ok(false, "adoptDownload should not have rejected");
|
||||
runTests();
|
||||
});
|
||||
},
|
||||
|
||||
function dictionaryNotProvided() {
|
||||
checkInvalidResult(undefined, "InvalidDownload");
|
||||
},
|
||||
// Missing fields immediately result in rejection with InvalidDownload
|
||||
function missingStorageName() {
|
||||
checkInvalidResult({
|
||||
url: "",
|
||||
// no storageName
|
||||
storagePath: "relpath/filename.txt",
|
||||
contentType: "text/plain",
|
||||
startTime: arbitraryDate
|
||||
}, "InvalidDownload", "missing storage name");
|
||||
},
|
||||
function nullStorageName() {
|
||||
checkInvalidResult({
|
||||
url: "",
|
||||
storageName: null,
|
||||
storagePath: "relpath/filename.txt",
|
||||
contentType: "text/plain",
|
||||
startTime: arbitraryDate
|
||||
}, "InvalidDownload", "null storage name");
|
||||
},
|
||||
function missingStoragePath() {
|
||||
checkInvalidResult({
|
||||
url: "",
|
||||
storageName: blobStorageName,
|
||||
// no storagePath
|
||||
contentType: "text/plain",
|
||||
startTime: arbitraryDate
|
||||
}, "InvalidDownload", "missing storage path");
|
||||
},
|
||||
function nullStoragePath() {
|
||||
checkInvalidResult({
|
||||
url: "",
|
||||
storageName: blobStorageName,
|
||||
storagePath: null,
|
||||
contentType: "text/plain",
|
||||
startTime: arbitraryDate
|
||||
}, "InvalidDownload", "null storage path");
|
||||
},
|
||||
function missingContentType() {
|
||||
checkInvalidResult({
|
||||
url: "",
|
||||
storageName: "sdcard",
|
||||
storagePath: "relpath/filename.txt",
|
||||
// no contentType
|
||||
startTime: arbitraryDate
|
||||
}, "InvalidDownload", "missing content type");
|
||||
},
|
||||
function nullContentType() {
|
||||
checkInvalidResult({
|
||||
url: "",
|
||||
storageName: "sdcard",
|
||||
storagePath: "relpath/filename.txt",
|
||||
contentType: null,
|
||||
startTime: arbitraryDate
|
||||
}, "InvalidDownload", "null content type");
|
||||
},
|
||||
// Incorrect storage names are likewise immediately invalidated
|
||||
function invalidStorageName() {
|
||||
checkInvalidResult({
|
||||
url: "",
|
||||
storageName: "ALMOST CERTAINLY DOES NOT EXIST",
|
||||
storagePath: "relpath/filename.txt",
|
||||
contentType: "text/plain",
|
||||
startTime: arbitraryDate
|
||||
}, "InvalidDownload", "invalid storage name");
|
||||
},
|
||||
// The existence of the file is validated in the parent process
|
||||
function legitStorageInvalidPath() {
|
||||
checkInvalidResult({
|
||||
url: "",
|
||||
storageName: blobStorageName,
|
||||
storagePath: "ALMOST CERTAINLY DOES NOT EXIST",
|
||||
contentType: "text/plain",
|
||||
startTime: arbitraryDate
|
||||
}, "AdoptNoSuchFile", "invalid path");
|
||||
},
|
||||
function allDone() {
|
||||
// Just in case, make sure no other mochitest could mess with us after we've
|
||||
// finished.
|
||||
navigator.mozDownloadManager.ondownloadstart = null;
|
||||
runTests();
|
||||
}
|
||||
];
|
||||
|
||||
function runTests() {
|
||||
if (!tests.length) {
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
var test = tests.shift();
|
||||
if (test.name) {
|
||||
info('starting test: ' + test.name);
|
||||
}
|
||||
test();
|
||||
}
|
||||
runTests();
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"name": "Downloads certified test fake app",
|
||||
"description": "Test",
|
||||
"launch_path": "http://mochi.test:8888/tests/dom/downloads/tests/testapp_downloads_adopt_download.html",
|
||||
"type": "certified",
|
||||
"permissions": {
|
||||
"device-storage:sdcard":{ "access": "readcreate" },
|
||||
"downloads": {}
|
||||
}
|
||||
}
|
|
@ -36,10 +36,32 @@ interface DOMDownloadManager : EventTarget {
|
|||
[UnsafeInPrerendering]
|
||||
Promise<DOMDownload> remove(DOMDownload download);
|
||||
|
||||
// Removes all the completed downloads from the set. Returns an
|
||||
// array of the completed downloads that were removed.
|
||||
// Removes all completed downloads. This kicks off an asynchronous process
|
||||
// that will eventually complete, but will not have completed by the time this
|
||||
// method returns. If you care about the side-effects of this method, know
|
||||
// that each existing download will have its onstatechange method invoked and
|
||||
// will have a new state of "finalized". (After the download is finalized, no
|
||||
// further events will be generated on it.)
|
||||
[UnsafeInPrerendering]
|
||||
Promise<sequence<DOMDownload>> clearAllDone();
|
||||
void clearAllDone();
|
||||
|
||||
// Add completed downloads from applications that must perform the download
|
||||
// process themselves. For example, email. The method is resolved with a
|
||||
// fully populated DOMDownload instance on success, or rejected in the
|
||||
// event all required options were not provided.
|
||||
//
|
||||
// The adopted download will also be reported via the ondownloadstart event
|
||||
// handler.
|
||||
//
|
||||
// Applications must currently be certified to use this, but it could be
|
||||
// widened at a later time.
|
||||
//
|
||||
// Note that "download" is not actually optional, but WebIDL requires that it
|
||||
// be marked as such because it is not followed by a required argument. The
|
||||
// promise will be rejected if the dictionary is omitted or the specified
|
||||
// file does not exist on disk.
|
||||
[AvailableIn=CertifiedApps]
|
||||
Promise<DOMDownload> adoptDownload(optional AdoptDownloadDict download);
|
||||
|
||||
// Fires when a new download starts.
|
||||
attribute EventHandler ondownloadstart;
|
||||
|
@ -59,7 +81,9 @@ interface DOMDownload : EventTarget {
|
|||
readonly attribute DOMString url;
|
||||
|
||||
// The full path in local storage where the file will end up once the download
|
||||
// is complete.
|
||||
// is complete. This is equivalent to the concatenation of the 'storagePath'
|
||||
// to the 'mountPoint' of the nsIVolume associated with the 'storageName'
|
||||
// (with delimiter).
|
||||
readonly attribute DOMString path;
|
||||
|
||||
// The DeviceStorage volume name on which the file is being downloaded.
|
||||
|
@ -69,7 +93,10 @@ interface DOMDownload : EventTarget {
|
|||
// downloaded.
|
||||
readonly attribute DOMString storagePath;
|
||||
|
||||
// The state of the download.
|
||||
// The state of the download. One of: downloading, stopped, succeeded, or
|
||||
// finalized. A finalized download is a download that has been removed /
|
||||
// cleared and is no longer tracked by the download manager and will not
|
||||
// receive any further onstatechange updates.
|
||||
readonly attribute DownloadState state;
|
||||
|
||||
// The mime type for this resource.
|
||||
|
@ -82,6 +109,10 @@ interface DOMDownload : EventTarget {
|
|||
// download (eg. in different windows) will have the same id.
|
||||
readonly attribute DOMString id;
|
||||
|
||||
// The manifestURL of the application that added this download. Only used for
|
||||
// downloads added via the adoptDownload API call.
|
||||
readonly attribute DOMString? sourceAppManifestURL;
|
||||
|
||||
// A DOM error object, that will be not null when a download is stopped
|
||||
// because something failed.
|
||||
readonly attribute DOMError? error;
|
||||
|
@ -100,3 +131,41 @@ interface DOMDownload : EventTarget {
|
|||
// - when the state and/or error attributes change.
|
||||
attribute EventHandler onstatechange;
|
||||
};
|
||||
|
||||
// Used to initialize the DOMDownload object for adopted downloads.
|
||||
// fields directly maps to the DOMDownload fields.
|
||||
dictionary AdoptDownloadDict {
|
||||
// The URL of this resource if there is one available. An empty string if
|
||||
// the download is not accessible via URL. An empty string is chosen over
|
||||
// null so that existinc code does not need to null-check but the value is
|
||||
// still falsey. (Note: If you do have a usable URL, you should probably not
|
||||
// be using the adoptDownload API and instead be initiating downloads the
|
||||
// normal way.)
|
||||
DOMString url;
|
||||
|
||||
// The storageName of the DeviceStorage instance the file was saved to.
|
||||
// Required but marked as optional so the bindings don't auto-coerce the value
|
||||
// null to "null".
|
||||
DOMString? storageName;
|
||||
// The path of the file within the DeviceStorage instance named by
|
||||
// 'storageName'. This is used to automatically compute the 'path' of the
|
||||
// download. Note that when DeviceStorage gives you a path to a file, the
|
||||
// first path segment is the name of the specific device storage and you do
|
||||
// *not* want to include this. For example, if DeviceStorage tells you the
|
||||
// file has a path of '/sdcard1/actual/path/file.ext', then the storageName
|
||||
// should be 'sdcard1' and the storagePath should be 'actual/path/file.ext'.
|
||||
//
|
||||
// The existence of the file will be validated will be validated with stat()
|
||||
// and the size the file-system tells us will be what we use.
|
||||
//
|
||||
// Required but marked as optional so the bindings don't auto-coerce the value
|
||||
// null to "null".
|
||||
DOMString? storagePath;
|
||||
|
||||
// The mime type for this resource. Required, but marked as optional because
|
||||
// WebIDL otherwise auto-coerces the value null to "null".
|
||||
DOMString? contentType;
|
||||
|
||||
// The time the download was started. If omitted, the current time is used.
|
||||
Date? startTime;
|
||||
};
|
||||
|
|
Загрузка…
Ссылка в новой задаче