Bug 1245603 - Implement browser.downloads.search(). r=kmag

MozReview-Commit-ID: 9XqkfZyeS8X
This commit is contained in:
Andrew Swan 2016-03-02 10:23:55 -08:00
parent b0cce4be97
commit b9f861bc36
6 changed files with 734 additions and 10 deletions

View File

@ -20,7 +20,267 @@ const {
ignoreEvent,
} = 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) => {
return {
@ -86,7 +346,7 @@ extensions.registerSchemaAPI("downloads", "downloads", (extension, context) => {
target: target,
})).then(dl => {
download = dl;
return Downloads.getList(Downloads.ALL);
return DownloadMap.getDownloadList();
}).then(list => {
list.add(download);
@ -94,9 +354,61 @@ extensions.registerSchemaAPI("downloads", "downloads", (extension, context) => {
download.tryToKeepPartialData = true;
download.start();
// Without other chrome.downloads methods, we can't actually
// do anything with the id so just return a dummy value for now.
return currentId++;
const item = DownloadMap.newFromDownload(download, extension);
return item.id;
});
},
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;
});
},

View File

@ -295,7 +295,7 @@
{
"name": "search",
"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.",
"parameters": [
{
@ -311,22 +311,26 @@
"startedBefore": {
"description": "Limits results to downloads that started before the given ms since the epoch.",
"optional": true,
"type": "string"
"type": "string",
"pattern": "^[1-9]\\d*$"
},
"startedAfter": {
"description": "Limits results to downloads that started after the given ms since the epoch.",
"optional": true,
"type": "string"
"type": "string",
"pattern": "^[1-9]\\d*$"
},
"endedBefore": {
"description": "Limits results to downloads that ended before the given ms since the epoch.",
"optional": true,
"type": "string"
"type": "string",
"pattern": "^[1-9]\\d*$"
},
"endedAfter": {
"description": "Limits results to downloads that ended after the given ms since the epoch.",
"optional": true,
"type": "string"
"type": "string",
"pattern": "^[1-9]\\d*$"
},
"totalBytesGreater": {
"description": "Limits results to downloads whose totalBytes is greater than the given integer.",

View File

@ -1,6 +1,8 @@
[DEFAULT]
skip-if = os == 'android'
support-files =
file_download.html
file_download.txt
[test_chrome_ext_downloads_download.html]
[test_chrome_ext_downloads_search.html]

View File

@ -0,0 +1,12 @@
<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">
</head>
<body>
<div>Download HTML File</div>
</body>
</html>

View File

@ -22,6 +22,7 @@ support-files =
file_privilege_escalation.html
file_ext_test_api_injection.js
file_permission_xhr.html
file_download.txt
[test_ext_simple.html]
[test_ext_schema.html]

View File

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