mirror of
https://gitlab.winehq.org/wine/wine-gecko.git
synced 2024-09-13 09:24:08 -07:00
Bug 1245603 - Implement browser.downloads.search(). r=kmag
MozReview-Commit-ID: 9XqkfZyeS8X
This commit is contained in:
parent
b0cce4be97
commit
b9f861bc36
@ -20,7 +20,267 @@ const {
|
|||||||
ignoreEvent,
|
ignoreEvent,
|
||||||
} = ExtensionUtils;
|
} = ExtensionUtils;
|
||||||
|
|
||||||
let currentId = 0;
|
const DOWNLOAD_ITEM_FIELDS = ["id", "url", "referrer", "filename", "incognito",
|
||||||
|
"danger", "mime", "startTime", "endTime",
|
||||||
|
"estimatedEndTime", "state", "canResume",
|
||||||
|
"error", "bytesReceived", "totalBytes",
|
||||||
|
"fileSize", "exists",
|
||||||
|
"byExtensionId", "byExtensionName"];
|
||||||
|
|
||||||
|
class DownloadItem {
|
||||||
|
constructor(id, download, extension) {
|
||||||
|
this.id = id;
|
||||||
|
this.download = download;
|
||||||
|
this.extension = extension;
|
||||||
|
}
|
||||||
|
|
||||||
|
get url() { return this.download.source.url; }
|
||||||
|
get referrer() { return this.download.source.referrer; }
|
||||||
|
get filename() { return this.download.target.path; }
|
||||||
|
get incognito() { return this.download.source.isPrivate; }
|
||||||
|
get danger() { return "safe"; } // TODO
|
||||||
|
get mime() { return this.download.contentType; }
|
||||||
|
get startTime() { return this.download.startTime; }
|
||||||
|
get endTime() { return null; } // TODO
|
||||||
|
get estimatedEndTime() { return null; } // TODO
|
||||||
|
get state() {
|
||||||
|
if (this.download.succeeded) {
|
||||||
|
return "complete";
|
||||||
|
}
|
||||||
|
if (this.download.stopped) {
|
||||||
|
return "interrupted";
|
||||||
|
}
|
||||||
|
return "in_progress";
|
||||||
|
}
|
||||||
|
get canResume() {
|
||||||
|
return this.download.stopped && this.download.hasPartialData;
|
||||||
|
}
|
||||||
|
get error() {
|
||||||
|
if (!this.download.stopped || this.download.succeeded) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
// TODO store this instead of calculating it
|
||||||
|
|
||||||
|
if (this.download.error) {
|
||||||
|
if (this.download.error.becauseSourceFailed) {
|
||||||
|
return "NETWORK_FAILED"; // TODO
|
||||||
|
}
|
||||||
|
if (this.download.error.becauseTargetFailed) {
|
||||||
|
return "FILE_FAILED"; // TODO
|
||||||
|
}
|
||||||
|
return "CRASH";
|
||||||
|
}
|
||||||
|
return "USER_CANCELED";
|
||||||
|
}
|
||||||
|
get bytesReceived() {
|
||||||
|
return this.download.currentBytes;
|
||||||
|
}
|
||||||
|
get totalBytes() {
|
||||||
|
return this.download.hasProgress ? this.download.totalBytes : -1;
|
||||||
|
}
|
||||||
|
get fileSize() {
|
||||||
|
// todo: this is supposed to be post-compression
|
||||||
|
return this.download.succeeded ? this.download.target.size : -1;
|
||||||
|
}
|
||||||
|
get exists() { return this.download.target.exists; }
|
||||||
|
get byExtensionId() { return this.extension ? this.extension.id : undefined; }
|
||||||
|
get byExtensionName() { return this.extension ? this.extension.name : undefined; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a cloneable version of this object by pulling all the
|
||||||
|
* fields into simple properties (instead of getters).
|
||||||
|
*
|
||||||
|
* @returns {object} A DownloadItem with flat properties,
|
||||||
|
* suitable for cloning.
|
||||||
|
*/
|
||||||
|
serialize() {
|
||||||
|
let obj = {};
|
||||||
|
for (let field of DOWNLOAD_ITEM_FIELDS) {
|
||||||
|
obj[field] = this[field];
|
||||||
|
}
|
||||||
|
if (obj.startTime) {
|
||||||
|
obj.startTime = obj.startTime.toISOString();
|
||||||
|
}
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// DownloadMap maps back and forth betwen the numeric identifiers used in
|
||||||
|
// the downloads WebExtension API and a Download object from the Downloads jsm.
|
||||||
|
// todo: make id and extension info persistent (bug 1247794)
|
||||||
|
const DownloadMap = {
|
||||||
|
currentId: 0,
|
||||||
|
loadPromise: null,
|
||||||
|
|
||||||
|
// Maps numeric id -> DownloadItem
|
||||||
|
byId: new Map(),
|
||||||
|
|
||||||
|
// Maps Download object -> DownloadItem
|
||||||
|
byDownload: new WeakMap(),
|
||||||
|
|
||||||
|
lazyInit() {
|
||||||
|
if (this.loadPromise == null) {
|
||||||
|
this.loadPromise = Downloads.getList(Downloads.ALL).then(list => {
|
||||||
|
let self = this;
|
||||||
|
return list.addView({
|
||||||
|
onDownloadAdded(download) {
|
||||||
|
self.newFromDownload(download, null);
|
||||||
|
},
|
||||||
|
|
||||||
|
onDownloadRemoved(download) {
|
||||||
|
const item = self.byDownload.get(download);
|
||||||
|
if (item != null) {
|
||||||
|
self.byDownload.delete(download);
|
||||||
|
self.byId.delete(item.id);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}).then(() => list.getAll())
|
||||||
|
.then(downloads => {
|
||||||
|
downloads.forEach(download => {
|
||||||
|
this.newFromDownload(download, null);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.then(() => list);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return this.loadPromise;
|
||||||
|
},
|
||||||
|
|
||||||
|
getDownloadList() {
|
||||||
|
return this.lazyInit();
|
||||||
|
},
|
||||||
|
|
||||||
|
getAll() {
|
||||||
|
return this.lazyInit().then(() => this.byId.values());
|
||||||
|
},
|
||||||
|
|
||||||
|
fromId(id) {
|
||||||
|
const download = this.byId.get(id);
|
||||||
|
if (!download) {
|
||||||
|
throw new Error(`Invalid download id ${id}`);
|
||||||
|
}
|
||||||
|
return download;
|
||||||
|
},
|
||||||
|
|
||||||
|
newFromDownload(download, extension) {
|
||||||
|
if (this.byDownload.has(download)) {
|
||||||
|
return this.byDownload.get(download);
|
||||||
|
}
|
||||||
|
|
||||||
|
const id = ++this.currentId;
|
||||||
|
let item = new DownloadItem(id, download, extension);
|
||||||
|
this.byId.set(id, item);
|
||||||
|
this.byDownload.set(download, item);
|
||||||
|
return item;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create a callable function that filters a DownloadItem based on a
|
||||||
|
// query object of the type passed to search() or erase().
|
||||||
|
function downloadQuery(query) {
|
||||||
|
let queryTerms = [];
|
||||||
|
let queryNegativeTerms = [];
|
||||||
|
if (query.query != null) {
|
||||||
|
for (let term of query.query) {
|
||||||
|
if (term[0] == "-") {
|
||||||
|
queryNegativeTerms.push(term.slice(1).toLowerCase());
|
||||||
|
} else {
|
||||||
|
queryTerms.push(term.toLowerCase());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeTime(arg, before) {
|
||||||
|
if (arg == null) {
|
||||||
|
return before ? Number.MAX_VALUE : 0;
|
||||||
|
}
|
||||||
|
return parseInt(arg, 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
const startedBefore = normalizeTime(query.startedBefore, true);
|
||||||
|
const startedAfter = normalizeTime(query.startedAfter, false);
|
||||||
|
// const endedBefore = normalizeTime(query.endedBefore, true);
|
||||||
|
// const endedAfter = normalizeTime(query.endedAfter, false);
|
||||||
|
|
||||||
|
const totalBytesGreater = query.totalBytesGreater || 0;
|
||||||
|
const totalBytesLess = (query.totalBytesLess != null)
|
||||||
|
? query.totalBytesLess : Number.MAX_VALUE;
|
||||||
|
|
||||||
|
// Handle options for which we can have a regular expression and/or
|
||||||
|
// an explicit value to match.
|
||||||
|
function makeMatch(regex, value, field) {
|
||||||
|
if (value == null && regex == null) {
|
||||||
|
return input => true;
|
||||||
|
}
|
||||||
|
|
||||||
|
let re;
|
||||||
|
try {
|
||||||
|
re = new RegExp(regex || "", "i");
|
||||||
|
} catch (err) {
|
||||||
|
throw new Error(`Invalid ${field}Regex: ${err.message}`);
|
||||||
|
}
|
||||||
|
if (value == null) {
|
||||||
|
return input => re.test(input);
|
||||||
|
}
|
||||||
|
|
||||||
|
value = value.toLowerCase();
|
||||||
|
if (re.test(value)) {
|
||||||
|
return input => (value == input);
|
||||||
|
} else {
|
||||||
|
return input => false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const matchFilename = makeMatch(query.filenameRegex, query.filename, "filename");
|
||||||
|
const matchUrl = makeMatch(query.urlRegex, query.url, "url");
|
||||||
|
|
||||||
|
return function(item) {
|
||||||
|
const url = item.url.toLowerCase();
|
||||||
|
const filename = item.filename.toLowerCase();
|
||||||
|
|
||||||
|
if (!queryTerms.every(term => url.includes(term) || filename.includes(term))) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (queryNegativeTerms.some(term => url.includes(term) || filename.includes(term))) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!matchFilename(filename) || !matchUrl(url)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!item.startTime) {
|
||||||
|
if (query.startedBefore != null || query.startedAfter != null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else if (item.startTime > startedBefore || item.startTime < startedAfter) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// todo endedBefore, endedAfter
|
||||||
|
|
||||||
|
if (item.totalBytes == -1) {
|
||||||
|
if (query.totalBytesGreater != null || query.totalBytesLess != null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else if (item.totalBytes <= totalBytesGreater || item.totalBytes >= totalBytesLess) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// todo: include danger, paused, error
|
||||||
|
const SIMPLE_ITEMS = ["id", "mime", "startTime", "endTime", "state",
|
||||||
|
"bytesReceived", "totalBytes", "fileSize", "exists"];
|
||||||
|
for (let field of SIMPLE_ITEMS) {
|
||||||
|
if (query[field] != null && item[field] != query[field]) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
extensions.registerSchemaAPI("downloads", "downloads", (extension, context) => {
|
extensions.registerSchemaAPI("downloads", "downloads", (extension, context) => {
|
||||||
return {
|
return {
|
||||||
@ -86,7 +346,7 @@ extensions.registerSchemaAPI("downloads", "downloads", (extension, context) => {
|
|||||||
target: target,
|
target: target,
|
||||||
})).then(dl => {
|
})).then(dl => {
|
||||||
download = dl;
|
download = dl;
|
||||||
return Downloads.getList(Downloads.ALL);
|
return DownloadMap.getDownloadList();
|
||||||
}).then(list => {
|
}).then(list => {
|
||||||
list.add(download);
|
list.add(download);
|
||||||
|
|
||||||
@ -94,9 +354,61 @@ extensions.registerSchemaAPI("downloads", "downloads", (extension, context) => {
|
|||||||
download.tryToKeepPartialData = true;
|
download.tryToKeepPartialData = true;
|
||||||
download.start();
|
download.start();
|
||||||
|
|
||||||
// Without other chrome.downloads methods, we can't actually
|
const item = DownloadMap.newFromDownload(download, extension);
|
||||||
// do anything with the id so just return a dummy value for now.
|
return item.id;
|
||||||
return currentId++;
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
search(query) {
|
||||||
|
let matchFn;
|
||||||
|
try {
|
||||||
|
matchFn = downloadQuery(query);
|
||||||
|
} catch (err) {
|
||||||
|
return Promise.reject({message: err.message});
|
||||||
|
}
|
||||||
|
|
||||||
|
let compareFn;
|
||||||
|
if (query.orderBy != null) {
|
||||||
|
const fields = query.orderBy.map(field => field[0] == "-"
|
||||||
|
? {reverse: true, name: field.slice(1)}
|
||||||
|
: {reverse: false, name: field});
|
||||||
|
|
||||||
|
for (let field of fields) {
|
||||||
|
if (!DOWNLOAD_ITEM_FIELDS.includes(field.name)) {
|
||||||
|
return Promise.reject({message: `Invalid orderBy field ${field.name}`});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
compareFn = (dl1, dl2) => {
|
||||||
|
for (let field of fields) {
|
||||||
|
const val1 = dl1[field.name];
|
||||||
|
const val2 = dl2[field.name];
|
||||||
|
|
||||||
|
if (val1 < val2) {
|
||||||
|
return field.reverse ? 1 : -1;
|
||||||
|
} else if (val1 > val2) {
|
||||||
|
return field.reverse ? -1 : 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return DownloadMap.getAll().then(downloads => {
|
||||||
|
if (compareFn) {
|
||||||
|
downloads = Array.from(downloads);
|
||||||
|
downloads.sort(compareFn);
|
||||||
|
}
|
||||||
|
let results = [];
|
||||||
|
for (let download of downloads) {
|
||||||
|
if (query.limit && results.length >= query.limit) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (matchFn(download)) {
|
||||||
|
results.push(download.serialize());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return results;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -295,7 +295,7 @@
|
|||||||
{
|
{
|
||||||
"name": "search",
|
"name": "search",
|
||||||
"type": "function",
|
"type": "function",
|
||||||
"unsupported": true,
|
"async": "callback",
|
||||||
"description": "Find <a href='#type-DownloadItem'>DownloadItems</a>. Set <code>query</code> to the empty object to get all <a href='#type-DownloadItem'>DownloadItems</a>. To get a specific <a href='#type-DownloadItem'>DownloadItem</a>, set only the <code>id</code> field.",
|
"description": "Find <a href='#type-DownloadItem'>DownloadItems</a>. Set <code>query</code> to the empty object to get all <a href='#type-DownloadItem'>DownloadItems</a>. To get a specific <a href='#type-DownloadItem'>DownloadItem</a>, set only the <code>id</code> field.",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
@ -311,22 +311,26 @@
|
|||||||
"startedBefore": {
|
"startedBefore": {
|
||||||
"description": "Limits results to downloads that started before the given ms since the epoch.",
|
"description": "Limits results to downloads that started before the given ms since the epoch.",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"type": "string"
|
"type": "string",
|
||||||
|
"pattern": "^[1-9]\\d*$"
|
||||||
},
|
},
|
||||||
"startedAfter": {
|
"startedAfter": {
|
||||||
"description": "Limits results to downloads that started after the given ms since the epoch.",
|
"description": "Limits results to downloads that started after the given ms since the epoch.",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"type": "string"
|
"type": "string",
|
||||||
|
"pattern": "^[1-9]\\d*$"
|
||||||
},
|
},
|
||||||
"endedBefore": {
|
"endedBefore": {
|
||||||
"description": "Limits results to downloads that ended before the given ms since the epoch.",
|
"description": "Limits results to downloads that ended before the given ms since the epoch.",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"type": "string"
|
"type": "string",
|
||||||
|
"pattern": "^[1-9]\\d*$"
|
||||||
},
|
},
|
||||||
"endedAfter": {
|
"endedAfter": {
|
||||||
"description": "Limits results to downloads that ended after the given ms since the epoch.",
|
"description": "Limits results to downloads that ended after the given ms since the epoch.",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"type": "string"
|
"type": "string",
|
||||||
|
"pattern": "^[1-9]\\d*$"
|
||||||
},
|
},
|
||||||
"totalBytesGreater": {
|
"totalBytesGreater": {
|
||||||
"description": "Limits results to downloads whose totalBytes is greater than the given integer.",
|
"description": "Limits results to downloads whose totalBytes is greater than the given integer.",
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
[DEFAULT]
|
[DEFAULT]
|
||||||
skip-if = os == 'android'
|
skip-if = os == 'android'
|
||||||
support-files =
|
support-files =
|
||||||
|
file_download.html
|
||||||
file_download.txt
|
file_download.txt
|
||||||
|
|
||||||
[test_chrome_ext_downloads_download.html]
|
[test_chrome_ext_downloads_download.html]
|
||||||
|
[test_chrome_ext_downloads_search.html]
|
||||||
|
@ -0,0 +1,12 @@
|
|||||||
|
<!DOCTYPE HTML>
|
||||||
|
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<div>Download HTML File</div>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
@ -22,6 +22,7 @@ support-files =
|
|||||||
file_privilege_escalation.html
|
file_privilege_escalation.html
|
||||||
file_ext_test_api_injection.js
|
file_ext_test_api_injection.js
|
||||||
file_permission_xhr.html
|
file_permission_xhr.html
|
||||||
|
file_download.txt
|
||||||
|
|
||||||
[test_ext_simple.html]
|
[test_ext_simple.html]
|
||||||
[test_ext_schema.html]
|
[test_ext_schema.html]
|
||||||
|
@ -0,0 +1,393 @@
|
|||||||
|
<!DOCTYPE HTML>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>WebExtension test</title>
|
||||||
|
<script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
|
||||||
|
<script src="chrome://mochikit/content/tests/SimpleTest/SpawnTask.js"></script>
|
||||||
|
<script src="chrome://mochikit/content/tests/SimpleTest/ExtensionTestUtils.js"></script>
|
||||||
|
<script type="text/javascript" src="head.js"></script>
|
||||||
|
<link rel="stylesheet" href="chrome://mochikit/contents/tests/SimpleTest/test.css"/>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<script type="text/javascript">
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
const {
|
||||||
|
interfaces: Ci,
|
||||||
|
utils: Cu,
|
||||||
|
} = Components;
|
||||||
|
|
||||||
|
Cu.import("resource://gre/modules/Services.jsm");
|
||||||
|
Cu.import("resource://gre/modules/FileUtils.jsm");
|
||||||
|
Cu.import("resource://gre/modules/Downloads.jsm");
|
||||||
|
|
||||||
|
const BASE = "http://mochi.test:8888/chrome/toolkit/components/extensions/test/mochitest";
|
||||||
|
const TXT_FILE = "file_download.txt";
|
||||||
|
const TXT_URL = BASE + "/" + TXT_FILE;
|
||||||
|
const TXT_LEN = 46;
|
||||||
|
const HTML_FILE = "file_download.html";
|
||||||
|
const HTML_URL = BASE + "/" + HTML_FILE;
|
||||||
|
const HTML_LEN = 117;
|
||||||
|
const BIG_LEN = 1000; // something bigger both TXT_LEN and HTML_LEN
|
||||||
|
|
||||||
|
function backgroundScript() {
|
||||||
|
browser.test.onMessage.addListener(function(msg) {
|
||||||
|
// extension functions throw on bad arguments, we can remove the extra
|
||||||
|
// promise when bug 1250223 is fixed.
|
||||||
|
if (msg == "download.request") {
|
||||||
|
Promise.resolve().then(() => browser.downloads.download(arguments[1]))
|
||||||
|
.then(id => {
|
||||||
|
browser.test.sendMessage("download.done", {status: "success", id});
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
browser.test.sendMessage("download.done", {status: "error", errmsg: error.message});
|
||||||
|
});
|
||||||
|
} else if (msg == "search.request") {
|
||||||
|
Promise.resolve().then(() => browser.downloads.search(arguments[1]))
|
||||||
|
.then(downloads => {
|
||||||
|
browser.test.sendMessage("search.done", {status: "success", downloads});
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
browser.test.sendMessage("search.done", {status: "error", errmsg: error.message});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
browser.test.sendMessage("ready");
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearDownloads(callback) {
|
||||||
|
return Downloads.getList(Downloads.ALL).then(list => {
|
||||||
|
return list.getAll().then(downloads => {
|
||||||
|
return Promise.all(downloads.map(download => list.remove(download)))
|
||||||
|
.then(() => downloads);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// This function is a bit of a sledgehammer, it looks at every download
|
||||||
|
// the browser knows about and waits for all active downloads to complete.
|
||||||
|
// But we only start one at a time and only do a handful in total.
|
||||||
|
// Replace this when we have onChanged (bug 1245600)
|
||||||
|
function waitForDownloads() {
|
||||||
|
return Downloads.getList(Downloads.ALL)
|
||||||
|
.then(list => list.getAll())
|
||||||
|
.then(downloads => {
|
||||||
|
let inprogress = downloads.filter(dl => !dl.stopped);
|
||||||
|
return Promise.all(inprogress.map(dl => dl.whenSucceeded()));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
add_task(function* test_search() {
|
||||||
|
const nsIFile = Ci.nsIFile;
|
||||||
|
let downloadDir = FileUtils.getDir("TmpD", ["downloads"]);
|
||||||
|
downloadDir.createUnique(nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
|
||||||
|
info(`downloadDir ${downloadDir.path}`);
|
||||||
|
|
||||||
|
function downloadPath(filename) {
|
||||||
|
let path = downloadDir.clone();
|
||||||
|
path.append(filename);
|
||||||
|
return path.path;
|
||||||
|
}
|
||||||
|
|
||||||
|
Services.prefs.setIntPref("browser.download.folderList", 2);
|
||||||
|
Services.prefs.setComplexValue("browser.download.dir", nsIFile, downloadDir);
|
||||||
|
|
||||||
|
SimpleTest.registerCleanupFunction(() => {
|
||||||
|
Services.prefs.clearUserPref("browser.download.folderList");
|
||||||
|
Services.prefs.clearUserPref("browser.download.dir");
|
||||||
|
downloadDir.remove(true);
|
||||||
|
return clearDownloads();
|
||||||
|
});
|
||||||
|
|
||||||
|
yield clearDownloads().then(downloads => {
|
||||||
|
info(`removed ${downloads.length} pre-existing downloads from history`);
|
||||||
|
});
|
||||||
|
|
||||||
|
let extension = ExtensionTestUtils.loadExtension({
|
||||||
|
background: `(${backgroundScript})()`,
|
||||||
|
manifest: {
|
||||||
|
permissions: ["downloads"],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
function download(options) {
|
||||||
|
extension.sendMessage("download.request", options);
|
||||||
|
return extension.awaitMessage("download.done");
|
||||||
|
}
|
||||||
|
|
||||||
|
function search(query) {
|
||||||
|
extension.sendMessage("search.request", query);
|
||||||
|
return extension.awaitMessage("search.done");
|
||||||
|
}
|
||||||
|
|
||||||
|
yield extension.startup();
|
||||||
|
yield extension.awaitMessage("ready");
|
||||||
|
info("extension started");
|
||||||
|
|
||||||
|
// Do some downloads...
|
||||||
|
const time1 = new Date();
|
||||||
|
|
||||||
|
let downloadIds = {};
|
||||||
|
let msg = yield download({url: TXT_URL});
|
||||||
|
is(msg.status, "success", "download() succeeded");
|
||||||
|
downloadIds.txt1 = msg.id;
|
||||||
|
|
||||||
|
const TXT_FILE2 = "NewFile.txt";
|
||||||
|
msg = yield download({url: TXT_URL, filename: TXT_FILE2});
|
||||||
|
is(msg.status, "success", "download() succeeded");
|
||||||
|
downloadIds.txt2 = msg.id;
|
||||||
|
|
||||||
|
const time2 = new Date();
|
||||||
|
|
||||||
|
msg = yield download({url: HTML_URL});
|
||||||
|
is(msg.status, "success", "download() succeeded");
|
||||||
|
downloadIds.html1 = msg.id;
|
||||||
|
|
||||||
|
const HTML_FILE2 = "renamed.html";
|
||||||
|
msg = yield download({url: HTML_URL, filename: HTML_FILE2});
|
||||||
|
is(msg.status, "success", "download() succeeded");
|
||||||
|
downloadIds.html2 = msg.id;
|
||||||
|
|
||||||
|
const time3 = new Date();
|
||||||
|
|
||||||
|
yield waitForDownloads();
|
||||||
|
|
||||||
|
// Search for each individual download and check
|
||||||
|
// the corresponding DownloadItem.
|
||||||
|
function* checkDownloadItem(id, expect) {
|
||||||
|
let msg = yield search({id});
|
||||||
|
is(msg.status, "success", "search() succeeded");
|
||||||
|
is(msg.downloads.length, 1, "search() found exactly 1 download");
|
||||||
|
|
||||||
|
Object.keys(expect).forEach(function(field) {
|
||||||
|
is(msg.downloads[0][field], expect[field], `DownloadItem.${field} is correct"`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
yield checkDownloadItem(downloadIds.txt1, {
|
||||||
|
url: TXT_URL,
|
||||||
|
filename: downloadPath(TXT_FILE),
|
||||||
|
mime: "text/plain",
|
||||||
|
state: "complete",
|
||||||
|
bytesReceived: TXT_LEN,
|
||||||
|
totalBytes: TXT_LEN,
|
||||||
|
fileSize: TXT_LEN,
|
||||||
|
exists: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
yield checkDownloadItem(downloadIds.txt2, {
|
||||||
|
url: TXT_URL,
|
||||||
|
filename: downloadPath(TXT_FILE2),
|
||||||
|
mime: "text/plain",
|
||||||
|
state: "complete",
|
||||||
|
bytesReceived: TXT_LEN,
|
||||||
|
totalBytes: TXT_LEN,
|
||||||
|
fileSize: TXT_LEN,
|
||||||
|
exists: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
yield checkDownloadItem(downloadIds.html1, {
|
||||||
|
url: HTML_URL,
|
||||||
|
filename: downloadPath(HTML_FILE),
|
||||||
|
mime: "text/html",
|
||||||
|
state: "complete",
|
||||||
|
bytesReceived: HTML_LEN,
|
||||||
|
totalBytes: HTML_LEN,
|
||||||
|
fileSize: HTML_LEN,
|
||||||
|
exists: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
yield checkDownloadItem(downloadIds.html2, {
|
||||||
|
url: HTML_URL,
|
||||||
|
filename: downloadPath(HTML_FILE2),
|
||||||
|
mime: "text/html",
|
||||||
|
state: "complete",
|
||||||
|
bytesReceived: HTML_LEN,
|
||||||
|
totalBytes: HTML_LEN,
|
||||||
|
fileSize: HTML_LEN,
|
||||||
|
exists: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
function* checkSearch(query, expected, description, exact) {
|
||||||
|
let msg = yield search(query);
|
||||||
|
is(msg.status, "success", "search() succeeded");
|
||||||
|
is(msg.downloads.length, expected.length, `search() for ${description} found exactly ${expected.length} downloads`);
|
||||||
|
|
||||||
|
let receivedIds = msg.downloads.map(item => item.id);
|
||||||
|
if (exact) {
|
||||||
|
receivedIds.forEach((id, idx) => {
|
||||||
|
is(id, downloadIds[expected[idx]], `search() for ${description} returned ${expected[idx]} in position ${idx}`);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
Object.keys(downloadIds).forEach(key => {
|
||||||
|
const id = downloadIds[key];
|
||||||
|
const thisExpected = expected.includes(key);
|
||||||
|
is(receivedIds.includes(id), thisExpected,
|
||||||
|
`search() for ${description} ${thisExpected ? "includes" : "does not include"} ${key}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that search with an invalid id returns nothing.
|
||||||
|
// NB: for now ids are not persistent and we start numbering them at 1
|
||||||
|
// so a sufficiently large number will be unused.
|
||||||
|
const INVALID_ID = 1000;
|
||||||
|
yield checkSearch({id: INVALID_ID}, [], "invalid id");
|
||||||
|
|
||||||
|
// Check that search on url works.
|
||||||
|
yield checkSearch({url: TXT_URL}, ["txt1", "txt2"], "url");
|
||||||
|
|
||||||
|
// Check that regexp on url works.
|
||||||
|
const HTML_REGEX = "[downlad]{8}\.html+$";
|
||||||
|
yield checkSearch({urlRegex: HTML_REGEX}, ["html1", "html2"], "url regexp");
|
||||||
|
|
||||||
|
// Check that compatible url+regexp works
|
||||||
|
yield checkSearch({url: HTML_URL, urlRegex: HTML_REGEX}, ["html1", "html2"], "compatible url+urlRegex");
|
||||||
|
|
||||||
|
// Check that incompatible url+regexp works
|
||||||
|
yield checkSearch({url: TXT_URL, urlRegex: HTML_REGEX}, [], "incompatible url+urlRegex");
|
||||||
|
|
||||||
|
// Check that search on filename works.
|
||||||
|
yield checkSearch({filename: downloadPath(TXT_FILE)}, ["txt1"], "filename");
|
||||||
|
|
||||||
|
// Check that regexp on filename works.
|
||||||
|
yield checkSearch({filenameRegex: HTML_REGEX}, ["html1"], "filename regex");
|
||||||
|
|
||||||
|
// Check that compatible filename+regexp works
|
||||||
|
yield checkSearch({filename: downloadPath(HTML_FILE), filenameRegex: HTML_REGEX}, ["html1"], "compatible filename+filename regex");
|
||||||
|
|
||||||
|
// Check that incompatible filename+regexp works
|
||||||
|
yield checkSearch({filename: downloadPath(TXT_FILE), filenameRegex: HTML_REGEX}, [], "incompatible filename+filename regex");
|
||||||
|
|
||||||
|
// Check that simple positive search terms work.
|
||||||
|
yield checkSearch({query: ["file_download"]}, ["txt1", "txt2", "html1", "html2"],
|
||||||
|
"term file_download");
|
||||||
|
yield checkSearch({query: ["NewFile"]}, ["txt2"], "term NewFile");
|
||||||
|
|
||||||
|
// Check that positive search terms work case-insensitive.
|
||||||
|
yield checkSearch({query: ["nEwfILe"]}, ["txt2"], "term nEwfiLe");
|
||||||
|
|
||||||
|
// Check that negative search terms work.
|
||||||
|
yield checkSearch({query: ["-txt"]}, ["html1", "html2"], "term -txt");
|
||||||
|
|
||||||
|
// Check that positive and negative search terms together work.
|
||||||
|
yield checkSearch({query: ["html", "-renamed"]}, ["html1"], "postive and negative terms");
|
||||||
|
|
||||||
|
// Check that startedBefore works with stringified milliseconds.
|
||||||
|
yield checkSearch({startedBefore: time1.valueOf().toString()}, [], "before time1");
|
||||||
|
yield checkSearch({startedBefore: time2.valueOf().toString()}, ["txt1", "txt2"], "before time2");
|
||||||
|
yield checkSearch({startedBefore: time3.valueOf().toString()}, ["txt1", "txt2", "html1", "html2"], "before time3");
|
||||||
|
|
||||||
|
// Check that startedBefore works with iso string.
|
||||||
|
// enable with fix for bug 1251766
|
||||||
|
// yield checkSearch({startedBefore: time1.toISOString()}, [], "before time1");
|
||||||
|
// yield checkSearch({startedBefore: time2.toISOString()}, ["txt1", "txt2"], "before time2");
|
||||||
|
// yield checkSearch({startedBefore: time3.toISOString()}, ["txt1", "txt2", "html1", "html2"], "before time3");
|
||||||
|
|
||||||
|
// Check that startedAfter works with stringified milliseconds.
|
||||||
|
yield checkSearch({startedAfter: time1.valueOf().toString()}, ["txt1", "txt2", "html1", "html2"], "after time1");
|
||||||
|
yield checkSearch({startedAfter: time2.valueOf().toString()}, ["html1", "html2"], "after time2");
|
||||||
|
yield checkSearch({startedAfter: time3.valueOf().toString()}, [], "after time3");
|
||||||
|
|
||||||
|
// Check that startedAfter works with iso string.
|
||||||
|
// enable with fix for bug 1251766
|
||||||
|
// yield checkSearch({startedAfter: time1.toISOString()}, ["txt1", "txt2", "html1", "html2"], "after time1");
|
||||||
|
// yield checkSearch({startedAfter: time2.toISOString()}, ["html1", "html2"], "after time2");
|
||||||
|
// yield checkSearch({startedAfter: time3.toISOString()}, [], "after time3");
|
||||||
|
|
||||||
|
// Check simple search on totalBytes
|
||||||
|
yield checkSearch({totalBytes: TXT_LEN}, ["txt1", "txt2"], "totalBytes");
|
||||||
|
yield checkSearch({totalBytes: HTML_LEN}, ["html1", "html2"], "totalBytes");
|
||||||
|
|
||||||
|
// Check simple test on totalBytes{Greater,Less}
|
||||||
|
// (NB: TXT_LEN < HTML_LEN < BIG_LEN)
|
||||||
|
yield checkSearch({totalBytesGreater: 0}, ["txt1", "txt2", "html1", "html2"], "totalBytesGreater than 0");
|
||||||
|
yield checkSearch({totalBytesGreater: TXT_LEN}, ["html1", "html2"], `totalBytesGreater than ${TXT_LEN}`);
|
||||||
|
yield checkSearch({totalBytesGreater: HTML_LEN}, [], `totalBytesGreater than ${HTML_LEN}`);
|
||||||
|
yield checkSearch({totalBytesLess: TXT_LEN}, [], `totalBytesLess than ${TXT_LEN}`);
|
||||||
|
yield checkSearch({totalBytesLess: HTML_LEN}, ["txt1", "txt2"], `totalBytesLess than ${HTML_LEN}`);
|
||||||
|
yield checkSearch({totalBytesLess: BIG_LEN}, ["txt1", "txt2", "html1", "html2"], `totalBytesLess than ${BIG_LEN}`);
|
||||||
|
|
||||||
|
// Check good combinations of totalBytes*.
|
||||||
|
yield checkSearch({totalBytes: HTML_LEN, totalBytesGreater: TXT_LEN}, ["html1", "html2"], "totalBytes and totalBytesGreater");
|
||||||
|
yield checkSearch({totalBytes: TXT_LEN, totalBytesLess: HTML_LEN}, ["txt1", "txt2"], "totalBytes and totalBytesGreater");
|
||||||
|
yield checkSearch({totalBytes: HTML_LEN, totalBytesLess: BIG_LEN, totalBytesGreater: 0}, ["html1", "html2"], "totalBytes and totalBytesLess and totalBytesGreater");
|
||||||
|
|
||||||
|
// Check bad combination of totalBytes*.
|
||||||
|
yield checkSearch({totalBytesLess: TXT_LEN, totalBytesGreater: HTML_LEN}, [], "bad totalBytesLess, totalBytesGreater combination");
|
||||||
|
yield checkSearch({totalBytes: TXT_LEN, totalBytesGreater: HTML_LEN}, [], "bad totalBytes, totalBytesGreater combination");
|
||||||
|
yield checkSearch({totalBytes: HTML_LEN, totalBytesLess: TXT_LEN}, [], "bad totalBytes, totalBytesLess combination");
|
||||||
|
|
||||||
|
// Check mime.
|
||||||
|
yield checkSearch({mime: "text/plain"}, ["txt1", "txt2"], "mime text/plain");
|
||||||
|
yield checkSearch({mime: "text/html"}, ["html1", "html2"], "mime text/htmlplain");
|
||||||
|
yield checkSearch({mime: "video/webm"}, [], "mime video/webm");
|
||||||
|
|
||||||
|
// Check fileSize.
|
||||||
|
yield checkSearch({fileSize: TXT_LEN}, ["txt1", "txt2"], "fileSize");
|
||||||
|
yield checkSearch({fileSize: HTML_LEN}, ["html1", "html2"], "fileSize");
|
||||||
|
|
||||||
|
// Fields like bytesReceived, paused, state, exists are meaningful
|
||||||
|
// for downloads that are in progress but have not yet completed.
|
||||||
|
// todo: add tests for these when we have better support for in-progress
|
||||||
|
// downloads (e.g., after pause(), resume() and cancel() are implemented)
|
||||||
|
|
||||||
|
// Check multiple query properties.
|
||||||
|
// We could make this testing arbitrarily complicated...
|
||||||
|
// We already tested combining fields with obvious interactions above
|
||||||
|
// (e.g., filename and filenameRegex or startTime and startedBefore/After)
|
||||||
|
// so now just throw as many fields as we can at a single search and
|
||||||
|
// make sure a simple case still works.
|
||||||
|
yield checkSearch({
|
||||||
|
url: TXT_URL,
|
||||||
|
urlRegex: "download",
|
||||||
|
filename: downloadPath(TXT_FILE),
|
||||||
|
filenameRegex: "download",
|
||||||
|
query: ["download"],
|
||||||
|
startedAfter: time1.valueOf().toString(),
|
||||||
|
startedBefore: time2.valueOf().toString(),
|
||||||
|
totalBytes: TXT_LEN,
|
||||||
|
totalBytesGreater: 0,
|
||||||
|
totalBytesLess: BIG_LEN,
|
||||||
|
mime: "text/plain",
|
||||||
|
fileSize: TXT_LEN,
|
||||||
|
}, ["txt1"], "many properties");
|
||||||
|
|
||||||
|
// Check simple orderBy (forward and backward).
|
||||||
|
yield checkSearch({orderBy: ["startTime"]}, ["txt1", "txt2", "html1", "html2"], "orderBy startTime", true);
|
||||||
|
yield checkSearch({orderBy: ["-startTime"]}, ["html2", "html1", "txt2", "txt1"], "orderBy -startTime", true);
|
||||||
|
|
||||||
|
// Check orderBy with multiple fields.
|
||||||
|
// NB: TXT_URL and HTML_URL differ only in extension and .html precedes .txt
|
||||||
|
yield checkSearch({orderBy: ["url", "-startTime"]}, ["html2", "html1", "txt2", "txt1"], "orderBy with multiple fields", true);
|
||||||
|
|
||||||
|
// Check orderBy with limit.
|
||||||
|
yield checkSearch({orderBy: ["url"], limit: 1}, ["html1"], "orderBy with limit", true);
|
||||||
|
|
||||||
|
// Check bad arguments.
|
||||||
|
function* checkBadSearch(query, pattern, description) {
|
||||||
|
let msg = yield search(query);
|
||||||
|
is(msg.status, "error", "search() failed");
|
||||||
|
ok(pattern.test(msg.errmsg), `error message for ${description} was correct (${msg.errmsg}).`);
|
||||||
|
}
|
||||||
|
|
||||||
|
yield checkBadSearch("myquery", /Incorrect argument type/, "query is not an object");
|
||||||
|
yield checkBadSearch({bogus: "boo"}, /Unexpected property/, "query contains an unknown field");
|
||||||
|
yield checkBadSearch({query: "query string"}, /Expected array/, "query.query is a string");
|
||||||
|
yield checkBadSearch({startedBefore: "i am not a number"}, /Type error/, "query.startedBefore is not a valid time");
|
||||||
|
yield checkBadSearch({startedAfter: "i am not a number"}, /Type error/, "query.startedAfter is not a valid time");
|
||||||
|
yield checkBadSearch({endedBefore: "i am not a number"}, /Type error/, "query.endedBefore is not a valid time");
|
||||||
|
yield checkBadSearch({endedAfter: "i am not a number"}, /Type error/, "query.endedAfter is not a valid time");
|
||||||
|
yield checkBadSearch({urlRegex: "["}, /Invalid urlRegex/, "query.urlRegexp is not a valid regular expression");
|
||||||
|
yield checkBadSearch({filenameRegex: "["}, /Invalid filenameRegex/, "query.filenameRegexp is not a valid regular expression");
|
||||||
|
yield checkBadSearch({orderBy: "startTime"}, /Expected array/, "query.orderBy is not an array");
|
||||||
|
yield checkBadSearch({orderBy: ["bogus"]}, /Invalid orderBy field/, "query.orderBy references a non-existent field");
|
||||||
|
|
||||||
|
yield extension.unload();
|
||||||
|
});
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
Loading…
Reference in New Issue
Block a user