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.ini65
-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.js110
-rw-r--r--browser/components/downloads/test/browser/browser_download_is_clickable.js78
-rw-r--r--browser/components/downloads/test/browser/browser_download_opens_on_click.js89
-rw-r--r--browser/components/downloads/test/browser/browser_download_opens_policy.js104
-rw-r--r--browser/components/downloads/test/browser/browser_download_overwrite.js126
-rw-r--r--browser/components/downloads/test/browser/browser_download_spam_protection.js220
-rw-r--r--browser/components/downloads/test/browser/browser_download_starts_in_tmp.js264
-rw-r--r--browser/components/downloads/test/browser/browser_downloads_autohide.js517
-rw-r--r--browser/components/downloads/test/browser/browser_downloads_context_menu_always_open_similar_files.js236
-rw-r--r--browser/components/downloads/test/browser/browser_downloads_context_menu_delete_file.js253
-rw-r--r--browser/components/downloads/test/browser/browser_downloads_context_menu_selection.js139
-rw-r--r--browser/components/downloads/test/browser/browser_downloads_keynav.js255
-rw-r--r--browser/components/downloads/test/browser/browser_downloads_panel_block.js185
-rw-r--r--browser/components/downloads/test/browser/browser_downloads_panel_context_menu.js421
-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_disable_items.js171
-rw-r--r--browser/components/downloads/test/browser/browser_downloads_panel_dontshow.js126
-rw-r--r--browser/components/downloads/test/browser/browser_downloads_panel_focus.js108
-rw-r--r--browser/components/downloads/test/browser/browser_downloads_panel_height.js35
-rw-r--r--browser/components/downloads/test/browser/browser_downloads_panel_opens.js674
-rw-r--r--browser/components/downloads/test/browser/browser_downloads_pauseResume.js49
-rw-r--r--browser/components/downloads/test/browser/browser_first_download_panel.js68
-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.js72
-rw-r--r--browser/components/downloads/test/browser/browser_image_mimetype_issues.js135
-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.js122
-rw-r--r--browser/components/downloads/test/browser/browser_library_select_all.js77
-rw-r--r--browser/components/downloads/test/browser/browser_overflow_anchor.js59
-rw-r--r--browser/components/downloads/test/browser/browser_pdfjs_preview.js753
-rw-r--r--browser/components/downloads/test/browser/browser_tempfilename.js88
-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.js448
-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/browser/test_spammy_page.html26
-rw-r--r--browser/components/downloads/test/unit/head.js67
-rw-r--r--browser/components/downloads/test/unit/test_DownloadLastDir_basics.js140
-rw-r--r--browser/components/downloads/test/unit/test_DownloadsCommon_getMimeInfo.js168
-rw-r--r--browser/components/downloads/test/unit/test_DownloadsCommon_isFileOfType.js147
-rw-r--r--browser/components/downloads/test/unit/test_DownloadsViewableInternally.js277
-rw-r--r--browser/components/downloads/test/unit/xpcshell.ini9
48 files changed, 7194 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..b7f4acb542
--- /dev/null
+++ b/browser/components/downloads/test/browser/browser.ini
@@ -0,0 +1,65 @@
+[DEFAULT]
+support-files = head.js
+
+[browser_about_downloads.js]
+[browser_basic_functionality.js]
+[browser_confirm_unblock_download.js]
+[browser_download_is_clickable.js]
+[browser_download_opens_on_click.js]
+[browser_download_opens_policy.js]
+[browser_download_overwrite.js]
+support-files =
+ foo.txt
+ foo.txt^headers^
+ !/toolkit/content/tests/browser/common/mockTransfer.js
+[browser_download_spam_protection.js]
+skip-if =
+ os == "linux" && bits == 64 # bug 1743263 & Bug 1742678
+ os == "win" && os_version == "6.1" # Skip on Azure - frequent failure
+support-files = test_spammy_page.html
+[browser_download_starts_in_tmp.js]
+[browser_downloads_autohide.js]
+[browser_downloads_context_menu_always_open_similar_files.js]
+[browser_downloads_context_menu_delete_file.js]
+[browser_downloads_context_menu_selection.js]
+[browser_downloads_keynav.js]
+[browser_downloads_panel_block.js]
+[browser_downloads_panel_context_menu.js]
+skip-if =
+ os == "win" && os_version == "10.0" && bits == 64 && !debug # Bug 1719949
+ win10_2004 && bits == 32 && debug # Bug 1727925
+[browser_downloads_panel_ctrl_click.js]
+[browser_downloads_panel_disable_items.js]
+support-files =
+ foo.txt
+ foo.txt^headers^
+[browser_downloads_panel_dontshow.js]
+[browser_downloads_panel_focus.js]
+[browser_downloads_panel_height.js]
+[browser_downloads_panel_opens.js]
+skip-if =
+ os == "linux" && verify && !debug # For some reason linux opt verify builds time out.
+support-files =
+ foo.txt
+ foo.txt^headers^
+[browser_downloads_pauseResume.js]
+[browser_first_download_panel.js]
+skip-if =
+ os == "linux" # Bug 949434
+[browser_go_to_download_page.js]
+[browser_iframe_gone_mid_download.js]
+[browser_image_mimetype_issues.js]
+https_first_disabled = true
+support-files =
+ not-really-a-jpeg.jpeg
+ not-really-a-jpeg.jpeg^headers^
+ blank.JPG
+[browser_indicatorDrop.js]
+[browser_libraryDrop.js]
+skip-if = (os == 'win' && os_version == '10.0' && ccov) # Bug 1306510
+[browser_library_clearall.js]
+[browser_library_select_all.js]
+[browser_overflow_anchor.js]
+skip-if = os == "linux" # Bug 952422
+[browser_pdfjs_preview.js]
+[browser_tempfilename.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..87e17ee293
--- /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.loadURIString(browser, "about:downloads");
+ await downloadsLoaded;
+ await SpecialPowers.spawn(browser, [], async function () {
+ let box = content.document.getElementById("downloadsListBox");
+ 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..769f41cccf
--- /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..d88fa9a0e5
--- /dev/null
+++ b/browser/components/downloads/test/browser/browser_confirm_unblock_download.js
@@ -0,0 +1,110 @@
+/* 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 }) {
+ let promise = BrowserTestUtils.promiseAlertDialog(buttonToClick);
+ is(
+ await DownloadsCommon.confirmUnblockDownload(args),
+ expectedResult,
+ `Expect ${expectedResult} from ${buttonToClick}`
+ );
+ await promise;
+}
+
+/**
+ * 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_INSECURE,
+ 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() {
+ for (let verdict of [
+ Downloads.Error.BLOCK_VERDICT_POTENTIALLY_UNWANTED,
+ Downloads.Error.BLOCK_VERDICT_INSECURE,
+ ]) {
+ let args = {
+ verdict,
+ 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() {
+ for (let verdict of [
+ Downloads.Error.BLOCK_VERDICT_UNCOMMON,
+ Downloads.Error.BLOCK_VERDICT_INSECURE,
+ ]) {
+ let args = {
+ verdict,
+ 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_is_clickable.js b/browser/components/downloads/test/browser/browser_download_is_clickable.js
new file mode 100644
index 0000000000..421a214df8
--- /dev/null
+++ b/browser/components/downloads/test/browser/browser_download_is_clickable.js
@@ -0,0 +1,78 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+ChromeUtils.defineESModuleGetters(this, {
+ DownloadsViewUI: "resource:///modules/DownloadsViewUI.sys.mjs",
+});
+
+const { TelemetryTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/TelemetryTestUtils.sys.mjs"
+);
+
+add_task(async function test_download_clickable() {
+ Services.telemetry.clearScalars();
+
+ startServer();
+ mustInterruptResponses();
+ let download = await promiseInterruptibleDownload();
+ let publicList = await Downloads.getList(Downloads.PUBLIC);
+ await publicList.add(download);
+
+ registerCleanupFunction(async function () {
+ await task_resetState();
+ Services.telemetry.clearScalars();
+ });
+
+ download.start();
+
+ await promiseDownloadHasProgress(download, 50);
+
+ await task_openPanel();
+
+ let listbox = document.getElementById("downloadsListBox");
+ ok(listbox, "Download list box present");
+
+ await TestUtils.waitForCondition(() => {
+ return listbox.childElementCount == 1;
+ });
+
+ info("All downloads show in the listbox.itemChildren ", listbox.itemChildren);
+
+ ok(
+ listbox.itemChildren[0].classList.contains("openWhenFinished"),
+ "Download should have clickable style when in progress"
+ );
+
+ ok(!download.launchWhenSucceeded, "launchWhenSucceeded should set to false");
+
+ ok(!download._launchedFromPanel, "LaunchFromPanel should set to false");
+
+ EventUtils.synthesizeMouseAtCenter(listbox.itemChildren[0], {});
+ ok(
+ download.launchWhenSucceeded,
+ "Should open the file when download is finished"
+ );
+ ok(download._launchedFromPanel, "File was scheduled to launch from panel");
+
+ EventUtils.synthesizeMouseAtCenter(listbox.itemChildren[0], {});
+
+ ok(
+ !download.launchWhenSucceeded,
+ "Should NOT open the file when download is finished"
+ );
+
+ ok(!download._launchedFromPanel, "File launch from panel was reset");
+
+ continueResponses();
+ await download.refresh();
+ await promiseDownloadHasProgress(download, 100);
+
+ TelemetryTestUtils.assertScalar(
+ TelemetryTestUtils.getProcessScalars("parent"),
+ "downloads.file_opened",
+ undefined,
+ "File opened from panel should not be incremented"
+ );
+});
diff --git a/browser/components/downloads/test/browser/browser_download_opens_on_click.js b/browser/components/downloads/test/browser/browser_download_opens_on_click.js
new file mode 100644
index 0000000000..1259e197e0
--- /dev/null
+++ b/browser/components/downloads/test/browser/browser_download_opens_on_click.js
@@ -0,0 +1,89 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+ChromeUtils.defineESModuleGetters(this, {
+ DownloadsViewUI: "resource:///modules/DownloadsViewUI.sys.mjs",
+});
+
+const { TelemetryTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/TelemetryTestUtils.sys.mjs"
+);
+
+add_task(async function test_download_opens_on_click() {
+ Services.telemetry.clearScalars();
+
+ startServer();
+ mustInterruptResponses();
+ let download = await promiseInterruptibleDownload();
+ let publicList = await Downloads.getList(Downloads.PUBLIC);
+ await publicList.add(download);
+
+ let oldLaunchFile = DownloadIntegration.launchFile;
+
+ let waitForLaunchFileCalled = new Promise(resolve => {
+ DownloadIntegration.launchFile = () => {
+ ok(true, "The file should be launched with an external application");
+ resolve();
+ };
+ });
+
+ registerCleanupFunction(async function () {
+ DownloadIntegration.launchFile = oldLaunchFile;
+ await task_resetState();
+ Services.telemetry.clearScalars();
+ });
+
+ TelemetryTestUtils.assertScalar(
+ TelemetryTestUtils.getProcessScalars("parent"),
+ "downloads.file_opened",
+ undefined,
+ "File opened from panel should not be initialized"
+ );
+
+ download.start();
+
+ await promiseDownloadHasProgress(download, 50);
+
+ await task_openPanel();
+
+ let listbox = document.getElementById("downloadsListBox");
+ ok(listbox, "Download list box present");
+
+ await TestUtils.waitForCondition(() => {
+ return listbox.childElementCount == 1;
+ });
+
+ info("All downloads show in the listbox.itemChildren ", listbox.itemChildren);
+
+ ok(
+ listbox.itemChildren[0].classList.contains("openWhenFinished"),
+ "Download should have clickable style when in progress"
+ );
+
+ ok(!download.launchWhenSucceeded, "launchWhenSucceeded should set to false");
+
+ ok(!download._launchedFromPanel, "LaunchFromPanel should set to false");
+
+ EventUtils.synthesizeMouseAtCenter(listbox.itemChildren[0], {});
+
+ ok(
+ download.launchWhenSucceeded,
+ "Should open the file when download is finished"
+ );
+ ok(download._launchedFromPanel, "File was scheduled to launch from panel");
+
+ continueResponses();
+ await download.refresh();
+ await promiseDownloadHasProgress(download, 100);
+
+ await waitForLaunchFileCalled;
+
+ TelemetryTestUtils.assertScalar(
+ TelemetryTestUtils.getProcessScalars("parent"),
+ "downloads.file_opened",
+ 1,
+ "File opened from panel should be incremented"
+ );
+});
diff --git a/browser/components/downloads/test/browser/browser_download_opens_policy.js b/browser/components/downloads/test/browser/browser_download_opens_policy.js
new file mode 100644
index 0000000000..97d9bef1db
--- /dev/null
+++ b/browser/components/downloads/test/browser/browser_download_opens_policy.js
@@ -0,0 +1,104 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { EnterprisePolicyTesting } = ChromeUtils.importESModule(
+ "resource://testing-common/EnterprisePolicyTesting.sys.mjs"
+);
+
+ChromeUtils.defineESModuleGetters(this, {
+ DownloadsViewUI: "resource:///modules/DownloadsViewUI.sys.mjs",
+});
+
+const { TelemetryTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/TelemetryTestUtils.sys.mjs"
+);
+
+add_task(async function test_download_opens_on_click() {
+ Services.telemetry.clearScalars();
+
+ await EnterprisePolicyTesting.setupPolicyEngineWithJson({
+ policies: {
+ ExemptDomainFileTypePairsFromFileTypeDownloadWarnings: [
+ {
+ file_extension: "jnlp",
+ domains: ["localhost"],
+ },
+ ],
+ },
+ });
+
+ startServer();
+ mustInterruptResponses();
+ let download = await promiseInterruptibleDownload(".jnlp");
+ let publicList = await Downloads.getList(Downloads.PUBLIC);
+ await publicList.add(download);
+
+ let oldLaunchFile = DownloadIntegration.launchFile;
+
+ let waitForLaunchFileCalled = new Promise(resolve => {
+ DownloadIntegration.launchFile = () => {
+ ok(true, "The file should be launched with an external application");
+ resolve();
+ };
+ });
+
+ registerCleanupFunction(async function () {
+ DownloadIntegration.launchFile = oldLaunchFile;
+ await task_resetState();
+ Services.telemetry.clearScalars();
+ });
+
+ TelemetryTestUtils.assertScalar(
+ TelemetryTestUtils.getProcessScalars("parent"),
+ "downloads.file_opened",
+ undefined,
+ "File opened from panel should not be initialized"
+ );
+
+ download.start();
+
+ await promiseDownloadHasProgress(download, 50);
+
+ await task_openPanel();
+
+ let listbox = document.getElementById("downloadsListBox");
+ ok(listbox, "Download list box present");
+
+ await TestUtils.waitForCondition(() => {
+ return listbox.childElementCount == 1;
+ });
+
+ info("All downloads show in the listbox.itemChildren ", listbox.itemChildren);
+
+ ok(
+ listbox.itemChildren[0].classList.contains("openWhenFinished"),
+ "Download should have clickable style when in progress"
+ );
+
+ ok(!download.launchWhenSucceeded, "launchWhenSucceeded should set to false");
+
+ ok(!download._launchedFromPanel, "LaunchFromPanel should set to false");
+
+ EventUtils.synthesizeMouseAtCenter(listbox.itemChildren[0], {});
+
+ ok(
+ download.launchWhenSucceeded,
+ "Should open the file when download is finished"
+ );
+ ok(download._launchedFromPanel, "File was scheduled to launch from panel");
+
+ continueResponses();
+ await download.refresh();
+ await promiseDownloadHasProgress(download, 100);
+
+ await waitForLaunchFileCalled;
+
+ TelemetryTestUtils.assertScalar(
+ TelemetryTestUtils.getProcessScalars("parent"),
+ "downloads.file_opened",
+ 1,
+ "File opened from panel should be incremented"
+ );
+});
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..7be16aa565
--- /dev/null
+++ b/browser/components/downloads/test/browser/browser_download_overwrite.js
@@ -0,0 +1,126 @@
+/* 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);
+
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/toolkit/content/tests/browser/common/mockTransfer.js",
+ this
+);
+
+add_setup(async function () {
+ // 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.always_ask_before_handling_new_types", false],
+ ["browser.download.useDownloadDir", false],
+ ],
+ });
+ // Set up the file picker.
+ let destDir = gTestTargetFile.parent;
+
+ MockFilePicker.displayDirectory = destDir;
+ MockFilePicker.showCallback = function (fp) {
+ MockFilePicker.setFiles([gTestTargetFile]);
+ return MockFilePicker.returnOK;
+ };
+ 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();
+ }
+ });
+
+ // Now try and download a thing to the file:
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ opening: TEST_ROOT + "foo.txt",
+ waitForLoad: false,
+ waitForStateStop: true,
+ },
+ async function () {
+ 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 publicDownloads = await Downloads.getList(Downloads.PUBLIC);
+ // 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);
+ }
+ },
+ });
+ });
+ // Now try and download a thing to the file:
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ opening: TEST_ROOT + "foo.txt",
+ waitForLoad: false,
+ waitForStateStop: true,
+ },
+ async function () {
+ 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_download_spam_protection.js b/browser/components/downloads/test/browser/browser_download_spam_protection.js
new file mode 100644
index 0000000000..8095fff18e
--- /dev/null
+++ b/browser/components/downloads/test/browser/browser_download_spam_protection.js
@@ -0,0 +1,220 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+ChromeUtils.defineESModuleGetters(this, {
+ DownloadSpamProtection: "resource:///modules/DownloadSpamProtection.sys.mjs",
+ PermissionTestUtils: "resource://testing-common/PermissionTestUtils.sys.mjs",
+});
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+ BrowserWindowTracker: "resource:///modules/BrowserWindowTracker.jsm",
+});
+
+const TEST_URI = "https://example.com";
+
+const TEST_PATH = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ TEST_URI
+);
+
+add_setup(async function () {
+ // Create temp directory
+ let time = new Date().getTime();
+ let tempDir = Services.dirsvc.get("TmpD", Ci.nsIFile);
+ tempDir.append(time);
+ Services.prefs.setIntPref("browser.download.folderList", 2);
+ Services.prefs.setComplexValue("browser.download.dir", Ci.nsIFile, tempDir);
+
+ PermissionTestUtils.add(
+ TEST_URI,
+ "automatic-download",
+ Services.perms.UNKNOWN_ACTION
+ );
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.download.enable_spam_prevention", true]],
+ clear: [
+ ["browser.download.alwaysOpenPanel"],
+ ["browser.download.always_ask_before_handling_new_types"],
+ ],
+ });
+
+ registerCleanupFunction(async () => {
+ Services.prefs.clearUserPref("browser.download.folderList");
+ Services.prefs.clearUserPref("browser.download.dir");
+ await IOUtils.remove(tempDir.path, { recursive: true });
+ });
+});
+
+add_task(async function check_download_spam_ui() {
+ await task_resetState();
+
+ let browserWin = BrowserWindowTracker.getTopWindow();
+ registerCleanupFunction(async () => {
+ for (let win of [browserWin, browserWin2]) {
+ win.DownloadsPanel.hidePanel();
+ DownloadIntegration.downloadSpamProtection.removeDownloadSpamForWindow(
+ TEST_URI,
+ win
+ );
+ }
+ let publicList = await Downloads.getList(Downloads.PUBLIC);
+ await publicList.removeFinished();
+ BrowserTestUtils.removeTab(newTab);
+ await BrowserTestUtils.closeWindow(browserWin2);
+ });
+ let observedBlockedDownloads = 0;
+ let gotAllBlockedDownloads = TestUtils.topicObserved(
+ "blocked-automatic-download",
+ () => {
+ return ++observedBlockedDownloads >= 99;
+ }
+ );
+
+ let newTab = await BrowserTestUtils.openNewForegroundTab(
+ browserWin.gBrowser,
+ TEST_PATH + "test_spammy_page.html"
+ );
+
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ "body",
+ {},
+ newTab.linkedBrowser
+ );
+
+ info("Waiting on all blocked downloads");
+ await gotAllBlockedDownloads;
+
+ let { downloadSpamProtection } = DownloadIntegration;
+ let spamList = downloadSpamProtection.getSpamListForWindow(browserWin);
+ is(
+ spamList._downloads[0].blockedDownloadsCount,
+ 99,
+ "99 blocked downloads recorded"
+ );
+ ok(
+ spamList._downloads[0].error.becauseBlockedByReputationCheck,
+ "Download blocked because of reputation"
+ );
+ is(
+ spamList._downloads[0].error.reputationCheckVerdict,
+ "DownloadSpam",
+ "Verdict is DownloadSpam"
+ );
+
+ browserWin.focus();
+ await BrowserTestUtils.waitForPopupEvent(
+ browserWin.DownloadsPanel.panel,
+ "shown"
+ );
+
+ ok(browserWin.DownloadsPanel.isPanelShowing, "Download panel should open");
+ await Downloads.getList(Downloads.PUBLIC);
+
+ let listbox = browserWin.document.getElementById("downloadsListBox");
+ ok(listbox, "Download list box present");
+
+ await TestUtils.waitForCondition(() => {
+ return listbox.childElementCount == 2 && !listbox.getAttribute("disabled");
+ }, "2 downloads = 1 allowed download and 1 for 99 downloads blocked");
+
+ let spamElement = listbox.itemChildren[0].classList.contains(
+ "temporary-block"
+ )
+ ? listbox.itemChildren[0]
+ : listbox.itemChildren[1];
+
+ ok(spamElement.classList.contains("temporary-block"), "Download is blocked");
+
+ info("Testing spam protection in a second window");
+
+ browserWin.DownloadsPanel.hidePanel();
+ DownloadIntegration.downloadSpamProtection.removeDownloadSpamForWindow(
+ TEST_URI,
+ browserWin
+ );
+
+ ok(
+ !browserWin.DownloadsPanel.isPanelShowing,
+ "Download panel should be closed in first window"
+ );
+ is(
+ listbox.childElementCount,
+ 1,
+ "First window's download list should have one item - the download that wasn't blocked"
+ );
+
+ let browserWin2 = await BrowserTestUtils.openNewBrowserWindow();
+ let observedBlockedDownloads2 = 0;
+ let gotAllBlockedDownloads2 = TestUtils.topicObserved(
+ "blocked-automatic-download",
+ () => {
+ return ++observedBlockedDownloads2 >= 100;
+ }
+ );
+
+ let newTab2 = await BrowserTestUtils.openNewForegroundTab(
+ browserWin2.gBrowser,
+ TEST_PATH + "test_spammy_page.html"
+ );
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ "body",
+ {},
+ newTab2.linkedBrowser
+ );
+
+ info("Waiting on all blocked downloads in second window");
+ await gotAllBlockedDownloads2;
+
+ let spamList2 = downloadSpamProtection.getSpamListForWindow(browserWin2);
+ is(
+ spamList2._downloads[0].blockedDownloadsCount,
+ 100,
+ "100 blocked downloads recorded in second window"
+ );
+ ok(
+ !spamList._downloads[0]?.blockedDownloadsCount,
+ "No blocked downloads in first window"
+ );
+
+ browserWin2.focus();
+ await BrowserTestUtils.waitForPopupEvent(
+ browserWin2.DownloadsPanel.panel,
+ "shown"
+ );
+
+ ok(
+ browserWin2.DownloadsPanel.isPanelShowing,
+ "Download panel should open in second window"
+ );
+
+ ok(
+ !browserWin.DownloadsPanel.isPanelShowing,
+ "Download panel should not open in first window"
+ );
+
+ let listbox2 = browserWin2.document.getElementById("downloadsListBox");
+ ok(listbox2, "Download list box present");
+
+ await TestUtils.waitForCondition(() => {
+ return (
+ listbox2.childElementCount == 2 && !listbox2.getAttribute("disabled")
+ );
+ }, "2 downloads = 1 allowed download from first window, and 1 for 100 downloads blocked in second window");
+
+ is(
+ listbox.childElementCount,
+ 1,
+ "First window's download list should still have one item - the download that wasn't blocked"
+ );
+
+ let spamElement2 = listbox2.itemChildren[0].classList.contains(
+ "temporary-block"
+ )
+ ? listbox2.itemChildren[0]
+ : listbox2.itemChildren[1];
+
+ ok(spamElement2.classList.contains("temporary-block"), "Download is blocked");
+});
diff --git a/browser/components/downloads/test/browser/browser_download_starts_in_tmp.js b/browser/components/downloads/test/browser/browser_download_starts_in_tmp.js
new file mode 100644
index 0000000000..1301e8fa1b
--- /dev/null
+++ b/browser/components/downloads/test/browser/browser_download_starts_in_tmp.js
@@ -0,0 +1,264 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const UCT_URI = "chrome://mozapps/content/downloads/unknownContentType.xhtml";
+// Need to start the server before `httpUrl` works.
+startServer();
+const DOWNLOAD_URL = httpUrl("interruptible.txt");
+
+let gDownloadDir;
+
+let gExternalHelperAppService = Cc[
+ "@mozilla.org/uriloader/external-helper-app-service;1"
+].getService(Ci.nsIExternalHelperAppService);
+gExternalHelperAppService.QueryInterface(Ci.nsIObserver);
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.download.start_downloads_in_tmp_dir", true],
+ ["browser.helperApps.deleteTempFileOnExit", true],
+ ],
+ });
+ registerCleanupFunction(task_resetState);
+ gDownloadDir = new FileUtils.File(await setDownloadDir());
+ registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("browser.download.dir");
+ Services.prefs.clearUserPref("browser.download.folderList");
+ });
+});
+
+add_task(async function test_download_asking_starts_in_tmp() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.download.always_ask_before_handling_new_types", true]],
+ });
+ let list = await Downloads.getList(Downloads.PUBLIC);
+ let downloadStarted = new Promise(resolve => {
+ let view = {
+ onDownloadAdded(download) {
+ list.removeView(view);
+ resolve(download);
+ },
+ };
+ list.addView(view);
+ });
+ // Wait for the download prompting dialog
+ let dialogPromise = BrowserTestUtils.domWindowOpenedAndLoaded(
+ null,
+ win => win.document.documentURI == UCT_URI
+ );
+ serveInterruptibleAsDownload();
+ mustInterruptResponses();
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: DOWNLOAD_URL,
+ waitForLoad: false,
+ waitForStop: true,
+ },
+ async function () {
+ let dialogWin = await dialogPromise;
+ let tempFile = dialogWin.dialog.mLauncher.targetFile;
+ ok(
+ !tempFile.parent.equals(gDownloadDir),
+ "Should not have put temp file in the downloads dir."
+ );
+
+ let dialogEl = dialogWin.document.querySelector("dialog");
+ dialogEl.getButton("accept").disabled = false;
+ dialogEl.acceptDialog();
+ let download = await downloadStarted;
+ is(
+ PathUtils.parent(download.target.path),
+ gDownloadDir.path,
+ "Should have put final file in the downloads dir."
+ );
+ continueResponses();
+ await download.whenSucceeded();
+ await IOUtils.remove(download.target.path);
+ }
+ );
+ await list.removeFinished();
+});
+
+add_task(async function test_download_asking_and_opening_opens_from_tmp() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.download.always_ask_before_handling_new_types", true]],
+ });
+ let list = await Downloads.getList(Downloads.PUBLIC);
+ let downloadStarted = new Promise(resolve => {
+ let view = {
+ onDownloadAdded(download) {
+ list.removeView(view);
+ resolve(download);
+ },
+ };
+ list.addView(view);
+ });
+ // Wait for the download prompting dialog
+ let dialogPromise = BrowserTestUtils.domWindowOpenedAndLoaded(
+ null,
+ win => win.document.documentURI == UCT_URI
+ );
+
+ let oldLaunchFile = DownloadIntegration.launchFile;
+ let promiseLaunchFileCalled = new Promise(resolve => {
+ DownloadIntegration.launchFile = file => {
+ ok(true, "The file should be launched with an external application");
+ resolve(file);
+ DownloadIntegration.launchFile = oldLaunchFile;
+ };
+ });
+ registerCleanupFunction(() => {
+ DownloadIntegration.launchFile = oldLaunchFile;
+ });
+
+ serveInterruptibleAsDownload();
+ mustInterruptResponses();
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: DOWNLOAD_URL,
+ waitForLoad: false,
+ waitForStop: true,
+ },
+ async function () {
+ let dialogWin = await dialogPromise;
+ let tempFile = dialogWin.dialog.mLauncher.targetFile;
+ ok(
+ !tempFile.parent.equals(gDownloadDir),
+ "Should not have put temp file in the downloads dir."
+ );
+
+ dialogWin.document.getElementById("open").click();
+ let dialogEl = dialogWin.document.querySelector("dialog");
+ dialogEl.getButton("accept").disabled = false;
+ dialogEl.acceptDialog();
+ let download = await downloadStarted;
+ isnot(
+ PathUtils.parent(download.target.path),
+ gDownloadDir.path,
+ "Should not have put final file in the downloads dir when it's supposed to be automatically opened."
+ );
+ continueResponses();
+ await download.whenSucceeded();
+ await download.refresh();
+ isnot(
+ PathUtils.parent(download.target.path),
+ gDownloadDir.path,
+ "Once finished the download should not be in the downloads dir when it's supposed to be automatically opened."
+ );
+ let file = await promiseLaunchFileCalled;
+ ok(
+ !file.parent.equals(gDownloadDir),
+ "Should not have put opened file in the downloads dir."
+ );
+
+ // Pretend that we've quit so we wipe all the files:
+ gExternalHelperAppService.observe(null, "profile-before-change", "");
+ // Now the file should go away, but that's async...
+
+ let f = new FileUtils.File(download.target.path);
+ await TestUtils.waitForCondition(
+ () => !f.exists(),
+ "Temp file should be removed",
+ 500
+ ).catch(err => ok(false, err));
+ ok(!f.exists(), "Temp file should be removed.");
+
+ await IOUtils.remove(download.target.path);
+ }
+ );
+ await list.removeFinished();
+});
+
+// Check that if we open the file automatically, it opens from the temp dir.
+add_task(async function test_download_automatically_opened_from_tmp() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.download.always_ask_before_handling_new_types", false]],
+ });
+ serveInterruptibleAsDownload();
+ mustInterruptResponses();
+
+ let list = await Downloads.getList(Downloads.PUBLIC);
+ let downloadStarted = new Promise(resolve => {
+ let view = {
+ onDownloadAdded(download) {
+ list.removeView(view);
+ resolve(download);
+ },
+ };
+ list.addView(view);
+ });
+
+ const handlerSvc = Cc["@mozilla.org/uriloader/handler-service;1"].getService(
+ Ci.nsIHandlerService
+ );
+ const mimeSvc = Cc["@mozilla.org/mime;1"].getService(Ci.nsIMIMEService);
+
+ let txtHandlerInfo = mimeSvc.getFromTypeAndExtension("text/plain", "txt");
+ txtHandlerInfo.preferredAction = Ci.nsIHandlerInfo.useSystemDefault;
+ txtHandlerInfo.alwaysAskBeforeHandling = false;
+ handlerSvc.store(txtHandlerInfo);
+ registerCleanupFunction(() => handlerSvc.remove(txtHandlerInfo));
+
+ let oldLaunchFile = DownloadIntegration.launchFile;
+ let promiseLaunchFileCalled = new Promise(resolve => {
+ DownloadIntegration.launchFile = file => {
+ ok(true, "The file should be launched with an external application");
+ resolve(file);
+ DownloadIntegration.launchFile = oldLaunchFile;
+ };
+ });
+ registerCleanupFunction(() => {
+ DownloadIntegration.launchFile = oldLaunchFile;
+ });
+
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: DOWNLOAD_URL,
+ waitForLoad: false,
+ waitForStop: true,
+ },
+ async function () {
+ let download = await downloadStarted;
+ isnot(
+ PathUtils.parent(download.target.partFilePath),
+ gDownloadDir.path,
+ "Should not start the download in the downloads dir."
+ );
+ continueResponses();
+ await download.whenSucceeded();
+ isnot(
+ PathUtils.parent(download.target.path),
+ gDownloadDir.path,
+ "Should not have put final file in the downloads dir."
+ );
+ let file = await promiseLaunchFileCalled;
+ ok(
+ !file.parent.equals(gDownloadDir),
+ "Should not have put opened file in the downloads dir."
+ );
+
+ // Pretend that we've quit so we wipe all the files:
+ gExternalHelperAppService.observe(null, "profile-before-change", "");
+ // Now the file should go away, but that's async...
+
+ let f = new FileUtils.File(download.target.path);
+ await TestUtils.waitForCondition(
+ () => !f.exists(),
+ "Temp file should be removed",
+ 500
+ ).catch(err => ok(false, err));
+ ok(!f.exists(), "Temp file should be removed.");
+
+ await IOUtils.remove(download.target.path);
+ }
+ );
+
+ handlerSvc.remove(txtHandlerInfo);
+ await list.removeFinished();
+});
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..9e3f8b6107
--- /dev/null
+++ b/browser/components/downloads/test/browser/browser_downloads_autohide.js
@@ -0,0 +1,517 @@
+/* -*- 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_setup(async () => {
+ // Disable window occlusion. See bug 1733955 / bug 1779559.
+ if (navigator.platform.indexOf("Win") == 0) {
+ await SpecialPowers.pushPrefEnv({
+ set: [["widget.windows.window_occlusion_tracking.enabled", false]],
+ });
+ }
+});
+
+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("reload-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_context_menu_always_open_similar_files.js b/browser/components/downloads/test/browser/browser_downloads_context_menu_always_open_similar_files.js
new file mode 100644
index 0000000000..6030d126c7
--- /dev/null
+++ b/browser/components/downloads/test/browser/browser_downloads_context_menu_always_open_similar_files.js
@@ -0,0 +1,236 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+let gMimeSvc = Cc["@mozilla.org/mime;1"].getService(Ci.nsIMIMEService);
+let gHandlerSvc = Cc["@mozilla.org/uriloader/handler-service;1"].getService(
+ Ci.nsIHandlerService
+);
+
+let gDownloadDir;
+const TestFiles = {};
+let downloads = [];
+const { handleInternally, saveToDisk, useSystemDefault, alwaysAsk } =
+ Ci.nsIHandlerInfo;
+
+function ensureMIMEState({ preferredAction, alwaysAskBeforeHandling = false }) {
+ const mimeInfo = gMimeSvc.getFromTypeAndExtension("text/plain", "txt");
+ mimeInfo.preferredAction = preferredAction;
+ mimeInfo.alwaysAskBeforeHandling = alwaysAskBeforeHandling;
+ gHandlerSvc.store(mimeInfo);
+}
+
+async function createDownloadFile() {
+ if (!gDownloadDir) {
+ gDownloadDir = await setDownloadDir();
+ }
+ info("Created download directory: " + gDownloadDir);
+ TestFiles.txt = await createDownloadedFile(
+ PathUtils.join(gDownloadDir, "downloaded.txt"),
+ "Test file"
+ );
+ info("Created downloaded text file at:" + TestFiles.txt.path);
+
+ info("Setting path for download file");
+ // Set target for download file. Otherwise, file will default to .file instead of txt
+ // when we prepare our downloads - particularly in task_addDownloads().
+ let targetPath = PathUtils.join(PathUtils.tempDir, "downloaded.txt");
+ let target = new FileUtils.File(targetPath);
+ target.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, FileUtils.PERMS_FILE);
+ downloads.push({
+ state: DownloadsCommon.DOWNLOAD_FINISHED,
+ contentType: "text/plain",
+ target,
+ });
+}
+
+async function prepareDownloadFiles(downloadList) {
+ // prepare downloads
+ await task_addDownloads(downloads);
+ let [download] = await downloadList.getAll();
+ info("Download succeeded? " + download.succeeded);
+ info("Download target exists? " + download.target.exists);
+}
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.download.always_ask_before_handling_new_types", false]],
+ });
+ const originalOpenDownload = DownloadsCommon.openDownload;
+ // overwrite DownloadsCommon.openDownload to prevent file from opening during tests
+ DownloadsCommon.openDownload = async () => {
+ info("Overwriting openDownload for tests");
+ };
+
+ registerCleanupFunction(async () => {
+ DownloadsCommon.openDownload = originalOpenDownload;
+ info("Resetting downloads and closing downloads panel");
+ await task_resetState();
+ });
+
+ // 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 createDownloadFile();
+ await prepareDownloadFiles(downloadList);
+});
+
+add_task(async function test_checkbox_useSystemDefault() {
+ // force mimetype pref
+ ensureMIMEState({ preferredAction: useSystemDefault });
+
+ await task_openPanel();
+ await TestUtils.waitForCondition(() => {
+ let downloadsListBox = document.getElementById("downloadsListBox");
+ downloadsListBox.removeAttribute("disabled");
+ return downloadsListBox.childElementCount == downloads.length;
+ });
+
+ info("trigger the context menu");
+ let itemTarget = document.querySelector(
+ "#downloadsListBox richlistitem .downloadMainArea"
+ );
+
+ let contextMenu = await openContextMenu(itemTarget);
+ let alwaysOpenSimilarFilesItem = contextMenu.querySelector(
+ ".downloadAlwaysOpenSimilarFilesMenuItem"
+ );
+
+ ok(
+ !BrowserTestUtils.is_hidden(alwaysOpenSimilarFilesItem),
+ "alwaysOpenSimilarFiles should be visible"
+ );
+ ok(
+ alwaysOpenSimilarFilesItem.hasAttribute("checked"),
+ "alwaysOpenSimilarFiles should have checkbox attribute"
+ );
+
+ contextMenu.hidePopup();
+ let hiddenPromise = BrowserTestUtils.waitForEvent(
+ DownloadsPanel.panel,
+ "popuphidden"
+ );
+ DownloadsPanel.hidePanel();
+ await hiddenPromise;
+});
+
+add_task(async function test_checkbox_saveToDisk() {
+ // force mimetype pref
+ ensureMIMEState({ preferredAction: saveToDisk });
+
+ await task_openPanel();
+ await TestUtils.waitForCondition(() => {
+ let downloadsListBox = document.getElementById("downloadsListBox");
+ downloadsListBox.removeAttribute("disabled");
+ return downloadsListBox.childElementCount == downloads.length;
+ });
+
+ info("trigger the context menu");
+ let itemTarget = document.querySelector(
+ "#downloadsListBox richlistitem .downloadMainArea"
+ );
+
+ let contextMenu = await openContextMenu(itemTarget);
+ let alwaysOpenSimilarFilesItem = contextMenu.querySelector(
+ ".downloadAlwaysOpenSimilarFilesMenuItem"
+ );
+
+ ok(
+ !BrowserTestUtils.is_hidden(alwaysOpenSimilarFilesItem),
+ "alwaysOpenSimilarFiles should be visible"
+ );
+ ok(
+ !alwaysOpenSimilarFilesItem.hasAttribute("checked"),
+ "alwaysOpenSimilarFiles should not have checkbox attribute"
+ );
+
+ contextMenu.hidePopup();
+ let hiddenPromise = BrowserTestUtils.waitForEvent(
+ DownloadsPanel.panel,
+ "popuphidden"
+ );
+ DownloadsPanel.hidePanel();
+ await hiddenPromise;
+});
+
+add_task(async function test_preferences_enable_alwaysOpenSimilarFiles() {
+ // Force mimetype pref
+ ensureMIMEState({ preferredAction: saveToDisk });
+
+ // open panel
+ await task_openPanel();
+ await TestUtils.waitForCondition(() => {
+ let downloadsListBox = document.getElementById("downloadsListBox");
+ downloadsListBox.removeAttribute("disabled");
+ return downloadsListBox.childElementCount == downloads.length;
+ });
+
+ info("trigger the context menu");
+ let itemTarget = document.querySelector(
+ "#downloadsListBox richlistitem .downloadMainArea"
+ );
+
+ let contextMenu = await openContextMenu(itemTarget);
+ let alwaysOpenSimilarFilesItem = contextMenu.querySelector(
+ ".downloadAlwaysOpenSimilarFilesMenuItem"
+ );
+
+ alwaysOpenSimilarFilesItem.click();
+
+ await TestUtils.waitForCondition(() => {
+ let mimeInfo = gMimeSvc.getFromTypeAndExtension("text/plain", "txt");
+ return mimeInfo.preferredAction === useSystemDefault;
+ });
+ let mimeInfo = gMimeSvc.getFromTypeAndExtension("text/plain", "txt");
+
+ is(
+ mimeInfo.preferredAction,
+ useSystemDefault,
+ "Preference should switch to useSystemDefault"
+ );
+
+ contextMenu.hidePopup();
+ DownloadsPanel.hidePanel();
+});
+
+add_task(async function test_preferences_disable_alwaysOpenSimilarFiles() {
+ // Force mimetype pref
+ ensureMIMEState({ preferredAction: useSystemDefault });
+
+ await task_openPanel();
+ await TestUtils.waitForCondition(() => {
+ let downloadsListBox = document.getElementById("downloadsListBox");
+ downloadsListBox.removeAttribute("disabled");
+ return downloadsListBox.childElementCount == downloads.length;
+ });
+
+ info("trigger the context menu");
+ let itemTarget = document.querySelector(
+ "#downloadsListBox richlistitem .downloadMainArea"
+ );
+
+ let contextMenu = await openContextMenu(itemTarget);
+ let alwaysOpenSimilarFilesItem = contextMenu.querySelector(
+ ".downloadAlwaysOpenSimilarFilesMenuItem"
+ );
+
+ alwaysOpenSimilarFilesItem.click();
+
+ await TestUtils.waitForCondition(() => {
+ let mimeInfo = gMimeSvc.getFromTypeAndExtension("text/plain", "txt");
+ return mimeInfo.preferredAction === saveToDisk;
+ });
+ let mimeInfo = gMimeSvc.getFromTypeAndExtension("text/plain", "txt");
+
+ is(
+ mimeInfo.preferredAction,
+ saveToDisk,
+ "Preference should switch to saveToDisk"
+ );
+
+ contextMenu.hidePopup();
+ DownloadsPanel.hidePanel();
+});
diff --git a/browser/components/downloads/test/browser/browser_downloads_context_menu_delete_file.js b/browser/components/downloads/test/browser/browser_downloads_context_menu_delete_file.js
new file mode 100644
index 0000000000..4615f0a369
--- /dev/null
+++ b/browser/components/downloads/test/browser/browser_downloads_context_menu_delete_file.js
@@ -0,0 +1,253 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+var { DownloadHistory } = ChromeUtils.importESModule(
+ "resource://gre/modules/DownloadHistory.sys.mjs"
+);
+let gDownloadDir;
+let downloads = [];
+
+async function createDownloadFiles() {
+ if (!gDownloadDir) {
+ gDownloadDir = await setDownloadDir();
+ }
+ info("Created download directory: " + gDownloadDir);
+ info("Setting path for download file");
+ downloads.push({
+ state: DownloadsCommon.DOWNLOAD_FINISHED,
+ contentType: "text/plain",
+ target: await createDownloadedFile(
+ PathUtils.join(gDownloadDir, "downloaded.txt"),
+ "Test file"
+ ),
+ });
+ downloads.push({
+ state: DownloadsCommon.DOWNLOAD_FINISHED,
+ contentType: "text/javascript",
+ target: await createDownloadedFile(
+ PathUtils.join(gDownloadDir, "downloaded.js"),
+ "Test file"
+ ),
+ });
+}
+
+add_setup(startServer);
+
+registerCleanupFunction(async function () {
+ await task_resetState();
+ await PlacesUtils.history.clear();
+});
+
+add_task(async function test_download_deleteFile() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.download.always_ask_before_handling_new_types", false],
+ ["browser.download.alwaysOpenPanel", false],
+ ["browser.download.clearHistoryOnDelete", 2],
+ ],
+ });
+
+ // 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();
+ await createDownloadFiles();
+ await task_addDownloads(downloads);
+ await task_openPanel();
+ await TestUtils.waitForCondition(() => {
+ let downloadsListBox = document.getElementById("downloadsListBox");
+ downloadsListBox.removeAttribute("disabled");
+ return downloadsListBox.childElementCount == downloads.length;
+ });
+
+ info("trigger the context menu");
+ let itemTarget = document.querySelector(
+ "#downloadsListBox richlistitem .downloadMainArea"
+ );
+ let contextMenu = await openContextMenu(itemTarget);
+ let deleteFileItem = contextMenu.querySelector(
+ '[command="downloadsCmd_deleteFile"]'
+ );
+ ok(
+ !BrowserTestUtils.is_hidden(deleteFileItem),
+ "deleteFileItem should be visible"
+ );
+
+ let target1 = downloads[1].target;
+ ok(target1.exists(), "downloaded.txt should exist");
+ info(`file path: ${target1.path}`);
+ let hiddenPromise = BrowserTestUtils.waitForEvent(
+ DownloadsPanel.panel,
+ "popuphidden"
+ );
+
+ contextMenu.activateItem(deleteFileItem);
+
+ await TestUtils.waitForCondition(() => !target1.exists());
+
+ await TestUtils.waitForCondition(() => {
+ let downloadsListBox = document.getElementById("downloadsListBox");
+ downloadsListBox.removeAttribute("disabled");
+ return downloadsListBox.childElementCount == 1;
+ });
+
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.download.clearHistoryOnDelete", 0]],
+ });
+ info("trigger the context menu again");
+ let itemTarget2 = document.querySelector(
+ "#downloadsListBox richlistitem .downloadMainArea"
+ );
+ let contextMenu2 = await openContextMenu(itemTarget2);
+ ok(
+ !BrowserTestUtils.is_hidden(deleteFileItem),
+ "deleteFileItem should be visible"
+ );
+ let target2 = downloads[0].target;
+ ok(target2.exists(), "downloaded.js should exist");
+ info(`file path: ${target2.path}`);
+ contextMenu2.activateItem(deleteFileItem);
+ await TestUtils.waitForCondition(() => !target2.exists());
+
+ let downloadsListBox = document.getElementById("downloadsListBox");
+ downloadsListBox.removeAttribute("disabled");
+ Assert.greater(
+ downloadsListBox.childElementCount,
+ 0,
+ "There should be a download in the list"
+ );
+
+ ok(
+ !DownloadsView.richListBox.selectedItem._shell.isCommandEnabled(
+ "downloadsCmd_deleteFile"
+ ),
+ "Delete file command should be disabled"
+ );
+
+ DownloadsPanel.hidePanel();
+ await hiddenPromise;
+});
+
+add_task(async function test_about_downloads_deleteFile_for_history_download() {
+ await task_resetState();
+ await PlacesUtils.history.clear();
+
+ if (!gDownloadDir) {
+ gDownloadDir = await setDownloadDir();
+ }
+
+ let targetFile = await createDownloadedFile(
+ PathUtils.join(gDownloadDir, "test-download.txt"),
+ "blah blah blah"
+ );
+ let endTime;
+ try {
+ endTime = targetFile.creationTime;
+ } catch (e) {
+ endTime = Date.now();
+ }
+ let download = {
+ source: {
+ url: httpUrl(targetFile.leafName),
+ isPrivate: false,
+ },
+ target: {
+ path: targetFile.path,
+ size: targetFile.fileSize,
+ },
+ succeeded: true,
+ stopped: true,
+ endTime,
+ fileSize: targetFile.fileSize,
+ state: 1,
+ };
+
+ function promiseWaitForVisit(aUrl) {
+ return new Promise(resolve => {
+ function listener(aEvents) {
+ Assert.equal(aEvents.length, 1);
+ let event = aEvents[0];
+ Assert.equal(event.type, "page-visited");
+ if (event.url == aUrl) {
+ PlacesObservers.removeListener(["page-visited"], listener);
+ resolve([
+ event.visitTime,
+ event.transitionType,
+ event.lastKnownTitle,
+ ]);
+ }
+ }
+ PlacesObservers.addListener(["page-visited"], listener);
+ });
+ }
+
+ function waitForAnnotation(sourceUriSpec, annotationName) {
+ return TestUtils.waitForCondition(async () => {
+ let pageInfo = await PlacesUtils.history.fetch(sourceUriSpec, {
+ includeAnnotations: true,
+ });
+ return pageInfo && pageInfo.annotations.has(annotationName);
+ }, `Should have found annotation ${annotationName} for ${sourceUriSpec}.`);
+ }
+
+ // Add the download to history using the XPCOM service, then use the
+ // DownloadHistory module to save the associated metadata.
+ let promiseFileAnnotation = waitForAnnotation(
+ download.source.url,
+ "downloads/destinationFileURI"
+ );
+ let promiseMetaAnnotation = waitForAnnotation(
+ download.source.url,
+ "downloads/metaData"
+ );
+ let promiseVisit = promiseWaitForVisit(download.source.url);
+ await DownloadHistory.addDownloadToHistory(download);
+ await promiseVisit;
+ await DownloadHistory.updateMetaData(download);
+ await Promise.all([promiseFileAnnotation, promiseMetaAnnotation]);
+
+ let win = await openLibrary("Downloads");
+ registerCleanupFunction(function () {
+ win?.close();
+ });
+
+ let box = win.document.getElementById("downloadsListBox");
+ ok(box, "Should have list of downloads");
+ is(box.children.length, 1, "Should have 1 download.");
+ let kid = box.firstChild;
+ let desc = kid.querySelector(".downloadTarget");
+ let dl = kid._shell.download;
+ // 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(
+ desc.value.includes("test-download"),
+ `Label '${desc.value}' should include 'test-download'`
+ );
+ ok(kid.selected, "First item should be selected.");
+ ok(dl.placesNode, "Download should have history.");
+ ok(targetFile.exists(), "Download target should exist.");
+ let contextMenu = win.document.getElementById("downloadsContextMenu");
+ let popupShownPromise = BrowserTestUtils.waitForEvent(
+ contextMenu,
+ "popupshown"
+ );
+ EventUtils.synthesizeMouseAtCenter(
+ kid,
+ { type: "contextmenu", button: 2 },
+ win
+ );
+ await popupShownPromise;
+ let popupHiddenPromise = BrowserTestUtils.waitForEvent(
+ contextMenu,
+ "popuphidden"
+ );
+ contextMenu.activateItem(
+ contextMenu.querySelector(".downloadDeleteFileMenuItem")
+ );
+ await popupHiddenPromise;
+ await TestUtils.waitForCondition(() => !targetFile.exists());
+ info("History download target deleted.");
+});
diff --git a/browser/components/downloads/test/browser/browser_downloads_context_menu_selection.js b/browser/components/downloads/test/browser/browser_downloads_context_menu_selection.js
new file mode 100644
index 0000000000..a4a8eacf36
--- /dev/null
+++ b/browser/components/downloads/test/browser/browser_downloads_context_menu_selection.js
@@ -0,0 +1,139 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Test that the context menu refers to the triggering item, even if the
+ * selection was not set preemptively.
+ */
+
+async function createDownloadFiles() {
+ let dir = await setDownloadDir();
+ let downloads = [];
+ downloads.push({
+ state: DownloadsCommon.DOWNLOAD_FAILED,
+ contentType: "text/plain",
+ target: new FileUtils.File(PathUtils.join(dir, "does-not-exist.txt")),
+ });
+ downloads.push({
+ state: DownloadsCommon.DOWNLOAD_FINISHED,
+ contentType: "text/plain",
+ target: await createDownloadedFile(PathUtils.join(dir, "file.txt"), "file"),
+ });
+ return downloads;
+}
+
+add_setup(async function setup() {
+ await PlacesUtils.history.clear();
+ await startServer();
+
+ registerCleanupFunction(async function () {
+ await task_resetState();
+ await PlacesUtils.history.clear();
+ });
+});
+
+add_task(async function test() {
+ // remove download files, empty out collections
+ let downloadList = await Downloads.getList(Downloads.ALL);
+ let downloadCount = (await downloadList.getAll()).length;
+ Assert.equal(downloadCount, 0, "There should be 0 downloads");
+ await task_resetState();
+ let downloads = await createDownloadFiles();
+ await task_addDownloads(downloads);
+ await task_openPanel();
+ let downloadsListBox = document.getElementById("downloadsListBox");
+ await TestUtils.waitForCondition(() => {
+ downloadsListBox.removeAttribute("disabled");
+ return downloadsListBox.childElementCount == downloads.length;
+ });
+
+ // Note we're not doing anything to set the selectedItem here, exactly to
+ // check the context menu doesn't depend on some selection prerequisite.
+
+ let first = downloadsListBox.querySelector("richlistitem");
+ let second = downloadsListBox.querySelector("richlistitem:nth-child(2)");
+
+ info("Check first item");
+ let firstDownload = DownloadsView.itemForElement(first).download;
+ is(
+ DownloadsCommon.stateOfDownload(firstDownload),
+ DownloadsCommon.DOWNLOAD_FINISHED,
+ "Download states match up"
+ );
+ // mousemove to the _other_ download, to ensure it doesn't confuse code.
+ EventUtils.synthesizeMouse(second, -5, -5, { type: "mousemove" });
+ await checkCommandsWithContextMenu(first, {
+ downloadsCmd_show: true,
+ cmd_delete: true,
+ });
+
+ info("Check second item");
+ let secondDownload = DownloadsView.itemForElement(second).download;
+ is(
+ DownloadsCommon.stateOfDownload(secondDownload),
+ DownloadsCommon.DOWNLOAD_FAILED,
+ "Download states match up"
+ );
+ // mousemove to the _other_ download, to ensure it doesn't confuse code.
+ EventUtils.synthesizeMouse(first, -5, -5, { type: "mousemove" });
+ await checkCommandsWithContextMenu(second, {
+ downloadsCmd_show: false,
+ cmd_delete: true,
+ });
+
+ info("Check we don't open a context menu between items.");
+ function listener() {
+ Assert.ok(false, "Should not open a context menu");
+ }
+ document.addEventListener("popupshown", listener);
+ let listRect = downloadsListBox.getBoundingClientRect();
+ let firstRect = first.getBoundingClientRect();
+ let secondRect = second.getBoundingClientRect();
+ let x = parseInt(firstRect.width / 2);
+ Assert.greater(
+ secondRect.y - firstRect.y - firstRect.height,
+ 1,
+ "There should be a gap of at least 1 px for this test"
+ );
+ let y = parseInt(firstRect.y - listRect.y + firstRect.height + 1);
+ info(`Right click at (${x}, ${y})`);
+ EventUtils.synthesizeMouse(downloadsListBox, x, y, {
+ type: "contextmenu",
+ button: 2,
+ });
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ await new Promise(r => setTimeout(r, 100));
+ document.removeEventListener("popupshown", listener);
+
+ let hiddenPromise = BrowserTestUtils.waitForEvent(
+ DownloadsPanel.panel,
+ "popuphidden"
+ );
+ DownloadsPanel.hidePanel();
+ await hiddenPromise;
+});
+
+async function checkCommandsWithContextMenu(element, commands) {
+ let contextMenu = await openContextMenu(element);
+ for (let command in commands) {
+ let enabled = commands[command];
+ let commandStatus = enabled ? "enabled" : "disabled";
+ info(`Checking command ${command} is ${commandStatus}`);
+
+ let commandElt = contextMenu.querySelector(`[command="${command}"]`);
+ Assert.equal(
+ !BrowserTestUtils.is_hidden(commandElt),
+ enabled,
+ `${command} should be ${enabled ? "visible" : "hidden"}`
+ );
+
+ Assert.strictEqual(
+ DownloadsView.richListBox.selectedItem._shell.isCommandEnabled(command),
+ enabled,
+ `${command} should be ${commandStatus}`
+ );
+ }
+ contextMenu.hidePopup();
+}
diff --git a/browser/components/downloads/test/browser/browser_downloads_keynav.js b/browser/components/downloads/test/browser/browser_downloads_keynav.js
new file mode 100644
index 0000000000..23acf20417
--- /dev/null
+++ b/browser/components/downloads/test/browser/browser_downloads_keynav.js
@@ -0,0 +1,255 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+registerCleanupFunction(async function () {
+ await task_resetState();
+});
+
+function changeSelection(listbox, down) {
+ let selectPromise = BrowserTestUtils.waitForEvent(listbox, "select");
+ EventUtils.synthesizeKey(down ? "VK_DOWN" : "VK_UP", {});
+ return selectPromise;
+}
+
+add_task(async function test_downloads_keynav() {
+ // Ensure that state is reset in case previous tests didn't finish.
+ await task_resetState();
+
+ await SpecialPowers.pushPrefEnv({ set: [["accessibility.tabfocus", 7]] });
+
+ // Move the mouse pointer out of the way first so it doesn't
+ // interfere with the selection.
+ let listbox = document.getElementById("downloadsListBox");
+ EventUtils.synthesizeMouse(listbox, -5, -5, { type: "mousemove" });
+
+ let downloads = [];
+ for (let i = 0; i < 2; i++) {
+ downloads.push({ state: DownloadsCommon.DOWNLOAD_FINISHED });
+ }
+ downloads.push({ state: DownloadsCommon.DOWNLOAD_FAILED });
+ downloads.push({ state: DownloadsCommon.DOWNLOAD_BLOCKED });
+
+ await task_addDownloads(downloads);
+ await task_openPanel();
+
+ is(document.activeElement, listbox, "downloads list is focused");
+ is(listbox.selectedIndex, 0, "downloads list selected index starts at 0");
+
+ let footer = document.getElementById("downloadsHistory");
+
+ await changeSelection(listbox, true);
+ is(
+ document.activeElement,
+ listbox,
+ "downloads list is focused after down to index 1"
+ );
+ is(
+ listbox.selectedIndex,
+ 1,
+ "downloads list selected index after down is pressed"
+ );
+
+ checkTabbing(listbox, 1);
+
+ await changeSelection(listbox, true);
+ is(
+ document.activeElement,
+ listbox,
+ "downloads list is focused after down to index 2"
+ );
+ is(
+ listbox.selectedIndex,
+ 2,
+ "downloads list selected index after down to index 2"
+ );
+
+ checkTabbing(listbox, 2);
+
+ await changeSelection(listbox, true);
+ is(
+ document.activeElement,
+ listbox,
+ "downloads list is focused after down to index 3"
+ );
+ is(
+ listbox.selectedIndex,
+ 3,
+ "downloads list selected index after down to index 3"
+ );
+
+ checkTabbing(listbox, 3);
+
+ await changeSelection(listbox, true);
+ is(document.activeElement, footer, "footer is focused");
+ is(
+ listbox.selectedIndex,
+ -1,
+ "downloads list selected index after down to footer"
+ );
+
+ EventUtils.synthesizeKey("VK_TAB", {});
+ is(
+ document.activeElement,
+ listbox,
+ "downloads list should be focused after tab when footer is focused"
+ );
+ is(
+ listbox.selectedIndex,
+ 0,
+ "downloads list should be focused after tab when footer is focused selected index"
+ );
+
+ // Move back to the footer.
+ EventUtils.synthesizeKey("VK_TAB", { shiftKey: true });
+ is(document.activeElement, footer, "downloads footer is focused again");
+ is(
+ listbox.selectedIndex,
+ 0,
+ "downloads footer is focused again selected index"
+ );
+
+ EventUtils.synthesizeKey("VK_DOWN", {});
+ is(
+ document.activeElement,
+ footer,
+ "downloads footer is still focused after down past footer"
+ );
+ is(
+ listbox.selectedIndex,
+ -1,
+ "downloads footer is still focused selected index after down past footer"
+ );
+
+ await changeSelection(listbox, false);
+ is(
+ document.activeElement,
+ listbox,
+ "downloads list is focused after up to index 3"
+ );
+ is(
+ listbox.selectedIndex,
+ 3,
+ "downloads list selected index after up to index 3"
+ );
+
+ await changeSelection(listbox, false);
+ is(
+ document.activeElement,
+ listbox,
+ "downloads list is focused after up to index 2"
+ );
+ is(
+ listbox.selectedIndex,
+ 2,
+ "downloads list selected index after up to index 2"
+ );
+
+ EventUtils.synthesizeMouseAtCenter(listbox.getItemAtIndex(0), {
+ type: "mousemove",
+ });
+ EventUtils.synthesizeMouseAtCenter(listbox.getItemAtIndex(1), {
+ type: "mousemove",
+ });
+ is(listbox.selectedIndex, 0, "downloads list selected index after mousemove");
+
+ checkTabbing(listbox, 0);
+
+ EventUtils.synthesizeKey("VK_UP", {});
+ is(
+ document.activeElement,
+ listbox,
+ "downloads list is still focused after up past start"
+ );
+ is(
+ listbox.selectedIndex,
+ 0,
+ "downloads list is still focused after up past start selected index"
+ );
+
+ // Move the mouse pointer out of the way again so we don't
+ // hover over an item unintentionally if this test is run in verify mode.
+ EventUtils.synthesizeMouse(listbox, -5, -5, { type: "mousemove" });
+
+ await task_resetState();
+});
+
+async function checkTabbing(listbox, buttonIndex) {
+ let button = listbox.getItemAtIndex(buttonIndex).querySelector("button");
+ let footer = document.getElementById("downloadsHistory");
+
+ listbox.clientWidth; // flush layout first
+
+ EventUtils.synthesizeKey("VK_TAB", {});
+ is(
+ document.activeElement,
+ button,
+ "downloads button is focused after tab is pressed"
+ );
+ is(
+ listbox.selectedIndex,
+ buttonIndex,
+ "downloads button selected index after tab is pressed"
+ );
+
+ EventUtils.synthesizeKey("VK_TAB", {});
+ is(
+ document.activeElement,
+ footer,
+ "downloads footer is focused after tab is pressed again"
+ );
+ is(
+ listbox.selectedIndex,
+ buttonIndex,
+ "downloads footer selected index after tab is pressed again"
+ );
+
+ EventUtils.synthesizeKey("VK_TAB", {});
+ is(
+ document.activeElement,
+ listbox,
+ "downloads list is focused after tab is pressed yet again"
+ );
+ is(
+ listbox.selectedIndex,
+ buttonIndex,
+ "downloads list selected index after tab is pressed yet again"
+ );
+
+ EventUtils.synthesizeKey("VK_TAB", { shiftKey: true });
+ is(
+ document.activeElement,
+ footer,
+ "downloads footer is focused after shift+tab is pressed"
+ );
+ is(
+ listbox.selectedIndex,
+ buttonIndex,
+ "downloads footer selected index after shift+tab is pressed"
+ );
+
+ EventUtils.synthesizeKey("VK_TAB", { shiftKey: true });
+ is(
+ document.activeElement,
+ button,
+ "downloads button is focused after shift+tab is pressed again"
+ );
+ is(
+ listbox.selectedIndex,
+ buttonIndex,
+ "downloads button selected index after shift+tab is pressed again"
+ );
+
+ EventUtils.synthesizeKey("VK_TAB", { shiftKey: true });
+ is(
+ document.activeElement,
+ listbox,
+ "downloads list is focused after shift+tab is pressed yet again"
+ );
+ is(
+ listbox.selectedIndex,
+ buttonIndex,
+ "downloads list selected index after shift+tab is pressed yet again"
+ );
+}
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..d1791a5862
--- /dev/null
+++ b/browser/components/downloads/test/browser/browser_downloads_panel_block.js
@@ -0,0 +1,185 @@
+/* Any copyright is dedicated to the Public Domain.
+ * https://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* 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();
+
+ // Handle items backwards, using lastElementChild, to ensure there's no
+ // code wrongly resetting the selection to the first item during the process.
+ let item = DownloadsView.richListBox.lastElementChild;
+
+ info("Open the panel and click the item to show the subview.");
+ let viewPromise = promiseViewShown(DownloadsBlockedSubview.subview);
+ EventUtils.synthesizeMouseAtCenter(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]
+ );
+
+ info("Go back to the main view.");
+ viewPromise = promiseViewShown(DownloadsBlockedSubview.mainView);
+ DownloadsBlockedSubview.panelMultiView.goBack();
+ await viewPromise;
+
+ info("Show the subview again.");
+ viewPromise = promiseViewShown(DownloadsBlockedSubview.subview);
+ EventUtils.synthesizeMouseAtCenter(item, {});
+ await viewPromise;
+
+ info("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 unblockPromise = promiseUnblockAndSaveCalled(item);
+ let hidePromise = promisePanelHidden();
+ // Simulate a mousemove to ensure it's not wrongly being handled by the
+ // panel as the user changing download selection.
+ EventUtils.synthesizeMouseAtCenter(
+ DownloadsBlockedSubview.elements.unblockButton,
+ { type: "mousemove" }
+ );
+ EventUtils.synthesizeMouseAtCenter(
+ DownloadsBlockedSubview.elements.unblockButton,
+ {}
+ );
+ info("waiting for unblockOpen");
+ await unblockPromise;
+ info("waiting for hide panel");
+ await hidePromise;
+
+ window.focus();
+ await SimpleTest.promiseFocus(window);
+
+ info("Reopen the panel and show the subview again.");
+ await openPanel();
+ viewPromise = promiseViewShown(DownloadsBlockedSubview.subview);
+ EventUtils.synthesizeMouseAtCenter(item, {});
+ await viewPromise;
+
+ info("Click the Remove button.");
+ // The panel should close and the item should be removed from it.
+ hidePromise = promisePanelHidden();
+ EventUtils.synthesizeMouseAtCenter(
+ DownloadsBlockedSubview.elements.deleteButton,
+ {}
+ );
+ info("Waiting for hide panel");
+ await hidePromise;
+
+ info("Open the panel again and check the item is gone.");
+ 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 promiseUnblockAndSaveCalled(item) {
+ return new Promise(resolve => {
+ let realFn = item._shell.unblockAndSave;
+ item._shell.unblockAndSave = async () => {
+ item._shell.unblockAndSave = realFn;
+ 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..6a85ba570b
--- /dev/null
+++ b/browser/components/downloads/test/browser/browser_downloads_panel_context_menu.js
@@ -0,0 +1,421 @@
+/*
+ Coverage for context menu state for downloads in the Downloads Panel
+*/
+
+let gDownloadDir;
+const TestFiles = {};
+
+let ReferrerInfo = Components.Constructor(
+ "@mozilla.org/referrer-info;1",
+ "nsIReferrerInfo",
+ "init"
+);
+
+// Load a new URI with a specific referrer.
+let exampleRefInfo = new ReferrerInfo(
+ Ci.nsIReferrerInfo.EMPTY,
+ true,
+ Services.io.newURI("https://example.org")
+);
+
+const MENU_ITEMS = {
+ pause: ".downloadPauseMenuItem",
+ resume: ".downloadResumeMenuItem",
+ unblock: '[command="downloadsCmd_unblock"]',
+ openInSystemViewer: '[command="downloadsCmd_openInSystemViewer"]',
+ alwaysOpenInSystemViewer: '[command="downloadsCmd_alwaysOpenInSystemViewer"]',
+ alwaysOpenSimilarFiles: '[command="downloadsCmd_alwaysOpenSimilarFiles"]',
+ show: '[command="downloadsCmd_show"]',
+ commandsSeparator: "menuseparator,.downloadCommandsSeparator",
+ openReferrer: ".downloadOpenReferrerMenuItem",
+ copyLocation: ".downloadCopyLocationMenuItem",
+ separator: "menuseparator",
+ deleteFile: ".downloadDeleteFileMenuItem",
+ delete: '[command="cmd_delete"]',
+ clearList: '[command="downloadsCmd_clearList"]',
+ clearDownloads: '[command="downloadsCmd_clearDownloads"]',
+};
+
+const TestCasesNewMimetypes = [
+ {
+ name: "Completed txt download",
+ downloads: [
+ {
+ state: DownloadsCommon.DOWNLOAD_FINISHED,
+ contentType: "text/plain",
+ target: {},
+ source: {
+ referrerInfo: exampleRefInfo,
+ },
+ },
+ ],
+ expected: {
+ menu: [
+ MENU_ITEMS.alwaysOpenSimilarFiles,
+ MENU_ITEMS.show,
+ MENU_ITEMS.commandsSeparator,
+ MENU_ITEMS.openReferrer,
+ MENU_ITEMS.copyLocation,
+ MENU_ITEMS.separator,
+ MENU_ITEMS.deleteFile,
+ MENU_ITEMS.delete,
+ MENU_ITEMS.clearList,
+ ],
+ },
+ },
+ {
+ name: "Canceled txt download",
+ downloads: [
+ {
+ state: DownloadsCommon.DOWNLOAD_CANCELED,
+ contentType: "text/plain",
+ target: {},
+ source: {
+ referrerInfo: exampleRefInfo,
+ },
+ },
+ ],
+ expected: {
+ menu: [
+ MENU_ITEMS.openReferrer,
+ MENU_ITEMS.copyLocation,
+ MENU_ITEMS.separator,
+ MENU_ITEMS.delete,
+ MENU_ITEMS.clearList,
+ ],
+ },
+ },
+ {
+ name: "Completed unknown ext download with application/octet-stream",
+ overrideExtension: "unknownExtension",
+ downloads: [
+ {
+ state: DownloadsCommon.DOWNLOAD_FINISHED,
+ contentType: "application/octet-stream",
+ target: {},
+ source: {
+ referrerInfo: exampleRefInfo,
+ },
+ },
+ ],
+ expected: {
+ menu: [
+ MENU_ITEMS.show,
+ MENU_ITEMS.commandsSeparator,
+ MENU_ITEMS.openReferrer,
+ MENU_ITEMS.copyLocation,
+ MENU_ITEMS.separator,
+ MENU_ITEMS.deleteFile,
+ MENU_ITEMS.delete,
+ MENU_ITEMS.clearList,
+ ],
+ },
+ },
+ {
+ name: "Completed txt download with application/octet-stream",
+ overrideExtension: "txt",
+ downloads: [
+ {
+ state: DownloadsCommon.DOWNLOAD_FINISHED,
+ contentType: "application/octet-stream",
+ target: {},
+ source: {
+ referrerInfo: exampleRefInfo,
+ },
+ },
+ ],
+ expected: {
+ menu: [
+ // Despite application/octet-stream content type, ensure
+ // alwaysOpenSimilarFiles still appears since txt files
+ // are supported file types.
+ MENU_ITEMS.alwaysOpenSimilarFiles,
+ MENU_ITEMS.show,
+ MENU_ITEMS.commandsSeparator,
+ MENU_ITEMS.openReferrer,
+ MENU_ITEMS.copyLocation,
+ MENU_ITEMS.separator,
+ MENU_ITEMS.deleteFile,
+ MENU_ITEMS.delete,
+ MENU_ITEMS.clearList,
+ ],
+ },
+ },
+];
+
+const TestCasesDeletedFile = [
+ {
+ name: "Download with file deleted",
+ downloads: [
+ {
+ state: DownloadsCommon.DOWNLOAD_FINISHED,
+ contentType: "text/plain",
+ target: {},
+ source: {
+ referrerInfo: exampleRefInfo,
+ },
+ deleted: true,
+ },
+ ],
+ expected: {
+ menu: [
+ MENU_ITEMS.alwaysOpenSimilarFiles,
+ MENU_ITEMS.openReferrer,
+ MENU_ITEMS.copyLocation,
+ MENU_ITEMS.separator,
+ MENU_ITEMS.delete,
+ MENU_ITEMS.clearList,
+ ],
+ },
+ },
+];
+
+const TestCasesMultipleFiles = [
+ {
+ name: "Multiple files",
+ downloads: [
+ {
+ state: DownloadsCommon.DOWNLOAD_FINISHED,
+ contentType: "text/plain",
+ target: {},
+ source: {
+ referrerInfo: exampleRefInfo,
+ },
+ },
+ {
+ state: DownloadsCommon.DOWNLOAD_FINISHED,
+ contentType: "text/plain",
+ target: {},
+ source: {
+ referrerInfo: exampleRefInfo,
+ },
+ deleted: true,
+ },
+ ],
+ expected: {
+ menu: [
+ MENU_ITEMS.alwaysOpenSimilarFiles,
+ MENU_ITEMS.openReferrer,
+ MENU_ITEMS.copyLocation,
+ MENU_ITEMS.separator,
+ MENU_ITEMS.delete,
+ MENU_ITEMS.clearList,
+ ],
+ },
+ itemIndex: 1,
+ },
+];
+
+add_setup(async function () {
+ // 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(
+ PathUtils.join(gDownloadDir, "downloaded.pdf"),
+ DATA_PDF
+ );
+ info("Created downloaded PDF file at:" + TestFiles.pdf.path);
+ TestFiles.txt = await createDownloadedFile(
+ PathUtils.join(gDownloadDir, "downloaded.txt"),
+ "Test file"
+ );
+ info("Created downloaded text file at:" + TestFiles.txt.path);
+ TestFiles.unknownExtension = await createDownloadedFile(
+ PathUtils.join(gDownloadDir, "downloaded.unknownExtension"),
+ "Test file"
+ );
+ info(
+ "Created downloaded unknownExtension file at:" +
+ TestFiles.unknownExtension.path
+ );
+ TestFiles.nonexistentFile = new FileUtils.File(
+ PathUtils.join(gDownloadDir, "nonexistent")
+ );
+ info(
+ "Created nonexistent downloaded file at:" + TestFiles.nonexistentFile.path
+ );
+});
+
+// non default mimetypes
+for (let testData of TestCasesNewMimetypes) {
+ 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]);
+}
+
+for (let testData of TestCasesDeletedFile) {
+ 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]);
+}
+
+for (let testData of TestCasesMultipleFiles) {
+ 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({
+ overrideExtension = null,
+ downloads = [],
+ expected,
+ itemIndex = 0,
+}) {
+ // prepare downloads
+ await prepareDownloads(downloads, overrideExtension);
+ let downloadList = await Downloads.getList(Downloads.PUBLIC);
+ let download = (await downloadList.getAll())[itemIndex];
+ info("Download succeeded? " + download.succeeded);
+ info("Download target exists? " + download.target.exists);
+
+ // open panel
+ await task_openPanel();
+ await TestUtils.waitForCondition(() => {
+ let downloadsListBox = document.getElementById("downloadsListBox");
+ downloadsListBox.removeAttribute("disabled");
+ return downloadsListBox.childElementCount == downloads.length;
+ });
+
+ let itemTarget = document
+ .querySelectorAll("#downloadsListBox richlistitem")
+ [itemIndex].querySelector(".downloadMainArea");
+ EventUtils.synthesizeMouse(itemTarget, 1, 1, { type: "mousemove" });
+ is(
+ DownloadsView.richListBox.selectedIndex,
+ 0,
+ "moving the mouse resets the richlistbox's selected index"
+ );
+
+ info("trigger the context menu");
+ let contextMenu = await openContextMenu(itemTarget);
+
+ // FIXME: This works in practice, but simulating the context menu opening
+ // doesn't seem to automatically set the selected index.
+ DownloadsView.richListBox.selectedIndex = itemIndex;
+ EventUtils.synthesizeMouse(itemTarget, 1, 1, { type: "mousemove" });
+ is(
+ DownloadsView.richListBox.selectedIndex,
+ itemIndex,
+ "selected index after opening the context menu and moving the mouse"
+ );
+
+ 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, overrideExtension = null) {
+ for (let props of downloads) {
+ info(JSON.stringify(props));
+ if (props.state !== DownloadsCommon.DOWNLOAD_FINISHED) {
+ continue;
+ }
+ if (props.deleted) {
+ props.target = TestFiles.nonexistentFile;
+ continue;
+ }
+ switch (props.contentType) {
+ case "application/pdf":
+ props.target = TestFiles.pdf;
+ break;
+ case "text/plain":
+ props.target = TestFiles.txt;
+ break;
+ case "application/octet-stream":
+ props.target = TestFiles[overrideExtension];
+ break;
+ }
+ ok(props.target instanceof Ci.nsIFile, "download target is a nsIFile");
+ }
+ // If we'd just insert downloads as defined in the test case, they would
+ // appear reversed in the panel, because they will be in descending insertion
+ // order (newest at the top). The problem is we define an itemIndex based on
+ // the downloads array, and it would be weird to define it based on a
+ // reversed order. Short, we just reverse the array to preserve the order.
+ await task_addDownloads(downloads.reverse());
+}
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_disable_items.js b/browser/components/downloads/test/browser/browser_downloads_panel_disable_items.js
new file mode 100644
index 0000000000..d3b5b91b96
--- /dev/null
+++ b/browser/components/downloads/test/browser/browser_downloads_panel_disable_items.js
@@ -0,0 +1,171 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_URI = "https://example.com";
+const TEST_PATH = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ TEST_URI
+);
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.download.alwaysOpenPanel", true],
+ ["browser.download.always_ask_before_handling_new_types", false],
+ ["security.dialog_enable_delay", 1000],
+ ],
+ });
+ // Remove download files from previous tests
+ await task_resetState();
+ 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");
+});
+
+/**
+ * Tests that the download items remain enabled when we manually open
+ * the downloads panel by clicking the downloads button.
+ */
+add_task(async function test_downloads_panel_downloads_button() {
+ let panelOpenedPromise = promisePanelOpened();
+ await task_addDownloads([{ state: DownloadsCommon.DOWNLOAD_FINISHED }]);
+ await panelOpenedPromise;
+
+ // The downloads panel will open automatically after task_addDownloads
+ // creates a download file. Let's close the panel and reopen it again
+ // (but this time manually) to ensure the download items are not disabled.
+ DownloadsPanel.hidePanel();
+
+ ok(!DownloadsPanel.isPanelShowing, "Downloads Panel should not be visible");
+
+ info("Manually open the download panel to view list of downloads");
+ let downloadsButton = document.getElementById("downloads-button");
+ EventUtils.synthesizeMouseAtCenter(downloadsButton, {});
+ let downloadsListBox = document.getElementById("downloadsListBox");
+
+ ok(downloadsListBox, "downloadsListBox richlistitem should be visible");
+ is(
+ downloadsListBox.childElementCount,
+ 1,
+ "downloadsListBox should have 1 download"
+ );
+ ok(
+ !downloadsListBox.getAttribute("disabled"),
+ "All download items in the downloads panel should not be disabled"
+ );
+
+ info("Cleaning up downloads");
+ await task_resetState();
+});
+
+/**
+ * Tests that the download items are disabled when the downloads panel is
+ * automatically opened as a result of a new download.
+ */
+add_task(async function test_downloads_panel_new_download() {
+ // Overwrite DownloadsCommon.openDownload to prevent file from opening during tests
+ const originalOpenDownload = DownloadsCommon.openDownload;
+ DownloadsCommon.openDownload = async () => {
+ ok(false, "openDownload was called when it was not expected");
+ };
+ let newTabPromise = BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ opening: TEST_PATH + "foo.txt",
+ waitForLoad: false,
+ waitForStateStop: true,
+ });
+
+ await promisePanelOpened();
+ let downloadsListBox = document.getElementById("downloadsListBox");
+
+ ok(downloadsListBox, "downloadsListBox richlistitem should be visible");
+ await BrowserTestUtils.waitForMutationCondition(
+ downloadsListBox,
+ { childList: true },
+ () => downloadsListBox.childElementCount == 1
+ );
+ info("downloadsListBox should have 1 download");
+ ok(
+ downloadsListBox.getAttribute("disabled"),
+ "All download items in the downloads panel should first be disabled"
+ );
+
+ let newTab = await newTabPromise;
+
+ // Press enter 6 times at 100ms intervals.
+ EventUtils.synthesizeKey("KEY_Enter", {}, window);
+ for (let i = 0; i < 5; i++) {
+ // There's no other way to allow some time to pass and ensure we're
+ // genuinely testing that these keypresses postpone the enabling of
+ // the items, so disable this check for this line:
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ await new Promise(r => setTimeout(r, 100));
+ EventUtils.synthesizeKey("KEY_Enter", {}, window);
+ }
+ // Measure when we finished.
+ let keyTime = Date.now();
+
+ await BrowserTestUtils.waitForMutationCondition(
+ downloadsListBox,
+ { attributeFilter: ["disabled"] },
+ () => !downloadsListBox.hasAttribute("disabled")
+ );
+ Assert.greater(
+ Date.now(),
+ keyTime + 750,
+ "Should have waited at least another 750ms after this keypress."
+ );
+ let openedDownload = new Promise(resolve => {
+ DownloadsCommon.openDownload = async () => {
+ ok(true, "openDownload should have been called");
+ resolve();
+ };
+ });
+
+ info("All download items in the download panel should now be enabled");
+ EventUtils.synthesizeKey("KEY_Enter", {}, window);
+ await openedDownload;
+
+ await task_resetState();
+ DownloadsCommon.openDownload = originalOpenDownload;
+ BrowserTestUtils.removeTab(newTab);
+});
+
+/**
+ * Tests that the disabled attribute does not exist when we close the
+ * downloads panel before the disabled state timeout resolves.
+ */
+add_task(async function test_downloads_panel_close_panel_early() {
+ info("Creating mock completed downloads");
+ await task_addDownloads([{ state: DownloadsCommon.DOWNLOAD_FINISHED }]);
+
+ // The downloads panel may open automatically after task_addDownloads
+ // creates a download file. Let's close the panel and reopen it again
+ // (but this time manually).
+ DownloadsPanel.hidePanel();
+
+ ok(!DownloadsPanel.isPanelShowing, "Downloads Panel should not be visible");
+
+ info("Manually open the download panel to view list of downloads");
+ let downloadsButton = document.getElementById("downloads-button");
+ EventUtils.synthesizeMouseAtCenter(downloadsButton, {});
+ let downloadsListBox = document.getElementById("downloadsListBox");
+
+ ok(downloadsListBox, "downloadsListBox richlistitem should be visible");
+ is(
+ downloadsListBox.childElementCount,
+ 1,
+ "downloadsListBox should have 1 download"
+ );
+
+ DownloadsPanel.hidePanel();
+ await BrowserTestUtils.waitForCondition(
+ () => !downloadsListBox.getAttribute("disabled")
+ );
+ info("downloadsListBox 'disabled' attribute should not exist");
+
+ info("Cleaning up downloads");
+ await task_resetState();
+});
diff --git a/browser/components/downloads/test/browser/browser_downloads_panel_dontshow.js b/browser/components/downloads/test/browser/browser_downloads_panel_dontshow.js
new file mode 100644
index 0000000000..28c7bc302f
--- /dev/null
+++ b/browser/components/downloads/test/browser/browser_downloads_panel_dontshow.js
@@ -0,0 +1,126 @@
+// This test verifies that the download panel opens when a
+// download occurs but not when a user manually saves a page.
+
+let MockFilePicker = SpecialPowers.MockFilePicker;
+MockFilePicker.init(window);
+
+async function promiseDownloadFinished(list) {
+ return new Promise(resolve => {
+ list.addView({
+ onDownloadChanged(download) {
+ download.launchWhenSucceeded = false;
+ if (download.succeeded || download.error) {
+ list.removeView(this);
+ resolve(download);
+ }
+ },
+ });
+ });
+}
+
+function openTestPage() {
+ return BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ `https://www.example.com/document-builder.sjs?html=
+ <html><body>
+ <a id='normallink' href='https://www.example.com'>Link1</a>
+ <a id='downloadlink' href='https://www.example.com' download='file.txt'>Link2</a>
+ </body</html>
+ `
+ );
+}
+
+add_task(async function download_saveas_file() {
+ let tab = await openTestPage();
+
+ for (let testname of ["save link", "save page"]) {
+ if (testname == "save link") {
+ let menu = document.getElementById("contentAreaContextMenu");
+ let popupShown = BrowserTestUtils.waitForEvent(menu, "popupshown");
+ BrowserTestUtils.synthesizeMouse(
+ "#normallink",
+ 5,
+ 5,
+ { type: "contextmenu", button: 2 },
+ gBrowser.selectedBrowser
+ );
+ await popupShown;
+ }
+
+ let list = await Downloads.getList(Downloads.PUBLIC);
+ let downloadFinishedPromise = promiseDownloadFinished(list);
+
+ let saveFile = Services.dirsvc.get("TmpD", Ci.nsIFile);
+ saveFile.append("testsavedir");
+ if (!saveFile.exists()) {
+ saveFile.create(Ci.nsIFile.DIRECTORY_TYPE, 0o755);
+ }
+
+ await new Promise(resolve => {
+ MockFilePicker.showCallback = function (fp) {
+ saveFile.append("sample");
+ MockFilePicker.setFiles([saveFile]);
+ setTimeout(() => {
+ resolve(fp.defaultString);
+ }, 0);
+ return Ci.nsIFilePicker.returnOK;
+ };
+
+ if (testname == "save link") {
+ let menu = document.getElementById("contentAreaContextMenu");
+ let menuitem = document.getElementById("context-savelink");
+ menu.activateItem(menuitem);
+ } else if (testname == "save page") {
+ document.getElementById("Browser:SavePage").doCommand();
+ }
+ });
+
+ await downloadFinishedPromise;
+ is(
+ DownloadsPanel.panel.state,
+ "closed",
+ "downloads panel closed after download link after " + testname
+ );
+ }
+
+ await task_resetState();
+
+ MockFilePicker.cleanup();
+ BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function download_link() {
+ let tab = await openTestPage();
+
+ let list = await Downloads.getList(Downloads.PUBLIC);
+ let downloadFinishedPromise = promiseDownloadFinished(list);
+
+ let panelOpenedPromise = promisePanelOpened();
+
+ BrowserTestUtils.synthesizeMouse(
+ "#downloadlink",
+ 5,
+ 5,
+ {},
+ gBrowser.selectedBrowser
+ );
+
+ let download = await downloadFinishedPromise;
+ await panelOpenedPromise;
+
+ is(
+ DownloadsPanel.panel.state,
+ "open",
+ "downloads panel open after download link clicked"
+ );
+
+ DownloadsPanel.hidePanel();
+
+ await task_resetState();
+
+ BrowserTestUtils.removeTab(tab);
+
+ try {
+ await IOUtils.remove(download.target.path);
+ } catch (ex) {}
+});
diff --git a/browser/components/downloads/test/browser/browser_downloads_panel_focus.js b/browser/components/downloads/test/browser/browser_downloads_panel_focus.js
new file mode 100644
index 0000000000..ecfae76b88
--- /dev/null
+++ b/browser/components/downloads/test/browser/browser_downloads_panel_focus.js
@@ -0,0 +1,108 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.download.always_ask_before_handling_new_types", false]],
+ });
+
+ registerCleanupFunction(async () => {
+ info("Resetting downloads and closing downloads panel");
+ await task_resetState();
+ });
+
+ 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");
+});
+
+// Test that the top item in the panel always gets focus upon opening the panel.
+add_task(async function test_focus() {
+ info("creating a download and setting it to in progress");
+ await task_addDownloads([{ state: DownloadsCommon.DOWNLOAD_DOWNLOADING }]);
+ let publicList = await Downloads.getList(Downloads.PUBLIC);
+ let downloads = await publicList.getAll();
+ downloads[0].stopped = false;
+
+ info("waiting for the panel to open");
+ await task_openPanel();
+ await BrowserTestUtils.waitForCondition(
+ () => !DownloadsView.richListBox.getAttribute("disabled")
+ );
+
+ is(
+ DownloadsView.richListBox.itemCount,
+ 1,
+ "there should be exactly one download listed"
+ );
+ // Most of the time if we want to check which thing has focus, we can just ask
+ // Services.focus to tell us. But the downloads panel uses a <richlistbox>,
+ // and when an item in one of those has focus, the focus manager actually
+ // thinks that *the list itself* has focus, and everything below that is
+ // handled within the widget. So, the best we can do is check that the list is
+ // focused and then that the selected item within the list is correct.
+ is(
+ Services.focus.focusedElement,
+ DownloadsView.richListBox,
+ "the downloads list should have focus"
+ );
+ is(
+ DownloadsView.richListBox.itemChildren[0],
+ DownloadsView.richListBox.selectedItem,
+ "the focused item should be the only download in the list"
+ );
+
+ info("closing the panel and creating a second download");
+ DownloadsPanel.hidePanel();
+ await task_addDownloads([{ state: DownloadsCommon.DOWNLOAD_DOWNLOADING }]);
+
+ info("waiting for the panel to open after starting the second download");
+ await task_openPanel();
+ await BrowserTestUtils.waitForCondition(
+ () => !DownloadsView.richListBox.getAttribute("disabled")
+ );
+
+ is(
+ DownloadsView.richListBox.itemCount,
+ 2,
+ "there should be two downloads listed"
+ );
+ is(
+ Services.focus.focusedElement,
+ DownloadsView.richListBox,
+ "the downloads list should have focus"
+ );
+ is(
+ DownloadsView.richListBox.itemChildren[0],
+ DownloadsView.richListBox.selectedItem,
+ "the focused item should be the first download in the list"
+ );
+
+ info("closing the panel and creating a third download");
+ DownloadsPanel.hidePanel();
+ await task_addDownloads([{ state: DownloadsCommon.DOWNLOAD_DOWNLOADING }]);
+
+ info("waiting for the panel to open after starting the third download");
+ await task_openPanel();
+ await BrowserTestUtils.waitForCondition(
+ () => !DownloadsView.richListBox.getAttribute("disabled")
+ );
+
+ is(
+ DownloadsView.richListBox.itemCount,
+ 3,
+ "there should be three downloads listed"
+ );
+ is(
+ Services.focus.focusedElement,
+ DownloadsView.richListBox,
+ "the downloads list should have focus"
+ );
+ is(
+ DownloadsView.richListBox.itemChildren[0],
+ DownloadsView.richListBox.selectedItem,
+ "the focused item should be the first download in the list"
+ );
+});
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..b154d20f84
--- /dev/null
+++ b/browser/components/downloads/test/browser/browser_downloads_panel_height.js
@@ -0,0 +1,35 @@
+/* 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");
+ // downloading two items since the download panel only shows up when at least one item is in it
+ await task_addDownloads([{ state: DownloadsCommon.DOWNLOAD_FINISHED }]);
+ 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();
+ // keep at least one item in the download list since the panel disabled when it is empty
+ await task_addDownloads([{ state: DownloadsCommon.DOWNLOAD_FINISHED }]);
+
+ 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_panel_opens.js b/browser/components/downloads/test/browser/browser_downloads_panel_opens.js
new file mode 100644
index 0000000000..499b5320da
--- /dev/null
+++ b/browser/components/downloads/test/browser/browser_downloads_panel_opens.js
@@ -0,0 +1,674 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+let { MockFilePicker } = SpecialPowers;
+MockFilePicker.init(window);
+registerCleanupFunction(() => MockFilePicker.cleanup());
+
+/**
+ * Check that the downloads panel opens when a download is spoofed.
+ */
+async function checkPanelOpens() {
+ info("Waiting for panel to open.");
+ let promise = promisePanelOpened();
+ DownloadsCommon.getData(window)._notifyDownloadEvent("start");
+ is(
+ DownloadsPanel.isPanelShowing,
+ true,
+ "Panel state should indicate a preparation to be opened."
+ );
+ await promise;
+
+ is(DownloadsPanel.panel.state, "open", "Panel should be opened.");
+
+ DownloadsPanel.hidePanel();
+}
+
+/**
+ * Start a download and check that the downloads panel opens correctly according
+ * to the download parameter, openDownloadsListOnStart
+ * @param {boolean} [openDownloadsListOnStart]
+ * true (default) - open downloads panel when download starts
+ * false - no downloads panel; update indicator attention state
+ */
+async function downloadAndCheckPanel({ openDownloadsListOnStart = true } = {}) {
+ info("creating a download and setting it to in progress");
+ await task_addDownloads([
+ {
+ state: DownloadsCommon.DOWNLOAD_DOWNLOADING,
+ openDownloadsListOnStart,
+ },
+ ]);
+ let publicList = await Downloads.getList(Downloads.PUBLIC);
+ let downloads = await publicList.getAll();
+ downloads[0].stopped = false;
+
+ // Make sure we remove that download at the end of the test.
+ let oldShowEventNotification = DownloadsIndicatorView.showEventNotification;
+ registerCleanupFunction(async () => {
+ for (let download of downloads) {
+ await publicList.remove(download);
+ }
+ DownloadsIndicatorView.showEventNotification = oldShowEventNotification;
+ });
+
+ // Instead of the panel opening, the download notification should be shown.
+ let promiseDownloadStartedNotification = new Promise(resolve => {
+ DownloadsIndicatorView.showEventNotification = aType => {
+ if (aType == "start") {
+ resolve();
+ }
+ };
+ });
+
+ DownloadsCommon.getData(window)._notifyDownloadEvent("start", {
+ openDownloadsListOnStart,
+ });
+ is(
+ DownloadsPanel.isPanelShowing,
+ false,
+ "Panel state should indicate it is not preparing to be opened"
+ );
+
+ info("waiting for download to start");
+ await promiseDownloadStartedNotification;
+
+ DownloadsIndicatorView.showEventNotification = oldShowEventNotification;
+ is(DownloadsPanel.panel.state, "closed", "Panel should be closed");
+}
+
+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();
+}
+
+/**
+ * Test that the downloads panel correctly opens or doesn't open based on
+ * whether the download triggered a dialog already. If askWhereToSave is true,
+ * we should get a file picker dialog. If preferredAction is alwaysAsk, we
+ * should get an unknown content type dialog. If neither of those is true, we
+ * should get no dialog at all, and expect the downloads panel to open.
+ * @param {boolean} [expectPanelToOpen] true - fail if panel doesn't open
+ * false (default) - fail if it opens
+ * @param {number} [preferredAction] Default download action:
+ * 0 (default) - save download to disk
+ * 1 - open UCT dialog first
+ * @param {boolean} [askWhereToSave] true - open file picker dialog
+ * false (default) - use download dir
+ */
+async function testDownloadsPanelAfterDialog({
+ expectPanelToOpen = false,
+ preferredAction,
+ askWhereToSave = false,
+} = {}) {
+ const { saveToDisk, alwaysAsk } = Ci.nsIHandlerInfo;
+ if (![saveToDisk, alwaysAsk].includes(preferredAction)) {
+ preferredAction = saveToDisk;
+ }
+ const openUCT = preferredAction === alwaysAsk;
+ const TEST_PATH = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "https://example.com"
+ );
+ const MimeSvc = Cc["@mozilla.org/mime;1"].getService(Ci.nsIMIMEService);
+ const HandlerSvc = Cc["@mozilla.org/uriloader/handler-service;1"].getService(
+ Ci.nsIHandlerService
+ );
+ let publicList = await Downloads.getList(Downloads.PUBLIC);
+
+ for (let download of await publicList.getAll()) {
+ await publicList.remove(download);
+ }
+
+ // We need to test the changes from bug 1739348, where the helper app service
+ // sets a flag based on whether a file picker dialog was opened, and this flag
+ // determines whether the downloads panel will be opened as the download
+ // starts. We need to actually hit "Save" for the download to start, but we
+ // can't interact with the real file picker dialog. So this temporarily
+ // replaces it with a barebones component that plugs into the helper app
+ // service and tells it to start saving the file to the default path.
+ if (askWhereToSave) {
+ MockFilePicker.returnValue = MockFilePicker.returnOK;
+ MockFilePicker.showCallback = function (fp) {
+ // Get the default location from the helper app service.
+ let testFile = MockFilePicker.displayDirectory.clone();
+ testFile.append(fp.defaultString);
+ info("File picker download path: " + testFile.path);
+ MockFilePicker.setFiles([testFile]);
+ MockFilePicker.filterIndex = 0; // kSaveAsType_Complete
+ MockFilePicker.showCallback = null;
+ // Confirm that saving should proceed. The helper app service uses this
+ // value to determine whether to invoke launcher.saveDestinationAvailable
+ return MockFilePicker.returnOK;
+ };
+ }
+
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.download.useDownloadDir", !askWhereToSave],
+ ["browser.download.always_ask_before_handling_new_types", openUCT],
+ ["security.dialog_enable_delay", 0],
+ ],
+ });
+
+ // Configure the handler for the file according to parameters.
+ let mimeInfo = MimeSvc.getFromTypeAndExtension("text/plain", "txt");
+ let existed = HandlerSvc.exists(mimeInfo);
+ mimeInfo.alwaysAskBeforeHandling = openUCT;
+ mimeInfo.preferredAction = preferredAction;
+ HandlerSvc.store(mimeInfo);
+ registerCleanupFunction(async () => {
+ // Reset the handler to its original state.
+ if (existed) {
+ HandlerSvc.store(mimeInfo);
+ } else {
+ HandlerSvc.remove(mimeInfo);
+ }
+ await publicList.removeFinished();
+ BrowserTestUtils.removeTab(loadingTab);
+ });
+
+ let dialogWindowPromise = BrowserTestUtils.domWindowOpenedAndLoaded();
+ let downloadFinishedPromise = new Promise(resolve => {
+ publicList.addView({
+ onDownloadChanged(download) {
+ info("Download changed!");
+ if (download.succeeded || download.error) {
+ info("Download succeeded or failed.");
+ publicList.removeView(this);
+ resolve(download);
+ }
+ },
+ });
+ });
+ let panelOpenedPromise = expectPanelToOpen ? promisePanelOpened() : null;
+
+ // Open the tab that will trigger the download.
+ let loadingTab = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ opening: TEST_PATH + "foo.txt",
+ waitForLoad: false,
+ waitForStateStop: true,
+ });
+
+ // Wait for a UCT dialog if the handler was set up to open one.
+ if (openUCT) {
+ let dialogWindow = await dialogWindowPromise;
+ is(
+ dialogWindow.location.href,
+ "chrome://mozapps/content/downloads/unknownContentType.xhtml",
+ "Should have seen the unknown content dialogWindow."
+ );
+ let doc = dialogWindow.document;
+ let dialog = doc.getElementById("unknownContentType");
+ let radio = doc.getElementById("save");
+ let button = dialog.getButton("accept");
+
+ await TestUtils.waitForCondition(
+ () => !button.disabled,
+ "Waiting for the UCT dialog's Accept button to be enabled."
+ );
+ ok(!radio.hidden, "The Save option should be visible");
+ // Make sure we aren't opening the file.
+ radio.click();
+ ok(radio.selected, "The Save option should be selected");
+ button.disabled = false;
+ dialog.acceptDialog();
+ }
+
+ info("Waiting for download to finish.");
+ let download = await downloadFinishedPromise;
+ ok(!download.error, "There should be no error.");
+ is(
+ DownloadsPanel.isPanelShowing,
+ expectPanelToOpen,
+ `Panel should${expectPanelToOpen ? " " : " not "}be showing.`
+ );
+ if (DownloadsPanel.isPanelShowing) {
+ await panelOpenedPromise;
+ let hiddenPromise = BrowserTestUtils.waitForPopupEvent(
+ DownloadsPanel.panel,
+ "hidden"
+ );
+ DownloadsPanel.hidePanel();
+ await hiddenPromise;
+ }
+ if (download?.target.exists) {
+ try {
+ info("Removing test file: " + download.target.path);
+ if (Services.appinfo.OS === "WINNT") {
+ await IOUtils.setPermissions(download.target.path, 0o600);
+ }
+ await IOUtils.remove(download.target.path);
+ } catch (ex) {
+ /* ignore */
+ }
+ }
+ for (let dl of await publicList.getAll()) {
+ await publicList.remove(dl);
+ }
+ BrowserTestUtils.removeTab(loadingTab);
+}
+
+/**
+ * Make sure the downloads panel opens automatically with a new download.
+ */
+add_task(async function test_downloads_panel_opens() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.download.always_ask_before_handling_new_types", false],
+ ["browser.download.alwaysOpenPanel", true],
+ ],
+ });
+ await checkPanelOpens();
+});
+
+add_task(async function test_customizemode_doesnt_wreck_things() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.download.always_ask_before_handling_new_types", false],
+ ["browser.download.alwaysOpenPanel", true],
+ ],
+ });
+
+ // Enter customize mode:
+ let customizationReadyPromise = BrowserTestUtils.waitForEvent(
+ gNavToolbox,
+ "customizationready"
+ );
+ gCustomizeMode.enter();
+ await customizationReadyPromise;
+
+ info("Try to open the panel (will not work, in customize mode)");
+ let promise = promisePanelOpened();
+ DownloadsCommon.getData(window)._notifyDownloadEvent("start");
+ await TestUtils.waitForCondition(
+ () => DownloadsPanel._state == DownloadsPanel.kStateHidden,
+ "Should try to show but stop short and hide the panel"
+ );
+ is(
+ DownloadsPanel._state,
+ DownloadsPanel.kStateHidden,
+ "Should not start opening the panel."
+ );
+
+ let afterCustomizationPromise = BrowserTestUtils.waitForEvent(
+ gNavToolbox,
+ "aftercustomization"
+ );
+ gCustomizeMode.exit();
+ await afterCustomizationPromise;
+
+ // Avoid a failure on Linux where the window isn't active for some reason,
+ // which prevents the window's downloads panel from opening.
+ if (Services.focus.activeWindow != window) {
+ info("Main window is not active, trying to focus.");
+ await SimpleTest.promiseFocus(window);
+ is(Services.focus.activeWindow, window, "Main window should be active.");
+ }
+ DownloadsCommon.getData(window)._notifyDownloadEvent("start");
+ await TestUtils.waitForCondition(
+ () => DownloadsPanel.isPanelShowing,
+ "Panel state should indicate a preparation to be opened"
+ );
+ await promise;
+
+ is(DownloadsPanel.panel.state, "open", "Panel should be opened");
+
+ DownloadsPanel.hidePanel();
+});
+
+/**
+ * Make sure the downloads panel _does not_ open automatically if we set the
+ * pref telling it not to do that.
+ */
+add_task(async function test_downloads_panel_opening_pref() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.download.always_ask_before_handling_new_types", false],
+ ["browser.download.alwaysOpenPanel", false],
+ ],
+ });
+ registerCleanupFunction(async () => {
+ await SpecialPowers.popPrefEnv();
+ });
+ await downloadAndCheckPanel();
+ await SpecialPowers.popPrefEnv();
+});
+
+/**
+ * Make sure the downloads panel _does not_ open automatically if we pass the
+ * parameter telling it not to do that to the download constructor.
+ */
+add_task(async function test_downloads_openDownloadsListOnStart_param() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.download.always_ask_before_handling_new_types", false],
+ ["browser.download.alwaysOpenPanel", true],
+ ],
+ });
+ registerCleanupFunction(async () => {
+ await SpecialPowers.popPrefEnv();
+ });
+ await downloadAndCheckPanel({ openDownloadsListOnStart: false });
+ await SpecialPowers.popPrefEnv();
+});
+
+/**
+ * Make sure the downloads panel _does not_ open automatically when an
+ * extension calls the browser.downloads.download API method while it is
+ * not handling user input, but that we do open it automatically when
+ * the same WebExtensions API is called while handling user input
+ * (See Bug 1759231)
+ */
+add_task(async function test_downloads_panel_on_webext_download_api() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.download.always_ask_before_handling_new_types", false],
+ ["browser.download.alwaysOpenPanel", true],
+ ],
+ });
+ registerCleanupFunction(async () => {
+ await SpecialPowers.popPrefEnv();
+ });
+
+ const extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["downloads"],
+ },
+ background() {
+ async function startDownload(downloadOptions) {
+ /* globals browser */
+ const downloadId = await browser.downloads.download(downloadOptions);
+ const downloadDone = new Promise(resolve => {
+ browser.downloads.onChanged.addListener(function listener(delta) {
+ browser.test.log(`downloads.onChanged = ${JSON.stringify(delta)}`);
+ if (
+ delta.id == downloadId &&
+ delta.state?.current !== "in_progress"
+ ) {
+ browser.downloads.onChanged.removeListener(listener);
+ resolve();
+ }
+ });
+ });
+
+ browser.test.sendMessage("start-download:done");
+ await downloadDone;
+ await browser.downloads.removeFile(downloadId);
+ browser.test.sendMessage("removed-download-file");
+ }
+
+ browser.test.onMessage.addListener(
+ (msg, { withHandlingUserInput, downloadOptions }) => {
+ if (msg !== "start-download") {
+ browser.test.fail(`Got unexpected test message: ${msg}`);
+ return;
+ }
+
+ if (withHandlingUserInput) {
+ browser.test.withHandlingUserInput(() =>
+ startDownload(downloadOptions)
+ );
+ } else {
+ startDownload(downloadOptions);
+ }
+ }
+ );
+ },
+ });
+
+ await extension.startup();
+
+ startServer();
+
+ async function testExtensionDownloadCall({ withHandlingUserInput }) {
+ mustInterruptResponses();
+ let rnd = Math.random();
+ let url = httpUrl(`interruptible.txt?q=${rnd}`);
+
+ extension.sendMessage("start-download", {
+ withHandlingUserInput,
+ downloadOptions: { url },
+ });
+ await extension.awaitMessage("start-download:done");
+
+ let publicList = await Downloads.getList(Downloads.PUBLIC);
+ let downloads = await publicList.getAll();
+
+ let download = downloads.find(d => d.source.url === url);
+ is(download.source.url, url, "download has the expected url");
+ is(
+ download.openDownloadsListOnStart,
+ withHandlingUserInput,
+ `download panel should ${withHandlingUserInput ? "open" : "stay closed"}`
+ );
+
+ continueResponses();
+ await extension.awaitMessage("removed-download-file");
+ }
+
+ info(
+ "Test extension downloads.download API method call without handling user input"
+ );
+ await testExtensionDownloadCall({ withHandlingUserInput: true });
+
+ info(
+ "Test extension downloads.download API method call while handling user input"
+ );
+ await testExtensionDownloadCall({ withHandlingUserInput: false });
+
+ await extension.unload();
+ await SpecialPowers.popPrefEnv();
+});
+
+/**
+ * Make sure the downloads panel opens automatically with new download, only if
+ * no other downloads are in progress.
+ */
+add_task(async function test_downloads_panel_remains_closed() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.download.always_ask_before_handling_new_types", false]],
+ });
+ await task_addDownloads([
+ { state: DownloadsCommon.DOWNLOAD_DOWNLOADING },
+ { state: DownloadsCommon.DOWNLOAD_DOWNLOADING },
+ ]);
+
+ let publicList = await Downloads.getList(Downloads.PUBLIC);
+ let downloads = await publicList.getAll();
+
+ info("setting 2 downloads to be in progress");
+ downloads[0].stopped = false;
+ downloads[1].stopped = false;
+
+ let oldShowEventNotification = DownloadsIndicatorView.showEventNotification;
+
+ registerCleanupFunction(async () => {
+ // Remove all downloads created during the test.
+ for (let download of downloads) {
+ await publicList.remove(download);
+ }
+ DownloadsIndicatorView.showEventNotification = oldShowEventNotification;
+ });
+
+ let promiseDownloadStartedNotification = new Promise(resolve => {
+ // Instead of downloads panel opening, download notification should be shown.
+ DownloadsIndicatorView.showEventNotification = aType => {
+ if (aType == "start") {
+ DownloadsIndicatorView.showEventNotification = oldShowEventNotification;
+ resolve();
+ }
+ };
+ });
+
+ DownloadsCommon.getData(window)._notifyDownloadEvent("start");
+
+ is(
+ DownloadsPanel.isPanelShowing,
+ false,
+ "Panel state should NOT indicate a preparation to be opened"
+ );
+
+ await promiseDownloadStartedNotification;
+
+ is(DownloadsPanel.panel.state, "closed", "Panel should be closed");
+
+ for (let download of downloads) {
+ await publicList.remove(download);
+ }
+ is((await publicList.getAll()).length, 0, "Should have no downloads left.");
+});
+
+/**
+ * Make sure the downloads panel doesn't open if the window isn't in the
+ * foreground.
+ */
+add_task(async function test_downloads_panel_inactive_window() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.download.always_ask_before_handling_new_types", false]],
+ });
+
+ let oldShowEventNotification = DownloadsIndicatorView.showEventNotification;
+
+ registerCleanupFunction(async () => {
+ DownloadsIndicatorView.showEventNotification = oldShowEventNotification;
+ });
+
+ let promiseDownloadStartedNotification = new Promise(resolve => {
+ // Instead of downloads panel opening, download notification should be shown.
+ DownloadsIndicatorView.showEventNotification = aType => {
+ if (aType == "start") {
+ DownloadsIndicatorView.showEventNotification = oldShowEventNotification;
+ resolve();
+ }
+ };
+ });
+
+ let testRunnerWindow = Array.from(Services.wm.getEnumerator("")).find(
+ someWin => someWin != window
+ );
+
+ await SimpleTest.promiseFocus(testRunnerWindow);
+
+ DownloadsCommon.getData(window)._notifyDownloadEvent("start");
+
+ is(
+ DownloadsPanel.isPanelShowing,
+ false,
+ "Panel state should NOT indicate a preparation to be opened"
+ );
+
+ await promiseDownloadStartedNotification;
+ await SimpleTest.promiseFocus(window);
+
+ is(DownloadsPanel.panel.state, "closed", "Panel should be closed");
+
+ testRunnerWindow = null;
+});
+
+/**
+ * When right-clicking the downloads toolbar button, there should be a menuitem
+ * for toggling alwaysOpenPanel. Check that it works correctly.
+ */
+add_task(async function test_alwaysOpenPanel_menuitem() {
+ const alwaysOpenPanelPref = "browser.download.alwaysOpenPanel";
+ let checkbox = document.getElementById(
+ "toolbar-context-always-open-downloads-panel"
+ );
+ let button = document.getElementById("downloads-button");
+
+ Services.prefs.clearUserPref(alwaysOpenPanelPref);
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.download.autohideButton", false]],
+ });
+ registerCleanupFunction(async () => {
+ await SpecialPowers.popPrefEnv();
+ Services.prefs.clearUserPref(alwaysOpenPanelPref);
+ });
+
+ is(button.hidden, false, "Downloads button should not be hidden.");
+
+ info("Check context menu for downloads button.");
+ await openContextMenu(button);
+ is(checkbox.hidden, false, "Always Open checkbox is visible.");
+ is(checkbox.getAttribute("checked"), "true", "Always Open is enabled.");
+
+ info("Disable Always Open via context menu.");
+ clickCheckbox(checkbox);
+ is(
+ Services.prefs.getBoolPref(alwaysOpenPanelPref),
+ false,
+ "Always Open pref has been set to false."
+ );
+
+ await downloadAndCheckPanel();
+
+ await openContextMenu(button);
+ is(checkbox.hidden, false, "Always Open checkbox is visible.");
+ isnot(checkbox.getAttribute("checked"), "true", "Always Open is disabled.");
+
+ info("Enable Always Open via context menu");
+ clickCheckbox(checkbox);
+ is(
+ Services.prefs.getBoolPref(alwaysOpenPanelPref),
+ true,
+ "Pref has been set to true"
+ );
+
+ await checkPanelOpens();
+});
+
+/**
+ * Verify that the downloads panel opens if the download did not open a file
+ * picker or UCT dialog
+ */
+add_task(async function test_downloads_panel_after_no_dialogs() {
+ await testDownloadsPanelAfterDialog({ expectPanelToOpen: true });
+ ok(true, "Downloads panel opened because no dialogs were opened.");
+});
+
+/**
+ * Verify that the downloads panel doesn't open if the download opened an
+ * unknown content type dialog (e.g. action = always ask)
+ */
+add_task(async function test_downloads_panel_after_UCT_dialog() {
+ await testDownloadsPanelAfterDialog({
+ expectPanelToOpen: false,
+ preferredAction: Ci.nsIHandlerInfo.alwaysAsk,
+ });
+ ok(true, "Downloads panel suppressed after UCT dialog.");
+});
+
+/**
+ * Verify that the downloads panel doesn't open if the download opened a file
+ * picker dialog (e.g. useDownloadDir = false)
+ */
+add_task(async function test_downloads_panel_after_file_picker_dialog() {
+ await testDownloadsPanelAfterDialog({
+ expectPanelToOpen: false,
+ preferredAction: Ci.nsIHandlerInfo.saveToDisk,
+ askWhereToSave: true,
+ });
+ ok(true, "Downloads panel suppressed after file picker dialog.");
+});
+
+/**
+ * Verify that the downloads panel doesn't open if the download opened both
+ * dialogs (e.g. default action = always ask AND useDownloadDir = false)
+ */
+add_task(async function test_downloads_panel_after_both_dialogs() {
+ await testDownloadsPanelAfterDialog({
+ expectPanelToOpen: false,
+ preferredAction: Ci.nsIHandlerInfo.alwaysAsk,
+ askWhereToSave: true,
+ });
+ ok(true, "Downloads panel suppressed after UCT and file picker dialogs.");
+});
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..60a4a8a371
--- /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("downloadsListBox");
+ 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..1beb33402a
--- /dev/null
+++ b/browser/components/downloads/test/browser/browser_first_download_panel.js
@@ -0,0 +1,68 @@
+/* -*- 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."
+ );
+
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.download.alwaysOpenPanel", false]],
+ });
+ // 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..938d54ccb2
--- /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("downloadsListBox");
+ 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"
+ );
+ contextMenu.activateItem(goToDownloadButton);
+
+ 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..a1b82fb9c2
--- /dev/null
+++ b/browser/components/downloads/test/browser/browser_iframe_gone_mid_download.js
@@ -0,0 +1,72 @@
+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.importESModule(
+ "resource://gre/modules/DownloadLastDir.sys.mjs"
+ );
+
+ 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 gDownloadLastDir.getFileAsync("http://www.mozilla.org/");
+ } catch (ex) {
+ ok(
+ false,
+ "Got an exception trying to get the directory where things should be saved."
+ );
+ console.error(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."
+ );
+ console.error(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..b893a26d89
--- /dev/null
+++ b/browser/components/downloads/test/browser/browser_image_mimetype_issues.js
@@ -0,0 +1,135 @@
+/* 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;
+ };
+ let menuitem = menu.querySelector("#context-saveimage");
+ menu.activateItem(menuitem);
+ });
+ }
+ );
+});
+
+/**
+ * Test with the "save link as" context menu.
+ */
+add_task(async function test_save_link_webp_with_jpeg_extension() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.download.always_ask_before_handling_new_types", false],
+ ["browser.download.useDownloadDir", false],
+ ],
+ });
+ 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;
+ };
+ let menuitem = menu.querySelector("#context-savelink");
+ menu.activateItem(menuitem);
+ });
+ }
+ );
+});
+
+/**
+ * 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..7957b96c43
--- /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..bac8dfeffb
--- /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("downloadsListBox");
+ 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..022d1b6977
--- /dev/null
+++ b/browser/components/downloads/test/browser/browser_library_clearall.js
@@ -0,0 +1,122 @@
+/* -*- 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.defineESModuleGetters(this, {
+ PlacesTestUtils: "resource://testing-common/PlacesTestUtils.sys.mjs",
+});
+
+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("downloadsListBox");
+ ok(listbox, "download list box present");
+
+ let promiseLength = waitForChildrenLength(listbox, DOWNLOAD_DATA.length);
+ await simulateDropAndCheck(win, listbox, DOWNLOAD_DATA);
+ await promiseLength;
+
+ let receivedNotifications = [];
+ const promiseNotification = PlacesTestUtils.waitForNotification(
+ "page-removed",
+ events => {
+ for (const { url, isRemovedFromStore } of events) {
+ Assert.ok(isRemovedFromStore);
+
+ if (DOWNLOAD_DATA.includes(url)) {
+ receivedNotifications.push(url);
+ }
+ }
+ return receivedNotifications.length == DOWNLOAD_DATA.length;
+ }
+ );
+
+ 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_setup(async function () {
+ // 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"
+ );
+ contextMenu.activateItem(clearDownloadsButton);
+ });
+});
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..3d2187b312
--- /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_setup(async function () {
+ 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("downloadsListBox");
+ 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..303bc81670
--- /dev/null
+++ b/browser/components/downloads/test/browser/browser_overflow_anchor.js
@@ -0,0 +1,59 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+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();
+ 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();
+ 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..cbd8516468
--- /dev/null
+++ b/browser/components/downloads/test/browser/browser_pdfjs_preview.js
@@ -0,0 +1,753 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+let gDownloadDir;
+
+// The test is long, and it's not worth splitting it since all the tests share
+// the same boilerplate code.
+requestLongerTimeout(4);
+
+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: "#downloadsListBox 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: "#downloadsListBox 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: "#downloadsListBox 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: "#downloadsListBox 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: "#downloadsListBox 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: "#downloadsListBox 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");
+ // NOTE: we are using sendMouseEvent instead of synthesizeMouseAtCenter
+ // here to prevent an unexpected timeout failure in devedition builds
+ // due to the ContentTaskUtils.waitForEvent promise never been resolved.
+ EventUtils.sendMouseEvent(
+ { type: "dblclick", ...modifiers },
+ itemTarget,
+ 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 = PathUtils.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 &&
+ !richlistbox.getAttribute("disabled")
+ );
+}
+
+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("downloadsListBox");
+ 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.loadURIString(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("downloadsListBox")
+ .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/browser_tempfilename.js b/browser/components/downloads/test/browser/browser_tempfilename.js
new file mode 100644
index 0000000000..e4dae6d944
--- /dev/null
+++ b/browser/components/downloads/test/browser/browser_tempfilename.js
@@ -0,0 +1,88 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function test_tempfilename() {
+ startServer();
+ let downloadURL = httpUrl("interruptible.txt");
+ let list = await Downloads.getList(Downloads.PUBLIC);
+ let downloadStarted = new Promise(resolve => {
+ let view = {
+ onDownloadAdded(download) {
+ list.removeView(view);
+ resolve(download);
+ },
+ };
+ list.addView(view);
+ });
+
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.download.always_ask_before_handling_new_types", false]],
+ });
+
+ const MimeSvc = Cc["@mozilla.org/mime;1"].getService(Ci.nsIMIMEService);
+ const HandlerSvc = Cc["@mozilla.org/uriloader/handler-service;1"].getService(
+ Ci.nsIHandlerService
+ );
+ let mimeInfo = MimeSvc.getFromTypeAndExtension(
+ HandlerSvc.getTypeFromExtension("txt"),
+ "txt"
+ );
+ let existed = HandlerSvc.exists(mimeInfo);
+ mimeInfo.alwaysAskBeforeHandling = false;
+ mimeInfo.preferredAction = Ci.nsIHandlerInfo.saveToDisk;
+ HandlerSvc.store(mimeInfo);
+
+ serveInterruptibleAsDownload();
+ mustInterruptResponses();
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: downloadURL,
+ waitForLoad: false,
+ waitForStop: true,
+ },
+ async () => {
+ let download = await downloadStarted;
+ registerCleanupFunction(async () => {
+ if (existed) {
+ HandlerSvc.store(mimeInfo);
+ } else {
+ HandlerSvc.remove(mimeInfo);
+ }
+ await download.finalize(true);
+ if (Services.appinfo.OS === "WINNT") {
+ // We need to make the file writable to delete it on Windows.
+ await IOUtils.setPermissions(download.target.path, 0o600);
+ }
+ await IOUtils.remove(download.target.path);
+ await download.finalize();
+ await list.removeFinished();
+ });
+
+ let { partFilePath } = download.target;
+ Assert.stringContains(
+ partFilePath,
+ "interruptible",
+ "Should keep bit of original filename."
+ );
+ isnot(
+ PathUtils.filename(partFilePath),
+ "interruptible.txt.part",
+ "Should not just have original filename."
+ );
+ ok(
+ partFilePath.endsWith(".txt.part"),
+ `${PathUtils.filename(partFilePath)} should end with .txt.part`
+ );
+ let promiseFinished = download.whenSucceeded();
+ continueResponses();
+ await promiseFinished;
+ ok(
+ !(await IOUtils.exists(download.target.partFilePath)),
+ "Temp file should be gone."
+ );
+ }
+ );
+});
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..49b4d8d04c
--- /dev/null
+++ b/browser/components/downloads/test/browser/head.js
@@ -0,0 +1,448 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Provides infrastructure for automated download components tests.
+ */
+
+// Globals
+
+ChromeUtils.defineESModuleGetters(this, {
+ Downloads: "resource://gre/modules/Downloads.sys.mjs",
+ DownloadsCommon: "resource:///modules/DownloadsCommon.sys.mjs",
+ FileUtils: "resource://gre/modules/FileUtils.sys.mjs",
+ PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs",
+});
+ChromeUtils.defineModuleGetter(
+ this,
+ "HttpServer",
+ "resource://testing-common/httpd.js"
+);
+
+let gTestTargetFile = new FileUtils.File(
+ PathUtils.join(
+ Services.dirsvc.get("TmpD", Ci.nsIFile).path,
+ "dm-ui-test.file"
+ )
+);
+
+gTestTargetFile.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, FileUtils.PERMS_FILE);
+Services.prefs.setIntPref("security.dialog_enable_delay", 0);
+
+// The file may have been already deleted when removing a paused download.
+// Also clear security.dialog_enable_delay pref.
+registerCleanupFunction(async () => {
+ Services.prefs.clearUserPref("security.dialog_enable_delay");
+
+ if (await IOUtils.exists(gTestTargetFile.path)) {
+ info("removing " + gTestTargetFile.path);
+ if (Services.appinfo.OS === "WINNT") {
+ // We need to make the file writable to delete it on Windows.
+ await IOUtils.setPermissions(gTestTargetFile.path, 0o600);
+ }
+ await IOUtils.remove(gTestTargetFile.path);
+ }
+});
+
+const DATA_PDF = atob(
+ "JVBERi0xLjANCjEgMCBvYmo8PC9UeXBlL0NhdGFsb2cvUGFnZXMgMiAwIFI+PmVuZG9iaiAyIDAgb2JqPDwvVHlwZS9QYWdlcy9LaWRzWzMgMCBSXS9Db3VudCAxPj5lbmRvYmogMyAwIG9iajw8L1R5cGUvUGFnZS9NZWRpYUJveFswIDAgMyAzXT4+ZW5kb2JqDQp4cmVmDQowIDQNCjAwMDAwMDAwMDAgNjU1MzUgZg0KMDAwMDAwMDAxMCAwMDAwMCBuDQowMDAwMDAwMDUzIDAwMDAwIG4NCjAwMDAwMDAxMDIgMDAwMDAgbg0KdHJhaWxlcjw8L1NpemUgNC9Sb290IDEgMCBSPj4NCnN0YXJ0eHJlZg0KMTQ5DQolRU9G"
+);
+
+const TEST_DATA_SHORT = "This test string is downloaded.";
+
+/**
+ * This is an internal reference that should not be used directly by tests.
+ */
+var _gDeferResponses = PromiseUtils.defer();
+
+/**
+ * Ensures that all the interruptible requests started after this function is
+ * called won't complete until the continueResponses function is called.
+ *
+ * Normally, the internal HTTP server returns all the available data as soon as
+ * a request is received. In order for some requests to be served one part at a
+ * time, special interruptible handlers are registered on the HTTP server. This
+ * allows testing events or actions that need to happen in the middle of a
+ * download.
+ *
+ * For example, the handler accessible at the httpUri("interruptible.txt")
+ * address returns the TEST_DATA_SHORT text, then it may block until the
+ * continueResponses method is called. At this point, the handler sends the
+ * TEST_DATA_SHORT text again to complete the response.
+ *
+ * If an interruptible request is started before the function is called, it may
+ * or may not be blocked depending on the actual sequence of events.
+ */
+function mustInterruptResponses() {
+ // If there are pending blocked requests, allow them to complete. This is
+ // done to prevent requests from being blocked forever, but should not affect
+ // the test logic, since previously started requests should not be monitored
+ // on the client side anymore.
+ _gDeferResponses.resolve();
+
+ info("Interruptible responses will be blocked midway.");
+ _gDeferResponses = PromiseUtils.defer();
+}
+
+/**
+ * Allows all the current and future interruptible requests to complete.
+ */
+function continueResponses() {
+ info("Interruptible responses are now allowed to continue.");
+ _gDeferResponses.resolve();
+}
+
+/**
+ * Creates a download, which could be interrupted in the middle of it's progress.
+ */
+function promiseInterruptibleDownload(extension = ".txt") {
+ let interruptibleFile = FileUtils.getFile("TmpD", [
+ `interruptible${extension}`,
+ ]);
+ interruptibleFile.createUnique(
+ Ci.nsIFile.NORMAL_FILE_TYPE,
+ FileUtils.PERMS_FILE
+ );
+
+ registerCleanupFunction(async () => {
+ if (await IOUtils.exists(interruptibleFile.path)) {
+ if (Services.appinfo.OS === "WINNT") {
+ // We need to make the file writable to delete it on Windows.
+ await IOUtils.setPermissions(interruptibleFile.path, 0o600);
+ }
+ await IOUtils.remove(interruptibleFile.path);
+ }
+ });
+
+ return Downloads.createDownload({
+ source: httpUrl("interruptible.txt"),
+ target: { path: interruptibleFile.path },
+ });
+}
+
+// Asynchronous support subroutines
+
+async function createDownloadedFile(pathname, contents) {
+ let file = new FileUtils.File(pathname);
+ if (file.exists()) {
+ info(`File at ${pathname} already exists`);
+ }
+ // No post-test cleanup necessary; tmp downloads directory is already removed after each test
+ await IOUtils.writeUTF8(pathname, contents);
+ ok(file.exists(), `Created ${pathname}`);
+ return file;
+}
+
+async function openContextMenu(itemElement, win = window) {
+ let popupShownPromise = BrowserTestUtils.waitForEvent(
+ itemElement.ownerDocument,
+ "popupshown"
+ );
+ EventUtils.synthesizeMouseAtCenter(
+ itemElement,
+ {
+ type: "contextmenu",
+ button: 2,
+ },
+ win
+ );
+ let { target } = await popupShownPromise;
+ return target;
+}
+
+function promiseFocus() {
+ return new Promise(resolve => {
+ waitForFocus(resolve);
+ });
+}
+
+function promisePanelOpened() {
+ if (DownloadsPanel.panel && DownloadsPanel.panel.state == "open") {
+ return Promise.resolve();
+ }
+
+ return new Promise(resolve => {
+ // Hook to wait until the panel is shown.
+ let originalOnPopupShown = DownloadsPanel.onPopupShown;
+ DownloadsPanel.onPopupShown = function () {
+ DownloadsPanel.onPopupShown = originalOnPopupShown;
+ originalOnPopupShown.apply(this, arguments);
+
+ // Defer to the next tick of the event loop so that we don't continue
+ // processing during the DOM event handler itself.
+ setTimeout(resolve, 0);
+ };
+ });
+}
+
+async function task_resetState() {
+ // Remove all downloads.
+ let publicList = await Downloads.getList(Downloads.PUBLIC);
+ let downloads = await publicList.getAll();
+ for (let download of downloads) {
+ await publicList.remove(download);
+ if (await IOUtils.exists(download.target.path)) {
+ await download.finalize(true);
+ info("removing " + download.target.path);
+ if (Services.appinfo.OS === "WINNT") {
+ // We need to make the file writable to delete it on Windows.
+ await IOUtils.setPermissions(download.target.path, 0o600);
+ }
+ await IOUtils.remove(download.target.path);
+ }
+ }
+
+ DownloadsPanel.hidePanel();
+
+ await promiseFocus();
+}
+
+async function task_addDownloads(aItems) {
+ let startTimeMs = Date.now();
+
+ let publicList = await Downloads.getList(Downloads.PUBLIC);
+ for (let item of aItems) {
+ let source = {
+ url: "http://www.example.com/test-download.txt",
+ ...item.source,
+ };
+ let target =
+ item.target instanceof Ci.nsIFile
+ ? item.target
+ : {
+ path: gTestTargetFile.path,
+ ...item.target,
+ };
+
+ let download = {
+ source,
+ target,
+ succeeded: item.state == DownloadsCommon.DOWNLOAD_FINISHED,
+ canceled:
+ item.state == DownloadsCommon.DOWNLOAD_CANCELED ||
+ item.state == DownloadsCommon.DOWNLOAD_PAUSED,
+ deleted: item.deleted ?? false,
+ error:
+ item.state == DownloadsCommon.DOWNLOAD_FAILED
+ ? new Error("Failed.")
+ : null,
+ hasPartialData: item.state == DownloadsCommon.DOWNLOAD_PAUSED,
+ hasBlockedData: item.hasBlockedData || false,
+ openDownloadsListOnStart: item.openDownloadsListOnStart ?? true,
+ contentType: item.contentType,
+ startTime: new Date(startTimeMs++),
+ };
+ // `"errorObj" in download` must be false when there's no error.
+ if (item.errorObj) {
+ download.errorObj = item.errorObj;
+ }
+ download = await Downloads.createDownload(download);
+ await publicList.add(download);
+ await download.refresh();
+ }
+}
+
+async function task_openPanel() {
+ await promiseFocus();
+
+ let promise = promisePanelOpened();
+ DownloadsPanel.showPanel();
+ await promise;
+}
+
+async function setDownloadDir() {
+ let tmpDir = PathUtils.join(
+ PathUtils.tempDir,
+ "testsavedir" + Math.floor(Math.random() * 2 ** 32)
+ );
+ // Create this dir if it doesn't exist (ignores existing dirs)
+ await IOUtils.makeDirectory(tmpDir);
+ registerCleanupFunction(async function () {
+ try {
+ await IOUtils.remove(tmpDir, { recursive: true });
+ } catch (e) {
+ console.error(e);
+ }
+ });
+ Services.prefs.setIntPref("browser.download.folderList", 2);
+ Services.prefs.setCharPref("browser.download.dir", tmpDir);
+ return tmpDir;
+}
+
+let gHttpServer = null;
+let gShouldServeInterruptibleFileAsDownload = false;
+function startServer() {
+ gHttpServer = new HttpServer();
+ gHttpServer.start(-1);
+ registerCleanupFunction(() => {
+ return new Promise(resolve => {
+ // Ensure all the pending HTTP requests have a chance to finish.
+ continueResponses();
+ // Stop the HTTP server, calling resolve when it's done.
+ gHttpServer.stop(resolve);
+ });
+ });
+
+ gHttpServer.identity.setPrimary(
+ "http",
+ "www.example.com",
+ gHttpServer.identity.primaryPort
+ );
+
+ gHttpServer.registerPathHandler("/file1.txt", (request, response) => {
+ response.setStatusLine(null, 200, "OK");
+ response.write("file1");
+ response.processAsync();
+ response.finish();
+ });
+ gHttpServer.registerPathHandler("/file2.txt", (request, response) => {
+ response.setStatusLine(null, 200, "OK");
+ response.write("file2");
+ response.processAsync();
+ response.finish();
+ });
+ gHttpServer.registerPathHandler("/file3.txt", (request, response) => {
+ response.setStatusLine(null, 200, "OK");
+ response.write("file3");
+ response.processAsync();
+ response.finish();
+ });
+
+ gHttpServer.registerPathHandler(
+ "/interruptible.txt",
+ function (aRequest, aResponse) {
+ info("Interruptible request started.");
+
+ // Process the first part of the response.
+ aResponse.processAsync();
+ aResponse.setHeader("Content-Type", "text/plain", false);
+ if (gShouldServeInterruptibleFileAsDownload) {
+ aResponse.setHeader("Content-Disposition", "attachment");
+ }
+ aResponse.setHeader(
+ "Content-Length",
+ "" + TEST_DATA_SHORT.length * 2,
+ false
+ );
+ aResponse.write(TEST_DATA_SHORT);
+
+ // Wait on the current deferred object, then finish the request.
+ _gDeferResponses.promise
+ .then(function RIH_onSuccess() {
+ aResponse.write(TEST_DATA_SHORT);
+ aResponse.finish();
+ info("Interruptible request finished.");
+ })
+ .catch(console.error);
+ }
+ );
+}
+
+function serveInterruptibleAsDownload() {
+ gShouldServeInterruptibleFileAsDownload = true;
+ registerCleanupFunction(
+ () => (gShouldServeInterruptibleFileAsDownload = false)
+ );
+}
+
+function httpUrl(aFileName) {
+ return (
+ "http://localhost:" + gHttpServer.identity.primaryPort + "/" + aFileName
+ );
+}
+
+function openLibrary(aLeftPaneRoot) {
+ let library = window.openDialog(
+ "chrome://browser/content/places/places.xhtml",
+ "",
+ "chrome,toolbar=yes,dialog=no,resizable",
+ aLeftPaneRoot
+ );
+
+ return new Promise(resolve => {
+ waitForFocus(resolve, library);
+ });
+}
+
+/**
+ * Waits for a download to reach its progress, in case it has not
+ * reached the expected progress already.
+ *
+ * @param aDownload
+ * The Download object to wait upon.
+ *
+ * @return {Promise}
+ * @resolves When the download has reached its progress.
+ * @rejects Never.
+ */
+function promiseDownloadHasProgress(aDownload, progress) {
+ return new Promise(resolve => {
+ // Wait for the download to reach its progress.
+ let onchange = function () {
+ let downloadInProgress =
+ !aDownload.stopped && aDownload.progress == progress;
+ let downloadFinished =
+ progress == 100 &&
+ aDownload.progress == progress &&
+ aDownload.succeeded;
+ if (downloadInProgress || downloadFinished) {
+ info(`Download reached ${progress}%`);
+ aDownload.onchange = null;
+ resolve();
+ }
+ };
+
+ // Register for the notification, but also call the function directly in
+ // case the download already reached the expected progress.
+ aDownload.onchange = onchange;
+ onchange();
+ });
+}
+
+/**
+ * Waits for a given button to become visible.
+ */
+function promiseButtonShown(id) {
+ let dwu = window.windowUtils;
+ return BrowserTestUtils.waitForCondition(() => {
+ let target = document.getElementById(id);
+ let bounds = dwu.getBoundsWithoutFlushing(target);
+ return bounds.width > 0 && bounds.height > 0;
+ }, `Waiting for button ${id} to have non-0 size`);
+}
+
+async function simulateDropAndCheck(win, dropTarget, urls) {
+ let dragData = [[{ type: "text/plain", data: urls.join("\n") }]];
+ let list = await Downloads.getList(Downloads.ALL);
+
+ let added = new Set();
+ let succeeded = new Set();
+ await new Promise(resolve => {
+ let view = {
+ onDownloadAdded(download) {
+ added.add(download.source.url);
+ },
+ onDownloadChanged(download) {
+ if (!added.has(download.source.url)) {
+ return;
+ }
+ if (!download.succeeded) {
+ return;
+ }
+ succeeded.add(download.source.url);
+ if (succeeded.size == urls.length) {
+ list.removeView(view).then(resolve);
+ }
+ },
+ };
+ list.addView(view).then(function () {
+ EventUtils.synthesizeDrop(dropTarget, dropTarget, dragData, "link", win);
+ });
+ });
+
+ for (let url of urls) {
+ ok(added.has(url), url + " is added to download");
+ }
+}
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/browser/test_spammy_page.html b/browser/components/downloads/test/browser/test_spammy_page.html
new file mode 100644
index 0000000000..92332bb1c0
--- /dev/null
+++ b/browser/components/downloads/test/browser/test_spammy_page.html
@@ -0,0 +1,26 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8" />
+ <title>Spam Page Test</title>
+</head>
+<body>
+ <p> Hello, it's the spammy page! </p>
+<script type="text/javascript">
+ let count = 0;
+window.onload = window.onclick = function() {
+ if (count < 100) {
+ count++;
+ let l = document.createElement('a');
+ l.href = 'data:text/plain,some text';
+ l.download = 'sometext.txt';
+
+ document.body.appendChild(l);
+ l.click();
+ }
+}
+</script>
+</body>
+</html>
diff --git a/browser/components/downloads/test/unit/head.js b/browser/components/downloads/test/unit/head.js
new file mode 100644
index 0000000000..2f0326e779
--- /dev/null
+++ b/browser/components/downloads/test/unit/head.js
@@ -0,0 +1,67 @@
+ChromeUtils.defineESModuleGetters(this, {
+ Downloads: "resource://gre/modules/Downloads.sys.mjs",
+ DownloadsCommon: "resource:///modules/DownloadsCommon.sys.mjs",
+ FileTestUtils: "resource://testing-common/FileTestUtils.sys.mjs",
+ FileUtils: "resource://gre/modules/FileUtils.sys.mjs",
+ TestUtils: "resource://testing-common/TestUtils.sys.mjs",
+});
+ChromeUtils.defineModuleGetter(
+ this,
+ "NetUtil",
+ "resource://gre/modules/NetUtil.jsm"
+);
+
+async function createDownloadedFile(pathname, contents) {
+ info("createDownloadedFile: " + pathname);
+ 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 IOUtils.writeUTF8(pathname, 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) {
+ console.error(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_setup(async function test_common_initialize() {
+ gDownloadDir = await setDownloadDir();
+ Services.prefs.setCharPref("browser.download.loglevel", "Debug");
+});
diff --git a/browser/components/downloads/test/unit/test_DownloadLastDir_basics.js b/browser/components/downloads/test/unit/test_DownloadLastDir_basics.js
new file mode 100644
index 0000000000..f1dfbe4733
--- /dev/null
+++ b/browser/components/downloads/test/unit/test_DownloadLastDir_basics.js
@@ -0,0 +1,140 @@
+/* Any copyright is dedicated to the Public Domain.
+ * https://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Basic test for setting and retrieving a download last dir.
+// More complex tests can be found in browser/components/privatebrowsing/.
+
+const SAVE_PER_SITE_PREF_BRANCH = "browser.download.lastDir";
+const SAVE_PER_SITE_PREF = SAVE_PER_SITE_PREF_BRANCH + ".savePerSite";
+
+let { FileUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/FileUtils.sys.mjs"
+);
+let { DownloadLastDir } = ChromeUtils.importESModule(
+ "resource://gre/modules/DownloadLastDir.sys.mjs"
+);
+
+add_task(
+ {
+ pref_set: [[SAVE_PER_SITE_PREF, true]],
+ },
+ async function test() {
+ let downloadLastDir = new DownloadLastDir(null);
+
+ let unknownUri = Services.io.newURI("https://unknown.org/");
+ Assert.deepEqual(
+ await downloadLastDir.getFileAsync(unknownUri),
+ null,
+ "Untracked URI, no pref set"
+ );
+
+ let dir1 = FileUtils.getDir("TmpD", ["dir1"], true);
+ let uri1 = Services.io.newURI("https://test1.moz.org");
+ downloadLastDir.setFile(uri1, dir1);
+ let dir2 = FileUtils.getDir("TmpD", ["dir2"], true);
+ let uri2 = Services.io.newURI("https://test2.moz.org");
+ downloadLastDir.setFile(uri2, dir2);
+ let dir3 = FileUtils.getDir("TmpD", ["dir3"], true);
+ downloadLastDir.setFile(null, dir3);
+ Assert.equal(
+ (await downloadLastDir.getFileAsync(uri1)).path,
+ dir1.path,
+ "Check common URI"
+ );
+ Assert.equal(
+ (await downloadLastDir.getFileAsync(uri2)).path,
+ dir2.path,
+ "Check common URI"
+ );
+ Assert.equal(downloadLastDir.file.path, dir3.path, "No URI");
+ Assert.equal(
+ (await downloadLastDir.getFileAsync(unknownUri)).path,
+ dir3.path,
+ "Untracked URI, pref set"
+ );
+
+ info("Check clearHistory removes all data");
+ let subject = {};
+ Services.obs.notifyObservers(subject, "browser:purge-session-history");
+ await subject.promise;
+ Assert.deepEqual(
+ await downloadLastDir.getFileAsync(uri1),
+ null,
+ "Check common URI after clear history returns null"
+ );
+ Assert.deepEqual(
+ await downloadLastDir.getFileAsync(uri2),
+ null,
+ "Check common URI after clear history returns null"
+ );
+ Assert.deepEqual(
+ await downloadLastDir.getFileAsync(unknownUri),
+ null,
+ "Check untracked URI after clear history returns null"
+ );
+
+ // file: URIs should all point to the same folder.
+ let fileUri1 = Services.io.newURI("file:///c:/test.txt");
+ downloadLastDir.setFile(uri1, dir3);
+ let dir4 = FileUtils.getDir("TmpD", ["dir4"], true);
+ let fileUri2 = Services.io.newURI("file:///d:/test.png");
+ downloadLastDir.setFile(uri1, dir4);
+ Assert.equal(
+ (await downloadLastDir.getFileAsync(fileUri1)).path,
+ dir4.path,
+ "Check file URI"
+ );
+ Assert.equal(
+ (await downloadLastDir.getFileAsync(fileUri2)).path,
+ dir4.path,
+ "Check file URI"
+ );
+ let unknownFileUri = Services.io.newURI("file:///e:/test.mkv");
+ Assert.equal(
+ (await downloadLastDir.getFileAsync(unknownFileUri)).path,
+ dir4.path,
+ "Untracked File URI, pref set"
+ );
+
+ // data: URIs should point to a folder per mime-type.
+ // Unspecified mime-type is handled as text/plain.
+ let dataUri1 = Services.io.newURI("data:text/plain;charset=UTF-8,1234");
+ downloadLastDir.setFile(dataUri1, dir1);
+ let dataUri2 = Services.io.newURI("data:image/png;base64,1234");
+ Assert.equal(
+ (await downloadLastDir.getFileAsync(dataUri2)).path,
+ dir1.path,
+ "Check data URI"
+ );
+ let dataUri3 = Services.io.newURI("data:image/png,5678");
+ downloadLastDir.setFile(dataUri3, dir2);
+ Assert.equal(
+ (await downloadLastDir.getFileAsync(dataUri2)).path,
+ dir2.path,
+ "Data URI was changed, same mime-type"
+ );
+ Assert.equal(
+ (await downloadLastDir.getFileAsync(dataUri1)).path,
+ dir1.path,
+ "Data URI was not changed, different mime-type"
+ );
+ let dataUri4 = Services.io.newURI("data:,");
+ Assert.equal(
+ (await downloadLastDir.getFileAsync(dataUri4)).path,
+ dir1.path,
+ "Data URI defaults to text/plain"
+ );
+ downloadLastDir.setFile(null, dir4);
+ let unknownDataUri = Services.io.newURI("data:application/zip,");
+ Assert.deepEqual(
+ (await downloadLastDir.getFileAsync(unknownDataUri)).path,
+ dir4.path,
+ "Untracked data URI"
+ );
+ Assert.equal(
+ (await downloadLastDir.getFileAsync(dataUri4)).path,
+ dir1.path,
+ "Data URI didn't change"
+ );
+ }
+);
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..3e87fa9ec9
--- /dev/null
+++ b/browser/components/downloads/test/unit/test_DownloadsCommon_getMimeInfo.js
@@ -0,0 +1,168 @@
+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() {
+ let profileDir = Services.dirsvc.get("ProfD", Ci.nsIFile).path;
+ Assert.ok(profileDir, "profileDir: " + profileDir);
+ for (let [filename, contents] of Object.entries(TESTFILES)) {
+ TESTFILES[filename] = await createDownloadedFile(
+ PathUtils.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 IOUtils.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..d965ac264a
--- /dev/null
+++ b/browser/components/downloads/test/unit/test_DownloadsCommon_isFileOfType.js
@@ -0,0 +1,147 @@
+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() {
+ let profileDir = Services.dirsvc.get("ProfD", Ci.nsIFile).path;
+ Assert.ok(profileDir, "profileDir: " + profileDir);
+ for (let [filename, contents] of Object.entries(TESTFILES)) {
+ TESTFILES[filename] = await createDownloadedFile(
+ PathUtils.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..07925bc7d5
--- /dev/null
+++ b/browser/components/downloads/test/unit/test_DownloadsViewableInternally.js
@@ -0,0 +1,277 @@
+/* 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 PREF_AVIF_ENABLED = "image.avif.enabled";
+const PDF_MIME = "application/pdf";
+const OCTET_MIME = "application/octet-stream";
+const XML_MIME = "text/xml";
+const SVG_MIME = "image/svg+xml";
+const AVIF_MIME = "image/avif";
+const WEBP_MIME = "image/webp";
+
+const { Integration } = ChromeUtils.importESModule(
+ "resource://gre/modules/Integration.sys.mjs"
+);
+const {
+ DownloadsViewableInternally,
+ PREF_ENABLED_TYPES,
+ PREF_BRANCH_WAS_REGISTERED,
+ PREF_BRANCH_PREVIOUS_ACTION,
+ PREF_BRANCH_PREVIOUS_ASK,
+} = ChromeUtils.importESModule(
+ "resource:///modules/DownloadsViewableInternally.sys.mjs"
+);
+
+/* global DownloadIntegration */
+Integration.downloads.defineESModuleGetter(
+ this,
+ "DownloadIntegration",
+ "resource://gre/modules/DownloadIntegration.sys.mjs"
+);
+
+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 && ext != "xml" && ext != "svg");
+ checkShouldView(mime, ext, expected);
+ if (ext != "xml" && ext != "svg") {
+ checkWasRegistered(ext, expected);
+ }
+}
+
+add_task(async function test_viewable_internally() {
+ Services.prefs.setCharPref(PREF_ENABLED_TYPES, "xml , svg,avif,webp");
+ Services.prefs.setBoolPref(PREF_SVG_DISABLED, false);
+ Services.prefs.setBoolPref(PREF_WEBP_ENABLED, true);
+ Services.prefs.setBoolPref(PREF_AVIF_ENABLED, true);
+
+ checkAll(XML_MIME, "xml", false);
+ checkAll(SVG_MIME, "svg", false);
+ checkAll(WEBP_MIME, "webp", false);
+ checkAll(AVIF_MIME, "avif", false);
+
+ DownloadsViewableInternally.register();
+
+ checkAll(XML_MIME, "xml", true);
+ checkAll(SVG_MIME, "svg", true);
+ checkAll(WEBP_MIME, "webp", true);
+ checkAll(AVIF_MIME, "avif", true);
+
+ // Remove webp so it won't be cleared
+ Services.prefs.clearUserPref(PREF_BRANCH_WAS_REGISTERED + "webp");
+
+ // Disable xml, avif and webp, check that avif becomes disabled
+ Services.prefs.setCharPref(PREF_ENABLED_TYPES, "svg");
+
+ // (XML is externally managed, and we just cleared the webp pref)
+ checkAll(XML_MIME, "xml", true);
+ checkPreferInternal(WEBP_MIME, "webp", true);
+
+ // Avif should be disabled
+ checkAll(AVIF_MIME, "avif", false);
+
+ // SVG shouldn't be cleared as it's still enabled
+ checkAll(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(WEBP_MIME), "imave/webp should be disabled by pref");
+ Assert.ok(!shouldView(AVIF_MIME), "image/avif should be disabled by pref");
+
+ // Enable, check that everything is enabled again
+ Services.prefs.setCharPref(PREF_ENABLED_TYPES, "xml,svg,webp,avif");
+
+ checkAll(XML_MIME, "xml", true);
+ checkAll(SVG_MIME, "svg", true);
+ checkPreferInternal(WEBP_MIME, "webp", true);
+ checkPreferInternal(AVIF_MIME, "avif", 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, SVG and XML should not be replaced, WebP should be saved.
+ Services.prefs.setCharPref(PREF_ENABLED_TYPES, "svg,webp,xml");
+
+ Assert.equal(
+ Services.prefs.prefHasUserValue(PREF_BRANCH_PREVIOUS_ACTION + "svg"),
+ false,
+ "svg action should not be stored"
+ );
+ Assert.equal(
+ Services.prefs.prefHasUserValue(PREF_BRANCH_PREVIOUS_ASK + "svg"),
+ false,
+ "svg ask should not be stored"
+ );
+ 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(SVG_MIME, "svg");
+ Assert.equal(
+ handler.preferredAction,
+ Ci.nsIHandlerInfo.saveToDisk,
+ "svg action should be preserved"
+ );
+ Assert.equal(
+ !!handler.alwaysAskBeforeHandling,
+ true,
+ "svg ask should be preserved"
+ );
+ // Clean up
+ HandlerService.remove(handler);
+ handler = MIMEService.getFromTypeAndExtension(XML_MIME, "xml");
+ Assert.equal(
+ handler.preferredAction,
+ Ci.nsIHandlerInfo.useSystemDefault,
+ "xml action should be preserved"
+ );
+ Assert.equal(
+ !!handler.alwaysAskBeforeHandling,
+ true,
+ "xml ask should be preserved"
+ );
+ // Clean up
+ HandlerService.remove(handler);
+ }
+ // It should still be possible to view XML internally
+ checkShouldView(XML_MIME, "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);
+ checkAll(SVG_MIME, "svg", false);
+ Services.prefs.setBoolPref(PREF_SVG_DISABLED, false);
+ {
+ let handler = MIMEService.getFromTypeAndExtension(SVG_MIME, "svg");
+ handler.preferredAction = Ci.nsIHandlerInfo.saveToDisk;
+ handler.alwaysAskBeforeHandling = false;
+ HandlerService.store(handler);
+ }
+
+ 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..9e67834c3e
--- /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' # bug 1730213
+
+[test_DownloadLastDir_basics.js]
+[test_DownloadsCommon_getMimeInfo.js]
+[test_DownloadsCommon_isFileOfType.js]
+[test_DownloadsViewableInternally.js]