summaryrefslogtreecommitdiffstats
path: root/toolkit/components/extensions/test/xpcshell/test_ext_downloads_search.js
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/extensions/test/xpcshell/test_ext_downloads_search.js')
-rw-r--r--toolkit/components/extensions/test/xpcshell/test_ext_downloads_search.js682
1 files changed, 682 insertions, 0 deletions
diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_downloads_search.js b/toolkit/components/extensions/test/xpcshell/test_ext_downloads_search.js
new file mode 100644
index 0000000000..37c497a9b6
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_downloads_search.js
@@ -0,0 +1,682 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+const { Downloads } = ChromeUtils.importESModule(
+ "resource://gre/modules/Downloads.sys.mjs"
+);
+
+const server = createHttpServer();
+server.registerDirectory("/data/", do_get_file("data"));
+
+const BASE = `http://localhost:${server.identity.primaryPort}/data`;
+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 EMPTY_FILE = "empty_file_download.txt";
+const EMPTY_URL = BASE + "/" + EMPTY_FILE;
+const EMPTY_LEN = 0;
+const BIG_LEN = 1000; // something bigger both TXT_LEN and HTML_LEN
+
+function backgroundScript() {
+ let complete = new Map();
+
+ function waitForComplete(id) {
+ if (complete.has(id)) {
+ return complete.get(id).promise;
+ }
+
+ let promise = new Promise(resolve => {
+ complete.set(id, { resolve });
+ });
+ complete.get(id).promise = promise;
+ return promise;
+ }
+
+ browser.downloads.onChanged.addListener(change => {
+ if (change.state && change.state.current == "complete") {
+ // Make sure we have a promise.
+ waitForComplete(change.id);
+ complete.get(change.id).resolve();
+ }
+ });
+
+ browser.test.onMessage.addListener(async (msg, ...args) => {
+ if (msg == "download.request") {
+ try {
+ let id = await browser.downloads.download(args[0]);
+ 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") {
+ try {
+ let downloads = await browser.downloads.search(args[0]);
+ browser.test.sendMessage("search.done", {
+ status: "success",
+ downloads,
+ });
+ } catch (error) {
+ browser.test.sendMessage("search.done", {
+ status: "error",
+ errmsg: error.message,
+ });
+ }
+ } else if (msg == "waitForComplete.request") {
+ await waitForComplete(args[0]);
+ browser.test.sendMessage("waitForComplete.done");
+ }
+ });
+
+ browser.test.sendMessage("ready");
+}
+
+async function clearDownloads(callback) {
+ let list = await Downloads.getList(Downloads.ALL);
+ let downloads = await list.getAll();
+
+ await Promise.all(downloads.map(download => list.remove(download)));
+
+ return downloads;
+}
+
+add_task(async 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);
+ Services.prefs.setBoolPref("privacy.reduceTimerPrecision", false);
+
+ registerCleanupFunction(async () => {
+ Services.prefs.clearUserPref("browser.download.folderList");
+ Services.prefs.clearUserPref("browser.download.dir");
+ Services.prefs.clearUserPref("privacy.reduceTimerPrecision");
+ await cleanupDir(downloadDir);
+ await clearDownloads();
+ });
+
+ await clearDownloads().then(downloads => {
+ info(`removed ${downloads.length} pre-existing downloads from history`);
+ });
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background: backgroundScript,
+ manifest: {
+ permissions: ["downloads"],
+ },
+ });
+
+ async function download(options) {
+ extension.sendMessage("download.request", options);
+ let result = await extension.awaitMessage("download.done");
+
+ if (result.status == "success") {
+ info(`wait for onChanged event to indicate ${result.id} is complete`);
+ extension.sendMessage("waitForComplete.request", result.id);
+
+ await extension.awaitMessage("waitForComplete.done");
+ }
+
+ return result;
+ }
+
+ function search(query) {
+ extension.sendMessage("search.request", query);
+ return extension.awaitMessage("search.done");
+ }
+
+ await extension.startup();
+ await extension.awaitMessage("ready");
+
+ // Do some downloads...
+ const time1 = new Date();
+
+ let downloadIds = {};
+ let msg = await download({ url: TXT_URL });
+ equal(msg.status, "success", "download() succeeded");
+ downloadIds.txt1 = msg.id;
+
+ const TXT_FILE2 = "NewFile.txt";
+ msg = await download({ url: TXT_URL, filename: TXT_FILE2 });
+ equal(msg.status, "success", "download() succeeded");
+ downloadIds.txt2 = msg.id;
+
+ msg = await download({ url: EMPTY_URL });
+ equal(msg.status, "success", "download() succeeded");
+ downloadIds.txt3 = msg.id;
+
+ const time2 = new Date();
+
+ msg = await download({ url: HTML_URL });
+ equal(msg.status, "success", "download() succeeded");
+ downloadIds.html1 = msg.id;
+
+ const HTML_FILE2 = "renamed.html";
+ msg = await download({ url: HTML_URL, filename: HTML_FILE2 });
+ equal(msg.status, "success", "download() succeeded");
+ downloadIds.html2 = msg.id;
+
+ const time3 = new Date();
+
+ // Search for each individual download and check
+ // the corresponding DownloadItem.
+ async function checkDownloadItem(id, expect) {
+ let item = await search({ id });
+ equal(item.status, "success", "search() succeeded");
+ equal(item.downloads.length, 1, "search() found exactly 1 download");
+
+ Object.keys(expect).forEach(function (field) {
+ equal(
+ item.downloads[0][field],
+ expect[field],
+ `DownloadItem.${field} is correct"`
+ );
+ });
+ }
+ await 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,
+ });
+
+ await 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,
+ });
+
+ await checkDownloadItem(downloadIds.txt3, {
+ url: EMPTY_URL,
+ filename: downloadPath(EMPTY_FILE),
+ mime: "text/plain",
+ state: "complete",
+ bytesReceived: EMPTY_LEN,
+ totalBytes: EMPTY_LEN,
+ fileSize: EMPTY_LEN,
+ exists: true,
+ });
+
+ await 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,
+ });
+
+ await 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,
+ });
+
+ async function checkSearch(query, expected, description, exact) {
+ let item = await search(query);
+ equal(item.status, "success", "search() succeeded");
+ equal(
+ item.downloads.length,
+ expected.length,
+ `search() for ${description} found exactly ${expected.length} downloads`
+ );
+
+ let receivedIds = item.downloads.map(i => i.id);
+ if (exact) {
+ receivedIds.forEach((id, idx) => {
+ equal(
+ 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);
+ equal(
+ 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;
+ await checkSearch({ id: INVALID_ID }, [], "invalid id");
+
+ // Check that search on url works.
+ await checkSearch({ url: TXT_URL }, ["txt1", "txt2"], "url");
+
+ // Check that regexp on url works.
+ const HTML_REGEX = "[download]{8}.html+$";
+ await checkSearch({ urlRegex: HTML_REGEX }, ["html1", "html2"], "url regexp");
+
+ // Check that compatible url+regexp works
+ await checkSearch(
+ { url: HTML_URL, urlRegex: HTML_REGEX },
+ ["html1", "html2"],
+ "compatible url+urlRegex"
+ );
+
+ // Check that incompatible url+regexp works
+ await checkSearch(
+ { url: TXT_URL, urlRegex: HTML_REGEX },
+ [],
+ "incompatible url+urlRegex"
+ );
+
+ // Check that search on filename works.
+ await checkSearch({ filename: downloadPath(TXT_FILE) }, ["txt1"], "filename");
+
+ // Check that regexp on filename works.
+ await checkSearch({ filenameRegex: HTML_REGEX }, ["html1"], "filename regex");
+
+ // Check that compatible filename+regexp works
+ await checkSearch(
+ { filename: downloadPath(HTML_FILE), filenameRegex: HTML_REGEX },
+ ["html1"],
+ "compatible filename+filename regex"
+ );
+
+ // Check that incompatible filename+regexp works
+ await checkSearch(
+ { filename: downloadPath(TXT_FILE), filenameRegex: HTML_REGEX },
+ [],
+ "incompatible filename+filename regex"
+ );
+
+ // Check that simple positive search terms work.
+ await checkSearch(
+ { query: ["file_download"] },
+ ["txt1", "txt2", "txt3", "html1", "html2"],
+ "term file_download"
+ );
+ await checkSearch({ query: ["NewFile"] }, ["txt2"], "term NewFile");
+
+ // Check that positive search terms work case-insensitive.
+ await checkSearch({ query: ["nEwfILe"] }, ["txt2"], "term nEwfiLe");
+
+ // Check that negative search terms work.
+ await checkSearch({ query: ["-txt"] }, ["html1", "html2"], "term -txt");
+
+ // Check that positive and negative search terms together work.
+ await checkSearch(
+ { query: ["html", "-renamed"] },
+ ["html1"],
+ "positive and negative terms"
+ );
+
+ async function checkSearchWithDate(query, expected, description) {
+ const fields = Object.keys(query);
+ if (fields.length != 1 || !(query[fields[0]] instanceof Date)) {
+ throw new Error("checkSearchWithDate expects exactly one Date field");
+ }
+ const field = fields[0];
+ const date = query[field];
+
+ let newquery = {};
+
+ // Check as a Date
+ newquery[field] = date;
+ await checkSearch(newquery, expected, `${description} as Date`);
+
+ // Check as numeric milliseconds
+ newquery[field] = date.valueOf();
+ await checkSearch(newquery, expected, `${description} as numeric ms`);
+
+ // Check as stringified milliseconds
+ newquery[field] = date.valueOf().toString();
+ await checkSearch(newquery, expected, `${description} as string ms`);
+
+ // Check as ISO string
+ newquery[field] = date.toISOString();
+ await checkSearch(newquery, expected, `${description} as iso string`);
+ }
+
+ // Check startedBefore
+ await checkSearchWithDate({ startedBefore: time1 }, [], "before time1");
+ await checkSearchWithDate(
+ { startedBefore: time2 },
+ ["txt1", "txt2", "txt3"],
+ "before time2"
+ );
+ await checkSearchWithDate(
+ { startedBefore: time3 },
+ ["txt1", "txt2", "txt3", "html1", "html2"],
+ "before time3"
+ );
+
+ // Check startedAfter
+ await checkSearchWithDate(
+ { startedAfter: time1 },
+ ["txt1", "txt2", "txt3", "html1", "html2"],
+ "after time1"
+ );
+ await checkSearchWithDate(
+ { startedAfter: time2 },
+ ["html1", "html2"],
+ "after time2"
+ );
+ await checkSearchWithDate({ startedAfter: time3 }, [], "after time3");
+
+ // Check simple search on totalBytes
+ await checkSearch({ totalBytes: TXT_LEN }, ["txt1", "txt2"], "totalBytes");
+ await checkSearch({ totalBytes: HTML_LEN }, ["html1", "html2"], "totalBytes");
+
+ // Check simple test on totalBytes{Greater,Less}
+ // (NB: TXT_LEN < HTML_LEN < BIG_LEN)
+ await checkSearch(
+ { totalBytesGreater: 0 },
+ ["txt1", "txt2", "html1", "html2"],
+ "totalBytesGreater than 0"
+ );
+ await checkSearch(
+ { totalBytesGreater: TXT_LEN },
+ ["html1", "html2"],
+ `totalBytesGreater than ${TXT_LEN}`
+ );
+ await checkSearch(
+ { totalBytesGreater: HTML_LEN },
+ [],
+ `totalBytesGreater than ${HTML_LEN}`
+ );
+ await checkSearch(
+ { totalBytesLess: TXT_LEN },
+ ["txt3"],
+ `totalBytesLess than ${TXT_LEN}`
+ );
+ await checkSearch(
+ { totalBytesLess: HTML_LEN },
+ ["txt1", "txt2", "txt3"],
+ `totalBytesLess than ${HTML_LEN}`
+ );
+ await checkSearch(
+ { totalBytesLess: BIG_LEN },
+ ["txt1", "txt2", "txt3", "html1", "html2"],
+ `totalBytesLess than ${BIG_LEN}`
+ );
+
+ // Bug 1503760 check if 0 byte files with no search query are returned.
+ await checkSearch(
+ {},
+ ["txt1", "txt2", "txt3", "html1", "html2"],
+ "totalBytesGreater than -1"
+ );
+
+ // Check good combinations of totalBytes*.
+ await checkSearch(
+ { totalBytes: HTML_LEN, totalBytesGreater: TXT_LEN },
+ ["html1", "html2"],
+ "totalBytes and totalBytesGreater"
+ );
+ await checkSearch(
+ { totalBytes: TXT_LEN, totalBytesLess: HTML_LEN },
+ ["txt1", "txt2"],
+ "totalBytes and totalBytesGreater"
+ );
+ await checkSearch(
+ { totalBytes: HTML_LEN, totalBytesLess: BIG_LEN, totalBytesGreater: 0 },
+ ["html1", "html2"],
+ "totalBytes and totalBytesLess and totalBytesGreater"
+ );
+
+ // Check bad combination of totalBytes*.
+ await checkSearch(
+ { totalBytesLess: TXT_LEN, totalBytesGreater: HTML_LEN },
+ [],
+ "bad totalBytesLess, totalBytesGreater combination"
+ );
+ await checkSearch(
+ { totalBytes: TXT_LEN, totalBytesGreater: HTML_LEN },
+ [],
+ "bad totalBytes, totalBytesGreater combination"
+ );
+ await checkSearch(
+ { totalBytes: HTML_LEN, totalBytesLess: TXT_LEN },
+ [],
+ "bad totalBytes, totalBytesLess combination"
+ );
+
+ // Check mime.
+ await checkSearch(
+ { mime: "text/plain" },
+ ["txt1", "txt2", "txt3"],
+ "mime text/plain"
+ );
+ await checkSearch(
+ { mime: "text/html" },
+ ["html1", "html2"],
+ "mime text/htmlplain"
+ );
+ await checkSearch({ mime: "video/webm" }, [], "mime video/webm");
+
+ // Check fileSize.
+ await checkSearch({ fileSize: TXT_LEN }, ["txt1", "txt2"], "fileSize");
+ await 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.
+ await 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).
+ await checkSearch(
+ { orderBy: ["startTime"] },
+ ["txt1", "txt2", "txt3", "html1", "html2"],
+ "orderBy startTime",
+ true
+ );
+ await checkSearch(
+ { orderBy: ["-startTime"] },
+ ["html2", "html1", "txt3", "txt2", "txt1"],
+ "orderBy -startTime",
+ true
+ );
+
+ // Check orderBy with multiple fields.
+ // NB: TXT_URL and HTML_URL differ only in extension and .html precedes .txt
+ // EMPTY_URL begins with e which precedes f
+ await checkSearch(
+ { orderBy: ["url", "-startTime"] },
+ ["txt3", "html2", "html1", "txt2", "txt1"],
+ "orderBy with multiple fields",
+ true
+ );
+
+ // Check orderBy with limit.
+ await checkSearch(
+ { orderBy: ["url"], limit: 1 },
+ ["txt3"],
+ "orderBy with limit",
+ true
+ );
+
+ // Check bad arguments.
+ async function checkBadSearch(query, pattern, description) {
+ let item = await search(query);
+ equal(item.status, "error", "search() failed");
+ ok(
+ pattern.test(item.errmsg),
+ `error message for ${description} was correct (${item.errmsg}).`
+ );
+ }
+
+ await checkBadSearch(
+ "myquery",
+ /Incorrect argument type/,
+ "query is not an object"
+ );
+ await checkBadSearch(
+ { bogus: "boo" },
+ /Unexpected property/,
+ "query contains an unknown field"
+ );
+ await checkBadSearch(
+ { query: "query string" },
+ /Expected array/,
+ "query.query is a string"
+ );
+ await checkBadSearch(
+ { startedBefore: "i am not a time" },
+ /Type error/,
+ "query.startedBefore is not a valid time"
+ );
+ await checkBadSearch(
+ { startedAfter: "i am not a time" },
+ /Type error/,
+ "query.startedAfter is not a valid time"
+ );
+ await checkBadSearch(
+ { endedBefore: "i am not a time" },
+ /Type error/,
+ "query.endedBefore is not a valid time"
+ );
+ await checkBadSearch(
+ { endedAfter: "i am not a time" },
+ /Type error/,
+ "query.endedAfter is not a valid time"
+ );
+ await checkBadSearch(
+ { urlRegex: "[" },
+ /Invalid urlRegex/,
+ "query.urlRegexp is not a valid regular expression"
+ );
+ await checkBadSearch(
+ { filenameRegex: "[" },
+ /Invalid filenameRegex/,
+ "query.filenameRegexp is not a valid regular expression"
+ );
+ await checkBadSearch(
+ { orderBy: "startTime" },
+ /Expected array/,
+ "query.orderBy is not an array"
+ );
+ await checkBadSearch(
+ { orderBy: ["bogus"] },
+ /Invalid orderBy field/,
+ "query.orderBy references a non-existent field"
+ );
+
+ await extension.unload();
+});
+
+// Test that downloads with totalBytes of -1 (ie, that have not yet started)
+// work properly. See bug 1519762 for details of a past regression in
+// this area.
+add_task(async function test_inprogress() {
+ let resume,
+ resumePromise = new Promise(resolve => {
+ resume = resolve;
+ });
+ let hit = false;
+ server.registerPathHandler("/data/slow", async (request, response) => {
+ hit = true;
+ response.processAsync();
+ await resumePromise;
+ response.setHeader("Content-type", "text/plain");
+ response.write("");
+ response.finish();
+ });
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["downloads"],
+ },
+ background() {
+ browser.test.onMessage.addListener(async (msg, url) => {
+ let id = await browser.downloads.download({ url });
+ let full = await browser.downloads.search({ id });
+
+ browser.test.assertEq(
+ full.length,
+ 1,
+ "Found new download in search results"
+ );
+ browser.test.assertEq(
+ full[0].totalBytes,
+ -1,
+ "New download still has totalBytes == -1"
+ );
+
+ browser.downloads.onChanged.addListener(info => {
+ if (info.id == id && info.state && info.state.current == "complete") {
+ browser.test.notifyPass("done");
+ }
+ });
+
+ browser.test.sendMessage("started");
+ });
+ },
+ });
+
+ await extension.startup();
+ extension.sendMessage("go", `${BASE}/slow`);
+ await extension.awaitMessage("started");
+ resume();
+ await extension.awaitFinish("done");
+ await extension.unload();
+ Assert.ok(hit, "slow path was actually hit");
+});