summaryrefslogtreecommitdiffstats
path: root/browser/components/downloads/test/browser/head.js
diff options
context:
space:
mode:
Diffstat (limited to 'browser/components/downloads/test/browser/head.js')
-rw-r--r--browser/components/downloads/test/browser/head.js448
1 files changed, 448 insertions, 0 deletions
diff --git a/browser/components/downloads/test/browser/head.js b/browser/components/downloads/test/browser/head.js
new file mode 100644
index 0000000000..49b4d8d04c
--- /dev/null
+++ b/browser/components/downloads/test/browser/head.js
@@ -0,0 +1,448 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Provides infrastructure for automated download components tests.
+ */
+
+// Globals
+
+ChromeUtils.defineESModuleGetters(this, {
+ Downloads: "resource://gre/modules/Downloads.sys.mjs",
+ DownloadsCommon: "resource:///modules/DownloadsCommon.sys.mjs",
+ FileUtils: "resource://gre/modules/FileUtils.sys.mjs",
+ PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs",
+});
+ChromeUtils.defineModuleGetter(
+ this,
+ "HttpServer",
+ "resource://testing-common/httpd.js"
+);
+
+let gTestTargetFile = new FileUtils.File(
+ PathUtils.join(
+ Services.dirsvc.get("TmpD", Ci.nsIFile).path,
+ "dm-ui-test.file"
+ )
+);
+
+gTestTargetFile.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, FileUtils.PERMS_FILE);
+Services.prefs.setIntPref("security.dialog_enable_delay", 0);
+
+// The file may have been already deleted when removing a paused download.
+// Also clear security.dialog_enable_delay pref.
+registerCleanupFunction(async () => {
+ Services.prefs.clearUserPref("security.dialog_enable_delay");
+
+ if (await IOUtils.exists(gTestTargetFile.path)) {
+ info("removing " + gTestTargetFile.path);
+ if (Services.appinfo.OS === "WINNT") {
+ // We need to make the file writable to delete it on Windows.
+ await IOUtils.setPermissions(gTestTargetFile.path, 0o600);
+ }
+ await IOUtils.remove(gTestTargetFile.path);
+ }
+});
+
+const DATA_PDF = atob(
+ "JVBERi0xLjANCjEgMCBvYmo8PC9UeXBlL0NhdGFsb2cvUGFnZXMgMiAwIFI+PmVuZG9iaiAyIDAgb2JqPDwvVHlwZS9QYWdlcy9LaWRzWzMgMCBSXS9Db3VudCAxPj5lbmRvYmogMyAwIG9iajw8L1R5cGUvUGFnZS9NZWRpYUJveFswIDAgMyAzXT4+ZW5kb2JqDQp4cmVmDQowIDQNCjAwMDAwMDAwMDAgNjU1MzUgZg0KMDAwMDAwMDAxMCAwMDAwMCBuDQowMDAwMDAwMDUzIDAwMDAwIG4NCjAwMDAwMDAxMDIgMDAwMDAgbg0KdHJhaWxlcjw8L1NpemUgNC9Sb290IDEgMCBSPj4NCnN0YXJ0eHJlZg0KMTQ5DQolRU9G"
+);
+
+const TEST_DATA_SHORT = "This test string is downloaded.";
+
+/**
+ * This is an internal reference that should not be used directly by tests.
+ */
+var _gDeferResponses = PromiseUtils.defer();
+
+/**
+ * Ensures that all the interruptible requests started after this function is
+ * called won't complete until the continueResponses function is called.
+ *
+ * Normally, the internal HTTP server returns all the available data as soon as
+ * a request is received. In order for some requests to be served one part at a
+ * time, special interruptible handlers are registered on the HTTP server. This
+ * allows testing events or actions that need to happen in the middle of a
+ * download.
+ *
+ * For example, the handler accessible at the httpUri("interruptible.txt")
+ * address returns the TEST_DATA_SHORT text, then it may block until the
+ * continueResponses method is called. At this point, the handler sends the
+ * TEST_DATA_SHORT text again to complete the response.
+ *
+ * If an interruptible request is started before the function is called, it may
+ * or may not be blocked depending on the actual sequence of events.
+ */
+function mustInterruptResponses() {
+ // If there are pending blocked requests, allow them to complete. This is
+ // done to prevent requests from being blocked forever, but should not affect
+ // the test logic, since previously started requests should not be monitored
+ // on the client side anymore.
+ _gDeferResponses.resolve();
+
+ info("Interruptible responses will be blocked midway.");
+ _gDeferResponses = PromiseUtils.defer();
+}
+
+/**
+ * Allows all the current and future interruptible requests to complete.
+ */
+function continueResponses() {
+ info("Interruptible responses are now allowed to continue.");
+ _gDeferResponses.resolve();
+}
+
+/**
+ * Creates a download, which could be interrupted in the middle of it's progress.
+ */
+function promiseInterruptibleDownload(extension = ".txt") {
+ let interruptibleFile = FileUtils.getFile("TmpD", [
+ `interruptible${extension}`,
+ ]);
+ interruptibleFile.createUnique(
+ Ci.nsIFile.NORMAL_FILE_TYPE,
+ FileUtils.PERMS_FILE
+ );
+
+ registerCleanupFunction(async () => {
+ if (await IOUtils.exists(interruptibleFile.path)) {
+ if (Services.appinfo.OS === "WINNT") {
+ // We need to make the file writable to delete it on Windows.
+ await IOUtils.setPermissions(interruptibleFile.path, 0o600);
+ }
+ await IOUtils.remove(interruptibleFile.path);
+ }
+ });
+
+ return Downloads.createDownload({
+ source: httpUrl("interruptible.txt"),
+ target: { path: interruptibleFile.path },
+ });
+}
+
+// Asynchronous support subroutines
+
+async function createDownloadedFile(pathname, contents) {
+ let file = new FileUtils.File(pathname);
+ if (file.exists()) {
+ info(`File at ${pathname} already exists`);
+ }
+ // No post-test cleanup necessary; tmp downloads directory is already removed after each test
+ await IOUtils.writeUTF8(pathname, contents);
+ ok(file.exists(), `Created ${pathname}`);
+ return file;
+}
+
+async function openContextMenu(itemElement, win = window) {
+ let popupShownPromise = BrowserTestUtils.waitForEvent(
+ itemElement.ownerDocument,
+ "popupshown"
+ );
+ EventUtils.synthesizeMouseAtCenter(
+ itemElement,
+ {
+ type: "contextmenu",
+ button: 2,
+ },
+ win
+ );
+ let { target } = await popupShownPromise;
+ return target;
+}
+
+function promiseFocus() {
+ return new Promise(resolve => {
+ waitForFocus(resolve);
+ });
+}
+
+function promisePanelOpened() {
+ if (DownloadsPanel.panel && DownloadsPanel.panel.state == "open") {
+ return Promise.resolve();
+ }
+
+ return new Promise(resolve => {
+ // Hook to wait until the panel is shown.
+ let originalOnPopupShown = DownloadsPanel.onPopupShown;
+ DownloadsPanel.onPopupShown = function () {
+ DownloadsPanel.onPopupShown = originalOnPopupShown;
+ originalOnPopupShown.apply(this, arguments);
+
+ // Defer to the next tick of the event loop so that we don't continue
+ // processing during the DOM event handler itself.
+ setTimeout(resolve, 0);
+ };
+ });
+}
+
+async function task_resetState() {
+ // Remove all downloads.
+ let publicList = await Downloads.getList(Downloads.PUBLIC);
+ let downloads = await publicList.getAll();
+ for (let download of downloads) {
+ await publicList.remove(download);
+ if (await IOUtils.exists(download.target.path)) {
+ await download.finalize(true);
+ info("removing " + download.target.path);
+ if (Services.appinfo.OS === "WINNT") {
+ // We need to make the file writable to delete it on Windows.
+ await IOUtils.setPermissions(download.target.path, 0o600);
+ }
+ await IOUtils.remove(download.target.path);
+ }
+ }
+
+ DownloadsPanel.hidePanel();
+
+ await promiseFocus();
+}
+
+async function task_addDownloads(aItems) {
+ let startTimeMs = Date.now();
+
+ let publicList = await Downloads.getList(Downloads.PUBLIC);
+ for (let item of aItems) {
+ let source = {
+ url: "http://www.example.com/test-download.txt",
+ ...item.source,
+ };
+ let target =
+ item.target instanceof Ci.nsIFile
+ ? item.target
+ : {
+ path: gTestTargetFile.path,
+ ...item.target,
+ };
+
+ let download = {
+ source,
+ target,
+ succeeded: item.state == DownloadsCommon.DOWNLOAD_FINISHED,
+ canceled:
+ item.state == DownloadsCommon.DOWNLOAD_CANCELED ||
+ item.state == DownloadsCommon.DOWNLOAD_PAUSED,
+ deleted: item.deleted ?? false,
+ error:
+ item.state == DownloadsCommon.DOWNLOAD_FAILED
+ ? new Error("Failed.")
+ : null,
+ hasPartialData: item.state == DownloadsCommon.DOWNLOAD_PAUSED,
+ hasBlockedData: item.hasBlockedData || false,
+ openDownloadsListOnStart: item.openDownloadsListOnStart ?? true,
+ contentType: item.contentType,
+ startTime: new Date(startTimeMs++),
+ };
+ // `"errorObj" in download` must be false when there's no error.
+ if (item.errorObj) {
+ download.errorObj = item.errorObj;
+ }
+ download = await Downloads.createDownload(download);
+ await publicList.add(download);
+ await download.refresh();
+ }
+}
+
+async function task_openPanel() {
+ await promiseFocus();
+
+ let promise = promisePanelOpened();
+ DownloadsPanel.showPanel();
+ await promise;
+}
+
+async function setDownloadDir() {
+ let tmpDir = PathUtils.join(
+ PathUtils.tempDir,
+ "testsavedir" + Math.floor(Math.random() * 2 ** 32)
+ );
+ // Create this dir if it doesn't exist (ignores existing dirs)
+ await IOUtils.makeDirectory(tmpDir);
+ registerCleanupFunction(async function () {
+ try {
+ await IOUtils.remove(tmpDir, { recursive: true });
+ } catch (e) {
+ console.error(e);
+ }
+ });
+ Services.prefs.setIntPref("browser.download.folderList", 2);
+ Services.prefs.setCharPref("browser.download.dir", tmpDir);
+ return tmpDir;
+}
+
+let gHttpServer = null;
+let gShouldServeInterruptibleFileAsDownload = false;
+function startServer() {
+ gHttpServer = new HttpServer();
+ gHttpServer.start(-1);
+ registerCleanupFunction(() => {
+ return new Promise(resolve => {
+ // Ensure all the pending HTTP requests have a chance to finish.
+ continueResponses();
+ // Stop the HTTP server, calling resolve when it's done.
+ gHttpServer.stop(resolve);
+ });
+ });
+
+ gHttpServer.identity.setPrimary(
+ "http",
+ "www.example.com",
+ gHttpServer.identity.primaryPort
+ );
+
+ gHttpServer.registerPathHandler("/file1.txt", (request, response) => {
+ response.setStatusLine(null, 200, "OK");
+ response.write("file1");
+ response.processAsync();
+ response.finish();
+ });
+ gHttpServer.registerPathHandler("/file2.txt", (request, response) => {
+ response.setStatusLine(null, 200, "OK");
+ response.write("file2");
+ response.processAsync();
+ response.finish();
+ });
+ gHttpServer.registerPathHandler("/file3.txt", (request, response) => {
+ response.setStatusLine(null, 200, "OK");
+ response.write("file3");
+ response.processAsync();
+ response.finish();
+ });
+
+ gHttpServer.registerPathHandler(
+ "/interruptible.txt",
+ function (aRequest, aResponse) {
+ info("Interruptible request started.");
+
+ // Process the first part of the response.
+ aResponse.processAsync();
+ aResponse.setHeader("Content-Type", "text/plain", false);
+ if (gShouldServeInterruptibleFileAsDownload) {
+ aResponse.setHeader("Content-Disposition", "attachment");
+ }
+ aResponse.setHeader(
+ "Content-Length",
+ "" + TEST_DATA_SHORT.length * 2,
+ false
+ );
+ aResponse.write(TEST_DATA_SHORT);
+
+ // Wait on the current deferred object, then finish the request.
+ _gDeferResponses.promise
+ .then(function RIH_onSuccess() {
+ aResponse.write(TEST_DATA_SHORT);
+ aResponse.finish();
+ info("Interruptible request finished.");
+ })
+ .catch(console.error);
+ }
+ );
+}
+
+function serveInterruptibleAsDownload() {
+ gShouldServeInterruptibleFileAsDownload = true;
+ registerCleanupFunction(
+ () => (gShouldServeInterruptibleFileAsDownload = false)
+ );
+}
+
+function httpUrl(aFileName) {
+ return (
+ "http://localhost:" + gHttpServer.identity.primaryPort + "/" + aFileName
+ );
+}
+
+function openLibrary(aLeftPaneRoot) {
+ let library = window.openDialog(
+ "chrome://browser/content/places/places.xhtml",
+ "",
+ "chrome,toolbar=yes,dialog=no,resizable",
+ aLeftPaneRoot
+ );
+
+ return new Promise(resolve => {
+ waitForFocus(resolve, library);
+ });
+}
+
+/**
+ * Waits for a download to reach its progress, in case it has not
+ * reached the expected progress already.
+ *
+ * @param aDownload
+ * The Download object to wait upon.
+ *
+ * @return {Promise}
+ * @resolves When the download has reached its progress.
+ * @rejects Never.
+ */
+function promiseDownloadHasProgress(aDownload, progress) {
+ return new Promise(resolve => {
+ // Wait for the download to reach its progress.
+ let onchange = function () {
+ let downloadInProgress =
+ !aDownload.stopped && aDownload.progress == progress;
+ let downloadFinished =
+ progress == 100 &&
+ aDownload.progress == progress &&
+ aDownload.succeeded;
+ if (downloadInProgress || downloadFinished) {
+ info(`Download reached ${progress}%`);
+ aDownload.onchange = null;
+ resolve();
+ }
+ };
+
+ // Register for the notification, but also call the function directly in
+ // case the download already reached the expected progress.
+ aDownload.onchange = onchange;
+ onchange();
+ });
+}
+
+/**
+ * Waits for a given button to become visible.
+ */
+function promiseButtonShown(id) {
+ let dwu = window.windowUtils;
+ return BrowserTestUtils.waitForCondition(() => {
+ let target = document.getElementById(id);
+ let bounds = dwu.getBoundsWithoutFlushing(target);
+ return bounds.width > 0 && bounds.height > 0;
+ }, `Waiting for button ${id} to have non-0 size`);
+}
+
+async function simulateDropAndCheck(win, dropTarget, urls) {
+ let dragData = [[{ type: "text/plain", data: urls.join("\n") }]];
+ let list = await Downloads.getList(Downloads.ALL);
+
+ let added = new Set();
+ let succeeded = new Set();
+ await new Promise(resolve => {
+ let view = {
+ onDownloadAdded(download) {
+ added.add(download.source.url);
+ },
+ onDownloadChanged(download) {
+ if (!added.has(download.source.url)) {
+ return;
+ }
+ if (!download.succeeded) {
+ return;
+ }
+ succeeded.add(download.source.url);
+ if (succeeded.size == urls.length) {
+ list.removeView(view).then(resolve);
+ }
+ },
+ };
+ list.addView(view).then(function () {
+ EventUtils.synthesizeDrop(dropTarget, dropTarget, dragData, "link", win);
+ });
+ });
+
+ for (let url of urls) {
+ ok(added.has(url), url + " is added to download");
+ }
+}