/* -*- 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" ); });