diff options
Diffstat (limited to 'toolkit/components/extensions/test/xpcshell/test_ext_downloads_misc.js')
-rw-r--r-- | toolkit/components/extensions/test/xpcshell/test_ext_downloads_misc.js | 1173 |
1 files changed, 1173 insertions, 0 deletions
diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_downloads_misc.js b/toolkit/components/extensions/test/xpcshell/test_ext_downloads_misc.js new file mode 100644 index 0000000000..bb54283c3c --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/test_ext_downloads_misc.js @@ -0,0 +1,1173 @@ +/* -*- 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 { TestUtils } = ChromeUtils.importESModule( + "resource://testing-common/TestUtils.sys.mjs" +); + +const server = createHttpServer(); +server.registerDirectory("/data/", do_get_file("data")); + +const ROOT = `http://localhost:${server.identity.primaryPort}`; +const BASE = `${ROOT}/data`; +const TXT_FILE = "file_download.txt"; +const TXT_URL = BASE + "/" + TXT_FILE; + +// Keep these in sync with code in interruptible.sjs +const INT_PARTIAL_LEN = 15; +const INT_TOTAL_LEN = 31; + +const TEST_DATA = "This is 31 bytes of sample data"; +const TOTAL_LEN = TEST_DATA.length; +const PARTIAL_LEN = 15; + +// A handler to let us systematically test pausing/resuming/canceling +// of downloads. This target represents a small text file but a simple +// GET will stall after sending part of the data, to give the test code +// a chance to pause or do other operations on an in-progress download. +// A resumed download (ie, a GET with a Range: header) will allow the +// download to complete. +function handleRequest(request, response) { + response.setHeader("Content-Type", "text/plain", false); + + if (request.hasHeader("Range")) { + let start, end; + let matches = request + .getHeader("Range") + .match(/^\s*bytes=(\d+)?-(\d+)?\s*$/); + if (matches != null) { + start = matches[1] ? parseInt(matches[1], 10) : 0; + end = matches[2] ? parseInt(matches[2], 10) : TOTAL_LEN - 1; + } + + if (end == undefined || end >= TOTAL_LEN) { + response.setStatusLine( + request.httpVersion, + 416, + "Requested Range Not Satisfiable" + ); + response.setHeader("Content-Range", `*/${TOTAL_LEN}`, false); + response.finish(); + return; + } + + response.setStatusLine(request.httpVersion, 206, "Partial Content"); + response.setHeader("Content-Range", `${start}-${end}/${TOTAL_LEN}`, false); + response.write(TEST_DATA.slice(start, end + 1)); + } else if (request.queryString.includes("stream")) { + response.processAsync(); + response.setHeader("Content-Length", "10000", false); + response.write("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"); + setInterval(() => { + response.write("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"); + }, 50); + } else { + response.processAsync(); + response.setHeader("Content-Length", `${TOTAL_LEN}`, false); + response.write(TEST_DATA.slice(0, PARTIAL_LEN)); + } + + registerCleanupFunction(() => { + try { + response.finish(); + } catch (e) { + // This will throw, but we don't care at this point. + } + }); +} + +server.registerPrefixHandler("/interruptible/", handleRequest); + +let interruptibleCount = 0; +function getInterruptibleUrl(filename = "interruptible.html") { + let n = interruptibleCount++; + return `${ROOT}/interruptible/${filename}?count=${n}`; +} + +function backgroundScript() { + let events = new Set(); + let eventWaiter = null; + + browser.downloads.onCreated.addListener(data => { + events.add({ type: "onCreated", data }); + if (eventWaiter) { + eventWaiter(); + } + }); + + browser.downloads.onChanged.addListener(data => { + events.add({ type: "onChanged", data }); + if (eventWaiter) { + eventWaiter(); + } + }); + + browser.downloads.onErased.addListener(data => { + events.add({ type: "onErased", data }); + if (eventWaiter) { + eventWaiter(); + } + }); + + // Returns a promise that will resolve when the given list of expected + // events have all been seen. By default, succeeds only if the exact list + // of expected events is seen in the given order. options.exact can be + // set to false to allow other events and options.inorder can be set to + // false to allow the events to arrive in any order. + function waitForEvents(expected, options = {}) { + function compare(a, b) { + if (typeof b == "object" && b != null) { + if (typeof a != "object") { + return false; + } + return Object.keys(b).every(fld => compare(a[fld], b[fld])); + } + return a == b; + } + + const exact = "exact" in options ? options.exact : true; + const inorder = "inorder" in options ? options.inorder : true; + return new Promise((resolve, reject) => { + function check() { + function fail(msg) { + browser.test.fail(msg); + reject(new Error(msg)); + } + if (events.size < expected.length) { + return; + } + if (exact && expected.length < events.size) { + fail( + `Got ${events.size} events but only expected ${expected.length}` + ); + return; + } + + let remaining = new Set(events); + if (inorder) { + for (let event of events) { + if (compare(event, expected[0])) { + expected.shift(); + remaining.delete(event); + } + } + } else { + expected = expected.filter(val => { + for (let remainingEvent of remaining) { + if (compare(remainingEvent, val)) { + remaining.delete(remainingEvent); + return false; + } + } + return true; + }); + } + + // Events that did occur have been removed from expected so if + // expected is empty, we're done. If we didn't see all the + // expected events and we're not looking for an exact match, + // then we just may not have seen the event yet, so return without + // failing and check() will be called again when a new event arrives. + if (!expected.length) { + events = remaining; + eventWaiter = null; + resolve(); + } else if (exact) { + fail( + `Mismatched event: expecting ${JSON.stringify( + expected[0] + )} but got ${JSON.stringify(Array.from(remaining)[0])}` + ); + } + } + eventWaiter = check; + check(); + }); + } + + browser.test.onMessage.addListener(async (msg, ...args) => { + let match = msg.match(/(\w+).request$/); + if (!match) { + return; + } + + let what = match[1]; + if (what == "waitForEvents") { + try { + await waitForEvents(...args); + browser.test.sendMessage("waitForEvents.done", { status: "success" }); + } catch (error) { + browser.test.sendMessage("waitForEvents.done", { + status: "error", + errmsg: error.message, + }); + } + } else if (what == "clearEvents") { + events = new Set(); + browser.test.sendMessage("clearEvents.done", { status: "success" }); + } else { + try { + let result = await browser.downloads[what](...args); + browser.test.sendMessage(`${what}.done`, { status: "success", result }); + } catch (error) { + browser.test.sendMessage(`${what}.done`, { + status: "error", + errmsg: error.message, + }); + } + } + }); + + browser.test.sendMessage("ready"); +} + +let downloadDir; +let extension; + +async function waitForCreatedPartFile(baseFilename = "interruptible.html") { + const partFilePath = PathUtils.join(downloadDir.path, `${baseFilename}.part`); + + info(`Wait for ${partFilePath} to be created`); + let lastError; + await TestUtils.waitForCondition( + () => + IOUtils.exists(partFilePath).catch(err => { + lastError = err; + return false; + }), + `Wait for the ${partFilePath} to exists before pausing the download` + ).catch(err => { + if (lastError) { + throw lastError; + } + throw err; + }); +} + +async function clearDownloads() { + let list = await Downloads.getList(Downloads.ALL); + let downloads = await list.getAll(); + + await Promise.all( + downloads.map(async download => { + await download.finalize(true); + list.remove(download); + }) + ); + + return downloads; +} + +function runInExtension(what, ...args) { + extension.sendMessage(`${what}.request`, ...args); + return extension.awaitMessage(`${what}.done`); +} + +// This is pretty simplistic, it looks for a progress update for a +// download of the given url in which the total bytes are exactly equal +// to the given value. Unless you know exactly how data will arrive from +// the server (eg see interruptible.sjs), it probably isn't very useful. +async function waitForProgress(url, testFn) { + let list = await Downloads.getList(Downloads.ALL); + + return new Promise(resolve => { + const view = { + onDownloadChanged(download) { + if (download.source.url == url && testFn(download.currentBytes)) { + list.removeView(view); + resolve(download.currentBytes); + } + }, + }; + list.addView(view); + }); +} + +add_setup(async () => { + const nsIFile = Ci.nsIFile; + downloadDir = FileUtils.getDir("TmpD", ["downloads"]); + downloadDir.createUnique(nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY); + info(`downloadDir ${downloadDir.path}`); + + Services.prefs.setIntPref("browser.download.folderList", 2); + Services.prefs.setComplexValue("browser.download.dir", nsIFile, downloadDir); + + registerCleanupFunction(async () => { + Services.prefs.clearUserPref("browser.download.folderList"); + Services.prefs.clearUserPref("browser.download.dir"); + await clearDownloads(); + downloadDir.remove(true); + }); + + await clearDownloads().then(downloads => { + info(`removed ${downloads.length} pre-existing downloads from history`); + }); + + extension = ExtensionTestUtils.loadExtension({ + background: backgroundScript, + manifest: { + permissions: ["downloads"], + }, + }); + + await extension.startup(); + await extension.awaitMessage("ready"); + + registerCleanupFunction(async () => { + await extension.unload(); + }); +}); + +add_task(async function test_events() { + let msg = await runInExtension("download", { url: TXT_URL }); + equal(msg.status, "success", "download() succeeded"); + const id = msg.result; + + msg = await runInExtension("waitForEvents", [ + { type: "onCreated", data: { id, url: TXT_URL } }, + { + type: "onChanged", + data: { + id, + state: { + previous: "in_progress", + current: "complete", + }, + }, + }, + ]); + equal(msg.status, "success", "got onCreated and onChanged events"); +}); + +add_task(async function test_cancel() { + let url = getInterruptibleUrl(); + info(url); + let msg = await runInExtension("download", { url }); + equal(msg.status, "success", "download() succeeded"); + const id = msg.result; + + let progressPromise = waitForProgress(url, bytes => bytes == INT_PARTIAL_LEN); + + msg = await runInExtension("waitForEvents", [ + { type: "onCreated", data: { id } }, + ]); + equal(msg.status, "success", "got created and changed events"); + + await progressPromise; + info(`download reached ${INT_PARTIAL_LEN} bytes`); + + msg = await runInExtension("cancel", id); + equal(msg.status, "success", "cancel() succeeded"); + + // TODO bug 1256243: This sequence of events is bogus + msg = await runInExtension("waitForEvents", [ + { + type: "onChanged", + data: { + state: { + previous: "in_progress", + current: "interrupted", + }, + paused: { + previous: false, + current: true, + }, + }, + }, + { + type: "onChanged", + data: { + id, + error: { + previous: null, + current: "USER_CANCELED", + }, + }, + }, + { + type: "onChanged", + data: { + id, + paused: { + previous: true, + current: false, + }, + }, + }, + ]); + equal( + msg.status, + "success", + "got onChanged events corresponding to cancel()" + ); + + msg = await runInExtension("search", { error: "USER_CANCELED" }); + equal(msg.status, "success", "search() succeeded"); + equal(msg.result.length, 1, "search() found 1 download"); + equal(msg.result[0].id, id, "download.id is correct"); + equal(msg.result[0].state, "interrupted", "download.state is correct"); + equal(msg.result[0].paused, false, "download.paused is correct"); + equal( + msg.result[0].estimatedEndTime, + null, + "download.estimatedEndTime is correct" + ); + equal(msg.result[0].canResume, false, "download.canResume is correct"); + equal(msg.result[0].error, "USER_CANCELED", "download.error is correct"); + equal( + msg.result[0].totalBytes, + INT_TOTAL_LEN, + "download.totalBytes is correct" + ); + equal(msg.result[0].exists, false, "download.exists is correct"); + + msg = await runInExtension("pause", id); + equal(msg.status, "error", "cannot pause a canceled download"); + + msg = await runInExtension("resume", id); + equal(msg.status, "error", "cannot resume a canceled download"); +}); + +add_task(async function test_pauseresume() { + const filename = "pauseresume.html"; + let url = getInterruptibleUrl(filename); + let msg = await runInExtension("download", { url }); + equal(msg.status, "success", "download() succeeded"); + const id = msg.result; + + let progressPromise = waitForProgress(url, bytes => bytes == INT_PARTIAL_LEN); + + msg = await runInExtension("waitForEvents", [ + { type: "onCreated", data: { id } }, + ]); + equal(msg.status, "success", "got created and changed events"); + + await progressPromise; + info(`download reached ${INT_PARTIAL_LEN} bytes`); + + // Prevent intermittent timeouts due to the part file not yet created + // (e.g. see Bug 1573360). + await waitForCreatedPartFile(filename); + + info("Pause the download item"); + msg = await runInExtension("pause", id); + equal(msg.status, "success", "pause() succeeded"); + + msg = await runInExtension("waitForEvents", [ + { + type: "onChanged", + data: { + id, + state: { + previous: "in_progress", + current: "interrupted", + }, + paused: { + previous: false, + current: true, + }, + canResume: { + previous: false, + current: true, + }, + }, + }, + { + type: "onChanged", + data: { + id, + error: { + previous: null, + current: "USER_CANCELED", + }, + }, + }, + ]); + equal(msg.status, "success", "got onChanged event corresponding to pause"); + + msg = await runInExtension("search", { paused: true }); + equal(msg.status, "success", "search() succeeded"); + equal(msg.result.length, 1, "search() found 1 download"); + equal(msg.result[0].id, id, "download.id is correct"); + equal(msg.result[0].state, "interrupted", "download.state is correct"); + equal(msg.result[0].paused, true, "download.paused is correct"); + equal( + msg.result[0].estimatedEndTime, + null, + "download.estimatedEndTime is correct" + ); + equal(msg.result[0].canResume, true, "download.canResume is correct"); + equal(msg.result[0].error, "USER_CANCELED", "download.error is correct"); + equal( + msg.result[0].bytesReceived, + INT_PARTIAL_LEN, + "download.bytesReceived is correct" + ); + equal( + msg.result[0].totalBytes, + INT_TOTAL_LEN, + "download.totalBytes is correct" + ); + equal(msg.result[0].exists, false, "download.exists is correct"); + + msg = await runInExtension("search", { error: "USER_CANCELED" }); + equal(msg.status, "success", "search() succeeded"); + let found = msg.result.filter(item => item.id == id); + equal(found.length, 1, "search() by error found the paused download"); + + msg = await runInExtension("pause", id); + equal(msg.status, "error", "cannot pause an already paused download"); + + msg = await runInExtension("resume", id); + equal(msg.status, "success", "resume() succeeded"); + + msg = await runInExtension("waitForEvents", [ + { + type: "onChanged", + data: { + id, + state: { + previous: "interrupted", + current: "in_progress", + }, + paused: { + previous: true, + current: false, + }, + canResume: { + previous: true, + current: false, + }, + error: { + previous: "USER_CANCELED", + current: null, + }, + }, + }, + { + type: "onChanged", + data: { + id, + state: { + previous: "in_progress", + current: "complete", + }, + }, + }, + ]); + equal(msg.status, "success", "got onChanged events for resume and complete"); + + msg = await runInExtension("search", { id }); + equal(msg.status, "success", "search() succeeded"); + equal(msg.result.length, 1, "search() found 1 download"); + equal(msg.result[0].state, "complete", "download.state is correct"); + equal(msg.result[0].paused, false, "download.paused is correct"); + equal( + msg.result[0].estimatedEndTime, + null, + "download.estimatedEndTime is correct" + ); + equal(msg.result[0].canResume, false, "download.canResume is correct"); + equal(msg.result[0].error, null, "download.error is correct"); + equal( + msg.result[0].bytesReceived, + INT_TOTAL_LEN, + "download.bytesReceived is correct" + ); + equal( + msg.result[0].totalBytes, + INT_TOTAL_LEN, + "download.totalBytes is correct" + ); + equal(msg.result[0].exists, true, "download.exists is correct"); + + msg = await runInExtension("pause", id); + equal(msg.status, "error", "cannot pause a completed download"); + + msg = await runInExtension("resume", id); + equal(msg.status, "error", "cannot resume a completed download"); +}); + +add_task(async function test_pausecancel() { + let url = getInterruptibleUrl(); + let msg = await runInExtension("download", { url }); + equal(msg.status, "success", "download() succeeded"); + const id = msg.result; + + let progressPromise = waitForProgress(url, bytes => bytes == INT_PARTIAL_LEN); + + msg = await runInExtension("waitForEvents", [ + { type: "onCreated", data: { id } }, + ]); + equal(msg.status, "success", "got created and changed events"); + + await progressPromise; + info(`download reached ${INT_PARTIAL_LEN} bytes`); + + msg = await runInExtension("pause", id); + equal(msg.status, "success", "pause() succeeded"); + + msg = await runInExtension("waitForEvents", [ + { + type: "onChanged", + data: { + id, + state: { + previous: "in_progress", + current: "interrupted", + }, + paused: { + previous: false, + current: true, + }, + canResume: { + previous: false, + current: true, + }, + }, + }, + { + type: "onChanged", + data: { + id, + error: { + previous: null, + current: "USER_CANCELED", + }, + }, + }, + ]); + equal(msg.status, "success", "got onChanged event corresponding to pause"); + + msg = await runInExtension("search", { paused: true }); + equal(msg.status, "success", "search() succeeded"); + equal(msg.result.length, 1, "search() found 1 download"); + equal(msg.result[0].id, id, "download.id is correct"); + equal(msg.result[0].state, "interrupted", "download.state is correct"); + equal(msg.result[0].paused, true, "download.paused is correct"); + equal( + msg.result[0].estimatedEndTime, + null, + "download.estimatedEndTime is correct" + ); + equal(msg.result[0].canResume, true, "download.canResume is correct"); + equal(msg.result[0].error, "USER_CANCELED", "download.error is correct"); + equal( + msg.result[0].bytesReceived, + INT_PARTIAL_LEN, + "download.bytesReceived is correct" + ); + equal( + msg.result[0].totalBytes, + INT_TOTAL_LEN, + "download.totalBytes is correct" + ); + equal(msg.result[0].exists, false, "download.exists is correct"); + + msg = await runInExtension("search", { error: "USER_CANCELED" }); + equal(msg.status, "success", "search() succeeded"); + let found = msg.result.filter(item => item.id == id); + equal(found.length, 1, "search() by error found the paused download"); + + msg = await runInExtension("cancel", id); + equal(msg.status, "success", "cancel() succeeded"); + + msg = await runInExtension("waitForEvents", [ + { + type: "onChanged", + data: { + id, + paused: { + previous: true, + current: false, + }, + canResume: { + previous: true, + current: false, + }, + }, + }, + ]); + equal(msg.status, "success", "got onChanged event for cancel"); + + msg = await runInExtension("search", { id }); + equal(msg.status, "success", "search() succeeded"); + equal(msg.result.length, 1, "search() found 1 download"); + equal(msg.result[0].state, "interrupted", "download.state is correct"); + equal(msg.result[0].paused, false, "download.paused is correct"); + equal( + msg.result[0].estimatedEndTime, + null, + "download.estimatedEndTime is correct" + ); + equal(msg.result[0].canResume, false, "download.canResume is correct"); + equal(msg.result[0].error, "USER_CANCELED", "download.error is correct"); + equal( + msg.result[0].totalBytes, + INT_TOTAL_LEN, + "download.totalBytes is correct" + ); + equal(msg.result[0].exists, false, "download.exists is correct"); +}); + +add_task(async function test_pause_resume_cancel_badargs() { + let BAD_ID = 1000; + + let msg = await runInExtension("pause", BAD_ID); + equal(msg.status, "error", "pause() failed with a bad download id"); + ok(/Invalid download id/.test(msg.errmsg), "error message is descriptive"); + + msg = await runInExtension("resume", BAD_ID); + equal(msg.status, "error", "resume() failed with a bad download id"); + ok(/Invalid download id/.test(msg.errmsg), "error message is descriptive"); + + msg = await runInExtension("cancel", BAD_ID); + equal(msg.status, "error", "cancel() failed with a bad download id"); + ok(/Invalid download id/.test(msg.errmsg), "error message is descriptive"); +}); + +add_task(async function test_file_removal() { + let msg = await runInExtension("download", { url: TXT_URL }); + equal(msg.status, "success", "download() succeeded"); + const id = msg.result; + + msg = await runInExtension("waitForEvents", [ + { type: "onCreated", data: { id, url: TXT_URL } }, + { + type: "onChanged", + data: { + id, + state: { + previous: "in_progress", + current: "complete", + }, + }, + }, + ]); + + equal(msg.status, "success", "got onCreated and onChanged events"); + + msg = await runInExtension("removeFile", id); + equal(msg.status, "success", "removeFile() succeeded"); + + msg = await runInExtension("removeFile", id); + equal( + msg.status, + "error", + "removeFile() fails since the file was already removed." + ); + equal( + msg.errmsg, + `Could not remove download id ${id} because the file doesn't exist`, + "removeFile() failed on removed file." + ); + + msg = await runInExtension("removeFile", 1000); + equal( + msg.errmsg, + "Invalid download id 1000", + "removeFile() failed due to non-existent id" + ); +}); + +add_task(async function test_file_removeFile_permission_failure() { + const inputDirname = "subdir_for_download"; + const inputFilename = "downloaded_filename.txt"; + const expectedDir = PathUtils.join(downloadDir.path, inputDirname); + const expectedPath = PathUtils.join(expectedDir, inputFilename); + + let msg = await runInExtension("download", { + url: TXT_URL, + filename: `${inputDirname}/${inputFilename}`, + }); + equal(msg.status, "success", "download() succeeded"); + const id = msg.result; + + msg = await runInExtension("waitForEvents", [ + { type: "onCreated", data: { id, url: TXT_URL } }, + { + type: "onChanged", + data: { + id, + state: { + previous: "in_progress", + current: "complete", + }, + }, + }, + ]); + + equal(msg.status, "success", "got onCreated and onChanged events"); + + msg = await runInExtension("search", { id }); + equal(msg.status, "success", "search() succeeded"); + equal(expectedPath, msg.result[0]?.filename, "Got expected filename"); + + async function withUndeletableFileUnix(testRemoveFile) { + try { + // Temporarily make directory unreadable/inaccessible. + await IOUtils.setPermissions(expectedDir, 0); + // Remove should fail with Unix error 13 (EACCES). + await testRemoveFile(); + } finally { + await IOUtils.setPermissions(expectedDir, 0o777); + } + } + async function withUndeletableFileWin(testRemoveFile) { + // On Windows, a directory marked as read-only does not prevent the deletion + // of its content. So we need an alternative approach here. + let stream = Cc["@mozilla.org/network/file-input-stream;1"].createInstance( + Ci.nsIFileInputStream + ); + try { + // Open a file handle. The file cannot be deleted until it is closed. + stream.init(await IOUtils.getFile(expectedPath), -1, 0, 0); + // Remove should fail with Win error 32 (ERROR_SHARING_VIOLATION). + await testRemoveFile(); + } finally { + stream.close(); + } + } + + const withUndeletableFile = + AppConstants.platform === "win" + ? withUndeletableFileWin + : withUndeletableFileUnix; + + let consoleOutput; + await withUndeletableFile(async () => { + consoleOutput = await promiseConsoleOutput(async () => { + msg = await runInExtension("removeFile", id); + }); + }); + + equal(msg.status, "error", "removeFile() fails due to missing dir perms"); + // Verify that an unexpected error is redacted, with a useful error message + // logged to the console. + // Note: if we ever decide to make the error for permission failures more + // useful, try to add a new test case for unexpected errors, even if + // completely artificial such as mocking + breaking an internal API. + equal(msg.errmsg, "An unexpected error occurred", "Error message redacted"); + + AddonTestUtils.checkMessages(consoleOutput.messages, { + expected: [{ message: /NotAllowedError/ }], + }); + + ok(await IOUtils.exists(expectedPath), "File exists before removeFile()"); + + msg = await runInExtension("removeFile", id); + equal(msg.status, "success", "removeFile() succeeded"); + + equal(await IOUtils.exists(expectedPath), false, "File was really removed"); + + // As a bonus: check that the re-created file can be deleted without issues. + await IOUtils.writeUTF8(expectedPath, "content here"); + + msg = await runInExtension("removeFile", id); + equal(msg.status, "success", "removeFile() succeeded after recreation"); + + equal(await IOUtils.exists(expectedPath), false, "File was removed again"); +}); + +add_task(async function test_removal_of_incomplete_download() { + const filename = "remove-incomplete.html"; + let url = getInterruptibleUrl(filename); + let msg = await runInExtension("download", { url }); + equal(msg.status, "success", "download() succeeded"); + const id = msg.result; + + let progressPromise = waitForProgress(url, bytes => bytes == INT_PARTIAL_LEN); + + msg = await runInExtension("waitForEvents", [ + { type: "onCreated", data: { id } }, + ]); + equal(msg.status, "success", "got created and changed events"); + + await progressPromise; + info(`download reached ${INT_PARTIAL_LEN} bytes`); + + // Prevent intermittent timeouts due to the part file not yet created + // (e.g. see Bug 1573360). + await waitForCreatedPartFile(filename); + + msg = await runInExtension("pause", id); + equal(msg.status, "success", "pause() succeeded"); + + msg = await runInExtension("waitForEvents", [ + { + type: "onChanged", + data: { + id, + state: { + previous: "in_progress", + current: "interrupted", + }, + paused: { + previous: false, + current: true, + }, + canResume: { + previous: false, + current: true, + }, + }, + }, + { + type: "onChanged", + data: { + id, + error: { + previous: null, + current: "USER_CANCELED", + }, + }, + }, + ]); + equal(msg.status, "success", "got onChanged event corresponding to pause"); + + msg = await runInExtension("removeFile", id); + equal(msg.status, "error", "removeFile() on paused download failed"); + + ok( + /Cannot remove incomplete download/.test(msg.errmsg), + "removeFile() failed due to download being incomplete" + ); + + msg = await runInExtension("resume", id); + equal(msg.status, "success", "resume() succeeded"); + + msg = await runInExtension("waitForEvents", [ + { + type: "onChanged", + data: { + id, + state: { + previous: "interrupted", + current: "in_progress", + }, + paused: { + previous: true, + current: false, + }, + canResume: { + previous: true, + current: false, + }, + error: { + previous: "USER_CANCELED", + current: null, + }, + }, + }, + { + type: "onChanged", + data: { + id, + state: { + previous: "in_progress", + current: "complete", + }, + }, + }, + ]); + equal(msg.status, "success", "got onChanged events for resume and complete"); + + msg = await runInExtension("removeFile", id); + equal( + msg.status, + "success", + "removeFile() succeeded following completion of resumed download." + ); +}); + +// Test erase(). We don't do elaborate testing of the query handling +// since it uses the exact same engine as search() which is tested +// more thoroughly in test_chrome_ext_downloads_search.html +add_task(async function test_erase() { + await clearDownloads(); + + await runInExtension("clearEvents"); + + async function download() { + let msg = await runInExtension("download", { url: TXT_URL }); + equal(msg.status, "success", "download succeeded"); + let id = msg.result; + + msg = await runInExtension( + "waitForEvents", + [ + { + type: "onChanged", + data: { id, state: { current: "complete" } }, + }, + ], + { exact: false } + ); + equal(msg.status, "success", "download finished"); + + return id; + } + + let ids = {}; + ids.dl1 = await download(); + ids.dl2 = await download(); + ids.dl3 = await download(); + + let msg = await runInExtension("search", {}); + equal(msg.status, "success", "search succeeded"); + equal(msg.result.length, 3, "search found 3 downloads"); + + msg = await runInExtension("clearEvents"); + + msg = await runInExtension("erase", { id: ids.dl1 }); + equal(msg.status, "success", "erase by id succeeded"); + + msg = await runInExtension("waitForEvents", [ + { type: "onErased", data: ids.dl1 }, + ]); + equal(msg.status, "success", "received onErased event"); + + msg = await runInExtension("search", {}); + equal(msg.status, "success", "search succeeded"); + equal(msg.result.length, 2, "search found 2 downloads"); + + msg = await runInExtension("erase", {}); + equal(msg.status, "success", "erase everything succeeded"); + + msg = await runInExtension( + "waitForEvents", + [ + { type: "onErased", data: ids.dl2 }, + { type: "onErased", data: ids.dl3 }, + ], + { inorder: false } + ); + equal(msg.status, "success", "received 2 onErased events"); + + msg = await runInExtension("search", {}); + equal(msg.status, "success", "search succeeded"); + equal(msg.result.length, 0, "search found 0 downloads"); +}); + +function loadImage(img, data) { + return new Promise(resolve => { + img.src = data; + img.onload = resolve; + }); +} + +add_task(async function test_getFileIcon() { + let webNav = Services.appShell.createWindowlessBrowser(false); + let docShell = webNav.docShell; + + let system = Services.scriptSecurityManager.getSystemPrincipal(); + docShell.createAboutBlankDocumentViewer(system, system); + + let img = webNav.document.createElement("img"); + + let msg = await runInExtension("download", { url: TXT_URL }); + equal(msg.status, "success", "download() succeeded"); + const id = msg.result; + + msg = await runInExtension("getFileIcon", id); + equal(msg.status, "success", "getFileIcon() succeeded"); + await loadImage(img, msg.result); + equal(img.height, 32, "returns an icon with the right height"); + equal(img.width, 32, "returns an icon with the right width"); + + msg = await runInExtension("waitForEvents", [ + { type: "onCreated", data: { id, url: TXT_URL } }, + { type: "onChanged" }, + ]); + equal(msg.status, "success", "got events"); + + msg = await runInExtension("getFileIcon", id); + equal(msg.status, "success", "getFileIcon() succeeded"); + await loadImage(img, msg.result); + equal(img.height, 32, "returns an icon with the right height after download"); + equal(img.width, 32, "returns an icon with the right width after download"); + + msg = await runInExtension("getFileIcon", id + 100); + equal(msg.status, "error", "getFileIcon() failed"); + ok(msg.errmsg.includes("Invalid download id"), "download id is invalid"); + + msg = await runInExtension("getFileIcon", id, { size: 127 }); + equal(msg.status, "success", "getFileIcon() succeeded"); + await loadImage(img, msg.result); + equal(img.height, 127, "returns an icon with the right custom height"); + equal(img.width, 127, "returns an icon with the right custom width"); + + msg = await runInExtension("getFileIcon", id, { size: 1 }); + equal(msg.status, "success", "getFileIcon() succeeded"); + await loadImage(img, msg.result); + equal(img.height, 1, "returns an icon with the right custom height"); + equal(img.width, 1, "returns an icon with the right custom width"); + + msg = await runInExtension("getFileIcon", id, { size: "foo" }); + equal(msg.status, "error", "getFileIcon() fails"); + ok(msg.errmsg.includes("Error processing size"), "size is not a number"); + + msg = await runInExtension("getFileIcon", id, { size: 0 }); + equal(msg.status, "error", "getFileIcon() fails"); + ok(msg.errmsg.includes("Error processing size"), "size is too small"); + + msg = await runInExtension("getFileIcon", id, { size: 128 }); + equal(msg.status, "error", "getFileIcon() fails"); + ok(msg.errmsg.includes("Error processing size"), "size is too big"); + + webNav.close(); +}); + +add_task(async function test_estimatedendtime() { + // Note we are not testing the actual value calculation of estimatedEndTime, + // only whether it is null/non-null at the appropriate times. + + let url = `${getInterruptibleUrl()}&stream=1`; + let msg = await runInExtension("download", { url }); + equal(msg.status, "success", "download() succeeded"); + const id = msg.result; + + let previousBytes = await waitForProgress(url, bytes => bytes > 0); + await waitForProgress(url, bytes => bytes > previousBytes); + + msg = await runInExtension("search", { id }); + equal(msg.status, "success", "search() succeeded"); + equal(msg.result.length, 1, "search() found 1 download"); + ok(msg.result[0].estimatedEndTime, "download.estimatedEndTime is correct"); + Assert.greater( + msg.result[0].bytesReceived, + 0, + "download.bytesReceived is correct" + ); + + msg = await runInExtension("cancel", id); + + msg = await runInExtension("search", { id }); + equal(msg.status, "success", "search() succeeded"); + equal(msg.result.length, 1, "search() found 1 download"); + ok(!msg.result[0].estimatedEndTime, "download.estimatedEndTime is correct"); +}); + +add_task(async function test_byExtension() { + let msg = await runInExtension("download", { url: TXT_URL }); + equal(msg.status, "success", "download() succeeded"); + const id = msg.result; + msg = await runInExtension("search", { id }); + + equal(msg.result.length, 1, "search() found 1 download"); + equal( + msg.result[0].byExtensionName, + "Generated extension", + "download.byExtensionName is correct" + ); + equal( + msg.result[0].byExtensionId, + extension.id, + "download.byExtensionId is correct" + ); +}); |