summaryrefslogtreecommitdiffstats
path: root/browser/components/downloads/test
diff options
context:
space:
mode:
Diffstat (limited to 'browser/components/downloads/test')
-rw-r--r--browser/components/downloads/test/browser/blank.JPGbin0 -> 631 bytes
-rw-r--r--browser/components/downloads/test/browser/browser.ini35
-rw-r--r--browser/components/downloads/test/browser/browser_about_downloads.js44
-rw-r--r--browser/components/downloads/test/browser/browser_basic_functionality.js59
-rw-r--r--browser/components/downloads/test/browser/browser_confirm_unblock_download.js94
-rw-r--r--browser/components/downloads/test/browser/browser_download_overwrite.js129
-rw-r--r--browser/components/downloads/test/browser/browser_downloads_autohide.js510
-rw-r--r--browser/components/downloads/test/browser/browser_downloads_panel_block.js181
-rw-r--r--browser/components/downloads/test/browser/browser_downloads_panel_context_menu.js204
-rw-r--r--browser/components/downloads/test/browser/browser_downloads_panel_ctrl_click.js35
-rw-r--r--browser/components/downloads/test/browser/browser_downloads_panel_height.js31
-rw-r--r--browser/components/downloads/test/browser/browser_downloads_pauseResume.js49
-rw-r--r--browser/components/downloads/test/browser/browser_first_download_panel.js65
-rw-r--r--browser/components/downloads/test/browser/browser_go_to_download_page.js93
-rw-r--r--browser/components/downloads/test/browser/browser_iframe_gone_mid_download.js76
-rw-r--r--browser/components/downloads/test/browser/browser_image_mimetype_issues.js133
-rw-r--r--browser/components/downloads/test/browser/browser_indicatorDrop.js38
-rw-r--r--browser/components/downloads/test/browser/browser_libraryDrop.js39
-rw-r--r--browser/components/downloads/test/browser/browser_library_clearall.js121
-rw-r--r--browser/components/downloads/test/browser/browser_library_select_all.js77
-rw-r--r--browser/components/downloads/test/browser/browser_overflow_anchor.js68
-rw-r--r--browser/components/downloads/test/browser/browser_pdfjs_preview.js749
-rw-r--r--browser/components/downloads/test/browser/foo.txt1
-rw-r--r--browser/components/downloads/test/browser/foo.txt^headers^2
-rw-r--r--browser/components/downloads/test/browser/head.js284
-rw-r--r--browser/components/downloads/test/browser/not-really-a-jpeg.jpegbin0 -> 42 bytes
-rw-r--r--browser/components/downloads/test/browser/not-really-a-jpeg.jpeg^headers^2
-rw-r--r--browser/components/downloads/test/unit/head.js89
-rw-r--r--browser/components/downloads/test/unit/test_DownloadsCommon_getMimeInfo.js175
-rw-r--r--browser/components/downloads/test/unit/test_DownloadsCommon_isFileOfType.js153
-rw-r--r--browser/components/downloads/test/unit/test_DownloadsViewableInternally.js252
-rw-r--r--browser/components/downloads/test/unit/xpcshell.ini9
32 files changed, 3797 insertions, 0 deletions
diff --git a/browser/components/downloads/test/browser/blank.JPG b/browser/components/downloads/test/browser/blank.JPG
new file mode 100644
index 0000000000..1cda9a53dc
--- /dev/null
+++ b/browser/components/downloads/test/browser/blank.JPG
Binary files differ
diff --git a/browser/components/downloads/test/browser/browser.ini b/browser/components/downloads/test/browser/browser.ini
new file mode 100644
index 0000000000..c8232aef5f
--- /dev/null
+++ b/browser/components/downloads/test/browser/browser.ini
@@ -0,0 +1,35 @@
+[DEFAULT]
+support-files = head.js
+
+[browser_about_downloads.js]
+[browser_basic_functionality.js]
+[browser_download_overwrite.js]
+support-files =
+ foo.txt
+ foo.txt^headers^
+ !/toolkit/content/tests/browser/common/mockTransfer.js
+[browser_first_download_panel.js]
+skip-if = os == "linux" # Bug 949434
+[browser_image_mimetype_issues.js]
+support-files =
+ not-really-a-jpeg.jpeg
+ not-really-a-jpeg.jpeg^headers^
+ blank.JPG
+[browser_library_select_all.js]
+[browser_overflow_anchor.js]
+skip-if = os == "linux" # Bug 952422
+[browser_confirm_unblock_download.js]
+[browser_iframe_gone_mid_download.js]
+[browser_indicatorDrop.js]
+[browser_libraryDrop.js]
+skip-if = (os == 'win' && os_version == '10.0' && ccov) # Bug 1306510
+[browser_library_clearall.js]
+[browser_downloads_panel_block.js]
+skip-if = true # Bug 1352792
+[browser_downloads_panel_context_menu.js]
+[browser_downloads_panel_ctrl_click.js]
+[browser_downloads_panel_height.js]
+[browser_downloads_autohide.js]
+[browser_go_to_download_page.js]
+[browser_pdfjs_preview.js]
+[browser_downloads_pauseResume.js]
diff --git a/browser/components/downloads/test/browser/browser_about_downloads.js b/browser/components/downloads/test/browser/browser_about_downloads.js
new file mode 100644
index 0000000000..ab6568ef24
--- /dev/null
+++ b/browser/components/downloads/test/browser/browser_about_downloads.js
@@ -0,0 +1,44 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Ensure about:downloads actually works.
+ */
+add_task(async function test_about_downloads() {
+ await task_resetState();
+ registerCleanupFunction(task_resetState);
+
+ await setDownloadDir();
+
+ await task_addDownloads([
+ { state: DownloadsCommon.DOWNLOAD_FINISHED },
+ { state: DownloadsCommon.DOWNLOAD_PAUSED },
+ ]);
+
+ await BrowserTestUtils.withNewTab("about:blank", async browser => {
+ let downloadsLoaded = BrowserTestUtils.waitForEvent(
+ browser,
+ "InitialDownloadsLoaded",
+ true
+ );
+ BrowserTestUtils.loadURI(browser, "about:downloads");
+ await downloadsLoaded;
+ await SpecialPowers.spawn(browser, [], async function() {
+ let box = content.document.getElementById("downloadsRichListBox");
+ ok(box, "Should have list of downloads");
+ is(box.children.length, 2, "Should have 2 downloads.");
+ for (let kid of box.children) {
+ let desc = kid.querySelector(".downloadTarget");
+ // This would just be an `is` check, but stray temp files
+ // if this test (or another in this dir) ever fails could throw that off.
+ ok(
+ /^dm-ui-test(-\d+)?.file$/.test(desc.value),
+ `Label '${desc.value}' should match 'dm-ui-test.file'`
+ );
+ }
+ ok(box.firstChild.selected, "First item should be selected.");
+ });
+ });
+});
diff --git a/browser/components/downloads/test/browser/browser_basic_functionality.js b/browser/components/downloads/test/browser/browser_basic_functionality.js
new file mode 100644
index 0000000000..7e58f95fca
--- /dev/null
+++ b/browser/components/downloads/test/browser/browser_basic_functionality.js
@@ -0,0 +1,59 @@
+/* -*- 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/ */
+
+registerCleanupFunction(async function() {
+ await task_resetState();
+});
+
+/**
+ * Make sure the downloads panel can display items in the right order and
+ * contains the expected data.
+ */
+add_task(async function test_basic_functionality() {
+ // Display one of each download state.
+ const DownloadData = [
+ { state: DownloadsCommon.DOWNLOAD_NOTSTARTED },
+ { state: DownloadsCommon.DOWNLOAD_PAUSED },
+ { state: DownloadsCommon.DOWNLOAD_FINISHED },
+ { state: DownloadsCommon.DOWNLOAD_FAILED },
+ { state: DownloadsCommon.DOWNLOAD_CANCELED },
+ ];
+
+ // Wait for focus first
+ await promiseFocus();
+
+ // Ensure that state is reset in case previous tests didn't finish.
+ await task_resetState();
+
+ // For testing purposes, show all the download items at once.
+ var originalCountLimit = DownloadsView.kItemCountLimit;
+ DownloadsView.kItemCountLimit = DownloadData.length;
+ registerCleanupFunction(function() {
+ DownloadsView.kItemCountLimit = originalCountLimit;
+ });
+
+ // Populate the downloads database with the data required by this test.
+ await task_addDownloads(DownloadData);
+
+ // Open the user interface and wait for data to be fully loaded.
+ await task_openPanel();
+
+ // Test item data and count. This also tests the ordering of the display.
+ let richlistbox = document.getElementById("downloadsListBox");
+ /* disabled for failing intermittently (bug 767828)
+ is(richlistbox.itemChildren.length, DownloadData.length,
+ "There is the correct number of richlistitems");
+ */
+ let itemCount = richlistbox.itemChildren.length;
+ for (let i = 0; i < itemCount; i++) {
+ let element = richlistbox.itemChildren[itemCount - i - 1];
+ let download = DownloadsView.itemForElement(element).download;
+ is(
+ DownloadsCommon.stateOfDownload(download),
+ DownloadData[i].state,
+ "Download states match up"
+ );
+ }
+});
diff --git a/browser/components/downloads/test/browser/browser_confirm_unblock_download.js b/browser/components/downloads/test/browser/browser_confirm_unblock_download.js
new file mode 100644
index 0000000000..f47ea4d9b2
--- /dev/null
+++ b/browser/components/downloads/test/browser/browser_confirm_unblock_download.js
@@ -0,0 +1,94 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests the dialog which allows the user to unblock a downloaded file.
+
+registerCleanupFunction(() => {});
+
+async function assertDialogResult({ args, buttonToClick, expectedResult }) {
+ BrowserTestUtils.promiseAlertDialogOpen(buttonToClick);
+ is(await DownloadsCommon.confirmUnblockDownload(args), expectedResult);
+}
+
+/**
+ * Tests the "unblock" dialog, for each of the possible verdicts.
+ */
+add_task(async function test_unblock_dialog_unblock() {
+ for (let verdict of [
+ Downloads.Error.BLOCK_VERDICT_MALWARE,
+ Downloads.Error.BLOCK_VERDICT_POTENTIALLY_UNWANTED,
+ Downloads.Error.BLOCK_VERDICT_UNCOMMON,
+ ]) {
+ let args = { verdict, window, dialogType: "unblock" };
+
+ // Test both buttons.
+ await assertDialogResult({
+ args,
+ buttonToClick: "accept",
+ expectedResult: "unblock",
+ });
+ await assertDialogResult({
+ args,
+ buttonToClick: "cancel",
+ expectedResult: "cancel",
+ });
+ }
+});
+
+/**
+ * Tests the "chooseUnblock" dialog for potentially unwanted downloads.
+ */
+add_task(async function test_chooseUnblock_dialog() {
+ let args = {
+ verdict: Downloads.Error.BLOCK_VERDICT_POTENTIALLY_UNWANTED,
+ window,
+ dialogType: "chooseUnblock",
+ };
+
+ // Test each of the three buttons.
+ await assertDialogResult({
+ args,
+ buttonToClick: "accept",
+ expectedResult: "unblock",
+ });
+ await assertDialogResult({
+ args,
+ buttonToClick: "cancel",
+ expectedResult: "cancel",
+ });
+ await assertDialogResult({
+ args,
+ buttonToClick: "extra1",
+ expectedResult: "confirmBlock",
+ });
+});
+
+/**
+ * Tests the "chooseOpen" dialog for uncommon downloads.
+ */
+add_task(async function test_chooseOpen_dialog() {
+ let args = {
+ verdict: Downloads.Error.BLOCK_VERDICT_UNCOMMON,
+ window,
+ dialogType: "chooseOpen",
+ };
+
+ // Test each of the three buttons.
+ await assertDialogResult({
+ args,
+ buttonToClick: "accept",
+ expectedResult: "open",
+ });
+ await assertDialogResult({
+ args,
+ buttonToClick: "cancel",
+ expectedResult: "cancel",
+ });
+ await assertDialogResult({
+ args,
+ buttonToClick: "extra1",
+ expectedResult: "confirmBlock",
+ });
+});
diff --git a/browser/components/downloads/test/browser/browser_download_overwrite.js b/browser/components/downloads/test/browser/browser_download_overwrite.js
new file mode 100644
index 0000000000..71eb715315
--- /dev/null
+++ b/browser/components/downloads/test/browser/browser_download_overwrite.js
@@ -0,0 +1,129 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_ROOT = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "http://example.com"
+);
+
+var MockFilePicker = SpecialPowers.MockFilePicker;
+MockFilePicker.init(window);
+
+/* import-globals-from ../../../../../toolkit/content/tests/browser/common/mockTransfer.js */
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/toolkit/content/tests/browser/common/mockTransfer.js",
+ this
+);
+
+add_task(async function setup() {
+ // head.js has helpers that write to a nice unique file we can use.
+ await createDownloadedFile(gTestTargetFile.path, "Hello.\n");
+ ok(gTestTargetFile.exists(), "We created a test file.");
+
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.download.useDownloadDir", false]],
+ });
+ // Set up the file picker.
+ let destDir = gTestTargetFile.parent;
+
+ MockFilePicker.displayDirectory = destDir;
+ MockFilePicker.showCallback = function(fp) {
+ MockFilePicker.setFiles([gTestTargetFile]);
+ };
+ registerCleanupFunction(function() {
+ MockFilePicker.cleanup();
+ if (gTestTargetFile.exists()) {
+ gTestTargetFile.remove(false);
+ }
+ });
+});
+
+// If we download a file and the user accepts overwriting an existing one,
+// we shouldn't first delete that file before moving the .part file into
+// place.
+add_task(async function test_overwrite_does_not_delete_first() {
+ let unregisteredTransfer = false;
+ let transferCompletePromise = new Promise(resolve => {
+ mockTransferCallback = resolve;
+ });
+ mockTransferRegisterer.register();
+
+ registerCleanupFunction(function() {
+ if (!unregisteredTransfer) {
+ mockTransferRegisterer.unregister();
+ }
+ });
+
+ let dialogPromise = BrowserTestUtils.domWindowOpenedAndLoaded();
+ // Now try and download a thing to the file:
+ await BrowserTestUtils.withNewTab(TEST_ROOT + "foo.txt", async function() {
+ let dialog = await dialogPromise;
+ info("Got dialog.");
+ let saveEl = dialog.document.getElementById("save");
+ dialog.document.getElementById("mode").selectedItem = saveEl;
+ // Allow accepting the dialog (to avoid the delay helper):
+ dialog.document
+ .getElementById("unknownContentType")
+ .getButton("accept").disabled = false;
+ // Then accept it:
+ dialog.document.querySelector("dialog").acceptDialog();
+ ok(await transferCompletePromise, "download should succeed");
+ ok(
+ gTestTargetFile.exists(),
+ "File should still exist and not have been deleted."
+ );
+ // Note: the download transfer is fake so data won't have been written to
+ // the file, so we can't verify that the download actually overwrites data
+ // like this.
+ mockTransferRegisterer.unregister();
+ unregisteredTransfer = true;
+ });
+});
+
+// If we download a file and the user accepts overwriting an existing one,
+// we should successfully overwrite its contents.
+add_task(async function test_overwrite_works() {
+ let dialogPromise = BrowserTestUtils.domWindowOpenedAndLoaded();
+ let publicDownloads = await Downloads.getList(Downloads.PUBLIC);
+ // Now try and download a thing to the file:
+ await BrowserTestUtils.withNewTab(TEST_ROOT + "foo.txt", async function() {
+ let dialog = await dialogPromise;
+ info("Got dialog.");
+ // First ensure we catch the download finishing.
+ let downloadFinishedPromise = new Promise(resolve => {
+ publicDownloads.addView({
+ onDownloadChanged(download) {
+ info("Download changed!");
+ if (download.succeeded || download.error) {
+ info("Download succeeded or errored");
+ publicDownloads.removeView(this);
+ publicDownloads.removeFinished();
+ resolve(download);
+ }
+ },
+ });
+ });
+ let saveEl = dialog.document.getElementById("save");
+ dialog.document.getElementById("mode").selectedItem = saveEl;
+ // Allow accepting the dialog (to avoid the delay helper):
+ dialog.document
+ .getElementById("unknownContentType")
+ .getButton("accept").disabled = false;
+ // Then accept it:
+ dialog.document.querySelector("dialog").acceptDialog();
+ info("wait for download to finish");
+ let download = await downloadFinishedPromise;
+ ok(download.succeeded, "Download should succeed");
+ ok(
+ gTestTargetFile.exists(),
+ "File should still exist and not have been deleted."
+ );
+ let contents = new TextDecoder().decode(
+ await IOUtils.read(gTestTargetFile.path)
+ );
+ info("Got: " + contents);
+ ok(contents.startsWith("Dummy"), "The file was overwritten.");
+ });
+});
diff --git a/browser/components/downloads/test/browser/browser_downloads_autohide.js b/browser/components/downloads/test/browser/browser_downloads_autohide.js
new file mode 100644
index 0000000000..f56957d7aa
--- /dev/null
+++ b/browser/components/downloads/test/browser/browser_downloads_autohide.js
@@ -0,0 +1,510 @@
+/* -*- 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/ */
+
+"use strict";
+
+const kDownloadAutoHidePref = "browser.download.autohideButton";
+
+registerCleanupFunction(async function() {
+ Services.prefs.clearUserPref(kDownloadAutoHidePref);
+ if (document.documentElement.hasAttribute("customizing")) {
+ await gCustomizeMode.reset();
+ await promiseCustomizeEnd();
+ } else {
+ CustomizableUI.reset();
+ }
+});
+
+add_task(async function checkStateDuringPrefFlips() {
+ ok(
+ Services.prefs.getBoolPref(kDownloadAutoHidePref),
+ "Should be autohiding the button by default"
+ );
+ ok(
+ !DownloadsIndicatorView.hasDownloads,
+ "Should be no downloads when starting the test"
+ );
+ let downloadsButton = document.getElementById("downloads-button");
+ ok(
+ downloadsButton.hasAttribute("hidden"),
+ "Button should be hidden in the toolbar"
+ );
+ await gCustomizeMode.addToPanel(downloadsButton);
+ ok(
+ !downloadsButton.hasAttribute("hidden"),
+ "Button shouldn't be hidden in the panel"
+ );
+ ok(
+ !Services.prefs.getBoolPref(kDownloadAutoHidePref),
+ "Pref got set to false when the user moved the button"
+ );
+ gCustomizeMode.addToToolbar(downloadsButton);
+ ok(
+ !Services.prefs.getBoolPref(kDownloadAutoHidePref),
+ "Pref remains false when the user moved the button"
+ );
+ Services.prefs.setBoolPref(kDownloadAutoHidePref, true);
+ ok(
+ downloadsButton.hasAttribute("hidden"),
+ "Button should be hidden again in the toolbar " +
+ "now that we flipped the pref"
+ );
+ Services.prefs.setBoolPref(kDownloadAutoHidePref, false);
+ ok(
+ !downloadsButton.hasAttribute("hidden"),
+ "Button shouldn't be hidden with autohide turned off"
+ );
+ await gCustomizeMode.addToPanel(downloadsButton);
+ ok(
+ !downloadsButton.hasAttribute("hidden"),
+ "Button shouldn't be hidden with autohide turned off " +
+ "after moving it to the panel"
+ );
+ gCustomizeMode.addToToolbar(downloadsButton);
+ ok(
+ !downloadsButton.hasAttribute("hidden"),
+ "Button shouldn't be hidden with autohide turned off " +
+ "after moving it back to the toolbar"
+ );
+ await gCustomizeMode.addToPanel(downloadsButton);
+ Services.prefs.setBoolPref(kDownloadAutoHidePref, true);
+ ok(
+ !downloadsButton.hasAttribute("hidden"),
+ "Button should still not be hidden with autohide turned back on " +
+ "because it's in the panel"
+ );
+ // Use CUI directly instead of the customize mode APIs,
+ // to avoid tripping the "automatically turn off autohide" code.
+ CustomizableUI.addWidgetToArea("downloads-button", "nav-bar");
+ ok(
+ downloadsButton.hasAttribute("hidden"),
+ "Button should be hidden again in the toolbar"
+ );
+ gCustomizeMode.removeFromArea(downloadsButton);
+ Services.prefs.setBoolPref(kDownloadAutoHidePref, false);
+ // Can't use gCustomizeMode.addToToolbar here because it doesn't work for
+ // palette items if the window isn't in customize mode:
+ CustomizableUI.addWidgetToArea(
+ downloadsButton.id,
+ CustomizableUI.AREA_NAVBAR
+ );
+ ok(
+ !downloadsButton.hasAttribute("hidden"),
+ "Button should be unhidden again in the toolbar " +
+ "even if the pref was flipped while the button was in the palette"
+ );
+ Services.prefs.setBoolPref(kDownloadAutoHidePref, true);
+});
+
+add_task(async function checkStateInCustomizeMode() {
+ ok(
+ Services.prefs.getBoolPref("browser.download.autohideButton"),
+ "Should be autohiding the button"
+ );
+ let downloadsButton = document.getElementById("downloads-button");
+ await promiseCustomizeStart();
+ ok(
+ !downloadsButton.hasAttribute("hidden"),
+ "Button should be shown in customize mode."
+ );
+ await promiseCustomizeEnd();
+ ok(
+ downloadsButton.hasAttribute("hidden"),
+ "Button should be hidden if it's in the toolbar " +
+ "after customize mode without any moves."
+ );
+ await promiseCustomizeStart();
+ await gCustomizeMode.addToPanel(downloadsButton);
+ ok(
+ !downloadsButton.hasAttribute("hidden"),
+ "Button should be shown in customize mode when moved to the panel"
+ );
+ gCustomizeMode.addToToolbar(downloadsButton);
+ ok(
+ !downloadsButton.hasAttribute("hidden"),
+ "Button should be shown in customize mode when moved back to the toolbar"
+ );
+ gCustomizeMode.removeFromArea(downloadsButton);
+ ok(
+ !downloadsButton.hasAttribute("hidden"),
+ "Button should be shown in customize mode when in the palette"
+ );
+ Services.prefs.setBoolPref(kDownloadAutoHidePref, false);
+ Services.prefs.setBoolPref(kDownloadAutoHidePref, true);
+ ok(
+ !downloadsButton.hasAttribute("hidden"),
+ "Button should be shown in customize mode " +
+ "even when flipping the autohide pref"
+ );
+ await gCustomizeMode.addToPanel(downloadsButton);
+ await promiseCustomizeEnd();
+ ok(
+ !downloadsButton.hasAttribute("hidden"),
+ "Button should be shown after customize mode when moved to the panel"
+ );
+ await promiseCustomizeStart();
+ gCustomizeMode.addToToolbar(downloadsButton);
+ await promiseCustomizeEnd();
+ ok(
+ !downloadsButton.hasAttribute("hidden"),
+ "Button should be shown in the toolbar after " +
+ "customize mode because we moved it."
+ );
+ await promiseCustomizeStart();
+ await gCustomizeMode.reset();
+ ok(
+ !downloadsButton.hasAttribute("hidden"),
+ "Button should be shown in the toolbar in customize mode after a reset."
+ );
+ await gCustomizeMode.undoReset();
+ ok(
+ !downloadsButton.hasAttribute("hidden"),
+ "Button should be shown in the toolbar in customize mode " +
+ "when undoing the reset."
+ );
+ await gCustomizeMode.addToPanel(downloadsButton);
+ await gCustomizeMode.reset();
+ ok(
+ !downloadsButton.hasAttribute("hidden"),
+ "Button should be shown in the toolbar in customize mode " +
+ "after a reset moved it."
+ );
+ await gCustomizeMode.undoReset();
+ ok(
+ !downloadsButton.hasAttribute("hidden"),
+ "Button should be shown in the panel in customize mode " +
+ "when undoing the reset."
+ );
+ await gCustomizeMode.reset();
+ await promiseCustomizeEnd();
+});
+
+add_task(async function checkStateInCustomizeModeMultipleWindows() {
+ ok(
+ Services.prefs.getBoolPref("browser.download.autohideButton"),
+ "Should be autohiding the button"
+ );
+ let downloadsButton = document.getElementById("downloads-button");
+ await promiseCustomizeStart();
+ ok(
+ !downloadsButton.hasAttribute("hidden"),
+ "Button should be shown in customize mode."
+ );
+ let otherWin = await BrowserTestUtils.openNewBrowserWindow();
+ let otherDownloadsButton = otherWin.document.getElementById(
+ "downloads-button"
+ );
+ ok(
+ otherDownloadsButton.hasAttribute("hidden"),
+ "Button should be hidden in the other window."
+ );
+
+ // Use CUI directly instead of the customize mode APIs,
+ // to avoid tripping the "automatically turn off autohide" code.
+ CustomizableUI.addWidgetToArea(
+ "downloads-button",
+ CustomizableUI.AREA_FIXED_OVERFLOW_PANEL
+ );
+ ok(
+ !downloadsButton.hasAttribute("hidden"),
+ "Button should still be shown in customize mode."
+ );
+ ok(
+ !otherDownloadsButton.hasAttribute("hidden"),
+ "Button should be shown in the other window too because it's in a panel."
+ );
+
+ CustomizableUI.addWidgetToArea(
+ "downloads-button",
+ CustomizableUI.AREA_NAVBAR
+ );
+ ok(
+ !downloadsButton.hasAttribute("hidden"),
+ "Button should still be shown in customize mode."
+ );
+ ok(
+ otherDownloadsButton.hasAttribute("hidden"),
+ "Button should be hidden again in the other window."
+ );
+
+ Services.prefs.setBoolPref(kDownloadAutoHidePref, false);
+ ok(
+ !downloadsButton.hasAttribute("hidden"),
+ "Button should be shown in customize mode"
+ );
+ ok(
+ !otherDownloadsButton.hasAttribute("hidden"),
+ "Button should be shown in the other window with the pref flipped"
+ );
+
+ Services.prefs.setBoolPref(kDownloadAutoHidePref, true);
+ ok(
+ !downloadsButton.hasAttribute("hidden"),
+ "Button should be shown in customize mode " +
+ "even when flipping the autohide pref"
+ );
+ ok(
+ otherDownloadsButton.hasAttribute("hidden"),
+ "Button should be hidden in the other window with the pref flipped again"
+ );
+
+ await gCustomizeMode.addToPanel(downloadsButton);
+ ok(
+ !downloadsButton.hasAttribute("hidden"),
+ "Button should still be shown in customize mode."
+ );
+ ok(
+ !otherDownloadsButton.hasAttribute("hidden"),
+ "Button should be shown in the other window too because it's in a panel."
+ );
+
+ gCustomizeMode.removeFromArea(downloadsButton);
+ ok(
+ !Services.prefs.getBoolPref(kDownloadAutoHidePref),
+ "Autohide pref turned off by moving the button"
+ );
+ ok(
+ !downloadsButton.hasAttribute("hidden"),
+ "Button should still be shown in customize mode."
+ );
+ // Don't need to assert in the other window - button is gone there.
+
+ await gCustomizeMode.reset();
+ ok(
+ Services.prefs.getBoolPref(kDownloadAutoHidePref),
+ "Autohide pref reset by reset()"
+ );
+ ok(
+ !downloadsButton.hasAttribute("hidden"),
+ "Button should still be shown in customize mode."
+ );
+ ok(
+ otherDownloadsButton.hasAttribute("hidden"),
+ "Button should be hidden in the other window."
+ );
+ ok(
+ otherDownloadsButton.closest("#nav-bar"),
+ "Button should be back in the nav bar in the other window."
+ );
+
+ await promiseCustomizeEnd();
+ ok(
+ downloadsButton.hasAttribute("hidden"),
+ "Button should be hidden again outside of customize mode"
+ );
+ await BrowserTestUtils.closeWindow(otherWin);
+});
+
+add_task(async function checkStateForDownloads() {
+ ok(
+ Services.prefs.getBoolPref("browser.download.autohideButton"),
+ "Should be autohiding the button"
+ );
+ let downloadsButton = document.getElementById("downloads-button");
+ ok(
+ downloadsButton.hasAttribute("hidden"),
+ "Button should be hidden when there are no downloads."
+ );
+
+ await task_addDownloads([{ state: DownloadsCommon.DOWNLOAD_DOWNLOADING }]);
+ ok(
+ !downloadsButton.hasAttribute("hidden"),
+ "Button should be unhidden when there are downloads."
+ );
+ let publicList = await Downloads.getList(Downloads.PUBLIC);
+ let downloads = await publicList.getAll();
+ for (let download of downloads) {
+ publicList.remove(download);
+ }
+ ok(
+ downloadsButton.hasAttribute("hidden"),
+ "Button should be hidden when the download is removed"
+ );
+ await task_addDownloads([{ state: DownloadsCommon.DOWNLOAD_DOWNLOADING }]);
+ ok(
+ !downloadsButton.hasAttribute("hidden"),
+ "Button should be unhidden when there are downloads."
+ );
+
+ Services.prefs.setBoolPref(kDownloadAutoHidePref, false);
+ ok(
+ !downloadsButton.hasAttribute("hidden"),
+ "Button should still be unhidden."
+ );
+
+ downloads = await publicList.getAll();
+ for (let download of downloads) {
+ publicList.remove(download);
+ }
+ ok(
+ !downloadsButton.hasAttribute("hidden"),
+ "Button should still be unhidden because the pref was flipped."
+ );
+ Services.prefs.setBoolPref(kDownloadAutoHidePref, true);
+ ok(
+ downloadsButton.hasAttribute("hidden"),
+ "Button should be hidden now that the pref flipped back " +
+ "because there were already no downloads."
+ );
+
+ gCustomizeMode.addToPanel(downloadsButton);
+ ok(
+ !downloadsButton.hasAttribute("hidden"),
+ "Button should not be hidden in the panel."
+ );
+
+ await task_addDownloads([{ state: DownloadsCommon.DOWNLOAD_DOWNLOADING }]);
+
+ downloads = await publicList.getAll();
+ for (let download of downloads) {
+ publicList.remove(download);
+ }
+
+ ok(
+ !downloadsButton.hasAttribute("hidden"),
+ "Button should still not be hidden in the panel " +
+ "when downloads count reaches 0 after being non-0."
+ );
+
+ CustomizableUI.reset();
+});
+
+/**
+ * Check that if the button is moved to the palette, we unhide it
+ * in customize mode even if it was always hidden. We use a new
+ * window to test this.
+ */
+add_task(async function checkStateWhenHiddenInPalette() {
+ ok(
+ Services.prefs.getBoolPref(kDownloadAutoHidePref),
+ "Pref should be causing us to autohide"
+ );
+ gCustomizeMode.removeFromArea(document.getElementById("downloads-button"));
+ // In a new window, the button will have been hidden
+ let otherWin = await BrowserTestUtils.openNewBrowserWindow();
+ ok(
+ !otherWin.document.getElementById("downloads-button"),
+ "Button shouldn't be visible in the window"
+ );
+
+ let paletteButton = otherWin.gNavToolbox.palette.querySelector(
+ "#downloads-button"
+ );
+ ok(paletteButton, "Button should exist in the palette");
+ if (paletteButton) {
+ ok(paletteButton.hidden, "Button will still have the hidden attribute");
+ await promiseCustomizeStart(otherWin);
+ ok(
+ !paletteButton.hidden,
+ "Button should no longer be hidden in customize mode"
+ );
+ ok(
+ otherWin.document.getElementById("downloads-button"),
+ "Button should be in the document now."
+ );
+ await promiseCustomizeEnd(otherWin);
+ // We purposefully don't assert anything about what happens next.
+ // It doesn't really matter if the button remains unhidden in
+ // the palette, and if we move it we'll unhide it then (the other
+ // tests check this).
+ }
+ await BrowserTestUtils.closeWindow(otherWin);
+ CustomizableUI.reset();
+});
+
+add_task(async function checkContextMenu() {
+ let contextMenu = document.getElementById("toolbar-context-menu");
+ let checkbox = document.getElementById(
+ "toolbar-context-autohide-downloads-button"
+ );
+ let button = document.getElementById("downloads-button");
+
+ is(
+ Services.prefs.getBoolPref(kDownloadAutoHidePref),
+ true,
+ "Pref should be causing us to autohide"
+ );
+ is(
+ DownloadsIndicatorView.hasDownloads,
+ false,
+ "Should be no downloads when starting the test"
+ );
+ is(button.hidden, true, "Downloads button is hidden");
+
+ info("Simulate a download to show the downloads button.");
+ DownloadsIndicatorView.hasDownloads = true;
+ is(button.hidden, false, "Downloads button is visible");
+
+ info("Check context menu");
+ await openContextMenu(button);
+ is(checkbox.hidden, false, "Auto-hide checkbox is visible");
+ is(checkbox.getAttribute("checked"), "true", "Auto-hide is enabled");
+
+ info("Disable auto-hide via context menu");
+ clickCheckbox(checkbox);
+ is(
+ Services.prefs.getBoolPref(kDownloadAutoHidePref),
+ false,
+ "Pref has been set to false"
+ );
+
+ info("Clear downloads");
+ DownloadsIndicatorView.hasDownloads = false;
+ is(button.hidden, false, "Downloads button is still visible");
+
+ info("Check context menu");
+ await openContextMenu(button);
+ is(checkbox.hidden, false, "Auto-hide checkbox is visible");
+ is(checkbox.hasAttribute("checked"), false, "Auto-hide is disabled");
+
+ info("Enable auto-hide via context menu");
+ clickCheckbox(checkbox);
+ is(button.hidden, true, "Downloads button is hidden");
+ is(
+ Services.prefs.getBoolPref(kDownloadAutoHidePref),
+ true,
+ "Pref has been set to true"
+ );
+
+ info("Check context menu in another button");
+ await openContextMenu(document.getElementById("home-button"));
+ is(checkbox.hidden, true, "Auto-hide checkbox is hidden");
+ contextMenu.hidePopup();
+
+ info("Open popup directly");
+ contextMenu.openPopup();
+ is(checkbox.hidden, true, "Auto-hide checkbox is hidden");
+ contextMenu.hidePopup();
+});
+
+function promiseCustomizeStart(aWindow = window) {
+ return new Promise(resolve => {
+ aWindow.gNavToolbox.addEventListener("customizationready", resolve, {
+ once: true,
+ });
+ aWindow.gCustomizeMode.enter();
+ });
+}
+
+function promiseCustomizeEnd(aWindow = window) {
+ return new Promise(resolve => {
+ aWindow.gNavToolbox.addEventListener("aftercustomization", resolve, {
+ once: true,
+ });
+ aWindow.gCustomizeMode.exit();
+ });
+}
+
+function clickCheckbox(checkbox) {
+ // Clicking a checkbox toggles its checkedness first.
+ if (checkbox.getAttribute("checked") == "true") {
+ checkbox.removeAttribute("checked");
+ } else {
+ checkbox.setAttribute("checked", "true");
+ }
+ // Then it runs the command and closes the popup.
+ checkbox.doCommand();
+ checkbox.parentElement.hidePopup();
+}
diff --git a/browser/components/downloads/test/browser/browser_downloads_panel_block.js b/browser/components/downloads/test/browser/browser_downloads_panel_block.js
new file mode 100644
index 0000000000..7e0678205c
--- /dev/null
+++ b/browser/components/downloads/test/browser/browser_downloads_panel_block.js
@@ -0,0 +1,181 @@
+/* eslint-disable mozilla/no-arbitrary-setTimeout */
+"use strict";
+
+add_task(async function mainTest() {
+ await task_resetState();
+
+ let verdicts = [
+ Downloads.Error.BLOCK_VERDICT_UNCOMMON,
+ Downloads.Error.BLOCK_VERDICT_MALWARE,
+ Downloads.Error.BLOCK_VERDICT_POTENTIALLY_UNWANTED,
+ Downloads.Error.BLOCK_VERDICT_INSECURE,
+ ];
+ await task_addDownloads(verdicts.map(v => makeDownload(v)));
+
+ // Check that the richlistitem for each download is correct.
+ for (let i = 0; i < verdicts.length; i++) {
+ await openPanel();
+
+ // The current item is always the first one in the listbox since each
+ // iteration of this loop removes the item at the end.
+ let item = DownloadsView.richListBox.firstElementChild;
+
+ // Open the panel and click the item to show the subview.
+ let viewPromise = promiseViewShown(DownloadsBlockedSubview.subview);
+ await EventUtils.sendMouseEvent({ type: "click" }, item);
+ await viewPromise;
+
+ // Items are listed in newest-to-oldest order, so e.g. the first item's
+ // verdict is the last element in the verdicts array.
+ Assert.ok(
+ DownloadsBlockedSubview.subview.getAttribute("verdict"),
+ verdicts[verdicts.count - i - 1]
+ );
+
+ // Go back to the main view.
+ viewPromise = promiseViewShown(DownloadsBlockedSubview.mainView);
+ DownloadsBlockedSubview.panelMultiView.goBack();
+ await viewPromise;
+
+ // Show the subview again.
+ viewPromise = promiseViewShown(DownloadsBlockedSubview.subview);
+ await EventUtils.sendMouseEvent({ type: "click" }, item);
+ await viewPromise;
+
+ // Click the Open button. The download should be unblocked and then opened,
+ // i.e., unblockAndOpenDownload() should be called on the item. The panel
+ // should also be closed as a result, so wait for that too.
+ let unblockOpenPromise = promiseUnblockAndOpenDownloadCalled(item);
+ let hidePromise = promisePanelHidden();
+ EventUtils.synthesizeMouse(
+ DownloadsBlockedSubview.elements.unblockButton,
+ 10,
+ 10,
+ {},
+ window
+ );
+ await unblockOpenPromise;
+ await hidePromise;
+
+ window.focus();
+ await SimpleTest.promiseFocus(window);
+
+ // Reopen the panel and show the subview again.
+ await openPanel();
+
+ viewPromise = promiseViewShown(DownloadsBlockedSubview.subview);
+ await EventUtils.sendMouseEvent({ type: "click" }, item);
+ await viewPromise;
+
+ // Click the Remove button. The panel should close and the item should be
+ // removed from it.
+ hidePromise = promisePanelHidden();
+ EventUtils.synthesizeMouse(
+ DownloadsBlockedSubview.elements.deleteButton,
+ 10,
+ 10,
+ {},
+ window
+ );
+ await hidePromise;
+
+ await openPanel();
+ Assert.ok(!item.parentNode);
+
+ hidePromise = promisePanelHidden();
+ DownloadsPanel.hidePanel();
+ await hidePromise;
+ }
+
+ await task_resetState();
+});
+
+async function openPanel() {
+ // This function is insane but something intermittently causes the panel to be
+ // closed as soon as it's opening on Linux ASAN. Maybe it would also happen
+ // on other build machines if the test ran often enough. Not only is the
+ // panel closed, it's closed while it's opening, leaving DownloadsPanel._state
+ // such that when you try to open the panel again, it thinks it's already
+ // open, but it's not. The result is that the test times out.
+ //
+ // What this does is call DownloadsPanel.showPanel over and over again until
+ // the panel is really open. There are a few wrinkles:
+ //
+ // (1) When panel.state is "open", check four more times (for a total of five)
+ // before returning to make the panel stays open.
+ // (2) If the panel is not open, check the _state. It should be either
+ // kStateUninitialized or kStateHidden. If it's not, then the panel is in the
+ // process of opening -- or maybe it's stuck in that process -- so reset the
+ // _state to kStateHidden.
+ // (3) If the _state is not kStateUninitialized or kStateHidden, then it may
+ // actually be properly opening and not stuck at all. To avoid always closing
+ // the panel while it's properly opening, use an exponential backoff mechanism
+ // for retries.
+ //
+ // If all that fails, then the test will time out, but it would have timed out
+ // anyway.
+
+ await promiseFocus();
+ await new Promise(resolve => {
+ let verifyCount = 5;
+ let backoff = 0;
+ let iBackoff = 0;
+ let interval = setInterval(() => {
+ if (DownloadsPanel.panel && DownloadsPanel.panel.state == "open") {
+ if (verifyCount > 0) {
+ verifyCount--;
+ } else {
+ clearInterval(interval);
+ resolve();
+ }
+ } else if (iBackoff < backoff) {
+ // Keep backing off before trying again.
+ iBackoff++;
+ } else {
+ // Try (or retry) opening the panel.
+ verifyCount = 5;
+ backoff = Math.max(1, 2 * backoff);
+ iBackoff = 0;
+ if (DownloadsPanel._state != DownloadsPanel.kStateUninitialized) {
+ DownloadsPanel._state = DownloadsPanel.kStateHidden;
+ }
+ DownloadsPanel.showPanel();
+ }
+ }, 100);
+ });
+}
+
+function promisePanelHidden() {
+ return BrowserTestUtils.waitForEvent(DownloadsPanel.panel, "popuphidden");
+}
+
+function makeDownload(verdict) {
+ return {
+ state: DownloadsCommon.DOWNLOAD_DIRTY,
+ hasBlockedData: true,
+ errorObj: {
+ result: Cr.NS_ERROR_FAILURE,
+ message: "Download blocked.",
+ becauseBlocked: true,
+ becauseBlockedByReputationCheck: true,
+ reputationCheckVerdict: verdict,
+ },
+ };
+}
+
+function promiseViewShown(view) {
+ return BrowserTestUtils.waitForEvent(view, "ViewShown");
+}
+
+function promiseUnblockAndOpenDownloadCalled(item) {
+ return new Promise(resolve => {
+ let realFn = item._shell.unblockAndOpenDownload;
+ item._shell.unblockAndOpenDownload = () => {
+ item._shell.unblockAndOpenDownload = realFn;
+ resolve();
+ // unblockAndOpenDownload returns a promise (that's resolved when the file
+ // is opened).
+ return Promise.resolve();
+ };
+ });
+}
diff --git a/browser/components/downloads/test/browser/browser_downloads_panel_context_menu.js b/browser/components/downloads/test/browser/browser_downloads_panel_context_menu.js
new file mode 100644
index 0000000000..31841fde5e
--- /dev/null
+++ b/browser/components/downloads/test/browser/browser_downloads_panel_context_menu.js
@@ -0,0 +1,204 @@
+/*
+ Coverage for context menu state for downloads in the Downloads Panel
+*/
+
+let gDownloadDir;
+const TestFiles = {};
+
+const MENU_ITEMS = {
+ pause: ".downloadPauseMenuItem",
+ resume: ".downloadResumeMenuItem",
+ unblock: '[command="downloadsCmd_unblock"]',
+ openInSystemViewer: '[command="downloadsCmd_openInSystemViewer"]',
+ alwaysOpenInSystemViewer: '[command="downloadsCmd_alwaysOpenInSystemViewer"]',
+ show: '[command="downloadsCmd_show"]',
+ commandsSeparator: "menuseparator,.downloadCommandsSeparator",
+ openReferrer: '[command="downloadsCmd_openReferrer"]',
+ copyLocation: '[command="downloadsCmd_copyLocation"]',
+ separator: "menuseparator",
+ delete: '[command="cmd_delete"]',
+ clearList: '[command="downloadsCmd_clearList"]',
+ clearDownloads: '[command="downloadsCmd_clearDownloads"]',
+};
+
+const TestCases = [
+ {
+ name: "Completed PDF download",
+ downloads: [
+ {
+ state: DownloadsCommon.DOWNLOAD_FINISHED,
+ contentType: "application/pdf",
+ target: {},
+ },
+ ],
+ expected: {
+ menu: [
+ MENU_ITEMS.openInSystemViewer,
+ MENU_ITEMS.alwaysOpenInSystemViewer,
+ MENU_ITEMS.show,
+ MENU_ITEMS.commandsSeparator,
+ MENU_ITEMS.openReferrer,
+ MENU_ITEMS.copyLocation,
+ MENU_ITEMS.separator,
+ MENU_ITEMS.delete,
+ MENU_ITEMS.clearList,
+ ],
+ },
+ },
+ {
+ name: "Canceled PDF download",
+ downloads: [
+ {
+ state: DownloadsCommon.DOWNLOAD_CANCELED,
+ contentType: "application/pdf",
+ target: {},
+ },
+ ],
+ expected: {
+ menu: [
+ MENU_ITEMS.openReferrer,
+ MENU_ITEMS.copyLocation,
+ MENU_ITEMS.separator,
+ MENU_ITEMS.delete,
+ MENU_ITEMS.clearList,
+ ],
+ },
+ },
+];
+
+add_task(async function test_setUp() {
+ // remove download files, empty out collections
+ let downloadList = await Downloads.getList(Downloads.ALL);
+ let downloadCount = (await downloadList.getAll()).length;
+ is(downloadCount, 0, "At the start of the test, there should be 0 downloads");
+
+ await task_resetState();
+ if (!gDownloadDir) {
+ gDownloadDir = await setDownloadDir();
+ }
+ info("Created download directory: " + gDownloadDir);
+
+ // create the downloaded files we'll need
+ TestFiles.pdf = await createDownloadedFile(
+ OS.Path.join(gDownloadDir, "downloaded.pdf"),
+ DATA_PDF
+ );
+ info("Created downloaded PDF file at:" + TestFiles.pdf.path);
+ TestFiles.txt = await createDownloadedFile(
+ OS.Path.join(gDownloadDir, "downloaded.txt"),
+ "Test file"
+ );
+ info("Created downloaded text file at:" + TestFiles.txt.path);
+});
+
+// register the tests
+for (let testData of TestCases) {
+ if (testData.skip) {
+ info("Skipping test:" + testData.name);
+ continue;
+ }
+ // use the 'name' property of each test case as the test function name
+ // so we get useful logs
+ let tmp = {
+ async [testData.name]() {
+ await testDownloadContextMenu(testData);
+ },
+ };
+ add_task(tmp[testData.name]);
+}
+
+async function testDownloadContextMenu({ downloads = [], expected }) {
+ // prepare downloads
+ await prepareDownloads(downloads);
+ let downloadList = await Downloads.getList(Downloads.PUBLIC);
+ let [firstDownload] = await downloadList.getAll();
+ info("Download succeeded? " + firstDownload.succeeded);
+ info("Download target exists? " + firstDownload.target.exists);
+
+ // open panel
+ await task_openPanel();
+ await TestUtils.waitForCondition(
+ () =>
+ document.getElementById("downloadsListBox").childElementCount ==
+ downloads.length
+ );
+
+ info("trigger the context menu");
+ let itemTarget = document.querySelector(
+ "#downloadsListBox richlistitem .downloadMainArea"
+ );
+
+ let contextMenu = await openContextMenu(itemTarget);
+
+ info("context menu should be open, verify its menu items");
+ let result = verifyContextMenu(contextMenu, expected.menu);
+
+ // close menus
+ contextMenu.hidePopup();
+ let hiddenPromise = BrowserTestUtils.waitForEvent(
+ DownloadsPanel.panel,
+ "popuphidden"
+ );
+ DownloadsPanel.hidePanel();
+ await hiddenPromise;
+
+ ok(!result, "Expected no errors verifying context menu items");
+
+ // clean up downloads
+ await downloadList.removeFinished();
+}
+
+// ----------------------------------------------------------------------------
+// Helpers
+
+function verifyContextMenu(contextMenu, itemSelectors) {
+ // Ignore hidden nodes
+ let items = Array.from(contextMenu.children).filter(n =>
+ BrowserTestUtils.is_visible(n)
+ );
+ let menuAsText = items
+ .map(n => {
+ return n.nodeName == "menuseparator"
+ ? "---"
+ : `${n.label} (${n.command})`;
+ })
+ .join("\n");
+ info("Got actual context menu items: \n" + menuAsText);
+
+ try {
+ is(
+ items.length,
+ itemSelectors.length,
+ "Context menu has the expected number of items"
+ );
+ for (let i = 0; i < items.length; i++) {
+ let selector = itemSelectors[i];
+ ok(
+ items[i].matches(selector),
+ `Item at ${i} matches expected selector: ${selector}`
+ );
+ }
+ } catch (ex) {
+ return ex;
+ }
+ return null;
+}
+
+async function prepareDownloads(downloads) {
+ for (let props of downloads) {
+ info(JSON.stringify(props));
+ if (props.state !== DownloadsCommon.DOWNLOAD_FINISHED) {
+ continue;
+ }
+ switch (props.contentType) {
+ case "application/pdf":
+ props.target = TestFiles.pdf;
+ break;
+ default:
+ props.target = TestFiles.txt;
+ break;
+ }
+ ok(props.target instanceof Ci.nsIFile, "download target is a nsIFile");
+ }
+ await task_addDownloads(downloads);
+}
diff --git a/browser/components/downloads/test/browser/browser_downloads_panel_ctrl_click.js b/browser/components/downloads/test/browser/browser_downloads_panel_ctrl_click.js
new file mode 100644
index 0000000000..57ef284bc1
--- /dev/null
+++ b/browser/components/downloads/test/browser/browser_downloads_panel_ctrl_click.js
@@ -0,0 +1,35 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function test_downloads_panel() {
+ // On macOS, ctrl-click shouldn't open the panel because this normally opens
+ // the context menu. This happens via the `contextmenu` event which is created
+ // by widget code, so our simulated clicks do not do so, so we can't test
+ // anything on macOS.
+ if (AppConstants.platform == "macosx") {
+ ok(true, "The test is ignored on Mac");
+ return;
+ }
+
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.download.autohideButton", false]],
+ });
+ await promiseButtonShown("downloads-button");
+
+ const button = document.getElementById("downloads-button");
+ let shownPromise = promisePanelOpened();
+ // Should still open the panel when Ctrl key is pressed.
+ EventUtils.synthesizeMouseAtCenter(button, { ctrlKey: true });
+ await shownPromise;
+ is(DownloadsPanel.panel.state, "open", "Check that panel state is 'open'");
+
+ // Close download panel
+ DownloadsPanel.hidePanel();
+ is(
+ DownloadsPanel.panel.state,
+ "closed",
+ "Check that panel state is 'closed'"
+ );
+});
diff --git a/browser/components/downloads/test/browser/browser_downloads_panel_height.js b/browser/components/downloads/test/browser/browser_downloads_panel_height.js
new file mode 100644
index 0000000000..f65be18cb4
--- /dev/null
+++ b/browser/components/downloads/test/browser/browser_downloads_panel_height.js
@@ -0,0 +1,31 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * This test exists because we use a <panelmultiview> element and it handles
+ * some of the height changes for us. We need to verify that the height is
+ * updated correctly if downloads are removed while the panel is hidden.
+ */
+add_task(async function test_height_reduced_after_removal() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.download.autohideButton", false]],
+ });
+ await promiseButtonShown("downloads-button");
+ await task_addDownloads([{ state: DownloadsCommon.DOWNLOAD_FINISHED }]);
+
+ await task_openPanel();
+ let panel = document.getElementById("downloadsPanel");
+ let heightBeforeRemoval = panel.getBoundingClientRect().height;
+
+ // We want to close the panel before we remove the download from the list.
+ DownloadsPanel.hidePanel();
+ await task_resetState();
+
+ await task_openPanel();
+ let heightAfterRemoval = panel.getBoundingClientRect().height;
+ Assert.greater(heightBeforeRemoval, heightAfterRemoval);
+
+ await task_resetState();
+});
diff --git a/browser/components/downloads/test/browser/browser_downloads_pauseResume.js b/browser/components/downloads/test/browser/browser_downloads_pauseResume.js
new file mode 100644
index 0000000000..21c786b1bb
--- /dev/null
+++ b/browser/components/downloads/test/browser/browser_downloads_pauseResume.js
@@ -0,0 +1,49 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+registerCleanupFunction(async function() {
+ await task_resetState();
+});
+
+add_task(async function test_downloads_library() {
+ let DownloadData = [];
+ for (let i = 0; i < 20; i++) {
+ DownloadData.push({ state: DownloadsCommon.DOWNLOAD_PAUSED });
+ }
+
+ // Ensure that state is reset in case previous tests didn't finish.
+ await task_resetState();
+
+ // Populate the downloads database with the data required by this test.
+ await task_addDownloads(DownloadData);
+
+ let win = await openLibrary("Downloads");
+ registerCleanupFunction(function() {
+ win.close();
+ });
+
+ let listbox = win.document.getElementById("downloadsRichListBox");
+ ok(listbox, "Download list box present");
+
+ // Select one of the downloads.
+ listbox.itemChildren[0].click();
+ listbox.itemChildren[0]._shell._download.hasPartialData = true;
+
+ EventUtils.synthesizeKey(" ", {}, win);
+ is(
+ listbox.itemChildren[0]._shell._downloadState,
+ DownloadsCommon.DOWNLOAD_DOWNLOADING,
+ "Download state toggled from paused to downloading"
+ );
+
+ // there is no event to wait for in some cases, we need to wait for the keypress to potentially propagate
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ await new Promise(r => setTimeout(r, 500));
+ is(
+ listbox.scrollTop,
+ 0,
+ "All downloads view did not scroll when spacebar event fired on a selected download"
+ );
+});
diff --git a/browser/components/downloads/test/browser/browser_first_download_panel.js b/browser/components/downloads/test/browser/browser_first_download_panel.js
new file mode 100644
index 0000000000..cbffcc35bd
--- /dev/null
+++ b/browser/components/downloads/test/browser/browser_first_download_panel.js
@@ -0,0 +1,65 @@
+/* -*- 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/ */
+/* eslint-disable mozilla/no-arbitrary-setTimeout */
+
+/**
+ * Make sure the downloads panel only opens automatically on the first
+ * download it notices. All subsequent downloads, even across sessions, should
+ * not open the panel automatically.
+ */
+add_task(async function test_first_download_panel() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.download.autohideButton", false]],
+ });
+ await promiseButtonShown("downloads-button");
+ // Clear the download panel has shown preference first as this test is used to
+ // verify this preference's behaviour.
+ let oldPrefValue = Services.prefs.getBoolPref("browser.download.panel.shown");
+ Services.prefs.setBoolPref("browser.download.panel.shown", false);
+
+ registerCleanupFunction(async function() {
+ // Clean up when the test finishes.
+ await task_resetState();
+
+ // Set the preference instead of clearing it afterwards to ensure the
+ // right value is used no matter what the default was. This ensures the
+ // panel doesn't appear and affect other tests.
+ Services.prefs.setBoolPref("browser.download.panel.shown", oldPrefValue);
+ });
+
+ // Ensure that state is reset in case previous tests didn't finish.
+ await task_resetState();
+
+ // With this set to false, we should automatically open the panel the first
+ // time a download is started.
+ DownloadsCommon.getData(window).panelHasShownBefore = false;
+
+ info("waiting for panel open");
+ let promise = promisePanelOpened();
+ DownloadsCommon.getData(window)._notifyDownloadEvent("start");
+ await promise;
+
+ // If we got here, that means the panel opened.
+ DownloadsPanel.hidePanel();
+
+ ok(
+ DownloadsCommon.getData(window).panelHasShownBefore,
+ "Should have recorded that the panel was opened on a download."
+ );
+
+ // Next, make sure that if we start another download, we don't open the
+ // panel automatically.
+ let originalOnPopupShown = DownloadsPanel.onPopupShown;
+ DownloadsPanel.onPopupShown = function() {
+ originalOnPopupShown.apply(this, arguments);
+ ok(false, "Should not have opened the downloads panel.");
+ };
+
+ DownloadsCommon.getData(window)._notifyDownloadEvent("start");
+
+ // Wait 2 seconds to ensure that the panel does not open.
+ await new Promise(resolve => setTimeout(resolve, 2000));
+ DownloadsPanel.onPopupShown = originalOnPopupShown;
+});
diff --git a/browser/components/downloads/test/browser/browser_go_to_download_page.js b/browser/components/downloads/test/browser/browser_go_to_download_page.js
new file mode 100644
index 0000000000..6a42a885a0
--- /dev/null
+++ b/browser/components/downloads/test/browser/browser_go_to_download_page.js
@@ -0,0 +1,93 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const ReferrerInfo = Components.Constructor(
+ "@mozilla.org/referrer-info;1",
+ "nsIReferrerInfo",
+ "init"
+);
+
+const TEST_REFERRER = "https://example.com/";
+
+registerCleanupFunction(async function() {
+ await task_resetState();
+ await PlacesUtils.history.clear();
+});
+
+async function addDownload(referrerInfo) {
+ let startTimeMs = Date.now();
+
+ let publicList = await Downloads.getList(Downloads.PUBLIC);
+ let downloadData = {
+ source: {
+ url: "http://www.example.com/test-download.txt",
+ referrerInfo,
+ },
+ target: {
+ path: gTestTargetFile.path,
+ },
+ startTime: new Date(startTimeMs++),
+ };
+ let download = await Downloads.createDownload(downloadData);
+ await publicList.add(download);
+ await download.start();
+}
+
+/**
+ * Make sure "Go To Download Page" is enabled and works as expected.
+ */
+add_task(async function test_go_to_download_page() {
+ let referrerInfo = new ReferrerInfo(
+ Ci.nsIReferrerInfo.NO_REFERRER,
+ true,
+ NetUtil.newURI(TEST_REFERRER)
+ );
+
+ let tabPromise = BrowserTestUtils.waitForNewTab(gBrowser, TEST_REFERRER);
+
+ // Wait for focus first
+ await promiseFocus();
+
+ // Ensure that state is reset in case previous tests didn't finish.
+ await task_resetState();
+
+ // Populate the downloads database with the data required by this test.
+ await addDownload(referrerInfo);
+
+ // Open the user interface and wait for data to be fully loaded.
+ await task_openPanel();
+
+ let win = await openLibrary("Downloads");
+ registerCleanupFunction(function() {
+ win.close();
+ });
+
+ let listbox = win.document.getElementById("downloadsRichListBox");
+ ok(listbox, "download list box present");
+
+ // Select one of the downloads.
+ listbox.itemChildren[0].click();
+
+ let contextMenu = win.document.getElementById("downloadsContextMenu");
+
+ let popupShownPromise = BrowserTestUtils.waitForEvent(
+ contextMenu,
+ "popupshown"
+ );
+ EventUtils.synthesizeMouseAtCenter(
+ listbox.itemChildren[0],
+ { type: "contextmenu", button: 2 },
+ win
+ );
+ await popupShownPromise;
+
+ // Find and click "Go To Download Page"
+ let goToDownloadButton = [...contextMenu.children].find(
+ child => child.command == "downloadsCmd_openReferrer"
+ );
+ goToDownloadButton.click();
+
+ let newTab = await tabPromise;
+ ok(newTab, "Go To Download Page opened a new tab");
+ gBrowser.removeTab(newTab);
+});
diff --git a/browser/components/downloads/test/browser/browser_iframe_gone_mid_download.js b/browser/components/downloads/test/browser/browser_iframe_gone_mid_download.js
new file mode 100644
index 0000000000..582b49d985
--- /dev/null
+++ b/browser/components/downloads/test/browser/browser_iframe_gone_mid_download.js
@@ -0,0 +1,76 @@
+const SAVE_PER_SITE_PREF = "browser.download.lastDir.savePerSite";
+
+function test_deleted_iframe(perSitePref, windowOptions = {}) {
+ return async function() {
+ await SpecialPowers.pushPrefEnv({
+ set: [[SAVE_PER_SITE_PREF, perSitePref]],
+ });
+ let { DownloadLastDir } = ChromeUtils.import(
+ "resource://gre/modules/DownloadLastDir.jsm"
+ );
+
+ let win = await BrowserTestUtils.openNewBrowserWindow(windowOptions);
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ win.gBrowser,
+ "about:mozilla"
+ );
+
+ let doc = tab.linkedBrowser.contentDocument;
+ let iframe = doc.createElement("iframe");
+ doc.body.appendChild(iframe);
+
+ ok(iframe.contentWindow, "iframe should have a window");
+ let gDownloadLastDir = new DownloadLastDir(iframe.contentWindow);
+ let cw = iframe.contentWindow;
+ let promiseIframeWindowGone = new Promise((resolve, reject) => {
+ Services.obs.addObserver(function obs(subject, topic) {
+ if (subject == cw) {
+ Services.obs.removeObserver(obs, topic);
+ resolve();
+ }
+ }, "dom-window-destroyed");
+ });
+ iframe.remove();
+ await promiseIframeWindowGone;
+ cw = null;
+ ok(!iframe.contentWindow, "Managed to destroy iframe");
+
+ let someDir = "blah";
+ try {
+ someDir = await new Promise((resolve, reject) => {
+ gDownloadLastDir.getFileAsync("http://www.mozilla.org/", function(dir) {
+ resolve(dir);
+ });
+ });
+ } catch (ex) {
+ ok(
+ false,
+ "Got an exception trying to get the directory where things should be saved."
+ );
+ Cu.reportError(ex);
+ }
+ // NB: someDir can legitimately be null here when set, hence the 'blah' workaround:
+ isnot(
+ someDir,
+ "blah",
+ "Should get a file even after the window was destroyed."
+ );
+
+ try {
+ gDownloadLastDir.setFile("http://www.mozilla.org/", null);
+ } catch (ex) {
+ ok(
+ false,
+ "Got an exception trying to set the directory where things should be saved."
+ );
+ Cu.reportError(ex);
+ }
+
+ await BrowserTestUtils.closeWindow(win);
+ };
+}
+
+add_task(test_deleted_iframe(false));
+add_task(test_deleted_iframe(false));
+add_task(test_deleted_iframe(true, { private: true }));
+add_task(test_deleted_iframe(true, { private: true }));
diff --git a/browser/components/downloads/test/browser/browser_image_mimetype_issues.js b/browser/components/downloads/test/browser/browser_image_mimetype_issues.js
new file mode 100644
index 0000000000..c0bb0b8d9e
--- /dev/null
+++ b/browser/components/downloads/test/browser/browser_image_mimetype_issues.js
@@ -0,0 +1,133 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_ROOT = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "http://example.com"
+);
+
+var MockFilePicker = SpecialPowers.MockFilePicker;
+MockFilePicker.init(window);
+
+/*
+ * Popular websites implement image optimization as serving files with
+ * extension ".jpg" but content type "image/webp". If we save such images,
+ * we should actually save them with a .webp extension as that is what
+ * they are.
+ */
+
+/**
+ * Test the above with the "save image as" context menu.
+ */
+add_task(async function test_save_image_webp_with_jpeg_extension() {
+ await BrowserTestUtils.withNewTab(
+ `data:text/html,<img src="${TEST_ROOT}/not-really-a-jpeg.jpeg?convert=webp">`,
+ async browser => {
+ let menu = document.getElementById("contentAreaContextMenu");
+ let popupShown = BrowserTestUtils.waitForEvent(menu, "popupshown");
+ BrowserTestUtils.synthesizeMouse(
+ "img",
+ 5,
+ 5,
+ { type: "contextmenu", button: 2 },
+ browser
+ );
+ await popupShown;
+
+ await new Promise(resolve => {
+ MockFilePicker.showCallback = function(fp) {
+ ok(
+ fp.defaultString.endsWith("webp"),
+ `filepicker for image has "${fp.defaultString}", should end in webp`
+ );
+ setTimeout(resolve, 0);
+ return Ci.nsIFilePicker.returnCancel;
+ };
+ EventUtils.synthesizeMouseAtCenter(
+ menu.querySelector("#context-saveimage"),
+ {}
+ );
+ });
+ }
+ );
+});
+
+/**
+ * Test with the "save link as" context menu.
+ */
+add_task(async function test_save_link_webp_with_jpeg_extension() {
+ await BrowserTestUtils.withNewTab(
+ `data:text/html,<a href="${TEST_ROOT}/not-really-a-jpeg.jpeg?convert=webp">Nice image</a>`,
+ async browser => {
+ let menu = document.getElementById("contentAreaContextMenu");
+ let popupShown = BrowserTestUtils.waitForEvent(menu, "popupshown");
+ BrowserTestUtils.synthesizeMouse(
+ "a[href]",
+ 5,
+ 5,
+ { type: "contextmenu", button: 2 },
+ browser
+ );
+ await popupShown;
+
+ await new Promise(resolve => {
+ MockFilePicker.showCallback = function(fp) {
+ ok(
+ fp.defaultString.endsWith("webp"),
+ `filepicker for link has "${fp.defaultString}", should end in webp`
+ );
+ setTimeout(resolve, 0);
+ return Ci.nsIFilePicker.returnCancel;
+ };
+ EventUtils.synthesizeMouseAtCenter(
+ menu.querySelector("#context-savelink"),
+ {}
+ );
+ });
+ }
+ );
+});
+
+/**
+ * Test with the main "save page" command.
+ */
+add_task(async function test_save_page_on_image_document() {
+ await BrowserTestUtils.withNewTab(
+ `${TEST_ROOT}/not-really-a-jpeg.jpeg?convert=webp`,
+ async browser => {
+ await new Promise(resolve => {
+ MockFilePicker.showCallback = function(fp) {
+ ok(
+ fp.defaultString.endsWith("webp"),
+ `filepicker for "save page" has "${fp.defaultString}", should end in webp`
+ );
+ setTimeout(resolve, 0);
+ return Ci.nsIFilePicker.returnCancel;
+ };
+ document.getElementById("Browser:SavePage").doCommand();
+ });
+ }
+ );
+});
+
+/**
+ * Make sure that a valid JPEG image using the .JPG extension doesn't
+ * get it replaced with .jpeg.
+ */
+add_task(async function test_save_page_on_JPEG_image_document() {
+ await BrowserTestUtils.withNewTab(`${TEST_ROOT}/blank.JPG`, async browser => {
+ await new Promise(resolve => {
+ MockFilePicker.showCallback = function(fp) {
+ ok(
+ fp.defaultString.endsWith("JPG"),
+ `filepicker for "save page" has "${fp.defaultString}", should end in JPG`
+ );
+ setTimeout(resolve, 0);
+ return Ci.nsIFilePicker.returnCancel;
+ };
+ document.getElementById("Browser:SavePage").doCommand();
+ });
+ });
+});
diff --git a/browser/components/downloads/test/browser/browser_indicatorDrop.js b/browser/components/downloads/test/browser/browser_indicatorDrop.js
new file mode 100644
index 0000000000..1cbdd89761
--- /dev/null
+++ b/browser/components/downloads/test/browser/browser_indicatorDrop.js
@@ -0,0 +1,38 @@
+/* -*- 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/ */
+
+registerCleanupFunction(async function() {
+ await task_resetState();
+ await PlacesUtils.history.clear();
+});
+
+add_task(async function test_indicatorDrop() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.download.autohideButton", false]],
+ });
+ let downloadButton = document.getElementById("downloads-button");
+ ok(downloadButton, "download button present");
+ await promiseButtonShown(downloadButton.id);
+
+ let EventUtils = {};
+ Services.scriptloader.loadSubScript(
+ "chrome://mochikit/content/tests/SimpleTest/EventUtils.js",
+ EventUtils
+ );
+
+ // Ensure that state is reset in case previous tests didn't finish.
+ await task_resetState();
+
+ await setDownloadDir();
+
+ startServer();
+
+ await simulateDropAndCheck(window, downloadButton, [httpUrl("file1.txt")]);
+ await simulateDropAndCheck(window, downloadButton, [
+ httpUrl("file1.txt"),
+ httpUrl("file2.txt"),
+ httpUrl("file3.txt"),
+ ]);
+});
diff --git a/browser/components/downloads/test/browser/browser_libraryDrop.js b/browser/components/downloads/test/browser/browser_libraryDrop.js
new file mode 100644
index 0000000000..d00ff2b9e6
--- /dev/null
+++ b/browser/components/downloads/test/browser/browser_libraryDrop.js
@@ -0,0 +1,39 @@
+/* -*- 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/ */
+
+registerCleanupFunction(async function() {
+ await task_resetState();
+ await PlacesUtils.history.clear();
+});
+
+add_task(async function test_indicatorDrop() {
+ let EventUtils = {};
+ Services.scriptloader.loadSubScript(
+ "chrome://mochikit/content/tests/SimpleTest/EventUtils.js",
+ EventUtils
+ );
+
+ // Ensure that state is reset in case previous tests didn't finish.
+ await task_resetState();
+
+ await setDownloadDir();
+
+ startServer();
+
+ let win = await openLibrary("Downloads");
+ registerCleanupFunction(function() {
+ win.close();
+ });
+
+ let listBox = win.document.getElementById("downloadsRichListBox");
+ ok(listBox, "download list box present");
+
+ await simulateDropAndCheck(win, listBox, [httpUrl("file1.txt")]);
+ await simulateDropAndCheck(win, listBox, [
+ httpUrl("file1.txt"),
+ httpUrl("file2.txt"),
+ httpUrl("file3.txt"),
+ ]);
+});
diff --git a/browser/components/downloads/test/browser/browser_library_clearall.js b/browser/components/downloads/test/browser/browser_library_clearall.js
new file mode 100644
index 0000000000..41080f23b0
--- /dev/null
+++ b/browser/components/downloads/test/browser/browser_library_clearall.js
@@ -0,0 +1,121 @@
+/* -*- 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/ */
+
+ChromeUtils.defineModuleGetter(
+ this,
+ "PlacesTestUtils",
+ "resource://testing-common/PlacesTestUtils.jsm"
+);
+
+let win;
+
+function waitForChildren(element, callback) {
+ let MutationObserver = element.ownerGlobal.MutationObserver;
+ return new Promise(resolve => {
+ let observer = new MutationObserver(() => {
+ if (callback()) {
+ observer.disconnect();
+ resolve();
+ }
+ });
+ observer.observe(element, { childList: true });
+ });
+}
+
+async function waitForChildrenLength(element, length, callback) {
+ if (element.childElementCount != length) {
+ await waitForChildren(element, () => element.childElementCount == length);
+ }
+}
+
+registerCleanupFunction(async function() {
+ await task_resetState();
+ await PlacesUtils.history.clear();
+});
+
+async function testClearingDownloads(clearCallback) {
+ const DOWNLOAD_DATA = [
+ httpUrl("file1.txt"),
+ httpUrl("file2.txt"),
+ httpUrl("file3.txt"),
+ ];
+
+ let listbox = win.document.getElementById("downloadsRichListBox");
+ ok(listbox, "download list box present");
+
+ let promiseLength = waitForChildrenLength(listbox, DOWNLOAD_DATA.length);
+ await simulateDropAndCheck(win, listbox, DOWNLOAD_DATA);
+ await promiseLength;
+
+ let receivedNotifications = [];
+ let promiseNotification = PlacesTestUtils.waitForNotification(
+ "onDeleteURI",
+ uri => {
+ if (DOWNLOAD_DATA.includes(uri.spec)) {
+ receivedNotifications.push(uri.spec);
+ }
+ return receivedNotifications.length == DOWNLOAD_DATA.length;
+ },
+ "history"
+ );
+
+ promiseLength = waitForChildrenLength(listbox, 0);
+ await clearCallback(listbox);
+ await promiseLength;
+
+ await promiseNotification;
+
+ Assert.deepEqual(
+ receivedNotifications.sort(),
+ DOWNLOAD_DATA.sort(),
+ "Should have received notifications for each URL"
+ );
+}
+
+add_task(async function setup() {
+ // Ensure that state is reset in case previous tests didn't finish.
+ await task_resetState();
+
+ await setDownloadDir();
+
+ startServer();
+
+ win = await openLibrary("Downloads");
+ registerCleanupFunction(function() {
+ win.close();
+ });
+});
+
+add_task(async function test_clear_downloads_toolbar() {
+ await testClearingDownloads(async () => {
+ win.document.getElementById("clearDownloadsButton").click();
+ });
+});
+
+add_task(async function test_clear_downloads_context_menu() {
+ await testClearingDownloads(async listbox => {
+ // Select one of the downloads.
+ listbox.itemChildren[0].click();
+
+ let contextMenu = win.document.getElementById("downloadsContextMenu");
+
+ let popupShownPromise = BrowserTestUtils.waitForEvent(
+ contextMenu,
+ "popupshown"
+ );
+ EventUtils.synthesizeMouseAtCenter(
+ listbox.itemChildren[0],
+ { type: "contextmenu", button: 2 },
+ win
+ );
+ await popupShownPromise;
+
+ // Find the clear context item.
+ let clearDownloadsButton = [...contextMenu.children].find(
+ child => child.command == "downloadsCmd_clearDownloads"
+ );
+ clearDownloadsButton.click();
+ });
+});
diff --git a/browser/components/downloads/test/browser/browser_library_select_all.js b/browser/components/downloads/test/browser/browser_library_select_all.js
new file mode 100644
index 0000000000..b3a0a9f263
--- /dev/null
+++ b/browser/components/downloads/test/browser/browser_library_select_all.js
@@ -0,0 +1,77 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+let gDownloadDir;
+
+add_task(async function setup() {
+ await task_resetState();
+
+ if (!gDownloadDir) {
+ gDownloadDir = await setDownloadDir();
+ }
+
+ await task_addDownloads([
+ {
+ state: DownloadsCommon.DOWNLOAD_FINISHED,
+ target: await createDownloadedFile(
+ PathUtils.join(gDownloadDir, "downloaded_one.txt"),
+ "Test file 1"
+ ),
+ },
+ {
+ state: DownloadsCommon.DOWNLOAD_FINISHED,
+ target: await createDownloadedFile(
+ PathUtils.join(gDownloadDir, "downloaded_two.txt"),
+ "Test file 2"
+ ),
+ },
+ ]);
+ registerCleanupFunction(async function() {
+ await task_resetState();
+ await PlacesUtils.history.clear();
+ });
+});
+
+add_task(async function test_select_all() {
+ let win = await openLibrary("Downloads");
+ registerCleanupFunction(() => {
+ win.close();
+ });
+
+ let listbox = win.document.getElementById("downloadsRichListBox");
+ Assert.ok(listbox, "download list box present");
+ listbox.focus();
+ await TestUtils.waitForCondition(
+ () => listbox.children.length == 2 && listbox.selectedItems.length == 1,
+ "waiting for both items to be present with one selected"
+ );
+ info("Select all the downloads");
+ win.goDoCommand("cmd_selectAll");
+ Assert.equal(
+ listbox.selectedItems.length,
+ listbox.children.length,
+ "All the items should be selected"
+ );
+
+ info("Search for a specific download");
+ let searchBox = win.document.getElementById("searchFilter");
+ searchBox.value = "_one";
+ win.PlacesSearchBox.search(searchBox.value);
+ await TestUtils.waitForCondition(() => {
+ let visibleItems = Array.from(listbox.children).filter(c => !c.hidden);
+ return (
+ visibleItems.length == 1 &&
+ visibleItems[0]._shell.download.target.path.includes("_one")
+ );
+ }, "Waiting for the search to complete");
+ Assert.equal(
+ listbox.selectedItems.length,
+ 0,
+ "Check previous selection has been cleared by the search"
+ );
+ info("Select all the downloads");
+ win.goDoCommand("cmd_selectAll");
+ Assert.equal(listbox.children.length, 2, "Both items are present");
+ Assert.equal(listbox.selectedItems.length, 1, "Only one item is selected");
+ Assert.ok(!listbox.selectedItem.hidden, "The selected item is not hidden");
+});
diff --git a/browser/components/downloads/test/browser/browser_overflow_anchor.js b/browser/components/downloads/test/browser/browser_overflow_anchor.js
new file mode 100644
index 0000000000..e07140c7a2
--- /dev/null
+++ b/browser/components/downloads/test/browser/browser_overflow_anchor.js
@@ -0,0 +1,68 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// This is the same value used by CustomizableUI tests.
+const kForceOverflowWidthPx = 450;
+
+registerCleanupFunction(async function() {
+ // Clean up when the test finishes.
+ await task_resetState();
+});
+
+/**
+ * Make sure the downloads button and indicator overflows into the nav-bar
+ * chevron properly, and then when those buttons are clicked in the overflow
+ * panel that the downloads panel anchors to the chevron`s icon.
+ */
+add_task(async function test_overflow_anchor() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.download.autohideButton", false]],
+ });
+ // Ensure that state is reset in case previous tests didn't finish.
+ await task_resetState();
+
+ // The downloads button should not be overflowed to begin with.
+ let button = CustomizableUI.getWidget("downloads-button").forWindow(window);
+ ok(!button.overflowed, "Downloads button should not be overflowed.");
+ is(
+ button.node.getAttribute("cui-areatype"),
+ "toolbar",
+ "Button should know it's in the toolbar"
+ );
+
+ await gCustomizeMode.addToPanel(button.node);
+
+ let promise = promisePanelOpened();
+ await EventUtils.sendMouseEvent(
+ { type: "mousedown", button: 0 },
+ button.node
+ );
+ info("waiting for panel to open");
+ await promise;
+
+ let panel = DownloadsPanel.panel;
+ let chevron = document.getElementById("nav-bar-overflow-button");
+
+ is(
+ panel.anchorNode,
+ chevron.icon,
+ "Panel should be anchored to the chevron`s icon."
+ );
+
+ DownloadsPanel.hidePanel();
+
+ gCustomizeMode.addToToolbar(button.node);
+
+ // Now try opening the panel again.
+ promise = promisePanelOpened();
+ await EventUtils.sendMouseEvent(
+ { type: "mousedown", button: 0 },
+ button.node
+ );
+ await promise;
+
+ let downloadsAnchor = button.node.badgeStack;
+ is(panel.anchorNode, downloadsAnchor);
+
+ DownloadsPanel.hidePanel();
+});
diff --git a/browser/components/downloads/test/browser/browser_pdfjs_preview.js b/browser/components/downloads/test/browser/browser_pdfjs_preview.js
new file mode 100644
index 0000000000..d183d0bcec
--- /dev/null
+++ b/browser/components/downloads/test/browser/browser_pdfjs_preview.js
@@ -0,0 +1,749 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+let gDownloadDir;
+
+SimpleTest.requestFlakyTimeout(
+ "Giving a chance for possible last-pb-context-exited to occur (Bug 1329912)"
+);
+
+/*
+ Coverage for opening downloaded PDFs from download views
+*/
+
+const TestCases = [
+ {
+ name: "Download panel, default click behavior",
+ whichUI: "downloadPanel",
+ itemSelector: "#downloadsListBox richlistitem .downloadMainArea",
+ async userEvents(itemTarget, win) {
+ EventUtils.synthesizeMouseAtCenter(itemTarget, {}, win);
+ },
+ expected: {
+ downloadCount: 1,
+ newWindow: false,
+ opensTab: true,
+ tabSelected: true,
+ },
+ },
+ {
+ name: "Download panel, system viewer menu items prefd off",
+ whichUI: "downloadPanel",
+ itemSelector: "#downloadsListBox richlistitem .downloadMainArea",
+ async userEvents(itemTarget, win) {
+ EventUtils.synthesizeMouseAtCenter(itemTarget, {}, win);
+ },
+ prefs: [
+ ["browser.download.openInSystemViewerContextMenuItem", false],
+ ["browser.download.alwaysOpenInSystemViewerContextMenuItem", false],
+ ],
+ expected: {
+ downloadCount: 1,
+ newWindow: false,
+ opensTab: true,
+ tabSelected: true,
+ useSystemMenuItemDisabled: true,
+ alwaysMenuItemDisabled: true,
+ },
+ },
+ {
+ name: "Download panel, open from keyboard",
+ whichUI: "downloadPanel",
+ itemSelector: "#downloadsListBox richlistitem .downloadMainArea",
+ async userEvents(itemTarget, win) {
+ itemTarget.focus();
+ EventUtils.synthesizeKey("VK_RETURN", {}, win);
+ },
+ expected: {
+ downloadCount: 1,
+ newWindow: false,
+ opensTab: true,
+ tabSelected: true,
+ },
+ },
+ {
+ name: "Download panel, open in new window",
+ whichUI: "downloadPanel",
+ itemSelector: "#downloadsListBox richlistitem .downloadMainArea",
+ async userEvents(itemTarget, win) {
+ EventUtils.synthesizeMouseAtCenter(itemTarget, { shiftKey: true }, win);
+ },
+ expected: {
+ downloadCount: 1,
+ newWindow: true,
+ opensTab: false,
+ tabSelected: true,
+ },
+ },
+ {
+ name: "Download panel, open foreground tab", // duplicates the default behavior
+ whichUI: "downloadPanel",
+ itemSelector: "#downloadsListBox richlistitem .downloadMainArea",
+ async userEvents(itemTarget, win) {
+ EventUtils.synthesizeMouseAtCenter(
+ itemTarget,
+ { ctrlKey: true, metaKey: true },
+ win
+ );
+ },
+ expected: {
+ downloadCount: 1,
+ newWindow: false,
+ opensTab: true,
+ tabSelected: true,
+ },
+ },
+ {
+ name: "Download panel, open background tab",
+ whichUI: "downloadPanel",
+ itemSelector: "#downloadsListBox richlistitem .downloadMainArea",
+ async userEvents(itemTarget, win) {
+ EventUtils.synthesizeMouseAtCenter(
+ itemTarget,
+ { ctrlKey: true, metaKey: true, shiftKey: true },
+ win
+ );
+ },
+ expected: {
+ downloadCount: 1,
+ newWindow: false,
+ opensTab: true,
+ tabSelected: false,
+ },
+ },
+
+ {
+ name: "Library all downloads dialog, default click behavior",
+ whichUI: "allDownloads",
+ async userEvents(itemTarget, win) {
+ // double click
+ await triggerDblclickOn(itemTarget, {}, win);
+ },
+ expected: {
+ downloadCount: 1,
+ newWindow: false,
+ opensTab: true,
+ tabSelected: true,
+ },
+ },
+ {
+ name: "Library all downloads dialog, system viewer menu items prefd off",
+ whichUI: "allDownloads",
+ async userEvents(itemTarget, win) {
+ // double click
+ await triggerDblclickOn(itemTarget, {}, win);
+ },
+ prefs: [
+ ["browser.download.openInSystemViewerContextMenuItem", false],
+ ["browser.download.alwaysOpenInSystemViewerContextMenuItem", false],
+ ],
+ expected: {
+ downloadCount: 1,
+ newWindow: false,
+ opensTab: true,
+ tabSelected: true,
+ useSystemMenuItemDisabled: true,
+ alwaysMenuItemDisabled: true,
+ },
+ },
+ {
+ name: "Library all downloads dialog, open from keyboard",
+ whichUI: "allDownloads",
+ async userEvents(itemTarget, win) {
+ itemTarget.focus();
+ EventUtils.synthesizeKey("VK_RETURN", {}, win);
+ },
+ expected: {
+ downloadCount: 1,
+ newWindow: false,
+ opensTab: true,
+ tabSelected: true,
+ },
+ },
+ {
+ name: "Library all downloads dialog, open in new window",
+ whichUI: "allDownloads",
+ async userEvents(itemTarget, win) {
+ // double click
+ await triggerDblclickOn(itemTarget, { shiftKey: true }, win);
+ },
+ expected: {
+ downloadCount: 1,
+ newWindow: true,
+ opensTab: false,
+ tabSelected: true,
+ },
+ },
+ {
+ name: "Library all downloads dialog, open foreground tab", // duplicates default behavior
+ whichUI: "allDownloads",
+ async userEvents(itemTarget, win) {
+ // double click
+ await triggerDblclickOn(
+ itemTarget,
+ { ctrlKey: true, metaKey: true },
+ win
+ );
+ },
+ expected: {
+ downloadCount: 1,
+ newWindow: false,
+ opensTab: true,
+ tabSelected: true,
+ },
+ },
+ {
+ name: "Library all downloads dialog, open background tab",
+ whichUI: "allDownloads",
+ async userEvents(itemTarget, win) {
+ // double click
+ await triggerDblclickOn(
+ itemTarget,
+ { ctrlKey: true, metaKey: true, shiftKey: true },
+ win
+ );
+ },
+ expected: {
+ downloadCount: 1,
+ newWindow: false,
+ opensTab: true,
+ tabSelected: false,
+ },
+ },
+ {
+ name: "about:downloads, default click behavior",
+ whichUI: "aboutDownloads",
+ itemSelector: "#downloadsRichListBox richlistitem .downloadContainer",
+ async userEvents(itemSelector, win) {
+ let browser = win.gBrowser.selectedBrowser;
+ is(browser.currentURI.spec, "about:downloads");
+ await contentTriggerDblclickOn(itemSelector, {}, browser);
+ },
+ expected: {
+ downloadCount: 1,
+ newWindow: false,
+ opensTab: true,
+ tabSelected: true,
+ },
+ },
+ {
+ name: "about:downloads, system viewer menu items prefd off",
+ whichUI: "aboutDownloads",
+ itemSelector: "#downloadsRichListBox richlistitem .downloadContainer",
+ async userEvents(itemSelector, win) {
+ let browser = win.gBrowser.selectedBrowser;
+ is(browser.currentURI.spec, "about:downloads");
+ await contentTriggerDblclickOn(itemSelector, {}, browser);
+ },
+ prefs: [
+ ["browser.download.openInSystemViewerContextMenuItem", false],
+ ["browser.download.alwaysOpenInSystemViewerContextMenuItem", false],
+ ],
+ expected: {
+ downloadCount: 1,
+ newWindow: false,
+ opensTab: true,
+ tabSelected: true,
+ useSystemMenuItemDisabled: true,
+ alwaysMenuItemDisabled: true,
+ },
+ },
+ {
+ name: "about:downloads, open in new window",
+ whichUI: "aboutDownloads",
+ itemSelector: "#downloadsRichListBox richlistitem .downloadContainer",
+ async userEvents(itemSelector, win) {
+ let browser = win.gBrowser.selectedBrowser;
+ is(browser.currentURI.spec, "about:downloads");
+ await contentTriggerDblclickOn(itemSelector, { shiftKey: true }, browser);
+ },
+ expected: {
+ downloadCount: 1,
+ newWindow: true,
+ opensTab: false,
+ tabSelected: true,
+ },
+ },
+ {
+ name: "about:downloads, open in foreground tab",
+ whichUI: "aboutDownloads",
+ itemSelector: "#downloadsRichListBox richlistitem .downloadContainer",
+ async userEvents(itemSelector, win) {
+ let browser = win.gBrowser.selectedBrowser;
+ is(browser.currentURI.spec, "about:downloads");
+ await contentTriggerDblclickOn(
+ itemSelector,
+ { ctrlKey: true, metaKey: true },
+ browser
+ );
+ },
+ expected: {
+ downloadCount: 1,
+ newWindow: false,
+ opensTab: true,
+ tabSelected: true,
+ },
+ },
+ {
+ name: "about:downloads, open in background tab",
+ whichUI: "aboutDownloads",
+ itemSelector: "#downloadsRichListBox richlistitem .downloadContainer",
+ async userEvents(itemSelector, win) {
+ let browser = win.gBrowser.selectedBrowser;
+ is(browser.currentURI.spec, "about:downloads");
+ await contentTriggerDblclickOn(
+ itemSelector,
+ { ctrlKey: true, metaKey: true, shiftKey: true },
+ browser
+ );
+ },
+ expected: {
+ downloadCount: 1,
+ newWindow: false,
+ opensTab: true,
+ tabSelected: false,
+ },
+ },
+ {
+ name: "Private download in about:downloads, opens in new private window",
+ skip: true, // Bug 1641770
+ whichUI: "aboutDownloads",
+ itemSelector: "#downloadsRichListBox richlistitem .downloadContainer",
+ async userEvents(itemSelector, win) {
+ let browser = win.gBrowser.selectedBrowser;
+ is(browser.currentURI.spec, "about:downloads");
+ await contentTriggerDblclickOn(itemSelector, { shiftKey: true }, browser);
+ },
+ isPrivate: true,
+ expected: {
+ downloadCount: 1,
+ newWindow: true,
+ opensTab: false,
+ tabSelected: true,
+ },
+ },
+];
+
+function triggerDblclickOn(target, modifiers = {}, win) {
+ let promise = BrowserTestUtils.waitForEvent(target, "dblclick");
+ EventUtils.synthesizeMouseAtCenter(
+ target,
+ Object.assign({ clickCount: 1 }, modifiers),
+ win
+ );
+ EventUtils.synthesizeMouseAtCenter(
+ target,
+ Object.assign({ clickCount: 2 }, modifiers),
+ win
+ );
+ return promise;
+}
+
+function contentTriggerDblclickOn(selector, eventModifiers = {}, browser) {
+ return SpecialPowers.spawn(
+ browser,
+ [selector, eventModifiers],
+ async function(itemSelector, modifiers) {
+ const EventUtils = ContentTaskUtils.getEventUtils(content);
+ let itemTarget = content.document.querySelector(itemSelector);
+ ok(itemTarget, "Download item target exists");
+
+ let doubleClicked = ContentTaskUtils.waitForEvent(itemTarget, "dblclick");
+ EventUtils.synthesizeMouseAtCenter(
+ itemTarget,
+ Object.assign({ clickCount: 1 }, modifiers),
+ content
+ );
+ EventUtils.synthesizeMouseAtCenter(
+ itemTarget,
+ Object.assign({ clickCount: 2 }, modifiers),
+ content
+ );
+ info("Waiting for double-click content task");
+ await doubleClicked;
+ }
+ );
+}
+
+async function verifyContextMenu(contextMenu, expected = {}) {
+ info("verifyContextMenu with expected: " + JSON.stringify(expected, null, 2));
+ let alwaysMenuItem = contextMenu.querySelector(
+ ".downloadAlwaysUseSystemDefaultMenuItem"
+ );
+ let useSystemMenuItem = contextMenu.querySelector(
+ ".downloadUseSystemDefaultMenuItem"
+ );
+ info("Waiting for the context menu to show up");
+ await TestUtils.waitForCondition(
+ () => BrowserTestUtils.is_visible(contextMenu),
+ "The context menu is visible"
+ );
+ await TestUtils.waitForTick();
+
+ info("Checking visibility of the system viewer menu items");
+ is(
+ BrowserTestUtils.is_hidden(useSystemMenuItem),
+ expected.useSystemMenuItemDisabled,
+ `The 'Use system viewer' menu item was ${
+ expected.useSystemMenuItemDisabled ? "hidden" : "visible"
+ }`
+ );
+ is(
+ BrowserTestUtils.is_hidden(alwaysMenuItem),
+ expected.alwaysMenuItemDisabled,
+ `The 'Use system viewer' menu item was ${
+ expected.alwaysMenuItemDisabled ? "hidden" : "visible"
+ }`
+ );
+
+ if (!expected.useSystemMenuItemDisabled && expected.alwaysChecked) {
+ is(
+ alwaysMenuItem.getAttribute("checked"),
+ "true",
+ "The 'Always...' menu item is checked"
+ );
+ } else if (!expected.useSystemMenuItemDisabled) {
+ ok(
+ !alwaysMenuItem.hasAttribute("checked"),
+ "The 'Always...' menu item not checked"
+ );
+ }
+}
+
+async function addPDFDownload(itemData) {
+ let startTimeMs = Date.now();
+ info("addPDFDownload with itemData: " + JSON.stringify(itemData, null, 2));
+
+ let downloadPathname = OS.Path.join(gDownloadDir, itemData.targetFilename);
+ delete itemData.targetFilename;
+
+ info("Creating saved download file at:" + downloadPathname);
+ let pdfFile = await createDownloadedFile(downloadPathname, DATA_PDF);
+ info("Created file at:" + pdfFile.path);
+
+ let downloadList = await Downloads.getList(
+ itemData.isPrivate ? Downloads.PRIVATE : Downloads.PUBLIC
+ );
+ let download = {
+ source: {
+ url: "https://example.com/some.pdf",
+ isPrivate: itemData.isPrivate,
+ },
+ target: {
+ path: pdfFile.path,
+ },
+ succeeded: DownloadsCommon.DOWNLOAD_FINISHED,
+ canceled: false,
+ error: null,
+ hasPartialData: false,
+ hasBlockedData: itemData.hasBlockedData || false,
+ startTime: new Date(startTimeMs++),
+ ...itemData,
+ };
+ if (itemData.errorObj) {
+ download.errorObj = itemData.errorObj;
+ }
+
+ await downloadList.add(await Downloads.createDownload(download));
+ return download;
+}
+
+async function testSetup() {
+ // remove download files, empty out collections
+ let downloadList = await Downloads.getList(Downloads.ALL);
+ let downloadCount = (await downloadList.getAll()).length;
+ is(downloadCount, 0, "At the start of the test, there should be 0 downloads");
+
+ await task_resetState();
+ if (!gDownloadDir) {
+ gDownloadDir = await setDownloadDir();
+ }
+ info("Created download directory: " + gDownloadDir);
+}
+
+async function openDownloadPanel(expectedItemCount) {
+ // Open the user interface and wait for data to be fully loaded.
+ let richlistbox = document.getElementById("downloadsListBox");
+ await task_openPanel();
+ await TestUtils.waitForCondition(
+ () => richlistbox.childElementCount == expectedItemCount
+ );
+}
+
+async function testOpenPDFPreview({
+ name,
+ whichUI,
+ downloadProperties,
+ itemSelector,
+ expected,
+ prefs = [],
+ userEvents,
+ isPrivate,
+}) {
+ info("Test case: " + name);
+ // Wait for focus first
+ await promiseFocus();
+ await testSetup();
+ if (prefs.length) {
+ await SpecialPowers.pushPrefEnv({
+ set: prefs,
+ });
+ }
+
+ // Populate downloads database with the data required by this test.
+ info("Adding download objects");
+ if (!downloadProperties) {
+ downloadProperties = {
+ targetFilename: "downloaded.pdf",
+ };
+ }
+ let download = await addPDFDownload({
+ ...downloadProperties,
+ isPrivate,
+ });
+ info("Got download pathname:" + download.target.path);
+ is(
+ !!download.source.isPrivate,
+ !!isPrivate,
+ `Added download is ${isPrivate ? "private" : "not private"} as expected`
+ );
+ let downloadList = await Downloads.getList(
+ isPrivate ? Downloads.PRIVATE : Downloads.PUBLIC
+ );
+ let downloads = await downloadList.getAll();
+ is(
+ downloads.length,
+ expected.downloadCount,
+ `${isPrivate ? "Private" : "Public"} list has expected ${
+ downloads.length
+ } downloads`
+ );
+
+ let pdfFileURI = NetUtil.newURI(new FileUtils.File(download.target.path));
+ info("pdfFileURI:" + pdfFileURI.spec);
+
+ let uiWindow = window;
+ let previewWindow = window;
+ // we never want to unload the test browser by loading the file: URI into it
+ await BrowserTestUtils.withNewTab("about:blank", async initialBrowser => {
+ let previewTab;
+ let previewHappened;
+
+ if (expected.newWindow) {
+ info(
+ "previewHappened will wait for new browser window with url: " +
+ pdfFileURI.spec
+ );
+ // wait for a new browser window
+ previewHappened = BrowserTestUtils.waitForNewWindow({
+ anyWindow: true,
+ url: pdfFileURI.spec,
+ });
+ } else if (expected.opensTab) {
+ // wait for a tab to be opened
+ info("previewHappened will wait for tab with URI:" + pdfFileURI.spec);
+ previewHappened = BrowserTestUtils.waitForNewTab(
+ gBrowser,
+ pdfFileURI.spec,
+ false, // dont wait for load
+ true // any tab, not just the next one
+ );
+ } else {
+ info(
+ "previewHappened will wait to load " +
+ pdfFileURI.spec +
+ " into the current tab"
+ );
+ previewHappened = BrowserTestUtils.browserLoaded(
+ initialBrowser,
+ false,
+ pdfFileURI.spec
+ );
+ }
+
+ let itemTarget;
+ let contextMenu;
+
+ switch (whichUI) {
+ case "downloadPanel":
+ info("Opening download panel");
+ await openDownloadPanel(expected.downloadCount);
+ info("/Opening download panel");
+ itemTarget = document.querySelector(itemSelector);
+ contextMenu = uiWindow.document.querySelector("#downloadsContextMenu");
+
+ break;
+ case "allDownloads":
+ // we'll be interacting with the library dialog
+ uiWindow = await openLibrary("Downloads");
+
+ let listbox = uiWindow.document.getElementById("downloadsRichListBox");
+ ok(listbox, "download list box present");
+ // wait for the expected number of items in the view,
+ // and for the first item to be visible && clickable
+ await TestUtils.waitForCondition(() => {
+ return (
+ listbox.itemChildren.length == expected.downloadCount &&
+ BrowserTestUtils.is_visible(listbox.itemChildren[0])
+ );
+ });
+ itemTarget = listbox.itemChildren[0];
+ contextMenu = uiWindow.document.querySelector("#downloadsContextMenu");
+
+ break;
+ case "aboutDownloads":
+ info("Preparing about:downloads browser window");
+
+ // Because of bug 1329912, we sometimes get a bogus last-pb-context-exited notification
+ // which removes all the private downloads and about:downloads renders a empty list
+ // we'll allow time for that to happen before loading about:downloads
+ let pbExitedOrTimeout = isPrivate
+ ? new Promise(resolve => {
+ const topic = "last-pb-context-exited";
+ const ENOUGH_TIME = 1000;
+ function observer() {
+ info(`Bogus ${topic} observed`);
+ done();
+ }
+ function done() {
+ clearTimeout(timerId);
+ Services.obs.removeObserver(observer, topic);
+ resolve();
+ }
+ /* eslint-disable mozilla/no-arbitrary-setTimeout */
+ const timerId = setTimeout(done, ENOUGH_TIME);
+ Services.obs.addObserver(observer, "last-pb-context-exited");
+ })
+ : Promise.resolve();
+
+ if (isPrivate) {
+ uiWindow = await BrowserTestUtils.openNewBrowserWindow({
+ private: true,
+ });
+ }
+ info(
+ "in aboutDownloads, initially there are tabs: " +
+ uiWindow.gBrowser.tabs.length
+ );
+
+ let browser = uiWindow.gBrowser.selectedBrowser;
+ await pbExitedOrTimeout;
+
+ info("Loading about:downloads");
+ let downloadsLoaded = BrowserTestUtils.waitForEvent(
+ browser,
+ "InitialDownloadsLoaded",
+ true
+ );
+ BrowserTestUtils.loadURI(browser, "about:downloads");
+ await BrowserTestUtils.browserLoaded(browser);
+ info("waiting for downloadsLoaded");
+ await downloadsLoaded;
+
+ await ContentTask.spawn(
+ browser,
+ [expected.downloadCount],
+ async function awaitListItems(expectedCount) {
+ await ContentTaskUtils.waitForCondition(
+ () =>
+ content.document.getElementById("downloadsRichListBox")
+ .childElementCount == expectedCount,
+ `Await ${expectedCount} download list items`
+ );
+ }
+ );
+ break;
+ }
+
+ if (contextMenu) {
+ info("trigger the contextmenu");
+ await openContextMenu(itemTarget || itemSelector, uiWindow);
+ info("context menu should be open, verify its menu items");
+ let expectedValues = {
+ useSystemMenuItemDisabled: false,
+ alwaysMenuItemDisabled: false,
+ ...expected,
+ };
+ await verifyContextMenu(contextMenu, expectedValues);
+ contextMenu.hidePopup();
+ } else {
+ todo(contextMenu, "No context menu checks for test: " + name);
+ }
+
+ info("Executing user events");
+ await userEvents(itemTarget || itemSelector, uiWindow);
+
+ info("Waiting for previewHappened");
+ let results = await previewHappened;
+ if (expected.newWindow) {
+ previewWindow = results;
+ info("New window expected, got previewWindow? " + previewWindow);
+ }
+ previewTab =
+ previewWindow.gBrowser.tabs[previewWindow.gBrowser.tabs.length - 1];
+ ok(previewTab, "Got preview tab");
+
+ let isSelected = previewWindow.gBrowser.selectedTab == previewTab;
+ if (expected.tabSelected) {
+ ok(isSelected, "The preview tab was selected");
+ } else {
+ ok(!isSelected, "The preview tab was opened in the background");
+ }
+
+ is(
+ previewTab.linkedBrowser.currentURI.spec,
+ pdfFileURI.spec,
+ "previewTab has the expected currentURI"
+ );
+
+ is(
+ PrivateBrowsingUtils.isBrowserPrivate(previewTab.linkedBrowser),
+ !!isPrivate,
+ `The preview tab was ${isPrivate ? "private" : "not private"} as expected`
+ );
+
+ info("cleaning up");
+ if (whichUI == "downloadPanel") {
+ DownloadsPanel.hidePanel();
+ }
+ let lastPBContextExitedPromise = isPrivate
+ ? TestUtils.topicObserved("last-pb-context-exited").then(() =>
+ TestUtils.waitForTick()
+ )
+ : Promise.resolve();
+
+ info("Test opened a new UI window? " + (uiWindow !== window));
+ if (uiWindow !== window) {
+ info("Closing uiWindow");
+ await BrowserTestUtils.closeWindow(uiWindow);
+ }
+ if (expected.newWindow) {
+ // will also close the previewTab
+ await BrowserTestUtils.closeWindow(previewWindow);
+ } else {
+ await BrowserTestUtils.removeTab(previewTab);
+ }
+ info("Waiting for lastPBContextExitedPromise");
+ await lastPBContextExitedPromise;
+ });
+ await downloadList.removeFinished();
+ if (prefs.length) {
+ await SpecialPowers.popPrefEnv();
+ }
+}
+
+// register the tests
+for (let testData of TestCases) {
+ if (testData.skip) {
+ info("Skipping test:" + testData.name);
+ continue;
+ }
+ // use the 'name' property of each test case as the test function name
+ // so we get useful logs
+ let tmp = {
+ async [testData.name]() {
+ await testOpenPDFPreview(testData);
+ },
+ };
+ add_task(tmp[testData.name]);
+}
diff --git a/browser/components/downloads/test/browser/foo.txt b/browser/components/downloads/test/browser/foo.txt
new file mode 100644
index 0000000000..77e7195596
--- /dev/null
+++ b/browser/components/downloads/test/browser/foo.txt
@@ -0,0 +1 @@
+Dummy content for unknownContentType_dialog_layout_data.txt
diff --git a/browser/components/downloads/test/browser/foo.txt^headers^ b/browser/components/downloads/test/browser/foo.txt^headers^
new file mode 100644
index 0000000000..2a3c472e26
--- /dev/null
+++ b/browser/components/downloads/test/browser/foo.txt^headers^
@@ -0,0 +1,2 @@
+Content-Type: text/plain
+Content-Disposition: attachment
diff --git a/browser/components/downloads/test/browser/head.js b/browser/components/downloads/test/browser/head.js
new file mode 100644
index 0000000000..a184bd1582
--- /dev/null
+++ b/browser/components/downloads/test/browser/head.js
@@ -0,0 +1,284 @@
+/* -*- 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.defineModuleGetter(
+ this,
+ "Downloads",
+ "resource://gre/modules/Downloads.jsm"
+);
+ChromeUtils.defineModuleGetter(
+ this,
+ "DownloadsCommon",
+ "resource:///modules/DownloadsCommon.jsm"
+);
+ChromeUtils.defineModuleGetter(
+ this,
+ "FileUtils",
+ "resource://gre/modules/FileUtils.jsm"
+);
+ChromeUtils.defineModuleGetter(
+ this,
+ "PlacesUtils",
+ "resource://gre/modules/PlacesUtils.jsm"
+);
+ChromeUtils.defineModuleGetter(
+ this,
+ "HttpServer",
+ "resource://testing-common/httpd.js"
+);
+
+var gTestTargetFile = FileUtils.getFile("TmpD", ["dm-ui-test.file"]);
+gTestTargetFile.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, FileUtils.PERMS_FILE);
+
+// The file may have been already deleted when removing a paused download.
+registerCleanupFunction(() =>
+ OS.File.remove(gTestTargetFile.path, { ignoreAbsent: true })
+);
+
+const DATA_PDF = atob(
+ "JVBERi0xLjANCjEgMCBvYmo8PC9UeXBlL0NhdGFsb2cvUGFnZXMgMiAwIFI+PmVuZG9iaiAyIDAgb2JqPDwvVHlwZS9QYWdlcy9LaWRzWzMgMCBSXS9Db3VudCAxPj5lbmRvYmogMyAwIG9iajw8L1R5cGUvUGFnZS9NZWRpYUJveFswIDAgMyAzXT4+ZW5kb2JqDQp4cmVmDQowIDQNCjAwMDAwMDAwMDAgNjU1MzUgZg0KMDAwMDAwMDAxMCAwMDAwMCBuDQowMDAwMDAwMDUzIDAwMDAwIG4NCjAwMDAwMDAxMDIgMDAwMDAgbg0KdHJhaWxlcjw8L1NpemUgNC9Sb290IDEgMCBSPj4NCnN0YXJ0eHJlZg0KMTQ5DQolRU9G"
+);
+
+// Asynchronous support subroutines
+
+async function createDownloadedFile(pathname, contents) {
+ let encoder = new TextEncoder();
+ 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 OS.File.writeAtomic(pathname, encoder.encode(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) {
+ publicList.remove(download);
+ await download.finalize(true);
+ }
+
+ 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,
+ error:
+ item.state == DownloadsCommon.DOWNLOAD_FAILED
+ ? new Error("Failed.")
+ : null,
+ hasPartialData: item.state == DownloadsCommon.DOWNLOAD_PAUSED,
+ hasBlockedData: item.hasBlockedData || false,
+ 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 = await PathUtils.getTempDir();
+ tmpDir = PathUtils.join(
+ tmpDir,
+ "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) {
+ Cu.reportError(e);
+ }
+ });
+ Services.prefs.setIntPref("browser.download.folderList", 2);
+ Services.prefs.setCharPref("browser.download.dir", tmpDir);
+ return tmpDir;
+}
+
+let gHttpServer = null;
+function startServer() {
+ gHttpServer = new HttpServer();
+ gHttpServer.start(-1);
+ registerCleanupFunction(async function() {
+ await new Promise(function(resolve) {
+ gHttpServer.stop(resolve);
+ });
+ });
+
+ 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();
+ });
+}
+
+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 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");
+ }
+}
diff --git a/browser/components/downloads/test/browser/not-really-a-jpeg.jpeg b/browser/components/downloads/test/browser/not-really-a-jpeg.jpeg
new file mode 100644
index 0000000000..04b7f003b4
--- /dev/null
+++ b/browser/components/downloads/test/browser/not-really-a-jpeg.jpeg
Binary files differ
diff --git a/browser/components/downloads/test/browser/not-really-a-jpeg.jpeg^headers^ b/browser/components/downloads/test/browser/not-really-a-jpeg.jpeg^headers^
new file mode 100644
index 0000000000..c1a7794310
--- /dev/null
+++ b/browser/components/downloads/test/browser/not-really-a-jpeg.jpeg^headers^
@@ -0,0 +1,2 @@
+Content-Type: image/webp
+
diff --git a/browser/components/downloads/test/unit/head.js b/browser/components/downloads/test/unit/head.js
new file mode 100644
index 0000000000..6769bfe79a
--- /dev/null
+++ b/browser/components/downloads/test/unit/head.js
@@ -0,0 +1,89 @@
+const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+
+ChromeUtils.defineModuleGetter(
+ this,
+ "Downloads",
+ "resource://gre/modules/Downloads.jsm"
+);
+ChromeUtils.defineModuleGetter(
+ this,
+ "DownloadsCommon",
+ "resource:///modules/DownloadsCommon.jsm"
+);
+ChromeUtils.defineModuleGetter(
+ this,
+ "FileUtils",
+ "resource://gre/modules/FileUtils.jsm"
+);
+ChromeUtils.defineModuleGetter(this, "OS", "resource://gre/modules/osfile.jsm");
+ChromeUtils.defineModuleGetter(
+ this,
+ "FileTestUtils",
+ "resource://testing-common/FileTestUtils.jsm"
+);
+ChromeUtils.defineModuleGetter(
+ this,
+ "NetUtil",
+ "resource://gre/modules/NetUtil.jsm"
+);
+ChromeUtils.defineModuleGetter(
+ this,
+ "TestUtils",
+ "resource://testing-common/TestUtils.jsm"
+);
+
+async function createDownloadedFile(pathname, contents) {
+ info("createDownloadedFile: " + pathname);
+ let encoder = new TextEncoder();
+ let file = new FileUtils.File(pathname);
+ if (file.exists()) {
+ info(`File at ${pathname} already exists`);
+ if (!contents) {
+ ok(
+ false,
+ `A file already exists at ${pathname}, but createDownloadedFile was asked to create a non-existant file`
+ );
+ }
+ }
+ if (contents) {
+ await OS.File.writeAtomic(pathname, encoder.encode(contents));
+ ok(file.exists(), `Created ${pathname}`);
+ }
+ // No post-test cleanup necessary; tmp downloads directory is already removed after each test
+ return file;
+}
+
+let gDownloadDir;
+
+async function setDownloadDir() {
+ let tmpDir = Services.dirsvc.get("TmpD", Ci.nsIFile).path;
+ tmpDir = PathUtils.join(
+ tmpDir,
+ "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) {
+ Cu.reportError(e);
+ }
+ });
+ Services.prefs.setIntPref("browser.download.folderList", 2);
+ Services.prefs.setCharPref("browser.download.dir", tmpDir);
+ return tmpDir;
+}
+
+/**
+ * All the tests are implemented with add_task, this starts them automatically.
+ */
+function run_test() {
+ do_get_profile();
+ run_next_test();
+}
+
+add_task(async function test_common_initialize() {
+ gDownloadDir = await setDownloadDir();
+ Services.prefs.setCharPref("browser.download.loglevel", "Debug");
+});
diff --git a/browser/components/downloads/test/unit/test_DownloadsCommon_getMimeInfo.js b/browser/components/downloads/test/unit/test_DownloadsCommon_getMimeInfo.js
new file mode 100644
index 0000000000..89d1bf708a
--- /dev/null
+++ b/browser/components/downloads/test/unit/test_DownloadsCommon_getMimeInfo.js
@@ -0,0 +1,175 @@
+const DATA_PDF = atob(
+ "JVBERi0xLjANCjEgMCBvYmo8PC9UeXBlL0NhdGFsb2cvUGFnZXMgMiAwIFI+PmVuZG9iaiAyIDAgb2JqPDwvVHlwZS9QYWdlcy9LaWRzWzMgMCBSXS9Db3VudCAxPj5lbmRvYmogMyAwIG9iajw8L1R5cGUvUGFnZS9NZWRpYUJveFswIDAgMyAzXT4+ZW5kb2JqDQp4cmVmDQowIDQNCjAwMDAwMDAwMDAgNjU1MzUgZg0KMDAwMDAwMDAxMCAwMDAwMCBuDQowMDAwMDAwMDUzIDAwMDAwIG4NCjAwMDAwMDAxMDIgMDAwMDAgbg0KdHJhaWxlcjw8L1NpemUgNC9Sb290IDEgMCBSPj4NCnN0YXJ0eHJlZg0KMTQ5DQolRU9G"
+);
+
+const DOWNLOAD_TEMPLATE = {
+ source: {
+ url: "https://example.com/download",
+ },
+ target: {
+ path: "",
+ },
+ contentType: "text/plain",
+ succeeded: DownloadsCommon.DOWNLOAD_FINISHED,
+ canceled: false,
+ error: null,
+ hasPartialData: false,
+ hasBlockedData: false,
+ startTime: new Date(Date.now() - 1000),
+};
+
+const TESTFILES = {
+ "download-test.txt": "Text file contents\n",
+ "download-test.pdf": DATA_PDF,
+ "download-test.PDF": DATA_PDF,
+ "download-test.xxunknown": "Unknown file contents\n",
+ "download-test": "No extension file contents\n",
+};
+let gPublicList;
+
+add_task(async function test_setup() {
+ Assert.ok(
+ OS.Constants.Path.profileDir,
+ "profileDir: " + OS.Constants.Path.profileDir
+ );
+ for (let [filename, contents] of Object.entries(TESTFILES)) {
+ TESTFILES[filename] = await createDownloadedFile(
+ OS.Path.join(gDownloadDir, filename),
+ contents
+ );
+ }
+ gPublicList = await Downloads.getList(Downloads.PUBLIC);
+});
+
+const TESTCASES = [
+ {
+ name: "Check returned value is null when the download did not succeed",
+ testFile: "download-test.txt",
+ contentType: "text/plain",
+ succeeded: false,
+ expected: null,
+ },
+ {
+ name:
+ "Check correct mime-info is returned when download contentType is unambiguous",
+ testFile: "download-test.txt",
+ contentType: "text/plain",
+ expected: {
+ type: "text/plain",
+ },
+ },
+ {
+ name:
+ "Returns correct mime-info from file extension when download contentType is missing",
+ testFile: "download-test.pdf",
+ contentType: undefined,
+ expected: {
+ type: "application/pdf",
+ },
+ },
+ {
+ name: "Returns correct mime-info from file extension case-insensitively",
+ testFile: "download-test.PDF",
+ contentType: undefined,
+ expected: {
+ type: "application/pdf",
+ },
+ },
+ {
+ name:
+ "Returns null when contentType is missing and file extension is unknown",
+ testFile: "download-test.xxunknown",
+ contentType: undefined,
+ expected: null,
+ },
+ {
+ name:
+ "Returns contentType when contentType is ambiguous and file extension is unknown",
+ testFile: "download-test.xxunknown",
+ contentType: "application/octet-stream",
+ expected: {
+ type: "application/octet-stream",
+ },
+ },
+ {
+ name:
+ "Returns contentType when contentType is ambiguous and there is no file extension",
+ testFile: "download-test",
+ contentType: "application/octet-stream",
+ expected: {
+ type: "application/octet-stream",
+ },
+ },
+ {
+ name: "Returns null when there's no contentType and no file extension",
+ testFile: "download-test",
+ contentType: undefined,
+ expected: null,
+ },
+];
+
+// add tests for each of the generic mime-types we recognize,
+// to ensure they prefer the associated mime-type of the target file extension
+for (let type of [
+ "application/octet-stream",
+ "binary/octet-stream",
+ "application/unknown",
+]) {
+ TESTCASES.push({
+ name: `Returns correct mime-info from file extension when contentType is generic (${type})`,
+ testFile: "download-test.pdf",
+ contentType: type,
+ expected: {
+ type: "application/pdf",
+ },
+ });
+}
+
+for (let testData of TESTCASES) {
+ let tmp = {
+ async [testData.name]() {
+ info("testing with: " + JSON.stringify(testData));
+ await test_getMimeInfo_basic_function(testData);
+ },
+ };
+ add_task(tmp[testData.name]);
+}
+
+/**
+ * Sanity test the DownloadsCommon.getMimeInfo method with test parameters
+ */
+async function test_getMimeInfo_basic_function(testData) {
+ let downloadData = {
+ ...DOWNLOAD_TEMPLATE,
+ source: "source" in testData ? testData.source : DOWNLOAD_TEMPLATE.source,
+ succeeded:
+ "succeeded" in testData
+ ? testData.succeeded
+ : DOWNLOAD_TEMPLATE.succeeded,
+ target: TESTFILES[testData.testFile],
+ contentType: testData.contentType,
+ };
+ Assert.ok(downloadData.target instanceof Ci.nsIFile, "target is a nsIFile");
+ let download = await Downloads.createDownload(downloadData);
+ await gPublicList.add(download);
+ await download.refresh();
+
+ Assert.ok(
+ await OS.File.exists(download.target.path),
+ "The file should actually exist."
+ );
+ let result = await DownloadsCommon.getMimeInfo(download);
+ if (testData.expected) {
+ Assert.equal(
+ result.type,
+ testData.expected.type,
+ "Got expected mimeInfo.type"
+ );
+ } else {
+ Assert.equal(
+ result,
+ null,
+ `Expected null, got object with type: ${result?.type}`
+ );
+ }
+}
diff --git a/browser/components/downloads/test/unit/test_DownloadsCommon_isFileOfType.js b/browser/components/downloads/test/unit/test_DownloadsCommon_isFileOfType.js
new file mode 100644
index 0000000000..e921d7b540
--- /dev/null
+++ b/browser/components/downloads/test/unit/test_DownloadsCommon_isFileOfType.js
@@ -0,0 +1,153 @@
+const DATA_PDF = atob(
+ "JVBERi0xLjANCjEgMCBvYmo8PC9UeXBlL0NhdGFsb2cvUGFnZXMgMiAwIFI+PmVuZG9iaiAyIDAgb2JqPDwvVHlwZS9QYWdlcy9LaWRzWzMgMCBSXS9Db3VudCAxPj5lbmRvYmogMyAwIG9iajw8L1R5cGUvUGFnZS9NZWRpYUJveFswIDAgMyAzXT4+ZW5kb2JqDQp4cmVmDQowIDQNCjAwMDAwMDAwMDAgNjU1MzUgZg0KMDAwMDAwMDAxMCAwMDAwMCBuDQowMDAwMDAwMDUzIDAwMDAwIG4NCjAwMDAwMDAxMDIgMDAwMDAgbg0KdHJhaWxlcjw8L1NpemUgNC9Sb290IDEgMCBSPj4NCnN0YXJ0eHJlZg0KMTQ5DQolRU9G"
+);
+
+const DOWNLOAD_TEMPLATE = {
+ source: {
+ url: "https://download-test.com/download",
+ },
+ target: {
+ path: "",
+ },
+ contentType: "text/plain",
+ succeeded: DownloadsCommon.DOWNLOAD_FINISHED,
+ canceled: false,
+ error: null,
+ hasPartialData: false,
+ hasBlockedData: false,
+ startTime: new Date(Date.now() - 1000),
+};
+
+const TESTFILES = {
+ "download-test.pdf": DATA_PDF,
+ "download-test.xxunknown": DATA_PDF,
+ "download-test-missing.pdf": null,
+};
+let gPublicList;
+
+add_task(async function test_setup() {
+ Assert.ok(
+ OS.Constants.Path.profileDir,
+ "profileDir: " + OS.Constants.Path.profileDir
+ );
+ for (let [filename, contents] of Object.entries(TESTFILES)) {
+ TESTFILES[filename] = await createDownloadedFile(
+ OS.Path.join(gDownloadDir, filename),
+ contents
+ );
+ }
+ gPublicList = await Downloads.getList(Downloads.PUBLIC);
+});
+
+const TESTCASES = [
+ {
+ name: "Null download arg",
+ typeArg: "application/pdf",
+ downloadProps: null,
+ expected: /TypeError/,
+ },
+ {
+ name: "Missing type arg",
+ typeArg: undefined,
+ downloadProps: {
+ target: "download-test.pdf",
+ },
+ expected: /TypeError/,
+ },
+ {
+ name: "Empty string type arg",
+ typeArg: "",
+ downloadProps: {
+ target: "download-test.pdf",
+ },
+ expected: false,
+ },
+ {
+ name:
+ "download succeeded, file exists, unknown extension but contentType matches",
+ typeArg: "application/pdf",
+ downloadProps: {
+ target: "download-test.xxunknown",
+ contentType: "application/pdf",
+ },
+ expected: true,
+ },
+ {
+ name:
+ "download succeeded, file exists, contentType is generic and file extension maps to matching mime-type",
+ typeArg: "application/pdf",
+ downloadProps: {
+ target: "download-test.pdf",
+ contentType: "application/unknown",
+ },
+ expected: true,
+ },
+ {
+ name: "download did not succeed",
+ typeArg: "application/pdf",
+ downloadProps: {
+ target: "download-test.pdf",
+ contentType: "application/pdf",
+ succeeded: false,
+ },
+ expected: false,
+ },
+ {
+ name: "file does not exist",
+ typeArg: "application/pdf",
+ downloadProps: {
+ target: "download-test-missing.pdf",
+ contentType: "application/pdf",
+ },
+ expected: false,
+ },
+ {
+ name:
+ "contentType is missing and file extension doesnt map to a known mime-type",
+ typeArg: "application/pdf",
+ downloadProps: {
+ contentType: undefined,
+ target: "download-test.xxunknown",
+ },
+ expected: false,
+ },
+];
+
+for (let testData of TESTCASES) {
+ let tmp = {
+ async [testData.name]() {
+ info("testing with: " + JSON.stringify(testData));
+ await test_isFileOfType(testData);
+ },
+ };
+ add_task(tmp[testData.name]);
+}
+
+/**
+ * Sanity test the DownloadsCommon.isFileOfType method with test parameters
+ */
+async function test_isFileOfType({ name, typeArg, downloadProps, expected }) {
+ let download, result;
+ if (downloadProps) {
+ let downloadData = {
+ ...DOWNLOAD_TEMPLATE,
+ ...downloadProps,
+ };
+ downloadData.target = TESTFILES[downloadData.target];
+ Assert.ok(downloadData.target instanceof Ci.nsIFile, "target is a nsIFile");
+ download = await Downloads.createDownload(downloadData);
+ await gPublicList.add(download);
+ await download.refresh();
+ }
+
+ if (typeof expected == "boolean") {
+ result = await DownloadsCommon.isFileOfType(download, typeArg);
+ Assert.equal(result, expected, "Expected result from call to isFileOfType");
+ } else {
+ Assert.throws(
+ () => DownloadsCommon.isFileOfType(download, typeArg),
+ expected,
+ "isFileOfType should throw an exception if either the download object or mime-type arguments are falsey"
+ );
+ }
+}
diff --git a/browser/components/downloads/test/unit/test_DownloadsViewableInternally.js b/browser/components/downloads/test/unit/test_DownloadsViewableInternally.js
new file mode 100644
index 0000000000..16aee6e209
--- /dev/null
+++ b/browser/components/downloads/test/unit/test_DownloadsViewableInternally.js
@@ -0,0 +1,252 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const PREF_SVG_DISABLED = "svg.disabled";
+const PREF_WEBP_ENABLED = "image.webp.enabled";
+const PDF_MIME = "application/pdf";
+const OCTET_MIME = "application/octet-stream";
+const XML_MIME = "text/xml";
+const SVG_MIME = "image/svg+xml";
+const WEBP_MIME = "image/webp";
+
+const { Integration } = ChromeUtils.import(
+ "resource://gre/modules/Integration.jsm"
+);
+const {
+ DownloadsViewableInternally,
+ PREF_ENABLED_TYPES,
+ PREF_BRANCH_WAS_REGISTERED,
+ PREF_BRANCH_PREVIOUS_ACTION,
+ PREF_BRANCH_PREVIOUS_ASK,
+} = ChromeUtils.import("resource:///modules/DownloadsViewableInternally.jsm");
+
+/* global DownloadIntegration */
+Integration.downloads.defineModuleGetter(
+ this,
+ "DownloadIntegration",
+ "resource://gre/modules/DownloadIntegration.jsm"
+);
+
+const HandlerService = Cc[
+ "@mozilla.org/uriloader/handler-service;1"
+].getService(Ci.nsIHandlerService);
+const MIMEService = Cc["@mozilla.org/mime;1"].getService(Ci.nsIMIMEService);
+
+function checkPreferInternal(mime, ext, expectedPreferInternal) {
+ const handler = MIMEService.getFromTypeAndExtension(mime, ext);
+ if (expectedPreferInternal) {
+ Assert.equal(
+ handler?.preferredAction,
+ Ci.nsIHandlerInfo.handleInternally,
+ `checking ${mime} preferredAction == handleInternally`
+ );
+ } else {
+ Assert.notEqual(
+ handler?.preferredAction,
+ Ci.nsIHandlerInfo.handleInternally,
+ `checking ${mime} preferredAction != handleInternally`
+ );
+ }
+}
+
+function shouldView(mime, ext) {
+ return DownloadIntegration.shouldViewDownloadInternally(mime, ext);
+}
+
+function checkShouldView(mime, ext, expectedShouldView) {
+ Assert.equal(
+ shouldView(mime, ext),
+ expectedShouldView,
+ `checking ${mime} shouldViewDownloadInternally`
+ );
+}
+
+function checkWasRegistered(ext, expectedWasRegistered) {
+ Assert.equal(
+ Services.prefs.getBoolPref(PREF_BRANCH_WAS_REGISTERED + ext, false),
+ expectedWasRegistered,
+ `checking ${ext} was registered pref`
+ );
+}
+
+function checkAll(mime, ext, expected) {
+ checkPreferInternal(mime, ext, expected);
+ checkShouldView(mime, ext, expected);
+ checkWasRegistered(ext, expected);
+}
+
+add_task(async function test_viewable_internally() {
+ Services.prefs.setCharPref(PREF_ENABLED_TYPES, "xml , svg,webp");
+ Services.prefs.setBoolPref(PREF_SVG_DISABLED, false);
+ Services.prefs.setBoolPref(PREF_WEBP_ENABLED, true);
+
+ checkAll(XML_MIME, "xml", false);
+ checkAll(SVG_MIME, "svg", false);
+ checkAll(WEBP_MIME, "webp", false);
+
+ DownloadsViewableInternally.register();
+
+ checkAll(XML_MIME, "xml", true);
+ checkAll(SVG_MIME, "svg", true);
+ checkAll(WEBP_MIME, "webp", true);
+
+ // Remove SVG so it won't be cleared
+ Services.prefs.clearUserPref(PREF_BRANCH_WAS_REGISTERED + "svg");
+
+ // Disable xml and svg, check that xml becomes disabled
+ Services.prefs.setCharPref(PREF_ENABLED_TYPES, "webp");
+
+ checkAll(XML_MIME, "xml", false);
+ checkAll(WEBP_MIME, "webp", true);
+
+ // SVG shouldn't be cleared
+ checkPreferInternal(SVG_MIME, "svg", true);
+
+ Assert.ok(
+ shouldView(PDF_MIME),
+ "application/pdf should be unaffected by pref"
+ );
+ Assert.ok(
+ shouldView(OCTET_MIME, "pdf"),
+ ".pdf should be accepted by extension"
+ );
+ Assert.ok(
+ shouldView(OCTET_MIME, "PDF"),
+ ".pdf should be detected case-insensitively"
+ );
+ Assert.ok(!shouldView(OCTET_MIME, "exe"), ".exe shouldn't be accepted");
+
+ Assert.ok(!shouldView(XML_MIME), "text/xml should be disabled by pref");
+ Assert.ok(!shouldView(SVG_MIME), "image/xml+svg should be disabled by pref");
+
+ // Enable, check that everything is enabled again
+ Services.prefs.setCharPref(PREF_ENABLED_TYPES, "xml,svg,webp");
+
+ checkPreferInternal(XML_MIME, "xml", true);
+ checkPreferInternal(SVG_MIME, "svg", true);
+
+ Assert.ok(
+ shouldView(PDF_MIME),
+ "application/pdf should be unaffected by pref"
+ );
+ Assert.ok(shouldView(XML_MIME), "text/xml should be enabled by pref");
+ Assert.ok(
+ shouldView("application/xml"),
+ "alternate MIME type application/xml should be accepted"
+ );
+ Assert.ok(
+ shouldView(OCTET_MIME, "xml"),
+ ".xml should be accepted by extension"
+ );
+
+ // Disable viewable internally, pre-set handlers.
+ Services.prefs.setCharPref(PREF_ENABLED_TYPES, "");
+
+ for (const [mime, ext, action, ask] of [
+ [XML_MIME, "xml", Ci.nsIHandlerInfo.useSystemDefault, true],
+ [SVG_MIME, "svg", Ci.nsIHandlerInfo.saveToDisk, true],
+ [WEBP_MIME, "webp", Ci.nsIHandlerInfo.saveToDisk, false],
+ ]) {
+ let handler = MIMEService.getFromTypeAndExtension(mime, ext);
+ handler.preferredAction = action;
+ handler.alwaysAskBeforeHandling = ask;
+
+ HandlerService.store(handler);
+ checkPreferInternal(mime, ext, false);
+
+ // Expect to read back the same values
+ handler = MIMEService.getFromTypeAndExtension(mime, ext);
+ Assert.equal(handler.preferredAction, action);
+ Assert.equal(handler.alwaysAskBeforeHandling, ask);
+ }
+
+ // Enable viewable internally, XML should not be replaced, SVG and WebP should be saved.
+ Services.prefs.setCharPref(PREF_ENABLED_TYPES, "svg,webp,xml");
+
+ Assert.equal(
+ Services.prefs.getIntPref(PREF_BRANCH_PREVIOUS_ACTION + "svg"),
+ Ci.nsIHandlerInfo.saveToDisk,
+ "svg action should be saved"
+ );
+ Assert.equal(
+ Services.prefs.getBoolPref(PREF_BRANCH_PREVIOUS_ASK + "svg"),
+ true,
+ "svg ask should be saved"
+ );
+ Assert.equal(
+ Services.prefs.getIntPref(PREF_BRANCH_PREVIOUS_ACTION + "webp"),
+ Ci.nsIHandlerInfo.saveToDisk,
+ "webp action should be saved"
+ );
+ Assert.equal(
+ Services.prefs.getBoolPref(PREF_BRANCH_PREVIOUS_ASK + "webp"),
+ false,
+ "webp ask should be saved"
+ );
+
+ {
+ let handler = MIMEService.getFromTypeAndExtension(XML_MIME, "xml");
+ Assert.equal(
+ handler.preferredAction,
+ Ci.nsIHandlerInfo.useSystemDefault,
+ "svg action should be preserved"
+ );
+ Assert.equal(
+ !!handler.alwaysAskBeforeHandling,
+ true,
+ "svg ask should be preserved"
+ );
+ // Clean up
+ HandlerService.remove(handler);
+ }
+ // It should still be possible to view XML internally
+ checkShouldView(XML_MIME, "xml", true);
+ checkWasRegistered("xml", true);
+
+ checkAll(SVG_MIME, "svg", true);
+ checkAll(WEBP_MIME, "webp", true);
+
+ // Disable SVG to test SVG enabled check (depends on the pref)
+ Services.prefs.setBoolPref(PREF_SVG_DISABLED, true);
+ // Should have restored the settings from above
+ {
+ let handler = MIMEService.getFromTypeAndExtension(SVG_MIME, "svg");
+ Assert.equal(handler.preferredAction, Ci.nsIHandlerInfo.saveToDisk);
+ Assert.equal(!!handler.alwaysAskBeforeHandling, true);
+ // Clean up
+ HandlerService.remove(handler);
+ }
+ checkAll(SVG_MIME, "svg", false);
+
+ Services.prefs.setBoolPref(PREF_SVG_DISABLED, false);
+ checkAll(SVG_MIME, "svg", true);
+
+ // Test WebP enabled check (depends on the pref)
+ Services.prefs.setBoolPref(PREF_WEBP_ENABLED, false);
+ // Should have restored the settings from above
+ {
+ let handler = MIMEService.getFromTypeAndExtension(WEBP_MIME, "webp");
+ Assert.equal(handler.preferredAction, Ci.nsIHandlerInfo.saveToDisk);
+ Assert.equal(!!handler.alwaysAskBeforeHandling, false);
+ // Clean up
+ HandlerService.remove(handler);
+ }
+ checkAll(WEBP_MIME, "webp", false);
+
+ Services.prefs.setBoolPref(PREF_WEBP_ENABLED, true);
+ checkAll(WEBP_MIME, "webp", true);
+
+ Assert.ok(!shouldView(null, "pdf"), "missing MIME shouldn't be accepted");
+ Assert.ok(!shouldView(null, "xml"), "missing MIME shouldn't be accepted");
+ Assert.ok(!shouldView(OCTET_MIME), "unsupported MIME shouldn't be accepted");
+ Assert.ok(!shouldView(OCTET_MIME, "exe"), ".exe shouldn't be accepted");
+});
+
+registerCleanupFunction(() => {
+ // Clear all types to remove any saved values
+ Services.prefs.setCharPref(PREF_ENABLED_TYPES, "");
+ // Reset to the defaults
+ Services.prefs.clearUserPref(PREF_ENABLED_TYPES);
+ Services.prefs.clearUserPref(PREF_SVG_DISABLED);
+ Services.prefs.clearUserPref(PREF_WEBP_ENABLED);
+});
diff --git a/browser/components/downloads/test/unit/xpcshell.ini b/browser/components/downloads/test/unit/xpcshell.ini
new file mode 100644
index 0000000000..c272510def
--- /dev/null
+++ b/browser/components/downloads/test/unit/xpcshell.ini
@@ -0,0 +1,9 @@
+[DEFAULT]
+head = head.js
+firefox-appdir = browser
+skip-if = toolkit == 'android'
+
+
+[test_DownloadsCommon_getMimeInfo.js]
+[test_DownloadsCommon_isFileOfType.js]
+[test_DownloadsViewableInternally.js]